svelte-multiselect 7.0.2 → 7.1.0

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.
@@ -12,6 +12,10 @@ export let breakpoint = 800; // any screen with more horizontal pixels is consid
12
12
  export let defaultDisabledTitle = `This option is disabled`;
13
13
  export let disabled = false;
14
14
  export let disabledInputTitle = `This input is disabled`;
15
+ // case-insensitive equality comparison after string coercion (looking only at the `label` key of object options)
16
+ export let duplicateFunc = (op1, op2) => `${get_label(op1)}`.toLowerCase() === `${get_label(op2)}`.toLowerCase();
17
+ export let duplicateOptionMsg = `This option is already selected`;
18
+ export let duplicates = false; // whether to allow duplicate options
15
19
  export let filterFunc = (op, searchText) => {
16
20
  if (!searchText)
17
21
  return true;
@@ -104,8 +108,11 @@ $: activeOption = activeIndex !== null ? matchingOptions[activeIndex] : null;
104
108
  function add(label, event) {
105
109
  if (maxSelect && maxSelect > 1 && _selected.length >= maxSelect)
106
110
  wiggle = true;
107
- // to prevent duplicate selection, we could add `&& !selectedLabels.includes(label)`
108
- if (maxSelect === null || maxSelect === 1 || _selected.length < maxSelect) {
111
+ if (!isNaN(Number(label)) && typeof _selectedLabels[0] === `number`)
112
+ label = Number(label); // convert to number if possible
113
+ const is_duplicate = _selected.some((option) => duplicateFunc(option, label));
114
+ if ((maxSelect === null || maxSelect === 1 || _selected.length < maxSelect) &&
115
+ (duplicates || !is_duplicate)) {
109
116
  // first check if we find option in the options list
110
117
  let option = options.find((op) => get_label(op) === label);
111
118
  if (!option && // this has the side-effect of not allowing to user to add the same
@@ -130,6 +137,9 @@ function add(label, event) {
130
137
  if (allowUserOptions === `append`)
131
138
  options = [...options, option];
132
139
  }
140
+ if (option === undefined) {
141
+ throw `Run time error, option with label ${label} not found in options list`;
142
+ }
133
143
  searchText = ``; // reset search string on selection
134
144
  if ([``, undefined, null].includes(option)) {
135
145
  console.error(`MultiSelect: encountered missing option with label ${label} (or option is poorly labeled)`);
@@ -325,7 +335,9 @@ function on_click_outside(event) {
325
335
  type="button"
326
336
  title="{removeBtnTitle} {get_label(option)}"
327
337
  >
328
- <slot name="remove-icon"><CrossIcon width="15px" /></slot>
338
+ <slot name="remove-icon">
339
+ <CrossIcon width="15px" />
340
+ </slot>
329
341
  </button>
330
342
  {/if}
331
343
  </li>
@@ -389,7 +401,9 @@ function on_click_outside(event) {
389
401
  on:mouseup|stopPropagation={remove_all}
390
402
  on:keydown={if_enter_or_space(remove_all)}
391
403
  >
392
- <slot name="remove-icon"><CrossIcon width="15px" /></slot>
404
+ <slot name="remove-icon">
405
+ <CrossIcon width="15px" />
406
+ </slot>
393
407
  </button>
394
408
  {/if}
395
409
  {/if}
@@ -449,7 +463,9 @@ function on_click_outside(event) {
449
463
  on:blur={() => (add_option_msg_is_active = false)}
450
464
  aria-selected="false"
451
465
  >
452
- {addOptionMsg}
466
+ {!duplicates && _selected.some((option) => duplicateFunc(option, searchText))
467
+ ? duplicateOptionMsg
468
+ : addOptionMsg}
453
469
  </li>
454
470
  {:else}
455
471
  <span>{noOptionsMsg}</span>
@@ -523,7 +539,8 @@ function on_click_outside(event) {
523
539
  :where(div.multiselect) ul.selected > li button:hover,
524
540
  :where(div.multiselect) button.remove-all:hover,
525
541
  :where(div.multiselect) button:focus {
526
- color: var(--sms-button-hover-color, lightskyblue);
542
+ color: var(--sms-remove-btn-hover-color, lightskyblue);
543
+ background: var(--sms-remove-btn-hover-bg, rgba(0, 0, 0, 0.2));
527
544
  }
528
545
  :where(div.multiselect) input {
529
546
  margin: auto 0; /* CSS reset */
@@ -662,7 +679,8 @@ function on_click_outside(event) {
662
679
  div.multiselect ul.selected > li button:hover,
663
680
  div.multiselect button.remove-all:hover,
664
681
  div.multiselect button:focus {
665
- color: var(--sms-button-hover-color, lightskyblue);
682
+ color: var(--sms-remove-btn-hover-color, lightskyblue);
683
+ background: var(--sms-remove-btn-hover-bg, rgba(0, 0, 0, 0.2));
666
684
  }
667
685
  div.multiselect input {
668
686
  margin: auto 0; /* CSS reset */
@@ -12,6 +12,9 @@ declare const __propDef: {
12
12
  defaultDisabledTitle?: string | undefined;
13
13
  disabled?: boolean | undefined;
14
14
  disabledInputTitle?: string | undefined;
15
+ duplicateFunc?: ((op1: Option, op2: Option) => boolean) | undefined;
16
+ duplicateOptionMsg?: string | undefined;
17
+ duplicates?: boolean | undefined;
15
18
  filterFunc?: ((op: Option, searchText: string) => boolean) | undefined;
16
19
  focusInputOnSelect?: boolean | "desktop" | undefined;
17
20
  id?: string | null | undefined;
@@ -1,5 +1,6 @@
1
- <svg {...$$props} viewBox="0 0 20 20" fill="currentColor">
1
+ <svg {...$$props} viewBox="0 0 24 24" fill="currentColor">
2
2
  <path
3
- d="M10 1.6a8.4 8.4 0 100 16.8 8.4 8.4 0 000-16.8zm4.789 11.461L13.06 14.79 10 11.729l-3.061 3.06L5.21 13.06 8.272 10 5.211 6.939 6.94 5.211 10 8.271l3.061-3.061 1.729 1.729L11.728 10l3.061 3.061z"
3
+ d="M18.3 5.71a.996.996 0 0 0-1.41 0L12 10.59L7.11 5.7A.996.996 0 1 0 5.7 7.11L10.59 12L5.7 16.89a.996.996 0 1 0 1.41 1.41L12 13.41l4.89 4.89a.996.996 0 1 0 1.41-1.41L13.41 12l4.89-4.89c.38-.38.38-1.02 0-1.4z"
4
4
  />
5
5
  </svg>
6
+ <!-- https://api.iconify.design/ic:round-clear.svg -->
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.2",
8
+ "version": "7.1.0",
9
9
  "type": "module",
10
10
  "svelte": "index.js",
11
11
  "main": "index.js",
package/readme.md CHANGED
@@ -138,6 +138,26 @@ import type { Option } from 'svelte-multiselect'
138
138
 
139
139
  Tooltip text to display on hover when the component is in `disabled` state.
140
140
 
141
+ <!-- prettier-ignore -->
142
+ 1. ```ts
143
+ duplicateFunc: (op1: Option, op2: Option) => boolean = (op1, op2) =>
144
+ `${get_label(op1)}`.toLowerCase() === `${get_label(op2)}`.toLowerCase()
145
+ ```
146
+
147
+ This option determines when two options are considered duplicates. Defaults to case-insensitive equality comparison after string coercion (looking only at the `label` key of object options). I.e. the default `duplicateFunc` considers `'Foo' == 'foo'`, `'42' == 42` and ``{ label: `Foo`, value: 0 } == { label: `foo`, value: 42 }``.
148
+
149
+ 1. ```ts
150
+ duplicates: boolean = false
151
+ ```
152
+
153
+ Whether to allow users to select duplicate options. Applies only to the selected item list, not the options dropdown. Keeping that free of duplicates is left to developer. The selected item list can have duplicates if `allowUserOptions` is truthy, `duplicates` is ` true` and users create the same option multiple times. Use `duplicateOptionMsg` to customize the message shown to user if `duplicates` is `false` and users attempt this and `duplicateFunc` to customize when a pair of options is considered a duplicate.
154
+
155
+ 1. ```ts
156
+ duplicateOptionMsg: string = `This option is already selected`
157
+ ```
158
+
159
+ Text to display to users when `allowUserOptions` is truthy and they try to create a new option that's already selected.
160
+
141
161
  1. ```ts
142
162
  filterFunc = (op: Option, searchText: string): boolean => {
143
163
  if (!searchText) return true
@@ -290,13 +310,13 @@ import type { Option } from 'svelte-multiselect'
290
310
  selectedLabels: (string | number)[] | string | number | null = []
291
311
  ```
292
312
 
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.
313
+ 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 (or numbers), `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.
294
314
 
295
315
  1. ```ts
296
316
  selectedValues: unknown[] | unknown | null = []
297
317
  ```
298
318
 
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.
319
+ 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 (or numbers), `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.
300
320
 
301
321
  1. ```ts
302
322
  sortSelected: boolean | ((op1: Option, op2: Option) => number) = false
@@ -460,7 +480,8 @@ If you only want to make small adjustments, you can pass the following CSS varia
460
480
  - `padding: var(--sms-selected-li-padding, 1pt 5pt)`: Height of selected options.
461
481
  - `color: var(--sms-selected-text-color, var(--sms-text-color))`: Text color for selected options.
462
482
  - `ul.selected > li button:hover, button.remove-all:hover, button:focus`
463
- - `color: var(--sms-button-hover-color, lightskyblue)`: Color of the remove-icon buttons for removing all or individual selected options when in `:focus` or `:hover` state.
483
+ - `color: var(--sms-remove-btn-hover-color, lightskyblue)`: Color of the remove-icon buttons for removing all or individual selected options when in `:focus` or `:hover` state.
484
+ - `background: var(--sms-remove-btn-hover-bg, rgba(0, 0, 0, 0.2))`: Background for hovered remove buttons.
464
485
  - `div.multiselect > ul.options`
465
486
  - `background: var(--sms-options-bg, white)`: Background of dropdown list.
466
487
  - `max-height: var(--sms-options-max-height, 50vh)`: Maximum height of options dropdown.