// 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; } } }