svelte-multiselect 1.2.3 → 2.0.0
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 +150 -86
- package/MultiSelect.svelte.d.ts +10 -4
- package/actions.d.ts +3 -0
- package/actions.js +16 -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 +16 -15
- package/readme.md +39 -33
package/MultiSelect.svelte
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
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
19
|
export let ulTokensClass = ``;
|
|
17
20
|
export let liTokenClass = ``;
|
|
@@ -19,47 +22,89 @@ export let ulOptionsClass = ``;
|
|
|
19
22
|
export let liOptionClass = ``;
|
|
20
23
|
export let removeBtnTitle = `Remove`;
|
|
21
24
|
export let removeAllTitle = `Remove all`;
|
|
25
|
+
export let defaultDisabledTitle = `This option is disabled`;
|
|
22
26
|
if (maxSelect !== null && maxSelect < 0) {
|
|
23
|
-
|
|
27
|
+
console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
24
28
|
}
|
|
25
|
-
$: single = maxSelect === 1;
|
|
26
|
-
if (!selected)
|
|
27
|
-
selected = single ? `` : [];
|
|
28
29
|
if (!(options?.length > 0))
|
|
29
30
|
console.error(`MultiSelect missing options`);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
if (!Array.isArray(selected))
|
|
32
|
+
console.error(`selected prop must be an array`);
|
|
33
|
+
function isObject(item) {
|
|
34
|
+
return typeof item === `object` && !Array.isArray(item) && item !== null;
|
|
33
35
|
}
|
|
36
|
+
onMount(() => {
|
|
37
|
+
selected = _options.filter((op) => op?.preselected);
|
|
38
|
+
});
|
|
39
|
+
// process proto options to full ones with mandatory labels
|
|
40
|
+
$: _options = options.map((rawOp) => {
|
|
41
|
+
// convert to objects internally if user passed list of strings or numbers as options
|
|
42
|
+
if (isObject(rawOp)) {
|
|
43
|
+
const op = { ...rawOp };
|
|
44
|
+
if (!op.value)
|
|
45
|
+
op.value = op.label;
|
|
46
|
+
return op;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
if (![`string`, `number`].includes(typeof rawOp)) {
|
|
50
|
+
console.error(`MultiSelect options must be objects, strings or numbers, got ${typeof rawOp}`);
|
|
51
|
+
}
|
|
52
|
+
// even if we logged error above, try to proceed hoping user knows what they're doing
|
|
53
|
+
return { label: rawOp, value: rawOp };
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
$: labels = _options.map((op) => op.label);
|
|
57
|
+
$: if (new Set(labels).size !== options.length) {
|
|
58
|
+
console.error(`Option labels must be unique. Duplicates found: ${labels.filter((label, idx) => labels.indexOf(label) !== idx)}`);
|
|
59
|
+
}
|
|
60
|
+
$: selectedLabels = selected.map((op) => op.label);
|
|
61
|
+
$: selectedValues = selected.map((op) => op.value);
|
|
34
62
|
const dispatch = createEventDispatcher();
|
|
35
|
-
let
|
|
63
|
+
let searchText = ``;
|
|
36
64
|
let showOptions = false;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
65
|
+
// options matching the current search text
|
|
66
|
+
$: matchingOptions = _options.filter((op) => {
|
|
67
|
+
if (!searchText)
|
|
68
|
+
return true;
|
|
69
|
+
return `${op.label}`.toLowerCase().includes(searchText.toLowerCase());
|
|
70
|
+
});
|
|
71
|
+
$: matchingEnabledOptions = matchingOptions.filter((op) => !op.disabled);
|
|
72
|
+
$: if (
|
|
73
|
+
// if there was an active option but it's not in the filtered list of options
|
|
74
|
+
(activeOption &&
|
|
75
|
+
!matchingEnabledOptions.map((op) => op.label).includes(activeOption.label)) ||
|
|
76
|
+
// or there's no active option but the user entered search text
|
|
41
77
|
(!activeOption && searchText))
|
|
42
|
-
|
|
43
|
-
|
|
78
|
+
// make the first filtered option active
|
|
79
|
+
activeOption = matchingEnabledOptions[0];
|
|
80
|
+
function add(label) {
|
|
44
81
|
if (!readonly &&
|
|
45
|
-
!
|
|
46
|
-
//
|
|
47
|
-
(maxSelect
|
|
82
|
+
!selectedLabels.includes(label) &&
|
|
83
|
+
// for maxselect = 1 we always replace current token with new selection
|
|
84
|
+
(maxSelect == null || maxSelect == 1 || selected.length < maxSelect)) {
|
|
48
85
|
searchText = ``; // reset search string on selection
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
input?.blur();
|
|
86
|
+
const token = _options.find((op) => op.label === label);
|
|
87
|
+
if (!token) {
|
|
88
|
+
console.error(`MultiSelect: option with label ${label} not found`);
|
|
89
|
+
return;
|
|
54
90
|
}
|
|
91
|
+
if (maxSelect === 1) {
|
|
92
|
+
selected = [token];
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
selected = [token, ...selected];
|
|
96
|
+
}
|
|
97
|
+
if (selected.length === maxSelect)
|
|
98
|
+
setOptionsVisible(false);
|
|
55
99
|
dispatch(`add`, { token });
|
|
56
100
|
dispatch(`change`, { token, type: `add` });
|
|
57
101
|
}
|
|
58
102
|
}
|
|
59
|
-
function remove(
|
|
60
|
-
if (
|
|
103
|
+
function remove(label) {
|
|
104
|
+
if (selected.length === 0 || readonly)
|
|
61
105
|
return;
|
|
62
|
-
selected = selected.filter((
|
|
106
|
+
selected = selected.filter((token) => label !== token.label);
|
|
107
|
+
const token = _options.find((option) => option.label === label);
|
|
63
108
|
dispatch(`remove`, { token });
|
|
64
109
|
dispatch(`change`, { token, type: `remove` });
|
|
65
110
|
}
|
|
@@ -70,57 +115,64 @@ function setOptionsVisible(show) {
|
|
|
70
115
|
showOptions = show;
|
|
71
116
|
if (show)
|
|
72
117
|
input?.focus();
|
|
118
|
+
else {
|
|
119
|
+
input?.blur();
|
|
120
|
+
activeOption = null;
|
|
121
|
+
}
|
|
73
122
|
}
|
|
123
|
+
// handle all keyboard events this component receives
|
|
74
124
|
function handleKeydown(event) {
|
|
125
|
+
// on escape: dismiss options dropdown and reset search text
|
|
75
126
|
if (event.key === `Escape`) {
|
|
76
127
|
setOptionsVisible(false);
|
|
77
128
|
searchText = ``;
|
|
78
129
|
}
|
|
130
|
+
// on enter key: toggle active option and reset search text
|
|
79
131
|
else if (event.key === `Enter`) {
|
|
80
132
|
if (activeOption) {
|
|
81
|
-
|
|
133
|
+
const { label, disabled } = activeOption;
|
|
134
|
+
if (disabled)
|
|
82
135
|
return;
|
|
83
|
-
|
|
136
|
+
selectedLabels.includes(label) ? remove(label) : add(label);
|
|
84
137
|
searchText = ``;
|
|
85
|
-
} // no active option means the options
|
|
138
|
+
} // no active option means the options dropdown is closed in which case enter means open it
|
|
86
139
|
else
|
|
87
140
|
setOptionsVisible(true);
|
|
88
141
|
}
|
|
142
|
+
// on up/down arrow keys: update active option
|
|
89
143
|
else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
|
|
144
|
+
if (activeOption === null) {
|
|
145
|
+
// if no option is active yet, make first one active
|
|
146
|
+
activeOption = matchingEnabledOptions[0];
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
90
149
|
const increment = event.key === `ArrowUp` ? -1 : 1;
|
|
91
|
-
const newActiveIdx =
|
|
150
|
+
const newActiveIdx = matchingEnabledOptions.indexOf(activeOption) + increment;
|
|
92
151
|
if (newActiveIdx < 0) {
|
|
93
|
-
|
|
152
|
+
// wrap around top
|
|
153
|
+
activeOption = matchingEnabledOptions[matchingEnabledOptions.length - 1];
|
|
154
|
+
// wrap around bottom
|
|
94
155
|
}
|
|
95
|
-
else {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
else
|
|
99
|
-
activeOption = filteredOptions[newActiveIdx];
|
|
156
|
+
else if (newActiveIdx === matchingEnabledOptions.length) {
|
|
157
|
+
activeOption = matchingEnabledOptions[0];
|
|
158
|
+
// default case
|
|
100
159
|
}
|
|
160
|
+
else
|
|
161
|
+
activeOption = matchingEnabledOptions[newActiveIdx];
|
|
101
162
|
}
|
|
102
163
|
else if (event.key === `Backspace`) {
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
}
|
|
164
|
+
const label = selectedLabels.pop();
|
|
165
|
+
if (label && !searchText)
|
|
166
|
+
remove(label);
|
|
107
167
|
}
|
|
108
168
|
}
|
|
109
169
|
const removeAll = () => {
|
|
110
170
|
dispatch(`remove`, { token: selected });
|
|
111
171
|
dispatch(`change`, { token: selected, type: `remove` });
|
|
112
|
-
selected =
|
|
172
|
+
selected = [];
|
|
113
173
|
searchText = ``;
|
|
114
174
|
};
|
|
115
|
-
|
|
116
|
-
$: isSelected = (option) => {
|
|
117
|
-
if (!(selected?.length > 0))
|
|
118
|
-
return false; // nothing is selected if `selected` is the empty array or string
|
|
119
|
-
if (single)
|
|
120
|
-
return selected === option;
|
|
121
|
-
else
|
|
122
|
-
return selected.includes(option);
|
|
123
|
-
};
|
|
175
|
+
$: isSelected = (label) => selectedLabels.includes(label);
|
|
124
176
|
const handleEnterAndSpaceKeys = (handler) => (event) => {
|
|
125
177
|
if ([`Enter`, `Space`].includes(event.code)) {
|
|
126
178
|
event.preventDefault();
|
|
@@ -129,31 +181,38 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
|
|
|
129
181
|
};
|
|
130
182
|
</script>
|
|
131
183
|
|
|
132
|
-
<!-- z-index: 2 when showOptions is true ensures the ul.tokens of one <MultiSelect />
|
|
184
|
+
<!-- z-index: 2 when showOptions is true ensures the ul.tokens of one <MultiSelect />
|
|
185
|
+
display above those of another following shortly after it -->
|
|
133
186
|
<div
|
|
187
|
+
{id}
|
|
134
188
|
class="multiselect {outerDivClass}"
|
|
135
189
|
class:readonly
|
|
136
|
-
class:single
|
|
137
|
-
style={showOptions ? `z-index: 2;` :
|
|
138
|
-
on:mouseup|stopPropagation={() => setOptionsVisible(true)}
|
|
139
|
-
|
|
190
|
+
class:single={maxSelect == 1}
|
|
191
|
+
style={showOptions ? `z-index: 2;` : undefined}
|
|
192
|
+
on:mouseup|stopPropagation={() => setOptionsVisible(true)}
|
|
193
|
+
use:onClickOutside={() => setOptionsVisible(false)}
|
|
194
|
+
use:onClickOutside={() => dispatch(`blur`)}
|
|
195
|
+
>
|
|
196
|
+
<ExpandIcon height="14pt" style="padding: 0 3pt 0 1pt;" />
|
|
140
197
|
<ul class="tokens {ulTokensClass}">
|
|
141
|
-
{#if
|
|
198
|
+
{#if maxSelect == 1 && selected[0]?.label}
|
|
142
199
|
<span on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}>
|
|
143
|
-
{selected}
|
|
200
|
+
{selected[0].label}
|
|
144
201
|
</span>
|
|
145
|
-
{:else
|
|
146
|
-
{#each selected as
|
|
202
|
+
{:else}
|
|
203
|
+
{#each selected as { label }}
|
|
147
204
|
<li
|
|
148
205
|
class={liTokenClass}
|
|
149
|
-
on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
|
|
150
|
-
|
|
206
|
+
on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
|
|
207
|
+
>
|
|
208
|
+
{label}
|
|
151
209
|
{#if !readonly}
|
|
152
210
|
<button
|
|
153
|
-
on:mouseup|stopPropagation={() => remove(
|
|
154
|
-
on:keydown={handleEnterAndSpaceKeys(() => remove(
|
|
211
|
+
on:mouseup|stopPropagation={() => remove(label)}
|
|
212
|
+
on:keydown={handleEnterAndSpaceKeys(() => remove(label))}
|
|
155
213
|
type="button"
|
|
156
|
-
title="{removeBtnTitle} {
|
|
214
|
+
title="{removeBtnTitle} {label}"
|
|
215
|
+
>
|
|
157
216
|
<CrossIcon height="12pt" />
|
|
158
217
|
</button>
|
|
159
218
|
{/if}
|
|
@@ -167,21 +226,23 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
|
|
|
167
226
|
on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
|
|
168
227
|
on:keydown={handleKeydown}
|
|
169
228
|
on:focus={() => setOptionsVisible(true)}
|
|
170
|
-
on:blur={() => dispatch(`blur`)}
|
|
171
|
-
on:blur={() => setOptionsVisible(false)}
|
|
172
229
|
{name}
|
|
173
|
-
placeholder={
|
|
230
|
+
placeholder={selectedLabels.length ? `` : placeholder}
|
|
231
|
+
/>
|
|
174
232
|
</ul>
|
|
175
233
|
{#if readonly}
|
|
176
234
|
<ReadOnlyIcon height="14pt" />
|
|
177
|
-
{:else}
|
|
235
|
+
{:else if selected.length > 0}
|
|
236
|
+
{#if maxSelect !== null && maxSelect > 1}
|
|
237
|
+
<span style="padding: 0 3pt;">{maxSelectMsg(selected.length, maxSelect)}</span>
|
|
238
|
+
{/if}
|
|
178
239
|
<button
|
|
179
240
|
type="button"
|
|
180
241
|
class="remove-all"
|
|
181
242
|
title={removeAllTitle}
|
|
182
243
|
on:mouseup|stopPropagation={removeAll}
|
|
183
244
|
on:keydown={handleEnterAndSpaceKeys(removeAll)}
|
|
184
|
-
|
|
245
|
+
>
|
|
185
246
|
<CrossIcon height="14pt" />
|
|
186
247
|
</button>
|
|
187
248
|
{/if}
|
|
@@ -190,20 +251,22 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
|
|
|
190
251
|
<ul
|
|
191
252
|
class="options {ulOptionsClass}"
|
|
192
253
|
class:hidden={!showOptions}
|
|
193
|
-
transition:fly={{ duration: 300, y: 40 }}
|
|
194
|
-
|
|
254
|
+
transition:fly|local={{ duration: 300, y: 40 }}
|
|
255
|
+
>
|
|
256
|
+
{#each matchingOptions as { label, disabled, title = '', selectedTitle, disabledTitle = defaultDisabledTitle }}
|
|
195
257
|
<li
|
|
196
258
|
on:mouseup|preventDefault|stopPropagation
|
|
197
259
|
on:mousedown|preventDefault|stopPropagation={() => {
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
isSelected(option) ? remove(option) : add(option)
|
|
260
|
+
if (disabled) return
|
|
261
|
+
isSelected(label) ? remove(label) : add(label)
|
|
201
262
|
}}
|
|
202
|
-
class:selected={isSelected(
|
|
203
|
-
class:active={activeOption ===
|
|
204
|
-
class:disabled
|
|
205
|
-
|
|
206
|
-
{
|
|
263
|
+
class:selected={isSelected(label)}
|
|
264
|
+
class:active={activeOption?.label === label}
|
|
265
|
+
class:disabled
|
|
266
|
+
title={disabled ? disabledTitle : (isSelected(label) && selectedTitle) || title}
|
|
267
|
+
class={liOptionClass}
|
|
268
|
+
>
|
|
269
|
+
{label}
|
|
207
270
|
</li>
|
|
208
271
|
{:else}
|
|
209
272
|
{noOptionsMsg}
|
|
@@ -222,6 +285,7 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
|
|
|
222
285
|
min-height: 18pt;
|
|
223
286
|
display: flex;
|
|
224
287
|
cursor: text;
|
|
288
|
+
padding: 0 3pt;
|
|
225
289
|
}
|
|
226
290
|
:where(.multiselect:focus-within) {
|
|
227
291
|
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
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
19
|
ulTokensClass?: string | undefined;
|
|
15
20
|
liTokenClass?: string | undefined;
|
|
@@ -17,6 +22,7 @@ declare const __propDef: {
|
|
|
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,16 @@
|
|
|
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
|
+
}
|
|
@@ -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": "2.0.0",
|
|
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
|
|
15
|
+
"@sveltejs/adapter-static": "^1.0.0-next.22",
|
|
16
|
+
"@sveltejs/kit": "^1.0.0-next.202",
|
|
17
|
+
"@typescript-eslint/eslint-plugin": "^5.7.0",
|
|
18
|
+
"@typescript-eslint/parser": "^5.7.0",
|
|
19
|
+
"eslint": "^8.5.0",
|
|
20
20
|
"eslint-plugin-svelte3": "^3.2.1",
|
|
21
21
|
"hastscript": "^7.0.2",
|
|
22
22
|
"mdsvex": "^0.9.8",
|
|
23
|
-
"prettier": "^2.
|
|
24
|
-
"prettier-plugin-svelte": "^2.
|
|
23
|
+
"prettier": "^2.5.1",
|
|
24
|
+
"prettier-plugin-svelte": "^2.5.1",
|
|
25
25
|
"rehype-autolink-headings": "^6.1.0",
|
|
26
26
|
"rehype-slug": "^5.0.0",
|
|
27
|
-
"svelte": "^3.44.
|
|
28
|
-
"svelte-check": "^2.2.
|
|
29
|
-
"svelte-preprocess": "^4.
|
|
30
|
-
"svelte-toc": "^0.1.
|
|
31
|
-
"svelte2tsx": "^0.4.
|
|
27
|
+
"svelte": "^3.44.3",
|
|
28
|
+
"svelte-check": "^2.2.11",
|
|
29
|
+
"svelte-preprocess": "^4.10.1",
|
|
30
|
+
"svelte-toc": "^0.1.10",
|
|
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.3"
|
|
35
35
|
},
|
|
36
36
|
"keywords": [
|
|
37
37
|
"svelte",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"exports": {
|
|
47
47
|
"./package.json": "./package.json",
|
|
48
48
|
"./MultiSelect.svelte": "./MultiSelect.svelte",
|
|
49
|
+
"./actions": "./actions.js",
|
|
49
50
|
".": "./index.js"
|
|
50
51
|
}
|
|
51
52
|
}
|
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,7 +29,7 @@ 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
|
|
|
@@ -75,16 +75,21 @@ Full list of props/bindable variables for this component:
|
|
|
75
75
|
|
|
76
76
|
<div class="table">
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
78
|
+
<!-- prettier-ignore -->
|
|
79
|
+
| name | default | description |
|
|
80
|
+
| :--------------- | :--------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
81
|
+
| `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. |
|
|
82
|
+
| `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
|
|
83
|
+
| `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
|
|
84
|
+
| `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. `() => ''`. |
|
|
85
|
+
| `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
|
|
86
|
+
| `selectedLabels` | `[]` | Labels of currently selected options. |
|
|
87
|
+
| `selectedValues` | `[]` | Values of currently selected options. |
|
|
88
|
+
| `readonly` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
|
|
89
|
+
| `placeholder` | `undefined` | String shown in the text input when no option is selected. |
|
|
90
|
+
| `input` | `undefined` | Handle to the `<input>` DOM node. |
|
|
91
|
+
| `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. |
|
|
92
|
+
| `id` | `undefined` | Applied to the top-level `<div>` e.g. for `document.getElementById()`. |
|
|
88
93
|
|
|
89
94
|
</div>
|
|
90
95
|
|
|
@@ -102,25 +107,15 @@ Full list of props/bindable variables for this component:
|
|
|
102
107
|
### Examples
|
|
103
108
|
|
|
104
109
|
<!-- prettier-ignore -->
|
|
105
|
-
- `on:add={(event) => console.log(event.detail.token)}`
|
|
106
|
-
- `on:remove={(event) => console.log(event.detail.token)}`.
|
|
107
|
-
- ``on:change={(event) => console.log(`${event.detail.type}: '${event.detail.token}'`)}``
|
|
110
|
+
- `on:add={(event) => console.log(event.detail.token.label)}`
|
|
111
|
+
- `on:remove={(event) => console.log(event.detail.token.label)}`.
|
|
112
|
+
- ``on:change={(event) => console.log(`${event.detail.type}: '${event.detail.token.label}'`)}``
|
|
108
113
|
- `on:blur={yourFunctionHere}`
|
|
109
114
|
|
|
110
115
|
```svelte
|
|
111
116
|
<MultiSelect
|
|
112
|
-
on:change={(e) => alert(`You ${e.detail.type}ed '${e.detail.token}'`)}
|
|
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
|
|
117
|
+
on:change={(e) => alert(`You ${e.detail.type}ed '${e.detail.token.label}'`)}
|
|
118
|
+
/>
|
|
124
119
|
```
|
|
125
120
|
|
|
126
121
|
## Styling
|
|
@@ -179,14 +174,14 @@ This simplified version of the DOM structure of this component shows where these
|
|
|
179
174
|
|
|
180
175
|
### Granular control through global CSS
|
|
181
176
|
|
|
182
|
-
You can alternatively style every part of this component with more fine-grained control by using the following `:global()` CSS selectors.
|
|
177
|
+
You can alternatively style every part of this component with more fine-grained control by using the following `:global()` CSS selectors. `ul.tokens` 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.
|
|
183
178
|
|
|
184
179
|
```css
|
|
185
180
|
:global(.multiselect) {
|
|
186
181
|
/* top-level wrapper div */
|
|
187
182
|
}
|
|
188
183
|
:global(.multiselect ul.tokens > li) {
|
|
189
|
-
/*
|
|
184
|
+
/* selected options */
|
|
190
185
|
}
|
|
191
186
|
:global(.multiselect ul.tokens > li button),
|
|
192
187
|
:global(.multiselect button.remove-all) {
|
|
@@ -196,7 +191,7 @@ You can alternatively style every part of this component with more fine-grained
|
|
|
196
191
|
/* dropdown options */
|
|
197
192
|
}
|
|
198
193
|
:global(.multiselect ul.options li) {
|
|
199
|
-
/* dropdown options */
|
|
194
|
+
/* dropdown list of available options */
|
|
200
195
|
}
|
|
201
196
|
:global(.multiselect ul.options li.selected) {
|
|
202
197
|
/* selected options in the dropdown list */
|
|
@@ -209,13 +204,24 @@ You can alternatively style every part of this component with more fine-grained
|
|
|
209
204
|
/* probably not necessary to style this state in most cases */
|
|
210
205
|
}
|
|
211
206
|
:global(.multiselect ul.options li.active) {
|
|
212
|
-
/* active means
|
|
207
|
+
/* active means item was navigated to with up/down arrow keys */
|
|
213
208
|
/* ready to be selected by pressing enter */
|
|
214
209
|
}
|
|
215
210
|
:global(.multiselect ul.options li.selected.active) {
|
|
216
|
-
/* both active and already selected, pressing enter now will deselect the
|
|
211
|
+
/* both active and already selected, pressing enter now will deselect the item */
|
|
217
212
|
}
|
|
218
213
|
:global(.multiselect ul.options li.disabled) {
|
|
219
|
-
/*
|
|
214
|
+
/* options with disabled key set to true (see props above) */
|
|
220
215
|
}
|
|
221
216
|
```
|
|
217
|
+
|
|
218
|
+
## Want to contribute?
|
|
219
|
+
|
|
220
|
+
To submit a PR, clone the repo, install dependencies and start the dev server to try out your changes.
|
|
221
|
+
|
|
222
|
+
```sh
|
|
223
|
+
git clone https://github.com/janosh/svelte-multiselect
|
|
224
|
+
cd svelte-multiselect
|
|
225
|
+
yarn
|
|
226
|
+
yarn dev
|
|
227
|
+
```
|