svelte-multiselect 8.0.3 → 8.1.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 +34 -9
- package/MultiSelect.svelte.d.ts +4 -1
- package/package.json +1 -1
- package/readme.md +29 -8
package/MultiSelect.svelte
CHANGED
|
@@ -22,6 +22,7 @@ export let filterFunc = (op, searchText) => {
|
|
|
22
22
|
return `${get_label(op)}`.toLowerCase().includes(searchText.toLowerCase());
|
|
23
23
|
};
|
|
24
24
|
export let focusInputOnSelect = `desktop`;
|
|
25
|
+
export let form_input;
|
|
25
26
|
export let id = null;
|
|
26
27
|
export let input = null;
|
|
27
28
|
export let inputClass = ``;
|
|
@@ -32,8 +33,9 @@ export let liOptionClass = ``;
|
|
|
32
33
|
export let liSelectedClass = ``;
|
|
33
34
|
export let loading = false;
|
|
34
35
|
export let matchingOptions = [];
|
|
35
|
-
export let maxSelect = null; // null means
|
|
36
|
+
export let maxSelect = null; // null means there is no upper limit for selected.length
|
|
36
37
|
export let maxSelectMsg = (current, max) => (max > 1 ? `${current}/${max}` : ``);
|
|
38
|
+
export let maxSelectMsgClass = ``;
|
|
37
39
|
export let name = null;
|
|
38
40
|
export let noMatchingOptionsMsg = `No matching options`;
|
|
39
41
|
export let open = false;
|
|
@@ -45,6 +47,7 @@ export let pattern = null;
|
|
|
45
47
|
export let placeholder = null;
|
|
46
48
|
export let removeAllTitle = `Remove all`;
|
|
47
49
|
export let removeBtnTitle = `Remove`;
|
|
50
|
+
export let minSelect = null; // null means there is no lower limit for selected.length
|
|
48
51
|
export let required = false;
|
|
49
52
|
export let resetFilterOnAdd = true;
|
|
50
53
|
export let searchText = ``;
|
|
@@ -63,7 +66,7 @@ const get_label = (op) => (op instanceof Object ? op.label : op);
|
|
|
63
66
|
$: value = maxSelect === 1 ? selected[0] ?? null : selected;
|
|
64
67
|
let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
|
|
65
68
|
if (!(options?.length > 0)) {
|
|
66
|
-
if (allowUserOptions || loading) {
|
|
69
|
+
if (allowUserOptions || loading || disabled) {
|
|
67
70
|
options = []; // initializing as array avoids errors when component mounts
|
|
68
71
|
}
|
|
69
72
|
else {
|
|
@@ -76,10 +79,13 @@ if (parseLabelsAsHtml && allowUserOptions) {
|
|
|
76
79
|
console.warn(`Don't combine parseLabelsAsHtml and allowUserOptions. It's susceptible to XSS attacks!`);
|
|
77
80
|
}
|
|
78
81
|
if (maxSelect !== null && maxSelect < 1) {
|
|
79
|
-
console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
82
|
+
console.error(`MultiSelect's maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
80
83
|
}
|
|
81
84
|
if (!Array.isArray(selected)) {
|
|
82
|
-
console.error(`
|
|
85
|
+
console.error(`MultiSelect's selected prop should always be an array, got ${selected}`);
|
|
86
|
+
}
|
|
87
|
+
if (maxSelect && typeof required === `number` && required > maxSelect) {
|
|
88
|
+
console.error(`MultiSelect maxSelect=${maxSelect} < required=${required}, makes it impossible for users to submit a valid form`);
|
|
83
89
|
}
|
|
84
90
|
const dispatch = createEventDispatcher();
|
|
85
91
|
let add_option_msg_is_active = false; // controls active state of <li>{addOptionMsg}</li>
|
|
@@ -161,6 +167,7 @@ function add(label, event) {
|
|
|
161
167
|
dispatch(`add`, { option });
|
|
162
168
|
dispatch(`change`, { option, type: `add` });
|
|
163
169
|
invalid = false; // reset error status whenever new items are selected
|
|
170
|
+
form_input?.setCustomValidity(``);
|
|
164
171
|
}
|
|
165
172
|
}
|
|
166
173
|
// remove an option from selected list
|
|
@@ -180,6 +187,7 @@ function remove(label) {
|
|
|
180
187
|
dispatch(`remove`, { option });
|
|
181
188
|
dispatch(`change`, { option, type: `remove` });
|
|
182
189
|
invalid = false; // reset error status whenever items are removed
|
|
190
|
+
form_input?.setCustomValidity(``);
|
|
183
191
|
}
|
|
184
192
|
function open_dropdown(event) {
|
|
185
193
|
if (disabled)
|
|
@@ -296,17 +304,30 @@ function on_click_outside(event) {
|
|
|
296
304
|
on:mouseup|stopPropagation={open_dropdown}
|
|
297
305
|
title={disabled ? disabledInputTitle : null}
|
|
298
306
|
aria-disabled={disabled ? `true` : null}
|
|
307
|
+
data-id={id}
|
|
299
308
|
>
|
|
300
309
|
<!-- bind:value={selected} prevents form submission if required prop is true and no options are selected -->
|
|
301
310
|
<input
|
|
302
|
-
{required}
|
|
303
311
|
{name}
|
|
304
|
-
|
|
312
|
+
required={Boolean(required)}
|
|
313
|
+
value={selected.length >= required ? JSON.stringify(selected) : null}
|
|
305
314
|
tabindex="-1"
|
|
306
315
|
aria-hidden="true"
|
|
307
316
|
aria-label="ignore this, used only to prevent form submission if select is required but empty"
|
|
308
317
|
class="form-control"
|
|
309
|
-
|
|
318
|
+
bind:this={form_input}
|
|
319
|
+
on:invalid={() => {
|
|
320
|
+
invalid = true
|
|
321
|
+
let msg
|
|
322
|
+
if (maxSelect && maxSelect > 1 && required > 1) {
|
|
323
|
+
msg = `Please select between ${required} and ${maxSelect} options`
|
|
324
|
+
} else if (required > 1) {
|
|
325
|
+
msg = `Please select at least ${required} options`
|
|
326
|
+
} else {
|
|
327
|
+
msg = `Please select an option`
|
|
328
|
+
}
|
|
329
|
+
form_input.setCustomValidity(msg)
|
|
330
|
+
}}
|
|
310
331
|
/>
|
|
311
332
|
<ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt;" />
|
|
312
333
|
<ul class="selected {ulSelectedClass}">
|
|
@@ -319,7 +340,7 @@ function on_click_outside(event) {
|
|
|
319
340
|
{get_label(option)}
|
|
320
341
|
{/if}
|
|
321
342
|
</slot>
|
|
322
|
-
{#if !disabled}
|
|
343
|
+
{#if !disabled && (minSelect === null || selected.length > minSelect)}
|
|
323
344
|
<button
|
|
324
345
|
on:mouseup|stopPropagation={() => remove(get_label(option))}
|
|
325
346
|
on:keydown={if_enter_or_space(() => remove(get_label(option)))}
|
|
@@ -377,7 +398,7 @@ function on_click_outside(event) {
|
|
|
377
398
|
{:else if selected.length > 0}
|
|
378
399
|
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
|
|
379
400
|
<Wiggle bind:wiggle angle={20}>
|
|
380
|
-
<span
|
|
401
|
+
<span class="max-select-msg {maxSelectMsgClass}">
|
|
381
402
|
{maxSelectMsg?.(selected.length, maxSelect)}
|
|
382
403
|
</span>
|
|
383
404
|
</Wiggle>
|
|
@@ -607,4 +628,8 @@ function on_click_outside(event) {
|
|
|
607
628
|
background: var(--sms-li-disabled-bg, #f5f5f6);
|
|
608
629
|
color: var(--sms-li-disabled-text, #b8b8b8);
|
|
609
630
|
}
|
|
631
|
+
|
|
632
|
+
:where(span.max-select-msg) {
|
|
633
|
+
padding: 0 3pt;
|
|
634
|
+
}
|
|
610
635
|
</style>
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ declare const __propDef: {
|
|
|
17
17
|
duplicates?: boolean | undefined;
|
|
18
18
|
filterFunc?: ((op: Option, searchText: string) => boolean) | undefined;
|
|
19
19
|
focusInputOnSelect?: boolean | "desktop" | undefined;
|
|
20
|
+
form_input: HTMLInputElement;
|
|
20
21
|
id?: string | null | undefined;
|
|
21
22
|
input?: HTMLInputElement | null | undefined;
|
|
22
23
|
inputClass?: string | undefined;
|
|
@@ -29,6 +30,7 @@ declare const __propDef: {
|
|
|
29
30
|
matchingOptions?: Option[] | undefined;
|
|
30
31
|
maxSelect?: number | null | undefined;
|
|
31
32
|
maxSelectMsg?: ((current: number, max: number) => string) | null | undefined;
|
|
33
|
+
maxSelectMsgClass?: string | undefined;
|
|
32
34
|
name?: string | null | undefined;
|
|
33
35
|
noMatchingOptionsMsg?: string | undefined;
|
|
34
36
|
open?: boolean | undefined;
|
|
@@ -40,7 +42,8 @@ declare const __propDef: {
|
|
|
40
42
|
placeholder?: string | null | undefined;
|
|
41
43
|
removeAllTitle?: string | undefined;
|
|
42
44
|
removeBtnTitle?: string | undefined;
|
|
43
|
-
|
|
45
|
+
minSelect?: number | null | undefined;
|
|
46
|
+
required?: number | boolean | undefined;
|
|
44
47
|
resetFilterOnAdd?: boolean | undefined;
|
|
45
48
|
searchText?: string | undefined;
|
|
46
49
|
selected?: Option[] | undefined;
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
<slot name="examples" />
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Features
|
|
25
25
|
|
|
26
26
|
- **Bindable:** `bind:selected` gives you an array of the currently selected options. Thanks to Svelte's 2-way binding, it can also control the component state externally through assignment `selected = ['foo', 42]`.
|
|
27
27
|
- **Keyboard friendly** for mouse-less form completion
|
|
@@ -36,9 +36,6 @@
|
|
|
36
36
|
|
|
37
37
|
## Recent breaking changes
|
|
38
38
|
|
|
39
|
-
- **v6.0.0** The prop `showOptions` which controls whether the list of dropdown options is currently open or closed was renamed to `open`. [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`. [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. [PR 105](https://github.com/janosh/svelte-multiselect/pull/105).
|
|
42
39
|
- **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. [PR 120](https://github.com/janosh/svelte-multiselect/pull/120).
|
|
43
40
|
- **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. [PR 123](https://github.com/janosh/svelte-multiselect/pull/123).
|
|
44
41
|
- **8.0.0**
|
|
@@ -171,6 +168,12 @@ Full list of props/bindable variables for this component. The `Option` type you
|
|
|
171
168
|
|
|
172
169
|
One of `true`, `false` or `'desktop'`. Whether to set the cursor back to the input element after selecting an element. 'desktop' means only do so if current window width is larger than the current value of `breakpoint` prop (default 800).
|
|
173
170
|
|
|
171
|
+
1. ```ts
|
|
172
|
+
form_input: HTMLInputElement
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Handle to the `<input>` DOM node that's responsible for form validity checks and passing selected options to form submission handlers. Only available after component mounts (`null` before then).
|
|
176
|
+
|
|
174
177
|
1. ```ts
|
|
175
178
|
id: string | null = null
|
|
176
179
|
```
|
|
@@ -193,7 +196,7 @@ Full list of props/bindable variables for this component. The `Option` type you
|
|
|
193
196
|
invalid: boolean = false
|
|
194
197
|
```
|
|
195
198
|
|
|
196
|
-
If `required=true
|
|
199
|
+
If `required = true, 1, 2, ...` and user tries to submit form but `selected = []` is empty/`selected.length < required`, `invalid` is automatically set to `true` and CSS class `invalid` applied to the top-level `div.multiselect`. `invalid` class is removed as soon as any change to `selected` is registered. `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.
|
|
197
200
|
|
|
198
201
|
1. ```ts
|
|
199
202
|
loading: boolean = false
|
|
@@ -226,6 +229,16 @@ Full list of props/bindable variables for this component. The `Option` type you
|
|
|
226
229
|
maxSelectMsg = (current: number, max: number) => `${current}/${max}`
|
|
227
230
|
```
|
|
228
231
|
|
|
232
|
+
Use CSS selector `span.max-select-msg` (or prop `maxSelectMsgClass` if you're using a CSS framework like Tailwind) to customize appearance of the message container.
|
|
233
|
+
|
|
234
|
+
1. ```ts
|
|
235
|
+
minSelect: number | null = null
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Conditionally render the `x` button which removes a selected option depending on the number of selected options. Meaning all remove buttons disappear if `selected.length <= minSelect`. E.g. if 2 options are selected and `minSelect={3}`, users will not be able to remove any selections until they selected more than 3 options.
|
|
239
|
+
|
|
240
|
+
Note: Prop `required={3}` should be used instead if you only care about the component state at form submission time. `minSelect={3}` should be used if you want to place constraints on component state at all times.
|
|
241
|
+
|
|
229
242
|
1. ```ts
|
|
230
243
|
name: string | null = null
|
|
231
244
|
```
|
|
@@ -287,10 +300,10 @@ Full list of props/bindable variables for this component. The `Option` type you
|
|
|
287
300
|
Title text to display when user hovers over button to remove selected option (which defaults to a cross icon).
|
|
288
301
|
|
|
289
302
|
1. ```ts
|
|
290
|
-
required: boolean = false
|
|
303
|
+
required: boolean | number = false
|
|
291
304
|
```
|
|
292
305
|
|
|
293
|
-
|
|
306
|
+
If `required = true, 1, 2, ...` forms can't be submitted without selecting given number of options. `true` means 1. `false` means even empty MultiSelect will pass form validity check. If user tries to submit a form containing MultiSelect with less than the required number of options, submission is aborted, MultiSelect scrolls into view and shows message "Please select at least `required` options".
|
|
294
307
|
|
|
295
308
|
1. ```ts
|
|
296
309
|
resetFilterOnAdd: boolean = true
|
|
@@ -517,6 +530,7 @@ The second method allows you to pass in custom classes to the important DOM elem
|
|
|
517
530
|
- `ulOptionsClass`: available options listed in the dropdown when component is in `open` state
|
|
518
531
|
- `liOptionClass`: list items selectable from dropdown list
|
|
519
532
|
- `liActiveOptionClass`: the currently active dropdown list item (i.e. hovered or navigated to with arrow keys)
|
|
533
|
+
- `maxSelectMsgClass`: small span towards the right end of the input field displaying to the user how many of the allowed number of options they've already selected
|
|
520
534
|
|
|
521
535
|
This simplified version of the DOM structure of the component shows where these classes are inserted:
|
|
522
536
|
|
|
@@ -527,6 +541,7 @@ This simplified version of the DOM structure of the component shows where these
|
|
|
527
541
|
<li class={liSelectedClass}>Selected 1</li>
|
|
528
542
|
<li class={liSelectedClass}>Selected 2</li>
|
|
529
543
|
</ul>
|
|
544
|
+
<span class="maxSelectMsgClass">2/5 selected</span>
|
|
530
545
|
<ul class="options {ulOptionsClass}">
|
|
531
546
|
<li class={liOptionClass}>Option 1</li>
|
|
532
547
|
<li class="{liOptionClass} {liActiveOptionClass}">
|
|
@@ -588,7 +603,7 @@ Odd as it may seem, you get the most fine-grained control over the styling of ev
|
|
|
588
603
|
|
|
589
604
|
## Want to contribute?
|
|
590
605
|
|
|
591
|
-
To submit a PR, clone the repo, install dependencies and start the dev server to
|
|
606
|
+
To submit a PR, clone the repo, install dependencies and start the dev server to see changes as you make them.
|
|
592
607
|
|
|
593
608
|
```sh
|
|
594
609
|
git clone https://github.com/janosh/svelte-multiselect
|
|
@@ -596,3 +611,9 @@ cd svelte-multiselect
|
|
|
596
611
|
pnpm install
|
|
597
612
|
pnpm dev
|
|
598
613
|
```
|
|
614
|
+
|
|
615
|
+
To make sure your changes didn't break anything, you can run the full test suite (which also runs in CI) using:
|
|
616
|
+
|
|
617
|
+
```sh
|
|
618
|
+
pnpm test
|
|
619
|
+
```
|