'use strict'; const customSelectRegistry = new WeakMap(); let customSelectOpenState = null; let customSelectSequence = 0; let customSelectObserver = null; let customSelectPropertiesPatched = false; function patchCustomSelectProperties() { if (customSelectPropertiesPatched) return; customSelectPropertiesPatched = true; ['value', 'selectedIndex'].forEach(property => { const descriptor = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, property); if (!descriptor?.get || !descriptor?.set || descriptor.configurable === false) return; try { Object.defineProperty(HTMLSelectElement.prototype, property, { configurable: descriptor.configurable, enumerable: descriptor.enumerable, get: descriptor.get, set(nextValue) { descriptor.set.call(this, nextValue); queueMicrotask(() => refreshCustomSelect(this)); }, }); } catch (_) { } }); } function customSelectSelectedOption(select) { return select.options?.[select.selectedIndex] || null; } function customSelectLabel(select) { const explicit = select.getAttribute('aria-label'); if (explicit) return explicit; const labelledBy = select.getAttribute('aria-labelledby'); if (labelledBy) { const text = labelledBy.split(/\s+/).map(id => document.getElementById(id)?.textContent?.trim()).filter(Boolean).join(' '); if (text) return text; } const label = select.closest('label'); const labelSpan = label?.querySelector(':scope > span'); return labelSpan?.textContent?.trim() || select.name || select.id || ''; } function customSelectOptionRows(select) { const rows = []; [...select.children].forEach(child => { if (child instanceof HTMLOptGroupElement) { rows.push({ type: 'group', label: child.label }); [...child.children].forEach(option => { if (option instanceof HTMLOptionElement) rows.push({ type: 'option', option, index: [...select.options].indexOf(option) }); }); } else if (child instanceof HTMLOptionElement) { rows.push({ type: 'option', option: child, index: [...select.options].indexOf(child) }); } }); return rows; } function positionCustomSelectMenu(state) { if (!state?.open || !state.trigger?.isConnected || !state.menu?.isConnected) return; const rect = state.trigger.getBoundingClientRect(); const margin = 8; const gap = 6; const toolbar = state.toolbar; const viewportWidth = document.documentElement.clientWidth || window.innerWidth; const viewportHeight = document.documentElement.clientHeight || window.innerHeight; const width = Math.min(Math.max(rect.width, toolbar ? 210 : 180), Math.max(180, viewportWidth - margin * 2)); const below = Math.max(0, viewportHeight - rect.bottom - gap - margin); const above = Math.max(0, rect.top - gap - margin); state.menu.style.width = `${Math.round(width)}px`; state.menu.style.maxHeight = '320px'; // Once the popover is visible, scrollHeight is the natural menu height. // Use that height to choose the side and, especially, to place short menus // directly above the trigger instead of at the top of a 320px allowance. const naturalHeight = Math.min(320, Math.max(0, state.menu.scrollHeight || state.menu.getBoundingClientRect().height || 0)); const desiredHeight = naturalHeight || 180; const useBelow = below >= desiredHeight || (below >= above && above < desiredHeight); const available = Math.max(1, Math.min(320, useBelow ? below : above)); state.menu.style.maxHeight = `${Math.round(available)}px`; const renderedHeight = naturalHeight ? Math.min(naturalHeight, available) : available; const left = Math.min(Math.max(margin, rect.left), Math.max(margin, viewportWidth - width - margin)); const rawTop = useBelow ? rect.bottom + gap : rect.top - gap - renderedHeight; const maxTop = Math.max(margin, viewportHeight - renderedHeight - margin); const top = Math.min(Math.max(margin, rawTop), maxTop); state.menu.style.left = `${Math.round(left)}px`; state.menu.style.top = `${Math.round(top)}px`; state.menu.dataset.placement = useBelow ? 'bottom' : 'top'; } function rebuildCustomSelectMenu(select, state) { const rows = customSelectOptionRows(select); state.menu.replaceChildren(); if (!rows.length) { const empty = document.createElement('div'); empty.className = 'custom-select-empty'; empty.textContent = '—'; state.menu.appendChild(empty); return; } rows.forEach(row => { if (row.type === 'group') { const group = document.createElement('div'); group.className = 'custom-select-group'; group.textContent = row.label; state.menu.appendChild(group); return; } const { option, index } = row; const button = document.createElement('button'); button.type = 'button'; button.className = 'custom-select-option'; button.dataset.index = String(index); button.setAttribute('role', 'option'); button.setAttribute('aria-selected', option.selected ? 'true' : 'false'); button.disabled = option.disabled || option.parentElement?.disabled || select.disabled; const text = document.createElement('span'); text.textContent = option.textContent || option.label || option.value || '—'; const check = document.createElement('b'); check.setAttribute('aria-hidden', 'true'); check.innerHTML = option.selected ? uiIcon('check') : ''; button.append(text, check); button.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); if (button.disabled) return; const nextIndex = Number(button.dataset.index); if (!Number.isInteger(nextIndex) || !select.options[nextIndex]) return; const changed = select.selectedIndex !== nextIndex; select.selectedIndex = nextIndex; refreshCustomSelect(select); closeCustomSelect(); if (changed) { select.dispatchEvent(new Event('input', { bubbles: true })); select.dispatchEvent(new Event('change', { bubbles: true })); } state.trigger.focus({ preventScroll: true }); }); state.menu.appendChild(button); }); } function refreshCustomSelect(select) { const state = customSelectRegistry.get(select); if (!state) return; const selected = customSelectSelectedOption(select); const text = selected?.textContent?.trim() || selected?.label || selected?.value || '—'; const label = customSelectLabel(select); const disabled = !!select.disabled; if (state.toolbar) { state.trigger.classList.toggle('custom-select-disabled', disabled); state.trigger.setAttribute('aria-disabled', disabled ? 'true' : 'false'); state.trigger.setAttribute('aria-label', label ? `${label}: ${text}` : text); state.trigger.tabIndex = disabled ? -1 : 0; } else { state.value.textContent = text; state.trigger.disabled = disabled; state.trigger.setAttribute('aria-label', label ? `${label}: ${text}` : text); state.wrapper.hidden = !!select.hidden; } if (state.open) { rebuildCustomSelectMenu(select, state); requestAnimationFrame(() => positionCustomSelectMenu(state)); } } function closeCustomSelect(state = customSelectOpenState) { if (!state?.open) return; state.open = false; state.trigger.setAttribute('aria-expanded', 'false'); state.trigger.classList.remove('custom-select-open'); state.wrapper?.classList.remove('custom-select-open'); if (typeof state.menu.hidePopover === 'function') { try { if (state.menu.matches(':popover-open')) state.menu.hidePopover(); } catch (_) { } } state.menu.classList.remove('custom-select-menu-fallback-open'); state.menu.hidden = true; if (customSelectOpenState === state) customSelectOpenState = null; } function openCustomSelect(select) { const state = customSelectRegistry.get(select); if (!state || select.disabled) return; if (customSelectOpenState === state && state.open) { closeCustomSelect(state); return; } closeCustomSelect(); refreshCustomSelect(select); rebuildCustomSelectMenu(select, state); state.open = true; customSelectOpenState = state; state.trigger.setAttribute('aria-expanded', 'true'); state.trigger.classList.add('custom-select-open'); state.wrapper?.classList.add('custom-select-open'); state.menu.hidden = false; positionCustomSelectMenu(state); if (typeof state.menu.showPopover === 'function') { try { state.menu.showPopover(); } catch (_) { state.menu.classList.add('custom-select-menu-fallback-open'); } } else { state.menu.classList.add('custom-select-menu-fallback-open'); } positionCustomSelectMenu(state); requestAnimationFrame(() => { positionCustomSelectMenu(state); state.menu.querySelector('.custom-select-option[aria-selected="true"]')?.scrollIntoView({ block: 'nearest' }); }); } function customSelectKeyboard(event, select) { if (select.disabled) return; const state = customSelectRegistry.get(select); if (!state) return; if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); if (!state.open) { openCustomSelect(select); return; } const enabled = [...state.menu.querySelectorAll('.custom-select-option:not(:disabled)')]; if (!enabled.length) return; const active = document.activeElement; const current = enabled.indexOf(active); const delta = event.key === 'ArrowUp' ? -1 : 1; enabled[(current + delta + enabled.length) % enabled.length].focus(); } else if (event.key === 'Escape' && state.open) { event.preventDefault(); closeCustomSelect(state); state.trigger.focus({ preventScroll: true }); } else if (event.key === 'Home' && state.open) { event.preventDefault(); state.menu.querySelector('.custom-select-option:not(:disabled)')?.focus(); } else if (event.key === 'End' && state.open) { event.preventDefault(); [...state.menu.querySelectorAll('.custom-select-option:not(:disabled)')].at(-1)?.focus(); } } function enhanceCustomSelect(select) { if (!(select instanceof HTMLSelectElement) || customSelectRegistry.has(select) || select.multiple || select.size > 1 || select.dataset.nativeSelect === 'true') return; const toolbarHost = select.classList.contains('toolbar-picker-select') ? select.closest('.toolbar-picker') : null; const originalSelectState = { tabIndex: select.getAttribute('tabindex'), ariaHidden: select.getAttribute('aria-hidden'), }; const originalToolbarState = toolbarHost ? { role: toolbarHost.getAttribute('role'), ariaHaspopup: toolbarHost.getAttribute('aria-haspopup'), ariaControls: toolbarHost.getAttribute('aria-controls'), ariaExpanded: toolbarHost.getAttribute('aria-expanded'), ariaDisabled: toolbarHost.getAttribute('aria-disabled'), } : null; const menu = document.createElement('div'); const menuId = `customSelectMenu${++customSelectSequence}`; menu.id = menuId; menu.className = 'custom-select-menu'; menu.setAttribute('role', 'listbox'); menu.setAttribute('popover', 'manual'); menu.hidden = true; document.body.appendChild(menu); let wrapper = null; let trigger = toolbarHost; let value = null; select.classList.add('select-native-proxy'); select.tabIndex = -1; select.setAttribute('aria-hidden', 'true'); if (toolbarHost) { toolbarHost.classList.add('custom-select-toolbar'); toolbarHost.setAttribute('role', 'button'); toolbarHost.setAttribute('aria-haspopup', 'listbox'); toolbarHost.setAttribute('aria-controls', menuId); toolbarHost.setAttribute('aria-expanded', 'false'); } else { wrapper = document.createElement('div'); wrapper.className = 'custom-select'; trigger = document.createElement('button'); trigger.type = 'button'; trigger.className = 'custom-select-trigger'; trigger.setAttribute('aria-haspopup', 'listbox'); trigger.setAttribute('aria-controls', menuId); trigger.setAttribute('aria-expanded', 'false'); value = document.createElement('span'); value.className = 'custom-select-value'; const chevron = document.createElement('span'); chevron.className = 'custom-select-chevron'; chevron.setAttribute('aria-hidden', 'true'); chevron.innerHTML = uiIcon('chevron-down'); trigger.append(value, chevron); wrapper.appendChild(trigger); select.insertAdjacentElement('afterend', wrapper); } const state = { select, wrapper, trigger, value, menu, toolbar: !!toolbarHost, open: false, originalSelectState, originalToolbarState, }; customSelectRegistry.set(select, state); trigger.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); openCustomSelect(select); }); trigger.addEventListener('keydown', event => customSelectKeyboard(event, select)); menu.addEventListener('keydown', event => customSelectKeyboard(event, select)); // Clicking an implicit