svelte-multiselect 6.1.0 → 7.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 +216 -71
- package/MultiSelect.svelte.d.ts +5 -5
- package/index.js +2 -1
- package/package.json +2 -1
- package/readme.md +30 -26
package/MultiSelect.svelte
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script>import { createEventDispatcher } from 'svelte';
|
|
1
|
+
<script>import { createEventDispatcher, tick } from 'svelte';
|
|
2
2
|
import { get_label, get_value } from './';
|
|
3
3
|
import CircleSpinner from './CircleSpinner.svelte';
|
|
4
4
|
import { CrossIcon, DisabledIcon, ExpandIcon } from './icons';
|
|
@@ -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,19 +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 = ``;
|
|
51
|
-
|
|
52
|
-
|
|
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;
|
|
53
65
|
if (!(options?.length > 0)) {
|
|
54
66
|
if (allowUserOptions) {
|
|
55
67
|
options = []; // initializing as array avoids errors when component mounts
|
|
@@ -65,22 +77,19 @@ if (parseLabelsAsHtml && allowUserOptions) {
|
|
|
65
77
|
if (maxSelect !== null && maxSelect < 1) {
|
|
66
78
|
console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
67
79
|
}
|
|
68
|
-
if (!Array.isArray(
|
|
69
|
-
console.error(`
|
|
80
|
+
if (!Array.isArray(_selected)) {
|
|
81
|
+
console.error(`internal variable _selected prop should always be an array, got ${_selected}`);
|
|
70
82
|
}
|
|
71
83
|
const dispatch = createEventDispatcher();
|
|
72
84
|
let add_option_msg_is_active = false; // controls active state of <li>{addOptionMsg}</li>
|
|
73
85
|
let window_width;
|
|
74
|
-
let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
|
|
75
|
-
$: selectedLabels = selected.map(get_label);
|
|
76
|
-
$: selectedValues = selected.map(get_value);
|
|
77
86
|
// formValue binds to input.form-control to prevent form submission if required
|
|
78
87
|
// prop is true and no options are selected
|
|
79
|
-
$:
|
|
80
|
-
$: if (
|
|
88
|
+
$: form_value = _selectedValues.join(`,`);
|
|
89
|
+
$: if (form_value)
|
|
81
90
|
invalid = false; // reset error status whenever component state changes
|
|
82
91
|
// options matching the current search text
|
|
83
|
-
$: 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
|
|
84
93
|
);
|
|
85
94
|
// raise if matchingOptions[activeIndex] does not yield a value
|
|
86
95
|
if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
@@ -90,10 +99,10 @@ if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
|
90
99
|
$: activeOption = activeIndex !== null ? matchingOptions[activeIndex] : null;
|
|
91
100
|
// add an option to selected list
|
|
92
101
|
function add(label, event) {
|
|
93
|
-
if (maxSelect && maxSelect > 1 &&
|
|
102
|
+
if (maxSelect && maxSelect > 1 && _selected.length >= maxSelect)
|
|
94
103
|
wiggle = true;
|
|
95
104
|
// to prevent duplicate selection, we could add `&& !selectedLabels.includes(label)`
|
|
96
|
-
if (maxSelect === null || maxSelect === 1 ||
|
|
105
|
+
if (maxSelect === null || maxSelect === 1 || _selected.length < maxSelect) {
|
|
97
106
|
// first check if we find option in the options list
|
|
98
107
|
let option = options.find((op) => get_label(op) === label);
|
|
99
108
|
if (!option && // this has the side-effect of not allowing to user to add the same
|
|
@@ -125,22 +134,22 @@ function add(label, event) {
|
|
|
125
134
|
}
|
|
126
135
|
if (maxSelect === 1) {
|
|
127
136
|
// for maxselect = 1 we always replace current option with new one
|
|
128
|
-
|
|
137
|
+
_selected = [option];
|
|
129
138
|
}
|
|
130
139
|
else {
|
|
131
|
-
|
|
140
|
+
_selected = [..._selected, option];
|
|
132
141
|
if (sortSelected === true) {
|
|
133
|
-
|
|
142
|
+
_selected = _selected.sort((op1, op2) => {
|
|
134
143
|
const [label1, label2] = [get_label(op1), get_label(op2)];
|
|
135
144
|
// coerce to string if labels are numbers
|
|
136
145
|
return `${label1}`.localeCompare(`${label2}`);
|
|
137
146
|
});
|
|
138
147
|
}
|
|
139
148
|
else if (typeof sortSelected === `function`) {
|
|
140
|
-
|
|
149
|
+
_selected = _selected.sort(sortSelected);
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
|
-
if (
|
|
152
|
+
if (_selected.length === maxSelect)
|
|
144
153
|
close_dropdown(event);
|
|
145
154
|
else if (focusInputOnSelect === true ||
|
|
146
155
|
(focusInputOnSelect === `desktop` && window_width > breakpoint)) {
|
|
@@ -152,10 +161,10 @@ function add(label, event) {
|
|
|
152
161
|
}
|
|
153
162
|
// remove an option from selected list
|
|
154
163
|
function remove(label) {
|
|
155
|
-
if (
|
|
164
|
+
if (_selected.length === 0)
|
|
156
165
|
return;
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
_selected.splice(_selectedLabels.lastIndexOf(label), 1);
|
|
167
|
+
_selected = _selected; // Svelte rerender after in-place splice
|
|
159
168
|
const option = options.find((option) => get_label(option) === label) ??
|
|
160
169
|
// if option with label could not be found but allowUserOptions is truthy,
|
|
161
170
|
// assume it was created by user and create correspondidng option object
|
|
@@ -195,7 +204,7 @@ async function handle_keydown(event) {
|
|
|
195
204
|
event.preventDefault(); // prevent enter key from triggering form submission
|
|
196
205
|
if (activeOption) {
|
|
197
206
|
const label = get_label(activeOption);
|
|
198
|
-
selectedLabels
|
|
207
|
+
selectedLabels?.includes(label) ? remove(label) : add(label, event);
|
|
199
208
|
searchText = ``;
|
|
200
209
|
}
|
|
201
210
|
else if (allowUserOptions && searchText.length > 0) {
|
|
@@ -232,28 +241,27 @@ async function handle_keydown(event) {
|
|
|
232
241
|
activeIndex = matchingOptions.length - 1;
|
|
233
242
|
if (autoScroll) {
|
|
234
243
|
// TODO This ugly timeout hack is needed to properly scroll element into view when wrapping
|
|
235
|
-
// around start/end of option list. Find a better solution than waiting 10 ms
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}, 10);
|
|
244
|
+
// around start/end of option list. Find a better solution than waiting 10 ms.
|
|
245
|
+
await tick();
|
|
246
|
+
const li = document.querySelector(`ul.options > li.active`);
|
|
247
|
+
if (li) {
|
|
248
|
+
li.parentNode?.scrollIntoView?.({ block: `center` });
|
|
249
|
+
li.scrollIntoViewIfNeeded?.();
|
|
250
|
+
}
|
|
243
251
|
}
|
|
244
252
|
}
|
|
245
253
|
// on backspace key: remove last selected option
|
|
246
|
-
else if (event.key === `Backspace` &&
|
|
247
|
-
remove(
|
|
254
|
+
else if (event.key === `Backspace` && _selectedLabels.length > 0 && !searchText) {
|
|
255
|
+
remove(_selectedLabels.at(-1));
|
|
248
256
|
}
|
|
249
257
|
}
|
|
250
258
|
function remove_all() {
|
|
251
|
-
dispatch(`removeAll`, { options:
|
|
252
|
-
dispatch(`change`, { options:
|
|
253
|
-
|
|
259
|
+
dispatch(`removeAll`, { options: _selected });
|
|
260
|
+
dispatch(`change`, { options: _selected, type: `removeAll` });
|
|
261
|
+
_selected = [];
|
|
254
262
|
searchText = ``;
|
|
255
263
|
}
|
|
256
|
-
$: is_selected = (label) =>
|
|
264
|
+
$: is_selected = (label) => _selectedLabels.includes(label);
|
|
257
265
|
const if_enter_or_space = (handler) => (event) => {
|
|
258
266
|
if ([`Enter`, `Space`].includes(event.code)) {
|
|
259
267
|
event.preventDefault();
|
|
@@ -286,9 +294,10 @@ function on_click_outside(event) {
|
|
|
286
294
|
title={disabled ? disabledInputTitle : null}
|
|
287
295
|
aria-disabled={disabled ? `true` : null}
|
|
288
296
|
>
|
|
297
|
+
<!-- formValue binds to input.form-control to prevent form submission if required prop is true and no options are selected -->
|
|
289
298
|
<input
|
|
290
299
|
{required}
|
|
291
|
-
bind:value={
|
|
300
|
+
bind:value={form_value}
|
|
292
301
|
tabindex="-1"
|
|
293
302
|
aria-hidden="true"
|
|
294
303
|
aria-label="ignore this, used only to prevent form submission if select is required but empty"
|
|
@@ -297,7 +306,7 @@ function on_click_outside(event) {
|
|
|
297
306
|
/>
|
|
298
307
|
<ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt;" />
|
|
299
308
|
<ul class="selected {ulSelectedClass}">
|
|
300
|
-
{#each
|
|
309
|
+
{#each _selected as option, idx}
|
|
301
310
|
<li class={liSelectedClass} aria-selected="true">
|
|
302
311
|
<slot name="selected" {option} {idx}>
|
|
303
312
|
{#if parseLabelsAsHtml}
|
|
@@ -325,7 +334,7 @@ function on_click_outside(event) {
|
|
|
325
334
|
{autocomplete}
|
|
326
335
|
bind:value={searchText}
|
|
327
336
|
on:mouseup|self|stopPropagation={open_dropdown}
|
|
328
|
-
on:keydown={handle_keydown}
|
|
337
|
+
on:keydown|stopPropagation={handle_keydown}
|
|
329
338
|
on:focus
|
|
330
339
|
on:focus={open_dropdown}
|
|
331
340
|
{id}
|
|
@@ -333,7 +342,7 @@ function on_click_outside(event) {
|
|
|
333
342
|
{disabled}
|
|
334
343
|
{inputmode}
|
|
335
344
|
{pattern}
|
|
336
|
-
placeholder={
|
|
345
|
+
placeholder={_selected.length == 0 ? placeholder : null}
|
|
337
346
|
aria-invalid={invalid ? `true` : null}
|
|
338
347
|
on:blur
|
|
339
348
|
on:change
|
|
@@ -360,16 +369,16 @@ function on_click_outside(event) {
|
|
|
360
369
|
<slot name="disabled-icon">
|
|
361
370
|
<DisabledIcon width="15px" />
|
|
362
371
|
</slot>
|
|
363
|
-
{:else if
|
|
372
|
+
{:else if _selected.length > 0}
|
|
364
373
|
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
|
|
365
374
|
<Wiggle bind:wiggle angle={20}>
|
|
366
375
|
<span style="padding: 0 3pt;">
|
|
367
|
-
{maxSelectMsg?.(
|
|
368
|
-
(maxSelect > 1 ? `${
|
|
376
|
+
{maxSelectMsg?.(_selected.length, maxSelect) ??
|
|
377
|
+
(maxSelect > 1 ? `${_selected.length}/${maxSelect}` : ``)}
|
|
369
378
|
</span>
|
|
370
379
|
</Wiggle>
|
|
371
380
|
{/if}
|
|
372
|
-
{#if maxSelect !== 1 &&
|
|
381
|
+
{#if maxSelect !== 1 && _selected.length > 1}
|
|
373
382
|
<button
|
|
374
383
|
type="button"
|
|
375
384
|
class="remove-all"
|
|
@@ -447,8 +456,146 @@ function on_click_outside(event) {
|
|
|
447
456
|
{/if}
|
|
448
457
|
</div>
|
|
449
458
|
|
|
450
|
-
<style
|
|
451
|
-
:
|
|
459
|
+
<style>:where(div.multiselect) {
|
|
460
|
+
position: relative;
|
|
461
|
+
align-items: center;
|
|
462
|
+
display: flex;
|
|
463
|
+
cursor: text;
|
|
464
|
+
border: var(--sms-border, 1pt solid lightgray);
|
|
465
|
+
border-radius: var(--sms-border-radius, 3pt);
|
|
466
|
+
background: var(--sms-bg);
|
|
467
|
+
max-width: var(--sms-max-width);
|
|
468
|
+
padding: var(--sms-padding, 0 3pt);
|
|
469
|
+
color: var(--sms-text-color);
|
|
470
|
+
font-size: var(--sms-font-size, inherit);
|
|
471
|
+
min-height: var(--sms-min-height, 19pt);
|
|
472
|
+
margin: var(--sms-margin);
|
|
473
|
+
}
|
|
474
|
+
:where(div.multiselect).open {
|
|
475
|
+
/* increase z-index when open to ensure the dropdown of one <MultiSelect />
|
|
476
|
+
displays above that of another slightly below it on the page */
|
|
477
|
+
z-index: var(--sms-open-z-index, 4);
|
|
478
|
+
}
|
|
479
|
+
:where(div.multiselect):focus-within {
|
|
480
|
+
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
|
|
481
|
+
}
|
|
482
|
+
:where(div.multiselect).disabled {
|
|
483
|
+
background: var(--sms-disabled-bg, lightgray);
|
|
484
|
+
cursor: not-allowed;
|
|
485
|
+
}
|
|
486
|
+
:where(div.multiselect) > ul.selected {
|
|
487
|
+
display: flex;
|
|
488
|
+
flex: 1;
|
|
489
|
+
padding: 0;
|
|
490
|
+
margin: 0;
|
|
491
|
+
flex-wrap: wrap;
|
|
492
|
+
}
|
|
493
|
+
:where(div.multiselect) > ul.selected > li {
|
|
494
|
+
align-items: center;
|
|
495
|
+
border-radius: 3pt;
|
|
496
|
+
display: flex;
|
|
497
|
+
margin: 2pt;
|
|
498
|
+
line-height: normal;
|
|
499
|
+
transition: 0.3s;
|
|
500
|
+
white-space: nowrap;
|
|
501
|
+
background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15));
|
|
502
|
+
padding: var(--sms-selected-li-padding, 1pt 5pt);
|
|
503
|
+
color: var(--sms-selected-text-color, var(--sms-text-color));
|
|
504
|
+
}
|
|
505
|
+
:where(div.multiselect) button {
|
|
506
|
+
border-radius: 50%;
|
|
507
|
+
display: flex;
|
|
508
|
+
transition: 0.2s;
|
|
509
|
+
color: inherit;
|
|
510
|
+
background: transparent;
|
|
511
|
+
border: none;
|
|
512
|
+
cursor: pointer;
|
|
513
|
+
outline: none;
|
|
514
|
+
padding: 0;
|
|
515
|
+
margin: 0 0 0 3pt; /* CSS reset */
|
|
516
|
+
}
|
|
517
|
+
:where(div.multiselect) button.remove-all {
|
|
518
|
+
margin: 0 3pt;
|
|
519
|
+
}
|
|
520
|
+
:where(div.multiselect) ul.selected > li button:hover,
|
|
521
|
+
:where(div.multiselect) button.remove-all:hover,
|
|
522
|
+
:where(div.multiselect) button:focus {
|
|
523
|
+
color: var(--sms-button-hover-color, lightskyblue);
|
|
524
|
+
}
|
|
525
|
+
:where(div.multiselect) input {
|
|
526
|
+
margin: auto 0; /* CSS reset */
|
|
527
|
+
padding: 0; /* CSS reset */
|
|
528
|
+
}
|
|
529
|
+
:where(div.multiselect) > ul.selected > li > input {
|
|
530
|
+
border: none;
|
|
531
|
+
outline: none;
|
|
532
|
+
background: none;
|
|
533
|
+
flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
|
|
534
|
+
min-width: 2em;
|
|
535
|
+
/* ensure input uses text color and not --sms-selected-text-color */
|
|
536
|
+
color: var(--sms-text-color);
|
|
537
|
+
font-size: inherit;
|
|
538
|
+
cursor: inherit; /* needed for disabled state */
|
|
539
|
+
border-radius: 0; /* reset ul.selected > li */
|
|
540
|
+
}
|
|
541
|
+
:where(div.multiselect) > ul.selected > li > input::placeholder {
|
|
542
|
+
padding-left: 5pt;
|
|
543
|
+
color: var(--sms-placeholder-color);
|
|
544
|
+
opacity: var(--sms-placeholder-opacity);
|
|
545
|
+
}
|
|
546
|
+
:where(div.multiselect) > input.form-control {
|
|
547
|
+
width: 2em;
|
|
548
|
+
position: absolute;
|
|
549
|
+
background: transparent;
|
|
550
|
+
border: none;
|
|
551
|
+
outline: none;
|
|
552
|
+
z-index: -1;
|
|
553
|
+
opacity: 0;
|
|
554
|
+
pointer-events: none;
|
|
555
|
+
}
|
|
556
|
+
:where(div.multiselect) > ul.options {
|
|
557
|
+
list-style: none;
|
|
558
|
+
padding: 4pt 0;
|
|
559
|
+
top: 100%;
|
|
560
|
+
left: 0;
|
|
561
|
+
width: 100%;
|
|
562
|
+
position: absolute;
|
|
563
|
+
border-radius: 1ex;
|
|
564
|
+
overflow: auto;
|
|
565
|
+
background: var(--sms-options-bg, white);
|
|
566
|
+
max-height: var(--sms-options-max-height, 50vh);
|
|
567
|
+
overscroll-behavior: var(--sms-options-overscroll, none);
|
|
568
|
+
box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
|
|
569
|
+
transition: all 0.2s;
|
|
570
|
+
}
|
|
571
|
+
:where(div.multiselect) > ul.options.hidden {
|
|
572
|
+
visibility: hidden;
|
|
573
|
+
opacity: 0;
|
|
574
|
+
transform: translateY(50px);
|
|
575
|
+
}
|
|
576
|
+
:where(div.multiselect) > ul.options > li {
|
|
577
|
+
padding: 3pt 2ex;
|
|
578
|
+
cursor: pointer;
|
|
579
|
+
scroll-margin: var(--sms-options-scroll-margin, 100px);
|
|
580
|
+
}
|
|
581
|
+
:where(div.multiselect) > ul.options span {
|
|
582
|
+
padding: 3pt 2ex;
|
|
583
|
+
}
|
|
584
|
+
:where(div.multiselect) > ul.options > li.selected {
|
|
585
|
+
background: var(--sms-li-selected-bg);
|
|
586
|
+
color: var(--sms-li-selected-color);
|
|
587
|
+
}
|
|
588
|
+
:where(div.multiselect) > ul.options > li.active {
|
|
589
|
+
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
|
590
|
+
}
|
|
591
|
+
:where(div.multiselect) > ul.options > li.disabled {
|
|
592
|
+
cursor: not-allowed;
|
|
593
|
+
background: var(--sms-li-disabled-bg, #f5f5f6);
|
|
594
|
+
color: var(--sms-li-disabled-text, #b8b8b8);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
@supports not selector(:where(div.multiselect)) {
|
|
598
|
+
div.multiselect {
|
|
452
599
|
position: relative;
|
|
453
600
|
align-items: center;
|
|
454
601
|
display: flex;
|
|
@@ -463,27 +610,26 @@ function on_click_outside(event) {
|
|
|
463
610
|
min-height: var(--sms-min-height, 19pt);
|
|
464
611
|
margin: var(--sms-margin);
|
|
465
612
|
}
|
|
466
|
-
|
|
613
|
+
div.multiselect.open {
|
|
467
614
|
/* increase z-index when open to ensure the dropdown of one <MultiSelect />
|
|
468
615
|
displays above that of another slightly below it on the page */
|
|
469
616
|
z-index: var(--sms-open-z-index, 4);
|
|
470
617
|
}
|
|
471
|
-
|
|
618
|
+
div.multiselect:focus-within {
|
|
472
619
|
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
|
|
473
620
|
}
|
|
474
|
-
|
|
621
|
+
div.multiselect.disabled {
|
|
475
622
|
background: var(--sms-disabled-bg, lightgray);
|
|
476
623
|
cursor: not-allowed;
|
|
477
624
|
}
|
|
478
|
-
|
|
479
|
-
:where(div.multiselect > ul.selected) {
|
|
625
|
+
div.multiselect > ul.selected {
|
|
480
626
|
display: flex;
|
|
481
627
|
flex: 1;
|
|
482
628
|
padding: 0;
|
|
483
629
|
margin: 0;
|
|
484
630
|
flex-wrap: wrap;
|
|
485
631
|
}
|
|
486
|
-
|
|
632
|
+
div.multiselect > ul.selected > li {
|
|
487
633
|
align-items: center;
|
|
488
634
|
border-radius: 3pt;
|
|
489
635
|
display: flex;
|
|
@@ -495,7 +641,7 @@ function on_click_outside(event) {
|
|
|
495
641
|
padding: var(--sms-selected-li-padding, 1pt 5pt);
|
|
496
642
|
color: var(--sms-selected-text-color, var(--sms-text-color));
|
|
497
643
|
}
|
|
498
|
-
|
|
644
|
+
div.multiselect button {
|
|
499
645
|
border-radius: 50%;
|
|
500
646
|
display: flex;
|
|
501
647
|
transition: 0.2s;
|
|
@@ -507,18 +653,19 @@ function on_click_outside(event) {
|
|
|
507
653
|
padding: 0;
|
|
508
654
|
margin: 0 0 0 3pt; /* CSS reset */
|
|
509
655
|
}
|
|
510
|
-
|
|
656
|
+
div.multiselect button.remove-all {
|
|
511
657
|
margin: 0 3pt;
|
|
512
658
|
}
|
|
513
|
-
|
|
659
|
+
div.multiselect ul.selected > li button:hover,
|
|
660
|
+
div.multiselect button.remove-all:hover,
|
|
661
|
+
div.multiselect button:focus {
|
|
514
662
|
color: var(--sms-button-hover-color, lightskyblue);
|
|
515
663
|
}
|
|
516
|
-
|
|
517
|
-
:where(div.multiselect input) {
|
|
664
|
+
div.multiselect input {
|
|
518
665
|
margin: auto 0; /* CSS reset */
|
|
519
666
|
padding: 0; /* CSS reset */
|
|
520
667
|
}
|
|
521
|
-
|
|
668
|
+
div.multiselect > ul.selected > li > input {
|
|
522
669
|
border: none;
|
|
523
670
|
outline: none;
|
|
524
671
|
background: none;
|
|
@@ -530,12 +677,12 @@ function on_click_outside(event) {
|
|
|
530
677
|
cursor: inherit; /* needed for disabled state */
|
|
531
678
|
border-radius: 0; /* reset ul.selected > li */
|
|
532
679
|
}
|
|
533
|
-
|
|
680
|
+
div.multiselect > ul.selected > li > input::placeholder {
|
|
534
681
|
padding-left: 5pt;
|
|
535
682
|
color: var(--sms-placeholder-color);
|
|
536
683
|
opacity: var(--sms-placeholder-opacity);
|
|
537
684
|
}
|
|
538
|
-
|
|
685
|
+
div.multiselect > input.form-control {
|
|
539
686
|
width: 2em;
|
|
540
687
|
position: absolute;
|
|
541
688
|
background: transparent;
|
|
@@ -545,8 +692,7 @@ function on_click_outside(event) {
|
|
|
545
692
|
opacity: 0;
|
|
546
693
|
pointer-events: none;
|
|
547
694
|
}
|
|
548
|
-
|
|
549
|
-
:where(div.multiselect > ul.options) {
|
|
695
|
+
div.multiselect > ul.options {
|
|
550
696
|
list-style: none;
|
|
551
697
|
padding: 4pt 0;
|
|
552
698
|
top: 100%;
|
|
@@ -561,30 +707,29 @@ function on_click_outside(event) {
|
|
|
561
707
|
box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
|
|
562
708
|
transition: all 0.2s;
|
|
563
709
|
}
|
|
564
|
-
|
|
710
|
+
div.multiselect > ul.options.hidden {
|
|
565
711
|
visibility: hidden;
|
|
566
712
|
opacity: 0;
|
|
567
713
|
transform: translateY(50px);
|
|
568
714
|
}
|
|
569
|
-
|
|
715
|
+
div.multiselect > ul.options > li {
|
|
570
716
|
padding: 3pt 2ex;
|
|
571
717
|
cursor: pointer;
|
|
572
718
|
scroll-margin: var(--sms-options-scroll-margin, 100px);
|
|
573
719
|
}
|
|
574
|
-
|
|
575
|
-
:where(div.multiselect > ul.options span) {
|
|
720
|
+
div.multiselect > ul.options span {
|
|
576
721
|
padding: 3pt 2ex;
|
|
577
722
|
}
|
|
578
|
-
|
|
723
|
+
div.multiselect > ul.options > li.selected {
|
|
579
724
|
background: var(--sms-li-selected-bg);
|
|
580
725
|
color: var(--sms-li-selected-color);
|
|
581
726
|
}
|
|
582
|
-
|
|
727
|
+
div.multiselect > ul.options > li.active {
|
|
583
728
|
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
|
584
729
|
}
|
|
585
|
-
|
|
730
|
+
div.multiselect > ul.options > li.disabled {
|
|
586
731
|
cursor: not-allowed;
|
|
587
732
|
background: var(--sms-li-disabled-bg, #f5f5f6);
|
|
588
733
|
color: var(--sms-li-disabled-text, #b8b8b8);
|
|
589
734
|
}
|
|
590
|
-
</style>
|
|
735
|
+
}</style>
|
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,19 +33,18 @@ 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;
|
|
46
|
-
inputmode?: string | undefined;
|
|
47
|
-
pattern?: string | undefined;
|
|
48
48
|
};
|
|
49
49
|
slots: {
|
|
50
50
|
selected: {
|
package/index.js
CHANGED
|
@@ -8,7 +8,8 @@ export const get_value = (op) => op instanceof Object ? op.value ?? op.label : o
|
|
|
8
8
|
// this polyfill was copied from
|
|
9
9
|
// https://github.com/nuxodin/lazyfill/blob/a8e63/polyfills/Element/prototype/scrollIntoViewIfNeeded.js
|
|
10
10
|
if (typeof Element !== `undefined` &&
|
|
11
|
-
!Element.prototype?.scrollIntoViewIfNeeded
|
|
11
|
+
!Element.prototype?.scrollIntoViewIfNeeded &&
|
|
12
|
+
typeof IntersectionObserver !== `undefined`) {
|
|
12
13
|
Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded = true) {
|
|
13
14
|
const el = this;
|
|
14
15
|
new IntersectionObserver(function ([entry]) {
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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.1",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"svelte": "index.js",
|
|
11
11
|
"main": "index.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"prettier-plugin-svelte": "^2.7.0",
|
|
28
28
|
"rehype-autolink-headings": "^6.1.1",
|
|
29
29
|
"rehype-slug": "^5.0.1",
|
|
30
|
+
"sass": "^1.55.0",
|
|
30
31
|
"svelte": "^3.50.1",
|
|
31
32
|
"svelte-check": "^2.9.0",
|
|
32
33
|
"svelte-github-corner": "^0.1.0",
|
package/readme.md
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
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
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).
|
|
44
45
|
|
|
45
46
|
## Installation
|
|
46
47
|
|
|
@@ -164,6 +165,12 @@ import type { Option } from 'svelte-multiselect'
|
|
|
164
165
|
|
|
165
166
|
Handle to the `<input>` DOM node. Only available after component mounts (`null` before then).
|
|
166
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
|
+
|
|
167
174
|
1. ```ts
|
|
168
175
|
invalid: boolean = false
|
|
169
176
|
```
|
|
@@ -186,7 +193,7 @@ import type { Option } from 'svelte-multiselect'
|
|
|
186
193
|
maxSelect: number | null = null
|
|
187
194
|
```
|
|
188
195
|
|
|
189
|
-
Positive integer to limit the number of options users can pick. `null` means no limit.
|
|
196
|
+
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.
|
|
190
197
|
|
|
191
198
|
1. ```ts
|
|
192
199
|
maxSelectMsg: ((current: number, max: number) => string) | null = null
|
|
@@ -220,7 +227,7 @@ import type { Option } from 'svelte-multiselect'
|
|
|
220
227
|
options: Option[]
|
|
221
228
|
```
|
|
222
229
|
|
|
223
|
-
**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.
|
|
224
231
|
|
|
225
232
|
1. ```ts
|
|
226
233
|
outerDiv: HTMLDivElement | null = null
|
|
@@ -234,6 +241,12 @@ import type { Option } from 'svelte-multiselect'
|
|
|
234
241
|
|
|
235
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).
|
|
236
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
|
+
|
|
237
250
|
1. ```ts
|
|
238
251
|
placeholder: string | null = null
|
|
239
252
|
```
|
|
@@ -265,22 +278,25 @@ import type { Option } from 'svelte-multiselect'
|
|
|
265
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.
|
|
266
279
|
|
|
267
280
|
1. ```ts
|
|
268
|
-
selected: Option[]
|
|
281
|
+
selected: Option[] | Option | null =
|
|
282
|
+
options
|
|
283
|
+
?.filter((op) => (op as ObjectOption)?.preselected)
|
|
284
|
+
.slice(0, maxSelect ?? undefined) ?? []
|
|
269
285
|
```
|
|
270
286
|
|
|
271
|
-
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.
|
|
272
288
|
|
|
273
289
|
1. ```ts
|
|
274
|
-
selectedLabels: (string | number)[] = []
|
|
290
|
+
selectedLabels: (string | number)[] | string | number | null = []
|
|
275
291
|
```
|
|
276
292
|
|
|
277
|
-
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.
|
|
278
294
|
|
|
279
295
|
1. ```ts
|
|
280
|
-
selectedValues: unknown[] = []
|
|
296
|
+
selectedValues: unknown[] | unknown | null = []
|
|
281
297
|
```
|
|
282
298
|
|
|
283
|
-
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.
|
|
284
300
|
|
|
285
301
|
1. ```ts
|
|
286
302
|
sortSelected: boolean | ((op1: Option, op2: Option) => number) = false
|
|
@@ -288,18 +304,6 @@ import type { Option } from 'svelte-multiselect'
|
|
|
288
304
|
|
|
289
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.
|
|
290
306
|
|
|
291
|
-
1. ```ts
|
|
292
|
-
inputmode: string = ``
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
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.
|
|
296
|
-
|
|
297
|
-
1. ```ts
|
|
298
|
-
pattern: string = ``
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
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.
|
|
302
|
-
|
|
303
307
|
## Slots
|
|
304
308
|
|
|
305
309
|
`MultiSelect.svelte` has 3 named slots:
|
|
@@ -483,12 +487,12 @@ For example, to change the background color of the options dropdown:
|
|
|
483
487
|
|
|
484
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).
|
|
485
489
|
|
|
486
|
-
- `outerDivClass`
|
|
487
|
-
- `ulSelectedClass
|
|
488
|
-
- `liSelectedClass
|
|
489
|
-
- `ulOptionsClass`
|
|
490
|
-
- `liOptionClass
|
|
491
|
-
- `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)
|
|
492
496
|
|
|
493
497
|
This simplified version of the DOM structure of the component shows where these classes are inserted:
|
|
494
498
|
|