Files
gemini-banlancer/frontend/js/components/customSelectV2.js
2025-11-24 20:47:12 +08:00

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