svelte-multiselect 11.1.0 → 11.2.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 +2 -1
- package/dist/CmdPalette.svelte +13 -7
- 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 +53 -0
- package/dist/FileDetails.svelte.d.ts +21 -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 +128 -75
- 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 +18 -37
- 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,15 +1,13 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
1
|
+
<script lang="ts">import { Icon, Wiggle } from './';
|
|
2
2
|
import { tick } from 'svelte';
|
|
3
3
|
import { flip } from 'svelte/animate';
|
|
4
4
|
import CircleSpinner from './CircleSpinner.svelte';
|
|
5
|
-
import Wiggle from './Wiggle.svelte';
|
|
6
|
-
import { CrossIcon, DisabledIcon, ExpandIcon } from './icons';
|
|
7
5
|
import { get_label, get_style, highlight_matching_nodes } from './utils';
|
|
8
6
|
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) => {
|
|
9
7
|
if (!searchText)
|
|
10
8
|
return true;
|
|
11
9
|
return `${get_label(opt)}`.toLowerCase().includes(searchText.toLowerCase());
|
|
12
|
-
}, closeDropdownOnSelect = `
|
|
10
|
+
}, 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
|
|
13
11
|
?.filter((opt) => opt instanceof Object && opt?.preselected)
|
|
14
12
|
.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();
|
|
15
13
|
$effect.pre(() => {
|
|
@@ -106,8 +104,9 @@ function add(option_to_add, event) {
|
|
|
106
104
|
else {
|
|
107
105
|
option_to_add = searchText; // else create custom option as string
|
|
108
106
|
}
|
|
109
|
-
oncreate?.({ option: option_to_add });
|
|
110
107
|
}
|
|
108
|
+
// Fire oncreate event for all user-created options, regardless of type
|
|
109
|
+
oncreate?.({ option: option_to_add });
|
|
111
110
|
if (allowUserOptions === `append`)
|
|
112
111
|
options = [...options, option_to_add];
|
|
113
112
|
}
|
|
@@ -136,9 +135,12 @@ function add(option_to_add, event) {
|
|
|
136
135
|
}
|
|
137
136
|
const reached_max_select = selected.length === maxSelect;
|
|
138
137
|
const dropdown_should_close = closeDropdownOnSelect === true ||
|
|
139
|
-
|
|
138
|
+
closeDropdownOnSelect === `retain-focus` ||
|
|
139
|
+
(closeDropdownOnSelect === `if-mobile` && window_width &&
|
|
140
|
+
window_width < breakpoint);
|
|
141
|
+
const should_retain_focus = closeDropdownOnSelect === `retain-focus`;
|
|
140
142
|
if (reached_max_select || dropdown_should_close) {
|
|
141
|
-
close_dropdown(event);
|
|
143
|
+
close_dropdown(event, should_retain_focus);
|
|
142
144
|
}
|
|
143
145
|
else if (!dropdown_should_close) {
|
|
144
146
|
input?.focus();
|
|
@@ -183,9 +185,10 @@ function open_dropdown(event) {
|
|
|
183
185
|
}
|
|
184
186
|
onopen?.({ event });
|
|
185
187
|
}
|
|
186
|
-
function close_dropdown(event) {
|
|
188
|
+
function close_dropdown(event, retain_focus = false) {
|
|
187
189
|
open = false;
|
|
188
|
-
|
|
190
|
+
if (!retain_focus)
|
|
191
|
+
input?.blur();
|
|
189
192
|
activeIndex = null;
|
|
190
193
|
onclose?.({ event });
|
|
191
194
|
}
|
|
@@ -196,8 +199,7 @@ async function handle_keydown(event) {
|
|
|
196
199
|
event.stopPropagation();
|
|
197
200
|
close_dropdown(event);
|
|
198
201
|
searchText = ``;
|
|
199
|
-
}
|
|
200
|
-
// on enter key: toggle active option and reset search text
|
|
202
|
+
} // on enter key: toggle active option and reset search text
|
|
201
203
|
else if (event.key === `Enter`) {
|
|
202
204
|
event.stopPropagation();
|
|
203
205
|
event.preventDefault(); // prevent enter key from triggering form submission
|
|
@@ -217,8 +219,7 @@ async function handle_keydown(event) {
|
|
|
217
219
|
// in which case enter means open it
|
|
218
220
|
open_dropdown(event);
|
|
219
221
|
}
|
|
220
|
-
}
|
|
221
|
-
// on up/down arrow keys: update active option
|
|
222
|
+
} // on up/down arrow keys: update active option
|
|
222
223
|
else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
|
|
223
224
|
event.stopPropagation();
|
|
224
225
|
// if no option is active yet, but there are matching options, make first one active
|
|
@@ -253,14 +254,12 @@ async function handle_keydown(event) {
|
|
|
253
254
|
if (li)
|
|
254
255
|
li.scrollIntoViewIfNeeded?.();
|
|
255
256
|
}
|
|
256
|
-
}
|
|
257
|
-
// on backspace key: remove last selected option
|
|
257
|
+
} // on backspace key: remove last selected option
|
|
258
258
|
else if (event.key === `Backspace` && selected.length > 0 && !searchText) {
|
|
259
259
|
event.stopPropagation();
|
|
260
260
|
// Don't prevent default, allow normal backspace behavior if not removing
|
|
261
261
|
remove(selected.at(-1), event);
|
|
262
|
-
}
|
|
263
|
-
// make first matching option active on any keypress (if none of the above special cases match)
|
|
262
|
+
} // make first matching option active on any keypress (if none of the above special cases match)
|
|
264
263
|
else if (matchingOptions.length > 0 && activeIndex === null) {
|
|
265
264
|
// Don't stop propagation or prevent default here, allow normal character input
|
|
266
265
|
activeIndex = 0;
|
|
@@ -268,10 +267,11 @@ async function handle_keydown(event) {
|
|
|
268
267
|
}
|
|
269
268
|
function remove_all(event) {
|
|
270
269
|
event.stopPropagation();
|
|
270
|
+
selected = []; // Set selected first
|
|
271
|
+
searchText = ``;
|
|
272
|
+
// Now trigger change events
|
|
271
273
|
onremoveAll?.({ options: selected });
|
|
272
274
|
onchange?.({ options: selected, type: `removeAll` });
|
|
273
|
-
selected = [];
|
|
274
|
-
searchText = ``;
|
|
275
275
|
}
|
|
276
276
|
let is_selected = $derived((label) => selected.map(get_label).includes(label));
|
|
277
277
|
const if_enter_or_space = (handler) => (event) => {
|
|
@@ -329,10 +329,38 @@ const handle_input_keydown = (event) => {
|
|
|
329
329
|
onkeydown?.(event);
|
|
330
330
|
};
|
|
331
331
|
const handle_input_focus = (event) => {
|
|
332
|
-
open_dropdown(event);
|
|
333
|
-
// Call original forwarded handler
|
|
332
|
+
open_dropdown(event);
|
|
334
333
|
onfocus?.(event);
|
|
335
334
|
};
|
|
335
|
+
// Override input's focus method to ensure dropdown opens on programmatic focus
|
|
336
|
+
// https://github.com/janosh/svelte-multiselect/issues/289
|
|
337
|
+
$effect(() => {
|
|
338
|
+
if (!input)
|
|
339
|
+
return;
|
|
340
|
+
const orig_focus = input.focus.bind(input);
|
|
341
|
+
input.focus = (options) => {
|
|
342
|
+
orig_focus(options);
|
|
343
|
+
if (!disabled && !open) {
|
|
344
|
+
open_dropdown(new FocusEvent(`focus`, { bubbles: true }));
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
return () => {
|
|
348
|
+
if (input)
|
|
349
|
+
input.focus = orig_focus;
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
const handle_input_blur = (event) => {
|
|
353
|
+
// For portalled dropdowns, don't close on blur since clicks on portalled elements
|
|
354
|
+
// will cause blur but we want to allow the click to register first
|
|
355
|
+
if (portal_params?.active) {
|
|
356
|
+
onblur?.(event); // Let the click handler manage closing for portalled dropdowns
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// For non-portalled dropdowns, close when focus moves outside the component
|
|
360
|
+
if (!outerDiv?.contains(event.relatedTarget))
|
|
361
|
+
close_dropdown(event);
|
|
362
|
+
onblur?.(event); // Call original handler (if any passed as component prop)
|
|
363
|
+
};
|
|
336
364
|
// reset form validation when required prop changes
|
|
337
365
|
// https://github.com/janosh/svelte-multiselect/issues/285
|
|
338
366
|
$effect.pre(() => {
|
|
@@ -343,7 +371,8 @@ function portal(node, params) {
|
|
|
343
371
|
let { target_node, active } = params;
|
|
344
372
|
if (!active)
|
|
345
373
|
return;
|
|
346
|
-
let render_in_place =
|
|
374
|
+
let render_in_place = typeof window === `undefined` ||
|
|
375
|
+
!document.body.contains(node);
|
|
347
376
|
if (!render_in_place) {
|
|
348
377
|
document.body.appendChild(node);
|
|
349
378
|
node.style.position = `fixed`;
|
|
@@ -369,7 +398,8 @@ function portal(node, params) {
|
|
|
369
398
|
return {
|
|
370
399
|
update(params) {
|
|
371
400
|
target_node = params.target_node;
|
|
372
|
-
render_in_place =
|
|
401
|
+
render_in_place = typeof window === `undefined` ||
|
|
402
|
+
!document.body.contains(node);
|
|
373
403
|
if (open && !render_in_place && target_node)
|
|
374
404
|
tick().then(update_position);
|
|
375
405
|
else if (!open || !target_node)
|
|
@@ -433,7 +463,11 @@ function portal(node, params) {
|
|
|
433
463
|
{#if expandIcon}
|
|
434
464
|
{@render expandIcon({ open })}
|
|
435
465
|
{:else}
|
|
436
|
-
<
|
|
466
|
+
<Icon
|
|
467
|
+
icon="ChevronExpand"
|
|
468
|
+
width="15px"
|
|
469
|
+
style="min-width: 1em; padding: 0 1pt; cursor: pointer"
|
|
470
|
+
/>
|
|
437
471
|
{/if}
|
|
438
472
|
<ul
|
|
439
473
|
class="selected {ulSelectedClass}"
|
|
@@ -442,7 +476,9 @@ function portal(node, params) {
|
|
|
442
476
|
>
|
|
443
477
|
{#each selected as option, idx (duplicates ? [key(option), idx] : key(option))}
|
|
444
478
|
{@const selectedOptionStyle =
|
|
445
|
-
[get_style(option, `selected`), liSelectedStyle].filter(Boolean).join(
|
|
479
|
+
[get_style(option, `selected`), liSelectedStyle].filter(Boolean).join(
|
|
480
|
+
` `,
|
|
481
|
+
) ||
|
|
446
482
|
null}
|
|
447
483
|
<li
|
|
448
484
|
class={liSelectedClass}
|
|
@@ -461,14 +497,14 @@ function portal(node, params) {
|
|
|
461
497
|
>
|
|
462
498
|
{#if selectedItem}
|
|
463
499
|
{@render selectedItem({
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
500
|
+
option,
|
|
501
|
+
idx,
|
|
502
|
+
})}
|
|
467
503
|
{:else if children}
|
|
468
504
|
{@render children({
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
505
|
+
option,
|
|
506
|
+
idx,
|
|
507
|
+
})}
|
|
472
508
|
{:else if parseLabelsAsHtml}
|
|
473
509
|
{@html get_label(option)}
|
|
474
510
|
{:else}
|
|
@@ -485,7 +521,7 @@ function portal(node, params) {
|
|
|
485
521
|
{#if removeIcon}
|
|
486
522
|
{@render removeIcon()}
|
|
487
523
|
{:else}
|
|
488
|
-
<
|
|
524
|
+
<Icon icon="Cross" width="15px" />
|
|
489
525
|
{/if}
|
|
490
526
|
</button>
|
|
491
527
|
{/if}
|
|
@@ -508,7 +544,7 @@ function portal(node, params) {
|
|
|
508
544
|
onkeydown={handle_input_keydown}
|
|
509
545
|
onfocus={handle_input_focus}
|
|
510
546
|
oninput={highlight_matching_options}
|
|
511
|
-
{
|
|
547
|
+
onblur={handle_input_blur}
|
|
512
548
|
{onclick}
|
|
513
549
|
{onkeyup}
|
|
514
550
|
{onmousedown}
|
|
@@ -520,16 +556,15 @@ function portal(node, params) {
|
|
|
520
556
|
{ontouchstart}
|
|
521
557
|
{...rest}
|
|
522
558
|
/>
|
|
523
|
-
<!-- the above on:* lines forward potentially useful DOM events -->
|
|
524
559
|
{@render afterInput?.({
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
560
|
+
selected,
|
|
561
|
+
disabled,
|
|
562
|
+
invalid,
|
|
563
|
+
id,
|
|
564
|
+
placeholder,
|
|
565
|
+
open,
|
|
566
|
+
required,
|
|
567
|
+
})}
|
|
533
568
|
</ul>
|
|
534
569
|
{#if loading}
|
|
535
570
|
{#if spinner}
|
|
@@ -542,7 +577,12 @@ function portal(node, params) {
|
|
|
542
577
|
{#if disabledIcon}
|
|
543
578
|
{@render disabledIcon()}
|
|
544
579
|
{:else}
|
|
545
|
-
<
|
|
580
|
+
<Icon
|
|
581
|
+
icon="Disabled"
|
|
582
|
+
width="14pt"
|
|
583
|
+
style="margin: 0 2pt"
|
|
584
|
+
data-name="disabled-icon"
|
|
585
|
+
/>
|
|
546
586
|
{/if}
|
|
547
587
|
{:else if selected.length > 0}
|
|
548
588
|
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
|
|
@@ -563,7 +603,7 @@ function portal(node, params) {
|
|
|
563
603
|
{#if removeIcon}
|
|
564
604
|
{@render removeIcon()}
|
|
565
605
|
{:else}
|
|
566
|
-
<
|
|
606
|
+
<Icon icon="Cross" width="15px" />
|
|
567
607
|
{/if}
|
|
568
608
|
</button>
|
|
569
609
|
{/if}
|
|
@@ -582,25 +622,29 @@ function portal(node, params) {
|
|
|
582
622
|
bind:this={ul_options}
|
|
583
623
|
style={ulOptionsStyle}
|
|
584
624
|
>
|
|
585
|
-
{#each matchingOptions.slice(0, Math.max(0, maxOptions ?? 0) || Infinity) as
|
|
625
|
+
{#each matchingOptions.slice(0, Math.max(0, maxOptions ?? 0) || Infinity) as
|
|
626
|
+
optionItem,
|
|
627
|
+
idx
|
|
628
|
+
(duplicates ? [key(optionItem), idx] : key(optionItem))
|
|
629
|
+
}
|
|
586
630
|
{@const {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
631
|
+
label,
|
|
632
|
+
disabled = null,
|
|
633
|
+
title = null,
|
|
634
|
+
selectedTitle = null,
|
|
635
|
+
disabledTitle = defaultDisabledTitle,
|
|
636
|
+
} = optionItem instanceof Object ? optionItem : { label: optionItem }}
|
|
593
637
|
{@const active = activeIndex === idx}
|
|
594
638
|
{@const optionStyle =
|
|
595
|
-
|
|
596
|
-
|
|
639
|
+
[get_style(optionItem, `option`), liOptionStyle].filter(Boolean).join(
|
|
640
|
+
` `,
|
|
641
|
+
) ||
|
|
642
|
+
null}
|
|
597
643
|
<li
|
|
598
644
|
onclick={(event) => {
|
|
599
645
|
if (!disabled) add(optionItem, event)
|
|
600
646
|
}}
|
|
601
|
-
title={disabled
|
|
602
|
-
? disabledTitle
|
|
603
|
-
: (is_selected(label) && selectedTitle) || title}
|
|
647
|
+
title={disabled ? disabledTitle : (is_selected(label) && selectedTitle) || title}
|
|
604
648
|
class:selected={is_selected(label)}
|
|
605
649
|
class:active
|
|
606
650
|
class:disabled
|
|
@@ -623,14 +667,14 @@ function portal(node, params) {
|
|
|
623
667
|
>
|
|
624
668
|
{#if option}
|
|
625
669
|
{@render option({
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
670
|
+
option: optionItem,
|
|
671
|
+
idx,
|
|
672
|
+
})}
|
|
629
673
|
{:else if children}
|
|
630
674
|
{@render children({
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
675
|
+
option: optionItem,
|
|
676
|
+
idx,
|
|
677
|
+
})}
|
|
634
678
|
{:else if parseLabelsAsHtml}
|
|
635
679
|
{@html get_label(optionItem)}
|
|
636
680
|
{:else}
|
|
@@ -642,15 +686,15 @@ function portal(node, params) {
|
|
|
642
686
|
{@const text_input_is_duplicate = selected.map(get_label).includes(searchText)}
|
|
643
687
|
{@const is_dupe = !duplicates && text_input_is_duplicate && `dupe`}
|
|
644
688
|
{@const can_create = Boolean(allowUserOptions && createOptionMsg) && `create`}
|
|
645
|
-
{@const no_match =
|
|
646
|
-
|
|
689
|
+
{@const no_match = Boolean(matchingOptions?.length == 0 && noMatchingOptionsMsg) &&
|
|
690
|
+
`no-match`}
|
|
647
691
|
{@const msgType = is_dupe || can_create || no_match}
|
|
648
692
|
{#if msgType}
|
|
649
693
|
{@const msg = {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
694
|
+
dupe: duplicateOptionMsg,
|
|
695
|
+
create: createOptionMsg,
|
|
696
|
+
'no-match': noMatchingOptionsMsg,
|
|
697
|
+
}[msgType]}
|
|
654
698
|
<li
|
|
655
699
|
onclick={(event) => {
|
|
656
700
|
if (msgType === `create` && allowUserOptions) {
|
|
@@ -668,10 +712,10 @@ function portal(node, params) {
|
|
|
668
712
|
}
|
|
669
713
|
}}
|
|
670
714
|
title={msgType === `create`
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
715
|
+
? createOptionMsg
|
|
716
|
+
: msgType === `dupe`
|
|
717
|
+
? duplicateOptionMsg
|
|
718
|
+
: ``}
|
|
675
719
|
class:active={option_msg_is_active}
|
|
676
720
|
onmouseover={() => (option_msg_is_active = true)}
|
|
677
721
|
onfocus={() => (option_msg_is_active = true)}
|
|
@@ -679,9 +723,11 @@ function portal(node, params) {
|
|
|
679
723
|
onblur={() => (option_msg_is_active = false)}
|
|
680
724
|
role="option"
|
|
681
725
|
aria-selected="false"
|
|
682
|
-
class="
|
|
726
|
+
class="
|
|
727
|
+
user-msg {liUserMsgClass} {option_msg_is_active
|
|
683
728
|
? liActiveUserMsgClass
|
|
684
|
-
: ``}
|
|
729
|
+
: ``}
|
|
730
|
+
"
|
|
685
731
|
style:cursor={{
|
|
686
732
|
dupe: `not-allowed`,
|
|
687
733
|
create: `pointer`,
|
|
@@ -792,6 +838,12 @@ function portal(node, params) {
|
|
|
792
838
|
cursor: inherit; /* needed for disabled state */
|
|
793
839
|
border-radius: 0; /* reset ul.selected > li */
|
|
794
840
|
}
|
|
841
|
+
|
|
842
|
+
/* When options are selected, placeholder is hidden in which case we minimize input width to avoid adding unnecessary width to div.multiselect */
|
|
843
|
+
:is(div.multiselect > ul.selected > input:not(:placeholder-shown)) {
|
|
844
|
+
min-width: 1px; /* Minimal width to remain interactive */
|
|
845
|
+
}
|
|
846
|
+
|
|
795
847
|
/* don't wrap ::placeholder rules in :is() as it seems to be overpowered by browser defaults i.t.o. specificity */
|
|
796
848
|
div.multiselect > ul.selected > input::placeholder {
|
|
797
849
|
padding-left: 5pt;
|
|
@@ -821,7 +873,8 @@ function portal(node, params) {
|
|
|
821
873
|
z-index: var(--sms-options-z-index, 3);
|
|
822
874
|
|
|
823
875
|
overflow: auto;
|
|
824
|
-
transition: all
|
|
876
|
+
transition: all
|
|
877
|
+
0.2s; /* Consider if this transition is desirable with portal positioning */
|
|
825
878
|
box-sizing: border-box;
|
|
826
879
|
background: var(--sms-options-bg, white);
|
|
827
880
|
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>
|