svelte-multiselect 2.0.0 → 3.1.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 +83 -63
- package/MultiSelect.svelte.d.ts +12 -3
- package/Wiggle.svelte +24 -0
- package/Wiggle.svelte.d.ts +25 -0
- package/package.json +20 -22
- package/readme.md +125 -49
package/MultiSelect.svelte
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { fly } from 'svelte/transition';
|
|
3
3
|
import { onClickOutside } from './actions';
|
|
4
4
|
import { CrossIcon, ExpandIcon, ReadOnlyIcon } from './icons';
|
|
5
|
+
import Wiggle from './Wiggle.svelte';
|
|
5
6
|
export let selected = [];
|
|
6
7
|
export let selectedLabels = [];
|
|
7
8
|
export let selectedValues = [];
|
|
@@ -16,12 +17,13 @@ export let id = undefined;
|
|
|
16
17
|
export let noOptionsMsg = `No matching options`;
|
|
17
18
|
export let activeOption = null;
|
|
18
19
|
export let outerDivClass = ``;
|
|
19
|
-
export let
|
|
20
|
-
export let
|
|
20
|
+
export let ulSelectedClass = ``;
|
|
21
|
+
export let liSelectedClass = ``;
|
|
21
22
|
export let ulOptionsClass = ``;
|
|
22
23
|
export let liOptionClass = ``;
|
|
23
24
|
export let removeBtnTitle = `Remove`;
|
|
24
25
|
export let removeAllTitle = `Remove all`;
|
|
26
|
+
// https://github.com/sveltejs/svelte/issues/6964
|
|
25
27
|
export let defaultDisabledTitle = `This option is disabled`;
|
|
26
28
|
if (maxSelect !== null && maxSelect < 0) {
|
|
27
29
|
console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
@@ -30,12 +32,13 @@ if (!(options?.length > 0))
|
|
|
30
32
|
console.error(`MultiSelect missing options`);
|
|
31
33
|
if (!Array.isArray(selected))
|
|
32
34
|
console.error(`selected prop must be an array`);
|
|
33
|
-
function isObject(item) {
|
|
34
|
-
return typeof item === `object` && !Array.isArray(item) && item !== null;
|
|
35
|
-
}
|
|
36
35
|
onMount(() => {
|
|
37
36
|
selected = _options.filter((op) => op?.preselected);
|
|
38
37
|
});
|
|
38
|
+
let wiggle = false;
|
|
39
|
+
function isObject(item) {
|
|
40
|
+
return typeof item === `object` && !Array.isArray(item) && item !== null;
|
|
41
|
+
}
|
|
39
42
|
// process proto options to full ones with mandatory labels
|
|
40
43
|
$: _options = options.map((rawOp) => {
|
|
41
44
|
// convert to objects internally if user passed list of strings or numbers as options
|
|
@@ -78,35 +81,37 @@ $: if (
|
|
|
78
81
|
// make the first filtered option active
|
|
79
82
|
activeOption = matchingEnabledOptions[0];
|
|
80
83
|
function add(label) {
|
|
84
|
+
if (selected.length - (maxSelect ?? 0) < 1)
|
|
85
|
+
wiggle = true;
|
|
81
86
|
if (!readonly &&
|
|
82
87
|
!selectedLabels.includes(label) &&
|
|
83
|
-
// for maxselect = 1 we always replace current
|
|
88
|
+
// for maxselect = 1 we always replace current option with new selection
|
|
84
89
|
(maxSelect == null || maxSelect == 1 || selected.length < maxSelect)) {
|
|
85
90
|
searchText = ``; // reset search string on selection
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
91
|
+
const option = _options.find((op) => op.label === label);
|
|
92
|
+
if (!option) {
|
|
88
93
|
console.error(`MultiSelect: option with label ${label} not found`);
|
|
89
94
|
return;
|
|
90
95
|
}
|
|
91
96
|
if (maxSelect === 1) {
|
|
92
|
-
selected = [
|
|
97
|
+
selected = [option];
|
|
93
98
|
}
|
|
94
99
|
else {
|
|
95
|
-
selected = [
|
|
100
|
+
selected = [option, ...selected];
|
|
96
101
|
}
|
|
97
102
|
if (selected.length === maxSelect)
|
|
98
103
|
setOptionsVisible(false);
|
|
99
|
-
dispatch(`add`, {
|
|
100
|
-
dispatch(`change`, {
|
|
104
|
+
dispatch(`add`, { option });
|
|
105
|
+
dispatch(`change`, { option, type: `add` });
|
|
101
106
|
}
|
|
102
107
|
}
|
|
103
108
|
function remove(label) {
|
|
104
109
|
if (selected.length === 0 || readonly)
|
|
105
110
|
return;
|
|
106
|
-
selected = selected.filter((
|
|
107
|
-
const
|
|
108
|
-
dispatch(`remove`, {
|
|
109
|
-
dispatch(`change`, {
|
|
111
|
+
selected = selected.filter((option) => label !== option.label);
|
|
112
|
+
const option = _options.find((option) => option.label === label);
|
|
113
|
+
dispatch(`remove`, { option });
|
|
114
|
+
dispatch(`change`, { option, type: `remove` });
|
|
110
115
|
}
|
|
111
116
|
function setOptionsVisible(show) {
|
|
112
117
|
// nothing to do if visibility is already as intended
|
|
@@ -130,9 +135,7 @@ function handleKeydown(event) {
|
|
|
130
135
|
// on enter key: toggle active option and reset search text
|
|
131
136
|
else if (event.key === `Enter`) {
|
|
132
137
|
if (activeOption) {
|
|
133
|
-
const { label
|
|
134
|
-
if (disabled)
|
|
135
|
-
return;
|
|
138
|
+
const { label } = activeOption;
|
|
136
139
|
selectedLabels.includes(label) ? remove(label) : add(label);
|
|
137
140
|
searchText = ``;
|
|
138
141
|
} // no active option means the options dropdown is closed in which case enter means open it
|
|
@@ -148,17 +151,30 @@ function handleKeydown(event) {
|
|
|
148
151
|
}
|
|
149
152
|
const increment = event.key === `ArrowUp` ? -1 : 1;
|
|
150
153
|
const newActiveIdx = matchingEnabledOptions.indexOf(activeOption) + increment;
|
|
154
|
+
const ulOps = document.querySelector(`ul.options`);
|
|
151
155
|
if (newActiveIdx < 0) {
|
|
152
156
|
// wrap around top
|
|
153
157
|
activeOption = matchingEnabledOptions[matchingEnabledOptions.length - 1];
|
|
154
|
-
|
|
158
|
+
if (ulOps)
|
|
159
|
+
ulOps.scrollTop = ulOps.scrollHeight;
|
|
155
160
|
}
|
|
156
161
|
else if (newActiveIdx === matchingEnabledOptions.length) {
|
|
162
|
+
// wrap around bottom
|
|
157
163
|
activeOption = matchingEnabledOptions[0];
|
|
158
|
-
|
|
164
|
+
if (ulOps)
|
|
165
|
+
ulOps.scrollTop = 0;
|
|
159
166
|
}
|
|
160
|
-
else
|
|
167
|
+
else {
|
|
168
|
+
// default case
|
|
161
169
|
activeOption = matchingEnabledOptions[newActiveIdx];
|
|
170
|
+
const li = document.querySelector(`ul.options > li.active`);
|
|
171
|
+
// scrollIntoViewIfNeeded() scrolls top edge of element into view so when moving
|
|
172
|
+
// downwards, we scroll to next sibling to make element fully visible
|
|
173
|
+
if (increment === 1)
|
|
174
|
+
li?.nextSibling?.scrollIntoViewIfNeeded();
|
|
175
|
+
else
|
|
176
|
+
li?.scrollIntoViewIfNeeded();
|
|
177
|
+
}
|
|
162
178
|
}
|
|
163
179
|
else if (event.key === `Backspace`) {
|
|
164
180
|
const label = selectedLabels.pop();
|
|
@@ -167,8 +183,8 @@ function handleKeydown(event) {
|
|
|
167
183
|
}
|
|
168
184
|
}
|
|
169
185
|
const removeAll = () => {
|
|
170
|
-
dispatch(`
|
|
171
|
-
dispatch(`change`, {
|
|
186
|
+
dispatch(`removeAll`, { options: selected });
|
|
187
|
+
dispatch(`change`, { options: selected, type: `removeAll` });
|
|
172
188
|
selected = [];
|
|
173
189
|
searchText = ``;
|
|
174
190
|
};
|
|
@@ -181,7 +197,7 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
|
|
|
181
197
|
};
|
|
182
198
|
</script>
|
|
183
199
|
|
|
184
|
-
<!-- z-index: 2 when showOptions is true ensures the ul.
|
|
200
|
+
<!-- z-index: 2 when showOptions is true ensures the ul.selected of one <MultiSelect />
|
|
185
201
|
display above those of another following shortly after it -->
|
|
186
202
|
<div
|
|
187
203
|
{id}
|
|
@@ -194,31 +210,24 @@ display above those of another following shortly after it -->
|
|
|
194
210
|
use:onClickOutside={() => dispatch(`blur`)}
|
|
195
211
|
>
|
|
196
212
|
<ExpandIcon height="14pt" style="padding: 0 3pt 0 1pt;" />
|
|
197
|
-
<ul class="
|
|
198
|
-
{#
|
|
199
|
-
<
|
|
200
|
-
{
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
>
|
|
216
|
-
<CrossIcon height="12pt" />
|
|
217
|
-
</button>
|
|
218
|
-
{/if}
|
|
219
|
-
</li>
|
|
220
|
-
{/each}
|
|
221
|
-
{/if}
|
|
213
|
+
<ul class="selected {ulSelectedClass}">
|
|
214
|
+
{#each selected as option, idx}
|
|
215
|
+
<li class={liSelectedClass}>
|
|
216
|
+
<slot name="renderSelected" {option} {idx}>
|
|
217
|
+
{option.label}
|
|
218
|
+
</slot>
|
|
219
|
+
{#if !readonly}
|
|
220
|
+
<button
|
|
221
|
+
on:mouseup|stopPropagation={() => remove(option.label)}
|
|
222
|
+
on:keydown={handleEnterAndSpaceKeys(() => remove(option.label))}
|
|
223
|
+
type="button"
|
|
224
|
+
title="{removeBtnTitle} {option.label}"
|
|
225
|
+
>
|
|
226
|
+
<CrossIcon height="12pt" />
|
|
227
|
+
</button>
|
|
228
|
+
{/if}
|
|
229
|
+
</li>
|
|
230
|
+
{/each}
|
|
222
231
|
<input
|
|
223
232
|
bind:this={input}
|
|
224
233
|
autocomplete="off"
|
|
@@ -234,7 +243,9 @@ display above those of another following shortly after it -->
|
|
|
234
243
|
<ReadOnlyIcon height="14pt" />
|
|
235
244
|
{:else if selected.length > 0}
|
|
236
245
|
{#if maxSelect !== null && maxSelect > 1}
|
|
237
|
-
<
|
|
246
|
+
<Wiggle bind:wiggle angle={20}>
|
|
247
|
+
<span style="padding: 0 3pt;">{maxSelectMsg(selected.length, maxSelect)}</span>
|
|
248
|
+
</Wiggle>
|
|
238
249
|
{/if}
|
|
239
250
|
<button
|
|
240
251
|
type="button"
|
|
@@ -253,7 +264,9 @@ display above those of another following shortly after it -->
|
|
|
253
264
|
class:hidden={!showOptions}
|
|
254
265
|
transition:fly|local={{ duration: 300, y: 40 }}
|
|
255
266
|
>
|
|
256
|
-
{#each matchingOptions as
|
|
267
|
+
{#each matchingOptions as option, idx}
|
|
268
|
+
{@const { label, disabled, title = null, selectedTitle } = option}
|
|
269
|
+
{@const { disabledTitle = defaultDisabledTitle } = option}
|
|
257
270
|
<li
|
|
258
271
|
on:mouseup|preventDefault|stopPropagation
|
|
259
272
|
on:mousedown|preventDefault|stopPropagation={() => {
|
|
@@ -266,7 +279,9 @@ display above those of another following shortly after it -->
|
|
|
266
279
|
title={disabled ? disabledTitle : (isSelected(label) && selectedTitle) || title}
|
|
267
280
|
class={liOptionClass}
|
|
268
281
|
>
|
|
269
|
-
{
|
|
282
|
+
<slot name="renderOptions" {option} {idx}>
|
|
283
|
+
{option.label}
|
|
284
|
+
</slot>
|
|
270
285
|
</li>
|
|
271
286
|
{:else}
|
|
272
287
|
{noOptionsMsg}
|
|
@@ -276,26 +291,28 @@ display above those of another following shortly after it -->
|
|
|
276
291
|
</div>
|
|
277
292
|
|
|
278
293
|
<style>
|
|
279
|
-
:where(.multiselect) {
|
|
294
|
+
:where(div.multiselect) {
|
|
280
295
|
position: relative;
|
|
281
296
|
margin: 1em 0;
|
|
282
297
|
border: var(--sms-border, 1pt solid lightgray);
|
|
283
298
|
border-radius: var(--sms-border-radius, 5pt);
|
|
299
|
+
background: var(--sms-input-bg);
|
|
300
|
+
height: var(--sms-input-height, 2em);
|
|
284
301
|
align-items: center;
|
|
285
302
|
min-height: 18pt;
|
|
286
303
|
display: flex;
|
|
287
304
|
cursor: text;
|
|
288
305
|
padding: 0 3pt;
|
|
289
306
|
}
|
|
290
|
-
:where(.multiselect:focus-within) {
|
|
307
|
+
:where(div.multiselect:focus-within) {
|
|
291
308
|
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
|
|
292
309
|
}
|
|
293
|
-
:where(.multiselect.readonly) {
|
|
310
|
+
:where(div.multiselect.readonly) {
|
|
294
311
|
background: var(--sms-readonly-bg, lightgray);
|
|
295
312
|
}
|
|
296
313
|
|
|
297
|
-
:where(ul.
|
|
298
|
-
background: var(--sms-
|
|
314
|
+
:where(ul.selected > li) {
|
|
315
|
+
background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue));
|
|
299
316
|
align-items: center;
|
|
300
317
|
border-radius: 4pt;
|
|
301
318
|
display: flex;
|
|
@@ -305,7 +322,7 @@ display above those of another following shortly after it -->
|
|
|
305
322
|
white-space: nowrap;
|
|
306
323
|
height: 16pt;
|
|
307
324
|
}
|
|
308
|
-
:where(ul.
|
|
325
|
+
:where(ul.selected > li button, button.remove-all) {
|
|
309
326
|
align-items: center;
|
|
310
327
|
border-radius: 50%;
|
|
311
328
|
display: flex;
|
|
@@ -320,29 +337,32 @@ display above those of another following shortly after it -->
|
|
|
320
337
|
outline: none;
|
|
321
338
|
padding: 0 2pt;
|
|
322
339
|
}
|
|
323
|
-
:where(ul.
|
|
340
|
+
:where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
|
|
324
341
|
color: var(--sms-remove-x-hover-focus-color, lightskyblue);
|
|
325
342
|
}
|
|
326
343
|
:where(button:focus) {
|
|
327
|
-
color: var(--sms-remove-x-hover-focus-color, lightskyblue);
|
|
328
344
|
transform: scale(1.04);
|
|
329
345
|
}
|
|
330
346
|
|
|
331
|
-
:where(.multiselect input) {
|
|
347
|
+
:where(div.multiselect input) {
|
|
332
348
|
border: none;
|
|
333
349
|
outline: none;
|
|
334
350
|
background: none;
|
|
335
351
|
color: var(--sms-text-color, inherit);
|
|
336
352
|
flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
|
|
337
353
|
min-width: 2em;
|
|
354
|
+
/* minimum font-size > 16px ensures iOS doesn't zoom in when focusing input */
|
|
355
|
+
/* https://stackoverflow.com/a/6394497 */
|
|
356
|
+
font-size: calc(16px + 0.1vw);
|
|
338
357
|
}
|
|
339
358
|
|
|
340
|
-
:where(ul.
|
|
359
|
+
:where(ul.selected) {
|
|
341
360
|
display: flex;
|
|
342
361
|
padding: 0;
|
|
343
362
|
margin: 0;
|
|
344
363
|
flex-wrap: wrap;
|
|
345
364
|
flex: 1;
|
|
365
|
+
overscroll-behavior: none;
|
|
346
366
|
}
|
|
347
367
|
|
|
348
368
|
:where(ul.options) {
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ declare const __propDef: {
|
|
|
16
16
|
noOptionsMsg?: string | undefined;
|
|
17
17
|
activeOption?: Option | null | undefined;
|
|
18
18
|
outerDivClass?: string | undefined;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
ulSelectedClass?: string | undefined;
|
|
20
|
+
liSelectedClass?: string | undefined;
|
|
21
21
|
ulOptionsClass?: string | undefined;
|
|
22
22
|
liOptionClass?: string | undefined;
|
|
23
23
|
removeBtnTitle?: string | undefined;
|
|
@@ -29,7 +29,16 @@ declare const __propDef: {
|
|
|
29
29
|
} & {
|
|
30
30
|
[evt: string]: CustomEvent<any>;
|
|
31
31
|
};
|
|
32
|
-
slots: {
|
|
32
|
+
slots: {
|
|
33
|
+
renderSelected: {
|
|
34
|
+
option: Option;
|
|
35
|
+
idx: any;
|
|
36
|
+
};
|
|
37
|
+
renderOptions: {
|
|
38
|
+
option: Option;
|
|
39
|
+
idx: any;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
33
42
|
};
|
|
34
43
|
export declare type MultiSelectProps = typeof __propDef.props;
|
|
35
44
|
export declare type MultiSelectEvents = typeof __propDef.events;
|
package/Wiggle.svelte
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script >import { spring } from 'svelte/motion';
|
|
2
|
+
// bind to this state and set it to true from parent
|
|
3
|
+
export let wiggle = false;
|
|
4
|
+
// intended use case: set max value during wiggle for one of angle, scale, dx, dy through props
|
|
5
|
+
export let angle = 0; // try 20
|
|
6
|
+
export let scale = 1; // try 1.2
|
|
7
|
+
export let dx = 0; // try 10
|
|
8
|
+
export let dy = 0; // try 10
|
|
9
|
+
export let duration = 200;
|
|
10
|
+
export let stiffness = 0.05;
|
|
11
|
+
export let damping = 0.1;
|
|
12
|
+
let restState = { angle: 0, scale: 1, dx: 0, dy: 0 };
|
|
13
|
+
let store = spring(restState, { stiffness, damping });
|
|
14
|
+
$: store.set(wiggle ? { scale, angle, dx, dy } : restState);
|
|
15
|
+
$: if (wiggle)
|
|
16
|
+
setTimeout(() => (wiggle = false), duration);
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<span
|
|
20
|
+
style:transform="rotate({$store.angle}deg) scale({$store.scale}) translate({$store.dx}px,
|
|
21
|
+
{$store.dy}px)"
|
|
22
|
+
>
|
|
23
|
+
<slot />
|
|
24
|
+
</span>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: {
|
|
4
|
+
wiggle?: boolean | undefined;
|
|
5
|
+
angle?: number | undefined;
|
|
6
|
+
scale?: number | undefined;
|
|
7
|
+
dx?: number | undefined;
|
|
8
|
+
dy?: number | undefined;
|
|
9
|
+
duration?: number | undefined;
|
|
10
|
+
stiffness?: number | undefined;
|
|
11
|
+
damping?: number | undefined;
|
|
12
|
+
};
|
|
13
|
+
events: {
|
|
14
|
+
[evt: string]: CustomEvent<any>;
|
|
15
|
+
};
|
|
16
|
+
slots: {
|
|
17
|
+
default: {};
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare type WiggleProps = typeof __propDef.props;
|
|
21
|
+
export declare type WiggleEvents = typeof __propDef.events;
|
|
22
|
+
export declare type WiggleSlots = typeof __propDef.slots;
|
|
23
|
+
export default class Wiggle extends SvelteComponentTyped<WiggleProps, WiggleEvents, WiggleSlots> {
|
|
24
|
+
}
|
|
25
|
+
export {};
|
package/package.json
CHANGED
|
@@ -5,33 +5,32 @@
|
|
|
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.1.1",
|
|
9
9
|
"type": "module",
|
|
10
|
-
"svelte": "
|
|
11
|
-
"bugs":
|
|
12
|
-
"url": "https://github.com/janosh/svelte-multiselect/issues"
|
|
13
|
-
},
|
|
10
|
+
"svelte": "index.js",
|
|
11
|
+
"bugs": "https://github.com/janosh/svelte-multiselect/issues",
|
|
14
12
|
"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.
|
|
20
|
-
"eslint-plugin-svelte3": "^3.
|
|
13
|
+
"@sveltejs/adapter-static": "^1.0.0-next.26",
|
|
14
|
+
"@sveltejs/kit": "^1.0.0-next.239",
|
|
15
|
+
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
|
16
|
+
"@typescript-eslint/parser": "^5.10.0",
|
|
17
|
+
"eslint": "^8.7.0",
|
|
18
|
+
"eslint-plugin-svelte3": "^3.4.0",
|
|
21
19
|
"hastscript": "^7.0.2",
|
|
22
20
|
"mdsvex": "^0.9.8",
|
|
23
21
|
"prettier": "^2.5.1",
|
|
24
|
-
"prettier-plugin-svelte": "^2.
|
|
25
|
-
"rehype-autolink-headings": "^6.1.
|
|
26
|
-
"rehype-slug": "^5.0.
|
|
27
|
-
"svelte": "^3.
|
|
28
|
-
"svelte-check": "^2.
|
|
29
|
-
"svelte-
|
|
30
|
-
"svelte-
|
|
31
|
-
"
|
|
22
|
+
"prettier-plugin-svelte": "^2.6.0",
|
|
23
|
+
"rehype-autolink-headings": "^6.1.1",
|
|
24
|
+
"rehype-slug": "^5.0.1",
|
|
25
|
+
"svelte": "^3.46.2",
|
|
26
|
+
"svelte-check": "^2.3.0",
|
|
27
|
+
"svelte-github-corner": "^0.1.0",
|
|
28
|
+
"svelte-preprocess": "^4.10.2",
|
|
29
|
+
"svelte-toc": "^0.2.2",
|
|
30
|
+
"svelte2tsx": "^0.4.14",
|
|
32
31
|
"tslib": "^2.3.1",
|
|
33
|
-
"typescript": "^4.5.
|
|
34
|
-
"vite": "^2.7.
|
|
32
|
+
"typescript": "^4.5.5",
|
|
33
|
+
"vite": "^2.7.13"
|
|
35
34
|
},
|
|
36
35
|
"keywords": [
|
|
37
36
|
"svelte",
|
|
@@ -46,7 +45,6 @@
|
|
|
46
45
|
"exports": {
|
|
47
46
|
"./package.json": "./package.json",
|
|
48
47
|
"./MultiSelect.svelte": "./MultiSelect.svelte",
|
|
49
|
-
"./actions": "./actions.js",
|
|
50
48
|
".": "./index.js"
|
|
51
49
|
}
|
|
52
50
|
}
|
package/readme.md
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
</p>
|
|
6
|
-
|
|
7
|
-
<h1 align="center">Svelte MultiSelect</h1>
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/janosh/svelte-toc/main/static/favicon.svg" alt="Svelte MultiSelect" height=60>
|
|
3
|
+
<br> Svelte MultiSelect
|
|
4
|
+
</h1>
|
|
8
5
|
|
|
9
6
|
<h4 align="center">
|
|
10
7
|
|
|
@@ -14,25 +11,39 @@
|
|
|
14
11
|
|
|
15
12
|
</h4>
|
|
16
13
|
|
|
14
|
+
<div class="hide-in-docs">
|
|
15
|
+
|
|
17
16
|
**[Live demo](https://svelte-multiselect.netlify.app)**.
|
|
18
17
|
|
|
19
18
|
</div>
|
|
20
19
|
|
|
21
|
-
<!-- remove above in docs -->
|
|
22
|
-
|
|
23
20
|
**Keyboard-friendly, zero-dependency multi-select Svelte component.**
|
|
24
21
|
|
|
22
|
+
<slot />
|
|
23
|
+
|
|
25
24
|
## Key Features
|
|
26
25
|
|
|
27
26
|
- **Single / multiple select:** pass `maxSelect={1}` prop to only allow one selection
|
|
28
27
|
- **Dropdowns:** scrollable lists for large numbers of options
|
|
29
28
|
- **Searchable:** start typing to filter options
|
|
30
|
-
- **Tagging:** selected options are recorded as tags
|
|
29
|
+
- **Tagging:** selected options are recorded as tags in the input
|
|
31
30
|
- **Server-side rendering:** no reliance on browser objects like `window` or `document`
|
|
32
31
|
- **Configurable:** see [props](#props)
|
|
33
32
|
- **No dependencies:** needs only Svelte as dev dependency
|
|
34
33
|
- **Keyboard friendly** for mouse-less form completion
|
|
35
34
|
|
|
35
|
+
## Recent breaking changes
|
|
36
|
+
|
|
37
|
+
- 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`).
|
|
38
|
+
- v3.0.0 changed the `event.detail` payload for `'add'`, `'remove'` and `'change'` events from `token` to `option`, e.g.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
on:add={(e) => console.log(e.detail.token.label)} // v2.0.0
|
|
42
|
+
on:add={(e) => console.log(e.detail.option.label)} // v3.0.0
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
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`.
|
|
46
|
+
|
|
36
47
|
## Installation
|
|
37
48
|
|
|
38
49
|
```sh
|
|
@@ -93,53 +104,118 @@ Full list of props/bindable variables for this component:
|
|
|
93
104
|
|
|
94
105
|
</div>
|
|
95
106
|
|
|
107
|
+
## Slots
|
|
108
|
+
|
|
109
|
+
`MultiSelect.svelte` accepts two named slots
|
|
110
|
+
|
|
111
|
+
- `slot="renderOptions"`
|
|
112
|
+
- `slot="renderSelected"`
|
|
113
|
+
|
|
114
|
+
to customize rendering individual options in the dropdown and the list of selected tags, respectively. Each renderer receives the full `option` object along with the zero-indexed position (`idx`) in its list, both available via the `let:` directive:
|
|
115
|
+
|
|
116
|
+
```svelte
|
|
117
|
+
<MultiSelect options={[`Banana`, `Apple`, `Mango`]}>
|
|
118
|
+
<span let:idx let:option slot="renderOptions">
|
|
119
|
+
{idx + 1}. {option.label}
|
|
120
|
+
{option.label === `Mango` ? `🎉` : ``}
|
|
121
|
+
</span>
|
|
122
|
+
|
|
123
|
+
<span let:idx let:option slot="renderSelected">
|
|
124
|
+
#{idx + 1}
|
|
125
|
+
{option.label}
|
|
126
|
+
</span>
|
|
127
|
+
</MultiSelect>
|
|
128
|
+
```
|
|
129
|
+
|
|
96
130
|
## Events
|
|
97
131
|
|
|
98
132
|
`MultiSelect.svelte` dispatches the following events:
|
|
99
133
|
|
|
100
|
-
| name
|
|
101
|
-
|
|
|
102
|
-
| `add`
|
|
103
|
-
| `remove`
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
134
|
+
| name | detail | description |
|
|
135
|
+
| ----------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
136
|
+
| `add` | `{ option: Option }` | Triggers when a new option is selected. |
|
|
137
|
+
| `remove` | `{ option: Option }` | Triggers when one selected option provided as `event.detail.option` is removed. |
|
|
138
|
+
| `removeAll` | `options: Option[]` | Triggers when all selected options are removed. The payload `event.detail.options` gives the options that were previously selected. |
|
|
139
|
+
| `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. |
|
|
140
|
+
| `blur` | none | Triggers when the input field looses focus. |
|
|
106
141
|
|
|
107
142
|
### Examples
|
|
108
143
|
|
|
109
144
|
<!-- prettier-ignore -->
|
|
110
|
-
- `on:add={(event) => console.log(event.detail.
|
|
111
|
-
- `on:remove={(event) => console.log(event.detail.
|
|
112
|
-
- ``on:change={(event) => console.log(`${event.detail.type}: '${event.detail.
|
|
145
|
+
- `on:add={(event) => console.log(event.detail.option.label)}`
|
|
146
|
+
- `on:remove={(event) => console.log(event.detail.option.label)}`.
|
|
147
|
+
- ``on:change={(event) => console.log(`${event.detail.type}: '${event.detail.option.label}'`)}``
|
|
113
148
|
- `on:blur={yourFunctionHere}`
|
|
114
149
|
|
|
115
150
|
```svelte
|
|
116
151
|
<MultiSelect
|
|
117
|
-
on:change={(e) => alert(`You ${e.detail.type}ed '${e.detail.
|
|
152
|
+
on:change={(e) => alert(`You ${e.detail.type}ed '${e.detail.option.label}'`)}
|
|
118
153
|
/>
|
|
119
154
|
```
|
|
120
155
|
|
|
156
|
+
## TypeScript
|
|
157
|
+
|
|
158
|
+
TypeScript users can import the types used for internal type safety:
|
|
159
|
+
|
|
160
|
+
```svelte
|
|
161
|
+
<script lang="ts">
|
|
162
|
+
import MultiSelect, {
|
|
163
|
+
Option,
|
|
164
|
+
Primitive,
|
|
165
|
+
ProtoOption,
|
|
166
|
+
} from 'svelte-multiselect'
|
|
167
|
+
|
|
168
|
+
const myOptions: Option[] = [
|
|
169
|
+
{ label: 'foo', value: 42 },
|
|
170
|
+
{ label: 'bar', value: 69 },
|
|
171
|
+
]
|
|
172
|
+
</script>
|
|
173
|
+
```
|
|
174
|
+
|
|
121
175
|
## Styling
|
|
122
176
|
|
|
123
|
-
There are 3 ways to style this component.
|
|
177
|
+
There are 3 ways to style this component. To understand which options do what, it helps to keep in mind this simplified DOM structure of the component:
|
|
178
|
+
|
|
179
|
+
```svelte
|
|
180
|
+
<div class="multiselect">
|
|
181
|
+
<ul class="selected">
|
|
182
|
+
<li>Selected 1</li>
|
|
183
|
+
<li>Selected 2</li>
|
|
184
|
+
</ul>
|
|
185
|
+
<ul class="options">
|
|
186
|
+
<li>Option 1</li>
|
|
187
|
+
<li>Option 2</li>
|
|
188
|
+
</ul>
|
|
189
|
+
</div>
|
|
190
|
+
```
|
|
124
191
|
|
|
125
192
|
### With CSS variables
|
|
126
193
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
- `
|
|
130
|
-
- `border
|
|
131
|
-
- `
|
|
132
|
-
- `
|
|
133
|
-
- `
|
|
134
|
-
- `
|
|
135
|
-
- `
|
|
136
|
-
- `
|
|
137
|
-
- `
|
|
138
|
-
- `
|
|
139
|
-
- `
|
|
140
|
-
- `
|
|
141
|
-
- `
|
|
142
|
-
- `
|
|
194
|
+
If you only want to make small adjustments, you can pass the following CSS variables directly to the component as props or define them in a `:global()` CSS context.
|
|
195
|
+
|
|
196
|
+
- `div.multiselect`:
|
|
197
|
+
- `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.
|
|
198
|
+
- `border-radius: var(--sms-border-radius, 5pt)`: Input border radius.
|
|
199
|
+
- `background: var(--sms-input-bg)`: Input background.
|
|
200
|
+
- `height: var(--sms-input-height, 2em)`: Input height.
|
|
201
|
+
- `border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue))`: Border when focused. Falls back to `--sms-active-color` if not set which in turn falls back on `cornflowerblue`.
|
|
202
|
+
- `background: var(--sms-readonly-bg, lightgray)`: Background when in readonly state.
|
|
203
|
+
- `div.multiselect input`
|
|
204
|
+
- `color: var(--sms-text-color, inherit)`: Input text color.
|
|
205
|
+
- `ul.selected > li`:
|
|
206
|
+
- `background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue))`: Background of selected options.
|
|
207
|
+
- `ul.selected > li button:hover, button.remove-all:hover`
|
|
208
|
+
- `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.
|
|
209
|
+
- `ul.options`
|
|
210
|
+
- `background: var(--sms-options-bg, white)`: Background of options list.
|
|
211
|
+
- `ul.options > li.selected`
|
|
212
|
+
- `background: var(--sms-li-selected-bg, inherit)`: Background of selected list items in options pane.
|
|
213
|
+
- `color: var(--sms-li-selected-color, inherit)`: Text color of selected list items in options pane.
|
|
214
|
+
- `ul.options > li.active`
|
|
215
|
+
- `background: var(--sms-li-active-bg, var(--sms-active-color, cornflowerblue))`: Background of active (currently with arrow keys highlighted) list item.
|
|
216
|
+
- `ul.options > li.disabled`
|
|
217
|
+
- `background: var(--sms-li-disabled-bg, #f5f5f6)`: Background of disabled options in the dropdown list.
|
|
218
|
+
- `color: var(--sms-li-disabled-text, #b8b8b8)`: Text color of disabled option in the dropdown list.
|
|
143
219
|
|
|
144
220
|
For example, to change the background color of the options dropdown:
|
|
145
221
|
|
|
@@ -152,38 +228,38 @@ For example, to change the background color of the options dropdown:
|
|
|
152
228
|
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).
|
|
153
229
|
|
|
154
230
|
- `outerDivClass`
|
|
155
|
-
- `
|
|
156
|
-
- `
|
|
231
|
+
- `ulSelectedClass`
|
|
232
|
+
- `liSelectedClass`
|
|
157
233
|
- `ulOptionsClass`
|
|
158
234
|
- `liOptionClass`
|
|
159
235
|
|
|
160
236
|
This simplified version of the DOM structure of this component shows where these classes are inserted:
|
|
161
237
|
|
|
162
238
|
```svelte
|
|
163
|
-
<div class={outerDivClass}>
|
|
164
|
-
<ul class={
|
|
165
|
-
<li class={
|
|
166
|
-
<li class={
|
|
239
|
+
<div class="multiselect {outerDivClass}">
|
|
240
|
+
<ul class="selected {ulSelectedClass}">
|
|
241
|
+
<li class={liSelectedClass}>Selected 1</li>
|
|
242
|
+
<li class={liSelectedClass}>Selected 2</li>
|
|
167
243
|
</ul>
|
|
168
|
-
<ul class={ulOptionsClass}>
|
|
169
|
-
<li class={liOptionClass}>
|
|
170
|
-
<li class={liOptionClass}>
|
|
244
|
+
<ul class="options {ulOptionsClass}">
|
|
245
|
+
<li class={liOptionClass}>Option 1</li>
|
|
246
|
+
<li class={liOptionClass}>Option 2</li>
|
|
171
247
|
</ul>
|
|
172
248
|
</div>
|
|
173
249
|
```
|
|
174
250
|
|
|
175
251
|
### Granular control through global CSS
|
|
176
252
|
|
|
177
|
-
You can alternatively style every part of this component with more fine-grained control by using the following `:global()` CSS selectors. `ul.
|
|
253
|
+
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.
|
|
178
254
|
|
|
179
255
|
```css
|
|
180
256
|
:global(.multiselect) {
|
|
181
257
|
/* top-level wrapper div */
|
|
182
258
|
}
|
|
183
|
-
:global(.multiselect ul.
|
|
259
|
+
:global(.multiselect ul.selected > li) {
|
|
184
260
|
/* selected options */
|
|
185
261
|
}
|
|
186
|
-
:global(.multiselect ul.
|
|
262
|
+
:global(.multiselect ul.selected > li button),
|
|
187
263
|
:global(.multiselect button.remove-all) {
|
|
188
264
|
/* buttons to remove a single or all selected options at once */
|
|
189
265
|
}
|