refactor: extract location JS into static file with nearby keyboard nav
Move locateUser and nearbyKeydown functions from an Askama macro into static/js/location.js served as a deferred script. Add arrow key, Enter, and Escape navigation for nearby cafe search results.
This commit is contained in:
parent
f4411fdc76
commit
aad927085b
3 changed files with 74 additions and 14 deletions
|
|
@ -55,6 +55,7 @@ pub(super) fn router() -> axum::Router<AppState> {
|
|||
get(searchable_select_js),
|
||||
)
|
||||
.route("/static/js/components/chip-scroll.js", get(chip_scroll_js))
|
||||
.route("/static/js/location.js", get(location_js))
|
||||
.route("/static/js/components/world-map.js", get(world_map_js))
|
||||
.route("/static/js/components/donut-chart.js", get(donut_chart_js))
|
||||
.route("/static/favicon-light.svg", get(favicon_light))
|
||||
|
|
@ -87,6 +88,16 @@ async fn webauthn_js() -> impl IntoResponse {
|
|||
)
|
||||
}
|
||||
|
||||
async fn location_js() -> impl IntoResponse {
|
||||
(
|
||||
[
|
||||
("content-type", "application/javascript; charset=utf-8"),
|
||||
("cache-control", "public, max-age=604800"),
|
||||
],
|
||||
include_str!("../../../../static/js/location.js"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn photo_capture_js() -> impl IntoResponse {
|
||||
(
|
||||
[
|
||||
|
|
|
|||
62
static/js/location.js
Normal file
62
static/js/location.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
const nearbyKeydown = (e, el) => {
|
||||
const root = el.closest("[data-location-root]");
|
||||
const results = root?.querySelector("#nearby-results");
|
||||
if (!results) return;
|
||||
const buttons = [...results.querySelectorAll("button")];
|
||||
if (!buttons.length) return;
|
||||
|
||||
const active = results.querySelector(".ss-active");
|
||||
let idx = active ? buttons.indexOf(active) : -1;
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
if (active) active.classList.remove("ss-active");
|
||||
idx = (idx + 1) % buttons.length;
|
||||
buttons[idx].classList.add("ss-active");
|
||||
buttons[idx].scrollIntoView({ block: "nearest" });
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
if (active) active.classList.remove("ss-active");
|
||||
idx = idx <= 0 ? buttons.length - 1 : idx - 1;
|
||||
buttons[idx].classList.add("ss-active");
|
||||
buttons[idx].scrollIntoView({ block: "nearest" });
|
||||
} else if (e.key === "Enter" && active) {
|
||||
e.preventDefault();
|
||||
active.click();
|
||||
} else if (e.key === "Escape") {
|
||||
results.classList.add("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
const locateUser = (triggerEl) => {
|
||||
const root = triggerEl.closest("[data-location-root]");
|
||||
const emit = (name, detail) =>
|
||||
root.dispatchEvent(new CustomEvent(name, { detail, bubbles: true }));
|
||||
if (!navigator.geolocation) {
|
||||
emit("location-error", {
|
||||
message: "Geolocation not supported by this browser.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
emit("location-start");
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => {
|
||||
emit("location-found", {
|
||||
lat: pos.coords.latitude,
|
||||
lng: pos.coords.longitude,
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
if (err.code === 1) {
|
||||
emit("location-error", {
|
||||
message: "Location access denied. Search by name instead.",
|
||||
});
|
||||
} else {
|
||||
emit("location-error", {
|
||||
message: "Could not determine location. Search by name instead.",
|
||||
});
|
||||
}
|
||||
},
|
||||
{ enableHighAccuracy: true, timeout: 15000 },
|
||||
);
|
||||
};
|
||||
|
|
@ -1,19 +1,5 @@
|
|||
{% import "partials/icons.html" as icons %}
|
||||
|
||||
{% macro location_search_js() %}
|
||||
const locateUser = (triggerEl) => { const root =
|
||||
triggerEl.closest('[data-location-root]'); const emit = (name, detail) =>
|
||||
root.dispatchEvent(new CustomEvent(name, { detail, bubbles: true })); if
|
||||
(!navigator.geolocation) { emit('location-error', { message: 'Geolocation not
|
||||
supported by this browser.' }); return; } emit('location-start');
|
||||
navigator.geolocation.getCurrentPosition( (pos) => { emit('location-found', {
|
||||
lat: pos.coords.latitude, lng: pos.coords.longitude }); }, (err) => { if
|
||||
(err.code === 1) { emit('location-error', { message: 'Location access denied.
|
||||
Search by name instead.' }); } else { emit('location-error', { message: 'Could
|
||||
not determine location. Search by name instead.' }); } }, {
|
||||
enableHighAccuracy: true, timeout: 15000 }, ); };
|
||||
{% endmacro %}
|
||||
|
||||
{% macro location_search(error_signal) %}
|
||||
<div
|
||||
data-location-root
|
||||
|
|
@ -49,6 +35,7 @@
|
|||
data-bind:_cafe-search
|
||||
class="input-field w-full"
|
||||
placeholder="Search for a cafe…"
|
||||
data-on:keydown="nearbyKeydown(evt, el)"
|
||||
data-on:input__debounce.350ms="if ($_locationFound) { @get('/api/v1/nearby-cafes?lat=' + $_userLat + '&lng=' + $_userLng + '&q=' + encodeURIComponent($_cafeSearch), {responseOverrides: {selector: '#nearby-results', mode: 'replace'}}) } else { $_cityName.length >= 2 && $_cafeSearch.length >= 2 && @get('/api/v1/nearby-cafes?near=' + encodeURIComponent($_cityName) + '&q=' + encodeURIComponent($_cafeSearch), {responseOverrides: {selector: '#nearby-results', mode: 'replace'}}) }"
|
||||
/>
|
||||
<div
|
||||
|
|
|
|||
Loading…
Reference in a new issue