svelte-multiselect 4.0.4 → 5.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/MultiSelect.svelte +141 -106
- package/MultiSelect.svelte.d.ts +8 -6
- package/index.d.ts +4 -7
- package/package.json +16 -15
- package/readme.md +49 -52
package/MultiSelect.svelte
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
<script >import { createEventDispatcher,
|
|
2
|
-
import
|
|
1
|
+
<script >import { createEventDispatcher, tick } from 'svelte';
|
|
2
|
+
import './';
|
|
3
3
|
import CircleSpinner from './CircleSpinner.svelte';
|
|
4
|
-
import { CrossIcon,
|
|
4
|
+
import { CrossIcon, DisabledIcon, ExpandIcon } from './icons';
|
|
5
5
|
import Wiggle from './Wiggle.svelte';
|
|
6
|
-
export let selected = [];
|
|
7
|
-
export let selectedLabels = [];
|
|
8
|
-
export let selectedValues = [];
|
|
9
6
|
export let searchText = ``;
|
|
10
7
|
export let showOptions = false;
|
|
11
8
|
export let maxSelect = null; // null means any number of options are selectable
|
|
@@ -13,6 +10,9 @@ export let maxSelectMsg = null;
|
|
|
13
10
|
export let disabled = false;
|
|
14
11
|
export let disabledTitle = `This field is disabled`;
|
|
15
12
|
export let options;
|
|
13
|
+
export let selected = [];
|
|
14
|
+
export let selectedLabels = [];
|
|
15
|
+
export let selectedValues = [];
|
|
16
16
|
export let input = null;
|
|
17
17
|
export let outerDiv = null;
|
|
18
18
|
export let placeholder = undefined;
|
|
@@ -23,7 +23,7 @@ export let activeOption = null;
|
|
|
23
23
|
export let filterFunc = (op, searchText) => {
|
|
24
24
|
if (!searchText)
|
|
25
25
|
return true;
|
|
26
|
-
return `${op
|
|
26
|
+
return `${get_label(op)}`.toLowerCase().includes(searchText.toLowerCase());
|
|
27
27
|
};
|
|
28
28
|
export let outerDivClass = ``;
|
|
29
29
|
export let ulSelectedClass = ``;
|
|
@@ -36,88 +36,99 @@ export let removeBtnTitle = `Remove`;
|
|
|
36
36
|
export let removeAllTitle = `Remove all`;
|
|
37
37
|
export let defaultDisabledTitle = `This option is disabled`;
|
|
38
38
|
export let allowUserOptions = false;
|
|
39
|
+
export let addOptionMsg = `Create this option...`;
|
|
39
40
|
export let autoScroll = true;
|
|
40
41
|
export let loading = false;
|
|
41
42
|
export let required = false;
|
|
42
43
|
export let autocomplete = `off`;
|
|
43
44
|
export let invalid = false;
|
|
44
|
-
|
|
45
|
+
export let sortSelected = false;
|
|
46
|
+
if (maxSelect !== null && maxSelect < 1) {
|
|
45
47
|
console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
46
48
|
}
|
|
47
49
|
if (!(options?.length > 0))
|
|
48
|
-
console.error(`MultiSelect missing options`);
|
|
50
|
+
console.error(`MultiSelect is missing options`);
|
|
49
51
|
if (!Array.isArray(selected))
|
|
50
52
|
console.error(`selected prop must be an array`);
|
|
51
|
-
onMount(() => {
|
|
52
|
-
selected = _options.filter((op) => op?.preselected) ?? [];
|
|
53
|
-
});
|
|
54
53
|
const dispatch = createEventDispatcher();
|
|
55
|
-
|
|
56
|
-
return typeof item === `object` && !Array.isArray(item) && item !== null;
|
|
57
|
-
}
|
|
54
|
+
let activeMsg = false; // controls active state of <li>{addOptionMsg}</li>
|
|
58
55
|
// process proto options to full ones with mandatory labels
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return option;
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
if (![`string`, `number`].includes(typeof rawOp)) {
|
|
68
|
-
console.warn(`MultiSelect options must be objects, strings or numbers, got ${typeof rawOp}`);
|
|
69
|
-
}
|
|
70
|
-
// even if we logged error above, try to proceed hoping user knows what they're doing
|
|
71
|
-
return { label: rawOp, value: rawOp };
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
$: labels = _options.map((op) => op.label);
|
|
75
|
-
$: if (new Set(labels).size !== options.length) {
|
|
76
|
-
console.warn(`Option labels should be unique. Duplicates found: ${labels.filter((label, idx) => labels.indexOf(label) !== idx)}`);
|
|
77
|
-
}
|
|
78
|
-
let wiggle = false;
|
|
79
|
-
$: selectedLabels = selected.map((op) => op.label);
|
|
80
|
-
$: selectedValues = selected.map((op) => op.value);
|
|
56
|
+
const get_value = (option) => (option instanceof Object ? option.value : option);
|
|
57
|
+
const get_label = (option) => (option instanceof Object ? option.label : option);
|
|
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);
|
|
81
61
|
// formValue binds to input.form-control to prevent form submission if required
|
|
82
62
|
// prop is true and no options are selected
|
|
83
63
|
$: formValue = selectedValues.join(`,`);
|
|
84
64
|
$: if (formValue)
|
|
85
65
|
invalid = false; // reset error status whenever component state changes
|
|
86
66
|
// options matching the current search text
|
|
87
|
-
$: matchingOptions =
|
|
88
|
-
|
|
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
|
+
);
|
|
71
|
+
// add an option to selected list
|
|
89
72
|
function add(label) {
|
|
90
73
|
if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
|
|
91
74
|
wiggle = true;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
75
|
+
// to prevent duplicate selection, we could add `&& !selectedLabels.includes(label)`
|
|
76
|
+
if (maxSelect === null || maxSelect === 1 || selected.length < maxSelect) {
|
|
77
|
+
// first check if we find option in the options list
|
|
78
|
+
let option = options.find((op) => get_value(op) === label);
|
|
79
|
+
if (!option && // this has the side-effect of not allowing to user to add the same
|
|
80
|
+
// custom option twice in append mode
|
|
81
|
+
[true, `append`].includes(allowUserOptions) &&
|
|
82
|
+
searchText.length > 0) {
|
|
83
|
+
// user entered text but no options match, so if allowUserOptions=true | 'append', we create new option
|
|
84
|
+
option = { label: searchText, value: searchText };
|
|
85
|
+
if (allowUserOptions === `append`)
|
|
86
|
+
options = [...options, option];
|
|
87
|
+
}
|
|
95
88
|
searchText = ``; // reset search string on selection
|
|
96
|
-
const option = _options.find((op) => op.label === label);
|
|
97
89
|
if (!option) {
|
|
98
90
|
console.error(`MultiSelect: option with label ${label} not found`);
|
|
99
91
|
return;
|
|
100
92
|
}
|
|
101
93
|
if (maxSelect === 1) {
|
|
94
|
+
// for maxselect = 1 we always replace current option with new one
|
|
102
95
|
selected = [option];
|
|
103
96
|
}
|
|
104
97
|
else {
|
|
105
|
-
selected = [
|
|
98
|
+
selected = [...selected, option];
|
|
99
|
+
if (sortSelected === true) {
|
|
100
|
+
selected = selected.sort((op1, op2) => {
|
|
101
|
+
const [label1, label2] = [get_label(op1), get_label(op2)];
|
|
102
|
+
// coerce to string if labels are numbers
|
|
103
|
+
return `${label1}`.localeCompare(`${label2}`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else if (typeof sortSelected === `function`) {
|
|
107
|
+
selected = selected.sort(sortSelected);
|
|
108
|
+
}
|
|
106
109
|
}
|
|
107
110
|
if (selected.length === maxSelect)
|
|
108
111
|
setOptionsVisible(false);
|
|
112
|
+
else
|
|
113
|
+
input?.focus();
|
|
109
114
|
dispatch(`add`, { option });
|
|
110
115
|
dispatch(`change`, { option, type: `add` });
|
|
111
116
|
}
|
|
112
117
|
}
|
|
118
|
+
// remove an option from selected list
|
|
113
119
|
function remove(label) {
|
|
114
120
|
if (selected.length === 0)
|
|
115
121
|
return;
|
|
116
|
-
|
|
122
|
+
selected.splice(selectedLabels.lastIndexOf(label), 1);
|
|
123
|
+
selected = selected; // Svelte rerender after in-place splice
|
|
124
|
+
const option = options.find((option) => get_label(option) === label) ??
|
|
125
|
+
// if option with label could not be found but allowUserOptions is truthy,
|
|
126
|
+
// assume it was created by user and create correspondidng option object
|
|
127
|
+
// on the fly for use as event payload
|
|
128
|
+
(allowUserOptions && { label, value: label });
|
|
117
129
|
if (!option) {
|
|
118
130
|
return console.error(`MultiSelect: option with label ${label} not found`);
|
|
119
131
|
}
|
|
120
|
-
selected = selected.filter((option) => label !== option.label);
|
|
121
132
|
dispatch(`remove`, { option });
|
|
122
133
|
dispatch(`change`, { option, type: `remove` });
|
|
123
134
|
}
|
|
@@ -144,16 +155,15 @@ async function handleKeydown(event) {
|
|
|
144
155
|
}
|
|
145
156
|
// on enter key: toggle active option and reset search text
|
|
146
157
|
else if (event.key === `Enter`) {
|
|
158
|
+
event.preventDefault(); // prevent enter key from triggering form submission
|
|
147
159
|
if (activeOption) {
|
|
148
|
-
const
|
|
160
|
+
const label = get_label(activeOption);
|
|
149
161
|
selectedLabels.includes(label) ? remove(label) : add(label);
|
|
150
162
|
searchText = ``;
|
|
151
163
|
}
|
|
152
|
-
else if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
options = [...options, { label: searchText, value: searchText }];
|
|
156
|
-
searchText = ``;
|
|
164
|
+
else if (allowUserOptions && searchText.length > 0) {
|
|
165
|
+
// user entered text but no options match, so if allowUserOptions is truthy, we create new option
|
|
166
|
+
add(searchText);
|
|
157
167
|
}
|
|
158
168
|
// no active option and no search text means the options dropdown is closed
|
|
159
169
|
// in which case enter means open it
|
|
@@ -162,24 +172,34 @@ async function handleKeydown(event) {
|
|
|
162
172
|
}
|
|
163
173
|
// on up/down arrow keys: update active option
|
|
164
174
|
else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
|
|
165
|
-
if
|
|
166
|
-
|
|
167
|
-
activeOption =
|
|
175
|
+
// if no option is active yet, but there are matching options, make first one active
|
|
176
|
+
if (activeOption === null && matchingOptions.length > 0) {
|
|
177
|
+
activeOption = matchingOptions[0];
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
else if (allowUserOptions && searchText.length > 0) {
|
|
181
|
+
// if allowUserOptions is truthy and user entered text but no options match, we make
|
|
182
|
+
// <li>{addUserMsg}</li> active on keydown (or toggle it if already active)
|
|
183
|
+
activeMsg = !activeMsg;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
else if (activeOption === null) {
|
|
187
|
+
// if no option is active and no options are matching, do nothing
|
|
168
188
|
return;
|
|
169
189
|
}
|
|
170
190
|
const increment = event.key === `ArrowUp` ? -1 : 1;
|
|
171
|
-
const newActiveIdx =
|
|
191
|
+
const newActiveIdx = matchingOptions.indexOf(activeOption) + increment;
|
|
172
192
|
if (newActiveIdx < 0) {
|
|
173
193
|
// wrap around top
|
|
174
|
-
activeOption =
|
|
194
|
+
activeOption = matchingOptions[matchingOptions.length - 1];
|
|
175
195
|
}
|
|
176
|
-
else if (newActiveIdx ===
|
|
196
|
+
else if (newActiveIdx === matchingOptions.length) {
|
|
177
197
|
// wrap around bottom
|
|
178
|
-
activeOption =
|
|
198
|
+
activeOption = matchingOptions[0];
|
|
179
199
|
}
|
|
180
200
|
else {
|
|
181
201
|
// default case: select next/previous in item list
|
|
182
|
-
activeOption =
|
|
202
|
+
activeOption = matchingOptions[newActiveIdx];
|
|
183
203
|
}
|
|
184
204
|
if (autoScroll) {
|
|
185
205
|
await tick();
|
|
@@ -188,10 +208,8 @@ async function handleKeydown(event) {
|
|
|
188
208
|
}
|
|
189
209
|
}
|
|
190
210
|
// on backspace key: remove last selected option
|
|
191
|
-
else if (event.key === `Backspace`) {
|
|
192
|
-
|
|
193
|
-
if (label && !searchText)
|
|
194
|
-
remove(label);
|
|
211
|
+
else if (event.key === `Backspace` && selectedLabels.length > 0 && !searchText) {
|
|
212
|
+
remove(selectedLabels.at(-1));
|
|
195
213
|
}
|
|
196
214
|
}
|
|
197
215
|
const removeAll = () => {
|
|
@@ -217,8 +235,6 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
|
|
|
217
235
|
}}
|
|
218
236
|
/>
|
|
219
237
|
|
|
220
|
-
<!-- z-index: 2 when showOptions is true ensures the ul.selected of one <MultiSelect />
|
|
221
|
-
display above those of another following shortly after it -->
|
|
222
238
|
<div
|
|
223
239
|
bind:this={outerDiv}
|
|
224
240
|
class:disabled
|
|
@@ -246,14 +262,14 @@ display above those of another following shortly after it -->
|
|
|
246
262
|
{#each selected as option, idx}
|
|
247
263
|
<li class={liSelectedClass} aria-selected="true">
|
|
248
264
|
<slot name="selected" {option} {idx}>
|
|
249
|
-
{option
|
|
265
|
+
{get_label(option)}
|
|
250
266
|
</slot>
|
|
251
267
|
{#if !disabled}
|
|
252
268
|
<button
|
|
253
|
-
on:mouseup|stopPropagation={() => remove(option
|
|
254
|
-
on:keydown={handleEnterAndSpaceKeys(() => remove(option
|
|
269
|
+
on:mouseup|stopPropagation={() => remove(get_label(option))}
|
|
270
|
+
on:keydown={handleEnterAndSpaceKeys(() => remove(get_label(option)))}
|
|
255
271
|
type="button"
|
|
256
|
-
title="{removeBtnTitle} {option
|
|
272
|
+
title="{removeBtnTitle} {get_label(option)}"
|
|
257
273
|
>
|
|
258
274
|
<CrossIcon width="15px" />
|
|
259
275
|
</button>
|
|
@@ -308,48 +324,60 @@ display above those of another following shortly after it -->
|
|
|
308
324
|
{/if}
|
|
309
325
|
{/if}
|
|
310
326
|
|
|
311
|
-
{
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
327
|
+
<ul class:hidden={!showOptions} class="options {ulOptionsClass}">
|
|
328
|
+
{#each matchingOptions as option, idx}
|
|
329
|
+
{@const {
|
|
330
|
+
label,
|
|
331
|
+
disabled = null,
|
|
332
|
+
title = null,
|
|
333
|
+
selectedTitle = null,
|
|
334
|
+
disabledTitle = defaultDisabledTitle,
|
|
335
|
+
} = option instanceof Object ? option : { label: option }}
|
|
336
|
+
{@const active = activeOption && get_label(activeOption) === label}
|
|
337
|
+
<li
|
|
338
|
+
on:mousedown|stopPropagation
|
|
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}
|
|
321
363
|
<li
|
|
322
|
-
on:
|
|
323
|
-
on:
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
class:disabled
|
|
331
|
-
class="{liOptionClass} {active ? liActiveOptionClass : ``}"
|
|
332
|
-
on:mouseover={() => {
|
|
333
|
-
if (disabled) return
|
|
334
|
-
activeOption = option
|
|
335
|
-
}}
|
|
336
|
-
on:focus={() => {
|
|
337
|
-
if (disabled) return
|
|
338
|
-
activeOption = option
|
|
339
|
-
}}
|
|
340
|
-
on:mouseout={() => (activeOption = null)}
|
|
341
|
-
on:blur={() => (activeOption = null)}
|
|
364
|
+
on:mousedown|stopPropagation
|
|
365
|
+
on:mouseup|stopPropagation={() => add(searchText)}
|
|
366
|
+
title={addOptionMsg}
|
|
367
|
+
class:active={activeMsg}
|
|
368
|
+
on:mouseover={() => (activeMsg = true)}
|
|
369
|
+
on:focus={() => (activeMsg = true)}
|
|
370
|
+
on:mouseout={() => (activeMsg = false)}
|
|
371
|
+
on:blur={() => (activeMsg = false)}
|
|
342
372
|
aria-selected="false"
|
|
343
373
|
>
|
|
344
|
-
|
|
345
|
-
{option.label}
|
|
346
|
-
</slot>
|
|
374
|
+
{addOptionMsg}
|
|
347
375
|
</li>
|
|
348
376
|
{:else}
|
|
349
377
|
<span>{noOptionsMsg}</span>
|
|
350
|
-
{/
|
|
351
|
-
|
|
352
|
-
|
|
378
|
+
{/if}
|
|
379
|
+
{/each}
|
|
380
|
+
</ul>
|
|
353
381
|
</div>
|
|
354
382
|
|
|
355
383
|
<style>
|
|
@@ -369,6 +397,8 @@ display above those of another following shortly after it -->
|
|
|
369
397
|
min-height: var(--sms-min-height, 19pt);
|
|
370
398
|
}
|
|
371
399
|
:where(div.multiselect.open) {
|
|
400
|
+
/* increase z-index when open to ensure the dropdown of one <MultiSelect />
|
|
401
|
+
displays above that of another slightly below it on the page */
|
|
372
402
|
z-index: var(--sms-open-z-index, 4);
|
|
373
403
|
}
|
|
374
404
|
:where(div.multiselect:focus-within) {
|
|
@@ -458,9 +488,14 @@ display above those of another following shortly after it -->
|
|
|
458
488
|
max-height: var(--sms-options-max-height, 50vh);
|
|
459
489
|
overscroll-behavior: var(--sms-options-overscroll, none);
|
|
460
490
|
box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
|
|
491
|
+
transition: all 0.2s;
|
|
492
|
+
opacity: 1;
|
|
493
|
+
transform: translateY(0);
|
|
461
494
|
}
|
|
462
495
|
:where(div.multiselect > ul.options.hidden) {
|
|
463
496
|
visibility: hidden;
|
|
497
|
+
opacity: 0;
|
|
498
|
+
transform: translateY(50px);
|
|
464
499
|
}
|
|
465
500
|
:where(div.multiselect > ul.options > li) {
|
|
466
501
|
padding: 3pt 2ex;
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { SvelteComponentTyped } from "svelte";
|
|
2
|
-
import
|
|
2
|
+
import { Option } from './';
|
|
3
3
|
declare const __propDef: {
|
|
4
4
|
props: {
|
|
5
|
-
selected?: Option[] | undefined;
|
|
6
|
-
selectedLabels?: Primitive[] | undefined;
|
|
7
|
-
selectedValues?: Primitive[] | undefined;
|
|
8
5
|
searchText?: string | undefined;
|
|
9
6
|
showOptions?: boolean | undefined;
|
|
10
7
|
maxSelect?: number | null | undefined;
|
|
11
8
|
maxSelectMsg?: ((current: number, max: number) => string) | null | undefined;
|
|
12
9
|
disabled?: boolean | undefined;
|
|
13
10
|
disabledTitle?: string | undefined;
|
|
14
|
-
options:
|
|
11
|
+
options: Option[];
|
|
12
|
+
selected?: Option[] | undefined;
|
|
13
|
+
selectedLabels?: (string | number)[] | undefined;
|
|
14
|
+
selectedValues?: (string | number)[] | undefined;
|
|
15
15
|
input?: HTMLInputElement | null | undefined;
|
|
16
16
|
outerDiv?: HTMLDivElement | null | undefined;
|
|
17
17
|
placeholder?: string | undefined;
|
|
@@ -31,14 +31,16 @@ declare const __propDef: {
|
|
|
31
31
|
removeAllTitle?: string | undefined;
|
|
32
32
|
defaultDisabledTitle?: string | undefined;
|
|
33
33
|
allowUserOptions?: boolean | "append" | undefined;
|
|
34
|
+
addOptionMsg?: string | undefined;
|
|
34
35
|
autoScroll?: boolean | undefined;
|
|
35
36
|
loading?: boolean | undefined;
|
|
36
37
|
required?: boolean | undefined;
|
|
37
38
|
autocomplete?: string | undefined;
|
|
38
39
|
invalid?: boolean | undefined;
|
|
40
|
+
sortSelected?: boolean | ((op1: Option, op2: Option) => number) | undefined;
|
|
39
41
|
};
|
|
40
42
|
events: {
|
|
41
|
-
|
|
43
|
+
mousedown: MouseEvent;
|
|
42
44
|
} & {
|
|
43
45
|
[evt: string]: CustomEvent<any>;
|
|
44
46
|
};
|
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: string | number;
|
|
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;
|
package/package.json
CHANGED
|
@@ -5,37 +5,38 @@
|
|
|
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.0",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"svelte": "index.js",
|
|
11
11
|
"bugs": "https://github.com/janosh/svelte-multiselect/issues",
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@sveltejs/adapter-static": "^1.0.0-next.29",
|
|
14
|
-
"@sveltejs/kit": "^1.0.0-next.
|
|
15
|
-
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.
|
|
16
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
17
|
-
"@typescript-eslint/parser": "^5.
|
|
18
|
-
"@vitest/ui": "^0.
|
|
19
|
-
"
|
|
14
|
+
"@sveltejs/kit": "^1.0.0-next.308",
|
|
15
|
+
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.41",
|
|
16
|
+
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
|
17
|
+
"@typescript-eslint/parser": "^5.18.0",
|
|
18
|
+
"@vitest/ui": "^0.9.0",
|
|
19
|
+
"c8": "^7.11.0",
|
|
20
|
+
"eslint": "^8.12.0",
|
|
20
21
|
"eslint-plugin-svelte3": "^3.4.1",
|
|
21
22
|
"hastscript": "^7.0.2",
|
|
22
23
|
"jsdom": "^19.0.0",
|
|
23
24
|
"mdsvex": "^0.10.5",
|
|
24
|
-
"playwright": "^1.20.
|
|
25
|
-
"prettier": "^2.6.
|
|
25
|
+
"playwright": "^1.20.2",
|
|
26
|
+
"prettier": "^2.6.2",
|
|
26
27
|
"prettier-plugin-svelte": "^2.6.0",
|
|
27
28
|
"rehype-autolink-headings": "^6.1.1",
|
|
28
29
|
"rehype-slug": "^5.0.1",
|
|
29
|
-
"svelte": "^3.46.
|
|
30
|
+
"svelte": "^3.46.6",
|
|
30
31
|
"svelte-check": "^2.4.6",
|
|
31
32
|
"svelte-github-corner": "^0.1.0",
|
|
32
|
-
"svelte-preprocess": "^4.10.
|
|
33
|
-
"svelte-toc": "^0.2.
|
|
33
|
+
"svelte-preprocess": "^4.10.5",
|
|
34
|
+
"svelte-toc": "^0.2.9",
|
|
34
35
|
"svelte2tsx": "^0.5.6",
|
|
35
36
|
"tslib": "^2.3.1",
|
|
36
|
-
"typescript": "^4.6.
|
|
37
|
-
"vite": "^2.
|
|
38
|
-
"vitest": "^0.
|
|
37
|
+
"typescript": "^4.6.3",
|
|
38
|
+
"vite": "^2.9.1",
|
|
39
|
+
"vitest": "^0.9.0"
|
|
39
40
|
},
|
|
40
41
|
"keywords": [
|
|
41
42
|
"svelte",
|
package/readme.md
CHANGED
|
@@ -35,15 +35,6 @@
|
|
|
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,33 +77,35 @@ 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
|
-
| `
|
|
80
|
+
| name | default | description |
|
|
81
|
+
| :--------------------- | :---------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| `options` | required prop | Array of strings/numbers or `Option` objects that will be listed in the dropdown. See `src/lib/index.ts` for admissible fields. The `label` is the only mandatory one. It must also be unique. |
|
|
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/pre-selected options when binding/passing as props respectively. |
|
|
88
|
+
| `selectedLabels` | `[]` | Labels of currently selected options. Supports binding but is read-only, i.e. since this value is reactive to `selected`, you cannot control `selected` by changing `selectedValues`. |
|
|
89
|
+
| `selectedValues` | `[]` | Values of currently selected options. Supports binding but is read-only, i.e. since this value is reactive to `selected`, you cannot control `selected` by changing `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
|
+
| `addOptionMsg` | `'Create this option...'` | Message shown to users after entering text when no options match their query and `allowUserOptions` is truthy. |
|
|
103
|
+
| `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 (cross icon) to remove selected option. |
|
|
105
|
+
| `removeAllTitle` | `'Remove all'` | Title text to display when user hovers over remove-all button. |
|
|
106
|
+
| `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/en-US/docs/Web/HTML/Attributes/autocomplete) for other admissible values. |
|
|
108
|
+
| `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. |
|
|
114
109
|
|
|
115
110
|
</div>
|
|
116
111
|
|
|
@@ -135,8 +130,8 @@ Full list of props/bindable variables for this component:
|
|
|
135
130
|
|
|
136
131
|
`MultiSelect.svelte` has 3 named slots:
|
|
137
132
|
|
|
138
|
-
- `slot="option"`: Customize rendering of dropdown options. Receives as props
|
|
139
|
-
- `slot="selected"`: Customize rendering selected
|
|
133
|
+
- `slot="option"`: Customize rendering of dropdown options. Receives as props an `option` and the zero-indexed position (`idx`) it has in the dropdown.
|
|
134
|
+
- `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.
|
|
140
135
|
- `slot="spinner"`: Custom spinner component to display when in `loading` state. Receives no props.
|
|
141
136
|
- `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.
|
|
142
137
|
|
|
@@ -164,25 +159,27 @@ Example:
|
|
|
164
159
|
|
|
165
160
|
`MultiSelect.svelte` dispatches the following events:
|
|
166
161
|
|
|
167
|
-
| name | detail | description
|
|
168
|
-
| ----------- | ---------------------------------------- |
|
|
169
|
-
| `add` | `{ option: Option }` | Triggers when a new option is selected.
|
|
170
|
-
| `remove` | `{ option: Option }` | Triggers when one selected option provided as `event.detail.option` is removed.
|
|
171
|
-
| `removeAll` | `options: Option[]` | Triggers when all selected options are removed. The payload `event.detail.options` gives the options that were previously selected.
|
|
172
|
-
| `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
|
|
173
|
-
| `blur` | none | Triggers when the input field looses focus.
|
|
162
|
+
| name | detail | description |
|
|
163
|
+
| ----------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
164
|
+
| `add` | `{ option: Option }` | Triggers when a new option is selected. |
|
|
165
|
+
| `remove` | `{ option: Option }` | Triggers when one selected option provided as `event.detail.option` is removed. |
|
|
166
|
+
| `removeAll` | `options: Option[]` | Triggers when all selected options are removed. The payload `event.detail.options` gives the options that were previously selected. |
|
|
167
|
+
| `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. |
|
|
168
|
+
| `blur` | none | Triggers when the input field looses focus. |
|
|
169
|
+
|
|
170
|
+
Depending on the data passed to the component the `options(s)` payload will either be objects or simple strings/numbers.
|
|
174
171
|
|
|
175
172
|
### Examples
|
|
176
173
|
|
|
177
174
|
<!-- prettier-ignore -->
|
|
178
|
-
- `on:add={(event) => console.log(event.detail.option
|
|
179
|
-
- `on:remove={(event) => console.log(event.detail.option
|
|
180
|
-
- ``on:change={(event) => console.log(`${event.detail.type}: '${event.detail.option
|
|
181
|
-
- `on:blur={
|
|
175
|
+
- `on:add={(event) => console.log(event.detail.option)}`
|
|
176
|
+
- `on:remove={(event) => console.log(event.detail.option)}`.
|
|
177
|
+
- ``on:change={(event) => console.log(`${event.detail.type}: '${event.detail.option}'`)}``
|
|
178
|
+
- `on:blur={myFunction}`
|
|
182
179
|
|
|
183
180
|
```svelte
|
|
184
181
|
<MultiSelect
|
|
185
|
-
on:change={(e) => alert(`You ${e.detail.type}ed '${e.detail.option
|
|
182
|
+
on:change={(e) => alert(`You ${e.detail.type}ed '${e.detail.option}'`)}
|
|
186
183
|
/>
|
|
187
184
|
```
|
|
188
185
|
|
|
@@ -281,7 +278,7 @@ The second method allows you to pass in custom classes to the important DOM elem
|
|
|
281
278
|
- `liOptionClass`
|
|
282
279
|
- `liActiveOptionClass`
|
|
283
280
|
|
|
284
|
-
This simplified version of the DOM structure of
|
|
281
|
+
This simplified version of the DOM structure of the component shows where these classes are inserted:
|
|
285
282
|
|
|
286
283
|
```svelte
|
|
287
284
|
<div class="multiselect {outerDivClass}">
|
|
@@ -381,7 +378,7 @@ export default {
|
|
|
381
378
|
}
|
|
382
379
|
```
|
|
383
380
|
|
|
384
|
-
Here's a [Stackblitz example](https://stackblitz.com/fork/github/davipon/test-svelte-multiselect?initialPath=__vitest__) that also uses [`
|
|
381
|
+
Here's a [Stackblitz example](https://stackblitz.com/fork/github/davipon/test-svelte-multiselect?initialPath=__vitest__) that also uses [`vitest-svelte-kit`](https://github.com/nickbreaton/vitest-svelte-kit).
|
|
385
382
|
|
|
386
383
|
## Want to contribute?
|
|
387
384
|
|