Add valid lines to stops
Deploy PBT / deploy (push) Failing after 25s

This commit is contained in:
2026-09-10 22:45:12 +02:00
parent 158311a329
commit 3535ebcba0
9 changed files with 424 additions and 160 deletions
+173 -87
View File
@@ -1,17 +1,16 @@
/* 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. */
/* Autocomplete helpers for the "Neue Fahrt" form.
- origin/destination: stop lookup from /api/stops, fills a hidden *_stop_id
when the text matches a known Austrian stop (free text still allowed).
- line: suggestions from /api/lines, scoped to the chosen origin/destination
stops. Always free text; when exactly one line is possible and the field is
untouched it is pre-filled.
Progressive enhancement: without JS every field stays a plain input. */
(function () {
"use strict";
var TYPE_LABEL = {
bus: "Bus",
tram: "Straßenbahn",
subway: "U-Bahn",
train: "Zug",
bus_station: "Busbahnhof",
other: "Haltestelle",
bus: "Bus", tram: "Straßenbahn", subway: "U-Bahn", train: "Zug",
bus_station: "Busbahnhof", other: "Haltestelle",
};
function debounce(fn, ms) {
@@ -23,12 +22,11 @@
};
}
function setup(input) {
/* Shared dropdown: owns the list element, keyboard nav and open/close.
opts: { fetchItems(query) -> Promise<item[]>,
fillOption(item, el), onChoose(item), minChars, onInput() } */
function attachCombo(input, opts) {
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;
@@ -36,109 +34,197 @@
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");
}
}
var lastQuery = null;
var minChars = opts.minChars != null ? opts.minChars : 2;
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;
if (opts.emptyText) {
var empty = document.createElement("div");
empty.className = "combo-empty";
empty.textContent = opts.emptyText;
list.appendChild(empty);
list.hidden = false;
} else {
close();
}
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);
items.forEach(function (item, i) {
var el = document.createElement("div");
el.className = "combo-option" + (i === active ? " active" : "");
opts.fillOption(item, el);
el.addEventListener("mousedown", function (e) { e.preventDefault(); choose(i); });
list.appendChild(el);
});
list.hidden = false;
}
function choose(i) {
var stop = items[i];
if (!stop) return;
input.value = stop.name;
markMatch(stop);
if (!items[i]) return;
opts.onChoose(items[i]);
close();
}
var search = debounce(function () {
var run = 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() : []; })
if (q.length < minChars && minChars > 0) { items = []; close(); return; }
Promise.resolve(opts.fetchItems(q))
.then(function (data) {
if (input.value.trim() !== lastQuery) return;
items = data;
items = data || [];
active = -1;
render();
var exact = data.filter(function (s) {
return s.name.toLowerCase() === lastQuery.toLowerCase();
})[0];
if (exact) { markMatch(exact); } else { clearMatch(); }
if (opts.afterFetch) opts.afterFetch(items, lastQuery);
})
.catch(function () { items = []; close(); });
}, 180);
}, 160);
input.setAttribute("autocomplete", "off");
input.addEventListener("input", function () { clearMatch(); search(); });
input.addEventListener("focus", function () { if (items.length) render(); });
input.addEventListener("input", function () {
if (opts.onInput) opts.onInput();
run();
});
input.addEventListener("focus", function () {
if (items.length) render(); else run();
});
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();
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" && active >= 0) { e.preventDefault(); choose(active); }
else if (e.key === "Escape") { close(); }
});
return { refresh: run, closeList: close };
}
function setupStopCombo(input, form) {
var wrap = input.closest(".combo");
var hidden = wrap.querySelector(input.dataset.stopTarget);
var hint = wrap.querySelector("[data-stop-hint]");
if (!hidden) return;
function clearMatch() {
if (!hidden.value) return;
hidden.value = "";
if (hint) { hint.textContent = ""; hint.classList.remove("matched"); }
form.dispatchEvent(new CustomEvent("stopchange"));
}
function markMatch(stop) {
hidden.value = String(stop.id);
if (hint) {
hint.textContent = "✓ " + stop.name +
(stop.municipality ? ", " + stop.municipality : "");
hint.classList.add("matched");
}
form.dispatchEvent(new CustomEvent("stopchange"));
}
attachCombo(input, {
emptyText: "Keine Haltestelle gefunden Eingabe wird als Freitext gespeichert.",
onInput: clearMatch,
fetchItems: function (q) {
return fetch("/api/stops?q=" + encodeURIComponent(q))
.then(function (r) { return r.ok ? r.json() : []; });
},
fillOption: function (stop, el) {
var name = document.createElement("span");
name.className = "name";
name.textContent = stop.name;
el.appendChild(name);
if (stop.municipality) {
var m = document.createElement("span");
m.className = "muni";
m.textContent = stop.municipality;
el.appendChild(m);
}
var kind = document.createElement("span");
kind.className = "kind";
kind.textContent = TYPE_LABEL[stop.type] || "";
el.appendChild(kind);
},
onChoose: function (stop) { input.value = stop.name; markMatch(stop); },
afterFetch: function (data, q) {
var exact = data.filter(function (s) {
return s.name.toLowerCase() === q.toLowerCase();
})[0];
if (exact) markMatch(exact); else clearMatch();
},
});
}
document.querySelectorAll("[data-stop-input]").forEach(setup);
function setupLineCombo(input, form) {
var touched = false;
var hint = input.closest(".combo").querySelector("[data-line-hint]");
input.addEventListener("input", function () { touched = true; });
function context() {
var o = form.querySelector("#origin_stop_id");
var d = form.querySelector("#destination_stop_id");
return {
origin_stop_id: (o && o.value) || "",
destination_stop_id: (d && d.value) || "",
};
}
var combo = attachCombo(input, {
minChars: 0,
onInput: function () { touched = true; },
fetchItems: function (q) {
var ctx = context();
if (!ctx.origin_stop_id && !ctx.destination_stop_id) return [];
var qs = "origin_stop_id=" + encodeURIComponent(ctx.origin_stop_id) +
"&destination_stop_id=" + encodeURIComponent(ctx.destination_stop_id) +
"&q=" + encodeURIComponent(q);
return fetch("/api/lines?" + qs).then(function (r) { return r.ok ? r.json() : []; });
},
fillOption: function (line, el) {
var ref = document.createElement("span");
ref.className = "name";
ref.textContent = line.ref;
el.appendChild(ref);
if (line.both) {
var tag = document.createElement("span");
tag.className = "kind";
tag.textContent = "beide Halte";
el.appendChild(tag);
}
},
onChoose: function (line) { input.value = line.ref; touched = true; },
});
// Re-scope suggestions when a stop is picked/cleared; pre-fill an
// unambiguous line into an untouched field.
form.addEventListener("stopchange", function () {
var ctx = context();
if (!ctx.origin_stop_id && !ctx.destination_stop_id) return;
var qs = "origin_stop_id=" + encodeURIComponent(ctx.origin_stop_id) +
"&destination_stop_id=" + encodeURIComponent(ctx.destination_stop_id);
fetch("/api/lines?" + qs)
.then(function (r) { return r.ok ? r.json() : []; })
.then(function (lines) {
if (!touched && input.value.trim() === "" && lines.length === 1) {
input.value = lines[0].ref;
}
if (hint) {
hint.textContent = lines.length
? lines.length + " Linie(n) an den gewählten Halten"
: "Keine Linien bekannt Freitext";
}
if (document.activeElement === input) combo.refresh();
})
.catch(function () {});
});
}
document.querySelectorAll("form").forEach(function (form) {
form.querySelectorAll("[data-stop-input]").forEach(function (i) { setupStopCombo(i, form); });
form.querySelectorAll("[data-line-input]").forEach(function (i) { setupLineCombo(i, form); });
});
})();