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.
@@ -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: HTMLDialogElement;
15
- input: HTMLInputElement;
14
+ dialog?: HTMLDialogElement | null | undefined;
15
+ input?: HTMLInputElement | null | undefined;
16
16
  placeholder?: string | undefined;
17
17
  };
18
18
  events: {
@@ -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 user re-orderings of selected options will be undone by sortSelected on component re-renders.`);
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 add_option_msg_is_active = false; // controls active state of <li>{createOptionMsg}</li>
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.map(get_label).includes(get_label(op)) // remove already selected options from dropdown list
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(label, event) {
123
+ function add(option, event) {
118
124
  if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
119
125
  wiggle = true;
120
- if (!isNaN(Number(label)) && typeof selected.map(get_label)[0] === `number`)
121
- label = Number(label); // convert to number if possible
122
- const is_duplicate = selected.some((option) => duplicateFunc(option, label));
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
- let option = options.find((op) => get_label(op) === label);
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 missing option with label ${label} (or option is poorly labeled)`);
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
- const label = get_label(activeOption);
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
- add_option_msg_is_active = !add_option_msg_is_active;
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.map(get_label).at(-1));
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
- class="selected {ulSelectedClass}"
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(get_label(option))}
408
- on:keydown={if_enter_or_space(() => remove(get_label(option)))}
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(label, event)
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
- {#if allowUserOptions && searchText}
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={add_option_msg_is_active}
540
- on:mouseover={() => (add_option_msg_is_active = true)}
541
- on:focus={() => (add_option_msg_is_active = true)}
542
- on:mouseout={() => (add_option_msg_is_active = false)}
543
- on:blur={() => (add_option_msg_is_active = false)}
544
- aria-selected="false"
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
- {!duplicates && selected.some((option) => duplicateFunc(option, searchText))
547
- ? duplicateOptionMsg
548
- : createOptionMsg}
588
+ {msg}
549
589
  </li>
550
- {:else}
551
- <span>{noMatchingOptionsMsg}</span>
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
- /* for noOptionsMsg */
697
- :where(div.multiselect > ul.options span) {
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 { MultiSelectEvents, Option as GenericOption } from './';
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, default as MultiSelect } from './MultiSelect.svelte';
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, default as MultiSelect } from './MultiSelect.svelte';
3
+ export { default as MultiSelect, default } from './MultiSelect.svelte';
4
4
  export { default as Wiggle } from './Wiggle.svelte';
5
- // Firefox lacks support for scrollIntoViewIfNeeded, see
6
- // https://github.com/janosh/svelte-multiselect/issues/87
7
- // this polyfill was copied from
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 el = this;
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
- el.scrollIntoView({
16
+ elem.scrollIntoView({
17
17
  block: place,
18
18
  inline: place,
19
19
  });
20
20
  }
21
21
  this.disconnect();
22
22
  });
23
- observer.observe(this);
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.5.0",
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.56.0"
26
+ "svelte": "^3.58.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@iconify/svelte": "^3.1.0",
30
- "@playwright/test": "^1.31.2",
31
- "@sveltejs/adapter-static": "^2.0.1",
32
- "@sveltejs/kit": "^1.11.0",
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.0.3",
35
- "@typescript-eslint/eslint-plugin": "^5.54.1",
36
- "@typescript-eslint/parser": "^5.54.1",
37
- "@vitest/coverage-c8": "^0.29.2",
38
- "eslint": "^8.35.0",
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.7.0",
42
- "jsdom": "^21.1.0",
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.4",
46
- "prettier-plugin-svelte": "^2.9.0",
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.1.0",
50
- "svelte-preprocess": "^5.0.1",
51
- "svelte-toc": "^0.5.2",
52
- "svelte-zoo": "^0.3.4",
53
- "svelte2tsx": "^0.6.3",
54
- "typescript": "^4.9.5",
55
- "vite": "^4.1.4",
56
- "vitest": "^0.29.2"
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` if not set which defaults to `cornflowerblue`.
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
- For example, to change the background color of the options dropdown:
552
-
553
- ```svelte
554
- <MultiSelect --sms-options-bg="white" />
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