import { setupPicker } from './page-picker.js'; const form = document.querySelector('[data-live-search]'); if (form) { const results = document.querySelector('#list-results'); const status = document.querySelector('#search-status'); const error = document.querySelector('#search-error'); const search = form.querySelector('input[type="search"]'); const fields = [...form.querySelectorAll('input[name],select[name]')]; let timer, controller, sequence = 0, composing = false; const cancel = () => { clearTimeout(timer); controller?.abort(); sequence++; results.removeAttribute('aria-busy'); }; const updateReset = () => { form.querySelector('[data-search-reset]').hidden = !fields.some(field => field.value !== (field.dataset.searchDefault || '')); }; const urlFor = parameters => { const url = new URL(form.action); for (const [name, value] of Object.entries(parameters)) { const fallback = fields.find(field => field.name === name)?.dataset.searchDefault || (name === 'seite' ? '1' : ''); if (String(value) !== fallback) url.searchParams.set(name, String(value)); } return url; }; const currentUrl = () => urlFor({ ...Object.fromEntries(new FormData(form)), q: search.value.trim(), seite: 1 }); const restoreFilters = url => { for (const field of fields) field.value = url.searchParams.get(field.name) ?? field.dataset.searchDefault ?? ''; updateReset(); }; async function load(url, navigation = 'replace', focusResults = false) { cancel(); const request = sequence; controller = new AbortController(); const signal = controller.signal; results.setAttribute('aria-busy', 'true'); error.hidden = true; status.textContent = 'Ergebnisse werden gesucht …'; updateReset(); try { const response = await fetch(url, { signal, credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json' } }); if (response.redirected || response.status === 401 || response.status === 403) throw new Error('Deine Sitzung ist abgelaufen. Bitte erneut anmelden und die Suche wiederholen.'); if (!response.ok) throw new Error('Die Suche konnte nicht geladen werden. Die bisherigen Ergebnisse bleiben sichtbar.'); const data = await response.json(); if (signal.aborted || request !== sequence) return; if (typeof data.html !== 'string' || !data.parameters || !Number.isInteger(data.total)) throw new Error('Die Suchantwort konnte nicht verarbeitet werden.'); const parsed = new DOMParser().parseFromString(data.html, 'text/html'); results.replaceChildren(...parsed.body.childNodes); results.querySelectorAll('.content-picker').forEach(picker => setupPicker(picker)); // Keep create/upload forms outside the replaced list on the same filtered screen. const context = { _list_query: data.parameters.q, _list_status: data.parameters.status, _list_limit: data.parameters.limit, _list_page: data.parameters.seite, q: data.parameters.q, archiv: data.parameters.archiv, seite: data.parameters.seite }; for (const [name, value] of Object.entries(context)) if (value !== undefined) { document.querySelectorAll(`form:not([data-live-search]) input[type="hidden"][name="${name}"]`).forEach(input => { input.value = value; }); } const canonical = urlFor(data.parameters); if (navigation !== 'none' && canonical.href !== location.href) history[navigation === 'push' ? 'pushState' : 'replaceState'](null, '', canonical); status.textContent = `${data.total} Treffer${data.parameters.q ? ` für „${data.parameters.q}“` : ''}.`; if (focusResults) { results.focus({ preventScroll: true }); results.scrollIntoView({ block: 'start' }); } } catch (failure) { if (signal.aborted || request !== sequence) return; error.querySelector('[role="alert"]').textContent = failure instanceof TypeError ? 'Verbindung unterbrochen. Die bisherigen Ergebnisse bleiben sichtbar.' : failure.message; error.hidden = false; status.textContent = 'Suche fehlgeschlagen.'; } finally { if (request === sequence) results.removeAttribute('aria-busy'); } } const queue = () => { cancel(); updateReset(); if (composing) return; error.hidden = true; status.textContent = 'Ergebnisse werden gesucht …'; results.setAttribute('aria-busy', 'true'); timer = setTimeout(() => load(currentUrl()), search.value ? 220 : 0); }; search.addEventListener('compositionstart', () => { composing = true; cancel(); }); search.addEventListener('compositionend', () => { composing = false; queue(); }); search.addEventListener('input', queue); form.addEventListener('change', event => { if (event.target.matches('select')) load(currentUrl(), 'push'); }); form.addEventListener('submit', event => { event.preventDefault(); if (!composing) load(currentUrl()); }); form.querySelector('[data-search-reset]').addEventListener('click', event => { event.preventDefault(); const url = new URL(form.action); restoreFilters(url); load(url, 'push'); search.focus(); }); results.addEventListener('click', event => { const link = event.target.closest('.pager a'); if (!link || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return; event.preventDefault(); load(new URL(link.href), 'push', true); }); // A delayed response must not overwrite a form the user has just started editing. results.addEventListener('input', () => { if (results.getAttribute('aria-busy') === 'true') { cancel(); status.textContent = 'Suche angehalten, während du einen Eintrag bearbeitest.'; } }); window.addEventListener('popstate', () => { const url = new URL(location.href); restoreFilters(url); load(url, 'none'); }); window.addEventListener('pagehide', cancel); document.querySelector('#search-retry').addEventListener('click', () => load(currentUrl())); form.classList.add('live-search-ready'); status.textContent = 'Die Suche aktualisiert sich beim Tippen.'; search.setAttribute('aria-controls', results.id); updateReset(); }