Add initial location search box
This commit is contained in:
104
src/lib/components/ui/LocationSearchBox.svelte
Normal file
104
src/lib/components/ui/LocationSearchBox.svelte
Normal file
@@ -0,0 +1,104 @@
|
||||
<script lang="ts">
|
||||
import Textbox from '$lib/components/Textbox.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface LocationRecord {
|
||||
n: string; // name
|
||||
t: string; // tiploc
|
||||
c?: string; // crs
|
||||
s: string; // search string
|
||||
}
|
||||
|
||||
let value = $state("");
|
||||
let results = $state<LocationRecord[]>([]);
|
||||
let locations: LocationRecord[] = [];
|
||||
|
||||
let showResults = $state(false);
|
||||
let selectedIndex = $state(-1);
|
||||
|
||||
const MAX_RESULTS = 10;
|
||||
|
||||
async function loadLocations() {
|
||||
const res = await fetch("/api/tiplocs");
|
||||
locations = await res.json();
|
||||
}
|
||||
|
||||
onMount(loadLocations);
|
||||
|
||||
function tokenize(query: string) {
|
||||
return query
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function search(query: string) {
|
||||
if (query.length < 2) {
|
||||
results = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const tokens = tokenize(query);
|
||||
|
||||
results = locations
|
||||
.filter(r => tokens.every(t => r.s.includes(t)))
|
||||
.slice(0, MAX_RESULTS);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
search(value);
|
||||
});
|
||||
|
||||
function choose(loc: LocationRecord) {
|
||||
value = loc.n;
|
||||
showResults = false;
|
||||
selectedIndex = -1;
|
||||
|
||||
// emit event later if needed
|
||||
}
|
||||
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
if (!results.length) return;
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
selectedIndex = Math.min(selectedIndex + 1, results.length - 1);
|
||||
}
|
||||
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0);
|
||||
}
|
||||
|
||||
if (e.key === "Enter" && selectedIndex >= 0) {
|
||||
choose(results[selectedIndex]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="location-search">
|
||||
<Textbox
|
||||
bind:value
|
||||
placeholder="Search for a station or TIPLOC"
|
||||
oninput={() => showResults = true}
|
||||
onkeydown={handleKey}
|
||||
/>
|
||||
|
||||
{#if showResults && results.length}
|
||||
<ul class="suggestions">
|
||||
{#each results as loc, i}
|
||||
<li
|
||||
class:selected={i === selectedIndex}
|
||||
onclick={() => choose(loc)}
|
||||
>
|
||||
<span class="name">{loc.n}</span>
|
||||
|
||||
{#if loc.c}
|
||||
<span class="crs">{loc.c}</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { HTLMInputAttributes } from 'svelte/elements';
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
|
||||
interface Props extends HTMLInputAttributes {
|
||||
value?: string;
|
||||
|
||||
Reference in New Issue
Block a user