svelte-multiselect 7.0.0 → 7.0.2

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.
@@ -1,5 +1,4 @@
1
- <script>import { createEventDispatcher } from 'svelte';
2
- import { get_label, get_value } from './';
1
+ <script>import { createEventDispatcher, tick } from 'svelte';
3
2
  import CircleSpinner from './CircleSpinner.svelte';
4
3
  import { CrossIcon, DisabledIcon, ExpandIcon } from './icons';
5
4
  import Wiggle from './Wiggle.svelte';
@@ -52,6 +51,10 @@ export let selectedValues = [];
52
51
  export let sortSelected = false;
53
52
  export let ulOptionsClass = ``;
54
53
  export let ulSelectedClass = ``;
54
+ // get the label key from an option object or the option itself if it's a string or number
55
+ const get_label = (op) => (op instanceof Object ? op.label : op);
56
+ // fallback on label if option is object and value is undefined
57
+ const get_value = (op) => (op instanceof Object ? op.value ?? op.label : op);
55
58
  // selected and _selected are identical except if maxSelect=1, selected will be the single item (or null)
56
59
  // in _selected which will always be an array for easier component internals. selected then solves
57
60
  // https://github.com/janosh/svelte-multiselect/issues/86
@@ -85,8 +88,8 @@ let add_option_msg_is_active = false; // controls active state of <li>{addOption
85
88
  let window_width;
86
89
  // formValue binds to input.form-control to prevent form submission if required
87
90
  // prop is true and no options are selected
88
- $: formValue = _selectedValues.join(`,`);
89
- $: if (formValue)
91
+ $: form_value = _selectedValues.join(`,`);
92
+ $: if (form_value)
90
93
  invalid = false; // reset error status whenever component state changes
91
94
  // options matching the current search text
92
95
  $: matchingOptions = options.filter((op) => filterFunc(op, searchText) && !_selectedLabels.includes(get_label(op)) // remove already selected options from dropdown list
@@ -128,8 +131,8 @@ function add(label, event) {
128
131
  options = [...options, option];
129
132
  }
130
133
  searchText = ``; // reset search string on selection
131
- if (!option) {
132
- console.error(`MultiSelect: option with label ${label} not found`);
134
+ if ([``, undefined, null].includes(option)) {
135
+ console.error(`MultiSelect: encountered missing option with label ${label} (or option is poorly labeled)`);
133
136
  return;
134
137
  }
135
138
  if (maxSelect === 1) {
@@ -204,7 +207,7 @@ async function handle_keydown(event) {
204
207
  event.preventDefault(); // prevent enter key from triggering form submission
205
208
  if (activeOption) {
206
209
  const label = get_label(activeOption);
207
- selectedLabels.includes(label) ? remove(label) : add(label, event);
210
+ selectedLabels?.includes(label) ? remove(label) : add(label, event);
208
211
  searchText = ``;
209
212
  }
210
213
  else if (allowUserOptions && searchText.length > 0) {
@@ -241,14 +244,13 @@ async function handle_keydown(event) {
241
244
  activeIndex = matchingOptions.length - 1;
242
245
  if (autoScroll) {
243
246
  // TODO This ugly timeout hack is needed to properly scroll element into view when wrapping
244
- // around start/end of option list. Find a better solution than waiting 10 ms to.
245
- setTimeout(() => {
246
- const li = document.querySelector(`ul.options > li.active`);
247
- if (li) {
248
- li.parentNode?.scrollIntoView({ block: `center` });
249
- li.scrollIntoViewIfNeeded();
250
- }
251
- }, 10);
247
+ // around start/end of option list. Find a better solution than waiting 10 ms.
248
+ await tick();
249
+ const li = document.querySelector(`ul.options > li.active`);
250
+ if (li) {
251
+ li.parentNode?.scrollIntoView?.({ block: `center` });
252
+ li.scrollIntoViewIfNeeded?.();
253
+ }
252
254
  }
253
255
  }
254
256
  // on backspace key: remove last selected option
@@ -295,9 +297,10 @@ function on_click_outside(event) {
295
297
  title={disabled ? disabledInputTitle : null}
296
298
  aria-disabled={disabled ? `true` : null}
297
299
  >
300
+ <!-- formValue binds to input.form-control to prevent form submission if required prop is true and no options are selected -->
298
301
  <input
299
302
  {required}
300
- bind:value={formValue}
303
+ bind:value={form_value}
301
304
  tabindex="-1"
302
305
  aria-hidden="true"
303
306
  aria-label="ignore this, used only to prevent form submission if select is required but empty"
@@ -456,8 +459,146 @@ function on_click_outside(event) {
456
459
  {/if}
457
460
  </div>
458
461
 
459
- <style>
460
- :where(div.multiselect) {
462
+ <style>:where(div.multiselect) {
463
+ position: relative;
464
+ align-items: center;
465
+ display: flex;
466
+ cursor: text;
467
+ border: var(--sms-border, 1pt solid lightgray);
468
+ border-radius: var(--sms-border-radius, 3pt);
469
+ background: var(--sms-bg);
470
+ max-width: var(--sms-max-width);
471
+ padding: var(--sms-padding, 0 3pt);
472
+ color: var(--sms-text-color);
473
+ font-size: var(--sms-font-size, inherit);
474
+ min-height: var(--sms-min-height, 19pt);
475
+ margin: var(--sms-margin);
476
+ }
477
+ :where(div.multiselect).open {
478
+ /* increase z-index when open to ensure the dropdown of one <MultiSelect />
479
+ displays above that of another slightly below it on the page */
480
+ z-index: var(--sms-open-z-index, 4);
481
+ }
482
+ :where(div.multiselect):focus-within {
483
+ border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
484
+ }
485
+ :where(div.multiselect).disabled {
486
+ background: var(--sms-disabled-bg, lightgray);
487
+ cursor: not-allowed;
488
+ }
489
+ :where(div.multiselect) > ul.selected {
490
+ display: flex;
491
+ flex: 1;
492
+ padding: 0;
493
+ margin: 0;
494
+ flex-wrap: wrap;
495
+ }
496
+ :where(div.multiselect) > ul.selected > li {
497
+ align-items: center;
498
+ border-radius: 3pt;
499
+ display: flex;
500
+ margin: 2pt;
501
+ line-height: normal;
502
+ transition: 0.3s;
503
+ white-space: nowrap;
504
+ background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15));
505
+ padding: var(--sms-selected-li-padding, 1pt 5pt);
506
+ color: var(--sms-selected-text-color, var(--sms-text-color));
507
+ }
508
+ :where(div.multiselect) button {
509
+ border-radius: 50%;
510
+ display: flex;
511
+ transition: 0.2s;
512
+ color: inherit;
513
+ background: transparent;
514
+ border: none;
515
+ cursor: pointer;
516
+ outline: none;
517
+ padding: 0;
518
+ margin: 0 0 0 3pt; /* CSS reset */
519
+ }
520
+ :where(div.multiselect) button.remove-all {
521
+ margin: 0 3pt;
522
+ }
523
+ :where(div.multiselect) ul.selected > li button:hover,
524
+ :where(div.multiselect) button.remove-all:hover,
525
+ :where(div.multiselect) button:focus {
526
+ color: var(--sms-button-hover-color, lightskyblue);
527
+ }
528
+ :where(div.multiselect) input {
529
+ margin: auto 0; /* CSS reset */
530
+ padding: 0; /* CSS reset */
531
+ }
532
+ :where(div.multiselect) > ul.selected > li > input {
533
+ border: none;
534
+ outline: none;
535
+ background: none;
536
+ flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
537
+ min-width: 2em;
538
+ /* ensure input uses text color and not --sms-selected-text-color */
539
+ color: var(--sms-text-color);
540
+ font-size: inherit;
541
+ cursor: inherit; /* needed for disabled state */
542
+ border-radius: 0; /* reset ul.selected > li */
543
+ }
544
+ :where(div.multiselect) > ul.selected > li > input::placeholder {
545
+ padding-left: 5pt;
546
+ color: var(--sms-placeholder-color);
547
+ opacity: var(--sms-placeholder-opacity);
548
+ }
549
+ :where(div.multiselect) > input.form-control {
550
+ width: 2em;
551
+ position: absolute;
552
+ background: transparent;
553
+ border: none;
554
+ outline: none;
555
+ z-index: -1;
556
+ opacity: 0;
557
+ pointer-events: none;
558
+ }
559
+ :where(div.multiselect) > ul.options {
560
+ list-style: none;
561
+ padding: 4pt 0;
562
+ top: 100%;
563
+ left: 0;
564
+ width: 100%;
565
+ position: absolute;
566
+ border-radius: 1ex;
567
+ overflow: auto;
568
+ background: var(--sms-options-bg, white);
569
+ max-height: var(--sms-options-max-height, 50vh);
570
+ overscroll-behavior: var(--sms-options-overscroll, none);
571
+ box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
572
+ transition: all 0.2s;
573
+ }
574
+ :where(div.multiselect) > ul.options.hidden {
575
+ visibility: hidden;
576
+ opacity: 0;
577
+ transform: translateY(50px);
578
+ }
579
+ :where(div.multiselect) > ul.options > li {
580
+ padding: 3pt 2ex;
581
+ cursor: pointer;
582
+ scroll-margin: var(--sms-options-scroll-margin, 100px);
583
+ }
584
+ :where(div.multiselect) > ul.options span {
585
+ padding: 3pt 2ex;
586
+ }
587
+ :where(div.multiselect) > ul.options > li.selected {
588
+ background: var(--sms-li-selected-bg);
589
+ color: var(--sms-li-selected-color);
590
+ }
591
+ :where(div.multiselect) > ul.options > li.active {
592
+ background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
593
+ }
594
+ :where(div.multiselect) > ul.options > li.disabled {
595
+ cursor: not-allowed;
596
+ background: var(--sms-li-disabled-bg, #f5f5f6);
597
+ color: var(--sms-li-disabled-text, #b8b8b8);
598
+ }
599
+
600
+ @supports not selector(:where(div.multiselect)) {
601
+ div.multiselect {
461
602
  position: relative;
462
603
  align-items: center;
463
604
  display: flex;
@@ -472,27 +613,26 @@ function on_click_outside(event) {
472
613
  min-height: var(--sms-min-height, 19pt);
473
614
  margin: var(--sms-margin);
474
615
  }
475
- :where(div.multiselect.open) {
616
+ div.multiselect.open {
476
617
  /* increase z-index when open to ensure the dropdown of one <MultiSelect />
477
618
  displays above that of another slightly below it on the page */
478
619
  z-index: var(--sms-open-z-index, 4);
479
620
  }
480
- :where(div.multiselect:focus-within) {
621
+ div.multiselect:focus-within {
481
622
  border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
482
623
  }
483
- :where(div.multiselect.disabled) {
624
+ div.multiselect.disabled {
484
625
  background: var(--sms-disabled-bg, lightgray);
485
626
  cursor: not-allowed;
486
627
  }
487
-
488
- :where(div.multiselect > ul.selected) {
628
+ div.multiselect > ul.selected {
489
629
  display: flex;
490
630
  flex: 1;
491
631
  padding: 0;
492
632
  margin: 0;
493
633
  flex-wrap: wrap;
494
634
  }
495
- :where(div.multiselect > ul.selected > li) {
635
+ div.multiselect > ul.selected > li {
496
636
  align-items: center;
497
637
  border-radius: 3pt;
498
638
  display: flex;
@@ -504,7 +644,7 @@ function on_click_outside(event) {
504
644
  padding: var(--sms-selected-li-padding, 1pt 5pt);
505
645
  color: var(--sms-selected-text-color, var(--sms-text-color));
506
646
  }
507
- :where(div.multiselect button) {
647
+ div.multiselect button {
508
648
  border-radius: 50%;
509
649
  display: flex;
510
650
  transition: 0.2s;
@@ -516,18 +656,19 @@ function on_click_outside(event) {
516
656
  padding: 0;
517
657
  margin: 0 0 0 3pt; /* CSS reset */
518
658
  }
519
- :where(div.multiselect button.remove-all) {
659
+ div.multiselect button.remove-all {
520
660
  margin: 0 3pt;
521
661
  }
522
- :where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
662
+ div.multiselect ul.selected > li button:hover,
663
+ div.multiselect button.remove-all:hover,
664
+ div.multiselect button:focus {
523
665
  color: var(--sms-button-hover-color, lightskyblue);
524
666
  }
525
-
526
- :where(div.multiselect input) {
667
+ div.multiselect input {
527
668
  margin: auto 0; /* CSS reset */
528
669
  padding: 0; /* CSS reset */
529
670
  }
530
- :where(div.multiselect > ul.selected > li > input) {
671
+ div.multiselect > ul.selected > li > input {
531
672
  border: none;
532
673
  outline: none;
533
674
  background: none;
@@ -539,12 +680,12 @@ function on_click_outside(event) {
539
680
  cursor: inherit; /* needed for disabled state */
540
681
  border-radius: 0; /* reset ul.selected > li */
541
682
  }
542
- :where(div.multiselect > ul.selected > li > input)::placeholder {
683
+ div.multiselect > ul.selected > li > input::placeholder {
543
684
  padding-left: 5pt;
544
685
  color: var(--sms-placeholder-color);
545
686
  opacity: var(--sms-placeholder-opacity);
546
687
  }
547
- :where(div.multiselect > input.form-control) {
688
+ div.multiselect > input.form-control {
548
689
  width: 2em;
549
690
  position: absolute;
550
691
  background: transparent;
@@ -554,8 +695,7 @@ function on_click_outside(event) {
554
695
  opacity: 0;
555
696
  pointer-events: none;
556
697
  }
557
-
558
- :where(div.multiselect > ul.options) {
698
+ div.multiselect > ul.options {
559
699
  list-style: none;
560
700
  padding: 4pt 0;
561
701
  top: 100%;
@@ -570,30 +710,29 @@ function on_click_outside(event) {
570
710
  box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
571
711
  transition: all 0.2s;
572
712
  }
573
- :where(div.multiselect > ul.options.hidden) {
713
+ div.multiselect > ul.options.hidden {
574
714
  visibility: hidden;
575
715
  opacity: 0;
576
716
  transform: translateY(50px);
577
717
  }
578
- :where(div.multiselect > ul.options > li) {
718
+ div.multiselect > ul.options > li {
579
719
  padding: 3pt 2ex;
580
720
  cursor: pointer;
581
721
  scroll-margin: var(--sms-options-scroll-margin, 100px);
582
722
  }
583
- /* for noOptionsMsg */
584
- :where(div.multiselect > ul.options span) {
723
+ div.multiselect > ul.options span {
585
724
  padding: 3pt 2ex;
586
725
  }
587
- :where(div.multiselect > ul.options > li.selected) {
726
+ div.multiselect > ul.options > li.selected {
588
727
  background: var(--sms-li-selected-bg);
589
728
  color: var(--sms-li-selected-color);
590
729
  }
591
- :where(div.multiselect > ul.options > li.active) {
730
+ div.multiselect > ul.options > li.active {
592
731
  background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
593
732
  }
594
- :where(div.multiselect > ul.options > li.disabled) {
733
+ div.multiselect > ul.options > li.disabled {
595
734
  cursor: not-allowed;
596
735
  background: var(--sms-li-disabled-bg, #f5f5f6);
597
736
  color: var(--sms-li-disabled-text, #b8b8b8);
598
737
  }
599
- </style>
738
+ }</style>
package/index.d.ts CHANGED
@@ -47,5 +47,3 @@ export declare type MultiSelectEvents = {
47
47
  touchmove: TouchEvent;
48
48
  touchstart: TouchEvent;
49
49
  };
50
- export declare const get_label: (op: Option) => string | number;
51
- export declare const get_value: (op: Option) => {};
package/index.js CHANGED
@@ -1,14 +1,11 @@
1
1
  export { default } from './MultiSelect.svelte';
2
- // get the label key from an option object or the option itself if it's a string or number
3
- export const get_label = (op) => (op instanceof Object ? op.label : op);
4
- // fallback on label if option is object and value is undefined
5
- export const get_value = (op) => op instanceof Object ? op.value ?? op.label : op;
6
2
  // Firefox lacks support for scrollIntoViewIfNeeded, see
7
3
  // https://github.com/janosh/svelte-multiselect/issues/87
8
4
  // this polyfill was copied from
9
5
  // https://github.com/nuxodin/lazyfill/blob/a8e63/polyfills/Element/prototype/scrollIntoViewIfNeeded.js
10
6
  if (typeof Element !== `undefined` &&
11
- !Element.prototype?.scrollIntoViewIfNeeded) {
7
+ !Element.prototype?.scrollIntoViewIfNeeded &&
8
+ typeof IntersectionObserver !== `undefined`) {
12
9
  Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded = true) {
13
10
  const el = this;
14
11
  new IntersectionObserver(function ([entry]) {
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "homepage": "https://svelte-multiselect.netlify.app",
6
6
  "repository": "https://github.com/janosh/svelte-multiselect",
7
7
  "license": "MIT",
8
- "version": "7.0.0",
8
+ "version": "7.0.2",
9
9
  "type": "module",
10
10
  "svelte": "index.js",
11
11
  "main": "index.js",
@@ -27,6 +27,7 @@
27
27
  "prettier-plugin-svelte": "^2.7.0",
28
28
  "rehype-autolink-headings": "^6.1.1",
29
29
  "rehype-slug": "^5.0.1",
30
+ "sass": "^1.55.0",
30
31
  "svelte": "^3.50.1",
31
32
  "svelte-check": "^2.9.0",
32
33
  "svelte-github-corner": "^0.1.0",
package/readme.md CHANGED
@@ -193,7 +193,7 @@ import type { Option } from 'svelte-multiselect'
193
193
  maxSelect: number | null = null
194
194
  ```
195
195
 
196
- Positive integer to limit the number of options users can pick. `null` means no limit.
196
+ Positive integer to limit the number of options users can pick. `null` means no limit. `maxSelect={1}` will change the type of `selected` to be a single `Option` (or `null`) (not a length-1 array). Likewise, the type of `selectedLabels` changes from `(string | number)[]` to `string | number | null` and `selectedValues` from `unknown[]` to `unknown | null`. `maxSelect={1}` will also give `div.multiselect` a class of `single`. I.e. you can target the selector `div.multiselect.single` to give single selects a different appearance from multi selects.
197
197
 
198
198
  1. ```ts
199
199
  maxSelectMsg: ((current: number, max: number) => string) | null = null