diff --git a/web/js/components/app-global-search.js b/web/js/components/app-global-search.js index e30071e..3da88a5 100644 --- a/web/js/components/app-global-search.js +++ b/web/js/components/app-global-search.js @@ -4,10 +4,13 @@ import { requireEl, optionalEl, warnOnce } from '../core/app-dom.js'; import { isEditableTarget } from '../core/app-form-utils.js'; -const input = requireEl('#side-search', { module: 'global-search' }); -const resultsEl = requireEl('[data-global-search-results]', { module: 'global-search' }); +export function initGlobalSearch() { + const input = requireEl('#side-search', { module: 'global-search' }); + const resultsEl = requireEl('[data-global-search-results]', { module: 'global-search' }); + if (!input || !resultsEl) {return null;} + if (input.dataset.globalSearchBound === '1') {return null;} + input.dataset.globalSearchBound = '1'; -if (input && resultsEl) { const emptyStateEl = optionalEl('[data-global-search-empty]'); const emptyHintEl = optionalEl('[data-search-empty-hint-template]'); const shortcutEl = optionalEl('[data-search-shortcut]'); @@ -163,22 +166,6 @@ if (input && resultsEl) { } }; - const onInput = () => { - const value = input.value.trim(); - if (value.length < minLength) { - clear(); - return; - } - if (pending) {window.clearTimeout(pending);} - pending = window.setTimeout(() => { - fetchCounts(value).catch((err) => { - warnOnce('FETCH_ERROR', 'Global search fetch error', { module: 'global-search', err }); - clear(); - }); - }, 300); - }; - - input.addEventListener('input', onInput); const submitSearch = () => { const value = input.value.trim(); if (value.length < minLength) {return;} @@ -186,14 +173,7 @@ if (input && resultsEl) { window.location.href = resultsPage.toString(); }; - input.addEventListener('keydown', (event) => { - if (event.key !== 'Enter') {return;} - event.preventDefault(); - submitSearch(); - }); - const focusSearch = () => { - if (!input) {return;} window.requestAnimationFrame(() => { input.focus(); input.select(); @@ -209,37 +189,72 @@ if (input && resultsEl) { focusSearch(); }; - if (emptyStateEl) { - emptyStateEl.addEventListener('click', () => { - focusSearch(); - }); - } + // --- Event handlers (named for cleanup) --- - document.addEventListener('keydown', (event) => { + const onInput = () => { + const value = input.value.trim(); + if (value.length < minLength) { + clear(); + return; + } + if (pending) {window.clearTimeout(pending);} + pending = window.setTimeout(() => { + fetchCounts(value).catch((err) => { + warnOnce('FETCH_ERROR', 'Global search fetch error', { module: 'global-search', err }); + clear(); + }); + }, 300); + }; + + const onInputKeydown = (event) => { + if (event.key !== 'Enter') {return;} + event.preventDefault(); + submitSearch(); + }; + + const onDocumentKeydown = (event) => { if (!(event.metaKey || event.ctrlKey)) {return;} const key = event.key.toLowerCase(); if (key !== 'k' && key !== '/') {return;} if (isEditableTarget(event.target) && event.target !== input) {return;} event.preventDefault(); openSearch(); - }); + }; + + const onEmptyStateClick = () => { + focusSearch(); + }; + + const onFormSubmit = (event) => { + event.preventDefault(); + submitSearch(); + }; + + const onToggleClick = () => { + panel.classList.toggle('search-details-hidden'); + if (toggleIcon) { + toggleIcon.classList.toggle('bi-dash-square'); + toggleIcon.classList.toggle('bi-plus-square'); + } + }; + + // --- Bind listeners --- + + input.addEventListener('input', onInput); + input.addEventListener('keydown', onInputKeydown); + document.addEventListener('keydown', onDocumentKeydown); + + if (emptyStateEl) { + emptyStateEl.addEventListener('click', onEmptyStateClick); + } const form = input.closest('form'); if (form) { - form.addEventListener('submit', (event) => { - event.preventDefault(); - submitSearch(); - }); + form.addEventListener('submit', onFormSubmit); } if (toggleButton && panel) { - toggleButton.addEventListener('click', () => { - panel.classList.toggle('search-details-hidden'); - if (toggleIcon) { - toggleIcon.classList.toggle('bi-dash-square'); - toggleIcon.classList.toggle('bi-plus-square'); - } - }); + toggleButton.addEventListener('click', onToggleClick); } // Seed from current URL search param if present. @@ -258,4 +273,32 @@ if (input && resultsEl) { } else { clear(); } + + // --- Cleanup --- + + const destroy = () => { + if (pending) {window.clearTimeout(pending);} + input.removeEventListener('input', onInput); + input.removeEventListener('keydown', onInputKeydown); + document.removeEventListener('keydown', onDocumentKeydown); + if (emptyStateEl) { + emptyStateEl.removeEventListener('click', onEmptyStateClick); + } + if (form) { + form.removeEventListener('submit', onFormSubmit); + } + if (toggleButton) { + toggleButton.removeEventListener('click', onToggleClick); + } + delete input.dataset.globalSearchBound; + }; + + return { destroy, openSearch, focusSearch }; +} + +// Auto-initialize on load +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => initGlobalSearch()); +} else { + initGlobalSearch(); }