more pdfjs
This commit is contained in:
@@ -166,19 +166,6 @@ export const PanelManagerProvider: React.FC<PanelManagerProviderProps> = ({ chil
|
|||||||
if (panel === 'sidebar' && (action === 'opened' || action === 'resized') && !meetsThreshold && effectiveDetailOpen) {
|
if (panel === 'sidebar' && (action === 'opened' || action === 'resized') && !meetsThreshold && effectiveDetailOpen) {
|
||||||
closeDetailPanel();
|
closeDetailPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedAction = action === 'resized'
|
|
||||||
? `${panel} resized to ${value}px`
|
|
||||||
: `${panel} ${action}`;
|
|
||||||
console.log(normalizedAction, {
|
|
||||||
sidebar: sidebarWidth > 0 ? `${sidebarWidth}px` : 'closed',
|
|
||||||
detail: `${detailWidth}px`,
|
|
||||||
freeSpace,
|
|
||||||
viewportWidth,
|
|
||||||
freeRatio,
|
|
||||||
minimalFreeRatio: MINIMAL_FREE_RATIO,
|
|
||||||
meetsThreshold,
|
|
||||||
});
|
|
||||||
}, [collapsed, closeDetailPanel, detailPanelOpen]);
|
}, [collapsed, closeDetailPanel, detailPanelOpen]);
|
||||||
|
|
||||||
const collapseSidebar = useCallback(() => {
|
const collapseSidebar = useCallback(() => {
|
||||||
|
|||||||
@@ -267,7 +267,6 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleContentClick = (event: React.MouseEvent<HTMLElement>) => {
|
const handleContentClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
event.stopPropagation();
|
|
||||||
if (isPdfDisplay) {
|
if (isPdfDisplay) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -347,6 +346,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
src={effectiveDisplay.url}
|
src={effectiveDisplay.url}
|
||||||
title={effectiveDisplay.alt || 'Document preview'}
|
title={effectiveDisplay.alt || 'Document preview'}
|
||||||
className="preview-zoom__pdf-viewer"
|
className="preview-zoom__pdf-viewer"
|
||||||
|
viewportRef={scrollRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
import type { JSX } from 'react';
|
import type { JSX } from 'react';
|
||||||
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
|
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
|
||||||
import { DownloadIcon } from '../ui/icons';
|
import { DownloadIcon } from '../ui/icons';
|
||||||
@@ -62,6 +62,7 @@ const DocumentViewerLayout = ({
|
|||||||
layoutMode = 'split',
|
layoutMode = 'split',
|
||||||
}: DocumentViewerLayoutProps): JSX.Element => {
|
}: DocumentViewerLayoutProps): JSX.Element => {
|
||||||
const isStacked = layoutMode === 'stacked';
|
const isStacked = layoutMode === 'stacked';
|
||||||
|
const viewportRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const previewContent = useMemo(() => {
|
const previewContent = useMemo(() => {
|
||||||
if (!document || !previewEntry?.url) {
|
if (!document || !previewEntry?.url) {
|
||||||
@@ -95,6 +96,7 @@ const DocumentViewerLayout = ({
|
|||||||
<PdfViewer
|
<PdfViewer
|
||||||
src={previewEntry.url}
|
src={previewEntry.url}
|
||||||
title={documentTitle ? `Preview of ${documentTitle}` : undefined}
|
title={documentTitle ? `Preview of ${documentTitle}` : undefined}
|
||||||
|
viewportRef={viewportRef}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -123,17 +125,17 @@ const DocumentViewerLayout = ({
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [previewEntry, document]);
|
}, [document, previewEntry, viewportRef]);
|
||||||
|
|
||||||
const renderViewportPane = useCallback(() => (
|
const renderViewportPane = useCallback(() => (
|
||||||
<div className="document-viewer__viewport">
|
<div className="document-viewer__viewport" ref={viewportRef}>
|
||||||
{!previewEntry?.url ? (
|
{!previewEntry?.url ? (
|
||||||
<div className="document-viewer__message">{previewLoadingMessage}</div>
|
<div className="document-viewer__message">{previewLoadingMessage}</div>
|
||||||
) : (
|
) : (
|
||||||
previewContent
|
previewContent
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
), [previewEntry?.url, previewLoadingMessage, previewContent]);
|
), [previewContent, previewEntry?.url, previewLoadingMessage, viewportRef]);
|
||||||
|
|
||||||
const viewportPane = renderViewportPane();
|
const viewportPane = renderViewportPane();
|
||||||
|
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
}
|
}
|
||||||
}, [hydrateDocument, document?.id]);
|
}, [hydrateDocument, document?.id]);
|
||||||
|
|
||||||
const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(null);
|
const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(null);
|
||||||
const isStackedLayout = useViewerLayoutMode(panelRef, document?.id);
|
const isStackedLayout = useViewerLayoutMode(panelRef, document?.id);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import type { JSX, MutableRefObject, RefObject } from 'react';
|
import type { CSSProperties, JSX, MutableRefObject, RefObject } from 'react';
|
||||||
import {
|
import {
|
||||||
GlobalWorkerOptions,
|
GlobalWorkerOptions,
|
||||||
getDocument,
|
getDocument,
|
||||||
@@ -17,7 +17,6 @@ import type {
|
|||||||
PDFDocumentProxy,
|
PDFDocumentProxy,
|
||||||
RenderTask,
|
RenderTask,
|
||||||
} from 'pdfjs-dist/types/src/display/api';
|
} from 'pdfjs-dist/types/src/display/api';
|
||||||
import { TextLayerBuilder } from 'pdfjs-dist/web/pdf_viewer.mjs';
|
|
||||||
GlobalWorkerOptions.workerSrc = new URL(
|
GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
import.meta.url,
|
import.meta.url,
|
||||||
@@ -27,9 +26,11 @@ interface PdfViewerProps {
|
|||||||
src: string;
|
src: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
viewportRef?: RefObject<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderStatus = 'idle' | 'loading' | 'ready' | 'error';
|
type RenderStatus = 'idle' | 'loading' | 'ready' | 'error';
|
||||||
|
type ViewMode = 'fit-width' | 'contain';
|
||||||
|
|
||||||
interface PageDescriptor {
|
interface PageDescriptor {
|
||||||
number: number;
|
number: number;
|
||||||
@@ -42,6 +43,24 @@ const MIN_RENDER_WIDTH = 1200;
|
|||||||
const MAX_RENDER_WIDTH = 2200;
|
const MAX_RENDER_WIDTH = 2200;
|
||||||
const RERENDER_DELTA = 48;
|
const RERENDER_DELTA = 48;
|
||||||
|
|
||||||
|
const getAvailableViewportSize = (viewportNode: Element, stackNode: Element) => {
|
||||||
|
const viewportStyle = window.getComputedStyle(viewportNode);
|
||||||
|
const viewportPaddingX = parseFloat(viewportStyle.paddingLeft || '0')
|
||||||
|
+ parseFloat(viewportStyle.paddingRight || '0');
|
||||||
|
const viewportPaddingY = parseFloat(viewportStyle.paddingTop || '0')
|
||||||
|
+ parseFloat(viewportStyle.paddingBottom || '0');
|
||||||
|
|
||||||
|
const stackStyle = window.getComputedStyle(stackNode);
|
||||||
|
const stackPaddingX = parseFloat(stackStyle.paddingLeft || '0')
|
||||||
|
+ parseFloat(stackStyle.paddingRight || '0');
|
||||||
|
const stackPaddingY = parseFloat(stackStyle.paddingTop || '0')
|
||||||
|
+ parseFloat(stackStyle.paddingBottom || '0');
|
||||||
|
|
||||||
|
const width = Math.max(0, viewportNode.clientWidth - viewportPaddingX - stackPaddingX);
|
||||||
|
const height = Math.max(0, viewportNode.clientHeight - viewportPaddingY - stackPaddingY);
|
||||||
|
return { width, height };
|
||||||
|
};
|
||||||
|
|
||||||
const resolvePdfWasmBaseUrl = (): string => {
|
const resolvePdfWasmBaseUrl = (): string => {
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') {
|
||||||
const base = document.baseURI || (typeof window !== 'undefined' ? window.location.href : '/');
|
const base = document.baseURI || (typeof window !== 'undefined' ? window.location.href : '/');
|
||||||
@@ -53,16 +72,25 @@ const resolvePdfWasmBaseUrl = (): string => {
|
|||||||
return '/pdfjs/wasm/';
|
return '/pdfjs/wasm/';
|
||||||
};
|
};
|
||||||
|
|
||||||
const PdfViewer = ({ src, title, className }: PdfViewerProps): JSX.Element => {
|
const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.Element => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [viewportWidth, setViewportWidth] = useState(0);
|
const [viewportWidth, setViewportWidth] = useState(() => window.innerWidth);
|
||||||
|
const [viewportHeight, setViewportHeight] = useState(() => window.innerHeight);
|
||||||
const [renderWidth, setRenderWidth] = useState(0);
|
const [renderWidth, setRenderWidth] = useState(0);
|
||||||
const [status, setStatus] = useState<RenderStatus>('idle');
|
const [status, setStatus] = useState<RenderStatus>('idle');
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const [pages, setPages] = useState<PageDescriptor[]>([]);
|
const [pages, setPages] = useState<PageDescriptor[]>([]);
|
||||||
const [pixelRatio, setPixelRatio] = useState(1);
|
const [pixelRatio, setPixelRatio] = useState(1);
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>('contain');
|
||||||
const pdfRef = useRef<PDFDocumentProxy | null>(null);
|
const pdfRef = useRef<PDFDocumentProxy | null>(null);
|
||||||
const wasmUrlRef = useRef<string | null>(null);
|
const wasmUrlRef = useRef<string | null>(null);
|
||||||
|
const focusTargetRef = useRef<{
|
||||||
|
ratioX: number;
|
||||||
|
ratioY: number;
|
||||||
|
pointerOffsetX: number;
|
||||||
|
pointerOffsetY: number;
|
||||||
|
} | null>(null);
|
||||||
|
const focusRatioRef = useRef<{ x: number; y: number } | null>(null);
|
||||||
const ensureWasmUrl = useCallback(() => {
|
const ensureWasmUrl = useCallback(() => {
|
||||||
if (!wasmUrlRef.current) {
|
if (!wasmUrlRef.current) {
|
||||||
wasmUrlRef.current = resolvePdfWasmBaseUrl();
|
wasmUrlRef.current = resolvePdfWasmBaseUrl();
|
||||||
@@ -71,45 +99,61 @@ const PdfViewer = ({ src, title, className }: PdfViewerProps): JSX.Element => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const container = containerRef.current;
|
let frameId: number | null = null;
|
||||||
if (!container) {
|
let observer: ResizeObserver | null = null;
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateWidth = () => {
|
const attach = () => {
|
||||||
const measurementTarget = (
|
const stackElement = containerRef.current;
|
||||||
container.closest('.document-viewer__viewport') as HTMLElement | null
|
const viewportElement = viewportRef?.current;
|
||||||
) || container.parentElement || container;
|
if (!stackElement || !viewportElement) {
|
||||||
const nextWidth = Math.max(1, Math.round(measurementTarget.clientWidth));
|
frameId = requestAnimationFrame(attach);
|
||||||
setViewportWidth((prev) => (prev === nextWidth ? prev : nextWidth));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateBounds = () => {
|
||||||
|
const { width, height } = getAvailableViewportSize(viewportElement, stackElement);
|
||||||
|
const nextWidth = Math.max(1, Math.round(width || 0));
|
||||||
|
const nextHeight = Math.max(1, Math.round(height || 0));
|
||||||
|
setViewportWidth((prev) => (prev === nextWidth ? prev : nextWidth));
|
||||||
|
setViewportHeight((prev) => (prev === nextHeight ? prev : nextHeight));
|
||||||
|
};
|
||||||
|
|
||||||
|
updateBounds();
|
||||||
|
|
||||||
|
observer = new ResizeObserver(() => {
|
||||||
|
updateBounds();
|
||||||
|
});
|
||||||
|
|
||||||
|
const observedNodes = new Set<Element>();
|
||||||
|
const observeNode = (node: Element | null) => {
|
||||||
|
if (!node || observedNodes.has(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
observer?.observe(node);
|
||||||
|
observedNodes.add(node);
|
||||||
|
};
|
||||||
|
|
||||||
|
observeNode(stackElement);
|
||||||
|
observeNode(stackElement.parentElement);
|
||||||
|
observeNode(viewportElement);
|
||||||
|
observeNode(stackElement.closest('.document-viewer__viewport'));
|
||||||
};
|
};
|
||||||
|
|
||||||
updateWidth();
|
attach();
|
||||||
|
|
||||||
const observer = new ResizeObserver(() => {
|
return () => {
|
||||||
updateWidth();
|
if (frameId !== null) {
|
||||||
});
|
cancelAnimationFrame(frameId);
|
||||||
observer.observe(container);
|
}
|
||||||
const parent = container.parentElement;
|
observer?.disconnect();
|
||||||
if (parent) {
|
};
|
||||||
observer.observe(parent);
|
}, [viewportRef]);
|
||||||
}
|
|
||||||
const viewportNode = container.closest('.document-viewer__viewport') as HTMLElement | null;
|
|
||||||
if (viewportNode) {
|
|
||||||
observer.observe(viewportNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (viewportWidth <= 0) {
|
if (viewportWidth <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const desiredWidth = Math.min(
|
const desiredWidth = Math.min(MAX_RENDER_WIDTH, Math.max(viewportWidth, MIN_RENDER_WIDTH));
|
||||||
MAX_RENDER_WIDTH,
|
|
||||||
Math.max(viewportWidth, MIN_RENDER_WIDTH),
|
|
||||||
);
|
|
||||||
setRenderWidth((current) => {
|
setRenderWidth((current) => {
|
||||||
if (current === 0 || desiredWidth > current + RERENDER_DELTA) {
|
if (current === 0 || desiredWidth > current + RERENDER_DELTA) {
|
||||||
return desiredWidth;
|
return desiredWidth;
|
||||||
@@ -118,6 +162,31 @@ const PdfViewer = ({ src, title, className }: PdfViewerProps): JSX.Element => {
|
|||||||
});
|
});
|
||||||
}, [viewportWidth]);
|
}, [viewportWidth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const focus = focusTargetRef.current;
|
||||||
|
if (!focus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const viewportElement = viewportRef?.current || containerRef.current?.closest('.document-viewer__viewport');
|
||||||
|
const stackElement = containerRef.current;
|
||||||
|
if (!viewportElement || !stackElement) {
|
||||||
|
focusTargetRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const contentWidth = Math.max(1, stackElement.scrollWidth || stackElement.clientWidth);
|
||||||
|
const contentHeight = Math.max(1, stackElement.scrollHeight || stackElement.clientHeight);
|
||||||
|
const targetX = focus.ratioX * contentWidth;
|
||||||
|
const targetY = focus.ratioY * contentHeight;
|
||||||
|
const nextScrollLeft = Math.max(0, targetX - focus.pointerOffsetX);
|
||||||
|
const nextScrollTop = Math.max(0, targetY - focus.pointerOffsetY);
|
||||||
|
viewportElement.scrollTo({
|
||||||
|
left: nextScrollLeft,
|
||||||
|
top: nextScrollTop,
|
||||||
|
behavior: 'auto',
|
||||||
|
});
|
||||||
|
focusTargetRef.current = null;
|
||||||
|
}, [viewMode, viewportRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!src || renderWidth <= 0) {
|
if (!src || renderWidth <= 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -146,6 +215,7 @@ const PdfViewer = ({ src, title, className }: PdfViewerProps): JSX.Element => {
|
|||||||
setPages([]);
|
setPages([]);
|
||||||
setStatus('loading');
|
setStatus('loading');
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
|
console.log('[PdfViewer] rendering pages', { renderWidth, mode: viewMode, src });
|
||||||
|
|
||||||
const renderDocument = async () => {
|
const renderDocument = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -223,6 +293,36 @@ const PdfViewer = ({ src, title, className }: PdfViewerProps): JSX.Element => {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
const showStatus = status !== 'ready' || pages.length === 0;
|
const showStatus = status !== 'ready' || pages.length === 0;
|
||||||
|
const stackStyle = useMemo<CSSProperties>(() => ({
|
||||||
|
'--pdf-viewer-viewport-width': `${viewportWidth}px`,
|
||||||
|
'--pdf-viewer-viewport-height': `${viewportHeight}px`,
|
||||||
|
}), [viewportHeight, viewportWidth]);
|
||||||
|
|
||||||
|
const toggleViewMode = useCallback(() => {
|
||||||
|
setViewMode((prev) => (prev === 'fit-width' ? 'contain' : 'fit-width'));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePageClick = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const stack = containerRef.current;
|
||||||
|
const viewportElement = viewportRef?.current || stack?.closest('.document-viewer__viewport');
|
||||||
|
if (stack && viewportElement) {
|
||||||
|
const viewportRect = viewportElement.getBoundingClientRect();
|
||||||
|
const pointerOffsetX = event.clientX - viewportRect.left;
|
||||||
|
const pointerOffsetY = event.clientY - viewportRect.top;
|
||||||
|
const contentWidth = Math.max(1, stack.scrollWidth || stack.clientWidth);
|
||||||
|
const contentHeight = Math.max(1, stack.scrollHeight || stack.clientHeight);
|
||||||
|
const ratioX = (viewportElement.scrollLeft + pointerOffsetX) / contentWidth;
|
||||||
|
const ratioY = (viewportElement.scrollTop + pointerOffsetY) / contentHeight;
|
||||||
|
focusTargetRef.current = {
|
||||||
|
ratioX: Math.max(0, Math.min(1, ratioX)),
|
||||||
|
ratioY: Math.max(0, Math.min(1, ratioY)),
|
||||||
|
pointerOffsetX: Math.max(0, Math.min(viewportElement.clientWidth, pointerOffsetX)),
|
||||||
|
pointerOffsetY: Math.max(0, Math.min(viewportElement.clientHeight, pointerOffsetY)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
event.stopPropagation();
|
||||||
|
toggleViewMode();
|
||||||
|
}, [toggleViewMode, viewportRef]);
|
||||||
|
|
||||||
const pageElements = useMemo(() => (
|
const pageElements = useMemo(() => (
|
||||||
pages.map((page) => (
|
pages.map((page) => (
|
||||||
@@ -232,26 +332,21 @@ const PdfViewer = ({ src, title, className }: PdfViewerProps): JSX.Element => {
|
|||||||
pdfRef={pdfRef}
|
pdfRef={pdfRef}
|
||||||
pixelRatio={pixelRatio}
|
pixelRatio={pixelRatio}
|
||||||
rootRef={containerRef}
|
rootRef={containerRef}
|
||||||
|
viewportRef={viewportRef}
|
||||||
|
onPageClick={handlePageClick}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))
|
))
|
||||||
), [pages, pixelRatio]);
|
), [handlePageClick, pages, pixelRatio, viewportRef]);
|
||||||
|
|
||||||
const handleCanvasStackClick = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
if (target.closest('.pdf-viewer__page-wrapper')) {
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={viewerClasses}>
|
<div className={viewerClasses}>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="pdf-viewer__canvas-stack"
|
className={`pdf-viewer__canvas-stack pdf-viewer__canvas-stack--${viewMode}`}
|
||||||
role="document"
|
role="document"
|
||||||
aria-label={title || 'PDF document'}
|
aria-label={title || 'PDF document'}
|
||||||
onClick={handleCanvasStackClick}
|
style={stackStyle}
|
||||||
>
|
>
|
||||||
{pageElements}
|
{pageElements}
|
||||||
</div>
|
</div>
|
||||||
@@ -271,6 +366,8 @@ interface PdfPageCanvasProps {
|
|||||||
pdfRef: MutableRefObject<PDFDocumentProxy | null>;
|
pdfRef: MutableRefObject<PDFDocumentProxy | null>;
|
||||||
pixelRatio: number;
|
pixelRatio: number;
|
||||||
rootRef: RefObject<HTMLDivElement>;
|
rootRef: RefObject<HTMLDivElement>;
|
||||||
|
viewportRef?: RefObject<HTMLElement | null>;
|
||||||
|
onPageClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PdfPageCanvas({
|
function PdfPageCanvas({
|
||||||
@@ -278,12 +375,13 @@ function PdfPageCanvas({
|
|||||||
pdfRef,
|
pdfRef,
|
||||||
pixelRatio,
|
pixelRatio,
|
||||||
rootRef,
|
rootRef,
|
||||||
|
viewportRef,
|
||||||
|
onPageClick,
|
||||||
}: PdfPageCanvasProps): JSX.Element {
|
}: PdfPageCanvasProps): JSX.Element {
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const renderTaskRef = useRef<RenderTask | null>(null);
|
const renderTaskRef = useRef<RenderTask | null>(null);
|
||||||
const hasRenderedRef = useRef(false);
|
const hasRenderedRef = useRef(false);
|
||||||
const textLayerBuilderRef = useRef<TextLayerBuilder | null>(null);
|
|
||||||
|
|
||||||
const renderPage = useCallback(async () => {
|
const renderPage = useCallback(async () => {
|
||||||
if (renderTaskRef.current || hasRenderedRef.current) {
|
if (renderTaskRef.current || hasRenderedRef.current) {
|
||||||
@@ -311,14 +409,7 @@ function PdfPageCanvas({
|
|||||||
|
|
||||||
const wrapper = wrapperRef.current;
|
const wrapper = wrapperRef.current;
|
||||||
if (wrapper) {
|
if (wrapper) {
|
||||||
textLayerBuilderRef.current?.cancel();
|
|
||||||
const textLayerBuilder = new TextLayerBuilder({
|
|
||||||
pdfPage: page,
|
|
||||||
});
|
|
||||||
textLayerBuilderRef.current = textLayerBuilder;
|
|
||||||
wrapper.querySelector('.textLayer')?.remove();
|
wrapper.querySelector('.textLayer')?.remove();
|
||||||
wrapper.appendChild(textLayerBuilder.div);
|
|
||||||
await textLayerBuilder.render({ viewport });
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to render PDF page', descriptor.number, error);
|
console.error('Failed to render PDF page', descriptor.number, error);
|
||||||
@@ -332,40 +423,55 @@ function PdfPageCanvas({
|
|||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
canvas.style.width = '100%';
|
canvas.width = descriptor.width;
|
||||||
canvas.style.height = 'auto';
|
canvas.height = descriptor.height;
|
||||||
canvas.style.setProperty('aspect-ratio', `${descriptor.width} / ${descriptor.height}`);
|
canvas.style.setProperty('aspect-ratio', `${descriptor.width} / ${descriptor.height}`);
|
||||||
const wrapperNode = wrapperRef.current;
|
const wrapperNode = wrapperRef.current;
|
||||||
return () => {
|
return () => {
|
||||||
renderTaskRef.current?.cancel();
|
renderTaskRef.current?.cancel();
|
||||||
textLayerBuilderRef.current?.cancel();
|
|
||||||
wrapperNode?.querySelector('.textLayer')?.remove();
|
wrapperNode?.querySelector('.textLayer')?.remove();
|
||||||
};
|
};
|
||||||
}, [descriptor.height, descriptor.width]);
|
}, [descriptor.height, descriptor.width]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const node = canvasRef.current;
|
const target = wrapperRef.current;
|
||||||
if (!node) {
|
if (!target) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const root = rootRef.current || undefined;
|
const root = viewportRef?.current
|
||||||
|
|| rootRef.current?.closest?.('.document-viewer__viewport') as HTMLElement | null
|
||||||
|
|| undefined;
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
console.log('[PdfViewer] intersection', {
|
||||||
void renderPage();
|
page: descriptor.number,
|
||||||
|
isIntersecting: entry.isIntersecting,
|
||||||
|
ratio: entry.intersectionRatio,
|
||||||
|
});
|
||||||
|
if (!entry.isIntersecting || hasRenderedRef.current) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
console.log('[PdfViewer] rendering page', descriptor.number);
|
||||||
|
void renderPage();
|
||||||
});
|
});
|
||||||
}, {
|
}, {
|
||||||
root,
|
root,
|
||||||
rootMargin: '200px 0px',
|
rootMargin: '200px 0px',
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
});
|
});
|
||||||
observer.observe(node);
|
console.log('[PdfViewer] observing page', descriptor.number, { rootMargin: '200px 0px', threshold: 0.1 });
|
||||||
|
observer.observe(target);
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [renderPage, rootRef]);
|
}, [renderPage, rootRef, viewportRef]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={wrapperRef} className="pdf-viewer__page-wrapper">
|
<div
|
||||||
|
ref={wrapperRef}
|
||||||
|
className="pdf-viewer__page-wrapper"
|
||||||
|
onClick={(event) => {
|
||||||
|
onPageClick(event);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
className="pdf-viewer__page"
|
className="pdf-viewer__page"
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +35,7 @@
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0.25rem 1rem 1rem;
|
padding: 0 1rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-viewer--stacked {
|
.document-viewer--stacked {
|
||||||
@@ -382,7 +381,7 @@
|
|||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
grid-area: viewport;
|
grid-area: viewport;
|
||||||
background: var(--surface-subtle);
|
background: var(--surface-subtle);
|
||||||
padding: 0.75rem;
|
--pdf-viewer-stack-padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-viewer__object {
|
.document-viewer__object {
|
||||||
@@ -435,13 +434,23 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: var(--pdf-viewer-stack-padding, 0);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
--pdf-viewer-viewport-width: 100vw;
|
||||||
|
--pdf-viewer-viewport-height: 100vh;
|
||||||
|
padding: var(--pdf-viewer-stack-padding, 0);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-viewer__page-wrapper {
|
.pdf-viewer__page-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: min(100%, 48rem);
|
}
|
||||||
|
|
||||||
|
.pdf-viewer__canvas-stack--contain .pdf-viewer__page {
|
||||||
|
width: auto;
|
||||||
|
max-width: min(100%, var(--pdf-viewer-viewport-width, 100vw));
|
||||||
|
max-height: var(--pdf-viewer-viewport-height, 100vh);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-viewer__page {
|
.pdf-viewer__page {
|
||||||
@@ -512,3 +521,8 @@
|
|||||||
.document-viewer__summary-tab-content {
|
.document-viewer__summary-tab-content {
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
.pdf-viewer__page-wrapper--contain .pdf-viewer__page {
|
||||||
|
width: auto;
|
||||||
|
max-width: 100vw;
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,6 +55,12 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: auto;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
--pdf-viewer-stack-padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-zoom__scroll:focus {
|
.preview-zoom__scroll:focus {
|
||||||
@@ -126,7 +132,9 @@
|
|||||||
.preview-zoom__pdf {
|
.preview-zoom__pdf {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-zoom__pdf-viewer {
|
.preview-zoom__pdf-viewer {
|
||||||
|
|||||||
Reference in New Issue
Block a user