svelte-multiselect 11.3.0 → 11.5.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/package.json +23 -16
- package/readme.md +222 -45
- package/dist/CircleSpinner.svelte +0 -29
- package/dist/CircleSpinner.svelte.d.ts +0 -8
- package/dist/CmdPalette.svelte +0 -74
- package/dist/CmdPalette.svelte.d.ts +0 -66
- package/dist/CodeExample.svelte +0 -87
- package/dist/CodeExample.svelte.d.ts +0 -25
- package/dist/CopyButton.svelte +0 -73
- package/dist/CopyButton.svelte.d.ts +0 -25
- package/dist/FileDetails.svelte +0 -52
- package/dist/FileDetails.svelte.d.ts +0 -23
- package/dist/GitHubCorner.svelte +0 -82
- package/dist/GitHubCorner.svelte.d.ts +0 -13
- package/dist/Icon.svelte +0 -23
- package/dist/Icon.svelte.d.ts +0 -8
- package/dist/MultiSelect.svelte +0 -1017
- package/dist/MultiSelect.svelte.d.ts +0 -25
- package/dist/Nav.svelte +0 -447
- package/dist/Nav.svelte.d.ts +0 -42
- package/dist/PrevNext.svelte +0 -101
- package/dist/PrevNext.svelte.d.ts +0 -56
- package/dist/Toggle.svelte +0 -74
- package/dist/Toggle.svelte.d.ts +0 -11
- package/dist/Wiggle.svelte +0 -15
- package/dist/Wiggle.svelte.d.ts +0 -15
- package/dist/attachments.d.ts +0 -49
- package/dist/attachments.js +0 -489
- package/dist/icons.d.ts +0 -47
- package/dist/icons.js +0 -46
- package/dist/index.d.ts +0 -16
- package/dist/index.js +0 -42
- package/dist/types.d.ts +0 -142
- package/dist/types.js +0 -1
- package/dist/utils.d.ts +0 -4
- package/dist/utils.js +0 -59
package/dist/MultiSelect.svelte
DELETED
|
@@ -1,1017 +0,0 @@
|
|
|
1
|
-
<script lang="ts" generics="Option extends import('./types').Option">import { tick } from 'svelte';
|
|
2
|
-
import { flip } from 'svelte/animate';
|
|
3
|
-
import { highlight_matches } from './attachments';
|
|
4
|
-
import CircleSpinner from './CircleSpinner.svelte';
|
|
5
|
-
import Icon from './Icon.svelte';
|
|
6
|
-
import { fuzzy_match, get_label, get_style } from './utils';
|
|
7
|
-
import Wiggle from './Wiggle.svelte';
|
|
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, keepSelectedInDropdown = false, key = (opt) => `${get_label(opt)}`.toLowerCase(), filterFunc = (opt, searchText) => {
|
|
9
|
-
if (!searchText)
|
|
10
|
-
return true;
|
|
11
|
-
const label = `${get_label(opt)}`;
|
|
12
|
-
return fuzzy
|
|
13
|
-
? fuzzy_match(searchText, label)
|
|
14
|
-
: label.toLowerCase().includes(searchText.toLowerCase());
|
|
15
|
-
}, fuzzy = true, closeDropdownOnSelect = `if-mobile`, 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(``), value = $bindable(null), selected = $bindable(value !== null && value !== undefined
|
|
16
|
-
? (Array.isArray(value) ? value : [value])
|
|
17
|
-
: (options
|
|
18
|
-
?.filter((opt) => opt instanceof Object && opt?.preselected)
|
|
19
|
-
.slice(0, maxSelect ?? undefined) ?? [])), sortSelected = false, selectedOptionsDraggable = !sortSelected, style = null, ulOptionsClass = ``, ulSelectedClass = ``, ulSelectedStyle = null, ulOptionsStyle = 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();
|
|
20
|
-
$effect.pre(() => {
|
|
21
|
-
// if maxSelect=1, value is the single item in selected (or null if selected is empty)
|
|
22
|
-
// this solves both https://github.com/janosh/svelte-multiselect/issues/86 and
|
|
23
|
-
// https://github.com/janosh/svelte-multiselect/issues/136
|
|
24
|
-
value = maxSelect === 1 ? (selected[0] ?? null) : selected;
|
|
25
|
-
}); // sync selected updates to value
|
|
26
|
-
$effect.pre(() => {
|
|
27
|
-
if (maxSelect === 1)
|
|
28
|
-
selected = value ? [value] : [];
|
|
29
|
-
else
|
|
30
|
-
selected = value ?? [];
|
|
31
|
-
}); // sync value updates to selected
|
|
32
|
-
let wiggle = $state(false); // controls wiggle animation when user tries to exceed maxSelect
|
|
33
|
-
if (!(options?.length > 0)) {
|
|
34
|
-
if (allowUserOptions || loading || disabled || allowEmpty) {
|
|
35
|
-
options = []; // initializing as array avoids errors when component mounts
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
// error on empty options if user is not allowed to create custom options and loading is false
|
|
39
|
-
// and component is not disabled and allowEmpty is false
|
|
40
|
-
console.error(`MultiSelect received no options`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (maxSelect !== null && maxSelect < 1) {
|
|
44
|
-
console.error(`MultiSelect's maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
45
|
-
}
|
|
46
|
-
if (!Array.isArray(selected)) {
|
|
47
|
-
console.error(`MultiSelect's selected prop should always be an array, got ${selected}`);
|
|
48
|
-
}
|
|
49
|
-
if (maxSelect && typeof required === `number` && required > maxSelect) {
|
|
50
|
-
console.error(`MultiSelect maxSelect=${maxSelect} < required=${required}, makes it impossible for users to submit a valid form`);
|
|
51
|
-
}
|
|
52
|
-
if (parseLabelsAsHtml && allowUserOptions) {
|
|
53
|
-
console.warn(`Don't combine parseLabelsAsHtml and allowUserOptions. It's susceptible to XSS attacks!`);
|
|
54
|
-
}
|
|
55
|
-
if (sortSelected && selectedOptionsDraggable) {
|
|
56
|
-
console.warn(`MultiSelect's sortSelected and selectedOptionsDraggable should not be combined as any ` +
|
|
57
|
-
`user re-orderings of selected options will be undone by sortSelected on component re-renders.`);
|
|
58
|
-
}
|
|
59
|
-
if (allowUserOptions && !createOptionMsg && createOptionMsg !== null) {
|
|
60
|
-
console.error(`MultiSelect has allowUserOptions=${allowUserOptions} but createOptionMsg=${createOptionMsg} is falsy. ` +
|
|
61
|
-
`This prevents the "Add option" <span> from showing up, resulting in a confusing user experience.`);
|
|
62
|
-
}
|
|
63
|
-
if (maxOptions &&
|
|
64
|
-
(typeof maxOptions != `number` || maxOptions < 0 || maxOptions % 1 != 0)) {
|
|
65
|
-
console.error(`MultiSelect's maxOptions must be undefined or a positive integer, got ${maxOptions}`);
|
|
66
|
-
}
|
|
67
|
-
let option_msg_is_active = $state(false); // controls active state of <li>{createOptionMsg}</li>
|
|
68
|
-
let window_width = $state(0);
|
|
69
|
-
// options matching the current search text
|
|
70
|
-
$effect.pre(() => {
|
|
71
|
-
matchingOptions = options.filter((opt) => filterFunc(opt, searchText) &&
|
|
72
|
-
// remove already selected options from dropdown list unless duplicate selections are allowed
|
|
73
|
-
// or keepSelectedInDropdown is enabled
|
|
74
|
-
(!selected.map(key).includes(key(opt)) || duplicates ||
|
|
75
|
-
keepSelectedInDropdown));
|
|
76
|
-
});
|
|
77
|
-
// raise if matchingOptions[activeIndex] does not yield a value
|
|
78
|
-
if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
79
|
-
throw `Run time error, activeIndex=${activeIndex} is out of bounds, matchingOptions.length=${matchingOptions.length}`;
|
|
80
|
-
}
|
|
81
|
-
// update activeOption when activeIndex changes
|
|
82
|
-
$effect(() => {
|
|
83
|
-
activeOption = matchingOptions[activeIndex ?? -1] ?? null;
|
|
84
|
-
});
|
|
85
|
-
// toggle an option between selected and unselected states (for keepSelectedInDropdown mode)
|
|
86
|
-
function toggle_option(option_to_toggle, event) {
|
|
87
|
-
const is_currently_selected = selected.map(key).includes(key(option_to_toggle));
|
|
88
|
-
if (is_currently_selected) {
|
|
89
|
-
if (minSelect === null || selected.length > minSelect) { // Only remove if it wouldn't violate minSelect
|
|
90
|
-
remove(option_to_toggle, event);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
else
|
|
94
|
-
add(option_to_toggle, event);
|
|
95
|
-
}
|
|
96
|
-
// add an option to selected list
|
|
97
|
-
function add(option_to_add, event) {
|
|
98
|
-
event.stopPropagation();
|
|
99
|
-
if (maxSelect !== null && selected.length >= maxSelect)
|
|
100
|
-
wiggle = true;
|
|
101
|
-
if (!isNaN(Number(option_to_add)) && typeof selected.map(get_label)[0] === `number`) {
|
|
102
|
-
option_to_add = Number(option_to_add); // convert to number if possible
|
|
103
|
-
}
|
|
104
|
-
const is_duplicate = selected.map(key).includes(key(option_to_add));
|
|
105
|
-
if ((maxSelect === null || maxSelect === 1 || selected.length < maxSelect) &&
|
|
106
|
-
(duplicates || !is_duplicate)) {
|
|
107
|
-
if (!options.includes(option_to_add) && // first check if we find option in the options list
|
|
108
|
-
// this has the side-effect of not allowing to user to add the same
|
|
109
|
-
// custom option twice in append mode
|
|
110
|
-
[true, `append`].includes(allowUserOptions) &&
|
|
111
|
-
searchText.length > 0) {
|
|
112
|
-
// user entered text but no options match, so if allowUserOptions = true | 'append', we create
|
|
113
|
-
// a new option from the user-entered text
|
|
114
|
-
if (typeof options[0] === `object`) {
|
|
115
|
-
// if 1st option is an object, we create new option as object to keep type homogeneity
|
|
116
|
-
option_to_add = { label: searchText };
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
if ([`number`, `undefined`].includes(typeof options[0]) &&
|
|
120
|
-
!isNaN(Number(searchText))) {
|
|
121
|
-
// create new option as number if it parses to a number and 1st option is also number or missing
|
|
122
|
-
option_to_add = Number(searchText);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
option_to_add = searchText; // else create custom option as string
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
// Fire oncreate event for all user-created options, regardless of type
|
|
129
|
-
oncreate?.({ option: option_to_add });
|
|
130
|
-
if (allowUserOptions === `append`)
|
|
131
|
-
options = [...options, option_to_add];
|
|
132
|
-
}
|
|
133
|
-
if (resetFilterOnAdd)
|
|
134
|
-
searchText = ``; // reset search string on selection
|
|
135
|
-
if ([``, undefined, null].includes(option_to_add)) {
|
|
136
|
-
console.error(`MultiSelect: encountered falsy option ${option_to_add}`);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
// for maxSelect = 1 we always replace current option with new one
|
|
140
|
-
if (maxSelect === 1)
|
|
141
|
-
selected = [option_to_add];
|
|
142
|
-
else {
|
|
143
|
-
selected = [...selected, option_to_add];
|
|
144
|
-
if (sortSelected === true) {
|
|
145
|
-
selected = selected.sort((op1, op2) => {
|
|
146
|
-
const [label1, label2] = [get_label(op1), get_label(op2)];
|
|
147
|
-
// coerce to string if labels are numbers
|
|
148
|
-
return `${label1}`.localeCompare(`${label2}`);
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
else if (typeof sortSelected === `function`) {
|
|
152
|
-
selected = selected.sort(sortSelected);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const reached_max_select = selected.length >= (maxSelect ?? Infinity);
|
|
156
|
-
const dropdown_should_close = closeDropdownOnSelect === true ||
|
|
157
|
-
closeDropdownOnSelect === `retain-focus` ||
|
|
158
|
-
(closeDropdownOnSelect === `if-mobile` && window_width &&
|
|
159
|
-
window_width < breakpoint);
|
|
160
|
-
const should_retain_focus = closeDropdownOnSelect === `retain-focus`;
|
|
161
|
-
if (reached_max_select || dropdown_should_close) {
|
|
162
|
-
close_dropdown(event, should_retain_focus);
|
|
163
|
-
}
|
|
164
|
-
else if (!dropdown_should_close)
|
|
165
|
-
input?.focus();
|
|
166
|
-
onadd?.({ option: option_to_add });
|
|
167
|
-
onchange?.({ option: option_to_add, type: `add` });
|
|
168
|
-
invalid = false; // reset error status whenever new items are selected
|
|
169
|
-
form_input?.setCustomValidity(``);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
// remove an option from selected list
|
|
173
|
-
function remove(option_to_drop, event) {
|
|
174
|
-
event.stopPropagation();
|
|
175
|
-
if (selected.length === 0)
|
|
176
|
-
return;
|
|
177
|
-
const idx = selected.findIndex((opt) => key(opt) === key(option_to_drop));
|
|
178
|
-
let [option_removed] = selected.splice(idx, 1); // remove option from selected list
|
|
179
|
-
if (option_removed === undefined && allowUserOptions) {
|
|
180
|
-
// if option with label could not be found but allowUserOptions is truthy,
|
|
181
|
-
// assume it was created by user and create corresponding option object
|
|
182
|
-
// on the fly for use as event payload
|
|
183
|
-
const other_ops_type = typeof options[0];
|
|
184
|
-
option_removed = (other_ops_type ? { label: option_to_drop } : option_to_drop);
|
|
185
|
-
}
|
|
186
|
-
if (option_removed === undefined) {
|
|
187
|
-
return console.error(`Multiselect can't remove selected option ${JSON.stringify(option_to_drop)}, not found in selected list`);
|
|
188
|
-
}
|
|
189
|
-
selected = [...selected]; // trigger Svelte rerender
|
|
190
|
-
invalid = false; // reset error status whenever items are removed
|
|
191
|
-
form_input?.setCustomValidity(``);
|
|
192
|
-
onremove?.({ option: option_removed });
|
|
193
|
-
onchange?.({ option: option_removed, type: `remove` });
|
|
194
|
-
}
|
|
195
|
-
function open_dropdown(event) {
|
|
196
|
-
event.stopPropagation();
|
|
197
|
-
if (disabled)
|
|
198
|
-
return;
|
|
199
|
-
open = true;
|
|
200
|
-
if (!(event instanceof FocusEvent)) {
|
|
201
|
-
// avoid double-focussing input when event that opened dropdown was already input FocusEvent
|
|
202
|
-
input?.focus();
|
|
203
|
-
}
|
|
204
|
-
onopen?.({ event });
|
|
205
|
-
}
|
|
206
|
-
function close_dropdown(event, retain_focus = false) {
|
|
207
|
-
open = false;
|
|
208
|
-
if (!retain_focus)
|
|
209
|
-
input?.blur();
|
|
210
|
-
activeIndex = null;
|
|
211
|
-
onclose?.({ event });
|
|
212
|
-
}
|
|
213
|
-
// handle all keyboard events this component receives
|
|
214
|
-
async function handle_keydown(event) {
|
|
215
|
-
// on escape or tab out of input: close options dropdown and reset search text
|
|
216
|
-
if (event.key === `Escape` || event.key === `Tab`) {
|
|
217
|
-
event.stopPropagation();
|
|
218
|
-
close_dropdown(event);
|
|
219
|
-
searchText = ``;
|
|
220
|
-
} // on enter key: toggle active option and reset search text
|
|
221
|
-
else if (event.key === `Enter`) {
|
|
222
|
-
event.stopPropagation();
|
|
223
|
-
event.preventDefault(); // prevent enter key from triggering form submission
|
|
224
|
-
if (activeOption) {
|
|
225
|
-
if (selected.includes(activeOption)) {
|
|
226
|
-
// Only remove if it wouldn't violate minSelect
|
|
227
|
-
if (minSelect === null || selected.length > minSelect) {
|
|
228
|
-
remove(activeOption, event);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
else
|
|
232
|
-
add(activeOption, event);
|
|
233
|
-
searchText = ``;
|
|
234
|
-
}
|
|
235
|
-
else if (allowUserOptions && searchText.length > 0) {
|
|
236
|
-
// user entered text but no options match, so if allowUserOptions is truthy, we create new option
|
|
237
|
-
add(searchText, event);
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
// no active option and no search text means the options dropdown is closed
|
|
241
|
-
// in which case enter means open it
|
|
242
|
-
open_dropdown(event);
|
|
243
|
-
}
|
|
244
|
-
} // on up/down arrow keys: update active option
|
|
245
|
-
else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
|
|
246
|
-
event.stopPropagation();
|
|
247
|
-
// if no option is active yet, but there are matching options, make first one active
|
|
248
|
-
if (activeIndex === null && matchingOptions.length > 0) {
|
|
249
|
-
event.preventDefault(); // Prevent scroll only if we handle the key
|
|
250
|
-
activeIndex = 0;
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
else if (allowUserOptions && !matchingOptions.length && searchText.length > 0) {
|
|
254
|
-
event.preventDefault(); // Prevent scroll only if we handle the key
|
|
255
|
-
// if allowUserOptions is truthy and user entered text but no options match, we make
|
|
256
|
-
// <li>{addUserMsg}</li> active on keydown (or toggle it if already active)
|
|
257
|
-
option_msg_is_active = !option_msg_is_active;
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
else if (activeIndex === null) {
|
|
261
|
-
// if no option is active and no options are matching, do nothing
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
event.preventDefault(); // Prevent scroll only if we handle the key
|
|
265
|
-
// if none of the above special cases apply, we make next/prev option
|
|
266
|
-
// active with wrap around at both ends
|
|
267
|
-
const increment = event.key === `ArrowUp` ? -1 : 1;
|
|
268
|
-
// Include user message in total count if it exists
|
|
269
|
-
const has_user_msg = searchText && ((allowUserOptions && createOptionMsg) ||
|
|
270
|
-
(!duplicates && selected.map(get_label).includes(searchText)) ||
|
|
271
|
-
(matchingOptions.length === 0 && noMatchingOptionsMsg));
|
|
272
|
-
const total_items = matchingOptions.length + (has_user_msg ? 1 : 0);
|
|
273
|
-
activeIndex = (activeIndex + increment) % total_items;
|
|
274
|
-
// in JS % behaves like remainder operator, not real modulo, so negative numbers stay negative
|
|
275
|
-
// need to do manual wrap around at 0
|
|
276
|
-
if (activeIndex < 0)
|
|
277
|
-
activeIndex = total_items - 1;
|
|
278
|
-
// Handle user message activation
|
|
279
|
-
if (has_user_msg && activeIndex === matchingOptions.length) {
|
|
280
|
-
option_msg_is_active = true;
|
|
281
|
-
activeOption = null;
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
option_msg_is_active = false;
|
|
285
|
-
activeOption = matchingOptions[activeIndex] ?? null;
|
|
286
|
-
}
|
|
287
|
-
if (autoScroll) {
|
|
288
|
-
await tick();
|
|
289
|
-
const li = document.querySelector(`ul.options > li.active`);
|
|
290
|
-
if (li)
|
|
291
|
-
li.scrollIntoViewIfNeeded?.();
|
|
292
|
-
}
|
|
293
|
-
} // on backspace key: remove last selected option
|
|
294
|
-
else if (event.key === `Backspace` && selected.length > 0 && !searchText) {
|
|
295
|
-
event.stopPropagation();
|
|
296
|
-
// Only remove option if it wouldn't violate minSelect
|
|
297
|
-
if (minSelect === null || selected.length > minSelect) {
|
|
298
|
-
const last_option = selected.at(-1);
|
|
299
|
-
if (last_option)
|
|
300
|
-
remove(last_option, event);
|
|
301
|
-
}
|
|
302
|
-
// Don't prevent default, allow normal backspace behavior if not removing
|
|
303
|
-
} // make first matching option active on any keypress (if none of the above special cases match)
|
|
304
|
-
else if (matchingOptions.length > 0 && activeIndex === null) {
|
|
305
|
-
// Don't stop propagation or prevent default here, allow normal character input
|
|
306
|
-
activeIndex = 0;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
function remove_all(event) {
|
|
310
|
-
event.stopPropagation();
|
|
311
|
-
// Keep the first minSelect items, remove the rest
|
|
312
|
-
let removed_options = [];
|
|
313
|
-
if (minSelect === null) {
|
|
314
|
-
// If no minSelect constraint, remove all
|
|
315
|
-
removed_options = selected;
|
|
316
|
-
selected = [];
|
|
317
|
-
searchText = ``;
|
|
318
|
-
}
|
|
319
|
-
else if (selected.length > minSelect) {
|
|
320
|
-
// Keep the first minSelect items
|
|
321
|
-
removed_options = selected.slice(minSelect);
|
|
322
|
-
selected = selected.slice(0, minSelect);
|
|
323
|
-
searchText = ``;
|
|
324
|
-
}
|
|
325
|
-
onremoveAll?.({ options: removed_options });
|
|
326
|
-
onchange?.({ options: selected, type: `removeAll` });
|
|
327
|
-
// If selected.length <= minSelect, do nothing (can't remove any more)
|
|
328
|
-
}
|
|
329
|
-
let is_selected = $derived((label) => selected.map(get_label).includes(label));
|
|
330
|
-
const if_enter_or_space = (handler) => (event) => {
|
|
331
|
-
if ([`Enter`, `Space`].includes(event.code)) {
|
|
332
|
-
event.preventDefault();
|
|
333
|
-
handler(event);
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
function on_click_outside(event) {
|
|
337
|
-
if (!outerDiv)
|
|
338
|
-
return;
|
|
339
|
-
const target = event.target;
|
|
340
|
-
// Check if click is inside the main component
|
|
341
|
-
if (outerDiv.contains(target))
|
|
342
|
-
return;
|
|
343
|
-
// If portal is active, also check if click is inside the portalled options dropdown
|
|
344
|
-
if (portal_params?.active && ul_options && ul_options.contains(target))
|
|
345
|
-
return;
|
|
346
|
-
// Click is outside both the main component and any portalled dropdown
|
|
347
|
-
close_dropdown(event);
|
|
348
|
-
}
|
|
349
|
-
let drag_idx = $state(null);
|
|
350
|
-
// event handlers enable dragging to reorder selected options
|
|
351
|
-
const drop = (target_idx) => (event) => {
|
|
352
|
-
if (!event.dataTransfer)
|
|
353
|
-
return;
|
|
354
|
-
event.dataTransfer.dropEffect = `move`;
|
|
355
|
-
const start_idx = parseInt(event.dataTransfer.getData(`text/plain`));
|
|
356
|
-
const new_selected = [...selected];
|
|
357
|
-
if (start_idx < target_idx) {
|
|
358
|
-
new_selected.splice(target_idx + 1, 0, new_selected[start_idx]);
|
|
359
|
-
new_selected.splice(start_idx, 1);
|
|
360
|
-
}
|
|
361
|
-
else {
|
|
362
|
-
new_selected.splice(target_idx, 0, new_selected[start_idx]);
|
|
363
|
-
new_selected.splice(start_idx + 1, 1);
|
|
364
|
-
}
|
|
365
|
-
selected = new_selected;
|
|
366
|
-
drag_idx = null;
|
|
367
|
-
};
|
|
368
|
-
const dragstart = (idx) => (event) => {
|
|
369
|
-
if (!event.dataTransfer)
|
|
370
|
-
return;
|
|
371
|
-
// only allow moving, not copying (also affects the cursor during drag)
|
|
372
|
-
event.dataTransfer.effectAllowed = `move`;
|
|
373
|
-
event.dataTransfer.dropEffect = `move`;
|
|
374
|
-
event.dataTransfer.setData(`text/plain`, `${idx}`);
|
|
375
|
-
};
|
|
376
|
-
let ul_options = $state();
|
|
377
|
-
const handle_input_keydown = (event) => {
|
|
378
|
-
handle_keydown(event); // Restore internal logic
|
|
379
|
-
// Call original forwarded handler
|
|
380
|
-
onkeydown?.(event);
|
|
381
|
-
};
|
|
382
|
-
const handle_input_focus = (event) => {
|
|
383
|
-
open_dropdown(event);
|
|
384
|
-
onfocus?.(event);
|
|
385
|
-
};
|
|
386
|
-
// Override input's focus method to ensure dropdown opens on programmatic focus
|
|
387
|
-
// https://github.com/janosh/svelte-multiselect/issues/289
|
|
388
|
-
$effect(() => {
|
|
389
|
-
if (!input)
|
|
390
|
-
return;
|
|
391
|
-
const orig_focus = input.focus.bind(input);
|
|
392
|
-
input.focus = (options) => {
|
|
393
|
-
orig_focus(options);
|
|
394
|
-
if (!disabled && !open) {
|
|
395
|
-
open_dropdown(new FocusEvent(`focus`, { bubbles: true }));
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
return () => {
|
|
399
|
-
if (input)
|
|
400
|
-
input.focus = orig_focus;
|
|
401
|
-
};
|
|
402
|
-
});
|
|
403
|
-
const handle_input_blur = (event) => {
|
|
404
|
-
// For portalled dropdowns, don't close on blur since clicks on portalled elements
|
|
405
|
-
// will cause blur but we want to allow the click to register first
|
|
406
|
-
// (otherwise mobile touch event is unable to select options https://github.com/janosh/svelte-multiselect/issues/335)
|
|
407
|
-
if (portal_params?.active) {
|
|
408
|
-
onblur?.(event); // Let the click handler manage closing for portalled dropdowns
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
// For non-portalled dropdowns, close when focus moves outside the component
|
|
412
|
-
if (!outerDiv?.contains(event.relatedTarget))
|
|
413
|
-
close_dropdown(event);
|
|
414
|
-
onblur?.(event); // Call original handler (if any passed as component prop)
|
|
415
|
-
};
|
|
416
|
-
// reset form validation when required prop changes
|
|
417
|
-
// https://github.com/janosh/svelte-multiselect/issues/285
|
|
418
|
-
$effect.pre(() => {
|
|
419
|
-
required = required; // trigger effect when required changes
|
|
420
|
-
form_input?.setCustomValidity(``);
|
|
421
|
-
});
|
|
422
|
-
function portal(node, params) {
|
|
423
|
-
let { target_node, active } = params;
|
|
424
|
-
if (!active)
|
|
425
|
-
return;
|
|
426
|
-
let render_in_place = typeof window === `undefined` ||
|
|
427
|
-
!document.body.contains(node);
|
|
428
|
-
if (!render_in_place) {
|
|
429
|
-
document.body.appendChild(node);
|
|
430
|
-
node.style.position = `fixed`;
|
|
431
|
-
const update_position = () => {
|
|
432
|
-
if (!target_node || !open)
|
|
433
|
-
return (node.hidden = true);
|
|
434
|
-
const rect = target_node.getBoundingClientRect();
|
|
435
|
-
node.style.left = `${rect.left}px`;
|
|
436
|
-
node.style.top = `${rect.bottom}px`;
|
|
437
|
-
node.style.width = `${rect.width}px`;
|
|
438
|
-
node.hidden = false;
|
|
439
|
-
};
|
|
440
|
-
if (open)
|
|
441
|
-
tick().then(update_position);
|
|
442
|
-
window.addEventListener(`scroll`, update_position, true);
|
|
443
|
-
window.addEventListener(`resize`, update_position);
|
|
444
|
-
$effect(() => {
|
|
445
|
-
if (open && target_node)
|
|
446
|
-
update_position();
|
|
447
|
-
else
|
|
448
|
-
node.hidden = true;
|
|
449
|
-
});
|
|
450
|
-
return {
|
|
451
|
-
update(params) {
|
|
452
|
-
target_node = params.target_node;
|
|
453
|
-
render_in_place = typeof window === `undefined` ||
|
|
454
|
-
!document.body.contains(node);
|
|
455
|
-
if (open && !render_in_place && target_node)
|
|
456
|
-
tick().then(update_position);
|
|
457
|
-
else if (!open || !target_node)
|
|
458
|
-
node.hidden = true;
|
|
459
|
-
},
|
|
460
|
-
destroy() {
|
|
461
|
-
if (!render_in_place)
|
|
462
|
-
node.remove();
|
|
463
|
-
window.removeEventListener(`scroll`, update_position, true);
|
|
464
|
-
window.removeEventListener(`resize`, update_position);
|
|
465
|
-
},
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
</script>
|
|
470
|
-
|
|
471
|
-
<svelte:window
|
|
472
|
-
onclick={on_click_outside}
|
|
473
|
-
ontouchstart={on_click_outside}
|
|
474
|
-
bind:innerWidth={window_width}
|
|
475
|
-
/>
|
|
476
|
-
|
|
477
|
-
<div
|
|
478
|
-
bind:this={outerDiv}
|
|
479
|
-
class:disabled
|
|
480
|
-
class:single={maxSelect === 1}
|
|
481
|
-
class:open
|
|
482
|
-
class:invalid
|
|
483
|
-
class="multiselect {outerDivClass} {rest.class ?? ``}"
|
|
484
|
-
onmouseup={open_dropdown}
|
|
485
|
-
title={disabled ? disabledInputTitle : null}
|
|
486
|
-
data-id={id}
|
|
487
|
-
role="searchbox"
|
|
488
|
-
tabindex="-1"
|
|
489
|
-
{style}
|
|
490
|
-
>
|
|
491
|
-
<!-- form control input invisible to the user, only purpose is to abort form submission if this component fails data validation -->
|
|
492
|
-
<!-- bind:value={selected} prevents form submission if required prop is true and no options are selected -->
|
|
493
|
-
<input
|
|
494
|
-
{name}
|
|
495
|
-
required={Boolean(required)}
|
|
496
|
-
value={selected.length >= Number(required) ? JSON.stringify(selected) : null}
|
|
497
|
-
tabindex="-1"
|
|
498
|
-
aria-hidden="true"
|
|
499
|
-
aria-label="ignore this, used only to prevent form submission if select is required but empty"
|
|
500
|
-
class="form-control"
|
|
501
|
-
bind:this={form_input}
|
|
502
|
-
oninvalid={() => {
|
|
503
|
-
invalid = true
|
|
504
|
-
let msg
|
|
505
|
-
if (maxSelect && maxSelect > 1 && Number(required) > 1) {
|
|
506
|
-
msg = `Please select between ${required} and ${maxSelect} options`
|
|
507
|
-
} else if (Number(required) > 1) {
|
|
508
|
-
msg = `Please select at least ${required} options`
|
|
509
|
-
} else {
|
|
510
|
-
msg = `Please select an option`
|
|
511
|
-
}
|
|
512
|
-
form_input?.setCustomValidity(msg)
|
|
513
|
-
}}
|
|
514
|
-
/>
|
|
515
|
-
{#if expandIcon}
|
|
516
|
-
{@render expandIcon({ open })}
|
|
517
|
-
{:else}
|
|
518
|
-
<Icon
|
|
519
|
-
icon="ChevronExpand"
|
|
520
|
-
style="width: 15px; min-width: 1em; padding: 0 1pt; cursor: pointer"
|
|
521
|
-
/>
|
|
522
|
-
{/if}
|
|
523
|
-
<ul
|
|
524
|
-
class="selected {ulSelectedClass}"
|
|
525
|
-
aria-label="selected options"
|
|
526
|
-
style={ulSelectedStyle}
|
|
527
|
-
>
|
|
528
|
-
{#each selected as option, idx (duplicates ? [key(option), idx] : key(option))}
|
|
529
|
-
{@const selectedOptionStyle =
|
|
530
|
-
[get_style(option, `selected`), liSelectedStyle].filter(Boolean).join(
|
|
531
|
-
` `,
|
|
532
|
-
) ||
|
|
533
|
-
null}
|
|
534
|
-
<li
|
|
535
|
-
class={liSelectedClass}
|
|
536
|
-
role="option"
|
|
537
|
-
aria-selected="true"
|
|
538
|
-
animate:flip={{ duration: 100 }}
|
|
539
|
-
draggable={selectedOptionsDraggable && !disabled && selected.length > 1}
|
|
540
|
-
ondragstart={dragstart(idx)}
|
|
541
|
-
ondragover={(event) => {
|
|
542
|
-
event.preventDefault() // needed for ondrop to fire
|
|
543
|
-
}}
|
|
544
|
-
ondrop={drop(idx)}
|
|
545
|
-
ondragenter={() => (drag_idx = idx)}
|
|
546
|
-
class:active={drag_idx === idx}
|
|
547
|
-
style={selectedOptionStyle}
|
|
548
|
-
>
|
|
549
|
-
{#if selectedItem}
|
|
550
|
-
{@render selectedItem({
|
|
551
|
-
option,
|
|
552
|
-
idx,
|
|
553
|
-
})}
|
|
554
|
-
{:else if children}
|
|
555
|
-
{@render children({
|
|
556
|
-
option,
|
|
557
|
-
idx,
|
|
558
|
-
})}
|
|
559
|
-
{:else if parseLabelsAsHtml}
|
|
560
|
-
{@html get_label(option)}
|
|
561
|
-
{:else}
|
|
562
|
-
{get_label(option)}
|
|
563
|
-
{/if}
|
|
564
|
-
{#if !disabled && (minSelect === null || selected.length > minSelect)}
|
|
565
|
-
<button
|
|
566
|
-
onclick={(event) => remove(option, event)}
|
|
567
|
-
onkeydown={if_enter_or_space((event) => remove(option, event))}
|
|
568
|
-
type="button"
|
|
569
|
-
title="{removeBtnTitle} {get_label(option)}"
|
|
570
|
-
class="remove"
|
|
571
|
-
>
|
|
572
|
-
{#if removeIcon}
|
|
573
|
-
{@render removeIcon()}
|
|
574
|
-
{:else}
|
|
575
|
-
<Icon icon="Cross" style="width: 15px" />
|
|
576
|
-
{/if}
|
|
577
|
-
</button>
|
|
578
|
-
{/if}
|
|
579
|
-
</li>
|
|
580
|
-
{/each}
|
|
581
|
-
<input
|
|
582
|
-
class={inputClass}
|
|
583
|
-
style={inputStyle}
|
|
584
|
-
bind:this={input}
|
|
585
|
-
bind:value={searchText}
|
|
586
|
-
{id}
|
|
587
|
-
{disabled}
|
|
588
|
-
{autocomplete}
|
|
589
|
-
{inputmode}
|
|
590
|
-
{pattern}
|
|
591
|
-
placeholder={selected.length === 0 ? placeholder : null}
|
|
592
|
-
aria-invalid={invalid ? `true` : null}
|
|
593
|
-
ondrop={() => false}
|
|
594
|
-
onmouseup={open_dropdown}
|
|
595
|
-
onkeydown={handle_input_keydown}
|
|
596
|
-
onfocus={handle_input_focus}
|
|
597
|
-
onblur={handle_input_blur}
|
|
598
|
-
{onclick}
|
|
599
|
-
{onkeyup}
|
|
600
|
-
{onmousedown}
|
|
601
|
-
{onmouseenter}
|
|
602
|
-
{onmouseleave}
|
|
603
|
-
{ontouchcancel}
|
|
604
|
-
{ontouchend}
|
|
605
|
-
{ontouchmove}
|
|
606
|
-
{ontouchstart}
|
|
607
|
-
{...rest}
|
|
608
|
-
/>
|
|
609
|
-
{@render afterInput?.({
|
|
610
|
-
selected,
|
|
611
|
-
disabled,
|
|
612
|
-
invalid,
|
|
613
|
-
id,
|
|
614
|
-
placeholder,
|
|
615
|
-
open,
|
|
616
|
-
required,
|
|
617
|
-
})}
|
|
618
|
-
</ul>
|
|
619
|
-
{#if loading}
|
|
620
|
-
{#if spinner}
|
|
621
|
-
{@render spinner()}
|
|
622
|
-
{:else}
|
|
623
|
-
<CircleSpinner />
|
|
624
|
-
{/if}
|
|
625
|
-
{/if}
|
|
626
|
-
{#if disabled}
|
|
627
|
-
{#if disabledIcon}
|
|
628
|
-
{@render disabledIcon()}
|
|
629
|
-
{:else}
|
|
630
|
-
<Icon
|
|
631
|
-
icon="Disabled"
|
|
632
|
-
style="width: 14pt; margin: 0 2pt"
|
|
633
|
-
data-name="disabled-icon"
|
|
634
|
-
aria-disabled="true"
|
|
635
|
-
/>
|
|
636
|
-
{/if}
|
|
637
|
-
{:else if selected.length > 0}
|
|
638
|
-
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
|
|
639
|
-
<Wiggle bind:wiggle angle={20}>
|
|
640
|
-
<span class="max-select-msg {maxSelectMsgClass}">
|
|
641
|
-
{maxSelectMsg?.(selected.length, maxSelect)}
|
|
642
|
-
</span>
|
|
643
|
-
</Wiggle>
|
|
644
|
-
{/if}
|
|
645
|
-
{#if maxSelect !== 1 && selected.length > 1}
|
|
646
|
-
<button
|
|
647
|
-
type="button"
|
|
648
|
-
class="remove remove-all"
|
|
649
|
-
title={removeAllTitle}
|
|
650
|
-
onclick={remove_all}
|
|
651
|
-
onkeydown={if_enter_or_space(remove_all)}
|
|
652
|
-
>
|
|
653
|
-
{#if removeIcon}
|
|
654
|
-
{@render removeIcon()}
|
|
655
|
-
{:else}
|
|
656
|
-
<Icon icon="Cross" style="width: 15px" />
|
|
657
|
-
{/if}
|
|
658
|
-
</button>
|
|
659
|
-
{/if}
|
|
660
|
-
{/if}
|
|
661
|
-
|
|
662
|
-
<!-- only render options dropdown if options or searchText is not empty (needed to avoid briefly flashing empty dropdown) -->
|
|
663
|
-
{#if (searchText && noMatchingOptionsMsg) || options?.length > 0}
|
|
664
|
-
<ul
|
|
665
|
-
use:portal={{ target_node: outerDiv, ...portal_params }}
|
|
666
|
-
{@attach highlight_matches({
|
|
667
|
-
query: searchText,
|
|
668
|
-
disabled: !highlightMatches,
|
|
669
|
-
fuzzy,
|
|
670
|
-
css_class: `sms-search-matches`,
|
|
671
|
-
// don't highlight text in the "Create this option..." message
|
|
672
|
-
node_filter: (node) =>
|
|
673
|
-
node?.parentElement?.closest(`li.user-msg`)
|
|
674
|
-
? NodeFilter.FILTER_REJECT
|
|
675
|
-
: NodeFilter.FILTER_ACCEPT,
|
|
676
|
-
})}
|
|
677
|
-
class:hidden={!open}
|
|
678
|
-
class="options {ulOptionsClass}"
|
|
679
|
-
role="listbox"
|
|
680
|
-
aria-multiselectable={maxSelect === null || maxSelect > 1}
|
|
681
|
-
aria-expanded={open}
|
|
682
|
-
aria-disabled={disabled ? `true` : null}
|
|
683
|
-
bind:this={ul_options}
|
|
684
|
-
style={ulOptionsStyle}
|
|
685
|
-
>
|
|
686
|
-
{#each matchingOptions.slice(
|
|
687
|
-
0,
|
|
688
|
-
maxOptions == null ? Infinity : Math.max(0, maxOptions),
|
|
689
|
-
) as
|
|
690
|
-
option_item,
|
|
691
|
-
idx
|
|
692
|
-
(duplicates ? [key(option_item), idx] : key(option_item))
|
|
693
|
-
}
|
|
694
|
-
{@const {
|
|
695
|
-
label,
|
|
696
|
-
disabled = null,
|
|
697
|
-
title = null,
|
|
698
|
-
selectedTitle = null,
|
|
699
|
-
disabledTitle = defaultDisabledTitle,
|
|
700
|
-
} = option_item instanceof Object ? option_item : { label: option_item }}
|
|
701
|
-
{@const active = activeIndex === idx}
|
|
702
|
-
{@const selected = is_selected(label)}
|
|
703
|
-
{@const optionStyle =
|
|
704
|
-
[get_style(option_item, `option`), liOptionStyle].filter(Boolean).join(
|
|
705
|
-
` `,
|
|
706
|
-
) ||
|
|
707
|
-
null}
|
|
708
|
-
<li
|
|
709
|
-
onclick={(event) => {
|
|
710
|
-
if (disabled) return
|
|
711
|
-
if (keepSelectedInDropdown) toggle_option(option_item, event)
|
|
712
|
-
else add(option_item, event)
|
|
713
|
-
}}
|
|
714
|
-
title={disabled ? disabledTitle : (selected && selectedTitle) || title}
|
|
715
|
-
class:selected
|
|
716
|
-
class:active
|
|
717
|
-
class:disabled
|
|
718
|
-
class="{liOptionClass} {active ? liActiveOptionClass : ``}"
|
|
719
|
-
onmouseover={() => {
|
|
720
|
-
if (!disabled) activeIndex = idx
|
|
721
|
-
}}
|
|
722
|
-
onfocus={() => {
|
|
723
|
-
if (!disabled) activeIndex = idx
|
|
724
|
-
}}
|
|
725
|
-
role="option"
|
|
726
|
-
aria-selected={selected ? `true` : `false`}
|
|
727
|
-
style={optionStyle}
|
|
728
|
-
onkeydown={(event) => {
|
|
729
|
-
if (!disabled && (event.key === `Enter` || event.code === `Space`)) {
|
|
730
|
-
event.preventDefault()
|
|
731
|
-
if (keepSelectedInDropdown) toggle_option(option_item, event)
|
|
732
|
-
else add(option_item, event)
|
|
733
|
-
}
|
|
734
|
-
}}
|
|
735
|
-
>
|
|
736
|
-
{#if keepSelectedInDropdown === `checkboxes`}
|
|
737
|
-
<input
|
|
738
|
-
type="checkbox"
|
|
739
|
-
class="option-checkbox"
|
|
740
|
-
checked={selected}
|
|
741
|
-
aria-label="Toggle {get_label(option_item)}"
|
|
742
|
-
tabindex="-1"
|
|
743
|
-
/>
|
|
744
|
-
{/if}
|
|
745
|
-
{#if option}
|
|
746
|
-
{@render option({
|
|
747
|
-
option: option_item,
|
|
748
|
-
idx,
|
|
749
|
-
})}
|
|
750
|
-
{:else if children}
|
|
751
|
-
{@render children({
|
|
752
|
-
option: option_item,
|
|
753
|
-
idx,
|
|
754
|
-
})}
|
|
755
|
-
{:else if parseLabelsAsHtml}
|
|
756
|
-
{@html get_label(option_item)}
|
|
757
|
-
{:else}
|
|
758
|
-
{get_label(option_item)}
|
|
759
|
-
{/if}
|
|
760
|
-
</li>
|
|
761
|
-
{/each}
|
|
762
|
-
{#if searchText}
|
|
763
|
-
{@const text_input_is_duplicate = selected.map(get_label).includes(searchText)}
|
|
764
|
-
{@const is_dupe = !duplicates && text_input_is_duplicate && `dupe`}
|
|
765
|
-
{@const can_create = Boolean(allowUserOptions && createOptionMsg) && `create`}
|
|
766
|
-
{@const no_match = Boolean(matchingOptions?.length === 0 && noMatchingOptionsMsg) &&
|
|
767
|
-
`no-match`}
|
|
768
|
-
{@const msgType = is_dupe || can_create || no_match}
|
|
769
|
-
{#if msgType}
|
|
770
|
-
{@const msg = {
|
|
771
|
-
dupe: duplicateOptionMsg,
|
|
772
|
-
create: createOptionMsg,
|
|
773
|
-
'no-match': noMatchingOptionsMsg,
|
|
774
|
-
}[msgType]}
|
|
775
|
-
<li
|
|
776
|
-
onclick={(event) => {
|
|
777
|
-
if (msgType === `create` && allowUserOptions) {
|
|
778
|
-
add(searchText as Option, event)
|
|
779
|
-
}
|
|
780
|
-
}}
|
|
781
|
-
onkeydown={(event) => {
|
|
782
|
-
if (
|
|
783
|
-
msgType === `create` &&
|
|
784
|
-
allowUserOptions &&
|
|
785
|
-
(event.key === `Enter` || event.code === `Space`)
|
|
786
|
-
) {
|
|
787
|
-
event.preventDefault()
|
|
788
|
-
add(searchText as Option, event)
|
|
789
|
-
}
|
|
790
|
-
}}
|
|
791
|
-
title={msgType === `create`
|
|
792
|
-
? createOptionMsg
|
|
793
|
-
: msgType === `dupe`
|
|
794
|
-
? duplicateOptionMsg
|
|
795
|
-
: ``}
|
|
796
|
-
class:active={option_msg_is_active}
|
|
797
|
-
onmouseover={() => (option_msg_is_active = true)}
|
|
798
|
-
onfocus={() => (option_msg_is_active = true)}
|
|
799
|
-
onmouseout={() => (option_msg_is_active = false)}
|
|
800
|
-
onblur={() => (option_msg_is_active = false)}
|
|
801
|
-
role="option"
|
|
802
|
-
aria-selected="false"
|
|
803
|
-
class="
|
|
804
|
-
user-msg {liUserMsgClass} {option_msg_is_active
|
|
805
|
-
? liActiveUserMsgClass
|
|
806
|
-
: ``}
|
|
807
|
-
"
|
|
808
|
-
style:cursor={{
|
|
809
|
-
dupe: `not-allowed`,
|
|
810
|
-
create: `pointer`,
|
|
811
|
-
'no-match': `default`,
|
|
812
|
-
}[msgType]}
|
|
813
|
-
>
|
|
814
|
-
{#if userMsg}
|
|
815
|
-
{@render userMsg({ searchText, msgType, msg })}
|
|
816
|
-
{:else}
|
|
817
|
-
{msg}
|
|
818
|
-
{/if}
|
|
819
|
-
</li>
|
|
820
|
-
{/if}
|
|
821
|
-
{/if}
|
|
822
|
-
</ul>
|
|
823
|
-
{/if}
|
|
824
|
-
</div>
|
|
825
|
-
|
|
826
|
-
<style>
|
|
827
|
-
:is(div.multiselect) {
|
|
828
|
-
position: relative;
|
|
829
|
-
align-items: center;
|
|
830
|
-
display: flex;
|
|
831
|
-
cursor: text;
|
|
832
|
-
box-sizing: border-box;
|
|
833
|
-
border: var(--sms-border, 1pt solid lightgray);
|
|
834
|
-
border-radius: var(--sms-border-radius, 3pt);
|
|
835
|
-
background: var(--sms-bg);
|
|
836
|
-
width: var(--sms-width);
|
|
837
|
-
max-width: var(--sms-max-width);
|
|
838
|
-
padding: var(--sms-padding, 0 3pt);
|
|
839
|
-
color: var(--sms-text-color);
|
|
840
|
-
font-size: var(--sms-font-size, inherit);
|
|
841
|
-
min-height: var(--sms-min-height, 22pt);
|
|
842
|
-
margin: var(--sms-margin);
|
|
843
|
-
}
|
|
844
|
-
:is(div.multiselect.open) {
|
|
845
|
-
/* increase z-index when open to ensure the dropdown of one <MultiSelect />
|
|
846
|
-
displays above that of another slightly below it on the page */
|
|
847
|
-
z-index: var(--sms-open-z-index, 4);
|
|
848
|
-
}
|
|
849
|
-
:is(div.multiselect:focus-within) {
|
|
850
|
-
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
|
|
851
|
-
}
|
|
852
|
-
:is(div.multiselect.disabled) {
|
|
853
|
-
background: var(--sms-disabled-bg, lightgray);
|
|
854
|
-
cursor: not-allowed;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
:is(div.multiselect > ul.selected) {
|
|
858
|
-
display: flex;
|
|
859
|
-
flex: 1;
|
|
860
|
-
padding: 0;
|
|
861
|
-
margin: 0;
|
|
862
|
-
flex-wrap: wrap;
|
|
863
|
-
}
|
|
864
|
-
:is(div.multiselect > ul.selected > li) {
|
|
865
|
-
align-items: center;
|
|
866
|
-
border-radius: 3pt;
|
|
867
|
-
display: flex;
|
|
868
|
-
margin: 2pt;
|
|
869
|
-
line-height: normal;
|
|
870
|
-
transition: 0.3s;
|
|
871
|
-
white-space: nowrap;
|
|
872
|
-
background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15));
|
|
873
|
-
padding: var(--sms-selected-li-padding, 1pt 5pt);
|
|
874
|
-
color: var(--sms-selected-text-color, var(--sms-text-color));
|
|
875
|
-
}
|
|
876
|
-
:is(div.multiselect > ul.selected > li[draggable='true']) {
|
|
877
|
-
cursor: grab;
|
|
878
|
-
}
|
|
879
|
-
:is(div.multiselect > ul.selected > li.active) {
|
|
880
|
-
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
|
881
|
-
}
|
|
882
|
-
:is(div.multiselect button) {
|
|
883
|
-
border-radius: 50%;
|
|
884
|
-
display: flex;
|
|
885
|
-
transition: 0.2s;
|
|
886
|
-
color: inherit;
|
|
887
|
-
background: transparent;
|
|
888
|
-
border: none;
|
|
889
|
-
cursor: pointer;
|
|
890
|
-
outline: none;
|
|
891
|
-
padding: 1pt;
|
|
892
|
-
margin: 0 0 0 3pt; /* CSS reset */
|
|
893
|
-
}
|
|
894
|
-
:is(div.multiselect button.remove-all) {
|
|
895
|
-
margin: 0 3pt;
|
|
896
|
-
}
|
|
897
|
-
:is(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
|
|
898
|
-
color: var(--sms-remove-btn-hover-color, lightskyblue);
|
|
899
|
-
background: var(--sms-remove-btn-hover-bg, rgba(0, 0, 0, 0.2));
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
:is(div.multiselect input) {
|
|
903
|
-
margin: auto 0; /* CSS reset */
|
|
904
|
-
padding: 0; /* CSS reset */
|
|
905
|
-
}
|
|
906
|
-
:is(div.multiselect > ul.selected > input) {
|
|
907
|
-
border: none;
|
|
908
|
-
outline: none;
|
|
909
|
-
background: none;
|
|
910
|
-
flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
|
|
911
|
-
min-width: 2em;
|
|
912
|
-
/* ensure input uses text color and not --sms-selected-text-color */
|
|
913
|
-
color: var(--sms-text-color);
|
|
914
|
-
font-size: inherit;
|
|
915
|
-
cursor: inherit; /* needed for disabled state */
|
|
916
|
-
border-radius: 0; /* reset ul.selected > li */
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
/* When options are selected, placeholder is hidden in which case we minimize input width to avoid adding unnecessary width to div.multiselect */
|
|
920
|
-
:is(div.multiselect > ul.selected > input:not(:placeholder-shown)) {
|
|
921
|
-
min-width: 1px; /* Minimal width to remain interactive */
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/* don't wrap ::placeholder rules in :is() as it seems to be overpowered by browser defaults i.t.o. specificity */
|
|
925
|
-
div.multiselect > ul.selected > input::placeholder {
|
|
926
|
-
padding-left: 5pt;
|
|
927
|
-
color: var(--sms-placeholder-color);
|
|
928
|
-
opacity: var(--sms-placeholder-opacity);
|
|
929
|
-
}
|
|
930
|
-
:is(div.multiselect > input.form-control) {
|
|
931
|
-
width: 2em;
|
|
932
|
-
position: absolute;
|
|
933
|
-
background: transparent;
|
|
934
|
-
border: none;
|
|
935
|
-
outline: none;
|
|
936
|
-
z-index: -1;
|
|
937
|
-
opacity: 0;
|
|
938
|
-
pointer-events: none;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
ul.options {
|
|
942
|
-
list-style: none;
|
|
943
|
-
/* top, left, width, position are managed by portal when active */
|
|
944
|
-
/* but provide defaults for non-portaled or initial state */
|
|
945
|
-
position: absolute; /* Default, overridden by portal to fixed when open */
|
|
946
|
-
top: 100%;
|
|
947
|
-
left: 0;
|
|
948
|
-
width: 100%;
|
|
949
|
-
/* Default z-index if not portaled/overridden by portal */
|
|
950
|
-
z-index: var(--sms-options-z-index, 3);
|
|
951
|
-
|
|
952
|
-
overflow: auto;
|
|
953
|
-
transition: all
|
|
954
|
-
0.2s; /* Consider if this transition is desirable with portal positioning */
|
|
955
|
-
box-sizing: border-box;
|
|
956
|
-
background: var(--sms-options-bg, white);
|
|
957
|
-
max-height: var(--sms-options-max-height, 50vh);
|
|
958
|
-
overscroll-behavior: var(--sms-options-overscroll, none);
|
|
959
|
-
box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
|
|
960
|
-
border: var(--sms-options-border);
|
|
961
|
-
border-width: var(--sms-options-border-width);
|
|
962
|
-
border-radius: var(--sms-options-border-radius, 1ex);
|
|
963
|
-
padding: var(--sms-options-padding);
|
|
964
|
-
margin: var(--sms-options-margin, inherit);
|
|
965
|
-
}
|
|
966
|
-
:is(div.multiselect.open) {
|
|
967
|
-
/* increase z-index when open to ensure the dropdown of one <MultiSelect />
|
|
968
|
-
displays above that of another slightly below it on the page */
|
|
969
|
-
/* This z-index is for the div.multiselect itself, portal has its own higher z-index */
|
|
970
|
-
z-index: var(--sms-open-z-index, 4);
|
|
971
|
-
}
|
|
972
|
-
ul.options.hidden {
|
|
973
|
-
visibility: hidden;
|
|
974
|
-
opacity: 0;
|
|
975
|
-
transform: translateY(50px);
|
|
976
|
-
pointer-events: none;
|
|
977
|
-
}
|
|
978
|
-
ul.options > li {
|
|
979
|
-
padding: 3pt 1ex;
|
|
980
|
-
cursor: pointer;
|
|
981
|
-
scroll-margin: var(--sms-options-scroll-margin, 100px);
|
|
982
|
-
border-left: 3px solid transparent;
|
|
983
|
-
}
|
|
984
|
-
ul.options .user-msg {
|
|
985
|
-
/* block needed so vertical padding applies to span */
|
|
986
|
-
display: block;
|
|
987
|
-
padding: 3pt 2ex;
|
|
988
|
-
}
|
|
989
|
-
ul.options > li.selected {
|
|
990
|
-
background: var(--sms-li-selected-plain-bg, rgba(0, 123, 255, 0.1));
|
|
991
|
-
border-left: var(
|
|
992
|
-
--sms-li-selected-plain-border,
|
|
993
|
-
3px solid var(--sms-active-color, cornflowerblue)
|
|
994
|
-
);
|
|
995
|
-
}
|
|
996
|
-
ul.options > li.active {
|
|
997
|
-
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
|
998
|
-
}
|
|
999
|
-
ul.options > li.disabled {
|
|
1000
|
-
cursor: not-allowed;
|
|
1001
|
-
background: var(--sms-li-disabled-bg, #f5f5f6);
|
|
1002
|
-
color: var(--sms-li-disabled-text, #b8b8b8);
|
|
1003
|
-
}
|
|
1004
|
-
/* Checkbox styling for keepSelectedInDropdown='checkboxes' mode */
|
|
1005
|
-
ul.options > li > input.option-checkbox {
|
|
1006
|
-
width: 16px;
|
|
1007
|
-
height: 16px;
|
|
1008
|
-
margin-right: 6px;
|
|
1009
|
-
accent-color: var(--sms-active-color, cornflowerblue);
|
|
1010
|
-
}
|
|
1011
|
-
:is(span.max-select-msg) {
|
|
1012
|
-
padding: 0 3pt;
|
|
1013
|
-
}
|
|
1014
|
-
::highlight(sms-search-matches) {
|
|
1015
|
-
color: mediumaquamarine;
|
|
1016
|
-
}
|
|
1017
|
-
</style>
|