158 lines
3.6 KiB
Svelte
158 lines
3.6 KiB
Svelte
<script lang="ts">
|
|
import type { Guest } from "$lib/types";
|
|
import * as db from "$lib/db";
|
|
|
|
interface Props {
|
|
value: string;
|
|
/** Wybranie gościa z podpowiedzi — rodzic uzupełnia pozostałe pola. */
|
|
onselect: (guest: Guest) => void;
|
|
}
|
|
|
|
let { value = $bindable(), onselect }: Props = $props();
|
|
|
|
let suggestions = $state<Guest[]>([]);
|
|
let open = $state(false);
|
|
let highlighted = $state(0);
|
|
let searchToken = 0;
|
|
|
|
async function oninput() {
|
|
const q = value.trim();
|
|
if (q.length < 2) {
|
|
suggestions = [];
|
|
open = false;
|
|
return;
|
|
}
|
|
const token = ++searchToken;
|
|
const found = await db.searchGuests(q);
|
|
if (token !== searchToken) return;
|
|
suggestions = found;
|
|
highlighted = 0;
|
|
open = found.length > 0;
|
|
}
|
|
|
|
function choose(guest: Guest) {
|
|
value = guest.name;
|
|
open = false;
|
|
onselect(guest);
|
|
}
|
|
|
|
function onkeydown(event: KeyboardEvent) {
|
|
if (!open) return;
|
|
if (event.key === "ArrowDown") {
|
|
event.preventDefault();
|
|
highlighted = (highlighted + 1) % suggestions.length;
|
|
} else if (event.key === "ArrowUp") {
|
|
event.preventDefault();
|
|
highlighted = (highlighted - 1 + suggestions.length) % suggestions.length;
|
|
} else if (event.key === "Enter") {
|
|
event.preventDefault();
|
|
choose(suggestions[highlighted]);
|
|
} else if (event.key === "Escape") {
|
|
// zamknij tylko podpowiedzi, nie cały dialog
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
open = false;
|
|
}
|
|
}
|
|
|
|
function onblur() {
|
|
open = false;
|
|
}
|
|
</script>
|
|
|
|
<div class="wrap">
|
|
<input
|
|
type="text"
|
|
bind:value
|
|
{oninput}
|
|
{onkeydown}
|
|
{onblur}
|
|
required
|
|
placeholder="Jan Kowalski"
|
|
autocomplete="off"
|
|
role="combobox"
|
|
aria-expanded={open}
|
|
aria-autocomplete="list"
|
|
aria-controls="guest-suggestions"
|
|
/>
|
|
{#if open}
|
|
<ul class="suggestions" id="guest-suggestions" role="listbox">
|
|
{#each suggestions as guest, i (guest.id)}
|
|
<li role="option" aria-selected={i === highlighted}>
|
|
<button
|
|
type="button"
|
|
class:hl={i === highlighted}
|
|
onclick={() => choose(guest)}
|
|
onpointerdown={(e) => e.preventDefault()}
|
|
>
|
|
<span class="n">{guest.name}</span>
|
|
{#if guest.phone}<span class="d">{guest.phone}</span>{/if}
|
|
{#if guest.address}<span class="d">{guest.address}</span>{/if}
|
|
</button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.wrap {
|
|
position: relative;
|
|
}
|
|
input {
|
|
width: 100%;
|
|
font: inherit;
|
|
font-size: 14px;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 7px 9px;
|
|
}
|
|
.suggestions {
|
|
position: absolute;
|
|
top: calc(100% + 2px);
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 10;
|
|
margin: 0;
|
|
padding: 4px;
|
|
list-style: none;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 14px rgb(0 0 0 / 0.15);
|
|
max-height: 220px;
|
|
overflow-y: auto;
|
|
}
|
|
.suggestions button {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: baseline;
|
|
width: 100%;
|
|
font: inherit;
|
|
font-size: 13px;
|
|
text-align: left;
|
|
padding: 6px 8px;
|
|
border: none;
|
|
border-radius: 5px;
|
|
background: transparent;
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
}
|
|
.suggestions button:hover,
|
|
.suggestions button.hl {
|
|
background: var(--today);
|
|
}
|
|
.n {
|
|
font-weight: 600;
|
|
}
|
|
.d {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
</style>
|