svelte-multiselect 3.3.0 → 4.0.2
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/CircleSpinner.svelte +29 -0
- package/CircleSpinner.svelte.d.ts +18 -0
- package/MultiSelect.svelte +136 -85
- package/MultiSelect.svelte.d.ts +12 -3
- package/icons/ChevronExpand.svelte +1 -6
- package/icons/ChevronExpand.svelte.d.ts +13 -8
- package/icons/Cross.svelte +1 -6
- package/icons/Cross.svelte.d.ts +13 -8
- package/icons/{ReadOnly.svelte → Disabled.svelte} +1 -6
- package/icons/Disabled.svelte.d.ts +23 -0
- package/icons/index.d.ts +2 -2
- package/icons/index.js +2 -2
- package/index.d.ts +18 -0
- package/package.json +16 -11
- package/readme.md +105 -65
- package/actions.d.ts +0 -3
- package/actions.js +0 -16
- package/icons/ReadOnly.svelte.d.ts +0 -18
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script >export let color = `cornflowerblue`;
|
|
2
|
+
export let duration = `1.5s`;
|
|
3
|
+
export let size = `1em`;
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<div
|
|
7
|
+
style="--duration: {duration}"
|
|
8
|
+
style:border-color="{color} transparent {color}
|
|
9
|
+
{color}"
|
|
10
|
+
style:width={size}
|
|
11
|
+
style:height={size}
|
|
12
|
+
/>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
div {
|
|
16
|
+
display: inline-block;
|
|
17
|
+
vertical-align: middle;
|
|
18
|
+
margin: 0 3pt;
|
|
19
|
+
border-width: calc(1em / 5);
|
|
20
|
+
border-style: solid;
|
|
21
|
+
border-radius: 50%;
|
|
22
|
+
animation: var(--duration) infinite rotate;
|
|
23
|
+
}
|
|
24
|
+
@keyframes rotate {
|
|
25
|
+
100% {
|
|
26
|
+
transform: rotate(360deg);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: {
|
|
4
|
+
color?: string | undefined;
|
|
5
|
+
duration?: string | undefined;
|
|
6
|
+
size?: string | undefined;
|
|
7
|
+
};
|
|
8
|
+
events: {
|
|
9
|
+
[evt: string]: CustomEvent<any>;
|
|
10
|
+
};
|
|
11
|
+
slots: {};
|
|
12
|
+
};
|
|
13
|
+
export declare type CircleSpinnerProps = typeof __propDef.props;
|
|
14
|
+
export declare type CircleSpinnerEvents = typeof __propDef.events;
|
|
15
|
+
export declare type CircleSpinnerSlots = typeof __propDef.slots;
|
|
16
|
+
export default class CircleSpinner extends SvelteComponentTyped<CircleSpinnerProps, CircleSpinnerEvents, CircleSpinnerSlots> {
|
|
17
|
+
}
|
|
18
|
+
export {};
|
package/MultiSelect.svelte
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<script >import { createEventDispatcher, onMount } from 'svelte';
|
|
1
|
+
<script >import { createEventDispatcher, onMount, tick } from 'svelte';
|
|
2
2
|
import { fly } from 'svelte/transition';
|
|
3
|
-
import
|
|
4
|
-
import { CrossIcon, ExpandIcon,
|
|
3
|
+
import CircleSpinner from './CircleSpinner.svelte';
|
|
4
|
+
import { CrossIcon, ExpandIcon, DisabledIcon } from './icons';
|
|
5
5
|
import Wiggle from './Wiggle.svelte';
|
|
6
6
|
export let selected = [];
|
|
7
7
|
export let selectedLabels = [];
|
|
@@ -10,7 +10,8 @@ export let searchText = ``;
|
|
|
10
10
|
export let showOptions = false;
|
|
11
11
|
export let maxSelect = null; // null means any number of options are selectable
|
|
12
12
|
export let maxSelectMsg = null;
|
|
13
|
-
export let
|
|
13
|
+
export let disabled = false;
|
|
14
|
+
export let disabledTitle = `This field is disabled`;
|
|
14
15
|
export let options;
|
|
15
16
|
export let input = null;
|
|
16
17
|
export let placeholder = undefined;
|
|
@@ -31,8 +32,13 @@ export let liOptionClass = ``;
|
|
|
31
32
|
export let liActiveOptionClass = ``;
|
|
32
33
|
export let removeBtnTitle = `Remove`;
|
|
33
34
|
export let removeAllTitle = `Remove all`;
|
|
34
|
-
// https://github.com/sveltejs/svelte/issues/6964
|
|
35
35
|
export let defaultDisabledTitle = `This option is disabled`;
|
|
36
|
+
export let allowUserOptions = false;
|
|
37
|
+
export let autoScroll = true;
|
|
38
|
+
export let loading = false;
|
|
39
|
+
export let required = false;
|
|
40
|
+
export let autocomplete = `off`;
|
|
41
|
+
export let invalid = false;
|
|
36
42
|
if (maxSelect !== null && maxSelect < 0) {
|
|
37
43
|
console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
38
44
|
}
|
|
@@ -41,9 +47,8 @@ if (!(options?.length > 0))
|
|
|
41
47
|
if (!Array.isArray(selected))
|
|
42
48
|
console.error(`selected prop must be an array`);
|
|
43
49
|
onMount(() => {
|
|
44
|
-
selected = _options.filter((op) => op?.preselected);
|
|
50
|
+
selected = _options.filter((op) => op?.preselected) ?? [];
|
|
45
51
|
});
|
|
46
|
-
let wiggle = false;
|
|
47
52
|
const dispatch = createEventDispatcher();
|
|
48
53
|
function isObject(item) {
|
|
49
54
|
return typeof item === `object` && !Array.isArray(item) && item !== null;
|
|
@@ -69,24 +74,21 @@ $: labels = _options.map((op) => op.label);
|
|
|
69
74
|
$: if (new Set(labels).size !== options.length) {
|
|
70
75
|
console.error(`Option labels must be unique. Duplicates found: ${labels.filter((label, idx) => labels.indexOf(label) !== idx)}`);
|
|
71
76
|
}
|
|
77
|
+
let wiggle = false;
|
|
72
78
|
$: selectedLabels = selected.map((op) => op.label);
|
|
73
79
|
$: selectedValues = selected.map((op) => op.value);
|
|
80
|
+
// formValue binds to input.form-control to prevent form submission if required
|
|
81
|
+
// prop is true and no options are selected
|
|
82
|
+
$: formValue = selectedValues.join(`,`);
|
|
83
|
+
$: if (formValue)
|
|
84
|
+
invalid = false; // reset error status whenever component state changes
|
|
74
85
|
// options matching the current search text
|
|
75
|
-
$: matchingOptions = _options.filter((op) => filterFunc(op, searchText));
|
|
86
|
+
$: matchingOptions = _options.filter((op) => filterFunc(op, searchText) && !selectedLabels.includes(op.label));
|
|
76
87
|
$: matchingEnabledOptions = matchingOptions.filter((op) => !op.disabled);
|
|
77
|
-
$: if (
|
|
78
|
-
// if there was an active option but it's not in the filtered list of options
|
|
79
|
-
(activeOption &&
|
|
80
|
-
!matchingEnabledOptions.map((op) => op.label).includes(activeOption.label)) ||
|
|
81
|
-
// or there's no active option but the user entered search text
|
|
82
|
-
(!activeOption && searchText))
|
|
83
|
-
// make the first filtered option active
|
|
84
|
-
activeOption = matchingEnabledOptions[0];
|
|
85
88
|
function add(label) {
|
|
86
89
|
if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
|
|
87
90
|
wiggle = true;
|
|
88
|
-
if (!
|
|
89
|
-
!selectedLabels.includes(label) &&
|
|
91
|
+
if (!selectedLabels.includes(label) &&
|
|
90
92
|
// for maxselect = 1 we always replace current option with new selection
|
|
91
93
|
(maxSelect === null || maxSelect === 1 || selected.length < maxSelect)) {
|
|
92
94
|
searchText = ``; // reset search string on selection
|
|
@@ -108,24 +110,32 @@ function add(label) {
|
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
function remove(label) {
|
|
111
|
-
if (selected.length === 0
|
|
113
|
+
if (selected.length === 0)
|
|
112
114
|
return;
|
|
113
|
-
selected = selected.filter((option) => label !== option.label);
|
|
114
115
|
const option = _options.find((option) => option.label === label);
|
|
116
|
+
if (!option) {
|
|
117
|
+
return console.error(`MultiSelect: option with label ${label} not found`);
|
|
118
|
+
}
|
|
119
|
+
selected = selected.filter((option) => label !== option.label);
|
|
115
120
|
dispatch(`remove`, { option });
|
|
116
121
|
dispatch(`change`, { option, type: `remove` });
|
|
117
122
|
}
|
|
118
123
|
function setOptionsVisible(show) {
|
|
124
|
+
if (disabled)
|
|
125
|
+
return;
|
|
119
126
|
showOptions = show;
|
|
120
|
-
if (show)
|
|
127
|
+
if (show) {
|
|
121
128
|
input?.focus();
|
|
129
|
+
dispatch(`focus`);
|
|
130
|
+
}
|
|
122
131
|
else {
|
|
123
132
|
input?.blur();
|
|
124
133
|
activeOption = null;
|
|
134
|
+
dispatch(`blur`);
|
|
125
135
|
}
|
|
126
136
|
}
|
|
127
137
|
// handle all keyboard events this component receives
|
|
128
|
-
function handleKeydown(event) {
|
|
138
|
+
async function handleKeydown(event) {
|
|
129
139
|
// on escape: dismiss options dropdown and reset search text
|
|
130
140
|
if (event.key === `Escape`) {
|
|
131
141
|
setOptionsVisible(false);
|
|
@@ -137,7 +147,15 @@ function handleKeydown(event) {
|
|
|
137
147
|
const { label } = activeOption;
|
|
138
148
|
selectedLabels.includes(label) ? remove(label) : add(label);
|
|
139
149
|
searchText = ``;
|
|
140
|
-
}
|
|
150
|
+
}
|
|
151
|
+
else if ([true, `append`].includes(allowUserOptions)) {
|
|
152
|
+
selected = [...selected, { label: searchText, value: searchText }];
|
|
153
|
+
if (allowUserOptions === `append`)
|
|
154
|
+
options = [...options, { label: searchText, value: searchText }];
|
|
155
|
+
searchText = ``;
|
|
156
|
+
}
|
|
157
|
+
// no active option and no search text means the options dropdown is closed
|
|
158
|
+
// in which case enter means open it
|
|
141
159
|
else
|
|
142
160
|
setOptionsVisible(true);
|
|
143
161
|
}
|
|
@@ -150,31 +168,25 @@ function handleKeydown(event) {
|
|
|
150
168
|
}
|
|
151
169
|
const increment = event.key === `ArrowUp` ? -1 : 1;
|
|
152
170
|
const newActiveIdx = matchingEnabledOptions.indexOf(activeOption) + increment;
|
|
153
|
-
const ulOps = document.querySelector(`ul.options`);
|
|
154
171
|
if (newActiveIdx < 0) {
|
|
155
172
|
// wrap around top
|
|
156
173
|
activeOption = matchingEnabledOptions[matchingEnabledOptions.length - 1];
|
|
157
|
-
if (ulOps)
|
|
158
|
-
ulOps.scrollTop = ulOps.scrollHeight;
|
|
159
174
|
}
|
|
160
175
|
else if (newActiveIdx === matchingEnabledOptions.length) {
|
|
161
176
|
// wrap around bottom
|
|
162
177
|
activeOption = matchingEnabledOptions[0];
|
|
163
|
-
if (ulOps)
|
|
164
|
-
ulOps.scrollTop = 0;
|
|
165
178
|
}
|
|
166
179
|
else {
|
|
167
|
-
// default case
|
|
180
|
+
// default case: select next/previous in item list
|
|
168
181
|
activeOption = matchingEnabledOptions[newActiveIdx];
|
|
182
|
+
}
|
|
183
|
+
if (autoScroll) {
|
|
184
|
+
await tick();
|
|
169
185
|
const li = document.querySelector(`ul.options > li.active`);
|
|
170
|
-
|
|
171
|
-
// downwards, we scroll to next sibling to make element fully visible
|
|
172
|
-
if (increment === 1)
|
|
173
|
-
li?.nextSibling?.scrollIntoViewIfNeeded();
|
|
174
|
-
else
|
|
175
|
-
li?.scrollIntoViewIfNeeded();
|
|
186
|
+
li?.scrollIntoViewIfNeeded();
|
|
176
187
|
}
|
|
177
188
|
}
|
|
189
|
+
// on backspace key: remove last selected option
|
|
178
190
|
else if (event.key === `Backspace`) {
|
|
179
191
|
const label = selectedLabels.pop();
|
|
180
192
|
if (label && !searchText)
|
|
@@ -199,29 +211,42 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
|
|
|
199
211
|
<!-- z-index: 2 when showOptions is true ensures the ul.selected of one <MultiSelect />
|
|
200
212
|
display above those of another following shortly after it -->
|
|
201
213
|
<div
|
|
202
|
-
class:
|
|
214
|
+
class:disabled
|
|
203
215
|
class:single={maxSelect === 1}
|
|
204
216
|
class:open={showOptions}
|
|
217
|
+
aria-expanded={showOptions}
|
|
218
|
+
aria-multiselectable={maxSelect === null || maxSelect > 1}
|
|
219
|
+
class:invalid
|
|
205
220
|
class="multiselect {outerDivClass}"
|
|
206
221
|
on:mouseup|stopPropagation={() => setOptionsVisible(true)}
|
|
207
|
-
|
|
208
|
-
|
|
222
|
+
on:focusout={() => setOptionsVisible(false)}
|
|
223
|
+
title={disabled ? disabledTitle : null}
|
|
224
|
+
aria-disabled={disabled ? `true` : null}
|
|
209
225
|
>
|
|
210
|
-
|
|
226
|
+
<!-- invisible input, used only to prevent form submission if required=true and no options selected -->
|
|
227
|
+
<input
|
|
228
|
+
{required}
|
|
229
|
+
bind:value={formValue}
|
|
230
|
+
tabindex="-1"
|
|
231
|
+
aria-hidden="true"
|
|
232
|
+
class="form-control"
|
|
233
|
+
on:invalid={() => (invalid = true)}
|
|
234
|
+
/>
|
|
235
|
+
<ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt;" />
|
|
211
236
|
<ul class="selected {ulSelectedClass}">
|
|
212
237
|
{#each selected as option, idx}
|
|
213
|
-
<li class={liSelectedClass}>
|
|
214
|
-
<slot name="
|
|
238
|
+
<li class={liSelectedClass} aria-selected="true">
|
|
239
|
+
<slot name="selected" {option} {idx}>
|
|
215
240
|
{option.label}
|
|
216
241
|
</slot>
|
|
217
|
-
{#if !
|
|
242
|
+
{#if !disabled}
|
|
218
243
|
<button
|
|
219
244
|
on:mouseup|stopPropagation={() => remove(option.label)}
|
|
220
245
|
on:keydown={handleEnterAndSpaceKeys(() => remove(option.label))}
|
|
221
246
|
type="button"
|
|
222
247
|
title="{removeBtnTitle} {option.label}"
|
|
223
248
|
>
|
|
224
|
-
<CrossIcon
|
|
249
|
+
<CrossIcon width="15px" />
|
|
225
250
|
</button>
|
|
226
251
|
{/if}
|
|
227
252
|
</li>
|
|
@@ -229,19 +254,29 @@ display above those of another following shortly after it -->
|
|
|
229
254
|
<li style="display: contents;">
|
|
230
255
|
<input
|
|
231
256
|
bind:this={input}
|
|
232
|
-
autocomplete
|
|
257
|
+
{autocomplete}
|
|
233
258
|
bind:value={searchText}
|
|
234
259
|
on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
|
|
235
260
|
on:keydown={handleKeydown}
|
|
236
261
|
on:focus={() => setOptionsVisible(true)}
|
|
262
|
+
on:blur={() => setOptionsVisible(false)}
|
|
237
263
|
{id}
|
|
238
264
|
{name}
|
|
265
|
+
{disabled}
|
|
239
266
|
placeholder={selectedLabels.length ? `` : placeholder}
|
|
267
|
+
aria-invalid={invalid ? `true` : null}
|
|
240
268
|
/>
|
|
241
269
|
</li>
|
|
242
270
|
</ul>
|
|
243
|
-
{#if
|
|
244
|
-
<
|
|
271
|
+
{#if loading}
|
|
272
|
+
<slot name="spinner">
|
|
273
|
+
<CircleSpinner />
|
|
274
|
+
</slot>
|
|
275
|
+
{/if}
|
|
276
|
+
{#if disabled}
|
|
277
|
+
<slot name="disabled-icon">
|
|
278
|
+
<DisabledIcon width="15px" />
|
|
279
|
+
</slot>
|
|
245
280
|
{:else if selected.length > 0}
|
|
246
281
|
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
|
|
247
282
|
<Wiggle bind:wiggle angle={20}>
|
|
@@ -251,7 +286,7 @@ display above those of another following shortly after it -->
|
|
|
251
286
|
</span>
|
|
252
287
|
</Wiggle>
|
|
253
288
|
{/if}
|
|
254
|
-
{#if maxSelect !== 1}
|
|
289
|
+
{#if maxSelect !== 1 && selected.length > 1}
|
|
255
290
|
<button
|
|
256
291
|
type="button"
|
|
257
292
|
class="remove-all"
|
|
@@ -259,7 +294,7 @@ display above those of another following shortly after it -->
|
|
|
259
294
|
on:mouseup|stopPropagation={removeAll}
|
|
260
295
|
on:keydown={handleEnterAndSpaceKeys(removeAll)}
|
|
261
296
|
>
|
|
262
|
-
<CrossIcon
|
|
297
|
+
<CrossIcon width="15px" />
|
|
263
298
|
</button>
|
|
264
299
|
{/if}
|
|
265
300
|
{/if}
|
|
@@ -285,8 +320,19 @@ display above those of another following shortly after it -->
|
|
|
285
320
|
class:active
|
|
286
321
|
class:disabled
|
|
287
322
|
class="{liOptionClass} {active ? liActiveOptionClass : ``}"
|
|
323
|
+
on:mouseover={() => {
|
|
324
|
+
if (disabled) return
|
|
325
|
+
activeOption = option
|
|
326
|
+
}}
|
|
327
|
+
on:focus={() => {
|
|
328
|
+
if (disabled) return
|
|
329
|
+
activeOption = option
|
|
330
|
+
}}
|
|
331
|
+
on:mouseout={() => (activeOption = null)}
|
|
332
|
+
on:blur={() => (activeOption = null)}
|
|
333
|
+
aria-selected="false"
|
|
288
334
|
>
|
|
289
|
-
<slot name="
|
|
335
|
+
<slot name="option" {option} {idx}>
|
|
290
336
|
{option.label}
|
|
291
337
|
</slot>
|
|
292
338
|
</li>
|
|
@@ -306,9 +352,11 @@ display above those of another following shortly after it -->
|
|
|
306
352
|
cursor: text;
|
|
307
353
|
padding: 0 3pt;
|
|
308
354
|
border: var(--sms-border, 1pt solid lightgray);
|
|
309
|
-
border-radius: var(--sms-border-radius,
|
|
355
|
+
border-radius: var(--sms-border-radius, 3pt);
|
|
310
356
|
background: var(--sms-input-bg);
|
|
311
357
|
min-height: var(--sms-input-min-height, 22pt);
|
|
358
|
+
color: var(--sms-text-color);
|
|
359
|
+
font-size: var(--sms-font-size, inherit);
|
|
312
360
|
}
|
|
313
361
|
:where(div.multiselect.open) {
|
|
314
362
|
z-index: var(--sms-open-z-index, 4);
|
|
@@ -316,8 +364,9 @@ display above those of another following shortly after it -->
|
|
|
316
364
|
:where(div.multiselect:focus-within) {
|
|
317
365
|
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
|
|
318
366
|
}
|
|
319
|
-
:where(div.multiselect.
|
|
320
|
-
background: var(--sms-
|
|
367
|
+
:where(div.multiselect.disabled) {
|
|
368
|
+
background: var(--sms-disabled-bg, lightgray);
|
|
369
|
+
cursor: not-allowed;
|
|
321
370
|
}
|
|
322
371
|
|
|
323
372
|
:where(div.multiselect > ul.selected) {
|
|
@@ -333,49 +382,62 @@ display above those of another following shortly after it -->
|
|
|
333
382
|
display: flex;
|
|
334
383
|
margin: 2pt;
|
|
335
384
|
line-height: normal;
|
|
336
|
-
padding: 1pt
|
|
385
|
+
padding: 1pt 5pt;
|
|
337
386
|
transition: 0.3s;
|
|
338
387
|
white-space: nowrap;
|
|
339
|
-
background: var(--sms-selected-bg,
|
|
388
|
+
background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15));
|
|
340
389
|
height: var(--sms-selected-li-height);
|
|
390
|
+
color: var(--sms-selected-text-color, var(--sms-text-color));
|
|
341
391
|
}
|
|
342
|
-
:where(div.multiselect
|
|
343
|
-
align-items: center;
|
|
392
|
+
:where(div.multiselect button) {
|
|
344
393
|
border-radius: 50%;
|
|
345
394
|
display: flex;
|
|
346
|
-
cursor: pointer;
|
|
347
395
|
transition: 0.2s;
|
|
348
|
-
}
|
|
349
|
-
:where(div.multiselect button) {
|
|
350
396
|
color: inherit;
|
|
351
397
|
background: transparent;
|
|
352
398
|
border: none;
|
|
353
399
|
cursor: pointer;
|
|
354
400
|
outline: none;
|
|
355
|
-
padding: 0
|
|
401
|
+
padding: 0;
|
|
402
|
+
margin: 0 0 0 3pt; /* CSS reset */
|
|
356
403
|
}
|
|
357
|
-
:where(
|
|
358
|
-
|
|
404
|
+
:where(div.multiselect button.remove-all) {
|
|
405
|
+
margin: 0 3pt;
|
|
359
406
|
}
|
|
360
|
-
:where(
|
|
361
|
-
|
|
407
|
+
:where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
|
|
408
|
+
color: var(--sms-button-hover-color, lightskyblue);
|
|
362
409
|
}
|
|
363
410
|
|
|
411
|
+
:where(div.multiselect input) {
|
|
412
|
+
margin: auto 0; /* CSS reset */
|
|
413
|
+
padding: 0; /* CSS reset */
|
|
414
|
+
}
|
|
364
415
|
:where(div.multiselect > ul.selected > li > input) {
|
|
365
416
|
border: none;
|
|
366
417
|
outline: none;
|
|
367
418
|
background: none;
|
|
368
419
|
flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
|
|
369
420
|
min-width: 2em;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
421
|
+
color: inherit;
|
|
422
|
+
font-size: inherit;
|
|
423
|
+
cursor: inherit; /* needed for disabled state */
|
|
424
|
+
}
|
|
425
|
+
:where(div.multiselect > ul.selected > li > input)::placeholder {
|
|
426
|
+
color: var(--sms-placeholder-color);
|
|
427
|
+
}
|
|
428
|
+
:where(div.multiselect > input.form-control) {
|
|
429
|
+
width: 2em;
|
|
430
|
+
position: absolute;
|
|
431
|
+
background: transparent;
|
|
432
|
+
border: none;
|
|
433
|
+
outline: none;
|
|
434
|
+
z-index: -1;
|
|
435
|
+
opacity: 0;
|
|
436
|
+
pointer-events: none;
|
|
374
437
|
}
|
|
375
438
|
|
|
376
439
|
:where(div.multiselect > ul.options) {
|
|
377
440
|
list-style: none;
|
|
378
|
-
max-height: 50vh;
|
|
379
441
|
padding: 0;
|
|
380
442
|
top: 100%;
|
|
381
443
|
left: 0;
|
|
@@ -384,6 +446,7 @@ display above those of another following shortly after it -->
|
|
|
384
446
|
border-radius: 1ex;
|
|
385
447
|
overflow: auto;
|
|
386
448
|
background: var(--sms-options-bg, white);
|
|
449
|
+
max-height: var(--sms-options-max-height, 50vh);
|
|
387
450
|
overscroll-behavior: var(--sms-options-overscroll, none);
|
|
388
451
|
box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
|
|
389
452
|
}
|
|
@@ -393,34 +456,22 @@ display above those of another following shortly after it -->
|
|
|
393
456
|
:where(div.multiselect > ul.options > li) {
|
|
394
457
|
padding: 3pt 2ex;
|
|
395
458
|
cursor: pointer;
|
|
459
|
+
scroll-margin: var(--sms-options-scroll-margin, 100px);
|
|
396
460
|
}
|
|
397
461
|
/* for noOptionsMsg */
|
|
398
462
|
:where(div.multiselect > ul.options span) {
|
|
399
463
|
padding: 3pt 2ex;
|
|
400
464
|
}
|
|
401
465
|
:where(div.multiselect > ul.options > li.selected) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
3pt solid var(--sms-selected-color, green)
|
|
405
|
-
);
|
|
406
|
-
background: var(--sms-li-selected-bg, inherit);
|
|
407
|
-
color: var(--sms-li-selected-color, inherit);
|
|
408
|
-
}
|
|
409
|
-
:where(div.multiselect > ul.options > li:not(.selected):hover) {
|
|
410
|
-
border-left: var(
|
|
411
|
-
--sms-li-not-selected-hover-border-left,
|
|
412
|
-
3pt solid var(--sms-active-color, cornflowerblue)
|
|
413
|
-
);
|
|
466
|
+
background: var(--sms-li-selected-bg);
|
|
467
|
+
color: var(--sms-li-selected-color);
|
|
414
468
|
}
|
|
415
469
|
:where(div.multiselect > ul.options > li.active) {
|
|
416
|
-
background: var(--sms-li-active-bg, var(--sms-active-color,
|
|
470
|
+
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
|
417
471
|
}
|
|
418
472
|
:where(div.multiselect > ul.options > li.disabled) {
|
|
419
473
|
cursor: not-allowed;
|
|
420
474
|
background: var(--sms-li-disabled-bg, #f5f5f6);
|
|
421
475
|
color: var(--sms-li-disabled-text, #b8b8b8);
|
|
422
476
|
}
|
|
423
|
-
:where(div.multiselect > ul.options > li.disabled:hover) {
|
|
424
|
-
border-left: unset;
|
|
425
|
-
}
|
|
426
477
|
</style>
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -9,7 +9,8 @@ declare const __propDef: {
|
|
|
9
9
|
showOptions?: boolean | undefined;
|
|
10
10
|
maxSelect?: number | null | undefined;
|
|
11
11
|
maxSelectMsg?: ((current: number, max: number) => string) | null | undefined;
|
|
12
|
-
|
|
12
|
+
disabled?: boolean | undefined;
|
|
13
|
+
disabledTitle?: string | undefined;
|
|
13
14
|
options: ProtoOption[];
|
|
14
15
|
input?: HTMLInputElement | null | undefined;
|
|
15
16
|
placeholder?: string | undefined;
|
|
@@ -27,6 +28,12 @@ declare const __propDef: {
|
|
|
27
28
|
removeBtnTitle?: string | undefined;
|
|
28
29
|
removeAllTitle?: string | undefined;
|
|
29
30
|
defaultDisabledTitle?: string | undefined;
|
|
31
|
+
allowUserOptions?: boolean | "append" | undefined;
|
|
32
|
+
autoScroll?: boolean | undefined;
|
|
33
|
+
loading?: boolean | undefined;
|
|
34
|
+
required?: boolean | undefined;
|
|
35
|
+
autocomplete?: string | undefined;
|
|
36
|
+
invalid?: boolean | undefined;
|
|
30
37
|
};
|
|
31
38
|
events: {
|
|
32
39
|
mouseup: MouseEvent;
|
|
@@ -34,11 +41,13 @@ declare const __propDef: {
|
|
|
34
41
|
[evt: string]: CustomEvent<any>;
|
|
35
42
|
};
|
|
36
43
|
slots: {
|
|
37
|
-
|
|
44
|
+
selected: {
|
|
38
45
|
option: Option;
|
|
39
46
|
idx: any;
|
|
40
47
|
};
|
|
41
|
-
|
|
48
|
+
spinner: {};
|
|
49
|
+
'disabled-icon': {};
|
|
50
|
+
option: {
|
|
42
51
|
option: Option;
|
|
43
52
|
idx: any;
|
|
44
53
|
};
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
<
|
|
2
|
-
export let height = width;
|
|
3
|
-
export let style = ``;
|
|
4
|
-
</script>
|
|
5
|
-
|
|
6
|
-
<svg {width} {height} {style} fill="currentColor" viewBox="0 0 16 16">
|
|
1
|
+
<svg {...$$props} fill="currentColor" viewBox="0 0 16 16">
|
|
7
2
|
<path
|
|
8
3
|
d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z"
|
|
9
4
|
/>
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
+
/** @typedef {typeof __propDef.props} ChevronExpandProps */
|
|
2
|
+
/** @typedef {typeof __propDef.events} ChevronExpandEvents */
|
|
3
|
+
/** @typedef {typeof __propDef.slots} ChevronExpandSlots */
|
|
4
|
+
export default class ChevronExpand extends SvelteComponentTyped<{
|
|
5
|
+
[x: string]: any;
|
|
6
|
+
}, {
|
|
7
|
+
[evt: string]: CustomEvent<any>;
|
|
8
|
+
}, {}> {
|
|
9
|
+
}
|
|
10
|
+
export type ChevronExpandProps = typeof __propDef.props;
|
|
11
|
+
export type ChevronExpandEvents = typeof __propDef.events;
|
|
12
|
+
export type ChevronExpandSlots = typeof __propDef.slots;
|
|
1
13
|
import { SvelteComponentTyped } from "svelte";
|
|
2
14
|
declare const __propDef: {
|
|
3
15
|
props: {
|
|
4
|
-
|
|
5
|
-
height?: string | number | undefined;
|
|
6
|
-
style?: string | undefined;
|
|
16
|
+
[x: string]: any;
|
|
7
17
|
};
|
|
8
18
|
events: {
|
|
9
19
|
[evt: string]: CustomEvent<any>;
|
|
10
20
|
};
|
|
11
21
|
slots: {};
|
|
12
22
|
};
|
|
13
|
-
export declare type ChevronExpandProps = typeof __propDef.props;
|
|
14
|
-
export declare type ChevronExpandEvents = typeof __propDef.events;
|
|
15
|
-
export declare type ChevronExpandSlots = typeof __propDef.slots;
|
|
16
|
-
export default class ChevronExpand extends SvelteComponentTyped<ChevronExpandProps, ChevronExpandEvents, ChevronExpandSlots> {
|
|
17
|
-
}
|
|
18
23
|
export {};
|
package/icons/Cross.svelte
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
<
|
|
2
|
-
export let height = width;
|
|
3
|
-
export let style = ``;
|
|
4
|
-
</script>
|
|
5
|
-
|
|
6
|
-
<svg {width} {height} {style} viewBox="0 0 20 20" fill="currentColor">
|
|
1
|
+
<svg {...$$props} viewBox="0 0 20 20" fill="currentColor">
|
|
7
2
|
<path
|
|
8
3
|
d="M10 1.6a8.4 8.4 0 100 16.8 8.4 8.4 0 000-16.8zm4.789 11.461L13.06 14.79 10 11.729l-3.061 3.06L5.21 13.06 8.272 10 5.211 6.939 6.94 5.211 10 8.271l3.061-3.061 1.729 1.729L11.728 10l3.061 3.061z"
|
|
9
4
|
/>
|
package/icons/Cross.svelte.d.ts
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
+
/** @typedef {typeof __propDef.props} CrossProps */
|
|
2
|
+
/** @typedef {typeof __propDef.events} CrossEvents */
|
|
3
|
+
/** @typedef {typeof __propDef.slots} CrossSlots */
|
|
4
|
+
export default class Cross extends SvelteComponentTyped<{
|
|
5
|
+
[x: string]: any;
|
|
6
|
+
}, {
|
|
7
|
+
[evt: string]: CustomEvent<any>;
|
|
8
|
+
}, {}> {
|
|
9
|
+
}
|
|
10
|
+
export type CrossProps = typeof __propDef.props;
|
|
11
|
+
export type CrossEvents = typeof __propDef.events;
|
|
12
|
+
export type CrossSlots = typeof __propDef.slots;
|
|
1
13
|
import { SvelteComponentTyped } from "svelte";
|
|
2
14
|
declare const __propDef: {
|
|
3
15
|
props: {
|
|
4
|
-
|
|
5
|
-
height?: string | number | undefined;
|
|
6
|
-
style?: string | undefined;
|
|
16
|
+
[x: string]: any;
|
|
7
17
|
};
|
|
8
18
|
events: {
|
|
9
19
|
[evt: string]: CustomEvent<any>;
|
|
10
20
|
};
|
|
11
21
|
slots: {};
|
|
12
22
|
};
|
|
13
|
-
export declare type CrossProps = typeof __propDef.props;
|
|
14
|
-
export declare type CrossEvents = typeof __propDef.events;
|
|
15
|
-
export declare type CrossSlots = typeof __propDef.slots;
|
|
16
|
-
export default class Cross extends SvelteComponentTyped<CrossProps, CrossEvents, CrossSlots> {
|
|
17
|
-
}
|
|
18
23
|
export {};
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
<
|
|
2
|
-
export let height = width;
|
|
3
|
-
export let style = ``;
|
|
4
|
-
</script>
|
|
5
|
-
|
|
6
|
-
<svg {width} {height} {style} viewBox="0 0 24 24" fill="currentColor">
|
|
1
|
+
<svg {...$$props} viewBox="0 0 24 24" fill="currentColor">
|
|
7
2
|
<path fill="none" d="M0 0h24v24H0V0z" />
|
|
8
3
|
<path
|
|
9
4
|
d="M14.48 11.95c.17.02.34.05.52.05 2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4c0 .18.03.35.05.52l3.43 3.43zm2.21 2.21L22.53 20H23v-2c0-2.14-3.56-3.5-6.31-3.84zM0 3.12l4 4V10H1v2h3v3h2v-3h2.88l2.51 2.51C9.19 15.11 7 16.3 7 18v2h9.88l4 4 1.41-1.41L1.41 1.71 0 3.12zM6.88 10H6v-.88l.88.88z"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** @typedef {typeof __propDef.props} DisabledProps */
|
|
2
|
+
/** @typedef {typeof __propDef.events} DisabledEvents */
|
|
3
|
+
/** @typedef {typeof __propDef.slots} DisabledSlots */
|
|
4
|
+
export default class Disabled extends SvelteComponentTyped<{
|
|
5
|
+
[x: string]: any;
|
|
6
|
+
}, {
|
|
7
|
+
[evt: string]: CustomEvent<any>;
|
|
8
|
+
}, {}> {
|
|
9
|
+
}
|
|
10
|
+
export type DisabledProps = typeof __propDef.props;
|
|
11
|
+
export type DisabledEvents = typeof __propDef.events;
|
|
12
|
+
export type DisabledSlots = typeof __propDef.slots;
|
|
13
|
+
import { SvelteComponentTyped } from "svelte";
|
|
14
|
+
declare const __propDef: {
|
|
15
|
+
props: {
|
|
16
|
+
[x: string]: any;
|
|
17
|
+
};
|
|
18
|
+
events: {
|
|
19
|
+
[evt: string]: CustomEvent<any>;
|
|
20
|
+
};
|
|
21
|
+
slots: {};
|
|
22
|
+
};
|
|
23
|
+
export {};
|
package/icons/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { default as CrossIcon } from './Cross.svelte';
|
|
2
1
|
export { default as ExpandIcon } from './ChevronExpand.svelte';
|
|
3
|
-
export { default as
|
|
2
|
+
export { default as CrossIcon } from './Cross.svelte';
|
|
3
|
+
export { default as DisabledIcon } from './Disabled.svelte';
|
package/icons/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { default as CrossIcon } from './Cross.svelte';
|
|
2
1
|
export { default as ExpandIcon } from './ChevronExpand.svelte';
|
|
3
|
-
export { default as
|
|
2
|
+
export { default as CrossIcon } from './Cross.svelte';
|
|
3
|
+
export { default as DisabledIcon } from './Disabled.svelte';
|
package/index.d.ts
CHANGED
|
@@ -13,3 +13,21 @@ export declare type Option = {
|
|
|
13
13
|
export declare type ProtoOption = Primitive | (Omit<Option, `value`> & {
|
|
14
14
|
value?: Primitive;
|
|
15
15
|
});
|
|
16
|
+
export declare type DispatchEvents = {
|
|
17
|
+
add: {
|
|
18
|
+
option: Option;
|
|
19
|
+
};
|
|
20
|
+
remove: {
|
|
21
|
+
option: Option;
|
|
22
|
+
};
|
|
23
|
+
removeAll: {
|
|
24
|
+
options: Option[];
|
|
25
|
+
};
|
|
26
|
+
change: {
|
|
27
|
+
option?: Option;
|
|
28
|
+
options?: Option[];
|
|
29
|
+
type: 'add' | 'remove' | 'removeAll';
|
|
30
|
+
};
|
|
31
|
+
focus: undefined;
|
|
32
|
+
blur: undefined;
|
|
33
|
+
};
|
package/package.json
CHANGED
|
@@ -5,19 +5,23 @@
|
|
|
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": "4.0.2",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"svelte": "index.js",
|
|
11
11
|
"bugs": "https://github.com/janosh/svelte-multiselect/issues",
|
|
12
12
|
"devDependencies": {
|
|
13
|
-
"@sveltejs/adapter-static": "^1.0.0-next.
|
|
14
|
-
"@sveltejs/kit": "^1.0.0-next.
|
|
15
|
-
"@
|
|
16
|
-
"@typescript-eslint/
|
|
17
|
-
"eslint": "^
|
|
18
|
-
"
|
|
13
|
+
"@sveltejs/adapter-static": "^1.0.0-next.29",
|
|
14
|
+
"@sveltejs/kit": "^1.0.0-next.295",
|
|
15
|
+
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.39",
|
|
16
|
+
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
|
17
|
+
"@typescript-eslint/parser": "^5.14.0",
|
|
18
|
+
"@vitest/ui": "^0.6.0",
|
|
19
|
+
"eslint": "^8.11.0",
|
|
20
|
+
"eslint-plugin-svelte3": "^3.4.1",
|
|
19
21
|
"hastscript": "^7.0.2",
|
|
22
|
+
"jsdom": "^19.0.0",
|
|
20
23
|
"mdsvex": "^0.10.5",
|
|
24
|
+
"playwright": "^1.19.2",
|
|
21
25
|
"prettier": "^2.5.1",
|
|
22
26
|
"prettier-plugin-svelte": "^2.6.0",
|
|
23
27
|
"rehype-autolink-headings": "^6.1.1",
|
|
@@ -25,12 +29,13 @@
|
|
|
25
29
|
"svelte": "^3.46.4",
|
|
26
30
|
"svelte-check": "^2.4.5",
|
|
27
31
|
"svelte-github-corner": "^0.1.0",
|
|
28
|
-
"svelte-preprocess": "^4.10.
|
|
29
|
-
"svelte-toc": "^0.2.
|
|
32
|
+
"svelte-preprocess": "^4.10.4",
|
|
33
|
+
"svelte-toc": "^0.2.7",
|
|
30
34
|
"svelte2tsx": "^0.5.5",
|
|
31
35
|
"tslib": "^2.3.1",
|
|
32
|
-
"typescript": "^4.
|
|
33
|
-
"vite": "^2.8.
|
|
36
|
+
"typescript": "^4.6.2",
|
|
37
|
+
"vite": "^2.8.6",
|
|
38
|
+
"vitest": "^0.6.0"
|
|
34
39
|
},
|
|
35
40
|
"keywords": [
|
|
36
41
|
"svelte",
|
package/readme.md
CHANGED
|
@@ -5,22 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
<h4 align="center">
|
|
7
7
|
|
|
8
|
+
[](https://github.com/janosh/svelte-multiselect/actions/workflows/test.yml)
|
|
8
9
|
[](https://app.netlify.com/sites/svelte-multiselect/deploys)
|
|
9
10
|
[](https://npmjs.com/package/svelte-multiselect)
|
|
10
11
|
[](https://results.pre-commit.ci/latest/github/janosh/svelte-multiselect/main)
|
|
11
|
-

|
|
12
|
+
[](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
|
|
12
13
|
|
|
13
14
|
</h4>
|
|
14
15
|
|
|
15
|
-
<
|
|
16
|
+
**Keyboard-friendly, zero-dependency multi-select Svelte component.** <strong class="hide-in-docs"><a href="https://svelte-multiselect.netlify.app">Live demo</a></strong>
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
</div>
|
|
20
|
-
|
|
21
|
-
**Keyboard-friendly, zero-dependency multi-select Svelte component.**
|
|
22
|
-
|
|
23
|
-
<slot />
|
|
18
|
+
<slot name="examples" />
|
|
24
19
|
|
|
25
20
|
## Key features
|
|
26
21
|
|
|
@@ -33,18 +28,26 @@
|
|
|
33
28
|
- **No dependencies:** needs only Svelte as dev dependency
|
|
34
29
|
- **Keyboard friendly** for mouse-less form completion
|
|
35
30
|
|
|
31
|
+
<slot name="nav" />
|
|
32
|
+
|
|
36
33
|
## Recent breaking changes
|
|
37
34
|
|
|
38
|
-
- v2.0.0 added the ability to pass options as objects. As a result, `bind:selected` no longer returns simple strings but objects, even if you still pass in `options` as strings. To get the same stuff you would have gotten from `bind:selected` before, there's now `bind:selectedLabels` (and `bind:selectedValues`).
|
|
39
35
|
- v3.0.0 changed the `event.detail` payload for `'add'`, `'remove'` and `'change'` events from `token` to `option`, e.g.
|
|
40
36
|
|
|
41
37
|
```js
|
|
42
|
-
on:add={(e) => console.log(e.detail.token.label)} // v2
|
|
43
|
-
on:add={(e) => console.log(e.detail.option.label)} // v3
|
|
38
|
+
on:add={(e) => console.log(e.detail.token.label)} // v2
|
|
39
|
+
on:add={(e) => console.log(e.detail.option.label)} // v3
|
|
44
40
|
```
|
|
45
41
|
|
|
46
42
|
It also added a separate event type `removeAll` for when the user removes all currently selected options at once which previously fired a normal `remove`. The props `ulTokensClass` and `liTokenClass` were renamed to `ulSelectedClass` and `liSelectedClass`. Similarly, the CSS variable `--sms-token-bg` changed to `--sms-selected-bg`.
|
|
47
43
|
|
|
44
|
+
- v4.0.0 renamed the slots for customizing how selected options and dropdown list items are rendered:
|
|
45
|
+
|
|
46
|
+
- old: `<slot name="renderOptions" />`, new: `<slot name="option" />`
|
|
47
|
+
- old: `<slot name="renderSelected" />`, new: `<slot name="selected" />`
|
|
48
|
+
|
|
49
|
+
- v4.0.1 renamed the `readonly` prop to `disabled` which now prevents all form or user interaction with this component including opening the dropdown list which was still possible before. See [#45](https://github.com/janosh/svelte-multiselect/issues/45) for details. The associated CSS class applied to the outer `div` was likewise renamed to `div.multiselect.{readonly=>disabled}`.
|
|
50
|
+
|
|
48
51
|
## Installation
|
|
49
52
|
|
|
50
53
|
```sh
|
|
@@ -57,28 +60,16 @@ yarn add -D svelte-multiselect
|
|
|
57
60
|
<script>
|
|
58
61
|
import MultiSelect from 'svelte-multiselect'
|
|
59
62
|
|
|
60
|
-
const
|
|
61
|
-
`Svelte`,
|
|
62
|
-
`React`,
|
|
63
|
-
`Vue`,
|
|
64
|
-
`Angular`,
|
|
65
|
-
`Polymer`,
|
|
66
|
-
`Ruby on Rails`,
|
|
67
|
-
`ASP.net`,
|
|
68
|
-
`Laravel`,
|
|
69
|
-
`Django`,
|
|
70
|
-
`Express`,
|
|
71
|
-
`Spring`,
|
|
72
|
-
]
|
|
63
|
+
const ui_libs = [`Svelte`, `React`, `Vue`, `Angular`, `...`]
|
|
73
64
|
|
|
74
|
-
let selected
|
|
65
|
+
let selected = []
|
|
75
66
|
</script>
|
|
76
67
|
|
|
77
|
-
Favorite
|
|
68
|
+
Favorite Frontend Frameworks?
|
|
78
69
|
|
|
79
70
|
<code>selected = {JSON.stringify(selected)}</code>
|
|
80
71
|
|
|
81
|
-
<MultiSelect bind:selected options={
|
|
72
|
+
<MultiSelect bind:selected options={ui_libs} />
|
|
82
73
|
```
|
|
83
74
|
|
|
84
75
|
## Props
|
|
@@ -88,22 +79,32 @@ Full list of props/bindable variables for this component:
|
|
|
88
79
|
<div class="table">
|
|
89
80
|
|
|
90
81
|
<!-- prettier-ignore -->
|
|
91
|
-
| name
|
|
92
|
-
|
|
|
93
|
-
| `options`
|
|
94
|
-
| `showOptions`
|
|
95
|
-
| `searchText`
|
|
96
|
-
| `activeOption`
|
|
97
|
-
| `maxSelect`
|
|
98
|
-
| `selected`
|
|
99
|
-
| `selectedLabels`
|
|
100
|
-
| `selectedValues`
|
|
101
|
-
| `noOptionsMsg`
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
82
|
+
| name | default | description |
|
|
83
|
+
| :--------------------- | :-------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `options` | required prop | Array of strings/numbers or `Option` objects that will be listed in the dropdown. See `src/lib/index.ts` for admissible fields. The `label` is the only mandatory one. It must also be unique. |
|
|
85
|
+
| `showOptions` | `false` | Bindable boolean that controls whether the options dropdown is visible. |
|
|
86
|
+
| `searchText` | `` | 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. |
|
|
87
|
+
| `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
|
|
88
|
+
| `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
|
|
89
|
+
| `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
|
|
90
|
+
| `selectedLabels` | `[]` | Labels of currently selected options. |
|
|
91
|
+
| `selectedValues` | `[]` | Values of currently selected options. |
|
|
92
|
+
| `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. |
|
|
93
|
+
| `disabled` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
|
|
94
|
+
| `disabledTitle` | `This field is disabled` | Tooltip text to display on hover when the component is in `disabled` state. |
|
|
95
|
+
| `placeholder` | `undefined` | String shown in the text input when no option is selected. |
|
|
96
|
+
| `input` | `undefined` | Handle to the `<input>` DOM node. |
|
|
97
|
+
| `id` | `undefined` | Applied to the `<input>` element for associating HTML form `<label>`s with this component for accessibility. Also, clicking a `<label>` with same `for` attribute as `id` will focus this component. |
|
|
98
|
+
| `name` | `id` | Applied to the `<input>` element. If not provided, will be set to the value of `id`. 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>`. |
|
|
99
|
+
| `required` | `false` | 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. |
|
|
100
|
+
| `autoScroll` | `true` | `false` disables keeping the active dropdown items in view when going up/down the list of options with arrow keys. |
|
|
101
|
+
| `allowUserOptions` | `false` | Whether users are allowed to enter values not in the dropdown list. `true` means add user-defined options to the selected list only, `'append'` means add to both options and selected. |
|
|
102
|
+
| `loading` | `false` | Whether the component should display a spinner to indicate it's in loading state. Use `<slot name='spinner'>` to specify a custom spinner. |
|
|
103
|
+
| `removeBtnTitle` | `'Remove'` | Title text to display when user hovers over button (cross icon) to remove selected option. |
|
|
104
|
+
| `removeAllTitle` | `'Remove all'` | Title text to display when user hovers over remove-all button. |
|
|
105
|
+
| `defaultDisabledTitle` | `'This option is disabled'` | Title text to display when user hovers over a disabled option. Each option can override this through its `disabledTitle` attribute. |
|
|
106
|
+
| `autocomplete` | `'off'` | Applied to the `<input>`. Specifies if browser is permitted to auto-fill this form field. See [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) for other admissible values. |
|
|
107
|
+
| `invalid` | `false` | If `required=true` and user tries to submit but `selected = []` is empty, `invalid` is automatically set to `true` and CSS class `invalid` applied to the top-level `div.multiselect`. `invalid` class is removed again as soon as the user selects an option. `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. |
|
|
107
108
|
|
|
108
109
|
</div>
|
|
109
110
|
|
|
@@ -126,24 +127,28 @@ Full list of props/bindable variables for this component:
|
|
|
126
127
|
|
|
127
128
|
## Slots
|
|
128
129
|
|
|
129
|
-
`MultiSelect.svelte`
|
|
130
|
+
`MultiSelect.svelte` has 3 named slots:
|
|
130
131
|
|
|
131
|
-
- `slot="
|
|
132
|
-
- `slot="
|
|
132
|
+
- `slot="option"`: Customize rendering of dropdown options. Receives as props the `option` object and the zero-indexed position (`idx`) it has in the dropdown.
|
|
133
|
+
- `slot="selected"`: Customize rendering selected tags. Receives as props the `option` object and the zero-indexed position (`idx`) it has in the list of selected items.
|
|
134
|
+
- `slot="spinner"`: Custom spinner component to display when in `loading` state. Receives no props.
|
|
135
|
+
- `slot="disabled-icon"`: Custom icon to display inside the input when in `disabled` state. Receives no props. Use an empty `<span slot="disabled-icon" />` or `div` to remove the default disabled icon.
|
|
133
136
|
|
|
134
|
-
|
|
137
|
+
Example:
|
|
135
138
|
|
|
136
139
|
```svelte
|
|
137
140
|
<MultiSelect options={[`Banana`, `Apple`, `Mango`]}>
|
|
138
|
-
<span let:idx let:option slot="
|
|
141
|
+
<span let:idx let:option slot="option">
|
|
139
142
|
{idx + 1}. {option.label}
|
|
140
143
|
{option.label === `Mango` ? `🎉` : ``}
|
|
141
144
|
</span>
|
|
142
145
|
|
|
143
|
-
<span let:idx let:option slot="
|
|
146
|
+
<span let:idx let:option slot="selected">
|
|
144
147
|
#{idx + 1}
|
|
145
148
|
{option.label}
|
|
146
149
|
</span>
|
|
150
|
+
|
|
151
|
+
<CustomSpinner slot="spinner">
|
|
147
152
|
</MultiSelect>
|
|
148
153
|
```
|
|
149
154
|
|
|
@@ -215,34 +220,35 @@ If you only want to make small adjustments, you can pass the following CSS varia
|
|
|
215
220
|
|
|
216
221
|
- `div.multiselect`
|
|
217
222
|
- `border: var(--sms-border, 1pt solid lightgray)`: Change this to e.g. to `1px solid red` to indicate this form field is in an invalid state.
|
|
218
|
-
- `border-radius: var(--sms-border-radius,
|
|
223
|
+
- `border-radius: var(--sms-border-radius, 3pt)`
|
|
219
224
|
- `background: var(--sms-input-bg)`
|
|
220
225
|
- `height: var(--sms-input-height, 2em)`
|
|
226
|
+
- `color: var(--sms-text-color)`
|
|
227
|
+
- `color: var(--sms-placeholder-color)`
|
|
221
228
|
- `div.multiselect.open`
|
|
222
229
|
- `z-index: var(--sms-open-z-index, 4)`: Increase this if needed to ensure the dropdown list is displayed atop all other page elements.
|
|
223
230
|
- `div.multiselect:focus-within`
|
|
224
231
|
- `border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue))`: Border when component has focus. Defaults to `--sms-active-color` if not set which defaults to `cornflowerblue`.
|
|
225
|
-
- `div.multiselect.
|
|
226
|
-
- `background: var(--sms-
|
|
227
|
-
- `div.multiselect > ul.selected > li > input`
|
|
228
|
-
- `color: var(--sms-text-color, inherit)`: Input text color.
|
|
232
|
+
- `div.multiselect.disabled`
|
|
233
|
+
- `background: var(--sms-disabled-bg, lightgray)`: Background when in disabled state.
|
|
229
234
|
- `div.multiselect > ul.selected > li`
|
|
230
|
-
- `background: var(--sms-selected-bg,
|
|
235
|
+
- `background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15))`: Background of selected options.
|
|
231
236
|
- `height: var(--sms-selected-li-height)`: Height of selected options.
|
|
237
|
+
- `color: var(--sms-selected-text-color, var(--sms-text-color))`: Text color for selected options.
|
|
232
238
|
- `ul.selected > li button:hover, button.remove-all:hover, button:focus`
|
|
233
|
-
- `color: var(--sms-
|
|
239
|
+
- `color: var(--sms-button-hover-color, lightskyblue)`: Color of the cross-icon buttons for removing all or individual selected options when in `:focus` or `:hover` state.
|
|
234
240
|
- `div.multiselect > ul.options`
|
|
235
241
|
- `background: var(--sms-options-bg, white)`: Background of dropdown list.
|
|
242
|
+
- `max-height: var(--sms-options-max-height, 50vh)`: Maximum height of options dropdown.
|
|
236
243
|
- `overscroll-behavior: var(--sms-options-overscroll, none)`: Whether scroll events bubble to parent elements when reaching the top/bottom of the options dropdown. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior).
|
|
237
244
|
- `box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);`: Box shadow of dropdown list.
|
|
245
|
+
- `div.multiselect > ul.options > li`
|
|
246
|
+
- `scroll-margin: var(--sms-options-scroll-margin, 100px)`: Top/bottom margin to keep between dropdown list items and top/bottom screen edge when auto-scrolling list to keep items in view.
|
|
238
247
|
- `div.multiselect > ul.options > li.selected`
|
|
239
|
-
- `
|
|
240
|
-
- `
|
|
241
|
-
- `color: var(--sms-li-selected-color, inherit)`: Text color of selected list items in options pane.
|
|
242
|
-
- `div.multiselect > ul.options > li:not(.selected):hover`
|
|
243
|
-
- `border-left: var(--sms-li-not-selected-hover-border-left, 3pt solid var(--sms-active-color, cornflowerblue))`
|
|
248
|
+
- `background: var(--sms-li-selected-bg)`: Background of selected list items in options pane.
|
|
249
|
+
- `color: var(--sms-li-selected-color)`: Text color of selected list items in options pane.
|
|
244
250
|
- `div.multiselect > ul.options > li.active`
|
|
245
|
-
- `background: var(--sms-li-active-bg, var(--sms-active-color,
|
|
251
|
+
- `background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)))`: Background of active dropdown item. Items become active either by mouseover or by navigating to them with arrow keys.
|
|
246
252
|
- `div.multiselect > ul.options > li.disabled`
|
|
247
253
|
- `background: var(--sms-li-disabled-bg, #f5f5f6)`: Background of disabled options in the dropdown list.
|
|
248
254
|
- `color: var(--sms-li-disabled-text, #b8b8b8)`: Text color of disabled option in the dropdown list.
|
|
@@ -292,8 +298,8 @@ You can alternatively style every part of this component with more fine-grained
|
|
|
292
298
|
:global(div.multiselect.open) {
|
|
293
299
|
/* top-level wrapper div when dropdown open */
|
|
294
300
|
}
|
|
295
|
-
:global(div.multiselect.
|
|
296
|
-
/* top-level wrapper div when in
|
|
301
|
+
:global(div.multiselect.disabled) {
|
|
302
|
+
/* top-level wrapper div when in disabled state */
|
|
297
303
|
}
|
|
298
304
|
:global(div.multiselect > ul.selected) {
|
|
299
305
|
/* selected list */
|
|
@@ -331,6 +337,40 @@ You can alternatively style every part of this component with more fine-grained
|
|
|
331
337
|
}
|
|
332
338
|
```
|
|
333
339
|
|
|
340
|
+
## Downstream testing
|
|
341
|
+
|
|
342
|
+
To test a Svelte component which imports `svelte-multiselect`, you need to configure your test runner to avoid [transpiling issues](https://github.com/janosh/svelte-multiselect/issues/48).
|
|
343
|
+
|
|
344
|
+
For Jest, exclude `svelte-multiselect` from `transformIgnorePatterns` in your `jest.config.json`:
|
|
345
|
+
|
|
346
|
+
```json
|
|
347
|
+
{
|
|
348
|
+
"transformIgnorePatterns": ["node_modules/?!(svelte-multiselect)"],
|
|
349
|
+
"transform": {
|
|
350
|
+
"^.+\\.[t|j]s?$": "esbuild-jest",
|
|
351
|
+
"^.+\\.svelte$": ["svelte-jester", { "preprocess": true }]
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
For Vitest, include `svelte-multiselect` in `deps.inline`:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
// vite.config.ts
|
|
360
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
|
361
|
+
|
|
362
|
+
export default {
|
|
363
|
+
plugins: [svelte({ hot: !process.env.VITEST })],
|
|
364
|
+
test: {
|
|
365
|
+
deps: {
|
|
366
|
+
inline: [/svelte-multiselect/],
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Here's a [Stackblitz example](https://stackblitz.com/fork/github/davipon/test-svelte-multiselect?initialPath=__vitest__) that also uses [`'vitest-svelte-kit'`](https://github.com/nickbreaton/vitest-svelte-kit).
|
|
373
|
+
|
|
334
374
|
## Want to contribute?
|
|
335
375
|
|
|
336
376
|
To submit a PR, clone the repo, install dependencies and start the dev server to try out your changes.
|
package/actions.d.ts
DELETED
package/actions.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export function onClickOutside(node, cb) {
|
|
2
|
-
const dispatchOnClickOutside = (event) => {
|
|
3
|
-
const clickWasOutside = node && !node.contains(event.target);
|
|
4
|
-
if (clickWasOutside && !event.defaultPrevented) {
|
|
5
|
-
node.dispatchEvent(new CustomEvent(`clickOutside`));
|
|
6
|
-
if (cb)
|
|
7
|
-
cb();
|
|
8
|
-
}
|
|
9
|
-
};
|
|
10
|
-
document.addEventListener(`click`, dispatchOnClickOutside);
|
|
11
|
-
return {
|
|
12
|
-
destroy() {
|
|
13
|
-
document.removeEventListener(`click`, dispatchOnClickOutside);
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { SvelteComponentTyped } from "svelte";
|
|
2
|
-
declare const __propDef: {
|
|
3
|
-
props: {
|
|
4
|
-
width?: string | number | undefined;
|
|
5
|
-
height?: string | number | undefined;
|
|
6
|
-
style?: string | undefined;
|
|
7
|
-
};
|
|
8
|
-
events: {
|
|
9
|
-
[evt: string]: CustomEvent<any>;
|
|
10
|
-
};
|
|
11
|
-
slots: {};
|
|
12
|
-
};
|
|
13
|
-
export declare type ReadOnlyProps = typeof __propDef.props;
|
|
14
|
-
export declare type ReadOnlyEvents = typeof __propDef.events;
|
|
15
|
-
export declare type ReadOnlySlots = typeof __propDef.slots;
|
|
16
|
-
export default class ReadOnly extends SvelteComponentTyped<ReadOnlyProps, ReadOnlyEvents, ReadOnlySlots> {
|
|
17
|
-
}
|
|
18
|
-
export {};
|