148 lines
4.2 KiB
Svelte
148 lines
4.2 KiB
Svelte
<script lang="ts">
|
|
import type { components } from '$lib/generated/api';
|
|
import { m } from '$lib/paraglide/messages.js';
|
|
import { clickOutside } from '$lib/shared/actions/clickOutside';
|
|
import { formatDate } from '$lib/shared/utils/date';
|
|
|
|
type Document = components['schemas']['Document'];
|
|
type DocumentSearchItem = components['schemas']['DocumentSearchItem'];
|
|
|
|
interface Props {
|
|
selectedDocuments?: Document[];
|
|
placeholder?: string;
|
|
hiddenInputName?: string;
|
|
}
|
|
|
|
let {
|
|
selectedDocuments = $bindable([]),
|
|
placeholder = m.geschichte_editor_search_document(),
|
|
hiddenInputName = 'documentIds'
|
|
}: Props = $props();
|
|
|
|
let searchTerm = $state('');
|
|
let results: Document[] = $state([]);
|
|
let showDropdown = $state(false);
|
|
let loading = $state(false);
|
|
let debounceTimer: ReturnType<typeof setTimeout>;
|
|
let inputEl: HTMLInputElement;
|
|
let dropdownStyle = $state('');
|
|
|
|
function updateDropdownPosition() {
|
|
if (!inputEl) return;
|
|
const rect = inputEl.getBoundingClientRect();
|
|
dropdownStyle = `position:fixed;top:${rect.bottom + 4}px;left:${rect.left}px;width:${rect.width}px`;
|
|
}
|
|
|
|
function handleInput() {
|
|
showDropdown = true;
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(async () => {
|
|
if (searchTerm.length < 1) {
|
|
results = [];
|
|
return;
|
|
}
|
|
loading = true;
|
|
try {
|
|
const res = await fetch(`/api/documents/search?q=${encodeURIComponent(searchTerm)}&size=10`);
|
|
if (res.ok) {
|
|
const body: { items: DocumentSearchItem[] } = await res.json();
|
|
const docs = body.items.map((it) => it.document);
|
|
results = docs.filter((d) => !selectedDocuments.some((s) => s.id === d.id));
|
|
}
|
|
} catch {
|
|
results = [];
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}, 300);
|
|
}
|
|
|
|
function selectDocument(doc: Document) {
|
|
selectedDocuments = [...selectedDocuments, doc];
|
|
searchTerm = '';
|
|
showDropdown = false;
|
|
results = [];
|
|
}
|
|
|
|
function removeDocument(id: string | undefined) {
|
|
selectedDocuments = selectedDocuments.filter((d) => d.id !== id);
|
|
}
|
|
|
|
function formatDocLabel(doc: Document): string {
|
|
if (doc.documentDate) return `${doc.title} · ${formatDate(doc.documentDate, 'short')}`;
|
|
return doc.title;
|
|
}
|
|
</script>
|
|
|
|
<svelte:window onscroll={updateDropdownPosition} onresize={updateDropdownPosition} />
|
|
|
|
{#each selectedDocuments as doc (doc.id)}
|
|
<input type="hidden" name={hiddenInputName} value={doc.id} />
|
|
{/each}
|
|
|
|
<div class="relative" use:clickOutside onclickoutside={() => (showDropdown = false)}>
|
|
<div
|
|
class="flex min-h-[38px] flex-wrap gap-2 rounded border border-line bg-surface p-2 shadow-sm focus-within:ring-2 focus-within:ring-focus-ring focus-within:outline-none"
|
|
>
|
|
{#each selectedDocuments as doc (doc.id)}
|
|
<span
|
|
class="inline-flex items-center gap-1 rounded bg-muted px-2 py-1 text-sm font-medium text-ink"
|
|
>
|
|
{formatDocLabel(doc)}
|
|
<button
|
|
type="button"
|
|
onclick={() => removeDocument(doc.id)}
|
|
class="ml-0.5 text-ink/50 hover:text-red-500 focus:outline-none"
|
|
aria-label={m.comp_multiselect_remove()}
|
|
>
|
|
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</span>
|
|
{/each}
|
|
|
|
<input
|
|
bind:this={inputEl}
|
|
type="text"
|
|
autocomplete="off"
|
|
bind:value={searchTerm}
|
|
oninput={handleInput}
|
|
onfocus={() => {
|
|
updateDropdownPosition();
|
|
showDropdown = true;
|
|
}}
|
|
placeholder={placeholder}
|
|
class="min-w-[120px] flex-1 border-none bg-transparent p-1 text-sm outline-none focus:ring-0"
|
|
/>
|
|
</div>
|
|
|
|
{#if showDropdown && (results.length > 0 || loading)}
|
|
<div
|
|
style={dropdownStyle}
|
|
class="ring-opacity-5 z-50 max-h-60 overflow-auto rounded-md bg-surface py-1 text-base shadow-lg ring-1 ring-black sm:text-sm"
|
|
>
|
|
{#if loading}
|
|
<div class="p-2 text-sm text-ink-2">{m.comp_multiselect_loading()}</div>
|
|
{:else}
|
|
{#each results as doc (doc.id)}
|
|
<div
|
|
class="cursor-pointer py-2 pr-9 pl-3 text-ink select-none hover:bg-muted"
|
|
onclick={() => selectDocument(doc)}
|
|
onkeydown={(e) => e.key === 'Enter' && selectDocument(doc)}
|
|
role="button"
|
|
tabindex="0"
|
|
>
|
|
{formatDocLabel(doc)}
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|