import { computed, toValue } from 'vue'; import type { ComputedRef, MaybeRefOrGetter } from 'vue'; import { isFunction, isNumber, isObject, isString, isSymbol } from '@robonen/stdlib'; /** * Comparator deciding whether an array element equals the searched value. */ export type UseArrayIncludesComparatorFn = (element: T, value: V, index: number, array: T[]) => boolean; export interface UseArrayIncludesOptions { /** * Index at which to start searching (negative counts from the end, like `Array.prototype.includes`). * * @default 0 */ fromIndex?: number; /** * Custom comparator function, or a key of `T` to compare a single property by. */ comparator?: UseArrayIncludesComparatorFn | keyof T; } export type UseArrayIncludesReturn = ComputedRef; function isArrayIncludesOptions(value: unknown): value is UseArrayIncludesOptions { // isObject matches PLAIN objects only, so functions/keys never reach here. return isObject(value) && ('fromIndex' in value || 'comparator' in value); } /** * @name useArrayIncludes * @category Array * @description Reactive `Array.prototype.includes` with an optional comparator and `fromIndex`. The source array and its items may be reactive. * * @param {MaybeRefOrGetter[]>} list The source array (items can be reactive) * @param {MaybeRefOrGetter} value The value to search for (may be reactive) * @param {UseArrayIncludesComparatorFn | keyof T | UseArrayIncludesOptions} [comparator] A comparator function, a key of `T` to compare by, or an options object with `comparator`/`fromIndex` * @returns {UseArrayIncludesReturn} A computed boolean that is `true` when the value is found * * @example * const list = ref([1, 2, 3, 4]); * const hasThree = useArrayIncludes(list, 3); // true * * @example * const list = ref([{ id: 1 }, { id: 2 }]); * const hasTwo = useArrayIncludes(list, 2, 'id'); // compare by key * * @example * const list = ref(['a', 'b', 'a']); * const fromSecond = useArrayIncludes(list, 'a', { fromIndex: 1 }); // true * * @since 0.0.14 */ export function useArrayIncludes( list: MaybeRefOrGetter>>, value: MaybeRefOrGetter, comparator?: UseArrayIncludesComparatorFn, ): UseArrayIncludesReturn; export function useArrayIncludes( list: MaybeRefOrGetter>>, value: MaybeRefOrGetter, comparator?: keyof T, ): UseArrayIncludesReturn; export function useArrayIncludes( list: MaybeRefOrGetter>>, value: MaybeRefOrGetter, options?: UseArrayIncludesOptions, ): UseArrayIncludesReturn; export function useArrayIncludes( list: MaybeRefOrGetter>>, value: MaybeRefOrGetter, comparator?: UseArrayIncludesComparatorFn | keyof T | UseArrayIncludesOptions, ): UseArrayIncludesReturn { let fromIndex = 0; let resolved = comparator; if (isArrayIncludesOptions(resolved)) { fromIndex = resolved.fromIndex ?? 0; resolved = resolved.comparator; } // Resolve the comparator once instead of on every recompute. let compare: UseArrayIncludesComparatorFn; if (isString(resolved) || isSymbol(resolved) || isNumber(resolved)) { const key = resolved as keyof T; compare = (element, searched) => element[key] === (searched as unknown); } else if (isFunction(resolved)) { compare = resolved; } else { compare = (element, searched) => (element as unknown) === searched; } return computed(() => { const array = toValue(list); const searched = toValue(value); const length = array.length; // Resolve a negative / out-of-range fromIndex the same way Array.includes does. let start = fromIndex < 0 ? length + fromIndex : fromIndex; if (start < 0) start = 0; for (let index = start; index < length; index++) { // `index` is bounded by `length`; `!` drops the index-access undefined. if (compare(toValue(array[index]!), searched, index, array as T[])) return true; } return false; }); }