fix(quellen): Suche ohne <form>, Click + Enter direkt binden

Browser-Quirk: in einem <form> blieb der fetch nach Submit hängen,
auch mit preventDefault() im submit-Handler. Status-Text bekam
"Suche läuft …", aber die Response kam nie an — der Browser hat den
fetch durch den Submit-State des Forms blockiert.

Lösung: <form> → <div>, Button auf type="button". Click direkt an
runSearch binden, Enter via keydown auf dem Input. Keine Form-Submit-
Semantik mehr.

Bei E2E-Smoketest mit Playwright reproduzierbar gefixt.
This commit is contained in:
Dotty Dotter 2026-05-09 01:09:45 +02:00
parent 37941f0a2b
commit e48cab6db3

View File

@ -196,11 +196,11 @@
Wortunscharf — Endungen sind egal, verwandte Begriffe werden ebenfalls Wortunscharf — Endungen sind egal, verwandte Begriffe werden ebenfalls
gefunden. gefunden.
</p> </p>
<form id="quellen-search-form" class="search-box"> <div id="quellen-search-form" class="search-box">
<input type="text" id="quellen-q" name="q" placeholder="z.B. Klimaschutz, soziale Gerechtigkeit, Mietpreisbremse" <input type="text" id="quellen-q" name="q" placeholder="z.B. Klimaschutz, soziale Gerechtigkeit, Mietpreisbremse"
autocomplete="off" minlength="2" maxlength="200"> autocomplete="off" minlength="2" maxlength="200">
<button type="submit" id="quellen-q-btn">Suchen</button> <button type="button" id="quellen-q-btn">Suchen</button>
</form> </div>
<div class="search-filter" style="margin-top:6px;"> <div class="search-filter" style="margin-top:6px;">
<label><input type="radio" name="qfilter" value="current" checked> nur aktuelle Programme</label> <label><input type="radio" name="qfilter" value="current" checked> nur aktuelle Programme</label>
<label><input type="radio" name="qfilter" value="all"> auch historische</label> <label><input type="radio" name="qfilter" value="all"> auch historische</label>
@ -318,8 +318,7 @@ function escHtml(s) {
.replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); .replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
} }
async function runSearch(ev) { async function runSearch() {
if (ev) ev.preventDefault();
const q = document.getElementById('quellen-q').value.trim(); const q = document.getElementById('quellen-q').value.trim();
const filter = (document.querySelector('input[name="qfilter"]:checked') || {}).value || 'current'; const filter = (document.querySelector('input[name="qfilter"]:checked') || {}).value || 'current';
const statusEl = document.getElementById('quellen-search-status'); const statusEl = document.getElementById('quellen-search-status');
@ -380,25 +379,29 @@ async function runSearch(ev) {
return false; return false;
} }
// Form-Submit + Filter-Wechsel binden. // Suche binden — kein <form>, sondern Click auf Button + Enter im Input.
// Direkt im body_scripts-Block — wenn das Script läuft, ist DOMContentLoaded // Frühere <form>-Variante hatte einen Browser-Quirk: trotz preventDefault()
// häufig schon vorbei (Script kommt am Ende des Body). Daher kein // im submit-Handler hat der anschließende fetch() nie eine Response
// addEventListener('DOMContentLoaded', …) als Wrapper. // erhalten (Status blieb auf "Suche läuft …"). Mit einem Plain-Container +
// Button onclick funktioniert es zuverlässig.
// //
// Wichtig: `runSearch` ist async und returnt Promise<false>. Das ist truthy, // Direkt im body_scripts-Block — wenn das Script läuft, ist DOMContentLoaded
// also würde `onsubmit="return runSearch(event)"` das Default-Submit NICHT // häufig schon vorbei. Daher kein addEventListener('DOMContentLoaded', …)
// verhindern, der fetch würde mit "Failed to fetch" abbrechen, weil die Page // als Wrapper, sondern IIFE mit Idempotenz-Marker.
// navigiert. Stattdessen über addEventListener binden — da bleibt
// `event.preventDefault()` synchron, bevor der Promise resolvt.
(function bindQuellenSearch() { (function bindQuellenSearch() {
const form = document.getElementById('quellen-search-form'); const btn = document.getElementById('quellen-q-btn');
if (form && !form._quellenBound) { if (btn && !btn._quellenBound) {
form._quellenBound = true; btn._quellenBound = true;
form.addEventListener('submit', (ev) => { btn.addEventListener('click', () => runSearch());
ev.preventDefault(); }
// Microtask-Queue erst leeren, sonst hängt der fetch in manchen const input = document.getElementById('quellen-q');
// Browsern beobachtbar (Submit-State blockt anschließenden fetch). if (input && !input._quellenBound) {
setTimeout(runSearch, 0); input._quellenBound = true;
input.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
runSearch();
}
}); });
} }
document.querySelectorAll('input[name="qfilter"]').forEach(r => { document.querySelectorAll('input[name="qfilter"]').forEach(r => {