-
-
Notifications
You must be signed in to change notification settings - Fork 300
/
Autocomplete.svelte
128 lines (118 loc) · 4.03 KB
/
Autocomplete.svelte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { slide } from 'svelte/transition';
import { flip } from 'svelte/animate';
const dispatch = createEventDispatcher();
// Types
import type { AutocompleteOption } from './types';
// Props
/**
* Bind the input value.
* @type {unknown}
*/
export let input: unknown = undefined;
/**
* Define values for the list
* @type {AutocompleteOption[]}
*/
export let options: AutocompleteOption[] = [];
/**
* Provide allowlist values
* @type {unknown[]}
*/
export let allowlist: unknown[] = [];
/**
* Provide denylist values
* @type {unknown[]}
*/
export let denylist: unknown[] = [];
/** Provide a HTML markup to display when no match is found. */
export let emptyState: string = 'No Results Found.';
/** Set the animation duration. Use zero to disable. */
export let duration: number = 200;
// Props (region)
/** Provide arbitrary classes to nav element. */
export let regionNav: string = '';
/** Provide arbitrary classes to each list. */
export let regionList: string = 'list-nav';
/** Provide arbitrary classes to each list item. */
export let regionItem: string = '';
/** Provide arbitrary classes to each button. */
export let regionButton: string = 'w-full';
/** Provide arbitrary classes to empty message. */
export let regionEmpty: string = 'text-center';
// TODO: hese are slated to be removed!
/** DEPRECATED: replace with allowlist */
export let whitelist: unknown[] = [];
/** DEPRECATED: replace with denylist */
export let blacklist: unknown[] = [];
// Silence warning about unused props:
const deprecated = [whitelist, blacklist];
// Local
let listedOptions = options;
// Allowed Options
function filterByAllowed(): void {
if (allowlist.length) {
listedOptions = [...options].filter((option: AutocompleteOption) => allowlist.includes(option.value));
} else {
// IMPORTANT: required if the list goes from populated -> empty
listedOptions = [...options];
}
}
// Denied Options
function filterByDenied(): void {
if (denylist.length) {
const denySet = new Set(denylist);
listedOptions = [...options].filter((option: AutocompleteOption) => !denySet.has(option.value));
} else {
// IMPORTANT: required if the list goes from populated -> empty
listedOptions = [...options];
}
}
function filterOptions(): AutocompleteOption[] {
// Create a local copy of options
let _options = [...listedOptions];
// Filter options
_options = _options.filter((option: AutocompleteOption) => {
// Format the input search value
const inputFormatted = String(input).toLowerCase().trim();
// Format the option
let optionFormatted = JSON.stringify([option.label, option.value, option.keywords]).toLowerCase();
// Check Match
if (optionFormatted.includes(inputFormatted)) return option;
});
return _options;
}
function onSelection(option: AutocompleteOption) {
/** @event {AutocompleteOption} selection - Fire on option select. */
dispatch('selection', option);
}
// State
$: if (allowlist) filterByAllowed();
$: if (denylist) filterByDenied();
$: optionsFiltered = input ? filterOptions() : listedOptions;
// Reactive
$: classsesBase = `${$$props.class ?? ''}`;
$: classesNav = `${regionNav}`;
$: classesList = `${regionList}`;
$: classesItem = `${regionItem}`;
$: classesButton = `${regionButton}`;
$: classesEmtpy = `${regionEmpty}`;
</script>
<div class="autocomplete {classsesBase}" data-testid="autocomplete">
{#if optionsFiltered.length > 0}
<nav class="autocomplete-nav {classesNav}">
<ul class="autocomplete-list {classesList}">
{#each optionsFiltered as option, i (option)}
<li class="autocomplete-item {classesItem}" animate:flip={{ duration }} transition:slide|local={{ duration }}>
<button class="autocomplete-button {classesButton}" type="button" on:click={() => onSelection(option)} on:click on:keypress>
{@html option.label}
</button>
</li>
{/each}
</ul>
</nav>
{:else}
<div class="autocomplete-empty {classesEmtpy}">{emptyState}</div>
{/if}
</div>