@enigmax/primitives 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-3BQVOOAM.js +388 -0
- package/dist/{chunk-QQFNAKMY.js → chunk-45UHLZYT.js} +1 -1
- package/dist/chunk-4VUHQFAT.js +130 -0
- package/dist/{chunk-D5A2ZMAG.js → chunk-4ZPMP47J.js} +6 -1
- package/dist/chunk-DSBYVA7V.js +713 -0
- package/dist/{chunk-FWVWX67R.js → chunk-FGBIZDV2.js} +3 -3
- package/dist/{chunk-R4ZAEE7V.js → chunk-KEVZ5XQV.js} +1 -1
- package/dist/chunk-N6PDHMAX.js +213 -0
- package/dist/chunk-NONGREXC.js +248 -0
- package/dist/chunk-VKL3DEIQ.js +804 -0
- package/dist/chunk-WPTBURIC.js +1627 -0
- package/dist/chunk-WSQC3PCC.js +466 -0
- package/dist/context-menu-D3FtTn7v.d.ts +174 -0
- package/dist/{index-dTdAbOWl.d.ts → index-DQNnohoo.d.ts} +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +8 -4
- package/dist/keys-D2zJs1uB.d.ts +100 -0
- package/dist/next/index.d.ts +12 -5
- package/dist/next/index.js +21 -14
- package/dist/react/button.d.ts +2 -2
- package/dist/react/context-menu.d.ts +202 -0
- package/dist/react/context-menu.js +6 -0
- package/dist/react/index.d.ts +11 -4
- package/dist/react/index.js +20 -13
- package/dist/react/input.d.ts +2 -2
- package/dist/react/input.js +1 -1
- package/dist/react/palette.d.ts +1 -1
- package/dist/react/palette.js +3 -3
- package/dist/react/search.d.ts +1 -1
- package/dist/react/search.js +2 -2
- package/dist/react/select.d.ts +217 -0
- package/dist/react/select.js +7 -0
- package/dist/react/selection.d.ts +104 -0
- package/dist/react/selection.js +4 -0
- package/dist/react/slot.d.ts +2 -2
- package/dist/react/toast.d.ts +165 -32
- package/dist/react/toast.js +1 -1
- package/dist/react-router/index.d.ts +12 -5
- package/dist/react-router/index.js +21 -14
- package/dist/search/index.d.ts +2 -2
- package/dist/search/index.js +1 -1
- package/dist/{search-DXxY8SEH.d.ts → search-DYgqRp37.d.ts} +20 -3
- package/dist/{search-UQEXAPQB.js → search-PBZORZ7P.js} +1 -1
- package/dist/select-ClSy-J1f.d.ts +100 -0
- package/dist/selection-B_pmzHpy.d.ts +150 -0
- package/package.json +21 -2
- package/recipes/context-menu/styles.css +177 -0
- package/recipes/input/styles.css +9 -0
- package/recipes/palette/styles.css +3 -0
- package/recipes/search/tailwind.tsx +3 -2
- package/recipes/select/styles.css +230 -0
- package/recipes/toast/styles.css +688 -192
- package/registry.json +434 -29
- package/src/core/context-menu.ts +694 -0
- package/src/core/keys.ts +264 -0
- package/src/core/search.ts +25 -1
- package/src/core/select.ts +404 -0
- package/src/core/selection.ts +648 -0
- package/src/index.ts +60 -1
- package/src/react/context-menu/context.ts +57 -0
- package/src/react/context-menu/index.tsx +94 -0
- package/src/react/context-menu/root.tsx +846 -0
- package/src/react/context-menu/styles.ts +186 -0
- package/src/react/index.ts +70 -2
- package/src/react/palette/root.tsx +2 -2
- package/src/react/select/context.ts +56 -0
- package/src/react/select/index.tsx +96 -0
- package/src/react/select/root.tsx +839 -0
- package/src/react/select/styles.ts +238 -0
- package/src/react/selection/index.tsx +115 -0
- package/src/react/selection/use-selection.ts +260 -0
- package/src/react/toast/NOTICE +20 -0
- package/src/react/toast/assets.tsx +85 -0
- package/src/react/toast/cn.ts +10 -0
- package/src/react/toast/hooks.ts +13 -0
- package/src/react/toast/index.tsx +736 -0
- package/src/react/toast/state.ts +207 -0
- package/src/react/toast/styles.ts +738 -0
- package/src/react/toast/types.ts +193 -0
- package/src/react/toaster.tsx +76 -341
- package/dist/chunk-JCCL7XKC.js +0 -465
- package/src/react/toast-styles.ts +0 -247
- /package/dist/{chunk-U3V4EHOB.js → chunk-3HDEZ2E7.js} +0 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts of a select that are not rendering: what is chosen, what is visible after the
|
|
3
|
+
* filter, and where the highlight goes when a key is pressed.
|
|
4
|
+
*
|
|
5
|
+
* Framework-agnostic on purpose - the React layer over this file is thin, and a renderer
|
|
6
|
+
* that is not React only has to draw. The two hard parts of a select live here: a selection
|
|
7
|
+
* that behaves the same whether one value or many are allowed, and a highlight that never
|
|
8
|
+
* lands on a disabled row.
|
|
9
|
+
*
|
|
10
|
+
* Filtering reuses the search core, so a select filters exactly like the search field does:
|
|
11
|
+
* accent-insensitive substring by default, Fuse.js when you hand over its constructor, or
|
|
12
|
+
* your own matcher. Fuse is never imported here - it is a peer the caller passes or not.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { typeaheadStep } from "@/core/keys";
|
|
16
|
+
import { createSearch, type FuseConstructor, type SearchOptions, type SearchInstance } from "@/core/search";
|
|
17
|
+
|
|
18
|
+
export interface SelectOption {
|
|
19
|
+
/** What ends up in `value`. Unique within the list. */
|
|
20
|
+
value: string;
|
|
21
|
+
/** What is read and what the filter searches. */
|
|
22
|
+
label: string;
|
|
23
|
+
/** A second line under the label. Searched as well. */
|
|
24
|
+
description?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Anything the renderer can draw beside the label - a ReactNode, an URL, a name. Typed
|
|
27
|
+
* as `unknown` because a core shared by every adapter cannot know what a node is.
|
|
28
|
+
*/
|
|
29
|
+
icon?: unknown;
|
|
30
|
+
/** Visible, listed, announced as unavailable, and never selectable or highlightable. */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** Rows carrying the same group render together under its name. */
|
|
33
|
+
group?: string;
|
|
34
|
+
/** Extra words the filter should match - synonyms, an old name, a code. */
|
|
35
|
+
keywords?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The fields the filter reads when the caller does not say. */
|
|
39
|
+
export const SELECT_SEARCH_KEYS = ["label", "description", "group", "keywords"];
|
|
40
|
+
|
|
41
|
+
export interface SelectOptions {
|
|
42
|
+
options?: readonly SelectOption[];
|
|
43
|
+
/** A string, a list when `multiple`, or null for nothing chosen. */
|
|
44
|
+
value?: string | readonly string[] | null;
|
|
45
|
+
multiple?: boolean;
|
|
46
|
+
/** Filter the list from a field inside the panel. */
|
|
47
|
+
searchable?: boolean;
|
|
48
|
+
/** Fuse.js's constructor, for fuzzy filtering. Omit it for the built-in matcher. */
|
|
49
|
+
fuse?: FuseConstructor;
|
|
50
|
+
fuseOptions?: Record<string, unknown>;
|
|
51
|
+
matcher?: SearchOptions<SelectOption>["matcher"];
|
|
52
|
+
searchKeys?: string[];
|
|
53
|
+
/** Close after a choice. Default: true when one value is allowed, false when many are. */
|
|
54
|
+
closeOnSelect?: boolean;
|
|
55
|
+
/** Every state change. */
|
|
56
|
+
onChange?: (state: SelectState) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Only the value, in the shape the caller asked for: a string, or a list when
|
|
59
|
+
* `multiple`. The React layer narrows this per mode, so a call site never has to
|
|
60
|
+
* widen it back by hand.
|
|
61
|
+
*/
|
|
62
|
+
onValueChange?: (value: string | string[], options: SelectOption[]) => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SelectState {
|
|
66
|
+
open: boolean;
|
|
67
|
+
query: string;
|
|
68
|
+
/** Index into `visible`. -1 when there is nothing to highlight. */
|
|
69
|
+
active: number;
|
|
70
|
+
/** What the panel shows: every option, or what survived the filter. */
|
|
71
|
+
visible: SelectOption[];
|
|
72
|
+
/** The chosen options, in the order they were chosen. */
|
|
73
|
+
selected: SelectOption[];
|
|
74
|
+
/** The chosen values, always as a list - `multiple` decides what is reported outwards. */
|
|
75
|
+
value: string[];
|
|
76
|
+
multiple: boolean;
|
|
77
|
+
/** True when a filter is running and matched nothing. */
|
|
78
|
+
empty: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SelectInstance {
|
|
82
|
+
readonly state: SelectState;
|
|
83
|
+
setOpen(open: boolean): void;
|
|
84
|
+
toggleOpen(): void;
|
|
85
|
+
setQuery(query: string): void;
|
|
86
|
+
/** Move the highlight, skipping disabled rows. */
|
|
87
|
+
move(key: SelectMoveKey): void;
|
|
88
|
+
setActive(index: number): void;
|
|
89
|
+
/** Choose a row: replaces the value, or toggles it when many are allowed. */
|
|
90
|
+
select(value: string): void;
|
|
91
|
+
/** Choose whatever is highlighted. */
|
|
92
|
+
selectActive(): void;
|
|
93
|
+
/** Drop one value. The × on a tag. */
|
|
94
|
+
remove(value: string): void;
|
|
95
|
+
clear(): void;
|
|
96
|
+
/** Jump to the row starting with what was typed, the way a native select does. */
|
|
97
|
+
typeahead(character: string): void;
|
|
98
|
+
update(options: Partial<SelectOptions>): void;
|
|
99
|
+
subscribe(listener: (state: SelectState) => void): () => void;
|
|
100
|
+
destroy(): void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type SelectMoveKey = "ArrowDown" | "ArrowUp" | "Home" | "End" | "PageDown" | "PageUp";
|
|
104
|
+
|
|
105
|
+
const PAGE = 5;
|
|
106
|
+
|
|
107
|
+
/** "Perú" must be reachable by typing "peru" - here as in the search core. */
|
|
108
|
+
function fold(value: string): string {
|
|
109
|
+
return value.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toList(value: SelectOptions["value"]): string[] {
|
|
113
|
+
if (value == null) return [];
|
|
114
|
+
const list = Array.isArray(value) ? [...value] : [value as string];
|
|
115
|
+
// The empty string is NOTHING CHOSEN, not a value. `value=""` is how React writes an
|
|
116
|
+
// empty controlled field and how HTML writes a placeholder option, so keeping it would
|
|
117
|
+
// leave a select that shows its placeholder while holding a value, offering a clear
|
|
118
|
+
// button for it and posting it with the form.
|
|
119
|
+
return list.filter((entry) => entry !== "");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sameValues(left: readonly string[], right: readonly string[]): boolean {
|
|
123
|
+
return left.length === right.length && left.every((entry, index) => entry === right[index]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** By identity: the option objects are the caller's, and a rebuilt list holds the same ones. */
|
|
127
|
+
function sameRows(left: readonly SelectOption[], right: readonly SelectOption[]): boolean {
|
|
128
|
+
return left.length === right.length && left.every((option, index) => option === right[index]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function sameState(left: SelectState, right: SelectState): boolean {
|
|
132
|
+
return left.open === right.open
|
|
133
|
+
&& left.query === right.query
|
|
134
|
+
&& left.active === right.active
|
|
135
|
+
&& left.multiple === right.multiple
|
|
136
|
+
&& left.empty === right.empty
|
|
137
|
+
&& sameValues(left.value, right.value)
|
|
138
|
+
&& sameRows(left.visible, right.visible)
|
|
139
|
+
&& sameRows(left.selected, right.selected);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createSelect(options: SelectOptions = {}): SelectInstance {
|
|
143
|
+
let opts: SelectOptions = { ...options };
|
|
144
|
+
let all: readonly SelectOption[] = opts.options ?? [];
|
|
145
|
+
let value = toList(opts.value);
|
|
146
|
+
let open = false;
|
|
147
|
+
let query = "";
|
|
148
|
+
let active = -1;
|
|
149
|
+
let visible: SelectOption[] = [...all];
|
|
150
|
+
let typed = "";
|
|
151
|
+
let typedAt = 0;
|
|
152
|
+
let destroyed = false;
|
|
153
|
+
const listeners = new Set<(state: SelectState) => void>();
|
|
154
|
+
|
|
155
|
+
// One engine, built once and updated: it indexes on construction, so rebuilding it per
|
|
156
|
+
// keystroke would re-index the whole list on every letter.
|
|
157
|
+
const engine: SearchInstance<SelectOption> = createSearch<SelectOption>({
|
|
158
|
+
items: all,
|
|
159
|
+
keys: opts.searchKeys ?? SELECT_SEARCH_KEYS,
|
|
160
|
+
fuse: opts.fuse,
|
|
161
|
+
fuseOptions: opts.fuseOptions,
|
|
162
|
+
matcher: opts.matcher,
|
|
163
|
+
// A select filters as you type. There is nothing to debounce: the data is already
|
|
164
|
+
// in memory, and a delay here is felt as the list lagging behind the field.
|
|
165
|
+
debounce: 0,
|
|
166
|
+
// An empty query in a select means "everything", not "nothing" - the opposite of a
|
|
167
|
+
// search field, where an empty query has nothing to show.
|
|
168
|
+
empty: "all"
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
function optionOf(candidate: string): SelectOption | undefined {
|
|
172
|
+
return all.find((option) => option.value === candidate);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function selectedOptions(): SelectOption[] {
|
|
176
|
+
// Mapped through the option list so a value with no option left (data reloaded,
|
|
177
|
+
// an id that no longer exists) simply disappears instead of rendering a blank tag.
|
|
178
|
+
return value.map(optionOf).filter((option): option is SelectOption => Boolean(option));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function snapshot(): SelectState {
|
|
182
|
+
return {
|
|
183
|
+
open,
|
|
184
|
+
query,
|
|
185
|
+
active,
|
|
186
|
+
visible,
|
|
187
|
+
selected: selectedOptions(),
|
|
188
|
+
value: [...value],
|
|
189
|
+
multiple: Boolean(opts.multiple),
|
|
190
|
+
empty: visible.length === 0
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function emit(): void {
|
|
195
|
+
const state = snapshot();
|
|
196
|
+
opts.onChange?.(state);
|
|
197
|
+
for (const listener of listeners) listener(state);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Positive modulo: the walk goes backwards as often as forwards. */
|
|
201
|
+
function wrap(index: number): number {
|
|
202
|
+
const length = visible.length;
|
|
203
|
+
return ((index % length) + length) % length;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function enabledIndex(from: number, step: number): number {
|
|
207
|
+
if (visible.length === 0) return -1;
|
|
208
|
+
// Walks at most once around the list: a list whose rows are all disabled is a real
|
|
209
|
+
// list, and it has to leave the highlight nowhere rather than spin looking for one.
|
|
210
|
+
for (let attempt = 0; attempt < visible.length; attempt++) {
|
|
211
|
+
const index = wrap(from + step * attempt);
|
|
212
|
+
if (!visible[index]?.disabled) return index;
|
|
213
|
+
}
|
|
214
|
+
return -1;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The option the highlight is on, so a filter can put it back where it was. */
|
|
218
|
+
let lastActive: SelectOption | undefined;
|
|
219
|
+
|
|
220
|
+
function refilter(): void {
|
|
221
|
+
visible = opts.searchable && query.trim()
|
|
222
|
+
? engine.searchNow(query).map((match) => match.item)
|
|
223
|
+
: [...all];
|
|
224
|
+
|
|
225
|
+
// The highlight follows the row it was on when that row survived the filter, so
|
|
226
|
+
// deleting a letter does not throw the selection back to the top of the list.
|
|
227
|
+
const current = active >= 0 ? visible.findIndex((option) => option === lastActive) : -1;
|
|
228
|
+
active = current >= 0 && !visible[current]?.disabled ? current : enabledIndex(0, 1);
|
|
229
|
+
lastActive = visible[active];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function reportValue(): void {
|
|
233
|
+
const chosen = selectedOptions();
|
|
234
|
+
opts.onValueChange?.((opts.multiple ? [...value] : value[0] ?? "") as never, chosen);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function closesOnSelect(): boolean {
|
|
238
|
+
return opts.closeOnSelect ?? !opts.multiple;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
refilter();
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
get state() { return snapshot(); },
|
|
245
|
+
|
|
246
|
+
setOpen(next: boolean) {
|
|
247
|
+
if (destroyed || open === next) return;
|
|
248
|
+
open = next;
|
|
249
|
+
if (open) {
|
|
250
|
+
// Opening starts clean and lands on what is already chosen: a select that
|
|
251
|
+
// reopens on the first row makes you find your own value again.
|
|
252
|
+
query = "";
|
|
253
|
+
refilter();
|
|
254
|
+
const chosen = value[0] ? visible.findIndex((option) => option.value === value[0]) : -1;
|
|
255
|
+
active = chosen >= 0 && !visible[chosen]?.disabled ? chosen : enabledIndex(0, 1);
|
|
256
|
+
lastActive = visible[active];
|
|
257
|
+
} else {
|
|
258
|
+
query = "";
|
|
259
|
+
typed = "";
|
|
260
|
+
refilter();
|
|
261
|
+
}
|
|
262
|
+
emit();
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
toggleOpen() { this.setOpen(!open); },
|
|
266
|
+
|
|
267
|
+
setQuery(next: string) {
|
|
268
|
+
if (destroyed) return;
|
|
269
|
+
query = next;
|
|
270
|
+
refilter();
|
|
271
|
+
emit();
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
move(key: SelectMoveKey) {
|
|
275
|
+
if (destroyed || visible.length === 0) return;
|
|
276
|
+
const from = active < 0 ? -1 : active;
|
|
277
|
+
switch (key) {
|
|
278
|
+
// Wrapping, because a select is a short list and the row after the last one
|
|
279
|
+
// is the first: clamping leaves the arrow key doing nothing, which reads as
|
|
280
|
+
// the panel having frozen.
|
|
281
|
+
case "ArrowDown": active = enabledIndex(from + 1, 1); break;
|
|
282
|
+
case "ArrowUp": active = enabledIndex(from - 1 + visible.length, -1); break;
|
|
283
|
+
case "Home": active = enabledIndex(0, 1); break;
|
|
284
|
+
case "End": active = enabledIndex(visible.length - 1, -1); break;
|
|
285
|
+
case "PageDown": active = enabledIndex(Math.min(visible.length - 1, from + PAGE), 1); break;
|
|
286
|
+
case "PageUp": active = enabledIndex(Math.max(0, from - PAGE), -1); break;
|
|
287
|
+
}
|
|
288
|
+
lastActive = visible[active];
|
|
289
|
+
emit();
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
setActive(index: number) {
|
|
293
|
+
// The row already highlighted is not a change: this arrives from `pointermove`,
|
|
294
|
+
// which fires on every pixel, and emitting there re-renders the whole panel
|
|
295
|
+
// sixty times a second - and the scroll it triggers moves the row under a
|
|
296
|
+
// stationary cursor, which fires it again.
|
|
297
|
+
if (destroyed || index === active || index < 0 || index >= visible.length || visible[index]?.disabled) return;
|
|
298
|
+
active = index;
|
|
299
|
+
lastActive = visible[active];
|
|
300
|
+
emit();
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
select(next: string) {
|
|
304
|
+
if (destroyed) return;
|
|
305
|
+
const option = optionOf(next);
|
|
306
|
+
// A disabled row is listed and announced, never chosen - including by a click
|
|
307
|
+
// that got through, which is the one path a renderer tends to forget.
|
|
308
|
+
if (!option || option.disabled) return;
|
|
309
|
+
|
|
310
|
+
if (opts.multiple) {
|
|
311
|
+
value = value.includes(next) ? value.filter((entry) => entry !== next) : [...value, next];
|
|
312
|
+
} else {
|
|
313
|
+
value = [next];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
reportValue();
|
|
317
|
+
if (closesOnSelect()) {
|
|
318
|
+
open = false;
|
|
319
|
+
query = "";
|
|
320
|
+
refilter();
|
|
321
|
+
}
|
|
322
|
+
emit();
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
selectActive() {
|
|
326
|
+
const option = visible[active];
|
|
327
|
+
if (option) this.select(option.value);
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
remove(next: string) {
|
|
331
|
+
if (destroyed || !value.includes(next)) return;
|
|
332
|
+
value = value.filter((entry) => entry !== next);
|
|
333
|
+
reportValue();
|
|
334
|
+
emit();
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
clear() {
|
|
338
|
+
if (destroyed || value.length === 0) return;
|
|
339
|
+
value = [];
|
|
340
|
+
reportValue();
|
|
341
|
+
emit();
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
typeahead(character: string) {
|
|
345
|
+
if (destroyed || character.length !== 1) return;
|
|
346
|
+
const step = typeaheadStep({ typed, at: typedAt }, character);
|
|
347
|
+
typed = step.typed;
|
|
348
|
+
typedAt = step.at;
|
|
349
|
+
|
|
350
|
+
const needle = fold(step.needle);
|
|
351
|
+
// A cycling press searches from the row AFTER this one, so pressing the letter
|
|
352
|
+
// again walks through everything starting with it instead of sticking on the
|
|
353
|
+
// first match; a word searches from the current row, so refining it does not
|
|
354
|
+
// jump off the option it already found.
|
|
355
|
+
const from = step.cycle ? 1 : 0;
|
|
356
|
+
for (let step = from; step < visible.length + from; step++) {
|
|
357
|
+
const index = wrap(Math.max(active, 0) + step);
|
|
358
|
+
const option = visible[index];
|
|
359
|
+
if (!option || option.disabled) continue;
|
|
360
|
+
if (fold(option.label).startsWith(needle)) {
|
|
361
|
+
active = index;
|
|
362
|
+
lastActive = option;
|
|
363
|
+
emit();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
update(next: Partial<SelectOptions>) {
|
|
370
|
+
if (destroyed) return;
|
|
371
|
+
const before = snapshot();
|
|
372
|
+
const hadOptions = next.options !== undefined;
|
|
373
|
+
opts = { ...opts, ...next };
|
|
374
|
+
if (hadOptions) all = next.options ?? [];
|
|
375
|
+
if (next.value !== undefined) value = toList(next.value);
|
|
376
|
+
if (hadOptions || next.searchKeys || next.fuse || next.fuseOptions || next.matcher) {
|
|
377
|
+
engine.update({
|
|
378
|
+
items: all,
|
|
379
|
+
keys: opts.searchKeys ?? SELECT_SEARCH_KEYS,
|
|
380
|
+
fuse: opts.fuse,
|
|
381
|
+
fuseOptions: opts.fuseOptions,
|
|
382
|
+
matcher: opts.matcher
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
refilter();
|
|
386
|
+
// An update that changed nothing must not emit. A renderer subscribes to this
|
|
387
|
+
// and pushes its props back in whenever it renders, so emitting regardless
|
|
388
|
+
// redraws the whole panel for a list, a filter and a value that are the ones it
|
|
389
|
+
// already had - and hands any renderer that rebuilds those props a render loop.
|
|
390
|
+
if (!sameState(before, snapshot())) emit();
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
subscribe(listener) {
|
|
394
|
+
listeners.add(listener);
|
|
395
|
+
return () => { listeners.delete(listener); };
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
destroy() {
|
|
399
|
+
destroyed = true;
|
|
400
|
+
listeners.clear();
|
|
401
|
+
engine.destroy();
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
}
|