145 lines
4.5 KiB
JavaScript
145 lines
4.5 KiB
JavaScript
/* 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 <input> 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);
|
||
})();
|