svelte-multiselect 3.2.3 → 4.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/CircleSpinner.svelte +29 -0
- package/CircleSpinner.svelte.d.ts +18 -0
- package/MultiSelect.svelte +137 -96
- package/MultiSelect.svelte.d.ts +15 -4
- package/icons/{ReadOnly.svelte → Disabled.svelte} +0 -0
- package/icons/{ReadOnly.svelte.d.ts → Disabled.svelte.d.ts} +4 -4
- package/icons/index.d.ts +2 -2
- package/icons/index.js +2 -2
- package/index.d.ts +17 -0
- package/package.json +14 -9
- package/readme.md +100 -39
- package/actions.d.ts +0 -3
- package/actions.js +0 -16
|
@@ -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,14 +1,17 @@
|
|
|
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 = [];
|
|
8
8
|
export let selectedValues = [];
|
|
9
|
+
export let searchText = ``;
|
|
10
|
+
export let showOptions = false;
|
|
9
11
|
export let maxSelect = null; // null means any number of options are selectable
|
|
10
|
-
export let maxSelectMsg =
|
|
11
|
-
export let
|
|
12
|
+
export let maxSelectMsg = null;
|
|
13
|
+
export let disabled = false;
|
|
14
|
+
export let disabledTitle = `This field is disabled`;
|
|
12
15
|
export let options;
|
|
13
16
|
export let input = null;
|
|
14
17
|
export let placeholder = undefined;
|
|
@@ -26,10 +29,15 @@ export let ulSelectedClass = ``;
|
|
|
26
29
|
export let liSelectedClass = ``;
|
|
27
30
|
export let ulOptionsClass = ``;
|
|
28
31
|
export let liOptionClass = ``;
|
|
32
|
+
export let liActiveOptionClass = ``;
|
|
29
33
|
export let removeBtnTitle = `Remove`;
|
|
30
34
|
export let removeAllTitle = `Remove all`;
|
|
31
|
-
// https://github.com/sveltejs/svelte/issues/6964
|
|
32
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`;
|
|
33
41
|
if (maxSelect !== null && maxSelect < 0) {
|
|
34
42
|
console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
35
43
|
}
|
|
@@ -38,9 +46,13 @@ if (!(options?.length > 0))
|
|
|
38
46
|
if (!Array.isArray(selected))
|
|
39
47
|
console.error(`selected prop must be an array`);
|
|
40
48
|
onMount(() => {
|
|
41
|
-
selected = _options.filter((op) => op?.preselected);
|
|
49
|
+
selected = _options.filter((op) => op?.preselected) ?? [];
|
|
42
50
|
});
|
|
43
51
|
let wiggle = false;
|
|
52
|
+
// formValue binds to input.form-control to prevent form submission if required
|
|
53
|
+
// prop is true and no options are selected
|
|
54
|
+
$: formValue = selectedValues.join(`,`);
|
|
55
|
+
const dispatch = createEventDispatcher();
|
|
44
56
|
function isObject(item) {
|
|
45
57
|
return typeof item === `object` && !Array.isArray(item) && item !== null;
|
|
46
58
|
}
|
|
@@ -67,27 +79,15 @@ $: if (new Set(labels).size !== options.length) {
|
|
|
67
79
|
}
|
|
68
80
|
$: selectedLabels = selected.map((op) => op.label);
|
|
69
81
|
$: selectedValues = selected.map((op) => op.value);
|
|
70
|
-
const dispatch = createEventDispatcher();
|
|
71
|
-
let searchText = ``;
|
|
72
|
-
let showOptions = false;
|
|
73
82
|
// options matching the current search text
|
|
74
|
-
$: matchingOptions = _options.filter((op) => filterFunc(op, searchText));
|
|
83
|
+
$: matchingOptions = _options.filter((op) => filterFunc(op, searchText) && !selectedLabels.includes(op.label));
|
|
75
84
|
$: matchingEnabledOptions = matchingOptions.filter((op) => !op.disabled);
|
|
76
|
-
$: if (
|
|
77
|
-
// if there was an active option but it's not in the filtered list of options
|
|
78
|
-
(activeOption &&
|
|
79
|
-
!matchingEnabledOptions.map((op) => op.label).includes(activeOption.label)) ||
|
|
80
|
-
// or there's no active option but the user entered search text
|
|
81
|
-
(!activeOption && searchText))
|
|
82
|
-
// make the first filtered option active
|
|
83
|
-
activeOption = matchingEnabledOptions[0];
|
|
84
85
|
function add(label) {
|
|
85
86
|
if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
|
|
86
87
|
wiggle = true;
|
|
87
|
-
if (!
|
|
88
|
-
!selectedLabels.includes(label) &&
|
|
88
|
+
if (!selectedLabels.includes(label) &&
|
|
89
89
|
// for maxselect = 1 we always replace current option with new selection
|
|
90
|
-
(maxSelect
|
|
90
|
+
(maxSelect === null || maxSelect === 1 || selected.length < maxSelect)) {
|
|
91
91
|
searchText = ``; // reset search string on selection
|
|
92
92
|
const option = _options.find((op) => op.label === label);
|
|
93
93
|
if (!option) {
|
|
@@ -107,14 +107,19 @@ function add(label) {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
function remove(label) {
|
|
110
|
-
if (selected.length === 0
|
|
110
|
+
if (selected.length === 0)
|
|
111
111
|
return;
|
|
112
|
-
selected = selected.filter((option) => label !== option.label);
|
|
113
112
|
const option = _options.find((option) => option.label === label);
|
|
113
|
+
if (!option) {
|
|
114
|
+
return console.error(`MultiSelect: option with label ${label} not found`);
|
|
115
|
+
}
|
|
116
|
+
selected = selected.filter((option) => label !== option.label);
|
|
114
117
|
dispatch(`remove`, { option });
|
|
115
118
|
dispatch(`change`, { option, type: `remove` });
|
|
116
119
|
}
|
|
117
120
|
function setOptionsVisible(show) {
|
|
121
|
+
if (disabled)
|
|
122
|
+
return;
|
|
118
123
|
showOptions = show;
|
|
119
124
|
if (show)
|
|
120
125
|
input?.focus();
|
|
@@ -124,7 +129,7 @@ function setOptionsVisible(show) {
|
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
// handle all keyboard events this component receives
|
|
127
|
-
function handleKeydown(event) {
|
|
132
|
+
async function handleKeydown(event) {
|
|
128
133
|
// on escape: dismiss options dropdown and reset search text
|
|
129
134
|
if (event.key === `Escape`) {
|
|
130
135
|
setOptionsVisible(false);
|
|
@@ -136,7 +141,15 @@ function handleKeydown(event) {
|
|
|
136
141
|
const { label } = activeOption;
|
|
137
142
|
selectedLabels.includes(label) ? remove(label) : add(label);
|
|
138
143
|
searchText = ``;
|
|
139
|
-
}
|
|
144
|
+
}
|
|
145
|
+
else if ([true, `append`].includes(allowUserOptions)) {
|
|
146
|
+
selected = [...selected, { label: searchText, value: searchText }];
|
|
147
|
+
if (allowUserOptions === `append`)
|
|
148
|
+
options = [...options, { label: searchText, value: searchText }];
|
|
149
|
+
searchText = ``;
|
|
150
|
+
}
|
|
151
|
+
// no active option and no search text means the options dropdown is closed
|
|
152
|
+
// in which case enter means open it
|
|
140
153
|
else
|
|
141
154
|
setOptionsVisible(true);
|
|
142
155
|
}
|
|
@@ -149,31 +162,25 @@ function handleKeydown(event) {
|
|
|
149
162
|
}
|
|
150
163
|
const increment = event.key === `ArrowUp` ? -1 : 1;
|
|
151
164
|
const newActiveIdx = matchingEnabledOptions.indexOf(activeOption) + increment;
|
|
152
|
-
const ulOps = document.querySelector(`ul.options`);
|
|
153
165
|
if (newActiveIdx < 0) {
|
|
154
166
|
// wrap around top
|
|
155
167
|
activeOption = matchingEnabledOptions[matchingEnabledOptions.length - 1];
|
|
156
|
-
if (ulOps)
|
|
157
|
-
ulOps.scrollTop = ulOps.scrollHeight;
|
|
158
168
|
}
|
|
159
169
|
else if (newActiveIdx === matchingEnabledOptions.length) {
|
|
160
170
|
// wrap around bottom
|
|
161
171
|
activeOption = matchingEnabledOptions[0];
|
|
162
|
-
if (ulOps)
|
|
163
|
-
ulOps.scrollTop = 0;
|
|
164
172
|
}
|
|
165
173
|
else {
|
|
166
|
-
// default case
|
|
174
|
+
// default case: select next/previous in item list
|
|
167
175
|
activeOption = matchingEnabledOptions[newActiveIdx];
|
|
176
|
+
}
|
|
177
|
+
if (autoScroll) {
|
|
178
|
+
await tick();
|
|
168
179
|
const li = document.querySelector(`ul.options > li.active`);
|
|
169
|
-
|
|
170
|
-
// downwards, we scroll to next sibling to make element fully visible
|
|
171
|
-
if (increment === 1)
|
|
172
|
-
li?.nextSibling?.scrollIntoViewIfNeeded();
|
|
173
|
-
else
|
|
174
|
-
li?.scrollIntoViewIfNeeded();
|
|
180
|
+
li?.scrollIntoViewIfNeeded();
|
|
175
181
|
}
|
|
176
182
|
}
|
|
183
|
+
// on backspace key: remove last selected option
|
|
177
184
|
else if (event.key === `Backspace`) {
|
|
178
185
|
const label = selectedLabels.pop();
|
|
179
186
|
if (label && !searchText)
|
|
@@ -198,22 +205,27 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
|
|
|
198
205
|
<!-- z-index: 2 when showOptions is true ensures the ul.selected of one <MultiSelect />
|
|
199
206
|
display above those of another following shortly after it -->
|
|
200
207
|
<div
|
|
201
|
-
class
|
|
202
|
-
class:
|
|
203
|
-
class:single={maxSelect == 1}
|
|
208
|
+
class:disabled
|
|
209
|
+
class:single={maxSelect === 1}
|
|
204
210
|
class:open={showOptions}
|
|
211
|
+
class="multiselect {outerDivClass}"
|
|
205
212
|
on:mouseup|stopPropagation={() => setOptionsVisible(true)}
|
|
206
|
-
|
|
207
|
-
|
|
213
|
+
on:focusout={() => {
|
|
214
|
+
setOptionsVisible(false)
|
|
215
|
+
dispatch(`blur`)
|
|
216
|
+
}}
|
|
217
|
+
title={disabled ? disabledTitle : null}
|
|
208
218
|
>
|
|
219
|
+
<!-- invisible input, used only to prevent form submission if required=true and no options selected -->
|
|
220
|
+
<input {required} bind:value={formValue} tabindex="-1" class="form-control" />
|
|
209
221
|
<ExpandIcon style="min-width: 1em; padding: 0 1pt;" />
|
|
210
222
|
<ul class="selected {ulSelectedClass}">
|
|
211
223
|
{#each selected as option, idx}
|
|
212
224
|
<li class={liSelectedClass}>
|
|
213
|
-
<slot name="
|
|
225
|
+
<slot name="selected" {option} {idx}>
|
|
214
226
|
{option.label}
|
|
215
227
|
</slot>
|
|
216
|
-
{#if !
|
|
228
|
+
{#if !disabled}
|
|
217
229
|
<button
|
|
218
230
|
on:mouseup|stopPropagation={() => remove(option.label)}
|
|
219
231
|
on:keydown={handleEnterAndSpaceKeys(() => remove(option.label))}
|
|
@@ -228,58 +240,83 @@ display above those of another following shortly after it -->
|
|
|
228
240
|
<li style="display: contents;">
|
|
229
241
|
<input
|
|
230
242
|
bind:this={input}
|
|
231
|
-
autocomplete
|
|
243
|
+
{autocomplete}
|
|
232
244
|
bind:value={searchText}
|
|
233
245
|
on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
|
|
234
246
|
on:keydown={handleKeydown}
|
|
235
247
|
on:focus={() => setOptionsVisible(true)}
|
|
248
|
+
on:blur={() => setOptionsVisible(false)}
|
|
236
249
|
{id}
|
|
237
250
|
{name}
|
|
251
|
+
{disabled}
|
|
238
252
|
placeholder={selectedLabels.length ? `` : placeholder}
|
|
239
253
|
/>
|
|
240
254
|
</li>
|
|
241
255
|
</ul>
|
|
242
|
-
{#if
|
|
243
|
-
<
|
|
256
|
+
{#if loading}
|
|
257
|
+
<slot name="spinner">
|
|
258
|
+
<CircleSpinner />
|
|
259
|
+
</slot>
|
|
260
|
+
{/if}
|
|
261
|
+
{#if disabled}
|
|
262
|
+
<slot name="disabled-icon">
|
|
263
|
+
<DisabledIcon height="14pt" />
|
|
264
|
+
</slot>
|
|
244
265
|
{:else if selected.length > 0}
|
|
245
|
-
{#if maxSelect
|
|
266
|
+
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
|
|
246
267
|
<Wiggle bind:wiggle angle={20}>
|
|
247
|
-
<span style="padding: 0 3pt;">
|
|
268
|
+
<span style="padding: 0 3pt;">
|
|
269
|
+
{maxSelectMsg?.(selected.length, maxSelect) ??
|
|
270
|
+
(maxSelect > 1 ? `${selected.length}/${maxSelect}` : ``)}
|
|
271
|
+
</span>
|
|
248
272
|
</Wiggle>
|
|
249
273
|
{/if}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
274
|
+
{#if maxSelect !== 1}
|
|
275
|
+
<button
|
|
276
|
+
type="button"
|
|
277
|
+
class="remove-all"
|
|
278
|
+
title={removeAllTitle}
|
|
279
|
+
on:mouseup|stopPropagation={removeAll}
|
|
280
|
+
on:keydown={handleEnterAndSpaceKeys(removeAll)}
|
|
281
|
+
>
|
|
282
|
+
<CrossIcon height="14pt" />
|
|
283
|
+
</button>
|
|
284
|
+
{/if}
|
|
259
285
|
{/if}
|
|
260
286
|
|
|
261
287
|
{#key showOptions}
|
|
262
288
|
<ul
|
|
263
|
-
class="options {ulOptionsClass}"
|
|
264
289
|
class:hidden={!showOptions}
|
|
290
|
+
class="options {ulOptionsClass}"
|
|
265
291
|
transition:fly|local={{ duration: 300, y: 40 }}
|
|
266
292
|
>
|
|
267
293
|
{#each matchingOptions as option, idx}
|
|
268
294
|
{@const { label, disabled, title = null, selectedTitle } = option}
|
|
269
295
|
{@const { disabledTitle = defaultDisabledTitle } = option}
|
|
296
|
+
{@const active = activeOption?.label === label}
|
|
270
297
|
<li
|
|
271
298
|
on:mouseup|preventDefault|stopPropagation
|
|
272
299
|
on:mousedown|preventDefault|stopPropagation={() => {
|
|
273
300
|
if (disabled) return
|
|
274
301
|
isSelected(label) ? remove(label) : add(label)
|
|
275
302
|
}}
|
|
303
|
+
title={disabled ? disabledTitle : (isSelected(label) && selectedTitle) || title}
|
|
276
304
|
class:selected={isSelected(label)}
|
|
277
|
-
class:active
|
|
305
|
+
class:active
|
|
278
306
|
class:disabled
|
|
279
|
-
|
|
280
|
-
|
|
307
|
+
class="{liOptionClass} {active ? liActiveOptionClass : ``}"
|
|
308
|
+
on:mouseover={() => {
|
|
309
|
+
if (disabled) return
|
|
310
|
+
activeOption = option
|
|
311
|
+
}}
|
|
312
|
+
on:focus={() => {
|
|
313
|
+
if (disabled) return
|
|
314
|
+
activeOption = option
|
|
315
|
+
}}
|
|
316
|
+
on:mouseout={() => (activeOption = null)}
|
|
317
|
+
on:blur={() => (activeOption = null)}
|
|
281
318
|
>
|
|
282
|
-
<slot name="
|
|
319
|
+
<slot name="option" {option} {idx}>
|
|
283
320
|
{option.label}
|
|
284
321
|
</slot>
|
|
285
322
|
</li>
|
|
@@ -299,9 +336,11 @@ display above those of another following shortly after it -->
|
|
|
299
336
|
cursor: text;
|
|
300
337
|
padding: 0 3pt;
|
|
301
338
|
border: var(--sms-border, 1pt solid lightgray);
|
|
302
|
-
border-radius: var(--sms-border-radius,
|
|
339
|
+
border-radius: var(--sms-border-radius, 3pt);
|
|
303
340
|
background: var(--sms-input-bg);
|
|
304
341
|
min-height: var(--sms-input-min-height, 22pt);
|
|
342
|
+
color: var(--sms-text-color);
|
|
343
|
+
font-size: var(--sms-font-size, inherit);
|
|
305
344
|
}
|
|
306
345
|
:where(div.multiselect.open) {
|
|
307
346
|
z-index: var(--sms-open-z-index, 4);
|
|
@@ -309,8 +348,9 @@ display above those of another following shortly after it -->
|
|
|
309
348
|
:where(div.multiselect:focus-within) {
|
|
310
349
|
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
|
|
311
350
|
}
|
|
312
|
-
:where(div.multiselect.
|
|
313
|
-
background: var(--sms-
|
|
351
|
+
:where(div.multiselect.disabled) {
|
|
352
|
+
background: var(--sms-disabled-bg, lightgray);
|
|
353
|
+
cursor: not-allowed;
|
|
314
354
|
}
|
|
315
355
|
|
|
316
356
|
:where(div.multiselect > ul.selected) {
|
|
@@ -326,26 +366,24 @@ display above those of another following shortly after it -->
|
|
|
326
366
|
display: flex;
|
|
327
367
|
margin: 2pt;
|
|
328
368
|
line-height: normal;
|
|
329
|
-
padding: 1pt
|
|
369
|
+
padding: 1pt 5pt;
|
|
330
370
|
transition: 0.3s;
|
|
331
371
|
white-space: nowrap;
|
|
332
|
-
background: var(--sms-selected-bg,
|
|
372
|
+
background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15));
|
|
333
373
|
height: var(--sms-selected-li-height);
|
|
374
|
+
color: var(--sms-selected-text-color, var(--sms-text-color));
|
|
334
375
|
}
|
|
335
|
-
:where(div.multiselect
|
|
336
|
-
align-items: center;
|
|
376
|
+
:where(div.multiselect button) {
|
|
337
377
|
border-radius: 50%;
|
|
338
378
|
display: flex;
|
|
339
|
-
cursor: pointer;
|
|
340
379
|
transition: 0.2s;
|
|
341
|
-
}
|
|
342
|
-
:where(div.multiselect button) {
|
|
343
380
|
color: inherit;
|
|
344
381
|
background: transparent;
|
|
345
382
|
border: none;
|
|
346
383
|
cursor: pointer;
|
|
347
384
|
outline: none;
|
|
348
|
-
padding: 0
|
|
385
|
+
padding: 0;
|
|
386
|
+
margin: 0 0 0 4pt; /* CSS reset */
|
|
349
387
|
}
|
|
350
388
|
:where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
|
|
351
389
|
color: var(--sms-remove-x-hover-focus-color, lightskyblue);
|
|
@@ -354,29 +392,44 @@ display above those of another following shortly after it -->
|
|
|
354
392
|
transform: scale(1.04);
|
|
355
393
|
}
|
|
356
394
|
|
|
395
|
+
:where(div.multiselect input) {
|
|
396
|
+
margin: auto 0; /* CSS reset */
|
|
397
|
+
padding: 0; /* CSS reset */
|
|
398
|
+
}
|
|
357
399
|
:where(div.multiselect > ul.selected > li > input) {
|
|
358
400
|
border: none;
|
|
359
401
|
outline: none;
|
|
360
402
|
background: none;
|
|
361
403
|
flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
|
|
362
404
|
min-width: 2em;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
405
|
+
color: inherit;
|
|
406
|
+
font-size: inherit;
|
|
407
|
+
cursor: inherit; /* needed for disabled state */
|
|
408
|
+
}
|
|
409
|
+
:where(div.multiselect > input.form-control) {
|
|
410
|
+
width: 2em;
|
|
411
|
+
position: absolute;
|
|
412
|
+
background: transparent;
|
|
413
|
+
border: none;
|
|
414
|
+
outline: none;
|
|
415
|
+
z-index: -1;
|
|
416
|
+
opacity: 0;
|
|
417
|
+
pointer-events: none;
|
|
367
418
|
}
|
|
368
419
|
|
|
369
420
|
:where(div.multiselect > ul.options) {
|
|
370
421
|
list-style: none;
|
|
371
|
-
max-height: 50vh;
|
|
372
422
|
padding: 0;
|
|
373
423
|
top: 100%;
|
|
424
|
+
left: 0;
|
|
374
425
|
width: 100%;
|
|
375
426
|
position: absolute;
|
|
376
427
|
border-radius: 1ex;
|
|
377
428
|
overflow: auto;
|
|
378
429
|
background: var(--sms-options-bg, white);
|
|
430
|
+
max-height: var(--sms-options-max-height, 50vh);
|
|
379
431
|
overscroll-behavior: var(--sms-options-overscroll, none);
|
|
432
|
+
box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
|
|
380
433
|
}
|
|
381
434
|
:where(div.multiselect > ul.options.hidden) {
|
|
382
435
|
visibility: hidden;
|
|
@@ -384,34 +437,22 @@ display above those of another following shortly after it -->
|
|
|
384
437
|
:where(div.multiselect > ul.options > li) {
|
|
385
438
|
padding: 3pt 2ex;
|
|
386
439
|
cursor: pointer;
|
|
440
|
+
scroll-margin: var(--sms-options-scroll-margin, 100px);
|
|
387
441
|
}
|
|
388
442
|
/* for noOptionsMsg */
|
|
389
443
|
:where(div.multiselect > ul.options span) {
|
|
390
444
|
padding: 3pt 2ex;
|
|
391
445
|
}
|
|
392
446
|
:where(div.multiselect > ul.options > li.selected) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
3pt solid var(--sms-selected-color, green)
|
|
396
|
-
);
|
|
397
|
-
background: var(--sms-li-selected-bg, inherit);
|
|
398
|
-
color: var(--sms-li-selected-color, inherit);
|
|
399
|
-
}
|
|
400
|
-
:where(div.multiselect > ul.options > li:not(.selected):hover) {
|
|
401
|
-
border-left: var(
|
|
402
|
-
--sms-li-not-selected-hover-border-left,
|
|
403
|
-
3pt solid var(--sms-active-color, cornflowerblue)
|
|
404
|
-
);
|
|
447
|
+
background: var(--sms-li-selected-bg);
|
|
448
|
+
color: var(--sms-li-selected-color);
|
|
405
449
|
}
|
|
406
450
|
:where(div.multiselect > ul.options > li.active) {
|
|
407
|
-
background: var(--sms-li-active-bg, var(--sms-active-color,
|
|
451
|
+
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
|
408
452
|
}
|
|
409
453
|
:where(div.multiselect > ul.options > li.disabled) {
|
|
410
454
|
cursor: not-allowed;
|
|
411
455
|
background: var(--sms-li-disabled-bg, #f5f5f6);
|
|
412
456
|
color: var(--sms-li-disabled-text, #b8b8b8);
|
|
413
457
|
}
|
|
414
|
-
:where(div.multiselect > ul.options > li.disabled:hover) {
|
|
415
|
-
border-left: unset;
|
|
416
|
-
}
|
|
417
458
|
</style>
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -5,9 +5,12 @@ declare const __propDef: {
|
|
|
5
5
|
selected?: Option[] | undefined;
|
|
6
6
|
selectedLabels?: Primitive[] | undefined;
|
|
7
7
|
selectedValues?: Primitive[] | undefined;
|
|
8
|
+
searchText?: string | undefined;
|
|
9
|
+
showOptions?: boolean | undefined;
|
|
8
10
|
maxSelect?: number | null | undefined;
|
|
9
|
-
maxSelectMsg?: ((current: number, max: number) => string) | undefined;
|
|
10
|
-
|
|
11
|
+
maxSelectMsg?: ((current: number, max: number) => string) | null | undefined;
|
|
12
|
+
disabled?: boolean | undefined;
|
|
13
|
+
disabledTitle?: string | undefined;
|
|
11
14
|
options: ProtoOption[];
|
|
12
15
|
input?: HTMLInputElement | null | undefined;
|
|
13
16
|
placeholder?: string | undefined;
|
|
@@ -21,9 +24,15 @@ declare const __propDef: {
|
|
|
21
24
|
liSelectedClass?: string | undefined;
|
|
22
25
|
ulOptionsClass?: string | undefined;
|
|
23
26
|
liOptionClass?: string | undefined;
|
|
27
|
+
liActiveOptionClass?: string | undefined;
|
|
24
28
|
removeBtnTitle?: string | undefined;
|
|
25
29
|
removeAllTitle?: string | undefined;
|
|
26
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;
|
|
27
36
|
};
|
|
28
37
|
events: {
|
|
29
38
|
mouseup: MouseEvent;
|
|
@@ -31,11 +40,13 @@ declare const __propDef: {
|
|
|
31
40
|
[evt: string]: CustomEvent<any>;
|
|
32
41
|
};
|
|
33
42
|
slots: {
|
|
34
|
-
|
|
43
|
+
selected: {
|
|
35
44
|
option: Option;
|
|
36
45
|
idx: any;
|
|
37
46
|
};
|
|
38
|
-
|
|
47
|
+
spinner: {};
|
|
48
|
+
'disabled-icon': {};
|
|
49
|
+
option: {
|
|
39
50
|
option: Option;
|
|
40
51
|
idx: any;
|
|
41
52
|
};
|
|
File without changes
|
|
@@ -10,9 +10,9 @@ declare const __propDef: {
|
|
|
10
10
|
};
|
|
11
11
|
slots: {};
|
|
12
12
|
};
|
|
13
|
-
export declare type
|
|
14
|
-
export declare type
|
|
15
|
-
export declare type
|
|
16
|
-
export default class
|
|
13
|
+
export declare type DisabledProps = typeof __propDef.props;
|
|
14
|
+
export declare type DisabledEvents = typeof __propDef.events;
|
|
15
|
+
export declare type DisabledSlots = typeof __propDef.slots;
|
|
16
|
+
export default class Disabled extends SvelteComponentTyped<DisabledProps, DisabledEvents, DisabledSlots> {
|
|
17
17
|
}
|
|
18
18
|
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,20 @@ 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
|
+
blur: undefined;
|
|
32
|
+
};
|
package/package.json
CHANGED
|
@@ -5,32 +5,37 @@
|
|
|
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.1",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"svelte": "index.js",
|
|
11
11
|
"bugs": "https://github.com/janosh/svelte-multiselect/issues",
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@sveltejs/adapter-static": "^1.0.0-next.28",
|
|
14
|
-
"@sveltejs/kit": "^1.0.0-next.
|
|
15
|
-
"@
|
|
14
|
+
"@sveltejs/kit": "^1.0.0-next.287",
|
|
15
|
+
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.38",
|
|
16
|
+
"@testing-library/svelte": "^3.0.3",
|
|
17
|
+
"@typescript-eslint/eslint-plugin": "^5.12.1",
|
|
16
18
|
"@typescript-eslint/parser": "^5.12.0",
|
|
19
|
+
"@vitest/ui": "^0.5.7",
|
|
17
20
|
"eslint": "^8.9.0",
|
|
18
|
-
"eslint-plugin-svelte3": "^3.4.
|
|
21
|
+
"eslint-plugin-svelte3": "^3.4.1",
|
|
19
22
|
"hastscript": "^7.0.2",
|
|
23
|
+
"jsdom": "^19.0.0",
|
|
20
24
|
"mdsvex": "^0.10.5",
|
|
21
25
|
"prettier": "^2.5.1",
|
|
22
26
|
"prettier-plugin-svelte": "^2.6.0",
|
|
23
27
|
"rehype-autolink-headings": "^6.1.1",
|
|
24
28
|
"rehype-slug": "^5.0.1",
|
|
25
29
|
"svelte": "^3.46.4",
|
|
26
|
-
"svelte-check": "^2.4.
|
|
30
|
+
"svelte-check": "^2.4.5",
|
|
27
31
|
"svelte-github-corner": "^0.1.0",
|
|
28
|
-
"svelte-preprocess": "^4.10.
|
|
29
|
-
"svelte-toc": "^0.2.
|
|
30
|
-
"svelte2tsx": "^0.5.
|
|
32
|
+
"svelte-preprocess": "^4.10.4",
|
|
33
|
+
"svelte-toc": "^0.2.6",
|
|
34
|
+
"svelte2tsx": "^0.5.5",
|
|
31
35
|
"tslib": "^2.3.1",
|
|
32
36
|
"typescript": "^4.5.5",
|
|
33
|
-
"vite": "^2.8.
|
|
37
|
+
"vite": "^2.8.4",
|
|
38
|
+
"vitest": "^0.5.7"
|
|
34
39
|
},
|
|
35
40
|
"keywords": [
|
|
36
41
|
"svelte",
|
package/readme.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
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)
|
|
@@ -35,16 +36,22 @@
|
|
|
35
36
|
|
|
36
37
|
## Recent breaking changes
|
|
37
38
|
|
|
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
39
|
- v3.0.0 changed the `event.detail` payload for `'add'`, `'remove'` and `'change'` events from `token` to `option`, e.g.
|
|
40
40
|
|
|
41
41
|
```js
|
|
42
|
-
on:add={(e) => console.log(e.detail.token.label)} // v2
|
|
43
|
-
on:add={(e) => console.log(e.detail.option.label)} // v3
|
|
42
|
+
on:add={(e) => console.log(e.detail.token.label)} // v2
|
|
43
|
+
on:add={(e) => console.log(e.detail.option.label)} // v3
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
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
47
|
|
|
48
|
+
- v4.0.0 renamed the slots for customizing how selected options and dropdown list items are rendered:
|
|
49
|
+
|
|
50
|
+
- old: `<slot name="renderOptions" />`, new: `<slot name="option" />`
|
|
51
|
+
- old: `<slot name="renderSelected" />`, new: `<slot name="selected" />`
|
|
52
|
+
|
|
53
|
+
- 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}`.
|
|
54
|
+
|
|
48
55
|
## Installation
|
|
49
56
|
|
|
50
57
|
```sh
|
|
@@ -71,7 +78,7 @@ yarn add -D svelte-multiselect
|
|
|
71
78
|
`Spring`,
|
|
72
79
|
]
|
|
73
80
|
|
|
74
|
-
let selected
|
|
81
|
+
let selected = []
|
|
75
82
|
</script>
|
|
76
83
|
|
|
77
84
|
Favorite Web Frameworks?
|
|
@@ -88,19 +95,31 @@ Full list of props/bindable variables for this component:
|
|
|
88
95
|
<div class="table">
|
|
89
96
|
|
|
90
97
|
<!-- prettier-ignore -->
|
|
91
|
-
| name
|
|
92
|
-
|
|
|
93
|
-
| `options`
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
98
|
+
| name | default | description |
|
|
99
|
+
| :--------------------- | :-------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
100
|
+
| `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. |
|
|
101
|
+
| `showOptions` | `false` | Bindable boolean that controls whether the options dropdown is visible. |
|
|
102
|
+
| `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. |
|
|
103
|
+
| `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
|
|
104
|
+
| `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
|
|
105
|
+
| `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
|
|
106
|
+
| `selectedLabels` | `[]` | Labels of currently selected options. |
|
|
107
|
+
| `selectedValues` | `[]` | Values of currently selected options. |
|
|
108
|
+
| `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. |
|
|
109
|
+
| `disabled` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
|
|
110
|
+
| `disabledTitle` | `This field is disabled` | Tooltip text to display on hover when the component is in `disabled` state. |
|
|
111
|
+
| `placeholder` | `undefined` | String shown in the text input when no option is selected. |
|
|
112
|
+
| `input` | `undefined` | Handle to the `<input>` DOM node. |
|
|
113
|
+
| `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. |
|
|
114
|
+
| `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>`. |
|
|
115
|
+
| `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. |
|
|
116
|
+
| `autoScroll` | `true` | `false` disables keeping the active dropdown items in view when going up/down the list of options with arrow keys. |
|
|
117
|
+
| `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. |
|
|
118
|
+
| `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. |
|
|
119
|
+
| `removeBtnTitle` | `'Remove'` | Title text to display when user hovers over button (cross icon) to remove selected option. |
|
|
120
|
+
| `removeAllTitle` | `'Remove all'` | Title text to display when user hovers over remove-all button. |
|
|
121
|
+
| `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. |
|
|
122
|
+
| `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. |
|
|
104
123
|
|
|
105
124
|
</div>
|
|
106
125
|
|
|
@@ -115,7 +134,7 @@ Full list of props/bindable variables for this component:
|
|
|
115
134
|
}
|
|
116
135
|
```
|
|
117
136
|
|
|
118
|
-
2. `maxSelectMsg = (current: number, max: number) => string`: Inform
|
|
137
|
+
2. `maxSelectMsg = (current: number, max: number) => string`: 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:
|
|
119
138
|
|
|
120
139
|
```ts
|
|
121
140
|
maxSelectMsg = (current: number, max: number) => `${current}/${max}`
|
|
@@ -123,24 +142,28 @@ Full list of props/bindable variables for this component:
|
|
|
123
142
|
|
|
124
143
|
## Slots
|
|
125
144
|
|
|
126
|
-
`MultiSelect.svelte`
|
|
145
|
+
`MultiSelect.svelte` has 3 named slots:
|
|
127
146
|
|
|
128
|
-
- `slot="
|
|
129
|
-
- `slot="
|
|
147
|
+
- `slot="option"`: Customize rendering of dropdown options. Receives as props the `option` object and the zero-indexed position (`idx`) it has in the dropdown.
|
|
148
|
+
- `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.
|
|
149
|
+
- `slot="spinner"`: Custom spinner component to display when in `loading` state. Receives no props.
|
|
150
|
+
- `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.
|
|
130
151
|
|
|
131
|
-
|
|
152
|
+
Example:
|
|
132
153
|
|
|
133
154
|
```svelte
|
|
134
155
|
<MultiSelect options={[`Banana`, `Apple`, `Mango`]}>
|
|
135
|
-
<span let:idx let:option slot="
|
|
156
|
+
<span let:idx let:option slot="option">
|
|
136
157
|
{idx + 1}. {option.label}
|
|
137
158
|
{option.label === `Mango` ? `🎉` : ``}
|
|
138
159
|
</span>
|
|
139
160
|
|
|
140
|
-
<span let:idx let:option slot="
|
|
161
|
+
<span let:idx let:option slot="selected">
|
|
141
162
|
#{idx + 1}
|
|
142
163
|
{option.label}
|
|
143
164
|
</span>
|
|
165
|
+
|
|
166
|
+
<CustomSpinner slot="spinner">
|
|
144
167
|
</MultiSelect>
|
|
145
168
|
```
|
|
146
169
|
|
|
@@ -212,33 +235,34 @@ If you only want to make small adjustments, you can pass the following CSS varia
|
|
|
212
235
|
|
|
213
236
|
- `div.multiselect`
|
|
214
237
|
- `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.
|
|
215
|
-
- `border-radius: var(--sms-border-radius,
|
|
238
|
+
- `border-radius: var(--sms-border-radius, 3pt)`
|
|
216
239
|
- `background: var(--sms-input-bg)`
|
|
217
240
|
- `height: var(--sms-input-height, 2em)`
|
|
241
|
+
- `color: var(--sms-text-color)`
|
|
218
242
|
- `div.multiselect.open`
|
|
219
243
|
- `z-index: var(--sms-open-z-index, 4)`: Increase this if needed to ensure the dropdown list is displayed atop all other page elements.
|
|
220
244
|
- `div.multiselect:focus-within`
|
|
221
245
|
- `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`.
|
|
222
|
-
- `div.multiselect.
|
|
223
|
-
- `background: var(--sms-
|
|
224
|
-
- `div.multiselect > ul.selected > li > input`
|
|
225
|
-
- `color: var(--sms-text-color, inherit)`: Input text color.
|
|
246
|
+
- `div.multiselect.disabled`
|
|
247
|
+
- `background: var(--sms-disabled-bg, lightgray)`: Background when in disabled state.
|
|
226
248
|
- `div.multiselect > ul.selected > li`
|
|
227
|
-
- `background: var(--sms-selected-bg,
|
|
249
|
+
- `background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15))`: Background of selected options.
|
|
228
250
|
- `height: var(--sms-selected-li-height)`: Height of selected options.
|
|
251
|
+
- `color: var(--sms-selected-text-color, var(--sms-text-color))`: Text color for selected options.
|
|
229
252
|
- `ul.selected > li button:hover, button.remove-all:hover, button:focus`
|
|
230
253
|
- `color: var(--sms-remove-x-hover-focus-color, lightskyblue)`: Color of the cross-icon buttons for removing all or individual selected options when in `:focus` or `:hover` state.
|
|
231
254
|
- `div.multiselect > ul.options`
|
|
232
255
|
- `background: var(--sms-options-bg, white)`: Background of dropdown list.
|
|
256
|
+
- `max-height: var(--sms-options-max-height, 50vh)`: Maximum height of options dropdown.
|
|
233
257
|
- `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).
|
|
258
|
+
- `box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);`: Box shadow of dropdown list.
|
|
259
|
+
- `div.multiselect > ul.options > li`
|
|
260
|
+
- `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.
|
|
234
261
|
- `div.multiselect > ul.options > li.selected`
|
|
235
|
-
- `
|
|
236
|
-
- `
|
|
237
|
-
- `color: var(--sms-li-selected-color, inherit)`: Text color of selected list items in options pane.
|
|
238
|
-
- `div.multiselect > ul.options > li:not(.selected):hover`
|
|
239
|
-
- `border-left: var(--sms-li-not-selected-hover-border-left, 3pt solid var(--sms-active-color, cornflowerblue))`
|
|
262
|
+
- `background: var(--sms-li-selected-bg)`: Background of selected list items in options pane.
|
|
263
|
+
- `color: var(--sms-li-selected-color)`: Text color of selected list items in options pane.
|
|
240
264
|
- `div.multiselect > ul.options > li.active`
|
|
241
|
-
- `background: var(--sms-li-active-bg, var(--sms-active-color,
|
|
265
|
+
- `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.
|
|
242
266
|
- `div.multiselect > ul.options > li.disabled`
|
|
243
267
|
- `background: var(--sms-li-disabled-bg, #f5f5f6)`: Background of disabled options in the dropdown list.
|
|
244
268
|
- `color: var(--sms-li-disabled-text, #b8b8b8)`: Text color of disabled option in the dropdown list.
|
|
@@ -258,6 +282,7 @@ The second method allows you to pass in custom classes to the important DOM elem
|
|
|
258
282
|
- `liSelectedClass`
|
|
259
283
|
- `ulOptionsClass`
|
|
260
284
|
- `liOptionClass`
|
|
285
|
+
- `liActiveOptionClass`
|
|
261
286
|
|
|
262
287
|
This simplified version of the DOM structure of this component shows where these classes are inserted:
|
|
263
288
|
|
|
@@ -269,7 +294,9 @@ This simplified version of the DOM structure of this component shows where these
|
|
|
269
294
|
</ul>
|
|
270
295
|
<ul class="options {ulOptionsClass}">
|
|
271
296
|
<li class={liOptionClass}>Option 1</li>
|
|
272
|
-
<li class={liOptionClass}
|
|
297
|
+
<li class="{liOptionClass} {liActiveOptionClass}">
|
|
298
|
+
Option 2 (currently active)
|
|
299
|
+
</li>
|
|
273
300
|
</ul>
|
|
274
301
|
</div>
|
|
275
302
|
```
|
|
@@ -285,8 +312,8 @@ You can alternatively style every part of this component with more fine-grained
|
|
|
285
312
|
:global(div.multiselect.open) {
|
|
286
313
|
/* top-level wrapper div when dropdown open */
|
|
287
314
|
}
|
|
288
|
-
:global(div.multiselect.
|
|
289
|
-
/* top-level wrapper div when in
|
|
315
|
+
:global(div.multiselect.disabled) {
|
|
316
|
+
/* top-level wrapper div when in disabled state */
|
|
290
317
|
}
|
|
291
318
|
:global(div.multiselect > ul.selected) {
|
|
292
319
|
/* selected list */
|
|
@@ -324,6 +351,40 @@ You can alternatively style every part of this component with more fine-grained
|
|
|
324
351
|
}
|
|
325
352
|
```
|
|
326
353
|
|
|
354
|
+
## Downstream testing
|
|
355
|
+
|
|
356
|
+
To test a Svelte component which imports `svelte-multiselect`, you need to configure your test runner to avoid [transpiling issues](https://github.com/EmilTholin/svelte-routing/issues/140#issuecomment-661682571).
|
|
357
|
+
|
|
358
|
+
For Jest, exclude `svelte-multiselect` from `transformIgnorePatterns` in your `jest.config.json`:
|
|
359
|
+
|
|
360
|
+
```json
|
|
361
|
+
{
|
|
362
|
+
"transformIgnorePatterns": ["node_modules/?!(svelte-multiselect)"],
|
|
363
|
+
"transform": {
|
|
364
|
+
"^.+\\.[t|j]s?$": "esbuild-jest",
|
|
365
|
+
"^.+\\.svelte$": ["svelte-jester", { "preprocess": true }]
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
For Vitest, include `svelte-multiselect` in `deps.inline`:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
// vite.config.ts
|
|
374
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
|
375
|
+
|
|
376
|
+
export default {
|
|
377
|
+
plugins: [svelte({ hot: !process.env.VITEST })],
|
|
378
|
+
test: {
|
|
379
|
+
deps: {
|
|
380
|
+
inline: [/svelte-multiselect/],
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
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).
|
|
387
|
+
|
|
327
388
|
## Want to contribute?
|
|
328
389
|
|
|
329
390
|
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
|
-
}
|