svelte-multiselect 8.5.0 → 8.6.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.
@@ -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 = ``;
@@ -128,7 +129,7 @@ function add(label, event) {
128
129
  // custom option twice in append mode
129
130
  [true, `append`].includes(allowUserOptions) &&
130
131
  searchText.length > 0) {
131
- // user entered text but no options match, so if allowUserOptions=true | 'append', we create
132
+ // user entered text but no options match, so if allowUserOptions = true | 'append', we create
132
133
  // a new option from the user-entered text
133
134
  if (typeof options[0] === `object`) {
134
135
  // if 1st option is an object, we create new option as object to keep type homogeneity
@@ -140,8 +141,10 @@ function add(label, event) {
140
141
  // create new option as number if it parses to a number and 1st option is also number or missing
141
142
  option = Number(searchText);
142
143
  }
143
- else
144
+ else {
144
145
  option = searchText; // else create custom option as string
146
+ }
147
+ dispatch(`create`, { option });
145
148
  }
146
149
  if (allowUserOptions === `append`)
147
150
  options = [...options, option];
@@ -199,10 +202,10 @@ function remove(label) {
199
202
  return console.error(`Multiselect can't remove selected option ${label}, not found in selected list`);
200
203
  }
201
204
  selected = selected.filter((op) => get_label(op) !== label); // remove option from selected list
202
- dispatch(`remove`, { option });
203
- dispatch(`change`, { option, type: `remove` });
204
205
  invalid = false; // reset error status whenever items are removed
205
206
  form_input?.setCustomValidity(``);
207
+ dispatch(`remove`, { option });
208
+ dispatch(`change`, { option, type: `remove` });
206
209
  }
207
210
  function open_dropdown(event) {
208
211
  if (disabled)
@@ -331,6 +334,47 @@ const dragstart = (idx) => (event) => {
331
334
  event.dataTransfer.dropEffect = `move`;
332
335
  event.dataTransfer.setData(`text/plain`, `${idx}`);
333
336
  };
337
+ let ul_options;
338
+ function highlight_matching_options(event) {
339
+ if (!highlightMatches || typeof CSS == `undefined` || !CSS.highlights)
340
+ return; // don't try if CSS highlight API not supported
341
+ // clear previous ranges from HighlightRegistry
342
+ CSS.highlights.clear();
343
+ // get input's search query
344
+ const query = event?.target?.value.trim().toLowerCase();
345
+ if (!query)
346
+ return;
347
+ const tree_walker = document.createTreeWalker(ul_options, NodeFilter.SHOW_TEXT);
348
+ const text_nodes = [];
349
+ let current_node = tree_walker.nextNode();
350
+ while (current_node) {
351
+ text_nodes.push(current_node);
352
+ current_node = tree_walker.nextNode();
353
+ }
354
+ // iterate over all text nodes and find matches
355
+ const ranges = text_nodes.map((el) => {
356
+ const text = el.textContent.toLowerCase();
357
+ const indices = [];
358
+ let start_pos = 0;
359
+ while (start_pos < text.length) {
360
+ const index = text.indexOf(query, start_pos);
361
+ if (index === -1)
362
+ break;
363
+ indices.push(index);
364
+ start_pos = index + query.length;
365
+ }
366
+ // create range object for each str found in the text node
367
+ return indices.map((index) => {
368
+ const range = new Range();
369
+ range.setStart(el, index);
370
+ range.setEnd(el, index + query.length);
371
+ return range;
372
+ });
373
+ });
374
+ // create Highlight object from ranges and add to registry
375
+ // eslint-disable-next-line no-undef
376
+ CSS.highlights.set(`search-results`, new Highlight(...ranges.flat()));
377
+ }
334
378
  </script>
335
379
 
336
380
  <svelte:window
@@ -376,17 +420,10 @@ const dragstart = (idx) => (event) => {
376
420
  <slot name="expand-icon" {open}>
377
421
  <ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt; cursor: pointer;" />
378
422
  </slot>
379
- <ul
380
- class="selected {ulSelectedClass}"
381
- role="listbox"
382
- aria-multiselectable={maxSelect === null || maxSelect > 1}
383
- aria-label="selected options"
384
- >
423
+ <ul class="selected {ulSelectedClass}" aria-label="selected options">
385
424
  {#each selected as option, idx (get_label(option))}
386
425
  <li
387
426
  class={liSelectedClass}
388
- role="option"
389
- aria-selected="true"
390
427
  animate:flip={{ duration: 100 }}
391
428
  draggable={selectedOptionsDraggable && !disabled && selected.length > 1}
392
429
  on:dragstart={dragstart(idx)}
@@ -425,6 +462,7 @@ const dragstart = (idx) => (event) => {
425
462
  on:keydown|stopPropagation={handle_keydown}
426
463
  on:focus
427
464
  on:focus={open_dropdown}
465
+ on:input={highlight_matching_options}
428
466
  {id}
429
467
  {disabled}
430
468
  {autocomplete}
@@ -482,14 +520,7 @@ const dragstart = (idx) => (event) => {
482
520
 
483
521
  <!-- only render options dropdown if options or searchText is not empty needed to avoid briefly flashing empty dropdown -->
484
522
  {#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
- >
523
+ <ul class:hidden={!open} class="options {ulOptionsClass}" bind:this={ul_options}>
493
524
  {#each matchingOptions as option, idx}
494
525
  {@const {
495
526
  label,
@@ -519,8 +550,6 @@ const dragstart = (idx) => (event) => {
519
550
  }}
520
551
  on:mouseout={() => (activeIndex = null)}
521
552
  on:blur={() => (activeIndex = null)}
522
- role="option"
523
- aria-selected="false"
524
553
  >
525
554
  <slot name="option" {option} {idx}>
526
555
  {#if parseLabelsAsHtml}
@@ -541,7 +570,6 @@ const dragstart = (idx) => (event) => {
541
570
  on:focus={() => (add_option_msg_is_active = true)}
542
571
  on:mouseout={() => (add_option_msg_is_active = false)}
543
572
  on:blur={() => (add_option_msg_is_active = false)}
544
- aria-selected="false"
545
573
  >
546
574
  {!duplicates && selected.some((option) => duplicateFunc(option, searchText))
547
575
  ? duplicateOptionMsg
@@ -713,4 +741,10 @@ const dragstart = (idx) => (event) => {
713
741
  :where(span.max-select-msg) {
714
742
  padding: 0 3pt;
715
743
  }
744
+ ::highlight(search-results) {
745
+ color: var(--sms-highlight-color, orange);
746
+ background: var(--sms-highlight-bg);
747
+ text-decoration: var(--sms-highlight-text-decoration);
748
+ text-decoration-color: var(--sms-highlight-text-decoration-color);
749
+ }
716
750
  </style>
@@ -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
@@ -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/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.0",
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.57.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@iconify/svelte": "^3.1.0",
30
30
  "@playwright/test": "^1.31.2",
31
31
  "@sveltejs/adapter-static": "^2.0.1",
32
- "@sveltejs/kit": "^1.11.0",
32
+ "@sveltejs/kit": "^1.12.0",
33
33
  "@sveltejs/package": "2.0.2",
34
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",
35
+ "@typescript-eslint/eslint-plugin": "^5.55.0",
36
+ "@typescript-eslint/parser": "^5.55.0",
37
+ "@vitest/coverage-c8": "^0.29.3",
38
+ "eslint": "^8.36.0",
39
39
  "eslint-plugin-svelte3": "^4.0.0",
40
40
  "hastscript": "^7.2.0",
41
41
  "highlight.js": "^11.7.0",
42
- "jsdom": "^21.1.0",
42
+ "jsdom": "^21.1.1",
43
43
  "mdsvex": "^0.10.6",
44
44
  "mdsvexamples": "^0.3.3",
45
45
  "prettier": "^2.8.4",
46
46
  "prettier-plugin-svelte": "^2.9.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.1.4",
50
+ "svelte-preprocess": "^5.0.3",
51
+ "svelte-toc": "^0.5.4",
52
+ "svelte-zoo": "^0.4.3",
53
+ "svelte2tsx": "^0.6.10",
54
+ "typescript": "5.0.2",
55
+ "vite": "^4.2.0",
56
+ "vitest": "^0.29.3"
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(search-results)` 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)`
@@ -547,12 +559,11 @@ 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(search-results)`: applies to search results in dropdown list that match the current search query if `highlightMatches=true`
563
+ - `color: var(--sms-highlight-color, orange)`
564
+ - `background: var(--sms-highlight-bg)`
565
+ - `text-decoration: var(--sms-highlight-text-decoration)`
566
+ - `text-decoration-color: var(--sms-highlight-text-decoration-color)`
556
567
 
557
568
  ### With CSS frameworks
558
569