PageSourceSearch

https://invoicecraftly.com/_assets/js/features/currency/currency-combobox.757d399144f7.js

js invoicecraftly.com collected 2026-09-24 14:47:02 UTC 6,880 bytes, 152 lines download raw bytes

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('<', '&lt;')
23    .replaceAll('>', '&gt;');
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.