@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.
Files changed (211) hide show
  1. package/package.json +1 -1
  2. package/dist/package.json +0 -56
  3. package/dist/src/UDocClient.d.ts +0 -383
  4. package/dist/src/UDocClient.d.ts.map +0 -1
  5. package/dist/src/UDocClient.js +0 -428
  6. package/dist/src/UDocClient.js.map +0 -1
  7. package/dist/src/UDocViewer.d.ts +0 -271
  8. package/dist/src/UDocViewer.d.ts.map +0 -1
  9. package/dist/src/UDocViewer.js +0 -769
  10. package/dist/src/UDocViewer.js.map +0 -1
  11. package/dist/src/fonts.d.ts +0 -29
  12. package/dist/src/fonts.d.ts.map +0 -1
  13. package/dist/src/fonts.js +0 -30
  14. package/dist/src/fonts.js.map +0 -1
  15. package/dist/src/index.d.ts +0 -9
  16. package/dist/src/index.d.ts.map +0 -1
  17. package/dist/src/index.js +0 -8
  18. package/dist/src/index.js.map +0 -1
  19. package/dist/src/performance/PerformanceCounter.d.ts +0 -132
  20. package/dist/src/performance/PerformanceCounter.d.ts.map +0 -1
  21. package/dist/src/performance/PerformanceCounter.js +0 -129
  22. package/dist/src/performance/PerformanceCounter.js.map +0 -1
  23. package/dist/src/performance/index.d.ts +0 -2
  24. package/dist/src/performance/index.d.ts.map +0 -1
  25. package/dist/src/performance/index.js +0 -2
  26. package/dist/src/performance/index.js.map +0 -1
  27. package/dist/src/ui/framework/component.d.ts +0 -68
  28. package/dist/src/ui/framework/component.d.ts.map +0 -1
  29. package/dist/src/ui/framework/component.js +0 -87
  30. package/dist/src/ui/framework/component.js.map +0 -1
  31. package/dist/src/ui/framework/dom.d.ts +0 -19
  32. package/dist/src/ui/framework/dom.d.ts.map +0 -1
  33. package/dist/src/ui/framework/dom.js +0 -29
  34. package/dist/src/ui/framework/dom.js.map +0 -1
  35. package/dist/src/ui/framework/events.d.ts +0 -18
  36. package/dist/src/ui/framework/events.d.ts.map +0 -1
  37. package/dist/src/ui/framework/events.js +0 -23
  38. package/dist/src/ui/framework/events.js.map +0 -1
  39. package/dist/src/ui/framework/index.d.ts +0 -15
  40. package/dist/src/ui/framework/index.d.ts.map +0 -1
  41. package/dist/src/ui/framework/index.js +0 -15
  42. package/dist/src/ui/framework/index.js.map +0 -1
  43. package/dist/src/ui/framework/selectors.d.ts +0 -51
  44. package/dist/src/ui/framework/selectors.d.ts.map +0 -1
  45. package/dist/src/ui/framework/selectors.js +0 -30
  46. package/dist/src/ui/framework/selectors.js.map +0 -1
  47. package/dist/src/ui/framework/store.d.ts +0 -37
  48. package/dist/src/ui/framework/store.d.ts.map +0 -1
  49. package/dist/src/ui/framework/store.js +0 -54
  50. package/dist/src/ui/framework/store.js.map +0 -1
  51. package/dist/src/ui/viewer/actions.d.ts +0 -131
  52. package/dist/src/ui/viewer/actions.d.ts.map +0 -1
  53. package/dist/src/ui/viewer/actions.js +0 -2
  54. package/dist/src/ui/viewer/actions.js.map +0 -1
  55. package/dist/src/ui/viewer/annotation/LinkRenderer.d.ts +0 -9
  56. package/dist/src/ui/viewer/annotation/LinkRenderer.d.ts.map +0 -1
  57. package/dist/src/ui/viewer/annotation/LinkRenderer.js +0 -17
  58. package/dist/src/ui/viewer/annotation/LinkRenderer.js.map +0 -1
  59. package/dist/src/ui/viewer/annotation/MarkupRenderer.d.ts +0 -21
  60. package/dist/src/ui/viewer/annotation/MarkupRenderer.d.ts.map +0 -1
  61. package/dist/src/ui/viewer/annotation/MarkupRenderer.js +0 -138
  62. package/dist/src/ui/viewer/annotation/MarkupRenderer.js.map +0 -1
  63. package/dist/src/ui/viewer/annotation/ShapeRenderer.d.ts +0 -33
  64. package/dist/src/ui/viewer/annotation/ShapeRenderer.d.ts.map +0 -1
  65. package/dist/src/ui/viewer/annotation/ShapeRenderer.js +0 -378
  66. package/dist/src/ui/viewer/annotation/ShapeRenderer.js.map +0 -1
  67. package/dist/src/ui/viewer/annotation/TextRenderer.d.ts +0 -23
  68. package/dist/src/ui/viewer/annotation/TextRenderer.d.ts.map +0 -1
  69. package/dist/src/ui/viewer/annotation/TextRenderer.js +0 -196
  70. package/dist/src/ui/viewer/annotation/TextRenderer.js.map +0 -1
  71. package/dist/src/ui/viewer/annotation/index.d.ts +0 -8
  72. package/dist/src/ui/viewer/annotation/index.d.ts.map +0 -1
  73. package/dist/src/ui/viewer/annotation/index.js +0 -8
  74. package/dist/src/ui/viewer/annotation/index.js.map +0 -1
  75. package/dist/src/ui/viewer/annotation/render.d.ts +0 -24
  76. package/dist/src/ui/viewer/annotation/render.d.ts.map +0 -1
  77. package/dist/src/ui/viewer/annotation/render.js +0 -184
  78. package/dist/src/ui/viewer/annotation/render.js.map +0 -1
  79. package/dist/src/ui/viewer/annotation/types.d.ts +0 -239
  80. package/dist/src/ui/viewer/annotation/types.d.ts.map +0 -1
  81. package/dist/src/ui/viewer/annotation/types.js +0 -7
  82. package/dist/src/ui/viewer/annotation/types.js.map +0 -1
  83. package/dist/src/ui/viewer/annotation/utils.d.ts +0 -37
  84. package/dist/src/ui/viewer/annotation/utils.d.ts.map +0 -1
  85. package/dist/src/ui/viewer/annotation/utils.js +0 -82
  86. package/dist/src/ui/viewer/annotation/utils.js.map +0 -1
  87. package/dist/src/ui/viewer/components/AnnotationPanel.d.ts +0 -19
  88. package/dist/src/ui/viewer/components/AnnotationPanel.d.ts.map +0 -1
  89. package/dist/src/ui/viewer/components/AnnotationPanel.js +0 -284
  90. package/dist/src/ui/viewer/components/AnnotationPanel.js.map +0 -1
  91. package/dist/src/ui/viewer/components/FloatingToolbar.d.ts +0 -9
  92. package/dist/src/ui/viewer/components/FloatingToolbar.d.ts.map +0 -1
  93. package/dist/src/ui/viewer/components/FloatingToolbar.js +0 -305
  94. package/dist/src/ui/viewer/components/FloatingToolbar.js.map +0 -1
  95. package/dist/src/ui/viewer/components/LeftPanel.d.ts +0 -10
  96. package/dist/src/ui/viewer/components/LeftPanel.d.ts.map +0 -1
  97. package/dist/src/ui/viewer/components/LeftPanel.js +0 -165
  98. package/dist/src/ui/viewer/components/LeftPanel.js.map +0 -1
  99. package/dist/src/ui/viewer/components/LoadingOverlay.d.ts +0 -12
  100. package/dist/src/ui/viewer/components/LoadingOverlay.d.ts.map +0 -1
  101. package/dist/src/ui/viewer/components/LoadingOverlay.js +0 -88
  102. package/dist/src/ui/viewer/components/LoadingOverlay.js.map +0 -1
  103. package/dist/src/ui/viewer/components/OutlinePanel.d.ts +0 -10
  104. package/dist/src/ui/viewer/components/OutlinePanel.d.ts.map +0 -1
  105. package/dist/src/ui/viewer/components/OutlinePanel.js +0 -169
  106. package/dist/src/ui/viewer/components/OutlinePanel.js.map +0 -1
  107. package/dist/src/ui/viewer/components/PasswordDialog.d.ts +0 -15
  108. package/dist/src/ui/viewer/components/PasswordDialog.d.ts.map +0 -1
  109. package/dist/src/ui/viewer/components/PasswordDialog.js +0 -143
  110. package/dist/src/ui/viewer/components/PasswordDialog.js.map +0 -1
  111. package/dist/src/ui/viewer/components/RightPanel.d.ts +0 -9
  112. package/dist/src/ui/viewer/components/RightPanel.d.ts.map +0 -1
  113. package/dist/src/ui/viewer/components/RightPanel.js +0 -102
  114. package/dist/src/ui/viewer/components/RightPanel.js.map +0 -1
  115. package/dist/src/ui/viewer/components/Spread.d.ts +0 -43
  116. package/dist/src/ui/viewer/components/Spread.d.ts.map +0 -1
  117. package/dist/src/ui/viewer/components/Spread.js +0 -345
  118. package/dist/src/ui/viewer/components/Spread.js.map +0 -1
  119. package/dist/src/ui/viewer/components/ThumbnailPanel.d.ts +0 -11
  120. package/dist/src/ui/viewer/components/ThumbnailPanel.d.ts.map +0 -1
  121. package/dist/src/ui/viewer/components/ThumbnailPanel.js +0 -240
  122. package/dist/src/ui/viewer/components/ThumbnailPanel.js.map +0 -1
  123. package/dist/src/ui/viewer/components/Toolbar.d.ts +0 -9
  124. package/dist/src/ui/viewer/components/Toolbar.d.ts.map +0 -1
  125. package/dist/src/ui/viewer/components/Toolbar.js +0 -93
  126. package/dist/src/ui/viewer/components/Toolbar.js.map +0 -1
  127. package/dist/src/ui/viewer/components/ViewModeMenu.d.ts +0 -9
  128. package/dist/src/ui/viewer/components/ViewModeMenu.d.ts.map +0 -1
  129. package/dist/src/ui/viewer/components/ViewModeMenu.js +0 -169
  130. package/dist/src/ui/viewer/components/ViewModeMenu.js.map +0 -1
  131. package/dist/src/ui/viewer/components/Viewport.d.ts +0 -10
  132. package/dist/src/ui/viewer/components/Viewport.d.ts.map +0 -1
  133. package/dist/src/ui/viewer/components/Viewport.js +0 -1076
  134. package/dist/src/ui/viewer/components/Viewport.js.map +0 -1
  135. package/dist/src/ui/viewer/effects.d.ts +0 -8
  136. package/dist/src/ui/viewer/effects.d.ts.map +0 -1
  137. package/dist/src/ui/viewer/effects.js +0 -207
  138. package/dist/src/ui/viewer/effects.js.map +0 -1
  139. package/dist/src/ui/viewer/icons.d.ts +0 -32
  140. package/dist/src/ui/viewer/icons.d.ts.map +0 -1
  141. package/dist/src/ui/viewer/icons.js +0 -44
  142. package/dist/src/ui/viewer/icons.js.map +0 -1
  143. package/dist/src/ui/viewer/index.d.ts +0 -6
  144. package/dist/src/ui/viewer/index.d.ts.map +0 -1
  145. package/dist/src/ui/viewer/index.js +0 -6
  146. package/dist/src/ui/viewer/index.js.map +0 -1
  147. package/dist/src/ui/viewer/layout/index.d.ts +0 -3
  148. package/dist/src/ui/viewer/layout/index.d.ts.map +0 -1
  149. package/dist/src/ui/viewer/layout/index.js +0 -3
  150. package/dist/src/ui/viewer/layout/index.js.map +0 -1
  151. package/dist/src/ui/viewer/layout/pixelAlign.d.ts +0 -7
  152. package/dist/src/ui/viewer/layout/pixelAlign.d.ts.map +0 -1
  153. package/dist/src/ui/viewer/layout/pixelAlign.js +0 -22
  154. package/dist/src/ui/viewer/layout/pixelAlign.js.map +0 -1
  155. package/dist/src/ui/viewer/layout/spreadLayout.d.ts +0 -93
  156. package/dist/src/ui/viewer/layout/spreadLayout.d.ts.map +0 -1
  157. package/dist/src/ui/viewer/layout/spreadLayout.js +0 -315
  158. package/dist/src/ui/viewer/layout/spreadLayout.js.map +0 -1
  159. package/dist/src/ui/viewer/navigation.d.ts +0 -80
  160. package/dist/src/ui/viewer/navigation.d.ts.map +0 -1
  161. package/dist/src/ui/viewer/navigation.js +0 -59
  162. package/dist/src/ui/viewer/navigation.js.map +0 -1
  163. package/dist/src/ui/viewer/reducer.d.ts +0 -4
  164. package/dist/src/ui/viewer/reducer.d.ts.map +0 -1
  165. package/dist/src/ui/viewer/reducer.js +0 -305
  166. package/dist/src/ui/viewer/reducer.js.map +0 -1
  167. package/dist/src/ui/viewer/shell.d.ts +0 -34
  168. package/dist/src/ui/viewer/shell.d.ts.map +0 -1
  169. package/dist/src/ui/viewer/shell.js +0 -93
  170. package/dist/src/ui/viewer/shell.js.map +0 -1
  171. package/dist/src/ui/viewer/state.d.ts +0 -89
  172. package/dist/src/ui/viewer/state.d.ts.map +0 -1
  173. package/dist/src/ui/viewer/state.js +0 -55
  174. package/dist/src/ui/viewer/state.js.map +0 -1
  175. package/dist/src/ui/viewer/styles-inline.d.ts +0 -2
  176. package/dist/src/ui/viewer/styles-inline.d.ts.map +0 -1
  177. package/dist/src/ui/viewer/styles-inline.js +0 -1584
  178. package/dist/src/ui/viewer/styles-inline.js.map +0 -1
  179. package/dist/src/ui/viewer/text/index.d.ts +0 -7
  180. package/dist/src/ui/viewer/text/index.d.ts.map +0 -1
  181. package/dist/src/ui/viewer/text/index.js +0 -3
  182. package/dist/src/ui/viewer/text/index.js.map +0 -1
  183. package/dist/src/ui/viewer/text/render.d.ts +0 -19
  184. package/dist/src/ui/viewer/text/render.d.ts.map +0 -1
  185. package/dist/src/ui/viewer/text/render.js +0 -228
  186. package/dist/src/ui/viewer/text/render.js.map +0 -1
  187. package/dist/src/ui/viewer/text/selection.d.ts +0 -12
  188. package/dist/src/ui/viewer/text/selection.d.ts.map +0 -1
  189. package/dist/src/ui/viewer/text/selection.js +0 -70
  190. package/dist/src/ui/viewer/text/selection.js.map +0 -1
  191. package/dist/src/ui/viewer/text/types.d.ts +0 -37
  192. package/dist/src/ui/viewer/text/types.d.ts.map +0 -1
  193. package/dist/src/ui/viewer/text/types.js +0 -7
  194. package/dist/src/ui/viewer/text/types.js.map +0 -1
  195. package/dist/src/wasm/LICENSE +0 -104
  196. package/dist/src/wasm/udoc.d.ts +0 -480
  197. package/dist/src/wasm/udoc.js +0 -1738
  198. package/dist/src/wasm/udoc_bg.wasm +0 -0
  199. package/dist/src/wasm/udoc_bg.wasm.d.ts +0 -45
  200. package/dist/src/worker/WorkerClient.d.ts +0 -335
  201. package/dist/src/worker/WorkerClient.d.ts.map +0 -1
  202. package/dist/src/worker/WorkerClient.js +0 -903
  203. package/dist/src/worker/WorkerClient.js.map +0 -1
  204. package/dist/src/worker/index.d.ts +0 -4
  205. package/dist/src/worker/index.d.ts.map +0 -1
  206. package/dist/src/worker/index.js +0 -2
  207. package/dist/src/worker/index.js.map +0 -1
  208. package/dist/src/worker/worker.d.ts +0 -437
  209. package/dist/src/worker/worker.d.ts.map +0 -1
  210. package/dist/src/worker/worker.js +0 -237
  211. 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