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