/** * Copied from chakra-ui, license MIT * Accessed 2021-12-26 * We actually don't use these yet, YAGNI be damned. * See also: https://github.com/chakra-ui/chakra-ui/blob/8b5eb96/packages/utils/src/array.ts */ export function getFirstItem(array: T[]): T | undefined { return array != null && array.length ? array[0] : undefined; } export function getLastItem(array: T[]): T | undefined { const length = array == null ? 0 : array.length; return length ? array[length - 1] : undefined; } export function getPrevItem(index: number, array: T[], loop = true): T { const prevIndex = getPrevIndex(index, array.length, loop); return array[prevIndex]; } export function getNextItem(index: number, array: T[], loop = true): T { const nextIndex = getNextIndex(index, array.length, 1, loop); return array[nextIndex]; } export function removeIndex(array: T[], index: number): T[] { return array.filter((_, idx) => idx !== index); } export function addItem(array: T[], item: T): T[] { return [...array, item]; } export function removeItem(array: T[], item: T): T[] { return array.filter((eachItem) => eachItem !== item); } /** * Get the next index based on the current index and step. * * @param currentIndex the current index * @param length the total length or count of items * @param step the number of steps * @param loop whether to circle back once `currentIndex` is at the start/end */ export function getNextIndex( currentIndex: number, length: number, step = 1, loop = true, ): number { const lastIndex = length - 1; if (currentIndex === -1) { return step > 0 ? 0 : lastIndex; } const nextIndex = currentIndex + step; if (nextIndex < 0) { return loop ? lastIndex : 0; } if (nextIndex >= length) { if (loop) return 0; return currentIndex > length ? length : currentIndex; } return nextIndex; } /** * Get's the previous index based on the current index. * Mostly used for keyboard navigation. * * @param index - the current index * @param count - the length or total count of items in the array * @param loop - whether we should circle back to the * first/last once `currentIndex` is at the start/end */ export function getPrevIndex( index: number, count: number, loop = true, ): number { return getNextIndex(index, count, -1, loop); } /** * Converts an array into smaller chunks or groups. * * @param array the array to chunk into group * @param size the length of each chunk */ export function chunk(array: T[], size: number): T[][] { return array.reduce((rows: T[][], currentValue: T, index: number) => { if (index % size === 0) { rows.push([currentValue]); } else { rows[rows.length - 1].push(currentValue); } return rows; }, [] as T[][]); } /** * Gets the next item based on a search string * * @param items array of items * @param searchString the search string * @param itemToString resolves an item to string * @param currentItem the current selected item */ export function getNextItemFromSearch( items: T[], searchString: string, itemToString: (item: T) => string, currentItem: T, ): T | undefined { if (searchString == null) { return currentItem; } // If current item doesn't exist, find the item that matches the search string if (!currentItem) { const foundItem = items.find((item) => itemToString(item).toLowerCase().startsWith(searchString.toLowerCase()), ); return foundItem; } // Filter items for ones that match the search string (case insensitive) const matchingItems = items.filter((item) => itemToString(item).toLowerCase().startsWith(searchString.toLowerCase()), ); // If there's a match, let's get the next item to select if (matchingItems.length > 0) { let nextIndex: number; // If the currentItem is in the available items, we move to the next available option if (matchingItems.includes(currentItem)) { const currentIndex = matchingItems.indexOf(currentItem); nextIndex = currentIndex + 1; if (nextIndex === matchingItems.length) { nextIndex = 0; } return matchingItems[nextIndex]; } // Else, we pick the first item in the available items nextIndex = items.indexOf(matchingItems[0]); return items[nextIndex]; } // a decent fallback to the currentItem return currentItem; }