Files
varo-website/src/components/admin/search/Search.svelte
bytedream ee8f595ecc
All checks were successful
deploy / build-and-deploy (/testvaro, /opt/website-test, website-test) (push) Successful in 22s
deploy / build-and-deploy (/varo, /opt/website, website) (push) Successful in 21s
add feedback and report things
2025-06-21 14:46:42 +02:00

100 lines
2.6 KiB
Svelte

<script lang="ts">
// types
interface Props {
id?: string;
value?: string | null;
label?: string;
readonly?: boolean;
required?: boolean;
mustMatch?: boolean;
requestSuggestions: (query: string, limit: number) => Promise<{ name: string; value: string }[]>;
onSubmit?: (value: { name: string; value: string } | null) => void;
}
// html bindings
let container: HTMLDivElement;
// inputs
let { id, value = $bindable(), label, readonly, required, mustMatch, requestSuggestions, onSubmit }: Props = $props();
// states
let inputValue = $derived(value);
let suggestions = $state<{ name: string; value: string }[]>([]);
let matched = $state(false);
// callbacks
async function onBodyMouseDown(e: MouseEvent) {
if (!container.contains(e.target as Node)) suggestions = [];
}
async function onSearchInput() {
if (readonly) return;
suggestions = await requestSuggestions(inputValue ?? '', 5);
let suggestion = suggestions.find((s) => s.name === inputValue);
if (suggestion != null) {
inputValue = value = suggestion.name;
matched = true;
onSubmit?.(suggestion);
} else if (!mustMatch) {
value = inputValue;
matched = false;
} else {
value = null;
matched = false;
onSubmit?.(null);
}
}
function onSuggestionClick(name: string) {
const suggestion = suggestions.find((s) => s.name === name)!;
inputValue = value = suggestion.name;
suggestions = [];
onSubmit?.(suggestion);
}
</script>
<svelte:body onmousedown={onBodyMouseDown} />
<fieldset class="fieldset">
<legend class="fieldset-legend">
<span>
{label}
{#if required}
<span class="text-red-700">*</span>
{/if}
</span>
</legend>
<div class="relative" bind:this={container}>
<input
{id}
{readonly}
type="search"
autocomplete="off"
class="input"
bind:value={inputValue}
oninput={() => onSearchInput()}
onfocusin={() => onSearchInput()}
pattern={mustMatch && matched ? `^(${suggestions.map((s) => s.name).join('|')})$` : undefined}
/>
{#if suggestions.length > 0}
<ul class="absolute bg-base-200 w-full z-20 menu menu-sm rounded-box">
{#each suggestions as suggestion (suggestion)}
<li class="w-full text-left">
<button
class="block w-full overflow-hidden text-ellipsis whitespace-nowrap"
title={suggestion.name}
onclick={() => onSuggestionClick(suggestion.name)}>{suggestion.name}</button
>
</li>
{/each}
</ul>
{/if}
</div>
<p class="fieldset-label"></p>
</fieldset>