svelte-multiselect 5.0.1 → 5.0.4

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 >export let color = `cornflowerblue`;
1
+ <script>export let color = `cornflowerblue`;
2
2
  export let duration = `1.5s`;
3
3
  export let size = `1em`;
4
4
  </script>
@@ -1,5 +1,5 @@
1
- <script >import { createEventDispatcher, tick } from 'svelte';
2
- import './';
1
+ <script>import { createEventDispatcher } from 'svelte';
2
+ import { get_label, get_value } from './';
3
3
  import CircleSpinner from './CircleSpinner.svelte';
4
4
  import { CrossIcon, DisabledIcon, ExpandIcon } from './icons';
5
5
  import Wiggle from './Wiggle.svelte';
@@ -10,6 +10,7 @@ export let maxSelectMsg = null;
10
10
  export let disabled = false;
11
11
  export let disabledTitle = `This field is disabled`;
12
12
  export let options;
13
+ export let matchingOptions = [];
13
14
  export let selected = [];
14
15
  export let selectedLabels = [];
15
16
  export let selectedValues = [];
@@ -36,6 +37,7 @@ export let removeBtnTitle = `Remove`;
36
37
  export let removeAllTitle = `Remove all`;
37
38
  export let defaultDisabledTitle = `This option is disabled`;
38
39
  export let allowUserOptions = false;
40
+ export let parseLabelsAsHtml = false; // should not be combined with allowUserOptions!
39
41
  export let addOptionMsg = `Create this option...`;
40
42
  export let autoScroll = true;
41
43
  export let loading = false;
@@ -43,18 +45,26 @@ export let required = false;
43
45
  export let autocomplete = `off`;
44
46
  export let invalid = false;
45
47
  export let sortSelected = false;
48
+ if (!(options?.length > 0)) {
49
+ if (allowUserOptions) {
50
+ options = []; // initializing as array avoids errors when component mounts
51
+ }
52
+ else {
53
+ // only error for empty options if user is not allowed to create custom options
54
+ console.error(`MultiSelect received no options`);
55
+ }
56
+ }
57
+ if (parseLabelsAsHtml && allowUserOptions) {
58
+ console.warn(`Don't combine parseLabelsAsHtml and allowUserOptions. It's susceptible to XSS attacks!`);
59
+ }
46
60
  if (maxSelect !== null && maxSelect < 1) {
47
61
  console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
48
62
  }
49
- if (!(options?.length > 0))
50
- console.error(`MultiSelect is missing options`);
51
- if (!Array.isArray(selected))
52
- console.error(`selected prop must be an array`);
63
+ if (!Array.isArray(selected)) {
64
+ console.error(`selected prop must be an array, got ${selected}`);
65
+ }
53
66
  const dispatch = createEventDispatcher();
54
67
  let activeMsg = false; // controls active state of <li>{addOptionMsg}</li>
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);
58
68
  let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
59
69
  $: selectedLabels = selected.map(get_label);
60
70
  $: selectedValues = selected.map(get_value);
@@ -68,6 +78,9 @@ $: matchingOptions = options.filter((op) => filterFunc(op, searchText) &&
68
78
  !(op instanceof Object && op.disabled) &&
69
79
  !selectedLabels.includes(get_label(op)) // remove already selected options from dropdown list
70
80
  );
81
+ // reset activeOption if it's no longer in the matchingOptions list
82
+ $: if (activeOption && !matchingOptions.includes(activeOption))
83
+ activeOption = null;
71
84
  // add an option to selected list
72
85
  function add(label) {
73
86
  if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
@@ -80,8 +93,21 @@ function add(label) {
80
93
  // custom option twice in append mode
81
94
  [true, `append`].includes(allowUserOptions) &&
82
95
  searchText.length > 0) {
83
- // user entered text but no options match, so if allowUserOptions=true | 'append', we create new option
84
- option = { label: searchText, value: searchText };
96
+ // user entered text but no options match, so if allowUserOptions=true | 'append', we create
97
+ // a new option from the user-entered text
98
+ if (typeof options[0] === `object`) {
99
+ // if 1st option is an object, we create new option as object to keep type homogeneity
100
+ option = { label: searchText, value: searchText };
101
+ }
102
+ else {
103
+ if ([`number`, `undefined`].includes(typeof options[0]) &&
104
+ !isNaN(Number(searchText))) {
105
+ // create new option as number if it parses to a number and 1st option is also number or missing
106
+ option = Number(searchText);
107
+ }
108
+ else
109
+ option = searchText; // else create custom option as string
110
+ }
85
111
  if (allowUserOptions === `append`)
86
112
  options = [...options, option];
87
113
  }
@@ -202,9 +228,12 @@ async function handleKeydown(event) {
202
228
  activeOption = matchingOptions[newActiveIdx];
203
229
  }
204
230
  if (autoScroll) {
205
- await tick();
206
- const li = document.querySelector(`ul.options > li.active`);
207
- li?.scrollIntoViewIfNeeded();
231
+ // TODO This ugly timeout hack is needed to properly scroll element into view when wrapping
232
+ // around start/end of option list. Find a better solution than waiting 10 ms to.
233
+ setTimeout(() => {
234
+ const li = document.querySelector(`ul.options > li.active`);
235
+ li?.scrollIntoView();
236
+ }, 10);
208
237
  }
209
238
  }
210
239
  // on backspace key: remove last selected option
@@ -218,7 +247,7 @@ function remove_all() {
218
247
  selected = [];
219
248
  searchText = ``;
220
249
  }
221
- $: isSelected = (label) => selectedLabels.includes(label);
250
+ $: is_selected = (label) => selectedLabels.includes(label);
222
251
  const if_enter_or_space = (handler) => (event) => {
223
252
  if ([`Enter`, `Space`].includes(event.code)) {
224
253
  event.preventDefault();
@@ -262,7 +291,11 @@ const if_enter_or_space = (handler) => (event) => {
262
291
  {#each selected as option, idx}
263
292
  <li class={liSelectedClass} aria-selected="true">
264
293
  <slot name="selected" {option} {idx}>
265
- {get_label(option)}
294
+ {#if parseLabelsAsHtml}
295
+ {@html get_label(option)}
296
+ {:else}
297
+ {get_label(option)}
298
+ {/if}
266
299
  </slot>
267
300
  {#if !disabled}
268
301
  <button
@@ -271,7 +304,7 @@ const if_enter_or_space = (handler) => (event) => {
271
304
  type="button"
272
305
  title="{removeBtnTitle} {get_label(option)}"
273
306
  >
274
- <CrossIcon width="15px" />
307
+ <slot name="remove-icon"><CrossIcon width="15px" /></slot>
275
308
  </button>
276
309
  {/if}
277
310
  </li>
@@ -319,65 +352,74 @@ const if_enter_or_space = (handler) => (event) => {
319
352
  on:mouseup|stopPropagation={remove_all}
320
353
  on:keydown={if_enter_or_space(remove_all)}
321
354
  >
322
- <CrossIcon width="15px" />
355
+ <slot name="remove-icon"><CrossIcon width="15px" /></slot>
323
356
  </button>
324
357
  {/if}
325
358
  {/if}
326
359
 
327
- <ul class:hidden={!showOptions} class="options {ulOptionsClass}">
328
- {#each matchingOptions as option, idx}
329
- {@const {
330
- label,
331
- disabled = null,
332
- title = null,
333
- selectedTitle = null,
334
- disabledTitle = defaultDisabledTitle,
335
- } = option instanceof Object ? option : { label: option }}
336
- {@const active = activeOption && get_label(activeOption) === label}
337
- <li
338
- on:mousedown|stopPropagation
339
- on:mouseup|stopPropagation={() => {
340
- if (!disabled) isSelected(label) ? remove(label) : add(label)
341
- }}
342
- title={disabled ? disabledTitle : (isSelected(label) && selectedTitle) || title}
343
- class:selected={isSelected(label)}
344
- class:active
345
- class:disabled
346
- class="{liOptionClass} {active ? liActiveOptionClass : ``}"
347
- on:mouseover={() => {
348
- if (!disabled) activeOption = option
349
- }}
350
- on:focus={() => {
351
- if (!disabled) activeOption = option
352
- }}
353
- on:mouseout={() => (activeOption = null)}
354
- on:blur={() => (activeOption = null)}
355
- aria-selected="false"
356
- >
357
- <slot name="option" {option} {idx}>
358
- {get_label(option)}
359
- </slot>
360
- </li>
361
- {:else}
362
- {#if allowUserOptions && searchText}
360
+ <!-- only render options dropdown if options or searchText is not empty needed to avoid briefly flashing empty dropdown -->
361
+ {#if searchText || options?.length > 0}
362
+ <ul class:hidden={!showOptions} class="options {ulOptionsClass}">
363
+ {#each matchingOptions as option, idx}
364
+ {@const {
365
+ label,
366
+ disabled = null,
367
+ title = null,
368
+ selectedTitle = null,
369
+ disabledTitle = defaultDisabledTitle,
370
+ } = option instanceof Object ? option : { label: option }}
371
+ {@const active = activeOption && get_label(activeOption) === label}
363
372
  <li
364
373
  on:mousedown|stopPropagation
365
- on:mouseup|stopPropagation={() => add(searchText)}
366
- title={addOptionMsg}
367
- class:active={activeMsg}
368
- on:mouseover={() => (activeMsg = true)}
369
- on:focus={() => (activeMsg = true)}
370
- on:mouseout={() => (activeMsg = false)}
371
- on:blur={() => (activeMsg = false)}
374
+ on:mouseup|stopPropagation={() => {
375
+ if (!disabled) is_selected(label) ? remove(label) : add(label)
376
+ }}
377
+ title={disabled
378
+ ? disabledTitle
379
+ : (is_selected(label) && selectedTitle) || title}
380
+ class:selected={is_selected(label)}
381
+ class:active
382
+ class:disabled
383
+ class="{liOptionClass} {active ? liActiveOptionClass : ``}"
384
+ on:mouseover={() => {
385
+ if (!disabled) activeOption = option
386
+ }}
387
+ on:focus={() => {
388
+ if (!disabled) activeOption = option
389
+ }}
390
+ on:mouseout={() => (activeOption = null)}
391
+ on:blur={() => (activeOption = null)}
372
392
  aria-selected="false"
373
393
  >
374
- {addOptionMsg}
394
+ <slot name="option" {option} {idx}>
395
+ {#if parseLabelsAsHtml}
396
+ {@html get_label(option)}
397
+ {:else}
398
+ {get_label(option)}
399
+ {/if}
400
+ </slot>
375
401
  </li>
376
402
  {:else}
377
- <span>{noOptionsMsg}</span>
378
- {/if}
379
- {/each}
380
- </ul>
403
+ {#if allowUserOptions && searchText}
404
+ <li
405
+ on:mousedown|stopPropagation
406
+ on:mouseup|stopPropagation={() => add(searchText)}
407
+ title={addOptionMsg}
408
+ class:active={activeMsg}
409
+ on:mouseover={() => (activeMsg = true)}
410
+ on:focus={() => (activeMsg = true)}
411
+ on:mouseout={() => (activeMsg = false)}
412
+ on:blur={() => (activeMsg = false)}
413
+ aria-selected="false"
414
+ >
415
+ {addOptionMsg}
416
+ </li>
417
+ {:else}
418
+ <span>{noOptionsMsg}</span>
419
+ {/if}
420
+ {/each}
421
+ </ul>
422
+ {/if}
381
423
  </div>
382
424
 
383
425
  <style>
@@ -462,7 +504,9 @@ const if_enter_or_space = (handler) => (event) => {
462
504
  cursor: inherit; /* needed for disabled state */
463
505
  }
464
506
  :where(div.multiselect > ul.selected > li > input)::placeholder {
507
+ padding-left: 5pt;
465
508
  color: var(--sms-placeholder-color);
509
+ opacity: var(--sms-placeholder-opacity);
466
510
  }
467
511
  :where(div.multiselect > input.form-control) {
468
512
  width: 2em;
@@ -477,7 +521,7 @@ const if_enter_or_space = (handler) => (event) => {
477
521
 
478
522
  :where(div.multiselect > ul.options) {
479
523
  list-style: none;
480
- padding: 0;
524
+ padding: 4pt 0;
481
525
  top: 100%;
482
526
  left: 0;
483
527
  width: 100%;
@@ -489,8 +533,6 @@ const if_enter_or_space = (handler) => (event) => {
489
533
  overscroll-behavior: var(--sms-options-overscroll, none);
490
534
  box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
491
535
  transition: all 0.2s;
492
- opacity: 1;
493
- transform: translateY(0);
494
536
  }
495
537
  :where(div.multiselect > ul.options.hidden) {
496
538
  visibility: hidden;
@@ -1,5 +1,5 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
- import { CustomEvents, Option } from './';
2
+ import type { MultiSelectEvents, Option } from './';
3
3
  declare const __propDef: {
4
4
  props: {
5
5
  searchText?: string | undefined;
@@ -9,6 +9,7 @@ declare const __propDef: {
9
9
  disabled?: boolean | undefined;
10
10
  disabledTitle?: string | undefined;
11
11
  options: Option[];
12
+ matchingOptions?: Option[] | undefined;
12
13
  selected?: Option[] | undefined;
13
14
  selectedLabels?: (string | number)[] | undefined;
14
15
  selectedValues?: unknown[] | undefined;
@@ -31,6 +32,7 @@ declare const __propDef: {
31
32
  removeAllTitle?: string | undefined;
32
33
  defaultDisabledTitle?: string | undefined;
33
34
  allowUserOptions?: boolean | "append" | undefined;
35
+ parseLabelsAsHtml?: boolean | undefined;
34
36
  addOptionMsg?: string | undefined;
35
37
  autoScroll?: boolean | undefined;
36
38
  loading?: boolean | undefined;
@@ -44,6 +46,7 @@ declare const __propDef: {
44
46
  option: Option;
45
47
  idx: any;
46
48
  };
49
+ 'remove-icon': {};
47
50
  spinner: {};
48
51
  'disabled-icon': {};
49
52
  option: {
@@ -52,7 +55,7 @@ declare const __propDef: {
52
55
  };
53
56
  };
54
57
  getters: {};
55
- events: CustomEvents;
58
+ events: MultiSelectEvents;
56
59
  };
57
60
  export declare type MultiSelectProps = typeof __propDef.props;
58
61
  export declare type MultiSelectEvents = typeof __propDef.events;
package/Wiggle.svelte CHANGED
@@ -1,4 +1,4 @@
1
- <script >import { spring } from 'svelte/motion';
1
+ <script>import { spring } from 'svelte/motion';
2
2
  // bind to this state and set it to true from parent
3
3
  export let wiggle = false;
4
4
  // intended use case: set max value during wiggle for one of angle, scale, dx, dy through props
package/index.d.ts CHANGED
@@ -28,6 +28,8 @@ export declare type DispatchEvents = {
28
28
  focus: undefined;
29
29
  blur: undefined;
30
30
  };
31
- export declare type CustomEvents = {
31
+ export declare type MultiSelectEvents = {
32
32
  [key in keyof DispatchEvents]: CustomEvent<DispatchEvents[key]>;
33
33
  };
34
+ export declare const get_label: (op: Option) => string | number;
35
+ export declare const get_value: (op: Option) => unknown;
package/index.js CHANGED
@@ -1 +1,5 @@
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;
package/package.json CHANGED
@@ -5,39 +5,37 @@
5
5
  "homepage": "https://svelte-multiselect.netlify.app",
6
6
  "repository": "https://github.com/janosh/svelte-multiselect",
7
7
  "license": "MIT",
8
- "version": "5.0.1",
8
+ "version": "5.0.4",
9
9
  "type": "module",
10
10
  "svelte": "index.js",
11
11
  "main": "index.js",
12
12
  "bugs": "https://github.com/janosh/svelte-multiselect/issues",
13
13
  "devDependencies": {
14
- "@sveltejs/adapter-static": "^1.0.0-next.29",
15
- "@sveltejs/kit": "^1.0.0-next.308",
16
- "@sveltejs/vite-plugin-svelte": "^1.0.0-next.41",
17
- "@typescript-eslint/eslint-plugin": "^5.18.0",
18
- "@typescript-eslint/parser": "^5.18.0",
19
- "@vitest/ui": "^0.9.0",
20
- "c8": "^7.11.0",
21
- "eslint": "^8.12.0",
22
- "eslint-plugin-svelte3": "^3.4.1",
14
+ "@playwright/test": "^1.23.1",
15
+ "@sveltejs/adapter-static": "^1.0.0-next.34",
16
+ "@sveltejs/kit": "^1.0.0-next.360",
17
+ "@sveltejs/vite-plugin-svelte": "^1.0.0-next.49",
18
+ "@typescript-eslint/eslint-plugin": "^5.30.5",
19
+ "@typescript-eslint/parser": "^5.30.5",
20
+ "eslint": "^8.19.0",
21
+ "eslint-plugin-svelte3": "^4.0.0",
23
22
  "hastscript": "^7.0.2",
24
- "jsdom": "^19.0.0",
25
- "mdsvex": "^0.10.5",
26
- "playwright": "^1.20.2",
27
- "prettier": "^2.6.2",
28
- "prettier-plugin-svelte": "^2.6.0",
23
+ "jsdom": "^20.0.0",
24
+ "mdsvex": "^0.10.6",
25
+ "prettier": "^2.7.1",
26
+ "prettier-plugin-svelte": "^2.7.0",
29
27
  "rehype-autolink-headings": "^6.1.1",
30
28
  "rehype-slug": "^5.0.1",
31
- "svelte": "^3.46.6",
32
- "svelte-check": "^2.4.6",
29
+ "svelte": "^3.48.0",
30
+ "svelte-check": "^2.8.0",
33
31
  "svelte-github-corner": "^0.1.0",
34
- "svelte-preprocess": "^4.10.5",
32
+ "svelte-preprocess": "^4.10.6",
35
33
  "svelte-toc": "^0.2.9",
36
- "svelte2tsx": "^0.5.6",
37
- "tslib": "^2.3.1",
38
- "typescript": "^4.6.3",
39
- "vite": "^2.9.1",
40
- "vitest": "^0.9.0"
34
+ "svelte2tsx": "^0.5.11",
35
+ "tslib": "^2.4.0",
36
+ "typescript": "^4.7.4",
37
+ "vite": "^2.9.13",
38
+ "vitest": "^0.18.0"
41
39
  },
42
40
  "keywords": [
43
41
  "svelte",
package/readme.md CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  </h4>
15
15
 
16
- **Keyboard-friendly, accessible multi-select Svelte component.**
16
+ **Keyboard-friendly, accessible and highly customizable multi-select component.**
17
17
  <strong class="hide-in-docs">
18
18
  <a href="https://svelte-multiselect.netlify.app">Docs</a>
19
19
  </strong>
@@ -22,24 +22,19 @@
22
22
 
23
23
  ## Key features
24
24
 
25
- - **Single / multiple select:** pass `maxSelect={1}` prop to only allow one selection
25
+ - **Bindable:** `bind:selected` gives you an array of the currently selected options. Thanks to Svelte's 2-way binding, it can also control the component state externally through assignment `selected = ['foo', 42]`.
26
+ - **Keyboard friendly** for mouse-less form completion
27
+ - **No run-time deps:** needs only Svelte as dev dependency
26
28
  - **Dropdowns:** scrollable lists for large numbers of options
27
29
  - **Searchable:** start typing to filter options
28
- - **Tagging:** selected options are recorded as tags in the input
29
- - **Server-side rendering:** no reliance on browser objects like `window` or `document`
30
+ - **Tagging:** selected options are listed as tags within the input
31
+ - **Single / multiple select:** pass `maxSelect={1, 2, 3, ...}` prop to restrict the number of selectable options
30
32
  - **Configurable:** see [props](#props)
31
- - **No dependencies:** needs only Svelte as dev dependency
32
- - **Keyboard friendly** for mouse-less form completion
33
33
 
34
34
  <slot name="nav" />
35
35
 
36
36
  ## Recent breaking changes
37
37
 
38
- - v4.0.0 renamed the slots for customizing how selected options and dropdown list items are rendered:
39
-
40
- - old: `<slot name="renderOptions" />`, new: `<slot name="option" />`
41
- - old: `<slot name="renderSelected" />`, new: `<slot name="selected" />`
42
-
43
38
  - v4.0.1 renamed the `readonly` prop to `disabled` which now prevents all form or user interaction with this component including opening the dropdown list which was still possible before. See [#45](https://github.com/janosh/svelte-multiselect/issues/45) for details. The associated CSS class applied to the outer `div` was likewise renamed to `div.multiselect.{readonly=>disabled}`.
44
39
 
45
40
  - v4.0.3 CSS variables starting with `--sms-input-<attr>` were renamed to just `--sms-<attr>`. E.g. `--sms-input-min-height` is now `--sms-min-height`.
@@ -84,13 +79,14 @@ Full list of props/bindable variables for this component:
84
79
  | `searchText` | `` | 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. |
85
80
  | `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
86
81
  | `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
87
- | `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
82
+ | `selected` | `[]` | 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. |
88
83
  | `selectedLabels` | `[]` | 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`. |
89
84
  | `selectedValues` | `[]` | 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`. |
85
+ | `matchingOptions` | `Option[]` | 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` (see [exposed methods](#exposed-methods) below for details on `filterFunc`). |
90
86
  | `sortSelected` | `boolean \| ((op1, op2) => number)` | 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. |
91
87
  | `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. |
92
88
  | `disabled` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
93
- | `disabledTitle` | `This field is disabled` | Tooltip text to display on hover when the component is in `disabled` state. |
89
+ | `disabledTitle` | `'This field is disabled'` | Tooltip text to display on hover when the component is in `disabled` state. |
94
90
  | `placeholder` | `undefined` | String shown in the text input when no option is selected. |
95
91
  | `input` | `null` | Handle to the `<input>` DOM node. Only available after component mounts (`null` before then). |
96
92
  | `outerDiv` | `null` | Handle to outer `<div class="multiselect">` that wraps the whole component. Only available after component mounts (`null` before then). |
@@ -99,12 +95,13 @@ Full list of props/bindable variables for this component:
99
95
  | `required` | `false` | Whether forms can be submitted without selecting any options. Aborts submission, is scrolled into view and shows help "Please fill out" message when true and user tries to submit with no options selected. |
100
96
  | `autoScroll` | `true` | `false` disables keeping the active dropdown items in view when going up/down the list of options with arrow keys. |
101
97
  | `allowUserOptions` | `false` | Whether users are allowed to enter values not in the dropdown list. `true` means add user-defined options to the selected list only, `'append'` means add to both options and selected. |
98
+ | `parseLabelsAsHtml` | `false` | 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). |
102
99
  | `addOptionMsg` | `'Create this option...'` | Message shown to users after entering text when no options match their query and `allowUserOptions` is truthy. |
103
100
  | `loading` | `false` | Whether the component should display a spinner to indicate it's in loading state. Use `<slot name='spinner'>` to specify a custom spinner. |
104
- | `removeBtnTitle` | `'Remove'` | Title text to display when user hovers over button (cross icon) to remove selected option. |
101
+ | `removeBtnTitle` | `'Remove'` | Title text to display when user hovers over button to remove selected option (which defaults to a cross icon). |
105
102
  | `removeAllTitle` | `'Remove all'` | Title text to display when user hovers over remove-all button. |
106
103
  | `defaultDisabledTitle` | `'This option is disabled'` | Title text to display when user hovers over a disabled option. Each option can override this through its `disabledTitle` attribute. |
107
- | `autocomplete` | `'off'` | Applied to the `<input>`. Specifies if browser is permitted to auto-fill this form field. See [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) for other admissible values. |
104
+ | `autocomplete` | `'off'` | Applied to the `<input>`. Specifies if browser is permitted to auto-fill this form field. See [MDN docs](https://developer.mozilla.org/docs/Web/HTML/Attributes/autocomplete) for other admissible values. |
108
105
  | `invalid` | `false` | If `required=true` and user tries to submit but `selected = []` is empty, `invalid` is automatically set to `true` and CSS class `invalid` applied to the top-level `div.multiselect`. `invalid` class is removed again as soon as the user selects an option. `invalid` can also be controlled externally by binding to it `<MultiSelect bind:invalid />` and setting it to `true` based on outside events or custom validation. |
109
106
 
110
107
  </div>
@@ -134,6 +131,7 @@ Full list of props/bindable variables for this component:
134
131
  - `slot="selected"`: Customize rendering of selected items. Receives as props an `option` and the zero-indexed position (`idx`) it has in the list of selected items.
135
132
  - `slot="spinner"`: Custom spinner component to display when in `loading` state. Receives no props.
136
133
  - `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.
134
+ - `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.
137
135
 
138
136
  Example:
139
137
 
@@ -152,6 +150,7 @@ Example:
152
150
  </span>
153
151
 
154
152
  <CustomSpinner slot="spinner">
153
+ <strong slot="remove-icon">X</strong>
155
154
  </MultiSelect>
156
155
  ```
157
156
 
@@ -239,16 +238,17 @@ If you only want to make small adjustments, you can pass the following CSS varia
239
238
  - `background: var(--sms-disabled-bg, lightgray)`: Background when in disabled state.
240
239
  - `div.multiselect input::placeholder`
241
240
  - `color: var(--sms-placeholder-color)`
241
+ - `color: var(--sms-placeholder-opacity)`
242
242
  - `div.multiselect > ul.selected > li`
243
243
  - `background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15))`: Background of selected options.
244
244
  - `padding: var(--sms-selected-li-padding, 5pt 1pt)`: Height of selected options.
245
245
  - `color: var(--sms-selected-text-color, var(--sms-text-color))`: Text color for selected options.
246
246
  - `ul.selected > li button:hover, button.remove-all:hover, button:focus`
247
- - `color: var(--sms-button-hover-color, lightskyblue)`: Color of the cross-icon buttons for removing all or individual selected options when in `:focus` or `:hover` state.
247
+ - `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.
248
248
  - `div.multiselect > ul.options`
249
249
  - `background: var(--sms-options-bg, white)`: Background of dropdown list.
250
250
  - `max-height: var(--sms-options-max-height, 50vh)`: Maximum height of options dropdown.
251
- - `overscroll-behavior: var(--sms-options-overscroll, none)`: Whether scroll events bubble to parent elements when reaching the top/bottom of the options dropdown. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior).
251
+ - `overscroll-behavior: var(--sms-options-overscroll, none)`: Whether scroll events bubble to parent elements when reaching the top/bottom of the options dropdown. See [MDN](https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior).
252
252
  - `box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);`: Box shadow of dropdown list.
253
253
  - `div.multiselect > ul.options > li`
254
254
  - `scroll-margin: var(--sms-options-scroll-margin, 100px)`: Top/bottom margin to keep between dropdown list items and top/bottom screen edge when auto-scrolling list to keep items in view.