59 lines
3.2 KiB
JavaScript
59 lines
3.2 KiB
JavaScript
/* Zoeken in de bovenbalk: live resultaten (search_live) onder het veld; Enter opent de volledige zoekpagina.
|
|
* Pijltjes lopen door de resultaten, Escape sluit, "/" springt naar het zoekveld. */
|
|
(() => {
|
|
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
|
|
document.querySelectorAll('form.tsearch').forEach((form) => {
|
|
const input = form.querySelector('input[name=q]');
|
|
const box = form.querySelector('.tsearch-results');
|
|
let timer = null, ctrl = null, last = '';
|
|
|
|
const items = () => [...box.querySelectorAll('a')];
|
|
const close = () => { box.classList.remove('open'); input.setAttribute('aria-expanded', 'false'); };
|
|
const open = () => { if (box.innerHTML) { box.classList.add('open'); input.setAttribute('aria-expanded', 'true'); } };
|
|
|
|
const render = (data) => {
|
|
box.innerHTML = data.groups.length
|
|
? data.groups.map((g) => `<div class="ts-group">${esc(g.label)}</div>` + g.items.map((i) =>
|
|
`<a class="ts-item" role="option" href="${esc(i.url)}">${esc(i.title)}${i.sub ? `<small>${esc(i.sub)}</small>` : ''}</a>`).join('')).join('')
|
|
+ `<a class="ts-all" href="${esc(data.url)}">Alle resultaten voor “${esc(data.q)}”</a>`
|
|
: `<div class="ts-empty">Niets gevonden voor “${esc(data.q)}”.</div>`;
|
|
open();
|
|
};
|
|
|
|
input.addEventListener('input', () => {
|
|
clearTimeout(timer);
|
|
const v = input.value.trim();
|
|
if (v.length < 2) { box.innerHTML = ''; close(); return; }
|
|
timer = setTimeout(() => {
|
|
if (v === last && box.innerHTML) { open(); return; }
|
|
if (ctrl) ctrl.abort();
|
|
ctrl = new AbortController();
|
|
fetch(`${form.dataset.live}?q=${encodeURIComponent(v)}`, { signal: ctrl.signal, headers: { Accept: 'application/json' } })
|
|
.then((r) => (r.ok ? r.json() : null))
|
|
.then((d) => { if (d && input.value.trim() === v) { last = v; render(d); } })
|
|
.catch(() => {});
|
|
}, 180);
|
|
});
|
|
|
|
form.addEventListener('keydown', (e) => {
|
|
const list = items(), at = list.indexOf(document.activeElement);
|
|
if (e.key === 'ArrowDown' && list.length) { e.preventDefault(); open(); list[Math.min(at + 1, list.length - 1)].focus(); }
|
|
else if (e.key === 'ArrowUp' && list.length) { e.preventDefault(); (at <= 0 ? input : list[at - 1]).focus(); }
|
|
else if (e.key === 'Escape') { close(); input.focus(); }
|
|
});
|
|
input.addEventListener('focus', () => { if (input.value.trim().length >= 2) open(); });
|
|
document.addEventListener('click', (e) => { if (!form.contains(e.target)) close(); });
|
|
|
|
// mobiel: het veld krijgt de focus zodra het uitklapmenu opengaat
|
|
const toggle = form.closest('.dropdown')?.querySelector('[data-bs-toggle="dropdown"]');
|
|
if (toggle) toggle.addEventListener('shown.bs.dropdown', () => input.focus());
|
|
});
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key !== '/' || e.ctrlKey || e.metaKey || e.altKey || e.target.closest('input, textarea, select, [contenteditable]')) return;
|
|
const input = [...document.querySelectorAll('form.tsearch input[name=q]')].find((i) => i.offsetParent);
|
|
if (input) { e.preventDefault(); input.focus(); input.select(); }
|
|
});
|
|
})();
|