svelte-multiselect 3.2.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CircleSpinner.svelte +29 -0
- package/CircleSpinner.svelte.d.ts +18 -0
- package/MultiSelect.svelte +127 -96
- package/MultiSelect.svelte.d.ts +12 -3
- package/index.d.ts +17 -0
- package/package.json +12 -12
- package/readme.md +107 -58
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script >export let color = `cornflowerblue`;
|
|
2
|
+
export let duration = `1.5s`;
|
|
3
|
+
export let size = `1em`;
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<div
|
|
7
|
+
style="--duration: {duration}"
|
|
8
|
+
style:border-color="{color} transparent {color}
|
|
9
|
+
{color}"
|
|
10
|
+
style:width={size}
|
|
11
|
+
style:height={size}
|
|
12
|
+
/>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
div {
|
|
16
|
+
display: inline-block;
|
|
17
|
+
vertical-align: middle;
|
|
18
|
+
margin: 0 3pt;
|
|
19
|
+
border-width: calc(1em / 5);
|
|
20
|
+
border-style: solid;
|
|
21
|
+
border-radius: 50%;
|
|
22
|
+
animation: var(--duration) infinite rotate;
|
|
23
|
+
}
|
|
24
|
+
@keyframes rotate {
|
|
25
|
+
100% {
|
|
26
|
+
transform: rotate(360deg);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: {
|
|
4
|
+
color?: string | undefined;
|
|
5
|
+
duration?: string | undefined;
|
|
6
|
+
size?: string | undefined;
|
|
7
|
+
};
|
|
8
|
+
events: {
|
|
9
|
+
[evt: string]: CustomEvent<any>;
|
|
10
|
+
};
|
|
11
|
+
slots: {};
|
|
12
|
+
};
|
|
13
|
+
export declare type CircleSpinnerProps = typeof __propDef.props;
|
|
14
|
+
export declare type CircleSpinnerEvents = typeof __propDef.events;
|
|
15
|
+
export declare type CircleSpinnerSlots = typeof __propDef.slots;
|
|
16
|
+
export default class CircleSpinner extends SvelteComponentTyped<CircleSpinnerProps, CircleSpinnerEvents, CircleSpinnerSlots> {
|
|
17
|
+
}
|
|
18
|
+
export {};
|
package/MultiSelect.svelte
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
<script >import { createEventDispatcher, onMount } from 'svelte';
|
|
1
|
+
<script >import { createEventDispatcher, onMount, tick } from 'svelte';
|
|
2
2
|
import { fly } from 'svelte/transition';
|
|
3
3
|
import { onClickOutside } from './actions';
|
|
4
|
+
import CircleSpinner from './CircleSpinner.svelte';
|
|
4
5
|
import { CrossIcon, ExpandIcon, ReadOnlyIcon } from './icons';
|
|
5
6
|
import Wiggle from './Wiggle.svelte';
|
|
6
7
|
export let selected = [];
|
|
7
8
|
export let selectedLabels = [];
|
|
8
9
|
export let selectedValues = [];
|
|
10
|
+
export let searchText = ``;
|
|
11
|
+
export let showOptions = false;
|
|
9
12
|
export let maxSelect = null; // null means any number of options are selectable
|
|
10
|
-
export let maxSelectMsg =
|
|
13
|
+
export let maxSelectMsg = null;
|
|
11
14
|
export let readonly = false;
|
|
12
15
|
export let options;
|
|
13
16
|
export let input = null;
|
|
@@ -16,15 +19,25 @@ export let id = undefined;
|
|
|
16
19
|
export let name = id;
|
|
17
20
|
export let noOptionsMsg = `No matching options`;
|
|
18
21
|
export let activeOption = null;
|
|
22
|
+
export let filterFunc = (op, searchText) => {
|
|
23
|
+
if (!searchText)
|
|
24
|
+
return true;
|
|
25
|
+
return `${op.label}`.toLowerCase().includes(searchText.toLowerCase());
|
|
26
|
+
};
|
|
19
27
|
export let outerDivClass = ``;
|
|
20
28
|
export let ulSelectedClass = ``;
|
|
21
29
|
export let liSelectedClass = ``;
|
|
22
30
|
export let ulOptionsClass = ``;
|
|
23
31
|
export let liOptionClass = ``;
|
|
32
|
+
export let liActiveOptionClass = ``;
|
|
24
33
|
export let removeBtnTitle = `Remove`;
|
|
25
34
|
export let removeAllTitle = `Remove all`;
|
|
26
35
|
// https://github.com/sveltejs/svelte/issues/6964
|
|
27
36
|
export let defaultDisabledTitle = `This option is disabled`;
|
|
37
|
+
export let allowUserOptions = false;
|
|
38
|
+
export let autoScroll = true;
|
|
39
|
+
export let loading = false;
|
|
40
|
+
export let required = false;
|
|
28
41
|
if (maxSelect !== null && maxSelect < 0) {
|
|
29
42
|
console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
30
43
|
}
|
|
@@ -36,6 +49,10 @@ onMount(() => {
|
|
|
36
49
|
selected = _options.filter((op) => op?.preselected);
|
|
37
50
|
});
|
|
38
51
|
let wiggle = false;
|
|
52
|
+
// formValue binds to input.form-control to prevent form submission if required
|
|
53
|
+
// prop is true and no options are selected
|
|
54
|
+
$: formValue = selectedValues.join(`,`);
|
|
55
|
+
const dispatch = createEventDispatcher();
|
|
39
56
|
function isObject(item) {
|
|
40
57
|
return typeof item === `object` && !Array.isArray(item) && item !== null;
|
|
41
58
|
}
|
|
@@ -62,31 +79,16 @@ $: if (new Set(labels).size !== options.length) {
|
|
|
62
79
|
}
|
|
63
80
|
$: selectedLabels = selected.map((op) => op.label);
|
|
64
81
|
$: selectedValues = selected.map((op) => op.value);
|
|
65
|
-
const dispatch = createEventDispatcher();
|
|
66
|
-
let searchText = ``;
|
|
67
|
-
let showOptions = false;
|
|
68
82
|
// 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
|
-
});
|
|
83
|
+
$: matchingOptions = _options.filter((op) => filterFunc(op, searchText) && !selectedLabels.includes(op.label));
|
|
74
84
|
$: matchingEnabledOptions = matchingOptions.filter((op) => !op.disabled);
|
|
75
|
-
$: if (
|
|
76
|
-
// if there was an active option but it's not in the filtered list of options
|
|
77
|
-
(activeOption &&
|
|
78
|
-
!matchingEnabledOptions.map((op) => op.label).includes(activeOption.label)) ||
|
|
79
|
-
// or there's no active option but the user entered search text
|
|
80
|
-
(!activeOption && searchText))
|
|
81
|
-
// make the first filtered option active
|
|
82
|
-
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) {
|
|
@@ -108,15 +110,15 @@ function add(label) {
|
|
|
108
110
|
function remove(label) {
|
|
109
111
|
if (selected.length === 0 || readonly)
|
|
110
112
|
return;
|
|
111
|
-
selected = selected.filter((option) => label !== option.label);
|
|
112
113
|
const option = _options.find((option) => option.label === label);
|
|
114
|
+
if (!option) {
|
|
115
|
+
return console.error(`MultiSelect: option with label ${label} not found`);
|
|
116
|
+
}
|
|
117
|
+
selected = selected.filter((option) => label !== option.label);
|
|
113
118
|
dispatch(`remove`, { option });
|
|
114
119
|
dispatch(`change`, { option, type: `remove` });
|
|
115
120
|
}
|
|
116
121
|
function setOptionsVisible(show) {
|
|
117
|
-
// nothing to do if visibility is already as intended
|
|
118
|
-
if (readonly || show === showOptions)
|
|
119
|
-
return;
|
|
120
122
|
showOptions = show;
|
|
121
123
|
if (show)
|
|
122
124
|
input?.focus();
|
|
@@ -126,7 +128,7 @@ function setOptionsVisible(show) {
|
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
130
|
// handle all keyboard events this component receives
|
|
129
|
-
function handleKeydown(event) {
|
|
131
|
+
async function handleKeydown(event) {
|
|
130
132
|
// on escape: dismiss options dropdown and reset search text
|
|
131
133
|
if (event.key === `Escape`) {
|
|
132
134
|
setOptionsVisible(false);
|
|
@@ -138,7 +140,15 @@ function handleKeydown(event) {
|
|
|
138
140
|
const { label } = activeOption;
|
|
139
141
|
selectedLabels.includes(label) ? remove(label) : add(label);
|
|
140
142
|
searchText = ``;
|
|
141
|
-
}
|
|
143
|
+
}
|
|
144
|
+
else if ([true, `append`].includes(allowUserOptions)) {
|
|
145
|
+
selected = [...selected, { label: searchText, value: searchText }];
|
|
146
|
+
if (allowUserOptions === `append`)
|
|
147
|
+
options = [...options, { label: searchText, value: searchText }];
|
|
148
|
+
searchText = ``;
|
|
149
|
+
}
|
|
150
|
+
// no active option and no search text means the options dropdown is closed
|
|
151
|
+
// in which case enter means open it
|
|
142
152
|
else
|
|
143
153
|
setOptionsVisible(true);
|
|
144
154
|
}
|
|
@@ -151,31 +161,25 @@ function handleKeydown(event) {
|
|
|
151
161
|
}
|
|
152
162
|
const increment = event.key === `ArrowUp` ? -1 : 1;
|
|
153
163
|
const newActiveIdx = matchingEnabledOptions.indexOf(activeOption) + increment;
|
|
154
|
-
const ulOps = document.querySelector(`ul.options`);
|
|
155
164
|
if (newActiveIdx < 0) {
|
|
156
165
|
// wrap around top
|
|
157
166
|
activeOption = matchingEnabledOptions[matchingEnabledOptions.length - 1];
|
|
158
|
-
if (ulOps)
|
|
159
|
-
ulOps.scrollTop = ulOps.scrollHeight;
|
|
160
167
|
}
|
|
161
168
|
else if (newActiveIdx === matchingEnabledOptions.length) {
|
|
162
169
|
// wrap around bottom
|
|
163
170
|
activeOption = matchingEnabledOptions[0];
|
|
164
|
-
if (ulOps)
|
|
165
|
-
ulOps.scrollTop = 0;
|
|
166
171
|
}
|
|
167
172
|
else {
|
|
168
|
-
// default case
|
|
173
|
+
// default case: select next/previous in item list
|
|
169
174
|
activeOption = matchingEnabledOptions[newActiveIdx];
|
|
175
|
+
}
|
|
176
|
+
if (autoScroll) {
|
|
177
|
+
await tick();
|
|
170
178
|
const li = document.querySelector(`ul.options > li.active`);
|
|
171
|
-
|
|
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();
|
|
179
|
+
li?.scrollIntoViewIfNeeded();
|
|
177
180
|
}
|
|
178
181
|
}
|
|
182
|
+
// on backspace key: remove last selected option
|
|
179
183
|
else if (event.key === `Backspace`) {
|
|
180
184
|
const label = selectedLabels.pop();
|
|
181
185
|
if (label && !searchText)
|
|
@@ -200,19 +204,21 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
|
|
|
200
204
|
<!-- z-index: 2 when showOptions is true ensures the ul.selected of one <MultiSelect />
|
|
201
205
|
display above those of another following shortly after it -->
|
|
202
206
|
<div
|
|
203
|
-
class="multiselect {outerDivClass}"
|
|
204
207
|
class:readonly
|
|
205
|
-
class:single={maxSelect
|
|
208
|
+
class:single={maxSelect === 1}
|
|
206
209
|
class:open={showOptions}
|
|
210
|
+
class="multiselect {outerDivClass}"
|
|
207
211
|
on:mouseup|stopPropagation={() => setOptionsVisible(true)}
|
|
208
212
|
use:onClickOutside={() => setOptionsVisible(false)}
|
|
209
213
|
use:onClickOutside={() => dispatch(`blur`)}
|
|
210
214
|
>
|
|
211
|
-
|
|
215
|
+
<!-- invisible input, used only to prevent form submission if required=true and no options selected -->
|
|
216
|
+
<input {required} bind:value={formValue} tabindex="-1" class="form-control" />
|
|
217
|
+
<ExpandIcon style="min-width: 1em; padding: 0 1pt;" />
|
|
212
218
|
<ul class="selected {ulSelectedClass}">
|
|
213
219
|
{#each selected as option, idx}
|
|
214
220
|
<li class={liSelectedClass}>
|
|
215
|
-
<slot name="
|
|
221
|
+
<slot name="selected" {option} {idx}>
|
|
216
222
|
{option.label}
|
|
217
223
|
</slot>
|
|
218
224
|
{#if !readonly}
|
|
@@ -227,59 +233,72 @@ display above those of another following shortly after it -->
|
|
|
227
233
|
{/if}
|
|
228
234
|
</li>
|
|
229
235
|
{/each}
|
|
236
|
+
<li style="display: contents;">
|
|
237
|
+
<input
|
|
238
|
+
bind:this={input}
|
|
239
|
+
autocomplete="off"
|
|
240
|
+
bind:value={searchText}
|
|
241
|
+
on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
|
|
242
|
+
on:keydown={handleKeydown}
|
|
243
|
+
on:focus={() => setOptionsVisible(true)}
|
|
244
|
+
{id}
|
|
245
|
+
{name}
|
|
246
|
+
placeholder={selectedLabels.length ? `` : placeholder}
|
|
247
|
+
/>
|
|
248
|
+
</li>
|
|
230
249
|
</ul>
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
on:keydown={handleKeydown}
|
|
237
|
-
on:focus={() => setOptionsVisible(true)}
|
|
238
|
-
{id}
|
|
239
|
-
{name}
|
|
240
|
-
placeholder={selectedLabels.length ? `` : placeholder}
|
|
241
|
-
/>
|
|
250
|
+
{#if loading}
|
|
251
|
+
<slot name="spinner">
|
|
252
|
+
<CircleSpinner />
|
|
253
|
+
</slot>
|
|
254
|
+
{/if}
|
|
242
255
|
{#if readonly}
|
|
243
256
|
<ReadOnlyIcon height="14pt" />
|
|
244
257
|
{:else if selected.length > 0}
|
|
245
|
-
{#if maxSelect
|
|
258
|
+
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
|
|
246
259
|
<Wiggle bind:wiggle angle={20}>
|
|
247
|
-
<span style="padding: 0 3pt;">
|
|
260
|
+
<span style="padding: 0 3pt;">
|
|
261
|
+
{maxSelectMsg?.(selected.length, maxSelect) ??
|
|
262
|
+
(maxSelect > 1 ? `${selected.length}/${maxSelect}` : ``)}
|
|
263
|
+
</span>
|
|
248
264
|
</Wiggle>
|
|
249
265
|
{/if}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
266
|
+
{#if maxSelect !== 1}
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
class="remove-all"
|
|
270
|
+
title={removeAllTitle}
|
|
271
|
+
on:mouseup|stopPropagation={removeAll}
|
|
272
|
+
on:keydown={handleEnterAndSpaceKeys(removeAll)}
|
|
273
|
+
>
|
|
274
|
+
<CrossIcon height="14pt" />
|
|
275
|
+
</button>
|
|
276
|
+
{/if}
|
|
259
277
|
{/if}
|
|
260
278
|
|
|
261
279
|
{#key showOptions}
|
|
262
280
|
<ul
|
|
263
|
-
class="options {ulOptionsClass}"
|
|
264
281
|
class:hidden={!showOptions}
|
|
282
|
+
class="options {ulOptionsClass}"
|
|
265
283
|
transition:fly|local={{ duration: 300, y: 40 }}
|
|
266
284
|
>
|
|
267
285
|
{#each matchingOptions as option, idx}
|
|
268
286
|
{@const { label, disabled, title = null, selectedTitle } = option}
|
|
269
287
|
{@const { disabledTitle = defaultDisabledTitle } = option}
|
|
288
|
+
{@const active = activeOption?.label === label}
|
|
270
289
|
<li
|
|
271
290
|
on:mouseup|preventDefault|stopPropagation
|
|
272
291
|
on:mousedown|preventDefault|stopPropagation={() => {
|
|
273
292
|
if (disabled) return
|
|
274
293
|
isSelected(label) ? remove(label) : add(label)
|
|
275
294
|
}}
|
|
295
|
+
title={disabled ? disabledTitle : (isSelected(label) && selectedTitle) || title}
|
|
276
296
|
class:selected={isSelected(label)}
|
|
277
|
-
class:active
|
|
297
|
+
class:active
|
|
278
298
|
class:disabled
|
|
279
|
-
|
|
280
|
-
class={liOptionClass}
|
|
299
|
+
class="{liOptionClass} {active ? liActiveOptionClass : ``}"
|
|
281
300
|
>
|
|
282
|
-
<slot name="
|
|
301
|
+
<slot name="option" {option} {idx}>
|
|
283
302
|
{option.label}
|
|
284
303
|
</slot>
|
|
285
304
|
</li>
|
|
@@ -294,15 +313,14 @@ display above those of another following shortly after it -->
|
|
|
294
313
|
:where(div.multiselect) {
|
|
295
314
|
position: relative;
|
|
296
315
|
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
316
|
align-items: center;
|
|
302
|
-
min-height: 18pt;
|
|
303
317
|
display: flex;
|
|
304
318
|
cursor: text;
|
|
305
319
|
padding: 0 3pt;
|
|
320
|
+
border: var(--sms-border, 1pt solid lightgray);
|
|
321
|
+
border-radius: var(--sms-border-radius, 5pt);
|
|
322
|
+
background: var(--sms-input-bg);
|
|
323
|
+
min-height: var(--sms-input-min-height, 22pt);
|
|
306
324
|
}
|
|
307
325
|
:where(div.multiselect.open) {
|
|
308
326
|
z-index: var(--sms-open-z-index, 4);
|
|
@@ -314,31 +332,33 @@ display above those of another following shortly after it -->
|
|
|
314
332
|
background: var(--sms-readonly-bg, lightgray);
|
|
315
333
|
}
|
|
316
334
|
|
|
317
|
-
:where(ul.selected) {
|
|
335
|
+
:where(div.multiselect > ul.selected) {
|
|
318
336
|
display: flex;
|
|
337
|
+
flex: 1;
|
|
319
338
|
padding: 0;
|
|
320
339
|
margin: 0;
|
|
321
340
|
flex-wrap: wrap;
|
|
322
341
|
}
|
|
323
|
-
:where(ul.selected > li) {
|
|
324
|
-
background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue));
|
|
342
|
+
:where(div.multiselect > ul.selected > li) {
|
|
325
343
|
align-items: center;
|
|
326
344
|
border-radius: 4pt;
|
|
327
345
|
display: flex;
|
|
328
346
|
margin: 2pt;
|
|
329
|
-
|
|
347
|
+
line-height: normal;
|
|
348
|
+
padding: 1pt 2pt 1pt 5pt;
|
|
330
349
|
transition: 0.3s;
|
|
331
350
|
white-space: nowrap;
|
|
332
|
-
|
|
351
|
+
background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue));
|
|
352
|
+
height: var(--sms-selected-li-height);
|
|
333
353
|
}
|
|
334
|
-
:where(ul.selected > li button, button.remove-all) {
|
|
354
|
+
:where(div.multiselect > ul.selected > li button, button.remove-all) {
|
|
335
355
|
align-items: center;
|
|
336
356
|
border-radius: 50%;
|
|
337
357
|
display: flex;
|
|
338
358
|
cursor: pointer;
|
|
339
359
|
transition: 0.2s;
|
|
340
360
|
}
|
|
341
|
-
:where(button) {
|
|
361
|
+
:where(div.multiselect button) {
|
|
342
362
|
color: inherit;
|
|
343
363
|
background: transparent;
|
|
344
364
|
border: none;
|
|
@@ -349,46 +369,58 @@ display above those of another following shortly after it -->
|
|
|
349
369
|
:where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
|
|
350
370
|
color: var(--sms-remove-x-hover-focus-color, lightskyblue);
|
|
351
371
|
}
|
|
352
|
-
:where(button:focus) {
|
|
372
|
+
:where(div.multiselect > button:focus) {
|
|
353
373
|
transform: scale(1.04);
|
|
354
374
|
}
|
|
355
375
|
|
|
356
|
-
:where(div.multiselect > input) {
|
|
376
|
+
:where(div.multiselect > ul.selected > li > input) {
|
|
357
377
|
border: none;
|
|
358
378
|
outline: none;
|
|
359
379
|
background: none;
|
|
360
|
-
color: var(--sms-text-color, inherit);
|
|
361
380
|
flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
|
|
362
381
|
min-width: 2em;
|
|
363
382
|
/* minimum font-size > 16px ensures iOS doesn't zoom in when focusing input */
|
|
364
383
|
/* https://stackoverflow.com/a/6394497 */
|
|
365
384
|
font-size: calc(16px + 0.1vw);
|
|
385
|
+
color: var(--sms-text-color, inherit);
|
|
386
|
+
}
|
|
387
|
+
:where(div.multiselect > input.form-control) {
|
|
388
|
+
width: 2em;
|
|
389
|
+
position: absolute;
|
|
390
|
+
background: transparent;
|
|
391
|
+
border: none;
|
|
392
|
+
outline: none;
|
|
393
|
+
z-index: -1;
|
|
394
|
+
opacity: 0;
|
|
366
395
|
}
|
|
367
396
|
|
|
368
|
-
:where(ul.options) {
|
|
397
|
+
:where(div.multiselect > ul.options) {
|
|
369
398
|
list-style: none;
|
|
370
|
-
max-height: 50vh;
|
|
371
399
|
padding: 0;
|
|
372
400
|
top: 100%;
|
|
401
|
+
left: 0;
|
|
373
402
|
width: 100%;
|
|
374
403
|
position: absolute;
|
|
375
404
|
border-radius: 1ex;
|
|
376
405
|
overflow: auto;
|
|
377
406
|
background: var(--sms-options-bg, white);
|
|
407
|
+
max-height: var(--sms-options-max-height, 50vh);
|
|
378
408
|
overscroll-behavior: var(--sms-options-overscroll, none);
|
|
409
|
+
box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
|
|
379
410
|
}
|
|
380
|
-
:where(ul.options.hidden) {
|
|
411
|
+
:where(div.multiselect > ul.options.hidden) {
|
|
381
412
|
visibility: hidden;
|
|
382
413
|
}
|
|
383
|
-
:where(ul.options li) {
|
|
414
|
+
:where(div.multiselect > ul.options > li) {
|
|
384
415
|
padding: 3pt 2ex;
|
|
385
416
|
cursor: pointer;
|
|
417
|
+
scroll-margin: var(--sms-options-scroll-margin, 100px);
|
|
386
418
|
}
|
|
387
419
|
/* for noOptionsMsg */
|
|
388
|
-
:where(ul.options span) {
|
|
420
|
+
:where(div.multiselect > ul.options span) {
|
|
389
421
|
padding: 3pt 2ex;
|
|
390
422
|
}
|
|
391
|
-
:where(ul.options li.selected) {
|
|
423
|
+
:where(div.multiselect > ul.options > li.selected) {
|
|
392
424
|
border-left: var(
|
|
393
425
|
--sms-li-selected-border-left,
|
|
394
426
|
3pt solid var(--sms-selected-color, green)
|
|
@@ -396,22 +428,21 @@ display above those of another following shortly after it -->
|
|
|
396
428
|
background: var(--sms-li-selected-bg, inherit);
|
|
397
429
|
color: var(--sms-li-selected-color, inherit);
|
|
398
430
|
}
|
|
399
|
-
:where(ul.options li:not(.selected):hover) {
|
|
431
|
+
:where(div.multiselect > ul.options > li:not(.selected):hover) {
|
|
400
432
|
border-left: var(
|
|
401
433
|
--sms-li-not-selected-hover-border-left,
|
|
402
434
|
3pt solid var(--sms-active-color, cornflowerblue)
|
|
403
435
|
);
|
|
404
|
-
border-left: 3pt solid var(--blue);
|
|
405
436
|
}
|
|
406
|
-
:where(ul.options li.active) {
|
|
437
|
+
:where(div.multiselect > ul.options > li.active) {
|
|
407
438
|
background: var(--sms-li-active-bg, var(--sms-active-color, cornflowerblue));
|
|
408
439
|
}
|
|
409
|
-
:where(ul.options li.disabled) {
|
|
440
|
+
:where(div.multiselect > ul.options > li.disabled) {
|
|
441
|
+
cursor: not-allowed;
|
|
410
442
|
background: var(--sms-li-disabled-bg, #f5f5f6);
|
|
411
443
|
color: var(--sms-li-disabled-text, #b8b8b8);
|
|
412
|
-
cursor: not-allowed;
|
|
413
444
|
}
|
|
414
|
-
:where(ul.options li.disabled:hover) {
|
|
445
|
+
:where(div.multiselect > ul.options > li.disabled:hover) {
|
|
415
446
|
border-left: unset;
|
|
416
447
|
}
|
|
417
448
|
</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,14 +17,20 @@ 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;
|
|
30
|
+
allowUserOptions?: boolean | "append" | undefined;
|
|
31
|
+
autoScroll?: boolean | undefined;
|
|
32
|
+
loading?: boolean | undefined;
|
|
33
|
+
required?: boolean | undefined;
|
|
26
34
|
};
|
|
27
35
|
events: {
|
|
28
36
|
mouseup: MouseEvent;
|
|
@@ -30,11 +38,12 @@ declare const __propDef: {
|
|
|
30
38
|
[evt: string]: CustomEvent<any>;
|
|
31
39
|
};
|
|
32
40
|
slots: {
|
|
33
|
-
|
|
41
|
+
selected: {
|
|
34
42
|
option: Option;
|
|
35
43
|
idx: any;
|
|
36
44
|
};
|
|
37
|
-
|
|
45
|
+
spinner: {};
|
|
46
|
+
option: {
|
|
38
47
|
option: Option;
|
|
39
48
|
idx: any;
|
|
40
49
|
};
|
package/index.d.ts
CHANGED
|
@@ -13,3 +13,20 @@ export declare type Option = {
|
|
|
13
13
|
export declare type ProtoOption = Primitive | (Omit<Option, `value`> & {
|
|
14
14
|
value?: Primitive;
|
|
15
15
|
});
|
|
16
|
+
export declare type DispatchEvents = {
|
|
17
|
+
add: {
|
|
18
|
+
option: Option;
|
|
19
|
+
};
|
|
20
|
+
remove: {
|
|
21
|
+
option: Option;
|
|
22
|
+
};
|
|
23
|
+
removeAll: {
|
|
24
|
+
options: Option[];
|
|
25
|
+
};
|
|
26
|
+
change: {
|
|
27
|
+
option?: Option;
|
|
28
|
+
options?: Option[];
|
|
29
|
+
type: 'add' | 'remove' | 'removeAll';
|
|
30
|
+
};
|
|
31
|
+
blur: undefined;
|
|
32
|
+
};
|
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": "
|
|
8
|
+
"version": "4.0.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
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
|
|
36
36
|
## Recent breaking changes
|
|
37
37
|
|
|
38
|
-
- v2.0.0 added the ability to pass options as objects. As a result, `bind:selected` no longer returns simple strings but objects, 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`).
|
|
39
38
|
- v3.0.0 changed the `event.detail` payload for `'add'`, `'remove'` and `'change'` events from `token` to `option`, e.g.
|
|
40
39
|
|
|
41
40
|
```js
|
|
@@ -45,6 +44,10 @@
|
|
|
45
44
|
|
|
46
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`.
|
|
47
46
|
|
|
47
|
+
- v4.0.0 renamed the slots for customizing how selected options and dropdown list items are rendered:
|
|
48
|
+
- old: `<slot name="renderOptions" />`, new: `<slot name="option" />`
|
|
49
|
+
- old: `<slot name="renderSelected" />`, new: `<slot name="selected" />`
|
|
50
|
+
|
|
48
51
|
## Installation
|
|
49
52
|
|
|
50
53
|
```sh
|
|
@@ -88,43 +91,69 @@ Full list of props/bindable variables for this component:
|
|
|
88
91
|
<div class="table">
|
|
89
92
|
|
|
90
93
|
<!-- prettier-ignore -->
|
|
91
|
-
| name
|
|
92
|
-
|
|
|
93
|
-
| `options`
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
94
|
+
| name | default | description |
|
|
95
|
+
| :----------------- | :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
96
|
+
| `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. |
|
|
97
|
+
| `showOptions` | `false` | Bindable boolean that controls whether the options dropdown is visible. |
|
|
98
|
+
| `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. |
|
|
99
|
+
| `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
|
|
100
|
+
| `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
|
|
101
|
+
| `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
|
|
102
|
+
| `selectedLabels` | `[]` | Labels of currently selected options. |
|
|
103
|
+
| `selectedValues` | `[]` | Values of currently selected options. |
|
|
104
|
+
| `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. |
|
|
105
|
+
| `readonly` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
|
|
106
|
+
| `placeholder` | `undefined` | String shown in the text input when no option is selected. |
|
|
107
|
+
| `input` | `undefined` | Handle to the `<input>` DOM node. |
|
|
108
|
+
| `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. |
|
|
109
|
+
| `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>`. |
|
|
110
|
+
| `required` | `false` | Whether forms can be submitted without selecting any options. Aborts submission, is scrolled into view and shows help "Please fill out" message when true and user tries to submit with no options selected. |
|
|
111
|
+
| `autoScroll` | `true` | `false` disables keeping the active dropdown items in view when going up/down the list of options with arrow keys. |
|
|
112
|
+
| `allowUserOptions` | `false` | Whether users are allowed to enter values not in the dropdown list. `true` means add user-defined options to the selected list only, `'append'` means add to both options and selected. |
|
|
113
|
+
| `loading` | `false` | Whether the component should display a spinner to indicate it's in loading state. Use `<slot name='spinner'>` to specify a custom spinner. |
|
|
105
114
|
|
|
106
115
|
</div>
|
|
107
116
|
|
|
117
|
+
## Exposed methods
|
|
118
|
+
|
|
119
|
+
1. `filterFunc = (op: Option, searchText: string) => boolean`: Determine what options are shown when user enters search string to filter dropdown list. Defaults to:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
filterFunc = (op: Option, searchText: string) => {
|
|
123
|
+
if (!searchText) return true
|
|
124
|
+
return `${op.label}`.toLowerCase().includes(searchText.toLowerCase())
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
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:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
maxSelectMsg = (current: number, max: number) => `${current}/${max}`
|
|
132
|
+
```
|
|
133
|
+
|
|
108
134
|
## Slots
|
|
109
135
|
|
|
110
|
-
`MultiSelect.svelte`
|
|
136
|
+
`MultiSelect.svelte` has 3 named slots:
|
|
111
137
|
|
|
112
|
-
- `slot="
|
|
113
|
-
- `slot="
|
|
138
|
+
- `slot="option"`: Customize rendering of dropdown options. Receives as props the `option` object and the zero-indexed position (`idx`) it has in the dropdown.
|
|
139
|
+
- `slot="selected"`: Customize rendering selected tags. Receives as props the `option` object and the zero-indexed position (`idx`) it has in the list of selected items.
|
|
140
|
+
- `slot="spinner"`: Custom spinner component to display when in `loading` state. Receives no props.
|
|
114
141
|
|
|
115
|
-
|
|
142
|
+
Example:
|
|
116
143
|
|
|
117
144
|
```svelte
|
|
118
145
|
<MultiSelect options={[`Banana`, `Apple`, `Mango`]}>
|
|
119
|
-
<span let:idx let:option slot="
|
|
146
|
+
<span let:idx let:option slot="option">
|
|
120
147
|
{idx + 1}. {option.label}
|
|
121
148
|
{option.label === `Mango` ? `🎉` : ``}
|
|
122
149
|
</span>
|
|
123
150
|
|
|
124
|
-
<span let:idx let:option slot="
|
|
151
|
+
<span let:idx let:option slot="selected">
|
|
125
152
|
#{idx + 1}
|
|
126
153
|
{option.label}
|
|
127
154
|
</span>
|
|
155
|
+
|
|
156
|
+
<CustomSpinner slot="spinner">
|
|
128
157
|
</MultiSelect>
|
|
129
158
|
```
|
|
130
159
|
|
|
@@ -194,30 +223,40 @@ There are 3 ways to style this component. To understand which options do what, i
|
|
|
194
223
|
|
|
195
224
|
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
225
|
|
|
197
|
-
- `div.multiselect
|
|
226
|
+
- `div.multiselect`
|
|
198
227
|
- `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
|
-
|
|
228
|
+
- `border-radius: var(--sms-border-radius, 5pt)`
|
|
229
|
+
- `background: var(--sms-input-bg)`
|
|
230
|
+
- `height: var(--sms-input-height, 2em)`
|
|
231
|
+
- `div.multiselect.open`
|
|
232
|
+
- `z-index: var(--sms-open-z-index, 4)`: Increase this if needed to ensure the dropdown list is displayed atop all other page elements.
|
|
233
|
+
- `div.multiselect:focus-within`
|
|
234
|
+
- `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`.
|
|
235
|
+
- `div.multiselect.readonly`
|
|
203
236
|
- `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`
|
|
237
|
+
- `div.multiselect > ul.selected > li > input`
|
|
207
238
|
- `color: var(--sms-text-color, inherit)`: Input text color.
|
|
208
|
-
- `ul.selected > li
|
|
239
|
+
- `div.multiselect > ul.selected > li`
|
|
209
240
|
- `background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue))`: Background of selected options.
|
|
210
|
-
- `
|
|
241
|
+
- `height: var(--sms-selected-li-height)`: Height of selected options.
|
|
242
|
+
- `ul.selected > li button:hover, button.remove-all:hover, button:focus`
|
|
211
243
|
- `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
|
-
- `
|
|
215
|
-
- `
|
|
244
|
+
- `div.multiselect > ul.options`
|
|
245
|
+
- `background: var(--sms-options-bg, white)`: Background of dropdown list.
|
|
246
|
+
- `max-height: var(--sms-options-max-height, 50vh)`: Maximum height of options dropdown.
|
|
247
|
+
- `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).
|
|
248
|
+
- `box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);`: Box shadow of dropdown list.
|
|
249
|
+
- `div.multiselect > ul.options > li`
|
|
250
|
+
- `scroll-margin: var(--sms-options-scroll-margin, 100px)`: Top/bottom margin to keep between dropdown list items and top/bottom screen edge when auto-scrolling list to keep items in view.
|
|
251
|
+
- `div.multiselect > ul.options > li.selected`
|
|
252
|
+
- `border-left: var(--sms-li-selected-border-left, 3pt solid var(--sms-selected-color, green))`
|
|
216
253
|
- `background: var(--sms-li-selected-bg, inherit)`: Background of selected list items in options pane.
|
|
217
254
|
- `color: var(--sms-li-selected-color, inherit)`: Text color of selected list items in options pane.
|
|
218
|
-
- `ul.options > li.
|
|
255
|
+
- `div.multiselect > ul.options > li:not(.selected):hover`
|
|
256
|
+
- `border-left: var(--sms-li-not-selected-hover-border-left, 3pt solid var(--sms-active-color, cornflowerblue))`
|
|
257
|
+
- `div.multiselect > ul.options > li.active`
|
|
219
258
|
- `background: var(--sms-li-active-bg, var(--sms-active-color, cornflowerblue))`: Background of active (currently with arrow keys highlighted) list item.
|
|
220
|
-
- `ul.options > li.disabled`
|
|
259
|
+
- `div.multiselect > ul.options > li.disabled`
|
|
221
260
|
- `background: var(--sms-li-disabled-bg, #f5f5f6)`: Background of disabled options in the dropdown list.
|
|
222
261
|
- `color: var(--sms-li-disabled-text, #b8b8b8)`: Text color of disabled option in the dropdown list.
|
|
223
262
|
|
|
@@ -236,6 +275,7 @@ The second method allows you to pass in custom classes to the important DOM elem
|
|
|
236
275
|
- `liSelectedClass`
|
|
237
276
|
- `ulOptionsClass`
|
|
238
277
|
- `liOptionClass`
|
|
278
|
+
- `liActiveOptionClass`
|
|
239
279
|
|
|
240
280
|
This simplified version of the DOM structure of this component shows where these classes are inserted:
|
|
241
281
|
|
|
@@ -247,7 +287,9 @@ This simplified version of the DOM structure of this component shows where these
|
|
|
247
287
|
</ul>
|
|
248
288
|
<ul class="options {ulOptionsClass}">
|
|
249
289
|
<li class={liOptionClass}>Option 1</li>
|
|
250
|
-
<li class={liOptionClass}
|
|
290
|
+
<li class="{liOptionClass} {liActiveOptionClass}">
|
|
291
|
+
Option 2 (currently active)
|
|
292
|
+
</li>
|
|
251
293
|
</ul>
|
|
252
294
|
</div>
|
|
253
295
|
```
|
|
@@ -257,40 +299,47 @@ This simplified version of the DOM structure of this component shows where these
|
|
|
257
299
|
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.
|
|
258
300
|
|
|
259
301
|
```css
|
|
260
|
-
:global(.multiselect) {
|
|
302
|
+
:global(div.multiselect) {
|
|
261
303
|
/* top-level wrapper div */
|
|
262
304
|
}
|
|
263
|
-
:global(.multiselect
|
|
264
|
-
/*
|
|
305
|
+
:global(div.multiselect.open) {
|
|
306
|
+
/* top-level wrapper div when dropdown open */
|
|
265
307
|
}
|
|
266
|
-
:global(.multiselect
|
|
267
|
-
|
|
308
|
+
:global(div.multiselect.readonly) {
|
|
309
|
+
/* top-level wrapper div when in readonly state */
|
|
310
|
+
}
|
|
311
|
+
:global(div.multiselect > ul.selected) {
|
|
312
|
+
/* selected list */
|
|
313
|
+
}
|
|
314
|
+
:global(div.multiselect > ul.selected > li) {
|
|
315
|
+
/* selected list items */
|
|
316
|
+
}
|
|
317
|
+
:global(div.multiselect button) {
|
|
318
|
+
/* target all buttons in this component */
|
|
319
|
+
}
|
|
320
|
+
:global(div.multiselect > ul.selected > li button, button.remove-all) {
|
|
268
321
|
/* buttons to remove a single or all selected options at once */
|
|
269
322
|
}
|
|
270
|
-
:global(.multiselect ul.
|
|
323
|
+
:global(div.multiselect > ul.selected > li > input) {
|
|
324
|
+
/* input inside the top-level wrapper div */
|
|
325
|
+
}
|
|
326
|
+
:global(div.multiselect > ul.options) {
|
|
271
327
|
/* dropdown options */
|
|
272
328
|
}
|
|
273
|
-
:global(.multiselect ul.options li) {
|
|
274
|
-
/* dropdown list
|
|
329
|
+
:global(div.multiselect > ul.options > li) {
|
|
330
|
+
/* dropdown list items */
|
|
275
331
|
}
|
|
276
|
-
:global(.multiselect ul.options li.selected) {
|
|
332
|
+
:global(div.multiselect > ul.options > li.selected) {
|
|
277
333
|
/* selected options in the dropdown list */
|
|
278
334
|
}
|
|
279
|
-
:global(.multiselect ul.options li:not(.selected):hover) {
|
|
335
|
+
:global(div.multiselect > ul.options > li:not(.selected):hover) {
|
|
280
336
|
/* unselected but hovered options in the dropdown list */
|
|
281
337
|
}
|
|
282
|
-
:global(.multiselect ul.options li.
|
|
283
|
-
/* selected and hovered options in the dropdown list */
|
|
284
|
-
/* probably not necessary to style this state in most cases */
|
|
285
|
-
}
|
|
286
|
-
:global(.multiselect ul.options li.active) {
|
|
338
|
+
:global(div.multiselect > ul.options > li.active) {
|
|
287
339
|
/* active means item was navigated to with up/down arrow keys */
|
|
288
340
|
/* ready to be selected by pressing enter */
|
|
289
341
|
}
|
|
290
|
-
:global(.multiselect ul.options li.
|
|
291
|
-
/* both active and already selected, pressing enter now will deselect the item */
|
|
292
|
-
}
|
|
293
|
-
:global(.multiselect ul.options li.disabled) {
|
|
342
|
+
:global(div.multiselect > ul.options > li.disabled) {
|
|
294
343
|
/* options with disabled key set to true (see props above) */
|
|
295
344
|
}
|
|
296
345
|
```
|