|
- 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 = `
- <button type="button" class="ss__trigger">
- <span class="ss__value">${esc(this.placeholder)}</span>
- <svg class="ss__arrow" viewBox="0 0 10 6"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linecap="round"/></svg>
- </button>
- <div class="ss__dropdown" hidden>
- <input type="text" class="ss__search" placeholder="${esc(searchPlaceholder)}">
- <div class="ss__list"></div>
- </div>`;
-
- 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 += `<div class="ss__group">${esc(g.label)}</div>`;
- }
- for (const item of filtered) {
- const active = String(item.id) === this.value ? ' ss__item--active' : '';
- const hl = idx === this.highlightIdx ? ' ss__item--highlight' : '';
- html += `<div class="ss__item${active}${hl}" data-value="${item.id}" data-label="${esc(item.name)}" data-idx="${idx}">${esc(item.name)}</div>`;
- visibleItems.push(item);
- idx++;
- }
- }
-
- this.list.innerHTML = html || `<div class="ss__empty">–</div>`;
- 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' });
- }
- }
|