diff --git a/package.json b/package.json index fa4448d..5752c7a 100644 --- a/package.json +++ b/package.json @@ -23,37 +23,37 @@ "update-coverage": "vitest tests/unit --run --coverage && npx istanbul-badges-readme" }, "dependencies": { - "svelte": "4.2.3" + "svelte": "4.2.8" }, "devDependencies": { - "@iconify/svelte": "^3.1.4", - "@playwright/test": "^1.39.0", - "@sveltejs/adapter-static": "^2.0.3", - "@sveltejs/kit": "^1.27.6", - "@sveltejs/package": "2.2.2", - "@sveltejs/vite-plugin-svelte": "2.5.2", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "@typescript-eslint/parser": "^6.11.0", - "@vitest/coverage-v8": "^0.34.6", - "eslint": "^8.53.0", - "eslint-plugin-svelte": "^2.35.0", + "@iconify/svelte": "^3.1.6", + "@playwright/test": "^1.40.1", + "@sveltejs/adapter-static": "^3.0.1", + "@sveltejs/kit": "^2.3.2", + "@sveltejs/package": "2.2.5", + "@sveltejs/vite-plugin-svelte": "3.0.1", + "@typescript-eslint/eslint-plugin": "^6.18.1", + "@typescript-eslint/parser": "^6.18.1", + "@vitest/coverage-v8": "^1.2.0", + "eslint": "^8.56.0", + "eslint-plugin-svelte": "^2.35.1", "hastscript": "^8.0.0", "highlight.js": "^11.9.0", - "jsdom": "^22.1.0", + "jsdom": "^23.2.0", "mdsvex": "^0.11.0", "mdsvexamples": "^0.4.1", - "prettier": "^3.1.0", - "prettier-plugin-svelte": "^3.1.0", + "prettier": "^3.2.1", + "prettier-plugin-svelte": "^3.1.2", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", - "svelte-check": "^3.6.0", - "svelte-preprocess": "^5.1.0", + "svelte-check": "^3.6.3", + "svelte-preprocess": "^5.1.3", "svelte-toc": "^0.5.6", "svelte-zoo": "^0.4.9", - "svelte2tsx": "^0.6.25", - "typescript": "5.2.2", - "vite": "^4.5.0", - "vitest": "^0.34.6" + "svelte2tsx": "^0.7.0", + "typescript": "5.3.3", + "vite": "^5.0.11", + "vitest": "^1.2.0" }, "keywords": [ "svelte", diff --git a/readme.md b/readme.md index e3c9d98..7998196 100644 --- a/readme.md +++ b/readme.md @@ -229,12 +229,30 @@ Full list of props/bindable variables for this component. The `Option` type you The `inputmode` attribute hints at the type of data the user may enter. Values like `'numeric' | 'tel' | 'email'` allow mobile browsers to display an appropriate virtual on-screen keyboard. See [MDN](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/inputmode) for details. If you want to suppress the on-screen keyboard to leave full-screen real estate for the dropdown list of options, set `inputmode="none"`. +1. ```ts + inputStyle: string | null = null + ``` + + One-off CSS rules applied to the `` element. + 1. ```ts invalid: boolean = false ``` If `required = true, 1, 2, ...` and user tries to submit form but `selected = []` is empty/`selected.length < required`, `invalid` is automatically set to `true` and CSS class `invalid` applied to the top-level `div.multiselect`. `invalid` class is removed as soon as any change to `selected` is registered. `invalid` can also be controlled externally by binding to it `` and setting it to `true` based on outside events or custom validation. +1. ```ts + liOptionStyle: string | null = null + ``` + + One-off CSS rules applied to the `
  • ` elements that wrap the dropdown options. + +1. ```ts + liSelectedStyle: string | null = null + ``` + + One-off CSS rules applied to the `
  • ` elements that wrap the selected options. + 1. ```ts loading: boolean = false ``` @@ -381,6 +399,24 @@ Full list of props/bindable variables for this component. The `Option` type you Whether selected options are draggable so users can change their order. +1. ```ts + style: string | null = null + ``` + + One-off CSS rules applied to the outer `
    ` that wraps the whole component for passing one-off CSS. + +1. ```ts + ulSelectedStyle: string | null = null + ``` + + One-off CSS rules applied to the `
      ` that wraps the list of selected options. + +1. ```ts + ulOptionsStyle: string | null = null + ``` + + One-off CSS rules applied to the `
        ` that wraps the list of selected options. + 1. ```ts value: Option | Option[] | null = null ``` diff --git a/src/lib/MultiSelect.svelte b/src/lib/MultiSelect.svelte index 7e71175..424484b 100644 --- a/src/lib/MultiSelect.svelte +++ b/src/lib/MultiSelect.svelte @@ -35,12 +35,15 @@ export let id: string | null = null export let input: HTMLInputElement | null = null export let inputClass: string = `` + export let inputStyle: string | null = null export let inputmode: string | null = null export let invalid: boolean = false export let liActiveOptionClass: string = `` export let liActiveUserMsgClass: string = `` export let liOptionClass: string = `` + export let liOptionStyle: string | null = null export let liSelectedClass: string = `` + export let liSelectedStyle: string | null = null export let liUserMsgClass: string = `` export let loading: boolean = false export let matchingOptions: Option[] = [] @@ -48,7 +51,7 @@ export let maxSelect: number | null = null // null means there is no upper limit for selected.length export let maxSelectMsg: ((current: number, max: number) => string) | null = ( current: number, - max: number + max: number, ) => (max > 1 ? `${current}/${max}` : ``) export let maxSelectMsgClass: string = `` export let name: string | null = null @@ -72,8 +75,11 @@ .slice(0, maxSelect ?? undefined) ?? [] // don't allow more than maxSelect preselected options export let sortSelected: boolean | ((op1: Option, op2: Option) => number) = false export let selectedOptionsDraggable: boolean = !sortSelected + export let style: string | null = null export let ulOptionsClass: string = `` export let ulSelectedClass: string = `` + export let ulSelectedStyle: string | null = null + export let ulOptionsStyle: string | null = null export let value: Option | Option[] | null = null const selected_to_value = (selected: Option[]) => { @@ -106,34 +112,34 @@ } if (maxSelect !== null && maxSelect < 1) { console.error( - `MultiSelect's maxSelect must be null or positive integer, got ${maxSelect}` + `MultiSelect's maxSelect must be null or positive integer, got ${maxSelect}`, ) } if (!Array.isArray(selected)) { console.error( - `MultiSelect's selected prop should always be an array, got ${selected}` + `MultiSelect's selected prop should always be an array, got ${selected}`, ) } if (maxSelect && typeof required === `number` && required > maxSelect) { console.error( - `MultiSelect maxSelect=${maxSelect} < required=${required}, makes it impossible for users to submit a valid form` + `MultiSelect maxSelect=${maxSelect} < required=${required}, makes it impossible for users to submit a valid form`, ) } if (parseLabelsAsHtml && allowUserOptions) { console.warn( - `Don't combine parseLabelsAsHtml and allowUserOptions. It's susceptible to XSS attacks!` + `Don't combine parseLabelsAsHtml and allowUserOptions. It's susceptible to XSS attacks!`, ) } 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.` + `user re-orderings of selected options will be undone by sortSelected on component re-renders.`, ) } if (allowUserOptions && !createOptionMsg && createOptionMsg !== null) { console.error( `MultiSelect has allowUserOptions=${allowUserOptions} but createOptionMsg=${createOptionMsg} is falsy. ` + - `This prevents the "Add option" from showing up, resulting in a confusing user experience.` + `This prevents the "Add option" from showing up, resulting in a confusing user experience.`, ) } if ( @@ -141,7 +147,7 @@ (typeof maxOptions != `number` || maxOptions < 0 || maxOptions % 1 != 0) ) { console.error( - `MultiSelect's maxOptions must be undefined or a positive integer, got ${maxOptions}` + `MultiSelect's maxOptions must be undefined or a positive integer, got ${maxOptions}`, ) } @@ -154,7 +160,7 @@ (opt) => filterFunc(opt, searchText) && // remove already selected options from dropdown list unless duplicate selections are allowed - (!selected.map(key).includes(key(opt)) || duplicates) + (!selected.map(key).includes(key(opt)) || duplicates), ) // raise if matchingOptions[activeIndex] does not yield a value @@ -261,8 +267,8 @@ if (option === undefined) { return console.error( `Multiselect can't remove selected option ${JSON.stringify( - to_remove - )}, not found in selected list` + to_remove, + )}, not found in selected list`, ) } @@ -474,6 +480,7 @@ data-id={id} role="searchbox" tabindex="-1" + {style} > @@ -502,7 +509,11 @@ -
          +
            {#each selected as option, idx (duplicates ? [key(option), idx] : key(option))}
          • (drag_idx = idx)} on:dragover|preventDefault class:active={drag_idx === idx} - style={get_style(option, `selected`)} + style="{get_style(option, `selected`)} {liSelectedStyle}" > @@ -544,6 +555,7 @@ {/each} {#each matchingOptions.slice(0, Math.max(0, maxOptions ?? 0) || Infinity) as option, idx} {@const { @@ -658,7 +671,7 @@ on:blur={() => (activeIndex = null)} role="option" aria-selected="false" - style={get_style(option, `option`)} + style="{get_style(option, `option`)} {liOptionStyle}" > diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 812af1a..b73022d 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -15,21 +15,22 @@ export const get_label = (opt: Option) => { return `${opt}` } +// this function is used extract CSS strings from a {selected, option} style object to be used in the style attribute of the option +// if the style is a string, it will be returned as is export function get_style( option: { style?: OptionStyle; [key: string]: unknown } | string | number, key: 'selected' | 'option' | null = null, ) { - if (!option?.style) return null + let css_str = `` if (![`selected`, `option`, null].includes(key)) { console.error(`MultiSelect: Invalid key=${key} for get_style`) - return } if (typeof option == `object` && option.style) { if (typeof option.style == `string`) { - return option.style + css_str = option.style } if (typeof option.style == `object`) { - if (key && key in option.style) return option.style[key] + if (key && key in option.style) return option.style[key] ?? `` else { console.error( `Invalid style object for option=${JSON.stringify(option)}`, @@ -37,4 +38,7 @@ export function get_style( } } } + // ensure css_str ends with a semicolon + if (css_str.trim() && !css_str.trim().endsWith(`;`)) css_str += `;` + return css_str } diff --git a/src/site/Examples.svx b/src/site/Examples.svx index c4fb175..257bee4 100644 --- a/src/site/Examples.svx +++ b/src/site/Examples.svx @@ -178,7 +178,6 @@