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