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

127 lines
4.5 KiB
JavaScript

// Filename: frontend/js/components/customSelect.js
export default class CustomSelect {
constructor(container) {
this.container = container;
this.trigger = this.container.querySelector('.custom-select-trigger');
this.panel = this.container.querySelector('.custom-select-panel');
if (!this.trigger || !this.panel) {
console.warn('CustomSelect cannot initialize: missing .custom-select-trigger or .custom-select-panel.', this.container);
return;
}
this.nativeSelect = this.container.querySelector('select');
this.triggerText = this.trigger.querySelector('span');
this.template = this.panel.querySelector('.custom-select-option-template');
if (typeof CustomSelect.openInstance === 'undefined') {
CustomSelect.openInstance = null;
CustomSelect.initGlobalListener();
}
if (this.nativeSelect) {
this.generateOptions();
this.updateTriggerText();
}
this.bindEvents();
}
static initGlobalListener() {
document.addEventListener('click', (event) => {
if (CustomSelect.openInstance && !CustomSelect.openInstance.container.contains(event.target)) {
CustomSelect.openInstance.close();
}
});
}
generateOptions() {
this.panel.querySelectorAll(':scope > *:not(.custom-select-option-template)').forEach(child => child.remove());
Array.from(this.nativeSelect.options).forEach(option => {
let item;
if (this.template) {
item = this.template.cloneNode(true);
item.classList.remove('custom-select-option-template');
item.removeAttribute('hidden');
} else {
item = document.createElement('a');
item.href = '#';
item.className = 'block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-600';
}
item.classList.add('custom-select-option');
item.textContent = option.textContent;
item.dataset.value = option.value;
if (option.selected) { item.classList.add('is-selected'); }
this.panel.appendChild(item);
});
}
bindEvents() {
this.trigger.addEventListener('click', (event) => {
// [NEW] Guard clause: If the trigger is functionally disabled, do nothing.
if (this.trigger.classList.contains('is-disabled')) {
return;
}
event.stopPropagation();
if (CustomSelect.openInstance && CustomSelect.openInstance !== this) {
CustomSelect.openInstance.close();
}
this.toggle();
});
if (this.nativeSelect) {
this.panel.addEventListener('click', (event) => {
event.preventDefault();
const option = event.target.closest('.custom-select-option');
if (option) { this.selectOption(option); }
});
}
}
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.panel.querySelectorAll('.custom-select-option').forEach(el => el.classList.remove('is-selected'));
optionEl.classList.add('is-selected');
this.close();
}
updateTriggerText() {
// [IMPROVEMENT] Guard against missing elements.
if (!this.nativeSelect || !this.triggerText) return;
const selectedOption = this.nativeSelect.options[this.nativeSelect.selectedIndex];
if (selectedOption) {
this.triggerText.textContent = selectedOption.textContent;
}
}
toggle() {
this.panel.classList.toggle('hidden');
if (this.panel.classList.contains('hidden')) {
if (CustomSelect.openInstance === this) {
CustomSelect.openInstance = null;
}
} else {
CustomSelect.openInstance = this;
}
}
open() {
this.panel.classList.remove('hidden');
CustomSelect.openInstance = this;
}
close() {
this.panel.classList.add('hidden');
if (CustomSelect.openInstance === this) {
CustomSelect.openInstance = null;
}
}
}