svelte-multiselect 7.1.0 → 8.0.1
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 +72 -217
- package/MultiSelect.svelte.d.ts +4 -4
- package/package.json +18 -18
- package/readme.md +30 -28
package/MultiSelect.svelte
CHANGED
|
@@ -33,9 +33,9 @@ export let liSelectedClass = ``;
|
|
|
33
33
|
export let loading = false;
|
|
34
34
|
export let matchingOptions = [];
|
|
35
35
|
export let maxSelect = null; // null means any number of options are selectable
|
|
36
|
-
export let maxSelectMsg =
|
|
36
|
+
export let maxSelectMsg = (current, max) => (max > 1 ? `${current}/${max}` : ``);
|
|
37
37
|
export let name = null;
|
|
38
|
-
export let
|
|
38
|
+
export let noMatchingOptionsMsg = `No matching options`;
|
|
39
39
|
export let open = false;
|
|
40
40
|
export let options;
|
|
41
41
|
export let outerDiv = null;
|
|
@@ -46,29 +46,22 @@ export let placeholder = null;
|
|
|
46
46
|
export let removeAllTitle = `Remove all`;
|
|
47
47
|
export let removeBtnTitle = `Remove`;
|
|
48
48
|
export let required = false;
|
|
49
|
+
export let resetFilterOnAdd = true;
|
|
49
50
|
export let searchText = ``;
|
|
50
51
|
export let selected = options
|
|
51
52
|
?.filter((op) => op?.preselected)
|
|
52
53
|
.slice(0, maxSelect ?? undefined) ?? [];
|
|
53
|
-
export let selectedLabels = [];
|
|
54
|
-
export let selectedValues = [];
|
|
55
54
|
export let sortSelected = false;
|
|
56
55
|
export let ulOptionsClass = ``;
|
|
57
56
|
export let ulSelectedClass = ``;
|
|
57
|
+
export let value = null;
|
|
58
58
|
// get the label key from an option object or the option itself if it's a string or number
|
|
59
59
|
const get_label = (op) => (op instanceof Object ? op.label : op);
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
// https://github.com/janosh/svelte-multiselect/issues/86
|
|
65
|
-
let _selected = (selected ?? []);
|
|
66
|
-
$: 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;
|
|
67
64
|
let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
|
|
68
|
-
$: _selectedLabels = _selected?.map(get_label) ?? [];
|
|
69
|
-
$: selectedLabels = maxSelect === 1 ? _selectedLabels[0] ?? null : _selectedLabels;
|
|
70
|
-
$: _selectedValues = _selected?.map(get_value) ?? [];
|
|
71
|
-
$: selectedValues = maxSelect === 1 ? _selectedValues[0] ?? null : _selectedValues;
|
|
72
65
|
if (!(options?.length > 0)) {
|
|
73
66
|
if (allowUserOptions) {
|
|
74
67
|
options = []; // initializing as array avoids errors when component mounts
|
|
@@ -84,19 +77,14 @@ if (parseLabelsAsHtml && allowUserOptions) {
|
|
|
84
77
|
if (maxSelect !== null && maxSelect < 1) {
|
|
85
78
|
console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
86
79
|
}
|
|
87
|
-
if (!Array.isArray(
|
|
88
|
-
console.error(`internal variable
|
|
80
|
+
if (!Array.isArray(selected)) {
|
|
81
|
+
console.error(`internal variable selected prop should always be an array, got ${selected}`);
|
|
89
82
|
}
|
|
90
83
|
const dispatch = createEventDispatcher();
|
|
91
84
|
let add_option_msg_is_active = false; // controls active state of <li>{addOptionMsg}</li>
|
|
92
85
|
let window_width;
|
|
93
|
-
// formValue binds to input.form-control to prevent form submission if required
|
|
94
|
-
// prop is true and no options are selected
|
|
95
|
-
$: form_value = _selectedValues.join(`,`);
|
|
96
|
-
$: if (form_value)
|
|
97
|
-
invalid = false; // reset error status whenever component state changes
|
|
98
86
|
// options matching the current search text
|
|
99
|
-
$: 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
|
|
100
88
|
);
|
|
101
89
|
// raise if matchingOptions[activeIndex] does not yield a value
|
|
102
90
|
if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
@@ -106,12 +94,12 @@ if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
|
106
94
|
$: activeOption = activeIndex !== null ? matchingOptions[activeIndex] : null;
|
|
107
95
|
// add an option to selected list
|
|
108
96
|
function add(label, event) {
|
|
109
|
-
if (maxSelect && maxSelect > 1 &&
|
|
97
|
+
if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
|
|
110
98
|
wiggle = true;
|
|
111
|
-
if (!isNaN(Number(label)) && typeof
|
|
99
|
+
if (!isNaN(Number(label)) && typeof selected.map(get_label)[0] === `number`)
|
|
112
100
|
label = Number(label); // convert to number if possible
|
|
113
|
-
const is_duplicate =
|
|
114
|
-
if ((maxSelect === null || maxSelect === 1 ||
|
|
101
|
+
const is_duplicate = selected.some((option) => duplicateFunc(option, label));
|
|
102
|
+
if ((maxSelect === null || maxSelect === 1 || selected.length < maxSelect) &&
|
|
115
103
|
(duplicates || !is_duplicate)) {
|
|
116
104
|
// first check if we find option in the options list
|
|
117
105
|
let option = options.find((op) => get_label(op) === label);
|
|
@@ -140,29 +128,30 @@ function add(label, event) {
|
|
|
140
128
|
if (option === undefined) {
|
|
141
129
|
throw `Run time error, option with label ${label} not found in options list`;
|
|
142
130
|
}
|
|
143
|
-
|
|
131
|
+
if (resetFilterOnAdd)
|
|
132
|
+
searchText = ``; // reset search string on selection
|
|
144
133
|
if ([``, undefined, null].includes(option)) {
|
|
145
134
|
console.error(`MultiSelect: encountered missing option with label ${label} (or option is poorly labeled)`);
|
|
146
135
|
return;
|
|
147
136
|
}
|
|
148
137
|
if (maxSelect === 1) {
|
|
149
138
|
// for maxselect = 1 we always replace current option with new one
|
|
150
|
-
|
|
139
|
+
selected = [option];
|
|
151
140
|
}
|
|
152
141
|
else {
|
|
153
|
-
|
|
142
|
+
selected = [...selected, option];
|
|
154
143
|
if (sortSelected === true) {
|
|
155
|
-
|
|
144
|
+
selected = selected.sort((op1, op2) => {
|
|
156
145
|
const [label1, label2] = [get_label(op1), get_label(op2)];
|
|
157
146
|
// coerce to string if labels are numbers
|
|
158
147
|
return `${label1}`.localeCompare(`${label2}`);
|
|
159
148
|
});
|
|
160
149
|
}
|
|
161
150
|
else if (typeof sortSelected === `function`) {
|
|
162
|
-
|
|
151
|
+
selected = selected.sort(sortSelected);
|
|
163
152
|
}
|
|
164
153
|
}
|
|
165
|
-
if (
|
|
154
|
+
if (selected.length === maxSelect)
|
|
166
155
|
close_dropdown(event);
|
|
167
156
|
else if (focusInputOnSelect === true ||
|
|
168
157
|
(focusInputOnSelect === `desktop` && window_width > breakpoint)) {
|
|
@@ -170,14 +159,15 @@ function add(label, event) {
|
|
|
170
159
|
}
|
|
171
160
|
dispatch(`add`, { option });
|
|
172
161
|
dispatch(`change`, { option, type: `add` });
|
|
162
|
+
invalid = false; // reset error status whenever new items are selected
|
|
173
163
|
}
|
|
174
164
|
}
|
|
175
165
|
// remove an option from selected list
|
|
176
166
|
function remove(label) {
|
|
177
|
-
if (
|
|
167
|
+
if (selected.length === 0)
|
|
178
168
|
return;
|
|
179
|
-
|
|
180
|
-
|
|
169
|
+
selected.splice(selected.map(get_label).lastIndexOf(label), 1);
|
|
170
|
+
selected = selected; // Svelte rerender after in-place splice
|
|
181
171
|
const option = options.find((option) => get_label(option) === label) ??
|
|
182
172
|
// if option with label could not be found but allowUserOptions is truthy,
|
|
183
173
|
// assume it was created by user and create correspondidng option object
|
|
@@ -188,6 +178,7 @@ function remove(label) {
|
|
|
188
178
|
}
|
|
189
179
|
dispatch(`remove`, { option });
|
|
190
180
|
dispatch(`change`, { option, type: `remove` });
|
|
181
|
+
invalid = false; // reset error status whenever items are removed
|
|
191
182
|
}
|
|
192
183
|
function open_dropdown(event) {
|
|
193
184
|
if (disabled)
|
|
@@ -217,7 +208,7 @@ async function handle_keydown(event) {
|
|
|
217
208
|
event.preventDefault(); // prevent enter key from triggering form submission
|
|
218
209
|
if (activeOption) {
|
|
219
210
|
const label = get_label(activeOption);
|
|
220
|
-
|
|
211
|
+
selected.map(get_label).includes(label) ? remove(label) : add(label, event);
|
|
221
212
|
searchText = ``;
|
|
222
213
|
}
|
|
223
214
|
else if (allowUserOptions && searchText.length > 0) {
|
|
@@ -264,17 +255,17 @@ async function handle_keydown(event) {
|
|
|
264
255
|
}
|
|
265
256
|
}
|
|
266
257
|
// on backspace key: remove last selected option
|
|
267
|
-
else if (event.key === `Backspace` &&
|
|
268
|
-
remove(
|
|
258
|
+
else if (event.key === `Backspace` && selected.length > 0 && !searchText) {
|
|
259
|
+
remove(selected.map(get_label).at(-1));
|
|
269
260
|
}
|
|
270
261
|
}
|
|
271
262
|
function remove_all() {
|
|
272
|
-
dispatch(`removeAll`, { options:
|
|
273
|
-
dispatch(`change`, { options:
|
|
274
|
-
|
|
263
|
+
dispatch(`removeAll`, { options: selected });
|
|
264
|
+
dispatch(`change`, { options: selected, type: `removeAll` });
|
|
265
|
+
selected = [];
|
|
275
266
|
searchText = ``;
|
|
276
267
|
}
|
|
277
|
-
$: is_selected = (label) =>
|
|
268
|
+
$: is_selected = (label) => selected.map(get_label).includes(label);
|
|
278
269
|
const if_enter_or_space = (handler) => (event) => {
|
|
279
270
|
if ([`Enter`, `Space`].includes(event.code)) {
|
|
280
271
|
event.preventDefault();
|
|
@@ -307,10 +298,10 @@ function on_click_outside(event) {
|
|
|
307
298
|
title={disabled ? disabledInputTitle : null}
|
|
308
299
|
aria-disabled={disabled ? `true` : null}
|
|
309
300
|
>
|
|
310
|
-
<!--
|
|
301
|
+
<!-- bind:value={selected} prevents form submission if required prop is true and no options are selected -->
|
|
311
302
|
<input
|
|
312
303
|
{required}
|
|
313
|
-
bind:value={
|
|
304
|
+
bind:value={selected}
|
|
314
305
|
tabindex="-1"
|
|
315
306
|
aria-hidden="true"
|
|
316
307
|
aria-label="ignore this, used only to prevent form submission if select is required but empty"
|
|
@@ -319,7 +310,7 @@ function on_click_outside(event) {
|
|
|
319
310
|
/>
|
|
320
311
|
<ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt;" />
|
|
321
312
|
<ul class="selected {ulSelectedClass}">
|
|
322
|
-
{#each
|
|
313
|
+
{#each selected as option, idx}
|
|
323
314
|
<li class={liSelectedClass} aria-selected="true">
|
|
324
315
|
<slot name="selected" {option} {idx}>
|
|
325
316
|
{#if parseLabelsAsHtml}
|
|
@@ -357,7 +348,7 @@ function on_click_outside(event) {
|
|
|
357
348
|
{disabled}
|
|
358
349
|
{inputmode}
|
|
359
350
|
{pattern}
|
|
360
|
-
placeholder={
|
|
351
|
+
placeholder={selected.length == 0 ? placeholder : null}
|
|
361
352
|
aria-invalid={invalid ? `true` : null}
|
|
362
353
|
on:blur
|
|
363
354
|
on:change
|
|
@@ -384,16 +375,15 @@ function on_click_outside(event) {
|
|
|
384
375
|
<slot name="disabled-icon">
|
|
385
376
|
<DisabledIcon width="15px" />
|
|
386
377
|
</slot>
|
|
387
|
-
{:else if
|
|
378
|
+
{:else if selected.length > 0}
|
|
388
379
|
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
|
|
389
380
|
<Wiggle bind:wiggle angle={20}>
|
|
390
381
|
<span style="padding: 0 3pt;">
|
|
391
|
-
{maxSelectMsg?.(
|
|
392
|
-
(maxSelect > 1 ? `${_selected.length}/${maxSelect}` : ``)}
|
|
382
|
+
{maxSelectMsg?.(selected.length, maxSelect)}
|
|
393
383
|
</span>
|
|
394
384
|
</Wiggle>
|
|
395
385
|
{/if}
|
|
396
|
-
{#if maxSelect !== 1 &&
|
|
386
|
+
{#if maxSelect !== 1 && selected.length > 1}
|
|
397
387
|
<button
|
|
398
388
|
type="button"
|
|
399
389
|
class="remove-all"
|
|
@@ -423,7 +413,7 @@ function on_click_outside(event) {
|
|
|
423
413
|
<li
|
|
424
414
|
on:mousedown|stopPropagation
|
|
425
415
|
on:mouseup|stopPropagation={(event) => {
|
|
426
|
-
if (!disabled)
|
|
416
|
+
if (!disabled) add(label, event)
|
|
427
417
|
}}
|
|
428
418
|
title={disabled
|
|
429
419
|
? disabledTitle
|
|
@@ -463,166 +453,29 @@ function on_click_outside(event) {
|
|
|
463
453
|
on:blur={() => (add_option_msg_is_active = false)}
|
|
464
454
|
aria-selected="false"
|
|
465
455
|
>
|
|
466
|
-
{!duplicates &&
|
|
456
|
+
{!duplicates && selected.some((option) => duplicateFunc(option, searchText))
|
|
467
457
|
? duplicateOptionMsg
|
|
468
458
|
: addOptionMsg}
|
|
469
459
|
</li>
|
|
470
460
|
{:else}
|
|
471
|
-
<span>{
|
|
461
|
+
<span>{noMatchingOptionsMsg}</span>
|
|
472
462
|
{/if}
|
|
473
463
|
{/each}
|
|
474
464
|
</ul>
|
|
475
465
|
{/if}
|
|
476
466
|
</div>
|
|
477
467
|
|
|
478
|
-
<style
|
|
479
|
-
|
|
480
|
-
align-items: center;
|
|
481
|
-
display: flex;
|
|
482
|
-
cursor: text;
|
|
483
|
-
border: var(--sms-border, 1pt solid lightgray);
|
|
484
|
-
border-radius: var(--sms-border-radius, 3pt);
|
|
485
|
-
background: var(--sms-bg);
|
|
486
|
-
max-width: var(--sms-max-width);
|
|
487
|
-
padding: var(--sms-padding, 0 3pt);
|
|
488
|
-
color: var(--sms-text-color);
|
|
489
|
-
font-size: var(--sms-font-size, inherit);
|
|
490
|
-
min-height: var(--sms-min-height, 19pt);
|
|
491
|
-
margin: var(--sms-margin);
|
|
492
|
-
}
|
|
493
|
-
:where(div.multiselect).open {
|
|
494
|
-
/* increase z-index when open to ensure the dropdown of one <MultiSelect />
|
|
495
|
-
displays above that of another slightly below it on the page */
|
|
496
|
-
z-index: var(--sms-open-z-index, 4);
|
|
497
|
-
}
|
|
498
|
-
:where(div.multiselect):focus-within {
|
|
499
|
-
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
|
|
500
|
-
}
|
|
501
|
-
:where(div.multiselect).disabled {
|
|
502
|
-
background: var(--sms-disabled-bg, lightgray);
|
|
503
|
-
cursor: not-allowed;
|
|
504
|
-
}
|
|
505
|
-
:where(div.multiselect) > ul.selected {
|
|
506
|
-
display: flex;
|
|
507
|
-
flex: 1;
|
|
508
|
-
padding: 0;
|
|
509
|
-
margin: 0;
|
|
510
|
-
flex-wrap: wrap;
|
|
511
|
-
}
|
|
512
|
-
:where(div.multiselect) > ul.selected > li {
|
|
513
|
-
align-items: center;
|
|
514
|
-
border-radius: 3pt;
|
|
515
|
-
display: flex;
|
|
516
|
-
margin: 2pt;
|
|
517
|
-
line-height: normal;
|
|
518
|
-
transition: 0.3s;
|
|
519
|
-
white-space: nowrap;
|
|
520
|
-
background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15));
|
|
521
|
-
padding: var(--sms-selected-li-padding, 1pt 5pt);
|
|
522
|
-
color: var(--sms-selected-text-color, var(--sms-text-color));
|
|
523
|
-
}
|
|
524
|
-
:where(div.multiselect) button {
|
|
525
|
-
border-radius: 50%;
|
|
526
|
-
display: flex;
|
|
527
|
-
transition: 0.2s;
|
|
528
|
-
color: inherit;
|
|
529
|
-
background: transparent;
|
|
530
|
-
border: none;
|
|
531
|
-
cursor: pointer;
|
|
532
|
-
outline: none;
|
|
533
|
-
padding: 0;
|
|
534
|
-
margin: 0 0 0 3pt; /* CSS reset */
|
|
535
|
-
}
|
|
536
|
-
:where(div.multiselect) button.remove-all {
|
|
537
|
-
margin: 0 3pt;
|
|
538
|
-
}
|
|
539
|
-
:where(div.multiselect) ul.selected > li button:hover,
|
|
540
|
-
:where(div.multiselect) button.remove-all:hover,
|
|
541
|
-
:where(div.multiselect) button:focus {
|
|
542
|
-
color: var(--sms-remove-btn-hover-color, lightskyblue);
|
|
543
|
-
background: var(--sms-remove-btn-hover-bg, rgba(0, 0, 0, 0.2));
|
|
544
|
-
}
|
|
545
|
-
:where(div.multiselect) input {
|
|
546
|
-
margin: auto 0; /* CSS reset */
|
|
547
|
-
padding: 0; /* CSS reset */
|
|
548
|
-
}
|
|
549
|
-
:where(div.multiselect) > ul.selected > li > input {
|
|
550
|
-
border: none;
|
|
551
|
-
outline: none;
|
|
552
|
-
background: none;
|
|
553
|
-
flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
|
|
554
|
-
min-width: 2em;
|
|
555
|
-
/* ensure input uses text color and not --sms-selected-text-color */
|
|
556
|
-
color: var(--sms-text-color);
|
|
557
|
-
font-size: inherit;
|
|
558
|
-
cursor: inherit; /* needed for disabled state */
|
|
559
|
-
border-radius: 0; /* reset ul.selected > li */
|
|
560
|
-
}
|
|
561
|
-
:where(div.multiselect) > ul.selected > li > input::placeholder {
|
|
562
|
-
padding-left: 5pt;
|
|
563
|
-
color: var(--sms-placeholder-color);
|
|
564
|
-
opacity: var(--sms-placeholder-opacity);
|
|
565
|
-
}
|
|
566
|
-
:where(div.multiselect) > input.form-control {
|
|
567
|
-
width: 2em;
|
|
568
|
-
position: absolute;
|
|
569
|
-
background: transparent;
|
|
570
|
-
border: none;
|
|
571
|
-
outline: none;
|
|
572
|
-
z-index: -1;
|
|
573
|
-
opacity: 0;
|
|
574
|
-
pointer-events: none;
|
|
575
|
-
}
|
|
576
|
-
:where(div.multiselect) > ul.options {
|
|
577
|
-
list-style: none;
|
|
578
|
-
padding: 4pt 0;
|
|
579
|
-
top: 100%;
|
|
580
|
-
left: 0;
|
|
581
|
-
width: 100%;
|
|
582
|
-
position: absolute;
|
|
583
|
-
border-radius: 1ex;
|
|
584
|
-
overflow: auto;
|
|
585
|
-
background: var(--sms-options-bg, white);
|
|
586
|
-
max-height: var(--sms-options-max-height, 50vh);
|
|
587
|
-
overscroll-behavior: var(--sms-options-overscroll, none);
|
|
588
|
-
box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
|
|
589
|
-
transition: all 0.2s;
|
|
590
|
-
}
|
|
591
|
-
:where(div.multiselect) > ul.options.hidden {
|
|
592
|
-
visibility: hidden;
|
|
593
|
-
opacity: 0;
|
|
594
|
-
transform: translateY(50px);
|
|
595
|
-
}
|
|
596
|
-
:where(div.multiselect) > ul.options > li {
|
|
597
|
-
padding: 3pt 2ex;
|
|
598
|
-
cursor: pointer;
|
|
599
|
-
scroll-margin: var(--sms-options-scroll-margin, 100px);
|
|
600
|
-
}
|
|
601
|
-
:where(div.multiselect) > ul.options span {
|
|
602
|
-
padding: 3pt 2ex;
|
|
603
|
-
}
|
|
604
|
-
:where(div.multiselect) > ul.options > li.selected {
|
|
605
|
-
background: var(--sms-li-selected-bg);
|
|
606
|
-
color: var(--sms-li-selected-color);
|
|
607
|
-
}
|
|
608
|
-
:where(div.multiselect) > ul.options > li.active {
|
|
609
|
-
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
|
610
|
-
}
|
|
611
|
-
:where(div.multiselect) > ul.options > li.disabled {
|
|
612
|
-
cursor: not-allowed;
|
|
613
|
-
background: var(--sms-li-disabled-bg, #f5f5f6);
|
|
614
|
-
color: var(--sms-li-disabled-text, #b8b8b8);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
@supports not selector(:where(div.multiselect)) {
|
|
618
|
-
div.multiselect {
|
|
468
|
+
<style>
|
|
469
|
+
:where(div.multiselect) {
|
|
619
470
|
position: relative;
|
|
620
471
|
align-items: center;
|
|
621
472
|
display: flex;
|
|
622
473
|
cursor: text;
|
|
474
|
+
box-sizing: border-box;
|
|
623
475
|
border: var(--sms-border, 1pt solid lightgray);
|
|
624
476
|
border-radius: var(--sms-border-radius, 3pt);
|
|
625
477
|
background: var(--sms-bg);
|
|
478
|
+
width: var(--sms-width);
|
|
626
479
|
max-width: var(--sms-max-width);
|
|
627
480
|
padding: var(--sms-padding, 0 3pt);
|
|
628
481
|
color: var(--sms-text-color);
|
|
@@ -630,26 +483,27 @@ function on_click_outside(event) {
|
|
|
630
483
|
min-height: var(--sms-min-height, 19pt);
|
|
631
484
|
margin: var(--sms-margin);
|
|
632
485
|
}
|
|
633
|
-
div.multiselect.open {
|
|
486
|
+
:where(div.multiselect.open) {
|
|
634
487
|
/* increase z-index when open to ensure the dropdown of one <MultiSelect />
|
|
635
488
|
displays above that of another slightly below it on the page */
|
|
636
489
|
z-index: var(--sms-open-z-index, 4);
|
|
637
490
|
}
|
|
638
|
-
div.multiselect:focus-within {
|
|
491
|
+
:where(div.multiselect:focus-within) {
|
|
639
492
|
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
|
|
640
493
|
}
|
|
641
|
-
div.multiselect.disabled {
|
|
494
|
+
:where(div.multiselect.disabled) {
|
|
642
495
|
background: var(--sms-disabled-bg, lightgray);
|
|
643
496
|
cursor: not-allowed;
|
|
644
497
|
}
|
|
645
|
-
|
|
498
|
+
|
|
499
|
+
:where(div.multiselect > ul.selected) {
|
|
646
500
|
display: flex;
|
|
647
501
|
flex: 1;
|
|
648
502
|
padding: 0;
|
|
649
503
|
margin: 0;
|
|
650
504
|
flex-wrap: wrap;
|
|
651
505
|
}
|
|
652
|
-
div.multiselect > ul.selected > li {
|
|
506
|
+
:where(div.multiselect > ul.selected > li) {
|
|
653
507
|
align-items: center;
|
|
654
508
|
border-radius: 3pt;
|
|
655
509
|
display: flex;
|
|
@@ -661,7 +515,7 @@ function on_click_outside(event) {
|
|
|
661
515
|
padding: var(--sms-selected-li-padding, 1pt 5pt);
|
|
662
516
|
color: var(--sms-selected-text-color, var(--sms-text-color));
|
|
663
517
|
}
|
|
664
|
-
div.multiselect button {
|
|
518
|
+
:where(div.multiselect button) {
|
|
665
519
|
border-radius: 50%;
|
|
666
520
|
display: flex;
|
|
667
521
|
transition: 0.2s;
|
|
@@ -673,20 +527,19 @@ function on_click_outside(event) {
|
|
|
673
527
|
padding: 0;
|
|
674
528
|
margin: 0 0 0 3pt; /* CSS reset */
|
|
675
529
|
}
|
|
676
|
-
div.multiselect button.remove-all {
|
|
530
|
+
:where(div.multiselect button.remove-all) {
|
|
677
531
|
margin: 0 3pt;
|
|
678
532
|
}
|
|
679
|
-
|
|
680
|
-
div.multiselect button.remove-all:hover,
|
|
681
|
-
div.multiselect button:focus {
|
|
533
|
+
:where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
|
|
682
534
|
color: var(--sms-remove-btn-hover-color, lightskyblue);
|
|
683
535
|
background: var(--sms-remove-btn-hover-bg, rgba(0, 0, 0, 0.2));
|
|
684
536
|
}
|
|
685
|
-
|
|
537
|
+
|
|
538
|
+
:where(div.multiselect input) {
|
|
686
539
|
margin: auto 0; /* CSS reset */
|
|
687
540
|
padding: 0; /* CSS reset */
|
|
688
541
|
}
|
|
689
|
-
div.multiselect > ul.selected > li > input {
|
|
542
|
+
:where(div.multiselect > ul.selected > li > input) {
|
|
690
543
|
border: none;
|
|
691
544
|
outline: none;
|
|
692
545
|
background: none;
|
|
@@ -698,12 +551,12 @@ div.multiselect button:focus {
|
|
|
698
551
|
cursor: inherit; /* needed for disabled state */
|
|
699
552
|
border-radius: 0; /* reset ul.selected > li */
|
|
700
553
|
}
|
|
701
|
-
div.multiselect > ul.selected > li > input::placeholder {
|
|
554
|
+
:where(div.multiselect > ul.selected > li > input::placeholder) {
|
|
702
555
|
padding-left: 5pt;
|
|
703
556
|
color: var(--sms-placeholder-color);
|
|
704
557
|
opacity: var(--sms-placeholder-opacity);
|
|
705
558
|
}
|
|
706
|
-
div.multiselect > input.form-control {
|
|
559
|
+
:where(div.multiselect > input.form-control) {
|
|
707
560
|
width: 2em;
|
|
708
561
|
position: absolute;
|
|
709
562
|
background: transparent;
|
|
@@ -713,7 +566,8 @@ div.multiselect button:focus {
|
|
|
713
566
|
opacity: 0;
|
|
714
567
|
pointer-events: none;
|
|
715
568
|
}
|
|
716
|
-
|
|
569
|
+
|
|
570
|
+
:where(div.multiselect > ul.options) {
|
|
717
571
|
list-style: none;
|
|
718
572
|
padding: 4pt 0;
|
|
719
573
|
top: 100%;
|
|
@@ -728,29 +582,30 @@ div.multiselect button:focus {
|
|
|
728
582
|
box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
|
|
729
583
|
transition: all 0.2s;
|
|
730
584
|
}
|
|
731
|
-
div.multiselect > ul.options.hidden {
|
|
585
|
+
:where(div.multiselect > ul.options.hidden) {
|
|
732
586
|
visibility: hidden;
|
|
733
587
|
opacity: 0;
|
|
734
588
|
transform: translateY(50px);
|
|
735
589
|
}
|
|
736
|
-
div.multiselect > ul.options > li {
|
|
590
|
+
:where(div.multiselect > ul.options > li) {
|
|
737
591
|
padding: 3pt 2ex;
|
|
738
592
|
cursor: pointer;
|
|
739
593
|
scroll-margin: var(--sms-options-scroll-margin, 100px);
|
|
740
594
|
}
|
|
741
|
-
|
|
595
|
+
/* for noOptionsMsg */
|
|
596
|
+
:where(div.multiselect > ul.options span) {
|
|
742
597
|
padding: 3pt 2ex;
|
|
743
598
|
}
|
|
744
|
-
div.multiselect > ul.options > li.selected {
|
|
599
|
+
:where(div.multiselect > ul.options > li.selected) {
|
|
745
600
|
background: var(--sms-li-selected-bg);
|
|
746
601
|
color: var(--sms-li-selected-color);
|
|
747
602
|
}
|
|
748
|
-
div.multiselect > ul.options > li.active {
|
|
603
|
+
:where(div.multiselect > ul.options > li.active) {
|
|
749
604
|
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
|
750
605
|
}
|
|
751
|
-
div.multiselect > ul.options > li.disabled {
|
|
606
|
+
:where(div.multiselect > ul.options > li.disabled) {
|
|
752
607
|
cursor: not-allowed;
|
|
753
608
|
background: var(--sms-li-disabled-bg, #f5f5f6);
|
|
754
609
|
color: var(--sms-li-disabled-text, #b8b8b8);
|
|
755
610
|
}
|
|
756
|
-
|
|
611
|
+
</style>
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ declare const __propDef: {
|
|
|
30
30
|
maxSelect?: number | null | undefined;
|
|
31
31
|
maxSelectMsg?: ((current: number, max: number) => string) | null | undefined;
|
|
32
32
|
name?: string | null | undefined;
|
|
33
|
-
|
|
33
|
+
noMatchingOptionsMsg?: string | undefined;
|
|
34
34
|
open?: boolean | undefined;
|
|
35
35
|
options: Option[];
|
|
36
36
|
outerDiv?: HTMLDivElement | null | undefined;
|
|
@@ -41,13 +41,13 @@ declare const __propDef: {
|
|
|
41
41
|
removeAllTitle?: string | undefined;
|
|
42
42
|
removeBtnTitle?: string | undefined;
|
|
43
43
|
required?: boolean | undefined;
|
|
44
|
+
resetFilterOnAdd?: boolean | undefined;
|
|
44
45
|
searchText?: string | undefined;
|
|
45
|
-
selected?: Option
|
|
46
|
-
selectedLabels?: string | number | (string | number)[] | null | undefined;
|
|
47
|
-
selectedValues?: unknown[] | unknown | null;
|
|
46
|
+
selected?: Option[] | undefined;
|
|
48
47
|
sortSelected?: boolean | ((op1: Option, op2: Option) => number) | undefined;
|
|
49
48
|
ulOptionsClass?: string | undefined;
|
|
50
49
|
ulSelectedClass?: string | undefined;
|
|
50
|
+
value?: Option | Option[] | null | undefined;
|
|
51
51
|
};
|
|
52
52
|
slots: {
|
|
53
53
|
selected: {
|
package/package.json
CHANGED
|
@@ -5,39 +5,39 @@
|
|
|
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.1",
|
|
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
|
-
"svelte": "^
|
|
32
|
-
"svelte-check": "^2.9.0",
|
|
31
|
+
"svelte": "^3.52.0",
|
|
32
|
+
"svelte-check": "^2.9.2",
|
|
33
33
|
"svelte-github-corner": "^0.1.0",
|
|
34
34
|
"svelte-preprocess": "^4.10.6",
|
|
35
35
|
"svelte-toc": "^0.4.0",
|
|
36
|
-
"svelte2tsx": "^0.5.
|
|
36
|
+
"svelte2tsx": "^0.5.20",
|
|
37
37
|
"tslib": "^2.4.0",
|
|
38
|
-
"typescript": "^4.8.
|
|
39
|
-
"vite": "^3.1.
|
|
40
|
-
"vitest": "^0.
|
|
38
|
+
"typescript": "^4.8.4",
|
|
39
|
+
"vite": "^3.1.8",
|
|
40
|
+
"vitest": "^0.24.3"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"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,18 +36,20 @@
|
|
|
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
|
|
|
48
50
|
```sh
|
|
49
51
|
npm install -D svelte-multiselect
|
|
50
|
-
pnpm
|
|
52
|
+
pnpm add -D svelte-multiselect
|
|
51
53
|
yarn add -D svelte-multiselect
|
|
52
54
|
```
|
|
53
55
|
|
|
@@ -71,11 +73,7 @@ Favorite Frontend Frameworks?
|
|
|
71
73
|
|
|
72
74
|
## Props
|
|
73
75
|
|
|
74
|
-
Full list of props/bindable variables for this component.
|
|
75
|
-
|
|
76
|
-
```ts
|
|
77
|
-
import type { Option } from 'svelte-multiselect'
|
|
78
|
-
```
|
|
76
|
+
Full list of props/bindable variables for this component. The `Option` type you see below is defined in [`src/lib/index.ts`](https://github.com/janosh/svelte-multiselect/blob/main/src/lib/index.ts) and can be imported as `import { type Option } from 'svelte-multiselect'`.
|
|
79
77
|
|
|
80
78
|
1. ```ts
|
|
81
79
|
activeIndex: number | null = null
|
|
@@ -216,7 +214,10 @@ import type { Option } from 'svelte-multiselect'
|
|
|
216
214
|
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.
|
|
217
215
|
|
|
218
216
|
1. ```ts
|
|
219
|
-
maxSelectMsg: ((current: number, max: number) => string) | null =
|
|
217
|
+
maxSelectMsg: ((current: number, max: number) => string) | null = (
|
|
218
|
+
current: number,
|
|
219
|
+
max: number
|
|
220
|
+
) => (max > 1 ? `${current}/${max}` : ``)
|
|
220
221
|
```
|
|
221
222
|
|
|
222
223
|
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:
|
|
@@ -232,7 +233,7 @@ import type { Option } from 'svelte-multiselect'
|
|
|
232
233
|
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.
|
|
233
234
|
|
|
234
235
|
1. ```ts
|
|
235
|
-
|
|
236
|
+
noMatchingOptionsMsg: string = `No matching options`
|
|
236
237
|
```
|
|
237
238
|
|
|
238
239
|
What message to show if no options match the user-entered search string.
|
|
@@ -291,6 +292,12 @@ import type { Option } from 'svelte-multiselect'
|
|
|
291
292
|
|
|
292
293
|
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.
|
|
293
294
|
|
|
295
|
+
1. ```ts
|
|
296
|
+
resetFilterOnAdd: boolean = true
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Whether text entered into the input to filter options in the dropdown list is reset to empty string when user selects an option.
|
|
300
|
+
|
|
294
301
|
1. ```ts
|
|
295
302
|
searchText: string = ``
|
|
296
303
|
```
|
|
@@ -298,31 +305,25 @@ import type { Option } from 'svelte-multiselect'
|
|
|
298
305
|
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.
|
|
299
306
|
|
|
300
307
|
1. ```ts
|
|
301
|
-
selected: Option[]
|
|
308
|
+
selected: Option[] =
|
|
302
309
|
options
|
|
303
310
|
?.filter((op) => (op as ObjectOption)?.preselected)
|
|
304
311
|
.slice(0, maxSelect ?? undefined) ?? []
|
|
305
312
|
```
|
|
306
313
|
|
|
307
|
-
Array of currently selected options. Supports 2-way binding `bind:selected={[1, 2, 3]}` to control component state externally
|
|
314
|
+
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.
|
|
308
315
|
|
|
309
316
|
1. ```ts
|
|
310
|
-
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
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 (or numbers), `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.
|
|
314
|
-
|
|
315
|
-
1. ```ts
|
|
316
|
-
selectedValues: unknown[] | unknown | null = []
|
|
317
|
+
sortSelected: boolean | ((op1: Option, op2: Option) => number) = false
|
|
317
318
|
```
|
|
318
319
|
|
|
319
|
-
|
|
320
|
+
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.
|
|
320
321
|
|
|
321
322
|
1. ```ts
|
|
322
|
-
|
|
323
|
+
value: Option | Option[] | null = null
|
|
323
324
|
```
|
|
324
325
|
|
|
325
|
-
|
|
326
|
+
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!
|
|
326
327
|
|
|
327
328
|
## Slots
|
|
328
329
|
|
|
@@ -463,6 +464,7 @@ If you only want to make small adjustments, you can pass the following CSS varia
|
|
|
463
464
|
- `background: var(--sms-bg)`
|
|
464
465
|
- `color: var(--sms-text-color)`
|
|
465
466
|
- `min-height: var(--sms-min-height, 19pt)`
|
|
467
|
+
- `width: var(--sms-width)`
|
|
466
468
|
- `max-width: var(--sms-max-width)`
|
|
467
469
|
- `margin: var(--sms-margin)`
|
|
468
470
|
- `font-size: var(--sms-font-size, inherit)`
|