export function setupPicker(picker, onSelect = () => {}) { if (picker.dataset.ready) return; picker.dataset.ready = 'true'; const input = picker.querySelector('[role="combobox"]'); const value = picker.querySelector('.picker-value'); const results = picker.querySelector('[role="listbox"]'); const status = picker.querySelector('.picker-status'); let controller, timer, current = -1, items = []; const close = () => { results.hidden = true; input.setAttribute('aria-expanded', 'false'); input.removeAttribute('aria-activedescendant'); current = -1; }; const select = item => { value.value = item?.id ?? ''; input.value = item ? `${item.title} (/${item.path === 'home' ? '' : item.path})` : ''; input.setCustomValidity(''); status.textContent = item ? 'Ausgewählt.' : ''; close(); value.dispatchEvent(new Event('change', { bubbles: true })); onSelect(item); }; const activate = index => { current = Math.max(0, Math.min(items.length - 1, index)); [...results.children].forEach((node, i) => node.setAttribute('aria-selected', String(i === current))); const option = results.children[current]; if (option) { input.setAttribute('aria-activedescendant', option.id); option.scrollIntoView({ block: 'nearest' }); } }; const search = async query => { controller?.abort(); controller = new AbortController(); const signal = controller.signal; const url = new URL(picker.dataset.source, location.href); url.searchParams.set('q', query); url.searchParams.set('type', picker.dataset.type || ''); url.searchParams.set('parents', picker.dataset.parents || '0'); status.textContent = 'Wird gesucht …'; try { const response = await fetch(url, { signal, credentials: 'same-origin' }); if (!response.ok || response.redirected) throw new Error(); const data = await response.json(); if (signal.aborted) return; items = data.items; current = -1; results.replaceChildren(); items.forEach((item, index) => { const option = document.createElement('button'); option.type = 'button'; option.tabIndex = -1; option.id = `${input.id}-option-${index}`; option.setAttribute('role', 'option'); option.setAttribute('aria-selected', 'false'); const title = document.createElement('strong'); title.textContent = item.title; const detail = document.createElement('span'); detail.textContent = `/${item.path === 'home' ? '' : item.path} · ${item.type === 'post' ? 'Beitrag' : 'Seite'}${item.published ? '' : ' · Entwurf'}`; option.append(title, detail); option.addEventListener('mousedown', event => event.preventDefault()); option.addEventListener('click', () => { select(item); input.focus({ preventScroll: true }); close(); }); results.append(option); }); results.hidden = !items.length; input.setAttribute('aria-expanded', String(Boolean(items.length))); status.textContent = items.length ? `${items.length} Treffer${items.length === 20 ? ' · Suche eingrenzen für weitere Ergebnisse' : ''}.` : 'Keine passenden Inhalte gefunden.'; } catch (error) { if (error.name !== 'AbortError') { close(); status.textContent = 'Inhalte konnten nicht geladen werden. Bitte erneut suchen oder neu anmelden.'; } } }; input.addEventListener('focus', () => search('')); input.addEventListener('input', () => { value.value = ''; input.setCustomValidity(input.value && !picker.closest('.menu-add') ? 'Bitte einen Suchtreffer auswählen oder die Suche leeren.' : ''); value.dispatchEvent(new Event('change', { bubbles: true })); controller?.abort(); clearTimeout(timer); timer = setTimeout(() => search(input.value), 180); }); input.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); controller?.abort(); clearTimeout(timer); close(); } if (['ArrowDown', 'ArrowUp'].includes(event.key) && !results.hidden) { event.preventDefault(); activate(current + (event.key === 'ArrowDown' ? 1 : -1)); } if (event.key === 'Enter' && current >= 0 && !results.hidden) { event.preventDefault(); select(items[current]); } }); picker.addEventListener('focusout', event => { if (!picker.contains(event.relatedTarget)) { controller?.abort(); clearTimeout(timer); close(); } }); picker.querySelector('.picker-clear').addEventListener('click', () => { controller?.abort(); clearTimeout(timer); select(null); input.focus(); }); } document.querySelectorAll('.content-picker').forEach(picker => setupPicker(picker));