svelte-multiselect 11.0.0-rc.1 → 11.1.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/dist/CircleSpinner.svelte +3 -4
- package/dist/CircleSpinner.svelte.d.ts +7 -19
- package/dist/CmdPalette.svelte +19 -37
- package/dist/CmdPalette.svelte.d.ts +19 -32
- package/dist/MultiSelect.svelte +322 -288
- package/dist/MultiSelect.svelte.d.ts +16 -105
- package/dist/Wiggle.svelte +10 -19
- package/dist/Wiggle.svelte.d.ts +14 -26
- package/dist/icons/ChevronExpand.svelte +5 -1
- package/dist/icons/ChevronExpand.svelte.d.ts +4 -25
- package/dist/icons/Cross.svelte +6 -2
- package/dist/icons/Cross.svelte.d.ts +4 -25
- package/dist/icons/Disabled.svelte +5 -1
- package/dist/icons/Disabled.svelte.d.ts +4 -25
- package/dist/icons/Octocat.svelte +5 -1
- package/dist/icons/Octocat.svelte.d.ts +4 -25
- package/dist/index.d.ts +2 -1
- package/dist/index.js +9 -7
- package/dist/props.d.ts +143 -0
- package/dist/props.js +1 -0
- package/dist/types.d.ts +0 -40
- package/dist/utils.d.ts +22 -3
- package/dist/utils.js +60 -3
- package/package.json +34 -31
- package/readme.md +45 -66
package/dist/MultiSelect.svelte
CHANGED
|
@@ -1,94 +1,30 @@
|
|
|
1
|
-
<script>import {
|
|
1
|
+
<script lang="ts">import { browser } from '$app/environment';
|
|
2
|
+
import { tick } from 'svelte';
|
|
2
3
|
import { flip } from 'svelte/animate';
|
|
3
4
|
import CircleSpinner from './CircleSpinner.svelte';
|
|
4
5
|
import Wiggle from './Wiggle.svelte';
|
|
5
6
|
import { CrossIcon, DisabledIcon, ExpandIcon } from './icons';
|
|
6
|
-
import { get_label, get_style } from './utils';
|
|
7
|
-
|
|
8
|
-
export let activeOption = null;
|
|
9
|
-
export let createOptionMsg = `Create this option...`;
|
|
10
|
-
export let allowUserOptions = false;
|
|
11
|
-
export let allowEmpty = false; // added for https://github.com/janosh/svelte-multiselect/issues/192
|
|
12
|
-
export let autocomplete = `off`;
|
|
13
|
-
export let autoScroll = true;
|
|
14
|
-
export let breakpoint = 800; // any screen with more horizontal pixels is considered desktop, below is mobile
|
|
15
|
-
export let defaultDisabledTitle = `This option is disabled`;
|
|
16
|
-
export let disabled = false;
|
|
17
|
-
export let disabledInputTitle = `This input is disabled`;
|
|
18
|
-
// prettier-ignore
|
|
19
|
-
export let duplicateOptionMsg = `This option is already selected`;
|
|
20
|
-
export let duplicates = false; // whether to allow duplicate options
|
|
21
|
-
// takes two options and returns true if they are equal
|
|
22
|
-
// case-insensitive equality comparison after string coercion and looks only at the `label` key of object options by default
|
|
23
|
-
export let key = (opt) => `${get_label(opt)}`.toLowerCase();
|
|
24
|
-
export let filterFunc = (opt, searchText) => {
|
|
7
|
+
import { get_label, get_style, highlight_matching_nodes } from './utils';
|
|
8
|
+
let { activeIndex = $bindable(null), activeOption = $bindable(null), createOptionMsg = `Create this option...`, allowUserOptions = false, allowEmpty = false, autocomplete = `off`, autoScroll = true, breakpoint = 800, defaultDisabledTitle = `This option is disabled`, disabled = false, disabledInputTitle = `This input is disabled`, duplicateOptionMsg = `This option is already selected`, duplicates = false, key = (opt) => `${get_label(opt)}`.toLowerCase(), filterFunc = (opt, searchText) => {
|
|
25
9
|
if (!searchText)
|
|
26
10
|
return true;
|
|
27
11
|
return `${get_label(opt)}`.toLowerCase().includes(searchText.toLowerCase());
|
|
28
|
-
}
|
|
29
|
-
export let closeDropdownOnSelect = `desktop`;
|
|
30
|
-
export let form_input = null;
|
|
31
|
-
export let highlightMatches = true;
|
|
32
|
-
export let id = null;
|
|
33
|
-
export let input = null;
|
|
34
|
-
export let inputClass = ``;
|
|
35
|
-
export let inputStyle = null;
|
|
36
|
-
export let inputmode = null;
|
|
37
|
-
export let invalid = false;
|
|
38
|
-
export let liActiveOptionClass = ``;
|
|
39
|
-
export let liActiveUserMsgClass = ``;
|
|
40
|
-
export let liOptionClass = ``;
|
|
41
|
-
export let liOptionStyle = null;
|
|
42
|
-
export let liSelectedClass = ``;
|
|
43
|
-
export let liSelectedStyle = null;
|
|
44
|
-
export let liUserMsgClass = ``;
|
|
45
|
-
export let loading = false;
|
|
46
|
-
export let matchingOptions = [];
|
|
47
|
-
export let maxOptions = undefined;
|
|
48
|
-
export let maxSelect = null; // null means there is no upper limit for selected.length
|
|
49
|
-
export let maxSelectMsg = (current, max) => (max > 1 ? `${current}/${max}` : ``);
|
|
50
|
-
export let maxSelectMsgClass = ``;
|
|
51
|
-
export let name = null;
|
|
52
|
-
export let noMatchingOptionsMsg = `No matching options`;
|
|
53
|
-
export let open = false;
|
|
54
|
-
export let options;
|
|
55
|
-
export let outerDiv = null;
|
|
56
|
-
export let outerDivClass = ``;
|
|
57
|
-
export let parseLabelsAsHtml = false; // should not be combined with allowUserOptions!
|
|
58
|
-
export let pattern = null;
|
|
59
|
-
export let placeholder = null;
|
|
60
|
-
export let removeAllTitle = `Remove all`;
|
|
61
|
-
export let removeBtnTitle = `Remove`;
|
|
62
|
-
export let minSelect = null; // null means there is no lower limit for selected.length
|
|
63
|
-
export let required = false;
|
|
64
|
-
export let resetFilterOnAdd = true;
|
|
65
|
-
export let searchText = ``;
|
|
66
|
-
export let selected = options
|
|
12
|
+
}, closeDropdownOnSelect = `desktop`, form_input = $bindable(null), highlightMatches = true, id = null, input = $bindable(null), inputClass = ``, inputStyle = null, inputmode = null, invalid = $bindable(false), liActiveOptionClass = ``, liActiveUserMsgClass = ``, liOptionClass = ``, liOptionStyle = null, liSelectedClass = ``, liSelectedStyle = null, liUserMsgClass = ``, loading = false, matchingOptions = $bindable([]), maxOptions = undefined, maxSelect = null, maxSelectMsg = (current, max) => (max > 1 ? `${current}/${max}` : ``), maxSelectMsgClass = ``, name = null, noMatchingOptionsMsg = `No matching options`, open = $bindable(false), options = $bindable(), outerDiv = $bindable(null), outerDivClass = ``, parseLabelsAsHtml = false, pattern = null, placeholder = null, removeAllTitle = `Remove all`, removeBtnTitle = `Remove`, minSelect = null, required = false, resetFilterOnAdd = true, searchText = $bindable(``), selected = $bindable(options
|
|
67
13
|
?.filter((opt) => opt instanceof Object && opt?.preselected)
|
|
68
|
-
.slice(0, maxSelect ?? undefined) ?? []
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
export let value = null;
|
|
77
|
-
const selected_to_value = (selected) => {
|
|
78
|
-
value = maxSelect === 1 ? selected[0] ?? null : selected;
|
|
79
|
-
};
|
|
80
|
-
const value_to_selected = (value) => {
|
|
14
|
+
.slice(0, maxSelect ?? undefined) ?? []), sortSelected = false, selectedOptionsDraggable = !sortSelected, style = null, ulOptionsClass = ``, ulSelectedClass = ``, ulSelectedStyle = null, ulOptionsStyle = null, value = $bindable(null), expandIcon, selectedItem, children, removeIcon, afterInput, spinner, disabledIcon, option, userMsg, onblur, onclick, onfocus, onkeydown, onkeyup, onmousedown, onmouseenter, onmouseleave, ontouchcancel, ontouchend, ontouchmove, ontouchstart, onadd, oncreate, onremove, onremoveAll, onchange, onopen, onclose, portal: portal_params = {}, ...rest } = $props();
|
|
15
|
+
$effect.pre(() => {
|
|
16
|
+
// if maxSelect=1, value is the single item in selected (or null if selected is empty)
|
|
17
|
+
// this solves both https://github.com/janosh/svelte-multiselect/issues/86 and
|
|
18
|
+
// https://github.com/janosh/svelte-multiselect/issues/136
|
|
19
|
+
value = maxSelect === 1 ? (selected[0] ?? null) : selected;
|
|
20
|
+
}); // sync selected updates to value
|
|
21
|
+
$effect.pre(() => {
|
|
81
22
|
if (maxSelect === 1)
|
|
82
23
|
selected = value ? [value] : [];
|
|
83
24
|
else
|
|
84
25
|
selected = value ?? [];
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// this solves both https://github.com/janosh/svelte-multiselect/issues/86 and
|
|
88
|
-
// https://github.com/janosh/svelte-multiselect/issues/136
|
|
89
|
-
$: selected_to_value(selected);
|
|
90
|
-
$: value_to_selected(value);
|
|
91
|
-
let wiggle = false; // controls wiggle animation when user tries to exceed maxSelect
|
|
26
|
+
}); // sync value updates to selected
|
|
27
|
+
let wiggle = $state(false); // controls wiggle animation when user tries to exceed maxSelect
|
|
92
28
|
if (!(options?.length > 0)) {
|
|
93
29
|
if (allowUserOptions || loading || disabled || allowEmpty) {
|
|
94
30
|
options = []; // initializing as array avoids errors when component mounts
|
|
@@ -123,30 +59,34 @@ if (maxOptions &&
|
|
|
123
59
|
(typeof maxOptions != `number` || maxOptions < 0 || maxOptions % 1 != 0)) {
|
|
124
60
|
console.error(`MultiSelect's maxOptions must be undefined or a positive integer, got ${maxOptions}`);
|
|
125
61
|
}
|
|
126
|
-
|
|
127
|
-
let
|
|
128
|
-
let window_width;
|
|
62
|
+
let option_msg_is_active = $state(false); // controls active state of <li>{createOptionMsg}</li>
|
|
63
|
+
let window_width = $state(0);
|
|
129
64
|
// options matching the current search text
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
65
|
+
$effect.pre(() => {
|
|
66
|
+
matchingOptions = options.filter((opt) => filterFunc(opt, searchText) &&
|
|
67
|
+
// remove already selected options from dropdown list unless duplicate selections are allowed
|
|
68
|
+
(!selected.map(key).includes(key(opt)) || duplicates));
|
|
69
|
+
});
|
|
133
70
|
// raise if matchingOptions[activeIndex] does not yield a value
|
|
134
71
|
if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
135
72
|
throw `Run time error, activeIndex=${activeIndex} is out of bounds, matchingOptions.length=${matchingOptions.length}`;
|
|
136
73
|
}
|
|
137
74
|
// update activeOption when activeIndex changes
|
|
138
|
-
|
|
75
|
+
$effect(() => {
|
|
76
|
+
activeOption = matchingOptions[activeIndex ?? -1] ?? null;
|
|
77
|
+
});
|
|
139
78
|
// add an option to selected list
|
|
140
|
-
function add(
|
|
141
|
-
|
|
79
|
+
function add(option_to_add, event) {
|
|
80
|
+
event.stopPropagation();
|
|
81
|
+
if (maxSelect !== null && selected.length >= maxSelect)
|
|
142
82
|
wiggle = true;
|
|
143
|
-
if (!isNaN(Number(
|
|
144
|
-
|
|
83
|
+
if (!isNaN(Number(option_to_add)) && typeof selected.map(get_label)[0] === `number`) {
|
|
84
|
+
option_to_add = Number(option_to_add); // convert to number if possible
|
|
145
85
|
}
|
|
146
|
-
const is_duplicate = selected.map(key).includes(key(
|
|
86
|
+
const is_duplicate = selected.map(key).includes(key(option_to_add));
|
|
147
87
|
if ((maxSelect === null || maxSelect === 1 || selected.length < maxSelect) &&
|
|
148
88
|
(duplicates || !is_duplicate)) {
|
|
149
|
-
if (!options.includes(
|
|
89
|
+
if (!options.includes(option_to_add) && // first check if we find option in the options list
|
|
150
90
|
// this has the side-effect of not allowing to user to add the same
|
|
151
91
|
// custom option twice in append mode
|
|
152
92
|
[true, `append`].includes(allowUserOptions) &&
|
|
@@ -155,34 +95,34 @@ function add(option, event) {
|
|
|
155
95
|
// a new option from the user-entered text
|
|
156
96
|
if (typeof options[0] === `object`) {
|
|
157
97
|
// if 1st option is an object, we create new option as object to keep type homogeneity
|
|
158
|
-
|
|
98
|
+
option_to_add = { label: searchText };
|
|
159
99
|
}
|
|
160
100
|
else {
|
|
161
101
|
if ([`number`, `undefined`].includes(typeof options[0]) &&
|
|
162
102
|
!isNaN(Number(searchText))) {
|
|
163
103
|
// create new option as number if it parses to a number and 1st option is also number or missing
|
|
164
|
-
|
|
104
|
+
option_to_add = Number(searchText);
|
|
165
105
|
}
|
|
166
106
|
else {
|
|
167
|
-
|
|
107
|
+
option_to_add = searchText; // else create custom option as string
|
|
168
108
|
}
|
|
169
|
-
|
|
109
|
+
oncreate?.({ option: option_to_add });
|
|
170
110
|
}
|
|
171
111
|
if (allowUserOptions === `append`)
|
|
172
|
-
options = [...options,
|
|
112
|
+
options = [...options, option_to_add];
|
|
173
113
|
}
|
|
174
114
|
if (resetFilterOnAdd)
|
|
175
115
|
searchText = ``; // reset search string on selection
|
|
176
|
-
if ([``, undefined, null].includes(
|
|
177
|
-
console.error(`MultiSelect: encountered falsy option ${
|
|
116
|
+
if ([``, undefined, null].includes(option_to_add)) {
|
|
117
|
+
console.error(`MultiSelect: encountered falsy option ${option_to_add}`);
|
|
178
118
|
return;
|
|
179
119
|
}
|
|
180
120
|
if (maxSelect === 1) {
|
|
181
121
|
// for maxSelect = 1 we always replace current option with new one
|
|
182
|
-
selected = [
|
|
122
|
+
selected = [option_to_add];
|
|
183
123
|
}
|
|
184
124
|
else {
|
|
185
|
-
selected = [...selected,
|
|
125
|
+
selected = [...selected, option_to_add];
|
|
186
126
|
if (sortSelected === true) {
|
|
187
127
|
selected = selected.sort((op1, op2) => {
|
|
188
128
|
const [label1, label2] = [get_label(op1), get_label(op2)];
|
|
@@ -196,42 +136,44 @@ function add(option, event) {
|
|
|
196
136
|
}
|
|
197
137
|
const reached_max_select = selected.length === maxSelect;
|
|
198
138
|
const dropdown_should_close = closeDropdownOnSelect === true ||
|
|
199
|
-
(closeDropdownOnSelect === `desktop` && window_width < breakpoint);
|
|
139
|
+
(closeDropdownOnSelect === `desktop` && window_width && window_width < breakpoint);
|
|
200
140
|
if (reached_max_select || dropdown_should_close) {
|
|
201
141
|
close_dropdown(event);
|
|
202
142
|
}
|
|
203
143
|
else if (!dropdown_should_close) {
|
|
204
144
|
input?.focus();
|
|
205
145
|
}
|
|
206
|
-
|
|
207
|
-
|
|
146
|
+
onadd?.({ option: option_to_add });
|
|
147
|
+
onchange?.({ option: option_to_add, type: `add` });
|
|
208
148
|
invalid = false; // reset error status whenever new items are selected
|
|
209
149
|
form_input?.setCustomValidity(``);
|
|
210
150
|
}
|
|
211
151
|
}
|
|
212
152
|
// remove an option from selected list
|
|
213
|
-
function remove(
|
|
153
|
+
function remove(option_to_drop, event) {
|
|
154
|
+
event.stopPropagation();
|
|
214
155
|
if (selected.length === 0)
|
|
215
156
|
return;
|
|
216
|
-
const idx = selected.findIndex((opt) => key(opt) === key(
|
|
217
|
-
let [
|
|
218
|
-
if (
|
|
157
|
+
const idx = selected.findIndex((opt) => key(opt) === key(option_to_drop));
|
|
158
|
+
let [option_removed] = selected.splice(idx, 1); // remove option from selected list
|
|
159
|
+
if (option_removed === undefined && allowUserOptions) {
|
|
219
160
|
// if option with label could not be found but allowUserOptions is truthy,
|
|
220
161
|
// assume it was created by user and create corresponding option object
|
|
221
162
|
// on the fly for use as event payload
|
|
222
163
|
const other_ops_type = typeof options[0];
|
|
223
|
-
|
|
164
|
+
option_removed = (other_ops_type ? { label: option_to_drop } : option_to_drop);
|
|
224
165
|
}
|
|
225
|
-
if (
|
|
226
|
-
return console.error(`Multiselect can't remove selected option ${JSON.stringify(
|
|
166
|
+
if (option_removed === undefined) {
|
|
167
|
+
return console.error(`Multiselect can't remove selected option ${JSON.stringify(option_to_drop)}, not found in selected list`);
|
|
227
168
|
}
|
|
228
169
|
selected = [...selected]; // trigger Svelte rerender
|
|
229
170
|
invalid = false; // reset error status whenever items are removed
|
|
230
171
|
form_input?.setCustomValidity(``);
|
|
231
|
-
|
|
232
|
-
|
|
172
|
+
onremove?.({ option: option_removed });
|
|
173
|
+
onchange?.({ option: option_removed, type: `remove` });
|
|
233
174
|
}
|
|
234
175
|
function open_dropdown(event) {
|
|
176
|
+
event.stopPropagation();
|
|
235
177
|
if (disabled)
|
|
236
178
|
return;
|
|
237
179
|
open = true;
|
|
@@ -239,45 +181,54 @@ function open_dropdown(event) {
|
|
|
239
181
|
// avoid double-focussing input when event that opened dropdown was already input FocusEvent
|
|
240
182
|
input?.focus();
|
|
241
183
|
}
|
|
242
|
-
|
|
184
|
+
onopen?.({ event });
|
|
243
185
|
}
|
|
244
186
|
function close_dropdown(event) {
|
|
245
187
|
open = false;
|
|
246
188
|
input?.blur();
|
|
247
189
|
activeIndex = null;
|
|
248
|
-
|
|
190
|
+
onclose?.({ event });
|
|
249
191
|
}
|
|
250
192
|
// handle all keyboard events this component receives
|
|
251
193
|
async function handle_keydown(event) {
|
|
252
194
|
// on escape or tab out of input: close options dropdown and reset search text
|
|
253
195
|
if (event.key === `Escape` || event.key === `Tab`) {
|
|
196
|
+
event.stopPropagation();
|
|
254
197
|
close_dropdown(event);
|
|
255
198
|
searchText = ``;
|
|
256
199
|
}
|
|
257
200
|
// on enter key: toggle active option and reset search text
|
|
258
201
|
else if (event.key === `Enter`) {
|
|
202
|
+
event.stopPropagation();
|
|
259
203
|
event.preventDefault(); // prevent enter key from triggering form submission
|
|
260
204
|
if (activeOption) {
|
|
261
|
-
selected.includes(activeOption)
|
|
205
|
+
if (selected.includes(activeOption))
|
|
206
|
+
remove(activeOption, event);
|
|
207
|
+
else
|
|
208
|
+
add(activeOption, event);
|
|
262
209
|
searchText = ``;
|
|
263
210
|
}
|
|
264
211
|
else if (allowUserOptions && searchText.length > 0) {
|
|
265
212
|
// user entered text but no options match, so if allowUserOptions is truthy, we create new option
|
|
266
213
|
add(searchText, event);
|
|
267
214
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
215
|
+
else {
|
|
216
|
+
// no active option and no search text means the options dropdown is closed
|
|
217
|
+
// in which case enter means open it
|
|
271
218
|
open_dropdown(event);
|
|
219
|
+
}
|
|
272
220
|
}
|
|
273
221
|
// on up/down arrow keys: update active option
|
|
274
222
|
else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
|
|
223
|
+
event.stopPropagation();
|
|
275
224
|
// if no option is active yet, but there are matching options, make first one active
|
|
276
225
|
if (activeIndex === null && matchingOptions.length > 0) {
|
|
226
|
+
event.preventDefault(); // Prevent scroll only if we handle the key
|
|
277
227
|
activeIndex = 0;
|
|
278
228
|
return;
|
|
279
229
|
}
|
|
280
230
|
else if (allowUserOptions && !matchingOptions.length && searchText.length > 0) {
|
|
231
|
+
event.preventDefault(); // Prevent scroll only if we handle the key
|
|
281
232
|
// if allowUserOptions is truthy and user entered text but no options match, we make
|
|
282
233
|
// <li>{addUserMsg}</li> active on keydown (or toggle it if already active)
|
|
283
234
|
option_msg_is_active = !option_msg_is_active;
|
|
@@ -287,7 +238,7 @@ async function handle_keydown(event) {
|
|
|
287
238
|
// if no option is active and no options are matching, do nothing
|
|
288
239
|
return;
|
|
289
240
|
}
|
|
290
|
-
event.preventDefault();
|
|
241
|
+
event.preventDefault(); // Prevent scroll only if we handle the key
|
|
291
242
|
// if none of the above special cases apply, we make next/prev option
|
|
292
243
|
// active with wrap around at both ends
|
|
293
244
|
const increment = event.key === `ArrowUp` ? -1 : 1;
|
|
@@ -305,24 +256,28 @@ async function handle_keydown(event) {
|
|
|
305
256
|
}
|
|
306
257
|
// on backspace key: remove last selected option
|
|
307
258
|
else if (event.key === `Backspace` && selected.length > 0 && !searchText) {
|
|
308
|
-
|
|
259
|
+
event.stopPropagation();
|
|
260
|
+
// Don't prevent default, allow normal backspace behavior if not removing
|
|
261
|
+
remove(selected.at(-1), event);
|
|
309
262
|
}
|
|
310
263
|
// make first matching option active on any keypress (if none of the above special cases match)
|
|
311
|
-
else if (matchingOptions.length > 0) {
|
|
264
|
+
else if (matchingOptions.length > 0 && activeIndex === null) {
|
|
265
|
+
// Don't stop propagation or prevent default here, allow normal character input
|
|
312
266
|
activeIndex = 0;
|
|
313
267
|
}
|
|
314
268
|
}
|
|
315
|
-
function remove_all() {
|
|
316
|
-
|
|
317
|
-
|
|
269
|
+
function remove_all(event) {
|
|
270
|
+
event.stopPropagation();
|
|
271
|
+
onremoveAll?.({ options: selected });
|
|
272
|
+
onchange?.({ options: selected, type: `removeAll` });
|
|
318
273
|
selected = [];
|
|
319
274
|
searchText = ``;
|
|
320
275
|
}
|
|
321
|
-
|
|
276
|
+
let is_selected = $derived((label) => selected.map(get_label).includes(label));
|
|
322
277
|
const if_enter_or_space = (handler) => (event) => {
|
|
323
278
|
if ([`Enter`, `Space`].includes(event.code)) {
|
|
324
279
|
event.preventDefault();
|
|
325
|
-
handler();
|
|
280
|
+
handler(event);
|
|
326
281
|
}
|
|
327
282
|
};
|
|
328
283
|
function on_click_outside(event) {
|
|
@@ -330,7 +285,7 @@ function on_click_outside(event) {
|
|
|
330
285
|
close_dropdown(event);
|
|
331
286
|
}
|
|
332
287
|
}
|
|
333
|
-
let drag_idx = null;
|
|
288
|
+
let drag_idx = $state(null);
|
|
334
289
|
// event handlers enable dragging to reorder selected options
|
|
335
290
|
const drop = (target_idx) => (event) => {
|
|
336
291
|
if (!event.dataTransfer)
|
|
@@ -357,62 +312,83 @@ const dragstart = (idx) => (event) => {
|
|
|
357
312
|
event.dataTransfer.dropEffect = `move`;
|
|
358
313
|
event.dataTransfer.setData(`text/plain`, `${idx}`);
|
|
359
314
|
};
|
|
360
|
-
let ul_options;
|
|
315
|
+
let ul_options = $state();
|
|
361
316
|
// highlight text matching user-entered search text in available options
|
|
362
317
|
function highlight_matching_options(event) {
|
|
363
|
-
if (!highlightMatches ||
|
|
364
|
-
return;
|
|
365
|
-
// clear previous ranges from HighlightRegistry
|
|
366
|
-
CSS.highlights.clear();
|
|
318
|
+
if (!highlightMatches || !ul_options)
|
|
319
|
+
return;
|
|
367
320
|
// get input's search query
|
|
368
321
|
const query = event?.target?.value.trim().toLowerCase();
|
|
369
322
|
if (!query)
|
|
370
323
|
return;
|
|
371
|
-
|
|
372
|
-
acceptNode: (node) => {
|
|
373
|
-
// don't highlight text in the "no matching options" message
|
|
374
|
-
if (node?.textContent === noMatchingOptionsMsg)
|
|
375
|
-
return NodeFilter.FILTER_REJECT;
|
|
376
|
-
return NodeFilter.FILTER_ACCEPT;
|
|
377
|
-
},
|
|
378
|
-
});
|
|
379
|
-
const text_nodes = [];
|
|
380
|
-
let current_node = tree_walker.nextNode();
|
|
381
|
-
while (current_node) {
|
|
382
|
-
text_nodes.push(current_node);
|
|
383
|
-
current_node = tree_walker.nextNode();
|
|
384
|
-
}
|
|
385
|
-
// iterate over all text nodes and find matches
|
|
386
|
-
const ranges = text_nodes.map((el) => {
|
|
387
|
-
const text = el.textContent?.toLowerCase();
|
|
388
|
-
const indices = [];
|
|
389
|
-
let start_pos = 0;
|
|
390
|
-
while (text && start_pos < text.length) {
|
|
391
|
-
const index = text.indexOf(query, start_pos);
|
|
392
|
-
if (index === -1)
|
|
393
|
-
break;
|
|
394
|
-
indices.push(index);
|
|
395
|
-
start_pos = index + query.length;
|
|
396
|
-
}
|
|
397
|
-
// create range object for each str found in the text node
|
|
398
|
-
return indices.map((index) => {
|
|
399
|
-
const range = new Range();
|
|
400
|
-
range.setStart(el, index);
|
|
401
|
-
range.setEnd(el, index + query.length);
|
|
402
|
-
return range;
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
// create Highlight object from ranges and add to registry
|
|
406
|
-
CSS.highlights.set(`sms-search-matches`, new Highlight(...ranges.flat()));
|
|
324
|
+
highlight_matching_nodes(ul_options, query, noMatchingOptionsMsg);
|
|
407
325
|
}
|
|
326
|
+
const handle_input_keydown = (event) => {
|
|
327
|
+
handle_keydown(event); // Restore internal logic
|
|
328
|
+
// Call original forwarded handler
|
|
329
|
+
onkeydown?.(event);
|
|
330
|
+
};
|
|
331
|
+
const handle_input_focus = (event) => {
|
|
332
|
+
open_dropdown(event); // Internal logic
|
|
333
|
+
// Call original forwarded handler
|
|
334
|
+
onfocus?.(event);
|
|
335
|
+
};
|
|
408
336
|
// reset form validation when required prop changes
|
|
409
337
|
// https://github.com/janosh/svelte-multiselect/issues/285
|
|
410
|
-
|
|
338
|
+
$effect.pre(() => {
|
|
339
|
+
required = required; // trigger effect when required changes
|
|
340
|
+
form_input?.setCustomValidity(``);
|
|
341
|
+
});
|
|
342
|
+
function portal(node, params) {
|
|
343
|
+
let { target_node, active } = params;
|
|
344
|
+
if (!active)
|
|
345
|
+
return;
|
|
346
|
+
let render_in_place = !browser || !document.body.contains(node);
|
|
347
|
+
if (!render_in_place) {
|
|
348
|
+
document.body.appendChild(node);
|
|
349
|
+
node.style.position = `fixed`;
|
|
350
|
+
const update_position = () => {
|
|
351
|
+
if (!target_node || !open)
|
|
352
|
+
return (node.hidden = true);
|
|
353
|
+
const rect = target_node.getBoundingClientRect();
|
|
354
|
+
node.style.left = `${rect.left}px`;
|
|
355
|
+
node.style.top = `${rect.bottom}px`;
|
|
356
|
+
node.style.width = `${rect.width}px`;
|
|
357
|
+
node.hidden = false;
|
|
358
|
+
};
|
|
359
|
+
if (open)
|
|
360
|
+
tick().then(update_position);
|
|
361
|
+
window.addEventListener(`scroll`, update_position, true);
|
|
362
|
+
window.addEventListener(`resize`, update_position);
|
|
363
|
+
$effect.pre(() => {
|
|
364
|
+
if (open && target_node)
|
|
365
|
+
update_position();
|
|
366
|
+
else
|
|
367
|
+
node.hidden = true;
|
|
368
|
+
});
|
|
369
|
+
return {
|
|
370
|
+
update(params) {
|
|
371
|
+
target_node = params.target_node;
|
|
372
|
+
render_in_place = !browser || !document.body.contains(node);
|
|
373
|
+
if (open && !render_in_place && target_node)
|
|
374
|
+
tick().then(update_position);
|
|
375
|
+
else if (!open || !target_node)
|
|
376
|
+
node.hidden = true;
|
|
377
|
+
},
|
|
378
|
+
destroy() {
|
|
379
|
+
if (!render_in_place)
|
|
380
|
+
node.remove();
|
|
381
|
+
window.removeEventListener(`scroll`, update_position, true);
|
|
382
|
+
window.removeEventListener(`resize`, update_position);
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
411
387
|
</script>
|
|
412
388
|
|
|
413
389
|
<svelte:window
|
|
414
|
-
|
|
415
|
-
|
|
390
|
+
onclick={on_click_outside}
|
|
391
|
+
ontouchstart={on_click_outside}
|
|
416
392
|
bind:innerWidth={window_width}
|
|
417
393
|
/>
|
|
418
394
|
|
|
@@ -422,8 +398,8 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
422
398
|
class:single={maxSelect === 1}
|
|
423
399
|
class:open
|
|
424
400
|
class:invalid
|
|
425
|
-
class="multiselect {outerDivClass}"
|
|
426
|
-
|
|
401
|
+
class="multiselect {outerDivClass} {rest.class ?? ``}"
|
|
402
|
+
onmouseup={open_dropdown}
|
|
427
403
|
title={disabled ? disabledInputTitle : null}
|
|
428
404
|
data-id={id}
|
|
429
405
|
role="searchbox"
|
|
@@ -434,14 +410,14 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
434
410
|
<!-- bind:value={selected} prevents form submission if required prop is true and no options are selected -->
|
|
435
411
|
<input
|
|
436
412
|
{name}
|
|
437
|
-
{required}
|
|
413
|
+
required={Boolean(required)}
|
|
438
414
|
value={selected.length >= Number(required) ? JSON.stringify(selected) : null}
|
|
439
415
|
tabindex="-1"
|
|
440
416
|
aria-hidden="true"
|
|
441
417
|
aria-label="ignore this, used only to prevent form submission if select is required but empty"
|
|
442
418
|
class="form-control"
|
|
443
419
|
bind:this={form_input}
|
|
444
|
-
|
|
420
|
+
oninvalid={() => {
|
|
445
421
|
invalid = true
|
|
446
422
|
let msg
|
|
447
423
|
if (maxSelect && maxSelect > 1 && Number(required) > 1) {
|
|
@@ -454,49 +430,63 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
454
430
|
form_input?.setCustomValidity(msg)
|
|
455
431
|
}}
|
|
456
432
|
/>
|
|
457
|
-
|
|
433
|
+
{#if expandIcon}
|
|
434
|
+
{@render expandIcon({ open })}
|
|
435
|
+
{:else}
|
|
458
436
|
<ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt; cursor: pointer;" />
|
|
459
|
-
|
|
437
|
+
{/if}
|
|
460
438
|
<ul
|
|
461
439
|
class="selected {ulSelectedClass}"
|
|
462
440
|
aria-label="selected options"
|
|
463
441
|
style={ulSelectedStyle}
|
|
464
442
|
>
|
|
465
443
|
{#each selected as option, idx (duplicates ? [key(option), idx] : key(option))}
|
|
444
|
+
{@const selectedOptionStyle =
|
|
445
|
+
[get_style(option, `selected`), liSelectedStyle].filter(Boolean).join(` `) ||
|
|
446
|
+
null}
|
|
466
447
|
<li
|
|
467
448
|
class={liSelectedClass}
|
|
468
449
|
role="option"
|
|
469
450
|
aria-selected="true"
|
|
470
451
|
animate:flip={{ duration: 100 }}
|
|
471
452
|
draggable={selectedOptionsDraggable && !disabled && selected.length > 1}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
453
|
+
ondragstart={dragstart(idx)}
|
|
454
|
+
ondragover={(event) => {
|
|
455
|
+
event.preventDefault() // needed for ondrop to fire
|
|
456
|
+
}}
|
|
457
|
+
ondrop={drop(idx)}
|
|
458
|
+
ondragenter={() => (drag_idx = idx)}
|
|
476
459
|
class:active={drag_idx === idx}
|
|
477
|
-
style=
|
|
460
|
+
style={selectedOptionStyle}
|
|
478
461
|
>
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
462
|
+
{#if selectedItem}
|
|
463
|
+
{@render selectedItem({
|
|
464
|
+
option,
|
|
465
|
+
idx,
|
|
466
|
+
})}
|
|
467
|
+
{:else if children}
|
|
468
|
+
{@render children({
|
|
469
|
+
option,
|
|
470
|
+
idx,
|
|
471
|
+
})}
|
|
472
|
+
{:else if parseLabelsAsHtml}
|
|
473
|
+
{@html get_label(option)}
|
|
474
|
+
{:else}
|
|
475
|
+
{get_label(option)}
|
|
476
|
+
{/if}
|
|
489
477
|
{#if !disabled && (minSelect === null || selected.length > minSelect)}
|
|
490
478
|
<button
|
|
491
|
-
|
|
492
|
-
|
|
479
|
+
onclick={(event) => remove(option, event)}
|
|
480
|
+
onkeydown={if_enter_or_space((event) => remove(option, event))}
|
|
493
481
|
type="button"
|
|
494
482
|
title="{removeBtnTitle} {get_label(option)}"
|
|
495
483
|
class="remove"
|
|
496
484
|
>
|
|
497
|
-
|
|
485
|
+
{#if removeIcon}
|
|
486
|
+
{@render removeIcon()}
|
|
487
|
+
{:else}
|
|
498
488
|
<CrossIcon width="15px" />
|
|
499
|
-
|
|
489
|
+
{/if}
|
|
500
490
|
</button>
|
|
501
491
|
{/if}
|
|
502
492
|
</li>
|
|
@@ -506,11 +496,6 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
506
496
|
style={inputStyle}
|
|
507
497
|
bind:this={input}
|
|
508
498
|
bind:value={searchText}
|
|
509
|
-
on:mouseup|self|stopPropagation={open_dropdown}
|
|
510
|
-
on:keydown|stopPropagation={handle_keydown}
|
|
511
|
-
on:focus
|
|
512
|
-
on:focus={open_dropdown}
|
|
513
|
-
on:input={highlight_matching_options}
|
|
514
499
|
{id}
|
|
515
500
|
{disabled}
|
|
516
501
|
{autocomplete}
|
|
@@ -518,41 +503,47 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
518
503
|
{pattern}
|
|
519
504
|
placeholder={selected.length == 0 ? placeholder : null}
|
|
520
505
|
aria-invalid={invalid ? `true` : null}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
506
|
+
ondrop={() => false}
|
|
507
|
+
onmouseup={open_dropdown}
|
|
508
|
+
onkeydown={handle_input_keydown}
|
|
509
|
+
onfocus={handle_input_focus}
|
|
510
|
+
oninput={highlight_matching_options}
|
|
511
|
+
{onblur}
|
|
512
|
+
{onclick}
|
|
513
|
+
{onkeyup}
|
|
514
|
+
{onmousedown}
|
|
515
|
+
{onmouseenter}
|
|
516
|
+
{onmouseleave}
|
|
517
|
+
{ontouchcancel}
|
|
518
|
+
{ontouchend}
|
|
519
|
+
{ontouchmove}
|
|
520
|
+
{ontouchstart}
|
|
521
|
+
{...rest}
|
|
534
522
|
/>
|
|
535
523
|
<!-- the above on:* lines forward potentially useful DOM events -->
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
/>
|
|
524
|
+
{@render afterInput?.({
|
|
525
|
+
selected,
|
|
526
|
+
disabled,
|
|
527
|
+
invalid,
|
|
528
|
+
id,
|
|
529
|
+
placeholder,
|
|
530
|
+
open,
|
|
531
|
+
required,
|
|
532
|
+
})}
|
|
546
533
|
</ul>
|
|
547
534
|
{#if loading}
|
|
548
|
-
|
|
535
|
+
{#if spinner}
|
|
536
|
+
{@render spinner()}
|
|
537
|
+
{:else}
|
|
549
538
|
<CircleSpinner />
|
|
550
|
-
|
|
539
|
+
{/if}
|
|
551
540
|
{/if}
|
|
552
541
|
{#if disabled}
|
|
553
|
-
|
|
542
|
+
{#if disabledIcon}
|
|
543
|
+
{@render disabledIcon()}
|
|
544
|
+
{:else}
|
|
554
545
|
<DisabledIcon width="14pt" style="margin: 0 2pt;" data-name="disabled-icon" />
|
|
555
|
-
|
|
546
|
+
{/if}
|
|
556
547
|
{:else if selected.length > 0}
|
|
557
548
|
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
|
|
558
549
|
<Wiggle bind:wiggle angle={20}>
|
|
@@ -566,12 +557,14 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
566
557
|
type="button"
|
|
567
558
|
class="remove remove-all"
|
|
568
559
|
title={removeAllTitle}
|
|
569
|
-
|
|
570
|
-
|
|
560
|
+
onclick={remove_all}
|
|
561
|
+
onkeydown={if_enter_or_space(remove_all)}
|
|
571
562
|
>
|
|
572
|
-
|
|
563
|
+
{#if removeIcon}
|
|
564
|
+
{@render removeIcon()}
|
|
565
|
+
{:else}
|
|
573
566
|
<CrossIcon width="15px" />
|
|
574
|
-
|
|
567
|
+
{/if}
|
|
575
568
|
</button>
|
|
576
569
|
{/if}
|
|
577
570
|
{/if}
|
|
@@ -579,6 +572,7 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
579
572
|
<!-- only render options dropdown if options or searchText is not empty (needed to avoid briefly flashing empty dropdown) -->
|
|
580
573
|
{#if (searchText && noMatchingOptionsMsg) || options?.length > 0}
|
|
581
574
|
<ul
|
|
575
|
+
use:portal={{ target_node: outerDiv, ...portal_params }}
|
|
582
576
|
class:hidden={!open}
|
|
583
577
|
class="options {ulOptionsClass}"
|
|
584
578
|
role="listbox"
|
|
@@ -588,19 +582,21 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
588
582
|
bind:this={ul_options}
|
|
589
583
|
style={ulOptionsStyle}
|
|
590
584
|
>
|
|
591
|
-
{#each matchingOptions.slice(0, Math.max(0, maxOptions ?? 0) || Infinity) as
|
|
585
|
+
{#each matchingOptions.slice(0, Math.max(0, maxOptions ?? 0) || Infinity) as optionItem, idx (duplicates ? [key(optionItem), idx] : key(optionItem))}
|
|
592
586
|
{@const {
|
|
593
587
|
label,
|
|
594
588
|
disabled = null,
|
|
595
589
|
title = null,
|
|
596
590
|
selectedTitle = null,
|
|
597
591
|
disabledTitle = defaultDisabledTitle,
|
|
598
|
-
} =
|
|
592
|
+
} = optionItem instanceof Object ? optionItem : { label: optionItem }}
|
|
599
593
|
{@const active = activeIndex === idx}
|
|
594
|
+
{@const optionStyle =
|
|
595
|
+
[get_style(optionItem, `option`), liOptionStyle].filter(Boolean).join(` `) ||
|
|
596
|
+
null}
|
|
600
597
|
<li
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (!disabled) add(option, event)
|
|
598
|
+
onclick={(event) => {
|
|
599
|
+
if (!disabled) add(optionItem, event)
|
|
604
600
|
}}
|
|
605
601
|
title={disabled
|
|
606
602
|
? disabledTitle
|
|
@@ -609,27 +605,37 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
609
605
|
class:active
|
|
610
606
|
class:disabled
|
|
611
607
|
class="{liOptionClass} {active ? liActiveOptionClass : ``}"
|
|
612
|
-
|
|
608
|
+
onmouseover={() => {
|
|
613
609
|
if (!disabled) activeIndex = idx
|
|
614
610
|
}}
|
|
615
|
-
|
|
611
|
+
onfocus={() => {
|
|
616
612
|
if (!disabled) activeIndex = idx
|
|
617
613
|
}}
|
|
618
|
-
on:mouseout={() => (activeIndex = null)}
|
|
619
|
-
on:blur={() => (activeIndex = null)}
|
|
620
614
|
role="option"
|
|
621
615
|
aria-selected="false"
|
|
622
|
-
style=
|
|
616
|
+
style={optionStyle}
|
|
617
|
+
onkeydown={(event) => {
|
|
618
|
+
if (!disabled && (event.key === `Enter` || event.code === `Space`)) {
|
|
619
|
+
event.preventDefault()
|
|
620
|
+
add(optionItem, event)
|
|
621
|
+
}
|
|
622
|
+
}}
|
|
623
623
|
>
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
624
|
+
{#if option}
|
|
625
|
+
{@render option({
|
|
626
|
+
option: optionItem,
|
|
627
|
+
idx,
|
|
628
|
+
})}
|
|
629
|
+
{:else if children}
|
|
630
|
+
{@render children({
|
|
631
|
+
option: optionItem,
|
|
632
|
+
idx,
|
|
633
|
+
})}
|
|
634
|
+
{:else if parseLabelsAsHtml}
|
|
635
|
+
{@html get_label(optionItem)}
|
|
636
|
+
{:else}
|
|
637
|
+
{get_label(optionItem)}
|
|
638
|
+
{/if}
|
|
633
639
|
</li>
|
|
634
640
|
{/each}
|
|
635
641
|
{#if searchText}
|
|
@@ -646,16 +652,31 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
646
652
|
'no-match': noMatchingOptionsMsg,
|
|
647
653
|
}[msgType]}
|
|
648
654
|
<li
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
655
|
+
onclick={(event) => {
|
|
656
|
+
if (msgType === `create` && allowUserOptions) {
|
|
657
|
+
add(searchText as Option, event)
|
|
658
|
+
}
|
|
652
659
|
}}
|
|
653
|
-
|
|
660
|
+
onkeydown={(event) => {
|
|
661
|
+
if (
|
|
662
|
+
msgType === `create` &&
|
|
663
|
+
allowUserOptions &&
|
|
664
|
+
(event.key === `Enter` || event.code === `Space`)
|
|
665
|
+
) {
|
|
666
|
+
event.preventDefault()
|
|
667
|
+
add(searchText as Option, event)
|
|
668
|
+
}
|
|
669
|
+
}}
|
|
670
|
+
title={msgType === `create`
|
|
671
|
+
? createOptionMsg
|
|
672
|
+
: msgType === `dupe`
|
|
673
|
+
? duplicateOptionMsg
|
|
674
|
+
: ``}
|
|
654
675
|
class:active={option_msg_is_active}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
676
|
+
onmouseover={() => (option_msg_is_active = true)}
|
|
677
|
+
onfocus={() => (option_msg_is_active = true)}
|
|
678
|
+
onmouseout={() => (option_msg_is_active = false)}
|
|
679
|
+
onblur={() => (option_msg_is_active = false)}
|
|
659
680
|
role="option"
|
|
660
681
|
aria-selected="false"
|
|
661
682
|
class="user-msg {liUserMsgClass} {option_msg_is_active
|
|
@@ -667,9 +688,11 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
667
688
|
'no-match': `default`,
|
|
668
689
|
}[msgType]}
|
|
669
690
|
>
|
|
670
|
-
|
|
691
|
+
{#if userMsg}
|
|
692
|
+
{@render userMsg({ searchText, msgType, msg })}
|
|
693
|
+
{:else}
|
|
671
694
|
{msg}
|
|
672
|
-
|
|
695
|
+
{/if}
|
|
673
696
|
</li>
|
|
674
697
|
{/if}
|
|
675
698
|
{/if}
|
|
@@ -678,7 +701,7 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
678
701
|
</div>
|
|
679
702
|
|
|
680
703
|
<style>
|
|
681
|
-
:
|
|
704
|
+
:is(div.multiselect) {
|
|
682
705
|
position: relative;
|
|
683
706
|
align-items: center;
|
|
684
707
|
display: flex;
|
|
@@ -695,27 +718,27 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
695
718
|
min-height: var(--sms-min-height, 22pt);
|
|
696
719
|
margin: var(--sms-margin);
|
|
697
720
|
}
|
|
698
|
-
:
|
|
721
|
+
:is(div.multiselect.open) {
|
|
699
722
|
/* increase z-index when open to ensure the dropdown of one <MultiSelect />
|
|
700
723
|
displays above that of another slightly below it on the page */
|
|
701
724
|
z-index: var(--sms-open-z-index, 4);
|
|
702
725
|
}
|
|
703
|
-
:
|
|
726
|
+
:is(div.multiselect:focus-within) {
|
|
704
727
|
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
|
|
705
728
|
}
|
|
706
|
-
:
|
|
729
|
+
:is(div.multiselect.disabled) {
|
|
707
730
|
background: var(--sms-disabled-bg, lightgray);
|
|
708
731
|
cursor: not-allowed;
|
|
709
732
|
}
|
|
710
733
|
|
|
711
|
-
:
|
|
734
|
+
:is(div.multiselect > ul.selected) {
|
|
712
735
|
display: flex;
|
|
713
736
|
flex: 1;
|
|
714
737
|
padding: 0;
|
|
715
738
|
margin: 0;
|
|
716
739
|
flex-wrap: wrap;
|
|
717
740
|
}
|
|
718
|
-
:
|
|
741
|
+
:is(div.multiselect > ul.selected > li) {
|
|
719
742
|
align-items: center;
|
|
720
743
|
border-radius: 3pt;
|
|
721
744
|
display: flex;
|
|
@@ -727,13 +750,13 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
727
750
|
padding: var(--sms-selected-li-padding, 1pt 5pt);
|
|
728
751
|
color: var(--sms-selected-text-color, var(--sms-text-color));
|
|
729
752
|
}
|
|
730
|
-
:
|
|
753
|
+
:is(div.multiselect > ul.selected > li[draggable='true']) {
|
|
731
754
|
cursor: grab;
|
|
732
755
|
}
|
|
733
|
-
:
|
|
756
|
+
:is(div.multiselect > ul.selected > li.active) {
|
|
734
757
|
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
|
735
758
|
}
|
|
736
|
-
:
|
|
759
|
+
:is(div.multiselect button) {
|
|
737
760
|
border-radius: 50%;
|
|
738
761
|
display: flex;
|
|
739
762
|
transition: 0.2s;
|
|
@@ -742,22 +765,22 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
742
765
|
border: none;
|
|
743
766
|
cursor: pointer;
|
|
744
767
|
outline: none;
|
|
745
|
-
padding:
|
|
768
|
+
padding: 1pt;
|
|
746
769
|
margin: 0 0 0 3pt; /* CSS reset */
|
|
747
770
|
}
|
|
748
|
-
:
|
|
771
|
+
:is(div.multiselect button.remove-all) {
|
|
749
772
|
margin: 0 3pt;
|
|
750
773
|
}
|
|
751
|
-
:
|
|
774
|
+
:is(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
|
|
752
775
|
color: var(--sms-remove-btn-hover-color, lightskyblue);
|
|
753
776
|
background: var(--sms-remove-btn-hover-bg, rgba(0, 0, 0, 0.2));
|
|
754
777
|
}
|
|
755
778
|
|
|
756
|
-
:
|
|
779
|
+
:is(div.multiselect input) {
|
|
757
780
|
margin: auto 0; /* CSS reset */
|
|
758
781
|
padding: 0; /* CSS reset */
|
|
759
782
|
}
|
|
760
|
-
:
|
|
783
|
+
:is(div.multiselect > ul.selected > input) {
|
|
761
784
|
border: none;
|
|
762
785
|
outline: none;
|
|
763
786
|
background: none;
|
|
@@ -769,13 +792,13 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
769
792
|
cursor: inherit; /* needed for disabled state */
|
|
770
793
|
border-radius: 0; /* reset ul.selected > li */
|
|
771
794
|
}
|
|
772
|
-
/* don't wrap ::placeholder rules in :
|
|
795
|
+
/* don't wrap ::placeholder rules in :is() as it seems to be overpowered by browser defaults i.t.o. specificity */
|
|
773
796
|
div.multiselect > ul.selected > input::placeholder {
|
|
774
797
|
padding-left: 5pt;
|
|
775
798
|
color: var(--sms-placeholder-color);
|
|
776
799
|
opacity: var(--sms-placeholder-opacity);
|
|
777
800
|
}
|
|
778
|
-
:
|
|
801
|
+
:is(div.multiselect > input.form-control) {
|
|
779
802
|
width: 2em;
|
|
780
803
|
position: absolute;
|
|
781
804
|
background: transparent;
|
|
@@ -786,14 +809,19 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
786
809
|
pointer-events: none;
|
|
787
810
|
}
|
|
788
811
|
|
|
789
|
-
|
|
812
|
+
ul.options {
|
|
790
813
|
list-style: none;
|
|
814
|
+
/* top, left, width, position are managed by portal when active */
|
|
815
|
+
/* but provide defaults for non-portaled or initial state */
|
|
816
|
+
position: absolute; /* Default, overridden by portal to fixed when open */
|
|
791
817
|
top: 100%;
|
|
792
818
|
left: 0;
|
|
793
819
|
width: 100%;
|
|
794
|
-
|
|
820
|
+
/* Default z-index if not portaled/overridden by portal */
|
|
821
|
+
z-index: var(--sms-options-z-index, 3);
|
|
822
|
+
|
|
795
823
|
overflow: auto;
|
|
796
|
-
transition: all 0.2s;
|
|
824
|
+
transition: all 0.2s; /* Consider if this transition is desirable with portal positioning */
|
|
797
825
|
box-sizing: border-box;
|
|
798
826
|
background: var(--sms-options-bg, white);
|
|
799
827
|
max-height: var(--sms-options-max-height, 50vh);
|
|
@@ -805,35 +833,41 @@ $: required, form_input?.setCustomValidity(``);
|
|
|
805
833
|
padding: var(--sms-options-padding);
|
|
806
834
|
margin: var(--sms-options-margin, inherit);
|
|
807
835
|
}
|
|
808
|
-
:
|
|
836
|
+
:is(div.multiselect.open) {
|
|
837
|
+
/* increase z-index when open to ensure the dropdown of one <MultiSelect />
|
|
838
|
+
displays above that of another slightly below it on the page */
|
|
839
|
+
/* This z-index is for the div.multiselect itself, portal has its own higher z-index */
|
|
840
|
+
z-index: var(--sms-open-z-index, 4);
|
|
841
|
+
}
|
|
842
|
+
ul.options.hidden {
|
|
809
843
|
visibility: hidden;
|
|
810
844
|
opacity: 0;
|
|
811
845
|
transform: translateY(50px);
|
|
812
846
|
}
|
|
813
|
-
|
|
847
|
+
ul.options > li {
|
|
814
848
|
padding: 3pt 2ex;
|
|
815
849
|
cursor: pointer;
|
|
816
850
|
scroll-margin: var(--sms-options-scroll-margin, 100px);
|
|
817
851
|
}
|
|
818
|
-
|
|
852
|
+
ul.options .user-msg {
|
|
819
853
|
/* block needed so vertical padding applies to span */
|
|
820
854
|
display: block;
|
|
821
855
|
padding: 3pt 2ex;
|
|
822
856
|
}
|
|
823
|
-
|
|
857
|
+
ul.options > li.selected {
|
|
824
858
|
background: var(--sms-li-selected-bg);
|
|
825
859
|
color: var(--sms-li-selected-color);
|
|
826
860
|
}
|
|
827
|
-
|
|
861
|
+
ul.options > li.active {
|
|
828
862
|
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
|
829
863
|
}
|
|
830
|
-
|
|
864
|
+
ul.options > li.disabled {
|
|
831
865
|
cursor: not-allowed;
|
|
832
866
|
background: var(--sms-li-disabled-bg, #f5f5f6);
|
|
833
867
|
color: var(--sms-li-disabled-text, #b8b8b8);
|
|
834
868
|
}
|
|
835
869
|
|
|
836
|
-
:
|
|
870
|
+
:is(span.max-select-msg) {
|
|
837
871
|
padding: 0 3pt;
|
|
838
872
|
}
|
|
839
873
|
::highlight(sms-search-matches) {
|