'use strict'; 'require baseclass'; 'require ui'; 'require dom'; 'require fs-fit as fit'; 'require fs-chrome as chrome'; /* Three jobs; 2, 3 and the value resync register with fs-fit (measuring and scheduling stay there): * 1. , values mirrored * both ways. It keeps its own observer on body (childList and `disabled`) so it also catches * selects added later by client CBI, and schedules the scan through fit.frame. * 2. data-table tagging and a11y roles: `.fs-dt`, adopted markup, data-title captions, ARIA roles. * 3. table fit decisions: the card/drop/shred ladder, scrollers, the action-column pin. * The native , which is `display: none`. Focus only. */ if (title) title.addEventListener('click', () => node.focus(), { signal: ac.signal }); sel.setAttribute('aria-hidden', 'true'); sel._fsDd = dd; sel._fsNode = node; sel._fsKey = choicesKey(sel); sel._fsAbort = ac; /* after the select: it must stay frameEl.firstChild for ui.Select to read its value on save */ sel.after(node); /* The dropdown re-dispatches its own bubbling `widget-change` on every pick, open/close and * setValue(), reaching form.js while `sel.value` still holds the old value (the Appearance view's * selectCtl applied 3x per pick). The form is to hear only ui.Select's own `widget-change`, which * the `change` we dispatch on `sel` below produces. */ node.addEventListener('widget-change', (ev) => ev.stopPropagation(), { signal: ac.signal }); /* stops our own dd->sel dispatch from echoing back through the sel->dd listener */ let syncing = false; /* our widget -> native select (user picked an option) */ node.addEventListener('cbi-dropdown-change', () => { const v = dd.getValue(); if (sel.value === v) return; syncing = true; sel.value = v; sel.dispatchEvent(new Event('change', { bubbles: true })); syncing = false; }, { signal: ac.signal }); sel.addEventListener('change', () => { if (syncing) return; showValue(dd, sel); }, { signal: ac.signal }); /* A failing validator lands on the hidden gives this free (picking a country out of 248 entries); enhance() hides it and * ui.Dropdown.handleKeydown does only Esc/Enter/Space/arrows. One document-level listener for every * .cbi-dropdown: only while open, printable keys without modifiers, buffer reset after a pause, a * repeated letter cycles, label first then value. Only highlights (setFocus); Enter/Esc stay * ui.Dropdown's. */ const TYPEAHEAD_RESET_MS = 1000; let _taBuf = '', _taTimer = null, _taLast = null; function typeaheadItems(sb) { const ul = sb.querySelector('ul.dropdown'); if (!ul) return []; return [...ul.children].filter((li) => li.tagName === 'LI' && !li.querySelector('input:not([type="hidden"]):not([type="checkbox"])') && li.getClientRects().length > 0); } function typeaheadLabel(li) { return (li.textContent || '').trim().toLowerCase(); } function wireTypeahead() { document.addEventListener('keydown', (ev) => { if (ev.ctrlKey || ev.altKey || ev.metaKey) return; if (!ev.key || ev.key.length !== 1 || ev.key === ' ') return; /* the create-item input is a text field: let the user type into it */ if (ev.target && ev.target.matches && ev.target.matches('input, textarea')) return; const sb = ev.target.closest?.('.cbi-dropdown[open]'); if (!sb) return; const items = typeaheadItems(sb); if (!items.length) return; /* a new dropdown starts a new search, however fast the user got here */ if (sb !== _taLast) { _taBuf = ''; _taLast = sb; } const ch = ev.key.toLowerCase(); const repeat = (_taBuf.length === 1 && _taBuf === ch); const needle = repeat ? ch : (_taBuf + ch); const start = items.findIndex((li) => li.classList.contains('focus')); const from = repeat ? start + 1 : 0; const matches = (n) => (li) => typeaheadLabel(li).startsWith(n) || String(li.getAttribute('data-value') || '').toLowerCase().startsWith(n); const match = matches(needle); /* wrap around: the second pass covers what the first skipped */ let hit = items.slice(from).find(match) ?? items.find(match); if (!hit && !repeat) { hit = items.find(matches(ch)); if (hit) _taBuf = ''; } if (!hit) return; _taBuf = repeat ? ch : (_taBuf + ch); if (_taTimer) window.clearTimeout(_taTimer); _taTimer = window.setTimeout(() => { _taBuf = ''; _taLast = null; }, TYPEAHEAD_RESET_MS); const inst = dom.findClassInstance(sb); if (inst) inst.setFocus(sb, hit, true); ev.preventDefault(); ev.stopPropagation(); }); } return baseclass.extend({ __init__() { wireTypeahead(); const scan = () => { document.querySelectorAll('select.cbi-input-select:not([data-fs-select])').forEach(enhance); document.querySelectorAll('select.cbi-input-select[data-fs-select="1"]').forEach(resync); }; scan(); fit.armGate(); fit.add(tagDataTables); fit.add(roleTables); fit.add(fitTables); fit.add(fitScrollables); fit.add(unpinActionColumn); fit.add(resyncValues); const scanSoon = fit.frame(scan); new MutationObserver((mutations) => { if (relevant(mutations)) scanSoon(); }).observe(document.body, { childList: true, subtree: true, /* a `disabled` flip mutates no childList, so watch it for resync()/enhance() */ attributes: true, attributeFilter: [ 'disabled' ] }); } });