@scalewing/react 1.41.0 → 1.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +180 -1
- package/dist/components/ActionMenu.d.ts +4 -2
- package/dist/components/ActionMenu.js +4 -8
- package/dist/components/Article.d.ts +60 -0
- package/dist/components/Article.js +42 -0
- package/dist/components/DescriptionList.d.ts +12 -2
- package/dist/components/DescriptionList.js +7 -3
- package/dist/components/DocumentViewer.d.ts +41 -0
- package/dist/components/DocumentViewer.js +72 -0
- package/dist/components/FileField.d.ts +55 -0
- package/dist/components/FileField.js +75 -0
- package/dist/components/FilterChips.d.ts +1 -1
- package/dist/components/FilterChips.js +1 -1
- package/dist/components/FilterChipsRow.d.ts +24 -0
- package/dist/components/FilterChipsRow.js +14 -0
- package/dist/components/ListRow.d.ts +31 -2
- package/dist/components/ListRow.js +19 -18
- package/dist/components/MarkdownView.d.ts +39 -0
- package/dist/components/MarkdownView.js +30 -0
- package/dist/components/NavCount.d.ts +15 -2
- package/dist/components/NavCount.js +16 -6
- package/dist/components/action-menu/assert-action-menu.d.ts +13 -0
- package/dist/components/action-menu/assert-action-menu.js +21 -0
- package/dist/components/chart/use-chart-width.js +2 -24
- package/dist/components/description-list-layout.d.ts +10 -0
- package/dist/components/description-list-layout.js +13 -0
- package/dist/components/document-viewer/document-viewer-labels.d.ts +23 -0
- package/dist/components/document-viewer/document-viewer-labels.js +23 -0
- package/dist/components/document-viewer/page-box.d.ts +8 -0
- package/dist/components/document-viewer/page-box.js +12 -0
- package/dist/components/document-viewer/pdf-document.d.ts +32 -0
- package/dist/components/document-viewer/pdf-document.js +66 -0
- package/dist/components/document-viewer/pdf-options.d.ts +36 -0
- package/dist/components/document-viewer/pdf-options.js +46 -0
- package/dist/components/document-viewer/render-pdf-page.d.ts +20 -0
- package/dist/components/document-viewer/render-pdf-page.js +38 -0
- package/dist/components/document-viewer/scroll-chain.d.ts +6 -0
- package/dist/components/document-viewer/scroll-chain.js +23 -0
- package/dist/components/document-viewer/use-error-report.d.ts +5 -0
- package/dist/components/document-viewer/use-error-report.js +13 -0
- package/dist/components/document-viewer/use-page-render.d.ts +21 -0
- package/dist/components/document-viewer/use-page-render.js +70 -0
- package/dist/components/document-viewer/use-pdf-document.d.ts +22 -0
- package/dist/components/document-viewer/use-pdf-document.js +47 -0
- package/dist/components/document-viewer/use-pinch-zoom.d.ts +12 -0
- package/dist/components/document-viewer/use-pinch-zoom.js +67 -0
- package/dist/components/document-viewer/use-zoom.d.ts +23 -0
- package/dist/components/document-viewer/use-zoom.js +70 -0
- package/dist/components/document-viewer/zoom.d.ts +29 -0
- package/dist/components/document-viewer/zoom.js +37 -0
- package/dist/components/file-field/file-field-checks.d.ts +13 -0
- package/dist/components/file-field/file-field-checks.js +23 -0
- package/dist/components/file-field/use-file-drop.d.ts +34 -0
- package/dist/components/file-field/use-file-drop.js +78 -0
- package/dist/components/file-field/use-swap-focus.d.ts +15 -0
- package/dist/components/file-field/use-swap-focus.js +40 -0
- package/dist/components/filter-chips/FilterChip.js +1 -1
- package/dist/components/filter-chips/options.d.ts +10 -0
- package/dist/components/filter-chips/options.js +8 -0
- package/dist/components/filter-chips/validate.d.ts +3 -3
- package/dist/components/filter-chips/validate.js +8 -4
- package/dist/components/list/ListRowContent.d.ts +9 -2
- package/dist/components/list/ListRowContent.js +6 -3
- package/dist/components/list/ListRowItem.d.ts +10 -0
- package/dist/components/list/ListRowItem.js +17 -0
- package/dist/components/list/row-naming.d.ts +17 -0
- package/dist/components/list/row-naming.js +19 -0
- package/dist/components/markdown/markdown-limits.d.ts +14 -0
- package/dist/components/markdown/markdown-limits.js +14 -0
- package/dist/components/markdown/markdown-model.d.ts +94 -0
- package/dist/components/markdown/markdown-model.js +8 -0
- package/dist/components/markdown/markdown-options.d.ts +18 -0
- package/dist/components/markdown/markdown-options.js +10 -0
- package/dist/components/markdown/markdown-rules.d.ts +28 -0
- package/dist/components/markdown/markdown-rules.js +81 -0
- package/dist/components/markdown/markdown-text.d.ts +17 -0
- package/dist/components/markdown/markdown-text.js +86 -0
- package/dist/components/markdown/parse-markdown.d.ts +4 -0
- package/dist/components/markdown/parse-markdown.js +21 -0
- package/dist/components/markdown/read-blocks.d.ts +8 -0
- package/dist/components/markdown/read-blocks.js +168 -0
- package/dist/components/markdown/read-inlines.d.ts +12 -0
- package/dist/components/markdown/read-inlines.js +126 -0
- package/dist/components/markdown/read-markdown.d.ts +9 -0
- package/dist/components/markdown/read-markdown.js +27 -0
- package/dist/components/markdown/reading-context.d.ts +18 -0
- package/dist/components/markdown/reading-context.js +35 -0
- package/dist/components/markdown/render-markdown.d.ts +10 -0
- package/dist/components/markdown/render-markdown.js +52 -0
- package/dist/components/slot.d.ts +9 -0
- package/dist/components/slot.js +13 -0
- package/dist/components/use-element-width.d.ts +11 -0
- package/dist/components/use-element-width.js +38 -0
- package/dist/css/css-action-menu-trigger.d.ts +6 -2
- package/dist/css/css-action-menu-trigger.js +50 -2
- package/dist/css/css-action-menu.js +4 -2
- package/dist/css/css-article.d.ts +2 -0
- package/dist/css/css-article.js +110 -0
- package/dist/css/css-components.js +15 -0
- package/dist/css/css-count-pill.js +14 -1
- package/dist/css/css-description-list.d.ts +1 -0
- package/dist/css/css-description-list.js +24 -0
- package/dist/css/css-document-viewer.d.ts +2 -0
- package/dist/css/css-document-viewer.js +186 -0
- package/dist/css/css-file-field.d.ts +2 -0
- package/dist/css/css-file-field.js +194 -0
- package/dist/css/css-filter-chip-tones.d.ts +3 -0
- package/dist/css/css-filter-chip-tones.js +28 -0
- package/dist/css/css-filter-chips-layout.d.ts +1 -0
- package/dist/css/css-filter-chips-layout.js +1 -1
- package/dist/css/css-filter-chips-row.d.ts +2 -0
- package/dist/css/css-filter-chips-row.js +79 -0
- package/dist/css/css-filter-chips.js +8 -0
- package/dist/css/css-font-mono.d.ts +3 -0
- package/dist/css/css-font-mono.js +23 -0
- package/dist/css/css-list-row-trailing.d.ts +2 -0
- package/dist/css/css-list-row-trailing.js +91 -0
- package/dist/css/css-list-row-truncate.d.ts +2 -0
- package/dist/css/css-list-row-truncate.js +29 -0
- package/dist/css/css-list.js +9 -1
- package/dist/css/css-markdown.d.ts +2 -0
- package/dist/css/css-markdown.js +144 -0
- package/dist/css/css-utilities.js +3 -0
- package/dist/css/css-variables.js +1 -0
- package/dist/css/spacing-classes.d.ts +1 -1
- package/dist/css/spacing-classes.js +1 -0
- package/dist/css/stylesheet.js +10 -0
- package/dist/document-viewer-worker.d.ts +9 -0
- package/dist/document-viewer-worker.js +9 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +8 -3
- package/dist/list-options.d.ts +5 -2
- package/dist/list-options.js +7 -2
- package/dist/styles.css +649 -0
- package/package.json +10 -2
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The viewer's words, all in the consumer's language. */
|
|
2
|
+
export type DocumentViewerLabels = {
|
|
3
|
+
/** Names the step to the page before, such as "Previous page". */
|
|
4
|
+
previous: string;
|
|
5
|
+
/** Names the step to the page after, such as "Next page". */
|
|
6
|
+
next: string;
|
|
7
|
+
/** Where the reader is, such as `(page, count) => \`Page ${page} of ${count}\``. */
|
|
8
|
+
page: (page: number, pageCount: number) => string;
|
|
9
|
+
/** While the PDF opens, such as "Opening the PDF…". */
|
|
10
|
+
loading: string;
|
|
11
|
+
/** When it cannot be shown, such as "This PDF couldn't be shown". */
|
|
12
|
+
failed: string;
|
|
13
|
+
/** A line under `failed`, such as what the reader can do instead. */
|
|
14
|
+
failedDetail?: string;
|
|
15
|
+
};
|
|
16
|
+
/** Fails closed on missing words: the viewer has no words of its own. */
|
|
17
|
+
export declare function assertDocumentViewer({ label, labels, workerSrc, }: {
|
|
18
|
+
label: string;
|
|
19
|
+
labels: DocumentViewerLabels;
|
|
20
|
+
workerSrc: string;
|
|
21
|
+
}): void;
|
|
22
|
+
/** The page shown: the one asked for, kept between 1 and the last page. */
|
|
23
|
+
export declare function shownPage(page: number, pageCount: number): number;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function isBlank(text) {
|
|
2
|
+
return typeof text !== 'string' || text.trim() === '';
|
|
3
|
+
}
|
|
4
|
+
/** Fails closed on missing words: the viewer has no words of its own. */
|
|
5
|
+
export function assertDocumentViewer({ label, labels, workerSrc, }) {
|
|
6
|
+
if (isBlank(label))
|
|
7
|
+
throw new RangeError('DocumentViewer label must be non-empty text');
|
|
8
|
+
if (isBlank(workerSrc))
|
|
9
|
+
throw new RangeError('DocumentViewer workerSrc must be a URL');
|
|
10
|
+
for (const key of ['previous', 'next', 'loading', 'failed'])
|
|
11
|
+
if (isBlank(labels?.[key]))
|
|
12
|
+
throw new RangeError(`DocumentViewer labels.${key} must be non-empty text`);
|
|
13
|
+
if (typeof labels.page !== 'function')
|
|
14
|
+
throw new RangeError('DocumentViewer labels.page must be a function');
|
|
15
|
+
if (labels.failedDetail !== undefined && isBlank(labels.failedDetail))
|
|
16
|
+
throw new RangeError('DocumentViewer labels.failedDetail must be non-empty text');
|
|
17
|
+
}
|
|
18
|
+
/** The page shown: the one asked for, kept between 1 and the last page. */
|
|
19
|
+
export function shownPage(page, pageCount) {
|
|
20
|
+
if (!Number.isInteger(page))
|
|
21
|
+
return 1;
|
|
22
|
+
return Math.min(Math.max(page, 1), Math.max(pageCount, 1));
|
|
23
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
/** A US Letter page's height over its width, the box's shape until known. */
|
|
3
|
+
export declare const letterAspect: number;
|
|
4
|
+
/**
|
|
5
|
+
* The page box's size, read by the generated `sw-document-viewer-page`
|
|
6
|
+
* rule: its width in CSS pixels and its height over its width.
|
|
7
|
+
*/
|
|
8
|
+
export declare function pageBoxStyle(width: number, aspect: number | null): CSSProperties;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** A US Letter page's height over its width, the box's shape until known. */
|
|
2
|
+
export const letterAspect = 11 / 8.5;
|
|
3
|
+
/**
|
|
4
|
+
* The page box's size, read by the generated `sw-document-viewer-page`
|
|
5
|
+
* rule: its width in CSS pixels and its height over its width.
|
|
6
|
+
*/
|
|
7
|
+
export function pageBoxStyle(width, aspect) {
|
|
8
|
+
return {
|
|
9
|
+
'--sw-document-page-width': `${Math.max(0, Math.round(width))}px`,
|
|
10
|
+
'--sw-document-page-aspect': String(aspect ?? letterAspect),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type PdfPageSize } from './render-pdf-page.js';
|
|
2
|
+
export type { PdfPageSize };
|
|
3
|
+
/** An open PDF, as DocumentViewer uses it. */
|
|
4
|
+
export type PdfDocument = {
|
|
5
|
+
pageCount: number;
|
|
6
|
+
/**
|
|
7
|
+
* Draws page `page` (from 1) `width` CSS pixels wide into `canvas`, and
|
|
8
|
+
* its text into `text` (transparent, for reading and selecting), and
|
|
9
|
+
* resolves with its size. `signal` cancels it.
|
|
10
|
+
*/
|
|
11
|
+
renderPage: (options: {
|
|
12
|
+
page: number;
|
|
13
|
+
width: number;
|
|
14
|
+
canvas: HTMLCanvasElement;
|
|
15
|
+
text: HTMLElement;
|
|
16
|
+
signal: AbortSignal;
|
|
17
|
+
}) => Promise<PdfPageSize>;
|
|
18
|
+
/** The aspect (height over width) of page `page`, without drawing it. */
|
|
19
|
+
pageAspect: (page: number) => Promise<number>;
|
|
20
|
+
destroy: () => Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
/** Whether an error is PDF.js reporting a cancelled render or text layer. */
|
|
23
|
+
export declare function isCancelled(error: unknown): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Opens a PDF from its bytes with PDF.js's legacy build (the modern one
|
|
26
|
+
* needs browsers newer than the ones stores run, such as for
|
|
27
|
+
* `Map.prototype.getOrInsertComputed`), loaded on first use (so a page
|
|
28
|
+
* without a viewer never downloads it), in a module worker started from
|
|
29
|
+
* `workerSrc` and ended with the document. Rejects when the bytes are not
|
|
30
|
+
* a PDF PDF.js can open.
|
|
31
|
+
*/
|
|
32
|
+
export declare function openPdf(data: ArrayBuffer | Uint8Array, workerSrc: string): Promise<PdfDocument>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { copyPdfBytes, hardenedPdfOptions } from './pdf-options.js';
|
|
2
|
+
import { renderPdfPage } from './render-pdf-page.js';
|
|
3
|
+
/** Whether an error is PDF.js reporting a cancelled render or text layer. */
|
|
4
|
+
export function isCancelled(error) {
|
|
5
|
+
return (error instanceof Error &&
|
|
6
|
+
(error.name === 'RenderingCancelledException' ||
|
|
7
|
+
error.name === 'AbortException'));
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Opens a PDF from its bytes with PDF.js's legacy build (the modern one
|
|
11
|
+
* needs browsers newer than the ones stores run, such as for
|
|
12
|
+
* `Map.prototype.getOrInsertComputed`), loaded on first use (so a page
|
|
13
|
+
* without a viewer never downloads it), in a module worker started from
|
|
14
|
+
* `workerSrc` and ended with the document. Rejects when the bytes are not
|
|
15
|
+
* a PDF PDF.js can open.
|
|
16
|
+
*/
|
|
17
|
+
export async function openPdf(data, workerSrc) {
|
|
18
|
+
const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
|
|
19
|
+
const port = new Worker(workerSrc, { type: 'module' });
|
|
20
|
+
const worker = pdfjs.PDFWorker.create({
|
|
21
|
+
port,
|
|
22
|
+
verbosity: hardenedPdfOptions.verbosity,
|
|
23
|
+
});
|
|
24
|
+
const task = pdfjs.getDocument({
|
|
25
|
+
...hardenedPdfOptions,
|
|
26
|
+
data: copyPdfBytes(data),
|
|
27
|
+
worker,
|
|
28
|
+
});
|
|
29
|
+
// The worker always ends, even when PDF.js fails to close the document.
|
|
30
|
+
async function destroy() {
|
|
31
|
+
try {
|
|
32
|
+
await task.destroy();
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
worker.destroy();
|
|
36
|
+
port.terminate();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
let document;
|
|
40
|
+
try {
|
|
41
|
+
document = await task.promise;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
// The open error is the one reported, never a failure to close.
|
|
45
|
+
await destroy().catch(() => undefined);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
const loaded = document;
|
|
49
|
+
return {
|
|
50
|
+
pageCount: loaded.numPages,
|
|
51
|
+
destroy,
|
|
52
|
+
async pageAspect(page) {
|
|
53
|
+
const { width, height } = (await loaded.getPage(page)).getViewport({
|
|
54
|
+
scale: 1,
|
|
55
|
+
});
|
|
56
|
+
return height / width;
|
|
57
|
+
},
|
|
58
|
+
async renderPage({ page, ...target }) {
|
|
59
|
+
return renderPdfPage({
|
|
60
|
+
page: await loaded.getPage(page),
|
|
61
|
+
...target,
|
|
62
|
+
createTextLayer: (options) => new pdfjs.TextLayer(options),
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The options every document opens with: nothing is fetched beyond the
|
|
3
|
+
* bytes given, no font, character map, colour profile or WebAssembly file
|
|
4
|
+
* (PDF.js falls back to system fonts and skips what it cannot decode), no
|
|
5
|
+
* XFA forms, no streaming or range requests, and no code evaluation.
|
|
6
|
+
*
|
|
7
|
+
* `isEvalSupported` is absent from PDF.js 6.4.299, whose bundles hold no
|
|
8
|
+
* `eval` or `new Function`; it stays here, set to `false`, so the
|
|
9
|
+
* hardening reads the same as the Teisoro API's and survives a downgrade.
|
|
10
|
+
*/
|
|
11
|
+
export declare const hardenedPdfOptions: {
|
|
12
|
+
readonly isEvalSupported: false;
|
|
13
|
+
readonly useWorkerFetch: false;
|
|
14
|
+
readonly useWasm: false;
|
|
15
|
+
readonly useSystemFonts: true;
|
|
16
|
+
readonly disableFontFace: false;
|
|
17
|
+
readonly enableXfa: false;
|
|
18
|
+
readonly disableRange: true;
|
|
19
|
+
readonly disableStream: true;
|
|
20
|
+
readonly disableAutoFetch: true;
|
|
21
|
+
readonly stopAtErrors: false;
|
|
22
|
+
readonly verbosity: 0;
|
|
23
|
+
};
|
|
24
|
+
/** The largest canvas drawn, in device pixels: within every browser's limit. */
|
|
25
|
+
export declare const maxCanvasPixels = 16000000;
|
|
26
|
+
/**
|
|
27
|
+
* The device-pixel ratio a page of `width` by `height` CSS pixels is drawn
|
|
28
|
+
* at: the screen's, lowered so the canvas stays under `maxCanvasPixels`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function canvasRatio(width: number, height: number, deviceRatio: number): number;
|
|
31
|
+
/**
|
|
32
|
+
* A private copy of the bytes: PDF.js hands its data to the worker, which
|
|
33
|
+
* detaches the buffer, so the consumer's own bytes (kept for a download)
|
|
34
|
+
* are never touched.
|
|
35
|
+
*/
|
|
36
|
+
export declare function copyPdfBytes(data: ArrayBuffer | Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The options every document opens with: nothing is fetched beyond the
|
|
3
|
+
* bytes given, no font, character map, colour profile or WebAssembly file
|
|
4
|
+
* (PDF.js falls back to system fonts and skips what it cannot decode), no
|
|
5
|
+
* XFA forms, no streaming or range requests, and no code evaluation.
|
|
6
|
+
*
|
|
7
|
+
* `isEvalSupported` is absent from PDF.js 6.4.299, whose bundles hold no
|
|
8
|
+
* `eval` or `new Function`; it stays here, set to `false`, so the
|
|
9
|
+
* hardening reads the same as the Teisoro API's and survives a downgrade.
|
|
10
|
+
*/
|
|
11
|
+
export const hardenedPdfOptions = {
|
|
12
|
+
isEvalSupported: false,
|
|
13
|
+
useWorkerFetch: false,
|
|
14
|
+
useWasm: false,
|
|
15
|
+
useSystemFonts: true,
|
|
16
|
+
disableFontFace: false,
|
|
17
|
+
enableXfa: false,
|
|
18
|
+
disableRange: true,
|
|
19
|
+
disableStream: true,
|
|
20
|
+
disableAutoFetch: true,
|
|
21
|
+
stopAtErrors: false,
|
|
22
|
+
verbosity: 0,
|
|
23
|
+
};
|
|
24
|
+
/** The largest canvas drawn, in device pixels: within every browser's limit. */
|
|
25
|
+
export const maxCanvasPixels = 16_000_000;
|
|
26
|
+
/**
|
|
27
|
+
* The device-pixel ratio a page of `width` by `height` CSS pixels is drawn
|
|
28
|
+
* at: the screen's, lowered so the canvas stays under `maxCanvasPixels`.
|
|
29
|
+
*/
|
|
30
|
+
export function canvasRatio(width, height, deviceRatio) {
|
|
31
|
+
const ratio = deviceRatio > 0 ? deviceRatio : 1;
|
|
32
|
+
const area = width * height * ratio * ratio;
|
|
33
|
+
if (area <= maxCanvasPixels)
|
|
34
|
+
return ratio;
|
|
35
|
+
return Math.sqrt(maxCanvasPixels / (width * height));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A private copy of the bytes: PDF.js hands its data to the worker, which
|
|
39
|
+
* detaches the buffer, so the consumer's own bytes (kept for a download)
|
|
40
|
+
* are never touched.
|
|
41
|
+
*/
|
|
42
|
+
export function copyPdfBytes(data) {
|
|
43
|
+
return data instanceof Uint8Array
|
|
44
|
+
? data.slice()
|
|
45
|
+
: new Uint8Array(data.slice(0));
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type PDFPageProxy, type TextLayer } from 'pdfjs-dist';
|
|
2
|
+
/** A page drawn: its CSS size at the width asked for. */
|
|
3
|
+
export type PdfPageSize = {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Draws one page `width` CSS pixels wide into `canvas`, at the screen's
|
|
9
|
+
* pixel ratio (capped), and its text into `text` (PDF.js's transparent
|
|
10
|
+
* text layer, at the scale the generated rules read). `signal` cancels
|
|
11
|
+
* both.
|
|
12
|
+
*/
|
|
13
|
+
export declare function renderPdfPage({ page, width, canvas, text, signal, createTextLayer, }: {
|
|
14
|
+
page: PDFPageProxy;
|
|
15
|
+
width: number;
|
|
16
|
+
canvas: HTMLCanvasElement;
|
|
17
|
+
text: HTMLElement;
|
|
18
|
+
signal: AbortSignal;
|
|
19
|
+
createTextLayer: (options: ConstructorParameters<typeof TextLayer>[0]) => TextLayer;
|
|
20
|
+
}): Promise<PdfPageSize>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { canvasRatio } from './pdf-options.js';
|
|
2
|
+
/**
|
|
3
|
+
* Draws one page `width` CSS pixels wide into `canvas`, at the screen's
|
|
4
|
+
* pixel ratio (capped), and its text into `text` (PDF.js's transparent
|
|
5
|
+
* text layer, at the scale the generated rules read). `signal` cancels
|
|
6
|
+
* both.
|
|
7
|
+
*/
|
|
8
|
+
export async function renderPdfPage({ page, width, canvas, text, signal, createTextLayer, }) {
|
|
9
|
+
const base = page.getViewport({ scale: 1 });
|
|
10
|
+
const viewport = page.getViewport({ scale: width / base.width });
|
|
11
|
+
const ratio = canvasRatio(viewport.width, viewport.height, window.devicePixelRatio);
|
|
12
|
+
canvas.width = Math.floor(viewport.width * ratio);
|
|
13
|
+
canvas.height = Math.floor(viewport.height * ratio);
|
|
14
|
+
const render = page.render({
|
|
15
|
+
canvas,
|
|
16
|
+
viewport,
|
|
17
|
+
transform: ratio === 1 ? undefined : [ratio, 0, 0, ratio, 0, 0],
|
|
18
|
+
});
|
|
19
|
+
text.replaceChildren();
|
|
20
|
+
text.style.setProperty('--total-scale-factor', String(viewport.scale));
|
|
21
|
+
const layer = createTextLayer({
|
|
22
|
+
textContentSource: page.streamTextContent(),
|
|
23
|
+
container: text,
|
|
24
|
+
viewport,
|
|
25
|
+
});
|
|
26
|
+
const cancel = () => {
|
|
27
|
+
render.cancel();
|
|
28
|
+
layer.cancel();
|
|
29
|
+
};
|
|
30
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
31
|
+
try {
|
|
32
|
+
await Promise.all([render.promise, layer.render()]);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
signal.removeEventListener('abort', cancel);
|
|
36
|
+
}
|
|
37
|
+
return { width: viewport.width, height: viewport.height };
|
|
38
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scrolls by `amount` pixels along one axis as a finger would: the
|
|
3
|
+
* element first, then each box around it that scrolls, up to the
|
|
4
|
+
* document, each taking what it can until the amount is spent.
|
|
5
|
+
*/
|
|
6
|
+
export declare function scrollChainBy(start: Element, axis: 'x' | 'y', amount: number): void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const scrolling = /^(auto|scroll|overlay)$/;
|
|
2
|
+
function scrolls(element, axis) {
|
|
3
|
+
if (element === element.ownerDocument.scrollingElement)
|
|
4
|
+
return true;
|
|
5
|
+
const style = getComputedStyle(element);
|
|
6
|
+
return scrolling.test(axis === 'x' ? style.overflowX : style.overflowY);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Scrolls by `amount` pixels along one axis as a finger would: the
|
|
10
|
+
* element first, then each box around it that scrolls, up to the
|
|
11
|
+
* document, each taking what it can until the amount is spent.
|
|
12
|
+
*/
|
|
13
|
+
export function scrollChainBy(start, axis, amount) {
|
|
14
|
+
let left = amount;
|
|
15
|
+
for (let element = start; element && Math.abs(left) >= 0.5; element = element.parentElement) {
|
|
16
|
+
if (!scrolls(element, axis))
|
|
17
|
+
continue;
|
|
18
|
+
const key = axis === 'x' ? 'scrollLeft' : 'scrollTop';
|
|
19
|
+
const was = element[key];
|
|
20
|
+
element[key] = was + left;
|
|
21
|
+
left -= element[key] - was;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* A stable function that reports an error to the consumer's latest
|
|
5
|
+
* `onError`, so a new callback each render never restarts a page's draw.
|
|
6
|
+
*/
|
|
7
|
+
export function useErrorReport(onError) {
|
|
8
|
+
const latest = useRef(onError);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
latest.current = onError;
|
|
11
|
+
});
|
|
12
|
+
return useCallback((error) => latest.current?.(error), []);
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import { type PdfDocument } from './pdf-document.js';
|
|
3
|
+
export type PageRenderState = 'drawing' | 'drawn' | 'failed';
|
|
4
|
+
/**
|
|
5
|
+
* Draws the shown page at `width` CSS pixels into the canvas and text
|
|
6
|
+
* layer, again whenever the page or the width changes, cancelling a draw
|
|
7
|
+
* that is no longer wanted. The page is visible while it draws, so it
|
|
8
|
+
* appears as PDF.js paints it. `aspect` is its height over its width, for
|
|
9
|
+
* the page's box, known before its first draw ends.
|
|
10
|
+
*/
|
|
11
|
+
export declare function usePageRender({ document, page, width, canvas, text, onError, }: {
|
|
12
|
+
document: PdfDocument | null;
|
|
13
|
+
page: number;
|
|
14
|
+
width: number;
|
|
15
|
+
canvas: RefObject<HTMLCanvasElement | null>;
|
|
16
|
+
text: RefObject<HTMLDivElement | null>;
|
|
17
|
+
onError: (error: unknown) => void;
|
|
18
|
+
}): {
|
|
19
|
+
state: PageRenderState;
|
|
20
|
+
aspect: number | null;
|
|
21
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { isCancelled } from './pdf-document.js';
|
|
4
|
+
/**
|
|
5
|
+
* Draws the shown page at `width` CSS pixels into the canvas and text
|
|
6
|
+
* layer, again whenever the page or the width changes, cancelling a draw
|
|
7
|
+
* that is no longer wanted. The page is visible while it draws, so it
|
|
8
|
+
* appears as PDF.js paints it. `aspect` is its height over its width, for
|
|
9
|
+
* the page's box, known before its first draw ends.
|
|
10
|
+
*/
|
|
11
|
+
export function usePageRender({ document, page, width, canvas, text, onError, }) {
|
|
12
|
+
const [outcome, setOutcome] = useState(null);
|
|
13
|
+
const [shape, setShape] = useState(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!document || !canvas.current || !text.current || width <= 0)
|
|
16
|
+
return;
|
|
17
|
+
const abort = new AbortController();
|
|
18
|
+
document
|
|
19
|
+
.renderPage({
|
|
20
|
+
page,
|
|
21
|
+
width,
|
|
22
|
+
canvas: canvas.current,
|
|
23
|
+
text: text.current,
|
|
24
|
+
signal: abort.signal,
|
|
25
|
+
})
|
|
26
|
+
.then((size) => {
|
|
27
|
+
if (abort.signal.aborted)
|
|
28
|
+
return;
|
|
29
|
+
setOutcome({
|
|
30
|
+
document,
|
|
31
|
+
page,
|
|
32
|
+
width,
|
|
33
|
+
state: 'drawn',
|
|
34
|
+
aspect: size.height / size.width,
|
|
35
|
+
});
|
|
36
|
+
}, (error) => {
|
|
37
|
+
if (abort.signal.aborted || isCancelled(error))
|
|
38
|
+
return;
|
|
39
|
+
setOutcome({ document, page, width, state: 'failed', aspect: null });
|
|
40
|
+
onError(error);
|
|
41
|
+
});
|
|
42
|
+
return () => abort.abort();
|
|
43
|
+
}, [document, page, width, canvas, text, onError]);
|
|
44
|
+
// The box takes the page's shape as soon as it is known.
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!document)
|
|
47
|
+
return;
|
|
48
|
+
let live = true;
|
|
49
|
+
document.pageAspect(page).then((aspect) => {
|
|
50
|
+
if (live)
|
|
51
|
+
setShape({ document, page, aspect });
|
|
52
|
+
}, () => undefined);
|
|
53
|
+
return () => {
|
|
54
|
+
live = false;
|
|
55
|
+
};
|
|
56
|
+
}, [document, page]);
|
|
57
|
+
const current = outcome &&
|
|
58
|
+
outcome.document === document &&
|
|
59
|
+
outcome.page === page &&
|
|
60
|
+
outcome.width === width
|
|
61
|
+
? outcome
|
|
62
|
+
: null;
|
|
63
|
+
const known = shape && shape.document === document && shape.page === page
|
|
64
|
+
? shape.aspect
|
|
65
|
+
: null;
|
|
66
|
+
return {
|
|
67
|
+
state: current?.state ?? 'drawing',
|
|
68
|
+
aspect: known ?? current?.aspect ?? null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type PdfDocument } from './pdf-document.js';
|
|
2
|
+
export type PdfDocumentState = {
|
|
3
|
+
status: 'loading';
|
|
4
|
+
} | {
|
|
5
|
+
status: 'ready';
|
|
6
|
+
document: PdfDocument;
|
|
7
|
+
} | {
|
|
8
|
+
status: 'failed';
|
|
9
|
+
error: unknown;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Opens the PDF in `data` and closes it (and its worker) when the bytes
|
|
13
|
+
* change or the viewer goes away. New bytes start a new open and show as
|
|
14
|
+
* loading until it settles, so a consumer's Try again is new bytes (or a
|
|
15
|
+
* new `key`).
|
|
16
|
+
*/
|
|
17
|
+
export declare function usePdfDocument(data: ArrayBuffer | Uint8Array, workerSrc: string, callbacks: {
|
|
18
|
+
onLoad?: (info: {
|
|
19
|
+
pageCount: number;
|
|
20
|
+
}) => void;
|
|
21
|
+
onError?: (error: unknown) => void;
|
|
22
|
+
}): PdfDocumentState;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { openPdf } from './pdf-document.js';
|
|
4
|
+
/** Closes a document no one shows any more; a failure to close is moot. */
|
|
5
|
+
function closeQuietly(document) {
|
|
6
|
+
document.destroy().catch(() => undefined);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Opens the PDF in `data` and closes it (and its worker) when the bytes
|
|
10
|
+
* change or the viewer goes away. New bytes start a new open and show as
|
|
11
|
+
* loading until it settles, so a consumer's Try again is new bytes (or a
|
|
12
|
+
* new `key`).
|
|
13
|
+
*/
|
|
14
|
+
export function usePdfDocument(data, workerSrc, callbacks) {
|
|
15
|
+
const [settled, setSettled] = useState(null);
|
|
16
|
+
// The callbacks are read when the open settles, never reasons to reopen.
|
|
17
|
+
const latest = useRef(callbacks);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
latest.current = callbacks;
|
|
20
|
+
});
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
let live = true;
|
|
23
|
+
let opened = null;
|
|
24
|
+
openPdf(data, workerSrc).then((document) => {
|
|
25
|
+
if (!live) {
|
|
26
|
+
closeQuietly(document);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
opened = document;
|
|
30
|
+
setSettled({ data, workerSrc, state: { status: 'ready', document } });
|
|
31
|
+
latest.current.onLoad?.({ pageCount: document.pageCount });
|
|
32
|
+
}, (error) => {
|
|
33
|
+
if (!live)
|
|
34
|
+
return;
|
|
35
|
+
setSettled({ data, workerSrc, state: { status: 'failed', error } });
|
|
36
|
+
latest.current.onError?.(error);
|
|
37
|
+
});
|
|
38
|
+
return () => {
|
|
39
|
+
live = false;
|
|
40
|
+
if (opened)
|
|
41
|
+
closeQuietly(opened);
|
|
42
|
+
};
|
|
43
|
+
}, [data, workerSrc]);
|
|
44
|
+
return settled && settled.data === data && settled.workerSrc === workerSrc
|
|
45
|
+
? settled.state
|
|
46
|
+
: { status: 'loading' };
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Two fingers on the page zoom it, between fit width and four times as
|
|
4
|
+
* wide, about the point between them. The page's box allows
|
|
5
|
+
* only panning (`touch-action: pan-x pan-y`), so the browser does not zoom
|
|
6
|
+
* the whole screen instead; a two-finger move is taken from the browser
|
|
7
|
+
* (a non-passive listener), one finger still scrolls.
|
|
8
|
+
*/
|
|
9
|
+
export declare function usePinchZoom(box: RefObject<HTMLElement | null>, zoom: number, zoomTo: (zoom: number, at: {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
}) => void): void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { clampZoom } from './zoom.js';
|
|
4
|
+
function spread(touches) {
|
|
5
|
+
const [a, b] = [touches[0], touches[1]];
|
|
6
|
+
return {
|
|
7
|
+
distance: Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY),
|
|
8
|
+
x: (a.clientX + b.clientX) / 2,
|
|
9
|
+
y: (a.clientY + b.clientY) / 2,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Two fingers on the page zoom it, between fit width and four times as
|
|
14
|
+
* wide, about the point between them. The page's box allows
|
|
15
|
+
* only panning (`touch-action: pan-x pan-y`), so the browser does not zoom
|
|
16
|
+
* the whole screen instead; a two-finger move is taken from the browser
|
|
17
|
+
* (a non-passive listener), one finger still scrolls.
|
|
18
|
+
*/
|
|
19
|
+
export function usePinchZoom(box, zoom, zoomTo) {
|
|
20
|
+
const current = useRef(zoom);
|
|
21
|
+
const pinch = useRef(null);
|
|
22
|
+
const setter = useRef(zoomTo);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
current.current = zoom;
|
|
25
|
+
setter.current = zoomTo;
|
|
26
|
+
});
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const element = box.current;
|
|
29
|
+
if (!element)
|
|
30
|
+
return;
|
|
31
|
+
function start(event) {
|
|
32
|
+
if (event.touches.length !== 2)
|
|
33
|
+
return;
|
|
34
|
+
pinch.current = {
|
|
35
|
+
distance: spread(event.touches).distance,
|
|
36
|
+
zoom: current.current,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function move(event) {
|
|
40
|
+
const begun = pinch.current;
|
|
41
|
+
if (!begun || event.touches.length !== 2 || !element)
|
|
42
|
+
return;
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
const now = spread(event.touches);
|
|
45
|
+
if (begun.distance <= 0)
|
|
46
|
+
return;
|
|
47
|
+
const next = clampZoom((begun.zoom * now.distance) / begun.distance);
|
|
48
|
+
if (next === current.current)
|
|
49
|
+
return;
|
|
50
|
+
setter.current(next, { x: now.x, y: now.y });
|
|
51
|
+
}
|
|
52
|
+
function end(event) {
|
|
53
|
+
if (event.touches.length < 2)
|
|
54
|
+
pinch.current = null;
|
|
55
|
+
}
|
|
56
|
+
element.addEventListener('touchstart', start, { passive: true });
|
|
57
|
+
element.addEventListener('touchmove', move, { passive: false });
|
|
58
|
+
element.addEventListener('touchend', end);
|
|
59
|
+
element.addEventListener('touchcancel', end);
|
|
60
|
+
return () => {
|
|
61
|
+
element.removeEventListener('touchstart', start);
|
|
62
|
+
element.removeEventListener('touchmove', move);
|
|
63
|
+
element.removeEventListener('touchend', end);
|
|
64
|
+
element.removeEventListener('touchcancel', end);
|
|
65
|
+
};
|
|
66
|
+
}, [box]);
|
|
67
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** How long a zoom rests before the page is drawn again at its size. */
|
|
2
|
+
export declare const zoomSettleMs = 150;
|
|
3
|
+
/**
|
|
4
|
+
* The page's zoom (1 is fit width) and the scrolling box it is shown in.
|
|
5
|
+
* `zoomTo` changes it about a point on the screen (`at`, in client
|
|
6
|
+
* pixels; the middle of the box's visible part when left out) so the spot
|
|
7
|
+
* of the page under it stays put: once the bigger or smaller page is laid
|
|
8
|
+
* out, the box scrolls across and the boxes around it (the window at
|
|
9
|
+
* last) scroll down by what keeps that spot in place. `drawnZoom`
|
|
10
|
+
* follows `zoom` once it rests, so a pinch stretches the drawn page and
|
|
11
|
+
* only its end draws it again. Another `resetKey` (another page) is fit
|
|
12
|
+
* width again.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useZoom(resetKey: string): {
|
|
15
|
+
box: import("react").RefObject<HTMLDivElement | null>;
|
|
16
|
+
page: import("react").RefObject<HTMLDivElement | null>;
|
|
17
|
+
zoom: number;
|
|
18
|
+
drawnZoom: number;
|
|
19
|
+
zoomTo: (next: number, at?: {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
}) => void;
|
|
23
|
+
};
|