@melt-ui/svelte
Version:

66 lines (65 loc) • 2.58 kB
JavaScript
import { get, writable } from 'svelte/store';
import { wrapArray } from './array.js';
import { debounce } from './debounce.js';
import { isHTMLElement } from './is.js';
import { handleRovingFocus } from './rovingFocus.js';
import { withGet } from './withGet.js';
/**
* Keys to ignore for typeahead so we aren't matching things
* like `Shift menu item` or `Control center` or `Alt menu` when
* a user presses those keys.
*/
const ignoredKeys = new Set(['Shift', 'Control', 'Alt', 'Meta', 'CapsLock', 'NumLock']);
/**
* Default options for the typeahead search.
* We default to roving focus when a match is found, but
* you can override this with the `onMatch` option.
*/
const defaults = {
onMatch: handleRovingFocus,
getCurrentItem: () => document.activeElement,
};
export function createTypeaheadSearch(args = {}) {
const withDefaults = { ...defaults, ...args };
const typed = withGet(writable([]));
const resetTyped = debounce(() => {
typed.update(() => []);
});
const handleTypeaheadSearch = (key, items) => {
if (ignoredKeys.has(key))
return;
const currentItem = withDefaults.getCurrentItem();
const $typed = get(typed);
if (!Array.isArray($typed)) {
return;
}
$typed.push(key.toLowerCase());
typed.set($typed);
const candidateItems = items.filter((item) => {
if (item.getAttribute('disabled') === 'true' ||
item.getAttribute('aria-disabled') === 'true' ||
item.hasAttribute('data-disabled')) {
return false;
}
return true;
});
const isRepeated = $typed.length > 1 && $typed.every((char) => char === $typed[0]);
const normalizeSearch = isRepeated ? $typed[0] : $typed.join('');
const currentItemIndex = isHTMLElement(currentItem) ? candidateItems.indexOf(currentItem) : -1;
let wrappedItems = wrapArray(candidateItems, Math.max(currentItemIndex, 0));
const excludeCurrentItem = normalizeSearch.length === 1;
if (excludeCurrentItem) {
wrappedItems = wrappedItems.filter((v) => v !== currentItem);
}
const nextItem = wrappedItems.find((item) => item?.innerText && item.innerText.toLowerCase().startsWith(normalizeSearch.toLowerCase()));
if (isHTMLElement(nextItem) && nextItem !== currentItem) {
withDefaults.onMatch(nextItem);
}
resetTyped();
};
return {
typed,
resetTyped,
handleTypeaheadSearch,
};
}