(() => {
const modal = document.getElementById('globalSearchModal');
const input = document.getElementById('globalSearchInput');
const results = document.getElementById('globalSearchResults');
if (!modal || !input || !results) return;
const labels = modal.dataset;
let groups = {};
try { groups = JSON.parse(labels.groups || '{}'); } catch {}
let timer;
let request;
const escapeText = value => {
const node = document.createElement('span');
node.textContent = String(value ?? '');
return node.innerHTML;
};
const notice = (message, danger = false) =>
'
' + escapeText(message) + '
';
document.addEventListener('keydown', event => {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
event.preventDefault();
bootstrap.Modal.getOrCreateInstance(modal).show();
}
});
modal.addEventListener('shown.bs.modal', () => input.focus());
input.addEventListener('input', () => {
clearTimeout(timer);
const query = input.value.trim();
if (query.length < 2) {
results.innerHTML = notice(labels.hint);
return;
}
timer = setTimeout(async () => {
request?.abort();
request = new AbortController();
results.innerHTML = notice(labels.loading);
try {
const response = await fetch('/search.json?q=' + encodeURIComponent(query), {
signal: request.signal,
headers: {Accept: 'application/json'}
});
if (!response.ok) throw new Error('Search failed');
const data = await response.json();
const entries = Object.entries(data.groups || {});
if (!entries.length) {
results.innerHTML = notice(labels.none);
return;
}
results.innerHTML = entries.map(([group, items]) =>
''
).join('') + '' + escapeText(labels.allResults) + '';
} catch (error) {
if (error.name !== 'AbortError') results.innerHTML = notice(labels.error, true);
}
}, 220);
});
})();