simplesvelte 2.3.1 → 2.4.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 +276 -164
- package/dist/Select.svelte.d.ts +7 -2
- package/dist/styles.css +5 -0
- package/package.json +21 -21
package/dist/Select.svelte
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
import Input from './Input.svelte'
|
|
4
|
-
import Label from './Label.svelte'
|
|
5
|
-
type Option = {
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type SelectOption = {
|
|
6
3
|
value: any
|
|
7
4
|
label: any
|
|
8
5
|
group?: string
|
|
9
6
|
[key: string]: any // Allow additional properties
|
|
10
7
|
}
|
|
11
8
|
|
|
9
|
+
export type SelectFetchParams = { filter: string | undefined }
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
import Input from './Input.svelte'
|
|
14
|
+
import Label from './Label.svelte'
|
|
15
|
+
|
|
12
16
|
type Props = {
|
|
13
17
|
value?: string | number | undefined | null | (string | number)[]
|
|
14
|
-
options
|
|
18
|
+
options?: SelectOption[]
|
|
19
|
+
fetchOptions?: (params: SelectFetchParams) => Promise<SelectOption[]>
|
|
20
|
+
debounceMs?: number
|
|
15
21
|
name?: string
|
|
16
22
|
label?: string
|
|
17
23
|
class?: string
|
|
@@ -31,7 +37,9 @@
|
|
|
31
37
|
|
|
32
38
|
let {
|
|
33
39
|
value = $bindable(undefined),
|
|
34
|
-
options:
|
|
40
|
+
options: staticOptions = [],
|
|
41
|
+
fetchOptions,
|
|
42
|
+
debounceMs = 300,
|
|
35
43
|
name,
|
|
36
44
|
label,
|
|
37
45
|
class: className = '',
|
|
@@ -44,7 +52,50 @@
|
|
|
44
52
|
onchange,
|
|
45
53
|
}: Props = $props()
|
|
46
54
|
|
|
47
|
-
|
|
55
|
+
// Async fetch state
|
|
56
|
+
let asyncItems = $state<SelectOption[]>([])
|
|
57
|
+
let isLoading = $state(false)
|
|
58
|
+
let fetchError = $state<string | undefined>(undefined)
|
|
59
|
+
let debounceTimeout: ReturnType<typeof setTimeout> | undefined
|
|
60
|
+
|
|
61
|
+
// Use async items if fetchOptions is provided, otherwise use static options
|
|
62
|
+
let items = $derived(fetchOptions ? asyncItems : staticOptions)
|
|
63
|
+
|
|
64
|
+
// Fetch options when filter changes (debounced)
|
|
65
|
+
async function fetchWithFilter(filterValue: string | undefined) {
|
|
66
|
+
if (!fetchOptions) return
|
|
67
|
+
|
|
68
|
+
// Clear previous timeout
|
|
69
|
+
if (debounceTimeout) clearTimeout(debounceTimeout)
|
|
70
|
+
|
|
71
|
+
debounceTimeout = setTimeout(async () => {
|
|
72
|
+
isLoading = true
|
|
73
|
+
fetchError = undefined
|
|
74
|
+
try {
|
|
75
|
+
asyncItems = await fetchOptions({ filter: filterValue })
|
|
76
|
+
} catch (err) {
|
|
77
|
+
fetchError = err instanceof Error ? err.message : 'Failed to fetch options'
|
|
78
|
+
asyncItems = []
|
|
79
|
+
} finally {
|
|
80
|
+
isLoading = false
|
|
81
|
+
}
|
|
82
|
+
}, debounceMs)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Trigger fetch when dropdown opens or filter input changes (if using async)
|
|
86
|
+
$effect(() => {
|
|
87
|
+
if (fetchOptions && dropdownOpen) {
|
|
88
|
+
// Track filterInput to re-run when it changes
|
|
89
|
+
const currentFilter = filterInput
|
|
90
|
+
fetchWithFilter(currentFilter || undefined)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
let dropdownOpen = $state(false)
|
|
95
|
+
|
|
96
|
+
// Generate unique ID for popover
|
|
97
|
+
const popoverId = `select-popover-${Math.random().toString(36).slice(2, 9)}`
|
|
98
|
+
const anchorName = `--anchor-${popoverId}`
|
|
48
99
|
|
|
49
100
|
// Ensure value is properly typed for single/multiple mode
|
|
50
101
|
// Normalize on access rather than maintaining separate state
|
|
@@ -77,7 +128,7 @@
|
|
|
77
128
|
if (!multiple) {
|
|
78
129
|
// Close dropdown and update filter immediately
|
|
79
130
|
filterInput = items.find((item) => item.value === itemValue)?.label || ''
|
|
80
|
-
|
|
131
|
+
closeDropdown()
|
|
81
132
|
value = itemValue
|
|
82
133
|
if (onchange) onchange(value)
|
|
83
134
|
return
|
|
@@ -107,7 +158,7 @@
|
|
|
107
158
|
function clearAll() {
|
|
108
159
|
value = multiple ? [] : null
|
|
109
160
|
filterInput = ''
|
|
110
|
-
|
|
161
|
+
closeDropdown()
|
|
111
162
|
if (onchange) onchange(value)
|
|
112
163
|
}
|
|
113
164
|
|
|
@@ -117,7 +168,7 @@
|
|
|
117
168
|
// Display value in filter box for single-select when closed
|
|
118
169
|
let filter = $derived.by(() => {
|
|
119
170
|
// In single select mode when dropdown is closed, show selected item
|
|
120
|
-
if (!multiple && !
|
|
171
|
+
if (!multiple && !dropdownOpen && selectedItem) {
|
|
121
172
|
return selectedItem.label
|
|
122
173
|
}
|
|
123
174
|
// Otherwise use the user's filter input
|
|
@@ -125,16 +176,19 @@
|
|
|
125
176
|
})
|
|
126
177
|
|
|
127
178
|
let filteredItems = $derived.by(() => {
|
|
179
|
+
// When using async fetch, server handles filtering - return items as-is
|
|
180
|
+
if (fetchOptions) return items
|
|
181
|
+
// Client-side filtering for static options
|
|
128
182
|
if (filter.length === 0) return items
|
|
129
183
|
return items.filter((item) => item.label.toLowerCase().includes(filter.toLowerCase()))
|
|
130
184
|
})
|
|
131
185
|
|
|
132
186
|
// Flatten filteredItems into a list with group headers and options for virtual scroll
|
|
133
|
-
type FlatListItem = { type: 'header'; group: string } | { type: 'option'; item:
|
|
187
|
+
type FlatListItem = { type: 'header'; group: string } | { type: 'option'; item: SelectOption }
|
|
134
188
|
let flatList = $derived.by(() => {
|
|
135
189
|
const result: FlatListItem[] = []
|
|
136
|
-
const groups: Record<string,
|
|
137
|
-
const ungrouped:
|
|
190
|
+
const groups: Record<string, SelectOption[]> = {}
|
|
191
|
+
const ungrouped: SelectOption[] = []
|
|
138
192
|
|
|
139
193
|
for (const item of filteredItems) {
|
|
140
194
|
if (item.group) {
|
|
@@ -147,8 +201,8 @@
|
|
|
147
201
|
|
|
148
202
|
// In multiple mode, separate selected and unselected items
|
|
149
203
|
if (multiple && Array.isArray(normalizedValue) && normalizedValue.length > 0) {
|
|
150
|
-
const selectedUngrouped:
|
|
151
|
-
const unselectedUngrouped:
|
|
204
|
+
const selectedUngrouped: SelectOption[] = []
|
|
205
|
+
const unselectedUngrouped: SelectOption[] = []
|
|
152
206
|
|
|
153
207
|
for (const item of ungrouped) {
|
|
154
208
|
if (normalizedValue.includes(item.value)) {
|
|
@@ -198,6 +252,7 @@
|
|
|
198
252
|
})
|
|
199
253
|
|
|
200
254
|
let searchEL: HTMLInputElement | undefined = $state(undefined)
|
|
255
|
+
let popoverEl: HTMLElement | undefined = $state(undefined)
|
|
201
256
|
|
|
202
257
|
// Virtual list implementation
|
|
203
258
|
let scrollTop = $state(0)
|
|
@@ -239,7 +294,7 @@
|
|
|
239
294
|
function scrollToSelected(node: HTMLDivElement) {
|
|
240
295
|
const unsubscribe = $effect.root(() => {
|
|
241
296
|
$effect(() => {
|
|
242
|
-
if (
|
|
297
|
+
if (dropdownOpen) {
|
|
243
298
|
// Only scroll on initial open, not on subsequent selection changes
|
|
244
299
|
if (hasScrolledOnOpen) return
|
|
245
300
|
hasScrolledOnOpen = true
|
|
@@ -296,6 +351,26 @@
|
|
|
296
351
|
}
|
|
297
352
|
return undefined
|
|
298
353
|
})
|
|
354
|
+
|
|
355
|
+
function openDropdown() {
|
|
356
|
+
if (!popoverEl) return
|
|
357
|
+
popoverEl.showPopover()
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function closeDropdown() {
|
|
361
|
+
if (!popoverEl) return
|
|
362
|
+
popoverEl.hidePopover()
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Handle popover toggle event to sync state
|
|
366
|
+
function handlePopoverToggle(e: ToggleEvent) {
|
|
367
|
+
dropdownOpen = e.newState === 'open'
|
|
368
|
+
if (dropdownOpen) {
|
|
369
|
+
searchEL?.focus()
|
|
370
|
+
} else {
|
|
371
|
+
hasScrolledOnOpen = false
|
|
372
|
+
}
|
|
373
|
+
}
|
|
299
374
|
</script>
|
|
300
375
|
|
|
301
376
|
<!-- Data inputs for form submission -->
|
|
@@ -309,167 +384,204 @@
|
|
|
309
384
|
|
|
310
385
|
<Label {label} {name} optional={!required} class={className} error={errorText}>
|
|
311
386
|
{#if !disabled}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
387
|
+
<!-- Trigger button with popover target and anchor positioning -->
|
|
388
|
+
<button
|
|
389
|
+
type="button"
|
|
390
|
+
popovertarget={popoverId}
|
|
391
|
+
role="combobox"
|
|
392
|
+
aria-expanded={dropdownOpen}
|
|
393
|
+
aria-haspopup="listbox"
|
|
394
|
+
aria-controls={popoverId}
|
|
395
|
+
class="select relative h-max min-h-10 w-full min-w-12 cursor-pointer bg-none! pr-1 text-left"
|
|
396
|
+
style="anchor-name: {anchorName}"
|
|
397
|
+
title={tooltipText}
|
|
398
|
+
onclick={() => {
|
|
399
|
+
searchEL?.focus()
|
|
400
|
+
// Only clear filter in single-select mode; in multi-select, keep filter for continued searching
|
|
401
|
+
if (!multiple) filterInput = ''
|
|
402
|
+
}}
|
|
403
|
+
onkeydown={(e) => {
|
|
404
|
+
if (e.key === 'Escape') {
|
|
405
|
+
closeDropdown()
|
|
406
|
+
}
|
|
318
407
|
}}>
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
onclick={(e) => {
|
|
408
|
+
{#if multiple}
|
|
409
|
+
<!-- Multi-select display with condensed chips -->
|
|
410
|
+
<div class="flex min-h-8 flex-wrap gap-1 p-1">
|
|
411
|
+
{#if selectedItems.length > 0}
|
|
412
|
+
<!-- Show first selected item -->
|
|
413
|
+
<div class="badge badge-neutral bg-base-200 text-base-content gap-1">
|
|
414
|
+
<span class="max-w-[200px] truncate">{selectedItems[0].label}</span>
|
|
415
|
+
<span
|
|
416
|
+
role="button"
|
|
417
|
+
tabindex="0"
|
|
418
|
+
class="btn btn-xs btn-circle btn-ghost hover:bg-base-300"
|
|
419
|
+
onclick={(e) => {
|
|
420
|
+
e.stopPropagation()
|
|
421
|
+
removeSelectedItem(selectedItems[0].value)
|
|
422
|
+
}}
|
|
423
|
+
onkeydown={(e) => {
|
|
424
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
425
|
+
e.preventDefault()
|
|
338
426
|
e.stopPropagation()
|
|
339
427
|
removeSelectedItem(selectedItems[0].value)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
428
|
+
}
|
|
429
|
+
}}>
|
|
430
|
+
✕
|
|
431
|
+
</span>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
{#if selectedItems.length > 1}
|
|
435
|
+
<!-- Show count indicator for remaining items -->
|
|
436
|
+
<div class="badge badge-ghost text-base-content/70">
|
|
437
|
+
(+{selectedItems.length - 1} more)
|
|
343
438
|
</div>
|
|
344
|
-
|
|
345
|
-
{#if selectedItems.length > 1}
|
|
346
|
-
<!-- Show count indicator for remaining items -->
|
|
347
|
-
<div class="badge badge-ghost text-base-content/70">
|
|
348
|
-
(+{selectedItems.length - 1} more)
|
|
349
|
-
</div>
|
|
350
|
-
{/if}
|
|
351
439
|
{/if}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
type="text"
|
|
355
|
-
class="h-full min-w-[120px] flex-1 outline-0 {detailsOpen ? 'cursor-text' : 'cursor-pointer'}"
|
|
356
|
-
bind:this={searchEL}
|
|
357
|
-
bind:value={filterInput}
|
|
358
|
-
onclick={() => {
|
|
359
|
-
detailsOpen = true
|
|
360
|
-
}}
|
|
361
|
-
placeholder="Search..."
|
|
362
|
-
required={required && (!Array.isArray(normalizedValue) || normalizedValue.length === 0)} />
|
|
363
|
-
</div>
|
|
364
|
-
{:else}
|
|
365
|
-
<!-- Single-select display -->
|
|
440
|
+
{/if}
|
|
441
|
+
<!-- Search input for filtering in multi-select -->
|
|
366
442
|
<input
|
|
367
443
|
type="text"
|
|
368
|
-
class="h-full w-
|
|
444
|
+
class="h-full min-w-[120px] flex-1 outline-0 {dropdownOpen ? 'cursor-text' : 'cursor-pointer'}"
|
|
369
445
|
bind:this={searchEL}
|
|
370
|
-
value={
|
|
371
|
-
oninput={(e) => (filterInput = e.currentTarget.value)}
|
|
372
|
-
onclick={() => {
|
|
373
|
-
detailsOpen = true
|
|
374
|
-
}}
|
|
375
|
-
{placeholder}
|
|
376
|
-
required={required && !normalizedValue} />
|
|
377
|
-
{/if}
|
|
378
|
-
{#if !required && ((multiple && Array.isArray(normalizedValue) && normalizedValue.length > 0) || (!multiple && normalizedValue))}
|
|
379
|
-
<button
|
|
380
|
-
type="button"
|
|
381
|
-
class="btn btn-sm btn-circle btn-ghost absolute top-1 right-1"
|
|
446
|
+
bind:value={filterInput}
|
|
382
447
|
onclick={(e) => {
|
|
448
|
+
e.stopPropagation()
|
|
449
|
+
openDropdown()
|
|
450
|
+
}}
|
|
451
|
+
placeholder="Search..."
|
|
452
|
+
required={required && (!Array.isArray(normalizedValue) || normalizedValue.length === 0)} />
|
|
453
|
+
</div>
|
|
454
|
+
{:else}
|
|
455
|
+
<!-- Single-select display -->
|
|
456
|
+
<input
|
|
457
|
+
type="text"
|
|
458
|
+
class="h-full w-full outline-0 {dropdownOpen ? 'cursor-text' : 'cursor-pointer'}"
|
|
459
|
+
bind:this={searchEL}
|
|
460
|
+
value={filter}
|
|
461
|
+
oninput={(e) => (filterInput = e.currentTarget.value)}
|
|
462
|
+
onclick={(e) => {
|
|
463
|
+
e.stopPropagation()
|
|
464
|
+
openDropdown()
|
|
465
|
+
}}
|
|
466
|
+
{placeholder}
|
|
467
|
+
required={required && !normalizedValue} />
|
|
468
|
+
{/if}
|
|
469
|
+
{#if !required && ((multiple && Array.isArray(normalizedValue) && normalizedValue.length > 0) || (!multiple && normalizedValue))}
|
|
470
|
+
<span
|
|
471
|
+
role="button"
|
|
472
|
+
tabindex="0"
|
|
473
|
+
class="btn btn-sm btn-circle btn-ghost bg-base-100 absolute top-1 right-1"
|
|
474
|
+
onclick={(e) => {
|
|
475
|
+
e.stopPropagation()
|
|
476
|
+
clearAll()
|
|
477
|
+
}}
|
|
478
|
+
onkeydown={(e) => {
|
|
479
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
480
|
+
e.preventDefault()
|
|
383
481
|
e.stopPropagation()
|
|
384
482
|
clearAll()
|
|
483
|
+
}
|
|
484
|
+
}}>
|
|
485
|
+
✕
|
|
486
|
+
</span>
|
|
487
|
+
{/if}
|
|
488
|
+
</button>
|
|
489
|
+
|
|
490
|
+
<!-- Dropdown content using popover API -->
|
|
491
|
+
<ul
|
|
492
|
+
bind:this={popoverEl}
|
|
493
|
+
id={popoverId}
|
|
494
|
+
popover
|
|
495
|
+
role="listbox"
|
|
496
|
+
class="dropdown menu bg-base-100 rounded-box z-50 mt-2 flex flex-col flex-nowrap gap-1 p-2 shadow outline m-0"
|
|
497
|
+
style="position-anchor: {anchorName}; position: absolute; top: anchor(bottom); left: anchor(left); width: anchor-size(width)"
|
|
498
|
+
ontoggle={handlePopoverToggle}>
|
|
499
|
+
{#if multiple && filteredItems.length > 1}
|
|
500
|
+
<!-- Select All / Clear All options for multi-select -->
|
|
501
|
+
<div class="flex gap-2">
|
|
502
|
+
<button
|
|
503
|
+
type="button"
|
|
504
|
+
class="btn btn-sm hover:bg-base-content/10 grow"
|
|
505
|
+
onclick={() => {
|
|
506
|
+
const allValues = filteredItems.map((item) => item.value)
|
|
507
|
+
value = [...allValues]
|
|
508
|
+
if (onchange) onchange(value)
|
|
385
509
|
}}>
|
|
386
|
-
|
|
510
|
+
Select All
|
|
387
511
|
</button>
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
{
|
|
431
|
-
<
|
|
432
|
-
class="
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}}>
|
|
452
|
-
{#if multiple}
|
|
453
|
-
<input
|
|
454
|
-
type="checkbox"
|
|
455
|
-
class="checkbox checkbox-sm text-primary-content! pointer-events-none"
|
|
456
|
-
checked={isSelected}
|
|
457
|
-
readonly />
|
|
458
|
-
{/if}
|
|
459
|
-
<span class="flex-1 overflow-hidden text-left text-nowrap text-ellipsis">{item.label}</span>
|
|
460
|
-
</button>
|
|
461
|
-
</li>
|
|
462
|
-
{/if}
|
|
463
|
-
{/each}
|
|
464
|
-
|
|
465
|
-
<!-- Virtual spacer for items after visible range -->
|
|
466
|
-
{#if visibleItems.endIndex < visibleItems.total}
|
|
467
|
-
<div style="height: {(visibleItems.total - visibleItems.endIndex) * itemHeight}px;"></div>
|
|
512
|
+
<button
|
|
513
|
+
type="button"
|
|
514
|
+
class="btn btn-sm hover:bg-base-content/10 grow"
|
|
515
|
+
onclick={() => {
|
|
516
|
+
value = []
|
|
517
|
+
if (onchange) onchange(value)
|
|
518
|
+
}}>
|
|
519
|
+
Clear All
|
|
520
|
+
</button>
|
|
521
|
+
</div>
|
|
522
|
+
{/if}
|
|
523
|
+
{#if isLoading}
|
|
524
|
+
<li class="m-2 flex items-center justify-center gap-2 text-sm text-gray-500">
|
|
525
|
+
<span class="loading loading-spinner loading-sm"></span>
|
|
526
|
+
Loading...
|
|
527
|
+
</li>
|
|
528
|
+
{:else if fetchError}
|
|
529
|
+
<li class="m-2 text-center text-sm text-error">{fetchError}</li>
|
|
530
|
+
{:else if filteredItems.length === 0}
|
|
531
|
+
<li class="m-2 text-center text-sm text-gray-500">No items found</li>
|
|
532
|
+
{/if}
|
|
533
|
+
|
|
534
|
+
{#if flatList.length > 0}
|
|
535
|
+
<div class="relative max-h-80 overflow-y-auto pr-2" use:scrollToSelected onscroll={handleScroll}>
|
|
536
|
+
<!-- Virtual spacer for items before visible range -->
|
|
537
|
+
{#if visibleItems.startIndex > 0}
|
|
538
|
+
<div style="height: {visibleItems.startIndex * itemHeight}px;"></div>
|
|
539
|
+
{/if}
|
|
540
|
+
|
|
541
|
+
<!-- Render only visible items (headers and options) -->
|
|
542
|
+
{#each visibleItems.items as entry, idx (entry.type === 'header' ? 'header-' + entry.group + '-' + idx : 'option-' + entry.item.value + '-' + idx)}
|
|
543
|
+
{#if entry.type === 'header'}
|
|
544
|
+
<li
|
|
545
|
+
class="bg-base-200 top-0 z-10 flex items-center justify-center px-2 text-lg font-bold text-gray-700"
|
|
546
|
+
style="height: {itemHeight}px;">
|
|
547
|
+
{entry.group}
|
|
548
|
+
</li>
|
|
549
|
+
{:else}
|
|
550
|
+
{@const item = entry.item}
|
|
551
|
+
{@const isSelected = multiple
|
|
552
|
+
? Array.isArray(normalizedValue) && normalizedValue.includes(item.value)
|
|
553
|
+
: item.value === normalizedValue}
|
|
554
|
+
<li style="height: {itemHeight}px;" role="option" aria-selected={isSelected}>
|
|
555
|
+
<button
|
|
556
|
+
class="flex h-full w-full items-center gap-2 {isSelected
|
|
557
|
+
? ' bg-primary text-primary-content hover:bg-primary/70!'
|
|
558
|
+
: ''}"
|
|
559
|
+
type="button"
|
|
560
|
+
onclick={(e) => {
|
|
561
|
+
e.stopPropagation()
|
|
562
|
+
toggleItemSelection(item.value)
|
|
563
|
+
searchEL?.focus()
|
|
564
|
+
}}>
|
|
565
|
+
{#if multiple}
|
|
566
|
+
<input
|
|
567
|
+
type="checkbox"
|
|
568
|
+
class="checkbox checkbox-sm text-primary-content! pointer-events-none"
|
|
569
|
+
checked={isSelected}
|
|
570
|
+
readonly />
|
|
571
|
+
{/if}
|
|
572
|
+
<span class="flex-1 overflow-hidden text-left text-nowrap text-ellipsis">{item.label}</span>
|
|
573
|
+
</button>
|
|
574
|
+
</li>
|
|
468
575
|
{/if}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
576
|
+
{/each}
|
|
577
|
+
|
|
578
|
+
<!-- Virtual spacer for items after visible range -->
|
|
579
|
+
{#if visibleItems.endIndex < visibleItems.total}
|
|
580
|
+
<div style="height: {(visibleItems.total - visibleItems.endIndex) * itemHeight}px;"></div>
|
|
581
|
+
{/if}
|
|
582
|
+
</div>
|
|
583
|
+
{/if}
|
|
584
|
+
</ul>
|
|
473
585
|
{:else}
|
|
474
586
|
<!-- Disabled state -->
|
|
475
587
|
{#if multiple}
|
package/dist/Select.svelte.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
type
|
|
1
|
+
export type SelectOption = {
|
|
2
2
|
value: any;
|
|
3
3
|
label: any;
|
|
4
4
|
group?: string;
|
|
5
5
|
[key: string]: any;
|
|
6
6
|
};
|
|
7
|
+
export type SelectFetchParams = {
|
|
8
|
+
filter: string | undefined;
|
|
9
|
+
};
|
|
7
10
|
type Props = {
|
|
8
11
|
value?: string | number | undefined | null | (string | number)[];
|
|
9
|
-
options
|
|
12
|
+
options?: SelectOption[];
|
|
13
|
+
fetchOptions?: (params: SelectFetchParams) => Promise<SelectOption[]>;
|
|
14
|
+
debounceMs?: number;
|
|
10
15
|
name?: string;
|
|
11
16
|
label?: string;
|
|
12
17
|
class?: string;
|
package/dist/styles.css
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simplesvelte",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "bun vite dev",
|
|
6
6
|
"build": "bun vite build && bun run prepack",
|
|
@@ -36,31 +36,31 @@
|
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@eslint/compat": "^1.4.1",
|
|
39
|
-
"@eslint/js": "^9.39.
|
|
40
|
-
"@sveltejs/adapter-cloudflare": "^7.2.
|
|
41
|
-
"@sveltejs/kit": "^2.
|
|
42
|
-
"@sveltejs/package": "^2.5.
|
|
43
|
-
"@sveltejs/vite-plugin-svelte": "^6.2.
|
|
44
|
-
"@tailwindcss/cli": "^4.1.
|
|
45
|
-
"@tailwindcss/vite": "^4.1.
|
|
46
|
-
"@testing-library/svelte": "^5.
|
|
39
|
+
"@eslint/js": "^9.39.2",
|
|
40
|
+
"@sveltejs/adapter-cloudflare": "^7.2.5",
|
|
41
|
+
"@sveltejs/kit": "^2.50.0",
|
|
42
|
+
"@sveltejs/package": "^2.5.7",
|
|
43
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
44
|
+
"@tailwindcss/cli": "^4.1.18",
|
|
45
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
46
|
+
"@testing-library/svelte": "^5.3.1",
|
|
47
47
|
"@testing-library/user-event": "^14.6.1",
|
|
48
|
-
"daisyui": "^5.
|
|
49
|
-
"eslint": "^9.39.
|
|
48
|
+
"daisyui": "^5.5.14",
|
|
49
|
+
"eslint": "^9.39.2",
|
|
50
50
|
"eslint-config-prettier": "^10.1.8",
|
|
51
|
-
"eslint-plugin-svelte": "^3.
|
|
51
|
+
"eslint-plugin-svelte": "^3.14.0",
|
|
52
52
|
"globals": "^16.5.0",
|
|
53
|
-
"jsdom": "^27.
|
|
54
|
-
"prettier": "^3.
|
|
55
|
-
"prettier-plugin-svelte": "^3.4.
|
|
53
|
+
"jsdom": "^27.4.0",
|
|
54
|
+
"prettier": "^3.8.1",
|
|
55
|
+
"prettier-plugin-svelte": "^3.4.1",
|
|
56
56
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
57
|
-
"publint": "^0.3.
|
|
58
|
-
"svelte": "^5.
|
|
59
|
-
"svelte-check": "^4.3.
|
|
60
|
-
"tailwindcss": "^4.1.
|
|
57
|
+
"publint": "^0.3.17",
|
|
58
|
+
"svelte": "^5.47.1",
|
|
59
|
+
"svelte-check": "^4.3.5",
|
|
60
|
+
"tailwindcss": "^4.1.18",
|
|
61
61
|
"typescript": "^5.9.3",
|
|
62
|
-
"typescript-eslint": "^8.
|
|
63
|
-
"vite": "^7.1
|
|
62
|
+
"typescript-eslint": "^8.53.1",
|
|
63
|
+
"vite": "^7.3.1",
|
|
64
64
|
"vitest": "^3.2.4"
|
|
65
65
|
},
|
|
66
66
|
"keywords": [
|