1import { 2 getCurrency, 3 getCurrencyDisplayName, 4 getCurrencyName, 5 searchCurrencies 6} from './currency-catalog.99f7d4e4da22.js'; 7import { t, getEditorMessageLocale } from '../localization/editor-message-runtime.a0a626bcf409.js'; 8import { getLocaleProfile } from '../localization/locale-profiles.9915b2d28745.js'; 9 10// Slice 7: the market's own language for currency names (e.g. "EUR â euro" in Norwegian) used to be 11// patched in after the fact by a market-specific MutationObserver. The combobox already knows the 12// currently active editor locale via getEditorMessageLocale() â deriving a formatting locale tag from 13// it here means every call site below renders the right name the first time, with no DOM re-scan. 14function currentFormattingLocale() { 15 return getLocaleProfile(getEditorMessageLocale()).formattingLocale; 16} 17 18function escapeAttribute(value) { 19 return String(value ?? '') 20 .replaceAll('&', '&') 21 .replaceAll('"', '"') 22 .replaceAll('<', '<') 23 .replaceAll('>', '>'); 24} 25 26export function currencyComboboxHTML(label, path, value, { 27 id = `field-${path.replace(/[^a-z0-9]+/gi, '-')}`, 28 icon = '' 29} = {}) { 30 const listboxId = `${id}-listbox`; 31 const code = String(value || '').trim().toUpperCase(); 32 return ` 33 <div class="field currency-field"> 34 <label for="${escapeAttribute(id)}">${icon}<span>${escapeAttribute(label)}</span></label> 35 <div class="currency-combobox"> 36 <input 37 id="${escapeAttribute(id)}" 38 class="currency-combobox-input" 39 type="text" 40 role="combobox" 41 aria-autocomplete="list" 42 aria-expanded="false" 43 aria-controls="${escapeAttribute(listboxId)}" 44 autocomplete="off" 45 spellcheck="false" 46 data-currency-path="${escapeAttribute(path)}" 47 data-currency-code="${escapeAttribute(code)}" 48 value="${escapeAttribute(getCurrencyDisplayName(code, currentFormattingLocale()))}"> 49 <button type="button" class="currency-combobox-toggle" aria-label="${escapeAttribute(t('editor.aria.showCurrencies'))}" tabindex="-1">â</button> 50 <div id="${escapeAttribute(listboxId)}" class="currency-combobox-list" role="listbox" hidden></div> 51 </div> 52 </div>`; 53} 54 55function optionHTML(currency, activeCode, locale) { 56 const selected = currency.code === activeCode; 57 return `<button type="button" class="currency-option${selected ? ' is-selected' : ''}" role="option" aria-selected="${selected}" data-currency-option="${currency.code}"> 58 <span><strong>${currency.code}</strong> ${escapeAttribute(getCurrencyName(currency.code, locale))}</span> 59 <small>${escapeAttribute(currency.symbol)}</small> 60 </button>`; 61} 62 63function resultsHTML(query, activeCode) { 64 const locale = currentFormattingLocale(); 65 const results = searchCurrencies(query); 66 const groups = []; 67 if (results.popular.length) { 68 groups.push(`<div class="currency-option-group" role="group" aria-label="${escapeAttribute(t('editor.aria.popularCurrencies'))}"><div class="currency-option-heading">${t('editor.field.popularCurrenciesHeading')}</div>${results.popular.map((currency) => optionHTML(currency, activeCode, locale)).join('')}</div>`); 69 } 70 if (results.all.length) { 71 groups.push(`<div class="currency-option-group" role="group" aria-label="${escapeAttribute(t('editor.field.allCurrencies'))}"><div class="currency-option-heading">${t('editor.field.allCurrencies')}</div>${results.all.map((currency) => optionHTML(currency, activeCode, locale)).join('')}</div>`); 72 } 73 return groups.join('') || `<p class="currency-empty">${t('editor.field.noCurrencyFound')}</p>`; 74} 75 76export function bindCurrencyComboboxes(root, { onSelect } = {}) { 77 root.querySelectorAll('[data-currency-path]').forEach((input) => { 78 if (input.dataset.currencyBound === 'true') return; 79 input.dataset.currencyBound = 'true'; 80 const wrapper = input.closest('.currency-combobox'); 81 const listbox = wrapper.querySelector('[role="listbox"]'); 82 const toggle = wrapper.querySelector('.currency-combobox-toggle'); 83 let activeIndex = -1; 84 85 const options = () => Array.from(listbox.querySelectorAll('[data-currency-option]')); 86 const show = (query = '') => { 87 listbox.innerHTML = resultsHTML(query, input.dataset.currencyCode); 88 listbox.hidden = false;
89 input.setAttribute('aria-expanded', 'true'); 90 activeIndex = -1; 91 }; 92 const close = ({ restore = true } = {}) => { 93 listbox.hidden = true; 94 input.setAttribute('aria-expanded', 'false'); 95 input.removeAttribute('aria-activedescendant'); 96 if (restore) input.value = getCurrencyDisplayName(input.dataset.currencyCode, currentFormattingLocale()); 97 activeIndex = -1; 98 }; 99 const focusOption = (nextIndex) => { 100 const available = options(); 101 if (!available.length) return; 102 activeIndex = (nextIndex + available.length) % available.length; 103 available.forEach((option, index) => option.classList.toggle('is-active', index === activeIndex)); 104 const active = available[activeIndex]; 105 if (!active.id) active.id = `${listbox.id}-option-${active.dataset.currencyOption}`; 106 input.setAttribute('aria-activedescendant', active.id); 107 active.scrollIntoView({ block: 'nearest' }); 108 }; 109 const choose = (code) => { 110 const currency = getCurrency(code); 111 if (!currency) return; 112 input.dataset.currencyCode = currency.code; 113 input.value = getCurrencyDisplayName(currency.code, currentFormattingLocale()); 114 close({ restore: false }); 115 onSelect?.(input.dataset.currencyPath, currency.code, input); 116 }; 117 118 input.addEventListener('focus', () => show('')); 119 input.addEventListener('input', () => show(input.value)); 120 input.addEventListener('keydown', (event) => { 121 if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { 122 event.preventDefault(); 123 if (listbox.hidden) show(''); 124 focusOption(activeIndex + (event.key === 'ArrowDown' ? 1 : -1)); 125 } else if (event.key === 'Enter' && activeIndex >= 0) { 126 event.preventDefault(); 127 choose(options()[activeIndex]?.dataset.currencyOption); 128 } else if (event.key === 'Escape') { 129 event.preventDefault(); 130 close(); 131 } else if (event.key === 'Tab') { 132 close(); 133 } 134 }); 135 wrapper.addEventListener('focusout', (event) => { 136 if (!wrapper.contains(event.relatedTarget)) close(); 137 }); 138 listbox.addEventListener('mousedown', (event) => event.preventDefault()); 139 listbox.addEventListener('click', (event) => { 140 const option = event.target.closest('[data-currency-option]'); 141 if (option) choose(option.dataset.currencyOption); 142 }); 143 toggle.addEventListener('click', () => { 144 if (listbox.hidden) { 145 input.focus(); 146 show(''); 147 } else { 148 close(); 149 } 150 }); 151 }); 152}
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.