import { computed, toValue, type ComputedRef, type Ref } from 'vue' export type TextSearchSource = | T | Ref | ComputedRef | (() => T) export type TextSearchCandidate = unknown | readonly unknown[] export interface UseTextSearchOptions { items: TextSearchSource query: TextSearchSource enabled?: TextSearchSource getText: (item: TItem) => TextSearchCandidate } export interface UseTextSearchReturn { query: ComputedRef filteredItems: ComputedRef matches: (item: TItem, query?: string | null | undefined) => boolean } /** Shared text-search filtering for tables, lists, selectors, and chip inputs. */ export function useTextSearch(options: UseTextSearchOptions): UseTextSearchReturn { const query = computed(() => normalizeSearchQuery(toValue(options.query))) const enabled = computed(() => toValue(options.enabled) ?? true) function matches(item: TItem, overrideQuery?: string | null | undefined): boolean { const activeQuery = overrideQuery === undefined ? query.value : normalizeSearchQuery(overrideQuery) if (!activeQuery) return true return candidateMatchesSearch(options.getText(item), activeQuery) } const filteredItems = computed(() => { const items = [...toValue(options.items)] if (!enabled.value || !query.value) return items return items.filter(item => matches(item)) }) return { query, filteredItems, matches, } } export function normalizeSearchQuery(value: string | null | undefined): string { return (value ?? '').trim().toLowerCase() } export function candidateMatchesSearch(candidate: TextSearchCandidate, query: string): boolean { const normalizedQuery = normalizeSearchQuery(query) if (!normalizedQuery) return true const values = Array.isArray(candidate) ? candidate : [candidate] return values.some((value) => { if (value === null || value === undefined) return false return String(value).toLowerCase().includes(normalizedQuery) }) }