svelte-multiselect 7.0.1 → 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.
@@ -1,5 +1,4 @@
1
1
  <script>import { createEventDispatcher, tick } from 'svelte';
2
- import { get_label, get_value } from './';
3
2
  import CircleSpinner from './CircleSpinner.svelte';
4
3
  import { CrossIcon, DisabledIcon, ExpandIcon } from './icons';
5
4
  import Wiggle from './Wiggle.svelte';
@@ -13,6 +12,10 @@ export let breakpoint = 800; // any screen with more horizontal pixels is consid
13
12
  export let defaultDisabledTitle = `This option is disabled`;
14
13
  export let disabled = false;
15
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
16
19
  export let filterFunc = (op, searchText) => {
17
20
  if (!searchText)
18
21
  return true;
@@ -52,6 +55,10 @@ export let selectedValues = [];
52
55
  export let sortSelected = false;
53
56
  export let ulOptionsClass = ``;
54
57
  export let ulSelectedClass = ``;
58
+ // get the label key from an option object or the option itself if it's a string or number
59
+ const get_label = (op) => (op instanceof Object ? op.label : op);
60
+ // fallback on label if option is object and value is undefined
61
+ const get_value = (op) => (op instanceof Object ? op.value ?? op.label : op);
55
62
  // selected and _selected are identical except if maxSelect=1, selected will be the single item (or null)
56
63
  // in _selected which will always be an array for easier component internals. selected then solves
57
64
  // https://github.com/janosh/svelte-multiselect/issues/86
@@ -101,8 +108,11 @@ $: activeOption = activeIndex !== null ? matchingOptions[activeIndex] : null;
101
108
  function add(label, event) {
102
109
  if (maxSelect && maxSelect > 1 && _selected.length >= maxSelect)
103
110
  wiggle = true;
104
- // to prevent duplicate selection, we could add `&& !selectedLabels.includes(label)`
105
- 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)) {
106
116
  // first check if we find option in the options list
107
117
  let option = options.find((op) => get_label(op) === label);
108
118
  if (!option && // this has the side-effect of not allowing to user to add the same
@@ -127,9 +137,12 @@ function add(label, event) {
127
137
  if (allowUserOptions === `append`)
128
138
  options = [...options, option];
129
139
  }
140
+ if (option === undefined) {
141
+ throw `Run time error, option with label ${label} not found in options list`;
142
+ }
130
143
  searchText = ``; // reset search string on selection
131
- if (!option) {
132
- console.error(`MultiSelect: option with label ${label} not found`);
144
+ if ([``, undefined, null].includes(option)) {
145
+ console.error(`MultiSelect: encountered missing option with label ${label} (or option is poorly labeled)`);
133
146
  return;
134
147
  }
135
148
  if (maxSelect === 1) {
@@ -322,7 +335,9 @@ function on_click_outside(event) {
322
335
  type="button"
323
336
  title="{removeBtnTitle} {get_label(option)}"
324
337
  >
325
- <slot name="remove-icon"><CrossIcon width="15px" /></slot>
338
+ <slot name="remove-icon">
339
+ <CrossIcon width="15px" />
340
+ </slot>
326
341
  </button>
327
342
  {/if}
328
343
  </li>
@@ -386,7 +401,9 @@ function on_click_outside(event) {
386
401
  on:mouseup|stopPropagation={remove_all}
387
402
  on:keydown={if_enter_or_space(remove_all)}
388
403
  >
389
- <slot name="remove-icon"><CrossIcon width="15px" /></slot>
404
+ <slot name="remove-icon">
405
+ <CrossIcon width="15px" />
406
+ </slot>
390
407
  </button>
391
408
  {/if}
392
409
  {/if}
@@ -446,7 +463,9 @@ function on_click_outside(event) {
446
463
  on:blur={() => (add_option_msg_is_active = false)}
447
464
  aria-selected="false"
448
465
  >
449
- {addOptionMsg}
466
+ {!duplicates && _selected.some((option) => duplicateFunc(option, searchText))
467
+ ? duplicateOptionMsg
468
+ : addOptionMsg}
450
469
  </li>
451
470
  {:else}
452
471
  <span>{noOptionsMsg}</span>
@@ -520,7 +539,8 @@ function on_click_outside(event) {
520
539
  :where(div.multiselect) ul.selected > li button:hover,
521
540
  :where(div.multiselect) button.remove-all:hover,
522
541
  :where(div.multiselect) button:focus {
523
- 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));
524
544
  }
525
545
  :where(div.multiselect) input {
526
546
  margin: auto 0; /* CSS reset */
@@ -659,7 +679,8 @@ function on_click_outside(event) {
659
679
  div.multiselect ul.selected > li button:hover,
660
680
  div.multiselect button.remove-all:hover,
661
681
  div.multiselect button:focus {
662
- 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));
663
684
  }
664
685
  div.multiselect input {
665
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/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,8 +1,4 @@
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
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.1",
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.