All files / hooks use-pdf-search.ts

0% Statements 0/53
0% Branches 0/20
0% Functions 0/9
0% Lines 0/48

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100                                                                                                                                                                                                       
"use client";
 
import { useEffect, useState } from "react";
import { pdfjs } from "react-pdf";
 
type PDFDocumentProxy = pdfjs.PDFDocumentProxy;
 
export function usePdfSearch(pageRefs: React.RefObject<Map<number, HTMLDivElement>>) {
  const [pdfDoc, setPdfDoc] = useState<PDFDocumentProxy | null>(null);
  const [searchQuery, setSearchQuery] = useState("");
  const [matchPages, setMatchPages] = useState<number[]>([]);
  const [matchIndex, setMatchIndex] = useState(0);
  const [isSearching, setIsSearching] = useState(false);
  const [searched, setSearched] = useState(false);
  const currentMatchPage = matchPages[matchIndex] ?? null;
 
  useEffect(() => {
    if (currentMatchPage === null) return;
    const el = pageRefs.current?.get(currentMatchPage);
    if (el) {
      el.scrollIntoView({ behavior: "smooth", block: "start" });
    }
  }, [currentMatchPage, matchIndex, pageRefs]);
 
  async function runSearch() {
    if (!pdfDoc || !searchQuery.trim()) return;
    setIsSearching(true);
    setMatchPages([]);
    setMatchIndex(0);
    setSearched(false);
 
    const query = searchQuery.trim().toLowerCase();
    const found: number[] = [];
 
    for (let i = 1; i <= pdfDoc.numPages; i++) {
      const page = await pdfDoc.getPage(i);
      const textContent = await page.getTextContent();
      const pageText = textContent.items
        .map((item) => ("str" in item ? item.str : ""))
        .join(" ")
        .toLowerCase();
 
      if (pageText.includes(query)) {
        found.push(i);
      }
    }
 
    setMatchPages(found);
    setMatchIndex(0);
    setSearched(true);
    setIsSearching(false);
  }
 
  function navigateNext() {
    if (searched && matchPages.length > 0) {
      setMatchIndex((prev) => (prev + 1) % matchPages.length);
    } else {
      void runSearch();
    }
  }
 
  function handleQueryChange(e: React.ChangeEvent<HTMLInputElement>) {
    setSearchQuery(e.target.value);
    setMatchPages([]);
    setMatchIndex(0);
    setSearched(false);
  }
 
  function handleClearQuery() {
    setSearchQuery("");
    setMatchPages([]);
    setMatchIndex(0);
    setSearched(false);
  }
 
  function highlightText(text: string) {
    if (!searchQuery.trim()) return text;
    const escaped = searchQuery.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const regex = new RegExp(`(${escaped})`, "gi");
    return text.replace(
      regex,
      `<mark class="bg-yellow-300 text-black rounded-sm">$1</mark>`,
    );
  }
 
  return {
    pdfDoc,
    setPdfDoc,
    searchQuery,
    matchPages,
    matchIndex,
    isSearching,
    searched,
    navigateNext,
    handleQueryChange,
    handleClearQuery,
    highlightText,
  };
}