/
webru
/
prisma-ui
Обзор
Документация
Войти
/
webru
/
prisma-ui
Код
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ops-select.js
222 строки
8 KB
webru
Improve selects, scrollbars, and datetime typography across the UI.
14 июн 2026, 12:38
14 июн 2026, 12:38
b637d23
Код
Авторство
О чём код?
/** * Prisma UI — Custom select dropdown (glass menu + hover highlight) * Enhances <select class="ops-select"> while keeping native element for forms. */ (function () { var OPEN_CLASS = "is-open"; var openDropdown = null; function copyModifierClasses(from, to) { ["ops-input--filter", "ops-input--sm", "ops-input--lg", "ops-input--mono"].forEach(function (cls) { if (from.classList.contains(cls)) to.classList.add(cls); }); } function getSelectedOption(select) { return select.options[select.selectedIndex] || select.options[0]; } function setSelected(wrapper, select, index) { select.selectedIndex = index; var selected = getSelectedOption(select); wrapper.querySelector(".ops-dropdown__value").textContent = selected ? selected.textContent : ""; wrapper.querySelectorAll(".ops-dropdown__option").forEach(function (btn, i) { btn.classList.toggle("is-selected", i === index); btn.setAttribute("aria-selected", i === index ? "true" : "false"); }); select.dispatchEvent(new Event("change", { bubbles: true })); } function positionMenu(wrapper, trigger, menu) { var rect = trigger.getBoundingClientRect(); menu.style.position = "fixed"; menu.style.top = rect.bottom + 4 + "px"; menu.style.left = rect.left + "px"; menu.style.width = rect.width + "px"; menu.style.minWidth = rect.width + "px"; } function resetMenuPosition(menu) { menu.style.position = ""; menu.style.top = ""; menu.style.left = ""; menu.style.width = ""; menu.style.minWidth = ""; } function closeDropdown(wrapper) { if (!wrapper) return; var menu = wrapper.querySelector(".ops-dropdown__menu"); wrapper.classList.remove(OPEN_CLASS); wrapper.querySelector(".ops-dropdown__trigger").setAttribute("aria-expanded", "false"); wrapper.querySelectorAll(".ops-dropdown__option").forEach(function (btn) { btn.classList.remove("is-focused"); }); if (menu) resetMenuPosition(menu); if (openDropdown === wrapper) openDropdown = null; } function openDropdownMenu(wrapper) { if (openDropdown && openDropdown !== wrapper) closeDropdown(openDropdown); var trigger = wrapper.querySelector(".ops-dropdown__trigger"); var menu = wrapper.querySelector(".ops-dropdown__menu"); positionMenu(wrapper, trigger, menu); wrapper.classList.add(OPEN_CLASS); trigger.setAttribute("aria-expanded", "true"); openDropdown = wrapper; var selected = wrapper.querySelector(".ops-dropdown__option.is-selected"); if (selected) selected.classList.add("is-focused"); } function focusOption(options, index) { options.forEach(function (btn, i) { btn.classList.toggle("is-focused", i === index); }); if (options[index]) options[index].scrollIntoView({ block: "nearest" }); } function enhanceSelect(select) { if (select.dataset.opsEnhanced === "true" || select.dataset.opsNative === "true") return; select.dataset.opsEnhanced = "true"; var wrapper = document.createElement("div"); wrapper.className = "ops-dropdown"; if (select.disabled) wrapper.classList.add("is-disabled"); if (select.classList.contains("ops-select--error")) wrapper.classList.add("ops-dropdown--error"); copyModifierClasses(select, wrapper); if (select.id) wrapper.setAttribute("data-for", select.id); select.parentNode.insertBefore(wrapper, select); wrapper.appendChild(select); select.classList.add("ops-dropdown__native"); select.tabIndex = -1; var trigger = document.createElement("button"); trigger.type = "button"; trigger.className = "ops-dropdown__trigger"; trigger.setAttribute("aria-haspopup", "listbox"); trigger.setAttribute("aria-expanded", "false"); if (select.disabled) trigger.disabled = true; if (select.id) trigger.setAttribute("aria-labelledby", select.id + "-label"); var value = document.createElement("span"); value.className = "ops-dropdown__value"; value.textContent = getSelectedOption(select).textContent; trigger.appendChild(value); var menu = document.createElement("div"); menu.className = "ops-dropdown__menu"; menu.setAttribute("role", "listbox"); var optionButtons = []; Array.from(select.options).forEach(function (opt, index) { var btn = document.createElement("button"); btn.type = "button"; btn.className = "ops-dropdown__option"; btn.setAttribute("role", "option"); btn.dataset.index = String(index); btn.textContent = opt.textContent; btn.disabled = opt.disabled; if (opt.selected) { btn.classList.add("is-selected"); btn.setAttribute("aria-selected", "true"); } else { btn.setAttribute("aria-selected", "false"); } btn.addEventListener("click", function () { if (btn.disabled) return; setSelected(wrapper, select, index); closeDropdown(wrapper); trigger.focus(); }); btn.addEventListener("mouseenter", function () { focusOption(optionButtons, index); }); optionButtons.push(btn); menu.appendChild(btn); }); trigger.addEventListener("click", function () { if (select.disabled) return; if (wrapper.classList.contains(OPEN_CLASS)) closeDropdown(wrapper); else openDropdownMenu(wrapper); }); trigger.addEventListener("keydown", function (e) { if (select.disabled) return; var focusedIndex = optionButtons.findIndex(function (btn) { return btn.classList.contains("is-focused"); }); if (focusedIndex < 0) focusedIndex = select.selectedIndex; if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); if (!wrapper.classList.contains(OPEN_CLASS)) openDropdownMenu(wrapper); var next = e.key === "ArrowDown" ? Math.min(focusedIndex + 1, optionButtons.length - 1) : Math.max(focusedIndex - 1, 0); while (optionButtons[next] && optionButtons[next].disabled) { next = e.key === "ArrowDown" ? next + 1 : next - 1; } if (optionButtons[next]) focusOption(optionButtons, next); } else if (e.key === "Enter" || e.key === " ") { if (!wrapper.classList.contains(OPEN_CLASS)) { e.preventDefault(); openDropdownMenu(wrapper); return; } e.preventDefault(); if (optionButtons[focusedIndex] && !optionButtons[focusedIndex].disabled) { setSelected(wrapper, select, focusedIndex); closeDropdown(wrapper); } } else if (e.key === "Escape") { closeDropdown(wrapper); } }); wrapper.appendChild(trigger); wrapper.appendChild(menu); } function initOpsSelects(root) { (root || document).querySelectorAll("select.ops-select").forEach(enhanceSelect); } document.addEventListener("click", function (e) { if (!openDropdown) return; if (!openDropdown.contains(e.target)) closeDropdown(openDropdown); }); document.addEventListener("keydown", function (e) { if (e.key === "Escape" && openDropdown) closeDropdown(openDropdown); }); window.addEventListener("resize", function () { if (!openDropdown) return; var trigger = openDropdown.querySelector(".ops-dropdown__trigger"); var menu = openDropdown.querySelector(".ops-dropdown__menu"); positionMenu(openDropdown, trigger, menu); }); document.addEventListener("scroll", function () { if (!openDropdown) return; var trigger = openDropdown.querySelector(".ops-dropdown__trigger"); var menu = openDropdown.querySelector(".ops-dropdown__menu"); positionMenu(openDropdown, trigger, menu); }, true); document.addEventListener("DOMContentLoaded", function () { initOpsSelects(); }); window.initOpsSelects = initOpsSelects; })();