Add address autocomplete
This commit is contained in:
parent
f4d5f282f3
commit
a12410d378
3 changed files with 164 additions and 4 deletions
107
static/app.js
107
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 =
|
||||
'<span class="ac-main">' + escapeHtml(main) + "</span>" +
|
||||
(sub ? '<br><span class="ac-sub">' + escapeHtml(sub) + "</span>" : "");
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@
|
|||
<label class="band-label band-70cm"><input type="checkbox" id="band-70cm" checked> 70cm</label>
|
||||
<span id="count" class="count"></span>
|
||||
</div>
|
||||
<div class="controls-row route-row">
|
||||
<input type="text" id="route-from" placeholder="From address" />
|
||||
<div class="controls-row route-row autocomplete-wrap">
|
||||
<input type="text" id="route-from" placeholder="From address" autocomplete="off" />
|
||||
<ul class="autocomplete-list" id="ac-from"></ul>
|
||||
</div>
|
||||
<div class="controls-row route-row">
|
||||
<input type="text" id="route-to" placeholder="To address" />
|
||||
<div class="controls-row route-row autocomplete-wrap">
|
||||
<input type="text" id="route-to" placeholder="To address" autocomplete="off" />
|
||||
<ul class="autocomplete-list" id="ac-to"></ul>
|
||||
</div>
|
||||
<div class="controls-row route-row">
|
||||
<button id="route-btn">Route</button>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue