class SwitchAutoComplete { /** * @param {HTMLElement|string} input * @param {HTMLElement|string} panel * @param {string[]} switchArray * @param {object} opts */ constructor(input, panel, switchArray = [], opts = {}) { this.inputEl = typeof input === 'string' ? document.querySelector(input) : input; this.panelEl = typeof panel === 'string' ? document.querySelector(panel) : panel; if (!this.inputEl || !this.panelEl) throw new Error('Invalid input/panel element'); this.opts = Object.assign({ previewOnFocus: true, previewCount: 30, resultLimit: 1000 }, opts); this.setArray(switchArray); this.activeIndex = -1; // 이벤트 바인딩 this.onInput = this._handleInput.bind(this); this.onFocus = this._handleFocus.bind(this); this.onKeydown = this._handleKeydown.bind(this); this.onClickOutside = this._handleClickOutside.bind(this); this.inputEl.addEventListener('input', this.onInput); this.inputEl.addEventListener('focus', this.onFocus); this.inputEl.addEventListener('keydown', this.onKeydown); document.addEventListener('mousedown', this.onClickOutside); } destroy() { this.inputEl.removeEventListener('input', this.onInput); this.inputEl.removeEventListener('focus', this.onFocus); this.inputEl.removeEventListener('keydown', this.onKeydown); document.removeEventListener('mousedown', this.onClickOutside); this.hidePanel(); } setArray(arr) { this.switchArray = Array.from(new Set((arr || []).filter(Boolean))); } _handleInput(e) { const q = (e.target.value || '').trim(); this._filterAndRender(q); } _handleFocus() { if (!this.inputEl.value && this.opts.previewOnFocus) { this._renderPanel(this.switchArray.slice(0, this.opts.resultLimit)); this.showPanel(); } } _handleKeydown(e) { if (this.panelEl.hidden) return; const items = this._items(); if (!items.length) return; if (e.key === 'ArrowDown') { e.preventDefault(); this._move(1); } else if (e.key === 'ArrowUp') { e.preventDefault(); this._move(-1); } else if (e.key === 'Enter') { if (this.activeIndex >= 0) { e.preventDefault(); this._pick(items[this.activeIndex].dataset.value); } } else if (e.key === 'Escape') { this.hidePanel(); } } _handleClickOutside(e) { if (!this.inputEl.closest('.ac-wrap')?.contains(e.target)) this.hidePanel(); } _filterAndRender(query) { if (!query) { this.hidePanel(); return; } const q = query.toLowerCase(); let results = this.switchArray.filter(s => s.toLowerCase().includes(q)); if (this.opts.resultLimit > 0) results = results.slice(0, this.opts.resultLimit); if (results.length) { this._renderPanel(results); this.showPanel(); } else { this.hidePanel(); } } _renderPanel(list) { this.panelEl.innerHTML = ''; this.activeIndex = -1; const frag = document.createDocumentFragment(); list.forEach((sw) => { const item = document.createElement('div'); item.className = 'ac-item'; item.textContent = sw; item.dataset.value = sw; item.addEventListener('mousedown', (ev) => { ev.preventDefault(); this._pick(sw); }); frag.appendChild(item); }); this.panelEl.appendChild(frag); } _items() { return Array.from(this.panelEl.querySelectorAll('.ac-item')); } _move(delta) { const items = this._items(); if (!items.length) return; this.activeIndex = (this.activeIndex + delta + items.length) % items.length; items.forEach(el => el.classList.remove('active')); const el = items[this.activeIndex]; el.classList.add('active'); el.scrollIntoView({ block: 'nearest' }); } _pick(value) { this.inputEl.value = value; this.hidePanel(); } showPanel() { this.panelEl.hidden = false; } hidePanel() { this.panelEl.hidden = true; this.activeIndex = -1; } } /* switchData(JSON) 읽기 */ function parseEmbeddedJson(id) { const el = document.getElementById(id); if (!el) return []; let txt = el.textContent || ''; txt = txt .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/(^|[^:])\/\/.*$/mg, '$1') .replace(/,\s*([}\]])/g, '$1'); try { const data = JSON.parse(txt); return Array.isArray(data) ? data : []; } catch (e) { console.error('switchData parse error', e); return []; } } /* 초기화 */ document.addEventListener('DOMContentLoaded', () => { const switches = parseEmbeddedJson('switchData'); const ac = new SwitchAutoComplete('#switchInput', '#switchPanel', switches, { previewOnFocus: true, resultLimit: 2000 }); });