svelte-select-5 6.2.0 → 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;
667
682
 
668
- const hover = filteredItems[hoverItemIndex];
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
+ }
669
691
 
670
- if (hover && hover.selectable === false) {
671
- if (increment === 1 || increment === -1) setHoverIndex(increment);
672
- return;
692
+ const hover = filteredItems[hoverItemIndex];
693
+
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
 
@@ -763,24 +795,21 @@
763
795
  if (inputAttributes || !searchable) assignInputAttributes();
764
796
  });
765
797
 
798
+ // Consolidated: Multiple-mode effects
766
799
  $effect(() => {
767
- if (multiple) setupMulti();
768
- });
769
-
770
- $effect(() => {
771
- if (prev_multiple && !multiple) setupSingle();
772
- });
773
-
774
- $effect(() => {
775
- if (multiple && value && value.length > 1) checkValueForDuplicates();
776
- });
777
-
778
- $effect(() => {
779
- if (value) dispatchSelectedItem();
800
+ if (multiple) {
801
+ setupMulti();
802
+ if (value && value.length > 1) checkValueForDuplicates();
803
+ } else if (prev_multiple) {
804
+ setupSingle();
805
+ }
780
806
  });
781
807
 
808
+ // Consolidated: Value change effects
782
809
  $effect(() => {
783
- if (!value && multiple && prev_value) {
810
+ if (value) {
811
+ dispatchSelectedItem();
812
+ } else if (prev_value) {
784
813
  oninput?.(value);
785
814
  }
786
815
  });
@@ -797,12 +826,16 @@
797
826
  if (!multiple && listOpen && value && filteredItems) setValueIndexAsHoverIndex();
798
827
  });
799
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
800
831
  $effect(() => {
801
- dispatchHover(hoverItemIndex);
802
- });
803
-
804
- $effect(() => {
805
- 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
+ }
806
839
  });
807
840
 
808
841
  $effect(() => {
@@ -822,8 +855,11 @@
822
855
  // Also handles case where justValue is set before items are loaded
823
856
  $effect(() => {
824
857
  const computed = computeJustValue();
825
- const isExternalChange = justValue !== prevJustValue &&
826
- 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;
827
863
 
828
864
  if (isExternalChange) {
829
865
  if (!items) {
@@ -852,10 +888,6 @@
852
888
  readonlyId = computeJustValue();
853
889
  });
854
890
 
855
- $effect(() => {
856
- if (!multiple && prev_value && !value) oninput?.(value);
857
- });
858
-
859
891
  $effect(() => {
860
892
  if (listOpen && filteredItems && !multiple && !value) checkHoverSelectable();
861
893
  });
@@ -868,24 +900,19 @@
868
900
  if (container && floatingConfig) floatingUpdate(Object.assign(_floatingConfig, floatingConfig));
869
901
  });
870
902
 
903
+ // Consolidated: List open effects
871
904
  $effect(() => {
872
905
  listMounted(list, listOpen);
906
+ if (listOpen) {
907
+ if (container && list) setListWidth();
908
+ if (input && !focused) handleFocus();
909
+ }
873
910
  });
874
911
 
912
+ // Consolidated: hoverItemIndex effects
875
913
  $effect(() => {
876
- if (listOpen && container && list) setListWidth();
877
- });
878
-
879
- $effect(() => {
880
- if (listOpen && multiple) hoverItemIndex = 0;
881
- });
882
-
883
- $effect(() => {
884
- if (input && listOpen && !focused) handleFocus();
885
- });
886
-
887
- $effect(() => {
888
- if (filterText) hoverItemIndex = 0;
914
+ if (filterText || (listOpen && multiple)) hoverItemIndex = 0;
915
+ dispatchHover(hoverItemIndex);
889
916
  });
890
917
 
891
918
  // Lifecycle
@@ -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;
667
682
 
668
- const hover = filteredItems[hoverItemIndex];
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
+ }
669
691
 
670
- if (hover && hover.selectable === false) {
671
- if (increment === 1 || increment === -1) setHoverIndex(increment);
672
- return;
692
+ const hover = filteredItems[hoverItemIndex];
693
+
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
 
@@ -763,24 +795,21 @@
763
795
  if (inputAttributes || !searchable) assignInputAttributes();
764
796
  });
765
797
 
798
+ // Consolidated: Multiple-mode effects
766
799
  $effect(() => {
767
- if (multiple) setupMulti();
768
- });
769
-
770
- $effect(() => {
771
- if (prev_multiple && !multiple) setupSingle();
772
- });
773
-
774
- $effect(() => {
775
- if (multiple && value && value.length > 1) checkValueForDuplicates();
776
- });
777
-
778
- $effect(() => {
779
- if (value) dispatchSelectedItem();
800
+ if (multiple) {
801
+ setupMulti();
802
+ if (value && value.length > 1) checkValueForDuplicates();
803
+ } else if (prev_multiple) {
804
+ setupSingle();
805
+ }
780
806
  });
781
807
 
808
+ // Consolidated: Value change effects
782
809
  $effect(() => {
783
- if (!value && multiple && prev_value) {
810
+ if (value) {
811
+ dispatchSelectedItem();
812
+ } else if (prev_value) {
784
813
  oninput?.(value);
785
814
  }
786
815
  });
@@ -797,12 +826,16 @@
797
826
  if (!multiple && listOpen && value && filteredItems) setValueIndexAsHoverIndex();
798
827
  });
799
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
800
831
  $effect(() => {
801
- dispatchHover(hoverItemIndex);
802
- });
803
-
804
- $effect(() => {
805
- 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
+ }
806
839
  });
807
840
 
808
841
  $effect(() => {
@@ -822,8 +855,11 @@
822
855
  // Also handles case where justValue is set before items are loaded
823
856
  $effect(() => {
824
857
  const computed = computeJustValue();
825
- const isExternalChange = justValue !== prevJustValue &&
826
- 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;
827
863
 
828
864
  if (isExternalChange) {
829
865
  if (!items) {
@@ -852,10 +888,6 @@
852
888
  readonlyId = computeJustValue();
853
889
  });
854
890
 
855
- $effect(() => {
856
- if (!multiple && prev_value && !value) oninput?.(value);
857
- });
858
-
859
891
  $effect(() => {
860
892
  if (listOpen && filteredItems && !multiple && !value) checkHoverSelectable();
861
893
  });
@@ -868,24 +900,19 @@
868
900
  if (container && floatingConfig) floatingUpdate(Object.assign(_floatingConfig, floatingConfig));
869
901
  });
870
902
 
903
+ // Consolidated: List open effects
871
904
  $effect(() => {
872
905
  listMounted(list, listOpen);
906
+ if (listOpen) {
907
+ if (container && list) setListWidth();
908
+ if (input && !focused) handleFocus();
909
+ }
873
910
  });
874
911
 
912
+ // Consolidated: hoverItemIndex effects
875
913
  $effect(() => {
876
- if (listOpen && container && list) setListWidth();
877
- });
878
-
879
- $effect(() => {
880
- if (listOpen && multiple) hoverItemIndex = 0;
881
- });
882
-
883
- $effect(() => {
884
- if (input && listOpen && !focused) handleFocus();
885
- });
886
-
887
- $effect(() => {
888
- if (filterText) hoverItemIndex = 0;
914
+ if (filterText || (listOpen && multiple)) hoverItemIndex = 0;
915
+ dispatchHover(hoverItemIndex);
889
916
  });
890
917
 
891
918
  // Lifecycle
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-select-5",
3
- "version": "6.2.0",
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)",