svelte-multiselect 8.6.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: {
@@ -100,13 +100,18 @@ if (parseLabelsAsHtml && allowUserOptions) {
100
100
  console.warn(`Don't combine parseLabelsAsHtml and allowUserOptions. It's susceptible to XSS attacks!`);
101
101
  }
102
102
  if (sortSelected && selectedOptionsDraggable) {
103
- 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.`);
104
109
  }
105
110
  const dispatch = createEventDispatcher();
106
- 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>
107
112
  let window_width;
108
113
  // options matching the current search text
109
- $: 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
110
115
  );
111
116
  // raise if matchingOptions[activeIndex] does not yield a value
112
117
  if (activeIndex !== null && !matchingOptions[activeIndex]) {
@@ -115,17 +120,17 @@ if (activeIndex !== null && !matchingOptions[activeIndex]) {
115
120
  // update activeOption when activeIndex changes
116
121
  $: activeOption = matchingOptions[activeIndex ?? -1] ?? null;
117
122
  // add an option to selected list
118
- function add(label, event) {
123
+ function add(option, event) {
119
124
  if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
120
125
  wiggle = true;
121
- if (!isNaN(Number(label)) && typeof selected.map(get_label)[0] === `number`)
122
- label = Number(label); // convert to number if possible
123
- 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));
124
130
  if ((maxSelect === null || maxSelect === 1 || selected.length < maxSelect) &&
125
131
  (duplicates || !is_duplicate)) {
126
- // first check if we find option in the options list
127
- let option = options.find((op) => get_label(op) === label);
128
- 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
129
134
  // custom option twice in append mode
130
135
  [true, `append`].includes(allowUserOptions) &&
131
136
  searchText.length > 0) {
@@ -149,13 +154,10 @@ function add(label, event) {
149
154
  if (allowUserOptions === `append`)
150
155
  options = [...options, option];
151
156
  }
152
- if (option === undefined) {
153
- throw `Run time error, option with label ${label} not found in options list`;
154
- }
155
157
  if (resetFilterOnAdd)
156
158
  searchText = ``; // reset search string on selection
157
159
  if ([``, undefined, null].includes(option)) {
158
- console.error(`MultiSelect: encountered missing option with label ${label} (or option is poorly labeled)`);
160
+ console.error(`MultiSelect: encountered falsy option ${option}`);
159
161
  return;
160
162
  }
161
163
  if (maxSelect === 1) {
@@ -234,8 +236,7 @@ async function handle_keydown(event) {
234
236
  else if (event.key === `Enter`) {
235
237
  event.preventDefault(); // prevent enter key from triggering form submission
236
238
  if (activeOption) {
237
- const label = get_label(activeOption);
238
- selected.map(get_label).includes(label) ? remove(label) : add(label, event);
239
+ selected.includes(activeOption) ? remove(activeOption) : add(activeOption, event);
239
240
  searchText = ``;
240
241
  }
241
242
  else if (allowUserOptions && searchText.length > 0) {
@@ -257,7 +258,7 @@ async function handle_keydown(event) {
257
258
  else if (allowUserOptions && !matchingOptions.length && searchText.length > 0) {
258
259
  // if allowUserOptions is truthy and user entered text but no options match, we make
259
260
  // <li>{addUserMsg}</li> active on keydown (or toggle it if already active)
260
- add_option_msg_is_active = !add_option_msg_is_active;
261
+ option_msg_is_active = !option_msg_is_active;
261
262
  return;
262
263
  }
263
264
  else if (activeIndex === null) {
@@ -282,7 +283,7 @@ async function handle_keydown(event) {
282
283
  }
283
284
  // on backspace key: remove last selected option
284
285
  else if (event.key === `Backspace` && selected.length > 0 && !searchText) {
285
- remove(selected.map(get_label).at(-1));
286
+ remove(selected.at(-1));
286
287
  }
287
288
  // make first matching option active on any keypress (if none of the above special cases match)
288
289
  else if (matchingOptions.length > 0) {
@@ -344,7 +345,14 @@ function highlight_matching_options(event) {
344
345
  const query = event?.target?.value.trim().toLowerCase();
345
346
  if (!query)
346
347
  return;
347
- const tree_walker = document.createTreeWalker(ul_options, NodeFilter.SHOW_TEXT);
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
+ });
348
356
  const text_nodes = [];
349
357
  let current_node = tree_walker.nextNode();
350
358
  while (current_node) {
@@ -353,10 +361,10 @@ function highlight_matching_options(event) {
353
361
  }
354
362
  // iterate over all text nodes and find matches
355
363
  const ranges = text_nodes.map((el) => {
356
- const text = el.textContent.toLowerCase();
364
+ const text = el.textContent?.toLowerCase();
357
365
  const indices = [];
358
366
  let start_pos = 0;
359
- while (start_pos < text.length) {
367
+ while (text && start_pos < text.length) {
360
368
  const index = text.indexOf(query, start_pos);
361
369
  if (index === -1)
362
370
  break;
@@ -373,7 +381,7 @@ function highlight_matching_options(event) {
373
381
  });
374
382
  // create Highlight object from ranges and add to registry
375
383
  // eslint-disable-next-line no-undef
376
- CSS.highlights.set(`search-results`, new Highlight(...ranges.flat()));
384
+ CSS.highlights.set(`sms-search-matches`, new Highlight(...ranges.flat()));
377
385
  }
378
386
  </script>
379
387
 
@@ -398,7 +406,7 @@ function highlight_matching_options(event) {
398
406
  <input
399
407
  {name}
400
408
  required={Boolean(required)}
401
- value={selected.length >= required ? JSON.stringify(selected) : null}
409
+ value={selected.length >= Number(required) ? JSON.stringify(selected) : null}
402
410
  tabindex="-1"
403
411
  aria-hidden="true"
404
412
  aria-label="ignore this, used only to prevent form submission if select is required but empty"
@@ -407,9 +415,9 @@ function highlight_matching_options(event) {
407
415
  on:invalid={() => {
408
416
  invalid = true
409
417
  let msg
410
- if (maxSelect && maxSelect > 1 && required > 1) {
418
+ if (maxSelect && maxSelect > 1 && Number(required) > 1) {
411
419
  msg = `Please select between ${required} and ${maxSelect} options`
412
- } else if (required > 1) {
420
+ } else if (Number(required) > 1) {
413
421
  msg = `Please select at least ${required} options`
414
422
  } else {
415
423
  msg = `Please select an option`
@@ -421,7 +429,7 @@ function highlight_matching_options(event) {
421
429
  <ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt; cursor: pointer;" />
422
430
  </slot>
423
431
  <ul class="selected {ulSelectedClass}" aria-label="selected options">
424
- {#each selected as option, idx (get_label(option))}
432
+ {#each selected as option, idx (option)}
425
433
  <li
426
434
  class={liSelectedClass}
427
435
  animate:flip={{ duration: 100 }}
@@ -441,8 +449,8 @@ function highlight_matching_options(event) {
441
449
  </slot>
442
450
  {#if !disabled && (minSelect === null || selected.length > minSelect)}
443
451
  <button
444
- on:mouseup|stopPropagation={() => remove(get_label(option))}
445
- 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))}
446
454
  type="button"
447
455
  title="{removeBtnTitle} {get_label(option)}"
448
456
  class="remove"
@@ -533,7 +541,7 @@ function highlight_matching_options(event) {
533
541
  <li
534
542
  on:mousedown|stopPropagation
535
543
  on:mouseup|stopPropagation={(event) => {
536
- if (!disabled) add(label, event)
544
+ if (!disabled) add(option, event)
537
545
  }}
538
546
  title={disabled
539
547
  ? disabledTitle
@@ -560,24 +568,30 @@ function highlight_matching_options(event) {
560
568
  </slot>
561
569
  </li>
562
570
  {:else}
563
- {#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}
564
577
  <li
565
578
  on:mousedown|stopPropagation
566
579
  on:mouseup|stopPropagation={(event) => add(searchText, event)}
567
580
  title={createOptionMsg}
568
- class:active={add_option_msg_is_active}
569
- on:mouseover={() => (add_option_msg_is_active = true)}
570
- on:focus={() => (add_option_msg_is_active = true)}
571
- on:mouseout={() => (add_option_msg_is_active = false)}
572
- on:blur={() => (add_option_msg_is_active = 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"
573
587
  >
574
- {!duplicates && selected.some((option) => duplicateFunc(option, searchText))
575
- ? duplicateOptionMsg
576
- : createOptionMsg}
588
+ {msg}
577
589
  </li>
578
- {:else}
579
- <span>{noMatchingOptionsMsg}</span>
590
+ {:else if noMatchingOptionsMsg}
591
+ <!-- use span to not have cursor: pointer -->
592
+ <span class="user-msg">{noMatchingOptionsMsg}</span>
580
593
  {/if}
594
+ <!-- Show nothing if all messages are empty -->
581
595
  {/each}
582
596
  </ul>
583
597
  {/if}
@@ -721,8 +735,9 @@ function highlight_matching_options(event) {
721
735
  cursor: pointer;
722
736
  scroll-margin: var(--sms-options-scroll-margin, 100px);
723
737
  }
724
- /* for noOptionsMsg */
725
- :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;
726
741
  padding: 3pt 2ex;
727
742
  }
728
743
  :where(div.multiselect > ul.options > li.selected) {
@@ -741,10 +756,7 @@ function highlight_matching_options(event) {
741
756
  :where(span.max-select-msg) {
742
757
  padding: 0 3pt;
743
758
  }
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);
759
+ ::highlight(sms-search-matches) {
760
+ color: mediumaquamarine;
749
761
  }
750
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;
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 = {
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.6.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.57.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.12.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.55.0",
36
- "@typescript-eslint/parser": "^5.55.0",
37
- "@vitest/coverage-c8": "^0.29.3",
38
- "eslint": "^8.36.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",
41
+ "highlight.js": "^11.8.0",
42
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.4",
49
+ "svelte-check": "^3.2.0",
50
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"
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
@@ -189,7 +189,7 @@ Full list of props/bindable variables for this component. The `Option` type you
189
189
  highlightMatches: boolean = true
190
190
  ```
191
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.
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
193
 
194
194
  1. ```ts
195
195
  id: string | null = null
@@ -526,7 +526,7 @@ Minimal example that changes the background color of the options dropdown:
526
526
  - `div.multiselect.open`
527
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.
528
528
  - `div.multiselect:focus-within`
529
- - `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`.
530
530
  - `div.multiselect.disabled`
531
531
  - `background: var(--sms-disabled-bg, lightgray)`: Background when in disabled state.
532
532
  - `div.multiselect input::placeholder`
@@ -559,11 +559,15 @@ Minimal example that changes the background color of the options dropdown:
559
559
  - `div.multiselect > ul.options > li.disabled`
560
560
  - `background: var(--sms-li-disabled-bg, #f5f5f6)`: Background of disabled options in the dropdown list.
561
561
  - `color: var(--sms-li-disabled-text, #b8b8b8)`: Text color of disabled option in the dropdown list.
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)`
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
+ ```
567
571
 
568
572
  ### With CSS frameworks
569
573