Skip to content
Draft
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
166 changes: 166 additions & 0 deletions app/react/V2/Components/PDFViewer.v2/PDF.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import React, { useEffect, useRef, useState } from 'react';
import { HandleTextSelection, TextSelection } from '@huridocs/react-text-selection-handler';
import { getDocument, PDFDocumentProxy, PDFDocumentLoadingTask } from 'pdfjs-dist/webpack.mjs';
import { EventBus, PDFLinkService, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs';
import 'pdfjs-dist/web/pdf_viewer.css';

const TEXT_LAYER_MODE = 1;
const CMAP_URL = 'legacy_character_maps/';
const CMAP_PACKED = true;
const noop = () => undefined;

type PDFEventBus = InstanceType<typeof EventBus>;

type PDFProps = {
fileUrl: string;
className?: string;
onSelect?: (selection: TextSelection) => void;
onDeselect?: () => void;
onDocumentLoaded?: (pdfDocument: PDFDocumentProxy, eventBus: PDFEventBus) => void;
onPageRendered?: (pageNumber: number) => void;
onPageChanged?: (pageNumber: number, previousPageNumber: number) => void;
onEventBusReady?: (eventBus: PDFEventBus) => void;
};

const PDF = ({
fileUrl,
className,
onSelect,
onDeselect,
onDocumentLoaded,
onPageRendered,
onPageChanged,
onEventBusReady,
}: PDFProps) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const viewerRef = useRef<HTMLDivElement | null>(null);
const eventBusRef = useRef<PDFEventBus | null>(null);
const pdfViewerRef = useRef<PDFViewer | null>(null);
const loadingTaskRef = useRef<PDFDocumentLoadingTask | null>(null);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const container = containerRef.current;
const viewer = viewerRef.current;

if (!container || !viewer) {
return undefined;
}

const eventBus = new EventBus();
const linkService = new PDFLinkService({ eventBus });
const pdfViewer = new PDFViewer({
container,
viewer,
eventBus,
linkService,
textLayerMode: TEXT_LAYER_MODE,
});

linkService.setViewer(pdfViewer);

eventBusRef.current = eventBus;
pdfViewerRef.current = pdfViewer;
onEventBusReady?.(eventBus);

const onPageRenderedInternal = (event: { pageNumber: number }) => {
const pageElement = viewerRef.current?.querySelector(
`.page[data-page-number="${event.pageNumber}"]`
);

pageElement?.setAttribute('data-region-selector-id', event.pageNumber.toString());
onPageRendered?.(event.pageNumber);
};

const onPageChangedInternal = (event: { pageNumber: number; previous: number }) => {
onPageChanged?.(event.pageNumber, event.previous);
};

eventBus.on('pagerendered', onPageRenderedInternal);
eventBus.on('pagechanging', onPageChangedInternal);

return () => {
eventBus.off('pagerendered', onPageRenderedInternal);
eventBus.off('pagechanging', onPageChangedInternal);
eventBusRef.current = null;
pdfViewerRef.current = null;
};
}, [onEventBusReady, onPageChanged, onPageRendered]);

useEffect(() => {
const eventBus = eventBusRef.current;
const pdfViewer = pdfViewerRef.current;

if (!eventBus || !pdfViewer || !fileUrl) {
return undefined;
}

setError(null);

const previousTask = loadingTaskRef.current;

if (previousTask) {
previousTask.destroy().catch(e => {
setError(e.message);
});
}

const loadingTask = getDocument({
url: fileUrl,
cMapUrl: CMAP_URL,
cMapPacked: CMAP_PACKED,
isEvalSupported: false,
});

loadingTaskRef.current = loadingTask;

loadingTask.promise
.then(pdfDocument => {
if (loadingTaskRef.current !== loadingTask) {
return;
}

pdfViewer.setDocument(pdfDocument);
pdfViewer.currentScaleValue = 'page-width';

// eventBus.dispatch('documentloaded', {
// source: pdfViewer,
// pdfDocument,
// });

if (onDocumentLoaded) {
onDocumentLoaded(pdfDocument, eventBus);
}
})
.catch((loadError: Error) => {
if (loadingTaskRef.current !== loadingTask) {
return;
}

setError(loadError.message);
});

return () => {
if (loadingTaskRef.current === loadingTask) {
loadingTask.destroy().catch(() => undefined);
loadingTaskRef.current = null;
}
};
}, [fileUrl, onDocumentLoaded]);

if (error) {
return <div>{error}</div>;
}

return (
<HandleTextSelection onSelect={onSelect || noop} onDeselect={onDeselect || noop}>
<div style={{ position: 'relative' }} className={className}>
<div ref={containerRef} style={{ position: 'absolute' }}>
<div ref={viewerRef} className="pdfViewer" />
</div>
</div>
</HandleTextSelection>
);
};

export { PDF, type PDFProps, type PDFEventBus };
37 changes: 23 additions & 14 deletions app/react/V2/Components/PDFViewer/PDF.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ import React, {
useRef,
useState,
} from 'react';
import loadable from '@loadable/component';
import {
SelectionRegion,
HandleTextSelection,
TextSelection,
} from '@huridocs/react-text-selection-handler';
import { PDFDocumentProxy } from 'pdfjs-dist';
import { getDocument, PDFDocumentProxy } from 'pdfjs-dist/webpack.mjs';
import { EventBus } from 'pdfjs-dist/web/pdf_viewer.mjs';
import { Translate } from '#app/I18N/index.js';
import { PDFJS, CMAP_URL, EventBus } from './pdfjs.js';
import { TextHighlight } from './types.js';
import { triggerScroll } from './functions/helpers.js';
import { highlightSnippetInPage, clearSnippets } from './functions/snippetToHighlight.js';
import { adjustSelectionsToScale } from './functions/handleTextSelection.js';
import { PDFPage } from './PDFPage.js';

type Snippet = { text: string; page: number; filename?: string };

Expand All @@ -29,12 +29,6 @@ interface PDFHandle {
deactivateSnippet: () => void;
}

const PDFPage = loadable(
async () => (await import(/* webpackChunkName: "LazyLoadPDFPage" */ './PDFPage')).PDFPage
);

const eventBus = new EventBus();

interface PDFProps {
fileUrl: string;
/** Highlights in scale=1 (normalized) coordinates; converted to display scale when drawing */
Expand All @@ -52,14 +46,15 @@ interface PDFProps {
}

const getPDFFile = async (fileUrl: string) =>
PDFJS.getDocument({
getDocument({
url: fileUrl,
cMapUrl: CMAP_URL,
cMapUrl: 'legacy_character_maps/',
cMapPacked: true,
isEvalSupported: false,
}).promise;

const PDF = forwardRef<PDFHandle, PDFProps>(
// eslint-disable-next-line max-statements
(
{
fileUrl,
Expand All @@ -75,12 +70,14 @@ const PDF = forwardRef<PDFHandle, PDFProps>(
) => {
const pageRefsMap = useRef<{ [key: string]: HTMLDivElement | null }>({});
const pdfContainerRef = useRef<HTMLDivElement>(null);
const resizeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const animationFrameIdRef = useRef(0);
const intersectionObserverRef = useRef<IntersectionObserver | null>();
const [currentScale, setCurrentScale] = useState(1);
const [pdf, setPDF] = useState<PDFDocumentProxy>();
const [error, setError] = useState<string>();
const [containerWidth, setContainerWidth] = useState<number | undefined>(undefined);
const resizeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const animationFrameIdRef = useRef(0);
const [pdfEventBus] = useState(new EventBus());

useImperativeHandle(
ref,
Expand Down Expand Up @@ -195,6 +192,17 @@ const PDF = forwardRef<PDFHandle, PDFProps>(
return () => undefined;
}, [pdf, containerWidth, onPdfReady]);

useEffect(() => {
const observerHandler: IntersectionObserverCallback = entries => {
console.log(entries);
};

intersectionObserverRef.current = new IntersectionObserver(observerHandler, {
rootMargin: '0px',
threshold: 1.0,
});
}, []);

if (error) {
return <div>{error}</div>;
}
Expand All @@ -219,7 +227,8 @@ const PDF = forwardRef<PDFHandle, PDFProps>(
<PDFPage
pdf={pdf}
page={number}
eventBus={eventBus}
eventBus={pdfEventBus}
intersectionObserver={intersectionObserverRef.current}
highlights={pageHighlights}
containerWidth={containerWidth}
onScaleChange={handleScaleChange}
Expand Down
Loading