diva.js 7.2.5 → 7.2.6
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/README.md +65 -11
- package/build/diva.debug.js +29165 -0
- package/build/diva.esm.js +17 -0
- package/build/diva.js +17 -0
- package/package.json +15 -1
- package/.clang-format +0 -7
- package/.github/workflows/npm-publish.yml +0 -45
- package/Makefile +0 -75
- package/elm.json +0 -32
- package/review/elm.json +0 -52
- package/review/src/ReviewConfig.elm +0 -87
- package/scripts/elm-esm.sh +0 -40
- package/scripts/minify-css.mjs +0 -31
- package/src/Filters.elm +0 -1044
- package/src/Main.elm +0 -1217
- package/src/Model.elm +0 -213
- package/src/Msg.elm +0 -59
- package/src/Utilities.elm +0 -46
- package/src/View/CollectionExplorer.elm +0 -172
- package/src/View/Helpers.elm +0 -86
- package/src/View/HtmlRenderer.elm +0 -136
- package/src/View/Icons.elm +0 -159
- package/src/View/ManifestInfoModal.elm +0 -363
- package/src/View/PageViewModal.elm +0 -1046
- package/src/View/Sidebar.elm +0 -786
- package/src/View/Toolbar.elm +0 -189
- package/src/View.elm +0 -244
- package/src/diva.ts +0 -802
- package/src/filters.ts +0 -1843
- package/src/styles/app.css +0 -328
- package/src/styles/collection.css +0 -75
- package/src/styles/modal.css +0 -388
- package/src/styles/sidebar.css +0 -215
- package/src/styles/theme.css +0 -39
- package/src/styles/toolbar.css +0 -154
- package/src/viewer-element.ts +0 -1307
- package/testing/index.html +0 -52
- package/testing/testing.html +0 -231
- package/tsconfig.json +0 -12
package/src/viewer-element.ts
DELETED
|
@@ -1,1307 +0,0 @@
|
|
|
1
|
-
import type * as OpenSeadragonType from "openseadragon";
|
|
2
|
-
|
|
3
|
-
declare const OpenSeadragon: typeof OpenSeadragonType;
|
|
4
|
-
|
|
5
|
-
const ZOOM_IN_FACTOR = 1.6
|
|
6
|
-
const ZOOM_OUT_FACTOR = 1 / ZOOM_IN_FACTOR
|
|
7
|
-
const PAGE_LABEL_TOP_PADDING_PX = 28;
|
|
8
|
-
const PAGE_GAP_VIEWPORT_UNITS = 0.06;
|
|
9
|
-
|
|
10
|
-
class OsdViewer extends HTMLElement
|
|
11
|
-
{
|
|
12
|
-
private container: HTMLDivElement|null = null;
|
|
13
|
-
private viewer: OpenSeadragonType.Viewer|null = null;
|
|
14
|
-
private loadToken = 0;
|
|
15
|
-
private tileSources: string[] = [];
|
|
16
|
-
private pageLabels: string[] = [];
|
|
17
|
-
private pageAspects: number[] = [];
|
|
18
|
-
private pageOffsets: number[] = [];
|
|
19
|
-
private pageHeights: number[] = [];
|
|
20
|
-
private pageRowHeights: number[] = [];
|
|
21
|
-
private pageXOffsets: number[] = [];
|
|
22
|
-
private layoutMode: "single"|"spread"|"spread-shift" = "single";
|
|
23
|
-
private viewingDirection: "ltr"|"rtl" = "ltr";
|
|
24
|
-
private hasFitFirstPage = false;
|
|
25
|
-
private loadedIndexes: Set<number> = new Set();
|
|
26
|
-
private loadingIndexes: Set<number> = new Set();
|
|
27
|
-
private loadedItems: Map<number, any> = new Map();
|
|
28
|
-
private pageOverlayElements: Map<number, HTMLDivElement> = new Map();
|
|
29
|
-
private targetIndex: number|null = null;
|
|
30
|
-
private scrollPlaneItem: any = null;
|
|
31
|
-
private isViewportInitialized = false;
|
|
32
|
-
private lastReportedIndex: number|null = null;
|
|
33
|
-
private lastReportedZoom: number|null = null;
|
|
34
|
-
private loadingTimer: number|null = null;
|
|
35
|
-
private isLoading = false;
|
|
36
|
-
private isClamping = false;
|
|
37
|
-
private suppressPageChange = true;
|
|
38
|
-
private suppressZoomChange = true;
|
|
39
|
-
private readonly handleWheelBound: (event: WheelEvent) => void;
|
|
40
|
-
private readonly handleDoubleClickBound: (event: MouseEvent) => void;
|
|
41
|
-
private readonly handleViewportChangeBound: () => void;
|
|
42
|
-
private readonly handleAnimationFinishBound: () => void;
|
|
43
|
-
private scrollbarTrack: HTMLDivElement|null = null;
|
|
44
|
-
private scrollbarThumb: HTMLDivElement|null = null;
|
|
45
|
-
private isScrollbarDragging = false;
|
|
46
|
-
private scrollbarMouseMove: ((e: MouseEvent) => void)|null = null;
|
|
47
|
-
private scrollbarMouseUp: (() => void)|null = null;
|
|
48
|
-
|
|
49
|
-
constructor()
|
|
50
|
-
{
|
|
51
|
-
super();
|
|
52
|
-
this.handleWheelBound = this.handleWheel.bind(this);
|
|
53
|
-
this.handleDoubleClickBound = this.handleDoubleClick.bind(this);
|
|
54
|
-
this.handleViewportChangeBound = this.handleViewportChange.bind(this);
|
|
55
|
-
this.handleAnimationFinishBound = this.handleAnimationFinish.bind(this);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
connectedCallback(): void
|
|
59
|
-
{
|
|
60
|
-
this.style.display = "block";
|
|
61
|
-
if (!this.container)
|
|
62
|
-
{
|
|
63
|
-
this.container = document.createElement("div");
|
|
64
|
-
this.container.className = "osd-container";
|
|
65
|
-
this.container.style.width = "100%";
|
|
66
|
-
this.container.style.height = "100%";
|
|
67
|
-
this.container.addEventListener("wheel", this.handleWheelBound, {passive : false, capture : true});
|
|
68
|
-
this.container.addEventListener("dblclick", this.handleDoubleClickBound);
|
|
69
|
-
this.appendChild(this.container);
|
|
70
|
-
|
|
71
|
-
this.createScrollbar();
|
|
72
|
-
}
|
|
73
|
-
this.syncViewer();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
disconnectedCallback(): void
|
|
77
|
-
{
|
|
78
|
-
if (this.loadingTimer !== null)
|
|
79
|
-
{
|
|
80
|
-
window.clearTimeout(this.loadingTimer);
|
|
81
|
-
this.loadingTimer = null;
|
|
82
|
-
}
|
|
83
|
-
if (this.container)
|
|
84
|
-
{
|
|
85
|
-
this.container.removeEventListener("wheel", this.handleWheelBound);
|
|
86
|
-
this.container.removeEventListener("dblclick", this.handleDoubleClickBound);
|
|
87
|
-
}
|
|
88
|
-
if (this.scrollbarMouseMove)
|
|
89
|
-
{
|
|
90
|
-
document.removeEventListener("mousemove", this.scrollbarMouseMove);
|
|
91
|
-
this.scrollbarMouseMove = null;
|
|
92
|
-
}
|
|
93
|
-
if (this.scrollbarMouseUp)
|
|
94
|
-
{
|
|
95
|
-
document.removeEventListener("mouseup", this.scrollbarMouseUp);
|
|
96
|
-
this.scrollbarMouseUp = null;
|
|
97
|
-
}
|
|
98
|
-
if (this.viewer)
|
|
99
|
-
{
|
|
100
|
-
this.clearPageOverlays();
|
|
101
|
-
this.viewer.destroy();
|
|
102
|
-
this.viewer = null;
|
|
103
|
-
}
|
|
104
|
-
this.scrollbarTrack = null;
|
|
105
|
-
this.scrollbarThumb = null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
private syncViewer(): void
|
|
109
|
-
{
|
|
110
|
-
const hasOsd = Boolean((window as any).OpenSeadragon);
|
|
111
|
-
if (!hasOsd || !this.container)
|
|
112
|
-
{
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (!this.viewer)
|
|
117
|
-
{
|
|
118
|
-
const options = {
|
|
119
|
-
element : this.container,
|
|
120
|
-
animationTime : 0.8,
|
|
121
|
-
showNavigationControl : false,
|
|
122
|
-
preserveViewport : true,
|
|
123
|
-
visibilityRatio : 0,
|
|
124
|
-
constrainDuringPan : false,
|
|
125
|
-
minZoomLevel : 0.1,
|
|
126
|
-
minZoomImageRatio : 0.1,
|
|
127
|
-
maxZoomPixelRatio : 2,
|
|
128
|
-
defaultZoomLevel : 0,
|
|
129
|
-
sequenceMode : false,
|
|
130
|
-
zoomPerScroll : 1,
|
|
131
|
-
crossOriginPolicy : "Anonymous",
|
|
132
|
-
loadTilesWithAjax : true,
|
|
133
|
-
ajaxWithCredentials : false,
|
|
134
|
-
gestureSettingsTrackpad :
|
|
135
|
-
{pinchToZoom : true, scrollToZoom : false, flickEnabled : true, dragToPan : true},
|
|
136
|
-
gestureSettingsMouse :
|
|
137
|
-
{scrollToZoom : false, clickToZoom : false, dblClickToZoom : false, dragToPan : true},
|
|
138
|
-
gestureSettingsTouch : {pinchToZoom : false, dragToPan : true}
|
|
139
|
-
} as any;
|
|
140
|
-
this.viewer = OpenSeadragon(options);
|
|
141
|
-
const viewer = this.viewer;
|
|
142
|
-
viewer.addHandler("pan", this.handleViewportChangeBound);
|
|
143
|
-
viewer.addHandler("zoom", this.handleViewportChangeBound);
|
|
144
|
-
viewer.addHandler("pan", () => this.updateScrollbar());
|
|
145
|
-
viewer.addHandler("zoom", () => this.updateScrollbar());
|
|
146
|
-
viewer.addHandler("animation-finish", this.handleAnimationFinishBound);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
public setLayoutMode(mode: string): void
|
|
151
|
-
{
|
|
152
|
-
const nextMode = mode === "spread" || mode === "spread-shift" ? mode : "single";
|
|
153
|
-
this.applyLayoutChange({mode : nextMode});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
public setViewingDirection(direction: string): void
|
|
157
|
-
{
|
|
158
|
-
const nextDirection = direction === "rtl" ? "rtl" : "ltr";
|
|
159
|
-
this.applyLayoutChange({direction : nextDirection});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
public setLayoutConfig(mode: string, direction: string): void
|
|
163
|
-
{
|
|
164
|
-
const nextMode = mode === "spread" || mode === "spread-shift" ? mode : "single";
|
|
165
|
-
const nextDirection = direction === "rtl" ? "rtl" : "ltr";
|
|
166
|
-
this.applyLayoutChange({mode : nextMode, direction : nextDirection});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
public setTileSources(tileSources: string[]): void
|
|
170
|
-
{
|
|
171
|
-
if (!Array.isArray(tileSources) || tileSources.length === 0)
|
|
172
|
-
{
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
this.syncViewer();
|
|
177
|
-
this.resetTileSources(tileSources);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
public setPageLabels(labels: string[]): void
|
|
181
|
-
{
|
|
182
|
-
if (!Array.isArray(labels))
|
|
183
|
-
{
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
this.pageLabels = labels;
|
|
188
|
-
this.pageOverlayElements.forEach((_element, index) => {
|
|
189
|
-
this.addOrUpdatePageOverlay(index);
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private resetTileSources(tileSources: string[]): void
|
|
194
|
-
{
|
|
195
|
-
if (!this.viewer)
|
|
196
|
-
{
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
// OSD 6 keeps more internal loader/cache state; close() clears world +
|
|
200
|
-
// queues safely.
|
|
201
|
-
if (typeof this.viewer.close === "function")
|
|
202
|
-
{
|
|
203
|
-
this.viewer.close();
|
|
204
|
-
}
|
|
205
|
-
else
|
|
206
|
-
{
|
|
207
|
-
this.viewer.world.removeAll();
|
|
208
|
-
}
|
|
209
|
-
this.loadToken += 1;
|
|
210
|
-
this.tileSources = tileSources;
|
|
211
|
-
this.hasFitFirstPage = false;
|
|
212
|
-
this.isViewportInitialized = false;
|
|
213
|
-
this.loadedIndexes.clear();
|
|
214
|
-
this.loadingIndexes.clear();
|
|
215
|
-
this.loadedItems.clear();
|
|
216
|
-
this.clearPageOverlays();
|
|
217
|
-
this.targetIndex = null;
|
|
218
|
-
this.clearScrollPlane();
|
|
219
|
-
this.buildOffsets();
|
|
220
|
-
this.ensureScrollPlane();
|
|
221
|
-
this.resetLoadingState();
|
|
222
|
-
this.suppressPageChange = true;
|
|
223
|
-
this.suppressZoomChange = true;
|
|
224
|
-
|
|
225
|
-
this.maybeLoadMore();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private handleViewportChange(): void
|
|
229
|
-
{
|
|
230
|
-
if (this.isClamping)
|
|
231
|
-
{
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
const viewport = this.viewer?.viewport;
|
|
235
|
-
if (!viewport)
|
|
236
|
-
{
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
this.maybeLoadMore(viewport);
|
|
240
|
-
this.maybeEmitPageChange(viewport);
|
|
241
|
-
this.clampTop(viewport);
|
|
242
|
-
this.clampBottom(viewport);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
private handleAnimationFinish(): void
|
|
246
|
-
{
|
|
247
|
-
this.updateScrollbar();
|
|
248
|
-
this.maybeEmitZoomChange();
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
private maybeLoadMore(viewport?: OpenSeadragonType.Viewport): void
|
|
252
|
-
{
|
|
253
|
-
if (this.tileSources.length === 0)
|
|
254
|
-
{
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const vp = viewport ?? this.viewer?.viewport;
|
|
259
|
-
if (!vp)
|
|
260
|
-
{
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const bounds = vp.getBounds(true);
|
|
265
|
-
const buffer = bounds.height * 1.5;
|
|
266
|
-
const viewTop = Math.max(0, bounds.y - buffer);
|
|
267
|
-
const viewBottom = bounds.y + bounds.height + buffer;
|
|
268
|
-
|
|
269
|
-
const range = this.indicesForRange(viewTop, viewBottom);
|
|
270
|
-
if (!range)
|
|
271
|
-
{
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const start = range[0];
|
|
276
|
-
const end = range[1];
|
|
277
|
-
let index = start;
|
|
278
|
-
while (index <= end)
|
|
279
|
-
{
|
|
280
|
-
this.ensurePageLoaded(index);
|
|
281
|
-
index += 1;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (this.targetIndex !== null)
|
|
285
|
-
{
|
|
286
|
-
this.ensurePageLoaded(this.targetIndex);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
private ensurePageLoaded(index: number): void
|
|
291
|
-
{
|
|
292
|
-
if (index < 0 || index >= this.tileSources.length || index >= this.pageOffsets.length)
|
|
293
|
-
{
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (this.loadedIndexes.has(index) || this.loadingIndexes.has(index))
|
|
298
|
-
{
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
this.loadTile(index);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
private loadTile(index: number): void
|
|
306
|
-
{
|
|
307
|
-
if (!this.viewer)
|
|
308
|
-
{
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const token = this.loadToken;
|
|
313
|
-
const tileSource = this.tileSources[index];
|
|
314
|
-
|
|
315
|
-
this.loadingIndexes.add(index);
|
|
316
|
-
this.updateLoadingState();
|
|
317
|
-
const yOffset = this.pageOffsets[index] || 0;
|
|
318
|
-
const xOffset = this.pageXOffsets[index] || 0;
|
|
319
|
-
const height = this.pageHeights[index] || 1;
|
|
320
|
-
|
|
321
|
-
this.viewer.addTiledImage({
|
|
322
|
-
tileSource,
|
|
323
|
-
x : xOffset,
|
|
324
|
-
y : yOffset,
|
|
325
|
-
width : 1,
|
|
326
|
-
success : (event: any) => {
|
|
327
|
-
if (token !== this.loadToken)
|
|
328
|
-
{
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
const item = event.item;
|
|
332
|
-
item.setPosition(new OpenSeadragon.Point(xOffset, yOffset), true);
|
|
333
|
-
item.setWidth(1, true);
|
|
334
|
-
item.setHeight(height, true);
|
|
335
|
-
this.loadedIndexes.add(index);
|
|
336
|
-
this.loadedItems.set(index, item);
|
|
337
|
-
this.addOrUpdatePageOverlay(index);
|
|
338
|
-
this.loadingIndexes.delete(index);
|
|
339
|
-
this.updateLoadingState();
|
|
340
|
-
if (!this.hasFitFirstPage)
|
|
341
|
-
{
|
|
342
|
-
this.hasFitFirstPage = true;
|
|
343
|
-
const viewer = this.viewer;
|
|
344
|
-
if (!viewer || !viewer.viewport)
|
|
345
|
-
{
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
const isSingleCanvas = this.isSingleCanvasLayout();
|
|
349
|
-
const bounds = isSingleCanvas
|
|
350
|
-
? item.getBounds()
|
|
351
|
-
: (this.isSpreadMode() ? this.getRowBounds(index) : item.getBounds());
|
|
352
|
-
viewer.viewport.fitBounds(bounds, true);
|
|
353
|
-
if (!isSingleCanvas)
|
|
354
|
-
{
|
|
355
|
-
viewer.viewport.zoomBy(0.95, viewer.viewport.getCenter(true), true);
|
|
356
|
-
}
|
|
357
|
-
this.alignTopAfterFit();
|
|
358
|
-
viewer.viewport.applyConstraints();
|
|
359
|
-
this.isViewportInitialized = true;
|
|
360
|
-
this.lockHorizontalPan();
|
|
361
|
-
this.recenterAfterFit();
|
|
362
|
-
this.flushInitialPageChange();
|
|
363
|
-
this.flushInitialZoomChange();
|
|
364
|
-
}
|
|
365
|
-
if (this.targetIndex === index)
|
|
366
|
-
{
|
|
367
|
-
this.targetIndex = null;
|
|
368
|
-
}
|
|
369
|
-
this.maybeLoadMore();
|
|
370
|
-
},
|
|
371
|
-
error : () => {
|
|
372
|
-
if (token !== this.loadToken)
|
|
373
|
-
{
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
this.loadingIndexes.delete(index);
|
|
377
|
-
this.updateLoadingState();
|
|
378
|
-
this.maybeLoadMore();
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
public setPageAspects(aspects: number[]): void
|
|
384
|
-
{
|
|
385
|
-
if (!Array.isArray(aspects))
|
|
386
|
-
{
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
this.pageAspects = aspects.filter((value) => value > 0);
|
|
391
|
-
this.buildOffsets();
|
|
392
|
-
this.updateLoadedItemPositions();
|
|
393
|
-
this.ensureScrollPlane();
|
|
394
|
-
this.maybeLoadMore();
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
private buildOffsets(): void
|
|
398
|
-
{
|
|
399
|
-
const count = this.tileSources.length > 0 ? this.tileSources.length : this.pageAspects.length;
|
|
400
|
-
this.pageOffsets = new Array(count);
|
|
401
|
-
this.pageHeights = new Array(count);
|
|
402
|
-
this.pageRowHeights = new Array(count);
|
|
403
|
-
this.pageXOffsets = new Array(count);
|
|
404
|
-
|
|
405
|
-
if (this.isSpreadMode())
|
|
406
|
-
{
|
|
407
|
-
this.buildSpreadOffsets(count);
|
|
408
|
-
}
|
|
409
|
-
else
|
|
410
|
-
{
|
|
411
|
-
this.buildSingleOffsets(count);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
private buildSingleOffsets(count: number): void
|
|
416
|
-
{
|
|
417
|
-
const gap = PAGE_GAP_VIEWPORT_UNITS;
|
|
418
|
-
let current = 0;
|
|
419
|
-
let fallback = this.pageAspects[0] || 1;
|
|
420
|
-
|
|
421
|
-
for (let index = 0; index < count; index += 1)
|
|
422
|
-
{
|
|
423
|
-
const height = this.pageAspects[index] || fallback;
|
|
424
|
-
fallback = height;
|
|
425
|
-
this.pageOffsets[index] = current;
|
|
426
|
-
this.pageHeights[index] = height;
|
|
427
|
-
this.pageRowHeights[index] = height;
|
|
428
|
-
this.pageXOffsets[index] = 0;
|
|
429
|
-
current += height + gap;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
private buildSpreadOffsets(count: number): void
|
|
434
|
-
{
|
|
435
|
-
const gap = PAGE_GAP_VIEWPORT_UNITS;
|
|
436
|
-
const isRtl = this.viewingDirection === "rtl";
|
|
437
|
-
let current = 0;
|
|
438
|
-
let index = 0;
|
|
439
|
-
let fallback = this.pageAspects[0] || 1;
|
|
440
|
-
|
|
441
|
-
if (this.layoutMode === "spread-shift" && count > 0)
|
|
442
|
-
{
|
|
443
|
-
const height = this.pageAspects[0] || fallback;
|
|
444
|
-
fallback = height;
|
|
445
|
-
this.pageOffsets[0] = current;
|
|
446
|
-
this.pageHeights[0] = height;
|
|
447
|
-
this.pageRowHeights[0] = height;
|
|
448
|
-
this.pageXOffsets[0] = isRtl ? 0 : 1;
|
|
449
|
-
current += height + gap;
|
|
450
|
-
index = 1;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
while (index < count)
|
|
454
|
-
{
|
|
455
|
-
const leftHeight = this.pageAspects[index] || fallback;
|
|
456
|
-
const rightIndex = index + 1;
|
|
457
|
-
const rightHeight = rightIndex < count ? this.pageAspects[rightIndex] || leftHeight : leftHeight;
|
|
458
|
-
fallback = rightHeight;
|
|
459
|
-
|
|
460
|
-
const rowHeight = Math.max(leftHeight, rightHeight);
|
|
461
|
-
|
|
462
|
-
this.pageOffsets[index] = current;
|
|
463
|
-
this.pageHeights[index] = leftHeight;
|
|
464
|
-
this.pageRowHeights[index] = rowHeight;
|
|
465
|
-
this.pageXOffsets[index] = isRtl ? 1 : 0;
|
|
466
|
-
|
|
467
|
-
if (rightIndex < count)
|
|
468
|
-
{
|
|
469
|
-
this.pageOffsets[rightIndex] = current;
|
|
470
|
-
this.pageHeights[rightIndex] = rightHeight;
|
|
471
|
-
this.pageRowHeights[rightIndex] = rowHeight;
|
|
472
|
-
this.pageXOffsets[rightIndex] = isRtl ? 0 : 1;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
current += rowHeight + gap;
|
|
476
|
-
index += 2;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
private updateLoadedItemPositions(): void
|
|
481
|
-
{
|
|
482
|
-
if (!this.viewer)
|
|
483
|
-
{
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
this.loadedItems.forEach((item, index) => {
|
|
488
|
-
const yOffset = this.pageOffsets[index];
|
|
489
|
-
const height = this.pageHeights[index];
|
|
490
|
-
const xOffset = this.pageXOffsets[index];
|
|
491
|
-
item.setPosition(new OpenSeadragon.Point(xOffset, yOffset), true);
|
|
492
|
-
item.setWidth(1, true);
|
|
493
|
-
item.setHeight(height, true);
|
|
494
|
-
this.addOrUpdatePageOverlay(index);
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
private addOrUpdatePageOverlay(index: number): void
|
|
499
|
-
{
|
|
500
|
-
if (!this.viewer)
|
|
501
|
-
{
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
let element = this.pageOverlayElements.get(index);
|
|
506
|
-
const isRightAligned = index % 2 === 0;
|
|
507
|
-
if (!element)
|
|
508
|
-
{
|
|
509
|
-
element = document.createElement("div");
|
|
510
|
-
element.className = "diva-page-overlay-label";
|
|
511
|
-
element.style.pointerEvents = "none";
|
|
512
|
-
element.style.color = "#ffffff";
|
|
513
|
-
element.style.background = "transparent";
|
|
514
|
-
element.style.padding = "0";
|
|
515
|
-
element.style.borderRadius = "0";
|
|
516
|
-
element.style.fontSize = "12px";
|
|
517
|
-
element.style.fontWeight = "600";
|
|
518
|
-
element.style.lineHeight = "1.2";
|
|
519
|
-
element.style.whiteSpace = "nowrap";
|
|
520
|
-
element.style.paddingBottom = "6px";
|
|
521
|
-
element.style.paddingLeft = isRightAligned ? "0" : "12px";
|
|
522
|
-
element.style.paddingRight = isRightAligned ? "12px" : "0";
|
|
523
|
-
this.pageOverlayElements.set(index, element);
|
|
524
|
-
}
|
|
525
|
-
element.textContent = this.pageLabels[index] || `Page ${index + 1}`;
|
|
526
|
-
|
|
527
|
-
const xOffset = (this.pageXOffsets[index] || 0) + (isRightAligned ? 1 : 0);
|
|
528
|
-
const yOffset = this.pageOffsets[index] || 0;
|
|
529
|
-
try
|
|
530
|
-
{
|
|
531
|
-
this.viewer.removeOverlay(element);
|
|
532
|
-
}
|
|
533
|
-
catch (_error)
|
|
534
|
-
{
|
|
535
|
-
// overlay may not yet exist in viewer; safe to ignore.
|
|
536
|
-
}
|
|
537
|
-
this.viewer.addOverlay({
|
|
538
|
-
element,
|
|
539
|
-
location : new OpenSeadragon.Point(xOffset, yOffset),
|
|
540
|
-
placement : isRightAligned ? OpenSeadragon.Placement.BOTTOM_RIGHT : OpenSeadragon.Placement.BOTTOM_LEFT
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
private clearPageOverlays(): void
|
|
545
|
-
{
|
|
546
|
-
if (this.viewer)
|
|
547
|
-
{
|
|
548
|
-
this.pageOverlayElements.forEach((element) => {
|
|
549
|
-
try
|
|
550
|
-
{
|
|
551
|
-
this.viewer?.removeOverlay(element);
|
|
552
|
-
}
|
|
553
|
-
catch (_error)
|
|
554
|
-
{
|
|
555
|
-
// ignore missing overlay errors during teardown/reset.
|
|
556
|
-
}
|
|
557
|
-
element.remove();
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
this.pageOverlayElements.clear();
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
private ensureScrollPlane(): void
|
|
564
|
-
{
|
|
565
|
-
if (!this.viewer || this.pageOffsets.length === 0)
|
|
566
|
-
{
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const totalHeight = this.getTotalHeight();
|
|
571
|
-
const layoutWidth = this.isSpreadMode() ? 2 : 1;
|
|
572
|
-
if (this.scrollPlaneItem)
|
|
573
|
-
{
|
|
574
|
-
this.scrollPlaneItem.setPosition(new OpenSeadragon.Point(0, 0), true);
|
|
575
|
-
this.scrollPlaneItem.setWidth(layoutWidth, true);
|
|
576
|
-
this.scrollPlaneItem.setHeight(totalHeight, true);
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const svg =
|
|
581
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect width="100%" height="100%" fill="transparent"/></svg>`;
|
|
582
|
-
const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
583
|
-
this.viewer.addTiledImage({
|
|
584
|
-
tileSource : {type : "image", url},
|
|
585
|
-
x : 0,
|
|
586
|
-
y : 0,
|
|
587
|
-
width : layoutWidth,
|
|
588
|
-
success : (event: any) => {
|
|
589
|
-
const item = event.item;
|
|
590
|
-
item.setOpacity(0);
|
|
591
|
-
item.setPosition(new OpenSeadragon.Point(0, 0), true);
|
|
592
|
-
item.setWidth(layoutWidth, true);
|
|
593
|
-
item.setHeight(totalHeight, true);
|
|
594
|
-
this.scrollPlaneItem = item;
|
|
595
|
-
}
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
private clearScrollPlane(): void
|
|
600
|
-
{
|
|
601
|
-
if (!this.viewer || !this.scrollPlaneItem)
|
|
602
|
-
{
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
this.viewer.world.removeItem(this.scrollPlaneItem);
|
|
607
|
-
this.scrollPlaneItem = null;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
private indicesForRange(start: number, end: number): [ number, number ]|null
|
|
611
|
-
{
|
|
612
|
-
if (this.pageOffsets.length === 0)
|
|
613
|
-
{
|
|
614
|
-
return null;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const startIndex = this.findIndexForOffset(start);
|
|
618
|
-
const endIndex = this.getRowEndIndex(this.findIndexForOffset(end));
|
|
619
|
-
return [ startIndex, endIndex ];
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
private findIndexForOffset(offset: number): number
|
|
623
|
-
{
|
|
624
|
-
let low = 0;
|
|
625
|
-
let high = this.pageOffsets.length - 1;
|
|
626
|
-
|
|
627
|
-
while (low <= high)
|
|
628
|
-
{
|
|
629
|
-
const mid = Math.floor((low + high) / 2);
|
|
630
|
-
const midOffset = this.pageOffsets[mid];
|
|
631
|
-
const midHeight = this.pageRowHeights[mid] || this.pageHeights[mid] || 1;
|
|
632
|
-
if (offset < midOffset)
|
|
633
|
-
{
|
|
634
|
-
high = mid - 1;
|
|
635
|
-
}
|
|
636
|
-
else if (offset > midOffset + midHeight)
|
|
637
|
-
{
|
|
638
|
-
low = mid + 1;
|
|
639
|
-
}
|
|
640
|
-
else
|
|
641
|
-
{
|
|
642
|
-
return this.getRowStartIndex(mid);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
if (low >= this.pageOffsets.length)
|
|
647
|
-
{
|
|
648
|
-
return this.pageOffsets.length - 1;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const clamped = Math.max(0, low);
|
|
652
|
-
return this.getRowStartIndex(clamped);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
public scrollToOffset(offset: number): void
|
|
656
|
-
{
|
|
657
|
-
const viewport = this.viewer?.viewport;
|
|
658
|
-
if (!viewport)
|
|
659
|
-
{
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
const bounds = viewport.getBounds(true);
|
|
664
|
-
const width = this.isSpreadMode() ? 2 : 1;
|
|
665
|
-
const rect = new OpenSeadragon.Rect(0, offset, width, bounds.height);
|
|
666
|
-
|
|
667
|
-
viewport.fitBounds(rect, true);
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
public scrollToIndex(index: number): void
|
|
671
|
-
{
|
|
672
|
-
if (index < 0 || index >= this.tileSources.length || index >= this.pageOffsets.length)
|
|
673
|
-
{
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
const offset = this.pageOffsets[index];
|
|
678
|
-
this.scrollToOffset(offset);
|
|
679
|
-
this.targetIndex = index;
|
|
680
|
-
this.ensurePageLoaded(index);
|
|
681
|
-
this.lastReportedIndex = index;
|
|
682
|
-
this.emitCustomEvent("diva-page-change", {index});
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
public setZoomLevel(zoom: number): void
|
|
686
|
-
{
|
|
687
|
-
const viewport = this.viewer?.viewport;
|
|
688
|
-
if (!viewport)
|
|
689
|
-
{
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const center = viewport.getCenter(true);
|
|
694
|
-
viewport.zoomTo(zoom, center, false);
|
|
695
|
-
viewport.applyConstraints();
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
public zoomBy(factor: number): void
|
|
699
|
-
{
|
|
700
|
-
const viewport = this.viewer?.viewport;
|
|
701
|
-
if (!viewport)
|
|
702
|
-
{
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
const center = viewport.getCenter(true);
|
|
707
|
-
viewport.zoomBy(factor, center, false);
|
|
708
|
-
viewport.applyConstraints();
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
private handleDoubleClick(event: MouseEvent): void
|
|
712
|
-
{
|
|
713
|
-
event.preventDefault();
|
|
714
|
-
|
|
715
|
-
const viewport = this.viewer?.viewport;
|
|
716
|
-
if (!viewport)
|
|
717
|
-
{
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// matches the factor set in the elm config.
|
|
722
|
-
const zoomBy = event.shiftKey ? ZOOM_OUT_FACTOR : ZOOM_IN_FACTOR;
|
|
723
|
-
const webPoint = new OpenSeadragon.Point(event.clientX, event.clientY);
|
|
724
|
-
const viewportPoint = viewport.pointFromPixel(webPoint);
|
|
725
|
-
const nextZoom = viewport.getZoom() * zoomBy;
|
|
726
|
-
|
|
727
|
-
viewport.zoomTo(nextZoom, viewportPoint, false);
|
|
728
|
-
viewport.applyConstraints();
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
private handleWheel(event: WheelEvent): void
|
|
732
|
-
{
|
|
733
|
-
if (!this.container)
|
|
734
|
-
{
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
event.preventDefault();
|
|
738
|
-
event.stopPropagation();
|
|
739
|
-
event.stopImmediatePropagation();
|
|
740
|
-
|
|
741
|
-
const viewport = this.viewer?.viewport;
|
|
742
|
-
if (!viewport)
|
|
743
|
-
{
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
const rect = this.container.getBoundingClientRect();
|
|
748
|
-
const deltaY = event.deltaY || 0;
|
|
749
|
-
const speed = 0.75;
|
|
750
|
-
const panY = rect.height > 0 ? (deltaY / rect.height) * speed : 0;
|
|
751
|
-
|
|
752
|
-
const center = viewport.getCenter(true);
|
|
753
|
-
const nextCenter = new OpenSeadragon.Point(this.getCenterX(), center.y + panY);
|
|
754
|
-
|
|
755
|
-
viewport.panTo(nextCenter, true);
|
|
756
|
-
this.recenterBoundsX();
|
|
757
|
-
this.clampTop();
|
|
758
|
-
this.clampBottom();
|
|
759
|
-
this.maybeEmitPageChange();
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
private lockHorizontalPan(): void
|
|
763
|
-
{
|
|
764
|
-
if (!this.isViewportInitialized)
|
|
765
|
-
{
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
const viewport = this.viewer?.viewport;
|
|
770
|
-
if (!viewport)
|
|
771
|
-
{
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const center = viewport.getCenter(true);
|
|
776
|
-
viewport.panTo(new OpenSeadragon.Point(this.getCenterX(), center.y), true);
|
|
777
|
-
viewport.applyConstraints();
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
private recenterAfterFit(): void
|
|
781
|
-
{
|
|
782
|
-
if (!this.viewer)
|
|
783
|
-
{
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
const handler = () => {
|
|
788
|
-
if (this.viewer)
|
|
789
|
-
{
|
|
790
|
-
this.viewer.removeHandler("animation-finish", handler);
|
|
791
|
-
}
|
|
792
|
-
this.recenterBoundsX();
|
|
793
|
-
};
|
|
794
|
-
|
|
795
|
-
if (typeof this.viewer.addOnceHandler == "function")
|
|
796
|
-
{
|
|
797
|
-
this.viewer.addOnceHandler("animation-finish", handler);
|
|
798
|
-
}
|
|
799
|
-
else
|
|
800
|
-
{
|
|
801
|
-
this.viewer.addHandler("animation-finish", handler);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
private recenterBoundsX(): void
|
|
806
|
-
{
|
|
807
|
-
const viewport = this.viewer?.viewport;
|
|
808
|
-
if (!viewport)
|
|
809
|
-
{
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
const center = viewport.getCenter(true);
|
|
814
|
-
const targetX = this.getCenterX();
|
|
815
|
-
if (Math.abs(center.x - targetX) < 0.0005)
|
|
816
|
-
{
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
viewport.panTo(new OpenSeadragon.Point(targetX, center.y), true);
|
|
821
|
-
viewport.applyConstraints();
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
private clampTop(viewport?: OpenSeadragonType.Viewport): void
|
|
825
|
-
{
|
|
826
|
-
const vp = viewport ?? this.viewer?.viewport;
|
|
827
|
-
if (!vp)
|
|
828
|
-
{
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
const bounds = vp.getBounds(true);
|
|
833
|
-
const topPadding = this.getTopPaddingViewport(bounds.height);
|
|
834
|
-
const minTop = -topPadding;
|
|
835
|
-
if (bounds.y >= minTop)
|
|
836
|
-
{
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
const clampedCenterY = (bounds.height / 2) + minTop;
|
|
841
|
-
this.isClamping = true;
|
|
842
|
-
vp.panTo(new OpenSeadragon.Point(this.getCenterX(), clampedCenterY), true);
|
|
843
|
-
vp.applyConstraints();
|
|
844
|
-
this.isClamping = false;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
private clampBottom(viewport?: OpenSeadragonType.Viewport): void
|
|
848
|
-
{
|
|
849
|
-
const vp = viewport ?? this.viewer?.viewport;
|
|
850
|
-
if (!vp)
|
|
851
|
-
{
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
if (this.pageOffsets.length === 0)
|
|
856
|
-
{
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
const totalHeight = this.getTotalHeight();
|
|
861
|
-
|
|
862
|
-
const bounds = vp.getBounds(true);
|
|
863
|
-
const containerHeight = this.container?.getBoundingClientRect().height || 1;
|
|
864
|
-
const paddingViewport = (20 / containerHeight) * bounds.height;
|
|
865
|
-
const maxBottom = totalHeight + paddingViewport;
|
|
866
|
-
|
|
867
|
-
const bottomEdge = bounds.y + bounds.height;
|
|
868
|
-
|
|
869
|
-
if (bottomEdge <= maxBottom)
|
|
870
|
-
{
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
const clampedCenterY = maxBottom - bounds.height / 2;
|
|
875
|
-
this.isClamping = true;
|
|
876
|
-
vp.panTo(new OpenSeadragon.Point(this.getCenterX(), clampedCenterY), true);
|
|
877
|
-
vp.applyConstraints();
|
|
878
|
-
this.isClamping = false;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
private maybeEmitPageChange(viewport?: OpenSeadragonType.Viewport): void
|
|
882
|
-
{
|
|
883
|
-
if (this.suppressPageChange || this.isScrollbarDragging)
|
|
884
|
-
{
|
|
885
|
-
return;
|
|
886
|
-
}
|
|
887
|
-
if (this.pageOffsets.length === 0)
|
|
888
|
-
{
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
const vp = viewport ?? this.viewer?.viewport;
|
|
893
|
-
if (!vp)
|
|
894
|
-
{
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
const center = vp.getCenter(true);
|
|
899
|
-
const index = this.findIndexForOffset(center.y);
|
|
900
|
-
if (this.lastReportedIndex === index)
|
|
901
|
-
{
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
this.lastReportedIndex = index;
|
|
906
|
-
this.emitCustomEvent("diva-page-change", {index});
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
private emitPageChangeInstant(): void
|
|
910
|
-
{
|
|
911
|
-
if (this.pageOffsets.length === 0)
|
|
912
|
-
{
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
const viewport = this.viewer?.viewport;
|
|
917
|
-
if (!viewport)
|
|
918
|
-
{
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
const center = viewport.getCenter(true);
|
|
923
|
-
const index = this.findIndexForOffset(center.y);
|
|
924
|
-
this.lastReportedIndex = index;
|
|
925
|
-
this.emitCustomEvent("diva-page-change", {index, instant : true});
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
private flushInitialPageChange(): void
|
|
929
|
-
{
|
|
930
|
-
if (this.pageOffsets.length === 0)
|
|
931
|
-
{
|
|
932
|
-
this.suppressPageChange = false;
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
if (!this.viewer || !this.viewer.viewport)
|
|
937
|
-
{
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
const center = this.viewer.viewport.getCenter(true);
|
|
942
|
-
const index = this.findIndexForOffset(center.y);
|
|
943
|
-
this.lastReportedIndex = index;
|
|
944
|
-
this.suppressPageChange = false;
|
|
945
|
-
this.emitCustomEvent("diva-page-change", {index});
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
private maybeEmitZoomChange(viewport?: OpenSeadragonType.Viewport): void
|
|
949
|
-
{
|
|
950
|
-
if (this.suppressZoomChange)
|
|
951
|
-
{
|
|
952
|
-
return;
|
|
953
|
-
}
|
|
954
|
-
const vp = viewport ?? this.viewer?.viewport;
|
|
955
|
-
if (!vp)
|
|
956
|
-
{
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
const zoom = vp.getZoom(true);
|
|
961
|
-
if (this.lastReportedZoom !== null && Math.abs(this.lastReportedZoom - zoom) < 0.0001)
|
|
962
|
-
{
|
|
963
|
-
return;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
this.lastReportedZoom = zoom;
|
|
967
|
-
this.emitCustomEvent("diva-zoom-change", {zoom});
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
private flushInitialZoomChange(): void
|
|
971
|
-
{
|
|
972
|
-
const viewport = this.viewer?.viewport;
|
|
973
|
-
if (!viewport)
|
|
974
|
-
{
|
|
975
|
-
return;
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
const zoom = viewport.getZoom(true);
|
|
979
|
-
this.lastReportedZoom = zoom;
|
|
980
|
-
this.suppressZoomChange = false;
|
|
981
|
-
this.emitCustomEvent("diva-zoom-change", {zoom});
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
private alignTopAfterFit(): void
|
|
985
|
-
{
|
|
986
|
-
if (!this.viewer)
|
|
987
|
-
{
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
const viewport = this.viewer.viewport;
|
|
992
|
-
if (!viewport)
|
|
993
|
-
{
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
const bounds = viewport.getBounds(true);
|
|
998
|
-
const topPadding = this.getTopPaddingViewport(bounds.height);
|
|
999
|
-
const minTop = -topPadding;
|
|
1000
|
-
if (bounds.y <= minTop)
|
|
1001
|
-
{
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
const center = viewport.getCenter(true);
|
|
1006
|
-
viewport.panTo(new OpenSeadragon.Point(center.x, (bounds.height / 2) + minTop), true);
|
|
1007
|
-
viewport.applyConstraints();
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
private getTopPaddingViewport(viewportHeight: number): number
|
|
1011
|
-
{
|
|
1012
|
-
const containerHeight = this.container?.getBoundingClientRect().height || 1;
|
|
1013
|
-
return (PAGE_LABEL_TOP_PADDING_PX / containerHeight) * viewportHeight;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
private resetLoadingState(): void
|
|
1017
|
-
{
|
|
1018
|
-
if (this.loadingTimer !== null)
|
|
1019
|
-
{
|
|
1020
|
-
window.clearTimeout(this.loadingTimer);
|
|
1021
|
-
this.loadingTimer = null;
|
|
1022
|
-
}
|
|
1023
|
-
this.isLoading = false;
|
|
1024
|
-
this.emitCustomEvent("diva-loading-change", {loading : false});
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
private updateLoadingState(): void
|
|
1028
|
-
{
|
|
1029
|
-
const shouldLoad = this.loadingIndexes.size > 0;
|
|
1030
|
-
|
|
1031
|
-
if (shouldLoad)
|
|
1032
|
-
{
|
|
1033
|
-
if (this.isLoading || this.loadingTimer !== null)
|
|
1034
|
-
{
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
this.loadingTimer = window.setTimeout(() => {
|
|
1038
|
-
this.loadingTimer = null;
|
|
1039
|
-
if (this.loadingIndexes.size > 0 && !this.isLoading)
|
|
1040
|
-
{
|
|
1041
|
-
this.isLoading = true;
|
|
1042
|
-
this.emitCustomEvent("diva-loading-change", {loading : true});
|
|
1043
|
-
}
|
|
1044
|
-
}, 300);
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
if (this.loadingTimer !== null)
|
|
1049
|
-
{
|
|
1050
|
-
window.clearTimeout(this.loadingTimer);
|
|
1051
|
-
this.loadingTimer = null;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
if (this.isLoading)
|
|
1055
|
-
{
|
|
1056
|
-
this.isLoading = false;
|
|
1057
|
-
this.emitCustomEvent("diva-loading-change", {loading : false});
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
private applyLayoutChange(next: {mode?: "single"|"spread"|"spread-shift"; direction?: "ltr" | "rtl"}): void
|
|
1062
|
-
{
|
|
1063
|
-
const nextMode = next.mode ?? this.layoutMode;
|
|
1064
|
-
const nextDirection = next.direction ?? this.viewingDirection;
|
|
1065
|
-
if (this.layoutMode === nextMode && this.viewingDirection === nextDirection)
|
|
1066
|
-
{
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
let anchorIndex: number|null = null;
|
|
1071
|
-
const viewport = this.viewer?.viewport;
|
|
1072
|
-
if (viewport && this.pageOffsets.length > 0)
|
|
1073
|
-
{
|
|
1074
|
-
const bounds = viewport.getBounds(true);
|
|
1075
|
-
const anchorOffset = bounds.y + (bounds.height * 0.5);
|
|
1076
|
-
anchorIndex = this.findIndexForOffset(anchorOffset);
|
|
1077
|
-
}
|
|
1078
|
-
else if (this.lastReportedIndex !== null)
|
|
1079
|
-
{
|
|
1080
|
-
anchorIndex = this.lastReportedIndex;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
this.layoutMode = nextMode;
|
|
1084
|
-
this.viewingDirection = nextDirection;
|
|
1085
|
-
this.buildOffsets();
|
|
1086
|
-
this.updateLoadedItemPositions();
|
|
1087
|
-
this.ensureScrollPlane();
|
|
1088
|
-
|
|
1089
|
-
if (anchorIndex !== null)
|
|
1090
|
-
{
|
|
1091
|
-
const clampedAnchor = Math.max(0, Math.min(anchorIndex, this.pageOffsets.length - 1));
|
|
1092
|
-
this.scrollToIndex(clampedAnchor);
|
|
1093
|
-
}
|
|
1094
|
-
else
|
|
1095
|
-
{
|
|
1096
|
-
this.lockHorizontalPan();
|
|
1097
|
-
this.recenterAfterFit();
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
this.maybeLoadMore();
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
private emitCustomEvent(name: string, detail: Record<string, any>): void
|
|
1104
|
-
{
|
|
1105
|
-
this.dispatchEvent(new CustomEvent(name, {detail}));
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
private getCenterX(): number { return this.isSpreadMode() ? 1 : 0.5; }
|
|
1109
|
-
|
|
1110
|
-
private isSingleCanvasLayout(): boolean
|
|
1111
|
-
{
|
|
1112
|
-
return this.tileSources.length === 1 && this.pageOffsets.length === 1;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
private getTotalHeight(): number
|
|
1116
|
-
{
|
|
1117
|
-
const lastIndex = this.pageOffsets.length - 1;
|
|
1118
|
-
return this.pageOffsets[lastIndex] + (this.pageRowHeights[lastIndex] || 1);
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
private getRowBounds(index: number): OpenSeadragonType.Rect
|
|
1122
|
-
{
|
|
1123
|
-
const yOffset = this.pageOffsets[index] || 0;
|
|
1124
|
-
const rowHeight = this.pageRowHeights[index] || this.pageHeights[index] || 1;
|
|
1125
|
-
const width = this.isSpreadMode() ? 2 : 1;
|
|
1126
|
-
return new OpenSeadragon.Rect(0, yOffset, width, rowHeight);
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
private isSpreadMode(): boolean { return this.layoutMode !== "single"; }
|
|
1130
|
-
|
|
1131
|
-
private getRowStartIndex(index: number): number
|
|
1132
|
-
{
|
|
1133
|
-
if (!this.isSpreadMode())
|
|
1134
|
-
{
|
|
1135
|
-
return index;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
if (this.layoutMode === "spread-shift")
|
|
1139
|
-
{
|
|
1140
|
-
if (index === 0)
|
|
1141
|
-
{
|
|
1142
|
-
return 0;
|
|
1143
|
-
}
|
|
1144
|
-
return index % 2 === 1 ? index : index - 1;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
return index - (index % 2);
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
private getRowEndIndex(startIndex: number): number
|
|
1151
|
-
{
|
|
1152
|
-
const maxIndex = this.pageOffsets.length - 1;
|
|
1153
|
-
if (!this.isSpreadMode())
|
|
1154
|
-
{
|
|
1155
|
-
return startIndex;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
if (this.layoutMode === "spread-shift" && startIndex === 0)
|
|
1159
|
-
{
|
|
1160
|
-
return 0;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
return Math.min(startIndex + 1, maxIndex);
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
private createScrollbar(): void
|
|
1167
|
-
{
|
|
1168
|
-
if (!this.container)
|
|
1169
|
-
{
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
this.scrollbarTrack = document.createElement("div");
|
|
1174
|
-
this.scrollbarTrack.className = "diva-scrollbar-track";
|
|
1175
|
-
|
|
1176
|
-
this.scrollbarThumb = document.createElement("div");
|
|
1177
|
-
this.scrollbarThumb.className = "diva-scrollbar-thumb";
|
|
1178
|
-
|
|
1179
|
-
this.scrollbarTrack.appendChild(this.scrollbarThumb);
|
|
1180
|
-
this.container.appendChild(this.scrollbarTrack);
|
|
1181
|
-
|
|
1182
|
-
this.setupScrollbarDrag();
|
|
1183
|
-
this.setupScrollbarTrackClick();
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
private updateScrollbar(): void
|
|
1187
|
-
{
|
|
1188
|
-
if (!this.viewer || !this.scrollbarThumb || !this.scrollbarTrack)
|
|
1189
|
-
{
|
|
1190
|
-
return;
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
if (this.pageOffsets.length === 0)
|
|
1194
|
-
{
|
|
1195
|
-
return;
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
const bounds = this.viewer.viewport.getBounds(true);
|
|
1199
|
-
const trackHeight = this.scrollbarTrack.clientHeight;
|
|
1200
|
-
|
|
1201
|
-
const totalHeight = this.getTotalHeight();
|
|
1202
|
-
const viewportHeight = bounds.height;
|
|
1203
|
-
const scrollTop = bounds.y;
|
|
1204
|
-
|
|
1205
|
-
const thumbHeight = Math.max(30, (viewportHeight / totalHeight) * trackHeight);
|
|
1206
|
-
|
|
1207
|
-
const maxScroll = totalHeight - viewportHeight;
|
|
1208
|
-
const scrollProgress = maxScroll > 0 ? Math.max(0, Math.min(1, scrollTop / maxScroll)) : 0;
|
|
1209
|
-
const thumbTop = scrollProgress * (trackHeight - thumbHeight);
|
|
1210
|
-
|
|
1211
|
-
this.scrollbarThumb.style.height = `${thumbHeight}px`;
|
|
1212
|
-
this.scrollbarThumb.style.top = `${Math.max(0, thumbTop)}px`;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
private setupScrollbarDrag(): void
|
|
1216
|
-
{
|
|
1217
|
-
if (!this.scrollbarThumb || !this.scrollbarTrack)
|
|
1218
|
-
{
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
let isDragging = false;
|
|
1223
|
-
let startY = 0;
|
|
1224
|
-
let startThumbTop = 0;
|
|
1225
|
-
|
|
1226
|
-
const onMouseDown = (e: MouseEvent): void => {
|
|
1227
|
-
isDragging = true;
|
|
1228
|
-
this.isScrollbarDragging = true;
|
|
1229
|
-
startY = e.clientY;
|
|
1230
|
-
startThumbTop = this.scrollbarThumb?.offsetTop || 0;
|
|
1231
|
-
e.preventDefault();
|
|
1232
|
-
e.stopPropagation();
|
|
1233
|
-
};
|
|
1234
|
-
|
|
1235
|
-
const onMouseMove = (e: MouseEvent): void => {
|
|
1236
|
-
if (!isDragging || !this.scrollbarTrack || !this.scrollbarThumb || !this.viewer)
|
|
1237
|
-
{
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
const deltaY = e.clientY - startY;
|
|
1242
|
-
const newThumbTop = startThumbTop + deltaY;
|
|
1243
|
-
const trackHeight = this.scrollbarTrack.clientHeight;
|
|
1244
|
-
const thumbHeight = this.scrollbarThumb.clientHeight;
|
|
1245
|
-
|
|
1246
|
-
const clampedThumbTop = Math.max(0, Math.min(newThumbTop, trackHeight - thumbHeight));
|
|
1247
|
-
const scrollProgress = (trackHeight - thumbHeight) > 0 ? clampedThumbTop / (trackHeight - thumbHeight) : 0;
|
|
1248
|
-
|
|
1249
|
-
const totalHeight = this.getTotalHeight();
|
|
1250
|
-
const viewportHeight = this.viewer.viewport.getBounds(true).height;
|
|
1251
|
-
const maxScroll = totalHeight - viewportHeight;
|
|
1252
|
-
const newScrollY = scrollProgress * maxScroll;
|
|
1253
|
-
|
|
1254
|
-
this.scrollToOffset(newScrollY);
|
|
1255
|
-
};
|
|
1256
|
-
|
|
1257
|
-
const onMouseUp = (): void => {
|
|
1258
|
-
if (isDragging)
|
|
1259
|
-
{
|
|
1260
|
-
isDragging = false;
|
|
1261
|
-
this.isScrollbarDragging = false;
|
|
1262
|
-
this.emitPageChangeInstant();
|
|
1263
|
-
}
|
|
1264
|
-
};
|
|
1265
|
-
|
|
1266
|
-
this.scrollbarMouseMove = onMouseMove;
|
|
1267
|
-
this.scrollbarMouseUp = onMouseUp;
|
|
1268
|
-
this.scrollbarThumb.addEventListener("mousedown", onMouseDown);
|
|
1269
|
-
document.addEventListener("mousemove", onMouseMove);
|
|
1270
|
-
document.addEventListener("mouseup", onMouseUp);
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
private setupScrollbarTrackClick(): void
|
|
1274
|
-
{
|
|
1275
|
-
if (!this.scrollbarTrack)
|
|
1276
|
-
{
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
this.scrollbarTrack.addEventListener("click", (e: MouseEvent) => {
|
|
1281
|
-
if (e.target === this.scrollbarThumb || !this.viewer)
|
|
1282
|
-
{
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
const rect = this.scrollbarTrack?.getBoundingClientRect();
|
|
1287
|
-
if (!rect)
|
|
1288
|
-
{
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
const clickY = e.clientY - rect.top;
|
|
1293
|
-
const trackHeight = rect.height;
|
|
1294
|
-
|
|
1295
|
-
const scrollProgress = Math.max(0, Math.min(1, clickY / trackHeight));
|
|
1296
|
-
|
|
1297
|
-
const totalHeight = this.getTotalHeight();
|
|
1298
|
-
const viewportHeight = this.viewer.viewport.getBounds(true).height;
|
|
1299
|
-
const maxScroll = totalHeight - viewportHeight;
|
|
1300
|
-
const newScrollY = scrollProgress * maxScroll;
|
|
1301
|
-
|
|
1302
|
-
this.scrollToOffset(newScrollY);
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
customElements.define("osd-viewer", OsdViewer);
|