combobox: use match sorter
This commit is contained in:
@@ -26,6 +26,7 @@
|
|||||||
import { scale } from 'svelte/transition';
|
import { scale } from 'svelte/transition';
|
||||||
import { generateIdentifier } from './util';
|
import { generateIdentifier } from './util';
|
||||||
import type { ClassValue } from 'svelte/elements';
|
import type { ClassValue } from 'svelte/elements';
|
||||||
|
import { matchSorter } from 'match-sorter';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -38,11 +39,14 @@
|
|||||||
invalidMessage?: string | null;
|
invalidMessage?: string | null;
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
loading?: boolean;
|
||||||
notFoundMessage?: string;
|
notFoundMessage?: string;
|
||||||
class?: ClassValue | null | undefined;
|
class?: ClassValue | null | undefined;
|
||||||
use?: () => void;
|
use?: () => void;
|
||||||
onvalidate?: (e: InputValidatorEvent) => void;
|
onvalidate?: (e: InputValidatorEvent) => void;
|
||||||
onchange?: (item: ComboboxOption) => void;
|
onchange?: (item: ComboboxOption) => void;
|
||||||
|
onsearch?: (query: string) => boolean | void;
|
||||||
|
onscroll?: (detail: { event: MouseEvent; top: boolean; false: boolean }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -56,11 +60,14 @@
|
|||||||
invalidMessage = 'Please select an option',
|
invalidMessage = 'Please select an option',
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
loading = false,
|
||||||
notFoundMessage = 'No results found',
|
notFoundMessage = 'No results found',
|
||||||
class: classValue,
|
class: classValue,
|
||||||
use,
|
use,
|
||||||
onvalidate,
|
onvalidate,
|
||||||
onchange
|
onchange,
|
||||||
|
onsearch,
|
||||||
|
onscroll
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let id = $derived(generateIdentifier('combobox', name));
|
let id = $derived(generateIdentifier('combobox', name));
|
||||||
@@ -74,32 +81,39 @@
|
|||||||
let searchContainer = $state<HTMLDivElement | null>(null);
|
let searchContainer = $state<HTMLDivElement | null>(null);
|
||||||
let pickerContainer = $state<HTMLDivElement | null>(null);
|
let pickerContainer = $state<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
/** stores options filtered according to search value */
|
||||||
const filteredItems = $derived(
|
const filteredItems = $derived(
|
||||||
searchValue === ''
|
searchValue === ''
|
||||||
? options
|
? options
|
||||||
: options.filter((item) => getLabel(item).toLowerCase().includes(searchValue.toLowerCase()))
|
: matchSorter(options, searchValue, { keys: [(item) => getLabel(item)] })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** stores currently highlighted option (according to hover state or keyboard navigation) */
|
||||||
let highlighted = $derived.by((): ComboboxOption | undefined => {
|
let highlighted = $derived.by((): ComboboxOption | undefined => {
|
||||||
if (filteredItems.length === 0) return undefined;
|
if (filteredItems.length === 0) return undefined;
|
||||||
if (value !== undefined && filteredItems.find((v) => v.value === value?.value)) return value;
|
if (value !== undefined && filteredItems.find((v) => v.value === value?.value)) return value;
|
||||||
return filteredItems[0];
|
return filteredItems[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** controls whether an icon should be displayed */
|
||||||
let iconVisible = $derived.by(() => {
|
let iconVisible = $derived.by(() => {
|
||||||
return (open && highlighted && highlighted.icon) || (value && value.icon && searchValue === '');
|
return (open && highlighted && highlighted.icon) || (value && value.icon && searchValue === '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** controls whether the highlighted option should be used in the selection preview */
|
||||||
let useHighlighted = $derived.by(() => {
|
let useHighlighted = $derived.by(() => {
|
||||||
return open && highlighted;
|
return open && highlighted;
|
||||||
});
|
});
|
||||||
|
|
||||||
const validateOpts: ValidatorOptions = { required };
|
const validateOpts: ValidatorOptions = { required };
|
||||||
|
|
||||||
|
/** returns the index of the highlighted item in filteredItems */
|
||||||
const getHightlightedID = () => {
|
const getHightlightedID = () => {
|
||||||
if (highlighted === undefined) return -1;
|
if (highlighted === undefined) return -1;
|
||||||
return filteredItems.findIndex((item) => item.value === highlighted?.value);
|
return filteredItems.findIndex((item) => item.value === highlighted?.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** action to set the minimum width of the combobox based on the number of options */
|
||||||
const minWidth: Action<HTMLDivElement, { options: ComboboxOption[] }> = (
|
const minWidth: Action<HTMLDivElement, { options: ComboboxOption[] }> = (
|
||||||
container,
|
container,
|
||||||
buildOpts
|
buildOpts
|
||||||
@@ -125,6 +139,7 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** updates the position of the picker */
|
||||||
const updatePickerRect = async () => {
|
const updatePickerRect = async () => {
|
||||||
if (!searchContainer || !pickerContainer) {
|
if (!searchContainer || !pickerContainer) {
|
||||||
await tick();
|
await tick();
|
||||||
@@ -166,6 +181,7 @@
|
|||||||
overlay.style.left = `${targetRect.left}px`;
|
overlay.style.left = `${targetRect.left}px`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** scrolls the picker to the highlighted item */
|
||||||
const scrollToHighlighted = () => {
|
const scrollToHighlighted = () => {
|
||||||
if (!pickerContainer || !highlighted) return;
|
if (!pickerContainer || !highlighted) return;
|
||||||
const highlightedElement = pickerContainer.querySelector(`[data-id="${highlighted.value}"]`);
|
const highlightedElement = pickerContainer.querySelector(`[data-id="${highlighted.value}"]`);
|
||||||
@@ -187,10 +203,12 @@
|
|||||||
|
|
||||||
const conditionalUse = $derived(use ? use : () => {});
|
const conditionalUse = $derived(use ? use : () => {});
|
||||||
|
|
||||||
|
/** focuses the combobox search input */
|
||||||
export const focus = () => {
|
export const focus = () => {
|
||||||
searchInput?.focus();
|
searchInput?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** sets the value of the combobox by its underlying value field */
|
||||||
export const setValueByString = (searchVal: string) => {
|
export const setValueByString = (searchVal: string) => {
|
||||||
const item = options.find((opt) => opt.value === searchVal);
|
const item = options.find((opt) => opt.value === searchVal);
|
||||||
if (item) {
|
if (item) {
|
||||||
|
|||||||
Reference in New Issue
Block a user