svelte-multiselect 1.2.2 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MultiSelect.svelte +194 -101
- package/MultiSelect.svelte.d.ts +12 -6
- package/actions.d.ts +3 -0
- package/actions.js +39 -0
- package/icons/ChevronExpand.svelte +2 -1
- package/icons/Cross.svelte +2 -1
- package/icons/ReadOnly.svelte +2 -1
- package/icons/index.d.ts +3 -0
- package/icons/index.js +3 -0
- package/index.d.ts +14 -0
- package/package.json +18 -18
- package/readme.md +69 -47
package/MultiSelect.svelte
CHANGED
|
@@ -1,67 +1,113 @@
|
|
|
1
|
-
<script >import { createEventDispatcher } from 'svelte';
|
|
1
|
+
<script >import { createEventDispatcher, onMount } from 'svelte';
|
|
2
2
|
import { fly } from 'svelte/transition';
|
|
3
|
-
import
|
|
4
|
-
import ExpandIcon from './icons
|
|
5
|
-
|
|
6
|
-
export let
|
|
3
|
+
import { onClickOutside } from './actions';
|
|
4
|
+
import { CrossIcon, ExpandIcon, ReadOnlyIcon } from './icons';
|
|
5
|
+
export let selected = [];
|
|
6
|
+
export let selectedLabels = [];
|
|
7
|
+
export let selectedValues = [];
|
|
7
8
|
export let maxSelect = null; // null means any number of options are selectable
|
|
9
|
+
export let maxSelectMsg = (current, max) => `${current}/${max}`;
|
|
8
10
|
export let readonly = false;
|
|
9
|
-
export let placeholder = ``;
|
|
10
11
|
export let options;
|
|
11
|
-
export let disabledOptions = [];
|
|
12
12
|
export let input = null;
|
|
13
|
-
export let
|
|
13
|
+
export let placeholder = undefined;
|
|
14
|
+
export let name = undefined;
|
|
15
|
+
export let id = undefined;
|
|
14
16
|
export let noOptionsMsg = `No matching options`;
|
|
17
|
+
export let activeOption = null;
|
|
15
18
|
export let outerDivClass = ``;
|
|
16
|
-
export let
|
|
17
|
-
export let
|
|
19
|
+
export let ulSelectedClass = ``;
|
|
20
|
+
export let liSelectedClass = ``;
|
|
18
21
|
export let ulOptionsClass = ``;
|
|
19
22
|
export let liOptionClass = ``;
|
|
20
23
|
export let removeBtnTitle = `Remove`;
|
|
21
24
|
export let removeAllTitle = `Remove all`;
|
|
25
|
+
// https://github.com/sveltejs/svelte/issues/6964
|
|
26
|
+
export let defaultDisabledTitle = `This option is disabled`;
|
|
22
27
|
if (maxSelect !== null && maxSelect < 0) {
|
|
23
|
-
|
|
28
|
+
console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
24
29
|
}
|
|
25
|
-
$: single = maxSelect === 1;
|
|
26
|
-
if (!selected)
|
|
27
|
-
selected = single ? `` : [];
|
|
28
30
|
if (!(options?.length > 0))
|
|
29
31
|
console.error(`MultiSelect missing options`);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
if (!Array.isArray(selected))
|
|
33
|
+
console.error(`selected prop must be an array`);
|
|
34
|
+
onMount(() => {
|
|
35
|
+
selected = _options.filter((op) => op?.preselected);
|
|
36
|
+
});
|
|
37
|
+
function isObject(item) {
|
|
38
|
+
return typeof item === `object` && !Array.isArray(item) && item !== null;
|
|
33
39
|
}
|
|
40
|
+
// process proto options to full ones with mandatory labels
|
|
41
|
+
$: _options = options.map((rawOp) => {
|
|
42
|
+
// convert to objects internally if user passed list of strings or numbers as options
|
|
43
|
+
if (isObject(rawOp)) {
|
|
44
|
+
const op = { ...rawOp };
|
|
45
|
+
if (!op.value)
|
|
46
|
+
op.value = op.label;
|
|
47
|
+
return op;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
if (![`string`, `number`].includes(typeof rawOp)) {
|
|
51
|
+
console.error(`MultiSelect options must be objects, strings or numbers, got ${typeof rawOp}`);
|
|
52
|
+
}
|
|
53
|
+
// even if we logged error above, try to proceed hoping user knows what they're doing
|
|
54
|
+
return { label: rawOp, value: rawOp };
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
$: labels = _options.map((op) => op.label);
|
|
58
|
+
$: if (new Set(labels).size !== options.length) {
|
|
59
|
+
console.error(`Option labels must be unique. Duplicates found: ${labels.filter((label, idx) => labels.indexOf(label) !== idx)}`);
|
|
60
|
+
}
|
|
61
|
+
$: selectedLabels = selected.map((op) => op.label);
|
|
62
|
+
$: selectedValues = selected.map((op) => op.value);
|
|
34
63
|
const dispatch = createEventDispatcher();
|
|
35
|
-
let
|
|
64
|
+
let searchText = ``;
|
|
36
65
|
let showOptions = false;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
66
|
+
// options matching the current search text
|
|
67
|
+
$: matchingOptions = _options.filter((op) => {
|
|
68
|
+
if (!searchText)
|
|
69
|
+
return true;
|
|
70
|
+
return `${op.label}`.toLowerCase().includes(searchText.toLowerCase());
|
|
71
|
+
});
|
|
72
|
+
$: matchingEnabledOptions = matchingOptions.filter((op) => !op.disabled);
|
|
73
|
+
$: if (
|
|
74
|
+
// if there was an active option but it's not in the filtered list of options
|
|
75
|
+
(activeOption &&
|
|
76
|
+
!matchingEnabledOptions.map((op) => op.label).includes(activeOption.label)) ||
|
|
77
|
+
// or there's no active option but the user entered search text
|
|
41
78
|
(!activeOption && searchText))
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
// make the first filtered option active
|
|
80
|
+
activeOption = matchingEnabledOptions[0];
|
|
81
|
+
function add(label) {
|
|
44
82
|
if (!readonly &&
|
|
45
|
-
!
|
|
46
|
-
//
|
|
47
|
-
(maxSelect
|
|
83
|
+
!selectedLabels.includes(label) &&
|
|
84
|
+
// for maxselect = 1 we always replace current option with new selection
|
|
85
|
+
(maxSelect == null || maxSelect == 1 || selected.length < maxSelect)) {
|
|
48
86
|
searchText = ``; // reset search string on selection
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
input?.blur();
|
|
87
|
+
const option = _options.find((op) => op.label === label);
|
|
88
|
+
if (!option) {
|
|
89
|
+
console.error(`MultiSelect: option with label ${label} not found`);
|
|
90
|
+
return;
|
|
54
91
|
}
|
|
55
|
-
|
|
56
|
-
|
|
92
|
+
if (maxSelect === 1) {
|
|
93
|
+
selected = [option];
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
selected = [option, ...selected];
|
|
97
|
+
}
|
|
98
|
+
if (selected.length === maxSelect)
|
|
99
|
+
setOptionsVisible(false);
|
|
100
|
+
dispatch(`add`, { option });
|
|
101
|
+
dispatch(`change`, { option, type: `add` });
|
|
57
102
|
}
|
|
58
103
|
}
|
|
59
|
-
function remove(
|
|
60
|
-
if (
|
|
104
|
+
function remove(label) {
|
|
105
|
+
if (selected.length === 0 || readonly)
|
|
61
106
|
return;
|
|
62
|
-
selected = selected.filter((
|
|
63
|
-
|
|
64
|
-
dispatch(`
|
|
107
|
+
selected = selected.filter((option) => label !== option.label);
|
|
108
|
+
const option = _options.find((option) => option.label === label);
|
|
109
|
+
dispatch(`remove`, { option });
|
|
110
|
+
dispatch(`change`, { option, type: `remove` });
|
|
65
111
|
}
|
|
66
112
|
function setOptionsVisible(show) {
|
|
67
113
|
// nothing to do if visibility is already as intended
|
|
@@ -70,83 +116,115 @@ function setOptionsVisible(show) {
|
|
|
70
116
|
showOptions = show;
|
|
71
117
|
if (show)
|
|
72
118
|
input?.focus();
|
|
119
|
+
else {
|
|
120
|
+
input?.blur();
|
|
121
|
+
activeOption = null;
|
|
122
|
+
}
|
|
73
123
|
}
|
|
124
|
+
// handle all keyboard events this component receives
|
|
74
125
|
function handleKeydown(event) {
|
|
126
|
+
// on escape: dismiss options dropdown and reset search text
|
|
75
127
|
if (event.key === `Escape`) {
|
|
76
128
|
setOptionsVisible(false);
|
|
77
129
|
searchText = ``;
|
|
78
130
|
}
|
|
131
|
+
// on enter key: toggle active option and reset search text
|
|
79
132
|
else if (event.key === `Enter`) {
|
|
80
133
|
if (activeOption) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
selected.includes(activeOption) ? remove(activeOption) : add(activeOption);
|
|
134
|
+
const { label } = activeOption;
|
|
135
|
+
selectedLabels.includes(label) ? remove(label) : add(label);
|
|
84
136
|
searchText = ``;
|
|
85
|
-
} // no active option means the options
|
|
137
|
+
} // no active option means the options dropdown is closed in which case enter means open it
|
|
86
138
|
else
|
|
87
139
|
setOptionsVisible(true);
|
|
88
140
|
}
|
|
141
|
+
// on up/down arrow keys: update active option
|
|
89
142
|
else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
|
|
143
|
+
if (activeOption === null) {
|
|
144
|
+
// if no option is active yet, make first one active
|
|
145
|
+
activeOption = matchingEnabledOptions[0];
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
90
148
|
const increment = event.key === `ArrowUp` ? -1 : 1;
|
|
91
|
-
const newActiveIdx =
|
|
149
|
+
const newActiveIdx = matchingEnabledOptions.indexOf(activeOption) + increment;
|
|
150
|
+
const ulOps = document.querySelector(`ul.options`);
|
|
92
151
|
if (newActiveIdx < 0) {
|
|
93
|
-
|
|
152
|
+
// wrap around top
|
|
153
|
+
activeOption = matchingEnabledOptions[matchingEnabledOptions.length - 1];
|
|
154
|
+
if (ulOps)
|
|
155
|
+
ulOps.scrollTop = ulOps.scrollHeight;
|
|
156
|
+
}
|
|
157
|
+
else if (newActiveIdx === matchingEnabledOptions.length) {
|
|
158
|
+
// wrap around bottom
|
|
159
|
+
activeOption = matchingEnabledOptions[0];
|
|
160
|
+
if (ulOps)
|
|
161
|
+
ulOps.scrollTop = 0;
|
|
94
162
|
}
|
|
95
163
|
else {
|
|
96
|
-
|
|
97
|
-
|
|
164
|
+
// default case
|
|
165
|
+
activeOption = matchingEnabledOptions[newActiveIdx];
|
|
166
|
+
const li = document.querySelector(`ul.options > li.active`);
|
|
167
|
+
// scrollIntoViewIfNeeded() scrolls top edge of element into view so when moving
|
|
168
|
+
// downwards, we scroll to next sibling to make element fully visible
|
|
169
|
+
if (increment === 1)
|
|
170
|
+
li?.nextSibling?.scrollIntoViewIfNeeded();
|
|
98
171
|
else
|
|
99
|
-
|
|
172
|
+
li?.scrollIntoViewIfNeeded();
|
|
100
173
|
}
|
|
101
174
|
}
|
|
102
175
|
else if (event.key === `Backspace`) {
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
}
|
|
176
|
+
const label = selectedLabels.pop();
|
|
177
|
+
if (label && !searchText)
|
|
178
|
+
remove(label);
|
|
107
179
|
}
|
|
108
180
|
}
|
|
109
181
|
const removeAll = () => {
|
|
110
|
-
dispatch(`
|
|
111
|
-
dispatch(`change`, {
|
|
112
|
-
selected =
|
|
182
|
+
dispatch(`removeAll`, { options: selected });
|
|
183
|
+
dispatch(`change`, { options: selected, type: `removeAll` });
|
|
184
|
+
selected = [];
|
|
113
185
|
searchText = ``;
|
|
114
186
|
};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
else
|
|
122
|
-
return selected.includes(option);
|
|
187
|
+
$: isSelected = (label) => selectedLabels.includes(label);
|
|
188
|
+
const handleEnterAndSpaceKeys = (handler) => (event) => {
|
|
189
|
+
if ([`Enter`, `Space`].includes(event.code)) {
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
handler();
|
|
192
|
+
}
|
|
123
193
|
};
|
|
124
194
|
</script>
|
|
125
195
|
|
|
126
|
-
<!-- z-index: 2 when showOptions is true ensures the ul.
|
|
196
|
+
<!-- z-index: 2 when showOptions is true ensures the ul.selected of one <MultiSelect />
|
|
197
|
+
display above those of another following shortly after it -->
|
|
127
198
|
<div
|
|
199
|
+
{id}
|
|
128
200
|
class="multiselect {outerDivClass}"
|
|
129
201
|
class:readonly
|
|
130
|
-
class:single
|
|
131
|
-
style={showOptions ? `z-index: 2;` :
|
|
132
|
-
on:mouseup|stopPropagation={() => setOptionsVisible(true)}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
202
|
+
class:single={maxSelect == 1}
|
|
203
|
+
style={showOptions ? `z-index: 2;` : undefined}
|
|
204
|
+
on:mouseup|stopPropagation={() => setOptionsVisible(true)}
|
|
205
|
+
use:onClickOutside={() => setOptionsVisible(false)}
|
|
206
|
+
use:onClickOutside={() => dispatch(`blur`)}
|
|
207
|
+
>
|
|
208
|
+
<ExpandIcon height="14pt" style="padding: 0 3pt 0 1pt;" />
|
|
209
|
+
<ul class="selected {ulSelectedClass}">
|
|
210
|
+
{#if maxSelect == 1 && selected[0]?.label}
|
|
136
211
|
<span on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}>
|
|
137
|
-
{selected}
|
|
212
|
+
{selected[0].label}
|
|
138
213
|
</span>
|
|
139
|
-
{:else
|
|
140
|
-
{#each selected as
|
|
214
|
+
{:else}
|
|
215
|
+
{#each selected as { label }}
|
|
141
216
|
<li
|
|
142
|
-
class={
|
|
143
|
-
on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
|
|
144
|
-
|
|
217
|
+
class={liSelectedClass}
|
|
218
|
+
on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
|
|
219
|
+
>
|
|
220
|
+
{label}
|
|
145
221
|
{#if !readonly}
|
|
146
222
|
<button
|
|
147
|
-
on:mouseup|stopPropagation={() => remove(
|
|
223
|
+
on:mouseup|stopPropagation={() => remove(label)}
|
|
224
|
+
on:keydown={handleEnterAndSpaceKeys(() => remove(label))}
|
|
148
225
|
type="button"
|
|
149
|
-
title="{removeBtnTitle} {
|
|
226
|
+
title="{removeBtnTitle} {label}"
|
|
227
|
+
>
|
|
150
228
|
<CrossIcon height="12pt" />
|
|
151
229
|
</button>
|
|
152
230
|
{/if}
|
|
@@ -160,20 +238,23 @@ $: isSelected = (option) => {
|
|
|
160
238
|
on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
|
|
161
239
|
on:keydown={handleKeydown}
|
|
162
240
|
on:focus={() => setOptionsVisible(true)}
|
|
163
|
-
on:blur={() => dispatch(`blur`)}
|
|
164
|
-
on:blur={() => setOptionsVisible(false)}
|
|
165
241
|
{name}
|
|
166
|
-
placeholder={
|
|
242
|
+
placeholder={selectedLabels.length ? `` : placeholder}
|
|
243
|
+
/>
|
|
167
244
|
</ul>
|
|
168
245
|
{#if readonly}
|
|
169
246
|
<ReadOnlyIcon height="14pt" />
|
|
170
|
-
{:else}
|
|
247
|
+
{:else if selected.length > 0}
|
|
248
|
+
{#if maxSelect !== null && maxSelect > 1}
|
|
249
|
+
<span style="padding: 0 3pt;">{maxSelectMsg(selected.length, maxSelect)}</span>
|
|
250
|
+
{/if}
|
|
171
251
|
<button
|
|
172
252
|
type="button"
|
|
173
253
|
class="remove-all"
|
|
174
254
|
title={removeAllTitle}
|
|
175
255
|
on:mouseup|stopPropagation={removeAll}
|
|
176
|
-
|
|
256
|
+
on:keydown={handleEnterAndSpaceKeys(removeAll)}
|
|
257
|
+
>
|
|
177
258
|
<CrossIcon height="14pt" />
|
|
178
259
|
</button>
|
|
179
260
|
{/if}
|
|
@@ -182,20 +263,22 @@ $: isSelected = (option) => {
|
|
|
182
263
|
<ul
|
|
183
264
|
class="options {ulOptionsClass}"
|
|
184
265
|
class:hidden={!showOptions}
|
|
185
|
-
transition:fly={{ duration: 300, y: 40 }}
|
|
186
|
-
|
|
266
|
+
transition:fly|local={{ duration: 300, y: 40 }}
|
|
267
|
+
>
|
|
268
|
+
{#each matchingOptions as { label, disabled, title = null, selectedTitle, disabledTitle = defaultDisabledTitle }}
|
|
187
269
|
<li
|
|
188
270
|
on:mouseup|preventDefault|stopPropagation
|
|
189
271
|
on:mousedown|preventDefault|stopPropagation={() => {
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
isSelected(option) ? remove(option) : add(option)
|
|
272
|
+
if (disabled) return
|
|
273
|
+
isSelected(label) ? remove(label) : add(label)
|
|
193
274
|
}}
|
|
194
|
-
class:selected={isSelected(
|
|
195
|
-
class:active={activeOption ===
|
|
196
|
-
class:disabled
|
|
197
|
-
|
|
198
|
-
{
|
|
275
|
+
class:selected={isSelected(label)}
|
|
276
|
+
class:active={activeOption?.label === label}
|
|
277
|
+
class:disabled
|
|
278
|
+
title={disabled ? disabledTitle : (isSelected(label) && selectedTitle) || title}
|
|
279
|
+
class={liOptionClass}
|
|
280
|
+
>
|
|
281
|
+
{label}
|
|
199
282
|
</li>
|
|
200
283
|
{:else}
|
|
201
284
|
{noOptionsMsg}
|
|
@@ -214,6 +297,7 @@ $: isSelected = (option) => {
|
|
|
214
297
|
min-height: 18pt;
|
|
215
298
|
display: flex;
|
|
216
299
|
cursor: text;
|
|
300
|
+
padding: 0 3pt;
|
|
217
301
|
}
|
|
218
302
|
:where(.multiselect:focus-within) {
|
|
219
303
|
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
|
|
@@ -222,8 +306,8 @@ $: isSelected = (option) => {
|
|
|
222
306
|
background: var(--sms-readonly-bg, lightgray);
|
|
223
307
|
}
|
|
224
308
|
|
|
225
|
-
:where(ul.
|
|
226
|
-
background: var(--sms-
|
|
309
|
+
:where(ul.selected > li) {
|
|
310
|
+
background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue));
|
|
227
311
|
align-items: center;
|
|
228
312
|
border-radius: 4pt;
|
|
229
313
|
display: flex;
|
|
@@ -233,16 +317,13 @@ $: isSelected = (option) => {
|
|
|
233
317
|
white-space: nowrap;
|
|
234
318
|
height: 16pt;
|
|
235
319
|
}
|
|
236
|
-
:where(ul.
|
|
320
|
+
:where(ul.selected > li button, button.remove-all) {
|
|
237
321
|
align-items: center;
|
|
238
322
|
border-radius: 50%;
|
|
239
323
|
display: flex;
|
|
240
324
|
cursor: pointer;
|
|
241
325
|
transition: 0.2s;
|
|
242
326
|
}
|
|
243
|
-
:where(ul.tokens > li button:hover, button.remove-all:hover) {
|
|
244
|
-
color: var(--sms-remove-x-hover-color, lightgray);
|
|
245
|
-
}
|
|
246
327
|
:where(button) {
|
|
247
328
|
color: inherit;
|
|
248
329
|
background: transparent;
|
|
@@ -251,21 +332,33 @@ $: isSelected = (option) => {
|
|
|
251
332
|
outline: none;
|
|
252
333
|
padding: 0 2pt;
|
|
253
334
|
}
|
|
335
|
+
:where(ul.selected > li button:hover, button.remove-all:hover) {
|
|
336
|
+
color: var(--sms-remove-x-hover-focus-color, lightskyblue);
|
|
337
|
+
}
|
|
338
|
+
:where(button:focus) {
|
|
339
|
+
color: var(--sms-remove-x-hover-focus-color, lightskyblue);
|
|
340
|
+
transform: scale(1.04);
|
|
341
|
+
}
|
|
254
342
|
|
|
255
343
|
:where(.multiselect input) {
|
|
256
344
|
border: none;
|
|
257
345
|
outline: none;
|
|
258
346
|
background: none;
|
|
259
347
|
color: var(--sms-text-color, inherit);
|
|
260
|
-
|
|
348
|
+
flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
|
|
349
|
+
min-width: 2em;
|
|
350
|
+
/* minimum font-size > 16px ensures iOS doesn't zoom in when focusing input */
|
|
351
|
+
/* https://stackoverflow.com/a/6394497 */
|
|
352
|
+
font-size: calc(16px + 0.1vw);
|
|
261
353
|
}
|
|
262
354
|
|
|
263
|
-
:where(ul.
|
|
355
|
+
:where(ul.selected) {
|
|
264
356
|
display: flex;
|
|
265
357
|
padding: 0;
|
|
266
358
|
margin: 0;
|
|
267
359
|
flex-wrap: wrap;
|
|
268
360
|
flex: 1;
|
|
361
|
+
overscroll-behavior: none;
|
|
269
362
|
}
|
|
270
363
|
|
|
271
364
|
:where(ul.options) {
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
import type { Option, Primitive, ProtoOption } from './';
|
|
2
3
|
declare const __propDef: {
|
|
3
4
|
props: {
|
|
4
|
-
selected
|
|
5
|
+
selected?: Option[] | undefined;
|
|
6
|
+
selectedLabels?: Primitive[] | undefined;
|
|
7
|
+
selectedValues?: Primitive[] | undefined;
|
|
5
8
|
maxSelect?: number | null | undefined;
|
|
9
|
+
maxSelectMsg?: ((current: number, max: number) => string) | undefined;
|
|
6
10
|
readonly?: boolean | undefined;
|
|
7
|
-
|
|
8
|
-
options: string[];
|
|
9
|
-
disabledOptions?: string[] | undefined;
|
|
11
|
+
options: ProtoOption[];
|
|
10
12
|
input?: HTMLInputElement | null | undefined;
|
|
13
|
+
placeholder?: string | undefined;
|
|
11
14
|
name?: string | undefined;
|
|
15
|
+
id?: string | undefined;
|
|
12
16
|
noOptionsMsg?: string | undefined;
|
|
17
|
+
activeOption?: Option | null | undefined;
|
|
13
18
|
outerDivClass?: string | undefined;
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
ulSelectedClass?: string | undefined;
|
|
20
|
+
liSelectedClass?: string | undefined;
|
|
16
21
|
ulOptionsClass?: string | undefined;
|
|
17
22
|
liOptionClass?: string | undefined;
|
|
18
23
|
removeBtnTitle?: string | undefined;
|
|
19
24
|
removeAllTitle?: string | undefined;
|
|
25
|
+
defaultDisabledTitle?: string | undefined;
|
|
20
26
|
};
|
|
21
27
|
events: {
|
|
22
28
|
mouseup: MouseEvent;
|
package/actions.d.ts
ADDED
package/actions.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|
|
17
|
+
// import { spring } from 'svelte/motion'
|
|
18
|
+
// export default function boop(node: HTMLElement, params = {}) {
|
|
19
|
+
// const { setter } = params
|
|
20
|
+
// const springyRotation = spring(
|
|
21
|
+
// { x: 0, y: 0, rotation: 0, scale: 1 },
|
|
22
|
+
// { stiffness: 0.1, damping: 0.15 }
|
|
23
|
+
// )
|
|
24
|
+
// node.style.display = `inline-block`
|
|
25
|
+
// const unsubscribe = springyRotation.subscribe(({ x, y, rotation, scale }) => {
|
|
26
|
+
// node.style.transform = `translate(${x}px, ${y}px) rotate(${rotation}deg) scale(${scale})`
|
|
27
|
+
// })
|
|
28
|
+
// return {
|
|
29
|
+
// update({ isBooped: x = 0, y = 0, rotation = 0, scale = 1, timing }) {
|
|
30
|
+
// springyRotation.set(
|
|
31
|
+
// isBooped
|
|
32
|
+
// ? { x, y, rotation, scale }
|
|
33
|
+
// : { x: 0, y: 0, rotation: 0, scale: 1 }
|
|
34
|
+
// )
|
|
35
|
+
// if (isBooped) window.setTimeout(() => setter(false), timing)
|
|
36
|
+
// },
|
|
37
|
+
// destroy: unsubscribe,
|
|
38
|
+
// }
|
|
39
|
+
// }
|
|
@@ -5,5 +5,6 @@ export let style = ``;
|
|
|
5
5
|
|
|
6
6
|
<svg {width} {height} {style} fill="currentColor" viewBox="0 0 16 16">
|
|
7
7
|
<path
|
|
8
|
-
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"
|
|
8
|
+
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
|
+
/>
|
|
9
10
|
</svg>
|
package/icons/Cross.svelte
CHANGED
|
@@ -5,5 +5,6 @@ export let style = ``;
|
|
|
5
5
|
|
|
6
6
|
<svg {width} {height} {style} viewBox="0 0 20 20" fill="currentColor">
|
|
7
7
|
<path
|
|
8
|
-
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"
|
|
8
|
+
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
|
+
/>
|
|
9
10
|
</svg>
|
package/icons/ReadOnly.svelte
CHANGED
|
@@ -6,5 +6,6 @@ export let style = ``;
|
|
|
6
6
|
<svg {width} {height} {style} viewBox="0 0 24 24" fill="currentColor">
|
|
7
7
|
<path fill="none" d="M0 0h24v24H0V0z" />
|
|
8
8
|
<path
|
|
9
|
-
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"
|
|
9
|
+
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"
|
|
10
|
+
/>
|
|
10
11
|
</svg>
|
package/icons/index.d.ts
ADDED
package/icons/index.js
ADDED
package/index.d.ts
CHANGED
|
@@ -1 +1,15 @@
|
|
|
1
1
|
export { default } from './MultiSelect.svelte';
|
|
2
|
+
export declare type Primitive = string | number;
|
|
3
|
+
export declare type Option = {
|
|
4
|
+
label: Primitive;
|
|
5
|
+
value: Primitive;
|
|
6
|
+
title?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
preselected?: boolean;
|
|
9
|
+
disabledTitle?: string;
|
|
10
|
+
selectedTitle?: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
};
|
|
13
|
+
export declare type ProtoOption = Primitive | (Omit<Option, `value`> & {
|
|
14
|
+
value?: Primitive;
|
|
15
|
+
});
|
package/package.json
CHANGED
|
@@ -5,33 +5,33 @@
|
|
|
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": "3.0.1",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"svelte": "MultiSelect.svelte",
|
|
11
11
|
"bugs": {
|
|
12
12
|
"url": "https://github.com/janosh/svelte-multiselect/issues"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"@sveltejs/adapter-static": "^1.0.0-next.
|
|
16
|
-
"@sveltejs/kit": "^1.0.0-next.
|
|
17
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
18
|
-
"@typescript-eslint/parser": "^5.
|
|
19
|
-
"eslint": "^8.0
|
|
20
|
-
"eslint-plugin-svelte3": "^3.
|
|
15
|
+
"@sveltejs/adapter-static": "^1.0.0-next.24",
|
|
16
|
+
"@sveltejs/kit": "^1.0.0-next.216",
|
|
17
|
+
"@typescript-eslint/eslint-plugin": "^5.9.0",
|
|
18
|
+
"@typescript-eslint/parser": "^5.9.0",
|
|
19
|
+
"eslint": "^8.6.0",
|
|
20
|
+
"eslint-plugin-svelte3": "^3.3.0",
|
|
21
21
|
"hastscript": "^7.0.2",
|
|
22
22
|
"mdsvex": "^0.9.8",
|
|
23
|
-
"prettier": "^2.
|
|
24
|
-
"prettier-plugin-svelte": "^2.
|
|
25
|
-
"rehype-autolink-headings": "^6.1.
|
|
26
|
-
"rehype-slug": "^5.0.
|
|
27
|
-
"svelte": "^3.
|
|
28
|
-
"svelte-check": "^2.2.
|
|
29
|
-
"svelte-preprocess": "^4.
|
|
30
|
-
"svelte-toc": "^0.
|
|
31
|
-
"svelte2tsx": "^0.4.
|
|
23
|
+
"prettier": "^2.5.1",
|
|
24
|
+
"prettier-plugin-svelte": "^2.5.1",
|
|
25
|
+
"rehype-autolink-headings": "^6.1.1",
|
|
26
|
+
"rehype-slug": "^5.0.1",
|
|
27
|
+
"svelte": "^3.45.0",
|
|
28
|
+
"svelte-check": "^2.2.11",
|
|
29
|
+
"svelte-preprocess": "^4.10.1",
|
|
30
|
+
"svelte-toc": "^0.2.0",
|
|
31
|
+
"svelte2tsx": "^0.4.12",
|
|
32
32
|
"tslib": "^2.3.1",
|
|
33
|
-
"typescript": "^4.
|
|
34
|
-
"vite": "^2.
|
|
33
|
+
"typescript": "^4.5.4",
|
|
34
|
+
"vite": "^2.7.10"
|
|
35
35
|
},
|
|
36
36
|
"keywords": [
|
|
37
37
|
"svelte",
|
package/readme.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
<!-- remove above in docs -->
|
|
22
22
|
|
|
23
|
-
Keyboard-friendly, zero-dependency multi-select Svelte component
|
|
23
|
+
**Keyboard-friendly, zero-dependency multi-select Svelte component.**
|
|
24
24
|
|
|
25
25
|
## Key Features
|
|
26
26
|
|
|
@@ -29,10 +29,22 @@ Keyboard-friendly, zero-dependency multi-select Svelte component.
|
|
|
29
29
|
- **Searchable:** start typing to filter options
|
|
30
30
|
- **Tagging:** selected options are recorded as tags within the text input
|
|
31
31
|
- **Server-side rendering:** no reliance on browser objects like `window` or `document`
|
|
32
|
-
- **Configurable:** see
|
|
32
|
+
- **Configurable:** see [props](#props)
|
|
33
33
|
- **No dependencies:** needs only Svelte as dev dependency
|
|
34
34
|
- **Keyboard friendly** for mouse-less form completion
|
|
35
35
|
|
|
36
|
+
## Recent breaking changes
|
|
37
|
+
|
|
38
|
+
- v2.0.0 added the ability to pass options as objects. As a result, `bind:selected` no longer returns simple strings but objects as well, even if you still pass in `options` as strings.
|
|
39
|
+
- v3.0.0 changed the `event.detail` payload for `'add'`, `'remove'` and `'change'` events from `token` to `option`, e.g.
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
on:add={(e) => console.log(e.detail.token.label)} // v2.0.0
|
|
43
|
+
on:add={(e) => console.log(e.detail.option.label)} // v3.0.0
|
|
44
|
+
```
|
|
45
|
+
|
|
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
|
+
|
|
36
48
|
## Installation
|
|
37
49
|
|
|
38
50
|
```sh
|
|
@@ -75,16 +87,21 @@ Full list of props/bindable variables for this component:
|
|
|
75
87
|
|
|
76
88
|
<div class="table">
|
|
77
89
|
|
|
78
|
-
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
90
|
+
<!-- prettier-ignore -->
|
|
91
|
+
| name | default | description |
|
|
92
|
+
| :--------------- | :--------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
93
|
+
| `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. |
|
|
94
|
+
| `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
|
|
95
|
+
| `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
|
|
96
|
+
| `maxSelectMsg` | ``(current: number, max: number) => `${current}/${max}` `` | Function that returns a string informing the user how many of the maximum allowed options they have currently selected. Return empty string to disable, i.e. `() => ''`. |
|
|
97
|
+
| `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
|
|
98
|
+
| `selectedLabels` | `[]` | Labels of currently selected options. |
|
|
99
|
+
| `selectedValues` | `[]` | Values of currently selected options. |
|
|
100
|
+
| `readonly` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
|
|
101
|
+
| `placeholder` | `undefined` | String shown in the text input when no option is selected. |
|
|
102
|
+
| `input` | `undefined` | Handle to the `<input>` DOM node. |
|
|
103
|
+
| `name` | `undefined` | Passed to the `<input>` for associating HTML form `<label>`s with this component. E.g. clicking a `<label>` with same name will focus this component. |
|
|
104
|
+
| `id` | `undefined` | Applied to the top-level `<div>` e.g. for `document.getElementById()`. |
|
|
88
105
|
|
|
89
106
|
</div>
|
|
90
107
|
|
|
@@ -92,35 +109,26 @@ Full list of props/bindable variables for this component:
|
|
|
92
109
|
|
|
93
110
|
`MultiSelect.svelte` dispatches the following events:
|
|
94
111
|
|
|
95
|
-
| name
|
|
96
|
-
|
|
|
97
|
-
| `add`
|
|
98
|
-
| `remove`
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
112
|
+
| name | detail | description |
|
|
113
|
+
| ----------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
114
|
+
| `add` | `{ option: Option }` | Triggers when a new option is selected. |
|
|
115
|
+
| `remove` | `{ option: Option }` | Triggers when one selected option provided as `event.detail.option` is removed. |
|
|
116
|
+
| `removeAll` | `options: Option[]` | Triggers when all selected options are removed. The payload `event.detail.options` gives the options that were previously selected. |
|
|
117
|
+
| `change` | `{ option?: Option, options?: Option[] }`, `type: 'add' \| 'remove' \| 'removeAll'` | Triggers when a option is either added or removed, or all options are removed at once. |
|
|
118
|
+
| `blur` | none | Triggers when the input field looses focus. |
|
|
101
119
|
|
|
102
120
|
### Examples
|
|
103
121
|
|
|
104
122
|
<!-- prettier-ignore -->
|
|
105
|
-
- `on:add={(event) => console.log(event.detail.
|
|
106
|
-
- `on:remove={(event) => console.log(event.detail.
|
|
107
|
-
- ``on:change={(event) => console.log(`${event.detail.type}: '${event.detail.
|
|
123
|
+
- `on:add={(event) => console.log(event.detail.option.label)}`
|
|
124
|
+
- `on:remove={(event) => console.log(event.detail.option.label)}`.
|
|
125
|
+
- ``on:change={(event) => console.log(`${event.detail.type}: '${event.detail.option.label}'`)}``
|
|
108
126
|
- `on:blur={yourFunctionHere}`
|
|
109
127
|
|
|
110
128
|
```svelte
|
|
111
129
|
<MultiSelect
|
|
112
|
-
on:change={(e) => alert(`You ${e.detail.type}ed '${e.detail.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
## Want to contribute?
|
|
116
|
-
|
|
117
|
-
To submit a PR, clone the repo, install dependencies and start the dev server to try out your changes first.
|
|
118
|
-
|
|
119
|
-
```sh
|
|
120
|
-
git clone https://github.com/janosh/svelte-multiselect
|
|
121
|
-
cd svelte-multiselect
|
|
122
|
-
yarn
|
|
123
|
-
yarn dev
|
|
130
|
+
on:change={(e) => alert(`You ${e.detail.type}ed '${e.detail.option.label}'`)}
|
|
131
|
+
/>
|
|
124
132
|
```
|
|
125
133
|
|
|
126
134
|
## Styling
|
|
@@ -134,10 +142,11 @@ The first, if you only want to make small adjustments, allows you to pass the fo
|
|
|
134
142
|
- `border: var(--sms-border, 1pt solid lightgray)`: Border around top-level `div.multiselect`. Change this to e.g. to `1px solid red` to indicate this form field is in an invalid state.
|
|
135
143
|
- `border-radius: var(--sms-border-radius, 5pt)`: `div.multiselect` border radius.
|
|
136
144
|
- `color: var(--sms-text-color, inherit)`: Input text color.
|
|
137
|
-
- `border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue))`: `div.multiselect` border when focused.
|
|
145
|
+
- `border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue))`: `div.multiselect` border when focused. Falls back to `--sms-active-color` if not set which in turn falls back on `cornflowerblue`.
|
|
138
146
|
- `background: var(--sms-readonly-bg, lightgray)`: Background when in readonly state.
|
|
139
|
-
- `background: var(--sms-
|
|
140
|
-
- `color: var(--sms-remove-x-hover-color, lightgray)`: Hover color of cross icon to remove selected
|
|
147
|
+
- `background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue))`: Background of selected options.
|
|
148
|
+
- `color: var(--sms-remove-x-hover+focus-color, lightgray)`: Hover color of cross icon to remove selected options.
|
|
149
|
+
- `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.
|
|
141
150
|
- `background: var(--sms-options-bg, white)`: Background of options list.
|
|
142
151
|
- `background: var(--sms-li-selected-bg, inherit)`: Background of selected list items in options pane.
|
|
143
152
|
- `color: var(--sms-li-selected-color, inherit)`: Text color of selected list items in options pane.
|
|
@@ -156,8 +165,8 @@ For example, to change the background color of the options dropdown:
|
|
|
156
165
|
The second method allows you to pass in custom classes to the important DOM elements of this component to target them with frameworks like [Tailwind CSS](https://tailwindcss.com).
|
|
157
166
|
|
|
158
167
|
- `outerDivClass`
|
|
159
|
-
- `
|
|
160
|
-
- `
|
|
168
|
+
- `ulSelectedClass`
|
|
169
|
+
- `liSelectedClass`
|
|
161
170
|
- `ulOptionsClass`
|
|
162
171
|
- `liOptionClass`
|
|
163
172
|
|
|
@@ -165,9 +174,9 @@ This simplified version of the DOM structure of this component shows where these
|
|
|
165
174
|
|
|
166
175
|
```svelte
|
|
167
176
|
<div class={outerDivClass}>
|
|
168
|
-
<ul class={
|
|
169
|
-
<li class={
|
|
170
|
-
<li class={
|
|
177
|
+
<ul class={ulSelectedClass}>
|
|
178
|
+
<li class={liSelectedClass}>First selected tag</li>
|
|
179
|
+
<li class={liSelectedClass}>Second selected tag</li>
|
|
171
180
|
</ul>
|
|
172
181
|
<ul class={ulOptionsClass}>
|
|
173
182
|
<li class={liOptionClass}>First available option</li>
|
|
@@ -178,16 +187,16 @@ This simplified version of the DOM structure of this component shows where these
|
|
|
178
187
|
|
|
179
188
|
### Granular control through global CSS
|
|
180
189
|
|
|
181
|
-
You can alternatively style every part of this component with more fine-grained control by using the following `:global()` CSS selectors.
|
|
190
|
+
You can alternatively style every part of this component with more fine-grained control by using the following `:global()` CSS selectors. `ul.selected` is the list of currently selected options rendered inside the component's input whereas `ul.options` is the list of available options that slides out when the component has focus.
|
|
182
191
|
|
|
183
192
|
```css
|
|
184
193
|
:global(.multiselect) {
|
|
185
194
|
/* top-level wrapper div */
|
|
186
195
|
}
|
|
187
|
-
:global(.multiselect ul.
|
|
188
|
-
/*
|
|
196
|
+
:global(.multiselect ul.selected > li) {
|
|
197
|
+
/* selected options */
|
|
189
198
|
}
|
|
190
|
-
:global(.multiselect ul.
|
|
199
|
+
:global(.multiselect ul.selected > li button),
|
|
191
200
|
:global(.multiselect button.remove-all) {
|
|
192
201
|
/* buttons to remove a single or all selected options at once */
|
|
193
202
|
}
|
|
@@ -195,7 +204,7 @@ You can alternatively style every part of this component with more fine-grained
|
|
|
195
204
|
/* dropdown options */
|
|
196
205
|
}
|
|
197
206
|
:global(.multiselect ul.options li) {
|
|
198
|
-
/* dropdown options */
|
|
207
|
+
/* dropdown list of available options */
|
|
199
208
|
}
|
|
200
209
|
:global(.multiselect ul.options li.selected) {
|
|
201
210
|
/* selected options in the dropdown list */
|
|
@@ -208,11 +217,24 @@ You can alternatively style every part of this component with more fine-grained
|
|
|
208
217
|
/* probably not necessary to style this state in most cases */
|
|
209
218
|
}
|
|
210
219
|
:global(.multiselect ul.options li.active) {
|
|
211
|
-
/* active means
|
|
220
|
+
/* active means item was navigated to with up/down arrow keys */
|
|
212
221
|
/* ready to be selected by pressing enter */
|
|
213
222
|
}
|
|
214
223
|
:global(.multiselect ul.options li.selected.active) {
|
|
224
|
+
/* both active and already selected, pressing enter now will deselect the item */
|
|
215
225
|
}
|
|
216
226
|
:global(.multiselect ul.options li.disabled) {
|
|
227
|
+
/* options with disabled key set to true (see props above) */
|
|
217
228
|
}
|
|
218
229
|
```
|
|
230
|
+
|
|
231
|
+
## Want to contribute?
|
|
232
|
+
|
|
233
|
+
To submit a PR, clone the repo, install dependencies and start the dev server to try out your changes.
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
git clone https://github.com/janosh/svelte-multiselect
|
|
237
|
+
cd svelte-multiselect
|
|
238
|
+
yarn
|
|
239
|
+
yarn dev
|
|
240
|
+
```
|