svelte-multiselect 5.0.3 → 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.
@@ -45,15 +45,24 @@ export let required = false;
45
45
  export let autocomplete = `off`;
46
46
  export let invalid = false;
47
47
  export let sortSelected = false;
48
- if (!(options?.length > 0))
49
- console.error(`MultiSelect received no options`);
50
- if (parseLabelsAsHtml && allowUserOptions)
51
- console.warn(`You shouldn't combine parseLabelsAsHtml and allowUserOptions. It's susceptible to XSS attacks!`);
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
+ }
52
60
  if (maxSelect !== null && maxSelect < 1) {
53
61
  console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
54
62
  }
55
- if (!Array.isArray(selected))
56
- 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
+ }
57
66
  const dispatch = createEventDispatcher();
58
67
  let activeMsg = false; // controls active state of <li>{addOptionMsg}</li>
59
68
  let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
@@ -86,10 +95,19 @@ function add(label) {
86
95
  searchText.length > 0) {
87
96
  // user entered text but no options match, so if allowUserOptions=true | 'append', we create
88
97
  // a new option from the user-entered text
89
- if (typeof options[0] === `string`)
90
- option = searchText;
91
- else
98
+ if (typeof options[0] === `object`) {
99
+ // if 1st option is an object, we create new option as object to keep type homogeneity
92
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
+ }
93
111
  if (allowUserOptions === `append`)
94
112
  options = [...options, option];
95
113
  }
@@ -286,7 +304,7 @@ const if_enter_or_space = (handler) => (event) => {
286
304
  type="button"
287
305
  title="{removeBtnTitle} {get_label(option)}"
288
306
  >
289
- <CrossIcon width="15px" />
307
+ <slot name="remove-icon"><CrossIcon width="15px" /></slot>
290
308
  </button>
291
309
  {/if}
292
310
  </li>
@@ -334,69 +352,74 @@ const if_enter_or_space = (handler) => (event) => {
334
352
  on:mouseup|stopPropagation={remove_all}
335
353
  on:keydown={if_enter_or_space(remove_all)}
336
354
  >
337
- <CrossIcon width="15px" />
355
+ <slot name="remove-icon"><CrossIcon width="15px" /></slot>
338
356
  </button>
339
357
  {/if}
340
358
  {/if}
341
359
 
342
- <ul class:hidden={!showOptions} class="options {ulOptionsClass}">
343
- {#each matchingOptions as option, idx}
344
- {@const {
345
- label,
346
- disabled = null,
347
- title = null,
348
- selectedTitle = null,
349
- disabledTitle = defaultDisabledTitle,
350
- } = option instanceof Object ? option : { label: option }}
351
- {@const active = activeOption && get_label(activeOption) === label}
352
- <li
353
- on:mousedown|stopPropagation
354
- on:mouseup|stopPropagation={() => {
355
- if (!disabled) is_selected(label) ? remove(label) : add(label)
356
- }}
357
- title={disabled ? disabledTitle : (is_selected(label) && selectedTitle) || title}
358
- class:selected={is_selected(label)}
359
- class:active
360
- class:disabled
361
- class="{liOptionClass} {active ? liActiveOptionClass : ``}"
362
- on:mouseover={() => {
363
- if (!disabled) activeOption = option
364
- }}
365
- on:focus={() => {
366
- if (!disabled) activeOption = option
367
- }}
368
- on:mouseout={() => (activeOption = null)}
369
- on:blur={() => (activeOption = null)}
370
- aria-selected="false"
371
- >
372
- <slot name="option" {option} {idx}>
373
- {#if parseLabelsAsHtml}
374
- {@html get_label(option)}
375
- {:else}
376
- {get_label(option)}
377
- {/if}
378
- </slot>
379
- </li>
380
- {:else}
381
- {#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}
382
372
  <li
383
373
  on:mousedown|stopPropagation
384
- on:mouseup|stopPropagation={() => add(searchText)}
385
- title={addOptionMsg}
386
- class:active={activeMsg}
387
- on:mouseover={() => (activeMsg = true)}
388
- on:focus={() => (activeMsg = true)}
389
- on:mouseout={() => (activeMsg = false)}
390
- 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)}
391
392
  aria-selected="false"
392
393
  >
393
- {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>
394
401
  </li>
395
402
  {:else}
396
- <span>{noOptionsMsg}</span>
397
- {/if}
398
- {/each}
399
- </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}
400
423
  </div>
401
424
 
402
425
  <style>
@@ -1,5 +1,5 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
- import { MultiSelectEvents, Option } from './';
2
+ import type { MultiSelectEvents, Option } from './';
3
3
  declare const __propDef: {
4
4
  props: {
5
5
  searchText?: string | undefined;
@@ -46,6 +46,7 @@ declare const __propDef: {
46
46
  option: Option;
47
47
  idx: any;
48
48
  };
49
+ 'remove-icon': {};
49
50
  spinner: {};
50
51
  'disabled-icon': {};
51
52
  option: {
package/package.json CHANGED
@@ -5,23 +5,23 @@
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.3",
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
+ "@playwright/test": "^1.23.1",
14
15
  "@sveltejs/adapter-static": "^1.0.0-next.34",
15
- "@sveltejs/kit": "1.0.0-next.345",
16
+ "@sveltejs/kit": "^1.0.0-next.360",
16
17
  "@sveltejs/vite-plugin-svelte": "^1.0.0-next.49",
17
- "@typescript-eslint/eslint-plugin": "^5.29.0",
18
- "@typescript-eslint/parser": "^5.29.0",
19
- "eslint": "^8.18.0",
18
+ "@typescript-eslint/eslint-plugin": "^5.30.5",
19
+ "@typescript-eslint/parser": "^5.30.5",
20
+ "eslint": "^8.19.0",
20
21
  "eslint-plugin-svelte3": "^4.0.0",
21
22
  "hastscript": "^7.0.2",
22
23
  "jsdom": "^20.0.0",
23
24
  "mdsvex": "^0.10.6",
24
- "playwright": "^1.22.2",
25
25
  "prettier": "^2.7.1",
26
26
  "prettier-plugin-svelte": "^2.7.0",
27
27
  "rehype-autolink-headings": "^6.1.1",
@@ -34,8 +34,8 @@
34
34
  "svelte2tsx": "^0.5.11",
35
35
  "tslib": "^2.4.0",
36
36
  "typescript": "^4.7.4",
37
- "vite": "^2.9.12",
38
- "vitest": "^0.16.0"
37
+ "vite": "^2.9.13",
38
+ "vitest": "^0.18.0"
39
39
  },
40
40
  "keywords": [
41
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>
@@ -24,7 +24,7 @@
24
24
 
25
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
26
  - **Keyboard friendly** for mouse-less form completion
27
- - **No 3rd-party dependencies:** needs only Svelte as dev dependency
27
+ - **No run-time deps:** needs only Svelte as dev dependency
28
28
  - **Dropdowns:** scrollable lists for large numbers of options
29
29
  - **Searchable:** start typing to filter options
30
30
  - **Tagging:** selected options are listed as tags within the input
@@ -98,7 +98,7 @@ Full list of props/bindable variables for this component:
98
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). |
99
99
  | `addOptionMsg` | `'Create this option...'` | Message shown to users after entering text when no options match their query and `allowUserOptions` is truthy. |
100
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. |
101
- | `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). |
102
102
  | `removeAllTitle` | `'Remove all'` | Title text to display when user hovers over remove-all button. |
103
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. |
104
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. |
@@ -131,6 +131,7 @@ Full list of props/bindable variables for this component:
131
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.
132
132
  - `slot="spinner"`: Custom spinner component to display when in `loading` state. Receives no props.
133
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.
134
135
 
135
136
  Example:
136
137
 
@@ -149,6 +150,7 @@ Example:
149
150
  </span>
150
151
 
151
152
  <CustomSpinner slot="spinner">
153
+ <strong slot="remove-icon">X</strong>
152
154
  </MultiSelect>
153
155
  ```
154
156
 
@@ -242,7 +244,7 @@ If you only want to make small adjustments, you can pass the following CSS varia
242
244
  - `padding: var(--sms-selected-li-padding, 5pt 1pt)`: Height of selected options.
243
245
  - `color: var(--sms-selected-text-color, var(--sms-text-color))`: Text color for selected options.
244
246
  - `ul.selected > li button:hover, button.remove-all:hover, button:focus`
245
- - `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.
246
248
  - `div.multiselect > ul.options`
247
249
  - `background: var(--sms-options-bg, white)`: Background of dropdown list.
248
250
  - `max-height: var(--sms-options-max-height, 50vh)`: Maximum height of options dropdown.