import { esc } from './utils.js'; export class SearchableSelect { constructor(container, { searchPlaceholder = '...' } = {}) { this.container = container; this.value = ''; this.label = ''; this.groups = []; this.open = false; this.highlightIdx = -1; this.placeholder = container.dataset.placeholder || '...'; this.onSelect = null; this.container.innerHTML = ` `; this.trigger = container.querySelector('.ss__trigger'); this.valueEl = container.querySelector('.ss__value'); this.dropdown = container.querySelector('.ss__dropdown'); this.search = container.querySelector('.ss__search'); this.list = container.querySelector('.ss__list'); this.trigger.addEventListener('click', (e) => { e.stopPropagation(); this.toggle(); }); this.search.addEventListener('input', () => this.render()); this.search.addEventListener('keydown', (e) => this.onKeydown(e)); this.list.addEventListener('click', (e) => { const item = e.target.closest('[data-value]'); if (item) this.select(item.dataset.value, item.dataset.label); }); document.addEventListener('click', (e) => { if (this.open && !this.container.contains(e.target)) this.close(); }); } setGroups(groups) { this.groups = groups; } setValue(val) { for (const g of this.groups) { for (const item of g.items) { if (String(item.id) === String(val)) { this.value = String(item.id); this.label = item.name; this.valueEl.textContent = item.name; this.valueEl.classList.add('ss__value--selected'); return; } } } this.value = ''; this.label = ''; this.valueEl.textContent = this.placeholder; this.valueEl.classList.remove('ss__value--selected'); } getValue() { return this.value; } toggle() { this.open ? this.close() : this.openDropdown(); } openDropdown() { this.open = true; this.dropdown.hidden = false; this.search.value = ''; this.highlightIdx = -1; this.render(); this.search.focus(); } close() { this.open = false; this.dropdown.hidden = true; } select(val, label) { this.value = val; this.label = label; this.valueEl.textContent = label; this.valueEl.classList.add('ss__value--selected'); this.close(); if (this.onSelect) this.onSelect(val, label); } focus() { this.openDropdown(); } render() { const q = this.search.value.toLowerCase().trim(); let html = ''; let idx = 0; const visibleItems = []; for (const g of this.groups) { const filtered = g.items.filter(item => !q || item.name.toLowerCase().includes(q) || g.label.toLowerCase().includes(q) ); if (!filtered.length) continue; if (g.label) { html += `
${esc(g.label)}
`; } for (const item of filtered) { const active = String(item.id) === this.value ? ' ss__item--active' : ''; const hl = idx === this.highlightIdx ? ' ss__item--highlight' : ''; html += `
${esc(item.name)}
`; visibleItems.push(item); idx++; } } this.list.innerHTML = html || `
`; this.visibleCount = visibleItems.length; } onKeydown(e) { if (e.key === 'ArrowDown') { e.preventDefault(); this.highlightIdx = Math.min(this.highlightIdx + 1, this.visibleCount - 1); this.render(); this.scrollToHighlight(); } else if (e.key === 'ArrowUp') { e.preventDefault(); this.highlightIdx = Math.max(this.highlightIdx - 1, 0); this.render(); this.scrollToHighlight(); } else if (e.key === 'Enter') { e.preventDefault(); const el = this.list.querySelector(`[data-idx="${this.highlightIdx}"]`); if (el) this.select(el.dataset.value, el.dataset.label); } else if (e.key === 'Escape') { this.close(); } } scrollToHighlight() { const el = this.list.querySelector('.ss__item--highlight'); if (el) el.scrollIntoView({ block: 'nearest' }); } }