Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Fragment>
<div className="widget-document-viewer-controls">
Expand All @@ -31,6 +32,7 @@ const BaseViewer = (props: BaseViewerProps): ReactElement => {
</div>
<div className="widget-document-viewer-controls-icons">{CustomControl}</div>
</div>
{SecondaryControl}
<div className="widget-document-viewer-content">{children}</div>
</Fragment>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -37,11 +50,40 @@ const PDFViewer: DocRendererElement = (props: DocumentRendererProps) => {
const [currentPage, setCurrentPage] = useState<number>(1);
const [pageInputValue, setPageInputValue] = useState<string>("1");
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [pdfDoc, setPdfDoc] = useState<PDFDocumentProxy | null>(null);
const [showSearch, setShowSearch] = useState<boolean>(false);
const [searchQuery, setSearchQuery] = useState<string>("");
const [debouncedQuery, setDebouncedQuery] = useState<string>("");
const searchInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
setSearchQuery(event.target.value);
}, []);

const handleSearchKeyDown = useCallback(
(event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Escape") {
event.preventDefault();
toggleSearch();
}
},
[toggleSearch]
);

const handlePageInputChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
// Allow only numbers and empty string
Expand Down Expand Up @@ -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 <div>No document selected</div>;
}
Expand All @@ -122,6 +197,39 @@ const PDFViewer: DocRendererElement = (props: DocumentRendererProps) => {
<BaseViewer
{...props}
fileName={file.value?.name || ""}
SecondaryControl={
showSearch ? (
<div className="widget-document-viewer-search-bar">
<input
ref={searchInputRef}
type="search"
value={searchQuery}
onChange={handleSearchInputChange}
onKeyDown={handleSearchKeyDown}
className="form-control widget-document-viewer-search-input"
aria-label="Search in document"
placeholder="Search…"
/>
<span className="widget-document-viewer-search-count" aria-live="polite">
{searchMatchLabel}
</span>
<button
onClick={goToPrevMatch}
disabled={matches.length === 0}
className="icons icon-Left btn btn-icon-only"
aria-label="Previous match"
title="Previous match"
></button>
<button
onClick={goToNextMatch}
disabled={matches.length === 0}
className="icons icon-Right btn btn-icon-only"
aria-label="Next match"
title="Next match"
></button>
</div>
) : null
}
CustomControl={
<Fragment>
<div className="widget-document-viewer-pagination">
Expand Down Expand Up @@ -158,6 +266,13 @@ const PDFViewer: DocRendererElement = (props: DocumentRendererProps) => {
title={"Go to next page"}
></button>
</div>
<button
onClick={toggleSearch}
className="icons icon-Search btn btn-icon-only widget-document-viewer-search-toggle"
aria-label={showSearch ? "Close search" : "Search in document"}
aria-pressed={showSearch}
title={showSearch ? "Close search" : "Search in document"}
></button>
<button
onClick={onDownloadClick}
className="icons icon-Download btn btn-icon-only"
Expand Down Expand Up @@ -202,7 +317,21 @@ const PDFViewer: DocRendererElement = (props: DocumentRendererProps) => {
})
}
>
<Page pageNumber={currentPage} scale={zoomLevel} />
<div className="widget-document-viewer-highlight-layer">
<Page pageNumber={currentPage} scale={zoomLevel} />
{highlightRects.map((rect, i) => (
<div
key={i}
className={`widget-document-viewer-highlight${rect.isCurrent ? " current" : ""}`}
style={{
left: rect.x,
top: rect.y,
width: rect.width,
height: rect.height
}}
/>
))}
</div>
</Document>
</If>
</BaseViewer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,58 @@ div.widget-document-viewer {
margin-right: 5px;
width: 5ch;
}

.widget-document-viewer-search-bar {
display: flex;
align-items: center;
gap: var(--spacing-small, 4px);
margin: 0 calc(-1 * var(--form-input-padding-x));
padding: var(--spacing-small, 4px) var(--spacing-large, 16px);
background-color: var(--gray-lighter);
border-bottom: 1px solid var(--border-color-default, #ced0d3);

.widget-document-viewer-search-input {
max-width: 200px;
padding: 3px 6px !important;
}

.widget-document-viewer-search-count {
min-width: 80px;
font-size: var(--font-size-small, 12px);
color: var(--text-color-secondary, #6c757d);
white-space: nowrap;
}
}

.react-pdf__Page__textContent {
.widget-document-viewer-search-match {
background-color: rgba(255, 210, 0, 0.45);
border-radius: 2px;
color: inherit;

&--current {
background-color: rgba(255, 140, 0, 0.75);
outline: 1px solid rgba(200, 100, 0, 0.8);
}
}
}

.widget-document-viewer-highlight-layer {
position: relative;
display: inline-block;
line-height: 0;

.widget-document-viewer-highlight {
position: absolute;
background-color: rgba(255, 210, 0, 0.4);
mix-blend-mode: multiply;
border-radius: 2px;
pointer-events: none;

&.current {
background-color: rgba(255, 140, 0, 0.6);
mix-blend-mode: multiply;
box-shadow: 0 0 0 1.5px rgba(200, 100, 0, 0.9);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ $icons: (
Left: "\e902",
ZoomIn: "\e901",
ZoomOut: "\e900",
FitToWidth: "\e904"
FitToWidth: "\e904",
Search: "\e905"
);

.icons.btn {
Expand All @@ -21,25 +22,42 @@ $icons: (
}
}

// Apply DocViewer font to all .icons elements anywhere in the widget
div.widget-document-viewer {
&-controls {
button {
margin-left: var(--spacing-smaller, 4px);
}
.icons {
font-family: "DocViewer" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
padding: var(--spacing-smallest) var(--spacing-small);
}

.icons {
font-family: "DocViewer" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
padding: var(--spacing-smallest) var(--spacing-small);

@each $name, $code in $icons {
&.icon-#{$name}:before {
content: $code;
}
@each $name, $code in $icons {
&.icon-#{$name}:before {
content: $code;
}
}
}
}
// Search toggle button — pressed state via aria-pressed so Atlas hover/focus work normally
// .widget-document-viewer-search-toggle {
// &[aria-pressed="true"] {
// background-color: var(--brand-primary, #264ae5);
// color: #fff;
// border-radius: var(--border-radius-default);

// &:hover,
// &:focus-visible {
// background-color: var(--brand-primary-dark, #1a36c4);
// color: #fff;
// }
// }
// }
// }
Loading
Loading