mirror of
https://github.com/documenso/documenso.git
synced 2026-07-27 02:15:05 +10:00
feat: add pdf image renderer (#2554)
## Description Replace the PDF renderer with an custom image renderer. This allows us to remove the "react-pdf" dependency and allows us to use a virtual list to improve performance.
This commit is contained in:
@@ -98,10 +98,8 @@ export const DocumentReadOnlyFields = ({
|
||||
setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true }));
|
||||
};
|
||||
|
||||
const highestPageNumber = Math.max(...fields.map((field) => field.page));
|
||||
|
||||
return (
|
||||
<ElementVisible target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}>
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{fields.map(
|
||||
(field) =>
|
||||
!hiddenFieldIds[field.secondaryId] && (
|
||||
@@ -163,7 +161,7 @@ export const DocumentReadOnlyFields = ({
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-center text-xs">
|
||||
<p className="mt-1 text-center text-xs text-muted-foreground">
|
||||
{getRecipientDisplayText(field.recipient)}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ import { createPortal } from 'react-dom';
|
||||
|
||||
import { useElementBounds } from '@documenso/lib/client-only/hooks/use-element-bounds';
|
||||
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { useIsPageInDom } from '@documenso/lib/client-only/hooks/use-is-page-in-dom';
|
||||
import {
|
||||
PDF_VIEWER_CONTENT_SELECTOR,
|
||||
PDF_VIEWER_PAGE_SELECTOR,
|
||||
} from '@documenso/lib/constants/pdf-viewer';
|
||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
|
||||
import type { RecipientColorStyles } from '../../lib/recipient-colors';
|
||||
@@ -81,6 +85,8 @@ export function FieldRootContainer({
|
||||
readonly,
|
||||
}: FieldRootContainerProps) {
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const isPageInDom = useIsPageInDom(field.page);
|
||||
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -88,6 +94,21 @@ export function FieldRootContainer({
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the validation signal on the PDF viewer container. When a field
|
||||
// mounts after the virtual list scrolls to its page, the per-element
|
||||
// `data-validate` attribute will not have been set yet. The signal on the
|
||||
// `[data-pdf-content]` container bridges this gap so newly-rendered fields
|
||||
// pick up the validation state immediately.
|
||||
const pdfContent = document.querySelector(PDF_VIEWER_CONTENT_SELECTOR);
|
||||
|
||||
if (
|
||||
pdfContent?.getAttribute('data-validate-fields') === 'true' &&
|
||||
isFieldUnsignedAndRequired(field)
|
||||
) {
|
||||
ref.current.setAttribute('data-validate', 'true');
|
||||
setIsValidating(true);
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((_mutations) => {
|
||||
if (ref.current) {
|
||||
setIsValidating(ref.current.getAttribute('data-validate') === 'true');
|
||||
@@ -101,7 +122,11 @@ export function FieldRootContainer({
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
}, [isPageInDom]);
|
||||
|
||||
if (!isPageInDom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldContainerPortal field={field}>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { type PDFDocumentProxy } from 'pdfjs-dist';
|
||||
|
||||
import type { PdfViewerRendererMode } from './pdf-viewer-konva';
|
||||
|
||||
export type LoadedPDFDocument = PDFDocumentProxy;
|
||||
|
||||
export type PDFViewerProps = {
|
||||
className?: string;
|
||||
onDocumentLoad?: () => void;
|
||||
renderer: PdfViewerRendererMode;
|
||||
[key: string]: unknown;
|
||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'onPageClick'>;
|
||||
|
||||
const EnvelopePdfViewer = lazy(async () => import('./pdf-viewer-konva'));
|
||||
|
||||
export const PDFViewerKonvaLazy = (props: PDFViewerProps) => {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div>
|
||||
<Trans>Loading...</Trans>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<EnvelopePdfViewer {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default PDFViewerKonvaLazy;
|
||||
@@ -1,213 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import Konva from 'konva';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { type PDFDocumentProxy } from 'pdfjs-dist';
|
||||
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
|
||||
|
||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
|
||||
export type LoadedPDFDocument = PDFDocumentProxy;
|
||||
|
||||
/**
|
||||
* This imports the worker from the `pdfjs-dist` package.
|
||||
*/
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
|
||||
import.meta.url,
|
||||
).toString();
|
||||
|
||||
const pdfViewerOptions = {
|
||||
cMapUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/static/cmaps/`,
|
||||
};
|
||||
|
||||
const PDFLoader = () => (
|
||||
<>
|
||||
<Loader className="h-12 w-12 animate-spin text-documenso" />
|
||||
|
||||
<p className="mt-4 text-muted-foreground">
|
||||
<Trans>Loading document...</Trans>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
export type PdfViewerRendererMode = 'editor' | 'preview' | 'signing';
|
||||
|
||||
const RendererErrorMessages: Record<
|
||||
PdfViewerRendererMode,
|
||||
{ title: MessageDescriptor; description: MessageDescriptor }
|
||||
> = {
|
||||
editor: {
|
||||
title: msg`Configuration Error`,
|
||||
description: msg`There was an issue rendering some fields, please review the fields and try again.`,
|
||||
},
|
||||
preview: {
|
||||
title: msg`Configuration Error`,
|
||||
description: msg`Something went wrong while rendering the document, some fields may be missing or corrupted.`,
|
||||
},
|
||||
signing: {
|
||||
title: msg`Configuration Error`,
|
||||
description: msg`Something went wrong while rendering the document, some fields may be missing or corrupted.`,
|
||||
},
|
||||
};
|
||||
|
||||
export type PdfViewerKonvaProps = {
|
||||
className?: string;
|
||||
onDocumentLoad?: () => void;
|
||||
customPageRenderer?: React.FunctionComponent;
|
||||
renderer: PdfViewerRendererMode;
|
||||
[key: string]: unknown;
|
||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'onPageClick'>;
|
||||
|
||||
export const PdfViewerKonva = ({
|
||||
className,
|
||||
onDocumentLoad,
|
||||
customPageRenderer,
|
||||
renderer,
|
||||
...props
|
||||
}: PdfViewerKonvaProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const $el = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { getPdfBuffer, currentEnvelopeItem, renderError } = useCurrentEnvelopeRender();
|
||||
|
||||
const [width, setWidth] = useState(0);
|
||||
const [numPages, setNumPages] = useState(0);
|
||||
const [pdfError, setPdfError] = useState(false);
|
||||
|
||||
const envelopeItemFile = useMemo(() => {
|
||||
const data = getPdfBuffer(currentEnvelopeItem?.id || '');
|
||||
|
||||
if (!data || data.status !== 'loaded') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
data: new Uint8Array(data.file),
|
||||
};
|
||||
}, [currentEnvelopeItem?.id, getPdfBuffer]);
|
||||
|
||||
const onDocumentLoaded = useCallback(
|
||||
(doc: PDFDocumentProxy) => {
|
||||
setNumPages(doc.numPages);
|
||||
},
|
||||
[onDocumentLoad],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if ($el.current) {
|
||||
const $current = $el.current;
|
||||
|
||||
const { width } = $current.getBoundingClientRect();
|
||||
|
||||
setWidth(width);
|
||||
|
||||
const onResize = () => {
|
||||
const { width } = $current.getBoundingClientRect();
|
||||
setWidth(width);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={$el} className={cn('w-full max-w-[800px]', className)} {...props}>
|
||||
{renderError && (
|
||||
<Alert variant="destructive" className="mb-4 max-w-[800px]">
|
||||
<AlertTitle>{t(RendererErrorMessages[renderer].title)}</AlertTitle>
|
||||
<AlertDescription>{t(RendererErrorMessages[renderer].description)}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{envelopeItemFile && Konva ? (
|
||||
<PDFDocument
|
||||
file={envelopeItemFile}
|
||||
className={cn('w-full rounded', {
|
||||
'h-[80vh] max-h-[60rem]': numPages === 0,
|
||||
})}
|
||||
onLoadSuccess={(d) => onDocumentLoaded(d)}
|
||||
// Uploading a invalid document causes an error which doesn't appear to be handled by the `error` prop.
|
||||
// Therefore we add some additional custom error handling.
|
||||
onSourceError={() => {
|
||||
setPdfError(true);
|
||||
}}
|
||||
externalLinkTarget="_blank"
|
||||
loading={
|
||||
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50 dark:bg-background">
|
||||
{pdfError ? (
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>
|
||||
<Trans>Something went wrong while loading the document.</Trans>
|
||||
</p>
|
||||
<p className="mt-1 text-sm">
|
||||
<Trans>Please try again or contact our support.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<PDFLoader />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
error={
|
||||
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50 dark:bg-background">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>
|
||||
<Trans>Something went wrong while loading the document.</Trans>
|
||||
</p>
|
||||
<p className="mt-1 text-sm">
|
||||
<Trans>Please try again or contact our support.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
options={pdfViewerOptions}
|
||||
>
|
||||
{Array(numPages)
|
||||
.fill(null)
|
||||
.map((_, i) => (
|
||||
<div key={i} className="last:-mb-2">
|
||||
<div className="rounded border border-border will-change-transform">
|
||||
<PDFPage
|
||||
pageNumber={i + 1}
|
||||
width={width}
|
||||
renderAnnotationLayer={false}
|
||||
renderTextLayer={false}
|
||||
loading={() => ''}
|
||||
renderMode={customPageRenderer ? 'custom' : 'canvas'}
|
||||
customRenderer={customPageRenderer}
|
||||
/>
|
||||
</div>
|
||||
<p className="my-2 text-center text-[11px] text-muted-foreground/80">
|
||||
<Trans>
|
||||
Page {i + 1} of {numPages}
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</PDFDocument>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-[80vh] max-h-[60rem] w-full flex-col items-center justify-center overflow-hidden rounded',
|
||||
)}
|
||||
>
|
||||
<PDFLoader />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PdfViewerKonva;
|
||||
@@ -66,14 +66,13 @@
|
||||
"framer-motion": "^12.23.24",
|
||||
"lucide-react": "^0.554.0",
|
||||
"luxon": "^3.7.2",
|
||||
"perfect-freehand": "^1.2.2",
|
||||
"pdfjs-dist": "5.4.296",
|
||||
"perfect-freehand": "^1.2.2",
|
||||
"react": "^18",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.66.1",
|
||||
"react-pdf": "^10.3.0",
|
||||
"react-rnd": "^10.5.2",
|
||||
"remeda": "^2.32.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
|
||||
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR, getPdfPagesCount } from '@documenso/lib/constants/pdf-viewer';
|
||||
import {
|
||||
type TFieldMetaSchema as FieldMeta,
|
||||
ZFieldMetaSchema,
|
||||
@@ -431,13 +431,15 @@ export const AddFieldsFormPartial = ({
|
||||
}
|
||||
|
||||
if (duplicateAll) {
|
||||
const pages = Array.from(document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR));
|
||||
const totalPages = getPdfPagesCount();
|
||||
|
||||
pages.forEach((_, index) => {
|
||||
const pageNumber = index + 1;
|
||||
if (totalPages < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
|
||||
if (pageNumber === lastActiveField.pageNumber) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newField: TAddFieldsFormSchema['fields'][0] = {
|
||||
@@ -450,7 +452,7 @@ export const AddFieldsFormPartial = ({
|
||||
};
|
||||
|
||||
append(newField);
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Rnd } from 'react-rnd';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
import { useElementBounds } from '@documenso/lib/client-only/hooks/use-element-bounds';
|
||||
import { useIsPageInDom } from '@documenso/lib/client-only/hooks/use-is-page-in-dom';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
@@ -50,7 +51,17 @@ export type FieldItemProps = {
|
||||
/**
|
||||
* The item when editing fields??
|
||||
*/
|
||||
export const FieldItem = ({
|
||||
export const FieldItem = (props: FieldItemProps) => {
|
||||
const isPageInDom = useIsPageInDom(props.field.pageNumber);
|
||||
|
||||
if (!isPageInDom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <FieldItemInner {...props} />;
|
||||
};
|
||||
|
||||
const FieldItemInner = ({
|
||||
fieldClassName,
|
||||
field,
|
||||
passive,
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import {
|
||||
type LoadedPDFDocument,
|
||||
type OnPDFViewerPageClick,
|
||||
PDFViewer,
|
||||
type PDFViewerProps,
|
||||
} from './base';
|
||||
|
||||
export { PDFViewer, type LoadedPDFDocument, type OnPDFViewerPageClick, type PDFViewerProps };
|
||||
|
||||
export default PDFViewer;
|
||||
@@ -1,290 +0,0 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { EnvelopeItem } from '@prisma/client';
|
||||
import { base64 } from '@scure/base';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { type PDFDocumentProxy } from 'pdfjs-dist';
|
||||
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { useToast } from '../use-toast';
|
||||
|
||||
export type LoadedPDFDocument = PDFDocumentProxy;
|
||||
|
||||
/**
|
||||
* This imports the worker from the `pdfjs-dist` package.
|
||||
* Wrapped in typeof window check to prevent SSR evaluation.
|
||||
*/
|
||||
if (typeof window !== 'undefined') {
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
|
||||
import.meta.url,
|
||||
).toString();
|
||||
}
|
||||
|
||||
const pdfViewerOptions = {
|
||||
cMapUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/static/cmaps/`,
|
||||
};
|
||||
|
||||
export type OnPDFViewerPageClick = (_event: {
|
||||
pageNumber: number;
|
||||
numPages: number;
|
||||
originalEvent: React.MouseEvent<HTMLDivElement, MouseEvent>;
|
||||
pageHeight: number;
|
||||
pageWidth: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
}) => void | Promise<void>;
|
||||
|
||||
const PDFLoader = () => (
|
||||
<>
|
||||
<Loader className="h-12 w-12 animate-spin text-documenso" />
|
||||
|
||||
<p className="mt-4 text-muted-foreground">
|
||||
<Trans>Loading document...</Trans>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
export type PDFViewerProps = {
|
||||
className?: string;
|
||||
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
||||
token: string | undefined;
|
||||
presignToken?: string | undefined;
|
||||
version: 'original' | 'signed';
|
||||
onDocumentLoad?: (_doc: LoadedPDFDocument) => void;
|
||||
onPageClick?: OnPDFViewerPageClick;
|
||||
overrideData?: string;
|
||||
customPageRenderer?: React.FunctionComponent;
|
||||
[key: string]: unknown;
|
||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'onPageClick'>;
|
||||
|
||||
export const PDFViewer = ({
|
||||
className,
|
||||
envelopeItem,
|
||||
token,
|
||||
presignToken,
|
||||
version,
|
||||
onDocumentLoad,
|
||||
onPageClick,
|
||||
overrideData,
|
||||
customPageRenderer,
|
||||
...props
|
||||
}: PDFViewerProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const $el = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isDocumentBytesLoading, setIsDocumentBytesLoading] = useState(false);
|
||||
const [documentBytes, setDocumentBytes] = useState<Uint8Array | null>(
|
||||
overrideData ? base64.decode(overrideData) : null,
|
||||
);
|
||||
|
||||
const [width, setWidth] = useState(0);
|
||||
const [numPages, setNumPages] = useState(0);
|
||||
const [pdfError, setPdfError] = useState(false);
|
||||
|
||||
const isLoading = isDocumentBytesLoading || !documentBytes;
|
||||
|
||||
const envelopeItemFile = useMemo(() => {
|
||||
if (!documentBytes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
data: documentBytes,
|
||||
};
|
||||
}, [documentBytes]);
|
||||
|
||||
const onDocumentLoaded = (doc: LoadedPDFDocument) => {
|
||||
setNumPages(doc.numPages);
|
||||
onDocumentLoad?.(doc);
|
||||
};
|
||||
|
||||
const onDocumentPageClick = (
|
||||
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
pageNumber: number,
|
||||
) => {
|
||||
const $el = event.target instanceof HTMLElement ? event.target : null;
|
||||
|
||||
if (!$el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $page = $el.closest(PDF_VIEWER_PAGE_SELECTOR);
|
||||
|
||||
if (!$page) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { height, width, top, left } = $page.getBoundingClientRect();
|
||||
|
||||
const pageX = event.clientX - left;
|
||||
const pageY = event.clientY - top;
|
||||
|
||||
if (onPageClick) {
|
||||
void onPageClick({
|
||||
pageNumber,
|
||||
numPages,
|
||||
originalEvent: event,
|
||||
pageHeight: height,
|
||||
pageWidth: width,
|
||||
pageX,
|
||||
pageY,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if ($el.current) {
|
||||
const $current = $el.current;
|
||||
|
||||
const { width } = $current.getBoundingClientRect();
|
||||
|
||||
setWidth(width);
|
||||
|
||||
const onResize = () => {
|
||||
const { width } = $current.getBoundingClientRect();
|
||||
|
||||
setWidth(width);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (overrideData) {
|
||||
const bytes = base64.decode(overrideData);
|
||||
|
||||
setDocumentBytes(bytes);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchDocumentBytes = async () => {
|
||||
try {
|
||||
setIsDocumentBytesLoading(true);
|
||||
|
||||
const documentUrl = getEnvelopeItemPdfUrl({
|
||||
type: 'view',
|
||||
envelopeItem: envelopeItem,
|
||||
token,
|
||||
presignToken,
|
||||
});
|
||||
|
||||
const bytes = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
||||
|
||||
setDocumentBytes(new Uint8Array(bytes));
|
||||
|
||||
setIsDocumentBytesLoading(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while loading the document.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
void fetchDocumentBytes();
|
||||
}, [envelopeItem.envelopeId, envelopeItem.id, token, version, toast, overrideData]);
|
||||
|
||||
return (
|
||||
<div ref={$el} className={cn('overflow-hidden', className)} {...props}>
|
||||
{isLoading ? (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-[80vh] max-h-[60rem] w-full flex-col items-center justify-center overflow-hidden rounded',
|
||||
)}
|
||||
>
|
||||
<PDFLoader />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<PDFDocument
|
||||
file={envelopeItemFile}
|
||||
className={cn('w-full overflow-hidden rounded', {
|
||||
'h-[80vh] max-h-[60rem]': numPages === 0,
|
||||
})}
|
||||
onLoadSuccess={(d) => onDocumentLoaded(d)}
|
||||
// Uploading a invalid document causes an error which doesn't appear to be handled by the `error` prop.
|
||||
// Therefore we add some additional custom error handling.
|
||||
onSourceError={() => {
|
||||
setPdfError(true);
|
||||
}}
|
||||
externalLinkTarget="_blank"
|
||||
loading={
|
||||
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50 dark:bg-background">
|
||||
{pdfError ? (
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>
|
||||
<Trans>Something went wrong while loading the document.</Trans>
|
||||
</p>
|
||||
<p className="mt-1 text-sm">
|
||||
<Trans>Please try again or contact our support.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<PDFLoader />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
error={
|
||||
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50 dark:bg-background">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>
|
||||
<Trans>Something went wrong while loading the document.</Trans>
|
||||
</p>
|
||||
<p className="mt-1 text-sm">
|
||||
<Trans>Please try again or contact our support.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
options={pdfViewerOptions}
|
||||
>
|
||||
{Array(numPages)
|
||||
.fill(null)
|
||||
.map((_, i) => (
|
||||
<div key={i} className="last:-mb-2">
|
||||
<div className="overflow-hidden rounded border border-border will-change-transform">
|
||||
<PDFPage
|
||||
pageNumber={i + 1}
|
||||
width={width}
|
||||
renderAnnotationLayer={false}
|
||||
renderTextLayer={false}
|
||||
loading={() => ''}
|
||||
renderMode={customPageRenderer ? 'custom' : 'canvas'}
|
||||
customRenderer={customPageRenderer}
|
||||
onClick={(e) => onDocumentPageClick(e, i + 1)}
|
||||
/>
|
||||
</div>
|
||||
<p className="my-2 text-center text-[11px] text-muted-foreground/80">
|
||||
<Trans>
|
||||
Page {i + 1} of {numPages}
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</PDFDocument>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PDFViewer;
|
||||
@@ -1 +0,0 @@
|
||||
export * from './base';
|
||||
@@ -1,19 +0,0 @@
|
||||
import { ClientOnly } from '../../components/client-only';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { PDFViewer, type PDFViewerProps } from './base.client';
|
||||
|
||||
export const PDFViewerLazy = (props: PDFViewerProps) => {
|
||||
return (
|
||||
<ClientOnly
|
||||
fallback={
|
||||
<div>
|
||||
<Trans>Loading...</Trans>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{() => <PDFViewer {...props} />}
|
||||
</ClientOnly>
|
||||
);
|
||||
};
|
||||
@@ -24,7 +24,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
|
||||
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR, getPdfPagesCount } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
|
||||
import {
|
||||
@@ -188,13 +188,15 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
}
|
||||
|
||||
if (duplicateAll) {
|
||||
const pages = Array.from(document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR));
|
||||
const totalPages = getPdfPagesCount();
|
||||
|
||||
pages.forEach((_, index) => {
|
||||
const pageNumber = index + 1;
|
||||
if (totalPages < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
|
||||
if (pageNumber === lastActiveField.pageNumber) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newField: TAddTemplateFieldsFormSchema['fields'][0] = {
|
||||
@@ -208,7 +210,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
};
|
||||
|
||||
append(newField);
|
||||
});
|
||||
}
|
||||
|
||||
void handleAutoSave();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user