svelte-multiselect 9.0.0 → 10.0.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,6 +1,4 @@
1
- <script>/* eslint-disable no-undef */ // TODO: remove when fixed
2
- // https://github.com/sveltejs/eslint-plugin-svelte3/issues/201
3
- import { tick } from 'svelte';
1
+ <script>import { tick } from 'svelte';
4
2
  import { fade } from 'svelte/transition';
5
3
  import Select from './MultiSelect.svelte';
6
4
  export let actions;
@@ -14,15 +14,16 @@ export let breakpoint = 800; // any screen with more horizontal pixels is consid
14
14
  export let defaultDisabledTitle = `This option is disabled`;
15
15
  export let disabled = false;
16
16
  export let disabledInputTitle = `This input is disabled`;
17
- // case-insensitive equality comparison after string coercion (looking only at the `label` key of object options)
18
17
  // prettier-ignore
19
- export let duplicateFunc = (op1, op2) => `${get_label(op1)}`.toLowerCase() === `${get_label(op2)}`.toLowerCase();
20
18
  export let duplicateOptionMsg = `This option is already selected`;
21
19
  export let duplicates = false; // whether to allow duplicate options
22
- export let filterFunc = (op, searchText) => {
20
+ // takes two options and returns true if they are equal
21
+ // case-insensitive equality comparison after string coercion and looks only at the `label` key of object options by default
22
+ export let key = (opt) => `${get_label(opt)}`.toLowerCase();
23
+ export let filterFunc = (opt, searchText) => {
23
24
  if (!searchText)
24
25
  return true;
25
- return `${get_label(op)}`.toLowerCase().includes(searchText.toLowerCase());
26
+ return `${get_label(opt)}`.toLowerCase().includes(searchText.toLowerCase());
26
27
  };
27
28
  export let focusInputOnSelect = `desktop`;
28
29
  export let form_input = null;
@@ -37,6 +38,7 @@ export let liOptionClass = ``;
37
38
  export let liSelectedClass = ``;
38
39
  export let loading = false;
39
40
  export let matchingOptions = [];
41
+ export let maxOptions = undefined;
40
42
  export let maxSelect = null; // null means there is no upper limit for selected.length
41
43
  export let maxSelectMsg = (current, max) => (max > 1 ? `${current}/${max}` : ``);
42
44
  export let maxSelectMsgClass = ``;
@@ -56,7 +58,7 @@ export let required = false;
56
58
  export let resetFilterOnAdd = true;
57
59
  export let searchText = ``;
58
60
  export let selected = options
59
- ?.filter((op) => op instanceof Object && op?.preselected)
61
+ ?.filter((opt) => opt instanceof Object && opt?.preselected)
60
62
  .slice(0, maxSelect ?? undefined) ?? [];
61
63
  export let sortSelected = false;
62
64
  export let selectedOptionsDraggable = !sortSelected;
@@ -64,14 +66,14 @@ export let ulOptionsClass = ``;
64
66
  export let ulSelectedClass = ``;
65
67
  export let value = null;
66
68
  // get the label key from an option object or the option itself if it's a string or number
67
- export const get_label = (op) => {
68
- if (op instanceof Object) {
69
- if (op.label === undefined) {
70
- console.error(`MultiSelect option ${JSON.stringify(op)} is an object but has no label key`);
69
+ export const get_label = (opt) => {
70
+ if (opt instanceof Object) {
71
+ if (opt.label === undefined) {
72
+ console.error(`MultiSelect option ${JSON.stringify(opt)} is an object but has no label key`);
71
73
  }
72
- return op.label;
74
+ return opt.label;
73
75
  }
74
- return op;
76
+ return `${opt}`;
75
77
  };
76
78
  // if maxSelect=1, value is the single item in selected (or null if selected is empty)
77
79
  // this solves both https://github.com/janosh/svelte-multiselect/issues/86 and
@@ -108,12 +110,17 @@ if (allowUserOptions && !createOptionMsg && createOptionMsg !== null) {
108
110
  console.error(`MultiSelect has allowUserOptions=${allowUserOptions} but createOptionMsg=${createOptionMsg} is falsy. ` +
109
111
  `This prevents the "Add option" <span> from showing up, resulting in a confusing user experience.`);
110
112
  }
113
+ if (maxOptions &&
114
+ (typeof maxOptions != `number` || maxOptions < 0 || maxOptions % 1 != 0)) {
115
+ console.error(`MultiSelect's maxOptions must be undefined or a positive integer, got ${maxOptions}`);
116
+ }
111
117
  const dispatch = createEventDispatcher();
112
118
  let option_msg_is_active = false; // controls active state of <li>{createOptionMsg}</li>
113
119
  let window_width;
114
120
  // options matching the current search text
115
- $: matchingOptions = options.filter((op) => filterFunc(op, searchText) && !selected.includes(op) // remove already selected options from dropdown list
116
- );
121
+ $: matchingOptions = options.filter((opt) => filterFunc(opt, searchText) &&
122
+ // remove already selected options from dropdown list unless duplicate selections are allowed
123
+ (!selected.map(key).includes(key(opt)) || duplicates));
117
124
  // raise if matchingOptions[activeIndex] does not yield a value
118
125
  if (activeIndex !== null && !matchingOptions[activeIndex]) {
119
126
  throw `Run time error, activeIndex=${activeIndex} is out of bounds, matchingOptions.length=${matchingOptions.length}`;
@@ -127,7 +134,7 @@ function add(option, event) {
127
134
  if (!isNaN(Number(option)) && typeof selected.map(get_label)[0] === `number`) {
128
135
  option = Number(option); // convert to number if possible
129
136
  }
130
- const is_duplicate = selected.some((op) => duplicateFunc(op, option));
137
+ const is_duplicate = selected.map(key).includes(key(option));
131
138
  if ((maxSelect === null || maxSelect === 1 || selected.length < maxSelect) &&
132
139
  (duplicates || !is_duplicate)) {
133
140
  if (!options.includes(option) && // first check if we find option in the options list
@@ -194,7 +201,7 @@ function add(option, event) {
194
201
  function remove(to_remove) {
195
202
  if (selected.length === 0)
196
203
  return;
197
- const idx = selected.findIndex((op) => JSON.stringify(op) === JSON.stringify(to_remove));
204
+ const idx = selected.findIndex((opt) => key(opt) === key(to_remove));
198
205
  let [option] = selected.splice(idx, 1); // remove option from selected list
199
206
  if (option === undefined && allowUserOptions) {
200
207
  // if option with label could not be found but allowUserOptions is truthy,
@@ -408,6 +415,7 @@ function highlight_matching_options(event) {
408
415
  role="searchbox"
409
416
  tabindex="-1"
410
417
  >
418
+ <!-- form control input invisible to the user, only purpose is to abort form submission if this component fails data validation -->
411
419
  <!-- bind:value={selected} prevents form submission if required prop is true and no options are selected -->
412
420
  <input
413
421
  {name}
@@ -435,7 +443,7 @@ function highlight_matching_options(event) {
435
443
  <ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt; cursor: pointer;" />
436
444
  </slot>
437
445
  <ul class="selected {ulSelectedClass}" aria-label="selected options">
438
- {#each selected as option, idx (option)}
446
+ {#each selected as option, idx (duplicates ? [key(option), idx] : key(option))}
439
447
  <li
440
448
  class={liSelectedClass}
441
449
  role="option"
@@ -504,6 +512,16 @@ function highlight_matching_options(event) {
504
512
  on:touchstart
505
513
  />
506
514
  <!-- the above on:* lines forward potentially useful DOM events -->
515
+ <slot
516
+ name="after-input"
517
+ {selected}
518
+ {disabled}
519
+ {invalid}
520
+ {id}
521
+ {placeholder}
522
+ {open}
523
+ {required}
524
+ />
507
525
  </ul>
508
526
  {#if loading}
509
527
  <slot name="spinner">
@@ -537,7 +555,7 @@ function highlight_matching_options(event) {
537
555
  {/if}
538
556
  {/if}
539
557
 
540
- <!-- only render options dropdown if options or searchText is not empty needed to avoid briefly flashing empty dropdown -->
558
+ <!-- only render options dropdown if options or searchText is not empty (needed to avoid briefly flashing empty dropdown) -->
541
559
  {#if (searchText && noMatchingOptionsMsg) || options?.length > 0}
542
560
  <ul
543
561
  class:hidden={!open}
@@ -548,7 +566,7 @@ function highlight_matching_options(event) {
548
566
  aria-disabled={disabled ? `true` : null}
549
567
  bind:this={ul_options}
550
568
  >
551
- {#each matchingOptions as option, idx}
569
+ {#each matchingOptions.slice(0, Math.max(0, maxOptions ?? 0) || Infinity) as option, idx}
552
570
  {@const {
553
571
  label,
554
572
  disabled = null,
@@ -591,11 +609,11 @@ function highlight_matching_options(event) {
591
609
  </slot>
592
610
  </li>
593
611
  {:else}
594
- {@const search_is_duplicate = selected.some((option) =>
595
- duplicateFunc(option, searchText)
596
- )}
612
+ {@const textInputIsDuplicate = selected.map(get_label).includes(searchText)}
613
+ <!-- set msg to duplicateOptionMsg if duplicates are not allowed and the user-entered
614
+ searchText is a duplicate, else set to createOptionMsg -->
597
615
  {@const msg =
598
- !duplicates && search_is_duplicate ? duplicateOptionMsg : createOptionMsg}
616
+ !duplicates && textInputIsDuplicate ? duplicateOptionMsg : createOptionMsg}
599
617
  {#if allowUserOptions && searchText && msg}
600
618
  <li
601
619
  on:mousedown|stopPropagation
@@ -610,7 +628,16 @@ function highlight_matching_options(event) {
610
628
  aria-selected="false"
611
629
  class="user-msg"
612
630
  >
613
- {msg}
631
+ <slot
632
+ name="user-msg"
633
+ {duplicateOptionMsg}
634
+ {createOptionMsg}
635
+ {textInputIsDuplicate}
636
+ {searchText}
637
+ {msg}
638
+ >
639
+ {msg}
640
+ </slot>
614
641
  </li>
615
642
  {:else if noMatchingOptionsMsg}
616
643
  <!-- use span to not have cursor: pointer -->
@@ -13,10 +13,10 @@ declare class __sveltets_Render<Option extends T> {
13
13
  defaultDisabledTitle?: string | undefined;
14
14
  disabled?: boolean | undefined;
15
15
  disabledInputTitle?: string | undefined;
16
- duplicateFunc?: ((op1: T, op2: T) => boolean) | undefined;
17
16
  duplicateOptionMsg?: string | undefined;
18
17
  duplicates?: boolean | undefined;
19
- filterFunc?: ((op: Option, searchText: string) => boolean) | undefined;
18
+ key?: ((opt: T) => unknown) | undefined;
19
+ filterFunc?: ((opt: Option, searchText: string) => boolean) | undefined;
20
20
  focusInputOnSelect?: boolean | "desktop" | undefined;
21
21
  form_input?: HTMLInputElement | null | undefined;
22
22
  highlightMatches?: boolean | undefined;
@@ -30,6 +30,7 @@ declare class __sveltets_Render<Option extends T> {
30
30
  liSelectedClass?: string | undefined;
31
31
  loading?: boolean | undefined;
32
32
  matchingOptions?: Option[] | undefined;
33
+ maxOptions?: number | undefined;
33
34
  maxSelect?: number | null | undefined;
34
35
  maxSelectMsg?: ((current: number, max: number) => string) | null | undefined;
35
36
  maxSelectMsgClass?: string | undefined;
@@ -54,7 +55,7 @@ declare class __sveltets_Render<Option extends T> {
54
55
  ulOptionsClass?: string | undefined;
55
56
  ulSelectedClass?: string | undefined;
56
57
  value?: Option | Option[] | null | undefined;
57
- get_label?: ((op: T) => string | number) | undefined;
58
+ get_label?: ((opt: T) => string | number) | undefined;
58
59
  };
59
60
  events(): MultiSelectEvents;
60
61
  slots(): {
@@ -70,18 +71,34 @@ declare class __sveltets_Render<Option extends T> {
70
71
  idx: any;
71
72
  };
72
73
  'remove-icon': {};
74
+ 'after-input': {
75
+ selected: Option[];
76
+ disabled: boolean;
77
+ invalid: boolean;
78
+ id: string | null;
79
+ placeholder: string | null;
80
+ open: boolean;
81
+ required: number | boolean;
82
+ };
73
83
  spinner: {};
74
84
  'disabled-icon': {};
75
85
  option: {
76
86
  option: Option;
77
87
  idx: any;
78
88
  };
89
+ 'user-msg': {
90
+ duplicateOptionMsg: string;
91
+ createOptionMsg: string | null;
92
+ textInputIsDuplicate: any;
93
+ searchText: string;
94
+ msg: any;
95
+ };
79
96
  };
80
97
  }
81
98
  export type MultiSelectProps<Option extends T> = ReturnType<__sveltets_Render<Option>['props']>;
82
99
  export type MultiSelectEvents<Option extends T> = ReturnType<__sveltets_Render<Option>['events']>;
83
100
  export type MultiSelectSlots<Option extends T> = ReturnType<__sveltets_Render<Option>['slots']>;
84
101
  export default class MultiSelect<Option extends T> extends SvelteComponentTyped<MultiSelectProps<Option>, MultiSelectEvents<Option>, MultiSelectSlots<Option>> {
85
- get get_label(): (op: T) => string | number;
102
+ get get_label(): (opt: T) => string | number;
86
103
  }
87
104
  export {};
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "homepage": "https://janosh.github.io/svelte-multiselect",
6
6
  "repository": "https://github.com/janosh/svelte-multiselect",
7
7
  "license": "MIT",
8
- "version": "9.0.0",
8
+ "version": "10.0.0",
9
9
  "type": "module",
10
10
  "svelte": "./dist/index.js",
11
11
  "bugs": "https://github.com/janosh/svelte-multiselect/issues",
@@ -23,37 +23,37 @@
23
23
  "update-coverage": "vitest tests/unit --run --coverage && npx istanbul-badges-readme"
24
24
  },
25
25
  "dependencies": {
26
- "svelte": "^4.0.0-next.0"
26
+ "svelte": "^4.0.1"
27
27
  },
28
28
  "devDependencies": {
29
- "@iconify/svelte": "^3.1.3",
30
- "@playwright/test": "^1.34.3",
29
+ "@iconify/svelte": "^3.1.4",
30
+ "@playwright/test": "^1.35.1",
31
31
  "@sveltejs/adapter-static": "^2.0.2",
32
- "@sveltejs/kit": "^1.19.0",
33
- "@sveltejs/package": "2.0.2",
34
- "@sveltejs/vite-plugin-svelte": "1.0.0-next.29",
35
- "@typescript-eslint/eslint-plugin": "^5.59.7",
36
- "@typescript-eslint/parser": "^5.59.7",
37
- "@vitest/coverage-c8": "^0.31.1",
38
- "eslint": "^8.41.0",
39
- "eslint-plugin-svelte3": "^4.0.0",
32
+ "@sveltejs/kit": "^1.21.0",
33
+ "@sveltejs/package": "2.1.0",
34
+ "@sveltejs/vite-plugin-svelte": "2.4.2",
35
+ "@typescript-eslint/eslint-plugin": "^5.60.1",
36
+ "@typescript-eslint/parser": "^5.60.1",
37
+ "@vitest/coverage-v8": "^0.32.2",
38
+ "eslint": "^8.44.0",
39
+ "eslint-plugin-svelte": "^2.32.2",
40
40
  "hastscript": "^7.2.0",
41
41
  "highlight.js": "^11.8.0",
42
- "jsdom": "^22.0.0",
43
- "mdsvex": "^0.10.6",
42
+ "jsdom": "^22.1.0",
43
+ "mdsvex": "^0.11.0",
44
44
  "mdsvexamples": "^0.3.3",
45
45
  "prettier": "^2.8.8",
46
- "prettier-plugin-svelte": "^2.10.0",
46
+ "prettier-plugin-svelte": "^2.10.1",
47
47
  "rehype-autolink-headings": "^6.1.1",
48
48
  "rehype-slug": "^5.1.0",
49
- "svelte-check": "^3.4.2",
50
- "svelte-preprocess": "^5.0.3",
49
+ "svelte-check": "^3.4.4",
50
+ "svelte-preprocess": "^5.0.4",
51
51
  "svelte-toc": "^0.5.5",
52
- "svelte-zoo": "^0.4.6",
53
- "svelte2tsx": "^0.6.15",
54
- "typescript": "5.0.4",
52
+ "svelte-zoo": "^0.4.8",
53
+ "svelte2tsx": "^0.6.16",
54
+ "typescript": "5.1.6",
55
55
  "vite": "^4.3.9",
56
- "vitest": "^0.31.1"
56
+ "vitest": "^0.32.2"
57
57
  },
58
58
  "keywords": [
59
59
  "svelte",
@@ -77,6 +77,7 @@
77
77
  "default": "./dist/index.js"
78
78
  }
79
79
  },
80
+ "types": "./dist/index.d.ts",
80
81
  "files": [
81
82
  "dist"
82
83
  ]
package/readme.md CHANGED
@@ -8,7 +8,7 @@
8
8
  [![Tests](https://github.com/janosh/svelte-multiselect/actions/workflows/test.yml/badge.svg)](https://github.com/janosh/svelte-multiselect/actions/workflows/test.yml)
9
9
  [![GitHub Pages](https://github.com/janosh/svelte-multiselect/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/janosh/svelte-multiselect/actions/workflows/gh-pages.yml)
10
10
  [![NPM version](https://img.shields.io/npm/v/svelte-multiselect?logo=NPM&color=purple)](https://npmjs.com/package/svelte-multiselect)
11
- [![Needs Svelte version](https://img.shields.io/npm/dependency-version/svelte-multiselect/svelte?color=teal&logo=Svelte&label=Svelte)](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
11
+ [![Needs Svelte version](https://img.shields.io/npm/dependency-version/svelte-multiselect/svelte?color=teal&logo=Svelte&label=Svelte)](https://github.com/sveltejs/svelte/blob/master/packages/svelte/CHANGELOG.md)
12
12
  [![REPL](https://img.shields.io/badge/Svelte-REPL-blue?label=Try%20it!)](https://svelte.dev/repl/a5a14b8f15d64cb083b567292480db05)
13
13
  [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-darkblue?logo=stackblitz)](https://stackblitz.com/github/janosh/svelte-multiselect)
14
14
 
@@ -59,6 +59,15 @@
59
59
  </MultiSelect>
60
60
  ```
61
61
 
62
+ - **v10.0.0** (2023-06-23)&nbsp; `duplicateFunc()` renamed to `key` in [#238](https://github.com/janosh/svelte-multiselect/pull/238). Signature changed:
63
+
64
+ ```diff
65
+ - duplicateFunc: (op1: T, op2: T) => boolean = (op1, op2) => `${get_label(op1)}`.toLowerCase() === `${get_label(op2)}`.toLowerCase()
66
+ + key: (opt: T) => unknown = (opt) => `${get_label(opt)}`.toLowerCase()
67
+ ```
68
+
69
+ Rather than implementing custom equality in `duplicateFunc`, the `key` function is now expected to map options to a unique identifier. `key(op1) === key(op2)` should mean `op1` and `op2` are the same option. `key` can return any type but usually best to return primitives (`string`, `number`, ...) for Svelte keyed each blocks (see [#217](https://github.com/janosh/svelte-multiselect/pull/217)).
70
+
62
71
  ## 🔨 &thinsp; Installation
63
72
 
64
73
  ```sh
@@ -156,30 +165,29 @@ Full list of props/bindable variables for this component. The `Option` type you
156
165
 
157
166
  Tooltip text to display on hover when the component is in `disabled` state.
158
167
 
159
- <!-- prettier-ignore -->
160
168
  1. ```ts
161
- duplicateFunc: (op1: T, op2: T) => boolean = (op1, op2) =>
162
- `${get_label(op1)}`.toLowerCase() === `${get_label(op2)}`.toLowerCase()
169
+ duplicates: boolean = false
163
170
  ```
164
171
 
165
- 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 }``.
172
+ 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 `key` to customize when a pair of options is considered equal.
166
173
 
167
174
  1. ```ts
168
- duplicates: boolean = false
175
+ duplicateOptionMsg: string = `This option is already selected`
169
176
  ```
170
177
 
171
- 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.
178
+ Text to display to users when `allowUserOptions` is truthy and they try to create a new option that's already selected.
172
179
 
180
+ <!-- prettier-ignore -->
173
181
  1. ```ts
174
- duplicateOptionMsg: string = `This option is already selected`
182
+ key: (opt: T) => unknown = (opt) => `${get_label(opt)}`.toLowerCase()
175
183
  ```
176
184
 
177
- Text to display to users when `allowUserOptions` is truthy and they try to create a new option that's already selected.
185
+ A function that maps options to a value by which equality of options is determined. Defaults to mapping options to their lower-cased label. E.g. by default ``const opt1 = { label: `foo`, id: 1 }`` and ``const opt2 = { label: `foo`, id: 2 }`` are considered equal. If you want to consider them different, you can set `key` to e.g. `key={(opt) => opt.id}` or ``key={(opt) => `${opt.label}-${opt.id}}`` or even `key={JSON.stringify}`.
178
186
 
179
187
  1. ```ts
180
- filterFunc = (op: Option, searchText: string): boolean => {
188
+ filterFunc = (opt: Option, searchText: string): boolean => {
181
189
  if (!searchText) return true
182
- return `${get_label(op)}`.toLowerCase().includes(searchText.toLowerCase())
190
+ return `${get_label(opt)}`.toLowerCase().includes(searchText.toLowerCase())
183
191
  }
184
192
  ```
185
193
 
@@ -219,7 +227,7 @@ Full list of props/bindable variables for this component. The `Option` type you
219
227
  inputmode: string | null = null
220
228
  ```
221
229
 
222
- 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.
230
+ The `inputmode` attribute hints at the type of data the user may enter. Values like `'numeric' | 'tel' | 'email'` allow mobile browsers to display an appropriate virtual on-screen keyboard. See [MDN](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/inputmode) for details. If you want to suppress the on-screen keyboard to leave full-screen real estate for the dropdown list of options, set `inputmode="none"`.
223
231
 
224
232
  1. ```ts
225
233
  invalid: boolean = false
@@ -239,6 +247,12 @@ Full list of props/bindable variables for this component. The `Option` type you
239
247
 
240
248
  List of options currently displayed to the user. Same as `options` unless the user entered `searchText` in which case this array contains only those options for which `filterFunc = (op: Option, searchText: string) => boolean` returned `true`.
241
249
 
250
+ 1. ```ts
251
+ maxOptions: number | undefined = undefined
252
+ ```
253
+
254
+ Positive integer to limit the number of options displayed in the dropdown. `undefined` and 0 mean no limit.
255
+
242
256
  1. ```ts
243
257
  maxSelect: number | null = null
244
258
  ```
@@ -383,8 +397,15 @@ Full list of props/bindable variables for this component. The `Option` type you
383
397
  1. `slot="disabled-icon"`: Custom icon to display inside the input when in `disabled` state. Receives no props. Use an empty `<span slot="disabled-icon" />` or `div` to remove the default disabled icon.
384
398
  1. `slot="expand-icon"`: Allows setting a custom icon to indicate to users that the Multiselect text input field is expandable into a dropdown list. Receives prop `open: boolean` which is true if the Multiselect dropdown is visible and false if it's hidden.
385
399
  1. `slot="remove-icon"`: Custom icon to display as remove button. Will be used both by buttons to remove individual selected options and the 'remove all' button that clears all options at once. Receives no props.
386
-
387
- Example:
400
+ 1. `slot="user-msg"`: Displayed like a dropdown item when the list is empty and user is allowed to create custom options based on text input (or if the user's text input clashes with an existing option). `let:props`:
401
+ - `duplicateOptionMsg: string`: See [props](#🔣-props).
402
+ - `createOptionMsg: string`: See [props](#🔣-props).
403
+ - `textInputIsDuplicate: boolean`: Whether user has typed text that matches an already existing option.
404
+ - `searchText: string`: The text user typed into search input.
405
+ - `msg: string`: `duplicateOptionMsg` if user input is a duplicate else `createOptionMsg`.
406
+ 1. `slot='after-input'`: ForPlaced after the search input. For arbitrary content like icons or temporary messages. Receives props `selected`, `disabled`, `invalid`, `id`, `placeholder`, `open`, `required`.
407
+
408
+ Example using several slots:
388
409
 
389
410
  ```svelte
390
411
  <MultiSelect options={[`Red`, `Green`, `Blue`, `Yellow`, `Purple`]} let:idx let:option>