diff --git a/static/app.js b/static/app.js index 16a9116..866438b 100644 --- a/static/app.js +++ b/static/app.js @@ -28,6 +28,113 @@ clearBtn.style.display = "none"; + // === Autocomplete === + function setupAutocomplete(input, listEl) { + var acTimer = null; + var acController = null; + var activeIdx = -1; + + function closeList() { + listEl.classList.remove("open"); + listEl.innerHTML = ""; + activeIdx = -1; + } + + function selectItem(displayName) { + input.value = displayName; + closeList(); + } + + function updateActive() { + var items = listEl.querySelectorAll("li"); + items.forEach(function (li, i) { + li.classList.toggle("active", i === activeIdx); + }); + if (activeIdx >= 0 && items[activeIdx]) { + items[activeIdx].scrollIntoView({ block: "nearest" }); + } + } + + input.addEventListener("input", function () { + clearTimeout(acTimer); + if (acController) acController.abort(); + var q = input.value.trim(); + if (q.length < 3) { + closeList(); + return; + } + acTimer = setTimeout(function () { + acController = new AbortController(); + fetch( + "https://nominatim.openstreetmap.org/search?" + + new URLSearchParams({ + q: q, + format: "json", + limit: "5", + addressdetails: "0", + }), + { signal: acController.signal } + ) + .then(function (r) { return r.json(); }) + .then(function (results) { + listEl.innerHTML = ""; + activeIdx = -1; + if (!results.length) { + closeList(); + return; + } + results.forEach(function (item) { + var li = document.createElement("li"); + var parts = item.display_name.split(", "); + var main = parts.slice(0, 2).join(", "); + var sub = parts.slice(2).join(", "); + li.innerHTML = + '' + escapeHtml(main) + "" + + (sub ? '
' + escapeHtml(sub) + "" : ""); + li.addEventListener("mousedown", function (e) { + e.preventDefault(); + selectItem(item.display_name); + }); + listEl.appendChild(li); + }); + listEl.classList.add("open"); + }) + .catch(function (err) { + if (err.name !== "AbortError") closeList(); + }); + }, 300); + }); + + input.addEventListener("keydown", function (e) { + if (!listEl.classList.contains("open")) return; + var items = listEl.querySelectorAll("li"); + if (e.key === "ArrowDown") { + e.preventDefault(); + activeIdx = Math.min(activeIdx + 1, items.length - 1); + updateActive(); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + activeIdx = Math.max(activeIdx - 1, 0); + updateActive(); + } else if (e.key === "Enter" && activeIdx >= 0) { + e.preventDefault(); + e.stopPropagation(); + var parts = items[activeIdx].querySelector(".ac-main").textContent; + var sub = items[activeIdx].querySelector(".ac-sub"); + selectItem(parts + (sub ? ", " + sub.textContent : "")); + } else if (e.key === "Escape") { + closeList(); + } + }); + + input.addEventListener("blur", function () { + setTimeout(closeList, 150); + }); + } + + setupAutocomplete(fromInput, document.getElementById("ac-from")); + setupAutocomplete(toInput, document.getElementById("ac-to")); + // === Utilities === function getSelectedBand() { var has2m = band2m.checked; diff --git a/static/index.html b/static/index.html index 1f2715d..6d6c1a2 100644 --- a/static/index.html +++ b/static/index.html @@ -16,11 +16,13 @@ -
- +
+ +
-
- +
+ +
    diff --git a/static/style.css b/static/style.css index aaec2a1..e0d9c92 100644 --- a/static/style.css +++ b/static/style.css @@ -69,6 +69,57 @@ html, body { margin-top: 6px; } +.autocomplete-wrap { + position: relative; +} + +.autocomplete-list { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1001; + background: white; + border: 1px solid #ccc; + border-top: none; + border-radius: 0 0 5px 5px; + list-style: none; + max-height: 180px; + overflow-y: auto; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.autocomplete-list.open { + display: block; +} + +.autocomplete-list li { + padding: 7px 10px; + font-size: 12px; + cursor: pointer; + border-bottom: 1px solid #f0f0f0; + line-height: 1.3; +} + +.autocomplete-list li:last-child { + border-bottom: none; +} + +.autocomplete-list li:hover, +.autocomplete-list li.active { + background: #f5f5f5; +} + +.autocomplete-list li .ac-main { + color: #333; +} + +.autocomplete-list li .ac-sub { + color: #999; + font-size: 11px; +} + .route-row input[type="text"] { width: 100%; padding: 7px 10px;