svelte-multiselect 3.1.0 → 3.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MultiSelect.svelte +70 -58
- package/MultiSelect.svelte.d.ts +4 -3
- package/Wiggle.svelte +24 -0
- package/Wiggle.svelte.d.ts +25 -0
- package/actions.js +0 -23
- package/package.json +13 -13
- package/readme.md +128 -70
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 = [];
|
|
@@ -11,10 +12,15 @@ export let readonly = false;
|
|
|
11
12
|
export let options;
|
|
12
13
|
export let input = null;
|
|
13
14
|
export let placeholder = undefined;
|
|
14
|
-
export let name = undefined;
|
|
15
15
|
export let id = undefined;
|
|
16
|
+
export let name = id;
|
|
16
17
|
export let noOptionsMsg = `No matching options`;
|
|
17
18
|
export let activeOption = null;
|
|
19
|
+
export let filterFunc = (op, searchText) => {
|
|
20
|
+
if (!searchText)
|
|
21
|
+
return true;
|
|
22
|
+
return `${op.label}`.toLowerCase().includes(searchText.toLowerCase());
|
|
23
|
+
};
|
|
18
24
|
export let outerDivClass = ``;
|
|
19
25
|
export let ulSelectedClass = ``;
|
|
20
26
|
export let liSelectedClass = ``;
|
|
@@ -34,6 +40,7 @@ if (!Array.isArray(selected))
|
|
|
34
40
|
onMount(() => {
|
|
35
41
|
selected = _options.filter((op) => op?.preselected);
|
|
36
42
|
});
|
|
43
|
+
let wiggle = false;
|
|
37
44
|
function isObject(item) {
|
|
38
45
|
return typeof item === `object` && !Array.isArray(item) && item !== null;
|
|
39
46
|
}
|
|
@@ -64,11 +71,7 @@ const dispatch = createEventDispatcher();
|
|
|
64
71
|
let searchText = ``;
|
|
65
72
|
let showOptions = false;
|
|
66
73
|
// 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
|
-
});
|
|
74
|
+
$: matchingOptions = _options.filter((op) => filterFunc(op, searchText));
|
|
72
75
|
$: matchingEnabledOptions = matchingOptions.filter((op) => !op.disabled);
|
|
73
76
|
$: if (
|
|
74
77
|
// if there was an active option but it's not in the filtered list of options
|
|
@@ -79,6 +82,8 @@ $: if (
|
|
|
79
82
|
// make the first filtered option active
|
|
80
83
|
activeOption = matchingEnabledOptions[0];
|
|
81
84
|
function add(label) {
|
|
85
|
+
if (selected.length - (maxSelect ?? 0) < 1)
|
|
86
|
+
wiggle = true;
|
|
82
87
|
if (!readonly &&
|
|
83
88
|
!selectedLabels.includes(label) &&
|
|
84
89
|
// for maxselect = 1 we always replace current option with new selection
|
|
@@ -196,11 +201,10 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
|
|
|
196
201
|
<!-- z-index: 2 when showOptions is true ensures the ul.selected of one <MultiSelect />
|
|
197
202
|
display above those of another following shortly after it -->
|
|
198
203
|
<div
|
|
199
|
-
{id}
|
|
200
204
|
class="multiselect {outerDivClass}"
|
|
201
205
|
class:readonly
|
|
202
206
|
class:single={maxSelect == 1}
|
|
203
|
-
|
|
207
|
+
class:open={showOptions}
|
|
204
208
|
on:mouseup|stopPropagation={() => setOptionsVisible(true)}
|
|
205
209
|
use:onClickOutside={() => setOptionsVisible(false)}
|
|
206
210
|
use:onClickOutside={() => dispatch(`blur`)}
|
|
@@ -209,7 +213,7 @@ display above those of another following shortly after it -->
|
|
|
209
213
|
<ul class="selected {ulSelectedClass}">
|
|
210
214
|
{#each selected as option, idx}
|
|
211
215
|
<li class={liSelectedClass}>
|
|
212
|
-
<slot name="
|
|
216
|
+
<slot name="renderSelected" {option} {idx}>
|
|
213
217
|
{option.label}
|
|
214
218
|
</slot>
|
|
215
219
|
{#if !readonly}
|
|
@@ -224,22 +228,25 @@ display above those of another following shortly after it -->
|
|
|
224
228
|
{/if}
|
|
225
229
|
</li>
|
|
226
230
|
{/each}
|
|
227
|
-
<input
|
|
228
|
-
bind:this={input}
|
|
229
|
-
autocomplete="off"
|
|
230
|
-
bind:value={searchText}
|
|
231
|
-
on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
|
|
232
|
-
on:keydown={handleKeydown}
|
|
233
|
-
on:focus={() => setOptionsVisible(true)}
|
|
234
|
-
{name}
|
|
235
|
-
placeholder={selectedLabels.length ? `` : placeholder}
|
|
236
|
-
/>
|
|
237
231
|
</ul>
|
|
232
|
+
<input
|
|
233
|
+
bind:this={input}
|
|
234
|
+
autocomplete="off"
|
|
235
|
+
bind:value={searchText}
|
|
236
|
+
on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
|
|
237
|
+
on:keydown={handleKeydown}
|
|
238
|
+
on:focus={() => setOptionsVisible(true)}
|
|
239
|
+
{id}
|
|
240
|
+
{name}
|
|
241
|
+
placeholder={selectedLabels.length ? `` : placeholder}
|
|
242
|
+
/>
|
|
238
243
|
{#if readonly}
|
|
239
244
|
<ReadOnlyIcon height="14pt" />
|
|
240
245
|
{:else if selected.length > 0}
|
|
241
|
-
{#if maxSelect !== null &&
|
|
242
|
-
<
|
|
246
|
+
{#if maxSelect !== null && maxSelectMsg !== null}
|
|
247
|
+
<Wiggle bind:wiggle angle={20}>
|
|
248
|
+
<span style="padding: 0 3pt;">{maxSelectMsg(selected.length, maxSelect)}</span>
|
|
249
|
+
</Wiggle>
|
|
243
250
|
{/if}
|
|
244
251
|
<button
|
|
245
252
|
type="button"
|
|
@@ -273,55 +280,66 @@ display above those of another following shortly after it -->
|
|
|
273
280
|
title={disabled ? disabledTitle : (isSelected(label) && selectedTitle) || title}
|
|
274
281
|
class={liOptionClass}
|
|
275
282
|
>
|
|
276
|
-
<slot name="
|
|
283
|
+
<slot name="renderOptions" {option} {idx}>
|
|
277
284
|
{option.label}
|
|
278
285
|
</slot>
|
|
279
286
|
</li>
|
|
280
287
|
{:else}
|
|
281
|
-
{noOptionsMsg}
|
|
288
|
+
<span>{noOptionsMsg}</span>
|
|
282
289
|
{/each}
|
|
283
290
|
</ul>
|
|
284
291
|
{/key}
|
|
285
292
|
</div>
|
|
286
293
|
|
|
287
294
|
<style>
|
|
288
|
-
:where(.multiselect) {
|
|
295
|
+
:where(div.multiselect) {
|
|
289
296
|
position: relative;
|
|
290
297
|
margin: 1em 0;
|
|
291
|
-
border: var(--sms-border, 1pt solid lightgray);
|
|
292
|
-
border-radius: var(--sms-border-radius, 5pt);
|
|
293
298
|
align-items: center;
|
|
294
299
|
min-height: 18pt;
|
|
295
300
|
display: flex;
|
|
296
301
|
cursor: text;
|
|
297
302
|
padding: 0 3pt;
|
|
303
|
+
border: var(--sms-border, 1pt solid lightgray);
|
|
304
|
+
border-radius: var(--sms-border-radius, 5pt);
|
|
305
|
+
background: var(--sms-input-bg);
|
|
306
|
+
height: var(--sms-input-height, 2em);
|
|
307
|
+
}
|
|
308
|
+
:where(div.multiselect.open) {
|
|
309
|
+
z-index: var(--sms-open-z-index, 4);
|
|
298
310
|
}
|
|
299
|
-
:where(.multiselect:focus-within) {
|
|
311
|
+
:where(div.multiselect:focus-within) {
|
|
300
312
|
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
|
|
301
313
|
}
|
|
302
|
-
:where(.multiselect.readonly) {
|
|
314
|
+
:where(div.multiselect.readonly) {
|
|
303
315
|
background: var(--sms-readonly-bg, lightgray);
|
|
304
316
|
}
|
|
305
317
|
|
|
306
|
-
:where(
|
|
307
|
-
|
|
318
|
+
:where(div.multiselect > ul.selected) {
|
|
319
|
+
display: flex;
|
|
320
|
+
padding: 0;
|
|
321
|
+
margin: 0;
|
|
322
|
+
flex-wrap: wrap;
|
|
323
|
+
}
|
|
324
|
+
:where(div.multiselect > ul.selected > li) {
|
|
308
325
|
align-items: center;
|
|
309
326
|
border-radius: 4pt;
|
|
310
327
|
display: flex;
|
|
311
328
|
margin: 2pt;
|
|
312
|
-
padding: 0 0 0
|
|
329
|
+
padding: 0 0 0 5pt;
|
|
313
330
|
transition: 0.3s;
|
|
314
331
|
white-space: nowrap;
|
|
315
|
-
|
|
332
|
+
background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue));
|
|
333
|
+
height: var(--sms-selected-li-height);
|
|
316
334
|
}
|
|
317
|
-
:where(ul.selected > li button, button.remove-all) {
|
|
335
|
+
:where(div.multiselect > ul.selected > li button, button.remove-all) {
|
|
318
336
|
align-items: center;
|
|
319
337
|
border-radius: 50%;
|
|
320
338
|
display: flex;
|
|
321
339
|
cursor: pointer;
|
|
322
340
|
transition: 0.2s;
|
|
323
341
|
}
|
|
324
|
-
:where(button) {
|
|
342
|
+
:where(div.multiselect button) {
|
|
325
343
|
color: inherit;
|
|
326
344
|
background: transparent;
|
|
327
345
|
border: none;
|
|
@@ -329,36 +347,26 @@ display above those of another following shortly after it -->
|
|
|
329
347
|
outline: none;
|
|
330
348
|
padding: 0 2pt;
|
|
331
349
|
}
|
|
332
|
-
:where(ul.selected > li button:hover, button.remove-all:hover) {
|
|
350
|
+
:where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
|
|
333
351
|
color: var(--sms-remove-x-hover-focus-color, lightskyblue);
|
|
334
352
|
}
|
|
335
|
-
:where(button:focus) {
|
|
336
|
-
color: var(--sms-remove-x-hover-focus-color, lightskyblue);
|
|
353
|
+
:where(div.multiselect > button:focus) {
|
|
337
354
|
transform: scale(1.04);
|
|
338
355
|
}
|
|
339
356
|
|
|
340
|
-
:where(.multiselect input) {
|
|
357
|
+
:where(div.multiselect > input) {
|
|
341
358
|
border: none;
|
|
342
359
|
outline: none;
|
|
343
360
|
background: none;
|
|
344
|
-
color: var(--sms-text-color, inherit);
|
|
345
361
|
flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
|
|
346
362
|
min-width: 2em;
|
|
347
363
|
/* minimum font-size > 16px ensures iOS doesn't zoom in when focusing input */
|
|
348
364
|
/* https://stackoverflow.com/a/6394497 */
|
|
349
365
|
font-size: calc(16px + 0.1vw);
|
|
366
|
+
color: var(--sms-text-color, inherit);
|
|
350
367
|
}
|
|
351
368
|
|
|
352
|
-
:where(ul.
|
|
353
|
-
display: flex;
|
|
354
|
-
padding: 0;
|
|
355
|
-
margin: 0;
|
|
356
|
-
flex-wrap: wrap;
|
|
357
|
-
flex: 1;
|
|
358
|
-
overscroll-behavior: none;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
:where(ul.options) {
|
|
369
|
+
:where(div.multiselect > ul.options) {
|
|
362
370
|
list-style: none;
|
|
363
371
|
max-height: 50vh;
|
|
364
372
|
padding: 0;
|
|
@@ -368,15 +376,20 @@ display above those of another following shortly after it -->
|
|
|
368
376
|
border-radius: 1ex;
|
|
369
377
|
overflow: auto;
|
|
370
378
|
background: var(--sms-options-bg, white);
|
|
379
|
+
overscroll-behavior: var(--sms-options-overscroll, none);
|
|
371
380
|
}
|
|
372
|
-
:where(ul.options.hidden) {
|
|
381
|
+
:where(div.multiselect > ul.options.hidden) {
|
|
373
382
|
visibility: hidden;
|
|
374
383
|
}
|
|
375
|
-
:where(ul.options li) {
|
|
384
|
+
:where(div.multiselect > ul.options > li) {
|
|
376
385
|
padding: 3pt 2ex;
|
|
377
386
|
cursor: pointer;
|
|
378
387
|
}
|
|
379
|
-
|
|
388
|
+
/* for noOptionsMsg */
|
|
389
|
+
:where(div.multiselect > ul.options span) {
|
|
390
|
+
padding: 3pt 2ex;
|
|
391
|
+
}
|
|
392
|
+
:where(div.multiselect > ul.options > li.selected) {
|
|
380
393
|
border-left: var(
|
|
381
394
|
--sms-li-selected-border-left,
|
|
382
395
|
3pt solid var(--sms-selected-color, green)
|
|
@@ -384,22 +397,21 @@ display above those of another following shortly after it -->
|
|
|
384
397
|
background: var(--sms-li-selected-bg, inherit);
|
|
385
398
|
color: var(--sms-li-selected-color, inherit);
|
|
386
399
|
}
|
|
387
|
-
:where(ul.options li:not(.selected):hover) {
|
|
400
|
+
:where(div.multiselect > ul.options > li:not(.selected):hover) {
|
|
388
401
|
border-left: var(
|
|
389
402
|
--sms-li-not-selected-hover-border-left,
|
|
390
403
|
3pt solid var(--sms-active-color, cornflowerblue)
|
|
391
404
|
);
|
|
392
|
-
border-left: 3pt solid var(--blue);
|
|
393
405
|
}
|
|
394
|
-
:where(ul.options li.active) {
|
|
406
|
+
:where(div.multiselect > ul.options > li.active) {
|
|
395
407
|
background: var(--sms-li-active-bg, var(--sms-active-color, cornflowerblue));
|
|
396
408
|
}
|
|
397
|
-
:where(ul.options li.disabled) {
|
|
409
|
+
:where(div.multiselect > ul.options > li.disabled) {
|
|
410
|
+
cursor: not-allowed;
|
|
398
411
|
background: var(--sms-li-disabled-bg, #f5f5f6);
|
|
399
412
|
color: var(--sms-li-disabled-text, #b8b8b8);
|
|
400
|
-
cursor: not-allowed;
|
|
401
413
|
}
|
|
402
|
-
:where(ul.options li.disabled:hover) {
|
|
414
|
+
:where(div.multiselect > ul.options > li.disabled:hover) {
|
|
403
415
|
border-left: unset;
|
|
404
416
|
}
|
|
405
417
|
</style>
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -11,10 +11,11 @@ declare const __propDef: {
|
|
|
11
11
|
options: ProtoOption[];
|
|
12
12
|
input?: HTMLInputElement | null | undefined;
|
|
13
13
|
placeholder?: string | undefined;
|
|
14
|
-
name?: string | undefined;
|
|
15
14
|
id?: string | undefined;
|
|
15
|
+
name?: string | undefined;
|
|
16
16
|
noOptionsMsg?: string | undefined;
|
|
17
17
|
activeOption?: Option | null | undefined;
|
|
18
|
+
filterFunc?: ((op: Option, searchText: string) => boolean) | undefined;
|
|
18
19
|
outerDivClass?: string | undefined;
|
|
19
20
|
ulSelectedClass?: string | undefined;
|
|
20
21
|
liSelectedClass?: string | undefined;
|
|
@@ -30,11 +31,11 @@ declare const __propDef: {
|
|
|
30
31
|
[evt: string]: CustomEvent<any>;
|
|
31
32
|
};
|
|
32
33
|
slots: {
|
|
33
|
-
|
|
34
|
+
renderSelected: {
|
|
34
35
|
option: Option;
|
|
35
36
|
idx: any;
|
|
36
37
|
};
|
|
37
|
-
|
|
38
|
+
renderOptions: {
|
|
38
39
|
option: Option;
|
|
39
40
|
idx: any;
|
|
40
41
|
};
|
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/actions.js
CHANGED
|
@@ -14,26 +14,3 @@ export function onClickOutside(node, cb) {
|
|
|
14
14
|
},
|
|
15
15
|
};
|
|
16
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
|
-
// }
|
package/package.json
CHANGED
|
@@ -5,32 +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": "3.
|
|
8
|
+
"version": "3.2.2",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"svelte": "index.js",
|
|
11
11
|
"bugs": "https://github.com/janosh/svelte-multiselect/issues",
|
|
12
12
|
"devDependencies": {
|
|
13
|
-
"@sveltejs/adapter-static": "^1.0.0-next.
|
|
14
|
-
"@sveltejs/kit": "^1.0.0-next.
|
|
15
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
16
|
-
"@typescript-eslint/parser": "^5.
|
|
17
|
-
"eslint": "^8.
|
|
13
|
+
"@sveltejs/adapter-static": "^1.0.0-next.28",
|
|
14
|
+
"@sveltejs/kit": "^1.0.0-next.269",
|
|
15
|
+
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
|
16
|
+
"@typescript-eslint/parser": "^5.12.0",
|
|
17
|
+
"eslint": "^8.9.0",
|
|
18
18
|
"eslint-plugin-svelte3": "^3.4.0",
|
|
19
19
|
"hastscript": "^7.0.2",
|
|
20
|
-
"mdsvex": "^0.
|
|
20
|
+
"mdsvex": "^0.10.5",
|
|
21
21
|
"prettier": "^2.5.1",
|
|
22
22
|
"prettier-plugin-svelte": "^2.6.0",
|
|
23
23
|
"rehype-autolink-headings": "^6.1.1",
|
|
24
24
|
"rehype-slug": "^5.0.1",
|
|
25
|
-
"svelte": "^3.46.
|
|
26
|
-
"svelte-check": "^2.3
|
|
25
|
+
"svelte": "^3.46.4",
|
|
26
|
+
"svelte-check": "^2.4.3",
|
|
27
27
|
"svelte-github-corner": "^0.1.0",
|
|
28
|
-
"svelte-preprocess": "^4.10.
|
|
29
|
-
"svelte-toc": "^0.2.
|
|
30
|
-
"svelte2tsx": "^0.
|
|
28
|
+
"svelte-preprocess": "^4.10.3",
|
|
29
|
+
"svelte-toc": "^0.2.5",
|
|
30
|
+
"svelte2tsx": "^0.5.3",
|
|
31
31
|
"tslib": "^2.3.1",
|
|
32
32
|
"typescript": "^4.5.5",
|
|
33
|
-
"vite": "^2.
|
|
33
|
+
"vite": "^2.8.2"
|
|
34
34
|
},
|
|
35
35
|
"keywords": [
|
|
36
36
|
"svelte",
|
package/readme.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
[](https://app.netlify.com/sites/svelte-multiselect/deploys)
|
|
9
9
|
[](https://npmjs.com/package/svelte-multiselect)
|
|
10
10
|
[](https://results.pre-commit.ci/latest/github/janosh/svelte-multiselect/main)
|
|
11
|
+

|
|
11
12
|
|
|
12
13
|
</h4>
|
|
13
14
|
|
|
@@ -21,12 +22,12 @@
|
|
|
21
22
|
|
|
22
23
|
<slot />
|
|
23
24
|
|
|
24
|
-
## Key
|
|
25
|
+
## Key features
|
|
25
26
|
|
|
26
27
|
- **Single / multiple select:** pass `maxSelect={1}` prop to only allow one selection
|
|
27
28
|
- **Dropdowns:** scrollable lists for large numbers of options
|
|
28
29
|
- **Searchable:** start typing to filter options
|
|
29
|
-
- **Tagging:** selected options are recorded as tags
|
|
30
|
+
- **Tagging:** selected options are recorded as tags in the input
|
|
30
31
|
- **Server-side rendering:** no reliance on browser objects like `window` or `document`
|
|
31
32
|
- **Configurable:** see [props](#props)
|
|
32
33
|
- **No dependencies:** needs only Svelte as dev dependency
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
|
|
35
36
|
## Recent breaking changes
|
|
36
37
|
|
|
37
|
-
- v2.0.0 added the ability to pass options as objects. As a result, `bind:selected` no longer returns simple strings but objects
|
|
38
|
+
- v2.0.0 added the ability to pass options as objects. As a result, `bind:selected` no longer returns simple strings but objects, even if you still pass in `options` as strings. To get the same stuff you would have gotten from `bind:selected` before, there's now `bind:selectedLabels` (and `bind:selectedValues`).
|
|
38
39
|
- v3.0.0 changed the `event.detail` payload for `'add'`, `'remove'` and `'change'` events from `token` to `option`, e.g.
|
|
39
40
|
|
|
40
41
|
```js
|
|
@@ -87,40 +88,59 @@ Full list of props/bindable variables for this component:
|
|
|
87
88
|
<div class="table">
|
|
88
89
|
|
|
89
90
|
<!-- prettier-ignore -->
|
|
90
|
-
| name | default
|
|
91
|
-
| :--------------- |
|
|
92
|
-
| `options` | required prop
|
|
93
|
-
| `activeOption` | `null`
|
|
94
|
-
| `maxSelect` | `null`
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `name` | `
|
|
103
|
-
| `id` | `undefined` | Applied to the top-level `<div>` e.g. for `document.getElementById()`. |
|
|
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
|
+
| `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
|
|
97
|
+
| `selectedLabels` | `[]` | Labels of currently selected options. |
|
|
98
|
+
| `selectedValues` | `[]` | Values of currently selected options. |
|
|
99
|
+
| `readonly` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
|
|
100
|
+
| `placeholder` | `undefined` | String shown in the text input when no option is selected. |
|
|
101
|
+
| `input` | `undefined` | Handle to the `<input>` DOM node. |
|
|
102
|
+
| `id` | `undefined` | Applied to the `<input>` element for associating HTML form `<label>`s with this component for accessibility. Also, clicking a `<label>` with same `for` attribute as `id` will focus this component. |
|
|
103
|
+
| `name` | `id` | Applied to the `<input>` element. If not provided, will be set to the value of `id`. Sets the key of this field in a submitted form data object. Not useful at the moment since the value is stored in Svelte state, not on the `<input>`. |
|
|
104
104
|
|
|
105
105
|
</div>
|
|
106
106
|
|
|
107
|
+
## Exposed methods
|
|
108
|
+
|
|
109
|
+
1. `filterFunc = (op: Option, searchText: string) => boolean`: Determine what options are shown when user enters search string to filter dropdown list. Defaults to:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
filterFunc = (op: Option, searchText: string) => {
|
|
113
|
+
if (!searchText) return true
|
|
114
|
+
return `${op.label}`.toLowerCase().includes(searchText.toLowerCase())
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
2. `maxSelectMsg = (current: number, max: number) => string`: Inform the user how many of the maximum allowed options they have currently selected. Return empty string to disable, i.e. `() => ''`. Is automatically disabled when `maxSelect === null`. Defaults to:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
maxSelectMsg = (current: number, max: number) => `${current}/${max}`
|
|
122
|
+
```
|
|
123
|
+
|
|
107
124
|
## Slots
|
|
108
125
|
|
|
109
126
|
`MultiSelect.svelte` accepts two named slots
|
|
110
127
|
|
|
111
|
-
- `slot="
|
|
112
|
-
- `slot="
|
|
128
|
+
- `slot="renderOptions"`
|
|
129
|
+
- `slot="renderSelected"`
|
|
113
130
|
|
|
114
131
|
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
132
|
|
|
116
133
|
```svelte
|
|
117
|
-
<MultiSelect options={[`Banana`, `
|
|
118
|
-
<span let:idx let:option slot="
|
|
119
|
-
{idx + 1}. {option.label}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
>
|
|
134
|
+
<MultiSelect options={[`Banana`, `Apple`, `Mango`]}>
|
|
135
|
+
<span let:idx let:option slot="renderOptions">
|
|
136
|
+
{idx + 1}. {option.label}
|
|
137
|
+
{option.label === `Mango` ? `🎉` : ``}
|
|
138
|
+
</span>
|
|
139
|
+
|
|
140
|
+
<span let:idx let:option slot="renderSelected">
|
|
141
|
+
#{idx + 1}
|
|
142
|
+
{option.label}
|
|
143
|
+
</span>
|
|
124
144
|
</MultiSelect>
|
|
125
145
|
```
|
|
126
146
|
|
|
@@ -152,7 +172,7 @@ to customize rendering individual options in the dropdown and the list of select
|
|
|
152
172
|
|
|
153
173
|
## TypeScript
|
|
154
174
|
|
|
155
|
-
TypeScript users can import the types used for internal type safety
|
|
175
|
+
TypeScript users can import the types used for internal type safety:
|
|
156
176
|
|
|
157
177
|
```svelte
|
|
158
178
|
<script lang="ts">
|
|
@@ -171,26 +191,57 @@ TypeScript users can import the types used for internal type safety for external
|
|
|
171
191
|
|
|
172
192
|
## Styling
|
|
173
193
|
|
|
174
|
-
There are 3 ways to style this component.
|
|
194
|
+
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:
|
|
195
|
+
|
|
196
|
+
```svelte
|
|
197
|
+
<div class="multiselect">
|
|
198
|
+
<ul class="selected">
|
|
199
|
+
<li>Selected 1</li>
|
|
200
|
+
<li>Selected 2</li>
|
|
201
|
+
</ul>
|
|
202
|
+
<ul class="options">
|
|
203
|
+
<li>Option 1</li>
|
|
204
|
+
<li>Option 2</li>
|
|
205
|
+
</ul>
|
|
206
|
+
</div>
|
|
207
|
+
```
|
|
175
208
|
|
|
176
209
|
### With CSS variables
|
|
177
210
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
- `
|
|
181
|
-
- `border
|
|
182
|
-
- `
|
|
183
|
-
- `
|
|
184
|
-
- `
|
|
185
|
-
- `
|
|
186
|
-
- `
|
|
187
|
-
- `
|
|
188
|
-
- `
|
|
189
|
-
- `
|
|
190
|
-
- `
|
|
191
|
-
- `
|
|
192
|
-
- `
|
|
193
|
-
- `
|
|
211
|
+
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.
|
|
212
|
+
|
|
213
|
+
- `div.multiselect`
|
|
214
|
+
- `border: var(--sms-border, 1pt solid lightgray)`: Change this to e.g. to `1px solid red` to indicate this form field is in an invalid state.
|
|
215
|
+
- `border-radius: var(--sms-border-radius, 5pt)`
|
|
216
|
+
- `background: var(--sms-input-bg)`
|
|
217
|
+
- `height: var(--sms-input-height, 2em)`
|
|
218
|
+
- `div.multiselect.open`
|
|
219
|
+
- `z-index: var(--sms-open-z-index, 4)`: Increase this if needed to ensure the dropdown list is displayed atop all other page elements.
|
|
220
|
+
- `div.multiselect:focus-within`
|
|
221
|
+
- `border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue))`: Border when component has focus. Defaults to `--sms-active-color` if not set which defaults to `cornflowerblue`.
|
|
222
|
+
- `div.multiselect.readonly`
|
|
223
|
+
- `background: var(--sms-readonly-bg, lightgray)`: Background when in readonly state.
|
|
224
|
+
- `div.multiselect > input`
|
|
225
|
+
- `color: var(--sms-text-color, inherit)`: Input text color.
|
|
226
|
+
- `div.multiselect > ul.selected > li`
|
|
227
|
+
- `background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue))`: Background of selected options.
|
|
228
|
+
- `height: var(--sms-selected-li-height)`: Height of selected options.
|
|
229
|
+
- `ul.selected > li button:hover, button.remove-all:hover, button:focus`
|
|
230
|
+
- `color: var(--sms-remove-x-hover-focus-color, lightskyblue)`: Color of the cross-icon buttons for removing all or individual selected options when in `:focus` or `:hover` state.
|
|
231
|
+
- `div.multiselect > ul.options`
|
|
232
|
+
- `background: var(--sms-options-bg, white)`: Background of dropdown list.
|
|
233
|
+
- `overscroll-behavior: var(--sms-options-overscroll, none)`: Whether scroll events bubble to parent elements when reaching the top/bottom of the options dropdown. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior).
|
|
234
|
+
- `div.multiselect > ul.options > li.selected`
|
|
235
|
+
- `border-left: var(--sms-li-selected-border-left, 3pt solid var(--sms-selected-color, green))`
|
|
236
|
+
- `background: var(--sms-li-selected-bg, inherit)`: Background of selected list items in options pane.
|
|
237
|
+
- `color: var(--sms-li-selected-color, inherit)`: Text color of selected list items in options pane.
|
|
238
|
+
- `div.multiselect > ul.options > li:not(.selected):hover`
|
|
239
|
+
- `border-left: var(--sms-li-not-selected-hover-border-left, 3pt solid var(--sms-active-color, cornflowerblue))`
|
|
240
|
+
- `div.multiselect > ul.options > li.active`
|
|
241
|
+
- `background: var(--sms-li-active-bg, var(--sms-active-color, cornflowerblue))`: Background of active (currently with arrow keys highlighted) list item.
|
|
242
|
+
- `div.multiselect > ul.options > li.disabled`
|
|
243
|
+
- `background: var(--sms-li-disabled-bg, #f5f5f6)`: Background of disabled options in the dropdown list.
|
|
244
|
+
- `color: var(--sms-li-disabled-text, #b8b8b8)`: Text color of disabled option in the dropdown list.
|
|
194
245
|
|
|
195
246
|
For example, to change the background color of the options dropdown:
|
|
196
247
|
|
|
@@ -211,14 +262,14 @@ The second method allows you to pass in custom classes to the important DOM elem
|
|
|
211
262
|
This simplified version of the DOM structure of this component shows where these classes are inserted:
|
|
212
263
|
|
|
213
264
|
```svelte
|
|
214
|
-
<div class={outerDivClass}>
|
|
215
|
-
<ul class={ulSelectedClass}>
|
|
216
|
-
<li class={liSelectedClass}>
|
|
217
|
-
<li class={liSelectedClass}>
|
|
265
|
+
<div class="multiselect {outerDivClass}">
|
|
266
|
+
<ul class="selected {ulSelectedClass}">
|
|
267
|
+
<li class={liSelectedClass}>Selected 1</li>
|
|
268
|
+
<li class={liSelectedClass}>Selected 2</li>
|
|
218
269
|
</ul>
|
|
219
|
-
<ul class={ulOptionsClass}>
|
|
220
|
-
<li class={liOptionClass}>
|
|
221
|
-
<li class={liOptionClass}>
|
|
270
|
+
<ul class="options {ulOptionsClass}">
|
|
271
|
+
<li class={liOptionClass}>Option 1</li>
|
|
272
|
+
<li class={liOptionClass}>Option 2</li>
|
|
222
273
|
</ul>
|
|
223
274
|
</div>
|
|
224
275
|
```
|
|
@@ -228,40 +279,47 @@ This simplified version of the DOM structure of this component shows where these
|
|
|
228
279
|
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.
|
|
229
280
|
|
|
230
281
|
```css
|
|
231
|
-
:global(.multiselect) {
|
|
282
|
+
:global(div.multiselect) {
|
|
232
283
|
/* top-level wrapper div */
|
|
233
284
|
}
|
|
234
|
-
:global(.multiselect
|
|
235
|
-
/*
|
|
285
|
+
:global(div.multiselect.open) {
|
|
286
|
+
/* top-level wrapper div when dropdown open */
|
|
287
|
+
}
|
|
288
|
+
:global(div.multiselect.readonly) {
|
|
289
|
+
/* top-level wrapper div when in readonly state */
|
|
290
|
+
}
|
|
291
|
+
:global(div.multiselect > ul.selected) {
|
|
292
|
+
/* selected list */
|
|
293
|
+
}
|
|
294
|
+
:global(div.multiselect > ul.selected > li) {
|
|
295
|
+
/* selected list items */
|
|
236
296
|
}
|
|
237
|
-
:global(.multiselect
|
|
238
|
-
|
|
297
|
+
:global(div.multiselect button) {
|
|
298
|
+
/* target all buttons in this component */
|
|
299
|
+
}
|
|
300
|
+
:global(div.multiselect > ul.selected > li button, button.remove-all) {
|
|
239
301
|
/* buttons to remove a single or all selected options at once */
|
|
240
302
|
}
|
|
241
|
-
:global(.multiselect
|
|
303
|
+
:global(div.multiselect > input) {
|
|
304
|
+
/* input inside the top-level wrapper div */
|
|
305
|
+
}
|
|
306
|
+
:global(div.multiselect > ul.options) {
|
|
242
307
|
/* dropdown options */
|
|
243
308
|
}
|
|
244
|
-
:global(.multiselect ul.options li) {
|
|
245
|
-
/* dropdown list
|
|
309
|
+
:global(div.multiselect > ul.options > li) {
|
|
310
|
+
/* dropdown list items */
|
|
246
311
|
}
|
|
247
|
-
:global(.multiselect ul.options li.selected) {
|
|
312
|
+
:global(div.multiselect > ul.options > li.selected) {
|
|
248
313
|
/* selected options in the dropdown list */
|
|
249
314
|
}
|
|
250
|
-
:global(.multiselect ul.options li:not(.selected):hover) {
|
|
315
|
+
:global(div.multiselect > ul.options > li:not(.selected):hover) {
|
|
251
316
|
/* unselected but hovered options in the dropdown list */
|
|
252
317
|
}
|
|
253
|
-
:global(.multiselect ul.options li.
|
|
254
|
-
/* selected and hovered options in the dropdown list */
|
|
255
|
-
/* probably not necessary to style this state in most cases */
|
|
256
|
-
}
|
|
257
|
-
:global(.multiselect ul.options li.active) {
|
|
318
|
+
:global(div.multiselect > ul.options > li.active) {
|
|
258
319
|
/* active means item was navigated to with up/down arrow keys */
|
|
259
320
|
/* ready to be selected by pressing enter */
|
|
260
321
|
}
|
|
261
|
-
:global(.multiselect ul.options li.
|
|
262
|
-
/* both active and already selected, pressing enter now will deselect the item */
|
|
263
|
-
}
|
|
264
|
-
:global(.multiselect ul.options li.disabled) {
|
|
322
|
+
:global(div.multiselect > ul.options > li.disabled) {
|
|
265
323
|
/* options with disabled key set to true (see props above) */
|
|
266
324
|
}
|
|
267
325
|
```
|