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">
|
<script lang="ts">
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import type { HTLMInputAttributes } from 'svelte/elements';
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
interface Props extends HTMLInputAttributes {
|
interface Props extends HTMLInputAttributes {
|
||||||
value?: string;
|
value?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user