svelte-multiselect 6.1.0 → 7.0.1

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,4 +1,4 @@
1
- <script>import { createEventDispatcher } from 'svelte';
1
+ <script>import { createEventDispatcher, tick } from 'svelte';
2
2
  import { get_label, get_value } from './';
3
3
  import CircleSpinner from './CircleSpinner.svelte';
4
4
  import { CrossIcon, DisabledIcon, ExpandIcon } from './icons';
@@ -22,6 +22,7 @@ export let focusInputOnSelect = `desktop`;
22
22
  export let id = null;
23
23
  export let input = null;
24
24
  export let inputClass = ``;
25
+ export let inputmode = null;
25
26
  export let invalid = false;
26
27
  export let liActiveOptionClass = ``;
27
28
  export let liOptionClass = ``;
@@ -37,19 +38,30 @@ export let options;
37
38
  export let outerDiv = null;
38
39
  export let outerDivClass = ``;
39
40
  export let parseLabelsAsHtml = false; // should not be combined with allowUserOptions!
41
+ export let pattern = null;
40
42
  export let placeholder = null;
41
43
  export let removeAllTitle = `Remove all`;
42
44
  export let removeBtnTitle = `Remove`;
43
45
  export let required = false;
44
46
  export let searchText = ``;
45
- export let selected = options?.filter((op) => op?.preselected) ?? [];
47
+ export let selected = options
48
+ ?.filter((op) => op?.preselected)
49
+ .slice(0, maxSelect ?? undefined) ?? [];
46
50
  export let selectedLabels = [];
47
51
  export let selectedValues = [];
48
52
  export let sortSelected = false;
49
53
  export let ulOptionsClass = ``;
50
54
  export let ulSelectedClass = ``;
51
- export let inputmode = ``;
52
- export let pattern = ``;
55
+ // selected and _selected are identical except if maxSelect=1, selected will be the single item (or null)
56
+ // in _selected which will always be an array for easier component internals. selected then solves
57
+ // https://github.com/janosh/svelte-multiselect/issues/86
58
+ let _selected = (selected ?? []);
59
+ $: selected = maxSelect === 1 ? _selected[0] ?? null : _selected;
60
+ let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
61
+ $: _selectedLabels = _selected?.map(get_label) ?? [];
62
+ $: selectedLabels = maxSelect === 1 ? _selectedLabels[0] ?? null : _selectedLabels;
63
+ $: _selectedValues = _selected?.map(get_value) ?? [];
64
+ $: selectedValues = maxSelect === 1 ? _selectedValues[0] ?? null : _selectedValues;
53
65
  if (!(options?.length > 0)) {
54
66
  if (allowUserOptions) {
55
67
  options = []; // initializing as array avoids errors when component mounts
@@ -65,22 +77,19 @@ if (parseLabelsAsHtml && allowUserOptions) {
65
77
  if (maxSelect !== null && maxSelect < 1) {
66
78
  console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
67
79
  }
68
- if (!Array.isArray(selected)) {
69
- console.error(`selected prop must be an array, got ${selected}`);
80
+ if (!Array.isArray(_selected)) {
81
+ console.error(`internal variable _selected prop should always be an array, got ${_selected}`);
70
82
  }
71
83
  const dispatch = createEventDispatcher();
72
84
  let add_option_msg_is_active = false; // controls active state of <li>{addOptionMsg}</li>
73
85
  let window_width;
74
- let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
75
- $: selectedLabels = selected.map(get_label);
76
- $: selectedValues = selected.map(get_value);
77
86
  // formValue binds to input.form-control to prevent form submission if required
78
87
  // prop is true and no options are selected
79
- $: formValue = selectedValues.join(`,`);
80
- $: if (formValue)
88
+ $: form_value = _selectedValues.join(`,`);
89
+ $: if (form_value)
81
90
  invalid = false; // reset error status whenever component state changes
82
91
  // options matching the current search text
83
- $: matchingOptions = options.filter((op) => filterFunc(op, searchText) && !selectedLabels.includes(get_label(op)) // remove already selected options from dropdown list
92
+ $: matchingOptions = options.filter((op) => filterFunc(op, searchText) && !_selectedLabels.includes(get_label(op)) // remove already selected options from dropdown list
84
93
  );
85
94
  // raise if matchingOptions[activeIndex] does not yield a value
86
95
  if (activeIndex !== null && !matchingOptions[activeIndex]) {
@@ -90,10 +99,10 @@ if (activeIndex !== null && !matchingOptions[activeIndex]) {
90
99
  $: activeOption = activeIndex !== null ? matchingOptions[activeIndex] : null;
91
100
  // add an option to selected list
92
101
  function add(label, event) {
93
- if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
102
+ if (maxSelect && maxSelect > 1 && _selected.length >= maxSelect)
94
103
  wiggle = true;
95
104
  // to prevent duplicate selection, we could add `&& !selectedLabels.includes(label)`
96
- if (maxSelect === null || maxSelect === 1 || selected.length < maxSelect) {
105
+ if (maxSelect === null || maxSelect === 1 || _selected.length < maxSelect) {
97
106
  // first check if we find option in the options list
98
107
  let option = options.find((op) => get_label(op) === label);
99
108
  if (!option && // this has the side-effect of not allowing to user to add the same
@@ -125,22 +134,22 @@ function add(label, event) {
125
134
  }
126
135
  if (maxSelect === 1) {
127
136
  // for maxselect = 1 we always replace current option with new one
128
- selected = [option];
137
+ _selected = [option];
129
138
  }
130
139
  else {
131
- selected = [...selected, option];
140
+ _selected = [..._selected, option];
132
141
  if (sortSelected === true) {
133
- selected = selected.sort((op1, op2) => {
142
+ _selected = _selected.sort((op1, op2) => {
134
143
  const [label1, label2] = [get_label(op1), get_label(op2)];
135
144
  // coerce to string if labels are numbers
136
145
  return `${label1}`.localeCompare(`${label2}`);
137
146
  });
138
147
  }
139
148
  else if (typeof sortSelected === `function`) {
140
- selected = selected.sort(sortSelected);
149
+ _selected = _selected.sort(sortSelected);
141
150
  }
142
151
  }
143
- if (selected.length === maxSelect)
152
+ if (_selected.length === maxSelect)
144
153
  close_dropdown(event);
145
154
  else if (focusInputOnSelect === true ||
146
155
  (focusInputOnSelect === `desktop` && window_width > breakpoint)) {
@@ -152,10 +161,10 @@ function add(label, event) {
152
161
  }
153
162
  // remove an option from selected list
154
163
  function remove(label) {
155
- if (selected.length === 0)
164
+ if (_selected.length === 0)
156
165
  return;
157
- selected.splice(selectedLabels.lastIndexOf(label), 1);
158
- selected = selected; // Svelte rerender after in-place splice
166
+ _selected.splice(_selectedLabels.lastIndexOf(label), 1);
167
+ _selected = _selected; // Svelte rerender after in-place splice
159
168
  const option = options.find((option) => get_label(option) === label) ??
160
169
  // if option with label could not be found but allowUserOptions is truthy,
161
170
  // assume it was created by user and create correspondidng option object
@@ -195,7 +204,7 @@ async function handle_keydown(event) {
195
204
  event.preventDefault(); // prevent enter key from triggering form submission
196
205
  if (activeOption) {
197
206
  const label = get_label(activeOption);
198
- selectedLabels.includes(label) ? remove(label) : add(label, event);
207
+ selectedLabels?.includes(label) ? remove(label) : add(label, event);
199
208
  searchText = ``;
200
209
  }
201
210
  else if (allowUserOptions && searchText.length > 0) {
@@ -232,28 +241,27 @@ async function handle_keydown(event) {
232
241
  activeIndex = matchingOptions.length - 1;
233
242
  if (autoScroll) {
234
243
  // TODO This ugly timeout hack is needed to properly scroll element into view when wrapping
235
- // around start/end of option list. Find a better solution than waiting 10 ms to.
236
- setTimeout(() => {
237
- const li = document.querySelector(`ul.options > li.active`);
238
- if (li) {
239
- li.parentNode?.scrollIntoView({ block: `center` });
240
- li.scrollIntoViewIfNeeded();
241
- }
242
- }, 10);
244
+ // around start/end of option list. Find a better solution than waiting 10 ms.
245
+ await tick();
246
+ const li = document.querySelector(`ul.options > li.active`);
247
+ if (li) {
248
+ li.parentNode?.scrollIntoView?.({ block: `center` });
249
+ li.scrollIntoViewIfNeeded?.();
250
+ }
243
251
  }
244
252
  }
245
253
  // on backspace key: remove last selected option
246
- else if (event.key === `Backspace` && selectedLabels.length > 0 && !searchText) {
247
- remove(selectedLabels.at(-1));
254
+ else if (event.key === `Backspace` && _selectedLabels.length > 0 && !searchText) {
255
+ remove(_selectedLabels.at(-1));
248
256
  }
249
257
  }
250
258
  function remove_all() {
251
- dispatch(`removeAll`, { options: selected });
252
- dispatch(`change`, { options: selected, type: `removeAll` });
253
- selected = [];
259
+ dispatch(`removeAll`, { options: _selected });
260
+ dispatch(`change`, { options: _selected, type: `removeAll` });
261
+ _selected = [];
254
262
  searchText = ``;
255
263
  }
256
- $: is_selected = (label) => selectedLabels.includes(label);
264
+ $: is_selected = (label) => _selectedLabels.includes(label);
257
265
  const if_enter_or_space = (handler) => (event) => {
258
266
  if ([`Enter`, `Space`].includes(event.code)) {
259
267
  event.preventDefault();
@@ -286,9 +294,10 @@ function on_click_outside(event) {
286
294
  title={disabled ? disabledInputTitle : null}
287
295
  aria-disabled={disabled ? `true` : null}
288
296
  >
297
+ <!-- formValue binds to input.form-control to prevent form submission if required prop is true and no options are selected -->
289
298
  <input
290
299
  {required}
291
- bind:value={formValue}
300
+ bind:value={form_value}
292
301
  tabindex="-1"
293
302
  aria-hidden="true"
294
303
  aria-label="ignore this, used only to prevent form submission if select is required but empty"
@@ -297,7 +306,7 @@ function on_click_outside(event) {
297
306
  />
298
307
  <ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt;" />
299
308
  <ul class="selected {ulSelectedClass}">
300
- {#each selected as option, idx}
309
+ {#each _selected as option, idx}
301
310
  <li class={liSelectedClass} aria-selected="true">
302
311
  <slot name="selected" {option} {idx}>
303
312
  {#if parseLabelsAsHtml}
@@ -325,7 +334,7 @@ function on_click_outside(event) {
325
334
  {autocomplete}
326
335
  bind:value={searchText}
327
336
  on:mouseup|self|stopPropagation={open_dropdown}
328
- on:keydown={handle_keydown}
337
+ on:keydown|stopPropagation={handle_keydown}
329
338
  on:focus
330
339
  on:focus={open_dropdown}
331
340
  {id}
@@ -333,7 +342,7 @@ function on_click_outside(event) {
333
342
  {disabled}
334
343
  {inputmode}
335
344
  {pattern}
336
- placeholder={selectedLabels.length ? `` : placeholder}
345
+ placeholder={_selected.length == 0 ? placeholder : null}
337
346
  aria-invalid={invalid ? `true` : null}
338
347
  on:blur
339
348
  on:change
@@ -360,16 +369,16 @@ function on_click_outside(event) {
360
369
  <slot name="disabled-icon">
361
370
  <DisabledIcon width="15px" />
362
371
  </slot>
363
- {:else if selected.length > 0}
372
+ {:else if _selected.length > 0}
364
373
  {#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
365
374
  <Wiggle bind:wiggle angle={20}>
366
375
  <span style="padding: 0 3pt;">
367
- {maxSelectMsg?.(selected.length, maxSelect) ??
368
- (maxSelect > 1 ? `${selected.length}/${maxSelect}` : ``)}
376
+ {maxSelectMsg?.(_selected.length, maxSelect) ??
377
+ (maxSelect > 1 ? `${_selected.length}/${maxSelect}` : ``)}
369
378
  </span>
370
379
  </Wiggle>
371
380
  {/if}
372
- {#if maxSelect !== 1 && selected.length > 1}
381
+ {#if maxSelect !== 1 && _selected.length > 1}
373
382
  <button
374
383
  type="button"
375
384
  class="remove-all"
@@ -447,8 +456,146 @@ function on_click_outside(event) {
447
456
  {/if}
448
457
  </div>
449
458
 
450
- <style>
451
- :where(div.multiselect) {
459
+ <style>:where(div.multiselect) {
460
+ position: relative;
461
+ align-items: center;
462
+ display: flex;
463
+ cursor: text;
464
+ border: var(--sms-border, 1pt solid lightgray);
465
+ border-radius: var(--sms-border-radius, 3pt);
466
+ background: var(--sms-bg);
467
+ max-width: var(--sms-max-width);
468
+ padding: var(--sms-padding, 0 3pt);
469
+ color: var(--sms-text-color);
470
+ font-size: var(--sms-font-size, inherit);
471
+ min-height: var(--sms-min-height, 19pt);
472
+ margin: var(--sms-margin);
473
+ }
474
+ :where(div.multiselect).open {
475
+ /* increase z-index when open to ensure the dropdown of one <MultiSelect />
476
+ displays above that of another slightly below it on the page */
477
+ z-index: var(--sms-open-z-index, 4);
478
+ }
479
+ :where(div.multiselect):focus-within {
480
+ border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
481
+ }
482
+ :where(div.multiselect).disabled {
483
+ background: var(--sms-disabled-bg, lightgray);
484
+ cursor: not-allowed;
485
+ }
486
+ :where(div.multiselect) > ul.selected {
487
+ display: flex;
488
+ flex: 1;
489
+ padding: 0;
490
+ margin: 0;
491
+ flex-wrap: wrap;
492
+ }
493
+ :where(div.multiselect) > ul.selected > li {
494
+ align-items: center;
495
+ border-radius: 3pt;
496
+ display: flex;
497
+ margin: 2pt;
498
+ line-height: normal;
499
+ transition: 0.3s;
500
+ white-space: nowrap;
501
+ background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15));
502
+ padding: var(--sms-selected-li-padding, 1pt 5pt);
503
+ color: var(--sms-selected-text-color, var(--sms-text-color));
504
+ }
505
+ :where(div.multiselect) button {
506
+ border-radius: 50%;
507
+ display: flex;
508
+ transition: 0.2s;
509
+ color: inherit;
510
+ background: transparent;
511
+ border: none;
512
+ cursor: pointer;
513
+ outline: none;
514
+ padding: 0;
515
+ margin: 0 0 0 3pt; /* CSS reset */
516
+ }
517
+ :where(div.multiselect) button.remove-all {
518
+ margin: 0 3pt;
519
+ }
520
+ :where(div.multiselect) ul.selected > li button:hover,
521
+ :where(div.multiselect) button.remove-all:hover,
522
+ :where(div.multiselect) button:focus {
523
+ color: var(--sms-button-hover-color, lightskyblue);
524
+ }
525
+ :where(div.multiselect) input {
526
+ margin: auto 0; /* CSS reset */
527
+ padding: 0; /* CSS reset */
528
+ }
529
+ :where(div.multiselect) > ul.selected > li > input {
530
+ border: none;
531
+ outline: none;
532
+ background: none;
533
+ flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
534
+ min-width: 2em;
535
+ /* ensure input uses text color and not --sms-selected-text-color */
536
+ color: var(--sms-text-color);
537
+ font-size: inherit;
538
+ cursor: inherit; /* needed for disabled state */
539
+ border-radius: 0; /* reset ul.selected > li */
540
+ }
541
+ :where(div.multiselect) > ul.selected > li > input::placeholder {
542
+ padding-left: 5pt;
543
+ color: var(--sms-placeholder-color);
544
+ opacity: var(--sms-placeholder-opacity);
545
+ }
546
+ :where(div.multiselect) > input.form-control {
547
+ width: 2em;
548
+ position: absolute;
549
+ background: transparent;
550
+ border: none;
551
+ outline: none;
552
+ z-index: -1;
553
+ opacity: 0;
554
+ pointer-events: none;
555
+ }
556
+ :where(div.multiselect) > ul.options {
557
+ list-style: none;
558
+ padding: 4pt 0;
559
+ top: 100%;
560
+ left: 0;
561
+ width: 100%;
562
+ position: absolute;
563
+ border-radius: 1ex;
564
+ overflow: auto;
565
+ background: var(--sms-options-bg, white);
566
+ max-height: var(--sms-options-max-height, 50vh);
567
+ overscroll-behavior: var(--sms-options-overscroll, none);
568
+ box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
569
+ transition: all 0.2s;
570
+ }
571
+ :where(div.multiselect) > ul.options.hidden {
572
+ visibility: hidden;
573
+ opacity: 0;
574
+ transform: translateY(50px);
575
+ }
576
+ :where(div.multiselect) > ul.options > li {
577
+ padding: 3pt 2ex;
578
+ cursor: pointer;
579
+ scroll-margin: var(--sms-options-scroll-margin, 100px);
580
+ }
581
+ :where(div.multiselect) > ul.options span {
582
+ padding: 3pt 2ex;
583
+ }
584
+ :where(div.multiselect) > ul.options > li.selected {
585
+ background: var(--sms-li-selected-bg);
586
+ color: var(--sms-li-selected-color);
587
+ }
588
+ :where(div.multiselect) > ul.options > li.active {
589
+ background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
590
+ }
591
+ :where(div.multiselect) > ul.options > li.disabled {
592
+ cursor: not-allowed;
593
+ background: var(--sms-li-disabled-bg, #f5f5f6);
594
+ color: var(--sms-li-disabled-text, #b8b8b8);
595
+ }
596
+
597
+ @supports not selector(:where(div.multiselect)) {
598
+ div.multiselect {
452
599
  position: relative;
453
600
  align-items: center;
454
601
  display: flex;
@@ -463,27 +610,26 @@ function on_click_outside(event) {
463
610
  min-height: var(--sms-min-height, 19pt);
464
611
  margin: var(--sms-margin);
465
612
  }
466
- :where(div.multiselect.open) {
613
+ div.multiselect.open {
467
614
  /* increase z-index when open to ensure the dropdown of one <MultiSelect />
468
615
  displays above that of another slightly below it on the page */
469
616
  z-index: var(--sms-open-z-index, 4);
470
617
  }
471
- :where(div.multiselect:focus-within) {
618
+ div.multiselect:focus-within {
472
619
  border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
473
620
  }
474
- :where(div.multiselect.disabled) {
621
+ div.multiselect.disabled {
475
622
  background: var(--sms-disabled-bg, lightgray);
476
623
  cursor: not-allowed;
477
624
  }
478
-
479
- :where(div.multiselect > ul.selected) {
625
+ div.multiselect > ul.selected {
480
626
  display: flex;
481
627
  flex: 1;
482
628
  padding: 0;
483
629
  margin: 0;
484
630
  flex-wrap: wrap;
485
631
  }
486
- :where(div.multiselect > ul.selected > li) {
632
+ div.multiselect > ul.selected > li {
487
633
  align-items: center;
488
634
  border-radius: 3pt;
489
635
  display: flex;
@@ -495,7 +641,7 @@ function on_click_outside(event) {
495
641
  padding: var(--sms-selected-li-padding, 1pt 5pt);
496
642
  color: var(--sms-selected-text-color, var(--sms-text-color));
497
643
  }
498
- :where(div.multiselect button) {
644
+ div.multiselect button {
499
645
  border-radius: 50%;
500
646
  display: flex;
501
647
  transition: 0.2s;
@@ -507,18 +653,19 @@ function on_click_outside(event) {
507
653
  padding: 0;
508
654
  margin: 0 0 0 3pt; /* CSS reset */
509
655
  }
510
- :where(div.multiselect button.remove-all) {
656
+ div.multiselect button.remove-all {
511
657
  margin: 0 3pt;
512
658
  }
513
- :where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
659
+ div.multiselect ul.selected > li button:hover,
660
+ div.multiselect button.remove-all:hover,
661
+ div.multiselect button:focus {
514
662
  color: var(--sms-button-hover-color, lightskyblue);
515
663
  }
516
-
517
- :where(div.multiselect input) {
664
+ div.multiselect input {
518
665
  margin: auto 0; /* CSS reset */
519
666
  padding: 0; /* CSS reset */
520
667
  }
521
- :where(div.multiselect > ul.selected > li > input) {
668
+ div.multiselect > ul.selected > li > input {
522
669
  border: none;
523
670
  outline: none;
524
671
  background: none;
@@ -530,12 +677,12 @@ function on_click_outside(event) {
530
677
  cursor: inherit; /* needed for disabled state */
531
678
  border-radius: 0; /* reset ul.selected > li */
532
679
  }
533
- :where(div.multiselect > ul.selected > li > input)::placeholder {
680
+ div.multiselect > ul.selected > li > input::placeholder {
534
681
  padding-left: 5pt;
535
682
  color: var(--sms-placeholder-color);
536
683
  opacity: var(--sms-placeholder-opacity);
537
684
  }
538
- :where(div.multiselect > input.form-control) {
685
+ div.multiselect > input.form-control {
539
686
  width: 2em;
540
687
  position: absolute;
541
688
  background: transparent;
@@ -545,8 +692,7 @@ function on_click_outside(event) {
545
692
  opacity: 0;
546
693
  pointer-events: none;
547
694
  }
548
-
549
- :where(div.multiselect > ul.options) {
695
+ div.multiselect > ul.options {
550
696
  list-style: none;
551
697
  padding: 4pt 0;
552
698
  top: 100%;
@@ -561,30 +707,29 @@ function on_click_outside(event) {
561
707
  box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
562
708
  transition: all 0.2s;
563
709
  }
564
- :where(div.multiselect > ul.options.hidden) {
710
+ div.multiselect > ul.options.hidden {
565
711
  visibility: hidden;
566
712
  opacity: 0;
567
713
  transform: translateY(50px);
568
714
  }
569
- :where(div.multiselect > ul.options > li) {
715
+ div.multiselect > ul.options > li {
570
716
  padding: 3pt 2ex;
571
717
  cursor: pointer;
572
718
  scroll-margin: var(--sms-options-scroll-margin, 100px);
573
719
  }
574
- /* for noOptionsMsg */
575
- :where(div.multiselect > ul.options span) {
720
+ div.multiselect > ul.options span {
576
721
  padding: 3pt 2ex;
577
722
  }
578
- :where(div.multiselect > ul.options > li.selected) {
723
+ div.multiselect > ul.options > li.selected {
579
724
  background: var(--sms-li-selected-bg);
580
725
  color: var(--sms-li-selected-color);
581
726
  }
582
- :where(div.multiselect > ul.options > li.active) {
727
+ div.multiselect > ul.options > li.active {
583
728
  background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
584
729
  }
585
- :where(div.multiselect > ul.options > li.disabled) {
730
+ div.multiselect > ul.options > li.disabled {
586
731
  cursor: not-allowed;
587
732
  background: var(--sms-li-disabled-bg, #f5f5f6);
588
733
  color: var(--sms-li-disabled-text, #b8b8b8);
589
734
  }
590
- </style>
735
+ }</style>
@@ -17,6 +17,7 @@ declare const __propDef: {
17
17
  id?: string | null | undefined;
18
18
  input?: HTMLInputElement | null | undefined;
19
19
  inputClass?: string | undefined;
20
+ inputmode?: string | null | undefined;
20
21
  invalid?: boolean | undefined;
21
22
  liActiveOptionClass?: string | undefined;
22
23
  liOptionClass?: string | undefined;
@@ -32,19 +33,18 @@ declare const __propDef: {
32
33
  outerDiv?: HTMLDivElement | null | undefined;
33
34
  outerDivClass?: string | undefined;
34
35
  parseLabelsAsHtml?: boolean | undefined;
36
+ pattern?: string | null | undefined;
35
37
  placeholder?: string | null | undefined;
36
38
  removeAllTitle?: string | undefined;
37
39
  removeBtnTitle?: string | undefined;
38
40
  required?: boolean | undefined;
39
41
  searchText?: string | undefined;
40
- selected?: Option[] | undefined;
41
- selectedLabels?: (string | number)[] | undefined;
42
- selectedValues?: unknown[] | undefined;
42
+ selected?: Option | Option[] | null | undefined;
43
+ selectedLabels?: string | number | (string | number)[] | null | undefined;
44
+ selectedValues?: unknown[] | unknown | null;
43
45
  sortSelected?: boolean | ((op1: Option, op2: Option) => number) | undefined;
44
46
  ulOptionsClass?: string | undefined;
45
47
  ulSelectedClass?: string | undefined;
46
- inputmode?: string | undefined;
47
- pattern?: string | undefined;
48
48
  };
49
49
  slots: {
50
50
  selected: {
package/index.js CHANGED
@@ -8,7 +8,8 @@ export const get_value = (op) => op instanceof Object ? op.value ?? op.label : o
8
8
  // this polyfill was copied from
9
9
  // https://github.com/nuxodin/lazyfill/blob/a8e63/polyfills/Element/prototype/scrollIntoViewIfNeeded.js
10
10
  if (typeof Element !== `undefined` &&
11
- !Element.prototype?.scrollIntoViewIfNeeded) {
11
+ !Element.prototype?.scrollIntoViewIfNeeded &&
12
+ typeof IntersectionObserver !== `undefined`) {
12
13
  Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded = true) {
13
14
  const el = this;
14
15
  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": "6.1.0",
8
+ "version": "7.0.1",
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
@@ -41,6 +41,7 @@
41
41
  - **v6.0.1** The prop `disabledTitle` which sets the title of the `<MultiSelect>` `<input>` node if in `disabled` mode was renamed to `disabledInputTitle`. See [PR 105](https://github.com/janosh/svelte-multiselect/pull/105).
42
42
  - **v6.0.1** The default margin of `1em 0` on the wrapper `div.multiselect` was removed. Instead, there is now a new CSS variable `--sms-margin`. Set it to `--sms-margin: 1em 0;` to restore the old appearance. See [PR 105](https://github.com/janosh/svelte-multiselect/pull/105).
43
43
  - **6.1.0** The `dispatch` events `focus` and `blur` were renamed to `open` and `close`, respectively. These actions refer to the dropdown list, i.e. `<MultiSelect on:open={(event) => console.log(event)}>` will trigger when the dropdown list opens. The focus and blur events are now regular DOM (not Svelte `dispatch`) events emitted by the `<input>` node. See [PR 120](https://github.com/janosh/svelte-multiselect/pull/120).
44
+ - **v7.0.0** `selected` (as well `selectedLabels` and `selectedValues`) used to be arrays always. Now, if `maxSelect=1`, they will no longer be a length-1 array but simply a single a option (label/value respectively) or `null` if no option is selected. See [PR 123](https://github.com/janosh/svelte-multiselect/pull/123).
44
45
 
45
46
  ## Installation
46
47
 
@@ -164,6 +165,12 @@ import type { Option } from 'svelte-multiselect'
164
165
 
165
166
  Handle to the `<input>` DOM node. Only available after component mounts (`null` before then).
166
167
 
168
+ 1. ```ts
169
+ inputmode: string | null = null
170
+ ```
171
+
172
+ The `inputmode` attribute hints at the type of data the user may enter. Values like `'numeric' | 'tel' | 'email'` allow browsers to display an appropriate virtual keyboard. See [MDN](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/inputmode) for details.
173
+
167
174
  1. ```ts
168
175
  invalid: boolean = false
169
176
  ```
@@ -186,7 +193,7 @@ import type { Option } from 'svelte-multiselect'
186
193
  maxSelect: number | null = null
187
194
  ```
188
195
 
189
- 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.
190
197
 
191
198
  1. ```ts
192
199
  maxSelectMsg: ((current: number, max: number) => string) | null = null
@@ -220,7 +227,7 @@ import type { Option } from 'svelte-multiselect'
220
227
  options: Option[]
221
228
  ```
222
229
 
223
- **The only required prop** (no default). Array of strings/numbers or `Option` objects to be listed in the dropdown. The only required key on objects is `label` which must also be unique. An object's `value` defaults to `label` if `undefined`. You can add arbitrary additional keys to your option objects. A few keys like `preselected` and `title` have special meaning though. See `src/lib/index.ts` for all special keys and their purpose.
230
+ **The only required prop** (no default). Array of strings/numbers or `Option` objects to be listed in the dropdown. The only required key on objects is `label` which must also be unique. An object's `value` defaults to `label` if `undefined`. You can add arbitrary additional keys to your option objects. A few keys like `preselected` and `title` have special meaning though. See type `ObjectOption` in [`src/lib/index.ts`](https://github.com/janosh/svelte-multiselect/blob/main/src/lib/index.ts) for all special keys and their purpose.
224
231
 
225
232
  1. ```ts
226
233
  outerDiv: HTMLDivElement | null = null
@@ -234,6 +241,12 @@ import type { Option } from 'svelte-multiselect'
234
241
 
235
242
  Whether option labels should be passed to [Svelte's `@html` directive](https://svelte.dev/tutorial/html-tags) or inserted into the DOM as plain text. `true` will raise an error if `allowUserOptions` is also truthy as it makes your site susceptible to [cross-site scripting (XSS) attacks](https://wikipedia.org/wiki/Cross-site_scripting).
236
243
 
244
+ 1. ```ts
245
+ pattern: string | null = null
246
+ ```
247
+
248
+ The pattern attribute specifies a regular expression which the input's value must match. If a non-null value doesn't match the `pattern` regex, the read-only `patternMismatch` property will be `true`. See [MDN](https://developer.mozilla.org/docs/Web/HTML/Attributes/pattern) for details.
249
+
237
250
  1. ```ts
238
251
  placeholder: string | null = null
239
252
  ```
@@ -265,22 +278,25 @@ import type { Option } from 'svelte-multiselect'
265
278
  Text the user-entered to filter down on the list of options. Binds both ways, i.e. can also be used to set the input text.
266
279
 
267
280
  1. ```ts
268
- selected: Option[] = options?.filter((op) => op?.preselected) ?? []
281
+ selected: Option[] | Option | null =
282
+ options
283
+ ?.filter((op) => (op as ObjectOption)?.preselected)
284
+ .slice(0, maxSelect ?? undefined) ?? []
269
285
  ```
270
286
 
271
- Array of currently selected options. Can be bound to `bind:selected={[1, 2, 3]}` to control component state externally or passed as prop to set pre-selected options that will already be populated when component mounts before any user interaction.
287
+ Array of currently selected options. Supports 2-way binding `bind:selected={[1, 2, 3]}` to control component state externally or passed as prop to set pre-selected options that will already be populated when component mounts before any user interaction. If `maxSelect={1}`, selected will not be an array but a single `Option` or `null` if no options are selected.
272
288
 
273
289
  1. ```ts
274
- selectedLabels: (string | number)[] = []
290
+ selectedLabels: (string | number)[] | string | number | null = []
275
291
  ```
276
292
 
277
- Labels of currently selected options. Exposed just for convenience, equivalent to `selected.map(op => op.label)` when options are objects. If options are simple strings, `selected === selectedLabels`. Supports binding but is read-only, i.e. since this value is reactive to `selected`, you cannot control `selected` by changing `bind:selectedLabels`.
293
+ Labels of currently selected options. Exposed just for convenience, equivalent to `selected.map(op => op.label)` when options are objects. If options are simple strings, `selected === selectedLabels`. Supports binding but is read-only, i.e. since this value is reactive to `selected`, you cannot control `selected` by changing `bind:selectedLabels`. If `maxSelect={1}`, selectedLabels will not be an array but a single `string | number` or `null` if no options are selected.
278
294
 
279
295
  1. ```ts
280
- selectedValues: unknown[] = []
296
+ selectedValues: unknown[] | unknown | null = []
281
297
  ```
282
298
 
283
- Values of currently selected options. Exposed just for convenience, equivalent to `selected.map(op => op.value)` when options are objects. If options are simple strings, `selected === selectedValues`. Supports binding but is read-only, i.e. since this value is reactive to `selected`, you cannot control `selected` by changing `bind:selectedValues`.
299
+ Values of currently selected options. Exposed just for convenience, equivalent to `selected.map(op => op.value)` when options are objects. If options are simple strings, `selected === selectedValues`. Supports binding but is read-only, i.e. since this value is reactive to `selected`, you cannot control `selected` by changing `bind:selectedValues`. If `maxSelect={1}`, selectedLabels will not be an array but a single value or `null` if no options are selected.
284
300
 
285
301
  1. ```ts
286
302
  sortSelected: boolean | ((op1: Option, op2: Option) => number) = false
@@ -288,18 +304,6 @@ import type { Option } from 'svelte-multiselect'
288
304
 
289
305
  Default behavior is to render selected items in the order they were chosen. `sortSelected={true}` uses default JS array sorting. A compare function enables custom logic for sorting selected options. See the [`/sort-selected`](https://svelte-multiselect.netlify.app/sort-selected) example.
290
306
 
291
- 1. ```ts
292
- inputmode: string = ``
293
- ```
294
-
295
- The `inputmode` attribute hints at the type of data the user may enter. Values like `'numeric' | 'tel' | 'email'` allow browsers to display an appropriate virtual keyboard. See [MDN](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/inputmode) for details.
296
-
297
- 1. ```ts
298
- pattern: string = ``
299
- ```
300
-
301
- The pattern attribute specifies a regular expression which the input's value must match. If a non-null value doesn't match the `pattern` regex, the read-only `patternMismatch` property will be `true`. See [MDN](https://developer.mozilla.org/docs/Web/HTML/Attributes/pattern) for details.
302
-
303
307
  ## Slots
304
308
 
305
309
  `MultiSelect.svelte` has 3 named slots:
@@ -483,12 +487,12 @@ For example, to change the background color of the options dropdown:
483
487
 
484
488
  The second method allows you to pass in custom classes to the important DOM elements of this component to target them with frameworks like [Tailwind CSS](https://tailwindcss.com).
485
489
 
486
- - `outerDivClass`
487
- - `ulSelectedClass`
488
- - `liSelectedClass`
489
- - `ulOptionsClass`
490
- - `liOptionClass`
491
- - `liActiveOptionClass`
490
+ - `outerDivClass`: wrapper `div` enclosing the whole component
491
+ - `ulSelectedClass`: list of selected options
492
+ - `liSelectedClass`: selected list items
493
+ - `ulOptionsClass`: available options listed in the dropdown when component is in `open` state
494
+ - `liOptionClass`: list items selectable from dropdown list
495
+ - `liActiveOptionClass`: the currently active dropdown list item (i.e. hovered or navigated to with arrow keys)
492
496
 
493
497
  This simplified version of the DOM structure of the component shows where these classes are inserted:
494
498