FumadocsLector
Code

Custom Search

Add powerful search capabilities to your PDF viewer with real-time highlighting

Loading...

Basic Usage

Create a PDF viewer with custom search functionality and result highlighting:

"use client";
 
import {
  Root,
  Pages,
  Page,
  CanvasLayer,
  TextLayer,
  HighlightLayer,
  Search,
} from "@anaralabs/lector";
import { useState } from "react";
 
const ResultItem = ({ result }) => (
  <div className="flex py-2 hover:bg-gray-50 cursor-pointer">
    <div className="flex-1 min-w-0">
      <p className="text-sm text-gray-900">{result.text}</p>
      <p className="text-sm text-gray-500">Page {result.pageNumber}</p>
    </div>
  </div>
);
 
const SearchUI = () => {
  const [searchText, setSearchText] = useState("");
 
  return (
    <div className="flex flex-col w-64 h-full">
      <div className="px-4 py-4 border-b border-gray-200 bg-white">
        <input
          type="text"
          value={searchText}
          onChange={(e) => setSearchText(e.target.value)}
          placeholder="Search in document..."
          className="w-full px-4 py-2 border rounded-lg"
        />
      </div>
      <div className="flex-1 overflow-y-auto px-4">
        {/* Results will appear here */}
      </div>
    </div>
  );
};
 
export default function CustomSearch() {
  return (
    <Root source="/pdf/large.pdf" className="flex bg-gray-50 h-[500px]">
      <Search>
        <SearchUI />
      </Search>
      <Pages className="p-4 w-full">
        <Page>
          <CanvasLayer />
          <TextLayer />
          <HighlightLayer className="bg-yellow-200/70" />
        </Page>
      </Pages>
    </Root>
  );
}

Advanced Implementation

For more advanced features like debouncing, fuzzy matching, and jumping to results, here's the complete implementation:

import { useDebounce } from "use-debounce";
import {
  calculateHighlightRects,
  SearchResult,
  usePdf,
  usePdfJump,
  useSearch,
} from "@anaralabs/lector";
import { useEffect, useState } from "react";
 
interface ResultItemProps {
  result: SearchResult;
}
 
const ResultItem = ({ result }: ResultItemProps) => {
  const { jumpToHighlightRects } = usePdfJump();
  const getPdfPageProxy = usePdf((state) => state.getPdfPageProxy);
 
  const onClick = async () => {
    const pageProxy = getPdfPageProxy(result.pageNumber);
    const rects = await calculateHighlightRects(pageProxy, {
      pageNumber: result.pageNumber,
      text: result.text,
      matchIndex: result.matchIndex,
    });
    jumpToHighlightRects(rects, "pixels");
  };
 
  return (
    <div
      className="flex py-2 hover:bg-gray-50 flex-col cursor-pointer"
      onClick={onClick}
    >
      <div className="flex-1 min-w-0">
        <p className="text-sm text-gray-900">{result.text}</p>
      </div>
      <div className="flex items-center gap-4 text-sm text-gray-500">
        <span className="ml-auto">Page {result.pageNumber}</span>
      </div>
    </div>
  );
};
 
interface ResultGroupProps {
  title: string;
  results: SearchResult[];
  displayCount?: number;
}
 
const ResultGroup = ({ title, results, displayCount }: ResultGroupProps) => {
  if (!results.length) return null;
 
  const displayResults = displayCount
    ? results.slice(0, displayCount)
    : results;
 
  return (
    <div className="space-y-2">
      <h3 className="text-sm font-medium text-gray-700">{title}</h3>
      <div className="divide-y divide-gray-100">
        {displayResults.map((result) => (
          <ResultItem
            key={`${result.pageNumber}-${result.matchIndex}`}
            result={result}
          />
        ))}
      </div>
    </div>
  );
};
 
export function SearchUI() {
  const [searchText, setSearchText] = useState("");
  const [debouncedSearchText] = useDebounce(searchText, 500);
  const [limit, setLimit] = useState(5);
  const { searchResults: results, search } = useSearch();
 
  useEffect(() => {
    setLimit(5);
    search(debouncedSearchText, { limit: 5 });
  }, [debouncedSearchText]);
 
  const handleLoadMore = async () => {
    const newLimit = limit + 5;
    await search(debouncedSearchText, { limit: newLimit });
    setLimit(newLimit);
  };
 
  return (
    <div className="flex flex-col w-80 h-full">
      <div className="px-4 py-4 border-b border-gray-200 bg-white">
        <input
          type="text"
          value={searchText}
          onChange={(e) => setSearchText(e.target.value)}
          placeholder="Search in document..."
          className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
        />
      </div>
      <div className="flex-1 overflow-y-auto px-4">
        <div className="py-4">
          <SearchResults
            searchText={searchText}
            results={results}
            onLoadMore={handleLoadMore}
          />
        </div>
      </div>
    </div>
  );
}

Features

  • Real-time search with debouncing
  • Result highlighting
  • Page jumping to search results
  • Load more functionality

Best Practices

  • Use debouncing to prevent excessive searches
  • Include proper page navigation
  • Handle empty states gracefully
  • Optimize search performance

On this page