svelte-select-5 6.2.1 → 6.2.3

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/Select.svelte CHANGED
@@ -45,6 +45,21 @@
45
45
  import ClearIcon from './ClearIcon.svelte';
46
46
  import LoadingIcon from './LoadingIcon.svelte';
47
47
 
48
+ // Performance: Shallow equality comparison (faster than JSON.stringify)
49
+ function shallowEqual(a, b) {
50
+ if (a === b) return true;
51
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;
52
+ const keysA = Object.keys(a);
53
+ if (keysA.length !== Object.keys(b).length) return false;
54
+ return keysA.every(key => a[key] === b[key]);
55
+ }
56
+
57
+ function arrayShallowEqual(a, b) {
58
+ if (a === b) return true;
59
+ if (!a || !b || a.length !== b.length) return false;
60
+ return a.every((item, i) => shallowEqual(item, b[i]));
61
+ }
62
+
48
63
  // Props with $props() rune
49
64
  let {
50
65
  // Bindable props (two-way binding)
@@ -177,6 +192,7 @@
177
192
  let _inputAttributes = $state({});
178
193
  let prevJustValue = $state(undefined);
179
194
  let pendingJustValue = $state(undefined);
195
+ let prevItemsRef = $state(undefined);
180
196
  let loadRequestVersion = 0;
181
197
  let isScrollingTimer;
182
198
 
@@ -253,13 +269,15 @@
253
269
  }
254
270
 
255
271
  function filterGroupedItems(_items) {
272
+ const groupValuesSet = new Set();
256
273
  const groupValues = [];
257
274
  const groups = {};
258
275
 
259
276
  _items.forEach((item) => {
260
277
  const groupValue = groupBy(item);
261
278
 
262
- if (!groupValues.includes(groupValue)) {
279
+ if (!groupValuesSet.has(groupValue)) {
280
+ groupValuesSet.add(groupValue);
263
281
  groupValues.push(groupValue);
264
282
  groups[groupValue] = [];
265
283
 
@@ -288,7 +306,7 @@
288
306
 
289
307
  function dispatchSelectedItem() {
290
308
  if (multiple) {
291
- if (JSON.stringify(value) !== JSON.stringify(prev_value)) {
309
+ if (!arrayShallowEqual(value, prev_value)) {
292
310
  if (checkValueForDuplicates()) {
293
311
  oninput?.(value);
294
312
  }
@@ -296,7 +314,7 @@
296
314
  return;
297
315
  }
298
316
 
299
- if (!prev_value || JSON.stringify(value[itemId]) !== JSON.stringify(prev_value[itemId])) {
317
+ if (!prev_value || value[itemId] !== prev_value[itemId]) {
300
318
  oninput?.(value);
301
319
  }
302
320
  }
@@ -383,22 +401,23 @@
383
401
  }
384
402
 
385
403
  function checkValueForDuplicates() {
386
- let noDuplicates = true;
387
- if (value) {
388
- const ids = [];
389
- const uniqueValues = [];
404
+ if (!value?.length) return true;
390
405
 
391
- value.forEach((val) => {
392
- if (!ids.includes(val[itemId])) {
393
- ids.push(val[itemId]);
394
- uniqueValues.push(val);
395
- } else {
396
- noDuplicates = false;
397
- }
398
- });
406
+ const seen = new Set();
407
+ const uniqueValues = [];
408
+ let noDuplicates = true;
399
409
 
400
- if (!noDuplicates) value = uniqueValues;
410
+ for (const val of value) {
411
+ const id = val[itemId];
412
+ if (seen.has(id)) {
413
+ noDuplicates = false;
414
+ } else {
415
+ seen.add(id);
416
+ uniqueValues.push(val);
417
+ }
401
418
  }
419
+
420
+ if (!noDuplicates) value = uniqueValues;
402
421
  return noDuplicates;
403
422
  }
404
423
 
@@ -419,8 +438,8 @@
419
438
  const found = findItem(selection);
420
439
  // Only update if found item has different properties (not just different reference)
421
440
  if (found && found[itemId] === selection[itemId]) {
422
- // Same itemId - check if other properties differ using JSON comparison
423
- if (JSON.stringify(found) !== JSON.stringify(selection)) {
441
+ // Same itemId - check if other properties differ using shallow comparison
442
+ if (!shallowEqual(found, selection)) {
424
443
  needsUpdate = true;
425
444
  return found;
426
445
  }
@@ -434,7 +453,7 @@
434
453
  const found = findItem();
435
454
  // Only update if found item has different properties
436
455
  if (found && found[itemId] === value[itemId]) {
437
- if (JSON.stringify(found) !== JSON.stringify(value)) {
456
+ if (!shallowEqual(found, value)) {
438
457
  value = found;
439
458
  }
440
459
  }
@@ -657,19 +676,32 @@
657
676
  return (hoverItemIndex = 0);
658
677
  }
659
678
 
660
- if (increment > 0 && hoverItemIndex === filteredItems.length - 1) {
661
- hoverItemIndex = 0;
662
- } else if (increment < 0 && hoverItemIndex === 0) {
663
- hoverItemIndex = filteredItems.length - 1;
664
- } else {
665
- hoverItemIndex = hoverItemIndex + increment;
666
- }
679
+ // Use loop instead of recursion to prevent stack overflow with many non-selectable items
680
+ const maxIterations = filteredItems.length;
681
+ let iterations = 0;
682
+
683
+ while (iterations < maxIterations) {
684
+ if (increment > 0 && hoverItemIndex === filteredItems.length - 1) {
685
+ hoverItemIndex = 0;
686
+ } else if (increment < 0 && hoverItemIndex === 0) {
687
+ hoverItemIndex = filteredItems.length - 1;
688
+ } else {
689
+ hoverItemIndex = hoverItemIndex + increment;
690
+ }
667
691
 
668
- const hover = filteredItems[hoverItemIndex];
692
+ const hover = filteredItems[hoverItemIndex];
669
693
 
670
- if (hover && hover.selectable === false) {
671
- if (increment === 1 || increment === -1) setHoverIndex(increment);
672
- return;
694
+ // Found a selectable item - done
695
+ if (!hover || hover.selectable !== false) {
696
+ return;
697
+ }
698
+
699
+ // Only continue for single-step increments
700
+ if (increment !== 1 && increment !== -1) {
701
+ return;
702
+ }
703
+
704
+ iterations++;
673
705
  }
674
706
  }
675
707
 
@@ -794,8 +826,16 @@
794
826
  if (!multiple && listOpen && value && filteredItems) setValueIndexAsHoverIndex();
795
827
  });
796
828
 
829
+ // Only run updateValueDisplay when items content actually changes (not just reference)
830
+ // This prevents loops when parent components create new array references on each render
797
831
  $effect(() => {
798
- updateValueDisplay(items);
832
+ if (items !== prevItemsRef) {
833
+ // Only update if content differs, not just reference
834
+ if (!arrayShallowEqual(items, prevItemsRef)) {
835
+ updateValueDisplay(items);
836
+ }
837
+ prevItemsRef = items;
838
+ }
799
839
  });
800
840
 
801
841
  $effect(() => {
@@ -815,8 +855,11 @@
815
855
  // Also handles case where justValue is set before items are loaded
816
856
  $effect(() => {
817
857
  const computed = computeJustValue();
818
- const isExternalChange = justValue !== prevJustValue &&
819
- JSON.stringify(justValue) !== JSON.stringify(computed);
858
+ // Compare justValue with computed - handles arrays (multiple) and primitives (single)
859
+ const valuesMatch = Array.isArray(justValue) && Array.isArray(computed)
860
+ ? justValue.length === computed.length && justValue.every((v, i) => v === computed[i])
861
+ : justValue === computed;
862
+ const isExternalChange = justValue !== prevJustValue && !valuesMatch;
820
863
 
821
864
  if (isExternalChange) {
822
865
  if (!items) {
@@ -45,6 +45,21 @@
45
45
  import ClearIcon from './ClearIcon.svelte';
46
46
  import LoadingIcon from './LoadingIcon.svelte';
47
47
 
48
+ // Performance: Shallow equality comparison (faster than JSON.stringify)
49
+ function shallowEqual(a, b) {
50
+ if (a === b) return true;
51
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;
52
+ const keysA = Object.keys(a);
53
+ if (keysA.length !== Object.keys(b).length) return false;
54
+ return keysA.every(key => a[key] === b[key]);
55
+ }
56
+
57
+ function arrayShallowEqual(a, b) {
58
+ if (a === b) return true;
59
+ if (!a || !b || a.length !== b.length) return false;
60
+ return a.every((item, i) => shallowEqual(item, b[i]));
61
+ }
62
+
48
63
  // Props with $props() rune
49
64
  let {
50
65
  // Bindable props (two-way binding)
@@ -177,6 +192,7 @@
177
192
  let _inputAttributes = $state({});
178
193
  let prevJustValue = $state(undefined);
179
194
  let pendingJustValue = $state(undefined);
195
+ let prevItemsRef = $state(undefined);
180
196
  let loadRequestVersion = 0;
181
197
  let isScrollingTimer;
182
198
 
@@ -253,13 +269,15 @@
253
269
  }
254
270
 
255
271
  function filterGroupedItems(_items) {
272
+ const groupValuesSet = new Set();
256
273
  const groupValues = [];
257
274
  const groups = {};
258
275
 
259
276
  _items.forEach((item) => {
260
277
  const groupValue = groupBy(item);
261
278
 
262
- if (!groupValues.includes(groupValue)) {
279
+ if (!groupValuesSet.has(groupValue)) {
280
+ groupValuesSet.add(groupValue);
263
281
  groupValues.push(groupValue);
264
282
  groups[groupValue] = [];
265
283
 
@@ -288,7 +306,7 @@
288
306
 
289
307
  function dispatchSelectedItem() {
290
308
  if (multiple) {
291
- if (JSON.stringify(value) !== JSON.stringify(prev_value)) {
309
+ if (!arrayShallowEqual(value, prev_value)) {
292
310
  if (checkValueForDuplicates()) {
293
311
  oninput?.(value);
294
312
  }
@@ -296,7 +314,7 @@
296
314
  return;
297
315
  }
298
316
 
299
- if (!prev_value || JSON.stringify(value[itemId]) !== JSON.stringify(prev_value[itemId])) {
317
+ if (!prev_value || value[itemId] !== prev_value[itemId]) {
300
318
  oninput?.(value);
301
319
  }
302
320
  }
@@ -383,22 +401,23 @@
383
401
  }
384
402
 
385
403
  function checkValueForDuplicates() {
386
- let noDuplicates = true;
387
- if (value) {
388
- const ids = [];
389
- const uniqueValues = [];
404
+ if (!value?.length) return true;
390
405
 
391
- value.forEach((val) => {
392
- if (!ids.includes(val[itemId])) {
393
- ids.push(val[itemId]);
394
- uniqueValues.push(val);
395
- } else {
396
- noDuplicates = false;
397
- }
398
- });
406
+ const seen = new Set();
407
+ const uniqueValues = [];
408
+ let noDuplicates = true;
399
409
 
400
- if (!noDuplicates) value = uniqueValues;
410
+ for (const val of value) {
411
+ const id = val[itemId];
412
+ if (seen.has(id)) {
413
+ noDuplicates = false;
414
+ } else {
415
+ seen.add(id);
416
+ uniqueValues.push(val);
417
+ }
401
418
  }
419
+
420
+ if (!noDuplicates) value = uniqueValues;
402
421
  return noDuplicates;
403
422
  }
404
423
 
@@ -419,8 +438,8 @@
419
438
  const found = findItem(selection);
420
439
  // Only update if found item has different properties (not just different reference)
421
440
  if (found && found[itemId] === selection[itemId]) {
422
- // Same itemId - check if other properties differ using JSON comparison
423
- if (JSON.stringify(found) !== JSON.stringify(selection)) {
441
+ // Same itemId - check if other properties differ using shallow comparison
442
+ if (!shallowEqual(found, selection)) {
424
443
  needsUpdate = true;
425
444
  return found;
426
445
  }
@@ -434,7 +453,7 @@
434
453
  const found = findItem();
435
454
  // Only update if found item has different properties
436
455
  if (found && found[itemId] === value[itemId]) {
437
- if (JSON.stringify(found) !== JSON.stringify(value)) {
456
+ if (!shallowEqual(found, value)) {
438
457
  value = found;
439
458
  }
440
459
  }
@@ -657,19 +676,32 @@
657
676
  return (hoverItemIndex = 0);
658
677
  }
659
678
 
660
- if (increment > 0 && hoverItemIndex === filteredItems.length - 1) {
661
- hoverItemIndex = 0;
662
- } else if (increment < 0 && hoverItemIndex === 0) {
663
- hoverItemIndex = filteredItems.length - 1;
664
- } else {
665
- hoverItemIndex = hoverItemIndex + increment;
666
- }
679
+ // Use loop instead of recursion to prevent stack overflow with many non-selectable items
680
+ const maxIterations = filteredItems.length;
681
+ let iterations = 0;
682
+
683
+ while (iterations < maxIterations) {
684
+ if (increment > 0 && hoverItemIndex === filteredItems.length - 1) {
685
+ hoverItemIndex = 0;
686
+ } else if (increment < 0 && hoverItemIndex === 0) {
687
+ hoverItemIndex = filteredItems.length - 1;
688
+ } else {
689
+ hoverItemIndex = hoverItemIndex + increment;
690
+ }
667
691
 
668
- const hover = filteredItems[hoverItemIndex];
692
+ const hover = filteredItems[hoverItemIndex];
669
693
 
670
- if (hover && hover.selectable === false) {
671
- if (increment === 1 || increment === -1) setHoverIndex(increment);
672
- return;
694
+ // Found a selectable item - done
695
+ if (!hover || hover.selectable !== false) {
696
+ return;
697
+ }
698
+
699
+ // Only continue for single-step increments
700
+ if (increment !== 1 && increment !== -1) {
701
+ return;
702
+ }
703
+
704
+ iterations++;
673
705
  }
674
706
  }
675
707
 
@@ -794,8 +826,16 @@
794
826
  if (!multiple && listOpen && value && filteredItems) setValueIndexAsHoverIndex();
795
827
  });
796
828
 
829
+ // Only run updateValueDisplay when items content actually changes (not just reference)
830
+ // This prevents loops when parent components create new array references on each render
797
831
  $effect(() => {
798
- updateValueDisplay(items);
832
+ if (items !== prevItemsRef) {
833
+ // Only update if content differs, not just reference
834
+ if (!arrayShallowEqual(items, prevItemsRef)) {
835
+ updateValueDisplay(items);
836
+ }
837
+ prevItemsRef = items;
838
+ }
799
839
  });
800
840
 
801
841
  $effect(() => {
@@ -815,8 +855,11 @@
815
855
  // Also handles case where justValue is set before items are loaded
816
856
  $effect(() => {
817
857
  const computed = computeJustValue();
818
- const isExternalChange = justValue !== prevJustValue &&
819
- JSON.stringify(justValue) !== JSON.stringify(computed);
858
+ // Compare justValue with computed - handles arrays (multiple) and primitives (single)
859
+ const valuesMatch = Array.isArray(justValue) && Array.isArray(computed)
860
+ ? justValue.length === computed.length && justValue.every((v, i) => v === computed[i])
861
+ : justValue === computed;
862
+ const isExternalChange = justValue !== prevJustValue && !valuesMatch;
820
863
 
821
864
  if (isExternalChange) {
822
865
  if (!items) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-select-5",
3
- "version": "6.2.1",
3
+ "version": "6.2.3",
4
4
  "description": "A <Select> component for Svelte 5 apps (fork of svelte-select)",
5
5
  "repository": "https://github.com/Dbone29/svelte-select-5.git",
6
6
  "author": "Robert Balfré <rob.balfre@gmail.com> (https://github.com/rob-balfre)",