Skip to content
Merged
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
198 changes: 150 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,84 +4,108 @@ React wrapper for pdfjs with multiple file support.

This library uses [pdf.js][1].

> What does in development mean:
>
> 1. **MINOR** versions represent **breaking changes**
> 1. **PATCH** versions represent **fixes _and_ features**
> 1. There are **no deprecation warnings** between releases

## Installation & Usage

`yarn add react-pdfjs-multi`
`npm i react-pdfjs-multi`

or
### Example Usage

`npm i react-pdfjs-multi`
#### Minimal example

Example Usage MultiViewer:
If you only need the defaults, you can render the component with just the `pdfs` array and the bundled CSS.

```javascript
```tsx
import React from 'react';
import { PdfMultiViewer } from 'react-pdfjs-multi';
import workerSrc from 'react-pdfjs-multi/dist/pdf.worker.min.mjs?url';
import 'react-pdfjs-multi/dist/react-pdfjs-multi.css';

const MultiViewerExample = () => {
const pdfFiles = [
'pdfs/test-pdf-a.pdf',
{
title:
'Trace-based Just-in-Time Type Specialization for DynamicLanguages',
source: 'pdfs/compressed.tracemonkey-pldi-09.pdf',
},
'pdfs/test-pdf-b.pdf',
'pdfs/test-pdf-c.pdf',
];

return (
<PdfMultiViewer
pdfs={pdfFiles}
i18nData={{
download: 'Herunterladen',
scaleDown: 'Verkleinern',
scaleUp: 'Vergrößern',
originalSize: 'Originalgröße',
pages: 'Seiten',
zoom: 'Automatischer Zoom',
}}
/>
);
const MinimalMultiViewer = () => (
<PdfMultiViewer pdfs={['/pdfs/sample-a.pdf', '/pdfs/sample-b.pdf']} />
);

export default MinimalMultiViewer;
```

#### MultiViewer customization

This example picks a non-default document via `initialLoadIndex`, and the `icons` prop overrides the visual assets via CSS variables.

```tsx
import React from 'react';
import { PdfMultiViewer, type IconConfig } from 'react-pdfjs-multi';
import workerSrc from 'react-pdfjs-multi/dist/pdf.worker.min.mjs?url';
import 'react-pdfjs-multi/dist/react-pdfjs-multi.css';

const pdfs = ['/pdfs/sample-a.pdf', '/pdfs/sample-b.pdf'];

const icons: IconConfig = {
zoomIn: '/icons/zoom-in.svg',
zoomOut: '/icons/zoom-out.svg',
rotateLeft: '/icons/rotate-left.svg',
rotateRight: '/icons/rotate-right.svg',
download: '/icons/download.svg',
toggleList: '/icons/toggle-list.svg',
selectArrow: '/icons/select-arrow.svg',
texture: 'none',
};

const MultiViewerExample = () => (
<PdfMultiViewer
pdfs={pdfs}
workerSrc={workerSrc}
icons={icons}
rendererIcons={icons}
initialLoadIndex={1}
i18nData={{
download: 'Download',
scaleDown: 'Scale down',
scaleUp: 'Scale up',
}}
/>
);

export default MultiViewerExample;
```

Example Usage Renderer (Typescript)
#### PdfRenderer integration

When building your own viewer, assign the worker once and listen to the `pdfChangeHook` if you need to cache zoom/rotation/scroll positions manually.

```typescript
import React, { FC, useState, useEffect } from 'react';
import { PdfRenderer, PdfjsLib, PDFDocumentProxy } from 'react-pdfjs-multi';
```tsx
import React, { FC, useEffect, useState } from 'react';
import {
PdfRenderer,
PdfjsLib,
type PDFDocumentProxy,
type RendererDocumentPosition,
} from 'react-pdfjs-multi';
import workerSrc from 'react-pdfjs-multi/dist/pdf.worker.min.mjs?url';

PdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;

const RendererExample: FC = () => {
const [pdfDoc, setPdfDoc] = useState<PDFDocumentProxy>();

const logPosition = (index: string, position: RendererDocumentPosition) => {
console.log('document position', index, position);
};

useEffect(() => {
const getPdfDoc = async () => {
const load = async () => {
const doc = await PdfjsLib.getDocument({
url: 'pdfs/compressed.tracemonkey-pldi-09.pdf',
url: '/pdfs/sample-a.pdf',
}).promise;

setPdfDoc(doc);
};

getPdfDoc();
load();
}, []);

if (!pdfDoc) return null;

return <PdfRenderer pdfDoc={pdfDoc} downloadBtn={false} />;
return <PdfRenderer pdfDoc={pdfDoc} downloadBtn pdfChangeHook={logPosition} />;
};

export default RendererExample;
Expand All @@ -91,14 +115,15 @@ export default RendererExample;

A minimal workspace example lives at `apps/example`.

- `apps/example/src/examples/DefaultExample.tsx`
- `apps/example/src/examples/ContrastExample.tsx` + `ContrastExample.css`

```bash
pnpm install
pnpm start
pnpm dev:example
```

Run `pnpm start` and `pnpm dev:example` in separate terminals during development.

### Styling

This library ships with optional default styles in
Expand All @@ -119,6 +144,70 @@ Example override:
}
```

#### Theming recipe (example)

Here is a compact “high-contrast” pattern similar to the example app. It uses
icon overrides, CSS variables, and a scoped theme class:

```tsx
import React from 'react';
import type { IconConfig } from 'react-pdfjs-multi';
import { PdfMultiViewer } from 'react-pdfjs-multi';

const icons: IconConfig = {
zoomIn: '/icons/zoom-in.svg',
zoomOut: '/icons/zoom-out.svg',
rotateLeft: '/icons/rotate-left.svg',
rotateRight: '/icons/rotate-right.svg',
download: '/icons/download.svg',
toggleList: '/icons/toggle-list.svg',
selectArrow: '/icons/select-arrow.svg',
};

const viewerStyle = {
'--pdfjs-multi-bg': '#0a0f1c',
'--pdfjs-multi-texture': 'none',
'--pdfjs-multi-text': '#e2e8f0',
'--pdfjs-multi-surface': 'rgba(15, 23, 42, 0.9)',
'--pdfjs-multi-surface-active': 'rgba(30, 41, 59, 0.92)',
'--pdfjs-multi-muted-text': '#94a3b8',
} as const;

const rendererStyle = {
'--pdfjs-multi-controls-bg': '#0f172a',
'--pdfjs-multi-controls-text': '#f8fafc',
'--pdfjs-multi-control-icon-size': '18px',
'--pdfjs-multi-select-text': '#f8fafc',
} as const;

const HighContrastViewer = () => (
<div className="theme-contrast">
<PdfMultiViewer
pdfs={['/pdfs/sample-a.pdf', '/pdfs/sample-b.pdf']}
icons={icons}
rendererIcons={icons}
style={viewerStyle}
rendererStyle={rendererStyle}
/>
</div>
);
```

```css
.theme-contrast .renderer-controls {
/* renderer-controls defines its own icon vars, so inherit in themed skins */
--pdfjs-multi-control-icon-zoom-out: inherit;
--pdfjs-multi-control-icon-zoom-in: inherit;
--pdfjs-multi-control-icon-rotate-left: inherit;
--pdfjs-multi-control-icon-rotate-right: inherit;
--pdfjs-multi-control-icon-download: inherit;
}

.theme-contrast .dropdown-toolbar-container {
--pdfjs-multi-select-icon: inherit;
}
```

Available variables (non-exhaustive):

| Scope | Variable | Default | Purpose |
Expand Down Expand Up @@ -166,24 +255,35 @@ const icons: IconConfig = {
};
```

If you want the same icon set applied to the renderer controls, pass
`rendererIcons` to `PdfMultiViewer` (or use `icons` directly on `PdfRenderer`).

### PdfMultiViewer

The MultiViewer allows you to pass an array of source strings or an object definition and it lazy-loads PDFs by default, showing the renderer as soon as the active PDF is loaded.
The MultiViewer allows you to pass an array of source strings or objects (optionally with a custom `title`) and it lazy-loads PDFs by default, showing the renderer as soon as the active PDF is loaded.
The list is guarded by the toggle in the option bar; on widths between ~330px and ~667px it switches into an overlay so the renderer keeps the extra space, and selecting a document when the list is overlaid closes it automatically. The viewer also records zoom, rotation, and scroll positions per document via the embedded `pdfChangeHook`, so revisiting a PDF restores the position you left it in.

Props:

| Name | Required | Default | Type | Description |
| --------------- | -------- | ------- | --------------- | -------------------------------------------------------------- |
| pdfs | true | | {array} | An array of strings or objects |
| pdfs | true | | {array} | Strings or `{ source, title? }` objects for each PDF |
| autoZoom | | true | {boolean} | enables/disables autoZoom on component mount and window resize |
| className | | | {string} | Adds a class to the outer viewer container |
| controls | | true | {boolean} | enables/disables controls to e.g. change renderer zoom |
| icons | | | {IconConfig} | Override default icons and texture |
| rendererIcons | | | {IconConfig} | Override renderer control icons |
| rendererClassName | | | {string} | Adds a class to the renderer container |
| rendererStyle | | | {CSSProperties} | Inline styles for the renderer container |
| i18nData | | {}\* | {I18nData} | An object of translated strings, default language is en |
| startIndex | | 0 | {number} | first pdf to display using array index |
| lazyLoad | | true | {boolean} | load only the active PDF until a list item is selected |
| initialLoadIndex| | startIndex | {number|string} | which PDF to prefetch on mount when lazyLoad is enabled |
| style | | | {CSSProperties} | Inline styles for the outer viewer container |
| workerSrc | | | {string} | pdf.js worker URL |

When `lazyLoad` is enabled (the default), you can use `initialLoadIndex` to prime a different PDF than the one shown first, and `PdfMultiViewer` already passes its own `pdfChangeHook` to `PdfRenderer` so zoom/rotation/scroll state is cached automatically; you only need to provide your own hook if you want that data for something else.

### i18n

To be able to use different i18n libraries eg. i18next or react-intl you can pass an i18n object with translated strings to the component.
Expand Down Expand Up @@ -216,10 +316,12 @@ Props:
| ------------- | -------- | ------- | ------------------ | -------------------------------------------------------------- |
| pdfDoc | true | | {PDFDocumentProxy} | A proxy of the pdf document to display |
| autoZoom | | true | {boolean} | enables/disables autoZoom on component mount and window resize |
| className | | | {string} | Adds a class to the renderer container |
| controls | | true | {boolean} | enables/disables controls to e.g. change renderer zoom |
| downloadBtn | | true | {boolean} | enables/disables download button |
| icons | | | {IconConfig} | Override default icons and texture |
| i18nData | | {}\* | {I18nDataRenderer} | An object of translated strings, default language is en |
| style | | | {CSSProperties} | Inline styles for the renderer container |
| zoom | | null | {number} | Initial Zoom |
| rotation | | null | {number} | Initial Rotation |
| scrollTop | | null | {number} | Initial ScrollTop |
Expand Down
1 change: 1 addition & 0 deletions apps/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.563.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-pdfjs-multi": "workspace:*"
Expand Down
76 changes: 61 additions & 15 deletions apps/example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,67 @@
import { PdfMultiViewer } from 'react-pdfjs-multi';
import { useState } from 'react';
import workerSrc from 'react-pdfjs-multi/dist/pdf.worker.min.mjs?url';
import { ContrastExample, DefaultExample } from './examples';

const baseUrl = import.meta.env.BASE_URL ?? '/';
const pdfs = [`${baseUrl}pdfs/sample-a.pdf`, `${baseUrl}pdfs/sample-b.pdf`];
const pdfs = [
{ title: 'Sample PDF A', source: `${baseUrl}pdfs/sample-a.pdf` },
`${baseUrl}pdfs/sample-b.pdf`,
];

const App = () => (
<div className="app">
<header className="app-header">
<div>
<h1>react-pdfjs-multi</h1>
<p>Example Multi-Viewer (Vite App)</p>
</div>
</header>
<section className="viewer-shell">
<PdfMultiViewer pdfs={pdfs} workerSrc={workerSrc} />
</section>
</div>
);
const examples = [
{
id: 'default',
label: 'Default',
description: 'Baseline styling with the stock texture and palette.',
Component: DefaultExample,
},
{
id: 'contrast',
label: 'Custom Styles',
description: 'Customized example of the Multiviewer',
Component: ContrastExample,
},
];

const App = () => {
const [activeExample, setActiveExample] = useState(examples[0].id);
const selectedExample =
examples.find((example) => example.id === activeExample) ?? examples[0];
const ExampleComponent = selectedExample.Component;

return (
<div className="app">
<header className="app-header">
<div>
<h1>react-pdfjs-multi</h1>
<p>Example Multi-Viewer (Vite App)</p>
</div>
<div className="example-switcher" role="tablist" aria-label="Examples">
{examples.map((example) => (
<button
className={`example-switcher-button${
example.id === activeExample ? ' active' : ''
}`}
key={example.id}
onClick={() => setActiveExample(example.id)}
type="button"
role="tab"
aria-selected={example.id === activeExample}
>
{example.label}
</button>
))}
</div>
</header>
<section className="example-meta" aria-live="polite">
<span className="example-meta-title">{selectedExample.label}</span>
<span className="example-meta-description">
{selectedExample.description}
</span>
</section>
<ExampleComponent pdfs={pdfs} workerSrc={workerSrc} />
</div>
);
};

export default App;
Loading