svelte-multiselect 3.2.0 → 3.3.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 +84 -77
- package/MultiSelect.svelte.d.ts +5 -1
- package/package.json +12 -12
- package/readme.md +87 -50
package/MultiSelect.svelte
CHANGED
|
@@ -6,8 +6,10 @@ import Wiggle from './Wiggle.svelte';
|
|
|
6
6
|
export let selected = [];
|
|
7
7
|
export let selectedLabels = [];
|
|
8
8
|
export let selectedValues = [];
|
|
9
|
+
export let searchText = ``;
|
|
10
|
+
export let showOptions = false;
|
|
9
11
|
export let maxSelect = null; // null means any number of options are selectable
|
|
10
|
-
export let maxSelectMsg =
|
|
12
|
+
export let maxSelectMsg = null;
|
|
11
13
|
export let readonly = false;
|
|
12
14
|
export let options;
|
|
13
15
|
export let input = null;
|
|
@@ -16,11 +18,17 @@ export let id = undefined;
|
|
|
16
18
|
export let name = id;
|
|
17
19
|
export let noOptionsMsg = `No matching options`;
|
|
18
20
|
export let activeOption = null;
|
|
21
|
+
export let filterFunc = (op, searchText) => {
|
|
22
|
+
if (!searchText)
|
|
23
|
+
return true;
|
|
24
|
+
return `${op.label}`.toLowerCase().includes(searchText.toLowerCase());
|
|
25
|
+
};
|
|
19
26
|
export let outerDivClass = ``;
|
|
20
27
|
export let ulSelectedClass = ``;
|
|
21
28
|
export let liSelectedClass = ``;
|
|
22
29
|
export let ulOptionsClass = ``;
|
|
23
30
|
export let liOptionClass = ``;
|
|
31
|
+
export let liActiveOptionClass = ``;
|
|
24
32
|
export let removeBtnTitle = `Remove`;
|
|
25
33
|
export let removeAllTitle = `Remove all`;
|
|
26
34
|
// https://github.com/sveltejs/svelte/issues/6964
|
|
@@ -36,6 +44,7 @@ onMount(() => {
|
|
|
36
44
|
selected = _options.filter((op) => op?.preselected);
|
|
37
45
|
});
|
|
38
46
|
let wiggle = false;
|
|
47
|
+
const dispatch = createEventDispatcher();
|
|
39
48
|
function isObject(item) {
|
|
40
49
|
return typeof item === `object` && !Array.isArray(item) && item !== null;
|
|
41
50
|
}
|
|
@@ -62,15 +71,8 @@ $: if (new Set(labels).size !== options.length) {
|
|
|
62
71
|
}
|
|
63
72
|
$: selectedLabels = selected.map((op) => op.label);
|
|
64
73
|
$: selectedValues = selected.map((op) => op.value);
|
|
65
|
-
const dispatch = createEventDispatcher();
|
|
66
|
-
let searchText = ``;
|
|
67
|
-
let showOptions = false;
|
|
68
74
|
// options matching the current search text
|
|
69
|
-
$: matchingOptions = _options.filter((op) =>
|
|
70
|
-
if (!searchText)
|
|
71
|
-
return true;
|
|
72
|
-
return `${op.label}`.toLowerCase().includes(searchText.toLowerCase());
|
|
73
|
-
});
|
|
75
|
+
$: matchingOptions = _options.filter((op) => filterFunc(op, searchText));
|
|
74
76
|
$: matchingEnabledOptions = matchingOptions.filter((op) => !op.disabled);
|
|
75
77
|
$: if (
|
|
76
78
|
// if there was an active option but it's not in the filtered list of options
|
|
@@ -81,12 +83,12 @@ $: if (
|
|
|
81
83
|
// make the first filtered option active
|
|
82
84
|
activeOption = matchingEnabledOptions[0];
|
|
83
85
|
function add(label) {
|
|
84
|
-
if (
|
|
86
|
+
if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
|
|
85
87
|
wiggle = true;
|
|
86
88
|
if (!readonly &&
|
|
87
89
|
!selectedLabels.includes(label) &&
|
|
88
90
|
// for maxselect = 1 we always replace current option with new selection
|
|
89
|
-
(maxSelect
|
|
91
|
+
(maxSelect === null || maxSelect === 1 || selected.length < maxSelect)) {
|
|
90
92
|
searchText = ``; // reset search string on selection
|
|
91
93
|
const option = _options.find((op) => op.label === label);
|
|
92
94
|
if (!option) {
|
|
@@ -114,9 +116,6 @@ function remove(label) {
|
|
|
114
116
|
dispatch(`change`, { option, type: `remove` });
|
|
115
117
|
}
|
|
116
118
|
function setOptionsVisible(show) {
|
|
117
|
-
// nothing to do if visibility is already as intended
|
|
118
|
-
if (readonly || show === showOptions)
|
|
119
|
-
return;
|
|
120
119
|
showOptions = show;
|
|
121
120
|
if (show)
|
|
122
121
|
input?.focus();
|
|
@@ -200,15 +199,15 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
|
|
|
200
199
|
<!-- z-index: 2 when showOptions is true ensures the ul.selected of one <MultiSelect />
|
|
201
200
|
display above those of another following shortly after it -->
|
|
202
201
|
<div
|
|
203
|
-
class="multiselect {outerDivClass}"
|
|
204
202
|
class:readonly
|
|
205
|
-
class:single={maxSelect
|
|
203
|
+
class:single={maxSelect === 1}
|
|
206
204
|
class:open={showOptions}
|
|
205
|
+
class="multiselect {outerDivClass}"
|
|
207
206
|
on:mouseup|stopPropagation={() => setOptionsVisible(true)}
|
|
208
207
|
use:onClickOutside={() => setOptionsVisible(false)}
|
|
209
208
|
use:onClickOutside={() => dispatch(`blur`)}
|
|
210
209
|
>
|
|
211
|
-
<ExpandIcon
|
|
210
|
+
<ExpandIcon style="min-width: 1em; padding: 0 1pt;" />
|
|
212
211
|
<ul class="selected {ulSelectedClass}">
|
|
213
212
|
{#each selected as option, idx}
|
|
214
213
|
<li class={liSelectedClass}>
|
|
@@ -227,57 +226,65 @@ display above those of another following shortly after it -->
|
|
|
227
226
|
{/if}
|
|
228
227
|
</li>
|
|
229
228
|
{/each}
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
229
|
+
<li style="display: contents;">
|
|
230
|
+
<input
|
|
231
|
+
bind:this={input}
|
|
232
|
+
autocomplete="off"
|
|
233
|
+
bind:value={searchText}
|
|
234
|
+
on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
|
|
235
|
+
on:keydown={handleKeydown}
|
|
236
|
+
on:focus={() => setOptionsVisible(true)}
|
|
237
|
+
{id}
|
|
238
|
+
{name}
|
|
239
|
+
placeholder={selectedLabels.length ? `` : placeholder}
|
|
240
|
+
/>
|
|
241
|
+
</li>
|
|
241
242
|
</ul>
|
|
242
243
|
{#if readonly}
|
|
243
244
|
<ReadOnlyIcon height="14pt" />
|
|
244
245
|
{:else if selected.length > 0}
|
|
245
|
-
{#if maxSelect
|
|
246
|
+
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
|
|
246
247
|
<Wiggle bind:wiggle angle={20}>
|
|
247
|
-
<span style="padding: 0 3pt;">
|
|
248
|
+
<span style="padding: 0 3pt;">
|
|
249
|
+
{maxSelectMsg?.(selected.length, maxSelect) ??
|
|
250
|
+
(maxSelect > 1 ? `${selected.length}/${maxSelect}` : ``)}
|
|
251
|
+
</span>
|
|
248
252
|
</Wiggle>
|
|
249
253
|
{/if}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
254
|
+
{#if maxSelect !== 1}
|
|
255
|
+
<button
|
|
256
|
+
type="button"
|
|
257
|
+
class="remove-all"
|
|
258
|
+
title={removeAllTitle}
|
|
259
|
+
on:mouseup|stopPropagation={removeAll}
|
|
260
|
+
on:keydown={handleEnterAndSpaceKeys(removeAll)}
|
|
261
|
+
>
|
|
262
|
+
<CrossIcon height="14pt" />
|
|
263
|
+
</button>
|
|
264
|
+
{/if}
|
|
259
265
|
{/if}
|
|
260
266
|
|
|
261
267
|
{#key showOptions}
|
|
262
268
|
<ul
|
|
263
|
-
class="options {ulOptionsClass}"
|
|
264
269
|
class:hidden={!showOptions}
|
|
270
|
+
class="options {ulOptionsClass}"
|
|
265
271
|
transition:fly|local={{ duration: 300, y: 40 }}
|
|
266
272
|
>
|
|
267
273
|
{#each matchingOptions as option, idx}
|
|
268
274
|
{@const { label, disabled, title = null, selectedTitle } = option}
|
|
269
275
|
{@const { disabledTitle = defaultDisabledTitle } = option}
|
|
276
|
+
{@const active = activeOption?.label === label}
|
|
270
277
|
<li
|
|
271
278
|
on:mouseup|preventDefault|stopPropagation
|
|
272
279
|
on:mousedown|preventDefault|stopPropagation={() => {
|
|
273
280
|
if (disabled) return
|
|
274
281
|
isSelected(label) ? remove(label) : add(label)
|
|
275
282
|
}}
|
|
283
|
+
title={disabled ? disabledTitle : (isSelected(label) && selectedTitle) || title}
|
|
276
284
|
class:selected={isSelected(label)}
|
|
277
|
-
class:active
|
|
285
|
+
class:active
|
|
278
286
|
class:disabled
|
|
279
|
-
|
|
280
|
-
class={liOptionClass}
|
|
287
|
+
class="{liOptionClass} {active ? liActiveOptionClass : ``}"
|
|
281
288
|
>
|
|
282
289
|
<slot name="renderOptions" {option} {idx}>
|
|
283
290
|
{option.label}
|
|
@@ -294,15 +301,14 @@ display above those of another following shortly after it -->
|
|
|
294
301
|
:where(div.multiselect) {
|
|
295
302
|
position: relative;
|
|
296
303
|
margin: 1em 0;
|
|
297
|
-
border: var(--sms-border, 1pt solid lightgray);
|
|
298
|
-
border-radius: var(--sms-border-radius, 5pt);
|
|
299
|
-
background: var(--sms-input-bg);
|
|
300
|
-
height: var(--sms-input-height, 2em);
|
|
301
304
|
align-items: center;
|
|
302
|
-
min-height: 18pt;
|
|
303
305
|
display: flex;
|
|
304
306
|
cursor: text;
|
|
305
307
|
padding: 0 3pt;
|
|
308
|
+
border: var(--sms-border, 1pt solid lightgray);
|
|
309
|
+
border-radius: var(--sms-border-radius, 5pt);
|
|
310
|
+
background: var(--sms-input-bg);
|
|
311
|
+
min-height: var(--sms-input-min-height, 22pt);
|
|
306
312
|
}
|
|
307
313
|
:where(div.multiselect.open) {
|
|
308
314
|
z-index: var(--sms-open-z-index, 4);
|
|
@@ -314,25 +320,33 @@ display above those of another following shortly after it -->
|
|
|
314
320
|
background: var(--sms-readonly-bg, lightgray);
|
|
315
321
|
}
|
|
316
322
|
|
|
317
|
-
:where(
|
|
318
|
-
|
|
323
|
+
:where(div.multiselect > ul.selected) {
|
|
324
|
+
display: flex;
|
|
325
|
+
flex: 1;
|
|
326
|
+
padding: 0;
|
|
327
|
+
margin: 0;
|
|
328
|
+
flex-wrap: wrap;
|
|
329
|
+
}
|
|
330
|
+
:where(div.multiselect > ul.selected > li) {
|
|
319
331
|
align-items: center;
|
|
320
332
|
border-radius: 4pt;
|
|
321
333
|
display: flex;
|
|
322
334
|
margin: 2pt;
|
|
323
|
-
|
|
335
|
+
line-height: normal;
|
|
336
|
+
padding: 1pt 2pt 1pt 5pt;
|
|
324
337
|
transition: 0.3s;
|
|
325
338
|
white-space: nowrap;
|
|
326
|
-
|
|
339
|
+
background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue));
|
|
340
|
+
height: var(--sms-selected-li-height);
|
|
327
341
|
}
|
|
328
|
-
:where(ul.selected > li button, button.remove-all) {
|
|
342
|
+
:where(div.multiselect > ul.selected > li button, button.remove-all) {
|
|
329
343
|
align-items: center;
|
|
330
344
|
border-radius: 50%;
|
|
331
345
|
display: flex;
|
|
332
346
|
cursor: pointer;
|
|
333
347
|
transition: 0.2s;
|
|
334
348
|
}
|
|
335
|
-
:where(button) {
|
|
349
|
+
:where(div.multiselect button) {
|
|
336
350
|
color: inherit;
|
|
337
351
|
background: transparent;
|
|
338
352
|
border: none;
|
|
@@ -343,54 +357,48 @@ display above those of another following shortly after it -->
|
|
|
343
357
|
:where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
|
|
344
358
|
color: var(--sms-remove-x-hover-focus-color, lightskyblue);
|
|
345
359
|
}
|
|
346
|
-
:where(button:focus) {
|
|
360
|
+
:where(div.multiselect > button:focus) {
|
|
347
361
|
transform: scale(1.04);
|
|
348
362
|
}
|
|
349
363
|
|
|
350
|
-
:where(div.multiselect input) {
|
|
364
|
+
:where(div.multiselect > ul.selected > li > input) {
|
|
351
365
|
border: none;
|
|
352
366
|
outline: none;
|
|
353
367
|
background: none;
|
|
354
|
-
color: var(--sms-text-color, inherit);
|
|
355
368
|
flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
|
|
356
369
|
min-width: 2em;
|
|
357
370
|
/* minimum font-size > 16px ensures iOS doesn't zoom in when focusing input */
|
|
358
371
|
/* https://stackoverflow.com/a/6394497 */
|
|
359
372
|
font-size: calc(16px + 0.1vw);
|
|
373
|
+
color: var(--sms-text-color, inherit);
|
|
360
374
|
}
|
|
361
375
|
|
|
362
|
-
:where(ul.
|
|
363
|
-
display: flex;
|
|
364
|
-
padding: 0;
|
|
365
|
-
margin: 0;
|
|
366
|
-
flex-wrap: wrap;
|
|
367
|
-
flex: 1;
|
|
368
|
-
overscroll-behavior: none;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
:where(ul.options) {
|
|
376
|
+
:where(div.multiselect > ul.options) {
|
|
372
377
|
list-style: none;
|
|
373
378
|
max-height: 50vh;
|
|
374
379
|
padding: 0;
|
|
375
380
|
top: 100%;
|
|
381
|
+
left: 0;
|
|
376
382
|
width: 100%;
|
|
377
383
|
position: absolute;
|
|
378
384
|
border-radius: 1ex;
|
|
379
385
|
overflow: auto;
|
|
380
386
|
background: var(--sms-options-bg, white);
|
|
387
|
+
overscroll-behavior: var(--sms-options-overscroll, none);
|
|
388
|
+
box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
|
|
381
389
|
}
|
|
382
|
-
:where(ul.options.hidden) {
|
|
390
|
+
:where(div.multiselect > ul.options.hidden) {
|
|
383
391
|
visibility: hidden;
|
|
384
392
|
}
|
|
385
|
-
:where(ul.options li) {
|
|
393
|
+
:where(div.multiselect > ul.options > li) {
|
|
386
394
|
padding: 3pt 2ex;
|
|
387
395
|
cursor: pointer;
|
|
388
396
|
}
|
|
389
397
|
/* for noOptionsMsg */
|
|
390
|
-
:where(ul.options span) {
|
|
398
|
+
:where(div.multiselect > ul.options span) {
|
|
391
399
|
padding: 3pt 2ex;
|
|
392
400
|
}
|
|
393
|
-
:where(ul.options li.selected) {
|
|
401
|
+
:where(div.multiselect > ul.options > li.selected) {
|
|
394
402
|
border-left: var(
|
|
395
403
|
--sms-li-selected-border-left,
|
|
396
404
|
3pt solid var(--sms-selected-color, green)
|
|
@@ -398,22 +406,21 @@ display above those of another following shortly after it -->
|
|
|
398
406
|
background: var(--sms-li-selected-bg, inherit);
|
|
399
407
|
color: var(--sms-li-selected-color, inherit);
|
|
400
408
|
}
|
|
401
|
-
:where(ul.options li:not(.selected):hover) {
|
|
409
|
+
:where(div.multiselect > ul.options > li:not(.selected):hover) {
|
|
402
410
|
border-left: var(
|
|
403
411
|
--sms-li-not-selected-hover-border-left,
|
|
404
412
|
3pt solid var(--sms-active-color, cornflowerblue)
|
|
405
413
|
);
|
|
406
|
-
border-left: 3pt solid var(--blue);
|
|
407
414
|
}
|
|
408
|
-
:where(ul.options li.active) {
|
|
415
|
+
:where(div.multiselect > ul.options > li.active) {
|
|
409
416
|
background: var(--sms-li-active-bg, var(--sms-active-color, cornflowerblue));
|
|
410
417
|
}
|
|
411
|
-
:where(ul.options li.disabled) {
|
|
418
|
+
:where(div.multiselect > ul.options > li.disabled) {
|
|
419
|
+
cursor: not-allowed;
|
|
412
420
|
background: var(--sms-li-disabled-bg, #f5f5f6);
|
|
413
421
|
color: var(--sms-li-disabled-text, #b8b8b8);
|
|
414
|
-
cursor: not-allowed;
|
|
415
422
|
}
|
|
416
|
-
:where(ul.options li.disabled:hover) {
|
|
423
|
+
:where(div.multiselect > ul.options > li.disabled:hover) {
|
|
417
424
|
border-left: unset;
|
|
418
425
|
}
|
|
419
426
|
</style>
|
package/MultiSelect.svelte.d.ts
CHANGED
|
@@ -5,8 +5,10 @@ declare const __propDef: {
|
|
|
5
5
|
selected?: Option[] | undefined;
|
|
6
6
|
selectedLabels?: Primitive[] | undefined;
|
|
7
7
|
selectedValues?: Primitive[] | undefined;
|
|
8
|
+
searchText?: string | undefined;
|
|
9
|
+
showOptions?: boolean | undefined;
|
|
8
10
|
maxSelect?: number | null | undefined;
|
|
9
|
-
maxSelectMsg?: ((current: number, max: number) => string) | undefined;
|
|
11
|
+
maxSelectMsg?: ((current: number, max: number) => string) | null | undefined;
|
|
10
12
|
readonly?: boolean | undefined;
|
|
11
13
|
options: ProtoOption[];
|
|
12
14
|
input?: HTMLInputElement | null | undefined;
|
|
@@ -15,11 +17,13 @@ declare const __propDef: {
|
|
|
15
17
|
name?: string | undefined;
|
|
16
18
|
noOptionsMsg?: string | undefined;
|
|
17
19
|
activeOption?: Option | null | undefined;
|
|
20
|
+
filterFunc?: ((op: Option, searchText: string) => boolean) | undefined;
|
|
18
21
|
outerDivClass?: string | undefined;
|
|
19
22
|
ulSelectedClass?: string | undefined;
|
|
20
23
|
liSelectedClass?: string | undefined;
|
|
21
24
|
ulOptionsClass?: string | undefined;
|
|
22
25
|
liOptionClass?: string | undefined;
|
|
26
|
+
liActiveOptionClass?: string | undefined;
|
|
23
27
|
removeBtnTitle?: string | undefined;
|
|
24
28
|
removeAllTitle?: string | undefined;
|
|
25
29
|
defaultDisabledTitle?: string | undefined;
|
package/package.json
CHANGED
|
@@ -5,16 +5,16 @@
|
|
|
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.3.0",
|
|
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.278",
|
|
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
20
|
"mdsvex": "^0.10.5",
|
|
@@ -22,15 +22,15 @@
|
|
|
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.4.
|
|
25
|
+
"svelte": "^3.46.4",
|
|
26
|
+
"svelte-check": "^2.4.5",
|
|
27
27
|
"svelte-github-corner": "^0.1.0",
|
|
28
|
-
"svelte-preprocess": "^4.10.
|
|
29
|
-
"svelte-toc": "^0.2.
|
|
30
|
-
"svelte2tsx": "^0.5.
|
|
28
|
+
"svelte-preprocess": "^4.10.3",
|
|
29
|
+
"svelte-toc": "^0.2.6",
|
|
30
|
+
"svelte2tsx": "^0.5.5",
|
|
31
31
|
"tslib": "^2.3.1",
|
|
32
32
|
"typescript": "^4.5.5",
|
|
33
|
-
"vite": "^2.
|
|
33
|
+
"vite": "^2.8.4"
|
|
34
34
|
},
|
|
35
35
|
"keywords": [
|
|
36
36
|
"svelte",
|
package/readme.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
<slot />
|
|
24
24
|
|
|
25
|
-
## Key
|
|
25
|
+
## Key features
|
|
26
26
|
|
|
27
27
|
- **Single / multiple select:** pass `maxSelect={1}` prop to only allow one selection
|
|
28
28
|
- **Dropdowns:** scrollable lists for large numbers of options
|
|
@@ -88,23 +88,42 @@ Full list of props/bindable variables for this component:
|
|
|
88
88
|
<div class="table">
|
|
89
89
|
|
|
90
90
|
<!-- prettier-ignore -->
|
|
91
|
-
| name | default
|
|
92
|
-
| :--------------- |
|
|
93
|
-
| `options` | required prop
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
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
|
+
| `showOptions` | `false` | Bindable boolean that controls whether the options dropdown is visible. |
|
|
95
|
+
| `searchText` | `` | Text the user-entered to filter down on the list of options. Binds both ways, i.e. can also be used to set the input text. |
|
|
96
|
+
| `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
|
|
97
|
+
| `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
|
|
98
|
+
| `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
|
|
99
|
+
| `selectedLabels` | `[]` | Labels of currently selected options. |
|
|
100
|
+
| `selectedValues` | `[]` | Values of currently selected options. |
|
|
101
|
+
| `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. |
|
|
102
|
+
| `readonly` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
|
|
103
|
+
| `placeholder` | `undefined` | String shown in the text input when no option is selected. |
|
|
104
|
+
| `input` | `undefined` | Handle to the `<input>` DOM node. |
|
|
105
|
+
| `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. |
|
|
106
|
+
| `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>`. |
|
|
105
107
|
|
|
106
108
|
</div>
|
|
107
109
|
|
|
110
|
+
## Exposed methods
|
|
111
|
+
|
|
112
|
+
1. `filterFunc = (op: Option, searchText: string) => boolean`: Determine what options are shown when user enters search string to filter dropdown list. Defaults to:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
filterFunc = (op: Option, searchText: string) => {
|
|
116
|
+
if (!searchText) return true
|
|
117
|
+
return `${op.label}`.toLowerCase().includes(searchText.toLowerCase())
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
2. `maxSelectMsg = (current: number, max: number) => string`: Inform users how many of the maximum allowed options they have already selected. Set `maxSelectMsg={null}` to not show a message. Defaults to `null` when `maxSelect={1}` or `maxSelect={null}`. Else if `maxSelect > 1`, defaults to:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
maxSelectMsg = (current: number, max: number) => `${current}/${max}`
|
|
125
|
+
```
|
|
126
|
+
|
|
108
127
|
## Slots
|
|
109
128
|
|
|
110
129
|
`MultiSelect.svelte` accepts two named slots
|
|
@@ -194,29 +213,37 @@ There are 3 ways to style this component. To understand which options do what, i
|
|
|
194
213
|
|
|
195
214
|
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.
|
|
196
215
|
|
|
197
|
-
- `div.multiselect
|
|
216
|
+
- `div.multiselect`
|
|
198
217
|
- `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.
|
|
199
|
-
- `border-radius: var(--sms-border-radius, 5pt)
|
|
200
|
-
- `background: var(--sms-input-bg)
|
|
201
|
-
- `height: var(--sms-input-height, 2em)
|
|
202
|
-
|
|
218
|
+
- `border-radius: var(--sms-border-radius, 5pt)`
|
|
219
|
+
- `background: var(--sms-input-bg)`
|
|
220
|
+
- `height: var(--sms-input-height, 2em)`
|
|
221
|
+
- `div.multiselect.open`
|
|
222
|
+
- `z-index: var(--sms-open-z-index, 4)`: Increase this if needed to ensure the dropdown list is displayed atop all other page elements.
|
|
223
|
+
- `div.multiselect:focus-within`
|
|
224
|
+
- `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`.
|
|
225
|
+
- `div.multiselect.readonly`
|
|
203
226
|
- `background: var(--sms-readonly-bg, lightgray)`: Background when in readonly state.
|
|
204
|
-
- `div.multiselect.
|
|
205
|
-
- `z-index: var(--sms-open-z-index, 4)`: Useful to ensure the dropdown list of options is displayed on top of other page elements of increased `z-index`.
|
|
206
|
-
- `div.multiselect input`
|
|
227
|
+
- `div.multiselect > ul.selected > li > input`
|
|
207
228
|
- `color: var(--sms-text-color, inherit)`: Input text color.
|
|
208
|
-
- `ul.selected > li
|
|
229
|
+
- `div.multiselect > ul.selected > li`
|
|
209
230
|
- `background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue))`: Background of selected options.
|
|
210
|
-
- `
|
|
231
|
+
- `height: var(--sms-selected-li-height)`: Height of selected options.
|
|
232
|
+
- `ul.selected > li button:hover, button.remove-all:hover, button:focus`
|
|
211
233
|
- `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.
|
|
212
|
-
- `ul.options`
|
|
213
|
-
- `background: var(--sms-options-bg, white)`: Background of
|
|
214
|
-
- `
|
|
234
|
+
- `div.multiselect > ul.options`
|
|
235
|
+
- `background: var(--sms-options-bg, white)`: Background of dropdown list.
|
|
236
|
+
- `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).
|
|
237
|
+
- `box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);`: Box shadow of dropdown list.
|
|
238
|
+
- `div.multiselect > ul.options > li.selected`
|
|
239
|
+
- `border-left: var(--sms-li-selected-border-left, 3pt solid var(--sms-selected-color, green))`
|
|
215
240
|
- `background: var(--sms-li-selected-bg, inherit)`: Background of selected list items in options pane.
|
|
216
241
|
- `color: var(--sms-li-selected-color, inherit)`: Text color of selected list items in options pane.
|
|
217
|
-
- `ul.options > li.
|
|
242
|
+
- `div.multiselect > ul.options > li:not(.selected):hover`
|
|
243
|
+
- `border-left: var(--sms-li-not-selected-hover-border-left, 3pt solid var(--sms-active-color, cornflowerblue))`
|
|
244
|
+
- `div.multiselect > ul.options > li.active`
|
|
218
245
|
- `background: var(--sms-li-active-bg, var(--sms-active-color, cornflowerblue))`: Background of active (currently with arrow keys highlighted) list item.
|
|
219
|
-
- `ul.options > li.disabled`
|
|
246
|
+
- `div.multiselect > ul.options > li.disabled`
|
|
220
247
|
- `background: var(--sms-li-disabled-bg, #f5f5f6)`: Background of disabled options in the dropdown list.
|
|
221
248
|
- `color: var(--sms-li-disabled-text, #b8b8b8)`: Text color of disabled option in the dropdown list.
|
|
222
249
|
|
|
@@ -235,6 +262,7 @@ The second method allows you to pass in custom classes to the important DOM elem
|
|
|
235
262
|
- `liSelectedClass`
|
|
236
263
|
- `ulOptionsClass`
|
|
237
264
|
- `liOptionClass`
|
|
265
|
+
- `liActiveOptionClass`
|
|
238
266
|
|
|
239
267
|
This simplified version of the DOM structure of this component shows where these classes are inserted:
|
|
240
268
|
|
|
@@ -246,7 +274,9 @@ This simplified version of the DOM structure of this component shows where these
|
|
|
246
274
|
</ul>
|
|
247
275
|
<ul class="options {ulOptionsClass}">
|
|
248
276
|
<li class={liOptionClass}>Option 1</li>
|
|
249
|
-
<li class={liOptionClass}
|
|
277
|
+
<li class="{liOptionClass} {liActiveOptionClass}">
|
|
278
|
+
Option 2 (currently active)
|
|
279
|
+
</li>
|
|
250
280
|
</ul>
|
|
251
281
|
</div>
|
|
252
282
|
```
|
|
@@ -256,40 +286,47 @@ This simplified version of the DOM structure of this component shows where these
|
|
|
256
286
|
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.
|
|
257
287
|
|
|
258
288
|
```css
|
|
259
|
-
:global(.multiselect) {
|
|
289
|
+
:global(div.multiselect) {
|
|
260
290
|
/* top-level wrapper div */
|
|
261
291
|
}
|
|
262
|
-
:global(.multiselect
|
|
263
|
-
/*
|
|
292
|
+
:global(div.multiselect.open) {
|
|
293
|
+
/* top-level wrapper div when dropdown open */
|
|
294
|
+
}
|
|
295
|
+
:global(div.multiselect.readonly) {
|
|
296
|
+
/* top-level wrapper div when in readonly state */
|
|
264
297
|
}
|
|
265
|
-
:global(.multiselect ul.selected
|
|
266
|
-
|
|
298
|
+
:global(div.multiselect > ul.selected) {
|
|
299
|
+
/* selected list */
|
|
300
|
+
}
|
|
301
|
+
:global(div.multiselect > ul.selected > li) {
|
|
302
|
+
/* selected list items */
|
|
303
|
+
}
|
|
304
|
+
:global(div.multiselect button) {
|
|
305
|
+
/* target all buttons in this component */
|
|
306
|
+
}
|
|
307
|
+
:global(div.multiselect > ul.selected > li button, button.remove-all) {
|
|
267
308
|
/* buttons to remove a single or all selected options at once */
|
|
268
309
|
}
|
|
269
|
-
:global(.multiselect ul.
|
|
310
|
+
:global(div.multiselect > ul.selected > li > input) {
|
|
311
|
+
/* input inside the top-level wrapper div */
|
|
312
|
+
}
|
|
313
|
+
:global(div.multiselect > ul.options) {
|
|
270
314
|
/* dropdown options */
|
|
271
315
|
}
|
|
272
|
-
:global(.multiselect ul.options li) {
|
|
273
|
-
/* dropdown list
|
|
316
|
+
:global(div.multiselect > ul.options > li) {
|
|
317
|
+
/* dropdown list items */
|
|
274
318
|
}
|
|
275
|
-
:global(.multiselect ul.options li.selected) {
|
|
319
|
+
:global(div.multiselect > ul.options > li.selected) {
|
|
276
320
|
/* selected options in the dropdown list */
|
|
277
321
|
}
|
|
278
|
-
:global(.multiselect ul.options li:not(.selected):hover) {
|
|
322
|
+
:global(div.multiselect > ul.options > li:not(.selected):hover) {
|
|
279
323
|
/* unselected but hovered options in the dropdown list */
|
|
280
324
|
}
|
|
281
|
-
:global(.multiselect ul.options li.
|
|
282
|
-
/* selected and hovered options in the dropdown list */
|
|
283
|
-
/* probably not necessary to style this state in most cases */
|
|
284
|
-
}
|
|
285
|
-
:global(.multiselect ul.options li.active) {
|
|
325
|
+
:global(div.multiselect > ul.options > li.active) {
|
|
286
326
|
/* active means item was navigated to with up/down arrow keys */
|
|
287
327
|
/* ready to be selected by pressing enter */
|
|
288
328
|
}
|
|
289
|
-
:global(.multiselect ul.options li.
|
|
290
|
-
/* both active and already selected, pressing enter now will deselect the item */
|
|
291
|
-
}
|
|
292
|
-
:global(.multiselect ul.options li.disabled) {
|
|
329
|
+
:global(div.multiselect > ul.options > li.disabled) {
|
|
293
330
|
/* options with disabled key set to true (see props above) */
|
|
294
331
|
}
|
|
295
332
|
```
|