@docmentis/udoc-viewer 0.5.2 → 0.5.3
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/package.json +1 -1
- package/dist/package.json +0 -56
- package/dist/src/UDocClient.d.ts +0 -383
- package/dist/src/UDocClient.d.ts.map +0 -1
- package/dist/src/UDocClient.js +0 -428
- package/dist/src/UDocClient.js.map +0 -1
- package/dist/src/UDocViewer.d.ts +0 -271
- package/dist/src/UDocViewer.d.ts.map +0 -1
- package/dist/src/UDocViewer.js +0 -769
- package/dist/src/UDocViewer.js.map +0 -1
- package/dist/src/fonts.d.ts +0 -29
- package/dist/src/fonts.d.ts.map +0 -1
- package/dist/src/fonts.js +0 -30
- package/dist/src/fonts.js.map +0 -1
- package/dist/src/index.d.ts +0 -9
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -8
- package/dist/src/index.js.map +0 -1
- package/dist/src/performance/PerformanceCounter.d.ts +0 -132
- package/dist/src/performance/PerformanceCounter.d.ts.map +0 -1
- package/dist/src/performance/PerformanceCounter.js +0 -129
- package/dist/src/performance/PerformanceCounter.js.map +0 -1
- package/dist/src/performance/index.d.ts +0 -2
- package/dist/src/performance/index.d.ts.map +0 -1
- package/dist/src/performance/index.js +0 -2
- package/dist/src/performance/index.js.map +0 -1
- package/dist/src/ui/framework/component.d.ts +0 -68
- package/dist/src/ui/framework/component.d.ts.map +0 -1
- package/dist/src/ui/framework/component.js +0 -87
- package/dist/src/ui/framework/component.js.map +0 -1
- package/dist/src/ui/framework/dom.d.ts +0 -19
- package/dist/src/ui/framework/dom.d.ts.map +0 -1
- package/dist/src/ui/framework/dom.js +0 -29
- package/dist/src/ui/framework/dom.js.map +0 -1
- package/dist/src/ui/framework/events.d.ts +0 -18
- package/dist/src/ui/framework/events.d.ts.map +0 -1
- package/dist/src/ui/framework/events.js +0 -23
- package/dist/src/ui/framework/events.js.map +0 -1
- package/dist/src/ui/framework/index.d.ts +0 -15
- package/dist/src/ui/framework/index.d.ts.map +0 -1
- package/dist/src/ui/framework/index.js +0 -15
- package/dist/src/ui/framework/index.js.map +0 -1
- package/dist/src/ui/framework/selectors.d.ts +0 -51
- package/dist/src/ui/framework/selectors.d.ts.map +0 -1
- package/dist/src/ui/framework/selectors.js +0 -30
- package/dist/src/ui/framework/selectors.js.map +0 -1
- package/dist/src/ui/framework/store.d.ts +0 -37
- package/dist/src/ui/framework/store.d.ts.map +0 -1
- package/dist/src/ui/framework/store.js +0 -54
- package/dist/src/ui/framework/store.js.map +0 -1
- package/dist/src/ui/viewer/actions.d.ts +0 -131
- package/dist/src/ui/viewer/actions.d.ts.map +0 -1
- package/dist/src/ui/viewer/actions.js +0 -2
- package/dist/src/ui/viewer/actions.js.map +0 -1
- package/dist/src/ui/viewer/annotation/LinkRenderer.d.ts +0 -9
- package/dist/src/ui/viewer/annotation/LinkRenderer.d.ts.map +0 -1
- package/dist/src/ui/viewer/annotation/LinkRenderer.js +0 -17
- package/dist/src/ui/viewer/annotation/LinkRenderer.js.map +0 -1
- package/dist/src/ui/viewer/annotation/MarkupRenderer.d.ts +0 -21
- package/dist/src/ui/viewer/annotation/MarkupRenderer.d.ts.map +0 -1
- package/dist/src/ui/viewer/annotation/MarkupRenderer.js +0 -138
- package/dist/src/ui/viewer/annotation/MarkupRenderer.js.map +0 -1
- package/dist/src/ui/viewer/annotation/ShapeRenderer.d.ts +0 -33
- package/dist/src/ui/viewer/annotation/ShapeRenderer.d.ts.map +0 -1
- package/dist/src/ui/viewer/annotation/ShapeRenderer.js +0 -378
- package/dist/src/ui/viewer/annotation/ShapeRenderer.js.map +0 -1
- package/dist/src/ui/viewer/annotation/TextRenderer.d.ts +0 -23
- package/dist/src/ui/viewer/annotation/TextRenderer.d.ts.map +0 -1
- package/dist/src/ui/viewer/annotation/TextRenderer.js +0 -196
- package/dist/src/ui/viewer/annotation/TextRenderer.js.map +0 -1
- package/dist/src/ui/viewer/annotation/index.d.ts +0 -8
- package/dist/src/ui/viewer/annotation/index.d.ts.map +0 -1
- package/dist/src/ui/viewer/annotation/index.js +0 -8
- package/dist/src/ui/viewer/annotation/index.js.map +0 -1
- package/dist/src/ui/viewer/annotation/render.d.ts +0 -24
- package/dist/src/ui/viewer/annotation/render.d.ts.map +0 -1
- package/dist/src/ui/viewer/annotation/render.js +0 -184
- package/dist/src/ui/viewer/annotation/render.js.map +0 -1
- package/dist/src/ui/viewer/annotation/types.d.ts +0 -239
- package/dist/src/ui/viewer/annotation/types.d.ts.map +0 -1
- package/dist/src/ui/viewer/annotation/types.js +0 -7
- package/dist/src/ui/viewer/annotation/types.js.map +0 -1
- package/dist/src/ui/viewer/annotation/utils.d.ts +0 -37
- package/dist/src/ui/viewer/annotation/utils.d.ts.map +0 -1
- package/dist/src/ui/viewer/annotation/utils.js +0 -82
- package/dist/src/ui/viewer/annotation/utils.js.map +0 -1
- package/dist/src/ui/viewer/components/AnnotationPanel.d.ts +0 -19
- package/dist/src/ui/viewer/components/AnnotationPanel.d.ts.map +0 -1
- package/dist/src/ui/viewer/components/AnnotationPanel.js +0 -284
- package/dist/src/ui/viewer/components/AnnotationPanel.js.map +0 -1
- package/dist/src/ui/viewer/components/FloatingToolbar.d.ts +0 -9
- package/dist/src/ui/viewer/components/FloatingToolbar.d.ts.map +0 -1
- package/dist/src/ui/viewer/components/FloatingToolbar.js +0 -305
- package/dist/src/ui/viewer/components/FloatingToolbar.js.map +0 -1
- package/dist/src/ui/viewer/components/LeftPanel.d.ts +0 -10
- package/dist/src/ui/viewer/components/LeftPanel.d.ts.map +0 -1
- package/dist/src/ui/viewer/components/LeftPanel.js +0 -165
- package/dist/src/ui/viewer/components/LeftPanel.js.map +0 -1
- package/dist/src/ui/viewer/components/LoadingOverlay.d.ts +0 -12
- package/dist/src/ui/viewer/components/LoadingOverlay.d.ts.map +0 -1
- package/dist/src/ui/viewer/components/LoadingOverlay.js +0 -88
- package/dist/src/ui/viewer/components/LoadingOverlay.js.map +0 -1
- package/dist/src/ui/viewer/components/OutlinePanel.d.ts +0 -10
- package/dist/src/ui/viewer/components/OutlinePanel.d.ts.map +0 -1
- package/dist/src/ui/viewer/components/OutlinePanel.js +0 -169
- package/dist/src/ui/viewer/components/OutlinePanel.js.map +0 -1
- package/dist/src/ui/viewer/components/PasswordDialog.d.ts +0 -15
- package/dist/src/ui/viewer/components/PasswordDialog.d.ts.map +0 -1
- package/dist/src/ui/viewer/components/PasswordDialog.js +0 -143
- package/dist/src/ui/viewer/components/PasswordDialog.js.map +0 -1
- package/dist/src/ui/viewer/components/RightPanel.d.ts +0 -9
- package/dist/src/ui/viewer/components/RightPanel.d.ts.map +0 -1
- package/dist/src/ui/viewer/components/RightPanel.js +0 -102
- package/dist/src/ui/viewer/components/RightPanel.js.map +0 -1
- package/dist/src/ui/viewer/components/Spread.d.ts +0 -43
- package/dist/src/ui/viewer/components/Spread.d.ts.map +0 -1
- package/dist/src/ui/viewer/components/Spread.js +0 -345
- package/dist/src/ui/viewer/components/Spread.js.map +0 -1
- package/dist/src/ui/viewer/components/ThumbnailPanel.d.ts +0 -11
- package/dist/src/ui/viewer/components/ThumbnailPanel.d.ts.map +0 -1
- package/dist/src/ui/viewer/components/ThumbnailPanel.js +0 -240
- package/dist/src/ui/viewer/components/ThumbnailPanel.js.map +0 -1
- package/dist/src/ui/viewer/components/Toolbar.d.ts +0 -9
- package/dist/src/ui/viewer/components/Toolbar.d.ts.map +0 -1
- package/dist/src/ui/viewer/components/Toolbar.js +0 -93
- package/dist/src/ui/viewer/components/Toolbar.js.map +0 -1
- package/dist/src/ui/viewer/components/ViewModeMenu.d.ts +0 -9
- package/dist/src/ui/viewer/components/ViewModeMenu.d.ts.map +0 -1
- package/dist/src/ui/viewer/components/ViewModeMenu.js +0 -169
- package/dist/src/ui/viewer/components/ViewModeMenu.js.map +0 -1
- package/dist/src/ui/viewer/components/Viewport.d.ts +0 -10
- package/dist/src/ui/viewer/components/Viewport.d.ts.map +0 -1
- package/dist/src/ui/viewer/components/Viewport.js +0 -1076
- package/dist/src/ui/viewer/components/Viewport.js.map +0 -1
- package/dist/src/ui/viewer/effects.d.ts +0 -8
- package/dist/src/ui/viewer/effects.d.ts.map +0 -1
- package/dist/src/ui/viewer/effects.js +0 -207
- package/dist/src/ui/viewer/effects.js.map +0 -1
- package/dist/src/ui/viewer/icons.d.ts +0 -32
- package/dist/src/ui/viewer/icons.d.ts.map +0 -1
- package/dist/src/ui/viewer/icons.js +0 -44
- package/dist/src/ui/viewer/icons.js.map +0 -1
- package/dist/src/ui/viewer/index.d.ts +0 -6
- package/dist/src/ui/viewer/index.d.ts.map +0 -1
- package/dist/src/ui/viewer/index.js +0 -6
- package/dist/src/ui/viewer/index.js.map +0 -1
- package/dist/src/ui/viewer/layout/index.d.ts +0 -3
- package/dist/src/ui/viewer/layout/index.d.ts.map +0 -1
- package/dist/src/ui/viewer/layout/index.js +0 -3
- package/dist/src/ui/viewer/layout/index.js.map +0 -1
- package/dist/src/ui/viewer/layout/pixelAlign.d.ts +0 -7
- package/dist/src/ui/viewer/layout/pixelAlign.d.ts.map +0 -1
- package/dist/src/ui/viewer/layout/pixelAlign.js +0 -22
- package/dist/src/ui/viewer/layout/pixelAlign.js.map +0 -1
- package/dist/src/ui/viewer/layout/spreadLayout.d.ts +0 -93
- package/dist/src/ui/viewer/layout/spreadLayout.d.ts.map +0 -1
- package/dist/src/ui/viewer/layout/spreadLayout.js +0 -315
- package/dist/src/ui/viewer/layout/spreadLayout.js.map +0 -1
- package/dist/src/ui/viewer/navigation.d.ts +0 -80
- package/dist/src/ui/viewer/navigation.d.ts.map +0 -1
- package/dist/src/ui/viewer/navigation.js +0 -59
- package/dist/src/ui/viewer/navigation.js.map +0 -1
- package/dist/src/ui/viewer/reducer.d.ts +0 -4
- package/dist/src/ui/viewer/reducer.d.ts.map +0 -1
- package/dist/src/ui/viewer/reducer.js +0 -305
- package/dist/src/ui/viewer/reducer.js.map +0 -1
- package/dist/src/ui/viewer/shell.d.ts +0 -34
- package/dist/src/ui/viewer/shell.d.ts.map +0 -1
- package/dist/src/ui/viewer/shell.js +0 -93
- package/dist/src/ui/viewer/shell.js.map +0 -1
- package/dist/src/ui/viewer/state.d.ts +0 -89
- package/dist/src/ui/viewer/state.d.ts.map +0 -1
- package/dist/src/ui/viewer/state.js +0 -55
- package/dist/src/ui/viewer/state.js.map +0 -1
- package/dist/src/ui/viewer/styles-inline.d.ts +0 -2
- package/dist/src/ui/viewer/styles-inline.d.ts.map +0 -1
- package/dist/src/ui/viewer/styles-inline.js +0 -1584
- package/dist/src/ui/viewer/styles-inline.js.map +0 -1
- package/dist/src/ui/viewer/text/index.d.ts +0 -7
- package/dist/src/ui/viewer/text/index.d.ts.map +0 -1
- package/dist/src/ui/viewer/text/index.js +0 -3
- package/dist/src/ui/viewer/text/index.js.map +0 -1
- package/dist/src/ui/viewer/text/render.d.ts +0 -19
- package/dist/src/ui/viewer/text/render.d.ts.map +0 -1
- package/dist/src/ui/viewer/text/render.js +0 -228
- package/dist/src/ui/viewer/text/render.js.map +0 -1
- package/dist/src/ui/viewer/text/selection.d.ts +0 -12
- package/dist/src/ui/viewer/text/selection.d.ts.map +0 -1
- package/dist/src/ui/viewer/text/selection.js +0 -70
- package/dist/src/ui/viewer/text/selection.js.map +0 -1
- package/dist/src/ui/viewer/text/types.d.ts +0 -37
- package/dist/src/ui/viewer/text/types.d.ts.map +0 -1
- package/dist/src/ui/viewer/text/types.js +0 -7
- package/dist/src/ui/viewer/text/types.js.map +0 -1
- package/dist/src/wasm/LICENSE +0 -104
- package/dist/src/wasm/udoc.d.ts +0 -480
- package/dist/src/wasm/udoc.js +0 -1738
- package/dist/src/wasm/udoc_bg.wasm +0 -0
- package/dist/src/wasm/udoc_bg.wasm.d.ts +0 -45
- package/dist/src/worker/WorkerClient.d.ts +0 -335
- package/dist/src/worker/WorkerClient.d.ts.map +0 -1
- package/dist/src/worker/WorkerClient.js +0 -903
- package/dist/src/worker/WorkerClient.js.map +0 -1
- package/dist/src/worker/index.d.ts +0 -4
- package/dist/src/worker/index.d.ts.map +0 -1
- package/dist/src/worker/index.js +0 -2
- package/dist/src/worker/index.js.map +0 -1
- package/dist/src/worker/worker.d.ts +0 -437
- package/dist/src/worker/worker.d.ts.map +0 -1
- package/dist/src/worker/worker.js +0 -237
- package/dist/src/worker/worker.js.map +0 -1
|
@@ -1,1076 +0,0 @@
|
|
|
1
|
-
import { subscribeSelector } from "../../framework/selectors";
|
|
2
|
-
import { getPointsToPixels } from "../state";
|
|
3
|
-
import { showAnnotationPopup, closeAnnotationPopup } from "../annotation";
|
|
4
|
-
import { calculateSpreads, calculateSpreadLayouts, findSpreadForPage, findVisibleSpreadRange, getSpreadPrimaryPage, getSpreadDimensions } from "../layout/spreadLayout";
|
|
5
|
-
import { createSpread } from "./Spread";
|
|
6
|
-
import { createFloatingToolbar } from "./FloatingToolbar";
|
|
7
|
-
import { on } from "../../framework/events";
|
|
8
|
-
import { getDevicePixelRatio, snapToDevice, toCssPixels, toDevicePixels } from "../layout";
|
|
9
|
-
function viewportSliceEqual(a, b) {
|
|
10
|
-
return (a.docId === b.docId &&
|
|
11
|
-
a.page === b.page &&
|
|
12
|
-
a.pageCount === b.pageCount &&
|
|
13
|
-
a.pageInfos === b.pageInfos &&
|
|
14
|
-
a.scrollMode === b.scrollMode &&
|
|
15
|
-
a.layoutMode === b.layoutMode &&
|
|
16
|
-
a.zoomMode === b.zoomMode &&
|
|
17
|
-
a.zoom === b.zoom &&
|
|
18
|
-
a.dpi === b.dpi &&
|
|
19
|
-
a.pageRotation === b.pageRotation &&
|
|
20
|
-
a.spacingMode === b.spacingMode &&
|
|
21
|
-
a.pageSpacing === b.pageSpacing &&
|
|
22
|
-
a.spreadSpacing === b.spreadSpacing &&
|
|
23
|
-
a.pageAnnotations === b.pageAnnotations &&
|
|
24
|
-
a.highlightedAnnotation === b.highlightedAnnotation &&
|
|
25
|
-
a.pageText === b.pageText);
|
|
26
|
-
}
|
|
27
|
-
let cachedScrollbarSize = null;
|
|
28
|
-
/** Number of spreads to render above/below viewport for smooth scrolling */
|
|
29
|
-
const RENDER_BUFFER = 2;
|
|
30
|
-
function parsePixel(value) {
|
|
31
|
-
const parsed = parseFloat(value);
|
|
32
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
33
|
-
}
|
|
34
|
-
function readInsets(style) {
|
|
35
|
-
return {
|
|
36
|
-
top: parsePixel(style.paddingTop),
|
|
37
|
-
right: parsePixel(style.paddingRight),
|
|
38
|
-
bottom: parsePixel(style.paddingBottom),
|
|
39
|
-
left: parsePixel(style.paddingLeft)
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
function addInsets(a, b) {
|
|
43
|
-
return {
|
|
44
|
-
top: a.top + b.top,
|
|
45
|
-
right: a.right + b.right,
|
|
46
|
-
bottom: a.bottom + b.bottom,
|
|
47
|
-
left: a.left + b.left
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
function readViewportMetrics(scrollArea, container) {
|
|
51
|
-
const scrollStyle = getComputedStyle(scrollArea);
|
|
52
|
-
const containerStyle = getComputedStyle(container);
|
|
53
|
-
const padding = addInsets(readInsets(scrollStyle), readInsets(containerStyle));
|
|
54
|
-
const width = scrollArea.clientWidth;
|
|
55
|
-
const height = scrollArea.clientHeight;
|
|
56
|
-
const innerWidth = Math.max(0, width - padding.left - padding.right);
|
|
57
|
-
const innerHeight = Math.max(0, height - padding.top - padding.bottom);
|
|
58
|
-
return {
|
|
59
|
-
width,
|
|
60
|
-
height,
|
|
61
|
-
innerWidth,
|
|
62
|
-
innerHeight,
|
|
63
|
-
padding
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
function metricsEqual(a, b) {
|
|
67
|
-
// Use epsilon tolerance to prevent oscillation from 1px fluctuations
|
|
68
|
-
const epsilon = 1;
|
|
69
|
-
return (Math.abs(a.width - b.width) <= epsilon &&
|
|
70
|
-
Math.abs(a.height - b.height) <= epsilon &&
|
|
71
|
-
Math.abs(a.innerWidth - b.innerWidth) <= epsilon &&
|
|
72
|
-
Math.abs(a.innerHeight - b.innerHeight) <= epsilon &&
|
|
73
|
-
a.padding.top === b.padding.top &&
|
|
74
|
-
a.padding.right === b.padding.right &&
|
|
75
|
-
a.padding.bottom === b.padding.bottom &&
|
|
76
|
-
a.padding.left === b.padding.left);
|
|
77
|
-
}
|
|
78
|
-
function resolveOverflowState(prev, delta, threshold) {
|
|
79
|
-
if (prev === null)
|
|
80
|
-
return delta > threshold;
|
|
81
|
-
if (prev)
|
|
82
|
-
return delta >= -threshold;
|
|
83
|
-
return delta > threshold;
|
|
84
|
-
}
|
|
85
|
-
function clamp(value, min, max) {
|
|
86
|
-
return Math.min(max, Math.max(min, value));
|
|
87
|
-
}
|
|
88
|
-
function getCenteredOffset(containerSize, contentSize) {
|
|
89
|
-
const dpr = getDevicePixelRatio();
|
|
90
|
-
const containerDevice = toDevicePixels(containerSize, dpr);
|
|
91
|
-
const contentDevice = toDevicePixels(contentSize, dpr);
|
|
92
|
-
const offsetDevice = Math.max(0, Math.floor((containerDevice - contentDevice) / 2));
|
|
93
|
-
return toCssPixels(offsetDevice, dpr);
|
|
94
|
-
}
|
|
95
|
-
function getScrollbarSize() {
|
|
96
|
-
if (cachedScrollbarSize)
|
|
97
|
-
return cachedScrollbarSize;
|
|
98
|
-
if (!document.body) {
|
|
99
|
-
cachedScrollbarSize = { width: 0, height: 0 };
|
|
100
|
-
return cachedScrollbarSize;
|
|
101
|
-
}
|
|
102
|
-
const probe = document.createElement("div");
|
|
103
|
-
probe.style.width = "100px";
|
|
104
|
-
probe.style.height = "100px";
|
|
105
|
-
probe.style.overflow = "scroll";
|
|
106
|
-
probe.style.position = "absolute";
|
|
107
|
-
probe.style.top = "-9999px";
|
|
108
|
-
document.body.appendChild(probe);
|
|
109
|
-
const width = probe.offsetWidth - probe.clientWidth;
|
|
110
|
-
const height = probe.offsetHeight - probe.clientHeight;
|
|
111
|
-
probe.remove();
|
|
112
|
-
cachedScrollbarSize = { width, height };
|
|
113
|
-
return cachedScrollbarSize;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Computes the zoom scale for fit modes.
|
|
117
|
-
*
|
|
118
|
-
* For fit-spread-width mode, we predict whether a vertical scrollbar will be needed
|
|
119
|
-
* and adjust the available width accordingly. This prevents the feedback loop where:
|
|
120
|
-
* 1. Scale is calculated based on viewport width
|
|
121
|
-
* 2. Content is taller than viewport, scrollbar appears
|
|
122
|
-
* 3. Scrollbar reduces viewport width, causing recalculation
|
|
123
|
-
*
|
|
124
|
-
* @param slice - Viewport state slice
|
|
125
|
-
* @param metrics - Current viewport metrics
|
|
126
|
-
* @param spreads - Array of spreads to layout
|
|
127
|
-
* @param scrollbarVisible - Whether vertical scrollbar is currently visible
|
|
128
|
-
*/
|
|
129
|
-
function computeScale(slice, metrics, spreads, scrollbarVisible) {
|
|
130
|
-
if (slice.zoomMode === "custom")
|
|
131
|
-
return slice.zoom;
|
|
132
|
-
if (metrics.innerWidth <= 0 || metrics.innerHeight <= 0)
|
|
133
|
-
return slice.zoom;
|
|
134
|
-
if (spreads.length === 0)
|
|
135
|
-
return slice.zoom;
|
|
136
|
-
let maxWidth = 0;
|
|
137
|
-
let maxHeight = 0;
|
|
138
|
-
for (const spread of spreads) {
|
|
139
|
-
const dims = getSpreadDimensions(spread, slice.pageInfos, 1, slice.pageSpacing, slice.dpi, slice.pageRotation);
|
|
140
|
-
maxWidth = Math.max(maxWidth, dims.width);
|
|
141
|
-
maxHeight = Math.max(maxHeight, dims.height);
|
|
142
|
-
}
|
|
143
|
-
if (maxWidth <= 0 || maxHeight <= 0)
|
|
144
|
-
return slice.zoom;
|
|
145
|
-
// Use snapped values to match layout calculations exactly.
|
|
146
|
-
// Round to the nearest device pixel to avoid scrollbar feedback loops.
|
|
147
|
-
const snappedSpacing = snapToDevice(slice.spreadSpacing);
|
|
148
|
-
const snappedViewportHeight = snapToDevice(metrics.innerHeight);
|
|
149
|
-
const scrollbar = getScrollbarSize();
|
|
150
|
-
// Calculate the "base" viewport width (without scrollbar).
|
|
151
|
-
// If scrollbar is currently visible, metrics.innerWidth already excludes it,
|
|
152
|
-
// so we add it back to get the full available width.
|
|
153
|
-
const baseViewportWidth = scrollbarVisible
|
|
154
|
-
? snapToDevice(metrics.innerWidth + scrollbar.width)
|
|
155
|
-
: snapToDevice(metrics.innerWidth);
|
|
156
|
-
const verticalSpacing = snappedSpacing * 2;
|
|
157
|
-
// Add 1 pixel buffer to compensate for device pixel rounding in layout calculations.
|
|
158
|
-
// Without this, the spread may be slightly smaller than the viewport, allowing
|
|
159
|
-
// a thin line of the next page to show at the bottom in continuous scroll mode.
|
|
160
|
-
// The same buffer is used in spread mode for consistency when switching modes.
|
|
161
|
-
// Scrollbar overflow in spread mode is handled by applySingleLayout using viewport height.
|
|
162
|
-
const targetHeight = Math.max(0, snappedViewportHeight - verticalSpacing + 1);
|
|
163
|
-
switch (slice.zoomMode) {
|
|
164
|
-
case "fit-spread-width": {
|
|
165
|
-
// Calculate scale assuming full viewport width (no scrollbar)
|
|
166
|
-
const fullWidthScale = baseViewportWidth / maxWidth;
|
|
167
|
-
// Predict if vertical scrollbar will be needed at this scale.
|
|
168
|
-
// For continuous mode: check if total content height exceeds viewport.
|
|
169
|
-
// For spread mode: check if tallest spread + spacing exceeds viewport.
|
|
170
|
-
let needsScrollbar;
|
|
171
|
-
if (slice.scrollMode === "continuous") {
|
|
172
|
-
// In continuous mode, calculate total content height
|
|
173
|
-
// Total height = sum of all spread heights + spacing between spreads
|
|
174
|
-
let totalHeight = 0;
|
|
175
|
-
for (const spread of spreads) {
|
|
176
|
-
const dims = getSpreadDimensions(spread, slice.pageInfos, fullWidthScale, slice.pageSpacing, slice.dpi, slice.pageRotation);
|
|
177
|
-
totalHeight += dims.height;
|
|
178
|
-
}
|
|
179
|
-
totalHeight += snappedSpacing * (spreads.length + 1); // spacing before first, between, and after last
|
|
180
|
-
needsScrollbar = totalHeight > snappedViewportHeight;
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
// In spread mode, check if the tallest spread + spacing exceeds viewport
|
|
184
|
-
const scaledMaxHeight = maxHeight * fullWidthScale;
|
|
185
|
-
needsScrollbar = scaledMaxHeight + verticalSpacing > snappedViewportHeight;
|
|
186
|
-
}
|
|
187
|
-
if (needsScrollbar && scrollbar.width > 0) {
|
|
188
|
-
// Scrollbar will be needed, calculate scale with reduced viewport width
|
|
189
|
-
const adjustedWidth = baseViewportWidth - scrollbar.width;
|
|
190
|
-
return adjustedWidth / maxWidth;
|
|
191
|
-
}
|
|
192
|
-
// No scrollbar needed, use full width
|
|
193
|
-
return fullWidthScale;
|
|
194
|
-
}
|
|
195
|
-
case "fit-spread-height":
|
|
196
|
-
// Content fits height by design, no vertical scrollbar needed
|
|
197
|
-
return targetHeight / maxHeight;
|
|
198
|
-
case "fit-spread":
|
|
199
|
-
// Content fits both dimensions by design, no scrollbar needed
|
|
200
|
-
return Math.min(baseViewportWidth / maxWidth, targetHeight / maxHeight);
|
|
201
|
-
default:
|
|
202
|
-
return slice.zoom;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
function buildLayout(slice, metrics, scrollbarVisible) {
|
|
206
|
-
const spreads = calculateSpreads(slice.pageCount, slice.layoutMode);
|
|
207
|
-
const scale = computeScale(slice, metrics, spreads, scrollbarVisible);
|
|
208
|
-
const layout = calculateSpreadLayouts(spreads, slice.pageInfos, scale, slice.pageSpacing, slice.spreadSpacing, slice.dpi, slice.pageRotation);
|
|
209
|
-
return {
|
|
210
|
-
spreads,
|
|
211
|
-
layouts: layout.layouts,
|
|
212
|
-
contentWidth: layout.contentWidth,
|
|
213
|
-
contentHeight: layout.contentHeight,
|
|
214
|
-
scale
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
function computeViewportUpdate(prevSlice, nextSlice, prevMetrics, nextMetrics) {
|
|
218
|
-
const metricsChanged = !prevMetrics || !metricsEqual(prevMetrics, nextMetrics);
|
|
219
|
-
const layoutChanged = !prevSlice ||
|
|
220
|
-
nextSlice.docId !== prevSlice.docId ||
|
|
221
|
-
nextSlice.pageCount !== prevSlice.pageCount ||
|
|
222
|
-
nextSlice.pageInfos !== prevSlice.pageInfos ||
|
|
223
|
-
nextSlice.scrollMode !== prevSlice.scrollMode ||
|
|
224
|
-
nextSlice.layoutMode !== prevSlice.layoutMode ||
|
|
225
|
-
nextSlice.zoomMode !== prevSlice.zoomMode ||
|
|
226
|
-
nextSlice.zoom !== prevSlice.zoom ||
|
|
227
|
-
nextSlice.dpi !== prevSlice.dpi ||
|
|
228
|
-
nextSlice.pageRotation !== prevSlice.pageRotation ||
|
|
229
|
-
nextSlice.pageSpacing !== prevSlice.pageSpacing ||
|
|
230
|
-
nextSlice.spreadSpacing !== prevSlice.spreadSpacing ||
|
|
231
|
-
metricsChanged;
|
|
232
|
-
const zoomModeChanged = !prevSlice || nextSlice.zoomMode !== prevSlice.zoomMode;
|
|
233
|
-
const spreadsChanged = !prevSlice ||
|
|
234
|
-
nextSlice.docId !== prevSlice.docId ||
|
|
235
|
-
nextSlice.pageCount !== prevSlice.pageCount ||
|
|
236
|
-
nextSlice.pageInfos !== prevSlice.pageInfos ||
|
|
237
|
-
nextSlice.layoutMode !== prevSlice.layoutMode;
|
|
238
|
-
const shouldClearSpreads = layoutChanged && spreadsChanged;
|
|
239
|
-
// Scroll to page on document change or zoom mode change (explicit user actions)
|
|
240
|
-
const shouldScrollToPage = spreadsChanged || zoomModeChanged;
|
|
241
|
-
// Restore tracked viewport position when layout changes but document structure is the same
|
|
242
|
-
// This maintains scroll position across mode switches, zoom changes, and viewport resizes
|
|
243
|
-
const shouldRestorePosition = layoutChanged && !spreadsChanged && !zoomModeChanged;
|
|
244
|
-
return {
|
|
245
|
-
layoutChanged,
|
|
246
|
-
shouldClearSpreads,
|
|
247
|
-
shouldRestorePosition,
|
|
248
|
-
shouldScrollToPage
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
export function createViewport() {
|
|
252
|
-
const el = document.createElement("div");
|
|
253
|
-
el.className = "udoc-viewport";
|
|
254
|
-
const scrollArea = document.createElement("div");
|
|
255
|
-
scrollArea.className = "udoc-viewport__scroll";
|
|
256
|
-
el.appendChild(scrollArea);
|
|
257
|
-
const container = document.createElement("div");
|
|
258
|
-
container.className = "udoc-viewport__container";
|
|
259
|
-
scrollArea.appendChild(container);
|
|
260
|
-
// Watermark with tamper protection - random class name on each instantiation
|
|
261
|
-
const wmClass = "_" + Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 10);
|
|
262
|
-
const wmHref = "https://docmentis.com";
|
|
263
|
-
const wmText = "Powered by docMentis";
|
|
264
|
-
const wmAttrs = { target: "_blank", rel: "noopener" };
|
|
265
|
-
// Inject dynamic styles for the random class name
|
|
266
|
-
const wmStyle = document.createElement("style");
|
|
267
|
-
wmStyle.textContent = `
|
|
268
|
-
.${wmClass} {
|
|
269
|
-
position: absolute;
|
|
270
|
-
right: 18px;
|
|
271
|
-
bottom: 4px;
|
|
272
|
-
padding: 2px 6px;
|
|
273
|
-
font-size: 12px;
|
|
274
|
-
font-weight: 500;
|
|
275
|
-
color: rgba(0, 0, 0, 0.3);
|
|
276
|
-
text-decoration: none;
|
|
277
|
-
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.5);
|
|
278
|
-
z-index: 10;
|
|
279
|
-
transition: color 0.15s ease;
|
|
280
|
-
}
|
|
281
|
-
.${wmClass}:hover {
|
|
282
|
-
color: rgba(0, 0, 0, 0.6);
|
|
283
|
-
}
|
|
284
|
-
@media (max-width: 768px) {
|
|
285
|
-
.${wmClass} {
|
|
286
|
-
bottom: 48px;
|
|
287
|
-
right: 10px;
|
|
288
|
-
font-size: 11px;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
`;
|
|
292
|
-
el.appendChild(wmStyle);
|
|
293
|
-
function createWatermark() {
|
|
294
|
-
const wm = document.createElement("a");
|
|
295
|
-
wm.className = wmClass;
|
|
296
|
-
wm.href = wmHref;
|
|
297
|
-
wm.target = wmAttrs.target;
|
|
298
|
-
wm.rel = wmAttrs.rel;
|
|
299
|
-
wm.textContent = wmText;
|
|
300
|
-
return wm;
|
|
301
|
-
}
|
|
302
|
-
let watermark = createWatermark();
|
|
303
|
-
el.appendChild(watermark);
|
|
304
|
-
// Protect watermark against removal and modification
|
|
305
|
-
const wmObserver = new MutationObserver((mutations) => {
|
|
306
|
-
let needsRestore = false;
|
|
307
|
-
// Check if watermark was removed from DOM
|
|
308
|
-
if (!el.contains(watermark)) {
|
|
309
|
-
needsRestore = true;
|
|
310
|
-
}
|
|
311
|
-
// Check if style element was removed
|
|
312
|
-
if (!el.contains(wmStyle)) {
|
|
313
|
-
el.appendChild(wmStyle);
|
|
314
|
-
}
|
|
315
|
-
// Check for attribute tampering on the watermark itself
|
|
316
|
-
for (const mutation of mutations) {
|
|
317
|
-
if (mutation.target === watermark) {
|
|
318
|
-
if (mutation.type === "attributes") {
|
|
319
|
-
needsRestore = true;
|
|
320
|
-
}
|
|
321
|
-
else if (mutation.type === "characterData" || mutation.type === "childList") {
|
|
322
|
-
needsRestore = true;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
// Check if watermark's text content was changed
|
|
326
|
-
if (mutation.target.parentNode === watermark && mutation.type === "characterData") {
|
|
327
|
-
needsRestore = true;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
if (needsRestore) {
|
|
331
|
-
// Remove old watermark if still in DOM but corrupted
|
|
332
|
-
if (el.contains(watermark)) {
|
|
333
|
-
watermark.remove();
|
|
334
|
-
}
|
|
335
|
-
// Create fresh watermark
|
|
336
|
-
watermark = createWatermark();
|
|
337
|
-
el.appendChild(watermark);
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
// Observe the parent for child removal and the watermark for attribute/content changes
|
|
341
|
-
wmObserver.observe(el, { childList: true, subtree: false });
|
|
342
|
-
wmObserver.observe(watermark, {
|
|
343
|
-
attributes: true,
|
|
344
|
-
childList: true,
|
|
345
|
-
characterData: true,
|
|
346
|
-
subtree: true
|
|
347
|
-
});
|
|
348
|
-
// Periodic integrity check (catches CSS-based hiding)
|
|
349
|
-
const wmIntegrityCheck = setInterval(() => {
|
|
350
|
-
// Restore style element if removed
|
|
351
|
-
if (!el.contains(wmStyle)) {
|
|
352
|
-
el.appendChild(wmStyle);
|
|
353
|
-
}
|
|
354
|
-
// Restore watermark if removed
|
|
355
|
-
if (!el.contains(watermark)) {
|
|
356
|
-
watermark = createWatermark();
|
|
357
|
-
el.appendChild(watermark);
|
|
358
|
-
wmObserver.observe(watermark, {
|
|
359
|
-
attributes: true,
|
|
360
|
-
childList: true,
|
|
361
|
-
characterData: true,
|
|
362
|
-
subtree: true
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
// Reset any inline style tampering
|
|
366
|
-
watermark.style.cssText = "";
|
|
367
|
-
watermark.removeAttribute("hidden");
|
|
368
|
-
if (watermark.className !== wmClass) {
|
|
369
|
-
watermark.className = wmClass;
|
|
370
|
-
}
|
|
371
|
-
}, 1000);
|
|
372
|
-
const floatingToolbar = createFloatingToolbar();
|
|
373
|
-
let workerClient = null;
|
|
374
|
-
let storeRef = null;
|
|
375
|
-
let unsubRender = null;
|
|
376
|
-
let unsubScroll = null;
|
|
377
|
-
let unsubNavigation = null;
|
|
378
|
-
let resizeObserver = null;
|
|
379
|
-
let currentSlice = null;
|
|
380
|
-
let lastSlice = null;
|
|
381
|
-
let lastMetrics = null;
|
|
382
|
-
let layoutState = null;
|
|
383
|
-
let spreadComponents = new Map();
|
|
384
|
-
let layoutDirty = false;
|
|
385
|
-
let lastVisibleRange = { start: 0, end: -1 };
|
|
386
|
-
let containerSize = { width: 0, height: 0 };
|
|
387
|
-
let lastOverflowX = null;
|
|
388
|
-
let lastOverflowY = null;
|
|
389
|
-
// Track the document position at viewport top edge for consistent positioning across mode switches
|
|
390
|
-
let viewportTopPosition = null;
|
|
391
|
-
let updateRaf = 0;
|
|
392
|
-
let scrollRaf = 0;
|
|
393
|
-
let renderDebounceTimer = 0;
|
|
394
|
-
let rendersPaused = false;
|
|
395
|
-
let resumeRenderAfterResize = false;
|
|
396
|
-
const RENDER_DEBOUNCE_MS = 50;
|
|
397
|
-
const unsubEvents = [];
|
|
398
|
-
function mount(parent, store, wc) {
|
|
399
|
-
parent.appendChild(el);
|
|
400
|
-
workerClient = wc;
|
|
401
|
-
storeRef = store;
|
|
402
|
-
floatingToolbar.mount(el, store);
|
|
403
|
-
currentSlice = selectViewport(store.getState());
|
|
404
|
-
scheduleUpdate();
|
|
405
|
-
unsubRender = subscribeSelector(store, selectViewport, (slice) => {
|
|
406
|
-
currentSlice = slice;
|
|
407
|
-
scheduleUpdate();
|
|
408
|
-
}, {
|
|
409
|
-
equality: viewportSliceEqual
|
|
410
|
-
});
|
|
411
|
-
unsubScroll = on(scrollArea, "scroll", () => {
|
|
412
|
-
if (!currentSlice || !layoutState)
|
|
413
|
-
return;
|
|
414
|
-
if (scrollRaf)
|
|
415
|
-
return;
|
|
416
|
-
scrollRaf = requestAnimationFrame(() => {
|
|
417
|
-
scrollRaf = 0;
|
|
418
|
-
if (!currentSlice || !layoutState || !lastMetrics)
|
|
419
|
-
return;
|
|
420
|
-
if (currentSlice.scrollMode === "continuous") {
|
|
421
|
-
updateVisibleSpreads(currentSlice, lastMetrics, layoutState);
|
|
422
|
-
}
|
|
423
|
-
// Keep viewport position tracking up to date on scroll (both modes)
|
|
424
|
-
updateViewportTopPosition(currentSlice, layoutState);
|
|
425
|
-
});
|
|
426
|
-
});
|
|
427
|
-
// Resize handler: layout updates immediately, but renders are only paused
|
|
428
|
-
// when resize events keep firing (e.g. dragging panel width). This avoids
|
|
429
|
-
// a one-off "flash" when panels are simply toggled.
|
|
430
|
-
const handleResize = () => {
|
|
431
|
-
const isOngoingResize = renderDebounceTimer !== 0;
|
|
432
|
-
if (isOngoingResize) {
|
|
433
|
-
rendersPaused = true;
|
|
434
|
-
resumeRenderAfterResize = true;
|
|
435
|
-
}
|
|
436
|
-
// Clear any pending render timer
|
|
437
|
-
if (renderDebounceTimer) {
|
|
438
|
-
clearTimeout(renderDebounceTimer);
|
|
439
|
-
}
|
|
440
|
-
// Schedule render after resize settles
|
|
441
|
-
renderDebounceTimer = window.setTimeout(() => {
|
|
442
|
-
renderDebounceTimer = 0;
|
|
443
|
-
if (resumeRenderAfterResize) {
|
|
444
|
-
resumeRenderAfterResize = false;
|
|
445
|
-
rendersPaused = false;
|
|
446
|
-
// Trigger re-render of visible spreads
|
|
447
|
-
scheduleUpdate();
|
|
448
|
-
}
|
|
449
|
-
}, RENDER_DEBOUNCE_MS);
|
|
450
|
-
// Layout updates immediately
|
|
451
|
-
scheduleUpdate();
|
|
452
|
-
};
|
|
453
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
454
|
-
resizeObserver = new ResizeObserver(handleResize);
|
|
455
|
-
resizeObserver.observe(scrollArea);
|
|
456
|
-
}
|
|
457
|
-
window.addEventListener("resize", handleResize);
|
|
458
|
-
unsubEvents.push(() => window.removeEventListener("resize", handleResize));
|
|
459
|
-
// Handle annotation clicks (links and sticky notes)
|
|
460
|
-
const handleAnnotationClick = (e) => {
|
|
461
|
-
const target = e.target;
|
|
462
|
-
// Handle sticky note (text) annotation - show popup
|
|
463
|
-
const textEl = target.closest(".udoc-annotation--text");
|
|
464
|
-
if (textEl) {
|
|
465
|
-
e.stopPropagation();
|
|
466
|
-
const annotationData = textEl.dataset.annotation;
|
|
467
|
-
if (annotationData) {
|
|
468
|
-
try {
|
|
469
|
-
const annotation = JSON.parse(annotationData);
|
|
470
|
-
showAnnotationPopup(annotation, textEl, container);
|
|
471
|
-
}
|
|
472
|
-
catch {
|
|
473
|
-
// Ignore parse errors
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
// Handle link annotation - navigate
|
|
479
|
-
const linkEl = target.closest(".udoc-annotation--link");
|
|
480
|
-
if (linkEl) {
|
|
481
|
-
const actionData = linkEl.getAttribute("data-action");
|
|
482
|
-
if (!actionData)
|
|
483
|
-
return;
|
|
484
|
-
try {
|
|
485
|
-
const action = JSON.parse(actionData);
|
|
486
|
-
if (action.actionType === "goTo" && action.destination) {
|
|
487
|
-
store.dispatch({ type: "NAVIGATE_TO_DESTINATION", destination: action.destination });
|
|
488
|
-
}
|
|
489
|
-
else if (action.actionType === "uri" && action.uri) {
|
|
490
|
-
window.open(action.uri, "_blank", "noopener");
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
catch {
|
|
494
|
-
// Ignore parse errors
|
|
495
|
-
}
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
// Click elsewhere closes any open popup
|
|
499
|
-
closeAnnotationPopup();
|
|
500
|
-
};
|
|
501
|
-
container.addEventListener("click", handleAnnotationClick);
|
|
502
|
-
unsubEvents.push(() => container.removeEventListener("click", handleAnnotationClick));
|
|
503
|
-
// Handle mouse wheel for page flip in spread mode
|
|
504
|
-
let wheelCooldown = false;
|
|
505
|
-
const handleWheel = (e) => {
|
|
506
|
-
if (!currentSlice || currentSlice.scrollMode !== "spread")
|
|
507
|
-
return;
|
|
508
|
-
if (!layoutState || layoutState.spreads.length === 0)
|
|
509
|
-
return;
|
|
510
|
-
// Prevent default scroll in spread mode
|
|
511
|
-
e.preventDefault();
|
|
512
|
-
// Debounce rapid wheel events
|
|
513
|
-
if (wheelCooldown)
|
|
514
|
-
return;
|
|
515
|
-
wheelCooldown = true;
|
|
516
|
-
setTimeout(() => { wheelCooldown = false; }, 150);
|
|
517
|
-
const currentSpreadIndex = findSpreadForPage(layoutState.spreads, currentSlice.page);
|
|
518
|
-
if (e.deltaY > 0) {
|
|
519
|
-
// Scroll down - next spread
|
|
520
|
-
const nextSpreadIndex = Math.min(currentSpreadIndex + 1, layoutState.spreads.length - 1);
|
|
521
|
-
if (nextSpreadIndex !== currentSpreadIndex) {
|
|
522
|
-
const nextPage = getSpreadPrimaryPage(layoutState.spreads[nextSpreadIndex]);
|
|
523
|
-
store.dispatch({ type: "SET_PAGE", page: nextPage });
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
else if (e.deltaY < 0) {
|
|
527
|
-
// Scroll up - previous spread
|
|
528
|
-
const prevSpreadIndex = Math.max(currentSpreadIndex - 1, 0);
|
|
529
|
-
if (prevSpreadIndex !== currentSpreadIndex) {
|
|
530
|
-
const prevPage = getSpreadPrimaryPage(layoutState.spreads[prevSpreadIndex]);
|
|
531
|
-
store.dispatch({ type: "SET_PAGE", page: prevPage });
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
};
|
|
535
|
-
scrollArea.addEventListener("wheel", handleWheel, { passive: false });
|
|
536
|
-
unsubEvents.push(() => scrollArea.removeEventListener("wheel", handleWheel));
|
|
537
|
-
unsubNavigation = store.subscribeEffect((prev, next) => {
|
|
538
|
-
if (prev.navigationTarget === next.navigationTarget)
|
|
539
|
-
return;
|
|
540
|
-
if (next.navigationTarget === null)
|
|
541
|
-
return;
|
|
542
|
-
const target = next.navigationTarget;
|
|
543
|
-
// Handle zoom change if specified
|
|
544
|
-
if (target.zoom !== undefined && target.zoom !== next.zoom) {
|
|
545
|
-
store.dispatch({ type: "SET_ZOOM", zoom: target.zoom });
|
|
546
|
-
}
|
|
547
|
-
if (next.scrollMode === "continuous") {
|
|
548
|
-
scrollToTarget(target);
|
|
549
|
-
}
|
|
550
|
-
else {
|
|
551
|
-
store.dispatch({ type: "SET_PAGE", page: target.page });
|
|
552
|
-
}
|
|
553
|
-
store.dispatch({ type: "CLEAR_NAVIGATION_TARGET" });
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
function scheduleUpdate() {
|
|
557
|
-
if (!currentSlice)
|
|
558
|
-
return;
|
|
559
|
-
if (updateRaf)
|
|
560
|
-
return;
|
|
561
|
-
updateRaf = requestAnimationFrame(() => {
|
|
562
|
-
updateRaf = 0;
|
|
563
|
-
if (!currentSlice)
|
|
564
|
-
return;
|
|
565
|
-
applyState(currentSlice);
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
function applyState(slice) {
|
|
569
|
-
scrollArea.style.paddingLeft = `${slice.pageSpacing}px`;
|
|
570
|
-
scrollArea.style.paddingRight = `${slice.pageSpacing}px`;
|
|
571
|
-
el.classList.toggle("udoc-viewport--seamless", slice.spacingMode === "none");
|
|
572
|
-
const metrics = readViewportMetrics(scrollArea, container);
|
|
573
|
-
const hasDoc = !!slice.docId && slice.pageCount > 0 && slice.pageInfos.length > 0;
|
|
574
|
-
if (!hasDoc || metrics.innerWidth <= 0 || metrics.innerHeight <= 0) {
|
|
575
|
-
clearSpreads();
|
|
576
|
-
container.style.height = "";
|
|
577
|
-
container.style.width = "";
|
|
578
|
-
scrollArea.style.overflowX = "hidden";
|
|
579
|
-
scrollArea.style.overflowY = "hidden";
|
|
580
|
-
layoutState = null;
|
|
581
|
-
lastVisibleRange = { start: 0, end: -1 };
|
|
582
|
-
viewportTopPosition = null;
|
|
583
|
-
syncEffectiveZoom(slice, null);
|
|
584
|
-
lastSlice = slice;
|
|
585
|
-
lastMetrics = metrics;
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
const plan = computeViewportUpdate(lastSlice, slice, lastMetrics, metrics);
|
|
589
|
-
if (plan.shouldClearSpreads) {
|
|
590
|
-
clearSpreads();
|
|
591
|
-
lastVisibleRange = { start: 0, end: -1 };
|
|
592
|
-
viewportTopPosition = null;
|
|
593
|
-
}
|
|
594
|
-
if (plan.layoutChanged || !layoutState) {
|
|
595
|
-
layoutState = buildLayout(slice, metrics, lastOverflowY ?? false);
|
|
596
|
-
layoutDirty = true;
|
|
597
|
-
lastMetrics = metrics;
|
|
598
|
-
}
|
|
599
|
-
syncEffectiveZoom(slice, layoutState);
|
|
600
|
-
if (slice.scrollMode === "continuous") {
|
|
601
|
-
applyContinuousLayout(metrics, layoutState);
|
|
602
|
-
if (plan.shouldScrollToPage) {
|
|
603
|
-
scrollToPage(slice.page, metrics);
|
|
604
|
-
}
|
|
605
|
-
else if (plan.shouldRestorePosition && viewportTopPosition) {
|
|
606
|
-
restoreViewportPosition(viewportTopPosition, metrics, layoutState);
|
|
607
|
-
}
|
|
608
|
-
updateVisibleSpreads(slice, metrics, layoutState);
|
|
609
|
-
}
|
|
610
|
-
else {
|
|
611
|
-
applySingleLayout(slice, metrics, layoutState);
|
|
612
|
-
if (plan.shouldScrollToPage) {
|
|
613
|
-
scrollArea.scrollTop = 0;
|
|
614
|
-
scrollArea.scrollLeft = 0;
|
|
615
|
-
lastOverflowX = null;
|
|
616
|
-
lastOverflowY = null;
|
|
617
|
-
}
|
|
618
|
-
else if (plan.shouldRestorePosition && viewportTopPosition) {
|
|
619
|
-
// In spread mode, restore position by showing the correct page
|
|
620
|
-
const targetPage = viewportTopPosition.page;
|
|
621
|
-
if (slice.page !== targetPage && storeRef) {
|
|
622
|
-
storeRef.dispatch({ type: "SET_PAGE", page: targetPage });
|
|
623
|
-
}
|
|
624
|
-
// Reset scroll and try to restore position within the spread if it's scrollable
|
|
625
|
-
scrollArea.scrollLeft = 0;
|
|
626
|
-
const spreadIndex = findSpreadForPage(layoutState.spreads, targetPage);
|
|
627
|
-
const layout = layoutState.layouts[spreadIndex];
|
|
628
|
-
if (layout && layout.height > metrics.innerHeight && !viewportTopPosition.inSpacing) {
|
|
629
|
-
// Spread is larger than viewport and we have a content offset
|
|
630
|
-
// Calculate scroll position to show the same relative position
|
|
631
|
-
const spreadTopInContainer = getCenteredOffset(containerSize.height, layout.height);
|
|
632
|
-
const offsetPixels = viewportTopPosition.offset * layout.height;
|
|
633
|
-
scrollArea.scrollTop = Math.max(0, spreadTopInContainer + offsetPixels);
|
|
634
|
-
}
|
|
635
|
-
else {
|
|
636
|
-
scrollArea.scrollTop = 0;
|
|
637
|
-
}
|
|
638
|
-
lastOverflowX = null;
|
|
639
|
-
lastOverflowY = null;
|
|
640
|
-
}
|
|
641
|
-
showSingleSpread(slice, metrics, layoutState);
|
|
642
|
-
}
|
|
643
|
-
updateOverflow(slice, metrics);
|
|
644
|
-
// Update tracked viewport position after layout is applied
|
|
645
|
-
updateViewportTopPosition(slice, layoutState);
|
|
646
|
-
lastSlice = slice;
|
|
647
|
-
}
|
|
648
|
-
function updateOverflow(slice, metrics) {
|
|
649
|
-
const epsilon = 1;
|
|
650
|
-
const scrollWidth = scrollArea.scrollWidth;
|
|
651
|
-
const scrollHeight = scrollArea.scrollHeight;
|
|
652
|
-
const clientWidth = scrollArea.clientWidth;
|
|
653
|
-
const clientHeight = scrollArea.clientHeight;
|
|
654
|
-
const scrollbar = getScrollbarSize();
|
|
655
|
-
// Apply hysteresis to avoid scrollbar-induced resize loops near 1px thresholds.
|
|
656
|
-
const assumeY = lastOverflowY ?? (scrollHeight - clientHeight > epsilon);
|
|
657
|
-
const availableWidth = clientWidth + (assumeY ? scrollbar.width : 0);
|
|
658
|
-
const deltaX = scrollWidth - availableWidth;
|
|
659
|
-
const needsX = resolveOverflowState(lastOverflowX, deltaX, epsilon);
|
|
660
|
-
const availableHeight = clientHeight + (needsX ? scrollbar.height : 0);
|
|
661
|
-
const deltaY = scrollHeight - availableHeight;
|
|
662
|
-
const finalNeedsY = resolveOverflowState(lastOverflowY, deltaY, epsilon);
|
|
663
|
-
lastOverflowX = needsX;
|
|
664
|
-
lastOverflowY = finalNeedsY;
|
|
665
|
-
scrollArea.style.overflowX = needsX ? "auto" : "hidden";
|
|
666
|
-
scrollArea.style.overflowY = finalNeedsY ? "auto" : "hidden";
|
|
667
|
-
}
|
|
668
|
-
function applyContinuousLayout(metrics, state) {
|
|
669
|
-
container.style.display = "block";
|
|
670
|
-
const width = snapToDevice(Math.max(metrics.innerWidth, state.contentWidth));
|
|
671
|
-
const height = snapToDevice(Math.max(metrics.innerHeight, state.contentHeight));
|
|
672
|
-
container.style.width = `${width}px`;
|
|
673
|
-
container.style.height = `${height}px`;
|
|
674
|
-
containerSize = { width, height };
|
|
675
|
-
}
|
|
676
|
-
function applySingleLayout(slice, metrics, state) {
|
|
677
|
-
container.style.display = "block";
|
|
678
|
-
const spreadIndex = findSpreadForPage(state.spreads, slice.page);
|
|
679
|
-
const layout = state.layouts[spreadIndex];
|
|
680
|
-
const spreadWidth = layout ? layout.width : 0;
|
|
681
|
-
const spreadHeight = layout ? layout.height : 0;
|
|
682
|
-
const snappedSpreadSpacing = snapToDevice(slice.spreadSpacing);
|
|
683
|
-
const width = snapToDevice(Math.max(metrics.innerWidth, spreadWidth));
|
|
684
|
-
const height = snapToDevice(Math.max(metrics.innerHeight, spreadHeight + snappedSpreadSpacing * 2));
|
|
685
|
-
container.style.width = `${width}px`;
|
|
686
|
-
container.style.height = `${height}px`;
|
|
687
|
-
containerSize = { width, height };
|
|
688
|
-
}
|
|
689
|
-
function syncEffectiveZoom(slice, state) {
|
|
690
|
-
if (!storeRef)
|
|
691
|
-
return;
|
|
692
|
-
const nextZoom = slice.zoomMode === "custom" ? null : state?.scale ?? null;
|
|
693
|
-
const current = storeRef.getState().effectiveZoom;
|
|
694
|
-
if (nextZoom === null) {
|
|
695
|
-
if (current === null)
|
|
696
|
-
return;
|
|
697
|
-
storeRef.dispatch({ type: "SET_EFFECTIVE_ZOOM", zoom: null });
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
if (current !== null && Math.abs(nextZoom - current) < 0.001)
|
|
701
|
-
return;
|
|
702
|
-
storeRef.dispatch({ type: "SET_EFFECTIVE_ZOOM", zoom: nextZoom });
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* Updates the tracked viewport top position based on current scroll state.
|
|
706
|
-
* This captures which page is at the viewport top and how far into it we are.
|
|
707
|
-
*/
|
|
708
|
-
function updateViewportTopPosition(slice, state) {
|
|
709
|
-
if (state.layouts.length === 0 || state.spreads.length === 0) {
|
|
710
|
-
viewportTopPosition = null;
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
let viewportTopInLayout;
|
|
714
|
-
if (slice.scrollMode === "continuous") {
|
|
715
|
-
// In continuous mode, viewport top position is directly from scrollTop
|
|
716
|
-
viewportTopInLayout = scrollArea.scrollTop;
|
|
717
|
-
}
|
|
718
|
-
else {
|
|
719
|
-
// In spread mode, the spread is centered in the viewport
|
|
720
|
-
// Calculate where the viewport top would be in layout coordinates
|
|
721
|
-
const spreadIndex = findSpreadForPage(state.spreads, slice.page);
|
|
722
|
-
const layout = state.layouts[spreadIndex];
|
|
723
|
-
if (!layout) {
|
|
724
|
-
viewportTopPosition = null;
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
// The spread is positioned at getCenteredOffset from container top
|
|
728
|
-
const spreadTopInContainer = getCenteredOffset(containerSize.height, layout.height);
|
|
729
|
-
// Account for any scroll within spread mode (for large spreads)
|
|
730
|
-
const viewportTopInContainer = scrollArea.scrollTop;
|
|
731
|
-
// Map to layout coordinates: layout.top is where this spread is in continuous layout
|
|
732
|
-
viewportTopInLayout = layout.top - spreadTopInContainer + viewportTopInContainer;
|
|
733
|
-
}
|
|
734
|
-
// Find which spread the viewport top is associated with
|
|
735
|
-
for (let i = 0; i < state.layouts.length; i++) {
|
|
736
|
-
const layout = state.layouts[i];
|
|
737
|
-
const spread = state.spreads[i];
|
|
738
|
-
if (!spread)
|
|
739
|
-
continue;
|
|
740
|
-
const spreadTop = layout.top;
|
|
741
|
-
const spreadBottom = layout.top + layout.height;
|
|
742
|
-
const offsetFromSpreadTop = viewportTopInLayout - spreadTop;
|
|
743
|
-
// Check if viewport is in the spacing area before this spread
|
|
744
|
-
if (viewportTopInLayout < spreadTop) {
|
|
745
|
-
// We're in the spacing above this spread
|
|
746
|
-
// Store the absolute pixel offset (negative) since spacing doesn't scale
|
|
747
|
-
viewportTopPosition = {
|
|
748
|
-
page: getSpreadPrimaryPage(spread),
|
|
749
|
-
offset: offsetFromSpreadTop, // negative value
|
|
750
|
-
inSpacing: true
|
|
751
|
-
};
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
// Check if viewport is within this spread's content
|
|
755
|
-
if (viewportTopInLayout < spreadBottom || i === state.layouts.length - 1) {
|
|
756
|
-
// We're within the spread content
|
|
757
|
-
// Store as ratio of spread height so it scales correctly
|
|
758
|
-
const offsetRatio = layout.height > 0 ? offsetFromSpreadTop / layout.height : 0;
|
|
759
|
-
viewportTopPosition = {
|
|
760
|
-
page: getSpreadPrimaryPage(spread),
|
|
761
|
-
offset: offsetRatio,
|
|
762
|
-
inSpacing: false
|
|
763
|
-
};
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
viewportTopPosition = null;
|
|
768
|
-
}
|
|
769
|
-
/**
|
|
770
|
-
* Restores the viewport to a previously tracked position.
|
|
771
|
-
*/
|
|
772
|
-
function restoreViewportPosition(position, metrics, state) {
|
|
773
|
-
const spreadIndex = findSpreadForPage(state.spreads, position.page);
|
|
774
|
-
const layout = state.layouts[spreadIndex];
|
|
775
|
-
if (!layout)
|
|
776
|
-
return;
|
|
777
|
-
let targetScrollTop;
|
|
778
|
-
if (position.inSpacing) {
|
|
779
|
-
// In spacing area: offset is absolute pixels (negative), doesn't scale
|
|
780
|
-
targetScrollTop = layout.top + position.offset;
|
|
781
|
-
}
|
|
782
|
-
else {
|
|
783
|
-
// In spread content: offset is ratio of spread height, scales with zoom
|
|
784
|
-
const offsetPixels = position.offset * layout.height;
|
|
785
|
-
targetScrollTop = layout.top + offsetPixels;
|
|
786
|
-
}
|
|
787
|
-
const maxScrollTop = Math.max(0, containerSize.height - metrics.innerHeight);
|
|
788
|
-
scrollArea.scrollTop = clamp(targetScrollTop, 0, maxScrollTop);
|
|
789
|
-
}
|
|
790
|
-
function updateVisibleSpreads(slice, metrics, state) {
|
|
791
|
-
if (!workerClient || !slice.docId)
|
|
792
|
-
return;
|
|
793
|
-
if (state.layouts.length === 0)
|
|
794
|
-
return;
|
|
795
|
-
const scrollTop = scrollArea.scrollTop;
|
|
796
|
-
const visibleRange = findVisibleSpreadRange(state.layouts, scrollTop, metrics.innerHeight, RENDER_BUFFER);
|
|
797
|
-
const rangeChanged = visibleRange.start !== lastVisibleRange.start ||
|
|
798
|
-
visibleRange.end !== lastVisibleRange.end;
|
|
799
|
-
const layoutOptions = {
|
|
800
|
-
pageInfos: slice.pageInfos,
|
|
801
|
-
scale: state.scale,
|
|
802
|
-
dpi: slice.dpi,
|
|
803
|
-
rotation: slice.pageRotation,
|
|
804
|
-
pageSpacing: slice.pageSpacing
|
|
805
|
-
};
|
|
806
|
-
if (layoutDirty || rangeChanged) {
|
|
807
|
-
for (const [index, spread] of spreadComponents) {
|
|
808
|
-
if (index < visibleRange.start || index > visibleRange.end) {
|
|
809
|
-
spread.destroy();
|
|
810
|
-
spreadComponents.delete(index);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
const renderOptions = {
|
|
814
|
-
docId: slice.docId,
|
|
815
|
-
scale: state.scale,
|
|
816
|
-
dpi: slice.dpi
|
|
817
|
-
};
|
|
818
|
-
for (let i = visibleRange.start; i <= visibleRange.end; i++) {
|
|
819
|
-
const layout = state.layouts[i];
|
|
820
|
-
if (!layout)
|
|
821
|
-
continue;
|
|
822
|
-
let spreadComp = spreadComponents.get(i);
|
|
823
|
-
if (!spreadComp) {
|
|
824
|
-
const spreadData = state.spreads[i];
|
|
825
|
-
spreadComp = createSpread(spreadData);
|
|
826
|
-
spreadComp.mount(container);
|
|
827
|
-
spreadComponents.set(i, spreadComp);
|
|
828
|
-
}
|
|
829
|
-
spreadComp.updateLayout(layoutOptions);
|
|
830
|
-
// Set spread position and dimensions from layout.
|
|
831
|
-
// Layout values are pre-snapped with cumulative consistency.
|
|
832
|
-
const spreadEl = spreadComp.getElement();
|
|
833
|
-
spreadEl.style.position = "absolute";
|
|
834
|
-
spreadEl.style.top = `${layout.top}px`;
|
|
835
|
-
spreadEl.style.width = `${layout.width}px`;
|
|
836
|
-
spreadEl.style.height = `${layout.height}px`;
|
|
837
|
-
spreadEl.style.left = `${getCenteredOffset(containerSize.width, layout.width)}px`;
|
|
838
|
-
spreadEl.style.transform = "none";
|
|
839
|
-
// Skip render during resize animation (renders debounced separately)
|
|
840
|
-
if (!rendersPaused) {
|
|
841
|
-
spreadComp.render(workerClient, renderOptions);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
lastVisibleRange = visibleRange;
|
|
845
|
-
// Only clear layoutDirty if renders actually happened
|
|
846
|
-
// Otherwise keep it dirty so renders happen when rendersPaused becomes false
|
|
847
|
-
if (!rendersPaused) {
|
|
848
|
-
layoutDirty = false;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
// Always update annotations and text on visible spreads (they may load after layout)
|
|
852
|
-
for (let i = visibleRange.start; i <= visibleRange.end; i++) {
|
|
853
|
-
const spreadComp = spreadComponents.get(i);
|
|
854
|
-
if (spreadComp) {
|
|
855
|
-
spreadComp.updateAnnotations(slice.pageAnnotations, layoutOptions, slice.highlightedAnnotation);
|
|
856
|
-
spreadComp.updateTextLayer(slice.pageText, layoutOptions);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
const viewportCenter = scrollTop + metrics.innerHeight / 2;
|
|
860
|
-
const focusPage = findFocusPage(viewportCenter, state);
|
|
861
|
-
if (focusPage !== null) {
|
|
862
|
-
workerClient.boostPageRenderPriority(slice.docId, focusPage);
|
|
863
|
-
}
|
|
864
|
-
updateCurrentPageFromScroll(scrollTop, metrics.innerHeight, state);
|
|
865
|
-
}
|
|
866
|
-
function showSingleSpread(slice, _metrics, state) {
|
|
867
|
-
if (!workerClient || !slice.docId)
|
|
868
|
-
return;
|
|
869
|
-
const spreadIndex = findSpreadForPage(state.spreads, slice.page);
|
|
870
|
-
const layout = state.layouts[spreadIndex];
|
|
871
|
-
if (!layout)
|
|
872
|
-
return;
|
|
873
|
-
for (const [index, spread] of spreadComponents) {
|
|
874
|
-
if (index !== spreadIndex) {
|
|
875
|
-
spread.destroy();
|
|
876
|
-
spreadComponents.delete(index);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
let spreadComp = spreadComponents.get(spreadIndex);
|
|
880
|
-
if (!spreadComp) {
|
|
881
|
-
const spreadData = state.spreads[spreadIndex];
|
|
882
|
-
spreadComp = createSpread(spreadData);
|
|
883
|
-
spreadComp.mount(container);
|
|
884
|
-
spreadComponents.set(spreadIndex, spreadComp);
|
|
885
|
-
}
|
|
886
|
-
const layoutOptions = {
|
|
887
|
-
pageInfos: slice.pageInfos,
|
|
888
|
-
scale: state.scale,
|
|
889
|
-
dpi: slice.dpi,
|
|
890
|
-
rotation: slice.pageRotation,
|
|
891
|
-
pageSpacing: slice.pageSpacing
|
|
892
|
-
};
|
|
893
|
-
spreadComp.updateLayout(layoutOptions);
|
|
894
|
-
spreadComp.updateAnnotations(slice.pageAnnotations, layoutOptions, slice.highlightedAnnotation);
|
|
895
|
-
spreadComp.updateTextLayer(slice.pageText, layoutOptions);
|
|
896
|
-
// Layout values are pre-snapped
|
|
897
|
-
const top = getCenteredOffset(containerSize.height, layout.height);
|
|
898
|
-
const spreadEl = spreadComp.getElement();
|
|
899
|
-
spreadEl.style.position = "absolute";
|
|
900
|
-
spreadEl.style.top = `${top}px`;
|
|
901
|
-
spreadEl.style.width = `${layout.width}px`;
|
|
902
|
-
spreadEl.style.height = `${layout.height}px`;
|
|
903
|
-
spreadEl.style.left = `${getCenteredOffset(containerSize.width, layout.width)}px`;
|
|
904
|
-
spreadEl.style.transform = "none";
|
|
905
|
-
// Skip render during resize animation (renders debounced separately)
|
|
906
|
-
if (!rendersPaused) {
|
|
907
|
-
spreadComp.render(workerClient, {
|
|
908
|
-
docId: slice.docId,
|
|
909
|
-
scale: state.scale,
|
|
910
|
-
dpi: slice.dpi
|
|
911
|
-
});
|
|
912
|
-
// Prerender adjacent pages for smooth page flipping
|
|
913
|
-
const dpr = getDevicePixelRatio();
|
|
914
|
-
const pointsToPixels = getPointsToPixels(slice.dpi);
|
|
915
|
-
const renderScale = pointsToPixels * state.scale * dpr;
|
|
916
|
-
workerClient.prerenderAdjacentPages(slice.docId, slice.page, renderScale, slice.pageInfos.length);
|
|
917
|
-
}
|
|
918
|
-
lastVisibleRange = { start: spreadIndex, end: spreadIndex };
|
|
919
|
-
// Only clear layoutDirty if renders actually happened
|
|
920
|
-
if (!rendersPaused) {
|
|
921
|
-
layoutDirty = false;
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
function findFocusPage(viewportCenter, state) {
|
|
925
|
-
if (state.layouts.length === 0 || state.spreads.length === 0)
|
|
926
|
-
return null;
|
|
927
|
-
let closestIndex = 0;
|
|
928
|
-
let closestDistance = Infinity;
|
|
929
|
-
for (let i = 0; i < state.layouts.length; i++) {
|
|
930
|
-
const layout = state.layouts[i];
|
|
931
|
-
const spreadCenter = layout.top + layout.height / 2;
|
|
932
|
-
const distance = Math.abs(spreadCenter - viewportCenter);
|
|
933
|
-
if (distance < closestDistance) {
|
|
934
|
-
closestDistance = distance;
|
|
935
|
-
closestIndex = i;
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
const spread = state.spreads[closestIndex];
|
|
939
|
-
return spread ? getSpreadPrimaryPage(spread) : null;
|
|
940
|
-
}
|
|
941
|
-
function updateCurrentPageFromScroll(scrollTop, viewportHeight, state) {
|
|
942
|
-
if (!storeRef)
|
|
943
|
-
return;
|
|
944
|
-
const viewportCenter = scrollTop + viewportHeight / 2;
|
|
945
|
-
const primaryPage = findFocusPage(viewportCenter, state);
|
|
946
|
-
if (primaryPage === null)
|
|
947
|
-
return;
|
|
948
|
-
const currentState = storeRef.getState();
|
|
949
|
-
if (currentState.page !== primaryPage) {
|
|
950
|
-
storeRef.dispatch({ type: "SET_PAGE", page: primaryPage });
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
function scrollToPage(page, metrics, center) {
|
|
954
|
-
if (!layoutState)
|
|
955
|
-
return;
|
|
956
|
-
if (layoutState.layouts.length === 0)
|
|
957
|
-
return;
|
|
958
|
-
const viewport = metrics ?? lastMetrics;
|
|
959
|
-
if (!viewport)
|
|
960
|
-
return;
|
|
961
|
-
const slice = currentSlice ?? lastSlice;
|
|
962
|
-
if (!slice)
|
|
963
|
-
return;
|
|
964
|
-
const spreadIndex = findSpreadForPage(layoutState.spreads, page);
|
|
965
|
-
const layout = layoutState.layouts[spreadIndex];
|
|
966
|
-
if (!layout)
|
|
967
|
-
return;
|
|
968
|
-
// Top-align with spacing (default), or center vertically if requested
|
|
969
|
-
const snappedSpreadSpacing = snapToDevice(slice.spreadSpacing);
|
|
970
|
-
const targetScrollTop = center
|
|
971
|
-
? layout.top - (viewport.innerHeight - layout.height) / 2
|
|
972
|
-
: layout.top - snappedSpreadSpacing;
|
|
973
|
-
const maxScrollTop = Math.max(0, containerSize.height - viewport.innerHeight);
|
|
974
|
-
scrollArea.scrollTop = clamp(targetScrollTop, 0, maxScrollTop);
|
|
975
|
-
}
|
|
976
|
-
function scrollToTarget(target, metrics) {
|
|
977
|
-
if (!layoutState)
|
|
978
|
-
return;
|
|
979
|
-
if (layoutState.layouts.length === 0)
|
|
980
|
-
return;
|
|
981
|
-
const viewport = metrics ?? lastMetrics;
|
|
982
|
-
if (!viewport)
|
|
983
|
-
return;
|
|
984
|
-
const slice = currentSlice ?? lastSlice;
|
|
985
|
-
if (!slice)
|
|
986
|
-
return;
|
|
987
|
-
const spreadIndex = findSpreadForPage(layoutState.spreads, target.page);
|
|
988
|
-
const layout = layoutState.layouts[spreadIndex];
|
|
989
|
-
if (!layout)
|
|
990
|
-
return;
|
|
991
|
-
let targetScrollTop;
|
|
992
|
-
let targetScrollLeft = 0;
|
|
993
|
-
// Apply scroll offset if specified (convert from PDF points to scaled pixels)
|
|
994
|
-
if (target.scrollTo && target.scrollTo.y !== undefined) {
|
|
995
|
-
// scrollTo.y is in PDF points from top of page (already Y-flipped by WASM)
|
|
996
|
-
// Full conversion: points * (dpi/72) * zoomScale
|
|
997
|
-
const dpiScale = getPointsToPixels(slice.dpi);
|
|
998
|
-
const pointsToPixels = dpiScale * layoutState.scale;
|
|
999
|
-
const yInPixels = target.scrollTo.y * pointsToPixels;
|
|
1000
|
-
// Position target at top of viewport
|
|
1001
|
-
targetScrollTop = layout.top + yInPixels;
|
|
1002
|
-
if (target.scrollTo.x !== undefined) {
|
|
1003
|
-
const xInPixels = target.scrollTo.x * pointsToPixels;
|
|
1004
|
-
// Center the x position in viewport if possible
|
|
1005
|
-
targetScrollLeft = Math.max(0, xInPixels - viewport.innerWidth / 2);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
else {
|
|
1009
|
-
// No specific scroll position - scroll to top of spread with spacing
|
|
1010
|
-
const snappedSpreadSpacing = snapToDevice(slice.spreadSpacing);
|
|
1011
|
-
targetScrollTop = layout.top - snappedSpreadSpacing;
|
|
1012
|
-
}
|
|
1013
|
-
const maxScrollTop = Math.max(0, containerSize.height - viewport.innerHeight);
|
|
1014
|
-
const maxScrollLeft = Math.max(0, containerSize.width - viewport.innerWidth);
|
|
1015
|
-
scrollArea.scrollTop = clamp(targetScrollTop, 0, maxScrollTop);
|
|
1016
|
-
scrollArea.scrollLeft = clamp(targetScrollLeft, 0, maxScrollLeft);
|
|
1017
|
-
}
|
|
1018
|
-
function clearSpreads() {
|
|
1019
|
-
for (const spread of spreadComponents.values()) {
|
|
1020
|
-
spread.destroy();
|
|
1021
|
-
}
|
|
1022
|
-
spreadComponents.clear();
|
|
1023
|
-
lastOverflowX = null;
|
|
1024
|
-
lastOverflowY = null;
|
|
1025
|
-
}
|
|
1026
|
-
function destroy() {
|
|
1027
|
-
if (unsubRender)
|
|
1028
|
-
unsubRender();
|
|
1029
|
-
if (unsubScroll)
|
|
1030
|
-
unsubScroll();
|
|
1031
|
-
if (unsubNavigation)
|
|
1032
|
-
unsubNavigation();
|
|
1033
|
-
if (resizeObserver)
|
|
1034
|
-
resizeObserver.disconnect();
|
|
1035
|
-
for (const off of unsubEvents)
|
|
1036
|
-
off();
|
|
1037
|
-
if (updateRaf)
|
|
1038
|
-
cancelAnimationFrame(updateRaf);
|
|
1039
|
-
if (scrollRaf)
|
|
1040
|
-
cancelAnimationFrame(scrollRaf);
|
|
1041
|
-
if (renderDebounceTimer)
|
|
1042
|
-
clearTimeout(renderDebounceTimer);
|
|
1043
|
-
wmObserver.disconnect();
|
|
1044
|
-
clearInterval(wmIntegrityCheck);
|
|
1045
|
-
floatingToolbar.destroy();
|
|
1046
|
-
clearSpreads();
|
|
1047
|
-
workerClient = null;
|
|
1048
|
-
storeRef = null;
|
|
1049
|
-
currentSlice = null;
|
|
1050
|
-
lastSlice = null;
|
|
1051
|
-
layoutState = null;
|
|
1052
|
-
el.remove();
|
|
1053
|
-
}
|
|
1054
|
-
return { el, mount, destroy };
|
|
1055
|
-
}
|
|
1056
|
-
function selectViewport(state) {
|
|
1057
|
-
return {
|
|
1058
|
-
docId: state.doc?.id ?? null,
|
|
1059
|
-
page: state.page,
|
|
1060
|
-
pageCount: state.pageCount,
|
|
1061
|
-
pageInfos: state.pageInfos,
|
|
1062
|
-
scrollMode: state.scrollMode,
|
|
1063
|
-
layoutMode: state.layoutMode,
|
|
1064
|
-
zoomMode: state.zoomMode,
|
|
1065
|
-
zoom: state.zoom,
|
|
1066
|
-
dpi: state.dpi,
|
|
1067
|
-
pageRotation: state.pageRotation,
|
|
1068
|
-
spacingMode: state.spacingMode,
|
|
1069
|
-
pageSpacing: state.pageSpacing,
|
|
1070
|
-
spreadSpacing: state.spreadSpacing,
|
|
1071
|
-
pageAnnotations: state.pageAnnotations,
|
|
1072
|
-
highlightedAnnotation: state.highlightedAnnotation,
|
|
1073
|
-
pageText: state.pageText
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
//# sourceMappingURL=Viewport.js.map
|