@laminapdf/viewer 0.2.0-alpha.4 → 0.2.0-alpha.5
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 +36 -8
- package/dist/client.d.ts +28 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +105 -5
- package/dist/client.js.map +1 -1
- package/dist/first-draw.d.ts +10 -0
- package/dist/first-draw.d.ts.map +1 -0
- package/dist/first-draw.js +33 -0
- package/dist/first-draw.js.map +1 -0
- package/dist/history/history.d.ts +65 -0
- package/dist/history/history.d.ts.map +1 -0
- package/dist/history/history.js +135 -0
- package/dist/history/history.js.map +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/layers/copy.d.ts +26 -0
- package/dist/layers/copy.d.ts.map +1 -0
- package/dist/layers/copy.js +150 -0
- package/dist/layers/copy.js.map +1 -0
- package/dist/layers/link-layer.d.ts +68 -0
- package/dist/layers/link-layer.d.ts.map +1 -0
- package/dist/layers/link-layer.js +279 -0
- package/dist/layers/link-layer.js.map +1 -0
- package/dist/layers/link-url.d.ts +19 -0
- package/dist/layers/link-url.d.ts.map +1 -0
- package/dist/layers/link-url.js +47 -0
- package/dist/layers/link-url.js.map +1 -0
- package/dist/layers/spans.d.ts +0 -6
- package/dist/layers/spans.d.ts.map +1 -1
- package/dist/layers/spans.js +2 -20
- package/dist/layers/spans.js.map +1 -1
- package/dist/layers/structure-layer.d.ts.map +1 -1
- package/dist/layers/structure-layer.js +3 -0
- package/dist/layers/structure-layer.js.map +1 -1
- package/dist/layers/text-layer.d.ts +20 -3
- package/dist/layers/text-layer.d.ts.map +1 -1
- package/dist/layers/text-layer.js +307 -23
- package/dist/layers/text-layer.js.map +1 -1
- package/dist/protocol.d.ts +79 -5
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +16 -4
- package/dist/protocol.js.map +1 -1
- package/dist/source/opfs.d.ts +35 -0
- package/dist/source/opfs.d.ts.map +1 -0
- package/dist/source/opfs.js +178 -0
- package/dist/source/opfs.js.map +1 -0
- package/dist/thumbnails/strip.d.ts +1 -1
- package/dist/thumbnails/strip.d.ts.map +1 -1
- package/dist/thumbnails/strip.js +14 -3
- package/dist/thumbnails/strip.js.map +1 -1
- package/dist/view/user-select.d.ts +3 -0
- package/dist/view/user-select.d.ts.map +1 -0
- package/dist/view/user-select.js +10 -0
- package/dist/view/user-select.js.map +1 -0
- package/dist/view/viewer.d.ts +61 -1
- package/dist/view/viewer.d.ts.map +1 -1
- package/dist/view/viewer.js +248 -40
- package/dist/view/viewer.js.map +1 -1
- package/dist/worker/document.d.ts +23 -1
- package/dist/worker/document.d.ts.map +1 -1
- package/dist/worker/document.js +68 -22
- package/dist/worker/document.js.map +1 -1
- package/dist/worker/find.d.ts.map +1 -1
- package/dist/worker/find.js +5 -18
- package/dist/worker/find.js.map +1 -1
- package/dist/worker/methods.d.ts.map +1 -1
- package/dist/worker/methods.js +23 -4
- package/dist/worker/methods.js.map +1 -1
- package/dist/worker/navigation.d.ts +5 -1
- package/dist/worker/navigation.d.ts.map +1 -1
- package/dist/worker/navigation.js +22 -4
- package/dist/worker/navigation.js.map +1 -1
- package/dist/worker/print.d.ts +14 -0
- package/dist/worker/print.d.ts.map +1 -0
- package/dist/worker/print.js +49 -0
- package/dist/worker/print.js.map +1 -0
- package/dist/worker/recent-pages.d.ts +14 -0
- package/dist/worker/recent-pages.d.ts.map +1 -0
- package/dist/worker/recent-pages.js +53 -0
- package/dist/worker/recent-pages.js.map +1 -0
- package/dist/worker/render.d.ts.map +1 -1
- package/dist/worker/render.js +5 -2
- package/dist/worker/render.js.map +1 -1
- package/dist/worker/text.d.ts +9 -1
- package/dist/worker/text.d.ts.map +1 -1
- package/dist/worker/text.js +29 -22
- package/dist/worker/text.js.map +1 -1
- package/package.json +6 -5
package/dist/view/viewer.js
CHANGED
|
@@ -12,16 +12,19 @@ import { pagePixels } from "@laminapdf/render";
|
|
|
12
12
|
import { BitmapCache } from "../canvas/cache.js";
|
|
13
13
|
import { CanvasPool } from "../canvas/pool.js";
|
|
14
14
|
import { FindController } from "../find/controller.js";
|
|
15
|
+
import { NavigationHistory } from "../history/history.js";
|
|
15
16
|
import { HighlightLayer } from "../layers/highlight-layer.js";
|
|
17
|
+
import { LinkLayer } from "../layers/link-layer.js";
|
|
16
18
|
import { TextLayer } from "../layers/text-layer.js";
|
|
17
19
|
import { isRotation, PageLayout, rotated } from "../layout/page-layout.js";
|
|
18
20
|
import { range } from "../layout/range.js";
|
|
19
|
-
import { renderKey, renderKeyFields } from "../protocol.js";
|
|
21
|
+
import { appearanceField, appearanceKey, renderKey, renderKeyFields, } from "../protocol.js";
|
|
20
22
|
import { backdropRatio, bounds, contains, lowScale, sharpScale, tilesFor } from "../render/tiles.js";
|
|
21
23
|
import { destinationView } from "../zoom/destination.js";
|
|
22
24
|
import { contentPosition, percentOfScale, pointAt, scaleFor, scrollToPoint, } from "../zoom/zoom.js";
|
|
23
25
|
import { deviceHints, viewerBudgets } from "./budgets.js";
|
|
24
26
|
import { TouchGestures } from "./touch.js";
|
|
27
|
+
import { setUserSelect } from "./user-select.js";
|
|
25
28
|
/** The built-in layer slots, bottom to top, above the canvas. */
|
|
26
29
|
const SLOTS = ["text", "annot", "highlight"];
|
|
27
30
|
/** The tallest scroll space browsers handle (Firefox stops near 17.9 M CSS px). */
|
|
@@ -29,6 +32,19 @@ const MAX_HEIGHT = 16_000_000;
|
|
|
29
32
|
function isAbort(e) {
|
|
30
33
|
return e instanceof DOMException && e.name === "AbortError";
|
|
31
34
|
}
|
|
35
|
+
/** The state before a document opens. */
|
|
36
|
+
function emptyState(zoom) {
|
|
37
|
+
return {
|
|
38
|
+
pageCount: 0,
|
|
39
|
+
currentPage: 0,
|
|
40
|
+
zoom,
|
|
41
|
+
percent: 100,
|
|
42
|
+
rotation: 0,
|
|
43
|
+
appearance: "original",
|
|
44
|
+
scrollTop: 0,
|
|
45
|
+
visibleRange: { first: 0, last: -1 },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
32
48
|
/** What a painted canvas depends on: a zoom or a rotation makes it stale. */
|
|
33
49
|
function geometryOf(layout) {
|
|
34
50
|
return `${layout.scale}:${layout.rotation}`;
|
|
@@ -64,10 +80,22 @@ export class Viewer {
|
|
|
64
80
|
/** The text layer, unless `text: false`. */
|
|
65
81
|
#text;
|
|
66
82
|
#highlights;
|
|
83
|
+
/** The link layer, unless `links: false`. */
|
|
84
|
+
#links;
|
|
67
85
|
/** Find in this viewer's document: query, options, matches, next and previous. */
|
|
68
86
|
find;
|
|
87
|
+
/**
|
|
88
|
+
* Back and forward through the places jumped from and to: links, `jumpToPage`,
|
|
89
|
+
* `jumpToDestination` and find's matches record a jump; scrolling doesn't.
|
|
90
|
+
*/
|
|
91
|
+
history;
|
|
69
92
|
/** Pages kept mounted whatever the scroll (`keepMounted`): those a selection touches. */
|
|
70
93
|
#pinned = new Set();
|
|
94
|
+
#regions;
|
|
95
|
+
/** The page a jump landed on, focused once it is mounted. */
|
|
96
|
+
#focusTarget;
|
|
97
|
+
/** True while a jump that leaves focus where it is (a find match) runs. */
|
|
98
|
+
#keepFocus = false;
|
|
71
99
|
#listeners = new Set();
|
|
72
100
|
#events = new Map();
|
|
73
101
|
#cleanup = [];
|
|
@@ -114,31 +142,28 @@ export class Viewer {
|
|
|
114
142
|
this.#spacer.className = "lamina-viewer-content";
|
|
115
143
|
Object.assign(this.#spacer.style, { position: "relative" });
|
|
116
144
|
this.#root.append(this.#spacer);
|
|
117
|
-
this.#state =
|
|
118
|
-
pageCount: 0,
|
|
119
|
-
currentPage: 0,
|
|
120
|
-
zoom: this.#zoom,
|
|
121
|
-
percent: 100,
|
|
122
|
-
rotation: 0,
|
|
123
|
-
scrollTop: 0,
|
|
124
|
-
visibleRange: { first: 0, last: -1 },
|
|
125
|
-
};
|
|
145
|
+
this.#state = emptyState(this.#zoom);
|
|
126
146
|
this.#listen();
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
root: this.#root,
|
|
130
|
-
client: this.#client,
|
|
131
|
-
rotation: () => this.#rotation,
|
|
132
|
-
keepMounted: (pages) => this.keepMounted(pages),
|
|
133
|
-
};
|
|
134
|
-
this.#text = new TextLayer(host, options.text === true || options.text === undefined ? {} : options.text);
|
|
147
|
+
this.#text = this.#textLayer(options.text);
|
|
148
|
+
if (this.#text !== undefined)
|
|
135
149
|
this.addLayer(this.#text);
|
|
150
|
+
this.history = new NavigationHistory({ place: () => this.#place(), show: (p) => this.#show(p) }, options.history);
|
|
151
|
+
this.#regions = options.pages ?? {};
|
|
152
|
+
if (this.#regions.focusOnJump === true) {
|
|
153
|
+
this.history.onEntry((entry, replaced) => {
|
|
154
|
+
if (replaced || this.#keepFocus)
|
|
155
|
+
return;
|
|
156
|
+
this.#focusTarget = entry.point.page;
|
|
157
|
+
this.#schedule();
|
|
158
|
+
});
|
|
136
159
|
}
|
|
137
160
|
this.find = new FindController({
|
|
138
161
|
find: (query, find, pages, signal) => this.#client.find(query, find, pages, { signal }),
|
|
139
162
|
pageCount: () => this.#sizes.length,
|
|
140
163
|
currentPage: () => this.#state.currentPage,
|
|
141
|
-
|
|
164
|
+
// Each match find moves to is a jump, so Back returns to where the search started.
|
|
165
|
+
// A match is a jump too, but focus stays in the find field.
|
|
166
|
+
reveal: (page, quads) => this.#jumpKeepingFocus(() => this.#reveal(page, quads)),
|
|
142
167
|
redraw: (pages) => this.#highlights.refresh(pages),
|
|
143
168
|
});
|
|
144
169
|
this.#highlights = new HighlightLayer({
|
|
@@ -147,6 +172,7 @@ export class Viewer {
|
|
|
147
172
|
rotation: () => this.#rotation,
|
|
148
173
|
});
|
|
149
174
|
this.addLayer(this.#highlights);
|
|
175
|
+
this.#links = this.#linkLayer(options.links);
|
|
150
176
|
}
|
|
151
177
|
/** The scroll container (append it, or use {@link mount}). */
|
|
152
178
|
get element() {
|
|
@@ -163,7 +189,9 @@ export class Viewer {
|
|
|
163
189
|
this.#cache.clear();
|
|
164
190
|
this.#document = document;
|
|
165
191
|
this.#pinned = new Set();
|
|
192
|
+
this.#focusTarget = undefined;
|
|
166
193
|
this.find.reset();
|
|
194
|
+
this.history.reset();
|
|
167
195
|
this.#text?.open(document);
|
|
168
196
|
this.#sizes = document.pages.map((p) => ({ width: p.width, height: p.height }));
|
|
169
197
|
this.#layout = undefined;
|
|
@@ -192,6 +220,23 @@ export class Viewer {
|
|
|
192
220
|
set.add(l);
|
|
193
221
|
return () => set.delete(l);
|
|
194
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Copies the text selection to the clipboard: by paragraph in reading order, or exact, as laid
|
|
225
|
+
* out. Call it in a user gesture (a click, a key). Resolves false when nothing is selected or
|
|
226
|
+
* the clipboard refused.
|
|
227
|
+
*/
|
|
228
|
+
copySelection(mode = "paragraph") {
|
|
229
|
+
return this.#text?.copy(mode) ?? Promise.resolve(false);
|
|
230
|
+
}
|
|
231
|
+
/** Whether a plain drag selects a rectangle, as an Alt/Option-drag does (column selection). */
|
|
232
|
+
get columnSelect() {
|
|
233
|
+
return this.#text?.columnMode ?? false;
|
|
234
|
+
}
|
|
235
|
+
/** Turns column selection by plain drag on or off; Alt/Option-drag selects a rectangle either way. */
|
|
236
|
+
set columnSelect(on) {
|
|
237
|
+
if (this.#text !== undefined)
|
|
238
|
+
this.#text.columnMode = on;
|
|
239
|
+
}
|
|
195
240
|
/**
|
|
196
241
|
* Keeps `pages` mounted wherever the view scrolls, until the next call replaces them: the pages
|
|
197
242
|
* a text selection touches, so scrolling never takes the selection away. A pinned page out of
|
|
@@ -253,6 +298,14 @@ export class Viewer {
|
|
|
253
298
|
this.#root.scrollLeft = Math.max(0, at.x);
|
|
254
299
|
this.#schedule();
|
|
255
300
|
}
|
|
301
|
+
/** {@link goToPage} as a history jump: Back returns to where the view was. */
|
|
302
|
+
jumpToPage(index) {
|
|
303
|
+
this.history.jump(() => this.goToPage(index));
|
|
304
|
+
}
|
|
305
|
+
/** {@link goToDestination} as a history jump: Back returns to where the view was. */
|
|
306
|
+
jumpToDestination(target) {
|
|
307
|
+
this.history.jump(() => this.goToDestination(target));
|
|
308
|
+
}
|
|
256
309
|
/**
|
|
257
310
|
* Sets the zoom (a mode, or a percentage clamped to 10–800 %), keeping the document point at
|
|
258
311
|
* `anchor` (viewport CSS px; default its centre) where it is.
|
|
@@ -275,6 +328,33 @@ export class Viewer {
|
|
|
275
328
|
// A half turn keeps every page's size, so no layer hears of it from a resize.
|
|
276
329
|
this.#text?.refresh();
|
|
277
330
|
this.#highlights.refresh();
|
|
331
|
+
this.#links?.refresh();
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Draws the pages in `appearance` (Sepia, Dark, High contrast or their own colours), keeping the
|
|
335
|
+
* scroll position: pages in view paint again, a quick pass then sharp tiles, and none of their
|
|
336
|
+
* old pixels is kept; other pages give their canvas back and paint when they come into view.
|
|
337
|
+
* The thumbnails follow.
|
|
338
|
+
*/
|
|
339
|
+
setAppearance(appearance) {
|
|
340
|
+
if (appearanceKey(appearance) === appearanceKey(this.#state.appearance))
|
|
341
|
+
return;
|
|
342
|
+
const { first, last } = this.#state.visibleRange;
|
|
343
|
+
for (const view of [...this.#pages.values()]) {
|
|
344
|
+
this.#abort(view);
|
|
345
|
+
view.tiles.clear();
|
|
346
|
+
view.detail?.tiles.clear();
|
|
347
|
+
if (view.index >= first && view.index <= last) {
|
|
348
|
+
view.appearance = appearanceKey(appearance);
|
|
349
|
+
view.painted = "";
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
// Its shell shows the new paper until it is seen (`#readyForAppearance`).
|
|
353
|
+
this.#releaseCanvas(view);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
this.#setState({ appearance });
|
|
357
|
+
this.#schedule();
|
|
278
358
|
}
|
|
279
359
|
/** The document point at viewport position (`x`, `y`) in CSS px. */
|
|
280
360
|
pointAt(x, y) {
|
|
@@ -321,11 +401,53 @@ export class Viewer {
|
|
|
321
401
|
this.#cache.clear();
|
|
322
402
|
this.find.reset();
|
|
323
403
|
this.#text?.destroy();
|
|
404
|
+
this.#links?.destroy();
|
|
324
405
|
for (const f of this.#cleanup.splice(0))
|
|
325
406
|
f();
|
|
326
407
|
this.#root.remove();
|
|
327
408
|
}
|
|
328
409
|
// --- internals ---
|
|
410
|
+
/** The link layer, added to every page, unless `links` is `false`. */
|
|
411
|
+
#linkLayer(links) {
|
|
412
|
+
if (links === false)
|
|
413
|
+
return undefined;
|
|
414
|
+
const layer = new LinkLayer({
|
|
415
|
+
root: this.#root,
|
|
416
|
+
client: this.#client,
|
|
417
|
+
points: (page) => this.#sizes[page],
|
|
418
|
+
rotation: () => this.#rotation,
|
|
419
|
+
pageCount: () => this.#sizes.length,
|
|
420
|
+
goTo: (target) => this.jumpToDestination(target),
|
|
421
|
+
goToPage: (page) => this.jumpToPage(page),
|
|
422
|
+
}, links === true || links === undefined ? {} : links);
|
|
423
|
+
this.addLayer(layer);
|
|
424
|
+
return layer;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Where the view is, for the history: the document point a gap below the viewport's top-left (so
|
|
428
|
+
* a page that `goToPage` put at the top, half a gap down, is the one named), and the zoom.
|
|
429
|
+
*/
|
|
430
|
+
#place() {
|
|
431
|
+
const layout = this.#layout;
|
|
432
|
+
if (layout === undefined || layout.pageCount === 0)
|
|
433
|
+
return undefined;
|
|
434
|
+
return { point: pointAt(layout, this.#root.clientWidth, this.#scroll(), { x: 0, y: this.#gap }), zoom: this.#zoom };
|
|
435
|
+
}
|
|
436
|
+
/** Puts the view back at a history `place`: its zoom, then its point a gap below the viewport's top-left. */
|
|
437
|
+
#show(place) {
|
|
438
|
+
this.#touch.stopMomentum();
|
|
439
|
+
if (place.zoom !== this.#zoom) {
|
|
440
|
+
this.#zoom = place.zoom;
|
|
441
|
+
this.#rezoom(undefined);
|
|
442
|
+
}
|
|
443
|
+
const layout = this.#layout;
|
|
444
|
+
if (layout === undefined || place.point.page >= layout.pageCount)
|
|
445
|
+
return;
|
|
446
|
+
const at = contentPosition(layout, this.#root.clientWidth, place.point);
|
|
447
|
+
this.#root.scrollTop = Math.max(0, at.y - this.#gap);
|
|
448
|
+
this.#root.scrollLeft = Math.max(0, at.x);
|
|
449
|
+
this.#schedule();
|
|
450
|
+
}
|
|
329
451
|
/**
|
|
330
452
|
* Scrolls so that `quads` (a find match, in page `page`'s points) are in view: nothing moves
|
|
331
453
|
* when they already are, else they come to the viewport's centre.
|
|
@@ -370,6 +492,25 @@ export class Viewer {
|
|
|
370
492
|
#viewport() {
|
|
371
493
|
return { width: this.#root.clientWidth || 800, height: this.#root.clientHeight || 600 };
|
|
372
494
|
}
|
|
495
|
+
/** The text layer `option` asks for, or none (`false`); it reports the selection as an event too. */
|
|
496
|
+
#textLayer(option) {
|
|
497
|
+
if (option === false)
|
|
498
|
+
return undefined;
|
|
499
|
+
const host = {
|
|
500
|
+
root: this.#root,
|
|
501
|
+
client: this.#client,
|
|
502
|
+
rotation: () => this.#rotation,
|
|
503
|
+
keepMounted: (pages) => this.keepMounted(pages),
|
|
504
|
+
};
|
|
505
|
+
const textOptions = option === true || option === undefined ? {} : option;
|
|
506
|
+
return new TextLayer(host, {
|
|
507
|
+
...textOptions,
|
|
508
|
+
onSelection: (selection) => {
|
|
509
|
+
textOptions.onSelection?.(selection);
|
|
510
|
+
this.#emit("selectionchange", { selection });
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
}
|
|
373
514
|
#emit(event, data) {
|
|
374
515
|
for (const l of this.#events.get(event) ?? [])
|
|
375
516
|
l(data);
|
|
@@ -480,6 +621,7 @@ export class Viewer {
|
|
|
480
621
|
const [first, last] = this.#mountRange(frame);
|
|
481
622
|
// Writes.
|
|
482
623
|
this.#placeShells(frame, first, last);
|
|
624
|
+
this.#focusJumpTarget();
|
|
483
625
|
this.#setState({
|
|
484
626
|
currentPage: layout.pageAt(frame.top + frame.height / 2),
|
|
485
627
|
scrollTop: frame.top,
|
|
@@ -516,6 +658,24 @@ export class Viewer {
|
|
|
516
658
|
if (this.#pages.size > this.#peakMounted)
|
|
517
659
|
this.#peakMounted = this.#pages.size;
|
|
518
660
|
}
|
|
661
|
+
#jumpKeepingFocus(go) {
|
|
662
|
+
this.#keepFocus = true;
|
|
663
|
+
try {
|
|
664
|
+
this.history.jump(go);
|
|
665
|
+
}
|
|
666
|
+
finally {
|
|
667
|
+
this.#keepFocus = false;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/** Focuses the page the last jump landed on, once it is mounted, keeping the scroll where it is. */
|
|
671
|
+
#focusJumpTarget() {
|
|
672
|
+
const target = this.#focusTarget;
|
|
673
|
+
const view = target === undefined ? undefined : this.#pages.get(target);
|
|
674
|
+
if (view === undefined)
|
|
675
|
+
return;
|
|
676
|
+
this.#focusTarget = undefined;
|
|
677
|
+
view.shell.focus({ preventScroll: true });
|
|
678
|
+
}
|
|
519
679
|
#notifyResize(view, width, height) {
|
|
520
680
|
for (const layer of this.#layers)
|
|
521
681
|
layer.onZoom?.(view.index, this.#slot(view, layer.name), { width, height });
|
|
@@ -533,7 +693,17 @@ export class Viewer {
|
|
|
533
693
|
const shell = document.createElement("div");
|
|
534
694
|
shell.className = "lamina-page";
|
|
535
695
|
shell.dataset.pageIndex = String(index);
|
|
536
|
-
|
|
696
|
+
// A region for assistive technology, and a place focus can land after a jump.
|
|
697
|
+
shell.setAttribute("role", "region");
|
|
698
|
+
shell.setAttribute("aria-roledescription", this.#regions.roleDescription ?? "page");
|
|
699
|
+
shell.setAttribute("aria-label", this.#pageName(index));
|
|
700
|
+
shell.tabIndex = -1;
|
|
701
|
+
// The paper shows before the page is drawn: white, or the page appearance's (--lamina-page-paper).
|
|
702
|
+
Object.assign(shell.style, {
|
|
703
|
+
position: "absolute",
|
|
704
|
+
contain: "strict",
|
|
705
|
+
background: "var(--lamina-page-paper, #fff)",
|
|
706
|
+
});
|
|
537
707
|
this.#spacer.append(shell);
|
|
538
708
|
const view = {
|
|
539
709
|
index,
|
|
@@ -543,6 +713,7 @@ export class Viewer {
|
|
|
543
713
|
controller: new AbortController(),
|
|
544
714
|
state: "placeholder",
|
|
545
715
|
painted: "",
|
|
716
|
+
appearance: appearanceKey(this.#state.appearance),
|
|
546
717
|
scale: 0,
|
|
547
718
|
tiles: new Set(),
|
|
548
719
|
detail: undefined,
|
|
@@ -565,9 +736,13 @@ export class Viewer {
|
|
|
565
736
|
slot.className = `lamina-layer-${name}`;
|
|
566
737
|
Object.assign(slot.style, { position: "absolute", inset: "0" });
|
|
567
738
|
// The slots above the text cover the whole page: taking the pointer, they would sit between
|
|
568
|
-
// every press and the spans, and no drag could select text.
|
|
569
|
-
|
|
739
|
+
// every press and the spans, and no drag could select text. Nor are they part of a selection:
|
|
740
|
+
// each element the viewer adds that holds nothing to select sets `none` itself, never an
|
|
741
|
+
// ancestor of the spans (user-select.ts).
|
|
742
|
+
if (name !== "text") {
|
|
570
743
|
slot.style.pointerEvents = "none";
|
|
744
|
+
setUserSelect(slot, "none");
|
|
745
|
+
}
|
|
571
746
|
view.slots.set(name, slot);
|
|
572
747
|
view.shell.append(slot);
|
|
573
748
|
}
|
|
@@ -580,6 +755,10 @@ export class Viewer {
|
|
|
580
755
|
view.pass++;
|
|
581
756
|
view.busy = false;
|
|
582
757
|
}
|
|
758
|
+
#pageName(index) {
|
|
759
|
+
const count = this.#sizes.length;
|
|
760
|
+
return this.#regions.name?.(index, count) ?? `Page ${index + 1} of ${count}`;
|
|
761
|
+
}
|
|
583
762
|
#unmount(view) {
|
|
584
763
|
this.#abort(view);
|
|
585
764
|
for (const layer of this.#layers)
|
|
@@ -635,7 +814,7 @@ export class Viewer {
|
|
|
635
814
|
}
|
|
636
815
|
/** The tile `clip` of page `index` at `scale`: its render request and cache key. */
|
|
637
816
|
#tile(index, layout, scale, clip) {
|
|
638
|
-
const request = { page: index, scale, rotation: layout.rotation, clip };
|
|
817
|
+
const request = { page: index, scale, rotation: layout.rotation, clip, ...appearanceField(this.#state.appearance) };
|
|
639
818
|
return { clip, request, key: renderKey(request, this.#document?.id ?? 0) };
|
|
640
819
|
}
|
|
641
820
|
/** The device px per pt of a sharp tile: the layout's scale × the device pixel ratio. */
|
|
@@ -665,6 +844,42 @@ export class Viewer {
|
|
|
665
844
|
return false;
|
|
666
845
|
return tiles.every((t) => detail.tiles.has(t.key));
|
|
667
846
|
}
|
|
847
|
+
/**
|
|
848
|
+
* Whether `view` may paint now, catching it up with the appearance: a page out of view when the
|
|
849
|
+
* appearance changed waits until it is seen (`prefetch` doesn't paint it), then starts afresh.
|
|
850
|
+
*/
|
|
851
|
+
#readyForAppearance(view, priority) {
|
|
852
|
+
const appearance = appearanceKey(this.#state.appearance);
|
|
853
|
+
if (view.appearance === appearance)
|
|
854
|
+
return true;
|
|
855
|
+
if (priority === "prefetch")
|
|
856
|
+
return false;
|
|
857
|
+
view.appearance = appearance;
|
|
858
|
+
view.painted = "";
|
|
859
|
+
return true;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Whether `view`'s canvas needs nothing for `frame`: painted at its geometry and, in view, with
|
|
863
|
+
* every tile in view drawn (then it is sharp). A margin page painted at its geometry waits.
|
|
864
|
+
*/
|
|
865
|
+
#upToDate(view, priority, frame) {
|
|
866
|
+
if (view.painted !== geometryOf(frame.layout))
|
|
867
|
+
return false;
|
|
868
|
+
if (priority === "prefetch")
|
|
869
|
+
return true;
|
|
870
|
+
// Sharp means every tile now in view is drawn: scrolling down a tall page sharpens the rest.
|
|
871
|
+
if (view.scale > 0 && this.#sharpInView(view, frame)) {
|
|
872
|
+
if (view.state !== "sharp") {
|
|
873
|
+
view.state = "sharp";
|
|
874
|
+
this.#emit("renderstats", this.stats());
|
|
875
|
+
}
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
// Tiles in view are missing (the page scrolled): it is no longer sharp until they are drawn.
|
|
879
|
+
if (view.state === "sharp")
|
|
880
|
+
view.state = "low";
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
668
883
|
/** Starts page `index`'s pass if it isn't busy and its canvas isn't done for `frame` yet. */
|
|
669
884
|
#paint(index, priority, frame) {
|
|
670
885
|
const view = this.#pages.get(index);
|
|
@@ -674,22 +889,8 @@ export class Viewer {
|
|
|
674
889
|
if (view === undefined || this.#document === undefined || view.busy || view.waiting || view.state === "error") {
|
|
675
890
|
return;
|
|
676
891
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
if (priority === "prefetch")
|
|
680
|
-
return;
|
|
681
|
-
// Sharp means every tile now in view is drawn: scrolling down a tall page sharpens the rest.
|
|
682
|
-
if (view.scale > 0 && this.#sharpInView(view, frame)) {
|
|
683
|
-
if (view.state !== "sharp") {
|
|
684
|
-
view.state = "sharp";
|
|
685
|
-
this.#emit("renderstats", this.stats());
|
|
686
|
-
}
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
// Tiles in view are missing (the page scrolled): it is no longer sharp until they are drawn.
|
|
690
|
-
if (view.state === "sharp")
|
|
691
|
-
view.state = "low";
|
|
692
|
-
}
|
|
892
|
+
if (!this.#readyForAppearance(view, priority) || this.#upToDate(view, priority, frame))
|
|
893
|
+
return;
|
|
693
894
|
view.busy = true;
|
|
694
895
|
const pass = view.pass;
|
|
695
896
|
void this.#pass(view, pass, priority, frame)
|
|
@@ -744,7 +945,12 @@ export class Viewer {
|
|
|
744
945
|
const cssW = layout.widths[index];
|
|
745
946
|
const cssH = layout.heights[index];
|
|
746
947
|
const shown = rotated(this.#sizes[index], layout.rotation);
|
|
747
|
-
const request = {
|
|
948
|
+
const request = {
|
|
949
|
+
page: index,
|
|
950
|
+
scale: lowScale(layout.scale * dpr, shown.width),
|
|
951
|
+
rotation: layout.rotation,
|
|
952
|
+
...appearanceField(this.#state.appearance),
|
|
953
|
+
};
|
|
748
954
|
const low = await this.#bitmap(request, renderKey(request, this.#document?.id ?? 0), priority, signal);
|
|
749
955
|
if (!live()) {
|
|
750
956
|
this.#drop(low);
|
|
@@ -921,6 +1127,7 @@ export class Viewer {
|
|
|
921
1127
|
});
|
|
922
1128
|
if (old === undefined) {
|
|
923
1129
|
canvas.className = "lamina-page-detail";
|
|
1130
|
+
setUserSelect(canvas, "none");
|
|
924
1131
|
if (view.canvas === undefined)
|
|
925
1132
|
view.shell.prepend(canvas);
|
|
926
1133
|
else
|
|
@@ -1010,6 +1217,7 @@ export class Viewer {
|
|
|
1010
1217
|
if (canvas === undefined)
|
|
1011
1218
|
return undefined;
|
|
1012
1219
|
canvas.className = "lamina-page-canvas";
|
|
1220
|
+
setUserSelect(canvas, "none");
|
|
1013
1221
|
Object.assign(canvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%" });
|
|
1014
1222
|
view.shell.prepend(canvas);
|
|
1015
1223
|
view.canvas = canvas;
|