PageSourceSearch

https://site.fr/_static/ad2521ddc1b5acb4/scripts/ui/dropdown-select.js

js site.fr collected 2026-10-02 04:05:40 UTC 5,546 bytes, 164 lines download raw bytes

1import { createDropdown, getUniqueDropdownMenuId } from './dropdown.js';
2
3// The select owns the value and form events; the button and listbox present it.
4const bindDropdownToSelect = ({
5  dropdownRoot,
6  trigger,
7  currentLabel,
8  select,
9  labelEl,
10  menuIdBase = 'dropdown-menu',
11  spacing,
12  menuClassName = '',
13  itemClassName = '',
14  menuAttributes = {},
15  emitInput = false,
16}) => {
17  if (!dropdownRoot || !trigger || !currentLabel || !select) {
18    return null;
19  }
20
21  const previousSelectHidden = select.hidden;
22  const previousTriggerHidden = trigger.hidden;
23  const previousLabelFor = labelEl?.getAttribute('for');
24  let destroyed = false;
25  let resetTimer = null;
26  const isDisabled = () => select.disabled || select.matches(':disabled');
27  const updateCurrent = () => {
28    const opt = Array.from(select.options).find((option) => option.value === select.value);
29    const label = (opt?.label || opt?.textContent || select.value || '').trim();
30    if (currentLabel.textContent !== label) currentLabel.textContent = label;
31    if (trigger.disabled !== isDisabled()) trigger.disabled = isDisabled();
32  };
33
34  const getOptions = () =>
35    Array.from(select.options).map((option) => ({
36      value: option.value,
37      label: (option.label || option.textContent || '').trim(),
38      disabled: option.disabled || option.parentElement?.disabled === true,
39      hidden: option.hidden || (option.parentElement?.tagName === 'OPTGROUP' && option.parentElement.hidden),
40    }));
41
42  const setValue = (value, { emit = true } = {}) => {
43    const next = String(value ?? '');
44    if (select.value === next) {
45      updateCurrent();
46      return;
47    }
48
49    select.value = next;
50    updateCurrent();
51    controller?.render();
52    if (emit) {
53      if (emitInput) select.dispatchEvent(new Event('input', { bubbles: true }));
54      select.dispatchEvent(new Event('change', { bubbles: true }));
55    }
56  };
57
58  const menuId = trigger.getAttribute('aria-controls') || menuIdBase;
59  if (!trigger.id) trigger.id = getUniqueDropdownMenuId(`${menuIdBase}-trigger`);
60  if (!currentLabel.id) currentLabel.id = getUniqueDropdownMenuId(`${menuIdBase}-value`);
61  trigger.setAttribute('role', 'combobox');
62  trigger.setAttribute('aria-haspopup', 'listbox');
63  trigger.setAttribute('aria-controls', menuId);
64
65  const controller = createDropdown({
66    dropdown: dropdownRoot,
67    trigger,
68    menuId,
69    spacing,
70    menuClassName: `fixed z-60 ${menuClassName}`.trim(),
71    menuAttributes: { role: 'listbox', 'aria-labelledby': trigger.id, ...menuAttributes, 'data-select-menu': '' },
72    openOnTypeahead: true,
73    renderMenu: (menuEl, setOpen) => {
74      updateCurrent();
75      const focusedValue = menuEl.contains(document.activeElement)
76        ? document.activeElement.getAttribute('data-select-value')
77        : null;
78      menuEl.replaceChildren();
79
80      for (const opt of getOptions()) {
81        if (opt.hidden) continue;
82        const item = document.createElement('button');
83        item.type = 'button';
84        item.tabIndex = -1;
85        item.className = itemClassName;
86        item.textContent = opt.label || opt.value;
87        item.setAttribute('role', 'option');
88        item.setAttribute('aria-selected', String(opt.value === select.value));
89        item.dataset.selectOption = '';
90        item.dataset.selectValue = opt.value;
91        item.dataset.dropdownItem = '';
92        item.dataset.label = opt.label || opt.value;
93
94        if (opt.disabled) {
95          item.disabled = true;
96          item.setAttribute('aria-disabled', 'true');
97        }
98        item.addEventListener('click', () => {
99          if (isDisabled() || opt.disabled) return;
100          setOpen(false, { restoreFocus: true });
101          setValue(opt.value);
102        });
103
104        menuEl.appendChild(item);
105      }
106
107      const rect = trigger.getBoundingClientRect();
108      menuEl.style.minWidth = `${Math.min(rect.width, window.innerWidth - 16)}px`;
109      if (focusedValue !== null) {
110        const enabled = Array.from(menuEl.querySelectorAll('[data-select-option]:not(:disabled)'));
111        const focused = enabled.find((item) => item.dataset.selectValue === focusedValue) || enabled[0];
112        focused?.focus({ preventScroll: true });
113      }
114    },
115  });
116
117  if (labelEl) {
118    labelEl.setAttribute('for', trigger.id);
119  }
120
121  trigger.hidden = false;
122  select.hidden = true;
123
124  const refresh = () => {
125    if (destroyed) return;
126    updateCurrent();
127    if (isDisabled()) controller?.close({ restoreFocus: false });
128    controller?.render();
129  };
130  const onReset = (event) => {
131    window.clearTimeout(resetTimer);
132    resetTimer = window.setTimeout(() => {
133      if (!event.defaultPrevented) refresh();
134    }, 0);
135  };
136  const form = select.form;
137  select.addEventListener('change', refresh);
138  form?.addEventListener('reset', onReset);
139
140  const destroyController = controller?.destroy;
141  if (controller) {
142    controller.destroy = (options) => {
143      if (destroyed) return;
144      destroyed = true;
145      window.clearTimeout(resetTimer);
146      select.removeEventListener('change', refresh);
147      form?.removeEventListener('reset', onReset);
148      destroyController(options);
149      select.hidden = previousSelectHidden;
150      trigger.hidden = previousTriggerHidden;
151      if (labelEl) {
152        if (previousLabelFor === null) labelEl.removeAttribute('for');
153        else labelEl.setAttribute('for', previousLabelFor);
154      }
155    };
156  }
157
158  updateCurrent();
159  controller?.render();
160
161  return { controller, setValue, updateCurrent, refresh };
162};
163
164export { bindDropdownToSelect };

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.