svelte-multiselect 8.5.0 → 8.6.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/CmdPalette.svelte +2 -2
- package/dist/CmdPalette.svelte.d.ts +2 -2
- package/dist/MultiSelect.svelte +107 -61
- package/dist/MultiSelect.svelte.d.ts +2 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +7 -7
- package/package.json +23 -23
- package/readme.md +22 -7
package/dist/CmdPalette.svelte
CHANGED
|
@@ -10,8 +10,8 @@ export let style = ``; // for dialog
|
|
|
10
10
|
// for span in option slot, has no effect when passing slot="option"
|
|
11
11
|
export let span_style = ``;
|
|
12
12
|
export let open = false;
|
|
13
|
-
export let dialog;
|
|
14
|
-
export let input;
|
|
13
|
+
export let dialog = null;
|
|
14
|
+
export let input = null;
|
|
15
15
|
export let placeholder = `Filter actions...`;
|
|
16
16
|
async function toggle(event) {
|
|
17
17
|
if (event.key === trigger && event.metaKey && !open) {
|
|
@@ -11,8 +11,8 @@ declare const __propDef: {
|
|
|
11
11
|
style?: string | undefined;
|
|
12
12
|
span_style?: string | undefined;
|
|
13
13
|
open?: boolean | undefined;
|
|
14
|
-
dialog
|
|
15
|
-
input
|
|
14
|
+
dialog?: HTMLDialogElement | null | undefined;
|
|
15
|
+
input?: HTMLInputElement | null | undefined;
|
|
16
16
|
placeholder?: string | undefined;
|
|
17
17
|
};
|
|
18
18
|
events: {
|
package/dist/MultiSelect.svelte
CHANGED
|
@@ -25,6 +25,7 @@ export let filterFunc = (op, searchText) => {
|
|
|
25
25
|
};
|
|
26
26
|
export let focusInputOnSelect = `desktop`;
|
|
27
27
|
export let form_input = null;
|
|
28
|
+
export let highlightMatches = true;
|
|
28
29
|
export let id = null;
|
|
29
30
|
export let input = null;
|
|
30
31
|
export let inputClass = ``;
|
|
@@ -99,13 +100,18 @@ if (parseLabelsAsHtml && allowUserOptions) {
|
|
|
99
100
|
console.warn(`Don't combine parseLabelsAsHtml and allowUserOptions. It's susceptible to XSS attacks!`);
|
|
100
101
|
}
|
|
101
102
|
if (sortSelected && selectedOptionsDraggable) {
|
|
102
|
-
console.warn(`MultiSelect's sortSelected and selectedOptionsDraggable should not be combined as any
|
|
103
|
+
console.warn(`MultiSelect's sortSelected and selectedOptionsDraggable should not be combined as any ` +
|
|
104
|
+
`user re-orderings of selected options will be undone by sortSelected on component re-renders.`);
|
|
105
|
+
}
|
|
106
|
+
if (allowUserOptions && !createOptionMsg) {
|
|
107
|
+
console.error(`MultiSelect's allowUserOptions=${allowUserOptions} but createOptionMsg=${createOptionMsg} is falsy. ` +
|
|
108
|
+
`This prevents the "Add option" <span> from showing up, resulting in a confusing user experience.`);
|
|
103
109
|
}
|
|
104
110
|
const dispatch = createEventDispatcher();
|
|
105
|
-
let
|
|
111
|
+
let option_msg_is_active = false; // controls active state of <li>{createOptionMsg}</li>
|
|
106
112
|
let window_width;
|
|
107
113
|
// options matching the current search text
|
|
108
|
-
$: matchingOptions = options.filter((op) => filterFunc(op, searchText) && !selected.
|
|
114
|
+
$: matchingOptions = options.filter((op) => filterFunc(op, searchText) && !selected.includes(op) // remove already selected options from dropdown list
|
|
109
115
|
);
|
|
110
116
|
// raise if matchingOptions[activeIndex] does not yield a value
|
|
111
117
|
if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
@@ -114,21 +120,21 @@ if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
|
|
114
120
|
// update activeOption when activeIndex changes
|
|
115
121
|
$: activeOption = matchingOptions[activeIndex ?? -1] ?? null;
|
|
116
122
|
// add an option to selected list
|
|
117
|
-
function add(
|
|
123
|
+
function add(option, event) {
|
|
118
124
|
if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
|
|
119
125
|
wiggle = true;
|
|
120
|
-
if (!isNaN(Number(
|
|
121
|
-
|
|
122
|
-
|
|
126
|
+
if (!isNaN(Number(option)) && typeof selected.map(get_label)[0] === `number`) {
|
|
127
|
+
option = Number(option); // convert to number if possible
|
|
128
|
+
}
|
|
129
|
+
const is_duplicate = selected.some((op) => duplicateFunc(op, option));
|
|
123
130
|
if ((maxSelect === null || maxSelect === 1 || selected.length < maxSelect) &&
|
|
124
131
|
(duplicates || !is_duplicate)) {
|
|
125
|
-
// first check if we find option in the options list
|
|
126
|
-
|
|
127
|
-
if (!option && // this has the side-effect of not allowing to user to add the same
|
|
132
|
+
if (!options.includes(option) && // first check if we find option in the options list
|
|
133
|
+
// this has the side-effect of not allowing to user to add the same
|
|
128
134
|
// custom option twice in append mode
|
|
129
135
|
[true, `append`].includes(allowUserOptions) &&
|
|
130
136
|
searchText.length > 0) {
|
|
131
|
-
// user entered text but no options match, so if allowUserOptions=true | 'append', we create
|
|
137
|
+
// user entered text but no options match, so if allowUserOptions = true | 'append', we create
|
|
132
138
|
// a new option from the user-entered text
|
|
133
139
|
if (typeof options[0] === `object`) {
|
|
134
140
|
// if 1st option is an object, we create new option as object to keep type homogeneity
|
|
@@ -140,19 +146,18 @@ function add(label, event) {
|
|
|
140
146
|
// create new option as number if it parses to a number and 1st option is also number or missing
|
|
141
147
|
option = Number(searchText);
|
|
142
148
|
}
|
|
143
|
-
else
|
|
149
|
+
else {
|
|
144
150
|
option = searchText; // else create custom option as string
|
|
151
|
+
}
|
|
152
|
+
dispatch(`create`, { option });
|
|
145
153
|
}
|
|
146
154
|
if (allowUserOptions === `append`)
|
|
147
155
|
options = [...options, option];
|
|
148
156
|
}
|
|
149
|
-
if (option === undefined) {
|
|
150
|
-
throw `Run time error, option with label ${label} not found in options list`;
|
|
151
|
-
}
|
|
152
157
|
if (resetFilterOnAdd)
|
|
153
158
|
searchText = ``; // reset search string on selection
|
|
154
159
|
if ([``, undefined, null].includes(option)) {
|
|
155
|
-
console.error(`MultiSelect: encountered
|
|
160
|
+
console.error(`MultiSelect: encountered falsy option ${option}`);
|
|
156
161
|
return;
|
|
157
162
|
}
|
|
158
163
|
if (maxSelect === 1) {
|
|
@@ -199,10 +204,10 @@ function remove(label) {
|
|
|
199
204
|
return console.error(`Multiselect can't remove selected option ${label}, not found in selected list`);
|
|
200
205
|
}
|
|
201
206
|
selected = selected.filter((op) => get_label(op) !== label); // remove option from selected list
|
|
202
|
-
dispatch(`remove`, { option });
|
|
203
|
-
dispatch(`change`, { option, type: `remove` });
|
|
204
207
|
invalid = false; // reset error status whenever items are removed
|
|
205
208
|
form_input?.setCustomValidity(``);
|
|
209
|
+
dispatch(`remove`, { option });
|
|
210
|
+
dispatch(`change`, { option, type: `remove` });
|
|
206
211
|
}
|
|
207
212
|
function open_dropdown(event) {
|
|
208
213
|
if (disabled)
|
|
@@ -231,8 +236,7 @@ async function handle_keydown(event) {
|
|
|
231
236
|
else if (event.key === `Enter`) {
|
|
232
237
|
event.preventDefault(); // prevent enter key from triggering form submission
|
|
233
238
|
if (activeOption) {
|
|
234
|
-
|
|
235
|
-
selected.map(get_label).includes(label) ? remove(label) : add(label, event);
|
|
239
|
+
selected.includes(activeOption) ? remove(activeOption) : add(activeOption, event);
|
|
236
240
|
searchText = ``;
|
|
237
241
|
}
|
|
238
242
|
else if (allowUserOptions && searchText.length > 0) {
|
|
@@ -254,7 +258,7 @@ async function handle_keydown(event) {
|
|
|
254
258
|
else if (allowUserOptions && !matchingOptions.length && searchText.length > 0) {
|
|
255
259
|
// if allowUserOptions is truthy and user entered text but no options match, we make
|
|
256
260
|
// <li>{addUserMsg}</li> active on keydown (or toggle it if already active)
|
|
257
|
-
|
|
261
|
+
option_msg_is_active = !option_msg_is_active;
|
|
258
262
|
return;
|
|
259
263
|
}
|
|
260
264
|
else if (activeIndex === null) {
|
|
@@ -279,7 +283,7 @@ async function handle_keydown(event) {
|
|
|
279
283
|
}
|
|
280
284
|
// on backspace key: remove last selected option
|
|
281
285
|
else if (event.key === `Backspace` && selected.length > 0 && !searchText) {
|
|
282
|
-
remove(selected.
|
|
286
|
+
remove(selected.at(-1));
|
|
283
287
|
}
|
|
284
288
|
// make first matching option active on any keypress (if none of the above special cases match)
|
|
285
289
|
else if (matchingOptions.length > 0) {
|
|
@@ -331,6 +335,54 @@ const dragstart = (idx) => (event) => {
|
|
|
331
335
|
event.dataTransfer.dropEffect = `move`;
|
|
332
336
|
event.dataTransfer.setData(`text/plain`, `${idx}`);
|
|
333
337
|
};
|
|
338
|
+
let ul_options;
|
|
339
|
+
function highlight_matching_options(event) {
|
|
340
|
+
if (!highlightMatches || typeof CSS == `undefined` || !CSS.highlights)
|
|
341
|
+
return; // don't try if CSS highlight API not supported
|
|
342
|
+
// clear previous ranges from HighlightRegistry
|
|
343
|
+
CSS.highlights.clear();
|
|
344
|
+
// get input's search query
|
|
345
|
+
const query = event?.target?.value.trim().toLowerCase();
|
|
346
|
+
if (!query)
|
|
347
|
+
return;
|
|
348
|
+
const tree_walker = document.createTreeWalker(ul_options, NodeFilter.SHOW_TEXT, {
|
|
349
|
+
acceptNode: (node) => {
|
|
350
|
+
// don't highlight text in the "no matching options" message
|
|
351
|
+
if (node?.textContent === noMatchingOptionsMsg)
|
|
352
|
+
return NodeFilter.FILTER_REJECT;
|
|
353
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
const text_nodes = [];
|
|
357
|
+
let current_node = tree_walker.nextNode();
|
|
358
|
+
while (current_node) {
|
|
359
|
+
text_nodes.push(current_node);
|
|
360
|
+
current_node = tree_walker.nextNode();
|
|
361
|
+
}
|
|
362
|
+
// iterate over all text nodes and find matches
|
|
363
|
+
const ranges = text_nodes.map((el) => {
|
|
364
|
+
const text = el.textContent?.toLowerCase();
|
|
365
|
+
const indices = [];
|
|
366
|
+
let start_pos = 0;
|
|
367
|
+
while (text && start_pos < text.length) {
|
|
368
|
+
const index = text.indexOf(query, start_pos);
|
|
369
|
+
if (index === -1)
|
|
370
|
+
break;
|
|
371
|
+
indices.push(index);
|
|
372
|
+
start_pos = index + query.length;
|
|
373
|
+
}
|
|
374
|
+
// create range object for each str found in the text node
|
|
375
|
+
return indices.map((index) => {
|
|
376
|
+
const range = new Range();
|
|
377
|
+
range.setStart(el, index);
|
|
378
|
+
range.setEnd(el, index + query.length);
|
|
379
|
+
return range;
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
// create Highlight object from ranges and add to registry
|
|
383
|
+
// eslint-disable-next-line no-undef
|
|
384
|
+
CSS.highlights.set(`sms-search-matches`, new Highlight(...ranges.flat()));
|
|
385
|
+
}
|
|
334
386
|
</script>
|
|
335
387
|
|
|
336
388
|
<svelte:window
|
|
@@ -354,7 +406,7 @@ const dragstart = (idx) => (event) => {
|
|
|
354
406
|
<input
|
|
355
407
|
{name}
|
|
356
408
|
required={Boolean(required)}
|
|
357
|
-
value={selected.length >= required ? JSON.stringify(selected) : null}
|
|
409
|
+
value={selected.length >= Number(required) ? JSON.stringify(selected) : null}
|
|
358
410
|
tabindex="-1"
|
|
359
411
|
aria-hidden="true"
|
|
360
412
|
aria-label="ignore this, used only to prevent form submission if select is required but empty"
|
|
@@ -363,9 +415,9 @@ const dragstart = (idx) => (event) => {
|
|
|
363
415
|
on:invalid={() => {
|
|
364
416
|
invalid = true
|
|
365
417
|
let msg
|
|
366
|
-
if (maxSelect && maxSelect > 1 && required > 1) {
|
|
418
|
+
if (maxSelect && maxSelect > 1 && Number(required) > 1) {
|
|
367
419
|
msg = `Please select between ${required} and ${maxSelect} options`
|
|
368
|
-
} else if (required > 1) {
|
|
420
|
+
} else if (Number(required) > 1) {
|
|
369
421
|
msg = `Please select at least ${required} options`
|
|
370
422
|
} else {
|
|
371
423
|
msg = `Please select an option`
|
|
@@ -376,17 +428,10 @@ const dragstart = (idx) => (event) => {
|
|
|
376
428
|
<slot name="expand-icon" {open}>
|
|
377
429
|
<ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt; cursor: pointer;" />
|
|
378
430
|
</slot>
|
|
379
|
-
<ul
|
|
380
|
-
|
|
381
|
-
role="listbox"
|
|
382
|
-
aria-multiselectable={maxSelect === null || maxSelect > 1}
|
|
383
|
-
aria-label="selected options"
|
|
384
|
-
>
|
|
385
|
-
{#each selected as option, idx (get_label(option))}
|
|
431
|
+
<ul class="selected {ulSelectedClass}" aria-label="selected options">
|
|
432
|
+
{#each selected as option, idx (option)}
|
|
386
433
|
<li
|
|
387
434
|
class={liSelectedClass}
|
|
388
|
-
role="option"
|
|
389
|
-
aria-selected="true"
|
|
390
435
|
animate:flip={{ duration: 100 }}
|
|
391
436
|
draggable={selectedOptionsDraggable && !disabled && selected.length > 1}
|
|
392
437
|
on:dragstart={dragstart(idx)}
|
|
@@ -404,8 +449,8 @@ const dragstart = (idx) => (event) => {
|
|
|
404
449
|
</slot>
|
|
405
450
|
{#if !disabled && (minSelect === null || selected.length > minSelect)}
|
|
406
451
|
<button
|
|
407
|
-
on:mouseup|stopPropagation={() => remove(
|
|
408
|
-
on:keydown={if_enter_or_space(() => remove(
|
|
452
|
+
on:mouseup|stopPropagation={() => remove(option)}
|
|
453
|
+
on:keydown={if_enter_or_space(() => remove(option))}
|
|
409
454
|
type="button"
|
|
410
455
|
title="{removeBtnTitle} {get_label(option)}"
|
|
411
456
|
class="remove"
|
|
@@ -425,6 +470,7 @@ const dragstart = (idx) => (event) => {
|
|
|
425
470
|
on:keydown|stopPropagation={handle_keydown}
|
|
426
471
|
on:focus
|
|
427
472
|
on:focus={open_dropdown}
|
|
473
|
+
on:input={highlight_matching_options}
|
|
428
474
|
{id}
|
|
429
475
|
{disabled}
|
|
430
476
|
{autocomplete}
|
|
@@ -482,14 +528,7 @@ const dragstart = (idx) => (event) => {
|
|
|
482
528
|
|
|
483
529
|
<!-- only render options dropdown if options or searchText is not empty needed to avoid briefly flashing empty dropdown -->
|
|
484
530
|
{#if (searchText && noMatchingOptionsMsg) || options?.length > 0}
|
|
485
|
-
<ul
|
|
486
|
-
class:hidden={!open}
|
|
487
|
-
class="options {ulOptionsClass}"
|
|
488
|
-
role="listbox"
|
|
489
|
-
aria-multiselectable={maxSelect === null || maxSelect > 1}
|
|
490
|
-
aria-expanded={open}
|
|
491
|
-
aria-disabled={disabled ? `true` : null}
|
|
492
|
-
>
|
|
531
|
+
<ul class:hidden={!open} class="options {ulOptionsClass}" bind:this={ul_options}>
|
|
493
532
|
{#each matchingOptions as option, idx}
|
|
494
533
|
{@const {
|
|
495
534
|
label,
|
|
@@ -502,7 +541,7 @@ const dragstart = (idx) => (event) => {
|
|
|
502
541
|
<li
|
|
503
542
|
on:mousedown|stopPropagation
|
|
504
543
|
on:mouseup|stopPropagation={(event) => {
|
|
505
|
-
if (!disabled) add(
|
|
544
|
+
if (!disabled) add(option, event)
|
|
506
545
|
}}
|
|
507
546
|
title={disabled
|
|
508
547
|
? disabledTitle
|
|
@@ -519,8 +558,6 @@ const dragstart = (idx) => (event) => {
|
|
|
519
558
|
}}
|
|
520
559
|
on:mouseout={() => (activeIndex = null)}
|
|
521
560
|
on:blur={() => (activeIndex = null)}
|
|
522
|
-
role="option"
|
|
523
|
-
aria-selected="false"
|
|
524
561
|
>
|
|
525
562
|
<slot name="option" {option} {idx}>
|
|
526
563
|
{#if parseLabelsAsHtml}
|
|
@@ -531,25 +568,30 @@ const dragstart = (idx) => (event) => {
|
|
|
531
568
|
</slot>
|
|
532
569
|
</li>
|
|
533
570
|
{:else}
|
|
534
|
-
{
|
|
571
|
+
{@const search_is_duplicate = selected.some((option) =>
|
|
572
|
+
duplicateFunc(option, searchText)
|
|
573
|
+
)}
|
|
574
|
+
{@const msg =
|
|
575
|
+
!duplicates && search_is_duplicate ? duplicateOptionMsg : createOptionMsg}
|
|
576
|
+
{#if allowUserOptions && searchText && msg}
|
|
535
577
|
<li
|
|
536
578
|
on:mousedown|stopPropagation
|
|
537
579
|
on:mouseup|stopPropagation={(event) => add(searchText, event)}
|
|
538
580
|
title={createOptionMsg}
|
|
539
|
-
class:active={
|
|
540
|
-
on:mouseover={() => (
|
|
541
|
-
on:focus={() => (
|
|
542
|
-
on:mouseout={() => (
|
|
543
|
-
on:blur={() => (
|
|
544
|
-
|
|
581
|
+
class:active={option_msg_is_active}
|
|
582
|
+
on:mouseover={() => (option_msg_is_active = true)}
|
|
583
|
+
on:focus={() => (option_msg_is_active = true)}
|
|
584
|
+
on:mouseout={() => (option_msg_is_active = false)}
|
|
585
|
+
on:blur={() => (option_msg_is_active = false)}
|
|
586
|
+
class="user-msg"
|
|
545
587
|
>
|
|
546
|
-
{
|
|
547
|
-
? duplicateOptionMsg
|
|
548
|
-
: createOptionMsg}
|
|
588
|
+
{msg}
|
|
549
589
|
</li>
|
|
550
|
-
{:else}
|
|
551
|
-
|
|
590
|
+
{:else if noMatchingOptionsMsg}
|
|
591
|
+
<!-- use span to not have cursor: pointer -->
|
|
592
|
+
<span class="user-msg">{noMatchingOptionsMsg}</span>
|
|
552
593
|
{/if}
|
|
594
|
+
<!-- Show nothing if all messages are empty -->
|
|
553
595
|
{/each}
|
|
554
596
|
</ul>
|
|
555
597
|
{/if}
|
|
@@ -693,8 +735,9 @@ const dragstart = (idx) => (event) => {
|
|
|
693
735
|
cursor: pointer;
|
|
694
736
|
scroll-margin: var(--sms-options-scroll-margin, 100px);
|
|
695
737
|
}
|
|
696
|
-
|
|
697
|
-
|
|
738
|
+
:where(div.multiselect > ul.options .user-msg) {
|
|
739
|
+
/* block needed so vertical padding applies to span */
|
|
740
|
+
display: block;
|
|
698
741
|
padding: 3pt 2ex;
|
|
699
742
|
}
|
|
700
743
|
:where(div.multiselect > ul.options > li.selected) {
|
|
@@ -713,4 +756,7 @@ const dragstart = (idx) => (event) => {
|
|
|
713
756
|
:where(span.max-select-msg) {
|
|
714
757
|
padding: 0 3pt;
|
|
715
758
|
}
|
|
759
|
+
::highlight(sms-search-matches) {
|
|
760
|
+
color: mediumaquamarine;
|
|
761
|
+
}
|
|
716
762
|
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SvelteComponentTyped } from "svelte";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Option as GenericOption, MultiSelectEvents } from './';
|
|
3
3
|
declare class __sveltets_Render<Option extends GenericOption> {
|
|
4
4
|
props(): {
|
|
5
5
|
activeIndex?: number | null | undefined;
|
|
@@ -19,6 +19,7 @@ declare class __sveltets_Render<Option extends GenericOption> {
|
|
|
19
19
|
filterFunc?: ((op: Option, searchText: string) => boolean) | undefined;
|
|
20
20
|
focusInputOnSelect?: boolean | "desktop" | undefined;
|
|
21
21
|
form_input?: HTMLInputElement | null | undefined;
|
|
22
|
+
highlightMatches?: boolean | undefined;
|
|
22
23
|
id?: string | null | undefined;
|
|
23
24
|
input?: HTMLInputElement | null | undefined;
|
|
24
25
|
inputClass?: string | undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { default as CircleSpinner } from './CircleSpinner.svelte';
|
|
2
2
|
export { default as CmdPalette } from './CmdPalette.svelte';
|
|
3
|
-
export { default
|
|
3
|
+
export { default as MultiSelect, default } from './MultiSelect.svelte';
|
|
4
4
|
export { default as Wiggle } from './Wiggle.svelte';
|
|
5
5
|
export type Option = string | number | ObjectOption;
|
|
6
6
|
export type ObjectOption = {
|
|
@@ -17,6 +17,9 @@ export type DispatchEvents<T = Option> = {
|
|
|
17
17
|
add: {
|
|
18
18
|
option: T;
|
|
19
19
|
};
|
|
20
|
+
create: {
|
|
21
|
+
option: T;
|
|
22
|
+
};
|
|
20
23
|
remove: {
|
|
21
24
|
option: T;
|
|
22
25
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
export { default as CircleSpinner } from './CircleSpinner.svelte';
|
|
2
2
|
export { default as CmdPalette } from './CmdPalette.svelte';
|
|
3
|
-
export { default
|
|
3
|
+
export { default as MultiSelect, default } from './MultiSelect.svelte';
|
|
4
4
|
export { default as Wiggle } from './Wiggle.svelte';
|
|
5
|
-
// Firefox lacks support for scrollIntoViewIfNeeded
|
|
6
|
-
// https://github.com/janosh/svelte-multiselect/issues/87
|
|
7
|
-
//
|
|
5
|
+
// Firefox lacks support for scrollIntoViewIfNeeded (https://caniuse.com/scrollintoviewifneeded).
|
|
6
|
+
// See https://github.com/janosh/svelte-multiselect/issues/87
|
|
7
|
+
// Polyfill copied from
|
|
8
8
|
// https://github.com/nuxodin/lazyfill/blob/a8e63/polyfills/Element/prototype/scrollIntoViewIfNeeded.js
|
|
9
9
|
// exported for testing
|
|
10
10
|
export function scroll_into_view_if_needed_polyfill(centerIfNeeded = true) {
|
|
11
|
-
const
|
|
11
|
+
const elem = this;
|
|
12
12
|
const observer = new IntersectionObserver(function ([entry]) {
|
|
13
13
|
const ratio = entry.intersectionRatio;
|
|
14
14
|
if (ratio < 1) {
|
|
15
15
|
const place = ratio <= 0 && centerIfNeeded ? `center` : `nearest`;
|
|
16
|
-
|
|
16
|
+
elem.scrollIntoView({
|
|
17
17
|
block: place,
|
|
18
18
|
inline: place,
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
this.disconnect();
|
|
22
22
|
});
|
|
23
|
-
observer.observe(
|
|
23
|
+
observer.observe(elem);
|
|
24
24
|
return observer; // return for testing
|
|
25
25
|
}
|
|
26
26
|
if (typeof Element !== `undefined` &&
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"homepage": "https://janosh.github.io/svelte-multiselect",
|
|
6
6
|
"repository": "https://github.com/janosh/svelte-multiselect",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"version": "8.
|
|
8
|
+
"version": "8.6.1",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"svelte": "./dist/index.js",
|
|
11
11
|
"bugs": "https://github.com/janosh/svelte-multiselect/issues",
|
|
@@ -23,37 +23,37 @@
|
|
|
23
23
|
"update-coverage": "vitest tests/unit --run --coverage && npx istanbul-badges-readme"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"svelte": "^3.
|
|
26
|
+
"svelte": "^3.58.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@iconify/svelte": "^3.1.
|
|
30
|
-
"@playwright/test": "^1.
|
|
31
|
-
"@sveltejs/adapter-static": "^2.0.
|
|
32
|
-
"@sveltejs/kit": "^1.
|
|
29
|
+
"@iconify/svelte": "^3.1.3",
|
|
30
|
+
"@playwright/test": "^1.33.0",
|
|
31
|
+
"@sveltejs/adapter-static": "^2.0.2",
|
|
32
|
+
"@sveltejs/kit": "^1.15.9",
|
|
33
33
|
"@sveltejs/package": "2.0.2",
|
|
34
|
-
"@sveltejs/vite-plugin-svelte": "^2.
|
|
35
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
36
|
-
"@typescript-eslint/parser": "^5.
|
|
37
|
-
"@vitest/coverage-c8": "^0.
|
|
38
|
-
"eslint": "^8.
|
|
34
|
+
"@sveltejs/vite-plugin-svelte": "^2.1.1",
|
|
35
|
+
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
|
36
|
+
"@typescript-eslint/parser": "^5.59.1",
|
|
37
|
+
"@vitest/coverage-c8": "^0.30.1",
|
|
38
|
+
"eslint": "^8.39.0",
|
|
39
39
|
"eslint-plugin-svelte3": "^4.0.0",
|
|
40
40
|
"hastscript": "^7.2.0",
|
|
41
|
-
"highlight.js": "^11.
|
|
42
|
-
"jsdom": "^21.1.
|
|
41
|
+
"highlight.js": "^11.8.0",
|
|
42
|
+
"jsdom": "^21.1.1",
|
|
43
43
|
"mdsvex": "^0.10.6",
|
|
44
44
|
"mdsvexamples": "^0.3.3",
|
|
45
|
-
"prettier": "^2.8.
|
|
46
|
-
"prettier-plugin-svelte": "^2.
|
|
45
|
+
"prettier": "^2.8.8",
|
|
46
|
+
"prettier-plugin-svelte": "^2.10.0",
|
|
47
47
|
"rehype-autolink-headings": "^6.1.1",
|
|
48
48
|
"rehype-slug": "^5.1.0",
|
|
49
|
-
"svelte-check": "^3.
|
|
50
|
-
"svelte-preprocess": "^5.0.
|
|
51
|
-
"svelte-toc": "^0.5.
|
|
52
|
-
"svelte-zoo": "^0.
|
|
53
|
-
"svelte2tsx": "^0.6.
|
|
54
|
-
"typescript": "
|
|
55
|
-
"vite": "^4.
|
|
56
|
-
"vitest": "^0.
|
|
49
|
+
"svelte-check": "^3.2.0",
|
|
50
|
+
"svelte-preprocess": "^5.0.3",
|
|
51
|
+
"svelte-toc": "^0.5.5",
|
|
52
|
+
"svelte-zoo": "^0.4.5",
|
|
53
|
+
"svelte2tsx": "^0.6.11",
|
|
54
|
+
"typescript": "5.0.4",
|
|
55
|
+
"vite": "^4.3.3",
|
|
56
|
+
"vitest": "^0.30.1"
|
|
57
57
|
},
|
|
58
58
|
"keywords": [
|
|
59
59
|
"svelte",
|
package/readme.md
CHANGED
|
@@ -185,6 +185,12 @@ Full list of props/bindable variables for this component. The `Option` type you
|
|
|
185
185
|
|
|
186
186
|
Handle to the `<input>` DOM node that's responsible for form validity checks and passing selected options to form submission handlers. Only available after component mounts (`null` before then).
|
|
187
187
|
|
|
188
|
+
1. ```ts
|
|
189
|
+
highlightMatches: boolean = true
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Whether to highlight text in the dropdown options that matches the current user-entered search query. Uses the [CSS Custom Highlight API](https://developer.mozilla.org/docs/Web/API/CSS_Custom_Highlight_API) with limited browser support and [styling options](https://developer.mozilla.org/docs/Web/CSS/::highlight). See `::highlight(sms-search-matches)` below for available CSS variables.
|
|
193
|
+
|
|
188
194
|
1. ```ts
|
|
189
195
|
id: string | null = null
|
|
190
196
|
```
|
|
@@ -500,6 +506,12 @@ There are 3 ways to style this component. To understand which options do what, i
|
|
|
500
506
|
|
|
501
507
|
If you only want to make small adjustments, you can pass the following CSS variables directly to the component as props or define them in a `:global()` CSS context. See [`app.css`](https://github.com/janosh/svelte-multiselect/blob/main/src/app.css) for how these variables are set on the demo site of this component.
|
|
502
508
|
|
|
509
|
+
Minimal example that changes the background color of the options dropdown:
|
|
510
|
+
|
|
511
|
+
```svelte
|
|
512
|
+
<MultiSelect --sms-options-bg="white" />
|
|
513
|
+
```
|
|
514
|
+
|
|
503
515
|
- `div.multiselect`
|
|
504
516
|
- `border: var(--sms-border, 1pt solid lightgray)`: Change this to e.g. to `1px solid red` to indicate this form field is in an invalid state.
|
|
505
517
|
- `border-radius: var(--sms-border-radius, 3pt)`
|
|
@@ -514,7 +526,7 @@ If you only want to make small adjustments, you can pass the following CSS varia
|
|
|
514
526
|
- `div.multiselect.open`
|
|
515
527
|
- `z-index: var(--sms-open-z-index, 4)`: Increase this if needed to ensure the dropdown list is displayed atop all other page elements.
|
|
516
528
|
- `div.multiselect:focus-within`
|
|
517
|
-
- `border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue))`: Border when component has focus. Defaults to `--sms-active-color`
|
|
529
|
+
- `border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue))`: Border when component has focus. Defaults to `--sms-active-color` which in turn defaults to `cornflowerblue`.
|
|
518
530
|
- `div.multiselect.disabled`
|
|
519
531
|
- `background: var(--sms-disabled-bg, lightgray)`: Background when in disabled state.
|
|
520
532
|
- `div.multiselect input::placeholder`
|
|
@@ -547,12 +559,15 @@ If you only want to make small adjustments, you can pass the following CSS varia
|
|
|
547
559
|
- `div.multiselect > ul.options > li.disabled`
|
|
548
560
|
- `background: var(--sms-li-disabled-bg, #f5f5f6)`: Background of disabled options in the dropdown list.
|
|
549
561
|
- `color: var(--sms-li-disabled-text, #b8b8b8)`: Text color of disabled option in the dropdown list.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
562
|
+
- `::highlight(sms-search-matches)`: applies to search results in dropdown list that match the current search query if `highlightMatches=true`. These styles [cannot be set via CSS variables](https://stackoverflow.com/a/56799215). Instead, use a new rule set. For example:
|
|
563
|
+
|
|
564
|
+
```css
|
|
565
|
+
::highlight(sms-search-matches) {
|
|
566
|
+
color: orange;
|
|
567
|
+
background: rgba(0, 0, 0, 0.15);
|
|
568
|
+
text-decoration: underline;
|
|
569
|
+
}
|
|
570
|
+
```
|
|
556
571
|
|
|
557
572
|
### With CSS frameworks
|
|
558
573
|
|