svelte-multiselect 8.0.4 → 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 +33 -8
- package/MultiSelect.svelte.d.ts +4 -1
- package/package.json +1 -1
- package/readme.md +28 -4
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 = ``;
|
|
@@ -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
|
@@ -168,6 +168,12 @@ Full list of props/bindable variables for this component. The `Option` type you
|
|
|
168
168
|
|
|
169
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).
|
|
170
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
|
+
|
|
171
177
|
1. ```ts
|
|
172
178
|
id: string | null = null
|
|
173
179
|
```
|
|
@@ -190,7 +196,7 @@ Full list of props/bindable variables for this component. The `Option` type you
|
|
|
190
196
|
invalid: boolean = false
|
|
191
197
|
```
|
|
192
198
|
|
|
193
|
-
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.
|
|
194
200
|
|
|
195
201
|
1. ```ts
|
|
196
202
|
loading: boolean = false
|
|
@@ -223,6 +229,16 @@ Full list of props/bindable variables for this component. The `Option` type you
|
|
|
223
229
|
maxSelectMsg = (current: number, max: number) => `${current}/${max}`
|
|
224
230
|
```
|
|
225
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
|
+
|
|
226
242
|
1. ```ts
|
|
227
243
|
name: string | null = null
|
|
228
244
|
```
|
|
@@ -284,10 +300,10 @@ Full list of props/bindable variables for this component. The `Option` type you
|
|
|
284
300
|
Title text to display when user hovers over button to remove selected option (which defaults to a cross icon).
|
|
285
301
|
|
|
286
302
|
1. ```ts
|
|
287
|
-
required: boolean = false
|
|
303
|
+
required: boolean | number = false
|
|
288
304
|
```
|
|
289
305
|
|
|
290
|
-
|
|
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".
|
|
291
307
|
|
|
292
308
|
1. ```ts
|
|
293
309
|
resetFilterOnAdd: boolean = true
|
|
@@ -514,6 +530,7 @@ The second method allows you to pass in custom classes to the important DOM elem
|
|
|
514
530
|
- `ulOptionsClass`: available options listed in the dropdown when component is in `open` state
|
|
515
531
|
- `liOptionClass`: list items selectable from dropdown list
|
|
516
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
|
|
517
534
|
|
|
518
535
|
This simplified version of the DOM structure of the component shows where these classes are inserted:
|
|
519
536
|
|
|
@@ -524,6 +541,7 @@ This simplified version of the DOM structure of the component shows where these
|
|
|
524
541
|
<li class={liSelectedClass}>Selected 1</li>
|
|
525
542
|
<li class={liSelectedClass}>Selected 2</li>
|
|
526
543
|
</ul>
|
|
544
|
+
<span class="maxSelectMsgClass">2/5 selected</span>
|
|
527
545
|
<ul class="options {ulOptionsClass}">
|
|
528
546
|
<li class={liOptionClass}>Option 1</li>
|
|
529
547
|
<li class="{liOptionClass} {liActiveOptionClass}">
|
|
@@ -585,7 +603,7 @@ Odd as it may seem, you get the most fine-grained control over the styling of ev
|
|
|
585
603
|
|
|
586
604
|
## Want to contribute?
|
|
587
605
|
|
|
588
|
-
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.
|
|
589
607
|
|
|
590
608
|
```sh
|
|
591
609
|
git clone https://github.com/janosh/svelte-multiselect
|
|
@@ -593,3 +611,9 @@ cd svelte-multiselect
|
|
|
593
611
|
pnpm install
|
|
594
612
|
pnpm dev
|
|
595
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
|
+
```
|