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.
Files changed (2) hide show
  1. package/dist/Select.svelte +65 -112
  2. package/package.json +1 -1
@@ -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
- // Initialize value as array for multiple mode, ensure it's always an array when multiple
51
- // Use derived to avoid timing issues with effects
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 && !Array.isArray(value)) {
54
- return value ? [value] : []
55
- } else if (!multiple && Array.isArray(value)) {
56
- return value.length > 0 ? value[0] : undefined
57
- }
58
- return value
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
- // Check if an item is selected in multi-select mode
83
- function isItemSelected(itemValue: any): boolean {
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
- filter = items.find((item) => item.value === itemValue)?.label || ''
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
- filter = ''
109
+ filterInput = ''
131
110
  detailsOpen = false
132
111
  if (onchange) onchange(value)
133
112
  }
134
113
 
135
- let filter = $state('')
136
- let filterMode = $state<'user' | 'auto'>('user') // Track if filter is user-controlled or auto-synced
114
+ // User's filter input - always editable
115
+ let filterInput = $state('')
137
116
 
138
- // Auto-sync filter for single select when not user-controlled
139
- $effect(() => {
140
- if (!multiple && !detailsOpen && filterMode === 'auto') {
141
- if (selectedItem) {
142
- filter = selectedItem.label
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
- // Display text for the input placeholder/value
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
- $effect(() => {
245
- if (detailsOpen && scrollContainer) {
246
- // Find the index of the selected item in the flat list
247
- let selectedIndex = -1
248
-
249
- if (!multiple && selectedItem) {
250
- // For single select, find the selected item
251
- selectedIndex = flatList.findIndex(
252
- (entry) => entry.type === 'option' && entry.item.value === selectedItem.value,
253
- )
254
- } else if (multiple && selectedItems.length > 0) {
255
- // For multi select, find the first selected item
256
- selectedIndex = flatList.findIndex(
257
- (entry) => entry.type === 'option' && selectedItems.some((selected) => selected.value === entry.item.value),
258
- )
259
- }
260
-
261
- if (selectedIndex >= 0) {
262
- // Calculate scroll position to center the selected item
263
- const targetScrollTop = Math.max(0, selectedIndex * itemHeight - containerHeight / 2 + itemHeight / 2)
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
- // Update both scroll state and actual scroll position
266
- // Set state first so virtual list calculates correctly
267
- scrollTop = targetScrollTop
268
- scrollContainer.scrollTop = targetScrollTop
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
- filter = ''
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={filter}
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
- bind:value={filter}
351
- oninput={handleFilterInput}
302
+ value={filter}
303
+ oninput={(e) => (filterInput = e.currentTarget.value)}
352
304
  onclick={() => {
353
305
  detailsOpen = true
354
306
  }}
355
- placeholder={displayText}
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" bind:this={scrollContainer} onscroll={handleScroll}>
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 = isItemSelected(item.value)}
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simplesvelte",
3
- "version": "2.2.15",
3
+ "version": "2.2.18",
4
4
  "scripts": {
5
5
  "dev": "bun vite dev",
6
6
  "build": "bun vite build && bun run prepack",