svelte-multiselect 6.0.3 → 7.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 +77 -47
- package/MultiSelect.svelte.d.ts +5 -3
- package/index.d.ts +18 -2
- package/package.json +11 -11
- package/readme.md +52 -26
package/MultiSelect.svelte
CHANGED
|
@@ -22,6 +22,7 @@ export let focusInputOnSelect = `desktop`;
|
|
|
22
22
|
export let id = null;
|
|
23
23
|
export let input = null;
|
|
24
24
|
export let inputClass = ``;
|
|
25
|
+
export let inputmode = null;
|
|
25
26
|
export let invalid = false;
|
|
26
27
|
export let liActiveOptionClass = ``;
|
|
27
28
|
export let liOptionClass = ``;
|
|
@@ -37,17 +38,30 @@ export let options;
|
|
|
37
38
|
export let outerDiv = null;
|
|
38
39
|
export let outerDivClass = ``;
|
|
39
40
|
export let parseLabelsAsHtml = false; // should not be combined with allowUserOptions!
|
|
41
|
+
export let pattern = null;
|
|
40
42
|
export let placeholder = null;
|
|
41
43
|
export let removeAllTitle = `Remove all`;
|
|
42
44
|
export let removeBtnTitle = `Remove`;
|
|
43
45
|
export let required = false;
|
|
44
46
|
export let searchText = ``;
|
|
45
|
-
export let selected = options
|
|
47
|
+
export let selected = options
|
|
48
|
+
?.filter((op) => op?.preselected)
|
|
49
|
+
.slice(0, maxSelect ?? undefined) ?? [];
|
|
46
50
|
export let selectedLabels = [];
|
|
47
51
|
export let selectedValues = [];
|
|
48
52
|
export let sortSelected = false;
|
|
49
53
|
export let ulOptionsClass = ``;
|
|
50
54
|
export let ulSelectedClass = ``;
|
|
55
|
+
// selected and _selected are identical except if maxSelect=1, selected will be the single item (or null)
|
|
56
|
+
// in _selected which will always be an array for easier component internals. selected then solves
|
|
57
|
+
// https://github.com/janosh/svelte-multiselect/issues/86
|
|
58
|
+
let _selected = (selected ?? []);
|
|
59
|
+
$: selected = maxSelect === 1 ? _selected[0] ?? null : _selected;
|
|
60
|
+
let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
|
|
61
|
+
$: _selectedLabels = _selected?.map(get_label) ?? [];
|
|
62
|
+
$: selectedLabels = maxSelect === 1 ? _selectedLabels[0] ?? null : _selectedLabels;
|
|
63
|
+
$: _selectedValues = _selected?.map(get_value) ?? [];
|
|
64
|
+
$: selectedValues = maxSelect === 1 ? _selectedValues[0] ?? null : _selectedValues;
|
|
51
65
|
if (!(options?.length > 0)) {
|
|
52
66
|
if (allowUserOptions) {
|
|
53
67
|
options = []; // initializing as array avoids errors when component mounts
|
|
@@ -63,35 +77,32 @@ if (parseLabelsAsHtml && allowUserOptions) {
|
|
|
63
77
|
if (maxSelect !== null && maxSelect < 1) {
|
|
64
78
|
console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
65
79
|
}
|
|
66
|
-
if (!Array.isArray(
|
|
67
|
-
console.error(`
|
|
80
|
+
if (!Array.isArray(_selected)) {
|
|
81
|
+
console.error(`internal variable _selected prop should always be an array, got ${_selected}`);
|
|
68
82
|
}
|
|
69
83
|
const dispatch = createEventDispatcher();
|
|
70
84
|
let add_option_msg_is_active = false; // controls active state of <li>{addOptionMsg}</li>
|
|
71
85
|
let window_width;
|
|
72
|
-
let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
|
|
73
|
-
$: selectedLabels = selected.map(get_label);
|
|
74
|
-
$: selectedValues = selected.map(get_value);
|
|
75
86
|
// formValue binds to input.form-control to prevent form submission if required
|
|
76
87
|
// prop is true and no options are selected
|
|
77
|
-
$: formValue =
|
|
88
|
+
$: formValue = _selectedValues.join(`,`);
|
|
78
89
|
$: if (formValue)
|
|
79
90
|
invalid = false; // reset error status whenever component state changes
|
|
80
91
|
// options matching the current search text
|
|
81
|
-
$: matchingOptions = options.filter((op) => filterFunc(op, searchText) && !
|
|
92
|
+
$: matchingOptions = options.filter((op) => filterFunc(op, searchText) && !_selectedLabels.includes(get_label(op)) // remove already selected options from dropdown list
|
|
82
93
|
);
|
|
83
94
|
// raise if matchingOptions[activeIndex] does not yield a value
|
|
84
95
|
if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
85
96
|
throw `Run time error, activeIndex=${activeIndex} is out of bounds, matchingOptions.length=${matchingOptions.length}`;
|
|
86
97
|
}
|
|
87
98
|
// update activeOption when activeIndex changes
|
|
88
|
-
$: activeOption = activeIndex ? matchingOptions[activeIndex] : null;
|
|
99
|
+
$: activeOption = activeIndex !== null ? matchingOptions[activeIndex] : null;
|
|
89
100
|
// add an option to selected list
|
|
90
|
-
function add(label) {
|
|
91
|
-
if (maxSelect && maxSelect > 1 &&
|
|
101
|
+
function add(label, event) {
|
|
102
|
+
if (maxSelect && maxSelect > 1 && _selected.length >= maxSelect)
|
|
92
103
|
wiggle = true;
|
|
93
104
|
// to prevent duplicate selection, we could add `&& !selectedLabels.includes(label)`
|
|
94
|
-
if (maxSelect === null || maxSelect === 1 ||
|
|
105
|
+
if (maxSelect === null || maxSelect === 1 || _selected.length < maxSelect) {
|
|
95
106
|
// first check if we find option in the options list
|
|
96
107
|
let option = options.find((op) => get_label(op) === label);
|
|
97
108
|
if (!option && // this has the side-effect of not allowing to user to add the same
|
|
@@ -123,23 +134,23 @@ function add(label) {
|
|
|
123
134
|
}
|
|
124
135
|
if (maxSelect === 1) {
|
|
125
136
|
// for maxselect = 1 we always replace current option with new one
|
|
126
|
-
|
|
137
|
+
_selected = [option];
|
|
127
138
|
}
|
|
128
139
|
else {
|
|
129
|
-
|
|
140
|
+
_selected = [..._selected, option];
|
|
130
141
|
if (sortSelected === true) {
|
|
131
|
-
|
|
142
|
+
_selected = _selected.sort((op1, op2) => {
|
|
132
143
|
const [label1, label2] = [get_label(op1), get_label(op2)];
|
|
133
144
|
// coerce to string if labels are numbers
|
|
134
145
|
return `${label1}`.localeCompare(`${label2}`);
|
|
135
146
|
});
|
|
136
147
|
}
|
|
137
148
|
else if (typeof sortSelected === `function`) {
|
|
138
|
-
|
|
149
|
+
_selected = _selected.sort(sortSelected);
|
|
139
150
|
}
|
|
140
151
|
}
|
|
141
|
-
if (
|
|
142
|
-
close_dropdown();
|
|
152
|
+
if (_selected.length === maxSelect)
|
|
153
|
+
close_dropdown(event);
|
|
143
154
|
else if (focusInputOnSelect === true ||
|
|
144
155
|
(focusInputOnSelect === `desktop` && window_width > breakpoint)) {
|
|
145
156
|
input?.focus();
|
|
@@ -150,10 +161,10 @@ function add(label) {
|
|
|
150
161
|
}
|
|
151
162
|
// remove an option from selected list
|
|
152
163
|
function remove(label) {
|
|
153
|
-
if (
|
|
164
|
+
if (_selected.length === 0)
|
|
154
165
|
return;
|
|
155
|
-
|
|
156
|
-
|
|
166
|
+
_selected.splice(_selectedLabels.lastIndexOf(label), 1);
|
|
167
|
+
_selected = _selected; // Svelte rerender after in-place splice
|
|
157
168
|
const option = options.find((option) => get_label(option) === label) ??
|
|
158
169
|
// if option with label could not be found but allowUserOptions is truthy,
|
|
159
170
|
// assume it was created by user and create correspondidng option object
|
|
@@ -165,24 +176,27 @@ function remove(label) {
|
|
|
165
176
|
dispatch(`remove`, { option });
|
|
166
177
|
dispatch(`change`, { option, type: `remove` });
|
|
167
178
|
}
|
|
168
|
-
function open_dropdown() {
|
|
179
|
+
function open_dropdown(event) {
|
|
169
180
|
if (disabled)
|
|
170
181
|
return;
|
|
171
182
|
open = true;
|
|
172
|
-
|
|
173
|
-
|
|
183
|
+
if (!(event instanceof FocusEvent)) {
|
|
184
|
+
// avoid double-focussing input when event that opened dropdown was already input FocusEvent
|
|
185
|
+
input?.focus();
|
|
186
|
+
}
|
|
187
|
+
dispatch(`open`, { event });
|
|
174
188
|
}
|
|
175
|
-
function close_dropdown() {
|
|
189
|
+
function close_dropdown(event) {
|
|
176
190
|
open = false;
|
|
177
191
|
input?.blur();
|
|
178
192
|
activeOption = null;
|
|
179
|
-
dispatch(`
|
|
193
|
+
dispatch(`close`, { event });
|
|
180
194
|
}
|
|
181
195
|
// handle all keyboard events this component receives
|
|
182
196
|
async function handle_keydown(event) {
|
|
183
197
|
// on escape or tab out of input: dismiss options dropdown and reset search text
|
|
184
198
|
if (event.key === `Escape` || event.key === `Tab`) {
|
|
185
|
-
close_dropdown();
|
|
199
|
+
close_dropdown(event);
|
|
186
200
|
searchText = ``;
|
|
187
201
|
}
|
|
188
202
|
// on enter key: toggle active option and reset search text
|
|
@@ -190,17 +204,17 @@ async function handle_keydown(event) {
|
|
|
190
204
|
event.preventDefault(); // prevent enter key from triggering form submission
|
|
191
205
|
if (activeOption) {
|
|
192
206
|
const label = get_label(activeOption);
|
|
193
|
-
selectedLabels.includes(label) ? remove(label) : add(label);
|
|
207
|
+
selectedLabels.includes(label) ? remove(label) : add(label, event);
|
|
194
208
|
searchText = ``;
|
|
195
209
|
}
|
|
196
210
|
else if (allowUserOptions && searchText.length > 0) {
|
|
197
211
|
// user entered text but no options match, so if allowUserOptions is truthy, we create new option
|
|
198
|
-
add(searchText);
|
|
212
|
+
add(searchText, event);
|
|
199
213
|
}
|
|
200
214
|
// no active option and no search text means the options dropdown is closed
|
|
201
215
|
// in which case enter means open it
|
|
202
216
|
else
|
|
203
|
-
open_dropdown();
|
|
217
|
+
open_dropdown(event);
|
|
204
218
|
}
|
|
205
219
|
// on up/down arrow keys: update active option
|
|
206
220
|
else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
|
|
@@ -238,17 +252,17 @@ async function handle_keydown(event) {
|
|
|
238
252
|
}
|
|
239
253
|
}
|
|
240
254
|
// on backspace key: remove last selected option
|
|
241
|
-
else if (event.key === `Backspace` &&
|
|
242
|
-
remove(
|
|
255
|
+
else if (event.key === `Backspace` && _selectedLabels.length > 0 && !searchText) {
|
|
256
|
+
remove(_selectedLabels.at(-1));
|
|
243
257
|
}
|
|
244
258
|
}
|
|
245
259
|
function remove_all() {
|
|
246
|
-
dispatch(`removeAll`, { options:
|
|
247
|
-
dispatch(`change`, { options:
|
|
248
|
-
|
|
260
|
+
dispatch(`removeAll`, { options: _selected });
|
|
261
|
+
dispatch(`change`, { options: _selected, type: `removeAll` });
|
|
262
|
+
_selected = [];
|
|
249
263
|
searchText = ``;
|
|
250
264
|
}
|
|
251
|
-
$: is_selected = (label) =>
|
|
265
|
+
$: is_selected = (label) => _selectedLabels.includes(label);
|
|
252
266
|
const if_enter_or_space = (handler) => (event) => {
|
|
253
267
|
if ([`Enter`, `Space`].includes(event.code)) {
|
|
254
268
|
event.preventDefault();
|
|
@@ -257,7 +271,7 @@ const if_enter_or_space = (handler) => (event) => {
|
|
|
257
271
|
};
|
|
258
272
|
function on_click_outside(event) {
|
|
259
273
|
if (outerDiv && !outerDiv.contains(event.target)) {
|
|
260
|
-
close_dropdown();
|
|
274
|
+
close_dropdown(event);
|
|
261
275
|
}
|
|
262
276
|
}
|
|
263
277
|
</script>
|
|
@@ -292,7 +306,7 @@ function on_click_outside(event) {
|
|
|
292
306
|
/>
|
|
293
307
|
<ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt;" />
|
|
294
308
|
<ul class="selected {ulSelectedClass}">
|
|
295
|
-
{#each
|
|
309
|
+
{#each _selected as option, idx}
|
|
296
310
|
<li class={liSelectedClass} aria-selected="true">
|
|
297
311
|
<slot name="selected" {option} {idx}>
|
|
298
312
|
{#if parseLabelsAsHtml}
|
|
@@ -320,14 +334,30 @@ function on_click_outside(event) {
|
|
|
320
334
|
{autocomplete}
|
|
321
335
|
bind:value={searchText}
|
|
322
336
|
on:mouseup|self|stopPropagation={open_dropdown}
|
|
323
|
-
on:keydown={handle_keydown}
|
|
337
|
+
on:keydown|stopPropagation={handle_keydown}
|
|
338
|
+
on:focus
|
|
324
339
|
on:focus={open_dropdown}
|
|
325
340
|
{id}
|
|
326
341
|
{name}
|
|
327
342
|
{disabled}
|
|
328
|
-
|
|
343
|
+
{inputmode}
|
|
344
|
+
{pattern}
|
|
345
|
+
placeholder={_selected.length == 0 ? placeholder : null}
|
|
329
346
|
aria-invalid={invalid ? `true` : null}
|
|
347
|
+
on:blur
|
|
348
|
+
on:change
|
|
349
|
+
on:click
|
|
350
|
+
on:keydown
|
|
351
|
+
on:keyup
|
|
352
|
+
on:mousedown
|
|
353
|
+
on:mouseenter
|
|
354
|
+
on:mouseleave
|
|
355
|
+
on:touchcancel
|
|
356
|
+
on:touchend
|
|
357
|
+
on:touchmove
|
|
358
|
+
on:touchstart
|
|
330
359
|
/>
|
|
360
|
+
<!-- the above on:* lines forward potentially useful DOM events -->
|
|
331
361
|
</li>
|
|
332
362
|
</ul>
|
|
333
363
|
{#if loading}
|
|
@@ -339,16 +369,16 @@ function on_click_outside(event) {
|
|
|
339
369
|
<slot name="disabled-icon">
|
|
340
370
|
<DisabledIcon width="15px" />
|
|
341
371
|
</slot>
|
|
342
|
-
{:else if
|
|
372
|
+
{:else if _selected.length > 0}
|
|
343
373
|
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
|
|
344
374
|
<Wiggle bind:wiggle angle={20}>
|
|
345
375
|
<span style="padding: 0 3pt;">
|
|
346
|
-
{maxSelectMsg?.(
|
|
347
|
-
(maxSelect > 1 ? `${
|
|
376
|
+
{maxSelectMsg?.(_selected.length, maxSelect) ??
|
|
377
|
+
(maxSelect > 1 ? `${_selected.length}/${maxSelect}` : ``)}
|
|
348
378
|
</span>
|
|
349
379
|
</Wiggle>
|
|
350
380
|
{/if}
|
|
351
|
-
{#if maxSelect !== 1 &&
|
|
381
|
+
{#if maxSelect !== 1 && _selected.length > 1}
|
|
352
382
|
<button
|
|
353
383
|
type="button"
|
|
354
384
|
class="remove-all"
|
|
@@ -375,8 +405,8 @@ function on_click_outside(event) {
|
|
|
375
405
|
{@const active = activeIndex === idx}
|
|
376
406
|
<li
|
|
377
407
|
on:mousedown|stopPropagation
|
|
378
|
-
on:mouseup|stopPropagation={() => {
|
|
379
|
-
if (!disabled) is_selected(label) ? remove(label) : add(label)
|
|
408
|
+
on:mouseup|stopPropagation={(event) => {
|
|
409
|
+
if (!disabled) is_selected(label) ? remove(label) : add(label, event)
|
|
380
410
|
}}
|
|
381
411
|
title={disabled
|
|
382
412
|
? disabledTitle
|
|
@@ -407,7 +437,7 @@ function on_click_outside(event) {
|
|
|
407
437
|
{#if allowUserOptions && searchText}
|
|
408
438
|
<li
|
|
409
439
|
on:mousedown|stopPropagation
|
|
410
|
-
on:mouseup|stopPropagation={() => add(searchText)}
|
|
440
|
+
on:mouseup|stopPropagation={(event) => add(searchText, event)}
|
|
411
441
|
title={addOptionMsg}
|
|
412
442
|
class:active={add_option_msg_is_active}
|
|
413
443
|
on:mouseover={() => (add_option_msg_is_active = true)}
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ declare const __propDef: {
|
|
|
17
17
|
id?: string | null | undefined;
|
|
18
18
|
input?: HTMLInputElement | null | undefined;
|
|
19
19
|
inputClass?: string | undefined;
|
|
20
|
+
inputmode?: string | null | undefined;
|
|
20
21
|
invalid?: boolean | undefined;
|
|
21
22
|
liActiveOptionClass?: string | undefined;
|
|
22
23
|
liOptionClass?: string | undefined;
|
|
@@ -32,14 +33,15 @@ declare const __propDef: {
|
|
|
32
33
|
outerDiv?: HTMLDivElement | null | undefined;
|
|
33
34
|
outerDivClass?: string | undefined;
|
|
34
35
|
parseLabelsAsHtml?: boolean | undefined;
|
|
36
|
+
pattern?: string | null | undefined;
|
|
35
37
|
placeholder?: string | null | undefined;
|
|
36
38
|
removeAllTitle?: string | undefined;
|
|
37
39
|
removeBtnTitle?: string | undefined;
|
|
38
40
|
required?: boolean | undefined;
|
|
39
41
|
searchText?: string | undefined;
|
|
40
|
-
selected?: Option[] | undefined;
|
|
41
|
-
selectedLabels?: (string | number)[] | undefined;
|
|
42
|
-
selectedValues?: unknown[] |
|
|
42
|
+
selected?: Option | Option[] | null | undefined;
|
|
43
|
+
selectedLabels?: string | number | (string | number)[] | null | undefined;
|
|
44
|
+
selectedValues?: unknown[] | unknown | null;
|
|
43
45
|
sortSelected?: boolean | ((op1: Option, op2: Option) => number) | undefined;
|
|
44
46
|
ulOptionsClass?: string | undefined;
|
|
45
47
|
ulSelectedClass?: string | undefined;
|
package/index.d.ts
CHANGED
|
@@ -25,11 +25,27 @@ export declare type DispatchEvents = {
|
|
|
25
25
|
options?: Option[];
|
|
26
26
|
type: 'add' | 'remove' | 'removeAll';
|
|
27
27
|
};
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
open: {
|
|
29
|
+
event: Event;
|
|
30
|
+
};
|
|
31
|
+
close: {
|
|
32
|
+
event: Event;
|
|
33
|
+
};
|
|
30
34
|
};
|
|
31
35
|
export declare type MultiSelectEvents = {
|
|
32
36
|
[key in keyof DispatchEvents]: CustomEvent<DispatchEvents[key]>;
|
|
37
|
+
} & {
|
|
38
|
+
blur: FocusEvent;
|
|
39
|
+
click: MouseEvent;
|
|
40
|
+
focus: FocusEvent;
|
|
41
|
+
keydown: KeyboardEvent;
|
|
42
|
+
keyup: KeyboardEvent;
|
|
43
|
+
mouseenter: MouseEvent;
|
|
44
|
+
mouseleave: MouseEvent;
|
|
45
|
+
touchcancel: TouchEvent;
|
|
46
|
+
touchend: TouchEvent;
|
|
47
|
+
touchmove: TouchEvent;
|
|
48
|
+
touchstart: TouchEvent;
|
|
33
49
|
};
|
|
34
50
|
export declare const get_label: (op: Option) => string | number;
|
|
35
51
|
export declare const get_value: (op: Option) => {};
|
package/package.json
CHANGED
|
@@ -5,20 +5,20 @@
|
|
|
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": "7.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
|
-
"@playwright/test": "^1.
|
|
14
|
+
"@playwright/test": "^1.26.0",
|
|
15
15
|
"@sveltejs/adapter-static": "^1.0.0-next.43",
|
|
16
|
-
"@sveltejs/kit": "^1.0.0-next.
|
|
17
|
-
"@sveltejs/package": "^1.0.0-next.
|
|
18
|
-
"@sveltejs/vite-plugin-svelte": "^1.0.
|
|
19
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
20
|
-
"@typescript-eslint/parser": "^5.
|
|
21
|
-
"eslint": "^8.
|
|
16
|
+
"@sveltejs/kit": "^1.0.0-next.502",
|
|
17
|
+
"@sveltejs/package": "^1.0.0-next.5",
|
|
18
|
+
"@sveltejs/vite-plugin-svelte": "^1.0.8",
|
|
19
|
+
"@typescript-eslint/eslint-plugin": "^5.38.0",
|
|
20
|
+
"@typescript-eslint/parser": "^5.38.0",
|
|
21
|
+
"eslint": "^8.24.0",
|
|
22
22
|
"eslint-plugin-svelte3": "^4.0.0",
|
|
23
23
|
"hastscript": "^7.0.2",
|
|
24
24
|
"jsdom": "^20.0.0",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"svelte-github-corner": "^0.1.0",
|
|
33
33
|
"svelte-preprocess": "^4.10.6",
|
|
34
34
|
"svelte-toc": "^0.4.0",
|
|
35
|
-
"svelte2tsx": "^0.5.
|
|
35
|
+
"svelte2tsx": "^0.5.18",
|
|
36
36
|
"tslib": "^2.4.0",
|
|
37
37
|
"typescript": "^4.8.3",
|
|
38
|
-
"vite": "^3.1.
|
|
39
|
-
"vitest": "^0.23.
|
|
38
|
+
"vite": "^3.1.3",
|
|
39
|
+
"vitest": "^0.23.4"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"svelte",
|
package/readme.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://app.netlify.com/sites/svelte-multiselect/deploys)
|
|
10
10
|
[](https://npmjs.com/package/svelte-multiselect)
|
|
11
11
|
[](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
|
|
12
|
-
[](https://svelte.dev/repl/a5a14b8f15d64cb083b567292480db05)
|
|
12
|
+
[](https://svelte.dev/repl/a5a14b8f15d64cb083b567292480db05)
|
|
13
13
|
[](https://stackblitz.com/github/janosh/svelte-multiselect)
|
|
14
14
|
|
|
15
15
|
</h4>
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
- **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).
|
|
41
41
|
- **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).
|
|
42
42
|
- **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).
|
|
43
|
+
- **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).
|
|
44
|
+
- **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).
|
|
43
45
|
|
|
44
46
|
## Installation
|
|
45
47
|
|
|
@@ -97,7 +99,8 @@ import type { Option } from 'svelte-multiselect'
|
|
|
97
99
|
allowUserOptions: boolean | 'append' = false
|
|
98
100
|
```
|
|
99
101
|
|
|
100
|
-
Whether users
|
|
102
|
+
Whether users can enter values that are not in the dropdown list. `true` means add user-defined options to the selected list only, `'append'` means add to both options and selected.
|
|
103
|
+
If `allowUserOptions` is `true` or `'append'` then the type `object | number | string` of entered value is determined from the first option of the list to keep type homogeneity.
|
|
101
104
|
|
|
102
105
|
1. ```ts
|
|
103
106
|
autocomplete: string = `off`
|
|
@@ -162,6 +165,12 @@ import type { Option } from 'svelte-multiselect'
|
|
|
162
165
|
|
|
163
166
|
Handle to the `<input>` DOM node. Only available after component mounts (`null` before then).
|
|
164
167
|
|
|
168
|
+
1. ```ts
|
|
169
|
+
inputmode: string | null = null
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The `inputmode` attribute hints at the type of data the user may enter. Values like `'numeric' | 'tel' | 'email'` allow browsers to display an appropriate virtual keyboard. See [MDN](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/inputmode) for details.
|
|
173
|
+
|
|
165
174
|
1. ```ts
|
|
166
175
|
invalid: boolean = false
|
|
167
176
|
```
|
|
@@ -218,7 +227,7 @@ import type { Option } from 'svelte-multiselect'
|
|
|
218
227
|
options: Option[]
|
|
219
228
|
```
|
|
220
229
|
|
|
221
|
-
**The only required prop** (no default). 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. A few keys like `preselected` and `title` have special meaning though. See `src/lib/index.ts` for all special keys and their purpose.
|
|
230
|
+
**The only required prop** (no default). 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. A few keys like `preselected` and `title` have special meaning though. See type `ObjectOption` in [`src/lib/index.ts`](https://github.com/janosh/svelte-multiselect/blob/main/src/lib/index.ts) for all special keys and their purpose.
|
|
222
231
|
|
|
223
232
|
1. ```ts
|
|
224
233
|
outerDiv: HTMLDivElement | null = null
|
|
@@ -232,6 +241,12 @@ import type { Option } from 'svelte-multiselect'
|
|
|
232
241
|
|
|
233
242
|
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).
|
|
234
243
|
|
|
244
|
+
1. ```ts
|
|
245
|
+
pattern: string | null = null
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The pattern attribute specifies a regular expression which the input's value must match. If a non-null value doesn't match the `pattern` regex, the read-only `patternMismatch` property will be `true`. See [MDN](https://developer.mozilla.org/docs/Web/HTML/Attributes/pattern) for details.
|
|
249
|
+
|
|
235
250
|
1. ```ts
|
|
236
251
|
placeholder: string | null = null
|
|
237
252
|
```
|
|
@@ -263,22 +278,25 @@ import type { Option } from 'svelte-multiselect'
|
|
|
263
278
|
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.
|
|
264
279
|
|
|
265
280
|
1. ```ts
|
|
266
|
-
selected: Option[]
|
|
281
|
+
selected: Option[] | Option | null =
|
|
282
|
+
options
|
|
283
|
+
?.filter((op) => (op as ObjectOption)?.preselected)
|
|
284
|
+
.slice(0, maxSelect ?? undefined) ?? []
|
|
267
285
|
```
|
|
268
286
|
|
|
269
|
-
Array of currently selected options.
|
|
287
|
+
Array of currently selected options. Supports 2-way binding `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. If `maxSelect={1}`, selected will not be an array but a single `Option` or `null` if no options are selected.
|
|
270
288
|
|
|
271
289
|
1. ```ts
|
|
272
|
-
selectedLabels: (string | number)[] = []
|
|
290
|
+
selectedLabels: (string | number)[] | string | number | null = []
|
|
273
291
|
```
|
|
274
292
|
|
|
275
|
-
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`.
|
|
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.
|
|
276
294
|
|
|
277
295
|
1. ```ts
|
|
278
|
-
selectedValues: unknown[] = []
|
|
296
|
+
selectedValues: unknown[] | unknown | null = []
|
|
279
297
|
```
|
|
280
298
|
|
|
281
|
-
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`.
|
|
299
|
+
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`. If `maxSelect={1}`, selectedLabels will not be an array but a single value or `null` if no options are selected.
|
|
282
300
|
|
|
283
301
|
1. ```ts
|
|
284
302
|
sortSelected: boolean | ((op1: Option, op2: Option) => number) = false
|
|
@@ -286,13 +304,6 @@ import type { Option } from 'svelte-multiselect'
|
|
|
286
304
|
|
|
287
305
|
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.
|
|
288
306
|
|
|
289
|
-
1. ```ts
|
|
290
|
-
userInputAs: 'string' | 'number' | 'object' =
|
|
291
|
-
options.length > 0 ? (typeof options[0] as 'object' | 'string' | 'number') : 'string'
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
What type new options created from user text input should be. Only relevant if `allowUserOptions=true | 'append'`. If not explicitly set, we default `userInputAs` to the type of the 1st option (if available, else `string`) to keep type homogeneity. E.g. if MultiSelect already contains at least one option and it's an object, new options from user-entered text will take the shape `{label: userText, value: userText}`. Likewise if the 1st existing option is a number of string. If MultiSelect starts out empty but you still want user-created custom options to be objects, pass `userInputAs='object'`.
|
|
295
|
-
|
|
296
307
|
## Slots
|
|
297
308
|
|
|
298
309
|
`MultiSelect.svelte` has 3 named slots:
|
|
@@ -353,10 +364,16 @@ Example:
|
|
|
353
364
|
Triggers when an option is either added or removed, or all options are removed at once. `type` is one of `'add' | 'remove' | 'removeAll'` and payload will be `option: Option` or `options: Option[]`, respectively.
|
|
354
365
|
|
|
355
366
|
1. ```ts
|
|
356
|
-
on:
|
|
367
|
+
on:open={(event) => console.log(`Multiselect dropdown was opened by ${event}`)}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Triggers when the dropdown list of options appears. Event is the DOM's `FocusEvent`,`KeyboardEvent` or `ClickEvent` that initiated this Svelte `dispatch` event.
|
|
371
|
+
|
|
372
|
+
1. ```ts
|
|
373
|
+
on:close={(event) => console.log(`Multiselect dropdown was closed by ${event}`)}
|
|
357
374
|
```
|
|
358
375
|
|
|
359
|
-
Triggers when the
|
|
376
|
+
Triggers when the dropdown list of options disappears. Event is the DOM's `FocusEvent`, `KeyboardEvent` or `ClickEvent` that initiated this Svelte `dispatch` event.
|
|
360
377
|
|
|
361
378
|
For example, here's how you might annoy your users with an alert every time one or more options are added or removed:
|
|
362
379
|
|
|
@@ -372,6 +389,15 @@ For example, here's how you might annoy your users with an alert every time one
|
|
|
372
389
|
|
|
373
390
|
> Note: Depending on the data passed to the component the `options(s)` payload will either be objects or simple strings/numbers.
|
|
374
391
|
|
|
392
|
+
This component also forwards many DOM events from the `<input>` node: `blur`, `change`, `click`, `keydown`, `keyup`, `mousedown`, `mouseenter`, `mouseleave`, `touchcancel`, `touchend`, `touchmove`, `touchstart`. You can register listeners for these just like for the above [Svelte `dispatch` events](https://svelte.dev/tutorial/component-events):
|
|
393
|
+
|
|
394
|
+
```svelte
|
|
395
|
+
<MultiSelect
|
|
396
|
+
options={[1, 2, 3]}
|
|
397
|
+
on:keyup={(event) => console.log('key', event.target.value)}
|
|
398
|
+
/>
|
|
399
|
+
```
|
|
400
|
+
|
|
375
401
|
## TypeScript
|
|
376
402
|
|
|
377
403
|
TypeScript users can import the types used for internal type safety:
|
|
@@ -461,12 +487,12 @@ For example, to change the background color of the options dropdown:
|
|
|
461
487
|
|
|
462
488
|
The second method allows you to pass in custom classes to the important DOM elements of this component to target them with frameworks like [Tailwind CSS](https://tailwindcss.com).
|
|
463
489
|
|
|
464
|
-
- `outerDivClass`
|
|
465
|
-
- `ulSelectedClass
|
|
466
|
-
- `liSelectedClass
|
|
467
|
-
- `ulOptionsClass`
|
|
468
|
-
- `liOptionClass
|
|
469
|
-
- `liActiveOptionClass
|
|
490
|
+
- `outerDivClass`: wrapper `div` enclosing the whole component
|
|
491
|
+
- `ulSelectedClass`: list of selected options
|
|
492
|
+
- `liSelectedClass`: selected list items
|
|
493
|
+
- `ulOptionsClass`: available options listed in the dropdown when component is in `open` state
|
|
494
|
+
- `liOptionClass`: list items selectable from dropdown list
|
|
495
|
+
- `liActiveOptionClass`: the currently active dropdown list item (i.e. hovered or navigated to with arrow keys)
|
|
470
496
|
|
|
471
497
|
This simplified version of the DOM structure of the component shows where these classes are inserted:
|
|
472
498
|
|
|
@@ -486,9 +512,9 @@ This simplified version of the DOM structure of the component shows where these
|
|
|
486
512
|
</div>
|
|
487
513
|
```
|
|
488
514
|
|
|
489
|
-
###
|
|
515
|
+
### With global CSS
|
|
490
516
|
|
|
491
|
-
|
|
517
|
+
Odd as it may seem, you get the most fine-grained control over the styling of every part of this component by using the following `:global()` CSS selectors. `ul.selected` is the list of currently selected options rendered inside the component's input whereas `ul.options` is the list of available options that slides out when the component is in its `open` state. See also [simplified DOM structure](#styling).
|
|
492
518
|
|
|
493
519
|
```css
|
|
494
520
|
:global(div.multiselect) {
|