This commit is contained in:
+144
@@ -0,0 +1,144 @@
|
||||
/* 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);
|
||||
})();
|
||||
Reference in New Issue
Block a user