@laminapdf/viewer 0.2.0-alpha.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -6
- package/dist/client.d.ts +60 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +219 -22
- package/dist/client.js.map +1 -1
- package/dist/edit/session.d.ts +113 -0
- package/dist/edit/session.d.ts.map +1 -0
- package/dist/edit/session.js +232 -0
- package/dist/edit/session.js.map +1 -0
- package/dist/edit-log.d.ts +37 -0
- package/dist/edit-log.d.ts.map +1 -0
- package/dist/edit-log.js +98 -0
- package/dist/edit-log.js.map +1 -0
- package/dist/find/controller.d.ts +6 -0
- package/dist/find/controller.d.ts.map +1 -1
- package/dist/find/controller.js +69 -2
- package/dist/find/controller.js.map +1 -1
- package/dist/history/history.d.ts +9 -0
- package/dist/history/history.d.ts.map +1 -1
- package/dist/history/history.js.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/layers/link-layer.d.ts +33 -0
- package/dist/layers/link-layer.d.ts.map +1 -1
- package/dist/layers/link-layer.js +251 -3
- package/dist/layers/link-layer.js.map +1 -1
- package/dist/layers/link-preview.d.ts +77 -0
- package/dist/layers/link-preview.d.ts.map +1 -0
- package/dist/layers/link-preview.js +202 -0
- package/dist/layers/link-preview.js.map +1 -0
- package/dist/layers/text-layer.d.ts +7 -3
- package/dist/layers/text-layer.d.ts.map +1 -1
- package/dist/layers/text-layer.js +3 -5
- package/dist/layers/text-layer.js.map +1 -1
- package/dist/layout/page-layout.d.ts +7 -0
- package/dist/layout/page-layout.d.ts.map +1 -1
- package/dist/layout/page-layout.js +15 -0
- package/dist/layout/page-layout.js.map +1 -1
- package/dist/protocol.d.ts +145 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +17 -2
- package/dist/protocol.js.map +1 -1
- package/dist/thumbnails/strip.d.ts.map +1 -1
- package/dist/thumbnails/strip.js +34 -1
- package/dist/thumbnails/strip.js.map +1 -1
- package/dist/view/previews.d.ts +47 -0
- package/dist/view/previews.d.ts.map +1 -0
- package/dist/view/previews.js +97 -0
- package/dist/view/previews.js.map +1 -0
- package/dist/view/viewer.d.ts +85 -7
- package/dist/view/viewer.d.ts.map +1 -1
- package/dist/view/viewer.js +347 -12
- package/dist/view/viewer.js.map +1 -1
- package/dist/worker/document.d.ts +56 -5
- package/dist/worker/document.d.ts.map +1 -1
- package/dist/worker/document.js +73 -2
- package/dist/worker/document.js.map +1 -1
- package/dist/worker/edit.d.ts +60 -0
- package/dist/worker/edit.d.ts.map +1 -0
- package/dist/worker/edit.js +383 -0
- package/dist/worker/edit.js.map +1 -0
- package/dist/worker/find.d.ts +2 -2
- package/dist/worker/find.d.ts.map +1 -1
- package/dist/worker/find.js +3 -3
- package/dist/worker/find.js.map +1 -1
- package/dist/worker/fingerprint.d.ts +10 -0
- package/dist/worker/fingerprint.d.ts.map +1 -0
- package/dist/worker/fingerprint.js +42 -0
- package/dist/worker/fingerprint.js.map +1 -0
- package/dist/worker/methods.d.ts.map +1 -1
- package/dist/worker/methods.js +34 -0
- package/dist/worker/methods.js.map +1 -1
- package/dist/worker/navigation.d.ts +2 -2
- package/dist/worker/navigation.d.ts.map +1 -1
- package/dist/worker/navigation.js +3 -3
- package/dist/worker/navigation.js.map +1 -1
- package/dist/worker/recent-pages.d.ts +2 -0
- package/dist/worker/recent-pages.d.ts.map +1 -1
- package/dist/worker/recent-pages.js +15 -0
- package/dist/worker/recent-pages.js.map +1 -1
- package/dist/worker/session.d.ts +61 -0
- package/dist/worker/session.d.ts.map +1 -0
- package/dist/worker/session.js +159 -0
- package/dist/worker/session.js.map +1 -0
- package/dist/worker/text.d.ts +2 -2
- package/dist/worker/text.d.ts.map +1 -1
- package/dist/worker/text.js +5 -4
- package/dist/worker/text.js.map +1 -1
- package/package.json +16 -10
package/dist/view/viewer.js
CHANGED
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
import { pagePixels } from "@laminapdf/render";
|
|
12
12
|
import { BitmapCache } from "../canvas/cache.js";
|
|
13
13
|
import { CanvasPool } from "../canvas/pool.js";
|
|
14
|
+
import { EditSession } from "../edit/session.js";
|
|
14
15
|
import { FindController } from "../find/controller.js";
|
|
15
|
-
import { NavigationHistory } from "../history/history.js";
|
|
16
|
+
import { NavigationHistory, } from "../history/history.js";
|
|
16
17
|
import { HighlightLayer } from "../layers/highlight-layer.js";
|
|
17
18
|
import { LinkLayer } from "../layers/link-layer.js";
|
|
18
19
|
import { TextLayer } from "../layers/text-layer.js";
|
|
19
|
-
import { isRotation, PageLayout, rotated } from "../layout/page-layout.js";
|
|
20
|
+
import { isRotation, keptTop, PageLayout, rotated } from "../layout/page-layout.js";
|
|
20
21
|
import { range } from "../layout/range.js";
|
|
21
|
-
import { appearanceField, appearanceKey,
|
|
22
|
+
import { appearanceField, appearanceKey, renderKeyFields, viewKey, } from "../protocol.js";
|
|
22
23
|
import { backdropRatio, bounds, contains, lowScale, sharpScale, tilesFor } from "../render/tiles.js";
|
|
23
24
|
import { destinationView } from "../zoom/destination.js";
|
|
24
25
|
import { contentPosition, percentOfScale, pointAt, scaleFor, scrollToPoint, } from "../zoom/zoom.js";
|
|
@@ -82,6 +83,9 @@ export class Viewer {
|
|
|
82
83
|
#highlights;
|
|
83
84
|
/** The link layer, unless `links: false`. */
|
|
84
85
|
#links;
|
|
86
|
+
/** The link previews' pictures, loaded with their code on the first preview (not at startup). */
|
|
87
|
+
#previews;
|
|
88
|
+
#loadingPreviews;
|
|
85
89
|
/** Find in this viewer's document: query, options, matches, next and previous. */
|
|
86
90
|
find;
|
|
87
91
|
/**
|
|
@@ -89,6 +93,11 @@ export class Viewer {
|
|
|
89
93
|
* `jumpToDestination` and find's matches record a jump; scrolling doesn't.
|
|
90
94
|
*/
|
|
91
95
|
history;
|
|
96
|
+
/**
|
|
97
|
+
* The open document's edit session: its history, and edits, undo, redo and going to a step,
|
|
98
|
+
* each redrawing the pages it touched. One call runs at a time, in the order they were made.
|
|
99
|
+
*/
|
|
100
|
+
edits;
|
|
92
101
|
/** Pages kept mounted whatever the scroll (`keepMounted`): those a selection touches. */
|
|
93
102
|
#pinned = new Set();
|
|
94
103
|
#regions;
|
|
@@ -99,11 +108,21 @@ export class Viewer {
|
|
|
99
108
|
#listeners = new Set();
|
|
100
109
|
#events = new Map();
|
|
101
110
|
#cleanup = [];
|
|
102
|
-
|
|
111
|
+
/** The open document's ID (renders name it), or undefined before one opens. */
|
|
112
|
+
#documentId;
|
|
113
|
+
/** Counts `open`s: an edit answered for a document since replaced redraws nothing. */
|
|
114
|
+
#opens = 0;
|
|
103
115
|
#sizes = [];
|
|
116
|
+
/** Each edited page's revision (`invalidate` bumps it), part of its bitmaps' cache keys. */
|
|
117
|
+
#revisions = new Map();
|
|
104
118
|
#layout;
|
|
105
119
|
#zoom;
|
|
106
120
|
#rotation = 0;
|
|
121
|
+
/**
|
|
122
|
+
* Where the view goes once it has a size, when it was opened or restored without one (a hidden
|
|
123
|
+
* host ignores scrolling): the top of a new document, or a kept position.
|
|
124
|
+
*/
|
|
125
|
+
#pending;
|
|
107
126
|
#state;
|
|
108
127
|
#frame;
|
|
109
128
|
#settling;
|
|
@@ -173,6 +192,30 @@ export class Viewer {
|
|
|
173
192
|
});
|
|
174
193
|
this.addLayer(this.#highlights);
|
|
175
194
|
this.#links = this.#linkLayer(options.links);
|
|
195
|
+
this.edits = this.#editSession();
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The edit session over this view's calls, which follows a restarted worker: its replay tells
|
|
199
|
+
* the session its history (`edit`), and lost edits leave it empty.
|
|
200
|
+
*/
|
|
201
|
+
#editSession() {
|
|
202
|
+
const edits = new EditSession({
|
|
203
|
+
applyEdit: (ops, call) => this.applyEdit(ops, call),
|
|
204
|
+
undoEdit: () => this.#editCall(() => this.#client.undoEdit()),
|
|
205
|
+
redoEdit: () => this.#editCall(() => this.#client.redoEdit()),
|
|
206
|
+
goToStep: (position) => this.#editCall(() => this.#client.goToStep(position)),
|
|
207
|
+
editHistory: () => this.#client.editHistory(),
|
|
208
|
+
planSave: (options) => this.#client.planSave(options),
|
|
209
|
+
copyEdits: (options) => this.#client.copyEdits(options),
|
|
210
|
+
adoptEdits: (token) => this.#client.adoptEdits(token),
|
|
211
|
+
});
|
|
212
|
+
this.#cleanup.push(this.#client.on("restarted", ({ reopened, edits: replay }) => {
|
|
213
|
+
if (reopened)
|
|
214
|
+
this.#reopened();
|
|
215
|
+
if (replay === "lost")
|
|
216
|
+
edits.reset(this.#client.document);
|
|
217
|
+
}), this.#client.on("edit", ({ history }) => edits.follow(history)));
|
|
218
|
+
return edits;
|
|
176
219
|
}
|
|
177
220
|
/** The scroll container (append it, or use {@link mount}). */
|
|
178
221
|
get element() {
|
|
@@ -187,16 +230,22 @@ export class Viewer {
|
|
|
187
230
|
open(document) {
|
|
188
231
|
this.#unmountAll();
|
|
189
232
|
this.#cache.clear();
|
|
190
|
-
this.#
|
|
233
|
+
this.#previews?.clear();
|
|
234
|
+
this.#documentId = document.id;
|
|
235
|
+
this.#opens++;
|
|
236
|
+
this.#revisions = new Map();
|
|
191
237
|
this.#pinned = new Set();
|
|
192
238
|
this.#focusTarget = undefined;
|
|
193
239
|
this.find.reset();
|
|
194
240
|
this.history.reset();
|
|
195
|
-
this
|
|
241
|
+
this.edits.reset(document);
|
|
242
|
+
this.#text?.open();
|
|
196
243
|
this.#sizes = document.pages.map((p) => ({ width: p.width, height: p.height }));
|
|
197
244
|
this.#layout = undefined;
|
|
198
245
|
this.#root.scrollTop = 0;
|
|
199
246
|
this.#root.scrollLeft = 0;
|
|
247
|
+
// A hidden view keeps its old scroll: the new document starts at its top once it shows.
|
|
248
|
+
this.#pending = this.#sized() ? undefined : "top";
|
|
200
249
|
this.#relayout(this.#viewport());
|
|
201
250
|
this.#schedule();
|
|
202
251
|
}
|
|
@@ -353,9 +402,153 @@ export class Viewer {
|
|
|
353
402
|
this.#releaseCanvas(view);
|
|
354
403
|
}
|
|
355
404
|
}
|
|
405
|
+
this.#previews?.clear();
|
|
356
406
|
this.#setState({ appearance });
|
|
357
407
|
this.#schedule();
|
|
358
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Where `target` goes, as a picture for a link preview (R07): one render of the region around its
|
|
411
|
+
* point (or the top of its page) through the document worker, and the text of the lines it shows.
|
|
412
|
+
* The viewer keeps the last few (`PREVIEW_CACHE`) and closes them on a zoom, rotation or
|
|
413
|
+
* appearance change and when the document closes, so draw the bitmap at once and never close it.
|
|
414
|
+
* Rejects with `AbortError` when `signal` aborts or the pictures are cleared while it draws.
|
|
415
|
+
*/
|
|
416
|
+
preview(target, options = {}) {
|
|
417
|
+
return this.#linkPreviews().then((previews) => previews.picture(target, options.signal ?? new AbortController().signal));
|
|
418
|
+
}
|
|
419
|
+
/** The pointer is over the link preview (`true`) or has left it: a hovered preview stays (WCAG 1.4.13). */
|
|
420
|
+
holdLinkPreview(held) {
|
|
421
|
+
this.#links?.hold(held);
|
|
422
|
+
}
|
|
423
|
+
/** Escape or Close on the link preview: it goes, and stays hidden until its link is left. */
|
|
424
|
+
dismissLinkPreview() {
|
|
425
|
+
this.#links?.dismiss();
|
|
426
|
+
}
|
|
427
|
+
/** Whether a link is previewed or focused, so {@link followLinkPreview} has one (`nav.follow-link`). */
|
|
428
|
+
canFollowLink() {
|
|
429
|
+
return this.#links?.canFollow() ?? false;
|
|
430
|
+
}
|
|
431
|
+
/** Follows the previewed link, else the focused one (`nav.follow-link`); `false` when there is neither. */
|
|
432
|
+
followLinkPreview() {
|
|
433
|
+
return this.#links?.follow() ?? false;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Draws `pages` again: an edit changed them. Their bitmaps go, here and in a thumbnail strip,
|
|
437
|
+
* and their layers read their text and links again; every other page keeps what it drew.
|
|
438
|
+
* `sizes` gives the pages whose displayed size in points changed (a quarter turn swaps width and
|
|
439
|
+
* height): the layout takes them, and the page at the top of the viewport stays there. A page in
|
|
440
|
+
* view keeps its old pixels until the new ones are drawn over them.
|
|
441
|
+
*/
|
|
442
|
+
invalidate(pages, sizes) {
|
|
443
|
+
const count = this.#sizes.length;
|
|
444
|
+
const changed = new Set([...pages, ...(sizes?.keys() ?? [])].filter((p) => Number.isInteger(p) && p >= 0 && p < count));
|
|
445
|
+
if (changed.size === 0)
|
|
446
|
+
return;
|
|
447
|
+
const resized = new Set();
|
|
448
|
+
for (const [page, size] of sizes ?? []) {
|
|
449
|
+
const old = this.#sizes[page];
|
|
450
|
+
if (old === undefined || (old.width === size.width && old.height === size.height))
|
|
451
|
+
continue;
|
|
452
|
+
this.#sizes[page] = { width: size.width, height: size.height };
|
|
453
|
+
resized.add(page);
|
|
454
|
+
}
|
|
455
|
+
for (const page of changed) {
|
|
456
|
+
this.#revisions.set(page, (this.#revisions.get(page) ?? 0) + 1);
|
|
457
|
+
this.#cache.prune(page, () => false);
|
|
458
|
+
}
|
|
459
|
+
if (resized.size > 0)
|
|
460
|
+
this.#resize();
|
|
461
|
+
const { first, last } = this.#state.visibleRange;
|
|
462
|
+
for (const page of changed) {
|
|
463
|
+
const view = this.#pages.get(page);
|
|
464
|
+
if (view === undefined)
|
|
465
|
+
continue;
|
|
466
|
+
this.#redraw(view, resized.has(page) || page < first || page > last);
|
|
467
|
+
this.#remountLayers(view);
|
|
468
|
+
}
|
|
469
|
+
this.find.refresh([...changed]);
|
|
470
|
+
const list = [...changed].sort((a, b) => a - b);
|
|
471
|
+
this.#emit("invalidate", { pages: list.map((index) => ({ index, ...this.#sizes[index] })) });
|
|
472
|
+
this.#emit("renderstats", this.stats());
|
|
473
|
+
this.#schedule();
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Applies `ops` to the open document through the client (`ViewerClient.applyEdit`) and redraws
|
|
477
|
+
* the pages they touched ({@link invalidate}): an edit on screen in one call. A failed edit
|
|
478
|
+
* changes nothing and redraws nothing.
|
|
479
|
+
*/
|
|
480
|
+
applyEdit(ops, options) {
|
|
481
|
+
return this.#editCall(() => this.#client.applyEdit(ops, options));
|
|
482
|
+
}
|
|
483
|
+
/** Runs an edit session call and redraws the pages it touched, unless another document opened meanwhile. */
|
|
484
|
+
async #editCall(call) {
|
|
485
|
+
const opens = this.#opens;
|
|
486
|
+
const result = await call();
|
|
487
|
+
// Another document opened meanwhile: these pages aren't its pages.
|
|
488
|
+
if (this.#opens === opens) {
|
|
489
|
+
this.invalidate(result.pages.map((page) => page.index), new Map(result.pages.map((page) => [page.index, page])));
|
|
490
|
+
}
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Every page's displayed size in points, as the view lays it out: the sizes the document opened
|
|
495
|
+
* with, and those edits gave since (`invalidate`). A thumbnail strip lays its pages out from it.
|
|
496
|
+
* With `documentId`, empty unless the view shows that document: a page count alone can't tell two
|
|
497
|
+
* documents apart.
|
|
498
|
+
*/
|
|
499
|
+
pageSizes(documentId) {
|
|
500
|
+
if (documentId !== undefined && documentId !== this.#documentId)
|
|
501
|
+
return [];
|
|
502
|
+
return this.#sizes;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Where the reader is: the document point a gap below the viewport's top-left, the zoom and the
|
|
506
|
+
* rotation; `undefined` without a document. {@link restore} puts the view back there.
|
|
507
|
+
*/
|
|
508
|
+
viewPosition() {
|
|
509
|
+
const pending = this.#pending;
|
|
510
|
+
if (pending === "top" && this.#sizes.length > 0) {
|
|
511
|
+
return { point: { page: 0, x: 0, y: 0 }, zoom: this.#zoom, rotation: this.#rotation };
|
|
512
|
+
}
|
|
513
|
+
if (pending !== undefined && pending !== "top")
|
|
514
|
+
return pending;
|
|
515
|
+
const place = this.#place();
|
|
516
|
+
return place === undefined ? undefined : { ...place, rotation: this.#rotation };
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Puts the view at `position` (a {@link viewPosition} kept from an earlier visit), as a history
|
|
520
|
+
* jump from where the view is, so Back returns there. Its page is clamped to the document's and
|
|
521
|
+
* its point to the page. Called in the task that opened the document, before the next frame, it
|
|
522
|
+
* paints no other page first.
|
|
523
|
+
*/
|
|
524
|
+
restore(position) {
|
|
525
|
+
if (this.#layout === undefined || this.#sizes.length === 0)
|
|
526
|
+
return;
|
|
527
|
+
// Hidden: it takes place once the view has a size (`#listenResize`).
|
|
528
|
+
if (!this.#sized()) {
|
|
529
|
+
const page = Math.min(this.#sizes.length - 1, Math.max(0, Math.trunc(position.point.page) || 0));
|
|
530
|
+
this.#pending = { ...position, point: { ...position.point, page } };
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
this.#pending = undefined;
|
|
534
|
+
this.history.jump(() => {
|
|
535
|
+
const turned = isRotation(position.rotation) && position.rotation !== this.#rotation;
|
|
536
|
+
if (turned)
|
|
537
|
+
this.#rotation = position.rotation;
|
|
538
|
+
const clamp = (v) => (Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : 0);
|
|
539
|
+
const page = Math.min(this.#sizes.length - 1, Math.max(0, Math.trunc(position.point.page) || 0));
|
|
540
|
+
// The zoom is set before the scroll: #show only rezooms when the zoom changes.
|
|
541
|
+
this.#zoom = position.zoom;
|
|
542
|
+
this.#rezoom(undefined);
|
|
543
|
+
this.#show({ point: { page, x: clamp(position.point.x), y: clamp(position.point.y) }, zoom: position.zoom });
|
|
544
|
+
// As rotate() does: the layers follow once the pages are laid out for the new rotation.
|
|
545
|
+
if (turned) {
|
|
546
|
+
this.#text?.refresh();
|
|
547
|
+
this.#highlights.refresh();
|
|
548
|
+
this.#links?.refresh();
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
359
552
|
/** The document point at viewport position (`x`, `y`) in CSS px. */
|
|
360
553
|
pointAt(x, y) {
|
|
361
554
|
const layout = this.#layout;
|
|
@@ -388,6 +581,7 @@ export class Viewer {
|
|
|
388
581
|
dropped: this.#dropped,
|
|
389
582
|
aborted: this.#aborted,
|
|
390
583
|
cacheBytes: this.#cache.bytes,
|
|
584
|
+
previewBitmaps: this.#previews?.size ?? 0,
|
|
391
585
|
pages,
|
|
392
586
|
};
|
|
393
587
|
}
|
|
@@ -399,6 +593,7 @@ export class Viewer {
|
|
|
399
593
|
clearTimeout(this.#settling);
|
|
400
594
|
this.#unmountAll();
|
|
401
595
|
this.#cache.clear();
|
|
596
|
+
this.#previews?.clear();
|
|
402
597
|
this.find.reset();
|
|
403
598
|
this.#text?.destroy();
|
|
404
599
|
this.#links?.destroy();
|
|
@@ -407,6 +602,24 @@ export class Viewer {
|
|
|
407
602
|
this.#root.remove();
|
|
408
603
|
}
|
|
409
604
|
// --- internals ---
|
|
605
|
+
/**
|
|
606
|
+
* The link previews' pictures, drawn at this viewer's zoom, rotation and appearance. Their code
|
|
607
|
+
* loads on the first preview, so the reader's startup script doesn't carry it.
|
|
608
|
+
*/
|
|
609
|
+
#linkPreviews() {
|
|
610
|
+
this.#loadingPreviews ??= import("./previews.js").then(({ LinkPreviews }) => {
|
|
611
|
+
this.#previews = new LinkPreviews({
|
|
612
|
+
client: this.#client,
|
|
613
|
+
size: (page) => this.#sizes[page],
|
|
614
|
+
rotation: () => this.#rotation,
|
|
615
|
+
percent: () => this.#state.percent,
|
|
616
|
+
appearance: () => this.#state.appearance,
|
|
617
|
+
dpr: () => this.#dpr(),
|
|
618
|
+
});
|
|
619
|
+
return this.#previews;
|
|
620
|
+
});
|
|
621
|
+
return this.#loadingPreviews;
|
|
622
|
+
}
|
|
410
623
|
/** The link layer, added to every page, unless `links` is `false`. */
|
|
411
624
|
#linkLayer(links) {
|
|
412
625
|
if (links === false)
|
|
@@ -419,6 +632,8 @@ export class Viewer {
|
|
|
419
632
|
pageCount: () => this.#sizes.length,
|
|
420
633
|
goTo: (target) => this.jumpToDestination(target),
|
|
421
634
|
goToPage: (page) => this.jumpToPage(page),
|
|
635
|
+
preview: (request) => this.#emit("linkpreview", request),
|
|
636
|
+
previewEnd: (anchor) => this.#emit("linkpreviewend", { anchor }),
|
|
422
637
|
}, links === true || links === undefined ? {} : links);
|
|
423
638
|
this.addLayer(layer);
|
|
424
639
|
return layer;
|
|
@@ -486,12 +701,104 @@ export class Viewer {
|
|
|
486
701
|
}
|
|
487
702
|
this.#schedule();
|
|
488
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* The worker restarted and opened the file again as it was, without the edits: the edited pages
|
|
706
|
+
* take their sizes from it and draw again (at a new revision, so no bitmap of theirs is reused).
|
|
707
|
+
*/
|
|
708
|
+
#reopened() {
|
|
709
|
+
const document = this.#client.document;
|
|
710
|
+
if (document === undefined || document.pages.length !== this.#sizes.length)
|
|
711
|
+
return;
|
|
712
|
+
this.#documentId = document.id;
|
|
713
|
+
const edited = [...this.#revisions.keys()];
|
|
714
|
+
const sizes = new Map(edited.map((index) => [index, document.pages[index]]));
|
|
715
|
+
this.invalidate(edited, sizes);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Lays the pages out again after some changed size, keeping the page at the viewport's top where
|
|
719
|
+
* it is (scaled into its new height, if it is one that changed). A fixed zoom stays; a fit mode
|
|
720
|
+
* fits again, since a page turned a quarter can be wider than the window at the old scale.
|
|
721
|
+
*/
|
|
722
|
+
#resize() {
|
|
723
|
+
const before = this.#layout;
|
|
724
|
+
if (before === undefined)
|
|
725
|
+
return;
|
|
726
|
+
const viewport = this.#viewport();
|
|
727
|
+
const scale = typeof this.#zoom === "string"
|
|
728
|
+
? scaleFor(this.#zoom, this.#reference(), this.#rotation, viewport, this.#gap)
|
|
729
|
+
: before.scale;
|
|
730
|
+
const after = new PageLayout(this.#sizes, { scale, rotation: before.rotation, gap: this.#gap });
|
|
731
|
+
if (after.height > MAX_HEIGHT) {
|
|
732
|
+
// Past what browsers can scroll: the zoom is capped again.
|
|
733
|
+
this.#rezoom(undefined);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const top = keptTop(before, after, this.#root.scrollTop);
|
|
737
|
+
this.#layout = after;
|
|
738
|
+
this.#spacer.style.height = `${after.height}px`;
|
|
739
|
+
this.#spacer.style.width = `${after.contentWidth(viewport.width)}px`;
|
|
740
|
+
this.#root.scrollTop = top;
|
|
741
|
+
if (scale !== before.scale) {
|
|
742
|
+
const percent = percentOfScale(scale);
|
|
743
|
+
this.#setState({ zoom: this.#zoom, percent });
|
|
744
|
+
this.#emit("zoomchange", { zoom: this.#zoom, percent });
|
|
745
|
+
this.#staleRenders();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Makes `view` draw its page again: its renders aborted (a late one is dropped), its tiles
|
|
750
|
+
* forgotten. A page in view at its old size keeps its pixels, and sharp tiles replace them; a
|
|
751
|
+
* page that changed size, or one outside the view, paints its quick pass first.
|
|
752
|
+
*/
|
|
753
|
+
#redraw(view, repaint) {
|
|
754
|
+
this.#abort(view);
|
|
755
|
+
view.waiting = false;
|
|
756
|
+
view.tiles.clear();
|
|
757
|
+
// A page whose render failed tries again from the start.
|
|
758
|
+
if (repaint || view.state === "error") {
|
|
759
|
+
this.#releaseDetail(view);
|
|
760
|
+
view.painted = "";
|
|
761
|
+
view.state = "placeholder";
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
view.detail?.tiles.clear();
|
|
765
|
+
if (view.state !== "placeholder")
|
|
766
|
+
view.state = "low";
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
/** Mounts `view`'s layers again, so they read its text and links again, at its size now. */
|
|
770
|
+
#remountLayers(view) {
|
|
771
|
+
const size = { width: this.#layout?.widths[view.index] ?? 0, height: this.#layout?.heights[view.index] ?? 0 };
|
|
772
|
+
for (const layer of this.#layers) {
|
|
773
|
+
const slot = this.#slot(view, layer.name);
|
|
774
|
+
layer.onUnmount?.(view.index, slot);
|
|
775
|
+
layer.onMount?.(view.index, slot);
|
|
776
|
+
layer.onZoom?.(view.index, slot, size);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
489
779
|
#scroll() {
|
|
490
780
|
return { left: this.#root.scrollLeft, top: this.#root.scrollTop };
|
|
491
781
|
}
|
|
492
782
|
#viewport() {
|
|
493
783
|
return { width: this.#root.clientWidth || 800, height: this.#root.clientHeight || 600 };
|
|
494
784
|
}
|
|
785
|
+
/** Whether the view has a size (it is in a shown container), so it can scroll. */
|
|
786
|
+
#sized() {
|
|
787
|
+
return this.#root.clientWidth > 0 && this.#root.clientHeight > 0;
|
|
788
|
+
}
|
|
789
|
+
/** Puts a view that just got its size where it was opened or restored to. */
|
|
790
|
+
#showPending(pending) {
|
|
791
|
+
this.#pending = undefined;
|
|
792
|
+
this.#relayout(this.#viewport());
|
|
793
|
+
// The scroll kept while hidden is the last document's: the top first, so a restore's history
|
|
794
|
+
// jump leaves from page 1.
|
|
795
|
+
this.#root.scrollTop = 0;
|
|
796
|
+
this.#root.scrollLeft = 0;
|
|
797
|
+
if (pending === "top")
|
|
798
|
+
this.#schedule();
|
|
799
|
+
else
|
|
800
|
+
this.restore(pending);
|
|
801
|
+
}
|
|
495
802
|
/** The text layer `option` asks for, or none (`false`); it reports the selection as an event too. */
|
|
496
803
|
#textLayer(option) {
|
|
497
804
|
if (option === false)
|
|
@@ -500,6 +807,7 @@ export class Viewer {
|
|
|
500
807
|
root: this.#root,
|
|
501
808
|
client: this.#client,
|
|
502
809
|
rotation: () => this.#rotation,
|
|
810
|
+
points: (page) => this.#sizes[page],
|
|
503
811
|
keepMounted: (pages) => this.keepMounted(pages),
|
|
504
812
|
};
|
|
505
813
|
const textOptions = option === true || option === undefined ? {} : option;
|
|
@@ -545,6 +853,8 @@ export class Viewer {
|
|
|
545
853
|
this.#spacer.style.height = `${layout.height}px`;
|
|
546
854
|
this.#spacer.style.width = `${layout.contentWidth(viewport.width)}px`;
|
|
547
855
|
if (zoomed) {
|
|
856
|
+
// Pictures drawn at the old zoom or rotation would show the wrong size or turn.
|
|
857
|
+
this.#previews?.clear();
|
|
548
858
|
const percent = percentOfScale(layout.scale);
|
|
549
859
|
this.#setState({ pageCount: layout.pageCount, zoom: this.#zoom, percent, rotation: this.#rotation });
|
|
550
860
|
this.#emit("zoomchange", { zoom: this.#zoom, percent });
|
|
@@ -565,8 +875,13 @@ export class Viewer {
|
|
|
565
875
|
const scroll = scrollToPoint(after, viewport.width, point, at);
|
|
566
876
|
this.#root.scrollTop = scroll.top;
|
|
567
877
|
this.#root.scrollLeft = scroll.left;
|
|
568
|
-
|
|
569
|
-
|
|
878
|
+
this.#staleRenders();
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Renders for the old geometry are stale: abort them (a page that failed gets another try); the
|
|
882
|
+
* canvases stretch until the zoom settles and the new pass paints, so no page is sharp.
|
|
883
|
+
*/
|
|
884
|
+
#staleRenders() {
|
|
570
885
|
let unsharpened = false;
|
|
571
886
|
for (const view of this.#pages.values()) {
|
|
572
887
|
this.#abort(view);
|
|
@@ -593,7 +908,9 @@ export class Viewer {
|
|
|
593
908
|
return;
|
|
594
909
|
this.#frame = requestAnimationFrame(() => {
|
|
595
910
|
this.#frame = undefined;
|
|
596
|
-
|
|
911
|
+
// A view waiting for its size has only an old scroll to go by: it draws once it is placed.
|
|
912
|
+
if (this.#pending === undefined)
|
|
913
|
+
this.#update();
|
|
597
914
|
});
|
|
598
915
|
}
|
|
599
916
|
/** The mounted range: visible ± margin pages, and at least a viewport above and below. */
|
|
@@ -815,7 +1132,11 @@ export class Viewer {
|
|
|
815
1132
|
/** The tile `clip` of page `index` at `scale`: its render request and cache key. */
|
|
816
1133
|
#tile(index, layout, scale, clip) {
|
|
817
1134
|
const request = { page: index, scale, rotation: layout.rotation, clip, ...appearanceField(this.#state.appearance) };
|
|
818
|
-
return { clip, request, key:
|
|
1135
|
+
return { clip, request, key: this.#key(request) };
|
|
1136
|
+
}
|
|
1137
|
+
/** `request`'s cache key: the render key with its page's revision. */
|
|
1138
|
+
#key(request) {
|
|
1139
|
+
return viewKey(request, this.#documentId ?? 0, this.#revisions.get(request.page) ?? 0);
|
|
819
1140
|
}
|
|
820
1141
|
/** The device px per pt of a sharp tile: the layout's scale × the device pixel ratio. */
|
|
821
1142
|
#fullScale(layout) {
|
|
@@ -886,7 +1207,7 @@ export class Viewer {
|
|
|
886
1207
|
// A page in the margin gives its detail canvas back for the pages in view.
|
|
887
1208
|
if (view !== undefined && priority === "prefetch")
|
|
888
1209
|
this.#releaseDetail(view);
|
|
889
|
-
if (view === undefined || this.#
|
|
1210
|
+
if (view === undefined || this.#documentId === undefined || view.busy || view.waiting || view.state === "error") {
|
|
890
1211
|
return;
|
|
891
1212
|
}
|
|
892
1213
|
if (!this.#readyForAppearance(view, priority) || this.#upToDate(view, priority, frame))
|
|
@@ -951,7 +1272,7 @@ export class Viewer {
|
|
|
951
1272
|
rotation: layout.rotation,
|
|
952
1273
|
...appearanceField(this.#state.appearance),
|
|
953
1274
|
};
|
|
954
|
-
const low = await this.#bitmap(request,
|
|
1275
|
+
const low = await this.#bitmap(request, this.#key(request), priority, signal);
|
|
955
1276
|
if (!live()) {
|
|
956
1277
|
this.#drop(low);
|
|
957
1278
|
return false;
|
|
@@ -1160,7 +1481,17 @@ export class Viewer {
|
|
|
1160
1481
|
const cached = this.#cache.get(key);
|
|
1161
1482
|
if (cached !== undefined && !isClosed(cached))
|
|
1162
1483
|
return { bitmap: cached, owned: false };
|
|
1484
|
+
const revision = this.#revisions.get(request.page) ?? 0;
|
|
1163
1485
|
const { bitmap } = await this.#client.render(request, { priority, signal });
|
|
1486
|
+
// The page was edited while this render waited: its pixels may be the old file's, and its key
|
|
1487
|
+
// names a revision nothing asks for again. The worker runs one call at a time and `invalidate`
|
|
1488
|
+
// runs only once the edit has answered, so an answer that comes after it, at the same
|
|
1489
|
+
// revision, drew the edited file; the worker's coalescing key needs no revision for the same
|
|
1490
|
+
// reason (no render can run across an edit).
|
|
1491
|
+
if ((this.#revisions.get(request.page) ?? 0) !== revision) {
|
|
1492
|
+
bitmap.close();
|
|
1493
|
+
throw new DOMException("the page changed while it rendered", "AbortError");
|
|
1494
|
+
}
|
|
1164
1495
|
const kept = this.#cache.set(key, request.page, bitmap, this.#state.currentPage);
|
|
1165
1496
|
return { bitmap, owned: !kept };
|
|
1166
1497
|
}
|
|
@@ -1330,6 +1661,10 @@ export class Viewer {
|
|
|
1330
1661
|
const resize = new ResizeObserver(() => {
|
|
1331
1662
|
if (this.#layout === undefined)
|
|
1332
1663
|
return;
|
|
1664
|
+
if (this.#pending !== undefined && this.#sized()) {
|
|
1665
|
+
this.#showPending(this.#pending);
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1333
1668
|
if (typeof this.#zoom === "string")
|
|
1334
1669
|
this.#rezoom(undefined);
|
|
1335
1670
|
else
|