Skip to content

Commit

Permalink
add pdf viewer
Browse files Browse the repository at this point in the history
  • Loading branch information
ajbura committed Sep 6, 2023
1 parent a20484c commit 4c9922d
Show file tree
Hide file tree
Showing 12 changed files with 1,028 additions and 162 deletions.
503 changes: 475 additions & 28 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"linkifyjs": "4.0.2",
"matrix-js-sdk": "24.1.0",
"millify": "6.1.0",
"pdfjs-dist": "3.10.111",
"prismjs": "1.29.0",
"prop-types": "15.8.1",
"react": "17.0.2",
Expand Down
36 changes: 36 additions & 0 deletions src/app/components/Pdf-viewer/PdfViewer.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { style } from '@vanilla-extract/css';
import { DefaultReset, color, config } from 'folds';

export const PdfViewer = style([
DefaultReset,
{
height: '100%',
},
]);

export const PdfViewerHeader = style([
DefaultReset,
{
paddingLeft: config.space.S200,
paddingRight: config.space.S200,
borderBottomWidth: config.borderWidth.B300,
flexShrink: 0,
gap: config.space.S200,
},
]);
export const PdfViewerFooter = style([
PdfViewerHeader,
{
borderTopWidth: config.borderWidth.B300,
borderBottomWidth: 0,
},
]);

export const PdfViewerContent = style([
DefaultReset,
{
backgroundColor: color.Surface.Container,
color: color.Surface.OnContainer,
overflow: 'hidden',
},
]);
242 changes: 242 additions & 0 deletions src/app/components/Pdf-viewer/PdfViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/* eslint-disable no-param-reassign */
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
import React, { FormEventHandler, useCallback, useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import {
Box,
Button,
Chip,
Header,
Icon,
IconButton,
Icons,
Input,
Menu,
PopOut,
Scroll,
Text,
as,
config,
} from 'folds';
import type * as PdfJsDist from 'pdfjs-dist';
import FocusTrap from 'focus-trap-react';
import * as css from './PdfViewer.css';
import { AsyncStatus } from '../../hooks/useAsyncCallback';
import { useZoom } from '../../hooks/useZoom';
import { usePdfDocumentLoader, usePdfJSLoader } from '../../plugins/pdfjs-dist';

export type PdfViewerProps = {
name: string;
src: string;
requestClose: () => void;
};

export const PdfViewer = as<'div', PdfViewerProps>(
({ className, name, src, requestClose, ...props }, ref) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const { zoom, zoomIn, zoomOut, setZoom } = useZoom(0.2);

const [pdfJSState, loadPdfJS] = usePdfJSLoader();
const [docState, loadPdfDocument] = usePdfDocumentLoader(
pdfJSState.status === AsyncStatus.Success ? pdfJSState.data : undefined,
src
);
const [pageNo, setPageNo] = useState(1);
const [openJump, setOpenJump] = useState(false);

useEffect(() => {
loadPdfJS();
}, [loadPdfJS]);
useEffect(() => {
if (pdfJSState.status === AsyncStatus.Success) {
loadPdfDocument();
}
}, [pdfJSState, loadPdfDocument]);

const loadPage = useCallback(
async (doc: PdfJsDist.PDFDocumentProxy, canvas: HTMLCanvasElement, pNo: number) => {
const page = await doc.getPage(pNo);
const pageViewport = page.getViewport({ scale: zoom });
const context = canvas.getContext('2d');
if (!context) return;

canvas.width = pageViewport.width;
canvas.height = pageViewport.height;

page.render({
canvasContext: context,
viewport: pageViewport,
});
scrollRef.current?.scrollTo({
top: 0,
});
},
[zoom]
);

useEffect(() => {
const canvas = canvasRef.current;
if (canvas && docState.status === AsyncStatus.Success) {
const doc = docState.data;
if (pageNo < 0 || pageNo > doc.numPages) return;
loadPage(doc, canvas, pageNo);
}
}, [docState, pageNo, loadPage]);

const handleJumpSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
if (docState.status !== AsyncStatus.Success) return;
const jumpInput = evt.currentTarget.jumpInput as HTMLInputElement;
if (!jumpInput) return;
const jumpTo = parseInt(jumpInput.value, 10);
setPageNo(Math.max(0, Math.min(docState.data.numPages, jumpTo)));
setOpenJump(false);
};

const handlePrevPage = () => {
setPageNo((n) => Math.max(n - 1, 0));
};

const handleNextPage = () => {
if (docState.status !== AsyncStatus.Success) return;
setPageNo((n) => Math.min(n + 1, docState.data.numPages));
};

return (
<Box className={classNames(css.PdfViewer, className)} direction="Column" {...props} ref={ref}>
<Header className={css.PdfViewerHeader} size="400">
<Box grow="Yes" alignItems="Center" gap="200">
<IconButton size="300" radii="300" onClick={requestClose}>
<Icon size="50" src={Icons.ArrowLeft} />
</IconButton>
<Text size="T300" truncate>
{name}
</Text>
</Box>
<Box shrink="No" alignItems="Center" gap="200">
<IconButton
variant={zoom < 1 ? 'Success' : 'SurfaceVariant'}
outlined={zoom < 1}
size="300"
radii="Pill"
onClick={zoomOut}
aria-label="Zoom Out"
>
<Icon size="50" src={Icons.Minus} />
</IconButton>
<Chip variant="SurfaceVariant" radii="Pill" onClick={() => setZoom(zoom === 1 ? 2 : 1)}>
<Text size="B300">{Math.round(zoom * 100)}%</Text>
</Chip>
<IconButton
variant={zoom > 1 ? 'Success' : 'SurfaceVariant'}
outlined={zoom > 1}
size="300"
radii="Pill"
onClick={zoomIn}
aria-label="Zoom In"
>
<Icon size="50" src={Icons.Plus} />
</IconButton>
</Box>
</Header>
<Box
grow="Yes"
className={css.PdfViewerContent}
justifyContent="Center"
alignItems="Center"
>
<Scroll
ref={scrollRef}
size="0"
direction="Both"
hideTrack
variant="Surface"
visibility="Hover"
>
<Box alignItems="Start" justifyContent="Center">
<canvas ref={canvasRef} />
</Box>
</Scroll>
</Box>
{docState.status === AsyncStatus.Success && (
<Header as="footer" className={css.PdfViewerFooter} size="400">
<Chip
variant="Secondary"
radii="300"
before={<Icon size="50" src={Icons.ChevronLeft} />}
onClick={handlePrevPage}
aria-disabled={pageNo <= 1}
>
<Text size="B300">Previous</Text>
</Chip>
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
<PopOut
open={openJump}
align="Center"
position="Top"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setOpenJump(false),
clickOutsideDeactivates: true,
}}
>
<Menu variant="Surface">
<Box
as="form"
onSubmit={handleJumpSubmit}
style={{ padding: config.space.S200 }}
direction="Column"
gap="200"
>
<Input
name="jumpInput"
size="300"
variant="Background"
defaultValue={pageNo}
min={0}
max={docState.data.numPages}
step={1}
outlined
type="number"
radii="300"
aria-label="Page Number"
/>
<Button type="submit" size="300" variant="Primary" radii="300">
<Text size="B300">Jump To Page</Text>
</Button>
</Box>
</Menu>
</FocusTrap>
}
>
{(anchorRef) => (
<Chip
onClick={() => setOpenJump(!openJump)}
ref={anchorRef}
variant="SurfaceVariant"
radii="300"
aria-pressed={openJump}
>
<Text size="B300">{`${pageNo}/${docState.data.numPages}`}</Text>
</Chip>
)}
</PopOut>
</Box>
<Chip
variant="Primary"
radii="300"
after={<Icon size="50" src={Icons.ChevronRight} />}
onClick={handleNextPage}
aria-disabled={pageNo >= docState.data.numPages}
>
<Text size="B300">Next</Text>
</Chip>
</Header>
)}
</Box>
);
}
);
1 change: 1 addition & 0 deletions src/app/components/Pdf-viewer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './PdfViewer';
1 change: 1 addition & 0 deletions src/app/components/image-viewer/ImageViewer.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const ImageViewerHeader = style([
paddingRight: config.space.S200,
borderBottomWidth: config.borderWidth.B300,
flexShrink: 0,
gap: config.space.S200,
},
]);

Expand Down
2 changes: 1 addition & 1 deletion src/app/components/message/layout/layout.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const BubbleAvatar = style([ModernAvatar]);

export const BubbleContent = style({
maxWidth: toRem(800),
padding: `${config.space.S100} ${config.space.S200}`,
padding: config.space.S200,
backgroundColor: color.SurfaceVariant.Container,
color: color.SurfaceVariant.OnContainer,
borderRadius: config.radii.R400,
Expand Down
1 change: 1 addition & 0 deletions src/app/components/text-viewer/TextViewer.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const TextViewerHeader = style([
paddingRight: config.space.S200,
borderBottomWidth: config.borderWidth.B300,
flexShrink: 0,
gap: config.space.S200,
},
]);

Expand Down
Loading

0 comments on commit 4c9922d

Please sign in to comment.