svelte-multiselect 11.2.3 → 11.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CircleSpinner.svelte.d.ts +3 -3
- package/dist/CmdPalette.svelte +20 -13
- package/dist/CmdPalette.svelte.d.ts +61 -14
- package/dist/CodeExample.svelte +10 -3
- package/dist/CodeExample.svelte.d.ts +6 -3
- package/dist/CopyButton.svelte +26 -4
- package/dist/CopyButton.svelte.d.ts +4 -4
- package/dist/FileDetails.svelte +3 -3
- package/dist/FileDetails.svelte.d.ts +6 -3
- package/dist/GitHubCorner.svelte.d.ts +3 -3
- package/dist/Icon.svelte.d.ts +4 -4
- package/dist/MultiSelect.svelte +86 -62
- package/dist/MultiSelect.svelte.d.ts +9 -9
- package/dist/Nav.svelte +447 -0
- package/dist/Nav.svelte.d.ts +42 -0
- package/dist/PrevNext.svelte +11 -10
- package/dist/PrevNext.svelte.d.ts +16 -15
- package/dist/Toggle.svelte +4 -8
- package/dist/Toggle.svelte.d.ts +5 -10
- package/dist/Wiggle.svelte.d.ts +3 -3
- package/dist/attachments.d.ts +9 -6
- package/dist/attachments.js +124 -35
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/types.d.ts +4 -20
- package/dist/utils.d.ts +3 -8
- package/dist/utils.js +24 -48
- package/package.json +19 -19
- package/readme.md +4 -6
- package/dist/RadioButtons.svelte +0 -67
- package/dist/RadioButtons.svelte.d.ts +0 -51
package/dist/MultiSelect.svelte
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
2
|
-
import { tick } from 'svelte';
|
|
1
|
+
<script lang="ts" generics="Option extends import('./types').Option">import { tick } from 'svelte';
|
|
3
2
|
import { flip } from 'svelte/animate';
|
|
4
|
-
import {
|
|
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';
|
|
5
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const label = `${get_label(opt)}`;
|
|
12
|
+
return fuzzy
|
|
13
|
+
? fuzzy_match(searchText, label)
|
|
14
|
+
: label.toLowerCase().includes(searchText.toLowerCase());
|
|
15
|
+
}, fuzzy = true, closeDropdownOnSelect = `if-mobile`, form_input = $bindable(null), highlightMatches = true, id = null, input = $bindable(null), inputClass = ``, inputStyle = null, inputmode = null, invalid = $bindable(false), liActiveOptionClass = ``, liActiveUserMsgClass = ``, liOptionClass = ``, liOptionStyle = null, liSelectedClass = ``, liSelectedStyle = null, liUserMsgClass = ``, loading = false, matchingOptions = $bindable([]), maxOptions = undefined, maxSelect = null, maxSelectMsg = (current, max) => (max > 1 ? `${current}/${max}` : ``), maxSelectMsgClass = ``, name = null, noMatchingOptionsMsg = `No matching options`, open = $bindable(false), options = $bindable(), outerDiv = $bindable(null), outerDivClass = ``, parseLabelsAsHtml = false, pattern = null, placeholder = null, removeAllTitle = `Remove all`, removeBtnTitle = `Remove`, minSelect = null, required = false, resetFilterOnAdd = true, searchText = $bindable(``), value = $bindable(null), selected = $bindable(value !== null && value !== undefined
|
|
16
|
+
? (Array.isArray(value) ? value : [value])
|
|
17
|
+
: (options
|
|
18
|
+
?.filter((opt) => opt instanceof Object && opt?.preselected)
|
|
19
|
+
.slice(0, maxSelect ?? undefined) ?? [])), sortSelected = false, selectedOptionsDraggable = !sortSelected, style = null, ulOptionsClass = ``, ulSelectedClass = ``, ulSelectedStyle = null, ulOptionsStyle = null, expandIcon, selectedItem, children, removeIcon, afterInput, spinner, disabledIcon, option, userMsg, onblur, onclick, onfocus, onkeydown, onkeyup, onmousedown, onmouseenter, onmouseleave, ontouchcancel, ontouchend, ontouchmove, ontouchstart, onadd, oncreate, onremove, onremoveAll, onchange, onopen, onclose, portal: portal_params = {}, ...rest } = $props();
|
|
12
20
|
$effect.pre(() => {
|
|
13
21
|
// if maxSelect=1, value is the single item in selected (or null if selected is empty)
|
|
14
22
|
// this solves both https://github.com/janosh/svelte-multiselect/issues/86 and
|
|
@@ -257,11 +265,25 @@ async function handle_keydown(event) {
|
|
|
257
265
|
// if none of the above special cases apply, we make next/prev option
|
|
258
266
|
// active with wrap around at both ends
|
|
259
267
|
const increment = event.key === `ArrowUp` ? -1 : 1;
|
|
260
|
-
|
|
268
|
+
// Include user message in total count if it exists
|
|
269
|
+
const has_user_msg = searchText && ((allowUserOptions && createOptionMsg) ||
|
|
270
|
+
(!duplicates && selected.map(get_label).includes(searchText)) ||
|
|
271
|
+
(matchingOptions.length === 0 && noMatchingOptionsMsg));
|
|
272
|
+
const total_items = matchingOptions.length + (has_user_msg ? 1 : 0);
|
|
273
|
+
activeIndex = (activeIndex + increment) % total_items;
|
|
261
274
|
// in JS % behaves like remainder operator, not real modulo, so negative numbers stay negative
|
|
262
275
|
// need to do manual wrap around at 0
|
|
263
276
|
if (activeIndex < 0)
|
|
264
|
-
activeIndex =
|
|
277
|
+
activeIndex = total_items - 1;
|
|
278
|
+
// Handle user message activation
|
|
279
|
+
if (has_user_msg && activeIndex === matchingOptions.length) {
|
|
280
|
+
option_msg_is_active = true;
|
|
281
|
+
activeOption = null;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
option_msg_is_active = false;
|
|
285
|
+
activeOption = matchingOptions[activeIndex] ?? null;
|
|
286
|
+
}
|
|
265
287
|
if (autoScroll) {
|
|
266
288
|
await tick();
|
|
267
289
|
const li = document.querySelector(`ul.options > li.active`);
|
|
@@ -273,7 +295,9 @@ async function handle_keydown(event) {
|
|
|
273
295
|
event.stopPropagation();
|
|
274
296
|
// Only remove option if it wouldn't violate minSelect
|
|
275
297
|
if (minSelect === null || selected.length > minSelect) {
|
|
276
|
-
|
|
298
|
+
const last_option = selected.at(-1);
|
|
299
|
+
if (last_option)
|
|
300
|
+
remove(last_option, event);
|
|
277
301
|
}
|
|
278
302
|
// Don't prevent default, allow normal backspace behavior if not removing
|
|
279
303
|
} // make first matching option active on any keypress (if none of the above special cases match)
|
|
@@ -310,9 +334,17 @@ const if_enter_or_space = (handler) => (event) => {
|
|
|
310
334
|
}
|
|
311
335
|
};
|
|
312
336
|
function on_click_outside(event) {
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
|
|
337
|
+
if (!outerDiv)
|
|
338
|
+
return;
|
|
339
|
+
const target = event.target;
|
|
340
|
+
// Check if click is inside the main component
|
|
341
|
+
if (outerDiv.contains(target))
|
|
342
|
+
return;
|
|
343
|
+
// If portal is active, also check if click is inside the portalled options dropdown
|
|
344
|
+
if (portal_params?.active && ul_options && ul_options.contains(target))
|
|
345
|
+
return;
|
|
346
|
+
// Click is outside both the main component and any portalled dropdown
|
|
347
|
+
close_dropdown(event);
|
|
316
348
|
}
|
|
317
349
|
let drag_idx = $state(null);
|
|
318
350
|
// event handlers enable dragging to reorder selected options
|
|
@@ -342,27 +374,6 @@ const dragstart = (idx) => (event) => {
|
|
|
342
374
|
event.dataTransfer.setData(`text/plain`, `${idx}`);
|
|
343
375
|
};
|
|
344
376
|
let ul_options = $state();
|
|
345
|
-
// Update highlights whenever search text changes (after ul_options is available)
|
|
346
|
-
$effect(() => {
|
|
347
|
-
if (ul_options && highlightMatches) {
|
|
348
|
-
if (searchText) {
|
|
349
|
-
highlight_matching_nodes(ul_options, searchText, noMatchingOptionsMsg);
|
|
350
|
-
}
|
|
351
|
-
else if (typeof CSS !== `undefined` && CSS.highlights) {
|
|
352
|
-
CSS.highlights.delete?.(`sms-search-matches`); // Clear highlights when search text is empty
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
// highlight text matching user-entered search text in available options
|
|
357
|
-
function highlight_matching_options(event) {
|
|
358
|
-
if (!highlightMatches || !ul_options)
|
|
359
|
-
return;
|
|
360
|
-
// get input's search query
|
|
361
|
-
const query = event?.target?.value.trim().toLowerCase();
|
|
362
|
-
if (!query)
|
|
363
|
-
return;
|
|
364
|
-
highlight_matching_nodes(ul_options, query, noMatchingOptionsMsg);
|
|
365
|
-
}
|
|
366
377
|
const handle_input_keydown = (event) => {
|
|
367
378
|
handle_keydown(event); // Restore internal logic
|
|
368
379
|
// Call original forwarded handler
|
|
@@ -392,6 +403,7 @@ $effect(() => {
|
|
|
392
403
|
const handle_input_blur = (event) => {
|
|
393
404
|
// For portalled dropdowns, don't close on blur since clicks on portalled elements
|
|
394
405
|
// will cause blur but we want to allow the click to register first
|
|
406
|
+
// (otherwise mobile touch event is unable to select options https://github.com/janosh/svelte-multiselect/issues/335)
|
|
395
407
|
if (portal_params?.active) {
|
|
396
408
|
onblur?.(event); // Let the click handler manage closing for portalled dropdowns
|
|
397
409
|
return;
|
|
@@ -429,7 +441,7 @@ function portal(node, params) {
|
|
|
429
441
|
tick().then(update_position);
|
|
430
442
|
window.addEventListener(`scroll`, update_position, true);
|
|
431
443
|
window.addEventListener(`resize`, update_position);
|
|
432
|
-
$effect
|
|
444
|
+
$effect(() => {
|
|
433
445
|
if (open && target_node)
|
|
434
446
|
update_position();
|
|
435
447
|
else
|
|
@@ -505,8 +517,7 @@ function portal(node, params) {
|
|
|
505
517
|
{:else}
|
|
506
518
|
<Icon
|
|
507
519
|
icon="ChevronExpand"
|
|
508
|
-
|
|
509
|
-
style="min-width: 1em; padding: 0 1pt; cursor: pointer"
|
|
520
|
+
style="width: 15px; min-width: 1em; padding: 0 1pt; cursor: pointer"
|
|
510
521
|
/>
|
|
511
522
|
{/if}
|
|
512
523
|
<ul
|
|
@@ -561,7 +572,7 @@ function portal(node, params) {
|
|
|
561
572
|
{#if removeIcon}
|
|
562
573
|
{@render removeIcon()}
|
|
563
574
|
{:else}
|
|
564
|
-
<Icon icon="Cross"
|
|
575
|
+
<Icon icon="Cross" style="width: 15px" />
|
|
565
576
|
{/if}
|
|
566
577
|
</button>
|
|
567
578
|
{/if}
|
|
@@ -577,13 +588,12 @@ function portal(node, params) {
|
|
|
577
588
|
{autocomplete}
|
|
578
589
|
{inputmode}
|
|
579
590
|
{pattern}
|
|
580
|
-
placeholder={selected.length
|
|
591
|
+
placeholder={selected.length === 0 ? placeholder : null}
|
|
581
592
|
aria-invalid={invalid ? `true` : null}
|
|
582
593
|
ondrop={() => false}
|
|
583
594
|
onmouseup={open_dropdown}
|
|
584
595
|
onkeydown={handle_input_keydown}
|
|
585
596
|
onfocus={handle_input_focus}
|
|
586
|
-
oninput={highlight_matching_options}
|
|
587
597
|
onblur={handle_input_blur}
|
|
588
598
|
{onclick}
|
|
589
599
|
{onkeyup}
|
|
@@ -619,8 +629,7 @@ function portal(node, params) {
|
|
|
619
629
|
{:else}
|
|
620
630
|
<Icon
|
|
621
631
|
icon="Disabled"
|
|
622
|
-
|
|
623
|
-
style="margin: 0 2pt"
|
|
632
|
+
style="width: 14pt; margin: 0 2pt"
|
|
624
633
|
data-name="disabled-icon"
|
|
625
634
|
aria-disabled="true"
|
|
626
635
|
/>
|
|
@@ -644,7 +653,7 @@ function portal(node, params) {
|
|
|
644
653
|
{#if removeIcon}
|
|
645
654
|
{@render removeIcon()}
|
|
646
655
|
{:else}
|
|
647
|
-
<Icon icon="Cross"
|
|
656
|
+
<Icon icon="Cross" style="width: 15px" />
|
|
648
657
|
{/if}
|
|
649
658
|
</button>
|
|
650
659
|
{/if}
|
|
@@ -654,6 +663,17 @@ function portal(node, params) {
|
|
|
654
663
|
{#if (searchText && noMatchingOptionsMsg) || options?.length > 0}
|
|
655
664
|
<ul
|
|
656
665
|
use:portal={{ target_node: outerDiv, ...portal_params }}
|
|
666
|
+
{@attach highlight_matches({
|
|
667
|
+
query: searchText,
|
|
668
|
+
disabled: !highlightMatches,
|
|
669
|
+
fuzzy,
|
|
670
|
+
css_class: `sms-search-matches`,
|
|
671
|
+
// don't highlight text in the "Create this option..." message
|
|
672
|
+
node_filter: (node) =>
|
|
673
|
+
node?.parentElement?.closest(`li.user-msg`)
|
|
674
|
+
? NodeFilter.FILTER_REJECT
|
|
675
|
+
: NodeFilter.FILTER_ACCEPT,
|
|
676
|
+
})}
|
|
657
677
|
class:hidden={!open}
|
|
658
678
|
class="options {ulOptionsClass}"
|
|
659
679
|
role="listbox"
|
|
@@ -663,10 +683,13 @@ function portal(node, params) {
|
|
|
663
683
|
bind:this={ul_options}
|
|
664
684
|
style={ulOptionsStyle}
|
|
665
685
|
>
|
|
666
|
-
{#each matchingOptions.slice(
|
|
667
|
-
|
|
686
|
+
{#each matchingOptions.slice(
|
|
687
|
+
0,
|
|
688
|
+
maxOptions == null ? Infinity : Math.max(0, maxOptions),
|
|
689
|
+
) as
|
|
690
|
+
option_item,
|
|
668
691
|
idx
|
|
669
|
-
(duplicates ? [key(
|
|
692
|
+
(duplicates ? [key(option_item), idx] : key(option_item))
|
|
670
693
|
}
|
|
671
694
|
{@const {
|
|
672
695
|
label,
|
|
@@ -674,21 +697,22 @@ function portal(node, params) {
|
|
|
674
697
|
title = null,
|
|
675
698
|
selectedTitle = null,
|
|
676
699
|
disabledTitle = defaultDisabledTitle,
|
|
677
|
-
} =
|
|
700
|
+
} = option_item instanceof Object ? option_item : { label: option_item }}
|
|
678
701
|
{@const active = activeIndex === idx}
|
|
702
|
+
{@const selected = is_selected(label)}
|
|
679
703
|
{@const optionStyle =
|
|
680
|
-
[get_style(
|
|
704
|
+
[get_style(option_item, `option`), liOptionStyle].filter(Boolean).join(
|
|
681
705
|
` `,
|
|
682
706
|
) ||
|
|
683
707
|
null}
|
|
684
708
|
<li
|
|
685
709
|
onclick={(event) => {
|
|
686
710
|
if (disabled) return
|
|
687
|
-
if (keepSelectedInDropdown) toggle_option(
|
|
688
|
-
else add(
|
|
711
|
+
if (keepSelectedInDropdown) toggle_option(option_item, event)
|
|
712
|
+
else add(option_item, event)
|
|
689
713
|
}}
|
|
690
|
-
title={disabled ? disabledTitle : (
|
|
691
|
-
class:selected
|
|
714
|
+
title={disabled ? disabledTitle : (selected && selectedTitle) || title}
|
|
715
|
+
class:selected
|
|
692
716
|
class:active
|
|
693
717
|
class:disabled
|
|
694
718
|
class="{liOptionClass} {active ? liActiveOptionClass : ``}"
|
|
@@ -699,13 +723,13 @@ function portal(node, params) {
|
|
|
699
723
|
if (!disabled) activeIndex = idx
|
|
700
724
|
}}
|
|
701
725
|
role="option"
|
|
702
|
-
aria-selected=
|
|
726
|
+
aria-selected={selected ? `true` : `false`}
|
|
703
727
|
style={optionStyle}
|
|
704
728
|
onkeydown={(event) => {
|
|
705
729
|
if (!disabled && (event.key === `Enter` || event.code === `Space`)) {
|
|
706
730
|
event.preventDefault()
|
|
707
|
-
if (keepSelectedInDropdown) toggle_option(
|
|
708
|
-
else add(
|
|
731
|
+
if (keepSelectedInDropdown) toggle_option(option_item, event)
|
|
732
|
+
else add(option_item, event)
|
|
709
733
|
}
|
|
710
734
|
}}
|
|
711
735
|
>
|
|
@@ -713,25 +737,25 @@ function portal(node, params) {
|
|
|
713
737
|
<input
|
|
714
738
|
type="checkbox"
|
|
715
739
|
class="option-checkbox"
|
|
716
|
-
checked={
|
|
717
|
-
aria-label="Toggle {get_label(
|
|
740
|
+
checked={selected}
|
|
741
|
+
aria-label="Toggle {get_label(option_item)}"
|
|
718
742
|
tabindex="-1"
|
|
719
743
|
/>
|
|
720
744
|
{/if}
|
|
721
745
|
{#if option}
|
|
722
746
|
{@render option({
|
|
723
|
-
option:
|
|
747
|
+
option: option_item,
|
|
724
748
|
idx,
|
|
725
749
|
})}
|
|
726
750
|
{:else if children}
|
|
727
751
|
{@render children({
|
|
728
|
-
option:
|
|
752
|
+
option: option_item,
|
|
729
753
|
idx,
|
|
730
754
|
})}
|
|
731
755
|
{:else if parseLabelsAsHtml}
|
|
732
|
-
{@html get_label(
|
|
756
|
+
{@html get_label(option_item)}
|
|
733
757
|
{:else}
|
|
734
|
-
{get_label(
|
|
758
|
+
{get_label(option_item)}
|
|
735
759
|
{/if}
|
|
736
760
|
</li>
|
|
737
761
|
{/each}
|
|
@@ -739,7 +763,7 @@ function portal(node, params) {
|
|
|
739
763
|
{@const text_input_is_duplicate = selected.map(get_label).includes(searchText)}
|
|
740
764
|
{@const is_dupe = !duplicates && text_input_is_duplicate && `dupe`}
|
|
741
765
|
{@const can_create = Boolean(allowUserOptions && createOptionMsg) && `create`}
|
|
742
|
-
{@const no_match = Boolean(matchingOptions?.length
|
|
766
|
+
{@const no_match = Boolean(matchingOptions?.length === 0 && noMatchingOptionsMsg) &&
|
|
743
767
|
`no-match`}
|
|
744
768
|
{@const msgType = is_dupe || can_create || no_match}
|
|
745
769
|
{#if msgType}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import type { MultiSelectProps
|
|
2
|
-
declare function $$render<Option extends
|
|
3
|
-
props: MultiSelectProps
|
|
1
|
+
import type { MultiSelectProps } from './types';
|
|
2
|
+
declare function $$render<Option extends import('./types').Option>(): {
|
|
3
|
+
props: MultiSelectProps<Option>;
|
|
4
4
|
exports: {};
|
|
5
|
-
bindings: "
|
|
5
|
+
bindings: "input" | "invalid" | "value" | "selected" | "open" | "activeIndex" | "activeOption" | "form_input" | "matchingOptions" | "options" | "outerDiv" | "searchText";
|
|
6
6
|
slots: {};
|
|
7
7
|
events: {};
|
|
8
8
|
};
|
|
9
|
-
declare class __sveltets_Render<Option extends
|
|
9
|
+
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(): "input" | "invalid" | "value" | "selected" | "open" | "activeIndex" | "activeOption" | "form_input" | "matchingOptions" | "options" | "outerDiv" | "searchText";
|
|
14
14
|
exports(): {};
|
|
15
15
|
}
|
|
16
16
|
interface $$IsomorphicComponent {
|
|
17
|
-
new <Option extends
|
|
17
|
+
new <Option extends import('./types').Option>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Option>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Option>['props']>, ReturnType<__sveltets_Render<Option>['events']>, ReturnType<__sveltets_Render<Option>['slots']>> & {
|
|
18
18
|
$$bindings?: ReturnType<__sveltets_Render<Option>['bindings']>;
|
|
19
19
|
} & ReturnType<__sveltets_Render<Option>['exports']>;
|
|
20
|
-
<Option extends
|
|
20
|
+
<Option extends import('./types').Option>(internal: unknown, props: ReturnType<__sveltets_Render<Option>['props']> & {}): ReturnType<__sveltets_Render<Option>['exports']>;
|
|
21
21
|
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
22
22
|
}
|
|
23
23
|
declare const MultiSelect: $$IsomorphicComponent;
|
|
24
|
-
type MultiSelect<Option extends
|
|
24
|
+
type MultiSelect<Option extends import('./types').Option> = InstanceType<typeof MultiSelect<Option>>;
|
|
25
25
|
export default MultiSelect;
|