@arbidocs/react 0.3.185 → 0.4.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/dist/{chunk-YJAYST7S.js → chunk-3CLGHHME.js} +880 -240
- package/dist/chunk-3CLGHHME.js.map +1 -0
- package/dist/chunk-6ALMBER7.js +176 -0
- package/dist/chunk-6ALMBER7.js.map +1 -0
- package/dist/chunk-BXP6S34V.cjs +284 -0
- package/dist/chunk-BXP6S34V.cjs.map +1 -0
- package/dist/chunk-EJ2BMHYK.js +272 -0
- package/dist/chunk-EJ2BMHYK.js.map +1 -0
- package/dist/{chunk-IZPXHVPJ.cjs → chunk-OU2QLIKS.cjs} +969 -344
- package/dist/chunk-OU2QLIKS.cjs.map +1 -0
- package/dist/chunk-RZQACYNQ.cjs +218 -0
- package/dist/chunk-RZQACYNQ.cjs.map +1 -0
- package/dist/{index-4WwLZsQO.d.cts → index-CcbVSNSP.d.cts} +40 -2
- package/dist/{index-4WwLZsQO.d.ts → index-CcbVSNSP.d.ts} +40 -2
- package/dist/index.cjs +589 -1070
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -155
- package/dist/index.d.ts +11 -155
- package/dist/index.js +223 -725
- package/dist/index.js.map +1 -1
- package/dist/shell/index.cjs +13 -4
- package/dist/shell/index.d.cts +1 -1
- package/dist/shell/index.d.ts +1 -1
- package/dist/shell/index.js +2 -1
- package/dist/ui/index.cjs +211 -190
- package/dist/ui/index.d.cts +204 -3
- package/dist/ui/index.d.ts +204 -3
- package/dist/ui/index.js +2 -1
- package/dist/viewers/index.cjs +327 -76
- package/dist/viewers/index.cjs.map +1 -1
- package/dist/viewers/index.d.cts +90 -4
- package/dist/viewers/index.d.ts +90 -4
- package/dist/viewers/index.js +316 -78
- package/dist/viewers/index.js.map +1 -1
- package/package.json +4 -3
- package/dist/chunk-3MLQUKT4.cjs +0 -97
- package/dist/chunk-3MLQUKT4.cjs.map +0 -1
- package/dist/chunk-IZPXHVPJ.cjs.map +0 -1
- package/dist/chunk-J3XEKOV5.js +0 -87
- package/dist/chunk-J3XEKOV5.js.map +0 -1
- package/dist/chunk-YJAYST7S.js.map +0 -1
package/dist/viewers/index.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ interface PdfViewerProps {
|
|
|
24
24
|
onLoadSuccess?: (numPages: number) => void;
|
|
25
25
|
/** Incremented to trigger scroll to currentPage */
|
|
26
26
|
scrollTrigger?: number;
|
|
27
|
+
/** Zoom as a multiplier of the fit-width size (1 = fit width). Default 1. */
|
|
28
|
+
zoom?: number;
|
|
29
|
+
/** Called by pinch and double-tap; the host owns the value (its buttons change it too). */
|
|
30
|
+
onZoomChange?: (zoom: number) => void;
|
|
27
31
|
/**
|
|
28
32
|
* Called as the user scrolls, with the page occupying most of the viewport.
|
|
29
33
|
* This is the reusable form of the monolith's `setViewingDocumentPage`.
|
|
@@ -38,7 +42,7 @@ interface PdfViewerProps {
|
|
|
38
42
|
/** Where the app serves the pdf.js worker, cmaps and standard fonts from. */
|
|
39
43
|
assets?: PdfViewerAssets;
|
|
40
44
|
}
|
|
41
|
-
declare function PdfViewer({ file, currentPage, onLoadSuccess, scrollTrigger, onPageChange, disableFontFace, assets, }: PdfViewerProps): React.JSX.Element;
|
|
45
|
+
declare function PdfViewer({ file, currentPage, onLoadSuccess, scrollTrigger, zoom, onZoomChange, onPageChange, disableFontFace, assets, }: PdfViewerProps): React.JSX.Element;
|
|
42
46
|
|
|
43
47
|
type DocResponse = components['schemas']['DocResponse'];
|
|
44
48
|
/** Which native renderer a file needs; `none` when there is no native view. */
|
|
@@ -129,6 +133,78 @@ interface VideoViewerProps {
|
|
|
129
133
|
/** Renders a video blob with the browser's native transport controls. */
|
|
130
134
|
declare function VideoViewer({ blob, title }: VideoViewerProps): React.JSX.Element | null;
|
|
131
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Zoom for page-stacked viewers (PDF + parsed/HTML).
|
|
138
|
+
*
|
|
139
|
+
* Browser pinch-zoom only magnifies the bitmap the page already painted: a PDF canvas rendered at
|
|
140
|
+
* fit-width stays blurry however far you pinch. The viewers therefore own the zoom: `zoom` is a
|
|
141
|
+
* multiplier of the fit-width size (1 = fit width, 2 = twice as wide). The PDF viewer re-renders
|
|
142
|
+
* its canvases at the new scale; the parsed viewer reflows its text. This module holds the pure
|
|
143
|
+
* math and the touch gesture hook (pinch, double-tap) both use.
|
|
144
|
+
*/
|
|
145
|
+
|
|
146
|
+
declare const MIN_ZOOM = 0.5;
|
|
147
|
+
declare const MAX_ZOOM = 4;
|
|
148
|
+
declare function clampZoom(zoom: number): number;
|
|
149
|
+
/** One zoom button step in (`1`) or out (`-1`). */
|
|
150
|
+
declare function stepZoom(zoom: number, direction: 1 | -1): number;
|
|
151
|
+
/** Double-tap toggles between fit width and a comfortable reading zoom. */
|
|
152
|
+
declare function doubleTapZoom(zoom: number): number;
|
|
153
|
+
declare function zoomLabel(zoom: number): string;
|
|
154
|
+
/** The zoom a pinch has reached: the start zoom scaled by how far the fingers moved apart. */
|
|
155
|
+
declare function pinchZoom(startZoom: number, startDistance: number, distance: number): number;
|
|
156
|
+
/**
|
|
157
|
+
* New scroll offset that keeps the content point under `focal` (a viewport coordinate) still while
|
|
158
|
+
* the zoom changes from `oldZoom` to `newZoom`.
|
|
159
|
+
*/
|
|
160
|
+
declare function anchoredScroll(scroll: number, focal: number, oldZoom: number, newZoom: number): number;
|
|
161
|
+
/**
|
|
162
|
+
* Device pixel ratio to rasterise a page of `cssWidth` x `cssHeight` at: the device's own, capped,
|
|
163
|
+
* and lowered further when the page would otherwise exceed the canvas pixel budget.
|
|
164
|
+
*/
|
|
165
|
+
declare function renderDpr(cssWidth: number, cssHeight: number, devicePixelRatio: number, maxPixels?: number): number;
|
|
166
|
+
interface PinchZoomOptions {
|
|
167
|
+
containerRef: RefObject<HTMLElement | null>;
|
|
168
|
+
zoom: number;
|
|
169
|
+
onZoomChange?: (zoom: number) => void;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Pinch and double-tap zoom on a scroller, plus focal-point preservation for EVERY zoom change
|
|
173
|
+
* (gesture, buttons, props): after the new zoom has laid out, the scroll offset is moved so the
|
|
174
|
+
* content under the pinch centre (or the viewport centre) stays where it was.
|
|
175
|
+
*
|
|
176
|
+
* The scroller must carry `touch-action: pan-x pan-y` so the browser leaves two-finger gestures
|
|
177
|
+
* to this hook instead of zooming the whole app.
|
|
178
|
+
*/
|
|
179
|
+
declare function usePinchZoom({ containerRef, zoom, onZoomChange }: PinchZoomOptions): void;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Scroll stability for page-stacked viewers (PDF + parsed/HTML).
|
|
183
|
+
*
|
|
184
|
+
* Pages are lazy: their height can change after the user is already looking at something further
|
|
185
|
+
* down (a placeholder replaced by the real page, a late canvas). Browsers that implement scroll
|
|
186
|
+
* anchoring (Chromium) hide that; Safari/WebKit does not, so the content visibly jumps under the
|
|
187
|
+
* finger and momentum scrolling is cut short. The viewers therefore do their own anchoring, the
|
|
188
|
+
* same way on every engine: native anchoring is switched off and a page that changes height
|
|
189
|
+
* entirely ABOVE the viewport shifts `scrollTop` by exactly that change.
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
/** One page of a stacked viewer (both viewers mark pages like this). */
|
|
193
|
+
declare const PAGE_SELECTOR = "[role=\"article\"][data-page-number]";
|
|
194
|
+
/**
|
|
195
|
+
* How far to move `scrollTop` after a page changed height from `previousHeight` to `nextHeight`.
|
|
196
|
+
* Only a page that lay completely above the visible top edge (`containerTop`) pushes the content
|
|
197
|
+
* the user is reading; anything at or below it just grows or shrinks away from the viewport.
|
|
198
|
+
*
|
|
199
|
+
* @param pageTop viewport-relative top of the page (unchanged by its own resize)
|
|
200
|
+
*/
|
|
201
|
+
declare function anchorDelta(previousHeight: number, nextHeight: number, pageTop: number, containerTop: number): number;
|
|
202
|
+
/**
|
|
203
|
+
* Keep the reading position when pages above it change height. `key` re-scans for pages when the
|
|
204
|
+
* page set changes (page count, first layout).
|
|
205
|
+
*/
|
|
206
|
+
declare function useScrollAnchor(containerRef: RefObject<HTMLElement | null>, key: unknown): void;
|
|
207
|
+
|
|
132
208
|
interface UsePageIntersectionObserverOptions {
|
|
133
209
|
/** Container element that scrolls */
|
|
134
210
|
containerRef: RefObject<HTMLElement | null>;
|
|
@@ -144,12 +220,22 @@ interface UsePageIntersectionObserverOptions {
|
|
|
144
220
|
onPageChange: (pageNumber: number) => void;
|
|
145
221
|
/** Optional callback with visibility data for custom logic (e.g., lazy loading) */
|
|
146
222
|
onVisibilityUpdate?: (visibilityMap: Map<number, number>) => void;
|
|
223
|
+
/**
|
|
224
|
+
* Re-subscribe when this changes (the set of page elements: page count, first layout). Callers
|
|
225
|
+
* may pass fresh inline callbacks every render: they are read through refs and never cause a
|
|
226
|
+
* re-subscribe (a re-subscribe re-delivers every page and, with a state-setting callback, loops).
|
|
227
|
+
*/
|
|
228
|
+
observeKey?: unknown;
|
|
147
229
|
}
|
|
148
230
|
/**
|
|
149
|
-
* Custom hook to track visible
|
|
231
|
+
* Custom hook to track the visible page of a page-stacked viewer.
|
|
150
232
|
* Used for bidirectional page synchronization in document viewers.
|
|
233
|
+
*
|
|
234
|
+
* IntersectionObserver wakes it when pages enter or leave the viewport; a passive scroll listener
|
|
235
|
+
* (at most one measurement per frame) keeps it current in between. The page itself is chosen from
|
|
236
|
+
* live geometry (see `measureVisiblePages`).
|
|
151
237
|
*/
|
|
152
|
-
declare function usePageIntersectionObserver({ containerRef, scrollCompleteRef, isEnabled, getPageElements, getPageNumber, onPageChange, onVisibilityUpdate, }: UsePageIntersectionObserverOptions): void;
|
|
238
|
+
declare function usePageIntersectionObserver({ containerRef, scrollCompleteRef, isEnabled, getPageElements, getPageNumber, onPageChange, onVisibilityUpdate, observeKey, }: UsePageIntersectionObserverOptions): void;
|
|
153
239
|
|
|
154
240
|
/** Pages to render before the visible area (canvas pre-load buffer) */
|
|
155
241
|
declare const LAZY_LOAD_BUFFER_BEFORE = 2;
|
|
@@ -165,4 +251,4 @@ declare const INITIAL_LAZY_LOAD_BUFFER: {
|
|
|
165
251
|
after: number;
|
|
166
252
|
};
|
|
167
253
|
|
|
168
|
-
export { AudioViewer, type AudioViewerProps, DocumentViewer, type DocumentViewerProps, DocxViewer, type DocxViewerProps, INITIAL_LAZY_LOAD_BUFFER, ImageViewer, type ImageViewerProps, LAZY_LOAD_BUFFER_AFTER, LAZY_LOAD_BUFFER_BEFORE, type NativeViewerKind, PdfViewer, type PdfViewerAssets, type PdfViewerProps, VideoViewer, type VideoViewerProps, XlsxViewer, type XlsxViewerProps, nativeViewerKind, usePageIntersectionObserver };
|
|
254
|
+
export { AudioViewer, type AudioViewerProps, DocumentViewer, type DocumentViewerProps, DocxViewer, type DocxViewerProps, INITIAL_LAZY_LOAD_BUFFER, ImageViewer, type ImageViewerProps, LAZY_LOAD_BUFFER_AFTER, LAZY_LOAD_BUFFER_BEFORE, MAX_ZOOM, MIN_ZOOM, type NativeViewerKind, PAGE_SELECTOR, PdfViewer, type PdfViewerAssets, type PdfViewerProps, VideoViewer, type VideoViewerProps, XlsxViewer, type XlsxViewerProps, anchorDelta, anchoredScroll, clampZoom, doubleTapZoom, nativeViewerKind, pinchZoom, renderDpr, stepZoom, usePageIntersectionObserver, usePinchZoom, useScrollAnchor, zoomLabel };
|
package/dist/viewers/index.js
CHANGED
|
@@ -1,11 +1,64 @@
|
|
|
1
1
|
import { useArbi, VideoViewer, AudioViewer, ImageViewer } from '../chunk-JKO3NR5L.js';
|
|
2
2
|
export { AudioViewer, ImageViewer, VideoViewer } from '../chunk-JKO3NR5L.js';
|
|
3
|
-
import { useEffect,
|
|
3
|
+
import { useEffect, useRef, useCallback, useLayoutEffect, useState, useMemo } from 'react';
|
|
4
4
|
import { Document, Page, pdfjs } from 'react-pdf';
|
|
5
5
|
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
|
6
6
|
import 'react-pdf/dist/Page/TextLayer.css';
|
|
7
7
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
8
8
|
|
|
9
|
+
var PAGE_SELECTOR = '[role="article"][data-page-number]';
|
|
10
|
+
function anchorDelta(previousHeight, nextHeight, pageTop, containerTop) {
|
|
11
|
+
const delta = nextHeight - previousHeight;
|
|
12
|
+
if (delta === 0) return 0;
|
|
13
|
+
return pageTop + previousHeight <= containerTop + 1 ? delta : 0;
|
|
14
|
+
}
|
|
15
|
+
function measureVisiblePages(pages, viewTop, viewBottom) {
|
|
16
|
+
const visibility = /* @__PURE__ */ new Map();
|
|
17
|
+
let page = null;
|
|
18
|
+
let bestPx = 0;
|
|
19
|
+
let score = 0;
|
|
20
|
+
const viewHeight = viewBottom - viewTop;
|
|
21
|
+
for (const p of pages) {
|
|
22
|
+
const height = p.bottom - p.top;
|
|
23
|
+
const px = Math.min(p.bottom, viewBottom) - Math.max(p.top, viewTop);
|
|
24
|
+
if (px <= 0 || height <= 0) continue;
|
|
25
|
+
visibility.set(p.n, px / height);
|
|
26
|
+
if (px > bestPx) {
|
|
27
|
+
bestPx = px;
|
|
28
|
+
page = p.n;
|
|
29
|
+
score = px / Math.min(height, viewHeight);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { page, score, visibility };
|
|
33
|
+
}
|
|
34
|
+
function useScrollAnchor(containerRef, key) {
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const container = containerRef.current;
|
|
37
|
+
if (!container || typeof ResizeObserver === "undefined") return;
|
|
38
|
+
container.style.overflowAnchor = "none";
|
|
39
|
+
const heights = /* @__PURE__ */ new Map();
|
|
40
|
+
const observer = new ResizeObserver((entries) => {
|
|
41
|
+
let shift = 0;
|
|
42
|
+
const containerTop = container.getBoundingClientRect().top;
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const el = entry.target;
|
|
45
|
+
const height = el.getBoundingClientRect().height;
|
|
46
|
+
const previous = heights.get(el);
|
|
47
|
+
heights.set(el, height);
|
|
48
|
+
if (previous === void 0) continue;
|
|
49
|
+
shift += anchorDelta(previous, height, el.getBoundingClientRect().top, containerTop);
|
|
50
|
+
}
|
|
51
|
+
if (shift !== 0 && !container.dataset.zooming) container.scrollTop += shift;
|
|
52
|
+
});
|
|
53
|
+
container.querySelectorAll(PAGE_SELECTOR).forEach((el) => {
|
|
54
|
+
heights.set(el, el.getBoundingClientRect().height);
|
|
55
|
+
observer.observe(el);
|
|
56
|
+
});
|
|
57
|
+
return () => observer.disconnect();
|
|
58
|
+
}, [containerRef, key]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/viewers/usePageIntersectionObserver.ts
|
|
9
62
|
var PAGE_UPDATE_DEBOUNCE_MS = 10;
|
|
10
63
|
var MIN_VISIBILITY_THRESHOLD = 0.15;
|
|
11
64
|
var INTERSECTION_THRESHOLDS = [0, 0.25, 0.5, 0.75, 1];
|
|
@@ -16,66 +69,194 @@ function usePageIntersectionObserver({
|
|
|
16
69
|
getPageElements,
|
|
17
70
|
getPageNumber,
|
|
18
71
|
onPageChange,
|
|
19
|
-
onVisibilityUpdate
|
|
72
|
+
onVisibilityUpdate,
|
|
73
|
+
observeKey
|
|
20
74
|
}) {
|
|
75
|
+
const latest = useRef({ getPageElements, getPageNumber, onPageChange, onVisibilityUpdate });
|
|
76
|
+
latest.current = { getPageElements, getPageNumber, onPageChange, onVisibilityUpdate };
|
|
21
77
|
useEffect(() => {
|
|
22
78
|
if (!isEnabled || !containerRef.current) return;
|
|
23
79
|
const container = containerRef.current;
|
|
24
|
-
const visibilityMap = /* @__PURE__ */ new Map();
|
|
25
80
|
let lastReportedPage = null;
|
|
26
81
|
let updateTimeout = null;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (scrollCompleteRef.current && mostVisiblePage !== lastReportedPage && maxVisibility > MIN_VISIBILITY_THRESHOLD) {
|
|
37
|
-
lastReportedPage = mostVisiblePage;
|
|
38
|
-
onPageChange(mostVisiblePage);
|
|
82
|
+
let frame = 0;
|
|
83
|
+
const evaluate = () => {
|
|
84
|
+
const root = container.getBoundingClientRect();
|
|
85
|
+
const pages = [];
|
|
86
|
+
for (const el of latest.current.getPageElements()) {
|
|
87
|
+
const n = latest.current.getPageNumber(el);
|
|
88
|
+
if (n <= 0) continue;
|
|
89
|
+
const r = el.getBoundingClientRect();
|
|
90
|
+
pages.push({ n, top: r.top, bottom: r.bottom });
|
|
39
91
|
}
|
|
40
|
-
|
|
41
|
-
|
|
92
|
+
const { page, score, visibility } = measureVisiblePages(pages, root.top, root.bottom);
|
|
93
|
+
if (scrollCompleteRef.current && page !== null && page !== lastReportedPage && score > MIN_VISIBILITY_THRESHOLD) {
|
|
94
|
+
lastReportedPage = page;
|
|
95
|
+
latest.current.onPageChange(page);
|
|
42
96
|
}
|
|
97
|
+
latest.current.onVisibilityUpdate?.(visibility);
|
|
43
98
|
};
|
|
44
99
|
const observer = new IntersectionObserver(
|
|
45
|
-
(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (pageNum > 0) {
|
|
49
|
-
visibilityMap.set(pageNum, entry.intersectionRatio);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
if (updateTimeout) {
|
|
53
|
-
clearTimeout(updateTimeout);
|
|
54
|
-
}
|
|
55
|
-
updateTimeout = setTimeout(updatePageNumber, PAGE_UPDATE_DEBOUNCE_MS);
|
|
100
|
+
() => {
|
|
101
|
+
if (updateTimeout) clearTimeout(updateTimeout);
|
|
102
|
+
updateTimeout = setTimeout(evaluate, PAGE_UPDATE_DEBOUNCE_MS);
|
|
56
103
|
},
|
|
57
|
-
{
|
|
58
|
-
root: container,
|
|
59
|
-
threshold: INTERSECTION_THRESHOLDS
|
|
60
|
-
}
|
|
104
|
+
{ root: container, threshold: INTERSECTION_THRESHOLDS }
|
|
61
105
|
);
|
|
62
|
-
const
|
|
63
|
-
|
|
106
|
+
const onScroll = () => {
|
|
107
|
+
if (!frame) {
|
|
108
|
+
frame = requestAnimationFrame(() => {
|
|
109
|
+
frame = 0;
|
|
110
|
+
evaluate();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
container.addEventListener("scroll", onScroll, { passive: true });
|
|
115
|
+
latest.current.getPageElements().forEach((element) => observer.observe(element));
|
|
64
116
|
return () => {
|
|
65
117
|
observer.disconnect();
|
|
66
|
-
|
|
67
|
-
|
|
118
|
+
container.removeEventListener("scroll", onScroll);
|
|
119
|
+
if (updateTimeout) clearTimeout(updateTimeout);
|
|
120
|
+
if (frame) cancelAnimationFrame(frame);
|
|
121
|
+
};
|
|
122
|
+
}, [containerRef, scrollCompleteRef, isEnabled, observeKey]);
|
|
123
|
+
}
|
|
124
|
+
var MIN_ZOOM = 0.5;
|
|
125
|
+
var MAX_ZOOM = 4;
|
|
126
|
+
var ZOOM_STEP = 1.25;
|
|
127
|
+
var DOUBLE_TAP_ZOOM = 2;
|
|
128
|
+
var MAX_CANVAS_PIXELS = 8e6;
|
|
129
|
+
var MAX_RENDER_DPR = 2;
|
|
130
|
+
function clampZoom(zoom) {
|
|
131
|
+
if (!Number.isFinite(zoom)) return 1;
|
|
132
|
+
return Math.round(Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom)) * 100) / 100;
|
|
133
|
+
}
|
|
134
|
+
function stepZoom(zoom, direction) {
|
|
135
|
+
return clampZoom(direction === 1 ? zoom * ZOOM_STEP : zoom / ZOOM_STEP);
|
|
136
|
+
}
|
|
137
|
+
function doubleTapZoom(zoom) {
|
|
138
|
+
return zoom > 1.05 ? 1 : DOUBLE_TAP_ZOOM;
|
|
139
|
+
}
|
|
140
|
+
function zoomLabel(zoom) {
|
|
141
|
+
return `${Math.round(zoom * 100)}%`;
|
|
142
|
+
}
|
|
143
|
+
function pinchZoom(startZoom, startDistance, distance2) {
|
|
144
|
+
if (startDistance <= 0) return startZoom;
|
|
145
|
+
return clampZoom(startZoom * distance2 / startDistance);
|
|
146
|
+
}
|
|
147
|
+
function anchoredScroll(scroll, focal, oldZoom, newZoom) {
|
|
148
|
+
if (oldZoom <= 0) return scroll;
|
|
149
|
+
return (scroll + focal) * newZoom / oldZoom - focal;
|
|
150
|
+
}
|
|
151
|
+
function renderDpr(cssWidth, cssHeight, devicePixelRatio, maxPixels = MAX_CANVAS_PIXELS) {
|
|
152
|
+
const wanted = Math.min(Math.max(devicePixelRatio || 1, 1), MAX_RENDER_DPR);
|
|
153
|
+
const area = cssWidth * cssHeight;
|
|
154
|
+
if (area <= 0) return wanted;
|
|
155
|
+
const fits = Math.sqrt(maxPixels / area);
|
|
156
|
+
return Math.max(1, Math.min(wanted, fits));
|
|
157
|
+
}
|
|
158
|
+
var distance = (a, b) => Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
|
159
|
+
var INTERACTIVE = 'button, a, input, textarea, select, [role="button"]';
|
|
160
|
+
var DOUBLE_TAP_MS = 300;
|
|
161
|
+
var DOUBLE_TAP_SLOP_PX = 30;
|
|
162
|
+
function usePinchZoom({ containerRef, zoom, onZoomChange }) {
|
|
163
|
+
const zoomRef = useRef(zoom);
|
|
164
|
+
const onChangeRef = useRef(onZoomChange);
|
|
165
|
+
onChangeRef.current = onZoomChange;
|
|
166
|
+
const focalRef = useRef(null);
|
|
167
|
+
const appliedZoomRef = useRef(zoom);
|
|
168
|
+
const request = useCallback((next, focal) => {
|
|
169
|
+
const clamped = clampZoom(next);
|
|
170
|
+
if (clamped === zoomRef.current) return;
|
|
171
|
+
focalRef.current = focal;
|
|
172
|
+
zoomRef.current = clamped;
|
|
173
|
+
onChangeRef.current?.(clamped);
|
|
174
|
+
}, []);
|
|
175
|
+
useLayoutEffect(() => {
|
|
176
|
+
zoomRef.current = zoom;
|
|
177
|
+
const container = containerRef.current;
|
|
178
|
+
const previous = appliedZoomRef.current;
|
|
179
|
+
appliedZoomRef.current = zoom;
|
|
180
|
+
if (!container || previous === zoom) return;
|
|
181
|
+
const rect = container.getBoundingClientRect();
|
|
182
|
+
const focal = focalRef.current ?? { x: rect.width / 2, y: rect.height / 2 };
|
|
183
|
+
focalRef.current = null;
|
|
184
|
+
container.dataset.zooming = "true";
|
|
185
|
+
container.scrollLeft = anchoredScroll(container.scrollLeft, focal.x, previous, zoom);
|
|
186
|
+
container.scrollTop = anchoredScroll(container.scrollTop, focal.y, previous, zoom);
|
|
187
|
+
requestAnimationFrame(
|
|
188
|
+
() => requestAnimationFrame(() => {
|
|
189
|
+
delete container.dataset.zooming;
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
}, [zoom, containerRef]);
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
const el = containerRef.current;
|
|
195
|
+
if (!el) return;
|
|
196
|
+
let pinch = null;
|
|
197
|
+
let lastTap = null;
|
|
198
|
+
let frame = 0;
|
|
199
|
+
let pending = null;
|
|
200
|
+
const focalOf = (a, b) => {
|
|
201
|
+
const rect = el.getBoundingClientRect();
|
|
202
|
+
return {
|
|
203
|
+
x: (a.clientX + b.clientX) / 2 - rect.left,
|
|
204
|
+
y: (a.clientY + b.clientY) / 2 - rect.top
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
const flush = () => {
|
|
208
|
+
frame = 0;
|
|
209
|
+
if (pending) request(pending.zoom, pending.focal);
|
|
210
|
+
pending = null;
|
|
211
|
+
};
|
|
212
|
+
const onStart = (e) => {
|
|
213
|
+
if (e.touches.length === 2) {
|
|
214
|
+
pinch = { startDistance: distance(e.touches[0], e.touches[1]), startZoom: zoomRef.current };
|
|
215
|
+
lastTap = null;
|
|
68
216
|
}
|
|
69
217
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
218
|
+
const onMove = (e) => {
|
|
219
|
+
if (!pinch || e.touches.length !== 2) return;
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
pending = {
|
|
222
|
+
zoom: pinchZoom(pinch.startZoom, pinch.startDistance, distance(e.touches[0], e.touches[1])),
|
|
223
|
+
focal: focalOf(e.touches[0], e.touches[1])
|
|
224
|
+
};
|
|
225
|
+
if (!frame) frame = requestAnimationFrame(flush);
|
|
226
|
+
};
|
|
227
|
+
const onEnd = (e) => {
|
|
228
|
+
if (pinch && e.touches.length < 2) {
|
|
229
|
+
pinch = null;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (e.touches.length !== 0 || e.changedTouches.length !== 1) return;
|
|
233
|
+
const t = e.changedTouches[0];
|
|
234
|
+
if (e.target?.closest?.(INTERACTIVE)) return;
|
|
235
|
+
const now = e.timeStamp;
|
|
236
|
+
if (lastTap && now - lastTap.time < DOUBLE_TAP_MS && Math.hypot(t.clientX - lastTap.x, t.clientY - lastTap.y) < DOUBLE_TAP_SLOP_PX) {
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
const rect = el.getBoundingClientRect();
|
|
239
|
+
request(doubleTapZoom(zoomRef.current), {
|
|
240
|
+
x: t.clientX - rect.left,
|
|
241
|
+
y: t.clientY - rect.top
|
|
242
|
+
});
|
|
243
|
+
lastTap = null;
|
|
244
|
+
} else {
|
|
245
|
+
lastTap = { time: now, x: t.clientX, y: t.clientY };
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
el.addEventListener("touchstart", onStart, { passive: true });
|
|
249
|
+
el.addEventListener("touchmove", onMove, { passive: false });
|
|
250
|
+
el.addEventListener("touchend", onEnd, { passive: false });
|
|
251
|
+
el.addEventListener("touchcancel", onEnd, { passive: false });
|
|
252
|
+
return () => {
|
|
253
|
+
el.removeEventListener("touchstart", onStart);
|
|
254
|
+
el.removeEventListener("touchmove", onMove);
|
|
255
|
+
el.removeEventListener("touchend", onEnd);
|
|
256
|
+
el.removeEventListener("touchcancel", onEnd);
|
|
257
|
+
if (frame) cancelAnimationFrame(frame);
|
|
258
|
+
};
|
|
259
|
+
}, [containerRef, request]);
|
|
79
260
|
}
|
|
80
261
|
|
|
81
262
|
// src/viewers/pdf-viewer-constants.ts
|
|
@@ -93,14 +274,28 @@ function ensurePdfWorker(workerSrc) {
|
|
|
93
274
|
pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
|
|
94
275
|
}
|
|
95
276
|
}
|
|
96
|
-
var CONTAINER_PADDING_PX =
|
|
97
|
-
var
|
|
277
|
+
var CONTAINER_PADDING_PX = 34;
|
|
278
|
+
var DEFAULT_PAGE_ASPECT_RATIO = 1.414;
|
|
279
|
+
var PAGE_BORDER_PX = 2;
|
|
280
|
+
var RERENDER_DEBOUNCE_MS = 250;
|
|
281
|
+
var ZOOMED_BUFFER_THRESHOLD = 1.5;
|
|
282
|
+
function sameSet(a, b) {
|
|
283
|
+
if (a.size !== b.size) return false;
|
|
284
|
+
for (const v of a) if (!b.has(v)) return false;
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
98
287
|
var TEXT_LAYER_BUFFER = 1;
|
|
288
|
+
var ratioOf = (page) => {
|
|
289
|
+
const { width, height } = page.getViewport({ scale: 1 });
|
|
290
|
+
return width > 0 && height > 0 ? height / width : DEFAULT_PAGE_ASPECT_RATIO;
|
|
291
|
+
};
|
|
99
292
|
function PdfViewer({
|
|
100
293
|
file,
|
|
101
294
|
currentPage,
|
|
102
295
|
onLoadSuccess,
|
|
103
296
|
scrollTrigger,
|
|
297
|
+
zoom = 1,
|
|
298
|
+
onZoomChange,
|
|
104
299
|
onPageChange,
|
|
105
300
|
disableFontFace = false,
|
|
106
301
|
assets
|
|
@@ -112,6 +307,8 @@ function PdfViewer({
|
|
|
112
307
|
ensurePdfWorker(workerSrc);
|
|
113
308
|
const [numPages, setNumPages] = useState(0);
|
|
114
309
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
310
|
+
const [defaultRatio, setDefaultRatio] = useState(DEFAULT_PAGE_ASPECT_RATIO);
|
|
311
|
+
const [pageRatios, setPageRatios] = useState(/* @__PURE__ */ new Map());
|
|
115
312
|
const [loadError, setLoadError] = useState(null);
|
|
116
313
|
const [documentKey, setDocumentKey] = useState(() => `pdf-${Date.now()}`);
|
|
117
314
|
const initialPage = currentPage || 1;
|
|
@@ -131,7 +328,10 @@ function PdfViewer({
|
|
|
131
328
|
const containerRef = useRef(null);
|
|
132
329
|
const pageRefs = useRef(/* @__PURE__ */ new Map());
|
|
133
330
|
const onDocumentLoadSuccess = useCallback(
|
|
134
|
-
(
|
|
331
|
+
(pdf) => {
|
|
332
|
+
const n = pdf.numPages;
|
|
333
|
+
void pdf.getPage?.(1).then((page) => setDefaultRatio(ratioOf(page))).catch(() => {
|
|
334
|
+
});
|
|
135
335
|
setNumPages(n);
|
|
136
336
|
setLoadError(null);
|
|
137
337
|
const expand = () => {
|
|
@@ -210,45 +410,51 @@ function PdfViewer({
|
|
|
210
410
|
});
|
|
211
411
|
}
|
|
212
412
|
}, [numPages]);
|
|
413
|
+
const currentPageRef = useRef(currentPage);
|
|
414
|
+
currentPageRef.current = currentPage;
|
|
213
415
|
useEffect(() => {
|
|
214
|
-
|
|
416
|
+
const currentPage2 = currentPageRef.current;
|
|
417
|
+
if (!scrollTrigger || numPages === 0 || !currentPage2) return;
|
|
215
418
|
scrollCompleteRef.current = false;
|
|
216
419
|
setRenderedPages((prev) => {
|
|
217
420
|
const newSet = new Set(prev);
|
|
218
|
-
for (let i = Math.max(1,
|
|
421
|
+
for (let i = Math.max(1, currentPage2 - LAZY_LOAD_BUFFER_BEFORE); i <= Math.min(numPages, currentPage2 + LAZY_LOAD_BUFFER_AFTER); i++) {
|
|
219
422
|
newSet.add(i);
|
|
220
423
|
}
|
|
221
424
|
return newSet;
|
|
222
425
|
});
|
|
223
426
|
requestAnimationFrame(() => {
|
|
224
427
|
requestAnimationFrame(() => {
|
|
225
|
-
const pageElement = pageRefs.current.get(
|
|
428
|
+
const pageElement = pageRefs.current.get(currentPage2);
|
|
226
429
|
if (pageElement) {
|
|
227
430
|
pageElement.scrollIntoView({ behavior: "instant", block: "start" });
|
|
228
431
|
}
|
|
229
432
|
scrollCompleteRef.current = true;
|
|
230
433
|
});
|
|
231
434
|
});
|
|
232
|
-
}, [scrollTrigger,
|
|
435
|
+
}, [scrollTrigger, numPages]);
|
|
233
436
|
const handleVisibilityUpdate = useCallback(
|
|
234
437
|
(visibilityMap) => {
|
|
235
438
|
const visiblePageNums = Array.from(visibilityMap.entries()).filter(([, ratio]) => ratio > 0).map(([pageNum]) => pageNum);
|
|
236
439
|
if (visiblePageNums.length > 0) {
|
|
237
440
|
const minVisible = Math.min(...visiblePageNums);
|
|
238
441
|
const maxVisible = Math.max(...visiblePageNums);
|
|
442
|
+
const zoomed = zoom > ZOOMED_BUFFER_THRESHOLD;
|
|
443
|
+
const before = zoomed ? 1 : LAZY_LOAD_BUFFER_BEFORE;
|
|
444
|
+
const after = zoomed ? 2 : LAZY_LOAD_BUFFER_AFTER;
|
|
239
445
|
const pagesToRender = /* @__PURE__ */ new Set();
|
|
240
|
-
for (let i = Math.max(1, minVisible -
|
|
446
|
+
for (let i = Math.max(1, minVisible - before); i <= Math.min(numPages, maxVisible + after); i++) {
|
|
241
447
|
pagesToRender.add(i);
|
|
242
448
|
}
|
|
243
|
-
setRenderedPages(pagesToRender);
|
|
449
|
+
setRenderedPages((prev) => sameSet(prev, pagesToRender) ? prev : pagesToRender);
|
|
244
450
|
const nearViewport = /* @__PURE__ */ new Set();
|
|
245
451
|
for (let i = Math.max(1, minVisible - TEXT_LAYER_BUFFER); i <= Math.min(numPages, maxVisible + TEXT_LAYER_BUFFER); i++) {
|
|
246
452
|
nearViewport.add(i);
|
|
247
453
|
}
|
|
248
|
-
setVisiblePages(nearViewport);
|
|
454
|
+
setVisiblePages((prev) => sameSet(prev, nearViewport) ? prev : nearViewport);
|
|
249
455
|
}
|
|
250
456
|
},
|
|
251
|
-
[numPages]
|
|
457
|
+
[numPages, zoom]
|
|
252
458
|
);
|
|
253
459
|
const documentOptions = useMemo(
|
|
254
460
|
() => ({
|
|
@@ -271,19 +477,30 @@ function PdfViewer({
|
|
|
271
477
|
getPageElements: () => Array.from(pageRefs.current.values()),
|
|
272
478
|
getPageNumber: (element) => parseInt(element.getAttribute("data-page-number") ?? "0"),
|
|
273
479
|
onPageChange: handlePageChange,
|
|
274
|
-
onVisibilityUpdate: handleVisibilityUpdate
|
|
480
|
+
onVisibilityUpdate: handleVisibilityUpdate,
|
|
481
|
+
observeKey: `${numPages}:${containerWidth > 0}`
|
|
275
482
|
});
|
|
483
|
+
useScrollAnchor(containerRef, `${numPages}:${containerWidth > 0}`);
|
|
484
|
+
usePinchZoom({ containerRef, zoom, onZoomChange });
|
|
485
|
+
const [renderZoom, setRenderZoom] = useState(zoom);
|
|
486
|
+
useEffect(() => {
|
|
487
|
+
if (zoom === renderZoom) return;
|
|
488
|
+
const timer = setTimeout(() => setRenderZoom(zoom), RERENDER_DEBOUNCE_MS);
|
|
489
|
+
return () => clearTimeout(timer);
|
|
490
|
+
}, [zoom, renderZoom]);
|
|
276
491
|
return /* @__PURE__ */ jsx(
|
|
277
492
|
"div",
|
|
278
493
|
{
|
|
279
494
|
ref: containerRef,
|
|
280
|
-
className: "w-full h-full overflow-
|
|
495
|
+
className: "w-full h-full overflow-auto bg-muted flex flex-col items-start p-4 pb-20",
|
|
496
|
+
style: { touchAction: "pan-x pan-y" },
|
|
281
497
|
role: "region",
|
|
282
498
|
"aria-label": "PDF document viewer",
|
|
283
499
|
"data-testid": "pdf-viewer",
|
|
284
500
|
children: /* @__PURE__ */ jsx(
|
|
285
501
|
Document,
|
|
286
502
|
{
|
|
503
|
+
className: "flex w-max min-w-full flex-col",
|
|
287
504
|
file,
|
|
288
505
|
onLoadSuccess: onDocumentLoadSuccess,
|
|
289
506
|
onLoadError: onDocumentLoadError,
|
|
@@ -300,7 +517,11 @@ function PdfViewer({
|
|
|
300
517
|
children: containerWidth > 0 && Array.from(new Array(numPages), (_, index) => {
|
|
301
518
|
const pageNumber = index + 1;
|
|
302
519
|
const shouldRender = renderedPages.has(pageNumber);
|
|
303
|
-
const
|
|
520
|
+
const ratio = pageRatios.get(pageNumber) ?? defaultRatio;
|
|
521
|
+
const pageWidth = containerWidth * zoom;
|
|
522
|
+
const pageHeight = pageWidth * ratio;
|
|
523
|
+
const renderWidth = containerWidth * renderZoom;
|
|
524
|
+
const scale = zoom / renderZoom;
|
|
304
525
|
const shouldRenderTextLayer = visiblePages.has(pageNumber);
|
|
305
526
|
return /* @__PURE__ */ jsx(
|
|
306
527
|
"div",
|
|
@@ -313,28 +534,45 @@ function PdfViewer({
|
|
|
313
534
|
}
|
|
314
535
|
},
|
|
315
536
|
"data-page-number": pageNumber,
|
|
316
|
-
className: "mb-6 shadow-xl bg-white border border-border",
|
|
317
|
-
style:
|
|
537
|
+
className: "mx-auto mb-6 shrink-0 shadow-xl bg-white border border-border",
|
|
538
|
+
style: { width: pageWidth + PAGE_BORDER_PX, height: pageHeight + PAGE_BORDER_PX },
|
|
318
539
|
role: "article",
|
|
319
540
|
"aria-label": `Page ${pageNumber} of ${numPages}`,
|
|
320
541
|
children: shouldRender ? /* @__PURE__ */ jsx(
|
|
321
|
-
|
|
542
|
+
"div",
|
|
322
543
|
{
|
|
323
|
-
pageNumber
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
devicePixelRatio: Math.min(window.devicePixelRatio || 1, 2),
|
|
328
|
-
loading: /* @__PURE__ */ jsx(
|
|
329
|
-
"div",
|
|
544
|
+
"data-testid": `pdf-page-scaler-${pageNumber}`,
|
|
545
|
+
style: scale === 1 ? void 0 : { transform: `scale(${scale})`, transformOrigin: "0 0" },
|
|
546
|
+
children: /* @__PURE__ */ jsx(
|
|
547
|
+
Page,
|
|
330
548
|
{
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
549
|
+
pageNumber,
|
|
550
|
+
width: renderWidth,
|
|
551
|
+
renderTextLayer: shouldRenderTextLayer,
|
|
552
|
+
renderAnnotationLayer: false,
|
|
553
|
+
onLoadSuccess: (page) => {
|
|
554
|
+
const ratio2 = page.originalHeight / page.originalWidth;
|
|
555
|
+
setPageRatios(
|
|
556
|
+
(prev) => Math.abs((prev.get(pageNumber) ?? defaultRatio) - ratio2) < 1e-3 ? prev : new Map(prev).set(pageNumber, ratio2)
|
|
557
|
+
);
|
|
558
|
+
},
|
|
559
|
+
devicePixelRatio: renderDpr(
|
|
560
|
+
renderWidth,
|
|
561
|
+
renderWidth * ratio,
|
|
562
|
+
window.devicePixelRatio || 1
|
|
563
|
+
),
|
|
564
|
+
loading: /* @__PURE__ */ jsx(
|
|
565
|
+
"div",
|
|
566
|
+
{
|
|
567
|
+
className: "flex items-center justify-center bg-muted",
|
|
568
|
+
style: { width: renderWidth, height: renderWidth * ratio },
|
|
569
|
+
children: /* @__PURE__ */ jsxs("div", { className: "text-sm text-muted-foreground", children: [
|
|
570
|
+
"Loading page ",
|
|
571
|
+
pageNumber,
|
|
572
|
+
"..."
|
|
573
|
+
] })
|
|
574
|
+
}
|
|
575
|
+
)
|
|
338
576
|
}
|
|
339
577
|
)
|
|
340
578
|
}
|
|
@@ -342,7 +580,7 @@ function PdfViewer({
|
|
|
342
580
|
"div",
|
|
343
581
|
{
|
|
344
582
|
className: "flex items-center justify-center bg-muted",
|
|
345
|
-
style: { width:
|
|
583
|
+
style: { width: pageWidth, height: pageHeight },
|
|
346
584
|
children: /* @__PURE__ */ jsxs("div", { className: "text-sm text-muted-foreground", children: [
|
|
347
585
|
"Page ",
|
|
348
586
|
pageNumber
|
|
@@ -647,6 +885,6 @@ function DocumentViewer({
|
|
|
647
885
|
return className ? /* @__PURE__ */ jsx("div", { className, "data-testid": "document-viewer", children: body }) : body;
|
|
648
886
|
}
|
|
649
887
|
|
|
650
|
-
export { DocumentViewer, DocxViewer, INITIAL_LAZY_LOAD_BUFFER, LAZY_LOAD_BUFFER_AFTER, LAZY_LOAD_BUFFER_BEFORE, PdfViewer, XlsxViewer, nativeViewerKind, usePageIntersectionObserver };
|
|
888
|
+
export { DocumentViewer, DocxViewer, INITIAL_LAZY_LOAD_BUFFER, LAZY_LOAD_BUFFER_AFTER, LAZY_LOAD_BUFFER_BEFORE, MAX_ZOOM, MIN_ZOOM, PAGE_SELECTOR, PdfViewer, XlsxViewer, anchorDelta, anchoredScroll, clampZoom, doubleTapZoom, nativeViewerKind, pinchZoom, renderDpr, stepZoom, usePageIntersectionObserver, usePinchZoom, useScrollAnchor, zoomLabel };
|
|
651
889
|
//# sourceMappingURL=index.js.map
|
|
652
890
|
//# sourceMappingURL=index.js.map
|