1class UniversallySwitcher extends HTMLElement{constructor(){super();this.attachShadow({mode:'open'});this._open=!1;this._onDocClick=this._onDocClick.bind(this);this._onKeyDown=this._onKeyDown.bind(this)} 2connectedCallback(){const raw=this.getAttribute('data-config');if(!raw)return;try{this._config=JSON.parse(raw)}catch{return} 3this.setAttribute('translate','no');this._render();this._bind()} 4disconnectedCallback(){document.removeEventListener('click',this._onDocClick,!0);document.removeEventListener('keydown',this._onKeyDown)} 5_render(){const{languages,showNames,showFlags,flagStyle,fixed,position}=this._config;if(!languages||!languages.length)return;const current=languages.find(l=>l.isCurrent);if(!current)return;const others=languages.filter(l=>!l.isCurrent&&!l.isDisabled);const flagRadius=flagStyle==='square'?'0':'50%';let hostStyles='';if(fixed){const posMap={bottom_right:'bottom:20px;right:20px',bottom_left:'bottom:20px;left:20px',top_right:'top:20px;right:20px',top_left:'top:20px;left:20px',};const pos=posMap[position]||posMap.bottom_right;hostStyles=`position:fixed;${pos};z-index:999999;`} 6const flagHtml=(lang)=>{if(!showFlags||!lang.flagUrl)return'';return `<img class="flag" src="${lang.flagUrl}" alt="" />`};const nameHtml=(lang)=>{if(!showNames)return'';return `<span class="name">${lang.originalName || lang.name || ''}</span>`};const codeHtml=(lang)=>{const code=(lang.region||lang.variant||'').split('-')[0];return `<span class="code">${code.toUpperCase()}</span>`};const labelHtml=(lang)=>{if(showFlags||showNames)return `${flagHtml(lang)}${nameHtml(lang)}`;return codeHtml(lang)};const itemsHtml=others.map(lang=>{const hreflang=lang.hreflang||lang.region||lang.variant||'';const dataLang=lang.isSource?'':(lang.urlPrefix||'');const href=lang.isSource?`${lang.url}${lang.url.includes('?') ? '&' : '?'}universally_switch=source`:lang.url;return `<li><a href="${href}" hreflang="${hreflang}" lang="${hreflang}" data-lang="${dataLang}">${labelHtml(lang)}</a></li>`}).join('');this.shadowRoot.innerHTML=` 7 <style> 8 :host { 9 display: inline-block; 10 font-family: inherit; 11 font-size: var(--universally-font-size, 14px); 12 line-height: 1.4; 13 ${hostStyles} 14 } 15 * { box-sizing: border-box; margin: 0; padding: 0; } 16 17 .trigger { 18 display: inline-flex; 19 align-items: center; 20 gap: 8px; 21 padding: var(--universally-trigger-padding, 8px 12px); 22 background: var(--universally-trigger-bg, #ffffff); 23 border: 1px solid var(--universally-trigger-border, #d1d5db); 24 border-radius: var(--universally-trigger-radius, 6px); 25 cursor: pointer; 26 font: inherit; 27 color: var(--universally-trigger-text, #111827); 28 transition: border-color 0.15s; 29 user-select: none; 30 } 31 .trigger:hover { border-color: var(--universally-trigger-border-hover, #9ca3af); } 32 .trigger:focus-visible { 33 outline: 2px solid var(--universally-focus, #0073aa); 34 outline-offset: 2px; 35 } 36 37 .arrow { 38 display: inline-block; 39 width: 0; 40 height: 0; 41 border-left: 4px solid transparent; 42 border-right: 4px solid transparent; 43 border-top: 5px solid currentColor; 44 transition: transform 0.2s; 45 } 46 :host([open]) .arrow { transform: rotate(180deg); } 47 48 .dropdown { 49 display: none; 50 position: absolute; 51 left: 0; 52 margin: 4px 0; 53 padding: 4px; 54 list-style: none; 55 background: var(--universally-dropdown-bg, #ffffff); 56 border: 1px solid var(--universally-dropdown-border, #d1d5db); 57 border-radius: var(--universally-dropdown-radius, 6px); 58 box-shadow: var(--universally-dropdown-shadow, 0 4px 12px rgba(0,0,0,0.1)); 59 min-width: 100%; 60 max-height: 200px; 61 overflow-y: auto; 62 z-index: 10; 63 } 64 :host([open]) .dropdown { display: block; } 65 .dropdown.above { bottom: 100%; top: auto; } 66 .dropdown.below { top: 100%; bottom: auto; } 67 68 .dropdown li { white-space: nowrap; } 69 .dropdown a { 70 display: flex; 71 align-items: center; 72 gap: 8px; 73 padding: 8px 10px; 74 color: var(--universally-dropdown-text, #111827); 75 text-decoration: none; 76 border-radius: 4px; 77 transition: background 0.15s; 78 font: inherit; 79 } 80 .dropdown a:hover { background: var(--universally-dropdown-hover-bg, #f3f4f6); } 81 .dropdown a:focus-visible { 82 outline: 2px solid var(--universally-focus, #0073aa); 83 outline-offset: -2px; 84 } 85 86 .flag {
87 width: 20px;${flagStyle === 'square' ? '' : '\n height: 20px;'} 88 object-fit: cover; 89 border-radius: ${flagRadius}; 90 flex-shrink: 0; 91 } 92 .name { color: inherit; } 93 .code { color: inherit; font-weight: 500; letter-spacing: 0.5px; } 94 95 .wrap { position: relative; display: inline-block; } 96 </style> 97 <div class="wrap"> 98 <button class="trigger" type="button" aria-expanded="false" aria-haspopup="listbox"> 99 ${labelHtml(current)}${showFlags && !showNames ? '' : '<span class="arrow" aria-hidden="true"></span>'} 100 </button> 101 <ul class="dropdown below" role="listbox">${itemsHtml}</ul> 102 </div> 103 `} 104_bind(){this._trigger=this.shadowRoot.querySelector('.trigger');this._dropdown=this.shadowRoot.querySelector('.dropdown');if(!this._trigger||!this._dropdown)return;this._trigger.addEventListener('click',(e)=>{e.stopPropagation();this._toggle()});this._dropdown.querySelectorAll('a[data-lang]').forEach((a)=>{a.addEventListener('click',()=>{this._persistLanguageChoice(a.getAttribute('data-lang')||'')})});document.addEventListener('click',this._onDocClick,!0);document.addEventListener('keydown',this._onKeyDown)} 105_persistLanguageChoice(urlPrefix){if(this._config.rememberLanguage===!1)return;const maxAge=60*60*24*30;const value=urlPrefix?encodeURIComponent(urlPrefix):'source';const secure=location.protocol==='https:'?';secure':'';document.cookie=`universally_lang=${value};path=/;max-age=${maxAge};samesite=Lax${secure}`} 106_toggle(){this._open=!this._open;if(this._open){this.setAttribute('open','');this._trigger.setAttribute('aria-expanded','true');this._positionDropdown()}else{this._close()}} 107_close(){this._open=!1;this.removeAttribute('open');this._trigger.setAttribute('aria-expanded','false')} 108_positionDropdown(){const dd=this._dropdown;const pad=20;dd.classList.remove('above');dd.classList.add('below');dd.style.left='0';const triggerRect=this._trigger.getBoundingClientRect();const ddRect=dd.getBoundingClientRect();const vh=window.innerHeight;const vw=window.innerWidth;const spaceBelow=vh-triggerRect.bottom;const spaceAbove=triggerRect.top;if(ddRect.height+pad>spaceBelow&&spaceAbove>spaceBelow){dd.classList.remove('below');dd.classList.add('above')} 109if(ddRect.right>vw-pad){dd.style.left=`${-(ddRect.right - vw + pad)}px`}else if(ddRect.left<pad){dd.style.left=`${pad - ddRect.left}px`}} 110_onDocClick(e){if(this._open&&!e.composedPath().includes(this)){this._close()}} 111_onKeyDown(e){if(e.key==='Escape'&&this._open){this._close();this._trigger.focus()}}} 112if(!customElements.get('universally-switcher')){customElements.define('universally-switcher',UniversallySwitcher)}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.