UNPKG

@ui18n/selector-web

Version:

🌐 零依赖Web Components语言选择器 - 支持所有框架和浏览器的通用组件

3 lines (2 loc) 16.5 kB
!function(n,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((n="undefined"!=typeof globalThis?globalThis:n||self).UI18nSelectorWeb={})}(this,function(n){"use strict";const e={"zh-CN":"简体中文","zh-TW":"繁體中文",en:"English","en-US":"English (US)","en-GB":"English (UK)",ja:"日本語",ko:"한국어",fr:"Français",de:"Deutsch",es:"Español",pt:"Português","pt-BR":"Português (Brasil)",ru:"Русский",it:"Italiano",nl:"Nederlands",pl:"Polski",tr:"Türkçe",ar:"العربية",th:"ไทย",vi:"Tiếng Việt",id:"Bahasa Indonesia",ms:"Bahasa Melayu",hi:"हिन्दी",bn:"বাংলা",ta:"தமிழ்",te:"తెలుగు",ur:"اردو",fa:"فارسی",he:"עברית",sv:"Svenska",da:"Dansk",no:"Norsk",fi:"Suomi",cs:"Čeština",hu:"Magyar",ro:"Română",el:"Ελληνικά",uk:"Українська"};function t(n){if(!n||"string"!=typeof n)return"en";const e=n.trim().toLowerCase();return"zh"===e||"zh-cn"===e||"zh-hans"===e?"zh-CN":"zh-tw"===e||"zh-hk"===e||"zh-hant"===e?"zh-TW":"en"===e||"en-us"===e?"en":e}function a(n){const a=t(n);return e[a]||a}function s(){return"undefined"==typeof navigator?"en":navigator.language||navigator.userLanguage||"en"}function o(){return a(s())}function i(n,e){if(!e||!e.trim())return n;const t=e.trim().toLowerCase();return n.filter(n=>{const e=a(n).toLowerCase(),s=n.toLowerCase();return e.includes(t)||s.includes(t)})}class r extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this._languages=[],this._current="",this._isOpen=!1,this._query="",this._activeIndex=0,this._showSearchBox=!0,this._showBrandSuffix=!0,this._brandSuffix="- ui18n",this._isComposing=!1}static get observedAttributes(){return["languages","current","theme","placeholder","show-search-box","show-brand-suffix","brand-suffix"]}connectedCallback(){this._setupEventListeners(),this.render(),this.dispatchEvent(new CustomEvent("selector-ready",{bubbles:!0,composed:!0}))}attributeChangedCallback(n,e,a){if(e!==a){switch(n){case"languages":this._languages=a?a.split(",").map(n=>n.trim()):[];break;case"current":this._current=t(a||"");break;case"show-search-box":this._showSearchBox="false"!==a;break;case"show-brand-suffix":this._showBrandSuffix="false"!==a;break;case"brand-suffix":this._brandSuffix=a||"- ui18n"}this.shadowRoot.innerHTML&&this.render()}}_getSearchPlaceholder(){const n=this.getAttribute("placeholder");if(n)return n;const e=o(),t=this._showBrandSuffix?`${e} ${this._brandSuffix}`:e;return console.log("[Selector] 搜索占位符:",{systemLabel:e,brandSuffix:this._brandSuffix,showBrandSuffix:this._showBrandSuffix,result:t}),t}_getFilteredLanguages(){return i(this._languages,this._query)}_getFixedLanguages(){const n=["zh-CN","en"].filter(n=>{const e=t(n);return this._languages.some(n=>t(n)===e)});return console.log("[Selector] 固定语言列表:",n,n.map(n=>({code:n,label:a(n)}))),n}render(){this.getAttribute("theme");const n=this._getFixedLanguages(),e=this._getFilteredLanguages(),t=a(this._current),s=this._getSearchPlaceholder();console.log("[Selector] render:",{current:this._current,currentLabel:t,fixedCount:n.length,filteredCount:e.length,query:this._query}),this.shadowRoot.innerHTML=`\n <style>\n :host {\n display: inline-block;\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\n position: relative;\n width: 100%;\n min-width: 180px;\n max-width: 360px;\n }\n\n * {\n box-sizing: border-box;\n }\n\n /* 触发按钮 */\n .trigger-button {\n width: 100%;\n padding: 8px 32px 8px 12px;\n font-size: 14px;\n border: 1px solid var(--ui18n-selector-border, #d1d5db);\n border-radius: 6px;\n background-color: var(--ui18n-selector-bg, #ffffff);\n color: var(--ui18n-selector-color, #1f2937);\n cursor: pointer;\n outline: none;\n text-align: left;\n transition: all 0.2s ease;\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n .trigger-button:hover {\n background-color: var(--ui18n-selector-hover, #f9fafb);\n border-color: var(--ui18n-selector-border-hover, #9ca3af);\n }\n\n .trigger-button:focus {\n border-color: var(--ui18n-selector-focus, #10b981);\n box-shadow: 0 0 0 3px var(--ui18n-selector-focus-shadow, rgba(16, 185, 129, 0.1));\n }\n\n .trigger-button[aria-expanded="true"] {\n border-color: var(--ui18n-selector-focus, #10b981);\n }\n\n .language-label {\n flex: 1;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n min-width: 0;\n }\n\n .language-code {\n color: #9ca3af;\n font-size: 12px;\n flex-shrink: 0;\n white-space: nowrap;\n }\n\n .arrow {\n position: absolute;\n right: 12px;\n top: 50%;\n transform: translateY(-50%);\n pointer-events: none;\n font-size: 10px;\n color: var(--ui18n-selector-color, #1f2937);\n transition: transform 0.2s ease;\n }\n\n .trigger-button[aria-expanded="true"] .arrow {\n transform: translateY(-50%) rotate(180deg);\n }\n\n /* 下拉菜单 */\n .dropdown {\n position: absolute;\n z-index: 50;\n margin-top: 8px;\n width: 100%;\n border: 1px solid var(--ui18n-dropdown-border, #e5e7eb);\n border-radius: 8px;\n background-color: var(--ui18n-dropdown-bg, #ffffff);\n box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);\n display: none;\n }\n\n .dropdown.open {\n display: block;\n }\n\n /* 固定语言列表 */\n .fixed-languages {\n border-bottom: 1px solid var(--ui18n-dropdown-border, #e5e7eb);\n padding: 4px 0;\n margin: 0;\n }\n\n /* 搜索框 */\n .search-container {\n padding: 8px;\n border-bottom: 1px solid var(--ui18n-dropdown-border, #e5e7eb);\n }\n\n /* 搜索结果列表 */\n .search-results {\n border-top: 1px solid var(--ui18n-dropdown-border, #e5e7eb);\n }\n\n .search-input {\n width: 100%;\n padding: 8px 12px;\n font-size: 14px;\n border: 1px solid var(--ui18n-input-border, #d1d5db);\n border-radius: 6px;\n background-color: var(--ui18n-input-bg, #ffffff);\n color: var(--ui18n-input-color, #1f2937);\n outline: none;\n transition: all 0.2s ease;\n }\n\n .search-input:focus {\n border-color: var(--ui18n-input-focus, #10b981);\n box-shadow: 0 0 0 3px var(--ui18n-input-focus-shadow, rgba(16, 185, 129, 0.1));\n }\n\n .search-input::placeholder {\n color: #9ca3af;\n }\n\n /* 语言列表 */\n .language-list {\n max-height: 256px;\n overflow-y: auto;\n padding: 4px 0;\n margin: 0;\n list-style: none;\n }\n\n .language-item {\n padding: 8px 12px;\n font-size: 14px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: space-between;\n transition: background-color 0.15s ease;\n white-space: nowrap;\n gap: 8px;\n }\n\n .language-item:hover {\n background-color: var(--ui18n-item-hover, #f3f4f6);\n }\n\n .language-item.active {\n background-color: var(--ui18n-item-active, #f3f4f6);\n }\n\n .language-item.selected {\n background-color: var(--ui18n-item-selected, #d1fae5);\n }\n\n .language-item-name {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n }\n\n .language-item-code {\n color: #9ca3af;\n font-size: 12px;\n flex-shrink: 0;\n white-space: nowrap;\n }\n\n .no-results {\n padding: 16px 12px;\n text-align: center;\n color: #6b7280;\n font-size: 14px;\n }\n\n /* 暗色主题 */\n :host([theme="dark"]) {\n --ui18n-selector-bg: #1f2937;\n --ui18n-selector-color: #f9fafb;\n --ui18n-selector-border: #374151;\n --ui18n-selector-hover: #374151;\n --ui18n-selector-border-hover: #4b5563;\n --ui18n-selector-focus: #10b981;\n --ui18n-dropdown-bg: #1f2937;\n --ui18n-dropdown-border: #374151;\n --ui18n-input-bg: #374151;\n --ui18n-input-color: #f9fafb;\n --ui18n-input-border: #4b5563;\n --ui18n-item-hover: #374151;\n --ui18n-item-active: #4b5563;\n --ui18n-item-selected: #064e3b;\n }\n\n /* 无障碍支持 */\n .sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n }\n </style>\n\n <div class="selector-container">\n <label class="sr-only" for="ui18n-lang-trigger">选择语言</label>\n <button\n id="ui18n-lang-trigger"\n class="trigger-button"\n type="button"\n aria-haspopup="listbox"\n aria-expanded="${this._isOpen}"\n aria-controls="ui18n-lang-dropdown"\n >\n <span class="language-label">${t}</span>\n <span class="language-code">(${this._current})</span>\n <span class="arrow">▼</span>\n </button>\n\n <div\n id="ui18n-lang-dropdown"\n class="dropdown ${this._isOpen?"open":""}"\n role="dialog"\n aria-modal="true"\n aria-label="语言选择器"\n >\n \x3c!-- 固定语言列表:中文和English --\x3e\n <ul\n class="language-list fixed-languages"\n role="listbox"\n aria-label="常用语言"\n >\n ${n.map(n=>`\n <li\n class="language-item ${n===this._current?"selected":""}"\n role="option"\n aria-selected="${n===this._current}"\n data-lang="${n}"\n data-type="fixed"\n >\n <span class="language-item-name">${a(n)}</span>\n <span class="language-item-code">(${n})</span>\n </li>\n `).join("")}\n </ul>\n\n \x3c!-- 搜索框 --\x3e\n ${this._showSearchBox?`\n <div class="search-container">\n <label class="sr-only" for="ui18n-lang-search">搜索语言</label>\n <input\n id="ui18n-lang-search"\n class="search-input"\n type="text"\n placeholder="${s}"\n value="${this._query}"\n autocomplete="off"\n />\n </div>\n `:""}\n\n \x3c!-- 搜索结果列表(仅在有搜索内容时显示) --\x3e\n ${this._query?`\n <ul\n class="language-list search-results"\n role="listbox"\n aria-label="搜索结果"\n >\n ${0===e.length?'\n <li class="no-results">\n 无法找到该语言,请重新输入\n </li>\n ':e.map((n,e)=>`\n <li\n class="language-item ${n===this._current?"selected":""} ${e===this._activeIndex?"active":""}"\n role="option"\n aria-selected="${n===this._current}"\n data-lang="${n}"\n data-index="${e}"\n data-type="search"\n >\n <span class="language-item-name">${a(n)}</span>\n <span class="language-item-code">(${n})</span>\n </li>\n `).join("")}\n </ul>\n `:""}\n </div>\n </div>\n `}_setupEventListeners(){this.shadowRoot.addEventListener("click",n=>{let e=n.target;for(;e&&e!==this.shadowRoot;){if("ui18n-lang-trigger"===e.id||e.closest("#ui18n-lang-trigger"))return this._toggleDropdown(),void n.stopPropagation();if(e.classList&&e.classList.contains("language-item")){const t=e.getAttribute("data-lang");return console.log("[Selector] 点击语言项:",{lang:t,target:e}),void(t&&(this._selectLanguage(t),n.stopPropagation()))}e=e.parentElement}}),this.shadowRoot.addEventListener("compositionstart",n=>{"ui18n-lang-search"===n.target.id&&(this._isComposing=!0)}),this.shadowRoot.addEventListener("compositionend",n=>{"ui18n-lang-search"===n.target.id&&(this._isComposing=!1,this._handleSearch(n))}),this.shadowRoot.addEventListener("input",n=>{"ui18n-lang-search"===n.target.id&&(this._isComposing||this._handleSearch(n))}),this.shadowRoot.addEventListener("keydown",n=>{"ui18n-lang-search"===n.target.id&&this._handleKeyDown(n)}),this.shadowRoot.addEventListener("mouseenter",n=>{const e=n.target.closest(".language-item");if(e){const n=parseInt(e.getAttribute("data-index"),10);isNaN(n)||(this._activeIndex=n,this.render())}},!0),this._handleOutsideClick=n=>{!this.contains(n.target)&&this._isOpen&&this._closeDropdown()},document.addEventListener("click",this._handleOutsideClick),this._handleEscKey=n=>{"Escape"===n.key&&this._isOpen&&this._closeDropdown()},document.addEventListener("keydown",this._handleEscKey)}disconnectedCallback(){this._handleOutsideClick&&document.removeEventListener("click",this._handleOutsideClick),this._handleEscKey&&document.removeEventListener("keydown",this._handleEscKey)}_toggleDropdown(){this._isOpen?this._closeDropdown():this._openDropdown()}_openDropdown(){this._isOpen=!0,this._query="",this._activeIndex=0,this.render(),setTimeout(()=>{const n=this.shadowRoot.getElementById("ui18n-lang-search");n&&n.focus()},0)}_closeDropdown(){this._isOpen=!1,this._query="",this._activeIndex=0,this.render()}_handleSearch(n){this._query=n.target.value,this._activeIndex=0,this.render(),this._refocusSearchInput()}_handleKeyDown(n){const e=this._getFilteredLanguages();"ArrowDown"===n.key?(n.preventDefault(),this._activeIndex=Math.min(this._activeIndex+1,e.length-1),this.render(),this._refocusSearchInput()):"ArrowUp"===n.key?(n.preventDefault(),this._activeIndex=Math.max(this._activeIndex-1,0),this.render(),this._refocusSearchInput()):"Enter"===n.key?(n.preventDefault(),e.length>0&&this._selectLanguage(e[this._activeIndex])):"Escape"===n.key&&(n.preventDefault(),this._closeDropdown())}_refocusSearchInput(){setTimeout(()=>{const n=this.shadowRoot.getElementById("ui18n-lang-search");n&&(n.focus(),n.setSelectionRange(n.value.length,n.value.length))},0)}_selectLanguage(n){const e=t(n);this._current=e,this.setAttribute("current",e),this.dispatchEvent(new CustomEvent("language-change",{detail:{language:e},bubbles:!0,composed:!0})),this._closeDropdown()}setLanguage(n){this._languages.includes(n)?this._selectLanguage(n):console.warn(`Language "${n}" is not in the available languages list`)}getAvailableLanguages(){return[...this._languages]}open(){this._openDropdown()}close(){this._closeDropdown()}refresh(){this.render()}}customElements.get("ui18n-language-selector")||customElements.define("ui18n-language-selector",r),"undefined"!=typeof window&&(window.UI18nLanguageSelector=r),"undefined"!=typeof window&&(window.UI18nLanguageSelector=r,"object"==typeof window.UI18nSelectorWeb&&(window.UI18nSelectorWeb.UI18nLanguageSelector=r),console.log("[UI18N Selector] Registered to window.UI18nLanguageSelector"),console.log("[UI18N Selector] Custom element:",customElements.get("ui18n-language-selector")?"registered":"pending")),n.LANGUAGE_NAMES=e,n.UI18nLanguageSelector=r,n.filterLanguages=i,n.getLanguageName=function(n){return e[n]||n},n.getUserSystemLanguage=s,n.getUserSystemLanguageLabel=o,n.labelForLang=a,n.normalizeLocale=t}); //# sourceMappingURL=index.umd.min.js.map