130 lines
4.3 KiB
JavaScript
130 lines
4.3 KiB
JavaScript
// Filename: frontend/js/components/customSelectV2.js
|
|
import { createPopper } from '../vendor/popper.esm.min.js';
|
|
|
|
export default class CustomSelectV2 {
|
|
constructor(container) {
|
|
this.container = container;
|
|
this.trigger = this.container.querySelector('.custom-select-trigger');
|
|
this.nativeSelect = this.container.querySelector('select');
|
|
this.template = this.container.querySelector('.custom-select-panel-template');
|
|
|
|
if (!this.trigger || !this.nativeSelect || !this.template) {
|
|
console.warn('CustomSelectV2 cannot initialize: missing required elements.', this.container);
|
|
return;
|
|
}
|
|
|
|
this.panel = null;
|
|
this.popperInstance = null;
|
|
this.isOpen = false;
|
|
this.triggerText = this.trigger.querySelector('span');
|
|
|
|
if (typeof CustomSelectV2.openInstance === 'undefined') {
|
|
CustomSelectV2.openInstance = null;
|
|
CustomSelectV2.initGlobalListener();
|
|
}
|
|
|
|
this.updateTriggerText();
|
|
this.bindEvents();
|
|
}
|
|
|
|
static initGlobalListener() {
|
|
document.addEventListener('click', (event) => {
|
|
const instance = CustomSelectV2.openInstance;
|
|
if (instance && !instance.container.contains(event.target) && (!instance.panel || !instance.panel.contains(event.target))) {
|
|
instance.close();
|
|
}
|
|
});
|
|
}
|
|
|
|
createPanel() {
|
|
const panelFragment = this.template.content.cloneNode(true);
|
|
this.panel = panelFragment.querySelector('.custom-select-panel');
|
|
document.body.appendChild(this.panel);
|
|
|
|
this.panel.innerHTML = '';
|
|
Array.from(this.nativeSelect.options).forEach(option => {
|
|
const item = document.createElement('a');
|
|
item.href = '#';
|
|
item.className = 'custom-select-option block w-full text-left px-3 py-1.5 text-sm text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-700';
|
|
item.textContent = option.textContent;
|
|
item.dataset.value = option.value;
|
|
if (option.selected) { item.classList.add('is-selected'); }
|
|
this.panel.appendChild(item);
|
|
});
|
|
|
|
this.panel.addEventListener('click', (event) => {
|
|
event.preventDefault();
|
|
const optionEl = event.target.closest('.custom-select-option');
|
|
if (optionEl) { this.selectOption(optionEl); }
|
|
});
|
|
}
|
|
|
|
bindEvents() {
|
|
this.trigger.addEventListener('click', (event) => {
|
|
event.stopPropagation();
|
|
if (CustomSelectV2.openInstance && CustomSelectV2.openInstance !== this) {
|
|
CustomSelectV2.openInstance.close();
|
|
}
|
|
this.toggle();
|
|
});
|
|
}
|
|
|
|
selectOption(optionEl) {
|
|
const selectedValue = optionEl.dataset.value;
|
|
if (this.nativeSelect.value !== selectedValue) {
|
|
this.nativeSelect.value = selectedValue;
|
|
this.nativeSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
this.updateTriggerText();
|
|
this.close();
|
|
}
|
|
|
|
updateTriggerText() {
|
|
const selectedOption = this.nativeSelect.options[this.nativeSelect.selectedIndex];
|
|
if (selectedOption) {
|
|
this.triggerText.textContent = selectedOption.textContent;
|
|
}
|
|
}
|
|
|
|
toggle() { this.isOpen ? this.close() : this.open(); }
|
|
|
|
open() {
|
|
if (this.isOpen) return;
|
|
this.isOpen = true;
|
|
|
|
if (!this.panel) { this.createPanel(); }
|
|
|
|
this.panel.style.display = 'block';
|
|
this.panel.offsetHeight;
|
|
|
|
this.popperInstance = createPopper(this.trigger, this.panel, {
|
|
placement: 'top-start',
|
|
modifiers: [
|
|
{ name: 'offset', options: { offset: [0, 8] } },
|
|
{ name: 'flip', options: { fallbackPlacements: ['bottom-start'] } }
|
|
],
|
|
});
|
|
|
|
CustomSelectV2.openInstance = this;
|
|
}
|
|
|
|
close() {
|
|
if (!this.isOpen) return;
|
|
this.isOpen = false;
|
|
|
|
if (this.popperInstance) {
|
|
this.popperInstance.destroy();
|
|
this.popperInstance = null;
|
|
}
|
|
|
|
if (this.panel) {
|
|
this.panel.remove();
|
|
this.panel = null;
|
|
}
|
|
|
|
if (CustomSelectV2.openInstance === this) {
|
|
CustomSelectV2.openInstance = null;
|
|
}
|
|
}
|
|
}
|