/** * Arod Search Modal Component * An expandable search interface with modal overlay * * Usage: * * * Events: * - search: Fired when user performs a search * - clear: Fired when search is cleared */ // ============================================ // AROD-SEARCH - Search Modal Component // ============================================ class ArodSearch extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.isOpen = false; this.searchDebounce = null; } connectedCallback() { this.render(); this.setupEventListeners(); this.setupKeyboardShortcuts(); } disconnectedCallback() { this.removeKeyboardShortcuts(); } render() { const placeholder = this.getAttribute('placeholder') || 'Search...'; this.shadowRoot.innerHTML = ` `; } setupEventListeners() { const trigger = this.shadowRoot.querySelector('.search-trigger'); const overlay = this.shadowRoot.querySelector('.modal-overlay'); const close = this.shadowRoot.querySelector('.search-close'); const input = this.shadowRoot.querySelector('.search-input'); trigger.addEventListener('click', () => this.open()); close.addEventListener('click', () => this.close()); overlay.addEventListener('click', (e) => { if (e.target === overlay) this.close(); }); input.addEventListener('input', (e) => { clearTimeout(this.searchDebounce); this.searchDebounce = setTimeout(() => { this.handleSearch(e.target.value); }, 200); }); input.addEventListener('keydown', (e) => { if (e.key === 'Escape') this.close(); else if (e.key === 'ArrowDown') this.navigateResults(1); else if (e.key === 'ArrowUp') this.navigateResults(-1); else if (e.key === 'Enter') this.selectResult(); }); } setupKeyboardShortcuts() { this.keyHandler = (e) => { // Cmd+K or Ctrl+K to open search if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); this.open(); } }; document.addEventListener('keydown', this.keyHandler); } removeKeyboardShortcuts() { if (this.keyHandler) { document.removeEventListener('keydown', this.keyHandler); } } open() { this.isOpen = true; const overlay = this.shadowRoot.querySelector('.modal-overlay'); const input = this.shadowRoot.querySelector('.search-input'); overlay.classList.add('open'); setTimeout(() => input.focus(), 100); } close() { this.isOpen = false; const overlay = this.shadowRoot.querySelector('.modal-overlay'); const input = this.shadowRoot.querySelector('.search-input'); const results = this.shadowRoot.querySelector('.search-results'); overlay.classList.remove('open'); input.value = ''; results.innerHTML = ''; } handleSearch(query) { if (!query) { this.clearResults(); return; } // Dispatch search event for external handling this.dispatchEvent(new CustomEvent('search', { detail: { query }, bubbles: true })); } // Public method to display search results displayResults(results) { const container = this.shadowRoot.querySelector('.search-results'); container.innerHTML = ''; if (!results || results.length === 0) { container.innerHTML = '
No results found
'; return; } results.forEach((result, index) => { const item = document.createElement('div'); item.className = 'search-result-item'; item.dataset.index = index; item.innerHTML = `
${result.title}
${result.snippet || ''}
`; item.addEventListener('click', () => { if (result.url) window.location.href = result.url; this.close(); }); container.appendChild(item); }); } clearResults() { const container = this.shadowRoot.querySelector('.search-results'); container.innerHTML = ''; this.dispatchEvent(new CustomEvent('clear', { bubbles: true })); } navigateResults(direction) { const results = this.shadowRoot.querySelectorAll('.search-result-item'); if (!results.length) return; const current = this.shadowRoot.querySelector('.search-result-item.selected'); let index = current ? parseInt(current.dataset.index) : -1; index += direction; if (index < 0) index = results.length - 1; if (index >= results.length) index = 0; results.forEach(r => r.classList.remove('selected')); results[index].classList.add('selected'); results[index].scrollIntoView({ block: 'nearest' }); } selectResult() { const selected = this.shadowRoot.querySelector('.search-result-item.selected'); if (selected) selected.click(); } } // Register component customElements.define('arod-search', ArodSearch);