/* Autocomplete for the origin/destination fields, backed by /api/stops. Progressive enhancement: without JS the inputs stay plain free-text fields. A picked stop fills the sibling hidden with its id; editing the text afterwards clears it again, so unmatched entries save as free text. */ (function () { "use strict"; var TYPE_LABEL = { bus: "Bus", tram: "Straßenbahn", subway: "U-Bahn", train: "Zug", bus_station: "Busbahnhof", other: "Haltestelle", }; function debounce(fn, ms) { var t; return function () { var args = arguments, self = this; clearTimeout(t); t = setTimeout(function () { fn.apply(self, args); }, ms); }; } function setup(input) { var wrap = input.closest(".combo"); var hidden = wrap.querySelector(input.dataset.stopTarget); var hint = wrap.querySelector("[data-stop-hint]"); if (!hidden) return; var list = document.createElement("div"); list.className = "combo-list"; list.hidden = true; wrap.appendChild(list); var items = []; var active = -1; var lastQuery = ""; function clearMatch() { hidden.value = ""; if (hint) { hint.textContent = ""; hint.classList.remove("matched"); } } function markMatch(stop) { hidden.value = String(stop.id); if (hint) { hint.textContent = "✓ " + stop.name + (stop.municipality ? ", " + stop.municipality : ""); hint.classList.add("matched"); } } function close() { list.hidden = true; active = -1; } function render() { list.innerHTML = ""; if (!items.length) { var empty = document.createElement("div"); empty.className = "combo-empty"; empty.textContent = "Keine Haltestelle gefunden – Eingabe wird als Freitext gespeichert."; list.appendChild(empty); list.hidden = false; return; } items.forEach(function (stop, i) { var opt = document.createElement("div"); opt.className = "combo-option" + (i === active ? " active" : ""); var name = document.createElement("span"); name.className = "name"; name.textContent = stop.name; opt.appendChild(name); if (stop.municipality) { var muni = document.createElement("span"); muni.className = "muni"; muni.textContent = stop.municipality; opt.appendChild(muni); } var kind = document.createElement("span"); kind.className = "kind"; kind.textContent = TYPE_LABEL[stop.type] || ""; opt.appendChild(kind); opt.addEventListener("mousedown", function (e) { e.preventDefault(); choose(i); }); list.appendChild(opt); }); list.hidden = false; } function choose(i) { var stop = items[i]; if (!stop) return; input.value = stop.name; markMatch(stop); close(); } var search = debounce(function () { var q = input.value.trim(); lastQuery = q; if (q.length < 2) { items = []; close(); return; } fetch("/api/stops?q=" + encodeURIComponent(q)) .then(function (r) { return r.ok ? r.json() : []; }) .then(function (data) { if (input.value.trim() !== lastQuery) return; items = data; active = -1; render(); var exact = data.filter(function (s) { return s.name.toLowerCase() === lastQuery.toLowerCase(); })[0]; if (exact) { markMatch(exact); } else { clearMatch(); } }) .catch(function () { items = []; close(); }); }, 180); input.setAttribute("autocomplete", "off"); input.addEventListener("input", function () { clearMatch(); search(); }); input.addEventListener("focus", function () { if (items.length) render(); }); input.addEventListener("blur", function () { setTimeout(close, 120); }); input.addEventListener("keydown", function (e) { if (list.hidden) return; if (e.key === "ArrowDown") { e.preventDefault(); active = Math.min(active + 1, items.length - 1); render(); } else if (e.key === "ArrowUp") { e.preventDefault(); active = Math.max(active - 1, 0); render(); } else if (e.key === "Enter") { if (active >= 0) { e.preventDefault(); choose(active); } } else if (e.key === "Escape") { close(); } }); } document.querySelectorAll("[data-stop-input]").forEach(setup); })();