svelte-5-select 1.0.0-beta.1 → 1.0.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/Select.svelte +194 -389
- package/dist/Select.svelte.d.ts +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/tailwind.css +4 -4
- package/dist/types.d.ts +81 -10
- package/dist/use-hover.svelte.d.ts +13 -0
- package/dist/use-hover.svelte.js +97 -0
- package/dist/use-load-options.svelte.d.ts +4 -0
- package/dist/use-load-options.svelte.js +68 -0
- package/dist/use-value.svelte.d.ts +14 -0
- package/dist/use-value.svelte.js +175 -0
- package/dist/utils.d.ts +2 -2
- package/package.json +23 -23
package/dist/Select.svelte
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { onDestroy, onMount, untrack } from 'svelte';
|
|
4
4
|
import { offset, flip, shift } from 'svelte-floating-ui/dom';
|
|
5
5
|
import { createFloatingActions } from 'svelte-floating-ui';
|
|
6
|
-
import type { FloatingConfig, SelectProps } from './types';
|
|
6
|
+
import type { FloatingConfig, SelectProps, SelectValue, ErrorEvent as SelectErrorEvent } from './types';
|
|
7
7
|
import { useAriaHandlers } from './aria-handlers.svelte';
|
|
8
8
|
|
|
9
9
|
import _filter from './filter';
|
|
@@ -14,12 +14,15 @@
|
|
|
14
14
|
import type { SelectItem } from './types';
|
|
15
15
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
16
16
|
import { useKeyboardNavigation } from './keyboard-navigation.svelte';
|
|
17
|
-
import {
|
|
17
|
+
import { useHover } from './use-hover.svelte';
|
|
18
|
+
import { useValue } from './use-value.svelte';
|
|
19
|
+
import { useLoadOptions } from './use-load-options.svelte';
|
|
20
|
+
import { areItemsEqual, isItemSelectableCheck, createGroupHeaderItem as _createGroupHeaderItem } from './utils';
|
|
18
21
|
|
|
19
22
|
const defaultItemFilter = (label: string, filterText: string, option: SelectItem): boolean =>
|
|
20
23
|
`${label}`.toLowerCase().includes(filterText?.toLowerCase());
|
|
21
24
|
|
|
22
|
-
const defaultOnError = (error:
|
|
25
|
+
const defaultOnError = (error: SelectErrorEvent): void => {};
|
|
23
26
|
const defaultOnLoaded = (options: SelectItem[]): void => {};
|
|
24
27
|
const defaultHandleClear = (e?: MouseEvent): void => {};
|
|
25
28
|
|
|
@@ -92,6 +95,7 @@
|
|
|
92
95
|
loadOptions = undefined,
|
|
93
96
|
|
|
94
97
|
// ARIA props
|
|
98
|
+
ariaLabel = undefined,
|
|
95
99
|
ariaFocused = () => {
|
|
96
100
|
return `Select is focused, type to refine list, press down to open the menu.`;
|
|
97
101
|
},
|
|
@@ -146,6 +150,9 @@
|
|
|
146
150
|
: value
|
|
147
151
|
);
|
|
148
152
|
|
|
153
|
+
const _generatedId = `svelte-select-${Math.random().toString(36).slice(2, 9)}`;
|
|
154
|
+
let _id = $derived(id ?? _generatedId);
|
|
155
|
+
|
|
149
156
|
const DEFAULT_INPUT_ATTRS = {
|
|
150
157
|
autocapitalize: 'none',
|
|
151
158
|
autocomplete: 'off',
|
|
@@ -164,7 +171,7 @@
|
|
|
164
171
|
|
|
165
172
|
handleClear = (e?: MouseEvent): void => {
|
|
166
173
|
clearState = true;
|
|
167
|
-
onclear(value);
|
|
174
|
+
onclear(value as SelectValue);
|
|
168
175
|
value = undefined;
|
|
169
176
|
closeList();
|
|
170
177
|
handleFocus();
|
|
@@ -173,10 +180,12 @@
|
|
|
173
180
|
let list = $state<HTMLDivElement | undefined>();
|
|
174
181
|
let _inputAttributes = $derived<HTMLInputAttributes>({
|
|
175
182
|
...DEFAULT_INPUT_ATTRS,
|
|
176
|
-
role:
|
|
177
|
-
'aria-controls': listOpen ? `listbox-${
|
|
183
|
+
role: 'combobox',
|
|
184
|
+
'aria-controls': listOpen ? `listbox-${_id}` : undefined,
|
|
178
185
|
'aria-expanded': listOpen,
|
|
179
186
|
'aria-haspopup': 'listbox',
|
|
187
|
+
'aria-activedescendant': listOpen ? `listbox-${_id}-item-${hoverItemIndex}` : undefined,
|
|
188
|
+
'aria-label': ariaLabel,
|
|
180
189
|
tabindex: 0,
|
|
181
190
|
readonly: !searchable,
|
|
182
191
|
id: id ? id : undefined,
|
|
@@ -184,9 +193,8 @@
|
|
|
184
193
|
});
|
|
185
194
|
let activeValue = $state<number | undefined>(undefined);
|
|
186
195
|
let prev_value = $state<SelectItem | SelectItem[] | string | null | undefined>();
|
|
187
|
-
let prev_filterText = $state();
|
|
196
|
+
let prev_filterText: string | undefined = $state();
|
|
188
197
|
let prev_multiple = $state();
|
|
189
|
-
let isScrollingTimer = $state<ReturnType<typeof setTimeout>>();
|
|
190
198
|
let isScrolling = $state(false);
|
|
191
199
|
let prefloat = $state(true);
|
|
192
200
|
let hasValue = $state(false);
|
|
@@ -233,6 +241,78 @@
|
|
|
233
241
|
autoUpdate: false,
|
|
234
242
|
});
|
|
235
243
|
|
|
244
|
+
// Initialize composables
|
|
245
|
+
const valueManager = useValue({
|
|
246
|
+
getState: () => ({
|
|
247
|
+
value,
|
|
248
|
+
prevValue: prev_value,
|
|
249
|
+
items,
|
|
250
|
+
multiple,
|
|
251
|
+
itemId,
|
|
252
|
+
label,
|
|
253
|
+
hasValue,
|
|
254
|
+
normalizedValue,
|
|
255
|
+
useJustValue,
|
|
256
|
+
justValue,
|
|
257
|
+
clearState,
|
|
258
|
+
closeListOnChange,
|
|
259
|
+
}),
|
|
260
|
+
setValue: (v) => value = v,
|
|
261
|
+
setJustValue: (v) => justValue = v,
|
|
262
|
+
setPrevValue: (v) => prev_value = v,
|
|
263
|
+
setClearState: (v) => clearState = v,
|
|
264
|
+
setActiveValue: (v) => activeValue = v,
|
|
265
|
+
setFilterText: (v) => filterText = v,
|
|
266
|
+
closeList,
|
|
267
|
+
oninput: (v) => oninput?.(v as SelectValue),
|
|
268
|
+
onchange: (v) => onchange?.(v as SelectValue),
|
|
269
|
+
onclear: (v) => onclear(v as SelectValue),
|
|
270
|
+
onselect: (s) => onselect?.(s),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const hoverManager = useHover({
|
|
274
|
+
getState: () => ({
|
|
275
|
+
listOpen,
|
|
276
|
+
filteredItems,
|
|
277
|
+
hoverItemIndex,
|
|
278
|
+
multiple,
|
|
279
|
+
value,
|
|
280
|
+
isScrolling,
|
|
281
|
+
groupBy,
|
|
282
|
+
itemId,
|
|
283
|
+
}),
|
|
284
|
+
setHoverItemIndex: (v) => hoverItemIndex = v,
|
|
285
|
+
setIsScrolling: (v) => isScrolling = v,
|
|
286
|
+
onhoveritem: (i) => onhoveritem?.(i),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const loadOptionsManager = useLoadOptions({
|
|
290
|
+
getState: () => ({
|
|
291
|
+
filterText,
|
|
292
|
+
prevFilterText: prev_filterText,
|
|
293
|
+
loadOptionsDeps,
|
|
294
|
+
loadOptions,
|
|
295
|
+
disabled,
|
|
296
|
+
multiple,
|
|
297
|
+
value,
|
|
298
|
+
items,
|
|
299
|
+
itemId,
|
|
300
|
+
useJustValue,
|
|
301
|
+
justValue,
|
|
302
|
+
listOpen,
|
|
303
|
+
debounceWait,
|
|
304
|
+
}),
|
|
305
|
+
setItems: (v) => items = v,
|
|
306
|
+
setValue: (v) => value = v,
|
|
307
|
+
setJustValue: (v) => justValue = v,
|
|
308
|
+
setLoading: (v) => loading = v,
|
|
309
|
+
setListOpen: (v) => listOpen = v,
|
|
310
|
+
debounce,
|
|
311
|
+
convertStringItemsToObjects: valueManager.convertStringItemsToObjects,
|
|
312
|
+
onloaded: (opts) => onloaded(opts),
|
|
313
|
+
onerror: (err) => onerror(err),
|
|
314
|
+
});
|
|
315
|
+
|
|
236
316
|
const keyboardNav = useKeyboardNavigation({
|
|
237
317
|
getState: () => ({
|
|
238
318
|
listOpen,
|
|
@@ -249,9 +329,9 @@
|
|
|
249
329
|
setHoverItemIndex: (v) => hoverItemIndex = v,
|
|
250
330
|
setActiveValue: (v) => activeValue = v,
|
|
251
331
|
closeList,
|
|
252
|
-
setHoverIndex,
|
|
332
|
+
setHoverIndex: hoverManager.setHoverIndex,
|
|
253
333
|
handleSelect,
|
|
254
|
-
handleMultiItemClear,
|
|
334
|
+
handleMultiItemClear: valueManager.handleMultiItemClear,
|
|
255
335
|
});
|
|
256
336
|
const handleKeyDown = keyboardNav.handleKeyDown;
|
|
257
337
|
|
|
@@ -261,6 +341,8 @@
|
|
|
261
341
|
if (listOpen) focused = true;
|
|
262
342
|
if (focused && input) input.focus();
|
|
263
343
|
});
|
|
344
|
+
|
|
345
|
+
// Filter items
|
|
264
346
|
$effect.pre(() => {
|
|
265
347
|
filterText;
|
|
266
348
|
value;
|
|
@@ -268,7 +350,7 @@
|
|
|
268
350
|
untrack(
|
|
269
351
|
() =>
|
|
270
352
|
(filteredItems = filter({
|
|
271
|
-
loadOptions: undefined,
|
|
353
|
+
loadOptions: undefined,
|
|
272
354
|
filterText,
|
|
273
355
|
items,
|
|
274
356
|
multiple,
|
|
@@ -278,57 +360,82 @@
|
|
|
278
360
|
label,
|
|
279
361
|
filterSelectedItems,
|
|
280
362
|
itemFilter,
|
|
281
|
-
convertStringItemsToObjects,
|
|
363
|
+
convertStringItemsToObjects: valueManager.convertStringItemsToObjects,
|
|
282
364
|
filterGroupedItems,
|
|
283
365
|
})),
|
|
284
366
|
);
|
|
285
367
|
});
|
|
368
|
+
|
|
369
|
+
// Set value on hasValue change
|
|
286
370
|
$effect(() => {
|
|
287
371
|
hasValue;
|
|
288
372
|
untrack(() => {
|
|
289
|
-
if (items) setValue();
|
|
373
|
+
if (items) valueManager.setValue();
|
|
290
374
|
});
|
|
291
375
|
});
|
|
376
|
+
|
|
377
|
+
// Setup multi when multiple changes
|
|
292
378
|
$effect(() => {
|
|
293
|
-
// Used when multiple is dynamically set
|
|
294
379
|
if (multiple) {
|
|
295
|
-
untrack(() => setupMulti());
|
|
380
|
+
untrack(() => valueManager.setupMulti());
|
|
296
381
|
}
|
|
297
382
|
});
|
|
383
|
+
|
|
384
|
+
// Clear value when switching from multiple to single
|
|
298
385
|
$effect(() => {
|
|
299
|
-
// Check BEFORE updating prev_multiple
|
|
300
386
|
if (prev_multiple && !multiple && value) {
|
|
301
387
|
value = null;
|
|
302
388
|
}
|
|
303
|
-
// Update prev_multiple AFTER the check
|
|
304
389
|
prev_multiple = multiple;
|
|
305
390
|
});
|
|
391
|
+
|
|
392
|
+
// Check for duplicates
|
|
306
393
|
$effect(() => {
|
|
307
|
-
if (multiple && value && value.length > 1)
|
|
394
|
+
if (multiple && value && value.length > 1) {
|
|
395
|
+
untrack(() => valueManager.checkValueForDuplicates());
|
|
396
|
+
}
|
|
308
397
|
});
|
|
398
|
+
|
|
399
|
+
// Dispatch selected item
|
|
309
400
|
$effect(() => {
|
|
310
|
-
if (value)
|
|
401
|
+
if (value) {
|
|
402
|
+
untrack(() => valueManager.dispatchSelectedItem());
|
|
403
|
+
}
|
|
311
404
|
});
|
|
405
|
+
|
|
406
|
+
// Value cleared notification
|
|
312
407
|
$effect(() => {
|
|
313
408
|
if (prev_value && !value) {
|
|
314
|
-
oninput?.(value || []);
|
|
409
|
+
oninput?.((value || []) as SelectValue);
|
|
315
410
|
}
|
|
316
411
|
});
|
|
412
|
+
|
|
413
|
+
// Close list on unfocus
|
|
317
414
|
$effect(() => {
|
|
318
415
|
if (!focused && input) closeList();
|
|
319
416
|
});
|
|
417
|
+
|
|
418
|
+
// Setup filter text
|
|
320
419
|
$effect(() => {
|
|
321
420
|
filterText;
|
|
322
421
|
untrack(() => {
|
|
323
422
|
if (filterText !== prev_filterText) setupFilterText();
|
|
324
423
|
});
|
|
325
424
|
});
|
|
425
|
+
|
|
426
|
+
// Set value index as hover when list opens
|
|
326
427
|
$effect(() => {
|
|
327
|
-
if (!multiple && listOpen && value && filteredItems)
|
|
428
|
+
if (!multiple && listOpen && value && filteredItems) {
|
|
429
|
+
untrack(() => hoverManager.setValueIndexAsHoverIndex());
|
|
430
|
+
}
|
|
328
431
|
});
|
|
432
|
+
|
|
433
|
+
// Fire onhoveritem
|
|
329
434
|
$effect(() => {
|
|
330
435
|
onhoveritem?.(hoverItemIndex);
|
|
331
436
|
});
|
|
437
|
+
|
|
438
|
+
// Compute hasValue
|
|
332
439
|
$effect(() => {
|
|
333
440
|
multiple;
|
|
334
441
|
value;
|
|
@@ -338,15 +445,18 @@
|
|
|
338
445
|
: !!value;
|
|
339
446
|
});
|
|
340
447
|
});
|
|
448
|
+
|
|
449
|
+
// Compute justValue
|
|
341
450
|
$effect(() => {
|
|
342
451
|
multiple;
|
|
343
452
|
itemId;
|
|
344
453
|
value;
|
|
345
454
|
untrack(() => {
|
|
346
|
-
justValue = computeJustValue();
|
|
455
|
+
justValue = valueManager.computeJustValue();
|
|
347
456
|
});
|
|
348
457
|
});
|
|
349
458
|
|
|
459
|
+
// Check hover selectable
|
|
350
460
|
$effect(() => {
|
|
351
461
|
filteredItems;
|
|
352
462
|
value;
|
|
@@ -355,21 +465,25 @@
|
|
|
355
465
|
untrack(() => {
|
|
356
466
|
if (listOpen && filteredItems.length > 0) {
|
|
357
467
|
if (!isItemSelectableCheck(filteredItems[hoverItemIndex])) {
|
|
358
|
-
checkHoverSelectable();
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
else if (groupBy && hoverItemIndex === 0) {
|
|
362
|
-
checkHoverSelectable();
|
|
468
|
+
hoverManager.checkHoverSelectable();
|
|
469
|
+
} else if (groupBy && hoverItemIndex === 0) {
|
|
470
|
+
hoverManager.checkHoverSelectable();
|
|
363
471
|
}
|
|
364
472
|
}
|
|
365
473
|
});
|
|
366
474
|
});
|
|
475
|
+
|
|
476
|
+
// Fire onfilter
|
|
367
477
|
$effect(() => {
|
|
368
478
|
if (filteredItems && listOpen) onfilter?.(filteredItems);
|
|
369
479
|
});
|
|
480
|
+
|
|
481
|
+
// Floating UI config
|
|
370
482
|
$effect(() => {
|
|
371
483
|
if (container && floatingConfig) floatingUpdate({..._floatingConfig, ...floatingConfig});
|
|
372
484
|
});
|
|
485
|
+
|
|
486
|
+
// List mounted
|
|
373
487
|
$effect(() => {
|
|
374
488
|
listOpen;
|
|
375
489
|
untrack(() => {
|
|
@@ -377,196 +491,53 @@
|
|
|
377
491
|
if (listOpen && container && list) setListWidth();
|
|
378
492
|
});
|
|
379
493
|
});
|
|
494
|
+
|
|
495
|
+
// Focus when list opens
|
|
380
496
|
$effect(() => {
|
|
381
497
|
if (input && listOpen && !focused) handleFocus();
|
|
382
498
|
});
|
|
499
|
+
|
|
500
|
+
// Reset hover on filterText change
|
|
383
501
|
$effect(() => {
|
|
384
502
|
if (filterText) {
|
|
385
503
|
untrack(() => {
|
|
386
|
-
hoverItemIndex = getFirstSelectableIndex();
|
|
504
|
+
hoverItemIndex = hoverManager.getFirstSelectableIndex();
|
|
387
505
|
});
|
|
388
506
|
}
|
|
389
507
|
});
|
|
508
|
+
|
|
509
|
+
// Auto update floating config
|
|
390
510
|
$effect(() => {
|
|
391
511
|
if (container && floatingConfig?.autoUpdate === undefined) {
|
|
392
512
|
_floatingConfig.autoUpdate = true;
|
|
393
513
|
}
|
|
394
514
|
});
|
|
515
|
+
|
|
516
|
+
// Disabled state
|
|
395
517
|
$effect(() => {
|
|
396
518
|
if (disabled) {
|
|
397
519
|
listOpen = false;
|
|
398
520
|
filterText = '';
|
|
399
521
|
}
|
|
400
522
|
});
|
|
523
|
+
|
|
524
|
+
// Load options handler
|
|
401
525
|
$effect(() => {
|
|
402
|
-
// Watch both filterText and loadOptionsDeps for changes
|
|
403
526
|
const currentFilterText = filterText;
|
|
404
|
-
// Properly track loadOptionsDeps by creating a new array (forces reading each value)
|
|
405
527
|
const currentDeps = [...loadOptionsDeps];
|
|
406
528
|
|
|
407
|
-
if (loadOptions
|
|
408
|
-
// Use untrack to prevent infinite loops when setting items/value/loading
|
|
529
|
+
if (loadOptions) {
|
|
409
530
|
untrack(() => {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// Determine if this is a filterText change (needs debounce) or deps change (immediate)
|
|
413
|
-
const isFilterTextChange = currentFilterText !== prev_filterText;
|
|
414
|
-
|
|
415
|
-
const executeLoad = async () => {
|
|
416
|
-
try {
|
|
417
|
-
const result = await loadOptions(currentFilterText);
|
|
418
|
-
|
|
419
|
-
// Check if result is string array and convert to SelectItem objects
|
|
420
|
-
if (result && result.length > 0 && typeof result[0] === 'string') {
|
|
421
|
-
items = convertStringItemsToObjects(result as string[]);
|
|
422
|
-
} else {
|
|
423
|
-
// Force reactivity with a new reference and proper typing
|
|
424
|
-
items = result ? (result.slice() as typeof items) : null;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Clear value if it's not in the new items list
|
|
428
|
-
if (value && items && items.length > 0) {
|
|
429
|
-
const valueExists = multiple
|
|
430
|
-
? Array.isArray(value) && value.every((v: any) =>
|
|
431
|
-
items?.some((item: any) =>
|
|
432
|
-
(typeof item === 'string' ? item : item[itemId]) === (typeof v === 'string' ? v : v[itemId])
|
|
433
|
-
)
|
|
434
|
-
)
|
|
435
|
-
: items.some((item: any) =>
|
|
436
|
-
(typeof item === 'string' ? item : item[itemId]) === (typeof value === 'string' ? value : (value as any)[itemId])
|
|
437
|
-
);
|
|
438
|
-
|
|
439
|
-
if (!valueExists) {
|
|
440
|
-
value = multiple ? [] : undefined;
|
|
441
|
-
// Also clear justValue when using useJustValue mode
|
|
442
|
-
if (useJustValue) {
|
|
443
|
-
justValue = multiple ? [] : '';
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
} else if (value && (!items || items.length === 0)) {
|
|
447
|
-
// Clear value if items is empty
|
|
448
|
-
value = multiple ? [] : undefined;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
loading = false;
|
|
452
|
-
// Ensure items is SelectItem[] for onloaded callback
|
|
453
|
-
onloaded((items || []) as SelectItem[]);
|
|
454
|
-
} catch (err) {
|
|
455
|
-
console.error('loadOptions error:', err);
|
|
456
|
-
// Pass the raw error to match expected behavior
|
|
457
|
-
onerror(err as any);
|
|
458
|
-
items = null;
|
|
459
|
-
loading = false;
|
|
460
|
-
}
|
|
461
|
-
};
|
|
462
|
-
|
|
463
|
-
// Use debounce for filterText changes, immediate for deps changes
|
|
464
|
-
if (isFilterTextChange) {
|
|
465
|
-
debounce(executeLoad, debounceWait);
|
|
466
|
-
} else {
|
|
467
|
-
executeLoad();
|
|
468
|
-
}
|
|
531
|
+
loadOptionsManager.handleLoadOptions(currentFilterText, currentDeps);
|
|
469
532
|
});
|
|
470
533
|
|
|
471
|
-
//
|
|
472
|
-
if (currentFilterText.length > 0 && !listOpen) {
|
|
534
|
+
// Keep outside untrack for proper reactivity
|
|
535
|
+
if (!disabled && currentFilterText.length > 0 && !listOpen) {
|
|
473
536
|
listOpen = true;
|
|
474
537
|
}
|
|
475
|
-
} else if (loadOptions && disabled) {
|
|
476
|
-
// When disabled and using loadOptions, clear the value and items
|
|
477
|
-
untrack(() => {
|
|
478
|
-
if (value || (useJustValue && justValue)) {
|
|
479
|
-
value = multiple ? [] : undefined;
|
|
480
|
-
if (useJustValue) {
|
|
481
|
-
justValue = multiple ? [] : '';
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
items = null;
|
|
485
|
-
});
|
|
486
538
|
}
|
|
487
539
|
});
|
|
488
540
|
|
|
489
|
-
function getFirstSelectableIndex(): number {
|
|
490
|
-
if (!groupBy || filteredItems.length === 0) return 0;
|
|
491
|
-
|
|
492
|
-
if (!isItemSelectableCheck(filteredItems[0])) {
|
|
493
|
-
const firstSelectable = filteredItems.findIndex(isItemSelectableCheck);
|
|
494
|
-
return firstSelectable >= 0 ? firstSelectable : 0;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
return 0;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function findItemByValue(id: any): SelectItem | undefined {
|
|
501
|
-
return (items as SelectItem[])?.find(item => item[itemId] === id);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function itemSelected(selection: SelectItem) {
|
|
505
|
-
if (selection) {
|
|
506
|
-
filterText = '';
|
|
507
|
-
const item = Object.assign({}, selection);
|
|
508
|
-
|
|
509
|
-
if (item.groupHeader && !item.selectable) return;
|
|
510
|
-
setValue();
|
|
511
|
-
updateValueDisplay(items);
|
|
512
|
-
value = multiple ? (value ? value.concat([item]) : [item]) : (value = item);
|
|
513
|
-
|
|
514
|
-
if (closeListOnChange) closeList();
|
|
515
|
-
activeValue = undefined;
|
|
516
|
-
onchange?.(value);
|
|
517
|
-
onselect?.(selection);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function setValue() {
|
|
522
|
-
prev_value = value;
|
|
523
|
-
if (typeof value === 'string') {
|
|
524
|
-
let item = findItemByValue(value);
|
|
525
|
-
value = item || {
|
|
526
|
-
[itemId]: value,
|
|
527
|
-
label: value,
|
|
528
|
-
};
|
|
529
|
-
} else if (multiple && Array.isArray(value) && value.length > 0) {
|
|
530
|
-
value = value.map((val) => {
|
|
531
|
-
if (typeof val === 'string') {
|
|
532
|
-
// Look up each string value in items
|
|
533
|
-
let item = findItemByValue(val);
|
|
534
|
-
return item || { value: val, label: val };
|
|
535
|
-
}
|
|
536
|
-
return val;
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function updateValueDisplay(items?: SelectItem[] | string[] | null): void {
|
|
542
|
-
if (!items || items.length === 0 || items.some((item) => typeof item !== 'object')) return;
|
|
543
|
-
if (!value) return; // Change back to value
|
|
544
|
-
|
|
545
|
-
if (Array.isArray(value)) { // Change back to value
|
|
546
|
-
if (value.some((selection: SelectItem) => !selection || !(selection as Record<string, any>)[itemId])) return;
|
|
547
|
-
value = value.map((selection) => findItem(selection) || selection);
|
|
548
|
-
} else if (typeof value === 'object') {
|
|
549
|
-
if (!(value as Record<string, any>)[itemId]) return;
|
|
550
|
-
value = findItem() || value;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function assignInputAttributes() {
|
|
555
|
-
_inputAttributes = { ...DEFAULT_INPUT_ATTRS, ...inputAttributes };
|
|
556
|
-
if (id) _inputAttributes['id'] = id;
|
|
557
|
-
if (!searchable) _inputAttributes['readonly'] = true;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function convertStringItemsToObjects(_items: string[]): SelectItem[] {
|
|
561
|
-
return _items.map((item, index) => {
|
|
562
|
-
return {
|
|
563
|
-
index,
|
|
564
|
-
value: item,
|
|
565
|
-
label: `${item}`,
|
|
566
|
-
};
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
|
|
570
541
|
function filterGroupedItems(_items: SelectItem[]): SelectItem[] {
|
|
571
542
|
if (!groupBy) return _items;
|
|
572
543
|
|
|
@@ -602,56 +573,8 @@
|
|
|
602
573
|
return sortedGroupedItems;
|
|
603
574
|
}
|
|
604
575
|
|
|
605
|
-
function dispatchSelectedItem() {
|
|
606
|
-
if (multiple) {
|
|
607
|
-
if (hasValueChanged(value, prev_value)) {
|
|
608
|
-
if (checkValueForDuplicates()) {
|
|
609
|
-
oninput?.(value || []);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
if (!prev_value || hasValueChanged((value as SelectItem)[itemId], (prev_value as SelectItem)[itemId])) {
|
|
616
|
-
oninput?.(value);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function setupMulti() {
|
|
621
|
-
if (value) {
|
|
622
|
-
if (Array.isArray(value)) {
|
|
623
|
-
value = [...value];
|
|
624
|
-
} else {
|
|
625
|
-
value = [value];
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function setValueIndexAsHoverIndex() {
|
|
631
|
-
if (!normalizedValue || Array.isArray(normalizedValue)) return;
|
|
632
|
-
|
|
633
|
-
const singleValue: SelectItem = normalizedValue;
|
|
634
|
-
|
|
635
|
-
const valueIndex = filteredItems.findIndex((i: SelectItem) => {
|
|
636
|
-
return (i as Record<string, any>)[itemId] === (singleValue as Record<string, any>)[itemId];
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
checkHoverSelectable(valueIndex, true);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function checkHoverSelectable(startingIndex = 0, ignoreGroup?: boolean) {
|
|
643
|
-
hoverItemIndex = startingIndex < 0 ? 0 : startingIndex;
|
|
644
|
-
if (!ignoreGroup && groupBy && filteredItems[hoverItemIndex] && !filteredItems[hoverItemIndex].selectable) {
|
|
645
|
-
setHoverIndex(1);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
576
|
function setupFilterText() {
|
|
650
|
-
// When loadOptions is defined, the unified $effect handles everything
|
|
651
|
-
// This function now only handles non-loadOptions cases
|
|
652
577
|
if (loadOptions) {
|
|
653
|
-
// loadOptions is handled by the unified $effect that watches filterText and loadOptionsDeps
|
|
654
|
-
// Just ensure the list opens if there's filter text
|
|
655
578
|
if (filterText.length > 0 && !listOpen) {
|
|
656
579
|
listOpen = true;
|
|
657
580
|
}
|
|
@@ -660,7 +583,6 @@
|
|
|
660
583
|
|
|
661
584
|
if (filterText.length === 0) return;
|
|
662
585
|
|
|
663
|
-
// Non-loadOptions case: just open the list
|
|
664
586
|
listOpen = true;
|
|
665
587
|
|
|
666
588
|
if (multiple) {
|
|
@@ -668,86 +590,6 @@
|
|
|
668
590
|
}
|
|
669
591
|
}
|
|
670
592
|
|
|
671
|
-
function computeJustValue(): any {
|
|
672
|
-
const hasJustValue = multiple
|
|
673
|
-
? (Array.isArray(justValue) && justValue.length > 0)
|
|
674
|
-
: (justValue !== '' && justValue != null);
|
|
675
|
-
|
|
676
|
-
if (useJustValue && !value && !clearState && hasJustValue) {
|
|
677
|
-
// Lookup justValue in items and set value
|
|
678
|
-
const typedItems = (items as SelectItem[]) || [];
|
|
679
|
-
if (multiple) {
|
|
680
|
-
value = typedItems.filter((item: SelectItem) =>
|
|
681
|
-
justValue.includes((item as Record<string, any>)[itemId])
|
|
682
|
-
);
|
|
683
|
-
} else {
|
|
684
|
-
value = typedItems.filter((item: SelectItem) =>
|
|
685
|
-
(item as Record<string, any>)[itemId] === justValue
|
|
686
|
-
)[0];
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// Capture clearState before resetting it
|
|
691
|
-
const wasClearing = clearState;
|
|
692
|
-
clearState = false;
|
|
693
|
-
|
|
694
|
-
// Preserve justValue when in useJustValue mode with no value
|
|
695
|
-
// BUT allow clearing when user clicked clear button
|
|
696
|
-
if (useJustValue && !value && !wasClearing) {
|
|
697
|
-
return justValue;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// Handle multiple selection
|
|
701
|
-
if (multiple && Array.isArray(value)) {
|
|
702
|
-
return value.map((item: SelectItem) => item[itemId]);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// Handle single selection
|
|
706
|
-
if (!value || typeof value === 'string' || Array.isArray(value)) {
|
|
707
|
-
return value;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
return value[itemId];
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function checkValueForDuplicates(): boolean {
|
|
714
|
-
if (!Array.isArray(value) || value.length === 0) return true;
|
|
715
|
-
|
|
716
|
-
const seen = new Set();
|
|
717
|
-
const uniqueValues = value.filter((val: SelectItem) => {
|
|
718
|
-
const id = val[itemId];
|
|
719
|
-
if (seen.has(id)) return false;
|
|
720
|
-
seen.add(id);
|
|
721
|
-
return true;
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
const noDuplicates = uniqueValues.length === value.length;
|
|
725
|
-
if (!noDuplicates) value = uniqueValues;
|
|
726
|
-
|
|
727
|
-
return noDuplicates;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function findItem(selection?: SelectItem): SelectItem | undefined {
|
|
731
|
-
let matchTo = selection ? selection[itemId] : (normalizedValue as SelectItem)[itemId];
|
|
732
|
-
return (items as SelectItem[])?.find(item => item[itemId] === matchTo);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
async function handleMultiItemClear(i:number): Promise<void> {
|
|
736
|
-
if (!Array.isArray(value)) return;
|
|
737
|
-
|
|
738
|
-
const itemToRemove = value[i];
|
|
739
|
-
|
|
740
|
-
clearState = true;
|
|
741
|
-
if (value.length === 1) {
|
|
742
|
-
value = undefined;
|
|
743
|
-
} else {
|
|
744
|
-
value = value.filter((item: SelectItem) => {
|
|
745
|
-
return item !== itemToRemove;
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
onclear(itemToRemove);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
593
|
function handleFocus(e?: FocusEvent): void {
|
|
752
594
|
if (focused && input === document?.activeElement) return;
|
|
753
595
|
if (e) {
|
|
@@ -782,13 +624,6 @@
|
|
|
782
624
|
listOpen = false;
|
|
783
625
|
}
|
|
784
626
|
|
|
785
|
-
function handleListScroll() {
|
|
786
|
-
clearTimeout(isScrollingTimer);
|
|
787
|
-
isScrollingTimer = setTimeout(() => {
|
|
788
|
-
isScrolling = false;
|
|
789
|
-
}, 100);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
627
|
function handleClickOutside(event: MouseEvent): void {
|
|
793
628
|
const target = event.target as Node;
|
|
794
629
|
if (!listOpen && !focused && container && !container.contains(target) && !list?.contains(target)) {
|
|
@@ -802,69 +637,18 @@
|
|
|
802
637
|
|
|
803
638
|
function handleSelect(item: SelectItem): void {
|
|
804
639
|
if (!item || item.selectable === false) return;
|
|
805
|
-
itemSelected(item);
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
function handleHover(i: number): void {
|
|
809
|
-
if (isScrolling) return;
|
|
810
|
-
hoverItemIndex = i;
|
|
640
|
+
valueManager.itemSelected(item);
|
|
811
641
|
}
|
|
812
642
|
|
|
813
643
|
function handleItemClick(item: SelectItem, i: number): void {
|
|
814
644
|
if (item?.selectable === false) return;
|
|
815
645
|
if (!multiple && areItemsEqual(normalizedValue, item, itemId)) return closeList();
|
|
816
|
-
if (isItemSelectable(item)) {
|
|
646
|
+
if (hoverManager.isItemSelectable(item)) {
|
|
817
647
|
hoverItemIndex = i;
|
|
818
648
|
handleSelect(item);
|
|
819
649
|
}
|
|
820
650
|
}
|
|
821
651
|
|
|
822
|
-
function setHoverIndex(increment: number) {
|
|
823
|
-
let selectableFilteredItems = filteredItems.filter(
|
|
824
|
-
(item) => !Object.hasOwn(item, 'selectable') || item.selectable === true,
|
|
825
|
-
);
|
|
826
|
-
|
|
827
|
-
if (selectableFilteredItems.length === 0) {
|
|
828
|
-
return (hoverItemIndex = 0);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// Get current item
|
|
832
|
-
const currentItem = filteredItems[hoverItemIndex];
|
|
833
|
-
const isCurrentSelectable = isItemSelectableCheck(currentItem);
|
|
834
|
-
|
|
835
|
-
// Find position in selectable items array
|
|
836
|
-
let currentSelectableIndex;
|
|
837
|
-
|
|
838
|
-
if (isCurrentSelectable) {
|
|
839
|
-
currentSelectableIndex = selectableFilteredItems.findIndex(item => item === currentItem);
|
|
840
|
-
} else {
|
|
841
|
-
// Starting from non-selectable item - treat as before first/after last depending on direction
|
|
842
|
-
currentSelectableIndex = increment > 0 ? -1 : selectableFilteredItems.length;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
// Calculate new position with wrapping
|
|
846
|
-
let newSelectableIndex;
|
|
847
|
-
if (increment > 0) {
|
|
848
|
-
newSelectableIndex = (currentSelectableIndex + 1) % selectableFilteredItems.length;
|
|
849
|
-
} else {
|
|
850
|
-
newSelectableIndex = (currentSelectableIndex - 1 + selectableFilteredItems.length) % selectableFilteredItems.length;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Map back to filteredItems
|
|
854
|
-
const newItem = selectableFilteredItems[newSelectableIndex];
|
|
855
|
-
hoverItemIndex = filteredItems.findIndex(item => item === newItem);
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
function isItemActive(item: SelectItem, val: SelectItem | SelectItem[] | null | undefined, itemId: string): boolean | undefined {
|
|
859
|
-
if (multiple) return;
|
|
860
|
-
const normalized = !val ? null : typeof val === 'string' ? { value: val, label: val } : val;
|
|
861
|
-
return areItemsEqual(normalized, item, itemId);
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
function isItemSelectable(item: SelectItem) {
|
|
865
|
-
return (item.groupHeader && item.selectable) || isItemSelectableCheck(item);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
652
|
function setListWidth():void {
|
|
869
653
|
if (!container || !list) return;
|
|
870
654
|
const { width } = container.getBoundingClientRect();
|
|
@@ -884,6 +668,17 @@
|
|
|
884
668
|
prev_filterText = filterText;
|
|
885
669
|
filterText = target.value;
|
|
886
670
|
}
|
|
671
|
+
|
|
672
|
+
export function reset(): void {
|
|
673
|
+
clearState = true;
|
|
674
|
+
value = undefined;
|
|
675
|
+
filterText = '';
|
|
676
|
+
listOpen = false;
|
|
677
|
+
hoverItemIndex = 0;
|
|
678
|
+
activeValue = undefined;
|
|
679
|
+
justValue = undefined;
|
|
680
|
+
focused = false;
|
|
681
|
+
}
|
|
887
682
|
</script>
|
|
888
683
|
|
|
889
684
|
<svelte:window onclick={handleClickOutside} onkeydown={handleKeyDown} />
|
|
@@ -908,7 +703,8 @@
|
|
|
908
703
|
class="svelte-select-list"
|
|
909
704
|
class:prefloat
|
|
910
705
|
style={listStyle}
|
|
911
|
-
onscroll={handleListScroll}
|
|
706
|
+
onscroll={hoverManager.handleListScroll}
|
|
707
|
+
onscrollend={hoverManager.handleListScrollEnd}
|
|
912
708
|
onpointerup={(ev) => {
|
|
913
709
|
ev.preventDefault();
|
|
914
710
|
ev.stopPropagation();
|
|
@@ -917,7 +713,8 @@
|
|
|
917
713
|
ev.preventDefault();
|
|
918
714
|
ev.stopPropagation();
|
|
919
715
|
}}
|
|
920
|
-
role="
|
|
716
|
+
role="listbox"
|
|
717
|
+
id="listbox-{_id}">
|
|
921
718
|
{#if listPrependSnippet}
|
|
922
719
|
{@render listPrependSnippet()}
|
|
923
720
|
{/if}
|
|
@@ -926,8 +723,8 @@
|
|
|
926
723
|
{:else if filteredItems?.length > 0}
|
|
927
724
|
{#each filteredItems as item, i}
|
|
928
725
|
<div
|
|
929
|
-
onmouseover={() => handleHover(i)}
|
|
930
|
-
onfocus={() => handleHover(i)}
|
|
726
|
+
onmouseover={() => hoverManager.handleHover(i)}
|
|
727
|
+
onfocus={() => hoverManager.handleHover(i)}
|
|
931
728
|
onclick={(ev) => {
|
|
932
729
|
ev.stopPropagation();
|
|
933
730
|
handleItemClick(item, i);
|
|
@@ -938,15 +735,19 @@
|
|
|
938
735
|
}}
|
|
939
736
|
class="list-item"
|
|
940
737
|
tabindex="-1"
|
|
941
|
-
role=
|
|
738
|
+
role={item.groupHeader ? 'presentation' : 'option'}
|
|
739
|
+
id="listbox-{_id}-item-{i}"
|
|
740
|
+
aria-selected={item.groupHeader ? undefined : hoverManager.isItemActive(item, normalizedValue, itemId) || false}>
|
|
942
741
|
<div
|
|
943
742
|
class="item"
|
|
944
743
|
class:list-group-title={item.groupHeader}
|
|
945
|
-
class:active={isItemActive(item, normalizedValue, itemId)}
|
|
744
|
+
class:active={hoverManager.isItemActive(item, normalizedValue, itemId)}
|
|
946
745
|
class:first={i === 0}
|
|
947
746
|
class:hover={hoverItemIndex === i}
|
|
948
747
|
class:group-item={item.groupItem}
|
|
949
|
-
class:not-selectable={item?.selectable === false}
|
|
748
|
+
class:not-selectable={item?.selectable === false}
|
|
749
|
+
role={item.groupHeader ? 'group' : undefined}
|
|
750
|
+
aria-label={item.groupHeader ? item[label] : undefined}>
|
|
950
751
|
{#if itemSnippet}
|
|
951
752
|
{@render itemSnippet(item, i)}
|
|
952
753
|
{:else}
|
|
@@ -995,7 +796,7 @@
|
|
|
995
796
|
class:disabled
|
|
996
797
|
onclick={(ev) => {
|
|
997
798
|
ev.preventDefault();
|
|
998
|
-
return multiFullItemClearable ? handleMultiItemClear(i) : {};
|
|
799
|
+
return multiFullItemClearable ? valueManager.handleMultiItemClear(i) : {};
|
|
999
800
|
}}
|
|
1000
801
|
onkeydown={(ev) => {
|
|
1001
802
|
ev.preventDefault();
|
|
@@ -1016,7 +817,7 @@
|
|
|
1016
817
|
onpointerup={(ev) => {
|
|
1017
818
|
ev.preventDefault();
|
|
1018
819
|
ev.stopPropagation();
|
|
1019
|
-
handleMultiItemClear(i);
|
|
820
|
+
valueManager.handleMultiItemClear(i);
|
|
1020
821
|
}}>
|
|
1021
822
|
{#if multiClearIconSnippet}
|
|
1022
823
|
{@render multiClearIconSnippet()}
|
|
@@ -1030,7 +831,7 @@
|
|
|
1030
831
|
{:else}
|
|
1031
832
|
<div class="selected-item" class:hide-selected-item={hideSelectedItem}>
|
|
1032
833
|
{#if selectionSnippet}
|
|
1033
|
-
{@render selectionSnippet(value)}
|
|
834
|
+
{@render selectionSnippet(value as SelectItem)}
|
|
1034
835
|
{:else}
|
|
1035
836
|
{!Array.isArray(normalizedValue) ? normalizedValue?.[label] : ''}
|
|
1036
837
|
{/if}
|
|
@@ -1084,14 +885,18 @@
|
|
|
1084
885
|
{/if}
|
|
1085
886
|
</div>
|
|
1086
887
|
{#if inputHiddenSnippet}
|
|
1087
|
-
{@render inputHiddenSnippet(value)}
|
|
1088
|
-
{:else}
|
|
1089
|
-
|
|
888
|
+
{@render inputHiddenSnippet(value as SelectValue)}
|
|
889
|
+
{:else if multiple && Array.isArray(value) && value.length > 0}
|
|
890
|
+
{#each value as item}
|
|
891
|
+
<input {name} type="hidden" value={useJustValue ? item[itemId] : JSON.stringify(item)} />
|
|
892
|
+
{/each}
|
|
893
|
+
{:else if !multiple}
|
|
894
|
+
<input {name} type="hidden" value={value ? (useJustValue ? justValue : JSON.stringify(value)) : ''} />
|
|
1090
895
|
{/if}
|
|
1091
896
|
|
|
1092
897
|
{#if required && (!value || value.length === 0)}
|
|
1093
898
|
{#if requiredSnippet}
|
|
1094
|
-
{@render requiredSnippet(value)}
|
|
899
|
+
{@render requiredSnippet(value as SelectValue)}
|
|
1095
900
|
{:else}
|
|
1096
901
|
<select class="required" required tabindex="-1" aria-hidden="true"></select>
|
|
1097
902
|
{/if}
|
|
@@ -1485,4 +1290,4 @@ input:focus {
|
|
|
1485
1290
|
bottom: 0;
|
|
1486
1291
|
right: 0;
|
|
1487
1292
|
}
|
|
1488
|
-
</style>
|
|
1293
|
+
</style>
|