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