61 lines
2.7 KiB
Svelte
61 lines
2.7 KiB
Svelte
<script lang="ts">
|
||
type Item = { id: string; name: string; pending?: boolean; proposerName?: string };
|
||
let {
|
||
items,
|
||
selected = $bindable<string[]>([]),
|
||
excludedIds = [],
|
||
fieldName,
|
||
emptyLabel = 'NO MATCHING RECORDS',
|
||
subjectLabel = 'TALENTS',
|
||
searchLabel = `FILTER AVAILABLE ${subjectLabel}`,
|
||
searchPlaceholder = `Search ${subjectLabel.toLocaleLowerCase()}…`,
|
||
emptySelectionLabel = `NO ${subjectLabel} ASSIGNED`
|
||
}: {
|
||
items: Item[];
|
||
selected?: string[];
|
||
excludedIds?: string[];
|
||
fieldName: string;
|
||
emptyLabel?: string;
|
||
subjectLabel?: string;
|
||
searchLabel?: string;
|
||
searchPlaceholder?: string;
|
||
emptySelectionLabel?: string;
|
||
} = $props();
|
||
let query = $state('');
|
||
const normalizedQuery = $derived(query.normalize('NFKC').trim().toLocaleLowerCase());
|
||
const available = $derived(items.filter((item) =>
|
||
!excludedIds.includes(item.id)
|
||
&& (!normalizedQuery || item.name.toLocaleLowerCase().includes(normalizedQuery))
|
||
));
|
||
const visible = $derived(available.slice(0, 30));
|
||
const selectedItems = $derived(selected.map((id) => items.find((item) => item.id === id)).filter((item): item is Item => Boolean(item)));
|
||
const toggle = (id: string) => {
|
||
selected = selected.includes(id) ? selected.filter((value) => value !== id) : [...selected, id];
|
||
};
|
||
</script>
|
||
|
||
<div class="assignment-picker">
|
||
{#each selected as id}<input type="hidden" name={fieldName} value={id}/>{/each}
|
||
<div class="assignment-selected">
|
||
<div class="assignment-summary"><span>ASSIGNED</span><b>{selected.length}</b></div>
|
||
<div class="assignment-chips">
|
||
{#each selectedItems as item}
|
||
<button type="button" class:pending-reference={item.pending} onclick={() => toggle(item.id)} aria-label={`Remove ${item.name}`}><span>{item.name}{#if item.pending}<small>PENDING</small>{/if}</span><b>×</b></button>
|
||
{:else}
|
||
<small>{emptySelectionLabel}</small>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
<label class="assignment-search"><span>{searchLabel}</span><input type="search" bind:value={query} placeholder={searchPlaceholder} autocomplete="off"/></label>
|
||
<div class="assignment-results-head"><span>{available.length} MATCHES</span>{#if available.length > visible.length}<b>SHOWING FIRST {visible.length}</b>{/if}</div>
|
||
<div class="assignment-results" role="list">
|
||
{#each visible as item}
|
||
<button type="button" class:selected={selected.includes(item.id)} class:pending-reference={item.pending} aria-pressed={selected.includes(item.id)} onclick={() => toggle(item.id)}>
|
||
<span>{item.name}{#if item.pending}<small>PENDING · {item.proposerName ?? 'CONTRIBUTOR'}</small>{/if}</span><b>{selected.includes(item.id) ? 'ASSIGNED' : 'ADD +'}</b>
|
||
</button>
|
||
{:else}
|
||
<p>{emptyLabel}</p>
|
||
{/each}
|
||
</div>
|
||
</div>
|