95 lines
6.0 KiB
JavaScript
95 lines
6.0 KiB
JavaScript
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();
|
|
}
|