simplesvelte 2.2.15 → 2.2.18
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 +65 -112
- package/package.json +1 -1
package/dist/Select.svelte
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { clickOutside } from './utils.js'
|
|
3
3
|
import Input from './Input.svelte'
|
|
4
4
|
import Label from './Label.svelte'
|
|
5
|
-
import { tick } from 'svelte'
|
|
6
5
|
type Option = {
|
|
7
6
|
value: any
|
|
8
7
|
label: any
|
|
@@ -47,21 +46,15 @@
|
|
|
47
46
|
|
|
48
47
|
let detailsOpen = $state(false)
|
|
49
48
|
|
|
50
|
-
//
|
|
51
|
-
//
|
|
49
|
+
// Ensure value is properly typed for single/multiple mode
|
|
50
|
+
// Normalize on access rather than maintaining separate state
|
|
52
51
|
let normalizedValue = $derived.by(() => {
|
|
53
|
-
if (multiple
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
// Sync normalized value back to value prop when it differs
|
|
62
|
-
$effect(() => {
|
|
63
|
-
if (normalizedValue !== value) {
|
|
64
|
-
value = normalizedValue
|
|
52
|
+
if (multiple) {
|
|
53
|
+
// For multiple mode, always work with arrays
|
|
54
|
+
return Array.isArray(value) ? value : value ? [value] : []
|
|
55
|
+
} else {
|
|
56
|
+
// For single mode, extract first value if array
|
|
57
|
+
return Array.isArray(value) ? (value.length > 0 ? value[0] : undefined) : value
|
|
65
58
|
}
|
|
66
59
|
})
|
|
67
60
|
|
|
@@ -79,26 +72,12 @@
|
|
|
79
72
|
return items.filter((item) => currentValue.includes(item.value))
|
|
80
73
|
})
|
|
81
74
|
|
|
82
|
-
//
|
|
83
|
-
function
|
|
84
|
-
if (!multiple) return itemValue === normalizedValue
|
|
85
|
-
const currentValue = normalizedValue
|
|
86
|
-
return Array.isArray(currentValue) && currentValue.includes(itemValue)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Toggle item selection in multi-select mode
|
|
90
|
-
async function toggleItemSelection(itemValue: any) {
|
|
75
|
+
// Remove specific item from multi-select
|
|
76
|
+
function toggleItemSelection(itemValue: any) {
|
|
91
77
|
if (!multiple) {
|
|
92
78
|
// Close dropdown and update filter immediately
|
|
93
|
-
|
|
79
|
+
filterInput = items.find((item) => item.value === itemValue)?.label || ''
|
|
94
80
|
detailsOpen = false
|
|
95
|
-
filterMode = 'auto'
|
|
96
|
-
|
|
97
|
-
// Wait for DOM update so details closes properly
|
|
98
|
-
await tick()
|
|
99
|
-
|
|
100
|
-
// Update value and call onchange - these happen synchronously
|
|
101
|
-
// even though we're in an async function
|
|
102
81
|
value = itemValue
|
|
103
82
|
if (onchange) onchange(value)
|
|
104
83
|
return
|
|
@@ -127,37 +106,24 @@
|
|
|
127
106
|
// Clear all selections
|
|
128
107
|
function clearAll() {
|
|
129
108
|
value = multiple ? [] : null
|
|
130
|
-
|
|
109
|
+
filterInput = ''
|
|
131
110
|
detailsOpen = false
|
|
132
111
|
if (onchange) onchange(value)
|
|
133
112
|
}
|
|
134
113
|
|
|
135
|
-
|
|
136
|
-
let
|
|
114
|
+
// User's filter input - always editable
|
|
115
|
+
let filterInput = $state('')
|
|
137
116
|
|
|
138
|
-
//
|
|
139
|
-
$
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
} else {
|
|
144
|
-
filter = ''
|
|
145
|
-
}
|
|
117
|
+
// Display value in filter box for single-select when closed
|
|
118
|
+
let filter = $derived.by(() => {
|
|
119
|
+
// In single select mode when dropdown is closed, show selected item
|
|
120
|
+
if (!multiple && !detailsOpen && selectedItem) {
|
|
121
|
+
return selectedItem.label
|
|
146
122
|
}
|
|
123
|
+
// Otherwise use the user's filter input
|
|
124
|
+
return filterInput
|
|
147
125
|
})
|
|
148
126
|
|
|
149
|
-
// Reset filter mode when user starts typing
|
|
150
|
-
function handleFilterInput() {
|
|
151
|
-
filterMode = 'user'
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Set filter mode to auto when closing dropdown
|
|
155
|
-
function handleDropdownClose() {
|
|
156
|
-
if (!multiple) {
|
|
157
|
-
filterMode = 'auto'
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
127
|
let filteredItems = $derived.by(() => {
|
|
162
128
|
if (filter.length === 0) return items
|
|
163
129
|
return items.filter((item) => item.label.toLowerCase().includes(filter.toLowerCase()))
|
|
@@ -190,22 +156,10 @@
|
|
|
190
156
|
}
|
|
191
157
|
return result
|
|
192
158
|
})
|
|
193
|
-
|
|
194
|
-
// Using $state.eager to ensure immediate UI feedback when selection changes
|
|
195
|
-
let displayText = $derived.by(() => {
|
|
196
|
-
if (multiple) {
|
|
197
|
-
const count = selectedItems.length
|
|
198
|
-
if (count === 0) return placeholder
|
|
199
|
-
if (count === 1) return selectedItems[0].label
|
|
200
|
-
return `${count} items selected`
|
|
201
|
-
}
|
|
202
|
-
const item = selectedItem
|
|
203
|
-
return item ? item.label : placeholder
|
|
204
|
-
})
|
|
159
|
+
|
|
205
160
|
let searchEL: HTMLInputElement | undefined = $state(undefined)
|
|
206
161
|
|
|
207
162
|
// Virtual list implementation
|
|
208
|
-
let scrollContainer: HTMLDivElement | undefined = $state(undefined)
|
|
209
163
|
let scrollTop = $state(0)
|
|
210
164
|
const itemHeight = 40 // Approximate height of each item in pixels
|
|
211
165
|
const containerHeight = 320 // max-h-80 = 320px
|
|
@@ -241,35 +195,42 @@
|
|
|
241
195
|
}
|
|
242
196
|
|
|
243
197
|
// Scroll to selected item when dropdown opens
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
198
|
+
function scrollToSelected(node: HTMLDivElement) {
|
|
199
|
+
const unsubscribe = $effect.root(() => {
|
|
200
|
+
$effect(() => {
|
|
201
|
+
if (detailsOpen) {
|
|
202
|
+
// Find the index of the selected item in the flat list
|
|
203
|
+
let selectedIndex = -1
|
|
204
|
+
|
|
205
|
+
if (!multiple && selectedItem) {
|
|
206
|
+
// For single select, find the selected item
|
|
207
|
+
selectedIndex = flatList.findIndex(
|
|
208
|
+
(entry) => entry.type === 'option' && entry.item.value === selectedItem.value,
|
|
209
|
+
)
|
|
210
|
+
} else if (multiple && selectedItems.length > 0) {
|
|
211
|
+
// For multi select, find the first selected item
|
|
212
|
+
selectedIndex = flatList.findIndex(
|
|
213
|
+
(entry) =>
|
|
214
|
+
entry.type === 'option' && selectedItems.some((selected) => selected.value === entry.item.value),
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (selectedIndex >= 0) {
|
|
219
|
+
// Calculate scroll position to center the selected item
|
|
220
|
+
const targetScrollTop = Math.max(0, selectedIndex * itemHeight - containerHeight / 2 + itemHeight / 2)
|
|
221
|
+
// Set scroll position directly on the DOM node
|
|
222
|
+
node.scrollTop = targetScrollTop
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
})
|
|
264
227
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
// If no selected item, scrollTop stays at 0 (already set when dropdown was closed)
|
|
228
|
+
return {
|
|
229
|
+
destroy() {
|
|
230
|
+
unsubscribe()
|
|
231
|
+
},
|
|
271
232
|
}
|
|
272
|
-
}
|
|
233
|
+
}
|
|
273
234
|
|
|
274
235
|
const errorText = $derived.by(() => {
|
|
275
236
|
if (error) return error
|
|
@@ -295,21 +256,13 @@
|
|
|
295
256
|
bind:open={detailsOpen}
|
|
296
257
|
use:clickOutside={() => {
|
|
297
258
|
if (!detailsOpen) return
|
|
298
|
-
if (!multiple) {
|
|
299
|
-
filter = selectedItem?.label ?? ''
|
|
300
|
-
} else {
|
|
301
|
-
filter = ''
|
|
302
|
-
}
|
|
303
|
-
console.log('clickOutside')
|
|
304
259
|
detailsOpen = false
|
|
305
|
-
handleDropdownClose()
|
|
306
260
|
}}>
|
|
307
261
|
<summary
|
|
308
262
|
class="select h-max min-h-10 w-full min-w-12 cursor-pointer !bg-none pr-1"
|
|
309
263
|
onclick={() => {
|
|
310
264
|
searchEL?.focus()
|
|
311
|
-
|
|
312
|
-
filterMode = 'user'
|
|
265
|
+
filterInput = ''
|
|
313
266
|
}}>
|
|
314
267
|
{#if multiple}
|
|
315
268
|
<!-- Multi-select display with chips -->
|
|
@@ -333,8 +286,7 @@
|
|
|
333
286
|
type="text"
|
|
334
287
|
class="h-full outline-0 {detailsOpen ? 'cursor-text' : 'cursor-pointer'}"
|
|
335
288
|
bind:this={searchEL}
|
|
336
|
-
bind:value={
|
|
337
|
-
oninput={handleFilterInput}
|
|
289
|
+
bind:value={filterInput}
|
|
338
290
|
onclick={() => {
|
|
339
291
|
detailsOpen = true
|
|
340
292
|
}}
|
|
@@ -347,15 +299,14 @@
|
|
|
347
299
|
type="text"
|
|
348
300
|
class="h-full w-full outline-0 {detailsOpen ? 'cursor-text' : 'cursor-pointer'}"
|
|
349
301
|
bind:this={searchEL}
|
|
350
|
-
|
|
351
|
-
oninput={
|
|
302
|
+
value={filter}
|
|
303
|
+
oninput={(e) => (filterInput = e.currentTarget.value)}
|
|
352
304
|
onclick={() => {
|
|
353
305
|
detailsOpen = true
|
|
354
306
|
}}
|
|
355
|
-
placeholder
|
|
307
|
+
{placeholder}
|
|
356
308
|
required={required && !normalizedValue} />
|
|
357
309
|
{/if}
|
|
358
|
-
|
|
359
310
|
{#if !required && ((multiple && Array.isArray(normalizedValue) && normalizedValue.length > 0) || (!multiple && normalizedValue))}
|
|
360
311
|
<button
|
|
361
312
|
type="button"
|
|
@@ -400,7 +351,7 @@
|
|
|
400
351
|
{/if}
|
|
401
352
|
|
|
402
353
|
{#if flatList.length > 0}
|
|
403
|
-
<div class="relative max-h-80 overflow-y-auto pr-2"
|
|
354
|
+
<div class="relative max-h-80 overflow-y-auto pr-2" use:scrollToSelected onscroll={handleScroll}>
|
|
404
355
|
<!-- Virtual spacer for items before visible range -->
|
|
405
356
|
{#if visibleItems.startIndex > 0}
|
|
406
357
|
<div style="height: {visibleItems.startIndex * itemHeight}px;"></div>
|
|
@@ -416,7 +367,9 @@
|
|
|
416
367
|
</li>
|
|
417
368
|
{:else}
|
|
418
369
|
{@const item = entry.item}
|
|
419
|
-
{@const isSelected =
|
|
370
|
+
{@const isSelected = multiple
|
|
371
|
+
? Array.isArray(normalizedValue) && normalizedValue.includes(item.value)
|
|
372
|
+
: item.value === normalizedValue}
|
|
420
373
|
<li style="height: {itemHeight}px;">
|
|
421
374
|
<button
|
|
422
375
|
class="flex h-full w-full items-center gap-2 {isSelected
|