svelte-multiselect 5.0.1 → 5.0.2

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';
@@ -36,6 +36,7 @@ export let removeBtnTitle = `Remove`;
36
36
  export let removeAllTitle = `Remove all`;
37
37
  export let defaultDisabledTitle = `This option is disabled`;
38
38
  export let allowUserOptions = false;
39
+ export let parseLabelsAsHtml = false; // should not be combined with allowUserOptions!
39
40
  export let addOptionMsg = `Create this option...`;
40
41
  export let autoScroll = true;
41
42
  export let loading = false;
@@ -43,18 +44,17 @@ export let required = false;
43
44
  export let autocomplete = `off`;
44
45
  export let invalid = false;
45
46
  export let sortSelected = false;
47
+ if (!(options?.length > 0))
48
+ console.error(`MultiSelect received no options`);
49
+ if (parseLabelsAsHtml && allowUserOptions)
50
+ console.warn(`You shouldn't combine parseLabelsAsHtml and allowUserOptions. It's susceptible to XSS attacks!`);
46
51
  if (maxSelect !== null && maxSelect < 1) {
47
52
  console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
48
53
  }
49
- if (!(options?.length > 0))
50
- console.error(`MultiSelect is missing options`);
51
54
  if (!Array.isArray(selected))
52
55
  console.error(`selected prop must be an array`);
53
56
  const dispatch = createEventDispatcher();
54
57
  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
58
  let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
59
59
  $: selectedLabels = selected.map(get_label);
60
60
  $: selectedValues = selected.map(get_value);
@@ -80,8 +80,12 @@ function add(label) {
80
80
  // custom option twice in append mode
81
81
  [true, `append`].includes(allowUserOptions) &&
82
82
  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 };
83
+ // user entered text but no options match, so if allowUserOptions=true | 'append', we create
84
+ // a new option from the user-entered text
85
+ if (typeof options[0] === `string`)
86
+ option = searchText;
87
+ else
88
+ option = { label: searchText, value: searchText };
85
89
  if (allowUserOptions === `append`)
86
90
  options = [...options, option];
87
91
  }
@@ -202,9 +206,12 @@ async function handleKeydown(event) {
202
206
  activeOption = matchingOptions[newActiveIdx];
203
207
  }
204
208
  if (autoScroll) {
205
- await tick();
206
- const li = document.querySelector(`ul.options > li.active`);
207
- li?.scrollIntoViewIfNeeded();
209
+ // TODO This ugly timeout hack is needed to properly scroll element into view when wrapping
210
+ // around start/end of option list. Find a better solution than waiting 10 ms to.
211
+ setTimeout(() => {
212
+ const li = document.querySelector(`ul.options > li.active`);
213
+ li?.scrollIntoView();
214
+ }, 10);
208
215
  }
209
216
  }
210
217
  // on backspace key: remove last selected option
@@ -262,7 +269,11 @@ const if_enter_or_space = (handler) => (event) => {
262
269
  {#each selected as option, idx}
263
270
  <li class={liSelectedClass} aria-selected="true">
264
271
  <slot name="selected" {option} {idx}>
265
- {get_label(option)}
272
+ {#if parseLabelsAsHtml}
273
+ {@html get_label(option)}
274
+ {:else}
275
+ {get_label(option)}
276
+ {/if}
266
277
  </slot>
267
278
  {#if !disabled}
268
279
  <button
@@ -355,7 +366,11 @@ const if_enter_or_space = (handler) => (event) => {
355
366
  aria-selected="false"
356
367
  >
357
368
  <slot name="option" {option} {idx}>
358
- {get_label(option)}
369
+ {#if parseLabelsAsHtml}
370
+ {@html get_label(option)}
371
+ {:else}
372
+ {get_label(option)}
373
+ {/if}
359
374
  </slot>
360
375
  </li>
361
376
  {:else}
@@ -462,7 +477,9 @@ const if_enter_or_space = (handler) => (event) => {
462
477
  cursor: inherit; /* needed for disabled state */
463
478
  }
464
479
  :where(div.multiselect > ul.selected > li > input)::placeholder {
480
+ padding-left: 5pt;
465
481
  color: var(--sms-placeholder-color);
482
+ opacity: var(--sms-placeholder-opacity);
466
483
  }
467
484
  :where(div.multiselect > input.form-control) {
468
485
  width: 2em;
@@ -477,7 +494,7 @@ const if_enter_or_space = (handler) => (event) => {
477
494
 
478
495
  :where(div.multiselect > ul.options) {
479
496
  list-style: none;
480
- padding: 0;
497
+ padding: 4pt 0;
481
498
  top: 100%;
482
499
  left: 0;
483
500
  width: 100%;
@@ -489,8 +506,6 @@ const if_enter_or_space = (handler) => (event) => {
489
506
  overscroll-behavior: var(--sms-options-overscroll, none);
490
507
  box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
491
508
  transition: all 0.2s;
492
- opacity: 1;
493
- transform: translateY(0);
494
509
  }
495
510
  :where(div.multiselect > ul.options.hidden) {
496
511
  visibility: hidden;
@@ -31,6 +31,7 @@ declare const __propDef: {
31
31
  removeAllTitle?: string | undefined;
32
32
  defaultDisabledTitle?: string | undefined;
33
33
  allowUserOptions?: boolean | "append" | undefined;
34
+ parseLabelsAsHtml?: boolean | undefined;
34
35
  addOptionMsg?: string | undefined;
35
36
  autoScroll?: boolean | undefined;
36
37
  loading?: boolean | undefined;
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
@@ -31,3 +31,5 @@ export declare type DispatchEvents = {
31
31
  export declare type CustomEvents = {
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.2",
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
+ "@sveltejs/adapter-static": "^1.0.0-next.34",
15
+ "@sveltejs/kit": "1.0.0-next.345",
16
+ "@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",
20
+ "eslint-plugin-svelte3": "^4.0.0",
23
21
  "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",
22
+ "jsdom": "^20.0.0",
23
+ "mdsvex": "^0.10.6",
24
+ "playwright": "^1.22.2",
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.12",
38
+ "vitest": "^0.16.0"
41
39
  },
42
40
  "keywords": [
43
41
  "svelte",
package/readme.md CHANGED
@@ -22,14 +22,14 @@
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 3rd-party dependencies:** 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
 
@@ -84,13 +84,13 @@ Full list of props/bindable variables for this component:
84
84
  | `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
85
  | `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
86
86
  | `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. |
87
+ | `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
88
  | `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
89
  | `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`. |
90
90
  | `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
91
  | `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. |
92
92
  | `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. |
93
+ | `disabledTitle` | `'This field is disabled'` | Tooltip text to display on hover when the component is in `disabled` state. |
94
94
  | `placeholder` | `undefined` | String shown in the text input when no option is selected. |
95
95
  | `input` | `null` | Handle to the `<input>` DOM node. Only available after component mounts (`null` before then). |
96
96
  | `outerDiv` | `null` | Handle to outer `<div class="multiselect">` that wraps the whole component. Only available after component mounts (`null` before then). |
@@ -99,6 +99,7 @@ Full list of props/bindable variables for this component:
99
99
  | `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
100
  | `autoScroll` | `true` | `false` disables keeping the active dropdown items in view when going up/down the list of options with arrow keys. |
101
101
  | `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. |
102
+ | `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
103
  | `addOptionMsg` | `'Create this option...'` | Message shown to users after entering text when no options match their query and `allowUserOptions` is truthy. |
103
104
  | `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
105
  | `removeBtnTitle` | `'Remove'` | Title text to display when user hovers over button (cross icon) to remove selected option. |
@@ -239,6 +240,7 @@ If you only want to make small adjustments, you can pass the following CSS varia
239
240
  - `background: var(--sms-disabled-bg, lightgray)`: Background when in disabled state.
240
241
  - `div.multiselect input::placeholder`
241
242
  - `color: var(--sms-placeholder-color)`
243
+ - `color: var(--sms-placeholder-opacity)`
242
244
  - `div.multiselect > ul.selected > li`
243
245
  - `background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15))`: Background of selected options.
244
246
  - `padding: var(--sms-selected-li-padding, 5pt 1pt)`: Height of selected options.