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:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user