Files
pbt/static/stops.js
T
zisco 3535ebcba0
Deploy PBT / deploy (push) Failing after 25s
Add valid lines to stops
2026-09-10 22:45:12 +02:00

231 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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",
};
function debounce(fn, ms) {
var t;
return function () {
var args = arguments, self = this;
clearTimeout(t);
t = setTimeout(function () { fn.apply(self, args); }, ms);
};
}
/* 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 list = document.createElement("div");
list.className = "combo-list";
list.hidden = true;
wrap.appendChild(list);
var items = [];
var active = -1;
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) {
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 (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) {
if (!items[i]) return;
opts.onChoose(items[i]);
close();
}
var run = debounce(function () {
var q = input.value.trim();
lastQuery = q;
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 || [];
active = -1;
render();
if (opts.afterFetch) opts.afterFetch(items, lastQuery);
})
.catch(function () { items = []; close(); });
}, 160);
input.setAttribute("autocomplete", "off");
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" && 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();
},
});
}
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); });
});
})();