svelte-multiselect 4.0.6 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CircleSpinner.svelte +1 -1
- package/MultiSelect.svelte +89 -67
- package/MultiSelect.svelte.d.ts +8 -9
- package/Wiggle.svelte +1 -1
- package/index.d.ts +9 -7
- package/index.js +4 -0
- package/package.json +22 -23
- package/readme.md +54 -56
package/CircleSpinner.svelte
CHANGED
package/MultiSelect.svelte
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
<script
|
|
1
|
+
<script>import { createEventDispatcher } from 'svelte';
|
|
2
|
+
import { get_label, get_value } from './';
|
|
2
3
|
import CircleSpinner from './CircleSpinner.svelte';
|
|
3
|
-
import { CrossIcon,
|
|
4
|
+
import { CrossIcon, DisabledIcon, ExpandIcon } from './icons';
|
|
4
5
|
import Wiggle from './Wiggle.svelte';
|
|
5
|
-
export let selectedLabels = [];
|
|
6
|
-
export let selectedValues = [];
|
|
7
6
|
export let searchText = ``;
|
|
8
7
|
export let showOptions = false;
|
|
9
8
|
export let maxSelect = null; // null means any number of options are selectable
|
|
@@ -11,7 +10,9 @@ export let maxSelectMsg = null;
|
|
|
11
10
|
export let disabled = false;
|
|
12
11
|
export let disabledTitle = `This field is disabled`;
|
|
13
12
|
export let options;
|
|
14
|
-
export let selected =
|
|
13
|
+
export let selected = [];
|
|
14
|
+
export let selectedLabels = [];
|
|
15
|
+
export let selectedValues = [];
|
|
15
16
|
export let input = null;
|
|
16
17
|
export let outerDiv = null;
|
|
17
18
|
export let placeholder = undefined;
|
|
@@ -22,7 +23,7 @@ export let activeOption = null;
|
|
|
22
23
|
export let filterFunc = (op, searchText) => {
|
|
23
24
|
if (!searchText)
|
|
24
25
|
return true;
|
|
25
|
-
return `${op
|
|
26
|
+
return `${get_label(op)}`.toLowerCase().includes(searchText.toLowerCase());
|
|
26
27
|
};
|
|
27
28
|
export let outerDivClass = ``;
|
|
28
29
|
export let ulSelectedClass = ``;
|
|
@@ -35,51 +36,38 @@ export let removeBtnTitle = `Remove`;
|
|
|
35
36
|
export let removeAllTitle = `Remove all`;
|
|
36
37
|
export let defaultDisabledTitle = `This option is disabled`;
|
|
37
38
|
export let allowUserOptions = false;
|
|
39
|
+
export let parseLabelsAsHtml = false; // should not be combined with allowUserOptions!
|
|
38
40
|
export let addOptionMsg = `Create this option...`;
|
|
39
41
|
export let autoScroll = true;
|
|
40
42
|
export let loading = false;
|
|
41
43
|
export let required = false;
|
|
42
44
|
export let autocomplete = `off`;
|
|
43
45
|
export let invalid = false;
|
|
44
|
-
|
|
46
|
+
export let sortSelected = false;
|
|
47
|
+
if (!(options?.length > 0))
|
|
48
|
+
console.error(`MultiSelect received no options`);
|
|
49
|
+
if (parseLabelsAsHtml && allowUserOptions)
|
|
50
|
+
console.warn(`You shouldn't combine parseLabelsAsHtml and allowUserOptions. It's susceptible to XSS attacks!`);
|
|
51
|
+
if (maxSelect !== null && maxSelect < 1) {
|
|
45
52
|
console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
46
53
|
}
|
|
47
|
-
if (!(options?.length > 0))
|
|
48
|
-
console.error(`MultiSelect missing options`);
|
|
49
54
|
if (!Array.isArray(selected))
|
|
50
55
|
console.error(`selected prop must be an array`);
|
|
51
56
|
const dispatch = createEventDispatcher();
|
|
52
57
|
let activeMsg = false; // controls active state of <li>{addOptionMsg}</li>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// process proto options to full ones with mandatory labels
|
|
57
|
-
$: _options = options.map((raw_op) => {
|
|
58
|
-
if (is_object(raw_op)) {
|
|
59
|
-
const option = { ...raw_op };
|
|
60
|
-
if (option.value === undefined)
|
|
61
|
-
option.value = option.label;
|
|
62
|
-
return option;
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
if (![`string`, `number`].includes(typeof raw_op)) {
|
|
66
|
-
console.warn(`MultiSelect options must be objects, strings or numbers, got ${typeof raw_op}`);
|
|
67
|
-
}
|
|
68
|
-
// even if we logged error above, try to proceed hoping user knows what they're doing
|
|
69
|
-
return { label: raw_op, value: raw_op };
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
let wiggle = false;
|
|
73
|
-
$: selectedLabels = selected.map((op) => op.label);
|
|
74
|
-
$: selectedValues = selected.map((op) => op.value);
|
|
58
|
+
let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
|
|
59
|
+
$: selectedLabels = selected.map(get_label);
|
|
60
|
+
$: selectedValues = selected.map(get_value);
|
|
75
61
|
// formValue binds to input.form-control to prevent form submission if required
|
|
76
62
|
// prop is true and no options are selected
|
|
77
63
|
$: formValue = selectedValues.join(`,`);
|
|
78
64
|
$: if (formValue)
|
|
79
65
|
invalid = false; // reset error status whenever component state changes
|
|
80
66
|
// options matching the current search text
|
|
81
|
-
$: matchingOptions =
|
|
82
|
-
|
|
67
|
+
$: matchingOptions = options.filter((op) => filterFunc(op, searchText) &&
|
|
68
|
+
!(op instanceof Object && op.disabled) &&
|
|
69
|
+
!selectedLabels.includes(get_label(op)) // remove already selected options from dropdown list
|
|
70
|
+
);
|
|
83
71
|
// add an option to selected list
|
|
84
72
|
function add(label) {
|
|
85
73
|
if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
|
|
@@ -87,15 +75,19 @@ function add(label) {
|
|
|
87
75
|
// to prevent duplicate selection, we could add `&& !selectedLabels.includes(label)`
|
|
88
76
|
if (maxSelect === null || maxSelect === 1 || selected.length < maxSelect) {
|
|
89
77
|
// first check if we find option in the options list
|
|
90
|
-
let option =
|
|
78
|
+
let option = options.find((op) => get_label(op) === label);
|
|
91
79
|
if (!option && // this has the side-effect of not allowing to user to add the same
|
|
92
80
|
// custom option twice in append mode
|
|
93
81
|
[true, `append`].includes(allowUserOptions) &&
|
|
94
82
|
searchText.length > 0) {
|
|
95
|
-
// user entered text but no options match, so if allowUserOptions=true | 'append', we create
|
|
96
|
-
|
|
83
|
+
// user entered text but no options match, so if allowUserOptions=true | 'append', we create
|
|
84
|
+
// a new option from the user-entered text
|
|
85
|
+
if (typeof options[0] === `string`)
|
|
86
|
+
option = searchText;
|
|
87
|
+
else
|
|
88
|
+
option = { label: searchText, value: searchText };
|
|
97
89
|
if (allowUserOptions === `append`)
|
|
98
|
-
|
|
90
|
+
options = [...options, option];
|
|
99
91
|
}
|
|
100
92
|
searchText = ``; // reset search string on selection
|
|
101
93
|
if (!option) {
|
|
@@ -108,6 +100,16 @@ function add(label) {
|
|
|
108
100
|
}
|
|
109
101
|
else {
|
|
110
102
|
selected = [...selected, option];
|
|
103
|
+
if (sortSelected === true) {
|
|
104
|
+
selected = selected.sort((op1, op2) => {
|
|
105
|
+
const [label1, label2] = [get_label(op1), get_label(op2)];
|
|
106
|
+
// coerce to string if labels are numbers
|
|
107
|
+
return `${label1}`.localeCompare(`${label2}`);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else if (typeof sortSelected === `function`) {
|
|
111
|
+
selected = selected.sort(sortSelected);
|
|
112
|
+
}
|
|
111
113
|
}
|
|
112
114
|
if (selected.length === maxSelect)
|
|
113
115
|
setOptionsVisible(false);
|
|
@@ -123,7 +125,7 @@ function remove(label) {
|
|
|
123
125
|
return;
|
|
124
126
|
selected.splice(selectedLabels.lastIndexOf(label), 1);
|
|
125
127
|
selected = selected; // Svelte rerender after in-place splice
|
|
126
|
-
const option =
|
|
128
|
+
const option = options.find((option) => get_label(option) === label) ??
|
|
127
129
|
// if option with label could not be found but allowUserOptions is truthy,
|
|
128
130
|
// assume it was created by user and create correspondidng option object
|
|
129
131
|
// on the fly for use as event payload
|
|
@@ -159,7 +161,7 @@ async function handleKeydown(event) {
|
|
|
159
161
|
else if (event.key === `Enter`) {
|
|
160
162
|
event.preventDefault(); // prevent enter key from triggering form submission
|
|
161
163
|
if (activeOption) {
|
|
162
|
-
const
|
|
164
|
+
const label = get_label(activeOption);
|
|
163
165
|
selectedLabels.includes(label) ? remove(label) : add(label);
|
|
164
166
|
searchText = ``;
|
|
165
167
|
}
|
|
@@ -175,8 +177,8 @@ async function handleKeydown(event) {
|
|
|
175
177
|
// on up/down arrow keys: update active option
|
|
176
178
|
else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
|
|
177
179
|
// if no option is active yet, but there are matching options, make first one active
|
|
178
|
-
if (activeOption === null &&
|
|
179
|
-
activeOption =
|
|
180
|
+
if (activeOption === null && matchingOptions.length > 0) {
|
|
181
|
+
activeOption = matchingOptions[0];
|
|
180
182
|
return;
|
|
181
183
|
}
|
|
182
184
|
else if (allowUserOptions && searchText.length > 0) {
|
|
@@ -185,24 +187,31 @@ async function handleKeydown(event) {
|
|
|
185
187
|
activeMsg = !activeMsg;
|
|
186
188
|
return;
|
|
187
189
|
}
|
|
190
|
+
else if (activeOption === null) {
|
|
191
|
+
// if no option is active and no options are matching, do nothing
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
188
194
|
const increment = event.key === `ArrowUp` ? -1 : 1;
|
|
189
|
-
const newActiveIdx =
|
|
195
|
+
const newActiveIdx = matchingOptions.indexOf(activeOption) + increment;
|
|
190
196
|
if (newActiveIdx < 0) {
|
|
191
197
|
// wrap around top
|
|
192
|
-
activeOption =
|
|
198
|
+
activeOption = matchingOptions[matchingOptions.length - 1];
|
|
193
199
|
}
|
|
194
|
-
else if (newActiveIdx ===
|
|
200
|
+
else if (newActiveIdx === matchingOptions.length) {
|
|
195
201
|
// wrap around bottom
|
|
196
|
-
activeOption =
|
|
202
|
+
activeOption = matchingOptions[0];
|
|
197
203
|
}
|
|
198
204
|
else {
|
|
199
205
|
// default case: select next/previous in item list
|
|
200
|
-
activeOption =
|
|
206
|
+
activeOption = matchingOptions[newActiveIdx];
|
|
201
207
|
}
|
|
202
208
|
if (autoScroll) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
209
|
+
// TODO This ugly timeout hack is needed to properly scroll element into view when wrapping
|
|
210
|
+
// around start/end of option list. Find a better solution than waiting 10 ms to.
|
|
211
|
+
setTimeout(() => {
|
|
212
|
+
const li = document.querySelector(`ul.options > li.active`);
|
|
213
|
+
li?.scrollIntoView();
|
|
214
|
+
}, 10);
|
|
206
215
|
}
|
|
207
216
|
}
|
|
208
217
|
// on backspace key: remove last selected option
|
|
@@ -210,14 +219,14 @@ async function handleKeydown(event) {
|
|
|
210
219
|
remove(selectedLabels.at(-1));
|
|
211
220
|
}
|
|
212
221
|
}
|
|
213
|
-
|
|
222
|
+
function remove_all() {
|
|
214
223
|
dispatch(`removeAll`, { options: selected });
|
|
215
224
|
dispatch(`change`, { options: selected, type: `removeAll` });
|
|
216
225
|
selected = [];
|
|
217
226
|
searchText = ``;
|
|
218
|
-
}
|
|
227
|
+
}
|
|
219
228
|
$: isSelected = (label) => selectedLabels.includes(label);
|
|
220
|
-
const
|
|
229
|
+
const if_enter_or_space = (handler) => (event) => {
|
|
221
230
|
if ([`Enter`, `Space`].includes(event.code)) {
|
|
222
231
|
event.preventDefault();
|
|
223
232
|
handler();
|
|
@@ -233,8 +242,6 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
|
|
|
233
242
|
}}
|
|
234
243
|
/>
|
|
235
244
|
|
|
236
|
-
<!-- z-index: 2 when showOptions is true ensures the ul.selected of one <MultiSelect />
|
|
237
|
-
display above those of another following shortly after it -->
|
|
238
245
|
<div
|
|
239
246
|
bind:this={outerDiv}
|
|
240
247
|
class:disabled
|
|
@@ -262,14 +269,18 @@ display above those of another following shortly after it -->
|
|
|
262
269
|
{#each selected as option, idx}
|
|
263
270
|
<li class={liSelectedClass} aria-selected="true">
|
|
264
271
|
<slot name="selected" {option} {idx}>
|
|
265
|
-
{
|
|
272
|
+
{#if parseLabelsAsHtml}
|
|
273
|
+
{@html get_label(option)}
|
|
274
|
+
{:else}
|
|
275
|
+
{get_label(option)}
|
|
276
|
+
{/if}
|
|
266
277
|
</slot>
|
|
267
278
|
{#if !disabled}
|
|
268
279
|
<button
|
|
269
|
-
on:mouseup|stopPropagation={() => remove(option
|
|
270
|
-
on:keydown={
|
|
280
|
+
on:mouseup|stopPropagation={() => remove(get_label(option))}
|
|
281
|
+
on:keydown={if_enter_or_space(() => remove(get_label(option)))}
|
|
271
282
|
type="button"
|
|
272
|
-
title="{removeBtnTitle} {option
|
|
283
|
+
title="{removeBtnTitle} {get_label(option)}"
|
|
273
284
|
>
|
|
274
285
|
<CrossIcon width="15px" />
|
|
275
286
|
</button>
|
|
@@ -316,8 +327,8 @@ display above those of another following shortly after it -->
|
|
|
316
327
|
type="button"
|
|
317
328
|
class="remove-all"
|
|
318
329
|
title={removeAllTitle}
|
|
319
|
-
on:mouseup|stopPropagation={
|
|
320
|
-
on:keydown={
|
|
330
|
+
on:mouseup|stopPropagation={remove_all}
|
|
331
|
+
on:keydown={if_enter_or_space(remove_all)}
|
|
321
332
|
>
|
|
322
333
|
<CrossIcon width="15px" />
|
|
323
334
|
</button>
|
|
@@ -326,9 +337,14 @@ display above those of another following shortly after it -->
|
|
|
326
337
|
|
|
327
338
|
<ul class:hidden={!showOptions} class="options {ulOptionsClass}">
|
|
328
339
|
{#each matchingOptions as option, idx}
|
|
329
|
-
{@const {
|
|
330
|
-
|
|
331
|
-
|
|
340
|
+
{@const {
|
|
341
|
+
label,
|
|
342
|
+
disabled = null,
|
|
343
|
+
title = null,
|
|
344
|
+
selectedTitle = null,
|
|
345
|
+
disabledTitle = defaultDisabledTitle,
|
|
346
|
+
} = option instanceof Object ? option : { label: option }}
|
|
347
|
+
{@const active = activeOption && get_label(activeOption) === label}
|
|
332
348
|
<li
|
|
333
349
|
on:mousedown|stopPropagation
|
|
334
350
|
on:mouseup|stopPropagation={() => {
|
|
@@ -350,7 +366,11 @@ display above those of another following shortly after it -->
|
|
|
350
366
|
aria-selected="false"
|
|
351
367
|
>
|
|
352
368
|
<slot name="option" {option} {idx}>
|
|
353
|
-
{
|
|
369
|
+
{#if parseLabelsAsHtml}
|
|
370
|
+
{@html get_label(option)}
|
|
371
|
+
{:else}
|
|
372
|
+
{get_label(option)}
|
|
373
|
+
{/if}
|
|
354
374
|
</slot>
|
|
355
375
|
</li>
|
|
356
376
|
{:else}
|
|
@@ -392,6 +412,8 @@ display above those of another following shortly after it -->
|
|
|
392
412
|
min-height: var(--sms-min-height, 19pt);
|
|
393
413
|
}
|
|
394
414
|
:where(div.multiselect.open) {
|
|
415
|
+
/* increase z-index when open to ensure the dropdown of one <MultiSelect />
|
|
416
|
+
displays above that of another slightly below it on the page */
|
|
395
417
|
z-index: var(--sms-open-z-index, 4);
|
|
396
418
|
}
|
|
397
419
|
:where(div.multiselect:focus-within) {
|
|
@@ -455,7 +477,9 @@ display above those of another following shortly after it -->
|
|
|
455
477
|
cursor: inherit; /* needed for disabled state */
|
|
456
478
|
}
|
|
457
479
|
:where(div.multiselect > ul.selected > li > input)::placeholder {
|
|
480
|
+
padding-left: 5pt;
|
|
458
481
|
color: var(--sms-placeholder-color);
|
|
482
|
+
opacity: var(--sms-placeholder-opacity);
|
|
459
483
|
}
|
|
460
484
|
:where(div.multiselect > input.form-control) {
|
|
461
485
|
width: 2em;
|
|
@@ -470,7 +494,7 @@ display above those of another following shortly after it -->
|
|
|
470
494
|
|
|
471
495
|
:where(div.multiselect > ul.options) {
|
|
472
496
|
list-style: none;
|
|
473
|
-
padding: 0;
|
|
497
|
+
padding: 4pt 0;
|
|
474
498
|
top: 100%;
|
|
475
499
|
left: 0;
|
|
476
500
|
width: 100%;
|
|
@@ -482,8 +506,6 @@ display above those of another following shortly after it -->
|
|
|
482
506
|
overscroll-behavior: var(--sms-options-overscroll, none);
|
|
483
507
|
box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
|
|
484
508
|
transition: all 0.2s;
|
|
485
|
-
opacity: 1;
|
|
486
|
-
transform: translateY(0);
|
|
487
509
|
}
|
|
488
510
|
:where(div.multiselect > ul.options.hidden) {
|
|
489
511
|
visibility: hidden;
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { SvelteComponentTyped } from "svelte";
|
|
2
|
-
import
|
|
2
|
+
import { CustomEvents, Option } from './';
|
|
3
3
|
declare const __propDef: {
|
|
4
4
|
props: {
|
|
5
|
-
selectedLabels?: Primitive[] | undefined;
|
|
6
|
-
selectedValues?: Primitive[] | undefined;
|
|
7
5
|
searchText?: string | undefined;
|
|
8
6
|
showOptions?: boolean | undefined;
|
|
9
7
|
maxSelect?: number | null | undefined;
|
|
10
8
|
maxSelectMsg?: ((current: number, max: number) => string) | null | undefined;
|
|
11
9
|
disabled?: boolean | undefined;
|
|
12
10
|
disabledTitle?: string | undefined;
|
|
13
|
-
options:
|
|
11
|
+
options: Option[];
|
|
14
12
|
selected?: Option[] | undefined;
|
|
13
|
+
selectedLabels?: (string | number)[] | undefined;
|
|
14
|
+
selectedValues?: unknown[] | undefined;
|
|
15
15
|
input?: HTMLInputElement | null | undefined;
|
|
16
16
|
outerDiv?: HTMLDivElement | null | undefined;
|
|
17
17
|
placeholder?: string | undefined;
|
|
@@ -31,17 +31,14 @@ declare const __propDef: {
|
|
|
31
31
|
removeAllTitle?: string | undefined;
|
|
32
32
|
defaultDisabledTitle?: string | undefined;
|
|
33
33
|
allowUserOptions?: boolean | "append" | undefined;
|
|
34
|
+
parseLabelsAsHtml?: boolean | undefined;
|
|
34
35
|
addOptionMsg?: string | undefined;
|
|
35
36
|
autoScroll?: boolean | undefined;
|
|
36
37
|
loading?: boolean | undefined;
|
|
37
38
|
required?: boolean | undefined;
|
|
38
39
|
autocomplete?: string | undefined;
|
|
39
40
|
invalid?: boolean | undefined;
|
|
40
|
-
|
|
41
|
-
events: {
|
|
42
|
-
mousedown: MouseEvent;
|
|
43
|
-
} & {
|
|
44
|
-
[evt: string]: CustomEvent<any>;
|
|
41
|
+
sortSelected?: boolean | ((op1: Option, op2: Option) => number) | undefined;
|
|
45
42
|
};
|
|
46
43
|
slots: {
|
|
47
44
|
selected: {
|
|
@@ -55,6 +52,8 @@ declare const __propDef: {
|
|
|
55
52
|
idx: any;
|
|
56
53
|
};
|
|
57
54
|
};
|
|
55
|
+
getters: {};
|
|
56
|
+
events: CustomEvents;
|
|
58
57
|
};
|
|
59
58
|
export declare type MultiSelectProps = typeof __propDef.props;
|
|
60
59
|
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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { default } from './MultiSelect.svelte';
|
|
2
|
-
export declare type
|
|
3
|
-
export declare type
|
|
4
|
-
label:
|
|
5
|
-
value
|
|
2
|
+
export declare type Option = string | number | ObjectOption;
|
|
3
|
+
export declare type ObjectOption = {
|
|
4
|
+
label: string | number;
|
|
5
|
+
value?: unknown;
|
|
6
6
|
title?: string;
|
|
7
7
|
disabled?: boolean;
|
|
8
8
|
preselected?: boolean;
|
|
@@ -10,9 +10,6 @@ export declare type Option = {
|
|
|
10
10
|
selectedTitle?: string;
|
|
11
11
|
[key: string]: unknown;
|
|
12
12
|
};
|
|
13
|
-
export declare type ProtoOption = Primitive | (Omit<Option, `value`> & {
|
|
14
|
-
value?: Primitive;
|
|
15
|
-
});
|
|
16
13
|
export declare type DispatchEvents = {
|
|
17
14
|
add: {
|
|
18
15
|
option: Option;
|
|
@@ -31,3 +28,8 @@ export declare type DispatchEvents = {
|
|
|
31
28
|
focus: undefined;
|
|
32
29
|
blur: undefined;
|
|
33
30
|
};
|
|
31
|
+
export declare type CustomEvents = {
|
|
32
|
+
[key in keyof DispatchEvents]: CustomEvent<DispatchEvents[key]>;
|
|
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,38 +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": "
|
|
8
|
+
"version": "5.0.2",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"svelte": "index.js",
|
|
11
|
+
"main": "index.js",
|
|
11
12
|
"bugs": "https://github.com/janosh/svelte-multiselect/issues",
|
|
12
13
|
"devDependencies": {
|
|
13
|
-
"@sveltejs/adapter-static": "^1.0.0-next.
|
|
14
|
-
"@sveltejs/kit": "
|
|
15
|
-
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.
|
|
16
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
17
|
-
"@typescript-eslint/parser": "^5.
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"eslint": "^8.12.0",
|
|
21
|
-
"eslint-plugin-svelte3": "^3.4.1",
|
|
14
|
+
"@sveltejs/adapter-static": "^1.0.0-next.34",
|
|
15
|
+
"@sveltejs/kit": "1.0.0-next.345",
|
|
16
|
+
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.49",
|
|
17
|
+
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
|
18
|
+
"@typescript-eslint/parser": "^5.29.0",
|
|
19
|
+
"eslint": "^8.18.0",
|
|
20
|
+
"eslint-plugin-svelte3": "^4.0.0",
|
|
22
21
|
"hastscript": "^7.0.2",
|
|
23
|
-
"jsdom": "^
|
|
24
|
-
"mdsvex": "^0.10.
|
|
25
|
-
"playwright": "^1.
|
|
26
|
-
"prettier": "^2.
|
|
27
|
-
"prettier-plugin-svelte": "^2.
|
|
22
|
+
"jsdom": "^20.0.0",
|
|
23
|
+
"mdsvex": "^0.10.6",
|
|
24
|
+
"playwright": "^1.22.2",
|
|
25
|
+
"prettier": "^2.7.1",
|
|
26
|
+
"prettier-plugin-svelte": "^2.7.0",
|
|
28
27
|
"rehype-autolink-headings": "^6.1.1",
|
|
29
28
|
"rehype-slug": "^5.0.1",
|
|
30
|
-
"svelte": "^3.
|
|
31
|
-
"svelte-check": "^2.
|
|
29
|
+
"svelte": "^3.48.0",
|
|
30
|
+
"svelte-check": "^2.8.0",
|
|
32
31
|
"svelte-github-corner": "^0.1.0",
|
|
33
|
-
"svelte-preprocess": "^4.10.
|
|
32
|
+
"svelte-preprocess": "^4.10.6",
|
|
34
33
|
"svelte-toc": "^0.2.9",
|
|
35
|
-
"svelte2tsx": "^0.5.
|
|
36
|
-
"tslib": "^2.
|
|
37
|
-
"typescript": "^4.
|
|
38
|
-
"vite": "^2.9.
|
|
39
|
-
"vitest": "^0.
|
|
34
|
+
"svelte2tsx": "^0.5.11",
|
|
35
|
+
"tslib": "^2.4.0",
|
|
36
|
+
"typescript": "^4.7.4",
|
|
37
|
+
"vite": "^2.9.12",
|
|
38
|
+
"vitest": "^0.16.0"
|
|
40
39
|
},
|
|
41
40
|
"keywords": [
|
|
42
41
|
"svelte",
|
package/readme.md
CHANGED
|
@@ -22,28 +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 3rd-party dependencies:** needs only Svelte as dev dependency
|
|
26
28
|
- **Dropdowns:** scrollable lists for large numbers of options
|
|
27
29
|
- **Searchable:** start typing to filter options
|
|
28
|
-
- **Tagging:** selected options are
|
|
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
|
-
- v3.0.0 changed the `event.detail` payload for `'add'`, `'remove'` and `'change'` events from `token` to `option`, e.g.
|
|
39
|
-
|
|
40
|
-
```js
|
|
41
|
-
on:add={(e) => console.log(e.detail.token.label)} // v2
|
|
42
|
-
on:add={(e) => console.log(e.detail.option.label)} // v3
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
It also added a separate event type `removeAll` for when the user removes all currently selected options at once which previously fired a normal `remove`. The props `ulTokensClass` and `liTokenClass` were renamed to `ulSelectedClass` and `liSelectedClass`. Similarly, the CSS variable `--sms-token-bg` changed to `--sms-selected-bg`.
|
|
46
|
-
|
|
47
38
|
- v4.0.0 renamed the slots for customizing how selected options and dropdown list items are rendered:
|
|
48
39
|
|
|
49
40
|
- old: `<slot name="renderOptions" />`, new: `<slot name="option" />`
|
|
@@ -53,6 +44,8 @@
|
|
|
53
44
|
|
|
54
45
|
- 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`.
|
|
55
46
|
|
|
47
|
+
- v5.0.0 Support both simple and object options. Previously string or number options were converted to objects internally and returned by `bind:selected`. Now, if you pass in `string[]`, that's what you'll get from `bind:selected`.
|
|
48
|
+
|
|
56
49
|
## Installation
|
|
57
50
|
|
|
58
51
|
```sh
|
|
@@ -84,34 +77,36 @@ Full list of props/bindable variables for this component:
|
|
|
84
77
|
<div class="table">
|
|
85
78
|
|
|
86
79
|
<!-- prettier-ignore -->
|
|
87
|
-
| name | default
|
|
88
|
-
| :--------------------- |
|
|
89
|
-
| `options` | required prop
|
|
90
|
-
| `showOptions` | `false`
|
|
91
|
-
| `searchText` | ``
|
|
92
|
-
| `activeOption` | `null`
|
|
93
|
-
| `maxSelect` | `null`
|
|
94
|
-
| `selected` | `[]`
|
|
95
|
-
| `selectedLabels` | `[]`
|
|
96
|
-
| `selectedValues` | `[]`
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
80
|
+
| name | default | description |
|
|
81
|
+
| :--------------------- | :---------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| `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. |
|
|
83
|
+
| `showOptions` | `false` | Bindable boolean that controls whether the options dropdown is visible. |
|
|
84
|
+
| `searchText` | `` | Text the user-entered to filter down on the list of options. Binds both ways, i.e. can also be used to set the input text. |
|
|
85
|
+
| `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
|
|
86
|
+
| `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
|
|
87
|
+
| `selected` | `[]` | Array of currently selected options. Can be bound to `bind:selected={[1, 2, 3]}` to control component state externally or passed as prop to set pre-selected options that will already be populated when component mounts before any user interaction. |
|
|
88
|
+
| `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
|
+
| `selectedValues` | `[]` | Values of currently selected options. Exposed just for convenience, equivalent to `selected.map(op => op.value)` when options are objects. If options are simple strings, `selected === selectedValues`. Supports binding but is read-only, i.e. since this value is reactive to `selected`, you cannot control `selected` by changing `bind:selectedValues`. |
|
|
90
|
+
| `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
|
+
| `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. |
|
|
92
|
+
| `disabled` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
|
|
93
|
+
| `disabledTitle` | `'This field is disabled'` | Tooltip text to display on hover when the component is in `disabled` state. |
|
|
94
|
+
| `placeholder` | `undefined` | String shown in the text input when no option is selected. |
|
|
95
|
+
| `input` | `null` | Handle to the `<input>` DOM node. Only available after component mounts (`null` before then). |
|
|
96
|
+
| `outerDiv` | `null` | Handle to outer `<div class="multiselect">` that wraps the whole component. Only available after component mounts (`null` before then). |
|
|
97
|
+
| `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. |
|
|
98
|
+
| `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>`. |
|
|
99
|
+
| `required` | `false` | Whether forms can be submitted without selecting any options. Aborts submission, is scrolled into view and shows help "Please fill out" message when true and user tries to submit with no options selected. |
|
|
100
|
+
| `autoScroll` | `true` | `false` disables keeping the active dropdown items in view when going up/down the list of options with arrow keys. |
|
|
101
|
+
| `allowUserOptions` | `false` | Whether users are allowed to enter values not in the dropdown list. `true` means add user-defined options to the selected list only, `'append'` means add to both options and selected. |
|
|
102
|
+
| `parseLabelsAsHtml` | `false` | Whether option labels should be passed to [Svelte's `@html` directive](https://svelte.dev/tutorial/html-tags) or inserted into the DOM as plain text. `true` will raise an error if `allowUserOptions` is also truthy as it makes your site susceptible to [cross-site scripting (XSS) attacks](https://wikipedia.org/wiki/Cross-site_scripting). |
|
|
103
|
+
| `addOptionMsg` | `'Create this option...'` | Message shown to users after entering text when no options match their query and `allowUserOptions` is truthy. |
|
|
104
|
+
| `loading` | `false` | Whether the component should display a spinner to indicate it's in loading state. Use `<slot name='spinner'>` to specify a custom spinner. |
|
|
105
|
+
| `removeBtnTitle` | `'Remove'` | Title text to display when user hovers over button (cross icon) to remove selected option. |
|
|
106
|
+
| `removeAllTitle` | `'Remove all'` | Title text to display when user hovers over remove-all button. |
|
|
107
|
+
| `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. |
|
|
108
|
+
| `autocomplete` | `'off'` | Applied to the `<input>`. Specifies if browser is permitted to auto-fill this form field. See [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) for other admissible values. |
|
|
109
|
+
| `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. |
|
|
115
110
|
|
|
116
111
|
</div>
|
|
117
112
|
|
|
@@ -136,8 +131,8 @@ Full list of props/bindable variables for this component:
|
|
|
136
131
|
|
|
137
132
|
`MultiSelect.svelte` has 3 named slots:
|
|
138
133
|
|
|
139
|
-
- `slot="option"`: Customize rendering of dropdown options. Receives as props
|
|
140
|
-
- `slot="selected"`: Customize rendering selected
|
|
134
|
+
- `slot="option"`: Customize rendering of dropdown options. Receives as props an `option` and the zero-indexed position (`idx`) it has in the dropdown.
|
|
135
|
+
- `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.
|
|
141
136
|
- `slot="spinner"`: Custom spinner component to display when in `loading` state. Receives no props.
|
|
142
137
|
- `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.
|
|
143
138
|
|
|
@@ -165,25 +160,27 @@ Example:
|
|
|
165
160
|
|
|
166
161
|
`MultiSelect.svelte` dispatches the following events:
|
|
167
162
|
|
|
168
|
-
| name | detail | description
|
|
169
|
-
| ----------- | ---------------------------------------- |
|
|
170
|
-
| `add` | `{ option: Option }` | Triggers when a new option is selected.
|
|
171
|
-
| `remove` | `{ option: Option }` | Triggers when one selected option provided as `event.detail.option` is removed.
|
|
172
|
-
| `removeAll` | `options: Option[]` | Triggers when all selected options are removed. The payload `event.detail.options` gives the options that were previously selected.
|
|
173
|
-
| `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
|
|
174
|
-
| `blur` | none | Triggers when the input field looses focus.
|
|
163
|
+
| name | detail | description |
|
|
164
|
+
| ----------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
165
|
+
| `add` | `{ option: Option }` | Triggers when a new option is selected. |
|
|
166
|
+
| `remove` | `{ option: Option }` | Triggers when one selected option provided as `event.detail.option` is removed. |
|
|
167
|
+
| `removeAll` | `options: Option[]` | Triggers when all selected options are removed. The payload `event.detail.options` gives the options that were previously selected. |
|
|
168
|
+
| `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. |
|
|
169
|
+
| `blur` | none | Triggers when the input field looses focus. |
|
|
170
|
+
|
|
171
|
+
Depending on the data passed to the component the `options(s)` payload will either be objects or simple strings/numbers.
|
|
175
172
|
|
|
176
173
|
### Examples
|
|
177
174
|
|
|
178
175
|
<!-- prettier-ignore -->
|
|
179
|
-
- `on:add={(event) => console.log(event.detail.option
|
|
180
|
-
- `on:remove={(event) => console.log(event.detail.option
|
|
181
|
-
- ``on:change={(event) => console.log(`${event.detail.type}: '${event.detail.option
|
|
182
|
-
- `on:blur={
|
|
176
|
+
- `on:add={(event) => console.log(event.detail.option)}`
|
|
177
|
+
- `on:remove={(event) => console.log(event.detail.option)}`.
|
|
178
|
+
- ``on:change={(event) => console.log(`${event.detail.type}: '${event.detail.option}'`)}``
|
|
179
|
+
- `on:blur={myFunction}`
|
|
183
180
|
|
|
184
181
|
```svelte
|
|
185
182
|
<MultiSelect
|
|
186
|
-
on:change={(e) => alert(`You ${e.detail.type}ed '${e.detail.option
|
|
183
|
+
on:change={(e) => alert(`You ${e.detail.type}ed '${e.detail.option}'`)}
|
|
187
184
|
/>
|
|
188
185
|
```
|
|
189
186
|
|
|
@@ -243,6 +240,7 @@ If you only want to make small adjustments, you can pass the following CSS varia
|
|
|
243
240
|
- `background: var(--sms-disabled-bg, lightgray)`: Background when in disabled state.
|
|
244
241
|
- `div.multiselect input::placeholder`
|
|
245
242
|
- `color: var(--sms-placeholder-color)`
|
|
243
|
+
- `color: var(--sms-placeholder-opacity)`
|
|
246
244
|
- `div.multiselect > ul.selected > li`
|
|
247
245
|
- `background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15))`: Background of selected options.
|
|
248
246
|
- `padding: var(--sms-selected-li-padding, 5pt 1pt)`: Height of selected options.
|