svelte-multiselect
Advanced tools
@@ -13,4 +13,4 @@ <script>/* eslint-disable no-undef */ // TODO: remove when fixed | ||
| export let open = false; | ||
| export let dialog; | ||
| export let input; | ||
| export let dialog = null; | ||
| export let input = null; | ||
| export let placeholder = `Filter actions...`; | ||
@@ -17,0 +17,0 @@ async function toggle(event) { |
@@ -14,4 +14,4 @@ import { SvelteComponentTyped } from "svelte"; | ||
| open?: boolean | undefined; | ||
| dialog: HTMLDialogElement; | ||
| input: HTMLInputElement; | ||
| dialog?: HTMLDialogElement | null | undefined; | ||
| input?: HTMLInputElement | null | undefined; | ||
| placeholder?: string | undefined; | ||
@@ -18,0 +18,0 @@ }; |
+1
-1
| export { default as CircleSpinner } from './CircleSpinner.svelte'; | ||
| export { default as CmdPalette } from './CmdPalette.svelte'; | ||
| export { default, default as MultiSelect } from './MultiSelect.svelte'; | ||
| export { default as MultiSelect, default } from './MultiSelect.svelte'; | ||
| export { default as Wiggle } from './Wiggle.svelte'; | ||
@@ -5,0 +5,0 @@ export type Option = string | number | ObjectOption; |
+7
-7
| export { default as CircleSpinner } from './CircleSpinner.svelte'; | ||
| export { default as CmdPalette } from './CmdPalette.svelte'; | ||
| export { default, default as MultiSelect } from './MultiSelect.svelte'; | ||
| export { default as MultiSelect, default } from './MultiSelect.svelte'; | ||
| export { default as Wiggle } from './Wiggle.svelte'; | ||
| // Firefox lacks support for scrollIntoViewIfNeeded, see | ||
| // https://github.com/janosh/svelte-multiselect/issues/87 | ||
| // this polyfill was copied from | ||
| // Firefox lacks support for scrollIntoViewIfNeeded (https://caniuse.com/scrollintoviewifneeded). | ||
| // See https://github.com/janosh/svelte-multiselect/issues/87 | ||
| // Polyfill copied from | ||
| // https://github.com/nuxodin/lazyfill/blob/a8e63/polyfills/Element/prototype/scrollIntoViewIfNeeded.js | ||
| // exported for testing | ||
| export function scroll_into_view_if_needed_polyfill(centerIfNeeded = true) { | ||
| const el = this; | ||
| const elem = this; | ||
| const observer = new IntersectionObserver(function ([entry]) { | ||
@@ -16,3 +16,3 @@ const ratio = entry.intersectionRatio; | ||
| const place = ratio <= 0 && centerIfNeeded ? `center` : `nearest`; | ||
| el.scrollIntoView({ | ||
| elem.scrollIntoView({ | ||
| block: place, | ||
@@ -24,3 +24,3 @@ inline: place, | ||
| }); | ||
| observer.observe(this); | ||
| observer.observe(elem); | ||
| return observer; // return for testing | ||
@@ -27,0 +27,0 @@ } |
+59
-47
@@ -103,9 +103,14 @@ <script>import { createEventDispatcher, tick } from 'svelte'; | ||
| if (sortSelected && selectedOptionsDraggable) { | ||
| 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.`); | ||
| 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.`); | ||
| } | ||
| if (allowUserOptions && !createOptionMsg) { | ||
| console.error(`MultiSelect's allowUserOptions=${allowUserOptions} but createOptionMsg=${createOptionMsg} is falsy. ` + | ||
| `This prevents the "Add option" <span> from showing up, resulting in a confusing user experience.`); | ||
| } | ||
| const dispatch = createEventDispatcher(); | ||
| let add_option_msg_is_active = false; // controls active state of <li>{createOptionMsg}</li> | ||
| let option_msg_is_active = false; // controls active state of <li>{createOptionMsg}</li> | ||
| let window_width; | ||
| // options matching the current search text | ||
| $: matchingOptions = options.filter((op) => filterFunc(op, searchText) && !selected.map(get_label).includes(get_label(op)) // remove already selected options from dropdown list | ||
| $: matchingOptions = options.filter((op) => filterFunc(op, searchText) && !selected.includes(op) // remove already selected options from dropdown list | ||
| ); | ||
@@ -119,13 +124,13 @@ // raise if matchingOptions[activeIndex] does not yield a value | ||
| // add an option to selected list | ||
| function add(label, event) { | ||
| function add(option, event) { | ||
| if (maxSelect && maxSelect > 1 && selected.length >= maxSelect) | ||
| wiggle = true; | ||
| if (!isNaN(Number(label)) && typeof selected.map(get_label)[0] === `number`) | ||
| label = Number(label); // convert to number if possible | ||
| const is_duplicate = selected.some((option) => duplicateFunc(option, label)); | ||
| if (!isNaN(Number(option)) && typeof selected.map(get_label)[0] === `number`) { | ||
| option = Number(option); // convert to number if possible | ||
| } | ||
| const is_duplicate = selected.some((op) => duplicateFunc(op, option)); | ||
| if ((maxSelect === null || maxSelect === 1 || selected.length < maxSelect) && | ||
| (duplicates || !is_duplicate)) { | ||
| // first check if we find option in the options list | ||
| let option = options.find((op) => get_label(op) === label); | ||
| if (!option && // this has the side-effect of not allowing to user to add the same | ||
| if (!options.includes(option) && // first check if we find option in the options list | ||
| // this has the side-effect of not allowing to user to add the same | ||
| // custom option twice in append mode | ||
@@ -154,9 +159,6 @@ [true, `append`].includes(allowUserOptions) && | ||
| } | ||
| if (option === undefined) { | ||
| throw `Run time error, option with label ${label} not found in options list`; | ||
| } | ||
| if (resetFilterOnAdd) | ||
| searchText = ``; // reset search string on selection | ||
| if ([``, undefined, null].includes(option)) { | ||
| console.error(`MultiSelect: encountered missing option with label ${label} (or option is poorly labeled)`); | ||
| console.error(`MultiSelect: encountered falsy option ${option}`); | ||
| return; | ||
@@ -240,4 +242,3 @@ } | ||
| if (activeOption) { | ||
| const label = get_label(activeOption); | ||
| selected.map(get_label).includes(label) ? remove(label) : add(label, event); | ||
| selected.includes(activeOption) ? remove(activeOption) : add(activeOption, event); | ||
| searchText = ``; | ||
@@ -264,3 +265,3 @@ } | ||
| // <li>{addUserMsg}</li> active on keydown (or toggle it if already active) | ||
| add_option_msg_is_active = !add_option_msg_is_active; | ||
| option_msg_is_active = !option_msg_is_active; | ||
| return; | ||
@@ -290,3 +291,3 @@ } | ||
| else if (event.key === `Backspace` && selected.length > 0 && !searchText) { | ||
| remove(selected.map(get_label).at(-1)); | ||
| remove(selected.at(-1)); | ||
| } | ||
@@ -353,3 +354,10 @@ // make first matching option active on any keypress (if none of the above special cases match) | ||
| return; | ||
| const tree_walker = document.createTreeWalker(ul_options, NodeFilter.SHOW_TEXT); | ||
| const tree_walker = document.createTreeWalker(ul_options, NodeFilter.SHOW_TEXT, { | ||
| acceptNode: (node) => { | ||
| // don't highlight text in the "no matching options" message | ||
| if (node?.textContent === noMatchingOptionsMsg) | ||
| return NodeFilter.FILTER_REJECT; | ||
| return NodeFilter.FILTER_ACCEPT; | ||
| }, | ||
| }); | ||
| const text_nodes = []; | ||
@@ -363,6 +371,6 @@ let current_node = tree_walker.nextNode(); | ||
| const ranges = text_nodes.map((el) => { | ||
| const text = el.textContent.toLowerCase(); | ||
| const text = el.textContent?.toLowerCase(); | ||
| const indices = []; | ||
| let start_pos = 0; | ||
| while (start_pos < text.length) { | ||
| while (text && start_pos < text.length) { | ||
| const index = text.indexOf(query, start_pos); | ||
@@ -384,3 +392,3 @@ if (index === -1) | ||
| // eslint-disable-next-line no-undef | ||
| CSS.highlights.set(`search-results`, new Highlight(...ranges.flat())); | ||
| CSS.highlights.set(`sms-search-matches`, new Highlight(...ranges.flat())); | ||
| } | ||
@@ -410,3 +418,3 @@ </script> | ||
| required={Boolean(required)} | ||
| value={selected.length >= required ? JSON.stringify(selected) : null} | ||
| value={selected.length >= Number(required) ? JSON.stringify(selected) : null} | ||
| tabindex="-1" | ||
@@ -420,5 +428,5 @@ aria-hidden="true" | ||
| let msg | ||
| if (maxSelect && maxSelect > 1 && required > 1) { | ||
| if (maxSelect && maxSelect > 1 && Number(required) > 1) { | ||
| msg = `Please select between ${required} and ${maxSelect} options` | ||
| } else if (required > 1) { | ||
| } else if (Number(required) > 1) { | ||
| msg = `Please select at least ${required} options` | ||
@@ -435,3 +443,3 @@ } else { | ||
| <ul class="selected {ulSelectedClass}" aria-label="selected options"> | ||
| {#each selected as option, idx (get_label(option))} | ||
| {#each selected as option, idx (option)} | ||
| <li | ||
@@ -456,4 +464,4 @@ class={liSelectedClass} | ||
| <button | ||
| on:mouseup|stopPropagation={() => remove(get_label(option))} | ||
| on:keydown={if_enter_or_space(() => remove(get_label(option)))} | ||
| on:mouseup|stopPropagation={() => remove(option)} | ||
| on:keydown={if_enter_or_space(() => remove(option))} | ||
| type="button" | ||
@@ -549,3 +557,3 @@ title="{removeBtnTitle} {get_label(option)}" | ||
| on:mouseup|stopPropagation={(event) => { | ||
| if (!disabled) add(label, event) | ||
| if (!disabled) add(option, event) | ||
| }} | ||
@@ -577,3 +585,8 @@ title={disabled | ||
| {:else} | ||
| {#if allowUserOptions && searchText} | ||
| {@const search_is_duplicate = selected.some((option) => | ||
| duplicateFunc(option, searchText) | ||
| )} | ||
| {@const msg = | ||
| !duplicates && search_is_duplicate ? duplicateOptionMsg : createOptionMsg} | ||
| {#if allowUserOptions && searchText && msg} | ||
| <li | ||
@@ -583,15 +596,16 @@ on:mousedown|stopPropagation | ||
| title={createOptionMsg} | ||
| class:active={add_option_msg_is_active} | ||
| on:mouseover={() => (add_option_msg_is_active = true)} | ||
| on:focus={() => (add_option_msg_is_active = true)} | ||
| on:mouseout={() => (add_option_msg_is_active = false)} | ||
| on:blur={() => (add_option_msg_is_active = false)} | ||
| class:active={option_msg_is_active} | ||
| on:mouseover={() => (option_msg_is_active = true)} | ||
| on:focus={() => (option_msg_is_active = true)} | ||
| on:mouseout={() => (option_msg_is_active = false)} | ||
| on:blur={() => (option_msg_is_active = false)} | ||
| class="user-msg" | ||
| > | ||
| {!duplicates && selected.some((option) => duplicateFunc(option, searchText)) | ||
| ? duplicateOptionMsg | ||
| : createOptionMsg} | ||
| {msg} | ||
| </li> | ||
| {:else} | ||
| <span>{noMatchingOptionsMsg}</span> | ||
| {:else if noMatchingOptionsMsg} | ||
| <!-- use span to not have cursor: pointer --> | ||
| <span class="user-msg">{noMatchingOptionsMsg}</span> | ||
| {/if} | ||
| <!-- Show nothing if all messages are empty --> | ||
| {/each} | ||
@@ -740,4 +754,5 @@ </ul> | ||
| } | ||
| /* for noOptionsMsg */ | ||
| :where(div.multiselect > ul.options span) { | ||
| :where(div.multiselect > ul.options .user-msg) { | ||
| /* block needed so vertical padding applies to span */ | ||
| display: block; | ||
| padding: 3pt 2ex; | ||
@@ -761,8 +776,5 @@ } | ||
| } | ||
| ::highlight(search-results) { | ||
| color: var(--sms-highlight-color, orange); | ||
| background: var(--sms-highlight-bg); | ||
| text-decoration: var(--sms-highlight-text-decoration); | ||
| text-decoration-color: var(--sms-highlight-text-decoration-color); | ||
| ::highlight(sms-search-matches) { | ||
| color: mediumaquamarine; | ||
| } | ||
| </style> |
| import { SvelteComponentTyped } from "svelte"; | ||
| import type { MultiSelectEvents, Option as GenericOption } from './'; | ||
| import type { Option as GenericOption, MultiSelectEvents } from './'; | ||
| declare class __sveltets_Render<Option extends GenericOption> { | ||
@@ -4,0 +4,0 @@ props(): { |
+21
-21
@@ -8,3 +8,3 @@ { | ||
| "license": "MIT", | ||
| "version": "8.6.0", | ||
| "version": "8.6.1", | ||
| "type": "module", | ||
@@ -27,33 +27,33 @@ "svelte": "./dist/index.js", | ||
| "dependencies": { | ||
| "svelte": "^3.57.0" | ||
| "svelte": "^3.58.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@iconify/svelte": "^3.1.0", | ||
| "@playwright/test": "^1.31.2", | ||
| "@sveltejs/adapter-static": "^2.0.1", | ||
| "@sveltejs/kit": "^1.12.0", | ||
| "@iconify/svelte": "^3.1.3", | ||
| "@playwright/test": "^1.33.0", | ||
| "@sveltejs/adapter-static": "^2.0.2", | ||
| "@sveltejs/kit": "^1.15.9", | ||
| "@sveltejs/package": "2.0.2", | ||
| "@sveltejs/vite-plugin-svelte": "^2.0.3", | ||
| "@typescript-eslint/eslint-plugin": "^5.55.0", | ||
| "@typescript-eslint/parser": "^5.55.0", | ||
| "@vitest/coverage-c8": "^0.29.3", | ||
| "eslint": "^8.36.0", | ||
| "@sveltejs/vite-plugin-svelte": "^2.1.1", | ||
| "@typescript-eslint/eslint-plugin": "^5.59.1", | ||
| "@typescript-eslint/parser": "^5.59.1", | ||
| "@vitest/coverage-c8": "^0.30.1", | ||
| "eslint": "^8.39.0", | ||
| "eslint-plugin-svelte3": "^4.0.0", | ||
| "hastscript": "^7.2.0", | ||
| "highlight.js": "^11.7.0", | ||
| "highlight.js": "^11.8.0", | ||
| "jsdom": "^21.1.1", | ||
| "mdsvex": "^0.10.6", | ||
| "mdsvexamples": "^0.3.3", | ||
| "prettier": "^2.8.4", | ||
| "prettier-plugin-svelte": "^2.9.0", | ||
| "prettier": "^2.8.8", | ||
| "prettier-plugin-svelte": "^2.10.0", | ||
| "rehype-autolink-headings": "^6.1.1", | ||
| "rehype-slug": "^5.1.0", | ||
| "svelte-check": "^3.1.4", | ||
| "svelte-check": "^3.2.0", | ||
| "svelte-preprocess": "^5.0.3", | ||
| "svelte-toc": "^0.5.4", | ||
| "svelte-zoo": "^0.4.3", | ||
| "svelte2tsx": "^0.6.10", | ||
| "typescript": "5.0.2", | ||
| "vite": "^4.2.0", | ||
| "vitest": "^0.29.3" | ||
| "svelte-toc": "^0.5.5", | ||
| "svelte-zoo": "^0.4.5", | ||
| "svelte2tsx": "^0.6.11", | ||
| "typescript": "5.0.4", | ||
| "vite": "^4.3.3", | ||
| "vitest": "^0.30.1" | ||
| }, | ||
@@ -60,0 +60,0 @@ "keywords": [ |
+11
-7
@@ -192,3 +192,3 @@ <h1 align="center"> | ||
| 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. | ||
| 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. | ||
@@ -530,3 +530,3 @@ 1. ```ts | ||
| - `div.multiselect:focus-within` | ||
| - `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`. | ||
| - `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`. | ||
| - `div.multiselect.disabled` | ||
@@ -564,8 +564,12 @@ - `background: var(--sms-disabled-bg, lightgray)`: Background when in disabled state. | ||
| - `color: var(--sms-li-disabled-text, #b8b8b8)`: Text color of disabled option in the dropdown list. | ||
| - `::highlight(search-results)`: applies to search results in dropdown list that match the current search query if `highlightMatches=true` | ||
| - `color: var(--sms-highlight-color, orange)` | ||
| - `background: var(--sms-highlight-bg)` | ||
| - `text-decoration: var(--sms-highlight-text-decoration)` | ||
| - `text-decoration-color: var(--sms-highlight-text-decoration-color)` | ||
| - `::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: | ||
| ```css | ||
| ::highlight(sms-search-matches) { | ||
| color: orange; | ||
| background: rgba(0, 0, 0, 0.15); | ||
| text-decoration: underline; | ||
| } | ||
| ``` | ||
| ### With CSS frameworks | ||
@@ -572,0 +576,0 @@ |
81853
0.66%660
0.61%Updated