svelte-multiselect 11.5.2 → 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/MultiSelect.svelte +207 -94
- package/dist/MultiSelect.svelte.d.ts +2 -2
- package/dist/Nav.svelte +108 -55
- package/dist/Nav.svelte.d.ts +1 -1
- package/dist/attachments.d.ts +2 -0
- package/dist/attachments.js +4 -1
- package/dist/heading-anchors.js +10 -16
- 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 +15 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +11 -0
- package/package.json +37 -14
|
@@ -21,7 +21,7 @@ declare function $$render<Action extends {
|
|
|
21
21
|
dialog_props?: HTMLAttributes<HTMLDialogElement>;
|
|
22
22
|
};
|
|
23
23
|
exports: {};
|
|
24
|
-
bindings: "
|
|
24
|
+
bindings: "open" | "input" | "dialog";
|
|
25
25
|
slots: {};
|
|
26
26
|
events: {};
|
|
27
27
|
};
|
|
@@ -37,7 +37,7 @@ declare class __sveltets_Render<Action extends {
|
|
|
37
37
|
props(): ReturnType<typeof $$render<Action>>['props'];
|
|
38
38
|
events(): ReturnType<typeof $$render<Action>>['events'];
|
|
39
39
|
slots(): ReturnType<typeof $$render<Action>>['slots'];
|
|
40
|
-
bindings(): "
|
|
40
|
+
bindings(): "open" | "input" | "dialog";
|
|
41
41
|
exports(): {};
|
|
42
42
|
}
|
|
43
43
|
interface $$IsomorphicComponent {
|
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 = {},
|
|
@@ -28,22 +28,22 @@ selectedFlipParams = { duration: 100 },
|
|
|
28
28
|
// Option grouping feature
|
|
29
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,83 @@ $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
|
+
};
|
|
128
210
|
// Debounced onsearch event - fires 150ms after search text stops changing
|
|
129
211
|
let search_debounce_timer = null;
|
|
130
212
|
let search_initialized = false;
|
|
@@ -158,13 +240,13 @@ let debounce_timer = null;
|
|
|
158
240
|
let effective_options = $derived(loadOptions ? loaded_options : (options ?? []));
|
|
159
241
|
// Cache selected keys and labels to avoid repeated .map() calls
|
|
160
242
|
let selected_keys = $derived(selected.map(key));
|
|
161
|
-
let selected_labels = $derived(selected.map(get_label));
|
|
243
|
+
let selected_labels = $derived(selected.map(utils.get_label));
|
|
162
244
|
// Sets for O(1) lookups (used in template, has_user_msg, group_header_state, batch operations)
|
|
163
245
|
let selected_keys_set = $derived(new Set(selected_keys));
|
|
164
246
|
let selected_labels_set = $derived(new Set(selected_labels));
|
|
165
247
|
// Memoized Set of disabled option keys for O(1) lookups in large option sets
|
|
166
248
|
let disabled_option_keys = $derived(new Set(effective_options
|
|
167
|
-
.filter((opt) => is_object(opt) && opt.disabled)
|
|
249
|
+
.filter((opt) => utils.is_object(opt) && opt.disabled)
|
|
168
250
|
.map(key)));
|
|
169
251
|
// Check if an option is disabled (uses memoized Set for O(1) lookup)
|
|
170
252
|
const is_disabled = (opt) => disabled_option_keys.has(key(opt));
|
|
@@ -175,7 +257,7 @@ let grouped_options = $derived.by(() => {
|
|
|
175
257
|
const groups_map = new SvelteMap();
|
|
176
258
|
const ungrouped = [];
|
|
177
259
|
for (const opt of matchingOptions) {
|
|
178
|
-
if (has_group(opt)) {
|
|
260
|
+
if (utils.has_group(opt)) {
|
|
179
261
|
const existing = groups_map.get(opt.group);
|
|
180
262
|
if (existing)
|
|
181
263
|
existing.push(opt);
|
|
@@ -203,7 +285,11 @@ let grouped_options = $derived.by(() => {
|
|
|
203
285
|
}
|
|
204
286
|
if (ungrouped.length === 0)
|
|
205
287
|
return grouped;
|
|
206
|
-
const ungrouped_entry = {
|
|
288
|
+
const ungrouped_entry = {
|
|
289
|
+
group: null,
|
|
290
|
+
options: ungrouped,
|
|
291
|
+
collapsed: false,
|
|
292
|
+
};
|
|
207
293
|
return ungroupedPosition === `first`
|
|
208
294
|
? [ungrouped_entry, ...grouped]
|
|
209
295
|
: [...grouped, ungrouped_entry];
|
|
@@ -272,8 +358,9 @@ function expand_groups(groups_to_expand) {
|
|
|
272
358
|
if (groups_to_expand.length === 0)
|
|
273
359
|
return;
|
|
274
360
|
update_collapsed_groups(`delete`, groups_to_expand);
|
|
275
|
-
for (const group of groups_to_expand)
|
|
361
|
+
for (const group of groups_to_expand) {
|
|
276
362
|
ongroupToggle?.({ group, collapsed: false });
|
|
363
|
+
}
|
|
277
364
|
}
|
|
278
365
|
// Get names of collapsed groups that have matching options
|
|
279
366
|
const get_collapsed_with_matches = () => grouped_options.flatMap(({ group, collapsed, options: opts }) => group && collapsed && opts.length > 0 ? [group] : []);
|
|
@@ -284,12 +371,12 @@ $effect(() => {
|
|
|
284
371
|
}
|
|
285
372
|
});
|
|
286
373
|
// Normalize placeholder prop (supports string or { text, persistent } object)
|
|
287
|
-
const placeholder_text = $derived(typeof placeholder === `string` ? placeholder : placeholder?.text ?? null);
|
|
374
|
+
const placeholder_text = $derived(typeof placeholder === `string` ? placeholder : (placeholder?.text ?? null));
|
|
288
375
|
const placeholder_persistent = $derived(typeof placeholder === `object` && placeholder?.persistent === true);
|
|
289
376
|
// Helper to sort selected options (used by add() and select_all())
|
|
290
377
|
function sort_selected(items) {
|
|
291
378
|
if (sortSelected === true) {
|
|
292
|
-
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)}`));
|
|
293
380
|
}
|
|
294
381
|
else if (typeof sortSelected === `function`) {
|
|
295
382
|
return items.toSorted(sortSelected);
|
|
@@ -340,9 +427,9 @@ let window_width = $state(0);
|
|
|
340
427
|
const matches_search = (opt, search) => {
|
|
341
428
|
if (filterFunc(opt, search))
|
|
342
429
|
return true;
|
|
343
|
-
if (searchMatchesGroups && search && has_group(opt)) {
|
|
430
|
+
if (searchMatchesGroups && search && utils.has_group(opt)) {
|
|
344
431
|
return fuzzy
|
|
345
|
-
? fuzzy_match(search, opt.group)
|
|
432
|
+
? utils.fuzzy_match(search, opt.group)
|
|
346
433
|
: opt.group.toLowerCase().includes(search.toLowerCase());
|
|
347
434
|
}
|
|
348
435
|
return false;
|
|
@@ -391,7 +478,8 @@ function add(option_to_add, event) {
|
|
|
391
478
|
event.stopPropagation();
|
|
392
479
|
if (maxSelect !== null && selected.length >= maxSelect)
|
|
393
480
|
wiggle = true;
|
|
394
|
-
if (!isNaN(Number(option_to_add)) &&
|
|
481
|
+
if (!isNaN(Number(option_to_add)) &&
|
|
482
|
+
typeof selected_labels[0] === `number`) {
|
|
395
483
|
option_to_add = Number(option_to_add); // convert to number if possible
|
|
396
484
|
}
|
|
397
485
|
const is_duplicate = selected_keys_set.has(key(option_to_add));
|
|
@@ -451,7 +539,7 @@ function add(option_to_add, event) {
|
|
|
451
539
|
}
|
|
452
540
|
clear_validity();
|
|
453
541
|
handle_dropdown_after_select(event);
|
|
454
|
-
last_action = { type: `add`, label: `${get_label(option_to_add)}` };
|
|
542
|
+
last_action = { type: `add`, label: `${utils.get_label(option_to_add)}` };
|
|
455
543
|
onadd?.({ option: option_to_add });
|
|
456
544
|
onchange?.({ option: option_to_add, type: `add` });
|
|
457
545
|
}
|
|
@@ -476,7 +564,7 @@ function remove(option_to_drop, event) {
|
|
|
476
564
|
}
|
|
477
565
|
selected = selected.filter((_, remove_idx) => remove_idx !== idx);
|
|
478
566
|
clear_validity();
|
|
479
|
-
last_action = { type: `remove`, label: `${get_label(option_removed)}` };
|
|
567
|
+
last_action = { type: `remove`, label: `${utils.get_label(option_removed)}` };
|
|
480
568
|
onremove?.({ option: option_removed });
|
|
481
569
|
onchange?.({ option: option_removed, type: `remove` });
|
|
482
570
|
}
|
|
@@ -506,7 +594,8 @@ function handle_dropdown_after_select(event) {
|
|
|
506
594
|
const reached_max = selected.length >= (maxSelect ?? Infinity);
|
|
507
595
|
const should_close = closeDropdownOnSelect === true ||
|
|
508
596
|
closeDropdownOnSelect === `retain-focus` ||
|
|
509
|
-
(closeDropdownOnSelect === `if-mobile` &&
|
|
597
|
+
(closeDropdownOnSelect === `if-mobile` &&
|
|
598
|
+
window_width &&
|
|
510
599
|
window_width < breakpoint);
|
|
511
600
|
if (reached_max || should_close) {
|
|
512
601
|
close_dropdown(event, closeDropdownOnSelect === `retain-focus`);
|
|
@@ -515,19 +604,24 @@ function handle_dropdown_after_select(event) {
|
|
|
515
604
|
input?.focus();
|
|
516
605
|
}
|
|
517
606
|
// Check if a user message (create option, duplicate warning, no match) is visible
|
|
518
|
-
const has_user_msg = $derived(searchText.length > 0 &&
|
|
519
|
-
(
|
|
520
|
-
|
|
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)));
|
|
521
611
|
// Handle arrow key navigation through options (uses navigable_options to skip collapsed groups)
|
|
522
612
|
async function handle_arrow_navigation(direction) {
|
|
523
613
|
ignore_hover = true;
|
|
524
614
|
// Auto-expand collapsed groups when keyboard navigating
|
|
525
|
-
if (keyboardExpandsCollapsedGroups &&
|
|
615
|
+
if (keyboardExpandsCollapsedGroups &&
|
|
616
|
+
collapsibleGroups &&
|
|
617
|
+
collapsedGroups.size > 0) {
|
|
526
618
|
expand_groups(get_collapsed_with_matches());
|
|
527
619
|
await tick();
|
|
528
620
|
}
|
|
529
621
|
// toggle user message when no options match but user can create
|
|
530
|
-
if (allowUserOptions &&
|
|
622
|
+
if (allowUserOptions &&
|
|
623
|
+
!navigable_options.length &&
|
|
624
|
+
searchText.length > 0) {
|
|
531
625
|
option_msg_is_active = !option_msg_is_active;
|
|
532
626
|
return;
|
|
533
627
|
}
|
|
@@ -550,10 +644,12 @@ async function handle_arrow_navigation(direction) {
|
|
|
550
644
|
option_msg_is_active = has_user_msg && activeIndex === navigable_options.length;
|
|
551
645
|
activeOption = option_msg_is_active
|
|
552
646
|
? null
|
|
553
|
-
: navigable_options[activeIndex] ?? null;
|
|
647
|
+
: (navigable_options[activeIndex] ?? null);
|
|
554
648
|
if (autoScroll) {
|
|
555
649
|
await tick();
|
|
556
|
-
document
|
|
650
|
+
document
|
|
651
|
+
.querySelector(`ul.options > li.active`)
|
|
652
|
+
?.scrollIntoViewIfNeeded?.();
|
|
557
653
|
}
|
|
558
654
|
// Fire onactivate for keyboard navigation only (not mouse hover)
|
|
559
655
|
onactivate?.({ option: activeOption, index: activeIndex });
|
|
@@ -587,6 +683,16 @@ async function handle_keydown(event) {
|
|
|
587
683
|
searchText = ``;
|
|
588
684
|
},
|
|
589
685
|
},
|
|
686
|
+
{
|
|
687
|
+
key: `undo`,
|
|
688
|
+
condition: () => !!canUndo,
|
|
689
|
+
action: () => undo?.(),
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
key: `redo`,
|
|
693
|
+
condition: () => !!canRedo,
|
|
694
|
+
action: () => redo?.(),
|
|
695
|
+
},
|
|
590
696
|
];
|
|
591
697
|
for (const { key, condition, action } of shortcut_actions) {
|
|
592
698
|
if (matches_shortcut(event, effective_shortcuts[key]) && condition()) {
|
|
@@ -659,7 +765,10 @@ function remove_all(event) {
|
|
|
659
765
|
// Only fire events if something was actually removed
|
|
660
766
|
if (removed_options.length > 0) {
|
|
661
767
|
searchText = ``; // always clear on remove all (resetFilterOnAdd only applies to add operations)
|
|
662
|
-
last_action = {
|
|
768
|
+
last_action = {
|
|
769
|
+
type: `removeAll`,
|
|
770
|
+
label: `${removed_options.length} options`,
|
|
771
|
+
};
|
|
663
772
|
onremoveAll?.({ options: removed_options });
|
|
664
773
|
onchange?.({ options: selected, type: `removeAll` });
|
|
665
774
|
}
|
|
@@ -742,8 +851,9 @@ function on_click_outside(event) {
|
|
|
742
851
|
if (outerDiv.contains(target))
|
|
743
852
|
return;
|
|
744
853
|
// If portal is active, also check if click is inside the portalled options dropdown
|
|
745
|
-
if (portal_params?.active && ul_options && ul_options.contains(target))
|
|
854
|
+
if (portal_params?.active && ul_options && ul_options.contains(target)) {
|
|
746
855
|
return;
|
|
856
|
+
}
|
|
747
857
|
// Click is outside both the main component and any portalled dropdown
|
|
748
858
|
close_dropdown(event);
|
|
749
859
|
}
|
|
@@ -855,8 +965,9 @@ function portal(node, params) {
|
|
|
855
965
|
target_node = params.target_node;
|
|
856
966
|
render_in_place = typeof window === `undefined` ||
|
|
857
967
|
!document.body.contains(node);
|
|
858
|
-
if (open && !render_in_place && target_node)
|
|
968
|
+
if (open && !render_in_place && target_node) {
|
|
859
969
|
tick().then(update_position);
|
|
970
|
+
}
|
|
860
971
|
else if (!open || !target_node)
|
|
861
972
|
node.hidden = true;
|
|
862
973
|
},
|
|
@@ -871,7 +982,8 @@ function portal(node, params) {
|
|
|
871
982
|
}
|
|
872
983
|
// Dynamic options loading - captures search at call time to avoid race conditions
|
|
873
984
|
async function load_dynamic_options(reset) {
|
|
874
|
-
if (!load_options_config ||
|
|
985
|
+
if (!load_options_config ||
|
|
986
|
+
load_options_loading ||
|
|
875
987
|
(!reset && !load_options_has_more)) {
|
|
876
988
|
return;
|
|
877
989
|
}
|
|
@@ -932,11 +1044,13 @@ $effect(() => {
|
|
|
932
1044
|
};
|
|
933
1045
|
});
|
|
934
1046
|
function handle_options_scroll(event) {
|
|
935
|
-
if (!load_options_config || load_options_loading || !load_options_has_more)
|
|
1047
|
+
if (!load_options_config || load_options_loading || !load_options_has_more) {
|
|
936
1048
|
return;
|
|
1049
|
+
}
|
|
937
1050
|
const { scrollTop, scrollHeight, clientHeight } = event.target;
|
|
938
|
-
if (scrollHeight - scrollTop - clientHeight <= 100)
|
|
1051
|
+
if (scrollHeight - scrollTop - clientHeight <= 100) {
|
|
939
1052
|
load_dynamic_options(false);
|
|
1053
|
+
}
|
|
940
1054
|
}
|
|
941
1055
|
</script>
|
|
942
1056
|
|
|
@@ -1000,11 +1114,9 @@ function handle_options_scroll(event) {
|
|
|
1000
1114
|
style={ulSelectedStyle}
|
|
1001
1115
|
>
|
|
1002
1116
|
{#each selected as option, idx (duplicates ? `${key(option)}-${idx}` : key(option))}
|
|
1003
|
-
{@const selectedOptionStyle =
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
) ||
|
|
1007
|
-
null}
|
|
1117
|
+
{@const selectedOptionStyle = [utils.get_style(option, `selected`), liSelectedStyle]
|
|
1118
|
+
.filter(Boolean)
|
|
1119
|
+
.join(` `) || null}
|
|
1008
1120
|
<li
|
|
1009
1121
|
class={liSelectedClass}
|
|
1010
1122
|
role="option"
|
|
@@ -1022,26 +1134,20 @@ function handle_options_scroll(event) {
|
|
|
1022
1134
|
onmouseup={(event) => event.stopPropagation()}
|
|
1023
1135
|
>
|
|
1024
1136
|
{#if selectedItem}
|
|
1025
|
-
{@render selectedItem({
|
|
1026
|
-
option,
|
|
1027
|
-
idx,
|
|
1028
|
-
})}
|
|
1137
|
+
{@render selectedItem({ option, idx })}
|
|
1029
1138
|
{:else if children}
|
|
1030
|
-
{@render children({
|
|
1031
|
-
option,
|
|
1032
|
-
idx,
|
|
1033
|
-
})}
|
|
1139
|
+
{@render children({ option, idx })}
|
|
1034
1140
|
{:else if parseLabelsAsHtml}
|
|
1035
|
-
{@html get_label(option)}
|
|
1141
|
+
{@html utils.get_label(option)}
|
|
1036
1142
|
{:else}
|
|
1037
|
-
{get_label(option)}
|
|
1143
|
+
{utils.get_label(option)}
|
|
1038
1144
|
{/if}
|
|
1039
1145
|
{#if !disabled && can_remove}
|
|
1040
1146
|
<button
|
|
1041
1147
|
onclick={(event) => remove(option, event)}
|
|
1042
1148
|
onkeydown={if_enter_or_space((event) => remove(option, event))}
|
|
1043
1149
|
type="button"
|
|
1044
|
-
title="{removeBtnTitle} {get_label(option)}"
|
|
1150
|
+
title="{removeBtnTitle} {utils.get_label(option)}"
|
|
1045
1151
|
class="remove"
|
|
1046
1152
|
>
|
|
1047
1153
|
{#if removeIcon}
|
|
@@ -1188,8 +1294,9 @@ function handle_options_scroll(event) {
|
|
|
1188
1294
|
(group_name ?? `ungrouped-${group_idx}`)
|
|
1189
1295
|
}
|
|
1190
1296
|
{#if group_name !== null}
|
|
1191
|
-
{@const { all_selected, selected_count } = group_header_state.get(
|
|
1192
|
-
|
|
1297
|
+
{@const { all_selected, selected_count } = group_header_state.get(
|
|
1298
|
+
group_name,
|
|
1299
|
+
) ?? { all_selected: false, selected_count: 0 }}
|
|
1193
1300
|
{@const handle_toggle = () =>
|
|
1194
1301
|
collapsibleGroups && toggle_group_collapsed(group_name)}
|
|
1195
1302
|
{@const handle_group_select = (event: Event) =>
|
|
@@ -1253,14 +1360,12 @@ function handle_options_scroll(event) {
|
|
|
1253
1360
|
title = null,
|
|
1254
1361
|
selectedTitle = null,
|
|
1255
1362
|
disabledTitle = defaultDisabledTitle,
|
|
1256
|
-
} = is_object(option_item) ? option_item : { label: option_item }}
|
|
1363
|
+
} = utils.is_object(option_item) ? option_item : { label: option_item }}
|
|
1257
1364
|
{@const active = activeIndex === flat_idx && flat_idx >= 0}
|
|
1258
1365
|
{@const selected = is_selected(label)}
|
|
1259
|
-
{@const optionStyle =
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
) ||
|
|
1263
|
-
null}
|
|
1366
|
+
{@const optionStyle = [utils.get_style(option_item, `option`), liOptionStyle]
|
|
1367
|
+
.filter(Boolean)
|
|
1368
|
+
.join(` `) || null}
|
|
1264
1369
|
{#if is_option_visible(flat_idx)}
|
|
1265
1370
|
<li
|
|
1266
1371
|
id="{internal_id}-opt-{flat_idx}"
|
|
@@ -1290,24 +1395,18 @@ function handle_options_scroll(event) {
|
|
|
1290
1395
|
type="checkbox"
|
|
1291
1396
|
class="option-checkbox"
|
|
1292
1397
|
checked={selected}
|
|
1293
|
-
aria-label="Toggle {get_label(option_item)}"
|
|
1398
|
+
aria-label="Toggle {utils.get_label(option_item)}"
|
|
1294
1399
|
tabindex="-1"
|
|
1295
1400
|
/>
|
|
1296
1401
|
{/if}
|
|
1297
1402
|
{#if option}
|
|
1298
|
-
{@render option({
|
|
1299
|
-
option: option_item,
|
|
1300
|
-
idx: flat_idx,
|
|
1301
|
-
})}
|
|
1403
|
+
{@render option({ option: option_item, idx: flat_idx })}
|
|
1302
1404
|
{:else if children}
|
|
1303
|
-
{@render children({
|
|
1304
|
-
option: option_item,
|
|
1305
|
-
idx: flat_idx,
|
|
1306
|
-
})}
|
|
1405
|
+
{@render children({ option: option_item, idx: flat_idx })}
|
|
1307
1406
|
{:else if parseLabelsAsHtml}
|
|
1308
|
-
{@html get_label(option_item)}
|
|
1407
|
+
{@html utils.get_label(option_item)}
|
|
1309
1408
|
{:else}
|
|
1310
|
-
{get_label(option_item)}
|
|
1409
|
+
{utils.get_label(option_item)}
|
|
1311
1410
|
{/if}
|
|
1312
1411
|
</li>
|
|
1313
1412
|
{/if}
|
|
@@ -1366,7 +1465,11 @@ function handle_options_scroll(event) {
|
|
|
1366
1465
|
{/if}
|
|
1367
1466
|
{/if}
|
|
1368
1467
|
{#if loadOptions && load_options_loading}
|
|
1369
|
-
<li
|
|
1468
|
+
<li
|
|
1469
|
+
class="loading-more"
|
|
1470
|
+
role="status"
|
|
1471
|
+
aria-label="Loading more options"
|
|
1472
|
+
>
|
|
1370
1473
|
<CircleSpinner />
|
|
1371
1474
|
</li>
|
|
1372
1475
|
{/if}
|
|
@@ -1427,7 +1530,10 @@ function handle_options_scroll(event) {
|
|
|
1427
1530
|
z-index: var(--sms-open-z-index, 4);
|
|
1428
1531
|
}
|
|
1429
1532
|
:where(div.multiselect:focus-within) {
|
|
1430
|
-
border: var(
|
|
1533
|
+
border: var(
|
|
1534
|
+
--sms-focus-border,
|
|
1535
|
+
1pt solid var(--sms-active-color, cornflowerblue)
|
|
1536
|
+
);
|
|
1431
1537
|
}
|
|
1432
1538
|
:where(div.multiselect.disabled) {
|
|
1433
1539
|
background: var(--sms-disabled-bg, light-dark(lightgray, #444));
|
|
@@ -1462,7 +1568,10 @@ function handle_options_scroll(event) {
|
|
|
1462
1568
|
:where(div.multiselect > ul.selected > li.active) {
|
|
1463
1569
|
background: var(
|
|
1464
1570
|
--sms-li-active-bg,
|
|
1465
|
-
var(
|
|
1571
|
+
var(
|
|
1572
|
+
--sms-active-color,
|
|
1573
|
+
light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.15))
|
|
1574
|
+
)
|
|
1466
1575
|
);
|
|
1467
1576
|
}
|
|
1468
1577
|
:is(div.multiselect button) {
|
|
@@ -1539,10 +1648,8 @@ function handle_options_scroll(event) {
|
|
|
1539
1648
|
width: 100%;
|
|
1540
1649
|
/* Default z-index if not portaled/overridden by portal */
|
|
1541
1650
|
z-index: var(--sms-options-z-index, 3);
|
|
1542
|
-
|
|
1543
1651
|
overflow: auto;
|
|
1544
|
-
transition: all
|
|
1545
|
-
0.2s; /* Consider if this transition is desirable with portal positioning */
|
|
1652
|
+
transition: all 0.2s; /* is this transition is desirable with portal positioning? */
|
|
1546
1653
|
box-sizing: border-box;
|
|
1547
1654
|
background: var(--sms-options-bg, light-dark(#fafafa, #1a1a1a));
|
|
1548
1655
|
max-height: var(--sms-options-max-height, 50vh);
|
|
@@ -1587,7 +1694,10 @@ function handle_options_scroll(event) {
|
|
|
1587
1694
|
:where(ul.options > li.active) {
|
|
1588
1695
|
background: var(
|
|
1589
1696
|
--sms-li-active-bg,
|
|
1590
|
-
var(
|
|
1697
|
+
var(
|
|
1698
|
+
--sms-active-color,
|
|
1699
|
+
light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.15))
|
|
1700
|
+
)
|
|
1591
1701
|
);
|
|
1592
1702
|
}
|
|
1593
1703
|
:where(ul.options > li.disabled) {
|
|
@@ -1641,7 +1751,10 @@ function handle_options_scroll(event) {
|
|
|
1641
1751
|
}
|
|
1642
1752
|
:where(ul.options > li.group-header:not(:first-child)) {
|
|
1643
1753
|
margin-top: var(--sms-group-header-margin-top, 4pt);
|
|
1644
|
-
border-top: var(
|
|
1754
|
+
border-top: var(
|
|
1755
|
+
--sms-group-header-border-top,
|
|
1756
|
+
1px solid light-dark(#eee, #333)
|
|
1757
|
+
);
|
|
1645
1758
|
}
|
|
1646
1759
|
:where(ul.options > li.group-header.collapsible) {
|
|
1647
1760
|
cursor: pointer;
|
|
@@ -2,7 +2,7 @@ import type { MultiSelectProps } from './types';
|
|
|
2
2
|
declare function $$render<Option extends import('./types').Option>(): {
|
|
3
3
|
props: MultiSelectProps<Option>;
|
|
4
4
|
exports: {};
|
|
5
|
-
bindings: "
|
|
5
|
+
bindings: "value" | "selected" | "invalid" | "open" | "activeIndex" | "activeOption" | "form_input" | "input" | "matchingOptions" | "maxSelect" | "options" | "outerDiv" | "searchText" | "collapsedGroups" | "collapseAllGroups" | "expandAllGroups" | "undo" | "redo" | "canUndo" | "canRedo";
|
|
6
6
|
slots: {};
|
|
7
7
|
events: {};
|
|
8
8
|
};
|
|
@@ -10,7 +10,7 @@ declare class __sveltets_Render<Option extends import('./types').Option> {
|
|
|
10
10
|
props(): ReturnType<typeof $$render<Option>>['props'];
|
|
11
11
|
events(): ReturnType<typeof $$render<Option>>['events'];
|
|
12
12
|
slots(): ReturnType<typeof $$render<Option>>['slots'];
|
|
13
|
-
bindings(): "
|
|
13
|
+
bindings(): "value" | "selected" | "invalid" | "open" | "activeIndex" | "activeOption" | "form_input" | "input" | "matchingOptions" | "maxSelect" | "options" | "outerDiv" | "searchText" | "collapsedGroups" | "collapseAllGroups" | "expandAllGroups" | "undo" | "redo" | "canUndo" | "canRedo";
|
|
14
14
|
exports(): {};
|
|
15
15
|
}
|
|
16
16
|
interface $$IsomorphicComponent {
|