svelte-multiselect 11.2.2 → 11.2.4
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 +15 -11
- package/dist/CmdPalette.svelte.d.ts +4 -2
- package/dist/CopyButton.svelte +27 -6
- package/dist/CopyButton.svelte.d.ts +3 -3
- package/dist/Icon.svelte.d.ts +2 -2
- package/dist/MultiSelect.svelte +150 -63
- package/dist/MultiSelect.svelte.d.ts +11 -4
- package/dist/PrevNext.svelte +9 -8
- package/dist/PrevNext.svelte.d.ts +32 -44
- package/dist/Toggle.svelte +2 -1
- package/dist/Toggle.svelte.d.ts +2 -2
- package/dist/attachments.d.ts +9 -6
- package/dist/attachments.js +187 -43
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +5 -20
- package/dist/utils.d.ts +3 -8
- package/dist/utils.js +24 -48
- package/package.json +17 -17
- package/readme.md +5 -5
- package/dist/RadioButtons.svelte +0 -67
- package/dist/RadioButtons.svelte.d.ts +0 -44
package/dist/CmdPalette.svelte
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
2
|
-
import
|
|
1
|
+
<script lang="ts">import { fade } from 'svelte/transition';
|
|
2
|
+
import MultiSelect from './MultiSelect.svelte';
|
|
3
3
|
let { actions, triggers = [`k`], close_keys = [`Escape`], fade_duration = 200, dialog_style = ``, open = $bindable(false), dialog = $bindable(null), input = $bindable(null), placeholder = `Filter actions...`, ...rest } = $props();
|
|
4
4
|
$effect(() => {
|
|
5
|
-
if (open && input && document.activeElement !== input)
|
|
5
|
+
if (open && input && document.activeElement !== input)
|
|
6
6
|
input.focus();
|
|
7
|
-
}
|
|
8
7
|
});
|
|
9
8
|
async function toggle(event) {
|
|
10
|
-
|
|
9
|
+
const is_trigger = triggers.includes(event.key) &&
|
|
10
|
+
(event.metaKey || event.ctrlKey);
|
|
11
|
+
if (is_trigger && !open)
|
|
11
12
|
open = true;
|
|
12
|
-
|
|
13
|
-
else if (close_keys.includes(event.key) && open) {
|
|
13
|
+
else if (close_keys.includes(event.key) && open)
|
|
14
14
|
open = false;
|
|
15
|
-
}
|
|
16
15
|
}
|
|
17
16
|
function close_if_outside(event) {
|
|
18
|
-
|
|
17
|
+
const target = event.target;
|
|
18
|
+
if (!target || !(target instanceof HTMLElement))
|
|
19
|
+
return;
|
|
20
|
+
if (open && !dialog?.contains(target) && !target.closest(`ul.options`)) {
|
|
19
21
|
open = false;
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
|
-
function trigger_action_and_close(
|
|
23
|
-
const { action, label } =
|
|
24
|
+
function trigger_action_and_close({ option }) {
|
|
25
|
+
const { action, label } = (option ?? {});
|
|
26
|
+
if (!action)
|
|
27
|
+
return;
|
|
24
28
|
action(label);
|
|
25
29
|
open = false;
|
|
26
30
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ComponentProps } from 'svelte';
|
|
2
|
+
import MultiSelect from './MultiSelect.svelte';
|
|
3
|
+
import type { ObjectOption } from './types';
|
|
2
4
|
interface Action extends ObjectOption {
|
|
3
5
|
label: string;
|
|
4
6
|
action: (label: string) => void;
|
|
5
7
|
}
|
|
6
|
-
interface Props extends Omit<
|
|
8
|
+
interface Props extends Omit<ComponentProps<typeof MultiSelect<Action>>, `options`> {
|
|
7
9
|
actions: Action[];
|
|
8
10
|
triggers?: string[];
|
|
9
11
|
close_keys?: string[];
|
package/dist/CopyButton.svelte
CHANGED
|
@@ -9,15 +9,23 @@ $effect(() => {
|
|
|
9
9
|
if (!global && !global_selector)
|
|
10
10
|
return;
|
|
11
11
|
const apply_copy_buttons = () => {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
: `position: absolute; top: 9pt; right: 9pt;`;
|
|
12
|
+
const btn_style = `position: absolute; top: 9pt; right: 9pt; ${rest.style ?? ``}`;
|
|
13
|
+
const skip_sel = skip_selector ?? as;
|
|
15
14
|
for (const code of document.querySelectorAll(global_selector ?? `pre > code`)) {
|
|
16
15
|
const pre = code.parentElement;
|
|
17
|
-
|
|
16
|
+
const content = code.textContent ?? ``;
|
|
17
|
+
if (pre && !pre.querySelector(`[data-sms-copy]`) &&
|
|
18
|
+
!(skip_sel && pre.querySelector(skip_sel))) {
|
|
18
19
|
mount(CopyButton, {
|
|
19
20
|
target: pre,
|
|
20
|
-
props: {
|
|
21
|
+
props: {
|
|
22
|
+
content,
|
|
23
|
+
as,
|
|
24
|
+
labels,
|
|
25
|
+
...rest,
|
|
26
|
+
style: btn_style,
|
|
27
|
+
'data-sms-copy': ``,
|
|
28
|
+
},
|
|
21
29
|
});
|
|
22
30
|
}
|
|
23
31
|
}
|
|
@@ -42,7 +50,20 @@ async function copy() {
|
|
|
42
50
|
|
|
43
51
|
{#if !(global || global_selector)}
|
|
44
52
|
{@const { text, icon } = labels[state]}
|
|
45
|
-
<svelte:element
|
|
53
|
+
<svelte:element
|
|
54
|
+
this={as}
|
|
55
|
+
onclick={copy}
|
|
56
|
+
onkeydown={(event) => {
|
|
57
|
+
if (event.key === `Enter` || event.key === ` `) {
|
|
58
|
+
event.preventDefault()
|
|
59
|
+
copy()
|
|
60
|
+
}
|
|
61
|
+
}}
|
|
62
|
+
role="button"
|
|
63
|
+
tabindex={0}
|
|
64
|
+
data-sms-copy=""
|
|
65
|
+
{...rest}
|
|
66
|
+
>
|
|
46
67
|
{#if children}
|
|
47
68
|
{@render children({ state, icon, text })}
|
|
48
69
|
{:else}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { CopyButton } from './';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
4
|
import type { IconName } from './icons';
|
|
4
5
|
type State = `ready` | `success` | `error`;
|
|
5
|
-
interface Props {
|
|
6
|
+
interface Props extends Omit<HTMLAttributes<HTMLButtonElement>, `children`> {
|
|
6
7
|
content?: string;
|
|
7
8
|
state?: State;
|
|
8
9
|
global_selector?: string | null;
|
|
9
|
-
global?: boolean
|
|
10
|
+
global?: boolean;
|
|
10
11
|
skip_selector?: string | null;
|
|
11
12
|
as?: string;
|
|
12
13
|
labels?: Record<State, {
|
|
@@ -18,7 +19,6 @@ interface Props {
|
|
|
18
19
|
icon: IconName;
|
|
19
20
|
text: string;
|
|
20
21
|
}]>;
|
|
21
|
-
[key: string]: unknown;
|
|
22
22
|
}
|
|
23
23
|
declare const CopyButton: import("svelte").Component<Props, {}, "state">;
|
|
24
24
|
type CopyButton = ReturnType<typeof CopyButton>;
|
package/dist/Icon.svelte.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
1
2
|
import { type IconName } from './icons';
|
|
2
|
-
interface Props {
|
|
3
|
+
interface Props extends HTMLAttributes<SVGSVGElement> {
|
|
3
4
|
icon: IconName;
|
|
4
|
-
[key: string]: unknown;
|
|
5
5
|
}
|
|
6
6
|
declare const Icon: import("svelte").Component<Props, {}, "">;
|
|
7
7
|
type Icon = ReturnType<typeof Icon>;
|
package/dist/MultiSelect.svelte
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
2
|
-
import { tick } from 'svelte';
|
|
1
|
+
<script lang="ts">import { tick } from 'svelte';
|
|
3
2
|
import { flip } from 'svelte/animate';
|
|
4
|
-
import {
|
|
5
|
-
|
|
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) => {
|
|
6
9
|
if (!searchText)
|
|
7
10
|
return true;
|
|
8
|
-
|
|
9
|
-
|
|
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(``), selected = $bindable(options
|
|
10
16
|
?.filter((opt) => opt instanceof Object && opt?.preselected)
|
|
11
17
|
.slice(0, maxSelect ?? undefined) ?? []), sortSelected = false, selectedOptionsDraggable = !sortSelected, style = null, ulOptionsClass = ``, ulSelectedClass = ``, ulSelectedStyle = null, ulOptionsStyle = null, value = $bindable(null), expandIcon, selectedItem, children, removeIcon, afterInput, spinner, disabledIcon, option, userMsg, onblur, onclick, onfocus, onkeydown, onkeyup, onmousedown, onmouseenter, onmouseleave, ontouchcancel, ontouchend, ontouchmove, ontouchstart, onadd, oncreate, onremove, onremoveAll, onchange, onopen, onclose, portal: portal_params = {}, ...rest } = $props();
|
|
12
18
|
$effect.pre(() => {
|
|
@@ -62,7 +68,9 @@ let window_width = $state(0);
|
|
|
62
68
|
$effect.pre(() => {
|
|
63
69
|
matchingOptions = options.filter((opt) => filterFunc(opt, searchText) &&
|
|
64
70
|
// remove already selected options from dropdown list unless duplicate selections are allowed
|
|
65
|
-
|
|
71
|
+
// or keepSelectedInDropdown is enabled
|
|
72
|
+
(!selected.map(key).includes(key(opt)) || duplicates ||
|
|
73
|
+
keepSelectedInDropdown));
|
|
66
74
|
});
|
|
67
75
|
// raise if matchingOptions[activeIndex] does not yield a value
|
|
68
76
|
if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
@@ -72,6 +80,17 @@ if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
|
72
80
|
$effect(() => {
|
|
73
81
|
activeOption = matchingOptions[activeIndex ?? -1] ?? null;
|
|
74
82
|
});
|
|
83
|
+
// toggle an option between selected and unselected states (for keepSelectedInDropdown mode)
|
|
84
|
+
function toggle_option(option_to_toggle, event) {
|
|
85
|
+
const is_currently_selected = selected.map(key).includes(key(option_to_toggle));
|
|
86
|
+
if (is_currently_selected) {
|
|
87
|
+
if (minSelect === null || selected.length > minSelect) { // Only remove if it wouldn't violate minSelect
|
|
88
|
+
remove(option_to_toggle, event);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else
|
|
92
|
+
add(option_to_toggle, event);
|
|
93
|
+
}
|
|
75
94
|
// add an option to selected list
|
|
76
95
|
function add(option_to_add, event) {
|
|
77
96
|
event.stopPropagation();
|
|
@@ -115,10 +134,9 @@ function add(option_to_add, event) {
|
|
|
115
134
|
console.error(`MultiSelect: encountered falsy option ${option_to_add}`);
|
|
116
135
|
return;
|
|
117
136
|
}
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
// for maxSelect = 1 we always replace current option with new one
|
|
138
|
+
if (maxSelect === 1)
|
|
120
139
|
selected = [option_to_add];
|
|
121
|
-
}
|
|
122
140
|
else {
|
|
123
141
|
selected = [...selected, option_to_add];
|
|
124
142
|
if (sortSelected === true) {
|
|
@@ -132,7 +150,7 @@ function add(option_to_add, event) {
|
|
|
132
150
|
selected = selected.sort(sortSelected);
|
|
133
151
|
}
|
|
134
152
|
}
|
|
135
|
-
const reached_max_select = selected.length
|
|
153
|
+
const reached_max_select = selected.length >= (maxSelect ?? Infinity);
|
|
136
154
|
const dropdown_should_close = closeDropdownOnSelect === true ||
|
|
137
155
|
closeDropdownOnSelect === `retain-focus` ||
|
|
138
156
|
(closeDropdownOnSelect === `if-mobile` && window_width &&
|
|
@@ -141,9 +159,8 @@ function add(option_to_add, event) {
|
|
|
141
159
|
if (reached_max_select || dropdown_should_close) {
|
|
142
160
|
close_dropdown(event, should_retain_focus);
|
|
143
161
|
}
|
|
144
|
-
else if (!dropdown_should_close)
|
|
162
|
+
else if (!dropdown_should_close)
|
|
145
163
|
input?.focus();
|
|
146
|
-
}
|
|
147
164
|
onadd?.({ option: option_to_add });
|
|
148
165
|
onchange?.({ option: option_to_add, type: `add` });
|
|
149
166
|
invalid = false; // reset error status whenever new items are selected
|
|
@@ -203,8 +220,12 @@ async function handle_keydown(event) {
|
|
|
203
220
|
event.stopPropagation();
|
|
204
221
|
event.preventDefault(); // prevent enter key from triggering form submission
|
|
205
222
|
if (activeOption) {
|
|
206
|
-
if (selected.includes(activeOption))
|
|
207
|
-
remove
|
|
223
|
+
if (selected.includes(activeOption)) {
|
|
224
|
+
// Only remove if it wouldn't violate minSelect
|
|
225
|
+
if (minSelect === null || selected.length > minSelect) {
|
|
226
|
+
remove(activeOption, event);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
208
229
|
else
|
|
209
230
|
add(activeOption, event);
|
|
210
231
|
searchText = ``;
|
|
@@ -242,11 +263,25 @@ async function handle_keydown(event) {
|
|
|
242
263
|
// if none of the above special cases apply, we make next/prev option
|
|
243
264
|
// active with wrap around at both ends
|
|
244
265
|
const increment = event.key === `ArrowUp` ? -1 : 1;
|
|
245
|
-
|
|
266
|
+
// Include user message in total count if it exists
|
|
267
|
+
const has_user_msg = searchText && ((allowUserOptions && createOptionMsg) ||
|
|
268
|
+
(!duplicates && selected.map(get_label).includes(searchText)) ||
|
|
269
|
+
(matchingOptions.length === 0 && noMatchingOptionsMsg));
|
|
270
|
+
const total_items = matchingOptions.length + (has_user_msg ? 1 : 0);
|
|
271
|
+
activeIndex = (activeIndex + increment) % total_items;
|
|
246
272
|
// in JS % behaves like remainder operator, not real modulo, so negative numbers stay negative
|
|
247
273
|
// need to do manual wrap around at 0
|
|
248
274
|
if (activeIndex < 0)
|
|
249
|
-
activeIndex =
|
|
275
|
+
activeIndex = total_items - 1;
|
|
276
|
+
// Handle user message activation
|
|
277
|
+
if (has_user_msg && activeIndex === matchingOptions.length) {
|
|
278
|
+
option_msg_is_active = true;
|
|
279
|
+
activeOption = null;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
option_msg_is_active = false;
|
|
283
|
+
activeOption = matchingOptions[activeIndex] ?? null;
|
|
284
|
+
}
|
|
250
285
|
if (autoScroll) {
|
|
251
286
|
await tick();
|
|
252
287
|
const li = document.querySelector(`ul.options > li.active`);
|
|
@@ -256,8 +291,13 @@ async function handle_keydown(event) {
|
|
|
256
291
|
} // on backspace key: remove last selected option
|
|
257
292
|
else if (event.key === `Backspace` && selected.length > 0 && !searchText) {
|
|
258
293
|
event.stopPropagation();
|
|
294
|
+
// Only remove option if it wouldn't violate minSelect
|
|
295
|
+
if (minSelect === null || selected.length > minSelect) {
|
|
296
|
+
const last_option = selected.at(-1);
|
|
297
|
+
if (last_option)
|
|
298
|
+
remove(last_option, event);
|
|
299
|
+
}
|
|
259
300
|
// Don't prevent default, allow normal backspace behavior if not removing
|
|
260
|
-
remove(selected.at(-1), event);
|
|
261
301
|
} // make first matching option active on any keypress (if none of the above special cases match)
|
|
262
302
|
else if (matchingOptions.length > 0 && activeIndex === null) {
|
|
263
303
|
// Don't stop propagation or prevent default here, allow normal character input
|
|
@@ -266,11 +306,23 @@ async function handle_keydown(event) {
|
|
|
266
306
|
}
|
|
267
307
|
function remove_all(event) {
|
|
268
308
|
event.stopPropagation();
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
309
|
+
// Keep the first minSelect items, remove the rest
|
|
310
|
+
let removed_options = [];
|
|
311
|
+
if (minSelect === null) {
|
|
312
|
+
// If no minSelect constraint, remove all
|
|
313
|
+
removed_options = selected;
|
|
314
|
+
selected = [];
|
|
315
|
+
searchText = ``;
|
|
316
|
+
}
|
|
317
|
+
else if (selected.length > minSelect) {
|
|
318
|
+
// Keep the first minSelect items
|
|
319
|
+
removed_options = selected.slice(minSelect);
|
|
320
|
+
selected = selected.slice(0, minSelect);
|
|
321
|
+
searchText = ``;
|
|
322
|
+
}
|
|
323
|
+
onremoveAll?.({ options: removed_options });
|
|
273
324
|
onchange?.({ options: selected, type: `removeAll` });
|
|
325
|
+
// If selected.length <= minSelect, do nothing (can't remove any more)
|
|
274
326
|
}
|
|
275
327
|
let is_selected = $derived((label) => selected.map(get_label).includes(label));
|
|
276
328
|
const if_enter_or_space = (handler) => (event) => {
|
|
@@ -280,9 +332,17 @@ const if_enter_or_space = (handler) => (event) => {
|
|
|
280
332
|
}
|
|
281
333
|
};
|
|
282
334
|
function on_click_outside(event) {
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
335
|
+
if (!outerDiv)
|
|
336
|
+
return;
|
|
337
|
+
const target = event.target;
|
|
338
|
+
// Check if click is inside the main component
|
|
339
|
+
if (outerDiv.contains(target))
|
|
340
|
+
return;
|
|
341
|
+
// If portal is active, also check if click is inside the portalled options dropdown
|
|
342
|
+
if (portal_params?.active && ul_options && ul_options.contains(target))
|
|
343
|
+
return;
|
|
344
|
+
// Click is outside both the main component and any portalled dropdown
|
|
345
|
+
close_dropdown(event);
|
|
286
346
|
}
|
|
287
347
|
let drag_idx = $state(null);
|
|
288
348
|
// event handlers enable dragging to reorder selected options
|
|
@@ -312,16 +372,6 @@ const dragstart = (idx) => (event) => {
|
|
|
312
372
|
event.dataTransfer.setData(`text/plain`, `${idx}`);
|
|
313
373
|
};
|
|
314
374
|
let ul_options = $state();
|
|
315
|
-
// highlight text matching user-entered search text in available options
|
|
316
|
-
function highlight_matching_options(event) {
|
|
317
|
-
if (!highlightMatches || !ul_options)
|
|
318
|
-
return;
|
|
319
|
-
// get input's search query
|
|
320
|
-
const query = event?.target?.value.trim().toLowerCase();
|
|
321
|
-
if (!query)
|
|
322
|
-
return;
|
|
323
|
-
highlight_matching_nodes(ul_options, query, noMatchingOptionsMsg);
|
|
324
|
-
}
|
|
325
375
|
const handle_input_keydown = (event) => {
|
|
326
376
|
handle_keydown(event); // Restore internal logic
|
|
327
377
|
// Call original forwarded handler
|
|
@@ -351,6 +401,7 @@ $effect(() => {
|
|
|
351
401
|
const handle_input_blur = (event) => {
|
|
352
402
|
// For portalled dropdowns, don't close on blur since clicks on portalled elements
|
|
353
403
|
// will cause blur but we want to allow the click to register first
|
|
404
|
+
// (otherwise mobile touch event is unable to select options https://github.com/janosh/svelte-multiselect/issues/335)
|
|
354
405
|
if (portal_params?.active) {
|
|
355
406
|
onblur?.(event); // Let the click handler manage closing for portalled dropdowns
|
|
356
407
|
return;
|
|
@@ -388,7 +439,7 @@ function portal(node, params) {
|
|
|
388
439
|
tick().then(update_position);
|
|
389
440
|
window.addEventListener(`scroll`, update_position, true);
|
|
390
441
|
window.addEventListener(`resize`, update_position);
|
|
391
|
-
$effect
|
|
442
|
+
$effect(() => {
|
|
392
443
|
if (open && target_node)
|
|
393
444
|
update_position();
|
|
394
445
|
else
|
|
@@ -464,8 +515,7 @@ function portal(node, params) {
|
|
|
464
515
|
{:else}
|
|
465
516
|
<Icon
|
|
466
517
|
icon="ChevronExpand"
|
|
467
|
-
|
|
468
|
-
style="min-width: 1em; padding: 0 1pt; cursor: pointer"
|
|
518
|
+
style="width: 15px; min-width: 1em; padding: 0 1pt; cursor: pointer"
|
|
469
519
|
/>
|
|
470
520
|
{/if}
|
|
471
521
|
<ul
|
|
@@ -520,7 +570,7 @@ function portal(node, params) {
|
|
|
520
570
|
{#if removeIcon}
|
|
521
571
|
{@render removeIcon()}
|
|
522
572
|
{:else}
|
|
523
|
-
<Icon icon="Cross"
|
|
573
|
+
<Icon icon="Cross" style="width: 15px" />
|
|
524
574
|
{/if}
|
|
525
575
|
</button>
|
|
526
576
|
{/if}
|
|
@@ -536,13 +586,12 @@ function portal(node, params) {
|
|
|
536
586
|
{autocomplete}
|
|
537
587
|
{inputmode}
|
|
538
588
|
{pattern}
|
|
539
|
-
placeholder={selected.length
|
|
589
|
+
placeholder={selected.length === 0 ? placeholder : null}
|
|
540
590
|
aria-invalid={invalid ? `true` : null}
|
|
541
591
|
ondrop={() => false}
|
|
542
592
|
onmouseup={open_dropdown}
|
|
543
593
|
onkeydown={handle_input_keydown}
|
|
544
594
|
onfocus={handle_input_focus}
|
|
545
|
-
oninput={highlight_matching_options}
|
|
546
595
|
onblur={handle_input_blur}
|
|
547
596
|
{onclick}
|
|
548
597
|
{onkeyup}
|
|
@@ -578,9 +627,9 @@ function portal(node, params) {
|
|
|
578
627
|
{:else}
|
|
579
628
|
<Icon
|
|
580
629
|
icon="Disabled"
|
|
581
|
-
|
|
582
|
-
style="margin: 0 2pt"
|
|
630
|
+
style="width: 14pt; margin: 0 2pt"
|
|
583
631
|
data-name="disabled-icon"
|
|
632
|
+
aria-disabled="true"
|
|
584
633
|
/>
|
|
585
634
|
{/if}
|
|
586
635
|
{:else if selected.length > 0}
|
|
@@ -602,7 +651,7 @@ function portal(node, params) {
|
|
|
602
651
|
{#if removeIcon}
|
|
603
652
|
{@render removeIcon()}
|
|
604
653
|
{:else}
|
|
605
|
-
<Icon icon="Cross"
|
|
654
|
+
<Icon icon="Cross" style="width: 15px" />
|
|
606
655
|
{/if}
|
|
607
656
|
</button>
|
|
608
657
|
{/if}
|
|
@@ -612,6 +661,17 @@ function portal(node, params) {
|
|
|
612
661
|
{#if (searchText && noMatchingOptionsMsg) || options?.length > 0}
|
|
613
662
|
<ul
|
|
614
663
|
use:portal={{ target_node: outerDiv, ...portal_params }}
|
|
664
|
+
{@attach highlight_matches({
|
|
665
|
+
query: searchText,
|
|
666
|
+
disabled: !highlightMatches,
|
|
667
|
+
fuzzy,
|
|
668
|
+
css_class: `sms-search-matches`,
|
|
669
|
+
// don't highlight text in the "Create this option..." message
|
|
670
|
+
node_filter: (node) =>
|
|
671
|
+
node?.parentElement?.closest(`li.user-msg`)
|
|
672
|
+
? NodeFilter.FILTER_REJECT
|
|
673
|
+
: NodeFilter.FILTER_ACCEPT,
|
|
674
|
+
})}
|
|
615
675
|
class:hidden={!open}
|
|
616
676
|
class="options {ulOptionsClass}"
|
|
617
677
|
role="listbox"
|
|
@@ -621,10 +681,13 @@ function portal(node, params) {
|
|
|
621
681
|
bind:this={ul_options}
|
|
622
682
|
style={ulOptionsStyle}
|
|
623
683
|
>
|
|
624
|
-
{#each matchingOptions.slice(
|
|
625
|
-
|
|
684
|
+
{#each matchingOptions.slice(
|
|
685
|
+
0,
|
|
686
|
+
maxOptions == null ? Infinity : Math.max(0, maxOptions),
|
|
687
|
+
) as
|
|
688
|
+
option_item,
|
|
626
689
|
idx
|
|
627
|
-
(duplicates ? [key(
|
|
690
|
+
(duplicates ? [key(option_item), idx] : key(option_item))
|
|
628
691
|
}
|
|
629
692
|
{@const {
|
|
630
693
|
label,
|
|
@@ -632,19 +695,22 @@ function portal(node, params) {
|
|
|
632
695
|
title = null,
|
|
633
696
|
selectedTitle = null,
|
|
634
697
|
disabledTitle = defaultDisabledTitle,
|
|
635
|
-
} =
|
|
698
|
+
} = option_item instanceof Object ? option_item : { label: option_item }}
|
|
636
699
|
{@const active = activeIndex === idx}
|
|
700
|
+
{@const selected = is_selected(label)}
|
|
637
701
|
{@const optionStyle =
|
|
638
|
-
[get_style(
|
|
702
|
+
[get_style(option_item, `option`), liOptionStyle].filter(Boolean).join(
|
|
639
703
|
` `,
|
|
640
704
|
) ||
|
|
641
705
|
null}
|
|
642
706
|
<li
|
|
643
707
|
onclick={(event) => {
|
|
644
|
-
if (
|
|
708
|
+
if (disabled) return
|
|
709
|
+
if (keepSelectedInDropdown) toggle_option(option_item, event)
|
|
710
|
+
else add(option_item, event)
|
|
645
711
|
}}
|
|
646
|
-
title={disabled ? disabledTitle : (
|
|
647
|
-
class:selected
|
|
712
|
+
title={disabled ? disabledTitle : (selected && selectedTitle) || title}
|
|
713
|
+
class:selected
|
|
648
714
|
class:active
|
|
649
715
|
class:disabled
|
|
650
716
|
class="{liOptionClass} {active ? liActiveOptionClass : ``}"
|
|
@@ -655,29 +721,39 @@ function portal(node, params) {
|
|
|
655
721
|
if (!disabled) activeIndex = idx
|
|
656
722
|
}}
|
|
657
723
|
role="option"
|
|
658
|
-
aria-selected=
|
|
724
|
+
aria-selected={selected ? `true` : `false`}
|
|
659
725
|
style={optionStyle}
|
|
660
726
|
onkeydown={(event) => {
|
|
661
727
|
if (!disabled && (event.key === `Enter` || event.code === `Space`)) {
|
|
662
728
|
event.preventDefault()
|
|
663
|
-
|
|
729
|
+
if (keepSelectedInDropdown) toggle_option(option_item, event)
|
|
730
|
+
else add(option_item, event)
|
|
664
731
|
}
|
|
665
732
|
}}
|
|
666
733
|
>
|
|
734
|
+
{#if keepSelectedInDropdown === `checkboxes`}
|
|
735
|
+
<input
|
|
736
|
+
type="checkbox"
|
|
737
|
+
class="option-checkbox"
|
|
738
|
+
checked={selected}
|
|
739
|
+
aria-label="Toggle {get_label(option_item)}"
|
|
740
|
+
tabindex="-1"
|
|
741
|
+
/>
|
|
742
|
+
{/if}
|
|
667
743
|
{#if option}
|
|
668
744
|
{@render option({
|
|
669
|
-
option:
|
|
745
|
+
option: option_item,
|
|
670
746
|
idx,
|
|
671
747
|
})}
|
|
672
748
|
{:else if children}
|
|
673
749
|
{@render children({
|
|
674
|
-
option:
|
|
750
|
+
option: option_item,
|
|
675
751
|
idx,
|
|
676
752
|
})}
|
|
677
753
|
{:else if parseLabelsAsHtml}
|
|
678
|
-
{@html get_label(
|
|
754
|
+
{@html get_label(option_item)}
|
|
679
755
|
{:else}
|
|
680
|
-
{get_label(
|
|
756
|
+
{get_label(option_item)}
|
|
681
757
|
{/if}
|
|
682
758
|
</li>
|
|
683
759
|
{/each}
|
|
@@ -685,7 +761,7 @@ function portal(node, params) {
|
|
|
685
761
|
{@const text_input_is_duplicate = selected.map(get_label).includes(searchText)}
|
|
686
762
|
{@const is_dupe = !duplicates && text_input_is_duplicate && `dupe`}
|
|
687
763
|
{@const can_create = Boolean(allowUserOptions && createOptionMsg) && `create`}
|
|
688
|
-
{@const no_match = Boolean(matchingOptions?.length
|
|
764
|
+
{@const no_match = Boolean(matchingOptions?.length === 0 && noMatchingOptionsMsg) &&
|
|
689
765
|
`no-match`}
|
|
690
766
|
{@const msgType = is_dupe || can_create || no_match}
|
|
691
767
|
{#if msgType}
|
|
@@ -895,11 +971,13 @@ function portal(node, params) {
|
|
|
895
971
|
visibility: hidden;
|
|
896
972
|
opacity: 0;
|
|
897
973
|
transform: translateY(50px);
|
|
974
|
+
pointer-events: none;
|
|
898
975
|
}
|
|
899
976
|
ul.options > li {
|
|
900
|
-
padding: 3pt
|
|
977
|
+
padding: 3pt 1ex;
|
|
901
978
|
cursor: pointer;
|
|
902
979
|
scroll-margin: var(--sms-options-scroll-margin, 100px);
|
|
980
|
+
border-left: 3px solid transparent;
|
|
903
981
|
}
|
|
904
982
|
ul.options .user-msg {
|
|
905
983
|
/* block needed so vertical padding applies to span */
|
|
@@ -907,8 +985,11 @@ function portal(node, params) {
|
|
|
907
985
|
padding: 3pt 2ex;
|
|
908
986
|
}
|
|
909
987
|
ul.options > li.selected {
|
|
910
|
-
background: var(--sms-li-selected-bg);
|
|
911
|
-
|
|
988
|
+
background: var(--sms-li-selected-plain-bg, rgba(0, 123, 255, 0.1));
|
|
989
|
+
border-left: var(
|
|
990
|
+
--sms-li-selected-plain-border,
|
|
991
|
+
3px solid var(--sms-active-color, cornflowerblue)
|
|
992
|
+
);
|
|
912
993
|
}
|
|
913
994
|
ul.options > li.active {
|
|
914
995
|
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
|
@@ -918,7 +999,13 @@ function portal(node, params) {
|
|
|
918
999
|
background: var(--sms-li-disabled-bg, #f5f5f6);
|
|
919
1000
|
color: var(--sms-li-disabled-text, #b8b8b8);
|
|
920
1001
|
}
|
|
921
|
-
|
|
1002
|
+
/* Checkbox styling for keepSelectedInDropdown='checkboxes' mode */
|
|
1003
|
+
ul.options > li > input.option-checkbox {
|
|
1004
|
+
width: 16px;
|
|
1005
|
+
height: 16px;
|
|
1006
|
+
margin-right: 6px;
|
|
1007
|
+
accent-color: var(--sms-active-color, cornflowerblue);
|
|
1008
|
+
}
|
|
922
1009
|
:is(span.max-select-msg) {
|
|
923
1010
|
padding: 0 3pt;
|
|
924
1011
|
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { MultiSelectProps, Option as T } from './types';
|
|
2
|
+
declare function $$render<Option extends T>(): {
|
|
3
|
+
props: MultiSelectProps;
|
|
4
|
+
exports: {};
|
|
5
|
+
bindings: "input" | "invalid" | "value" | "selected" | "open" | "activeIndex" | "activeOption" | "form_input" | "matchingOptions" | "options" | "outerDiv" | "searchText";
|
|
6
|
+
slots: {};
|
|
7
|
+
events: {};
|
|
8
|
+
};
|
|
2
9
|
declare class __sveltets_Render<Option extends T> {
|
|
3
|
-
props():
|
|
4
|
-
events():
|
|
5
|
-
slots():
|
|
6
|
-
bindings(): "input" | "invalid" | "
|
|
10
|
+
props(): ReturnType<typeof $$render<Option>>['props'];
|
|
11
|
+
events(): ReturnType<typeof $$render<Option>>['events'];
|
|
12
|
+
slots(): ReturnType<typeof $$render<Option>>['slots'];
|
|
13
|
+
bindings(): "input" | "invalid" | "value" | "selected" | "open" | "activeIndex" | "activeOption" | "form_input" | "matchingOptions" | "options" | "outerDiv" | "searchText";
|
|
7
14
|
exports(): {};
|
|
8
15
|
}
|
|
9
16
|
interface $$IsomorphicComponent {
|
package/dist/PrevNext.svelte
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
<script lang="ts">let { items = [], node = `nav`, current = ``, log = `errors`, nav_options = { replace_state: true, no_scroll: true }, titles = { prev: `← Previous`, next: `Next →` }, onkeyup = ({ prev, next }) => ({
|
|
2
|
-
ArrowLeft: prev[0],
|
|
3
|
-
ArrowRight: next[0],
|
|
4
|
-
}), prev_snippet, children, between, next_snippet, ...rest } = $props();
|
|
1
|
+
<script lang="ts">let { items = [], node = `nav`, current = ``, log = `errors`, nav_options = { replace_state: true, no_scroll: true }, titles = { prev: `← Previous`, next: `Next →` }, onkeyup = ({ prev, next }) => ({ ArrowLeft: prev[0], ArrowRight: next[0] }), prev_snippet, children, between, next_snippet, min_items = 3, ...rest } = $props();
|
|
5
2
|
// Convert items to consistent [key, value] format
|
|
6
3
|
let items_arr = $derived((items ?? []).map((itm) => (typeof itm === `string` ? [itm, itm] : itm)));
|
|
7
4
|
// Calculate prev/next items with wraparound
|
|
@@ -11,8 +8,8 @@ let next = $derived(items_arr[idx + 1] ?? items_arr[0]);
|
|
|
11
8
|
// Validation and logging
|
|
12
9
|
$effect.pre(() => {
|
|
13
10
|
if (log !== `silent`) {
|
|
14
|
-
if (items_arr.length <
|
|
15
|
-
console.warn(`PrevNext received ${items_arr.length} items - minimum of
|
|
11
|
+
if (items_arr.length < min_items && log === `verbose`) {
|
|
12
|
+
console.warn(`PrevNext received ${items_arr.length} items - minimum of ${min_items} expected`);
|
|
16
13
|
}
|
|
17
14
|
if (idx < 0 && log === `errors`) {
|
|
18
15
|
const valid = items_arr.map(([key]) => key);
|
|
@@ -23,9 +20,13 @@ $effect.pre(() => {
|
|
|
23
20
|
function handle_keyup(event) {
|
|
24
21
|
if (!onkeyup)
|
|
25
22
|
return;
|
|
23
|
+
if ((items_arr?.length ?? 0) < min_items)
|
|
24
|
+
return;
|
|
25
|
+
if (!prev || !next)
|
|
26
|
+
return;
|
|
26
27
|
const key_map = onkeyup({ prev, next });
|
|
27
28
|
const to = key_map[event.key];
|
|
28
|
-
if (to) {
|
|
29
|
+
if (to !== undefined) {
|
|
29
30
|
const { replace_state, no_scroll } = nav_options;
|
|
30
31
|
const [scroll_x, scroll_y] = no_scroll
|
|
31
32
|
? [window.scrollX, window.scrollY]
|
|
@@ -41,7 +42,7 @@ export {};
|
|
|
41
42
|
|
|
42
43
|
<svelte:window onkeyup={handle_keyup} />
|
|
43
44
|
|
|
44
|
-
{#if items_arr.length
|
|
45
|
+
{#if items_arr.length >= min_items}
|
|
45
46
|
<svelte:element this={node} class="prev-next" {...rest}>
|
|
46
47
|
<!-- ensures `prev` is a defined [key, value] tuple.
|
|
47
48
|
Due to prior normalization of the `items` prop, any defined `prev` item
|