simplesvelte 2.2.16 → 2.2.19

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.
@@ -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,25 +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
-
96
- // Wait for DOM update so details closes properly
97
- await tick()
98
-
99
- // Update value and call onchange - these happen synchronously
100
- // even though we're in an async function
101
81
  value = itemValue
102
82
  if (onchange) onchange(value)
103
83
  return
@@ -126,22 +106,22 @@
126
106
  // Clear all selections
127
107
  function clearAll() {
128
108
  value = multiple ? [] : null
129
- filter = ''
109
+ filterInput = ''
130
110
  detailsOpen = false
131
111
  if (onchange) onchange(value)
132
112
  }
133
113
 
134
- let filter = $state('')
114
+ // User's filter input - always editable
115
+ let filterInput = $state('')
135
116
 
136
- // Auto-sync filter for single select when dropdown is closed
137
- $effect(() => {
138
- if (!multiple && !detailsOpen) {
139
- if (selectedItem) {
140
- filter = selectedItem.label
141
- } else {
142
- filter = ''
143
- }
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
144
122
  }
123
+ // Otherwise use the user's filter input
124
+ return filterInput
145
125
  })
146
126
 
147
127
  let filteredItems = $derived.by(() => {
@@ -180,7 +160,6 @@
180
160
  let searchEL: HTMLInputElement | undefined = $state(undefined)
181
161
 
182
162
  // Virtual list implementation
183
- let scrollContainer: HTMLDivElement | undefined = $state(undefined)
184
163
  let scrollTop = $state(0)
185
164
  const itemHeight = 40 // Approximate height of each item in pixels
186
165
  const containerHeight = 320 // max-h-80 = 320px
@@ -216,35 +195,42 @@
216
195
  }
217
196
 
218
197
  // Scroll to selected item when dropdown opens
219
- $effect(() => {
220
- if (detailsOpen && scrollContainer) {
221
- // Find the index of the selected item in the flat list
222
- let selectedIndex = -1
223
-
224
- if (!multiple && selectedItem) {
225
- // For single select, find the selected item
226
- selectedIndex = flatList.findIndex(
227
- (entry) => entry.type === 'option' && entry.item.value === selectedItem.value,
228
- )
229
- } else if (multiple && selectedItems.length > 0) {
230
- // For multi select, find the first selected item
231
- selectedIndex = flatList.findIndex(
232
- (entry) => entry.type === 'option' && selectedItems.some((selected) => selected.value === entry.item.value),
233
- )
234
- }
235
-
236
- if (selectedIndex >= 0) {
237
- // Calculate scroll position to center the selected item
238
- 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
+ })
239
227
 
240
- // Update both scroll state and actual scroll position
241
- // Set state first so virtual list calculates correctly
242
- scrollTop = targetScrollTop
243
- scrollContainer.scrollTop = targetScrollTop
244
- }
245
- // If no selected item, scrollTop stays at 0 (already set when dropdown was closed)
228
+ return {
229
+ destroy() {
230
+ unsubscribe()
231
+ },
246
232
  }
247
- })
233
+ }
248
234
 
249
235
  const errorText = $derived.by(() => {
250
236
  if (error) return error
@@ -276,7 +262,7 @@
276
262
  class="select h-max min-h-10 w-full min-w-12 cursor-pointer !bg-none pr-1"
277
263
  onclick={() => {
278
264
  searchEL?.focus()
279
- filter = ''
265
+ filterInput = ''
280
266
  }}>
281
267
  {#if multiple}
282
268
  <!-- Multi-select display with chips -->
@@ -300,7 +286,7 @@
300
286
  type="text"
301
287
  class="h-full outline-0 {detailsOpen ? 'cursor-text' : 'cursor-pointer'}"
302
288
  bind:this={searchEL}
303
- bind:value={filter}
289
+ bind:value={filterInput}
304
290
  onclick={() => {
305
291
  detailsOpen = true
306
292
  }}
@@ -313,7 +299,8 @@
313
299
  type="text"
314
300
  class="h-full w-full outline-0 {detailsOpen ? 'cursor-text' : 'cursor-pointer'}"
315
301
  bind:this={searchEL}
316
- bind:value={filter}
302
+ value={filter}
303
+ oninput={(e) => (filterInput = e.currentTarget.value)}
317
304
  onclick={() => {
318
305
  detailsOpen = true
319
306
  }}
@@ -364,7 +351,7 @@
364
351
  {/if}
365
352
 
366
353
  {#if flatList.length > 0}
367
- <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}>
368
355
  <!-- Virtual spacer for items before visible range -->
369
356
  {#if visibleItems.startIndex > 0}
370
357
  <div style="height: {visibleItems.startIndex * itemHeight}px;"></div>
@@ -380,7 +367,9 @@
380
367
  </li>
381
368
  {:else}
382
369
  {@const item = entry.item}
383
- {@const isSelected = isItemSelected(item.value)}
370
+ {@const isSelected = multiple
371
+ ? Array.isArray(normalizedValue) && normalizedValue.includes(item.value)
372
+ : item.value === normalizedValue}
384
373
  <li style="height: {itemHeight}px;">
385
374
  <button
386
375
  class="flex h-full w-full items-center gap-2 {isSelected
package/dist/utils.js CHANGED
@@ -113,13 +113,13 @@ export function clickOutside(element, callbackFunction) {
113
113
  callbackFunction();
114
114
  }
115
115
  }
116
- document.body.addEventListener('click', onClick);
116
+ document.documentElement.addEventListener('click', onClick);
117
117
  return {
118
118
  update(newCallbackFunction) {
119
119
  callbackFunction = newCallbackFunction;
120
120
  },
121
121
  destroy() {
122
- document.body.removeEventListener('click', onClick);
122
+ document.documentElement.removeEventListener('click', onClick);
123
123
  },
124
124
  };
125
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simplesvelte",
3
- "version": "2.2.16",
3
+ "version": "2.2.19",
4
4
  "scripts": {
5
5
  "dev": "bun vite dev",
6
6
  "build": "bun vite build && bun run prepack",