svelte-multiselect 11.1.1 → 11.2.1
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 +2 -1
- package/dist/CmdPalette.svelte +16 -9
- package/dist/CmdPalette.svelte.d.ts +4 -5
- package/dist/CodeExample.svelte +80 -0
- package/dist/CodeExample.svelte.d.ts +22 -0
- package/dist/CopyButton.svelte +47 -0
- package/dist/CopyButton.svelte.d.ts +25 -0
- package/dist/FileDetails.svelte +52 -0
- package/dist/FileDetails.svelte.d.ts +20 -0
- package/dist/GitHubCorner.svelte +82 -0
- package/dist/GitHubCorner.svelte.d.ts +13 -0
- package/dist/Icon.svelte +23 -0
- package/dist/Icon.svelte.d.ts +8 -0
- package/dist/MultiSelect.svelte +129 -76
- package/dist/MultiSelect.svelte.d.ts +2 -3
- package/dist/PrevNext.svelte +100 -0
- package/dist/PrevNext.svelte.d.ts +48 -0
- package/dist/RadioButtons.svelte +67 -0
- package/dist/RadioButtons.svelte.d.ts +44 -0
- package/dist/Toggle.svelte +78 -0
- package/dist/Toggle.svelte.d.ts +16 -0
- package/dist/icons.d.ts +47 -0
- package/dist/icons.js +46 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +10 -3
- package/dist/types.d.ts +141 -0
- package/dist/utils.d.ts +6 -22
- package/dist/utils.js +17 -25
- package/package.json +17 -36
- package/readme.md +287 -121
- package/dist/icons/ChevronExpand.svelte +0 -9
- package/dist/icons/ChevronExpand.svelte.d.ts +0 -4
- package/dist/icons/Cross.svelte +0 -10
- package/dist/icons/Cross.svelte.d.ts +0 -4
- package/dist/icons/Disabled.svelte +0 -10
- package/dist/icons/Disabled.svelte.d.ts +0 -4
- package/dist/icons/Octocat.svelte +0 -9
- package/dist/icons/Octocat.svelte.d.ts +0 -4
- package/dist/icons/index.d.ts +0 -4
- package/dist/icons/index.js +0 -4
- package/dist/props.d.ts +0 -143
- package/dist/props.js +0 -1
package/dist/MultiSelect.svelte
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
1
|
+
<script lang="ts">import { CircleSpinner, Icon, Wiggle } from './';
|
|
2
|
+
import { tick } from 'svelte';
|
|
2
3
|
import { flip } from 'svelte/animate';
|
|
3
|
-
import CircleSpinner from './CircleSpinner.svelte';
|
|
4
|
-
import Wiggle from './Wiggle.svelte';
|
|
5
|
-
import { CrossIcon, DisabledIcon, ExpandIcon } from './icons';
|
|
6
4
|
import { get_label, get_style, highlight_matching_nodes } from './utils';
|
|
7
5
|
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, key = (opt) => `${get_label(opt)}`.toLowerCase(), filterFunc = (opt, searchText) => {
|
|
8
6
|
if (!searchText)
|
|
9
7
|
return true;
|
|
10
8
|
return `${get_label(opt)}`.toLowerCase().includes(searchText.toLowerCase());
|
|
11
|
-
}, closeDropdownOnSelect = `
|
|
9
|
+
}, 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
|
|
12
10
|
?.filter((opt) => opt instanceof Object && opt?.preselected)
|
|
13
11
|
.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();
|
|
14
12
|
$effect.pre(() => {
|
|
@@ -105,8 +103,9 @@ function add(option_to_add, event) {
|
|
|
105
103
|
else {
|
|
106
104
|
option_to_add = searchText; // else create custom option as string
|
|
107
105
|
}
|
|
108
|
-
oncreate?.({ option: option_to_add });
|
|
109
106
|
}
|
|
107
|
+
// Fire oncreate event for all user-created options, regardless of type
|
|
108
|
+
oncreate?.({ option: option_to_add });
|
|
110
109
|
if (allowUserOptions === `append`)
|
|
111
110
|
options = [...options, option_to_add];
|
|
112
111
|
}
|
|
@@ -135,9 +134,12 @@ function add(option_to_add, event) {
|
|
|
135
134
|
}
|
|
136
135
|
const reached_max_select = selected.length === maxSelect;
|
|
137
136
|
const dropdown_should_close = closeDropdownOnSelect === true ||
|
|
138
|
-
|
|
137
|
+
closeDropdownOnSelect === `retain-focus` ||
|
|
138
|
+
(closeDropdownOnSelect === `if-mobile` && window_width &&
|
|
139
|
+
window_width < breakpoint);
|
|
140
|
+
const should_retain_focus = closeDropdownOnSelect === `retain-focus`;
|
|
139
141
|
if (reached_max_select || dropdown_should_close) {
|
|
140
|
-
close_dropdown(event);
|
|
142
|
+
close_dropdown(event, should_retain_focus);
|
|
141
143
|
}
|
|
142
144
|
else if (!dropdown_should_close) {
|
|
143
145
|
input?.focus();
|
|
@@ -182,9 +184,10 @@ function open_dropdown(event) {
|
|
|
182
184
|
}
|
|
183
185
|
onopen?.({ event });
|
|
184
186
|
}
|
|
185
|
-
function close_dropdown(event) {
|
|
187
|
+
function close_dropdown(event, retain_focus = false) {
|
|
186
188
|
open = false;
|
|
187
|
-
|
|
189
|
+
if (!retain_focus)
|
|
190
|
+
input?.blur();
|
|
188
191
|
activeIndex = null;
|
|
189
192
|
onclose?.({ event });
|
|
190
193
|
}
|
|
@@ -195,8 +198,7 @@ async function handle_keydown(event) {
|
|
|
195
198
|
event.stopPropagation();
|
|
196
199
|
close_dropdown(event);
|
|
197
200
|
searchText = ``;
|
|
198
|
-
}
|
|
199
|
-
// on enter key: toggle active option and reset search text
|
|
201
|
+
} // on enter key: toggle active option and reset search text
|
|
200
202
|
else if (event.key === `Enter`) {
|
|
201
203
|
event.stopPropagation();
|
|
202
204
|
event.preventDefault(); // prevent enter key from triggering form submission
|
|
@@ -216,8 +218,7 @@ async function handle_keydown(event) {
|
|
|
216
218
|
// in which case enter means open it
|
|
217
219
|
open_dropdown(event);
|
|
218
220
|
}
|
|
219
|
-
}
|
|
220
|
-
// on up/down arrow keys: update active option
|
|
221
|
+
} // on up/down arrow keys: update active option
|
|
221
222
|
else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
|
|
222
223
|
event.stopPropagation();
|
|
223
224
|
// if no option is active yet, but there are matching options, make first one active
|
|
@@ -252,14 +253,12 @@ async function handle_keydown(event) {
|
|
|
252
253
|
if (li)
|
|
253
254
|
li.scrollIntoViewIfNeeded?.();
|
|
254
255
|
}
|
|
255
|
-
}
|
|
256
|
-
// on backspace key: remove last selected option
|
|
256
|
+
} // on backspace key: remove last selected option
|
|
257
257
|
else if (event.key === `Backspace` && selected.length > 0 && !searchText) {
|
|
258
258
|
event.stopPropagation();
|
|
259
259
|
// Don't prevent default, allow normal backspace behavior if not removing
|
|
260
260
|
remove(selected.at(-1), event);
|
|
261
|
-
}
|
|
262
|
-
// make first matching option active on any keypress (if none of the above special cases match)
|
|
261
|
+
} // make first matching option active on any keypress (if none of the above special cases match)
|
|
263
262
|
else if (matchingOptions.length > 0 && activeIndex === null) {
|
|
264
263
|
// Don't stop propagation or prevent default here, allow normal character input
|
|
265
264
|
activeIndex = 0;
|
|
@@ -267,10 +266,11 @@ async function handle_keydown(event) {
|
|
|
267
266
|
}
|
|
268
267
|
function remove_all(event) {
|
|
269
268
|
event.stopPropagation();
|
|
269
|
+
selected = []; // Set selected first
|
|
270
|
+
searchText = ``;
|
|
271
|
+
// Now trigger change events
|
|
270
272
|
onremoveAll?.({ options: selected });
|
|
271
273
|
onchange?.({ options: selected, type: `removeAll` });
|
|
272
|
-
selected = [];
|
|
273
|
-
searchText = ``;
|
|
274
274
|
}
|
|
275
275
|
let is_selected = $derived((label) => selected.map(get_label).includes(label));
|
|
276
276
|
const if_enter_or_space = (handler) => (event) => {
|
|
@@ -328,10 +328,38 @@ const handle_input_keydown = (event) => {
|
|
|
328
328
|
onkeydown?.(event);
|
|
329
329
|
};
|
|
330
330
|
const handle_input_focus = (event) => {
|
|
331
|
-
open_dropdown(event);
|
|
332
|
-
// Call original forwarded handler
|
|
331
|
+
open_dropdown(event);
|
|
333
332
|
onfocus?.(event);
|
|
334
333
|
};
|
|
334
|
+
// Override input's focus method to ensure dropdown opens on programmatic focus
|
|
335
|
+
// https://github.com/janosh/svelte-multiselect/issues/289
|
|
336
|
+
$effect(() => {
|
|
337
|
+
if (!input)
|
|
338
|
+
return;
|
|
339
|
+
const orig_focus = input.focus.bind(input);
|
|
340
|
+
input.focus = (options) => {
|
|
341
|
+
orig_focus(options);
|
|
342
|
+
if (!disabled && !open) {
|
|
343
|
+
open_dropdown(new FocusEvent(`focus`, { bubbles: true }));
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
return () => {
|
|
347
|
+
if (input)
|
|
348
|
+
input.focus = orig_focus;
|
|
349
|
+
};
|
|
350
|
+
});
|
|
351
|
+
const handle_input_blur = (event) => {
|
|
352
|
+
// For portalled dropdowns, don't close on blur since clicks on portalled elements
|
|
353
|
+
// will cause blur but we want to allow the click to register first
|
|
354
|
+
if (portal_params?.active) {
|
|
355
|
+
onblur?.(event); // Let the click handler manage closing for portalled dropdowns
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
// For non-portalled dropdowns, close when focus moves outside the component
|
|
359
|
+
if (!outerDiv?.contains(event.relatedTarget))
|
|
360
|
+
close_dropdown(event);
|
|
361
|
+
onblur?.(event); // Call original handler (if any passed as component prop)
|
|
362
|
+
};
|
|
335
363
|
// reset form validation when required prop changes
|
|
336
364
|
// https://github.com/janosh/svelte-multiselect/issues/285
|
|
337
365
|
$effect.pre(() => {
|
|
@@ -342,7 +370,8 @@ function portal(node, params) {
|
|
|
342
370
|
let { target_node, active } = params;
|
|
343
371
|
if (!active)
|
|
344
372
|
return;
|
|
345
|
-
let render_in_place = typeof window === `undefined` ||
|
|
373
|
+
let render_in_place = typeof window === `undefined` ||
|
|
374
|
+
!document.body.contains(node);
|
|
346
375
|
if (!render_in_place) {
|
|
347
376
|
document.body.appendChild(node);
|
|
348
377
|
node.style.position = `fixed`;
|
|
@@ -368,7 +397,8 @@ function portal(node, params) {
|
|
|
368
397
|
return {
|
|
369
398
|
update(params) {
|
|
370
399
|
target_node = params.target_node;
|
|
371
|
-
render_in_place = typeof window === `undefined` ||
|
|
400
|
+
render_in_place = typeof window === `undefined` ||
|
|
401
|
+
!document.body.contains(node);
|
|
372
402
|
if (open && !render_in_place && target_node)
|
|
373
403
|
tick().then(update_position);
|
|
374
404
|
else if (!open || !target_node)
|
|
@@ -432,7 +462,11 @@ function portal(node, params) {
|
|
|
432
462
|
{#if expandIcon}
|
|
433
463
|
{@render expandIcon({ open })}
|
|
434
464
|
{:else}
|
|
435
|
-
<
|
|
465
|
+
<Icon
|
|
466
|
+
icon="ChevronExpand"
|
|
467
|
+
width="15px"
|
|
468
|
+
style="min-width: 1em; padding: 0 1pt; cursor: pointer"
|
|
469
|
+
/>
|
|
436
470
|
{/if}
|
|
437
471
|
<ul
|
|
438
472
|
class="selected {ulSelectedClass}"
|
|
@@ -441,7 +475,9 @@ function portal(node, params) {
|
|
|
441
475
|
>
|
|
442
476
|
{#each selected as option, idx (duplicates ? [key(option), idx] : key(option))}
|
|
443
477
|
{@const selectedOptionStyle =
|
|
444
|
-
[get_style(option, `selected`), liSelectedStyle].filter(Boolean).join(
|
|
478
|
+
[get_style(option, `selected`), liSelectedStyle].filter(Boolean).join(
|
|
479
|
+
` `,
|
|
480
|
+
) ||
|
|
445
481
|
null}
|
|
446
482
|
<li
|
|
447
483
|
class={liSelectedClass}
|
|
@@ -460,14 +496,14 @@ function portal(node, params) {
|
|
|
460
496
|
>
|
|
461
497
|
{#if selectedItem}
|
|
462
498
|
{@render selectedItem({
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
499
|
+
option,
|
|
500
|
+
idx,
|
|
501
|
+
})}
|
|
466
502
|
{:else if children}
|
|
467
503
|
{@render children({
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
504
|
+
option,
|
|
505
|
+
idx,
|
|
506
|
+
})}
|
|
471
507
|
{:else if parseLabelsAsHtml}
|
|
472
508
|
{@html get_label(option)}
|
|
473
509
|
{:else}
|
|
@@ -484,7 +520,7 @@ function portal(node, params) {
|
|
|
484
520
|
{#if removeIcon}
|
|
485
521
|
{@render removeIcon()}
|
|
486
522
|
{:else}
|
|
487
|
-
<
|
|
523
|
+
<Icon icon="Cross" width="15px" />
|
|
488
524
|
{/if}
|
|
489
525
|
</button>
|
|
490
526
|
{/if}
|
|
@@ -507,7 +543,7 @@ function portal(node, params) {
|
|
|
507
543
|
onkeydown={handle_input_keydown}
|
|
508
544
|
onfocus={handle_input_focus}
|
|
509
545
|
oninput={highlight_matching_options}
|
|
510
|
-
{
|
|
546
|
+
onblur={handle_input_blur}
|
|
511
547
|
{onclick}
|
|
512
548
|
{onkeyup}
|
|
513
549
|
{onmousedown}
|
|
@@ -519,16 +555,15 @@ function portal(node, params) {
|
|
|
519
555
|
{ontouchstart}
|
|
520
556
|
{...rest}
|
|
521
557
|
/>
|
|
522
|
-
<!-- the above on:* lines forward potentially useful DOM events -->
|
|
523
558
|
{@render afterInput?.({
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
559
|
+
selected,
|
|
560
|
+
disabled,
|
|
561
|
+
invalid,
|
|
562
|
+
id,
|
|
563
|
+
placeholder,
|
|
564
|
+
open,
|
|
565
|
+
required,
|
|
566
|
+
})}
|
|
532
567
|
</ul>
|
|
533
568
|
{#if loading}
|
|
534
569
|
{#if spinner}
|
|
@@ -541,7 +576,12 @@ function portal(node, params) {
|
|
|
541
576
|
{#if disabledIcon}
|
|
542
577
|
{@render disabledIcon()}
|
|
543
578
|
{:else}
|
|
544
|
-
<
|
|
579
|
+
<Icon
|
|
580
|
+
icon="Disabled"
|
|
581
|
+
width="14pt"
|
|
582
|
+
style="margin: 0 2pt"
|
|
583
|
+
data-name="disabled-icon"
|
|
584
|
+
/>
|
|
545
585
|
{/if}
|
|
546
586
|
{:else if selected.length > 0}
|
|
547
587
|
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
|
|
@@ -562,7 +602,7 @@ function portal(node, params) {
|
|
|
562
602
|
{#if removeIcon}
|
|
563
603
|
{@render removeIcon()}
|
|
564
604
|
{:else}
|
|
565
|
-
<
|
|
605
|
+
<Icon icon="Cross" width="15px" />
|
|
566
606
|
{/if}
|
|
567
607
|
</button>
|
|
568
608
|
{/if}
|
|
@@ -581,25 +621,29 @@ function portal(node, params) {
|
|
|
581
621
|
bind:this={ul_options}
|
|
582
622
|
style={ulOptionsStyle}
|
|
583
623
|
>
|
|
584
|
-
{#each matchingOptions.slice(0, Math.max(0, maxOptions ?? 0) || Infinity) as
|
|
624
|
+
{#each matchingOptions.slice(0, Math.max(0, maxOptions ?? 0) || Infinity) as
|
|
625
|
+
optionItem,
|
|
626
|
+
idx
|
|
627
|
+
(duplicates ? [key(optionItem), idx] : key(optionItem))
|
|
628
|
+
}
|
|
585
629
|
{@const {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
630
|
+
label,
|
|
631
|
+
disabled = null,
|
|
632
|
+
title = null,
|
|
633
|
+
selectedTitle = null,
|
|
634
|
+
disabledTitle = defaultDisabledTitle,
|
|
635
|
+
} = optionItem instanceof Object ? optionItem : { label: optionItem }}
|
|
592
636
|
{@const active = activeIndex === idx}
|
|
593
637
|
{@const optionStyle =
|
|
594
|
-
|
|
595
|
-
|
|
638
|
+
[get_style(optionItem, `option`), liOptionStyle].filter(Boolean).join(
|
|
639
|
+
` `,
|
|
640
|
+
) ||
|
|
641
|
+
null}
|
|
596
642
|
<li
|
|
597
643
|
onclick={(event) => {
|
|
598
644
|
if (!disabled) add(optionItem, event)
|
|
599
645
|
}}
|
|
600
|
-
title={disabled
|
|
601
|
-
? disabledTitle
|
|
602
|
-
: (is_selected(label) && selectedTitle) || title}
|
|
646
|
+
title={disabled ? disabledTitle : (is_selected(label) && selectedTitle) || title}
|
|
603
647
|
class:selected={is_selected(label)}
|
|
604
648
|
class:active
|
|
605
649
|
class:disabled
|
|
@@ -622,14 +666,14 @@ function portal(node, params) {
|
|
|
622
666
|
>
|
|
623
667
|
{#if option}
|
|
624
668
|
{@render option({
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
669
|
+
option: optionItem,
|
|
670
|
+
idx,
|
|
671
|
+
})}
|
|
628
672
|
{:else if children}
|
|
629
673
|
{@render children({
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
674
|
+
option: optionItem,
|
|
675
|
+
idx,
|
|
676
|
+
})}
|
|
633
677
|
{:else if parseLabelsAsHtml}
|
|
634
678
|
{@html get_label(optionItem)}
|
|
635
679
|
{:else}
|
|
@@ -641,15 +685,15 @@ function portal(node, params) {
|
|
|
641
685
|
{@const text_input_is_duplicate = selected.map(get_label).includes(searchText)}
|
|
642
686
|
{@const is_dupe = !duplicates && text_input_is_duplicate && `dupe`}
|
|
643
687
|
{@const can_create = Boolean(allowUserOptions && createOptionMsg) && `create`}
|
|
644
|
-
{@const no_match =
|
|
645
|
-
|
|
688
|
+
{@const no_match = Boolean(matchingOptions?.length == 0 && noMatchingOptionsMsg) &&
|
|
689
|
+
`no-match`}
|
|
646
690
|
{@const msgType = is_dupe || can_create || no_match}
|
|
647
691
|
{#if msgType}
|
|
648
692
|
{@const msg = {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
693
|
+
dupe: duplicateOptionMsg,
|
|
694
|
+
create: createOptionMsg,
|
|
695
|
+
'no-match': noMatchingOptionsMsg,
|
|
696
|
+
}[msgType]}
|
|
653
697
|
<li
|
|
654
698
|
onclick={(event) => {
|
|
655
699
|
if (msgType === `create` && allowUserOptions) {
|
|
@@ -667,10 +711,10 @@ function portal(node, params) {
|
|
|
667
711
|
}
|
|
668
712
|
}}
|
|
669
713
|
title={msgType === `create`
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
714
|
+
? createOptionMsg
|
|
715
|
+
: msgType === `dupe`
|
|
716
|
+
? duplicateOptionMsg
|
|
717
|
+
: ``}
|
|
674
718
|
class:active={option_msg_is_active}
|
|
675
719
|
onmouseover={() => (option_msg_is_active = true)}
|
|
676
720
|
onfocus={() => (option_msg_is_active = true)}
|
|
@@ -678,9 +722,11 @@ function portal(node, params) {
|
|
|
678
722
|
onblur={() => (option_msg_is_active = false)}
|
|
679
723
|
role="option"
|
|
680
724
|
aria-selected="false"
|
|
681
|
-
class="
|
|
725
|
+
class="
|
|
726
|
+
user-msg {liUserMsgClass} {option_msg_is_active
|
|
682
727
|
? liActiveUserMsgClass
|
|
683
|
-
: ``}
|
|
728
|
+
: ``}
|
|
729
|
+
"
|
|
684
730
|
style:cursor={{
|
|
685
731
|
dupe: `not-allowed`,
|
|
686
732
|
create: `pointer`,
|
|
@@ -791,6 +837,12 @@ function portal(node, params) {
|
|
|
791
837
|
cursor: inherit; /* needed for disabled state */
|
|
792
838
|
border-radius: 0; /* reset ul.selected > li */
|
|
793
839
|
}
|
|
840
|
+
|
|
841
|
+
/* When options are selected, placeholder is hidden in which case we minimize input width to avoid adding unnecessary width to div.multiselect */
|
|
842
|
+
:is(div.multiselect > ul.selected > input:not(:placeholder-shown)) {
|
|
843
|
+
min-width: 1px; /* Minimal width to remain interactive */
|
|
844
|
+
}
|
|
845
|
+
|
|
794
846
|
/* don't wrap ::placeholder rules in :is() as it seems to be overpowered by browser defaults i.t.o. specificity */
|
|
795
847
|
div.multiselect > ul.selected > input::placeholder {
|
|
796
848
|
padding-left: 5pt;
|
|
@@ -820,7 +872,8 @@ function portal(node, params) {
|
|
|
820
872
|
z-index: var(--sms-options-z-index, 3);
|
|
821
873
|
|
|
822
874
|
overflow: auto;
|
|
823
|
-
transition: all
|
|
875
|
+
transition: all
|
|
876
|
+
0.2s; /* Consider if this transition is desirable with portal positioning */
|
|
824
877
|
box-sizing: border-box;
|
|
825
878
|
background: var(--sms-options-bg, white);
|
|
826
879
|
max-height: var(--sms-options-max-height, 50vh);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import type { MultiSelectProps } from './
|
|
2
|
-
import type { Option as T } from './types';
|
|
1
|
+
import type { MultiSelectProps, Option as T } from './types';
|
|
3
2
|
declare class __sveltets_Render<Option extends T> {
|
|
4
3
|
props(): MultiSelectProps<T>;
|
|
5
4
|
events(): {};
|
|
6
5
|
slots(): {};
|
|
7
|
-
bindings(): "
|
|
6
|
+
bindings(): "open" | "input" | "value" | "selected" | "invalid" | "activeIndex" | "activeOption" | "form_input" | "matchingOptions" | "options" | "outerDiv" | "searchText";
|
|
8
7
|
exports(): {};
|
|
9
8
|
}
|
|
10
9
|
interface $$IsomorphicComponent {
|
|
@@ -0,0 +1,100 @@
|
|
|
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();
|
|
5
|
+
// Convert items to consistent [key, value] format
|
|
6
|
+
let items_arr = $derived((items ?? []).map((itm) => (typeof itm === `string` ? [itm, itm] : itm)));
|
|
7
|
+
// Calculate prev/next items with wraparound
|
|
8
|
+
let idx = $derived(items_arr.findIndex(([key]) => key === current));
|
|
9
|
+
let prev = $derived(items_arr[idx - 1] ?? items_arr[items_arr.length - 1]);
|
|
10
|
+
let next = $derived(items_arr[idx + 1] ?? items_arr[0]);
|
|
11
|
+
// Validation and logging
|
|
12
|
+
$effect.pre(() => {
|
|
13
|
+
if (log !== `silent`) {
|
|
14
|
+
if (items_arr.length < 2 && log === `verbose`) {
|
|
15
|
+
console.warn(`PrevNext received ${items_arr.length} items - minimum of 2 expected`);
|
|
16
|
+
}
|
|
17
|
+
if (idx < 0 && log === `errors`) {
|
|
18
|
+
const valid = items_arr.map(([key]) => key);
|
|
19
|
+
console.error(`PrevNext received invalid current=${current}, expected one of ${valid}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
function handle_keyup(event) {
|
|
24
|
+
if (!onkeyup)
|
|
25
|
+
return;
|
|
26
|
+
const key_map = onkeyup({ prev, next });
|
|
27
|
+
const to = key_map[event.key];
|
|
28
|
+
if (to) {
|
|
29
|
+
const { replace_state, no_scroll } = nav_options;
|
|
30
|
+
const [scroll_x, scroll_y] = no_scroll
|
|
31
|
+
? [window.scrollX, window.scrollY]
|
|
32
|
+
: [0, 0];
|
|
33
|
+
const goto = window.history[replace_state ? `replaceState` : `pushState`];
|
|
34
|
+
goto.call(window.history, {}, ``, to); // Navigate using appropriate history method
|
|
35
|
+
if (no_scroll)
|
|
36
|
+
window.scrollTo(scroll_x, scroll_y); // Restore scroll position if needed
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<svelte:window onkeyup={handle_keyup} />
|
|
43
|
+
|
|
44
|
+
{#if items_arr.length > 2}
|
|
45
|
+
<svelte:element this={node} class="prev-next" {...rest}>
|
|
46
|
+
<!-- ensures `prev` is a defined [key, value] tuple.
|
|
47
|
+
Due to prior normalization of the `items` prop, any defined `prev` item
|
|
48
|
+
is guaranteed to be a 2-element array except if `prev` is null.
|
|
49
|
+
-->
|
|
50
|
+
{#if prev?.length >= 2}
|
|
51
|
+
{#if prev_snippet}
|
|
52
|
+
{@render prev_snippet({ item: prev })}
|
|
53
|
+
{:else if children}
|
|
54
|
+
{@render children({ kind: `prev`, item: prev })}
|
|
55
|
+
{:else}
|
|
56
|
+
<div>
|
|
57
|
+
{#if titles.prev}<span>{@html titles.prev}</span>{/if}
|
|
58
|
+
<a href={prev[0]}>{prev[0]}</a>
|
|
59
|
+
</div>
|
|
60
|
+
{/if}
|
|
61
|
+
{/if}
|
|
62
|
+
{@render between?.()}
|
|
63
|
+
{#if next?.length >= 2}
|
|
64
|
+
{#if next_snippet}
|
|
65
|
+
{@render next_snippet({ item: next })}
|
|
66
|
+
{:else if children}
|
|
67
|
+
{@render children({ kind: `next`, item: next })}
|
|
68
|
+
{:else}
|
|
69
|
+
<div>
|
|
70
|
+
{#if titles.next}<span>{@html titles.next}</span>{/if}
|
|
71
|
+
<a href={next[0]}>{next[0]}</a>
|
|
72
|
+
</div>
|
|
73
|
+
{/if}
|
|
74
|
+
{/if}
|
|
75
|
+
</svelte:element>
|
|
76
|
+
{/if}
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.prev-next {
|
|
80
|
+
display: flex;
|
|
81
|
+
list-style: none;
|
|
82
|
+
place-content: space-between;
|
|
83
|
+
gap: var(--prev-next-gap, 2em);
|
|
84
|
+
padding: var(--prev-next-padding, 0);
|
|
85
|
+
margin: var(--prev-next-margin, 3em auto);
|
|
86
|
+
}
|
|
87
|
+
.prev-next a {
|
|
88
|
+
color: var(--prev-next-color);
|
|
89
|
+
background: var(--prev-next-link-bg);
|
|
90
|
+
padding: var(--prev-next-link-padding);
|
|
91
|
+
border-radius: var(--prev-next-link-border-radius);
|
|
92
|
+
}
|
|
93
|
+
.prev-next span {
|
|
94
|
+
display: block;
|
|
95
|
+
margin: var(--prev-next-label-margin, 0 auto 1ex);
|
|
96
|
+
}
|
|
97
|
+
.prev-next > div:nth-child(2) {
|
|
98
|
+
text-align: right;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export type Item = string | [string, unknown];
|
|
3
|
+
declare class __sveltets_Render<T extends Item> {
|
|
4
|
+
props(): {
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
items?: T[] | undefined;
|
|
7
|
+
node?: string;
|
|
8
|
+
current?: string;
|
|
9
|
+
log?: `verbose` | `errors` | `silent`;
|
|
10
|
+
nav_options?: {
|
|
11
|
+
replace_state: boolean;
|
|
12
|
+
no_scroll: boolean;
|
|
13
|
+
} | undefined;
|
|
14
|
+
titles?: {
|
|
15
|
+
prev: string;
|
|
16
|
+
next: string;
|
|
17
|
+
} | undefined;
|
|
18
|
+
onkeyup?: ((obj: {
|
|
19
|
+
prev: Item;
|
|
20
|
+
next: Item;
|
|
21
|
+
}) => Record<string, string>) | null | undefined;
|
|
22
|
+
prev_snippet?: Snippet<[{
|
|
23
|
+
item: Item;
|
|
24
|
+
}]> | undefined;
|
|
25
|
+
children?: Snippet<[{
|
|
26
|
+
kind: `prev` | `next`;
|
|
27
|
+
item: Item;
|
|
28
|
+
}]> | undefined;
|
|
29
|
+
between?: Snippet<[]>;
|
|
30
|
+
next_snippet?: Snippet<[{
|
|
31
|
+
item: Item;
|
|
32
|
+
}]> | undefined;
|
|
33
|
+
};
|
|
34
|
+
events(): {};
|
|
35
|
+
slots(): {};
|
|
36
|
+
bindings(): "";
|
|
37
|
+
exports(): {};
|
|
38
|
+
}
|
|
39
|
+
interface $$IsomorphicComponent {
|
|
40
|
+
new <T extends Item>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
41
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
42
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
43
|
+
<T extends Item>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
44
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
45
|
+
}
|
|
46
|
+
declare const PrevNext: $$IsomorphicComponent;
|
|
47
|
+
type PrevNext<T extends Item> = InstanceType<typeof PrevNext<T>>;
|
|
48
|
+
export default PrevNext;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script lang="ts">// get the label key from an option object or the option itself if it's a string or number
|
|
2
|
+
const get_label = (op) => {
|
|
3
|
+
if (op instanceof Object) {
|
|
4
|
+
if (op.label === undefined) {
|
|
5
|
+
console.error(`RadioButton option ${JSON.stringify(op)} is an object but has no label key`);
|
|
6
|
+
}
|
|
7
|
+
return op.label;
|
|
8
|
+
}
|
|
9
|
+
return op;
|
|
10
|
+
};
|
|
11
|
+
let { options, selected = $bindable(), id = null, name = null, disabled = false, required = false, aria_label = null, onclick, onchange, oninput, option_snippet, children, ...rest } = $props();
|
|
12
|
+
export {};
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div {id} {...rest}>
|
|
16
|
+
{#each options as option (JSON.stringify(option))}
|
|
17
|
+
{@const label = get_label(option)}
|
|
18
|
+
{@const active = selected && get_label(option) === get_label(selected)}
|
|
19
|
+
<label class:active aria-label={aria_label}>
|
|
20
|
+
<input
|
|
21
|
+
type="radio"
|
|
22
|
+
value={option}
|
|
23
|
+
{name}
|
|
24
|
+
{disabled}
|
|
25
|
+
{required}
|
|
26
|
+
bind:group={selected}
|
|
27
|
+
{onchange}
|
|
28
|
+
{oninput}
|
|
29
|
+
{onclick}
|
|
30
|
+
/>
|
|
31
|
+
{#if option_snippet}
|
|
32
|
+
{@render option_snippet({ option, selected, active })}
|
|
33
|
+
{:else if children}
|
|
34
|
+
{@render children({ option, selected, active })}
|
|
35
|
+
{:else}<span>{label}</span>{/if}
|
|
36
|
+
</label>
|
|
37
|
+
{/each}
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<style>
|
|
41
|
+
div {
|
|
42
|
+
max-width: max-content;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
height: fit-content;
|
|
45
|
+
display: var(--radio-btn-display, inline-flex);
|
|
46
|
+
border-radius: var(--radio-btn-border-radius, 0.5em);
|
|
47
|
+
}
|
|
48
|
+
input {
|
|
49
|
+
display: none;
|
|
50
|
+
}
|
|
51
|
+
span {
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
display: inline-block;
|
|
54
|
+
color: var(--radio-btn-color, white);
|
|
55
|
+
padding: var(--radio-btn-padding, 2pt 5pt);
|
|
56
|
+
background: var(--radio-btn-bg, black);
|
|
57
|
+
transition: var(--radio-btn-transition, background 0.3s, transform 0.3s);
|
|
58
|
+
}
|
|
59
|
+
label:not(.active) span:hover {
|
|
60
|
+
background: var(--radio-btn-hover-bg, cornflowerblue);
|
|
61
|
+
color: var(--radio-btn-hover-color, white);
|
|
62
|
+
}
|
|
63
|
+
label.active span {
|
|
64
|
+
box-shadow: var(--radio-btn-checked-shadow, inset 0 0 1em -3pt black);
|
|
65
|
+
background: var(--radio-btn-checked-bg, darkcyan);
|
|
66
|
+
}
|
|
67
|
+
</style>
|