Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/components/ui/pdf/PdfViewer.tsx
T

249 lines
11 KiB
TypeScript

import { useEffect, useState, useRef, useCallback } from "react";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
interface Props {
file: any;
className?: string;
fileName?: any;
editing?: boolean;
canDownload?: boolean;
onRemoveFile?: () => void;
size?: {
height?: number;
width?: number;
};
scale?: number;
}
declare global {
interface Window {
pdfjsLib: any;
}
}
const PDFJS_SCRIPT_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.min.js";
const PDFJS_WORKER_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.worker.min.js";
let pdfJsLoadPromise: Promise<void> | undefined;
function loadPDFJS(): Promise<void> {
if (window.pdfjsLib) {
configurePdfWorker();
return Promise.resolve();
}
pdfJsLoadPromise ??= new Promise<void>((resolve, reject) => {
const existingScript = document.querySelector<HTMLScriptElement>(`script[src="${PDFJS_SCRIPT_SRC}"]`);
const script = existingScript ?? document.createElement("script");
script.src = PDFJS_SCRIPT_SRC;
script.async = true;
script.onload = () => {
configurePdfWorker();
resolve();
};
script.onerror = () => reject(new Error("components.media.errors.pdfLoadFailed"));
if (!existingScript) {
document.body.appendChild(script);
}
});
return pdfJsLoadPromise;
}
function configurePdfWorker() {
if (window.pdfjsLib?.GlobalWorkerOptions) {
window.pdfjsLib.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_SRC;
}
}
export default function PdfViewer({ className, file, onRemoveFile, fileName = "", editing = false, canDownload = true, size, scale = 0.49 }: Props) {
const { t } = useTranslation();
const canvasRef = useRef<HTMLCanvasElement>(null);
const renderTaskRef = useRef<any>(null);
const [pdfRef, setPdfRef] = useState<any>();
const [currentPage, setCurrentPage] = useState(1);
const [pageCount, setPageCount] = useState(0);
const [error, setError] = useState("");
const downloadPdf = () => {
const downloadLink = document.createElement("a");
const name = (fileName ?? "document") + ".pdf";
downloadLink.href = file;
downloadLink.download = name;
downloadLink.click();
};
const renderPage = useCallback(
async (pageNum: number, pdf = pdfRef) => {
if (pdf) {
setPageCount(pdf.numPages);
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = canvasRef.current;
if (canvas) {
canvas.height = size?.height ?? viewport.height;
canvas.width = size?.width ?? viewport.width;
if (renderTaskRef.current) {
renderTaskRef.current.cancel();
}
const renderContext = {
canvasContext: canvas.getContext("2d"),
viewport: viewport,
};
renderTaskRef.current = page.render(renderContext);
renderTaskRef.current.promise.catch((error: any) => {
if (error.name !== "RenderingCancelledException") {
// eslint-disable-next-line no-console
console.error(error);
}
});
}
}
},
[pdfRef, scale, size]
);
useEffect(() => {
const initPdf = async () => {
try {
await loadPDFJS();
const PDFJS = window.pdfjsLib;
const loadingTask = PDFJS.getDocument(file);
const loadedPdf = await loadingTask.promise;
setPdfRef(loadedPdf);
} catch (error: any) {
setError(t(error instanceof Error ? error.message : "components.media.errors.pdfLoadFailed"));
}
};
void initPdf();
}, [file, t]);
useEffect(() => {
renderPage(currentPage, pdfRef);
}, [pdfRef, currentPage, renderPage]);
const nextPage = () => {
pdfRef && currentPage < pdfRef.numPages && setCurrentPage(currentPage + 1);
};
const prevPage = () => {
currentPage > 1 && setCurrentPage(currentPage - 1);
};
return (
<div
id="pdf-viewer"
className={clsx(className, "border-border-default text-text-neutral-tertiary bg-surface-neutral-primary items-center overflow-hidden rounded-md border border-dashed")}
style={{
height: size?.height ?? "auto",
width: size?.width ?? "auto",
}}
>
{error ? (
<div className="text-red-500">{error}</div>
) : !pdfRef ? (
<div className={clsx("flex items-center justify-center", size ? "h-full p-12" : "h-64 p-12")}>
<div className="base-spinner text-text-neutral-tertiary flex justify-center p-12 text-sm italic"></div>
</div>
) : (
<>
<div className="flex items-center justify-between p-2">
<div className="flex items-center space-x-2">
<div className="left-0 top-0 ml-1 mt-1 flex origin-top-left cursor-default items-center space-x-2">
<span className="relative z-0 inline-flex rounded-md shadow-2xs">
<button
type="button"
className={clsx(
"focus:border-theme-500 shadow-focus border-border-default text-text-neutral-tertiary bg-surface-neutral-primary relative inline-flex items-center rounded-l-md border px-1 py-1 text-xs font-medium focus:z-10 focus:outline-hidden focus:ring-1",
currentPage === 1 && "bg-surface-neutral-secondary cursor-not-allowed",
currentPage !== 1 && "hover:bg-surface-neutral-secondary"
)}
disabled={currentPage === 1}
onClick={(e) => {
e.preventDefault();
prevPage();
}}
>
<span className="sr-only">{t("shared.previous")}</span>
<svg className="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
fillRule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</button>
<span className="border-border-default bg-surface-neutral-primary text-text-neutral-primary/80 relative -ml-px inline-flex select-none items-center border px-2 py-1 text-xs font-medium">
{currentPage} / {pageCount}
</span>
<button
type="button"
className={clsx(
"focus:border-theme-500 shadow-focus hover:bg-surface-neutral-secondary border-border-default text-text-neutral-tertiary bg-surface-neutral-primary relative -ml-px inline-flex items-center rounded-r-md border px-1 py-1 text-xs font-medium focus:z-10 focus:outline-hidden focus:ring-1",
currentPage === pageCount && "bg-surface-neutral-secondary cursor-not-allowed",
currentPage !== pageCount && "hover:bg-surface-neutral-secondary"
)}
disabled={currentPage === pageCount}
onClick={(e) => {
e.preventDefault();
nextPage();
}}
>
<span className="sr-only">{t("shared.next")}</span>
<svg className="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
fillRule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
</button>
</span>
</div>
{canDownload && (
<button
type="button"
className="shadow-focus hover:bg-surface-neutral-secondary border-border-default text-text-neutral-tertiary right-0 top-0 mr-0 mt-1 inline-flex origin-top-right items-center rounded px-1.5 py-1.5 text-xs font-medium focus:outline-hidden focus:ring-2 focus:ring-offset-1"
aria-label={t("shared.download")}
onClick={(e) => {
e.preventDefault();
downloadPdf();
}}
>
<svg className="h-4 w-4" fill="currentColor" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24" height="24" viewBox="0 0 24 24">
<path d="M 11 2 C 10.448 2 10 2.448 10 3 L 10 11 L 6.5 11 A 0.5 0.5 0 0 0 6 11.5 A 0.5 0.5 0 0 0 6.1464844 11.853516 A 0.5 0.5 0 0 0 6.1777344 11.882812 L 11.283203 16.697266 L 11.316406 16.728516 A 1 1 0 0 0 12 17 A 1 1 0 0 0 12.683594 16.728516 L 12.697266 16.716797 A 1 1 0 0 0 12.707031 16.705078 L 17.810547 11.892578 A 0.5 0 0 0 17.839844 11.865234 L 17.847656 11.859375 A 0.5 0 0 0 17.853516 11.853516 A 0.5 0 0 0 18 11.5 A 0.5 0 0 0 17.5 11 L 14 11 L 14 3 C 14 2.448 13.552 2 13 2 L 12 2 L 11 2 z M 3 20 A 1.0001 1.0001 0 1 0 3 22 L 21 22 A 1.0001 1.0001 0 1 0 21 20 L 3 20 z"></path>
</svg>
</button>
)}
</div>
<div className="flex cursor-default items-center space-x-2">
{editing && (
<button
type="button"
className="shadow-focus hover:bg-surface-neutral-secondary border-border-default text-text-neutral-tertiary right-0 top-0 mr-0 mt-1 inline-flex origin-top-right items-center rounded px-1.5 py-1.5 text-xs font-medium focus:outline-hidden focus:ring-2 focus:ring-offset-1"
aria-label={t("shared.remove")}
onClick={onRemoveFile}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
)}
</div>
</div>
<div className="mx-auto flex items-center justify-center py-4 text-center">
<canvas ref={canvasRef}></canvas>
</div>
</>
)}
</div>
);
}