/* 主题化 combobox:替代原生 select 弹层,日夜均走设计 token。保留原生字段提交。 */ (function (global) { var listbox = null; var active = null; var activeIndex = -1; var bound = false; var patched = false; var opening = false; function optionList(select) { return Array.prototype.slice.call(select.options); } function selectedOption(select) { return select.options[select.selectedIndex] || null; } function isPlaceholder(option) { if (!option) return true; return option.value === "" || option.hasAttribute("data-placeholder"); } function triggerLabel(select) { var option = selectedOption(select); if (!option) return "请选择"; return option.textContent || "请选择"; } function ensureListbox() { if (listbox) return; listbox = document.createElement("div"); listbox.id = "ds-listbox"; listbox.className = "ds-listbox"; listbox.hidden = true; listbox.setAttribute("role", "listbox"); listbox.setAttribute("tabindex", "-1"); document.body.appendChild(listbox); listbox.addEventListener("mousedown", function (event) { event.preventDefault(); }); listbox.addEventListener("click", function (event) { var item = event.target.closest("[data-opt-index]"); if (!item || item.getAttribute("aria-disabled") === "true") return; commit(Number(item.getAttribute("data-opt-index"))); }); } function escapeId(id) { if (global.CSS && typeof CSS.escape === "function") return CSS.escape(id); return String(id).replace(/\\/g, "\\\\").replace(/"/g, '\\"'); } function findLabel(select) { if (select.id) { var byFor = document.querySelector('label[for="' + escapeId(select.id) + '"]'); if (byFor) return byFor; } return select.closest("label"); } function labelText(select) { var label = findLabel(select); if (label) return (label.textContent || "").replace(/\s+/g, " ").trim(); if (select.getAttribute("aria-label")) return select.getAttribute("aria-label"); return ""; } function syncTrigger(select) { var wrap = select.closest(".ds-combo"); if (!wrap) return; var btn = wrap.querySelector(".ds-combo-trigger"); if (!btn) return; var option = selectedOption(select); btn.textContent = triggerLabel(select); btn.classList.toggle("is-placeholder", isPlaceholder(option)); wrap.classList.toggle("is-disabled", select.disabled); btn.disabled = select.disabled; if (select.disabled) btn.setAttribute("aria-disabled", "true"); else btn.removeAttribute("aria-disabled"); } function enhance(select) { if (!select || select.dataset.dsCombo === "1") return; if (select.multiple || Number(select.size) > 1) return; select.dataset.dsCombo = "1"; var wrap = document.createElement("div"); wrap.className = "ds-combo"; if (select.classList.contains("ds-dp-ym")) wrap.classList.add("ds-combo--compact"); select.parentNode.insertBefore(wrap, select); wrap.appendChild(select); select.classList.add("ds-combo-native"); select.tabIndex = -1; select.setAttribute("aria-hidden", "true"); var btn = document.createElement("button"); btn.type = "button"; btn.className = "ds-combo-trigger"; btn.setAttribute("role", "combobox"); btn.setAttribute("aria-haspopup", "listbox"); btn.setAttribute("aria-expanded", "false"); btn.setAttribute("aria-controls", "ds-listbox"); var name = labelText(select); if (name) btn.setAttribute("aria-label", name); wrap.appendChild(btn); syncTrigger(select); select.addEventListener("focus", function () { if (!opening) btn.focus({ preventScroll: true }); }); select.addEventListener("invalid", function () { btn.focus({ preventScroll: true }); }); select.addEventListener("change", function () { syncTrigger(select); }); wrap._dsSelect = select; wrap._dsTrigger = btn; select._dsComboSync = function () { syncTrigger(select); if (active === wrap) fillListbox(); }; } function scan(root) { var scope = root && root.querySelectorAll ? root : document; scope.querySelectorAll("select.select").forEach(enhance); } function enabledIndexes(select) { var out = []; optionList(select).forEach(function (option, index) { if (!option.disabled) out.push(index); }); return out; } function fillListbox() { if (!active || !listbox) return; var select = active._dsSelect; var html = ""; optionList(select).forEach(function (option, index) { var selected = index === select.selectedIndex; var disabled = option.disabled; var cls = "ds-listbox-option"; if (isPlaceholder(option)) cls += " is-placeholder"; if (selected) cls += " is-selected"; if (disabled) cls += " is-disabled"; if (index === activeIndex) cls += " is-active"; html += '
" + (option.textContent || "") + "
"; }); listbox.innerHTML = html; var btn = active._dsTrigger; if (activeIndex >= 0) btn.setAttribute("aria-activedescendant", "ds-listbox-opt-" + activeIndex); else btn.removeAttribute("aria-activedescendant"); position(); var activeEl = listbox.querySelector(".is-active") || listbox.querySelector(".is-selected"); if (activeEl && activeEl.scrollIntoView) { activeEl.scrollIntoView({ block: "nearest" }); } } function viewportBox() { var vv = global.visualViewport; if (vv) { return { left: vv.offsetLeft, top: vv.offsetTop, width: vv.width, height: vv.height }; } return { left: 0, top: 0, width: global.innerWidth, height: global.innerHeight }; } function position() { if (!listbox || listbox.hidden || !active) return; var rect = active.getBoundingClientRect(); var view = viewportBox(); var gap = 4; var width = Math.max(rect.width, 120); listbox.style.minWidth = Math.round(width) + "px"; listbox.style.width = "max-content"; listbox.style.maxWidth = Math.round(Math.min(view.width - 16, Math.max(width, 280))) + "px"; var height = listbox.offsetHeight || 200; var left = rect.left; var top = rect.bottom + gap; if (top + height > view.top + view.height - 8 && rect.top - gap - height >= view.top + 8) { top = rect.top - gap - height; } if (left + width > view.left + view.width - 8) { left = Math.max(view.left + 8, rect.right - width); } if (left < view.left + 8) left = view.left + 8; if (top < view.top + 8) top = view.top + 8; listbox.style.left = Math.round(left) + "px"; listbox.style.top = Math.round(top) + "px"; } function openCombo(wrap) { if (!wrap || wrap.classList.contains("is-disabled")) return; var select = wrap._dsSelect; if (!select || select.disabled) return; ensureListbox(); if (active && active !== wrap) closeCombo(); active = wrap; activeIndex = select.selectedIndex >= 0 ? select.selectedIndex : 0; if (select.options[activeIndex] && select.options[activeIndex].disabled) { var enabled = enabledIndexes(select); activeIndex = enabled.length ? enabled[0] : -1; } wrap.classList.add("is-open"); wrap._dsTrigger.setAttribute("aria-expanded", "true"); document.body.appendChild(listbox); listbox.hidden = false; fillListbox(); } function closeCombo() { if (!active) { if (listbox) listbox.hidden = true; return; } var wrap = active; wrap.classList.remove("is-open"); wrap._dsTrigger.setAttribute("aria-expanded", "false"); wrap._dsTrigger.removeAttribute("aria-activedescendant"); active = null; activeIndex = -1; if (listbox) { listbox.hidden = true; listbox.innerHTML = ""; } } function commit(index) { if (!active) return; var select = active._dsSelect; var option = select.options[index]; if (!option || option.disabled) return; var wrap = active; opening = true; select.selectedIndex = index; select.dispatchEvent(new Event("input", { bubbles: true })); select.dispatchEvent(new Event("change", { bubbles: true })); syncTrigger(select); closeCombo(); wrap._dsTrigger.focus({ preventScroll: true }); opening = false; } function moveActive(delta) { if (!active) return; var enabled = enabledIndexes(active._dsSelect); if (!enabled.length) return; var pos = enabled.indexOf(activeIndex); if (pos < 0) pos = delta > 0 ? -1 : 0; pos = (pos + delta + enabled.length * 8) % enabled.length; activeIndex = enabled[pos]; fillListbox(); } function onPointer(event) { var wrap = event.target.closest && event.target.closest(".ds-combo"); if (wrap && wrap._dsTrigger && (event.target === wrap._dsTrigger || wrap._dsTrigger.contains(event.target))) { if (event.type === "mousedown" && event.button === 0) event.preventDefault(); if (wrap.classList.contains("is-open")) closeCombo(); else openCombo(wrap); return; } if (listbox && !listbox.hidden && listbox.contains(event.target)) return; if (active) closeCombo(); } function onKey(event) { var wrap = event.target.closest && event.target.closest(".ds-combo"); if (event.key === "Escape") { if (active) { event.preventDefault(); var trigger = active._dsTrigger; closeCombo(); if (trigger) trigger.focus({ preventScroll: true }); } return; } if (!wrap || event.target !== wrap._dsTrigger) return; var open = wrap.classList.contains("is-open"); if (event.key === "ArrowDown") { event.preventDefault(); if (!open) openCombo(wrap); else moveActive(1); return; } if (event.key === "ArrowUp") { event.preventDefault(); if (!open) openCombo(wrap); else moveActive(-1); return; } if (event.key === "Home" && open) { event.preventDefault(); var first = enabledIndexes(wrap._dsSelect); if (first.length) { activeIndex = first[0]; fillListbox(); } return; } if (event.key === "End" && open) { event.preventDefault(); var last = enabledIndexes(wrap._dsSelect); if (last.length) { activeIndex = last[last.length - 1]; fillListbox(); } return; } if ((event.key === "Enter" || event.key === " ") && open) { event.preventDefault(); if (activeIndex >= 0) commit(activeIndex); return; } if ((event.key === "Enter" || event.key === " ") && !open) { event.preventDefault(); openCombo(wrap); } } function patchSelectValue() { if (patched) return; patched = true; ["value", "selectedIndex"].forEach(function (prop) { var desc = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, prop); if (!desc || !desc.set || !desc.get) return; Object.defineProperty(HTMLSelectElement.prototype, prop, { get: desc.get, set: function (value) { desc.set.call(this, value); if (typeof this._dsComboSync === "function") this._dsComboSync(); }, configurable: true, enumerable: desc.enumerable, }); }); } function bind() { if (bound) return; bound = true; patchSelectValue(); document.addEventListener("mousedown", onPointer, true); document.addEventListener("keydown", onKey); document.addEventListener("scroll", position, true); global.addEventListener("resize", position); if (global.visualViewport) { global.visualViewport.addEventListener("resize", position); global.visualViewport.addEventListener("scroll", position); } if (typeof MutationObserver === "function") { new MutationObserver(function (records) { records.forEach(function (record) { if (record.type === "childList") { record.addedNodes.forEach(function (node) { if (node.nodeType !== 1) return; if (node.matches && node.matches("select.select")) enhance(node); if (node.querySelectorAll) scan(node); }); if (record.target && record.target.tagName === "SELECT" && record.target._dsComboSync) { record.target._dsComboSync(); } } if (record.type === "attributes" && record.target.tagName === "SELECT" && record.target._dsComboSync) { record.target._dsComboSync(); } }); }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["disabled", "value"], }); } scan(document); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", bind); } else { bind(); } global.JinniuSelect = { scan: scan, open: function (select) { if (select && select.closest) openCombo(select.closest(".ds-combo")); }, close: closeCombo, }; })(window);