({ content, containerRef, onClose }: SearchBarProps)
| 11 | } |
| 12 | |
| 13 | export function SearchBar({ content, containerRef, onClose }: SearchBarProps) { |
| 14 | const [query, setQuery] = useState(""); |
| 15 | const [isRegex, setIsRegex] = useState(false); |
| 16 | const [caseSensitive, setCaseSensitive] = useState(false); |
| 17 | const [currentMatch, setCurrentMatch] = useState(0); |
| 18 | const [totalMatches, setTotalMatches] = useState(0); |
| 19 | const [hasError, setHasError] = useState(false); |
| 20 | const inputRef = useRef<HTMLInputElement>(null); |
| 21 | |
| 22 | useEffect(() => { |
| 23 | inputRef.current?.focus(); |
| 24 | }, []); |
| 25 | |
| 26 | // Compute matches |
| 27 | useEffect(() => { |
| 28 | if (!query) { |
| 29 | setTotalMatches(0); |
| 30 | setCurrentMatch(0); |
| 31 | clearHighlights(); |
| 32 | return; |
| 33 | } |
| 34 | |
| 35 | try { |
| 36 | const flags = caseSensitive ? "g" : "gi"; |
| 37 | const pattern = isRegex ? new RegExp(query, flags) : new RegExp(escapeRegex(query), flags); |
| 38 | const matches = Array.from(content.matchAll(pattern)); |
| 39 | setTotalMatches(matches.length); |
| 40 | setCurrentMatch(matches.length > 0 ? 1 : 0); |
| 41 | setHasError(false); |
| 42 | } catch { |
| 43 | setHasError(true); |
| 44 | setTotalMatches(0); |
| 45 | setCurrentMatch(0); |
| 46 | } |
| 47 | }, [query, isRegex, caseSensitive, content]); |
| 48 | |
| 49 | // Apply DOM highlights |
| 50 | useEffect(() => { |
| 51 | if (!containerRef.current) return; |
| 52 | clearHighlights(); |
| 53 | |
| 54 | if (!query || hasError || totalMatches === 0) return; |
| 55 | |
| 56 | try { |
| 57 | const flags = caseSensitive ? "g" : "gi"; |
| 58 | const pattern = isRegex ? new RegExp(query, flags) : new RegExp(escapeRegex(query), flags); |
| 59 | |
| 60 | const walker = document.createTreeWalker( |
| 61 | containerRef.current, |
| 62 | NodeFilter.SHOW_TEXT, |
| 63 | { |
| 64 | acceptNode: (node) => { |
| 65 | // Skip nodes inside already-marked elements |
| 66 | if ((node.parentElement as HTMLElement)?.tagName === "MARK") { |
| 67 | return NodeFilter.FILTER_REJECT; |
| 68 | } |
| 69 | return NodeFilter.FILTER_ACCEPT; |
| 70 | }, |
nothing calls this directly
no test coverage detected