100 lines
2.6 KiB
Svelte
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>
|