svelte-multiselect 5.0.6 → 6.0.1
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.
- package/MultiSelect.svelte +81 -70
- package/MultiSelect.svelte.d.ts +33 -30
- package/index.d.ts +1 -1
- package/package.json +16 -15
- package/readme.md +222 -60
package/MultiSelect.svelte
CHANGED
|
@@ -3,48 +3,51 @@ 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';
|
|
6
|
-
export let
|
|
7
|
-
export let showOptions = false;
|
|
8
|
-
export let maxSelect = null; // null means any number of options are selectable
|
|
9
|
-
export let maxSelectMsg = null;
|
|
10
|
-
export let disabled = false;
|
|
11
|
-
export let disabledTitle = `This field is disabled`;
|
|
12
|
-
export let options;
|
|
13
|
-
export let matchingOptions = [];
|
|
14
|
-
export let selected = [];
|
|
15
|
-
export let selectedLabels = [];
|
|
16
|
-
export let selectedValues = [];
|
|
17
|
-
export let input = null;
|
|
18
|
-
export let outerDiv = null;
|
|
19
|
-
export let placeholder = undefined;
|
|
20
|
-
export let id = undefined;
|
|
21
|
-
export let name = id;
|
|
22
|
-
export let noOptionsMsg = `No matching options`;
|
|
6
|
+
export let activeIndex = null;
|
|
23
7
|
export let activeOption = null;
|
|
8
|
+
export let addOptionMsg = `Create this option...`;
|
|
9
|
+
export let allowUserOptions = false;
|
|
10
|
+
export let autocomplete = `off`;
|
|
11
|
+
export let autoScroll = true;
|
|
12
|
+
export let breakpoint = 800; // any screen with more horizontal pixels is considered desktop, below is mobile
|
|
13
|
+
export let defaultDisabledTitle = `This option is disabled`;
|
|
14
|
+
export let disabled = false;
|
|
15
|
+
export let disabledInputTitle = `This input is disabled`;
|
|
24
16
|
export let filterFunc = (op, searchText) => {
|
|
25
17
|
if (!searchText)
|
|
26
18
|
return true;
|
|
27
19
|
return `${get_label(op)}`.toLowerCase().includes(searchText.toLowerCase());
|
|
28
20
|
};
|
|
29
|
-
export let
|
|
30
|
-
export let
|
|
31
|
-
export let
|
|
32
|
-
export let ulOptionsClass = ``;
|
|
33
|
-
export let liOptionClass = ``;
|
|
34
|
-
export let liActiveOptionClass = ``;
|
|
21
|
+
export let focusInputOnSelect = `desktop`;
|
|
22
|
+
export let id = null;
|
|
23
|
+
export let input = null;
|
|
35
24
|
export let inputClass = ``;
|
|
36
|
-
export let
|
|
37
|
-
export let
|
|
38
|
-
export let
|
|
39
|
-
export let
|
|
40
|
-
export let parseLabelsAsHtml = false; // should not be combined with allowUserOptions!
|
|
41
|
-
export let addOptionMsg = `Create this option...`;
|
|
42
|
-
export let autoScroll = true;
|
|
25
|
+
export let invalid = false;
|
|
26
|
+
export let liActiveOptionClass = ``;
|
|
27
|
+
export let liOptionClass = ``;
|
|
28
|
+
export let liSelectedClass = ``;
|
|
43
29
|
export let loading = false;
|
|
30
|
+
export let matchingOptions = [];
|
|
31
|
+
export let maxSelect = null; // null means any number of options are selectable
|
|
32
|
+
export let maxSelectMsg = null;
|
|
33
|
+
export let name = null;
|
|
34
|
+
export let noOptionsMsg = `No matching options`;
|
|
35
|
+
export let open = false;
|
|
36
|
+
export let options;
|
|
37
|
+
export let outerDiv = null;
|
|
38
|
+
export let outerDivClass = ``;
|
|
39
|
+
export let parseLabelsAsHtml = false; // should not be combined with allowUserOptions!
|
|
40
|
+
export let placeholder = undefined;
|
|
41
|
+
export let removeAllTitle = `Remove all`;
|
|
42
|
+
export let removeBtnTitle = `Remove`;
|
|
44
43
|
export let required = false;
|
|
45
|
-
export let
|
|
46
|
-
export let
|
|
44
|
+
export let searchText = ``;
|
|
45
|
+
export let selected = [];
|
|
46
|
+
export let selectedLabels = [];
|
|
47
|
+
export let selectedValues = [];
|
|
47
48
|
export let sortSelected = false;
|
|
49
|
+
export let ulOptionsClass = ``;
|
|
50
|
+
export let ulSelectedClass = ``;
|
|
48
51
|
if (!(options?.length > 0)) {
|
|
49
52
|
if (allowUserOptions) {
|
|
50
53
|
options = []; // initializing as array avoids errors when component mounts
|
|
@@ -65,6 +68,7 @@ if (!Array.isArray(selected)) {
|
|
|
65
68
|
}
|
|
66
69
|
const dispatch = createEventDispatcher();
|
|
67
70
|
let activeMsg = false; // controls active state of <li>{addOptionMsg}</li>
|
|
71
|
+
let window_width;
|
|
68
72
|
let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
|
|
69
73
|
$: selectedLabels = selected.map(get_label);
|
|
70
74
|
$: selectedValues = selected.map(get_value);
|
|
@@ -78,9 +82,12 @@ $: matchingOptions = options.filter((op) => filterFunc(op, searchText) &&
|
|
|
78
82
|
!(op instanceof Object && op.disabled) &&
|
|
79
83
|
!selectedLabels.includes(get_label(op)) // remove already selected options from dropdown list
|
|
80
84
|
);
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
// raise if matchingOptions[activeIndex] does not yield a value
|
|
86
|
+
if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
87
|
+
throw `Run time error, activeIndex=${activeIndex} is out of bounds, matchingOptions.length=${matchingOptions.length}`;
|
|
88
|
+
}
|
|
89
|
+
// update activeOption when activeIndex changes
|
|
90
|
+
$: activeOption = activeIndex ? matchingOptions[activeIndex] : null;
|
|
84
91
|
// add an option to selected list
|
|
85
92
|
function add(label) {
|
|
86
93
|
if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
|
|
@@ -134,9 +141,11 @@ function add(label) {
|
|
|
134
141
|
}
|
|
135
142
|
}
|
|
136
143
|
if (selected.length === maxSelect)
|
|
137
|
-
|
|
138
|
-
else
|
|
144
|
+
close_dropdown();
|
|
145
|
+
else if (focusInputOnSelect === true ||
|
|
146
|
+
(focusInputOnSelect === `desktop` && window_width > breakpoint)) {
|
|
139
147
|
input?.focus();
|
|
148
|
+
}
|
|
140
149
|
dispatch(`add`, { option });
|
|
141
150
|
dispatch(`change`, { option, type: `add` });
|
|
142
151
|
}
|
|
@@ -158,25 +167,24 @@ function remove(label) {
|
|
|
158
167
|
dispatch(`remove`, { option });
|
|
159
168
|
dispatch(`change`, { option, type: `remove` });
|
|
160
169
|
}
|
|
161
|
-
function
|
|
170
|
+
function open_dropdown() {
|
|
162
171
|
if (disabled)
|
|
163
172
|
return;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
173
|
+
open = true;
|
|
174
|
+
input?.focus();
|
|
175
|
+
dispatch(`focus`);
|
|
176
|
+
}
|
|
177
|
+
function close_dropdown() {
|
|
178
|
+
open = false;
|
|
179
|
+
input?.blur();
|
|
180
|
+
activeOption = null;
|
|
181
|
+
dispatch(`blur`);
|
|
174
182
|
}
|
|
175
183
|
// handle all keyboard events this component receives
|
|
176
|
-
async function
|
|
184
|
+
async function handle_keydown(event) {
|
|
177
185
|
// on escape or tab out of input: dismiss options dropdown and reset search text
|
|
178
186
|
if (event.key === `Escape` || event.key === `Tab`) {
|
|
179
|
-
|
|
187
|
+
close_dropdown();
|
|
180
188
|
searchText = ``;
|
|
181
189
|
}
|
|
182
190
|
// on enter key: toggle active option and reset search text
|
|
@@ -194,7 +202,7 @@ async function handleKeydown(event) {
|
|
|
194
202
|
// no active option and no search text means the options dropdown is closed
|
|
195
203
|
// in which case enter means open it
|
|
196
204
|
else
|
|
197
|
-
|
|
205
|
+
open_dropdown();
|
|
198
206
|
}
|
|
199
207
|
// on up/down arrow keys: update active option
|
|
200
208
|
else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
|
|
@@ -257,27 +265,30 @@ const if_enter_or_space = (handler) => (event) => {
|
|
|
257
265
|
handler();
|
|
258
266
|
}
|
|
259
267
|
};
|
|
268
|
+
function on_click_outside(event) {
|
|
269
|
+
if (outerDiv && !outerDiv.contains(event.target)) {
|
|
270
|
+
close_dropdown();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
260
273
|
</script>
|
|
261
274
|
|
|
262
275
|
<svelte:window
|
|
263
|
-
on:click={
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
}}
|
|
276
|
+
on:click={on_click_outside}
|
|
277
|
+
on:touchstart={on_click_outside}
|
|
278
|
+
bind:innerWidth={window_width}
|
|
268
279
|
/>
|
|
269
280
|
|
|
270
281
|
<div
|
|
271
282
|
bind:this={outerDiv}
|
|
272
283
|
class:disabled
|
|
273
284
|
class:single={maxSelect === 1}
|
|
274
|
-
class:open
|
|
275
|
-
aria-expanded={
|
|
285
|
+
class:open
|
|
286
|
+
aria-expanded={open}
|
|
276
287
|
aria-multiselectable={maxSelect === null || maxSelect > 1}
|
|
277
288
|
class:invalid
|
|
278
289
|
class="multiselect {outerDivClass}"
|
|
279
|
-
on:mouseup|stopPropagation={
|
|
280
|
-
title={disabled ?
|
|
290
|
+
on:mouseup|stopPropagation={open_dropdown}
|
|
291
|
+
title={disabled ? disabledInputTitle : null}
|
|
281
292
|
aria-disabled={disabled ? `true` : null}
|
|
282
293
|
>
|
|
283
294
|
<input
|
|
@@ -318,9 +329,9 @@ const if_enter_or_space = (handler) => (event) => {
|
|
|
318
329
|
bind:this={input}
|
|
319
330
|
{autocomplete}
|
|
320
331
|
bind:value={searchText}
|
|
321
|
-
on:mouseup|self|stopPropagation={
|
|
322
|
-
on:keydown={
|
|
323
|
-
on:focus={
|
|
332
|
+
on:mouseup|self|stopPropagation={open_dropdown}
|
|
333
|
+
on:keydown={handle_keydown}
|
|
334
|
+
on:focus={open_dropdown}
|
|
324
335
|
{id}
|
|
325
336
|
{name}
|
|
326
337
|
{disabled}
|
|
@@ -362,7 +373,7 @@ const if_enter_or_space = (handler) => (event) => {
|
|
|
362
373
|
|
|
363
374
|
<!-- only render options dropdown if options or searchText is not empty needed to avoid briefly flashing empty dropdown -->
|
|
364
375
|
{#if searchText || options?.length > 0}
|
|
365
|
-
<ul class:hidden={!
|
|
376
|
+
<ul class:hidden={!open} class="options {ulOptionsClass}">
|
|
366
377
|
{#each matchingOptions as option, idx}
|
|
367
378
|
{@const {
|
|
368
379
|
label,
|
|
@@ -371,7 +382,7 @@ const if_enter_or_space = (handler) => (event) => {
|
|
|
371
382
|
selectedTitle = null,
|
|
372
383
|
disabledTitle = defaultDisabledTitle,
|
|
373
384
|
} = option instanceof Object ? option : { label: option }}
|
|
374
|
-
{@const active =
|
|
385
|
+
{@const active = activeIndex === idx}
|
|
375
386
|
<li
|
|
376
387
|
on:mousedown|stopPropagation
|
|
377
388
|
on:mouseup|stopPropagation={() => {
|
|
@@ -385,13 +396,13 @@ const if_enter_or_space = (handler) => (event) => {
|
|
|
385
396
|
class:disabled
|
|
386
397
|
class="{liOptionClass} {active ? liActiveOptionClass : ``}"
|
|
387
398
|
on:mouseover={() => {
|
|
388
|
-
if (!disabled)
|
|
399
|
+
if (!disabled) activeIndex = idx
|
|
389
400
|
}}
|
|
390
401
|
on:focus={() => {
|
|
391
|
-
if (!disabled)
|
|
402
|
+
if (!disabled) activeIndex = idx
|
|
392
403
|
}}
|
|
393
|
-
on:mouseout={() => (
|
|
394
|
-
on:blur={() => (
|
|
404
|
+
on:mouseout={() => (activeIndex = null)}
|
|
405
|
+
on:blur={() => (activeIndex = null)}
|
|
395
406
|
aria-selected="false"
|
|
396
407
|
>
|
|
397
408
|
<slot name="option" {option} {idx}>
|
|
@@ -428,7 +439,6 @@ const if_enter_or_space = (handler) => (event) => {
|
|
|
428
439
|
<style>
|
|
429
440
|
:where(div.multiselect) {
|
|
430
441
|
position: relative;
|
|
431
|
-
margin: 1em 0;
|
|
432
442
|
align-items: center;
|
|
433
443
|
display: flex;
|
|
434
444
|
cursor: text;
|
|
@@ -440,6 +450,7 @@ const if_enter_or_space = (handler) => (event) => {
|
|
|
440
450
|
color: var(--sms-text-color);
|
|
441
451
|
font-size: var(--sms-font-size, inherit);
|
|
442
452
|
min-height: var(--sms-min-height, 19pt);
|
|
453
|
+
margin: var(--sms-margin);
|
|
443
454
|
}
|
|
444
455
|
:where(div.multiselect.open) {
|
|
445
456
|
/* increase z-index when open to ensure the dropdown of one <MultiSelect />
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -2,44 +2,47 @@ import { SvelteComponentTyped } from "svelte";
|
|
|
2
2
|
import type { MultiSelectEvents, Option } from './';
|
|
3
3
|
declare const __propDef: {
|
|
4
4
|
props: {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
activeIndex?: number | null | undefined;
|
|
6
|
+
activeOption?: Option | null | undefined;
|
|
7
|
+
addOptionMsg?: string | undefined;
|
|
8
|
+
allowUserOptions?: boolean | "append" | undefined;
|
|
9
|
+
autocomplete?: string | undefined;
|
|
10
|
+
autoScroll?: boolean | undefined;
|
|
11
|
+
breakpoint?: number | undefined;
|
|
12
|
+
defaultDisabledTitle?: string | undefined;
|
|
13
|
+
disabled?: boolean | undefined;
|
|
14
|
+
disabledInputTitle?: string | undefined;
|
|
15
|
+
filterFunc?: ((op: Option, searchText: string) => boolean) | undefined;
|
|
16
|
+
focusInputOnSelect?: boolean | "desktop" | undefined;
|
|
17
|
+
id?: string | null | undefined;
|
|
18
|
+
input?: HTMLInputElement | null | undefined;
|
|
19
|
+
inputClass?: string | undefined;
|
|
20
|
+
invalid?: boolean | undefined;
|
|
21
|
+
liActiveOptionClass?: string | undefined;
|
|
22
|
+
liOptionClass?: string | undefined;
|
|
23
|
+
liSelectedClass?: string | undefined;
|
|
24
|
+
loading?: boolean | undefined;
|
|
25
|
+
matchingOptions?: Option[] | undefined;
|
|
7
26
|
maxSelect?: number | null | undefined;
|
|
8
27
|
maxSelectMsg?: ((current: number, max: number) => string) | null | undefined;
|
|
9
|
-
|
|
10
|
-
|
|
28
|
+
name?: string | null | undefined;
|
|
29
|
+
noOptionsMsg?: string | undefined;
|
|
30
|
+
open?: boolean | undefined;
|
|
11
31
|
options: Option[];
|
|
12
|
-
matchingOptions?: Option[] | undefined;
|
|
13
|
-
selected?: Option[] | undefined;
|
|
14
|
-
selectedLabels?: (string | number)[] | undefined;
|
|
15
|
-
selectedValues?: unknown[] | undefined;
|
|
16
|
-
input?: HTMLInputElement | null | undefined;
|
|
17
32
|
outerDiv?: HTMLDivElement | null | undefined;
|
|
18
|
-
placeholder?: string | undefined;
|
|
19
|
-
id?: string | undefined;
|
|
20
|
-
name?: string | undefined;
|
|
21
|
-
noOptionsMsg?: string | undefined;
|
|
22
|
-
activeOption?: Option | null | undefined;
|
|
23
|
-
filterFunc?: ((op: Option, searchText: string) => boolean) | undefined;
|
|
24
33
|
outerDivClass?: string | undefined;
|
|
25
|
-
ulSelectedClass?: string | undefined;
|
|
26
|
-
liSelectedClass?: string | undefined;
|
|
27
|
-
ulOptionsClass?: string | undefined;
|
|
28
|
-
liOptionClass?: string | undefined;
|
|
29
|
-
liActiveOptionClass?: string | undefined;
|
|
30
|
-
inputClass?: string | undefined;
|
|
31
|
-
removeBtnTitle?: string | undefined;
|
|
32
|
-
removeAllTitle?: string | undefined;
|
|
33
|
-
defaultDisabledTitle?: string | undefined;
|
|
34
|
-
allowUserOptions?: boolean | "append" | undefined;
|
|
35
34
|
parseLabelsAsHtml?: boolean | undefined;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
placeholder?: string | undefined;
|
|
36
|
+
removeAllTitle?: string | undefined;
|
|
37
|
+
removeBtnTitle?: string | undefined;
|
|
39
38
|
required?: boolean | undefined;
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
searchText?: string | undefined;
|
|
40
|
+
selected?: Option[] | undefined;
|
|
41
|
+
selectedLabels?: (string | number)[] | undefined;
|
|
42
|
+
selectedValues?: unknown[] | undefined;
|
|
42
43
|
sortSelected?: boolean | ((op1: Option, op2: Option) => number) | undefined;
|
|
44
|
+
ulOptionsClass?: string | undefined;
|
|
45
|
+
ulSelectedClass?: string | undefined;
|
|
43
46
|
};
|
|
44
47
|
slots: {
|
|
45
48
|
selected: {
|
package/index.d.ts
CHANGED
|
@@ -32,4 +32,4 @@ export declare type MultiSelectEvents = {
|
|
|
32
32
|
[key in keyof DispatchEvents]: CustomEvent<DispatchEvents[key]>;
|
|
33
33
|
};
|
|
34
34
|
export declare const get_label: (op: Option) => string | number;
|
|
35
|
-
export declare const get_value: (op: Option) =>
|
|
35
|
+
export declare const get_value: (op: Option) => {};
|
package/package.json
CHANGED
|
@@ -5,19 +5,20 @@
|
|
|
5
5
|
"homepage": "https://svelte-multiselect.netlify.app",
|
|
6
6
|
"repository": "https://github.com/janosh/svelte-multiselect",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"version": "
|
|
8
|
+
"version": "6.0.1",
|
|
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.
|
|
15
|
-
"@sveltejs/adapter-static": "^1.0.0-next.
|
|
16
|
-
"@sveltejs/kit": "^1.0.0-next.
|
|
17
|
-
"@sveltejs/
|
|
18
|
-
"@
|
|
19
|
-
"@typescript-eslint/
|
|
20
|
-
"eslint": "^
|
|
14
|
+
"@playwright/test": "^1.25.2",
|
|
15
|
+
"@sveltejs/adapter-static": "^1.0.0-next.43",
|
|
16
|
+
"@sveltejs/kit": "^1.0.0-next.481",
|
|
17
|
+
"@sveltejs/package": "^1.0.0-next.3",
|
|
18
|
+
"@sveltejs/vite-plugin-svelte": "^1.0.5",
|
|
19
|
+
"@typescript-eslint/eslint-plugin": "^5.37.0",
|
|
20
|
+
"@typescript-eslint/parser": "^5.37.0",
|
|
21
|
+
"eslint": "^8.23.1",
|
|
21
22
|
"eslint-plugin-svelte3": "^4.0.0",
|
|
22
23
|
"hastscript": "^7.0.2",
|
|
23
24
|
"jsdom": "^20.0.0",
|
|
@@ -26,16 +27,16 @@
|
|
|
26
27
|
"prettier-plugin-svelte": "^2.7.0",
|
|
27
28
|
"rehype-autolink-headings": "^6.1.1",
|
|
28
29
|
"rehype-slug": "^5.0.1",
|
|
29
|
-
"svelte": "^3.
|
|
30
|
-
"svelte-check": "^2.
|
|
30
|
+
"svelte": "^3.50.1",
|
|
31
|
+
"svelte-check": "^2.9.0",
|
|
31
32
|
"svelte-github-corner": "^0.1.0",
|
|
32
33
|
"svelte-preprocess": "^4.10.6",
|
|
33
|
-
"svelte-toc": "^0.
|
|
34
|
-
"svelte2tsx": "^0.5.
|
|
34
|
+
"svelte-toc": "^0.4.0",
|
|
35
|
+
"svelte2tsx": "^0.5.17",
|
|
35
36
|
"tslib": "^2.4.0",
|
|
36
|
-
"typescript": "^4.
|
|
37
|
-
"vite": "^3.0
|
|
38
|
-
"vitest": "^0.
|
|
37
|
+
"typescript": "^4.8.3",
|
|
38
|
+
"vite": "^3.1.0",
|
|
39
|
+
"vitest": "^0.23.2"
|
|
39
40
|
},
|
|
40
41
|
"keywords": [
|
|
41
42
|
"svelte",
|
package/readme.md
CHANGED
|
@@ -5,18 +5,19 @@
|
|
|
5
5
|
|
|
6
6
|
<h4 align="center">
|
|
7
7
|
|
|
8
|
-
[](https://svelte.dev/repl/a5a14b8f15d64cb083b567292480db05)
|
|
9
8
|
[](https://github.com/janosh/svelte-multiselect/actions/workflows/test.yml)
|
|
10
9
|
[](https://app.netlify.com/sites/svelte-multiselect/deploys)
|
|
11
10
|
[](https://npmjs.com/package/svelte-multiselect)
|
|
12
11
|
[](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
|
|
12
|
+
[](https://svelte.dev/repl/a5a14b8f15d64cb083b567292480db05)
|
|
13
|
+
[](https://stackblitz.com/github/janosh/svelte-multiselect)
|
|
13
14
|
|
|
14
15
|
</h4>
|
|
15
16
|
|
|
16
17
|
**Keyboard-friendly, accessible and highly customizable multi-select component.**
|
|
17
|
-
<
|
|
18
|
-
<a href="https://svelte-multiselect.netlify.app">
|
|
19
|
-
</
|
|
18
|
+
<span class="hide-in-docs">
|
|
19
|
+
<a href="https://svelte-multiselect.netlify.app">View the docs</a>
|
|
20
|
+
</span>
|
|
20
21
|
|
|
21
22
|
<slot name="examples" />
|
|
22
23
|
|
|
@@ -35,11 +36,10 @@
|
|
|
35
36
|
|
|
36
37
|
## Recent breaking changes
|
|
37
38
|
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
- v5.0.0 Support both simple and object options. Previously strings and numbers were converted to `{ value, label }` objects internally and returned by `bind:selected`. Now, if you pass in `string[]`, that's exactly what you'll get from `bind:selected`.
|
|
39
|
+
- **v5.0.0** Supports both simple and object options. Previously strings and numbers were converted to `{ value, label }` objects internally and returned by `bind:selected`. Now, if you pass in `string[]`, that's exactly what you'll get from `bind:selected`. See [PR 76](https://github.com/janosh/svelte-multiselect/pull/76).
|
|
40
|
+
- **v6.0.0** The prop `showOptions` which controls whether the list of dropdown options is currently open or closed was renamed to `open`. See [PR 103](https://github.com/janosh/svelte-multiselect/pull/103).
|
|
41
|
+
- **v6.0.1** The prop `disabledTitle` which sets the title of the `<MultiSelect>` `<input>` node if in `disabled` mode was renamed to `disabledInputTitle`. See [PR 105](https://github.com/janosh/svelte-multiselect/pull/105).
|
|
42
|
+
- **v6.0.1** The default margin of `1em 0` on the wrapper `div.multiselect` was removed. Instead, there is now a new CSS variable `--sms-margin`. Set it to `--sms-margin: 1em 0;` to restore the old appearance. See [PR 105](https://github.com/janosh/svelte-multiselect/pull/105).
|
|
43
43
|
|
|
44
44
|
## Installation
|
|
45
45
|
|
|
@@ -69,64 +69,223 @@ Favorite Frontend Frameworks?
|
|
|
69
69
|
|
|
70
70
|
## Props
|
|
71
71
|
|
|
72
|
-
Full list of props/bindable variables for this component:
|
|
72
|
+
Full list of props/bindable variables for this component. In the type hints below, `Option` is:
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
```ts
|
|
75
|
+
import type { Option } from 'svelte-multiselect'
|
|
76
|
+
```
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
| `options` | required prop | Array of strings/numbers or `Option` objects to be listed in the dropdown. The only required key on objects is `label` which must also be unique. An object's `value` defaults to `label` if `undefined`. You can add arbitrary additional keys to your option objects. MultiSelect A few keys like `preselected` and `title` have special meaning though. See `src/lib/index.ts` for all special keys and their purpose. |
|
|
80
|
-
| `showOptions` | `false` | Bindable boolean that controls whether the options dropdown is visible. |
|
|
81
|
-
| `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. |
|
|
82
|
-
| `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
|
|
83
|
-
| `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
|
|
84
|
-
| `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. |
|
|
85
|
-
| `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`. |
|
|
86
|
-
| `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`. |
|
|
87
|
-
| `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`). |
|
|
88
|
-
| `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. |
|
|
89
|
-
| `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. |
|
|
90
|
-
| `disabled` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
|
|
91
|
-
| `disabledTitle` | `'This field is disabled'` | Tooltip text to display on hover when the component is in `disabled` state. |
|
|
92
|
-
| `placeholder` | `undefined` | String shown in the text input when no option is selected. |
|
|
93
|
-
| `input` | `null` | Handle to the `<input>` DOM node. Only available after component mounts (`null` before then). |
|
|
94
|
-
| `outerDiv` | `null` | Handle to outer `<div class="multiselect">` that wraps the whole component. Only available after component mounts (`null` before then). |
|
|
95
|
-
| `id` | `undefined` | Applied to the `<input>` element for associating HTML form `<label>`s with this component for accessibility. Also, clicking a `<label>` with same `for` attribute as `id` will focus this component. |
|
|
96
|
-
| `name` | `id` | Applied to the `<input>` element. If not provided, will be set to the value of `id`. Sets the key of this field in a submitted form data object. Not useful at the moment since the value is stored in Svelte state, not on the `<input>`. |
|
|
97
|
-
| `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. |
|
|
98
|
-
| `autoScroll` | `true` | `false` disables keeping the active dropdown items in view when going up/down the list of options with arrow keys. |
|
|
99
|
-
| `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. |
|
|
100
|
-
| `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). |
|
|
101
|
-
| `addOptionMsg` | `'Create this option...'` | Message shown to users after entering text when no options match their query and `allowUserOptions` is truthy. |
|
|
102
|
-
| `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. |
|
|
103
|
-
| `removeBtnTitle` | `'Remove'` | Title text to display when user hovers over button to remove selected option (which defaults to a cross icon). |
|
|
104
|
-
| `removeAllTitle` | `'Remove all'` | Title text to display when user hovers over remove-all button. |
|
|
105
|
-
| `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. |
|
|
106
|
-
| `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. |
|
|
107
|
-
| `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. |
|
|
78
|
+
1. ```ts
|
|
79
|
+
activeIndex: integer | null = null
|
|
80
|
+
```
|
|
108
81
|
|
|
109
|
-
|
|
82
|
+
Zero-based index of currently active option in the array of currently matching options, i.e. if the user typed a search string into the input and only a subset of options match, this index refers to the array position of the matching subset of options
|
|
110
83
|
|
|
111
|
-
|
|
84
|
+
1. ```ts
|
|
85
|
+
activeOption: Option | null = null
|
|
86
|
+
```
|
|
112
87
|
|
|
113
|
-
|
|
88
|
+
Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys.
|
|
114
89
|
|
|
115
|
-
|
|
116
|
-
|
|
90
|
+
1. ```ts
|
|
91
|
+
addOptionMsg: string = 'Create this option...'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Message shown to users after entering text when no options match their query and `allowUserOptions` is truthy.
|
|
95
|
+
|
|
96
|
+
1. ```ts
|
|
97
|
+
allowUserOptions: boolean = false
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
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.
|
|
101
|
+
|
|
102
|
+
1. ```ts
|
|
103
|
+
autocomplete: string = 'off'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
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.
|
|
107
|
+
|
|
108
|
+
1. ```ts
|
|
109
|
+
autoScroll: boolean = true
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`false` disables keeping the active dropdown items in view when going up/down the list of options with arrow keys.
|
|
113
|
+
|
|
114
|
+
1. ```ts
|
|
115
|
+
breakpoint: integer = 800
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Screens wider than `breakpoint` in pixels will be considered `'desktop'`, everything narrower as `'mobile'`.
|
|
119
|
+
|
|
120
|
+
1. ```ts
|
|
121
|
+
defaultDisabledTitle: string = 'This option is disabled'
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Title text to display when user hovers over a disabled option. Each option can override this through its `disabledTitle` attribute.
|
|
125
|
+
|
|
126
|
+
1. ```ts
|
|
127
|
+
disabled: boolean = false
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Disable the component. It will still be rendered but users won't be able to interact with it.
|
|
131
|
+
|
|
132
|
+
1. ```ts
|
|
133
|
+
disabledInputTitle: string = 'This input is disabled'
|
|
134
|
+
```
|
|
117
135
|
|
|
118
|
-
|
|
136
|
+
Tooltip text to display on hover when the component is in `disabled` state.
|
|
137
|
+
|
|
138
|
+
1. ```ts
|
|
139
|
+
filterFunc: = (op: Option, searchText: string): boolean => {
|
|
119
140
|
if (!searchText) return true
|
|
120
|
-
return `${op
|
|
141
|
+
return `${get_label(op)}`.toLowerCase().includes(searchText.toLowerCase())
|
|
121
142
|
}
|
|
122
143
|
```
|
|
123
144
|
|
|
124
|
-
|
|
145
|
+
Customize how dropdown options are filtered when user enters search string into `<MultiSelect />`. Defaults to:
|
|
146
|
+
|
|
147
|
+
1. ```ts
|
|
148
|
+
focusInputOnSelect: boolean | 'desktop' = `desktop`
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
One of `true`, `false` or `'desktop'`. Whether to set the cursor back to the input element after selecting an element. 'desktop' means only do so if current window width is larger than the current value of `breakpoint` prop (default 800).
|
|
152
|
+
|
|
153
|
+
1. ```ts
|
|
154
|
+
id: string | null = null
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Applied to the `<input>` element for associating HTML form `<label>`s with this component for accessibility. Also, clicking a `<label>` with same `for` attribute as `id` will focus this component.
|
|
158
|
+
|
|
159
|
+
1. ```ts
|
|
160
|
+
input: HTMLInputElement | null = null
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Handle to the `<input>` DOM node. Only available after component mounts (`null` before then).
|
|
164
|
+
|
|
165
|
+
1. ```ts
|
|
166
|
+
invalid: boolean = false
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
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.
|
|
170
|
+
|
|
171
|
+
1. ```ts
|
|
172
|
+
loading: boolean = false
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Whether the component should display a spinner to indicate it's in loading state. Use `<slot name='spinner'>` to specify a custom spinner.
|
|
176
|
+
|
|
177
|
+
1. ```ts
|
|
178
|
+
matchingOptions: Option[] = []
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
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`.
|
|
182
|
+
|
|
183
|
+
1. ```ts
|
|
184
|
+
maxSelect: number | null = null
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Positive integer to limit the number of options users can pick. `null` means no limit.
|
|
188
|
+
|
|
189
|
+
1. ```ts
|
|
190
|
+
maxSelectMsg: (current: number, max: number): string = null
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Inform users how many of the maximum allowed options they have already selected. Set `maxSelectMsg={null}` to not show a message. Defaults to `null` when `maxSelect={1}` or `maxSelect={null}`. Else if `maxSelect > 1`, defaults to:
|
|
125
194
|
|
|
126
195
|
```ts
|
|
127
196
|
maxSelectMsg = (current: number, max: number) => `${current}/${max}`
|
|
128
197
|
```
|
|
129
198
|
|
|
199
|
+
1. ```ts
|
|
200
|
+
name: string | null = null
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Applied to the `<input>` element. Sets the key of this field in a submitted form data object. Not useful at the moment since the value is stored in Svelte state, not on the `<input>` node.
|
|
204
|
+
|
|
205
|
+
1. ```ts
|
|
206
|
+
noOptionsMsg: string = 'No matching options'
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
What message to show if no options match the user-entered search string.
|
|
210
|
+
|
|
211
|
+
1. ```ts
|
|
212
|
+
open: boolean = false
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Whether the dropdown list is currently visible. Is two-way bindable, i.e. can be used for external control of when the options are visible.
|
|
216
|
+
|
|
217
|
+
1. ```ts
|
|
218
|
+
options: Option[]
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**The only required prop** (no default). Array of strings/numbers or `Option` objects to be listed in the dropdown. The only required key on objects is `label` which must also be unique. An object's `value` defaults to `label` if `undefined`. You can add arbitrary additional keys to your option objects. A few keys like `preselected` and `title` have special meaning though. See `src/lib/index.ts` for all special keys and their purpose.
|
|
222
|
+
|
|
223
|
+
1. ```ts
|
|
224
|
+
outerDiv: HTMLDivElement | null = null
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Handle to outer `<div class="multiselect">` that wraps the whole component. Only available after component mounts (`null` before then).
|
|
228
|
+
|
|
229
|
+
1. ```ts
|
|
230
|
+
parseLabelsAsHtml: boolean = false
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
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).
|
|
234
|
+
|
|
235
|
+
1. ```ts
|
|
236
|
+
placeholder: string | null = null
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
String shown in the text input when no option is selected.
|
|
240
|
+
|
|
241
|
+
1. ```ts
|
|
242
|
+
removeAllTitle: string = 'Remove all'
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Title text to display when user hovers over remove-all button.
|
|
246
|
+
|
|
247
|
+
1. ```ts
|
|
248
|
+
removeBtnTitle: string = 'Remove'
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Title text to display when user hovers over button to remove selected option (which defaults to a cross icon).
|
|
252
|
+
|
|
253
|
+
1. ```ts
|
|
254
|
+
required: boolean = false
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
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.
|
|
258
|
+
|
|
259
|
+
1. ```ts
|
|
260
|
+
searchText: string = ''
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
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.
|
|
264
|
+
|
|
265
|
+
1. ```ts
|
|
266
|
+
selected: Option[] = []
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
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.
|
|
270
|
+
|
|
271
|
+
1. ```ts
|
|
272
|
+
selectedLabels: (string | number)[] = []
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
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`.
|
|
276
|
+
|
|
277
|
+
1. ```ts
|
|
278
|
+
selectedValues: unknown[] = []
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
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`.
|
|
282
|
+
|
|
283
|
+
1. ```ts
|
|
284
|
+
sortSelected: boolean | ((op1: Option, op2: Option) => number) = false
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
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.
|
|
288
|
+
|
|
130
289
|
## Slots
|
|
131
290
|
|
|
132
291
|
`MultiSelect.svelte` has 3 named slots:
|
|
@@ -162,6 +321,8 @@ Example:
|
|
|
162
321
|
|
|
163
322
|
`MultiSelect.svelte` dispatches the following events:
|
|
164
323
|
|
|
324
|
+
<div class="table">
|
|
325
|
+
|
|
165
326
|
| name | detail | description |
|
|
166
327
|
| ----------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
167
328
|
| `add` | `{ option: Option }` | Triggers when a new option is selected. |
|
|
@@ -170,6 +331,8 @@ Example:
|
|
|
170
331
|
| `change` | `type: 'add' \| 'remove' \| 'removeAll'` | Triggers when a option is either added or removed, or all options are removed at once. Payload will be a single or an array of `Option`s, respectively. |
|
|
171
332
|
| `blur` | none | Triggers when the input field looses focus. |
|
|
172
333
|
|
|
334
|
+
</div>
|
|
335
|
+
|
|
173
336
|
Depending on the data passed to the component the `options(s)` payload will either be objects or simple strings/numbers.
|
|
174
337
|
|
|
175
338
|
### Examples
|
|
@@ -192,16 +355,14 @@ TypeScript users can import the types used for internal type safety:
|
|
|
192
355
|
|
|
193
356
|
```svelte
|
|
194
357
|
<script lang="ts">
|
|
195
|
-
import MultiSelect, {
|
|
196
|
-
Option,
|
|
197
|
-
Primitive,
|
|
198
|
-
ProtoOption,
|
|
199
|
-
} from 'svelte-multiselect'
|
|
358
|
+
import MultiSelect, { Option, ObjectOption } from 'svelte-multiselect'
|
|
200
359
|
|
|
201
|
-
const myOptions:
|
|
360
|
+
const myOptions: ObjectOption[] = [
|
|
202
361
|
{ label: 'foo', value: 42 },
|
|
203
362
|
{ label: 'bar', value: 69 },
|
|
204
363
|
]
|
|
364
|
+
// an Option can be string | number | ObjectOption
|
|
365
|
+
const myNumbers: Option[] = [42, 69]
|
|
205
366
|
</script>
|
|
206
367
|
```
|
|
207
368
|
|
|
@@ -224,7 +385,7 @@ There are 3 ways to style this component. To understand which options do what, i
|
|
|
224
385
|
|
|
225
386
|
### With CSS variables
|
|
226
387
|
|
|
227
|
-
If you only want to make small adjustments, you can pass the following CSS variables directly to the component as props or define them in a `:global()` CSS context.
|
|
388
|
+
If you only want to make small adjustments, you can pass the following CSS variables directly to the component as props or define them in a `:global()` CSS context. See [`app.css`](https://github.com/janosh/svelte-multiselect/blob/main/src/app.css) for how these variables are set for the demo site of this component.
|
|
228
389
|
|
|
229
390
|
- `div.multiselect`
|
|
230
391
|
- `border: var(--sms-border, 1pt solid lightgray)`: Change this to e.g. to `1px solid red` to indicate this form field is in an invalid state.
|
|
@@ -234,6 +395,7 @@ If you only want to make small adjustments, you can pass the following CSS varia
|
|
|
234
395
|
- `color: var(--sms-text-color)`
|
|
235
396
|
- `min-height: var(--sms-min-height)`
|
|
236
397
|
- `max-width: var(--sms-max-width)`
|
|
398
|
+
- `margin: var(--sms-margin)`
|
|
237
399
|
- `div.multiselect.open`
|
|
238
400
|
- `z-index: var(--sms-open-z-index, 4)`: Increase this if needed to ensure the dropdown list is displayed atop all other page elements.
|
|
239
401
|
- `div.multiselect:focus-within`
|
|
@@ -391,6 +553,6 @@ To submit a PR, clone the repo, install dependencies and start the dev server to
|
|
|
391
553
|
```sh
|
|
392
554
|
git clone https://github.com/janosh/svelte-multiselect
|
|
393
555
|
cd svelte-multiselect
|
|
394
|
-
|
|
395
|
-
|
|
556
|
+
npm install
|
|
557
|
+
npm run dev
|
|
396
558
|
```
|