diff --git a/packages/pluggableWidgets/document-viewer-web/src/assets/DocViewer.woff2 b/packages/pluggableWidgets/document-viewer-web/src/assets/DocViewer.woff2 index 16ebb6d15e..b3d19dce0f 100644 Binary files a/packages/pluggableWidgets/document-viewer-web/src/assets/DocViewer.woff2 and b/packages/pluggableWidgets/document-viewer-web/src/assets/DocViewer.woff2 differ diff --git a/packages/pluggableWidgets/document-viewer-web/src/components/BaseViewer.tsx b/packages/pluggableWidgets/document-viewer-web/src/components/BaseViewer.tsx index e395ca1968..fa69e79567 100644 --- a/packages/pluggableWidgets/document-viewer-web/src/components/BaseViewer.tsx +++ b/packages/pluggableWidgets/document-viewer-web/src/components/BaseViewer.tsx @@ -19,10 +19,11 @@ interface BaseControlViewerProps extends PropsWithChildren { interface BaseViewerProps extends PropsWithChildren { fileName: string; CustomControl?: ReactNode; + SecondaryControl?: ReactNode; } const BaseViewer = (props: BaseViewerProps): ReactElement => { - const { fileName, CustomControl, children } = props; + const { fileName, CustomControl, SecondaryControl, children } = props; return (
@@ -31,6 +32,7 @@ const BaseViewer = (props: BaseViewerProps): ReactElement => {
{CustomControl}
+ {SecondaryControl}
{children}
); diff --git a/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx b/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx index 1f3b7da0a6..dc63054fb2 100644 --- a/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx +++ b/packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx @@ -1,8 +1,21 @@ -import { ChangeEvent, FormEvent, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useState } from "react"; +import { + ChangeEvent, + FormEvent, + Fragment, + KeyboardEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState +} from "react"; import { Document, Page, pdfjs } from "react-pdf"; import "react-pdf/dist/Page/AnnotationLayer.css"; import "react-pdf/dist/Page/TextLayer.css"; +import type { PDFDocumentProxy } from "pdfjs-dist"; import { downloadFile } from "../utils/helpers"; +import { usePDFHighlightPositions } from "../utils/usePDFHighlightPositions"; +import { usePDFSearch } from "../utils/usePDFSearch"; import { useZoomScale } from "../utils/useZoomScale"; import BaseViewer from "./BaseViewer"; import { DocRendererElement, DocumentRendererProps, DocumentStatus } from "./documentRenderer"; @@ -37,11 +50,40 @@ const PDFViewer: DocRendererElement = (props: DocumentRendererProps) => { const [currentPage, setCurrentPage] = useState(1); const [pageInputValue, setPageInputValue] = useState("1"); const [pdfUrl, setPdfUrl] = useState(null); + const [pdfDoc, setPdfDoc] = useState(null); + const [showSearch, setShowSearch] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [debouncedQuery, setDebouncedQuery] = useState(""); + const searchInputRef = useRef(null); const onDownloadClick = useCallback(() => { downloadFile(file.value?.uri); }, [file]); + const toggleSearch = useCallback(() => { + setShowSearch(prev => { + if (prev) { + setSearchQuery(""); + setDebouncedQuery(""); + } + return !prev; + }); + }, []); + + const handleSearchInputChange = useCallback((event: ChangeEvent) => { + setSearchQuery(event.target.value); + }, []); + + const handleSearchKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault(); + toggleSearch(); + } + }, + [toggleSearch] + ); + const handlePageInputChange = useCallback((event: ChangeEvent) => { const value = event.target.value; // Allow only numbers and empty string @@ -102,18 +144,51 @@ const PDFViewer: DocRendererElement = (props: DocumentRendererProps) => { if (file.value?.uri) { setCurrentPage(1); setPageInputValue("1"); + setPdfDoc(null); + setSearchQuery(""); + setDebouncedQuery(""); } }, [file.value]); + // Debounce search query to avoid triggering search on every keystroke + useEffect(() => { + const timer = setTimeout(() => setDebouncedQuery(searchQuery), 300); + return () => clearTimeout(timer); + }, [searchQuery]); + + // Auto-focus search input when search bar opens + useEffect(() => { + if (showSearch) { + searchInputRef.current?.focus(); + } + }, [showSearch]); + // Sync page input value with current page useEffect(() => { setPageInputValue(currentPage.toString()); }, [currentPage]); - function onDocumentLoadSuccess({ numPages }: { numPages: number }): void { - setNumberOfPages(numPages); + function onDocumentLoadSuccess(pdf: PDFDocumentProxy): void { + setNumberOfPages(pdf.numPages); + setPdfDoc(pdf); } + const { matches, currentMatchIndex, goToNextMatch, goToPrevMatch, isSearching } = usePDFSearch( + pdfDoc, + debouncedQuery, + setCurrentPage + ); + + const highlightRects = usePDFHighlightPositions(pdfDoc, currentPage, zoomLevel, matches, currentMatchIndex); + + const searchMatchLabel = debouncedQuery.trim() + ? isSearching + ? "Searching…" + : matches.length === 0 + ? "No results" + : `${currentMatchIndex + 1} of ${matches.length}` + : ""; + if (!file.value?.uri) { return
No document selected
; } @@ -122,6 +197,39 @@ const PDFViewer: DocRendererElement = (props: DocumentRendererProps) => { + + + {searchMatchLabel} + + + + + ) : null + } CustomControl={
@@ -158,6 +266,13 @@ const PDFViewer: DocRendererElement = (props: DocumentRendererProps) => { title={"Go to next page"} >
+