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";
|
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 ===
|
// === Utilities ===
|
||||||
function getSelectedBand() {
|
function getSelectedBand() {
|
||||||
var has2m = band2m.checked;
|
var has2m = band2m.checked;
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,13 @@
|
||||||
<label class="band-label band-70cm"><input type="checkbox" id="band-70cm" checked> 70cm</label>
|
<label class="band-label band-70cm"><input type="checkbox" id="band-70cm" checked> 70cm</label>
|
||||||
<span id="count" class="count"></span>
|
<span id="count" class="count"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-row route-row">
|
<div class="controls-row route-row autocomplete-wrap">
|
||||||
<input type="text" id="route-from" placeholder="From address" />
|
<input type="text" id="route-from" placeholder="From address" autocomplete="off" />
|
||||||
|
<ul class="autocomplete-list" id="ac-from"></ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-row route-row">
|
<div class="controls-row route-row autocomplete-wrap">
|
||||||
<input type="text" id="route-to" placeholder="To address" />
|
<input type="text" id="route-to" placeholder="To address" autocomplete="off" />
|
||||||
|
<ul class="autocomplete-list" id="ac-to"></ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-row route-row">
|
<div class="controls-row route-row">
|
||||||
<button id="route-btn">Route</button>
|
<button id="route-btn">Route</button>
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,57 @@ html, body {
|
||||||
margin-top: 6px;
|
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"] {
|
.route-row input[type="text"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue