Files
basecoat/src/js/select.js
T

95 lines
3.4 KiB
JavaScript
Raw Normal View History

2025-06-12 10:37:19 +08:00
(() => {
const initSelect = (select) => {
const trigger = select.querySelector(':scope > [popovertarget]');
const selectedValue = trigger.querySelector(':scope > span');
const popover = select.querySelector(':scope > [popover]');
const listbox = popover.querySelector('[role="listbox"]');
const input = select.querySelector(':scope > input[type="hidden"]');
if (!trigger || !popover || !listbox || !input) return;
2025-05-01 20:53:03 +08:00
2025-06-12 10:37:19 +08:00
const options = Array.from(listbox.querySelectorAll('[role="option"]'));
let activeIndex = -1;
2025-05-01 20:53:03 +08:00
2025-06-12 10:37:19 +08:00
const updateValue = (option) => {
2025-06-08 12:32:52 +08:00
if (option) {
2025-06-12 10:37:19 +08:00
selectedValue.innerHTML = option.dataset.label || option.innerHTML;
input.value = option.dataset.value;
listbox.querySelector('[role="option"][aria-selected="true"]')?.removeAttribute('aria-selected');
option.setAttribute('aria-selected', 'true');
2025-05-01 20:53:03 +08:00
}
2025-06-12 10:37:19 +08:00
};
2025-05-01 20:53:03 +08:00
2025-06-12 10:37:19 +08:00
const selectOption = (option) => {
if (!option) return;
updateValue(option);
trigger.removeAttribute('aria-activedescendant');
options.forEach(opt => opt.classList.remove('active'));
activeIndex = -1;
popover.hidePopover();
};
let initialOption = options.find(opt => input.value && opt.dataset.value === input.value);
if (!initialOption && options.length > 0) initialOption = options[0];
updateValue(initialOption);
trigger.addEventListener('keydown', (e) => {
if (!popover.matches(':popover-open')) return;
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
if (activeIndex > -1) options[activeIndex]?.classList.remove('active');
if (e.key === 'ArrowDown') activeIndex = (activeIndex + 1) % options.length;
else activeIndex = (activeIndex - 1 + options.length) % options.length;
const activeOption = options[activeIndex];
if (activeOption) {
activeOption.classList.add('active');
if (activeOption.id) {
trigger.setAttribute('aria-activedescendant', activeOption.id);
}
activeOption.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
} else if (e.key === 'Enter') {
e.preventDefault();
selectOption(options[activeIndex]);
2025-05-01 20:53:03 +08:00
}
2025-06-12 10:37:19 +08:00
});
listbox.addEventListener('click', (e) => {
const clickedOption = e.target.closest('[role="option"]');
if (clickedOption) selectOption(clickedOption);
});
popover.addEventListener('toggle', (e) => {
if (e.newState === 'closed') {
trigger.removeAttribute('aria-activedescendant');
if (activeIndex > -1) options[activeIndex]?.classList.remove('active');
activeIndex = -1;
2025-05-01 20:53:03 +08:00
}
2025-06-12 10:37:19 +08:00
});
select.dataset.selectInitialized = true;
};
document.querySelectorAll('div.select:not([data-select-initialized])').forEach(initSelect);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches('div.select:not([data-select-initialized])')) {
initSelect(node);
}
node.querySelectorAll('div.select:not([data-select-initialized])').forEach(initSelect);
2025-05-01 20:53:03 +08:00
}
});
2025-06-12 10:37:19 +08:00
});
});
2025-05-01 20:53:03 +08:00
2025-06-12 10:37:19 +08:00
// Start observing the whole document
observer.observe(document.body, { childList: true, subtree: true });
})();