svelte-multiselect 8.6.2 → 10.0.0
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/dist/CmdPalette.svelte +11 -17
- package/dist/CmdPalette.svelte.d.ts +3 -4
- package/dist/MultiSelect.svelte +82 -33
- package/dist/MultiSelect.svelte.d.ts +25 -4
- package/package.json +24 -23
- package/readme.md +55 -27
package/dist/CmdPalette.svelte
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
|
-
<script
|
|
2
|
-
// https://github.com/sveltejs/eslint-plugin-svelte3/issues/201
|
|
3
|
-
import { tick } from 'svelte';
|
|
1
|
+
<script>import { tick } from 'svelte';
|
|
4
2
|
import { fade } from 'svelte/transition';
|
|
5
|
-
import Select from '.';
|
|
3
|
+
import Select from './MultiSelect.svelte';
|
|
6
4
|
export let actions;
|
|
7
|
-
export let
|
|
5
|
+
export let triggers = [`k`];
|
|
6
|
+
export let close_keys = [`Escape`];
|
|
8
7
|
export let fade_duration = 200; // in ms
|
|
9
8
|
export let style = ``; // for dialog
|
|
10
|
-
// for span in option slot, has no effect when passing slot
|
|
9
|
+
// for span in option slot, has no effect when passing a slot
|
|
11
10
|
export let span_style = ``;
|
|
12
11
|
export let open = false;
|
|
13
12
|
export let dialog = null;
|
|
14
13
|
export let input = null;
|
|
15
14
|
export let placeholder = `Filter actions...`;
|
|
16
15
|
async function toggle(event) {
|
|
17
|
-
if (event.key
|
|
16
|
+
if (triggers.includes(event.key) && event.metaKey && !open) {
|
|
18
17
|
// open on cmd+trigger
|
|
19
18
|
open = true;
|
|
20
19
|
await tick(); // wait for dialog to open and input to be mounted
|
|
21
20
|
input?.focus();
|
|
22
21
|
}
|
|
23
|
-
else if (event.key
|
|
24
|
-
// close on escape
|
|
22
|
+
else if (close_keys.includes(event.key) && open) {
|
|
25
23
|
open = false;
|
|
26
24
|
}
|
|
27
25
|
}
|
|
@@ -39,22 +37,18 @@ function run_and_close(event) {
|
|
|
39
37
|
<svelte:window on:keydown={toggle} on:click={close_if_outside} />
|
|
40
38
|
|
|
41
39
|
{#if open}
|
|
42
|
-
<dialog
|
|
43
|
-
class:open
|
|
44
|
-
bind:this={dialog}
|
|
45
|
-
transition:fade={{ duration: fade_duration }}
|
|
46
|
-
{style}
|
|
47
|
-
>
|
|
40
|
+
<dialog open bind:this={dialog} transition:fade={{ duration: fade_duration }} {style}>
|
|
48
41
|
<Select
|
|
49
42
|
options={actions}
|
|
50
43
|
bind:input
|
|
51
44
|
{placeholder}
|
|
52
45
|
on:add={run_and_close}
|
|
53
46
|
on:keydown={toggle}
|
|
54
|
-
{...$$
|
|
47
|
+
{...$$restProps}
|
|
48
|
+
let:option
|
|
55
49
|
>
|
|
56
50
|
<!-- wait for https://github.com/sveltejs/svelte/pull/8304 -->
|
|
57
|
-
<slot
|
|
51
|
+
<slot>
|
|
58
52
|
<span style={span_style}>{option.label}</span>
|
|
59
53
|
</slot>
|
|
60
54
|
</Select>
|
|
@@ -6,7 +6,8 @@ declare const __propDef: {
|
|
|
6
6
|
label: string;
|
|
7
7
|
action: () => void;
|
|
8
8
|
}[];
|
|
9
|
-
|
|
9
|
+
triggers?: string[] | undefined;
|
|
10
|
+
close_keys?: string[] | undefined;
|
|
10
11
|
fade_duration?: number | undefined;
|
|
11
12
|
style?: string | undefined;
|
|
12
13
|
span_style?: string | undefined;
|
|
@@ -19,9 +20,7 @@ declare const __propDef: {
|
|
|
19
20
|
[evt: string]: CustomEvent<any>;
|
|
20
21
|
};
|
|
21
22
|
slots: {
|
|
22
|
-
|
|
23
|
-
slot: string;
|
|
24
|
-
};
|
|
23
|
+
default: {};
|
|
25
24
|
};
|
|
26
25
|
};
|
|
27
26
|
export type CmdPaletteProps = typeof __propDef.props;
|
package/dist/MultiSelect.svelte
CHANGED
|
@@ -14,15 +14,16 @@ export let breakpoint = 800; // any screen with more horizontal pixels is consid
|
|
|
14
14
|
export let defaultDisabledTitle = `This option is disabled`;
|
|
15
15
|
export let disabled = false;
|
|
16
16
|
export let disabledInputTitle = `This input is disabled`;
|
|
17
|
-
// case-insensitive equality comparison after string coercion (looking only at the `label` key of object options)
|
|
18
17
|
// prettier-ignore
|
|
19
|
-
export let duplicateFunc = (op1, op2) => `${get_label(op1)}`.toLowerCase() === `${get_label(op2)}`.toLowerCase();
|
|
20
18
|
export let duplicateOptionMsg = `This option is already selected`;
|
|
21
19
|
export let duplicates = false; // whether to allow duplicate options
|
|
22
|
-
|
|
20
|
+
// takes two options and returns true if they are equal
|
|
21
|
+
// case-insensitive equality comparison after string coercion and looks only at the `label` key of object options by default
|
|
22
|
+
export let key = (opt) => `${get_label(opt)}`.toLowerCase();
|
|
23
|
+
export let filterFunc = (opt, searchText) => {
|
|
23
24
|
if (!searchText)
|
|
24
25
|
return true;
|
|
25
|
-
return `${get_label(
|
|
26
|
+
return `${get_label(opt)}`.toLowerCase().includes(searchText.toLowerCase());
|
|
26
27
|
};
|
|
27
28
|
export let focusInputOnSelect = `desktop`;
|
|
28
29
|
export let form_input = null;
|
|
@@ -37,6 +38,7 @@ export let liOptionClass = ``;
|
|
|
37
38
|
export let liSelectedClass = ``;
|
|
38
39
|
export let loading = false;
|
|
39
40
|
export let matchingOptions = [];
|
|
41
|
+
export let maxOptions = undefined;
|
|
40
42
|
export let maxSelect = null; // null means there is no upper limit for selected.length
|
|
41
43
|
export let maxSelectMsg = (current, max) => (max > 1 ? `${current}/${max}` : ``);
|
|
42
44
|
export let maxSelectMsgClass = ``;
|
|
@@ -56,7 +58,7 @@ export let required = false;
|
|
|
56
58
|
export let resetFilterOnAdd = true;
|
|
57
59
|
export let searchText = ``;
|
|
58
60
|
export let selected = options
|
|
59
|
-
?.filter((
|
|
61
|
+
?.filter((opt) => opt instanceof Object && opt?.preselected)
|
|
60
62
|
.slice(0, maxSelect ?? undefined) ?? [];
|
|
61
63
|
export let sortSelected = false;
|
|
62
64
|
export let selectedOptionsDraggable = !sortSelected;
|
|
@@ -64,14 +66,14 @@ export let ulOptionsClass = ``;
|
|
|
64
66
|
export let ulSelectedClass = ``;
|
|
65
67
|
export let value = null;
|
|
66
68
|
// get the label key from an option object or the option itself if it's a string or number
|
|
67
|
-
export const get_label = (
|
|
68
|
-
if (
|
|
69
|
-
if (
|
|
70
|
-
console.error(`MultiSelect option ${JSON.stringify(
|
|
69
|
+
export const get_label = (opt) => {
|
|
70
|
+
if (opt instanceof Object) {
|
|
71
|
+
if (opt.label === undefined) {
|
|
72
|
+
console.error(`MultiSelect option ${JSON.stringify(opt)} is an object but has no label key`);
|
|
71
73
|
}
|
|
72
|
-
return
|
|
74
|
+
return opt.label;
|
|
73
75
|
}
|
|
74
|
-
return
|
|
76
|
+
return `${opt}`;
|
|
75
77
|
};
|
|
76
78
|
// if maxSelect=1, value is the single item in selected (or null if selected is empty)
|
|
77
79
|
// this solves both https://github.com/janosh/svelte-multiselect/issues/86 and
|
|
@@ -108,12 +110,17 @@ if (allowUserOptions && !createOptionMsg && createOptionMsg !== null) {
|
|
|
108
110
|
console.error(`MultiSelect has allowUserOptions=${allowUserOptions} but createOptionMsg=${createOptionMsg} is falsy. ` +
|
|
109
111
|
`This prevents the "Add option" <span> from showing up, resulting in a confusing user experience.`);
|
|
110
112
|
}
|
|
113
|
+
if (maxOptions &&
|
|
114
|
+
(typeof maxOptions != `number` || maxOptions < 0 || maxOptions % 1 != 0)) {
|
|
115
|
+
console.error(`MultiSelect's maxOptions must be undefined or a positive integer, got ${maxOptions}`);
|
|
116
|
+
}
|
|
111
117
|
const dispatch = createEventDispatcher();
|
|
112
118
|
let option_msg_is_active = false; // controls active state of <li>{createOptionMsg}</li>
|
|
113
119
|
let window_width;
|
|
114
120
|
// options matching the current search text
|
|
115
|
-
$: matchingOptions = options.filter((
|
|
116
|
-
|
|
121
|
+
$: matchingOptions = options.filter((opt) => filterFunc(opt, searchText) &&
|
|
122
|
+
// remove already selected options from dropdown list unless duplicate selections are allowed
|
|
123
|
+
(!selected.map(key).includes(key(opt)) || duplicates));
|
|
117
124
|
// raise if matchingOptions[activeIndex] does not yield a value
|
|
118
125
|
if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
119
126
|
throw `Run time error, activeIndex=${activeIndex} is out of bounds, matchingOptions.length=${matchingOptions.length}`;
|
|
@@ -127,7 +134,7 @@ function add(option, event) {
|
|
|
127
134
|
if (!isNaN(Number(option)) && typeof selected.map(get_label)[0] === `number`) {
|
|
128
135
|
option = Number(option); // convert to number if possible
|
|
129
136
|
}
|
|
130
|
-
const is_duplicate = selected.
|
|
137
|
+
const is_duplicate = selected.map(key).includes(key(option));
|
|
131
138
|
if ((maxSelect === null || maxSelect === 1 || selected.length < maxSelect) &&
|
|
132
139
|
(duplicates || !is_duplicate)) {
|
|
133
140
|
if (!options.includes(option) && // first check if we find option in the options list
|
|
@@ -194,7 +201,7 @@ function add(option, event) {
|
|
|
194
201
|
function remove(to_remove) {
|
|
195
202
|
if (selected.length === 0)
|
|
196
203
|
return;
|
|
197
|
-
const idx = selected.findIndex((
|
|
204
|
+
const idx = selected.findIndex((opt) => key(opt) === key(to_remove));
|
|
198
205
|
let [option] = selected.splice(idx, 1); // remove option from selected list
|
|
199
206
|
if (option === undefined && allowUserOptions) {
|
|
200
207
|
// if option with label could not be found but allowUserOptions is truthy,
|
|
@@ -339,6 +346,7 @@ const dragstart = (idx) => (event) => {
|
|
|
339
346
|
event.dataTransfer.setData(`text/plain`, `${idx}`);
|
|
340
347
|
};
|
|
341
348
|
let ul_options;
|
|
349
|
+
// highlight text matching user-entered search text in available options
|
|
342
350
|
function highlight_matching_options(event) {
|
|
343
351
|
if (!highlightMatches || typeof CSS == `undefined` || !CSS.highlights)
|
|
344
352
|
return; // don't try if CSS highlight API not supported
|
|
@@ -404,7 +412,10 @@ function highlight_matching_options(event) {
|
|
|
404
412
|
on:mouseup|stopPropagation={open_dropdown}
|
|
405
413
|
title={disabled ? disabledInputTitle : null}
|
|
406
414
|
data-id={id}
|
|
415
|
+
role="searchbox"
|
|
416
|
+
tabindex="-1"
|
|
407
417
|
>
|
|
418
|
+
<!-- form control input invisible to the user, only purpose is to abort form submission if this component fails data validation -->
|
|
408
419
|
<!-- bind:value={selected} prevents form submission if required prop is true and no options are selected -->
|
|
409
420
|
<input
|
|
410
421
|
{name}
|
|
@@ -432,9 +443,11 @@ function highlight_matching_options(event) {
|
|
|
432
443
|
<ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt; cursor: pointer;" />
|
|
433
444
|
</slot>
|
|
434
445
|
<ul class="selected {ulSelectedClass}" aria-label="selected options">
|
|
435
|
-
{#each selected as option, idx (option)}
|
|
446
|
+
{#each selected as option, idx (duplicates ? [key(option), idx] : key(option))}
|
|
436
447
|
<li
|
|
437
448
|
class={liSelectedClass}
|
|
449
|
+
role="option"
|
|
450
|
+
aria-selected="true"
|
|
438
451
|
animate:flip={{ duration: 100 }}
|
|
439
452
|
draggable={selectedOptionsDraggable && !disabled && selected.length > 1}
|
|
440
453
|
on:dragstart={dragstart(idx)}
|
|
@@ -445,10 +458,13 @@ function highlight_matching_options(event) {
|
|
|
445
458
|
>
|
|
446
459
|
<!-- on:dragover|preventDefault needed for the drop to succeed https://stackoverflow.com/a/31085796 -->
|
|
447
460
|
<slot name="selected" {option} {idx}>
|
|
448
|
-
{
|
|
449
|
-
{
|
|
450
|
-
|
|
451
|
-
{
|
|
461
|
+
<slot {option} {idx}>
|
|
462
|
+
{#if parseLabelsAsHtml}
|
|
463
|
+
{@html get_label(option)}
|
|
464
|
+
{:else}
|
|
465
|
+
{get_label(option)}
|
|
466
|
+
{/if}
|
|
467
|
+
</slot>
|
|
452
468
|
</slot>
|
|
453
469
|
{#if !disabled && (minSelect === null || selected.length > minSelect)}
|
|
454
470
|
<button
|
|
@@ -496,6 +512,16 @@ function highlight_matching_options(event) {
|
|
|
496
512
|
on:touchstart
|
|
497
513
|
/>
|
|
498
514
|
<!-- the above on:* lines forward potentially useful DOM events -->
|
|
515
|
+
<slot
|
|
516
|
+
name="after-input"
|
|
517
|
+
{selected}
|
|
518
|
+
{disabled}
|
|
519
|
+
{invalid}
|
|
520
|
+
{id}
|
|
521
|
+
{placeholder}
|
|
522
|
+
{open}
|
|
523
|
+
{required}
|
|
524
|
+
/>
|
|
499
525
|
</ul>
|
|
500
526
|
{#if loading}
|
|
501
527
|
<slot name="spinner">
|
|
@@ -529,10 +555,18 @@ function highlight_matching_options(event) {
|
|
|
529
555
|
{/if}
|
|
530
556
|
{/if}
|
|
531
557
|
|
|
532
|
-
<!-- only render options dropdown if options or searchText is not empty needed to avoid briefly flashing empty dropdown -->
|
|
558
|
+
<!-- only render options dropdown if options or searchText is not empty (needed to avoid briefly flashing empty dropdown) -->
|
|
533
559
|
{#if (searchText && noMatchingOptionsMsg) || options?.length > 0}
|
|
534
|
-
<ul
|
|
535
|
-
{
|
|
560
|
+
<ul
|
|
561
|
+
class:hidden={!open}
|
|
562
|
+
class="options {ulOptionsClass}"
|
|
563
|
+
role="listbox"
|
|
564
|
+
aria-multiselectable={maxSelect === null || maxSelect > 1}
|
|
565
|
+
aria-expanded={open}
|
|
566
|
+
aria-disabled={disabled ? `true` : null}
|
|
567
|
+
bind:this={ul_options}
|
|
568
|
+
>
|
|
569
|
+
{#each matchingOptions.slice(0, Math.max(0, maxOptions ?? 0) || Infinity) as option, idx}
|
|
536
570
|
{@const {
|
|
537
571
|
label,
|
|
538
572
|
disabled = null,
|
|
@@ -561,21 +595,25 @@ function highlight_matching_options(event) {
|
|
|
561
595
|
}}
|
|
562
596
|
on:mouseout={() => (activeIndex = null)}
|
|
563
597
|
on:blur={() => (activeIndex = null)}
|
|
598
|
+
role="option"
|
|
599
|
+
aria-selected="false"
|
|
564
600
|
>
|
|
565
601
|
<slot name="option" {option} {idx}>
|
|
566
|
-
{
|
|
567
|
-
{
|
|
568
|
-
|
|
569
|
-
{
|
|
570
|
-
|
|
602
|
+
<slot {option} {idx}>
|
|
603
|
+
{#if parseLabelsAsHtml}
|
|
604
|
+
{@html get_label(option)}
|
|
605
|
+
{:else}
|
|
606
|
+
{get_label(option)}
|
|
607
|
+
{/if}
|
|
608
|
+
</slot>
|
|
571
609
|
</slot>
|
|
572
610
|
</li>
|
|
573
611
|
{:else}
|
|
574
|
-
{@const
|
|
575
|
-
|
|
576
|
-
|
|
612
|
+
{@const textInputIsDuplicate = selected.map(get_label).includes(searchText)}
|
|
613
|
+
<!-- set msg to duplicateOptionMsg if duplicates are not allowed and the user-entered
|
|
614
|
+
searchText is a duplicate, else set to createOptionMsg -->
|
|
577
615
|
{@const msg =
|
|
578
|
-
!duplicates &&
|
|
616
|
+
!duplicates && textInputIsDuplicate ? duplicateOptionMsg : createOptionMsg}
|
|
579
617
|
{#if allowUserOptions && searchText && msg}
|
|
580
618
|
<li
|
|
581
619
|
on:mousedown|stopPropagation
|
|
@@ -586,9 +624,20 @@ function highlight_matching_options(event) {
|
|
|
586
624
|
on:focus={() => (option_msg_is_active = true)}
|
|
587
625
|
on:mouseout={() => (option_msg_is_active = false)}
|
|
588
626
|
on:blur={() => (option_msg_is_active = false)}
|
|
627
|
+
role="option"
|
|
628
|
+
aria-selected="false"
|
|
589
629
|
class="user-msg"
|
|
590
630
|
>
|
|
591
|
-
|
|
631
|
+
<slot
|
|
632
|
+
name="user-msg"
|
|
633
|
+
{duplicateOptionMsg}
|
|
634
|
+
{createOptionMsg}
|
|
635
|
+
{textInputIsDuplicate}
|
|
636
|
+
{searchText}
|
|
637
|
+
{msg}
|
|
638
|
+
>
|
|
639
|
+
{msg}
|
|
640
|
+
</slot>
|
|
592
641
|
</li>
|
|
593
642
|
{:else if noMatchingOptionsMsg}
|
|
594
643
|
<!-- use span to not have cursor: pointer -->
|
|
@@ -13,10 +13,10 @@ declare class __sveltets_Render<Option extends T> {
|
|
|
13
13
|
defaultDisabledTitle?: string | undefined;
|
|
14
14
|
disabled?: boolean | undefined;
|
|
15
15
|
disabledInputTitle?: string | undefined;
|
|
16
|
-
duplicateFunc?: ((op1: T, op2: T) => boolean) | undefined;
|
|
17
16
|
duplicateOptionMsg?: string | undefined;
|
|
18
17
|
duplicates?: boolean | undefined;
|
|
19
|
-
|
|
18
|
+
key?: ((opt: T) => unknown) | undefined;
|
|
19
|
+
filterFunc?: ((opt: Option, searchText: string) => boolean) | undefined;
|
|
20
20
|
focusInputOnSelect?: boolean | "desktop" | undefined;
|
|
21
21
|
form_input?: HTMLInputElement | null | undefined;
|
|
22
22
|
highlightMatches?: boolean | undefined;
|
|
@@ -30,6 +30,7 @@ declare class __sveltets_Render<Option extends T> {
|
|
|
30
30
|
liSelectedClass?: string | undefined;
|
|
31
31
|
loading?: boolean | undefined;
|
|
32
32
|
matchingOptions?: Option[] | undefined;
|
|
33
|
+
maxOptions?: number | undefined;
|
|
33
34
|
maxSelect?: number | null | undefined;
|
|
34
35
|
maxSelectMsg?: ((current: number, max: number) => string) | null | undefined;
|
|
35
36
|
maxSelectMsgClass?: string | undefined;
|
|
@@ -54,7 +55,7 @@ declare class __sveltets_Render<Option extends T> {
|
|
|
54
55
|
ulOptionsClass?: string | undefined;
|
|
55
56
|
ulSelectedClass?: string | undefined;
|
|
56
57
|
value?: Option | Option[] | null | undefined;
|
|
57
|
-
get_label?: ((
|
|
58
|
+
get_label?: ((opt: T) => string | number) | undefined;
|
|
58
59
|
};
|
|
59
60
|
events(): MultiSelectEvents;
|
|
60
61
|
slots(): {
|
|
@@ -65,19 +66,39 @@ declare class __sveltets_Render<Option extends T> {
|
|
|
65
66
|
option: Option;
|
|
66
67
|
idx: any;
|
|
67
68
|
};
|
|
69
|
+
default: {
|
|
70
|
+
option: Option;
|
|
71
|
+
idx: any;
|
|
72
|
+
};
|
|
68
73
|
'remove-icon': {};
|
|
74
|
+
'after-input': {
|
|
75
|
+
selected: Option[];
|
|
76
|
+
disabled: boolean;
|
|
77
|
+
invalid: boolean;
|
|
78
|
+
id: string | null;
|
|
79
|
+
placeholder: string | null;
|
|
80
|
+
open: boolean;
|
|
81
|
+
required: number | boolean;
|
|
82
|
+
};
|
|
69
83
|
spinner: {};
|
|
70
84
|
'disabled-icon': {};
|
|
71
85
|
option: {
|
|
72
86
|
option: Option;
|
|
73
87
|
idx: any;
|
|
74
88
|
};
|
|
89
|
+
'user-msg': {
|
|
90
|
+
duplicateOptionMsg: string;
|
|
91
|
+
createOptionMsg: string | null;
|
|
92
|
+
textInputIsDuplicate: any;
|
|
93
|
+
searchText: string;
|
|
94
|
+
msg: any;
|
|
95
|
+
};
|
|
75
96
|
};
|
|
76
97
|
}
|
|
77
98
|
export type MultiSelectProps<Option extends T> = ReturnType<__sveltets_Render<Option>['props']>;
|
|
78
99
|
export type MultiSelectEvents<Option extends T> = ReturnType<__sveltets_Render<Option>['events']>;
|
|
79
100
|
export type MultiSelectSlots<Option extends T> = ReturnType<__sveltets_Render<Option>['slots']>;
|
|
80
101
|
export default class MultiSelect<Option extends T> extends SvelteComponentTyped<MultiSelectProps<Option>, MultiSelectEvents<Option>, MultiSelectSlots<Option>> {
|
|
81
|
-
get get_label(): (
|
|
102
|
+
get get_label(): (opt: T) => string | number;
|
|
82
103
|
}
|
|
83
104
|
export {};
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"homepage": "https://janosh.github.io/svelte-multiselect",
|
|
6
6
|
"repository": "https://github.com/janosh/svelte-multiselect",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"version": "
|
|
8
|
+
"version": "10.0.0",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"svelte": "./dist/index.js",
|
|
11
11
|
"bugs": "https://github.com/janosh/svelte-multiselect/issues",
|
|
@@ -19,41 +19,41 @@
|
|
|
19
19
|
"test": "vitest --run --coverage tests/unit/*.ts && playwright test tests/*.test.ts",
|
|
20
20
|
"test:unit": "vitest tests/unit/*.ts",
|
|
21
21
|
"test:e2e": "playwright test tests/*.test.ts",
|
|
22
|
-
"changelog": "npx auto-changelog --package --output changelog.md --
|
|
22
|
+
"changelog": "npx auto-changelog --package --output changelog.md --hide-empty-releases --hide-credit --commit-limit false",
|
|
23
23
|
"update-coverage": "vitest tests/unit --run --coverage && npx istanbul-badges-readme"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"svelte": "^
|
|
26
|
+
"svelte": "^4.0.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@iconify/svelte": "^3.1.
|
|
30
|
-
"@playwright/test": "^1.
|
|
29
|
+
"@iconify/svelte": "^3.1.4",
|
|
30
|
+
"@playwright/test": "^1.35.1",
|
|
31
31
|
"@sveltejs/adapter-static": "^2.0.2",
|
|
32
|
-
"@sveltejs/kit": "^1.
|
|
33
|
-
"@sveltejs/package": "2.0
|
|
34
|
-
"@sveltejs/vite-plugin-svelte": "
|
|
35
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
36
|
-
"@typescript-eslint/parser": "^5.
|
|
37
|
-
"@vitest/coverage-
|
|
38
|
-
"eslint": "^8.
|
|
39
|
-
"eslint-plugin-
|
|
32
|
+
"@sveltejs/kit": "^1.21.0",
|
|
33
|
+
"@sveltejs/package": "2.1.0",
|
|
34
|
+
"@sveltejs/vite-plugin-svelte": "2.4.2",
|
|
35
|
+
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
|
36
|
+
"@typescript-eslint/parser": "^5.60.1",
|
|
37
|
+
"@vitest/coverage-v8": "^0.32.2",
|
|
38
|
+
"eslint": "^8.44.0",
|
|
39
|
+
"eslint-plugin-svelte": "^2.32.2",
|
|
40
40
|
"hastscript": "^7.2.0",
|
|
41
41
|
"highlight.js": "^11.8.0",
|
|
42
|
-
"jsdom": "^22.
|
|
43
|
-
"mdsvex": "^0.
|
|
42
|
+
"jsdom": "^22.1.0",
|
|
43
|
+
"mdsvex": "^0.11.0",
|
|
44
44
|
"mdsvexamples": "^0.3.3",
|
|
45
45
|
"prettier": "^2.8.8",
|
|
46
|
-
"prettier-plugin-svelte": "^2.10.
|
|
46
|
+
"prettier-plugin-svelte": "^2.10.1",
|
|
47
47
|
"rehype-autolink-headings": "^6.1.1",
|
|
48
48
|
"rehype-slug": "^5.1.0",
|
|
49
|
-
"svelte-check": "^3.
|
|
50
|
-
"svelte-preprocess": "^5.0.
|
|
49
|
+
"svelte-check": "^3.4.4",
|
|
50
|
+
"svelte-preprocess": "^5.0.4",
|
|
51
51
|
"svelte-toc": "^0.5.5",
|
|
52
|
-
"svelte-zoo": "^0.4.
|
|
53
|
-
"svelte2tsx": "^0.6.
|
|
54
|
-
"typescript": "5.
|
|
55
|
-
"vite": "^4.3.
|
|
56
|
-
"vitest": "^0.
|
|
52
|
+
"svelte-zoo": "^0.4.8",
|
|
53
|
+
"svelte2tsx": "^0.6.16",
|
|
54
|
+
"typescript": "5.1.6",
|
|
55
|
+
"vite": "^4.3.9",
|
|
56
|
+
"vitest": "^0.32.2"
|
|
57
57
|
},
|
|
58
58
|
"keywords": [
|
|
59
59
|
"svelte",
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"default": "./dist/index.js"
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
|
+
"types": "./dist/index.d.ts",
|
|
80
81
|
"files": [
|
|
81
82
|
"dist"
|
|
82
83
|
]
|
package/readme.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://github.com/janosh/svelte-multiselect/actions/workflows/test.yml)
|
|
9
9
|
[](https://github.com/janosh/svelte-multiselect/actions/workflows/gh-pages.yml)
|
|
10
10
|
[](https://npmjs.com/package/svelte-multiselect)
|
|
11
|
-
[](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
|
|
11
|
+
[](https://github.com/sveltejs/svelte/blob/master/packages/svelte/CHANGELOG.md)
|
|
12
12
|
[](https://svelte.dev/repl/a5a14b8f15d64cb083b567292480db05)
|
|
13
13
|
[](https://stackblitz.com/github/janosh/svelte-multiselect)
|
|
14
14
|
|
|
@@ -36,16 +36,37 @@
|
|
|
36
36
|
|
|
37
37
|
## 🧪   Coverage
|
|
38
38
|
|
|
39
|
-
| Statements | Branches
|
|
40
|
-
| ------------------------------------------------------------------------------------------ |
|
|
41
|
-
|  |  |  |
|
|
42
42
|
|
|
43
43
|
## 📜   Breaking changes
|
|
44
44
|
|
|
45
|
-
- **8.0.0
|
|
45
|
+
- **8.0.0** (2022-10-22)
|
|
46
46
|
- Props `selectedLabels` and `selectedValues` were removed. If you were using them, they were equivalent to assigning `bind:selected` to a local variable and then running `selectedLabels = selected.map(option => option.label)` and `selectedValues = selected.map(option => option.value)` if your options were objects with `label` and `value` keys. If they were simple strings/numbers, there was no point in using `selected{Labels,Values}` anyway. [PR 138](https://github.com/janosh/svelte-multiselect/pull/138)
|
|
47
47
|
- Prop `noOptionsMsg` was renamed to `noMatchingOptionsMsg`. [PR 133](https://github.com/janosh/svelte-multiselect/pull/133).
|
|
48
|
-
- **v8.3.0
|
|
48
|
+
- **v8.3.0** (2023-01-25) `addOptionMsg` was renamed to `createOptionMsg` (no major since version since it's rarely used) [sha](https://github.com/janosh/svelte-multiselect/commits).
|
|
49
|
+
- **v9.0.0** (2023-06-01) Svelte bumped from v3 to v4. Also, not breaking but noteworthy: MultiSelect received a default slot that functions as both `"option"` and `"selected"`. If you previously had two identical slots for `"option"` and `"selected"`, you can now remove the `name` from one of them and drop the other:
|
|
50
|
+
|
|
51
|
+
```diff
|
|
52
|
+
<MultiSelect
|
|
53
|
+
{options}
|
|
54
|
+
+ let:option
|
|
55
|
+
>
|
|
56
|
+
- <SlotComponent let:option {option} slot="selected" />
|
|
57
|
+
- <SlotComponent let:option {option} slot="option" />
|
|
58
|
+
+ <SlotComponent {option} />
|
|
59
|
+
</MultiSelect>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- **v10.0.0** (2023-06-23) `duplicateFunc()` renamed to `key` in [#238](https://github.com/janosh/svelte-multiselect/pull/238). Signature changed:
|
|
63
|
+
|
|
64
|
+
```diff
|
|
65
|
+
- duplicateFunc: (op1: T, op2: T) => boolean = (op1, op2) => `${get_label(op1)}`.toLowerCase() === `${get_label(op2)}`.toLowerCase()
|
|
66
|
+
+ key: (opt: T) => unknown = (opt) => `${get_label(opt)}`.toLowerCase()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Rather than implementing custom equality in `duplicateFunc`, the `key` function is now expected to map options to a unique identifier. `key(op1) === key(op2)` should mean `op1` and `op2` are the same option. `key` can return any type but usually best to return primitives (`string`, `number`, ...) for Svelte keyed each blocks (see [#217](https://github.com/janosh/svelte-multiselect/pull/217)).
|
|
49
70
|
|
|
50
71
|
## 🔨   Installation
|
|
51
72
|
|
|
@@ -144,30 +165,29 @@ Full list of props/bindable variables for this component. The `Option` type you
|
|
|
144
165
|
|
|
145
166
|
Tooltip text to display on hover when the component is in `disabled` state.
|
|
146
167
|
|
|
147
|
-
<!-- prettier-ignore -->
|
|
148
168
|
1. ```ts
|
|
149
|
-
|
|
150
|
-
`${get_label(op1)}`.toLowerCase() === `${get_label(op2)}`.toLowerCase()
|
|
169
|
+
duplicates: boolean = false
|
|
151
170
|
```
|
|
152
171
|
|
|
153
|
-
|
|
172
|
+
Whether to allow users to select duplicate options. Applies only to the selected item list, not the options dropdown. Keeping that free of duplicates is left to developer. The selected item list can have duplicates if `allowUserOptions` is truthy, `duplicates` is `true` and users create the same option multiple times. Use `duplicateOptionMsg` to customize the message shown to user if `duplicates` is `false` and users attempt this and `key` to customize when a pair of options is considered equal.
|
|
154
173
|
|
|
155
174
|
1. ```ts
|
|
156
|
-
|
|
175
|
+
duplicateOptionMsg: string = `This option is already selected`
|
|
157
176
|
```
|
|
158
177
|
|
|
159
|
-
|
|
178
|
+
Text to display to users when `allowUserOptions` is truthy and they try to create a new option that's already selected.
|
|
160
179
|
|
|
180
|
+
<!-- prettier-ignore -->
|
|
161
181
|
1. ```ts
|
|
162
|
-
|
|
182
|
+
key: (opt: T) => unknown = (opt) => `${get_label(opt)}`.toLowerCase()
|
|
163
183
|
```
|
|
164
184
|
|
|
165
|
-
|
|
185
|
+
A function that maps options to a value by which equality of options is determined. Defaults to mapping options to their lower-cased label. E.g. by default ``const opt1 = { label: `foo`, id: 1 }`` and ``const opt2 = { label: `foo`, id: 2 }`` are considered equal. If you want to consider them different, you can set `key` to e.g. `key={(opt) => opt.id}` or ``key={(opt) => `${opt.label}-${opt.id}}`` or even `key={JSON.stringify}`.
|
|
166
186
|
|
|
167
187
|
1. ```ts
|
|
168
|
-
filterFunc = (
|
|
188
|
+
filterFunc = (opt: Option, searchText: string): boolean => {
|
|
169
189
|
if (!searchText) return true
|
|
170
|
-
return `${get_label(
|
|
190
|
+
return `${get_label(opt)}`.toLowerCase().includes(searchText.toLowerCase())
|
|
171
191
|
}
|
|
172
192
|
```
|
|
173
193
|
|
|
@@ -189,7 +209,7 @@ Full list of props/bindable variables for this component. The `Option` type you
|
|
|
189
209
|
highlightMatches: boolean = true
|
|
190
210
|
```
|
|
191
211
|
|
|
192
|
-
Whether to highlight text in the dropdown options that matches the current user-entered search query. Uses the [CSS Custom Highlight API](https://developer.mozilla.org/docs/Web/API/CSS_Custom_Highlight_API) with limited browser support and [styling options](https://developer.mozilla.org/docs/Web/CSS/::highlight). See `::highlight(sms-search-matches)` below for available CSS variables.
|
|
212
|
+
Whether to highlight text in the dropdown options that matches the current user-entered search query. Uses the [CSS Custom Highlight API](https://developer.mozilla.org/docs/Web/API/CSS_Custom_Highlight_API) with [limited browser support](https://caniuse.com/mdn-api_css_highlights) (70% as of May 2023) and [styling options](https://developer.mozilla.org/docs/Web/CSS/::highlight). See `::highlight(sms-search-matches)` below for available CSS variables.
|
|
193
213
|
|
|
194
214
|
1. ```ts
|
|
195
215
|
id: string | null = null
|
|
@@ -207,7 +227,7 @@ Full list of props/bindable variables for this component. The `Option` type you
|
|
|
207
227
|
inputmode: string | null = null
|
|
208
228
|
```
|
|
209
229
|
|
|
210
|
-
The `inputmode` attribute hints at the type of data the user may enter. Values like `'numeric' | 'tel' | 'email'` allow browsers to display an appropriate virtual keyboard. See [MDN](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/inputmode) for details.
|
|
230
|
+
The `inputmode` attribute hints at the type of data the user may enter. Values like `'numeric' | 'tel' | 'email'` allow mobile browsers to display an appropriate virtual on-screen keyboard. See [MDN](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/inputmode) for details. If you want to suppress the on-screen keyboard to leave full-screen real estate for the dropdown list of options, set `inputmode="none"`.
|
|
211
231
|
|
|
212
232
|
1. ```ts
|
|
213
233
|
invalid: boolean = false
|
|
@@ -227,6 +247,12 @@ Full list of props/bindable variables for this component. The `Option` type you
|
|
|
227
247
|
|
|
228
248
|
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`.
|
|
229
249
|
|
|
250
|
+
1. ```ts
|
|
251
|
+
maxOptions: number | undefined = undefined
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Positive integer to limit the number of options displayed in the dropdown. `undefined` and 0 mean no limit.
|
|
255
|
+
|
|
230
256
|
1. ```ts
|
|
231
257
|
maxSelect: number | null = null
|
|
232
258
|
```
|
|
@@ -371,18 +397,20 @@ Full list of props/bindable variables for this component. The `Option` type you
|
|
|
371
397
|
1. `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.
|
|
372
398
|
1. `slot="expand-icon"`: Allows setting a custom icon to indicate to users that the Multiselect text input field is expandable into a dropdown list. Receives prop `open: boolean` which is true if the Multiselect dropdown is visible and false if it's hidden.
|
|
373
399
|
1. `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.
|
|
400
|
+
1. `slot="user-msg"`: Displayed like a dropdown item when the list is empty and user is allowed to create custom options based on text input (or if the user's text input clashes with an existing option). `let:props`:
|
|
401
|
+
- `duplicateOptionMsg: string`: See [props](#🔣-props).
|
|
402
|
+
- `createOptionMsg: string`: See [props](#🔣-props).
|
|
403
|
+
- `textInputIsDuplicate: boolean`: Whether user has typed text that matches an already existing option.
|
|
404
|
+
- `searchText: string`: The text user typed into search input.
|
|
405
|
+
- `msg: string`: `duplicateOptionMsg` if user input is a duplicate else `createOptionMsg`.
|
|
406
|
+
1. `slot='after-input'`: ForPlaced after the search input. For arbitrary content like icons or temporary messages. Receives props `selected`, `disabled`, `invalid`, `id`, `placeholder`, `open`, `required`.
|
|
374
407
|
|
|
375
|
-
Example:
|
|
408
|
+
Example using several slots:
|
|
376
409
|
|
|
377
410
|
```svelte
|
|
378
|
-
<MultiSelect options={[`Red`, `Green`, `Blue`, `Yellow`, `Purple`]}>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
{option.label}
|
|
382
|
-
<span style:background={option.label} style=" width: 1em; height: 1em;" />
|
|
383
|
-
</span>
|
|
384
|
-
|
|
385
|
-
<span let:idx let:option slot="selected">
|
|
411
|
+
<MultiSelect options={[`Red`, `Green`, `Blue`, `Yellow`, `Purple`]} let:idx let:option>
|
|
412
|
+
<!-- default slot overrides rendering of both dropdown-listed and selected options -->
|
|
413
|
+
<span>
|
|
386
414
|
{idx + 1}
|
|
387
415
|
{option.label}
|
|
388
416
|
<span style:background={option.label} style=" width: 1em; height: 1em;" />
|