svelte-multiselect 7.0.2 → 8.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 +65 -57
- package/MultiSelect.svelte.d.ts +7 -4
- package/icons/Cross.svelte +3 -2
- package/package.json +18 -17
- package/readme.md +49 -23
package/MultiSelect.svelte
CHANGED
|
@@ -12,6 +12,10 @@ export let breakpoint = 800; // any screen with more horizontal pixels is consid
|
|
|
12
12
|
export let defaultDisabledTitle = `This option is disabled`;
|
|
13
13
|
export let disabled = false;
|
|
14
14
|
export let disabledInputTitle = `This input is disabled`;
|
|
15
|
+
// case-insensitive equality comparison after string coercion (looking only at the `label` key of object options)
|
|
16
|
+
export let duplicateFunc = (op1, op2) => `${get_label(op1)}`.toLowerCase() === `${get_label(op2)}`.toLowerCase();
|
|
17
|
+
export let duplicateOptionMsg = `This option is already selected`;
|
|
18
|
+
export let duplicates = false; // whether to allow duplicate options
|
|
15
19
|
export let filterFunc = (op, searchText) => {
|
|
16
20
|
if (!searchText)
|
|
17
21
|
return true;
|
|
@@ -29,9 +33,9 @@ export let liSelectedClass = ``;
|
|
|
29
33
|
export let loading = false;
|
|
30
34
|
export let matchingOptions = [];
|
|
31
35
|
export let maxSelect = null; // null means any number of options are selectable
|
|
32
|
-
export let maxSelectMsg =
|
|
36
|
+
export let maxSelectMsg = (current, max) => (max > 1 ? `${current}/${max}` : ``);
|
|
33
37
|
export let name = null;
|
|
34
|
-
export let
|
|
38
|
+
export let noMatchingOptionsMsg = `No matching options`;
|
|
35
39
|
export let open = false;
|
|
36
40
|
export let options;
|
|
37
41
|
export let outerDiv = null;
|
|
@@ -42,29 +46,22 @@ export let placeholder = null;
|
|
|
42
46
|
export let removeAllTitle = `Remove all`;
|
|
43
47
|
export let removeBtnTitle = `Remove`;
|
|
44
48
|
export let required = false;
|
|
49
|
+
export let resetFilterOnAdd = true;
|
|
45
50
|
export let searchText = ``;
|
|
46
51
|
export let selected = options
|
|
47
52
|
?.filter((op) => op?.preselected)
|
|
48
53
|
.slice(0, maxSelect ?? undefined) ?? [];
|
|
49
|
-
export let selectedLabels = [];
|
|
50
|
-
export let selectedValues = [];
|
|
51
54
|
export let sortSelected = false;
|
|
52
55
|
export let ulOptionsClass = ``;
|
|
53
56
|
export let ulSelectedClass = ``;
|
|
57
|
+
export let value = null;
|
|
54
58
|
// get the label key from an option object or the option itself if it's a string or number
|
|
55
59
|
const get_label = (op) => (op instanceof Object ? op.label : op);
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
// https://github.com/janosh/svelte-multiselect/issues/86
|
|
61
|
-
let _selected = (selected ?? []);
|
|
62
|
-
$: selected = maxSelect === 1 ? _selected[0] ?? null : _selected;
|
|
60
|
+
// if maxSelect=1, value is the single item in selected (or null if selected is empty)
|
|
61
|
+
// this solves both https://github.com/janosh/svelte-multiselect/issues/86 and
|
|
62
|
+
// https://github.com/janosh/svelte-multiselect/issues/136
|
|
63
|
+
$: value = maxSelect === 1 ? selected[0] ?? null : selected;
|
|
63
64
|
let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
|
|
64
|
-
$: _selectedLabels = _selected?.map(get_label) ?? [];
|
|
65
|
-
$: selectedLabels = maxSelect === 1 ? _selectedLabels[0] ?? null : _selectedLabels;
|
|
66
|
-
$: _selectedValues = _selected?.map(get_value) ?? [];
|
|
67
|
-
$: selectedValues = maxSelect === 1 ? _selectedValues[0] ?? null : _selectedValues;
|
|
68
65
|
if (!(options?.length > 0)) {
|
|
69
66
|
if (allowUserOptions) {
|
|
70
67
|
options = []; // initializing as array avoids errors when component mounts
|
|
@@ -80,19 +77,14 @@ if (parseLabelsAsHtml && allowUserOptions) {
|
|
|
80
77
|
if (maxSelect !== null && maxSelect < 1) {
|
|
81
78
|
console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
82
79
|
}
|
|
83
|
-
if (!Array.isArray(
|
|
84
|
-
console.error(`internal variable
|
|
80
|
+
if (!Array.isArray(selected)) {
|
|
81
|
+
console.error(`internal variable selected prop should always be an array, got ${selected}`);
|
|
85
82
|
}
|
|
86
83
|
const dispatch = createEventDispatcher();
|
|
87
84
|
let add_option_msg_is_active = false; // controls active state of <li>{addOptionMsg}</li>
|
|
88
85
|
let window_width;
|
|
89
|
-
// formValue binds to input.form-control to prevent form submission if required
|
|
90
|
-
// prop is true and no options are selected
|
|
91
|
-
$: form_value = _selectedValues.join(`,`);
|
|
92
|
-
$: if (form_value)
|
|
93
|
-
invalid = false; // reset error status whenever component state changes
|
|
94
86
|
// options matching the current search text
|
|
95
|
-
$: matchingOptions = options.filter((op) => filterFunc(op, searchText) && !
|
|
87
|
+
$: matchingOptions = options.filter((op) => filterFunc(op, searchText) && !selected.map(get_label).includes(get_label(op)) // remove already selected options from dropdown list
|
|
96
88
|
);
|
|
97
89
|
// raise if matchingOptions[activeIndex] does not yield a value
|
|
98
90
|
if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
@@ -102,10 +94,13 @@ if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
|
102
94
|
$: activeOption = activeIndex !== null ? matchingOptions[activeIndex] : null;
|
|
103
95
|
// add an option to selected list
|
|
104
96
|
function add(label, event) {
|
|
105
|
-
if (maxSelect && maxSelect > 1 &&
|
|
97
|
+
if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
|
|
106
98
|
wiggle = true;
|
|
107
|
-
|
|
108
|
-
|
|
99
|
+
if (!isNaN(Number(label)) && typeof selected.map(get_label)[0] === `number`)
|
|
100
|
+
label = Number(label); // convert to number if possible
|
|
101
|
+
const is_duplicate = selected.some((option) => duplicateFunc(option, label));
|
|
102
|
+
if ((maxSelect === null || maxSelect === 1 || selected.length < maxSelect) &&
|
|
103
|
+
(duplicates || !is_duplicate)) {
|
|
109
104
|
// first check if we find option in the options list
|
|
110
105
|
let option = options.find((op) => get_label(op) === label);
|
|
111
106
|
if (!option && // this has the side-effect of not allowing to user to add the same
|
|
@@ -130,29 +125,33 @@ function add(label, event) {
|
|
|
130
125
|
if (allowUserOptions === `append`)
|
|
131
126
|
options = [...options, option];
|
|
132
127
|
}
|
|
133
|
-
|
|
128
|
+
if (option === undefined) {
|
|
129
|
+
throw `Run time error, option with label ${label} not found in options list`;
|
|
130
|
+
}
|
|
131
|
+
if (resetFilterOnAdd)
|
|
132
|
+
searchText = ``; // reset search string on selection
|
|
134
133
|
if ([``, undefined, null].includes(option)) {
|
|
135
134
|
console.error(`MultiSelect: encountered missing option with label ${label} (or option is poorly labeled)`);
|
|
136
135
|
return;
|
|
137
136
|
}
|
|
138
137
|
if (maxSelect === 1) {
|
|
139
138
|
// for maxselect = 1 we always replace current option with new one
|
|
140
|
-
|
|
139
|
+
selected = [option];
|
|
141
140
|
}
|
|
142
141
|
else {
|
|
143
|
-
|
|
142
|
+
selected = [...selected, option];
|
|
144
143
|
if (sortSelected === true) {
|
|
145
|
-
|
|
144
|
+
selected = selected.sort((op1, op2) => {
|
|
146
145
|
const [label1, label2] = [get_label(op1), get_label(op2)];
|
|
147
146
|
// coerce to string if labels are numbers
|
|
148
147
|
return `${label1}`.localeCompare(`${label2}`);
|
|
149
148
|
});
|
|
150
149
|
}
|
|
151
150
|
else if (typeof sortSelected === `function`) {
|
|
152
|
-
|
|
151
|
+
selected = selected.sort(sortSelected);
|
|
153
152
|
}
|
|
154
153
|
}
|
|
155
|
-
if (
|
|
154
|
+
if (selected.length === maxSelect)
|
|
156
155
|
close_dropdown(event);
|
|
157
156
|
else if (focusInputOnSelect === true ||
|
|
158
157
|
(focusInputOnSelect === `desktop` && window_width > breakpoint)) {
|
|
@@ -160,14 +159,15 @@ function add(label, event) {
|
|
|
160
159
|
}
|
|
161
160
|
dispatch(`add`, { option });
|
|
162
161
|
dispatch(`change`, { option, type: `add` });
|
|
162
|
+
invalid = false; // reset error status whenever new items are selected
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
// remove an option from selected list
|
|
166
166
|
function remove(label) {
|
|
167
|
-
if (
|
|
167
|
+
if (selected.length === 0)
|
|
168
168
|
return;
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
selected.splice(selected.map(get_label).lastIndexOf(label), 1);
|
|
170
|
+
selected = selected; // Svelte rerender after in-place splice
|
|
171
171
|
const option = options.find((option) => get_label(option) === label) ??
|
|
172
172
|
// if option with label could not be found but allowUserOptions is truthy,
|
|
173
173
|
// assume it was created by user and create correspondidng option object
|
|
@@ -178,6 +178,7 @@ function remove(label) {
|
|
|
178
178
|
}
|
|
179
179
|
dispatch(`remove`, { option });
|
|
180
180
|
dispatch(`change`, { option, type: `remove` });
|
|
181
|
+
invalid = false; // reset error status whenever items are removed
|
|
181
182
|
}
|
|
182
183
|
function open_dropdown(event) {
|
|
183
184
|
if (disabled)
|
|
@@ -207,7 +208,7 @@ async function handle_keydown(event) {
|
|
|
207
208
|
event.preventDefault(); // prevent enter key from triggering form submission
|
|
208
209
|
if (activeOption) {
|
|
209
210
|
const label = get_label(activeOption);
|
|
210
|
-
|
|
211
|
+
selected.map(get_label).includes(label) ? remove(label) : add(label, event);
|
|
211
212
|
searchText = ``;
|
|
212
213
|
}
|
|
213
214
|
else if (allowUserOptions && searchText.length > 0) {
|
|
@@ -254,17 +255,17 @@ async function handle_keydown(event) {
|
|
|
254
255
|
}
|
|
255
256
|
}
|
|
256
257
|
// on backspace key: remove last selected option
|
|
257
|
-
else if (event.key === `Backspace` &&
|
|
258
|
-
remove(
|
|
258
|
+
else if (event.key === `Backspace` && selected.length > 0 && !searchText) {
|
|
259
|
+
remove(selected.map(get_label).at(-1));
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
262
|
function remove_all() {
|
|
262
|
-
dispatch(`removeAll`, { options:
|
|
263
|
-
dispatch(`change`, { options:
|
|
264
|
-
|
|
263
|
+
dispatch(`removeAll`, { options: selected });
|
|
264
|
+
dispatch(`change`, { options: selected, type: `removeAll` });
|
|
265
|
+
selected = [];
|
|
265
266
|
searchText = ``;
|
|
266
267
|
}
|
|
267
|
-
$: is_selected = (label) =>
|
|
268
|
+
$: is_selected = (label) => selected.map(get_label).includes(label);
|
|
268
269
|
const if_enter_or_space = (handler) => (event) => {
|
|
269
270
|
if ([`Enter`, `Space`].includes(event.code)) {
|
|
270
271
|
event.preventDefault();
|
|
@@ -297,10 +298,10 @@ function on_click_outside(event) {
|
|
|
297
298
|
title={disabled ? disabledInputTitle : null}
|
|
298
299
|
aria-disabled={disabled ? `true` : null}
|
|
299
300
|
>
|
|
300
|
-
<!--
|
|
301
|
+
<!-- bind:value={selected} prevents form submission if required prop is true and no options are selected -->
|
|
301
302
|
<input
|
|
302
303
|
{required}
|
|
303
|
-
bind:value={
|
|
304
|
+
bind:value={selected}
|
|
304
305
|
tabindex="-1"
|
|
305
306
|
aria-hidden="true"
|
|
306
307
|
aria-label="ignore this, used only to prevent form submission if select is required but empty"
|
|
@@ -309,7 +310,7 @@ function on_click_outside(event) {
|
|
|
309
310
|
/>
|
|
310
311
|
<ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt;" />
|
|
311
312
|
<ul class="selected {ulSelectedClass}">
|
|
312
|
-
{#each
|
|
313
|
+
{#each selected as option, idx}
|
|
313
314
|
<li class={liSelectedClass} aria-selected="true">
|
|
314
315
|
<slot name="selected" {option} {idx}>
|
|
315
316
|
{#if parseLabelsAsHtml}
|
|
@@ -325,7 +326,9 @@ function on_click_outside(event) {
|
|
|
325
326
|
type="button"
|
|
326
327
|
title="{removeBtnTitle} {get_label(option)}"
|
|
327
328
|
>
|
|
328
|
-
<slot name="remove-icon"
|
|
329
|
+
<slot name="remove-icon">
|
|
330
|
+
<CrossIcon width="15px" />
|
|
331
|
+
</slot>
|
|
329
332
|
</button>
|
|
330
333
|
{/if}
|
|
331
334
|
</li>
|
|
@@ -345,7 +348,7 @@ function on_click_outside(event) {
|
|
|
345
348
|
{disabled}
|
|
346
349
|
{inputmode}
|
|
347
350
|
{pattern}
|
|
348
|
-
placeholder={
|
|
351
|
+
placeholder={selected.length == 0 ? placeholder : null}
|
|
349
352
|
aria-invalid={invalid ? `true` : null}
|
|
350
353
|
on:blur
|
|
351
354
|
on:change
|
|
@@ -372,16 +375,15 @@ function on_click_outside(event) {
|
|
|
372
375
|
<slot name="disabled-icon">
|
|
373
376
|
<DisabledIcon width="15px" />
|
|
374
377
|
</slot>
|
|
375
|
-
{:else if
|
|
378
|
+
{:else if selected.length > 0}
|
|
376
379
|
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
|
|
377
380
|
<Wiggle bind:wiggle angle={20}>
|
|
378
381
|
<span style="padding: 0 3pt;">
|
|
379
|
-
{maxSelectMsg?.(
|
|
380
|
-
(maxSelect > 1 ? `${_selected.length}/${maxSelect}` : ``)}
|
|
382
|
+
{maxSelectMsg?.(selected.length, maxSelect)}
|
|
381
383
|
</span>
|
|
382
384
|
</Wiggle>
|
|
383
385
|
{/if}
|
|
384
|
-
{#if maxSelect !== 1 &&
|
|
386
|
+
{#if maxSelect !== 1 && selected.length > 1}
|
|
385
387
|
<button
|
|
386
388
|
type="button"
|
|
387
389
|
class="remove-all"
|
|
@@ -389,7 +391,9 @@ function on_click_outside(event) {
|
|
|
389
391
|
on:mouseup|stopPropagation={remove_all}
|
|
390
392
|
on:keydown={if_enter_or_space(remove_all)}
|
|
391
393
|
>
|
|
392
|
-
<slot name="remove-icon"
|
|
394
|
+
<slot name="remove-icon">
|
|
395
|
+
<CrossIcon width="15px" />
|
|
396
|
+
</slot>
|
|
393
397
|
</button>
|
|
394
398
|
{/if}
|
|
395
399
|
{/if}
|
|
@@ -409,7 +413,7 @@ function on_click_outside(event) {
|
|
|
409
413
|
<li
|
|
410
414
|
on:mousedown|stopPropagation
|
|
411
415
|
on:mouseup|stopPropagation={(event) => {
|
|
412
|
-
if (!disabled)
|
|
416
|
+
if (!disabled) add(label, event)
|
|
413
417
|
}}
|
|
414
418
|
title={disabled
|
|
415
419
|
? disabledTitle
|
|
@@ -449,10 +453,12 @@ function on_click_outside(event) {
|
|
|
449
453
|
on:blur={() => (add_option_msg_is_active = false)}
|
|
450
454
|
aria-selected="false"
|
|
451
455
|
>
|
|
452
|
-
{
|
|
456
|
+
{!duplicates && selected.some((option) => duplicateFunc(option, searchText))
|
|
457
|
+
? duplicateOptionMsg
|
|
458
|
+
: addOptionMsg}
|
|
453
459
|
</li>
|
|
454
460
|
{:else}
|
|
455
|
-
<span>{
|
|
461
|
+
<span>{noMatchingOptionsMsg}</span>
|
|
456
462
|
{/if}
|
|
457
463
|
{/each}
|
|
458
464
|
</ul>
|
|
@@ -523,7 +529,8 @@ function on_click_outside(event) {
|
|
|
523
529
|
:where(div.multiselect) ul.selected > li button:hover,
|
|
524
530
|
:where(div.multiselect) button.remove-all:hover,
|
|
525
531
|
:where(div.multiselect) button:focus {
|
|
526
|
-
color: var(--sms-
|
|
532
|
+
color: var(--sms-remove-btn-hover-color, lightskyblue);
|
|
533
|
+
background: var(--sms-remove-btn-hover-bg, rgba(0, 0, 0, 0.2));
|
|
527
534
|
}
|
|
528
535
|
:where(div.multiselect) input {
|
|
529
536
|
margin: auto 0; /* CSS reset */
|
|
@@ -662,7 +669,8 @@ function on_click_outside(event) {
|
|
|
662
669
|
div.multiselect ul.selected > li button:hover,
|
|
663
670
|
div.multiselect button.remove-all:hover,
|
|
664
671
|
div.multiselect button:focus {
|
|
665
|
-
color: var(--sms-
|
|
672
|
+
color: var(--sms-remove-btn-hover-color, lightskyblue);
|
|
673
|
+
background: var(--sms-remove-btn-hover-bg, rgba(0, 0, 0, 0.2));
|
|
666
674
|
}
|
|
667
675
|
div.multiselect input {
|
|
668
676
|
margin: auto 0; /* CSS reset */
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ declare const __propDef: {
|
|
|
12
12
|
defaultDisabledTitle?: string | undefined;
|
|
13
13
|
disabled?: boolean | undefined;
|
|
14
14
|
disabledInputTitle?: string | undefined;
|
|
15
|
+
duplicateFunc?: ((op1: Option, op2: Option) => boolean) | undefined;
|
|
16
|
+
duplicateOptionMsg?: string | undefined;
|
|
17
|
+
duplicates?: boolean | undefined;
|
|
15
18
|
filterFunc?: ((op: Option, searchText: string) => boolean) | undefined;
|
|
16
19
|
focusInputOnSelect?: boolean | "desktop" | undefined;
|
|
17
20
|
id?: string | null | undefined;
|
|
@@ -27,7 +30,7 @@ declare const __propDef: {
|
|
|
27
30
|
maxSelect?: number | null | undefined;
|
|
28
31
|
maxSelectMsg?: ((current: number, max: number) => string) | null | undefined;
|
|
29
32
|
name?: string | null | undefined;
|
|
30
|
-
|
|
33
|
+
noMatchingOptionsMsg?: string | undefined;
|
|
31
34
|
open?: boolean | undefined;
|
|
32
35
|
options: Option[];
|
|
33
36
|
outerDiv?: HTMLDivElement | null | undefined;
|
|
@@ -38,13 +41,13 @@ declare const __propDef: {
|
|
|
38
41
|
removeAllTitle?: string | undefined;
|
|
39
42
|
removeBtnTitle?: string | undefined;
|
|
40
43
|
required?: boolean | undefined;
|
|
44
|
+
resetFilterOnAdd?: boolean | undefined;
|
|
41
45
|
searchText?: string | undefined;
|
|
42
|
-
selected?: Option
|
|
43
|
-
selectedLabels?: string | number | (string | number)[] | null | undefined;
|
|
44
|
-
selectedValues?: unknown[] | unknown | null;
|
|
46
|
+
selected?: Option[] | undefined;
|
|
45
47
|
sortSelected?: boolean | ((op1: Option, op2: Option) => number) | undefined;
|
|
46
48
|
ulOptionsClass?: string | undefined;
|
|
47
49
|
ulSelectedClass?: string | undefined;
|
|
50
|
+
value?: Option | Option[] | null | undefined;
|
|
48
51
|
};
|
|
49
52
|
slots: {
|
|
50
53
|
selected: {
|
package/icons/Cross.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
<svg {...$$props} viewBox="0 0
|
|
1
|
+
<svg {...$$props} viewBox="0 0 24 24" fill="currentColor">
|
|
2
2
|
<path
|
|
3
|
-
d="
|
|
3
|
+
d="M18.3 5.71a.996.996 0 0 0-1.41 0L12 10.59L7.11 5.7A.996.996 0 1 0 5.7 7.11L10.59 12L5.7 16.89a.996.996 0 1 0 1.41 1.41L12 13.41l4.89 4.89a.996.996 0 1 0 1.41-1.41L13.41 12l4.89-4.89c.38-.38.38-1.02 0-1.4z"
|
|
4
4
|
/>
|
|
5
5
|
</svg>
|
|
6
|
+
<!-- https://api.iconify.design/ic:round-clear.svg -->
|
package/package.json
CHANGED
|
@@ -5,39 +5,40 @@
|
|
|
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": "8.0.0",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"svelte": "index.js",
|
|
11
11
|
"main": "index.js",
|
|
12
12
|
"bugs": "https://github.com/janosh/svelte-multiselect/issues",
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@
|
|
15
|
-
"@
|
|
16
|
-
"@sveltejs/
|
|
14
|
+
"@iconify/svelte": "^3.0.0",
|
|
15
|
+
"@playwright/test": "^1.27.1",
|
|
16
|
+
"@sveltejs/adapter-static": "^1.0.0-next.44",
|
|
17
|
+
"@sveltejs/kit": "^1.0.0-next.516",
|
|
17
18
|
"@sveltejs/package": "^1.0.0-next.5",
|
|
18
|
-
"@sveltejs/vite-plugin-svelte": "^1.0.
|
|
19
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
20
|
-
"@typescript-eslint/parser": "^5.
|
|
21
|
-
"eslint": "^8.
|
|
19
|
+
"@sveltejs/vite-plugin-svelte": "^1.0.9",
|
|
20
|
+
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
|
21
|
+
"@typescript-eslint/parser": "^5.40.0",
|
|
22
|
+
"eslint": "^8.25.0",
|
|
22
23
|
"eslint-plugin-svelte3": "^4.0.0",
|
|
23
|
-
"hastscript": "^7.0
|
|
24
|
-
"jsdom": "^20.0.
|
|
24
|
+
"hastscript": "^7.1.0",
|
|
25
|
+
"jsdom": "^20.0.1",
|
|
25
26
|
"mdsvex": "^0.10.6",
|
|
26
27
|
"prettier": "^2.7.1",
|
|
27
|
-
"prettier-plugin-svelte": "^2.
|
|
28
|
+
"prettier-plugin-svelte": "^2.8.0",
|
|
28
29
|
"rehype-autolink-headings": "^6.1.1",
|
|
29
30
|
"rehype-slug": "^5.0.1",
|
|
30
31
|
"sass": "^1.55.0",
|
|
31
|
-
"svelte": "^3.
|
|
32
|
-
"svelte-check": "^2.9.
|
|
32
|
+
"svelte": "^3.52.0",
|
|
33
|
+
"svelte-check": "^2.9.2",
|
|
33
34
|
"svelte-github-corner": "^0.1.0",
|
|
34
35
|
"svelte-preprocess": "^4.10.6",
|
|
35
36
|
"svelte-toc": "^0.4.0",
|
|
36
|
-
"svelte2tsx": "^0.5.
|
|
37
|
+
"svelte2tsx": "^0.5.20",
|
|
37
38
|
"tslib": "^2.4.0",
|
|
38
|
-
"typescript": "^4.8.
|
|
39
|
-
"vite": "^3.1.
|
|
40
|
-
"vitest": "^0.
|
|
39
|
+
"typescript": "^4.8.4",
|
|
40
|
+
"vite": "^3.1.8",
|
|
41
|
+
"vitest": "^0.24.3"
|
|
41
42
|
},
|
|
42
43
|
"keywords": [
|
|
43
44
|
"svelte",
|
package/readme.md
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
[](https://github.com/janosh/svelte-multiselect/actions/workflows/test.yml)
|
|
9
9
|
[](https://app.netlify.com/sites/svelte-multiselect/deploys)
|
|
10
10
|
[](https://npmjs.com/package/svelte-multiselect)
|
|
11
|
-
[](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
|
|
12
|
-
[](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
|
|
12
|
+
[](https://svelte.dev/repl/a5a14b8f15d64cb083b567292480db05)
|
|
13
13
|
[](https://stackblitz.com/github/janosh/svelte-multiselect)
|
|
14
14
|
|
|
15
15
|
</h4>
|
|
@@ -36,12 +36,14 @@
|
|
|
36
36
|
|
|
37
37
|
## Recent breaking changes
|
|
38
38
|
|
|
39
|
-
- **
|
|
40
|
-
- **v6.0.
|
|
41
|
-
- **v6.0.1
|
|
42
|
-
- **
|
|
43
|
-
- **
|
|
44
|
-
- **
|
|
39
|
+
- **v6.0.0** The prop `showOptions` which controls whether the list of dropdown options is currently open or closed was renamed to `open`. See [PR 103](https://github.com/janosh/svelte-multiselect/pull/103).
|
|
40
|
+
- **v6.0.1** The prop `disabledTitle` which sets the title of the `<MultiSelect>` `<input>` node if in `disabled` mode was renamed to `disabledInputTitle`. See [PR 105](https://github.com/janosh/svelte-multiselect/pull/105).
|
|
41
|
+
- **v6.0.1** The default margin of `1em 0` on the wrapper `div.multiselect` was removed. Instead, there is now a new CSS variable `--sms-margin`. Set it to `--sms-margin: 1em 0;` to restore the old appearance. See [PR 105](https://github.com/janosh/svelte-multiselect/pull/105).
|
|
42
|
+
- **6.1.0** The `dispatch` events `focus` and `blur` were renamed to `open` and `close`, respectively. These actions refer to the dropdown list, i.e. `<MultiSelect on:open={(event) => console.log(event)}>` will trigger when the dropdown list opens. The focus and blur events are now regular DOM (not Svelte `dispatch`) events emitted by the `<input>` node. See [PR 120](https://github.com/janosh/svelte-multiselect/pull/120).
|
|
43
|
+
- **v7.0.0** `selected` (as well `selectedLabels` and `selectedValues`) used to be arrays always. Now, if `maxSelect=1`, they will no longer be a length-1 array but simply a single a option (label/value respectively) or `null` if no option is selected. See [PR 123](https://github.com/janosh/svelte-multiselect/pull/123).
|
|
44
|
+
- **8.0.0**
|
|
45
|
+
- 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. See [PR 138](https://github.com/janosh/svelte-multiselect/pull/138)
|
|
46
|
+
- Prop `noOptionsMsg` was renamed to `noMatchingOptionsMsg`. See [PR 133](https://github.com/janosh/svelte-multiselect/pull/133).
|
|
45
47
|
|
|
46
48
|
## Installation
|
|
47
49
|
|
|
@@ -138,6 +140,26 @@ import type { Option } from 'svelte-multiselect'
|
|
|
138
140
|
|
|
139
141
|
Tooltip text to display on hover when the component is in `disabled` state.
|
|
140
142
|
|
|
143
|
+
<!-- prettier-ignore -->
|
|
144
|
+
1. ```ts
|
|
145
|
+
duplicateFunc: (op1: Option, op2: Option) => boolean = (op1, op2) =>
|
|
146
|
+
`${get_label(op1)}`.toLowerCase() === `${get_label(op2)}`.toLowerCase()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
This option determines when two options are considered duplicates. Defaults to case-insensitive equality comparison after string coercion (looking only at the `label` key of object options). I.e. the default `duplicateFunc` considers `'Foo' == 'foo'`, `'42' == 42` and ``{ label: `Foo`, value: 0 } == { label: `foo`, value: 42 }``.
|
|
150
|
+
|
|
151
|
+
1. ```ts
|
|
152
|
+
duplicates: boolean = false
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
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 `duplicateFunc` to customize when a pair of options is considered a duplicate.
|
|
156
|
+
|
|
157
|
+
1. ```ts
|
|
158
|
+
duplicateOptionMsg: string = `This option is already selected`
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Text to display to users when `allowUserOptions` is truthy and they try to create a new option that's already selected.
|
|
162
|
+
|
|
141
163
|
1. ```ts
|
|
142
164
|
filterFunc = (op: Option, searchText: string): boolean => {
|
|
143
165
|
if (!searchText) return true
|
|
@@ -196,7 +218,10 @@ import type { Option } from 'svelte-multiselect'
|
|
|
196
218
|
Positive integer to limit the number of options users can pick. `null` means no limit. `maxSelect={1}` will change the type of `selected` to be a single `Option` (or `null`) (not a length-1 array). Likewise, the type of `selectedLabels` changes from `(string | number)[]` to `string | number | null` and `selectedValues` from `unknown[]` to `unknown | null`. `maxSelect={1}` will also give `div.multiselect` a class of `single`. I.e. you can target the selector `div.multiselect.single` to give single selects a different appearance from multi selects.
|
|
197
219
|
|
|
198
220
|
1. ```ts
|
|
199
|
-
maxSelectMsg: ((current: number, max: number) => string) | null =
|
|
221
|
+
maxSelectMsg: ((current: number, max: number) => string) | null = (
|
|
222
|
+
current: number,
|
|
223
|
+
max: number
|
|
224
|
+
) => (max > 1 ? `${current}/${max}` : ``)
|
|
200
225
|
```
|
|
201
226
|
|
|
202
227
|
Inform users how many of the maximum allowed options they have already selected. Set `maxSelectMsg={null}` to not show a message. Defaults to `null` when `maxSelect={1}` or `maxSelect={null}`. Else if `maxSelect > 1`, defaults to:
|
|
@@ -212,7 +237,7 @@ import type { Option } from 'svelte-multiselect'
|
|
|
212
237
|
Applied to the `<input>` element. Sets the key of this field in a submitted form data object. Not useful at the moment since the value is stored in Svelte state, not on the `<input>` node.
|
|
213
238
|
|
|
214
239
|
1. ```ts
|
|
215
|
-
|
|
240
|
+
noMatchingOptionsMsg: string = `No matching options`
|
|
216
241
|
```
|
|
217
242
|
|
|
218
243
|
What message to show if no options match the user-entered search string.
|
|
@@ -271,6 +296,12 @@ import type { Option } from 'svelte-multiselect'
|
|
|
271
296
|
|
|
272
297
|
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.
|
|
273
298
|
|
|
299
|
+
1. ```ts
|
|
300
|
+
resetFilterOnAdd: boolean = true
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Whether text entered into the input to filter options in the dropdown list is reset to empty string when user selects an option.
|
|
304
|
+
|
|
274
305
|
1. ```ts
|
|
275
306
|
searchText: string = ``
|
|
276
307
|
```
|
|
@@ -278,31 +309,25 @@ import type { Option } from 'svelte-multiselect'
|
|
|
278
309
|
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.
|
|
279
310
|
|
|
280
311
|
1. ```ts
|
|
281
|
-
selected: Option[]
|
|
312
|
+
selected: Option[] =
|
|
282
313
|
options
|
|
283
314
|
?.filter((op) => (op as ObjectOption)?.preselected)
|
|
284
315
|
.slice(0, maxSelect ?? undefined) ?? []
|
|
285
316
|
```
|
|
286
317
|
|
|
287
|
-
Array of currently selected options. Supports 2-way binding `bind:selected={[1, 2, 3]}` to control component state externally
|
|
288
|
-
|
|
289
|
-
1. ```ts
|
|
290
|
-
selectedLabels: (string | number)[] | string | number | null = []
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
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`. If `maxSelect={1}`, selectedLabels will not be an array but a single `string | number` or `null` if no options are selected.
|
|
318
|
+
Array of currently selected options. Supports 2-way binding `bind:selected={[1, 2, 3]}` to control component state externally. Can be passed as prop to set pre-selected options that will already be populated when component mounts before any user interaction.
|
|
294
319
|
|
|
295
320
|
1. ```ts
|
|
296
|
-
|
|
321
|
+
sortSelected: boolean | ((op1: Option, op2: Option) => number) = false
|
|
297
322
|
```
|
|
298
323
|
|
|
299
|
-
|
|
324
|
+
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.
|
|
300
325
|
|
|
301
326
|
1. ```ts
|
|
302
|
-
|
|
327
|
+
value: Option | Option[] | null = null
|
|
303
328
|
```
|
|
304
329
|
|
|
305
|
-
|
|
330
|
+
If `maxSelect={1}`, `value` will be the single item in `selected` (or `null` if `selected` is empty). If `maxSelect != 1`, `maxSelect` and `selected` are equal. Warning: `value` supports 1-way binding only, meaning `bind:value` will update `value` when internal component state changes but changing `value` externally will not update internal component state. This is because `value` is already reactive to `selected` and making `selected` reactive to `value` would be cyclic. Suggestions for better solutions that solve both [#86](https://github.com/janosh/svelte-multiselect/issues/86) and [#136](https://github.com/janosh/svelte-multiselect/issues/136) welcome!
|
|
306
331
|
|
|
307
332
|
## Slots
|
|
308
333
|
|
|
@@ -460,7 +485,8 @@ If you only want to make small adjustments, you can pass the following CSS varia
|
|
|
460
485
|
- `padding: var(--sms-selected-li-padding, 1pt 5pt)`: Height of selected options.
|
|
461
486
|
- `color: var(--sms-selected-text-color, var(--sms-text-color))`: Text color for selected options.
|
|
462
487
|
- `ul.selected > li button:hover, button.remove-all:hover, button:focus`
|
|
463
|
-
- `color: var(--sms-
|
|
488
|
+
- `color: var(--sms-remove-btn-hover-color, lightskyblue)`: Color of the remove-icon buttons for removing all or individual selected options when in `:focus` or `:hover` state.
|
|
489
|
+
- `background: var(--sms-remove-btn-hover-bg, rgba(0, 0, 0, 0.2))`: Background for hovered remove buttons.
|
|
464
490
|
- `div.multiselect > ul.options`
|
|
465
491
|
- `background: var(--sms-options-bg, white)`: Background of dropdown list.
|
|
466
492
|
- `max-height: var(--sms-options-max-height, 50vh)`: Maximum height of options dropdown.
|