refactor: wrap global search in exported init function with destroy() cleanup

Convert app-global-search.js from a self-executing module to an
exported initGlobalSearch() function following the established
component pattern. Extract 6 inline handlers to named variables,
add destroy() for proper listener cleanup, double-bind guard,
and return a public API (destroy, openSearch, focusSearch).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 23:01:04 +01:00
parent 1cc63a5fbe
commit a3a403c2ec

View File

@@ -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();
}