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.
- package/CircleSpinner.svelte +1 -1
- package/MultiSelect.svelte +111 -69
- package/MultiSelect.svelte.d.ts +5 -2
- package/Wiggle.svelte +1 -1
- package/index.d.ts +3 -1
- package/index.js +4 -0
- package/package.json +21 -23
- package/readme.md +17 -17
package/CircleSpinner.svelte
CHANGED
package/MultiSelect.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<script
|
|
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 (!(
|
|
50
|
-
console.error(`
|
|
51
|
-
|
|
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
|
|
84
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
$:
|
|
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
|
-
{
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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={() =>
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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;
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SvelteComponentTyped } from "svelte";
|
|
2
|
-
import {
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
"@
|
|
15
|
-
"@sveltejs/
|
|
16
|
-
"@sveltejs/
|
|
17
|
-
"@
|
|
18
|
-
"@typescript-eslint/
|
|
19
|
-
"@
|
|
20
|
-
"
|
|
21
|
-
"eslint": "^
|
|
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": "^
|
|
25
|
-
"mdsvex": "^0.10.
|
|
26
|
-
"
|
|
27
|
-
"prettier": "^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.
|
|
32
|
-
"svelte-check": "^2.
|
|
29
|
+
"svelte": "^3.48.0",
|
|
30
|
+
"svelte-check": "^2.8.0",
|
|
33
31
|
"svelte-github-corner": "^0.1.0",
|
|
34
|
-
"svelte-preprocess": "^4.10.
|
|
32
|
+
"svelte-preprocess": "^4.10.6",
|
|
35
33
|
"svelte-toc": "^0.2.9",
|
|
36
|
-
"svelte2tsx": "^0.5.
|
|
37
|
-
"tslib": "^2.
|
|
38
|
-
"typescript": "^4.
|
|
39
|
-
"vite": "^2.9.
|
|
40
|
-
"vitest": "^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
|
|
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
|
-
- **
|
|
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
|
|
29
|
-
- **
|
|
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
|
|
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`
|
|
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 (
|
|
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/
|
|
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
|
|
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/
|
|
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.
|