svelte-multiselect 11.5.1 → 11.6.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/CmdPalette.svelte.d.ts +2 -2
- package/dist/CodeExample.svelte +1 -1
- package/dist/MultiSelect.svelte +324 -161
- package/dist/MultiSelect.svelte.d.ts +2 -2
- package/dist/Nav.svelte +408 -172
- package/dist/Nav.svelte.d.ts +38 -37
- package/dist/Wiggle.svelte +9 -2
- package/dist/Wiggle.svelte.d.ts +7 -4
- package/dist/attachments.d.ts +7 -1
- package/dist/attachments.js +164 -108
- package/dist/heading-anchors.d.ts +14 -0
- package/dist/heading-anchors.js +114 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/live-examples/highlighter.js +62 -0
- package/dist/live-examples/index.d.ts +7 -0
- package/dist/live-examples/index.js +23 -0
- package/dist/live-examples/mdsvex-transform.d.ts +32 -0
- package/dist/live-examples/mdsvex-transform.js +184 -0
- package/dist/live-examples/vite-plugin.d.ts +6 -0
- package/dist/live-examples/vite-plugin.js +170 -0
- package/dist/types.d.ts +45 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +11 -0
- package/package.json +44 -20
- package/readme.md +25 -1
package/dist/MultiSelect.svelte
CHANGED
|
@@ -5,17 +5,17 @@ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
|
5
5
|
import { highlight_matches } from './attachments';
|
|
6
6
|
import CircleSpinner from './CircleSpinner.svelte';
|
|
7
7
|
import Icon from './Icon.svelte';
|
|
8
|
-
import
|
|
8
|
+
import * as utils from './utils';
|
|
9
9
|
import Wiggle from './Wiggle.svelte';
|
|
10
|
-
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) => {
|
|
10
|
+
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) => `${utils.get_label(opt)}`.toLowerCase(), filterFunc = (opt, searchText) => {
|
|
11
11
|
if (!searchText)
|
|
12
12
|
return true;
|
|
13
|
-
const label = `${get_label(opt)}`;
|
|
13
|
+
const label = `${utils.get_label(opt)}`;
|
|
14
14
|
return fuzzy
|
|
15
|
-
? fuzzy_match(searchText, label)
|
|
15
|
+
? utils.fuzzy_match(searchText, label)
|
|
16
16
|
: label.toLowerCase().includes(searchText.toLowerCase());
|
|
17
17
|
}, fuzzy = true, closeDropdownOnSelect = false, 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 = $bindable(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
|
|
18
|
-
?
|
|
18
|
+
? Array.isArray(value) ? value : [value]
|
|
19
19
|
: (options
|
|
20
20
|
?.filter((opt) => typeof opt === `object` && opt !== null && opt?.preselected)
|
|
21
21
|
.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, onselectAll, onreorder, portal: portal_params = {},
|
|
@@ -26,24 +26,24 @@ loadOptions,
|
|
|
26
26
|
// Animation parameters for selected options flip animation
|
|
27
27
|
selectedFlipParams = { duration: 100 },
|
|
28
28
|
// Option grouping feature
|
|
29
|
-
collapsibleGroups = false, collapsedGroups = $bindable(new Set()), groupSelectAll = false, ungroupedPosition = `first`, groupSortOrder = `none`, searchExpandsCollapsedGroups = false, searchMatchesGroups = false, keyboardExpandsCollapsedGroups = false, stickyGroupHeaders = false, liGroupHeaderClass = ``, liGroupHeaderStyle = null, groupHeader, ongroupToggle, oncollapseAll, onexpandAll, collapseAllGroups = $bindable(), expandAllGroups = $bindable(),
|
|
29
|
+
collapsibleGroups = false, collapsedGroups = $bindable(new Set()), groupSelectAll = false, ungroupedPosition = `first`, groupSortOrder = `none`, searchExpandsCollapsedGroups = false, searchMatchesGroups = false, keyboardExpandsCollapsedGroups = false, stickyGroupHeaders = false, liGroupHeaderClass = ``, liGroupHeaderStyle = null, groupHeader, ongroupToggle, oncollapseAll, onexpandAll, onsearch, onmaxreached, onduplicate, onactivate, collapseAllGroups = $bindable(), expandAllGroups = $bindable(),
|
|
30
30
|
// Keyboard shortcuts for common actions
|
|
31
|
-
shortcuts = {},
|
|
31
|
+
shortcuts = {},
|
|
32
|
+
// Selection history for undo/redo (enabled by default, set to false or 0 to disable)
|
|
33
|
+
history = true, undo = $bindable(), redo = $bindable(), canUndo = $bindable(false), canRedo = $bindable(false), onundo, onredo, ...rest } = $props();
|
|
32
34
|
// Generate unique IDs for ARIA associations (combobox pattern)
|
|
33
35
|
// Uses provided id prop or generates a random one using crypto API
|
|
34
|
-
const internal_id = $derived(id ?? `sms-${
|
|
36
|
+
const internal_id = $derived(id ?? `sms-${utils.get_uuid().slice(0, 8)}`);
|
|
35
37
|
const listbox_id = $derived(`${internal_id}-listbox`);
|
|
36
38
|
// Parse shortcut string into modifier+key parts
|
|
37
39
|
function parse_shortcut(shortcut) {
|
|
38
40
|
const parts = shortcut.toLowerCase().split(`+`).map((part) => part.trim());
|
|
39
41
|
const key = parts.pop() ?? ``;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
meta: parts.includes(`meta`) || parts.includes(`cmd`),
|
|
46
|
-
};
|
|
42
|
+
const ctrl = parts.includes(`ctrl`);
|
|
43
|
+
const shift = parts.includes(`shift`);
|
|
44
|
+
const alt = parts.includes(`alt`);
|
|
45
|
+
const meta = parts.includes(`meta`) || parts.includes(`cmd`);
|
|
46
|
+
return { key, ctrl, shift, alt, meta };
|
|
47
47
|
}
|
|
48
48
|
function matches_shortcut(event, shortcut) {
|
|
49
49
|
if (!shortcut)
|
|
@@ -57,19 +57,22 @@ function matches_shortcut(event, shortcut) {
|
|
|
57
57
|
const shift_matches = event.shiftKey === parsed.shift;
|
|
58
58
|
const alt_matches = event.altKey === parsed.alt;
|
|
59
59
|
const meta_matches = event.metaKey === parsed.meta;
|
|
60
|
-
return
|
|
60
|
+
return key_matches && ctrl_matches && shift_matches && alt_matches && meta_matches;
|
|
61
61
|
}
|
|
62
|
+
// Platform detection for keyboard shortcuts (Mac uses Cmd, others use Ctrl)
|
|
63
|
+
const is_mac = typeof navigator !== `undefined` &&
|
|
64
|
+
/Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
|
|
65
|
+
const mod_key = is_mac ? `meta` : `ctrl`;
|
|
62
66
|
// Default shortcuts
|
|
63
67
|
const default_shortcuts = {
|
|
64
|
-
select_all:
|
|
65
|
-
clear_all:
|
|
68
|
+
select_all: `${mod_key}+a`,
|
|
69
|
+
clear_all: `${mod_key}+shift+a`,
|
|
66
70
|
open: null,
|
|
67
71
|
close: null,
|
|
72
|
+
undo: `${mod_key}+z`,
|
|
73
|
+
redo: `${mod_key}+shift+z`,
|
|
68
74
|
};
|
|
69
|
-
const effective_shortcuts = $derived({
|
|
70
|
-
...default_shortcuts,
|
|
71
|
-
...shortcuts,
|
|
72
|
-
});
|
|
75
|
+
const effective_shortcuts = $derived({ ...default_shortcuts, ...shortcuts });
|
|
73
76
|
// Extract loadOptions config into single derived object (supports both simple function and config object)
|
|
74
77
|
const load_options_config = $derived.by(() => {
|
|
75
78
|
if (!loadOptions)
|
|
@@ -94,8 +97,8 @@ function values_equal(val1, val2) {
|
|
|
94
97
|
if (empty1 && empty2)
|
|
95
98
|
return true;
|
|
96
99
|
if (Array.isArray(val1) && Array.isArray(val2)) {
|
|
97
|
-
return val1.length === val2.length &&
|
|
98
|
-
val1.every((item, idx) => item === val2[idx]);
|
|
100
|
+
return (val1.length === val2.length &&
|
|
101
|
+
val1.every((item, idx) => item === val2[idx]));
|
|
99
102
|
}
|
|
100
103
|
return false;
|
|
101
104
|
}
|
|
@@ -104,15 +107,17 @@ function values_equal(val1, val2) {
|
|
|
104
107
|
// infinite loops with reactive wrappers that clone arrays. See issue #309.
|
|
105
108
|
$effect.pre(() => {
|
|
106
109
|
const new_value = maxSelect === 1 ? (selected[0] ?? null) : selected;
|
|
107
|
-
if (!values_equal(untrack(() => value), new_value))
|
|
110
|
+
if (!values_equal(untrack(() => value), new_value)) {
|
|
108
111
|
value = new_value;
|
|
112
|
+
}
|
|
109
113
|
});
|
|
110
114
|
$effect.pre(() => {
|
|
111
115
|
const new_selected = maxSelect === 1
|
|
112
116
|
? (value ? [value] : [])
|
|
113
117
|
: (Array.isArray(value) ? value : []);
|
|
114
|
-
if (!values_equal(untrack(() => selected), new_selected))
|
|
118
|
+
if (!values_equal(untrack(() => selected), new_selected)) {
|
|
115
119
|
selected = new_selected;
|
|
120
|
+
}
|
|
116
121
|
});
|
|
117
122
|
let wiggle = $state(false); // controls wiggle animation when user tries to exceed maxSelect
|
|
118
123
|
let ignore_hover = $state(false); // ignore mouseover during keyboard navigation to prevent scroll-triggered hover
|
|
@@ -125,6 +130,107 @@ $effect(() => {
|
|
|
125
130
|
return () => clearTimeout(timer);
|
|
126
131
|
}
|
|
127
132
|
});
|
|
133
|
+
// History tracking for undo/redo
|
|
134
|
+
const max_history = $derived(history === true
|
|
135
|
+
? 50
|
|
136
|
+
: typeof history === `number` && Number.isFinite(history)
|
|
137
|
+
? Math.max(0, Math.floor(history))
|
|
138
|
+
: 0);
|
|
139
|
+
let history_stack = $state([]);
|
|
140
|
+
let history_index = $state(-1); // -1 = no history yet
|
|
141
|
+
let prev_selected = null; // null = uninitialized, sync on first run
|
|
142
|
+
// Track changes to selected via $effect (catches internal + external changes)
|
|
143
|
+
$effect(() => {
|
|
144
|
+
// Disabled when max_history is 0 (handles false, 0, negative, non-finite inputs)
|
|
145
|
+
const history_disabled = !(max_history > 0);
|
|
146
|
+
if (history_disabled) {
|
|
147
|
+
// Clear history when disabled so re-enabling starts fresh
|
|
148
|
+
history_stack = [];
|
|
149
|
+
history_index = -1;
|
|
150
|
+
// Don't read `selected` here to avoid creating unnecessary reactive dependency
|
|
151
|
+
prev_selected = null;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Initialize prev_selected on first run to avoid phantom undo from [] → initial selection
|
|
155
|
+
if (prev_selected === null) {
|
|
156
|
+
prev_selected = [...selected];
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Check if actually changed (avoid duplicates from reactive updates)
|
|
160
|
+
if (values_equal(selected, prev_selected))
|
|
161
|
+
return;
|
|
162
|
+
// On first change, push initial state first
|
|
163
|
+
if (history_stack.length === 0) {
|
|
164
|
+
history_stack = [[...prev_selected]];
|
|
165
|
+
history_index = 0;
|
|
166
|
+
}
|
|
167
|
+
// Truncate any redo states
|
|
168
|
+
if (history_index < history_stack.length - 1) {
|
|
169
|
+
history_stack = history_stack.slice(0, history_index + 1);
|
|
170
|
+
}
|
|
171
|
+
// Push new state
|
|
172
|
+
history_stack = [...history_stack, [...selected]];
|
|
173
|
+
history_index = history_stack.length - 1;
|
|
174
|
+
// Trim to max size (remove oldest)
|
|
175
|
+
if (history_stack.length > max_history) {
|
|
176
|
+
const excess = history_stack.length - max_history;
|
|
177
|
+
history_stack = history_stack.slice(excess);
|
|
178
|
+
history_index = Math.max(0, history_index - excess);
|
|
179
|
+
}
|
|
180
|
+
prev_selected = [...selected];
|
|
181
|
+
});
|
|
182
|
+
// Derived canUndo/canRedo (update bindable props reactively)
|
|
183
|
+
$effect(() => {
|
|
184
|
+
canUndo = max_history > 0 && !disabled && history_index > 0;
|
|
185
|
+
canRedo = max_history > 0 && !disabled && history_index < history_stack.length - 1;
|
|
186
|
+
});
|
|
187
|
+
// Undo: restore previous state
|
|
188
|
+
undo = () => {
|
|
189
|
+
if (max_history <= 0 || disabled || history_index <= 0)
|
|
190
|
+
return false;
|
|
191
|
+
const previous = [...selected];
|
|
192
|
+
history_index--;
|
|
193
|
+
selected = [...history_stack[history_index]];
|
|
194
|
+
prev_selected = [...selected]; // sync tracker to prevent $effect re-recording
|
|
195
|
+
onundo?.({ previous, current: selected });
|
|
196
|
+
return true;
|
|
197
|
+
};
|
|
198
|
+
// Redo: restore next state
|
|
199
|
+
redo = () => {
|
|
200
|
+
if (max_history <= 0 || disabled || history_index >= history_stack.length - 1) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const previous = [...selected];
|
|
204
|
+
history_index++;
|
|
205
|
+
selected = [...history_stack[history_index]];
|
|
206
|
+
prev_selected = [...selected]; // sync tracker to prevent $effect re-recording
|
|
207
|
+
onredo?.({ previous, current: selected });
|
|
208
|
+
return true;
|
|
209
|
+
};
|
|
210
|
+
// Debounced onsearch event - fires 150ms after search text stops changing
|
|
211
|
+
let search_debounce_timer = null;
|
|
212
|
+
let search_initialized = false;
|
|
213
|
+
$effect(() => {
|
|
214
|
+
const current_search = searchText;
|
|
215
|
+
// Skip initial mount - only fire on actual user input
|
|
216
|
+
if (!search_initialized) {
|
|
217
|
+
search_initialized = true;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!onsearch)
|
|
221
|
+
return; // cleanup handles any pending timer
|
|
222
|
+
search_debounce_timer = setTimeout(() => {
|
|
223
|
+
// Optional chaining in case onsearch is removed while timer is pending
|
|
224
|
+
onsearch?.({
|
|
225
|
+
searchText: current_search,
|
|
226
|
+
matchingCount: matchingOptions.length,
|
|
227
|
+
});
|
|
228
|
+
}, 150);
|
|
229
|
+
return () => {
|
|
230
|
+
if (search_debounce_timer)
|
|
231
|
+
clearTimeout(search_debounce_timer);
|
|
232
|
+
};
|
|
233
|
+
});
|
|
128
234
|
// Internal state for loadOptions feature (null = never loaded)
|
|
129
235
|
let loaded_options = $state([]);
|
|
130
236
|
let load_options_has_more = $state(true);
|
|
@@ -134,13 +240,13 @@ let debounce_timer = null;
|
|
|
134
240
|
let effective_options = $derived(loadOptions ? loaded_options : (options ?? []));
|
|
135
241
|
// Cache selected keys and labels to avoid repeated .map() calls
|
|
136
242
|
let selected_keys = $derived(selected.map(key));
|
|
137
|
-
let selected_labels = $derived(selected.map(get_label));
|
|
243
|
+
let selected_labels = $derived(selected.map(utils.get_label));
|
|
138
244
|
// Sets for O(1) lookups (used in template, has_user_msg, group_header_state, batch operations)
|
|
139
245
|
let selected_keys_set = $derived(new Set(selected_keys));
|
|
140
246
|
let selected_labels_set = $derived(new Set(selected_labels));
|
|
141
247
|
// Memoized Set of disabled option keys for O(1) lookups in large option sets
|
|
142
248
|
let disabled_option_keys = $derived(new Set(effective_options
|
|
143
|
-
.filter((opt) => is_object(opt) && opt.disabled)
|
|
249
|
+
.filter((opt) => utils.is_object(opt) && opt.disabled)
|
|
144
250
|
.map(key)));
|
|
145
251
|
// Check if an option is disabled (uses memoized Set for O(1) lookup)
|
|
146
252
|
const is_disabled = (opt) => disabled_option_keys.has(key(opt));
|
|
@@ -151,7 +257,7 @@ let grouped_options = $derived.by(() => {
|
|
|
151
257
|
const groups_map = new SvelteMap();
|
|
152
258
|
const ungrouped = [];
|
|
153
259
|
for (const opt of matchingOptions) {
|
|
154
|
-
if (has_group(opt)) {
|
|
260
|
+
if (utils.has_group(opt)) {
|
|
155
261
|
const existing = groups_map.get(opt.group);
|
|
156
262
|
if (existing)
|
|
157
263
|
existing.push(opt);
|
|
@@ -179,7 +285,11 @@ let grouped_options = $derived.by(() => {
|
|
|
179
285
|
}
|
|
180
286
|
if (ungrouped.length === 0)
|
|
181
287
|
return grouped;
|
|
182
|
-
const ungrouped_entry = {
|
|
288
|
+
const ungrouped_entry = {
|
|
289
|
+
group: null,
|
|
290
|
+
options: ungrouped,
|
|
291
|
+
collapsed: false,
|
|
292
|
+
};
|
|
183
293
|
return ungroupedPosition === `first`
|
|
184
294
|
? [ungrouped_entry, ...grouped]
|
|
185
295
|
: [...grouped, ungrouped_entry];
|
|
@@ -248,8 +358,9 @@ function expand_groups(groups_to_expand) {
|
|
|
248
358
|
if (groups_to_expand.length === 0)
|
|
249
359
|
return;
|
|
250
360
|
update_collapsed_groups(`delete`, groups_to_expand);
|
|
251
|
-
for (const group of groups_to_expand)
|
|
361
|
+
for (const group of groups_to_expand) {
|
|
252
362
|
ongroupToggle?.({ group, collapsed: false });
|
|
363
|
+
}
|
|
253
364
|
}
|
|
254
365
|
// Get names of collapsed groups that have matching options
|
|
255
366
|
const get_collapsed_with_matches = () => grouped_options.flatMap(({ group, collapsed, options: opts }) => group && collapsed && opts.length > 0 ? [group] : []);
|
|
@@ -260,61 +371,65 @@ $effect(() => {
|
|
|
260
371
|
}
|
|
261
372
|
});
|
|
262
373
|
// Normalize placeholder prop (supports string or { text, persistent } object)
|
|
263
|
-
const placeholder_text = $derived(typeof placeholder === `string` ? placeholder : placeholder?.text ?? null);
|
|
374
|
+
const placeholder_text = $derived(typeof placeholder === `string` ? placeholder : (placeholder?.text ?? null));
|
|
264
375
|
const placeholder_persistent = $derived(typeof placeholder === `object` && placeholder?.persistent === true);
|
|
265
376
|
// Helper to sort selected options (used by add() and select_all())
|
|
266
377
|
function sort_selected(items) {
|
|
267
378
|
if (sortSelected === true) {
|
|
268
|
-
return items.toSorted((op1, op2) => `${get_label(op1)}`.localeCompare(`${get_label(op2)}`));
|
|
379
|
+
return items.toSorted((op1, op2) => `${utils.get_label(op1)}`.localeCompare(`${utils.get_label(op2)}`));
|
|
269
380
|
}
|
|
270
381
|
else if (typeof sortSelected === `function`) {
|
|
271
382
|
return items.toSorted(sortSelected);
|
|
272
383
|
}
|
|
273
384
|
return items;
|
|
274
385
|
}
|
|
275
|
-
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
386
|
+
untrack(() => {
|
|
387
|
+
if (!loadOptions && !((options?.length ?? 0) > 0)) {
|
|
388
|
+
if (allowUserOptions || loading || disabled || allowEmpty) {
|
|
389
|
+
options = []; // initializing as array avoids errors when component mounts
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
// error on empty options if user is not allowed to create custom options and loading is false
|
|
393
|
+
// and component is not disabled and allowEmpty is false
|
|
394
|
+
console.error(`MultiSelect: received no options`);
|
|
395
|
+
}
|
|
283
396
|
}
|
|
284
|
-
}
|
|
397
|
+
});
|
|
285
398
|
if (maxSelect !== null && maxSelect < 1) {
|
|
286
399
|
console.error(`MultiSelect: maxSelect must be null or positive integer, got ${maxSelect}`);
|
|
287
400
|
}
|
|
288
401
|
if (!Array.isArray(selected)) {
|
|
289
402
|
console.error(`MultiSelect: selected prop should always be an array, got ${selected}`);
|
|
290
403
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
`
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
`
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
(
|
|
307
|
-
|
|
308
|
-
}
|
|
404
|
+
$effect(() => {
|
|
405
|
+
if (maxSelect && typeof required === `number` && required > maxSelect) {
|
|
406
|
+
console.error(`MultiSelect: maxSelect=${maxSelect} < required=${required}, makes it impossible for users to submit a valid form`);
|
|
407
|
+
}
|
|
408
|
+
if (parseLabelsAsHtml && allowUserOptions) {
|
|
409
|
+
console.warn(`MultiSelect: don't combine parseLabelsAsHtml and allowUserOptions. It's susceptible to XSS attacks!`);
|
|
410
|
+
}
|
|
411
|
+
if (sortSelected && selectedOptionsDraggable) {
|
|
412
|
+
console.warn(`MultiSelect: sortSelected and selectedOptionsDraggable should not be combined as any ` +
|
|
413
|
+
`user re-orderings of selected options will be undone by sortSelected on component re-renders.`);
|
|
414
|
+
}
|
|
415
|
+
if (allowUserOptions && !createOptionMsg && createOptionMsg !== null) {
|
|
416
|
+
console.error(`MultiSelect: allowUserOptions=${allowUserOptions} but createOptionMsg=${createOptionMsg} is falsy. ` +
|
|
417
|
+
`This prevents the "Add option" <span> from showing up, resulting in a confusing user experience.`);
|
|
418
|
+
}
|
|
419
|
+
if (maxOptions &&
|
|
420
|
+
(typeof maxOptions != `number` || maxOptions < 0 || maxOptions % 1 != 0)) {
|
|
421
|
+
console.error(`MultiSelect: maxOptions must be undefined or a positive integer, got ${maxOptions}`);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
309
424
|
let option_msg_is_active = $state(false); // controls active state of <li>{createOptionMsg}</li>
|
|
310
425
|
let window_width = $state(0);
|
|
311
426
|
// Check if option matches search text (label or optionally group name)
|
|
312
427
|
const matches_search = (opt, search) => {
|
|
313
428
|
if (filterFunc(opt, search))
|
|
314
429
|
return true;
|
|
315
|
-
if (searchMatchesGroups && search && has_group(opt)) {
|
|
430
|
+
if (searchMatchesGroups && search && utils.has_group(opt)) {
|
|
316
431
|
return fuzzy
|
|
317
|
-
? fuzzy_match(search, opt.group)
|
|
432
|
+
? utils.fuzzy_match(search, opt.group)
|
|
318
433
|
: opt.group.toLowerCase().includes(search.toLowerCase());
|
|
319
434
|
}
|
|
320
435
|
return false;
|
|
@@ -363,10 +478,19 @@ function add(option_to_add, event) {
|
|
|
363
478
|
event.stopPropagation();
|
|
364
479
|
if (maxSelect !== null && selected.length >= maxSelect)
|
|
365
480
|
wiggle = true;
|
|
366
|
-
if (!isNaN(Number(option_to_add)) &&
|
|
481
|
+
if (!isNaN(Number(option_to_add)) &&
|
|
482
|
+
typeof selected_labels[0] === `number`) {
|
|
367
483
|
option_to_add = Number(option_to_add); // convert to number if possible
|
|
368
484
|
}
|
|
369
485
|
const is_duplicate = selected_keys_set.has(key(option_to_add));
|
|
486
|
+
const max_reached = maxSelect !== null && maxSelect !== 1 &&
|
|
487
|
+
selected.length >= maxSelect;
|
|
488
|
+
// Fire events for blocked add attempts
|
|
489
|
+
if (max_reached) {
|
|
490
|
+
onmaxreached?.({ selected, maxSelect, attemptedOption: option_to_add });
|
|
491
|
+
}
|
|
492
|
+
if (is_duplicate && !duplicates)
|
|
493
|
+
onduplicate?.({ option: option_to_add });
|
|
370
494
|
if ((maxSelect === null || maxSelect === 1 || selected.length < maxSelect) &&
|
|
371
495
|
(duplicates || !is_duplicate)) {
|
|
372
496
|
if (!effective_options.includes(option_to_add) && // first check if we find option in the options list
|
|
@@ -415,7 +539,7 @@ function add(option_to_add, event) {
|
|
|
415
539
|
}
|
|
416
540
|
clear_validity();
|
|
417
541
|
handle_dropdown_after_select(event);
|
|
418
|
-
last_action = { type: `add`, label: `${get_label(option_to_add)}` };
|
|
542
|
+
last_action = { type: `add`, label: `${utils.get_label(option_to_add)}` };
|
|
419
543
|
onadd?.({ option: option_to_add });
|
|
420
544
|
onchange?.({ option: option_to_add, type: `add` });
|
|
421
545
|
}
|
|
@@ -440,7 +564,7 @@ function remove(option_to_drop, event) {
|
|
|
440
564
|
}
|
|
441
565
|
selected = selected.filter((_, remove_idx) => remove_idx !== idx);
|
|
442
566
|
clear_validity();
|
|
443
|
-
last_action = { type: `remove`, label: `${get_label(option_removed)}` };
|
|
567
|
+
last_action = { type: `remove`, label: `${utils.get_label(option_removed)}` };
|
|
444
568
|
onremove?.({ option: option_removed });
|
|
445
569
|
onchange?.({ option: option_removed, type: `remove` });
|
|
446
570
|
}
|
|
@@ -470,7 +594,8 @@ function handle_dropdown_after_select(event) {
|
|
|
470
594
|
const reached_max = selected.length >= (maxSelect ?? Infinity);
|
|
471
595
|
const should_close = closeDropdownOnSelect === true ||
|
|
472
596
|
closeDropdownOnSelect === `retain-focus` ||
|
|
473
|
-
(closeDropdownOnSelect === `if-mobile` &&
|
|
597
|
+
(closeDropdownOnSelect === `if-mobile` &&
|
|
598
|
+
window_width &&
|
|
474
599
|
window_width < breakpoint);
|
|
475
600
|
if (reached_max || should_close) {
|
|
476
601
|
close_dropdown(event, closeDropdownOnSelect === `retain-focus`);
|
|
@@ -479,19 +604,24 @@ function handle_dropdown_after_select(event) {
|
|
|
479
604
|
input?.focus();
|
|
480
605
|
}
|
|
481
606
|
// Check if a user message (create option, duplicate warning, no match) is visible
|
|
482
|
-
const has_user_msg = $derived(searchText.length > 0 &&
|
|
483
|
-
(
|
|
484
|
-
|
|
607
|
+
const has_user_msg = $derived(searchText.length > 0 &&
|
|
608
|
+
Boolean((allowUserOptions && createOptionMsg) ||
|
|
609
|
+
(!duplicates && selected_labels_set.has(searchText)) ||
|
|
610
|
+
(navigable_options.length === 0 && noMatchingOptionsMsg)));
|
|
485
611
|
// Handle arrow key navigation through options (uses navigable_options to skip collapsed groups)
|
|
486
612
|
async function handle_arrow_navigation(direction) {
|
|
487
613
|
ignore_hover = true;
|
|
488
614
|
// Auto-expand collapsed groups when keyboard navigating
|
|
489
|
-
if (keyboardExpandsCollapsedGroups &&
|
|
615
|
+
if (keyboardExpandsCollapsedGroups &&
|
|
616
|
+
collapsibleGroups &&
|
|
617
|
+
collapsedGroups.size > 0) {
|
|
490
618
|
expand_groups(get_collapsed_with_matches());
|
|
491
619
|
await tick();
|
|
492
620
|
}
|
|
493
621
|
// toggle user message when no options match but user can create
|
|
494
|
-
if (allowUserOptions &&
|
|
622
|
+
if (allowUserOptions &&
|
|
623
|
+
!navigable_options.length &&
|
|
624
|
+
searchText.length > 0) {
|
|
495
625
|
option_msg_is_active = !option_msg_is_active;
|
|
496
626
|
return;
|
|
497
627
|
}
|
|
@@ -503,17 +633,26 @@ async function handle_arrow_navigation(direction) {
|
|
|
503
633
|
}
|
|
504
634
|
else {
|
|
505
635
|
const total = navigable_options.length + (has_user_msg ? 1 : 0);
|
|
636
|
+
// Guard against division by zero (can happen if options filtered away before effect resets activeIndex)
|
|
637
|
+
if (total === 0) {
|
|
638
|
+
activeIndex = null;
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
506
641
|
activeIndex = (activeIndex + direction + total) % total; // +total handles negative mod
|
|
507
642
|
}
|
|
508
643
|
// update active state based on new index
|
|
509
644
|
option_msg_is_active = has_user_msg && activeIndex === navigable_options.length;
|
|
510
645
|
activeOption = option_msg_is_active
|
|
511
646
|
? null
|
|
512
|
-
: navigable_options[activeIndex] ?? null;
|
|
647
|
+
: (navigable_options[activeIndex] ?? null);
|
|
513
648
|
if (autoScroll) {
|
|
514
649
|
await tick();
|
|
515
|
-
document
|
|
650
|
+
document
|
|
651
|
+
.querySelector(`ul.options > li.active`)
|
|
652
|
+
?.scrollIntoViewIfNeeded?.();
|
|
516
653
|
}
|
|
654
|
+
// Fire onactivate for keyboard navigation only (not mouse hover)
|
|
655
|
+
onactivate?.({ option: activeOption, index: activeIndex });
|
|
517
656
|
}
|
|
518
657
|
// handle all keyboard events this component receives
|
|
519
658
|
async function handle_keydown(event) {
|
|
@@ -544,6 +683,16 @@ async function handle_keydown(event) {
|
|
|
544
683
|
searchText = ``;
|
|
545
684
|
},
|
|
546
685
|
},
|
|
686
|
+
{
|
|
687
|
+
key: `undo`,
|
|
688
|
+
condition: () => !!canUndo,
|
|
689
|
+
action: () => undo?.(),
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
key: `redo`,
|
|
693
|
+
condition: () => !!canRedo,
|
|
694
|
+
action: () => redo?.(),
|
|
695
|
+
},
|
|
547
696
|
];
|
|
548
697
|
for (const { key, condition, action } of shortcut_actions) {
|
|
549
698
|
if (matches_shortcut(event, effective_shortcuts[key]) && condition()) {
|
|
@@ -616,7 +765,10 @@ function remove_all(event) {
|
|
|
616
765
|
// Only fire events if something was actually removed
|
|
617
766
|
if (removed_options.length > 0) {
|
|
618
767
|
searchText = ``; // always clear on remove all (resetFilterOnAdd only applies to add operations)
|
|
619
|
-
last_action = {
|
|
768
|
+
last_action = {
|
|
769
|
+
type: `removeAll`,
|
|
770
|
+
label: `${removed_options.length} options`,
|
|
771
|
+
};
|
|
620
772
|
onremoveAll?.({ options: removed_options });
|
|
621
773
|
onchange?.({ options: selected, type: `removeAll` });
|
|
622
774
|
}
|
|
@@ -699,8 +851,9 @@ function on_click_outside(event) {
|
|
|
699
851
|
if (outerDiv.contains(target))
|
|
700
852
|
return;
|
|
701
853
|
// If portal is active, also check if click is inside the portalled options dropdown
|
|
702
|
-
if (portal_params?.active && ul_options && ul_options.contains(target))
|
|
854
|
+
if (portal_params?.active && ul_options && ul_options.contains(target)) {
|
|
703
855
|
return;
|
|
856
|
+
}
|
|
704
857
|
// Click is outside both the main component and any portalled dropdown
|
|
705
858
|
close_dropdown(event);
|
|
706
859
|
}
|
|
@@ -812,8 +965,9 @@ function portal(node, params) {
|
|
|
812
965
|
target_node = params.target_node;
|
|
813
966
|
render_in_place = typeof window === `undefined` ||
|
|
814
967
|
!document.body.contains(node);
|
|
815
|
-
if (open && !render_in_place && target_node)
|
|
968
|
+
if (open && !render_in_place && target_node) {
|
|
816
969
|
tick().then(update_position);
|
|
970
|
+
}
|
|
817
971
|
else if (!open || !target_node)
|
|
818
972
|
node.hidden = true;
|
|
819
973
|
},
|
|
@@ -828,7 +982,8 @@ function portal(node, params) {
|
|
|
828
982
|
}
|
|
829
983
|
// Dynamic options loading - captures search at call time to avoid race conditions
|
|
830
984
|
async function load_dynamic_options(reset) {
|
|
831
|
-
if (!load_options_config ||
|
|
985
|
+
if (!load_options_config ||
|
|
986
|
+
load_options_loading ||
|
|
832
987
|
(!reset && !load_options_has_more)) {
|
|
833
988
|
return;
|
|
834
989
|
}
|
|
@@ -889,11 +1044,13 @@ $effect(() => {
|
|
|
889
1044
|
};
|
|
890
1045
|
});
|
|
891
1046
|
function handle_options_scroll(event) {
|
|
892
|
-
if (!load_options_config || load_options_loading || !load_options_has_more)
|
|
1047
|
+
if (!load_options_config || load_options_loading || !load_options_has_more) {
|
|
893
1048
|
return;
|
|
1049
|
+
}
|
|
894
1050
|
const { scrollTop, scrollHeight, clientHeight } = event.target;
|
|
895
|
-
if (scrollHeight - scrollTop - clientHeight <= 100)
|
|
1051
|
+
if (scrollHeight - scrollTop - clientHeight <= 100) {
|
|
896
1052
|
load_dynamic_options(false);
|
|
1053
|
+
}
|
|
897
1054
|
}
|
|
898
1055
|
</script>
|
|
899
1056
|
|
|
@@ -957,11 +1114,9 @@ function handle_options_scroll(event) {
|
|
|
957
1114
|
style={ulSelectedStyle}
|
|
958
1115
|
>
|
|
959
1116
|
{#each selected as option, idx (duplicates ? `${key(option)}-${idx}` : key(option))}
|
|
960
|
-
{@const selectedOptionStyle =
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
) ||
|
|
964
|
-
null}
|
|
1117
|
+
{@const selectedOptionStyle = [utils.get_style(option, `selected`), liSelectedStyle]
|
|
1118
|
+
.filter(Boolean)
|
|
1119
|
+
.join(` `) || null}
|
|
965
1120
|
<li
|
|
966
1121
|
class={liSelectedClass}
|
|
967
1122
|
role="option"
|
|
@@ -979,26 +1134,20 @@ function handle_options_scroll(event) {
|
|
|
979
1134
|
onmouseup={(event) => event.stopPropagation()}
|
|
980
1135
|
>
|
|
981
1136
|
{#if selectedItem}
|
|
982
|
-
{@render selectedItem({
|
|
983
|
-
option,
|
|
984
|
-
idx,
|
|
985
|
-
})}
|
|
1137
|
+
{@render selectedItem({ option, idx })}
|
|
986
1138
|
{:else if children}
|
|
987
|
-
{@render children({
|
|
988
|
-
option,
|
|
989
|
-
idx,
|
|
990
|
-
})}
|
|
1139
|
+
{@render children({ option, idx })}
|
|
991
1140
|
{:else if parseLabelsAsHtml}
|
|
992
|
-
{@html get_label(option)}
|
|
1141
|
+
{@html utils.get_label(option)}
|
|
993
1142
|
{:else}
|
|
994
|
-
{get_label(option)}
|
|
1143
|
+
{utils.get_label(option)}
|
|
995
1144
|
{/if}
|
|
996
1145
|
{#if !disabled && can_remove}
|
|
997
1146
|
<button
|
|
998
1147
|
onclick={(event) => remove(option, event)}
|
|
999
1148
|
onkeydown={if_enter_or_space((event) => remove(option, event))}
|
|
1000
1149
|
type="button"
|
|
1001
|
-
title="{removeBtnTitle} {get_label(option)}"
|
|
1150
|
+
title="{removeBtnTitle} {utils.get_label(option)}"
|
|
1002
1151
|
class="remove"
|
|
1003
1152
|
>
|
|
1004
1153
|
{#if removeIcon}
|
|
@@ -1145,8 +1294,9 @@ function handle_options_scroll(event) {
|
|
|
1145
1294
|
(group_name ?? `ungrouped-${group_idx}`)
|
|
1146
1295
|
}
|
|
1147
1296
|
{#if group_name !== null}
|
|
1148
|
-
{@const { all_selected, selected_count } = group_header_state.get(
|
|
1149
|
-
|
|
1297
|
+
{@const { all_selected, selected_count } = group_header_state.get(
|
|
1298
|
+
group_name,
|
|
1299
|
+
) ?? { all_selected: false, selected_count: 0 }}
|
|
1150
1300
|
{@const handle_toggle = () =>
|
|
1151
1301
|
collapsibleGroups && toggle_group_collapsed(group_name)}
|
|
1152
1302
|
{@const handle_group_select = (event: Event) =>
|
|
@@ -1210,14 +1360,12 @@ function handle_options_scroll(event) {
|
|
|
1210
1360
|
title = null,
|
|
1211
1361
|
selectedTitle = null,
|
|
1212
1362
|
disabledTitle = defaultDisabledTitle,
|
|
1213
|
-
} = is_object(option_item) ? option_item : { label: option_item }}
|
|
1363
|
+
} = utils.is_object(option_item) ? option_item : { label: option_item }}
|
|
1214
1364
|
{@const active = activeIndex === flat_idx && flat_idx >= 0}
|
|
1215
1365
|
{@const selected = is_selected(label)}
|
|
1216
|
-
{@const optionStyle =
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
) ||
|
|
1220
|
-
null}
|
|
1366
|
+
{@const optionStyle = [utils.get_style(option_item, `option`), liOptionStyle]
|
|
1367
|
+
.filter(Boolean)
|
|
1368
|
+
.join(` `) || null}
|
|
1221
1369
|
{#if is_option_visible(flat_idx)}
|
|
1222
1370
|
<li
|
|
1223
1371
|
id="{internal_id}-opt-{flat_idx}"
|
|
@@ -1247,24 +1395,18 @@ function handle_options_scroll(event) {
|
|
|
1247
1395
|
type="checkbox"
|
|
1248
1396
|
class="option-checkbox"
|
|
1249
1397
|
checked={selected}
|
|
1250
|
-
aria-label="Toggle {get_label(option_item)}"
|
|
1398
|
+
aria-label="Toggle {utils.get_label(option_item)}"
|
|
1251
1399
|
tabindex="-1"
|
|
1252
1400
|
/>
|
|
1253
1401
|
{/if}
|
|
1254
1402
|
{#if option}
|
|
1255
|
-
{@render option({
|
|
1256
|
-
option: option_item,
|
|
1257
|
-
idx: flat_idx,
|
|
1258
|
-
})}
|
|
1403
|
+
{@render option({ option: option_item, idx: flat_idx })}
|
|
1259
1404
|
{:else if children}
|
|
1260
|
-
{@render children({
|
|
1261
|
-
option: option_item,
|
|
1262
|
-
idx: flat_idx,
|
|
1263
|
-
})}
|
|
1405
|
+
{@render children({ option: option_item, idx: flat_idx })}
|
|
1264
1406
|
{:else if parseLabelsAsHtml}
|
|
1265
|
-
{@html get_label(option_item)}
|
|
1407
|
+
{@html utils.get_label(option_item)}
|
|
1266
1408
|
{:else}
|
|
1267
|
-
{get_label(option_item)}
|
|
1409
|
+
{utils.get_label(option_item)}
|
|
1268
1410
|
{/if}
|
|
1269
1411
|
</li>
|
|
1270
1412
|
{/if}
|
|
@@ -1323,7 +1465,11 @@ function handle_options_scroll(event) {
|
|
|
1323
1465
|
{/if}
|
|
1324
1466
|
{/if}
|
|
1325
1467
|
{#if loadOptions && load_options_loading}
|
|
1326
|
-
<li
|
|
1468
|
+
<li
|
|
1469
|
+
class="loading-more"
|
|
1470
|
+
role="status"
|
|
1471
|
+
aria-label="Loading more options"
|
|
1472
|
+
>
|
|
1327
1473
|
<CircleSpinner />
|
|
1328
1474
|
</li>
|
|
1329
1475
|
{/if}
|
|
@@ -1359,7 +1505,9 @@ function handle_options_scroll(event) {
|
|
|
1359
1505
|
border: 0;
|
|
1360
1506
|
}
|
|
1361
1507
|
|
|
1362
|
-
:
|
|
1508
|
+
/* Use :where() for elements with user-overridable class props (outerDivClass, ulSelectedClass, liSelectedClass)
|
|
1509
|
+
so user-provided classes take precedence. See: https://github.com/janosh/svelte-multiselect/issues/380 */
|
|
1510
|
+
:where(div.multiselect) {
|
|
1363
1511
|
position: relative;
|
|
1364
1512
|
align-items: center;
|
|
1365
1513
|
display: flex;
|
|
@@ -1376,27 +1524,30 @@ function handle_options_scroll(event) {
|
|
|
1376
1524
|
min-height: var(--sms-min-height, 22pt);
|
|
1377
1525
|
margin: var(--sms-margin);
|
|
1378
1526
|
}
|
|
1379
|
-
:
|
|
1527
|
+
:where(div.multiselect.open) {
|
|
1380
1528
|
/* increase z-index when open to ensure the dropdown of one <MultiSelect />
|
|
1381
1529
|
displays above that of another slightly below it on the page */
|
|
1382
1530
|
z-index: var(--sms-open-z-index, 4);
|
|
1383
1531
|
}
|
|
1384
|
-
:
|
|
1385
|
-
border: var(
|
|
1532
|
+
:where(div.multiselect:focus-within) {
|
|
1533
|
+
border: var(
|
|
1534
|
+
--sms-focus-border,
|
|
1535
|
+
1pt solid var(--sms-active-color, cornflowerblue)
|
|
1536
|
+
);
|
|
1386
1537
|
}
|
|
1387
|
-
:
|
|
1538
|
+
:where(div.multiselect.disabled) {
|
|
1388
1539
|
background: var(--sms-disabled-bg, light-dark(lightgray, #444));
|
|
1389
1540
|
cursor: not-allowed;
|
|
1390
1541
|
}
|
|
1391
1542
|
|
|
1392
|
-
:
|
|
1543
|
+
:where(div.multiselect > ul.selected) {
|
|
1393
1544
|
display: flex;
|
|
1394
1545
|
flex: 1;
|
|
1395
1546
|
padding: 0;
|
|
1396
1547
|
margin: 0;
|
|
1397
1548
|
flex-wrap: wrap;
|
|
1398
1549
|
}
|
|
1399
|
-
:
|
|
1550
|
+
:where(div.multiselect > ul.selected > li) {
|
|
1400
1551
|
align-items: center;
|
|
1401
1552
|
border-radius: 3pt;
|
|
1402
1553
|
display: flex;
|
|
@@ -1411,13 +1562,16 @@ function handle_options_scroll(event) {
|
|
|
1411
1562
|
padding: var(--sms-selected-li-padding, 1pt 5pt);
|
|
1412
1563
|
color: var(--sms-selected-text-color, var(--sms-text-color));
|
|
1413
1564
|
}
|
|
1414
|
-
:
|
|
1565
|
+
:where(div.multiselect > ul.selected > li[draggable='true']) {
|
|
1415
1566
|
cursor: grab;
|
|
1416
1567
|
}
|
|
1417
|
-
:
|
|
1568
|
+
:where(div.multiselect > ul.selected > li.active) {
|
|
1418
1569
|
background: var(
|
|
1419
1570
|
--sms-li-active-bg,
|
|
1420
|
-
var(
|
|
1571
|
+
var(
|
|
1572
|
+
--sms-active-color,
|
|
1573
|
+
light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.15))
|
|
1574
|
+
)
|
|
1421
1575
|
);
|
|
1422
1576
|
}
|
|
1423
1577
|
:is(div.multiselect button) {
|
|
@@ -1448,7 +1602,7 @@ function handle_options_scroll(event) {
|
|
|
1448
1602
|
margin: auto 0; /* CSS reset */
|
|
1449
1603
|
padding: 0; /* CSS reset */
|
|
1450
1604
|
}
|
|
1451
|
-
:
|
|
1605
|
+
:where(div.multiselect > ul.selected > input) {
|
|
1452
1606
|
border: none;
|
|
1453
1607
|
outline: none;
|
|
1454
1608
|
background: none;
|
|
@@ -1462,7 +1616,7 @@ function handle_options_scroll(event) {
|
|
|
1462
1616
|
}
|
|
1463
1617
|
|
|
1464
1618
|
/* When options are selected, placeholder is hidden in which case we minimize input width to avoid adding unnecessary width to div.multiselect */
|
|
1465
|
-
:
|
|
1619
|
+
:where(div.multiselect > ul.selected > input:not(:placeholder-shown)) {
|
|
1466
1620
|
min-width: 1px; /* Minimal width to remain interactive */
|
|
1467
1621
|
}
|
|
1468
1622
|
|
|
@@ -1483,7 +1637,8 @@ function handle_options_scroll(event) {
|
|
|
1483
1637
|
pointer-events: none;
|
|
1484
1638
|
}
|
|
1485
1639
|
|
|
1486
|
-
ul.options
|
|
1640
|
+
/* Use :where() for ul.options elements with class props (ulOptionsClass, liOptionClass, liUserMsgClass) */
|
|
1641
|
+
:where(ul.options) {
|
|
1487
1642
|
list-style: none;
|
|
1488
1643
|
/* top, left, width, position are managed by portal when active */
|
|
1489
1644
|
/* but provide defaults for non-portaled or initial state */
|
|
@@ -1493,10 +1648,8 @@ function handle_options_scroll(event) {
|
|
|
1493
1648
|
width: 100%;
|
|
1494
1649
|
/* Default z-index if not portaled/overridden by portal */
|
|
1495
1650
|
z-index: var(--sms-options-z-index, 3);
|
|
1496
|
-
|
|
1497
1651
|
overflow: auto;
|
|
1498
|
-
transition: all
|
|
1499
|
-
0.2s; /* Consider if this transition is desirable with portal positioning */
|
|
1652
|
+
transition: all 0.2s; /* is this transition is desirable with portal positioning? */
|
|
1500
1653
|
box-sizing: border-box;
|
|
1501
1654
|
background: var(--sms-options-bg, light-dark(#fafafa, #1a1a1a));
|
|
1502
1655
|
max-height: var(--sms-options-max-height, 50vh);
|
|
@@ -1511,24 +1664,24 @@ function handle_options_scroll(event) {
|
|
|
1511
1664
|
padding: var(--sms-options-padding);
|
|
1512
1665
|
margin: var(--sms-options-margin, 6pt 0 0 0);
|
|
1513
1666
|
}
|
|
1514
|
-
ul.options.hidden {
|
|
1667
|
+
:where(ul.options.hidden) {
|
|
1515
1668
|
visibility: hidden;
|
|
1516
1669
|
opacity: 0;
|
|
1517
1670
|
transform: translateY(50px);
|
|
1518
1671
|
pointer-events: none;
|
|
1519
1672
|
}
|
|
1520
|
-
ul.options > li {
|
|
1673
|
+
:where(ul.options > li) {
|
|
1521
1674
|
padding: 3pt 1ex;
|
|
1522
1675
|
cursor: pointer;
|
|
1523
1676
|
scroll-margin: var(--sms-options-scroll-margin, 100px);
|
|
1524
1677
|
border-left: 3px solid transparent;
|
|
1525
1678
|
}
|
|
1526
|
-
ul.options .user-msg {
|
|
1679
|
+
:where(ul.options .user-msg) {
|
|
1527
1680
|
/* block needed so vertical padding applies to span */
|
|
1528
1681
|
display: block;
|
|
1529
1682
|
padding: 3pt 2ex;
|
|
1530
1683
|
}
|
|
1531
|
-
ul.options > li.selected {
|
|
1684
|
+
:where(ul.options > li.selected) {
|
|
1532
1685
|
background: var(
|
|
1533
1686
|
--sms-li-selected-plain-bg,
|
|
1534
1687
|
light-dark(rgba(0, 123, 255, 0.1), rgba(100, 180, 255, 0.2))
|
|
@@ -1538,26 +1691,29 @@ function handle_options_scroll(event) {
|
|
|
1538
1691
|
3px solid var(--sms-active-color, cornflowerblue)
|
|
1539
1692
|
);
|
|
1540
1693
|
}
|
|
1541
|
-
ul.options > li.active {
|
|
1694
|
+
:where(ul.options > li.active) {
|
|
1542
1695
|
background: var(
|
|
1543
1696
|
--sms-li-active-bg,
|
|
1544
|
-
var(
|
|
1697
|
+
var(
|
|
1698
|
+
--sms-active-color,
|
|
1699
|
+
light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.15))
|
|
1700
|
+
)
|
|
1545
1701
|
);
|
|
1546
1702
|
}
|
|
1547
|
-
ul.options > li.disabled {
|
|
1703
|
+
:where(ul.options > li.disabled) {
|
|
1548
1704
|
cursor: not-allowed;
|
|
1549
1705
|
background: var(--sms-li-disabled-bg, light-dark(#f5f5f6, #2a2a2a));
|
|
1550
1706
|
color: var(--sms-li-disabled-text, light-dark(#b8b8b8, #666));
|
|
1551
1707
|
}
|
|
1552
|
-
/* Checkbox styling for keepSelectedInDropdown='checkboxes' mode */
|
|
1553
|
-
ul.options > li > input.option-checkbox {
|
|
1708
|
+
/* Checkbox styling for keepSelectedInDropdown='checkboxes' mode - internal, no class prop */
|
|
1709
|
+
:is(ul.options > li > input.option-checkbox) {
|
|
1554
1710
|
width: 16px;
|
|
1555
1711
|
height: 16px;
|
|
1556
1712
|
margin-right: 6px;
|
|
1557
1713
|
accent-color: var(--sms-active-color, cornflowerblue);
|
|
1558
1714
|
}
|
|
1559
|
-
/* Select all option styling */
|
|
1560
|
-
ul.options > li.select-all {
|
|
1715
|
+
/* Select all option styling - has liSelectAllClass prop */
|
|
1716
|
+
:where(ul.options > li.select-all) {
|
|
1561
1717
|
border-bottom: var(
|
|
1562
1718
|
--sms-select-all-border-bottom,
|
|
1563
1719
|
1px solid light-dark(lightgray, #555)
|
|
@@ -1567,7 +1723,7 @@ function handle_options_scroll(event) {
|
|
|
1567
1723
|
background: var(--sms-select-all-bg, transparent);
|
|
1568
1724
|
margin-bottom: var(--sms-select-all-margin-bottom, 2pt);
|
|
1569
1725
|
}
|
|
1570
|
-
ul.options > li.select-all:hover {
|
|
1726
|
+
:where(ul.options > li.select-all:hover) {
|
|
1571
1727
|
background: var(
|
|
1572
1728
|
--sms-select-all-hover-bg,
|
|
1573
1729
|
var(
|
|
@@ -1579,8 +1735,8 @@ function handle_options_scroll(event) {
|
|
|
1579
1735
|
)
|
|
1580
1736
|
);
|
|
1581
1737
|
}
|
|
1582
|
-
/* Group header styling */
|
|
1583
|
-
ul.options > li.group-header {
|
|
1738
|
+
/* Group header styling - has liGroupHeaderClass prop */
|
|
1739
|
+
:where(ul.options > li.group-header) {
|
|
1584
1740
|
display: flex;
|
|
1585
1741
|
align-items: center;
|
|
1586
1742
|
font-weight: var(--sms-group-header-font-weight, 600);
|
|
@@ -1593,30 +1749,34 @@ function handle_options_scroll(event) {
|
|
|
1593
1749
|
text-transform: var(--sms-group-header-text-transform, uppercase);
|
|
1594
1750
|
letter-spacing: var(--sms-group-header-letter-spacing, 0.5px);
|
|
1595
1751
|
}
|
|
1596
|
-
ul.options > li.group-header:not(:first-child) {
|
|
1752
|
+
:where(ul.options > li.group-header:not(:first-child)) {
|
|
1597
1753
|
margin-top: var(--sms-group-header-margin-top, 4pt);
|
|
1598
|
-
border-top: var(
|
|
1754
|
+
border-top: var(
|
|
1755
|
+
--sms-group-header-border-top,
|
|
1756
|
+
1px solid light-dark(#eee, #333)
|
|
1757
|
+
);
|
|
1599
1758
|
}
|
|
1600
|
-
ul.options > li.group-header.collapsible {
|
|
1759
|
+
:where(ul.options > li.group-header.collapsible) {
|
|
1601
1760
|
cursor: pointer;
|
|
1602
1761
|
}
|
|
1603
|
-
ul.options > li.group-header.collapsible:hover {
|
|
1762
|
+
:where(ul.options > li.group-header.collapsible:hover) {
|
|
1604
1763
|
background: var(
|
|
1605
1764
|
--sms-group-header-hover-bg,
|
|
1606
1765
|
light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05))
|
|
1607
1766
|
);
|
|
1608
1767
|
}
|
|
1609
|
-
|
|
1768
|
+
/* Internal elements without class props - keep :is() for specificity */
|
|
1769
|
+
:is(ul.options > li.group-header .group-label) {
|
|
1610
1770
|
flex: 1;
|
|
1611
1771
|
}
|
|
1612
|
-
ul.options > li.group-header .group-count {
|
|
1772
|
+
:is(ul.options > li.group-header .group-count) {
|
|
1613
1773
|
opacity: 0.6;
|
|
1614
1774
|
font-size: 0.9em;
|
|
1615
1775
|
font-weight: normal;
|
|
1616
1776
|
margin-left: 4pt;
|
|
1617
1777
|
}
|
|
1618
1778
|
/* Sticky group headers when enabled */
|
|
1619
|
-
ul.options > li.group-header.sticky {
|
|
1779
|
+
:where(ul.options > li.group-header.sticky) {
|
|
1620
1780
|
position: sticky;
|
|
1621
1781
|
top: 0;
|
|
1622
1782
|
z-index: 1;
|
|
@@ -1626,17 +1786,20 @@ function handle_options_scroll(event) {
|
|
|
1626
1786
|
);
|
|
1627
1787
|
}
|
|
1628
1788
|
/* Indent grouped options for visual hierarchy */
|
|
1629
|
-
|
|
1789
|
+
:where(
|
|
1790
|
+
ul.options > li:not(.group-header):not(.select-all):not(.user-msg):not(.loading-more)
|
|
1791
|
+
) {
|
|
1630
1792
|
padding-left: var(
|
|
1631
1793
|
--sms-group-item-padding-left,
|
|
1632
1794
|
var(--sms-group-option-indent, 1.5ex)
|
|
1633
1795
|
);
|
|
1634
1796
|
}
|
|
1635
|
-
/* Collapse/expand animation for group chevron icon */
|
|
1636
|
-
ul.options > li.group-header :global(svg) {
|
|
1797
|
+
/* Collapse/expand animation for group chevron icon - internal, keep :is() for specificity */
|
|
1798
|
+
:is(ul.options > li.group-header) :global(svg) {
|
|
1637
1799
|
transition: transform var(--sms-group-collapse-duration, 0.15s) ease-out;
|
|
1638
1800
|
}
|
|
1639
|
-
|
|
1801
|
+
/* Keep :is() for internal buttons without class props */
|
|
1802
|
+
:is(ul.options > li.group-header button.group-select-all) {
|
|
1640
1803
|
font-size: 0.9em;
|
|
1641
1804
|
font-weight: normal;
|
|
1642
1805
|
text-transform: none;
|
|
@@ -1649,23 +1812,23 @@ function handle_options_scroll(event) {
|
|
|
1649
1812
|
border-radius: 3pt;
|
|
1650
1813
|
aspect-ratio: auto; /* override global button aspect-ratio: 1 */
|
|
1651
1814
|
}
|
|
1652
|
-
ul.options > li.group-header button.group-select-all:hover {
|
|
1815
|
+
:is(ul.options > li.group-header button.group-select-all:hover) {
|
|
1653
1816
|
background: var(
|
|
1654
1817
|
--sms-group-select-all-hover-bg,
|
|
1655
1818
|
light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1))
|
|
1656
1819
|
);
|
|
1657
1820
|
}
|
|
1658
|
-
ul.options > li.group-header button.group-select-all.deselect {
|
|
1821
|
+
:is(ul.options > li.group-header button.group-select-all.deselect) {
|
|
1659
1822
|
color: var(--sms-group-deselect-color, light-dark(#c44, #f77));
|
|
1660
1823
|
}
|
|
1661
|
-
:
|
|
1824
|
+
:where(span.max-select-msg) {
|
|
1662
1825
|
padding: 0 3pt;
|
|
1663
1826
|
}
|
|
1664
1827
|
::highlight(sms-search-matches) {
|
|
1665
1828
|
color: light-dark(#1a8870, mediumaquamarine);
|
|
1666
1829
|
}
|
|
1667
|
-
/* Loading more indicator for infinite scrolling */
|
|
1668
|
-
ul.options > li.loading-more {
|
|
1830
|
+
/* Loading more indicator for infinite scrolling - internal, no class prop */
|
|
1831
|
+
:is(ul.options > li.loading-more) {
|
|
1669
1832
|
display: flex;
|
|
1670
1833
|
justify-content: center;
|
|
1671
1834
|
align-items: center;
|