const closeEventName = "nodedc-select-open"; const makeSelectId = () => `ndc-select-${Math.random().toString(16).slice(2)}`; export function createGlassSelect({ host, value, options = [], ariaLabel = "Выбрать", disabled = false, onChange = null, }) { if (!host) return null; const selectId = makeSelectId(); let currentValue = value; let currentOptions = [...options]; let open = false; let menu = null; let menuRect = { top: 0, left: 0, width: 0 }; host.innerHTML = ""; const root = document.createElement("div"); root.className = "nodedc-select"; const control = document.createElement("div"); control.className = "nodedc-select__control"; const valueEl = document.createElement("div"); valueEl.className = "nodedc-select__value"; const toggle = document.createElement("button"); toggle.type = "button"; toggle.className = "nodedc-select__toggle"; toggle.setAttribute("aria-label", ariaLabel); toggle.setAttribute("aria-haspopup", "listbox"); toggle.disabled = !!disabled; const chevron = document.createElement("span"); chevron.className = "nodedc-select__chevron"; chevron.setAttribute("aria-hidden", "true"); toggle.appendChild(chevron); control.appendChild(valueEl); control.appendChild(toggle); root.appendChild(control); host.appendChild(root); const selectedOption = () => ( currentOptions.find((option) => option.value === currentValue) || currentOptions[0] || null ); const updateValue = () => { const selected = selectedOption(); valueEl.textContent = selected?.label || ""; toggle.setAttribute("aria-expanded", open ? "true" : "false"); }; const updateMenuRect = () => { const rect = root.getBoundingClientRect(); const toggleWidth = 42; const gap = 8; const menuMaxHeight = 232; const bottomTop = rect.bottom + 6; const top = bottomTop + menuMaxHeight > window.innerHeight - 12 ? Math.max(12, rect.top - menuMaxHeight - 6) : bottomTop; menuRect = { top, left: rect.left, width: Math.max(180, rect.width - toggleWidth - gap), }; if (menu) { menu.style.top = `${menuRect.top}px`; menu.style.left = `${menuRect.left}px`; menu.style.width = `${menuRect.width}px`; } }; const renderMenuOptions = () => { if (!menu) return; menu.innerHTML = ""; currentOptions.forEach((option) => { const item = document.createElement("button"); item.type = "button"; item.className = "nodedc-dropdown-option nodedc-select__option"; item.dataset.value = option.value; if (option.value === currentValue) { item.dataset.selected = "true"; } item.setAttribute("role", "option"); item.setAttribute("aria-selected", option.value === currentValue ? "true" : "false"); item.textContent = option.label; item.addEventListener("click", () => { setValue(option.value, { emit: true }); closeMenu(); }); menu.appendChild(item); }); }; const openMenu = () => { if (toggle.disabled || open) return; open = true; updateValue(); updateMenuRect(); window.dispatchEvent(new CustomEvent(closeEventName, { detail: { id: selectId } })); menu = document.createElement("div"); menu.className = "nodedc-dropdown-surface nodedc-select__menu"; menu.setAttribute("role", "listbox"); menu.style.position = "fixed"; menu.style.top = `${menuRect.top}px`; menu.style.left = `${menuRect.left}px`; menu.style.width = `${menuRect.width}px`; renderMenuOptions(); document.body.appendChild(menu); document.addEventListener("mousedown", handleDocumentPointerDown); document.addEventListener("keydown", handleKeyDown); window.addEventListener(closeEventName, handleOtherSelectOpen); window.addEventListener("resize", updateMenuRect); window.addEventListener("scroll", updateMenuRect, true); }; function closeMenu() { if (!open) return; open = false; updateValue(); if (menu) { menu.remove(); menu = null; } document.removeEventListener("mousedown", handleDocumentPointerDown); document.removeEventListener("keydown", handleKeyDown); window.removeEventListener(closeEventName, handleOtherSelectOpen); window.removeEventListener("resize", updateMenuRect); window.removeEventListener("scroll", updateMenuRect, true); } function handleDocumentPointerDown(event) { const target = event.target; if ( target instanceof Node && !root.contains(target) && (!menu || !menu.contains(target)) ) { closeMenu(); } } function handleKeyDown(event) { if (event.key === "Escape") closeMenu(); } function handleOtherSelectOpen(event) { if (event?.detail?.id !== selectId) closeMenu(); } function setValue(nextValue, { emit = false } = {}) { const nextOption = currentOptions.find((option) => option.value === nextValue); if (!nextOption) return; const changed = currentValue !== nextValue; currentValue = nextValue; updateValue(); renderMenuOptions(); if (emit && changed && typeof onChange === "function") { onChange(nextValue); } } const setOptions = (nextOptions = [], nextValue = currentValue) => { currentOptions = [...nextOptions]; currentValue = currentOptions.some((option) => option.value === nextValue) ? nextValue : currentOptions[0]?.value; updateValue(); renderMenuOptions(); }; const setDisabled = (nextDisabled) => { toggle.disabled = !!nextDisabled; if (toggle.disabled) closeMenu(); }; const destroy = () => { closeMenu(); root.remove(); }; toggle.addEventListener("click", () => { if (open) { closeMenu(); } else { openMenu(); } }); valueEl.addEventListener("click", openMenu); updateValue(); return { root, setValue, setOptions, setDisabled, close: closeMenu, destroy, getValue: () => currentValue, }; }