genus-pdf-viewer 0.1.13 → 0.2.1
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 +128 -63
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/{workers → dist}/pdf.worker.min.mjs +0 -0
- package/dist/styles.css +132 -0
- package/dist/styles.d.ts +2 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +212 -0
- package/dist/styles.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/viewer.d.ts +27 -0
- package/dist/viewer.d.ts.map +1 -0
- package/dist/viewer.js +678 -0
- package/dist/viewer.js.map +1 -0
- package/package.json +51 -20
- package/fesm2022/genus-pdf-viewer.mjs +0 -1346
- package/fesm2022/genus-pdf-viewer.mjs.map +0 -1
- package/genus-pdf-viewer-0.1.13.tgz +0 -0
- package/index.d.ts +0 -119
- package/schematics/collection.json +0 -10
- package/schematics/ng-add/index.js +0 -171
- package/schematics/ng-add/schema.json +0 -27
package/dist/viewer.js
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import * as pdfjsLib from "pdfjs-dist";
|
|
2
|
+
import { VIEWER_STYLES } from "./styles.js";
|
|
3
|
+
const GENUS_PDF_VIEWER_TAG = "genus-pdf-viewer";
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
page: 1,
|
|
6
|
+
zoom: 1,
|
|
7
|
+
minZoom: 0.5,
|
|
8
|
+
maxZoom: 3,
|
|
9
|
+
zoomStep: 0.25,
|
|
10
|
+
fit: "width",
|
|
11
|
+
continuous: false,
|
|
12
|
+
showToolbar: true,
|
|
13
|
+
allowDownload: true,
|
|
14
|
+
withCredentials: false,
|
|
15
|
+
};
|
|
16
|
+
function parseBooleanAttribute(value, fallback) {
|
|
17
|
+
if (value === null) {
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
return value !== "false";
|
|
21
|
+
}
|
|
22
|
+
function parseNumberAttribute(value, fallback) {
|
|
23
|
+
if (value === null) {
|
|
24
|
+
return fallback;
|
|
25
|
+
}
|
|
26
|
+
const parsed = Number(value);
|
|
27
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
28
|
+
}
|
|
29
|
+
function isSameSource(left, right) {
|
|
30
|
+
if (typeof left === "string" && typeof right === "string") {
|
|
31
|
+
return left === right;
|
|
32
|
+
}
|
|
33
|
+
if (left instanceof URL && right instanceof URL) {
|
|
34
|
+
return left.toString() === right.toString();
|
|
35
|
+
}
|
|
36
|
+
return left === right;
|
|
37
|
+
}
|
|
38
|
+
function sameHeaders(left, right) {
|
|
39
|
+
const leftEntries = Object.entries(left ?? {});
|
|
40
|
+
const rightEntries = Object.entries(right ?? {});
|
|
41
|
+
if (leftEntries.length !== rightEntries.length) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return leftEntries.every(([key, value]) => right?.[key] === value);
|
|
45
|
+
}
|
|
46
|
+
function clampPage(page, pageCount) {
|
|
47
|
+
return Math.max(1, Math.min(pageCount, Math.round(page)));
|
|
48
|
+
}
|
|
49
|
+
function clampZoom(zoom, minZoom, maxZoom) {
|
|
50
|
+
return Math.max(minZoom, Math.min(maxZoom, zoom));
|
|
51
|
+
}
|
|
52
|
+
function cloneSource(source) {
|
|
53
|
+
if (source instanceof Uint8Array) {
|
|
54
|
+
return new Uint8Array(source);
|
|
55
|
+
}
|
|
56
|
+
return source;
|
|
57
|
+
}
|
|
58
|
+
function resolveTitle(config) {
|
|
59
|
+
if (config.title) {
|
|
60
|
+
return config.title;
|
|
61
|
+
}
|
|
62
|
+
if (typeof config.src === "string") {
|
|
63
|
+
const lastPart = config.src.split("/").filter(Boolean).pop();
|
|
64
|
+
return lastPart ?? "PDF viewer";
|
|
65
|
+
}
|
|
66
|
+
if (config.src instanceof URL) {
|
|
67
|
+
const lastPart = config.src.pathname.split("/").filter(Boolean).pop();
|
|
68
|
+
return lastPart ?? "PDF viewer";
|
|
69
|
+
}
|
|
70
|
+
if (config.src instanceof Blob && config.src.type === "application/pdf") {
|
|
71
|
+
return "Document.pdf";
|
|
72
|
+
}
|
|
73
|
+
return "PDF viewer";
|
|
74
|
+
}
|
|
75
|
+
async function toDocumentParameters(config) {
|
|
76
|
+
if (typeof config.src === "string") {
|
|
77
|
+
return {
|
|
78
|
+
url: config.src,
|
|
79
|
+
withCredentials: config.withCredentials,
|
|
80
|
+
httpHeaders: config.httpHeaders,
|
|
81
|
+
stopAtErrors: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (config.src instanceof URL) {
|
|
85
|
+
return {
|
|
86
|
+
url: config.src.toString(),
|
|
87
|
+
withCredentials: config.withCredentials,
|
|
88
|
+
httpHeaders: config.httpHeaders,
|
|
89
|
+
stopAtErrors: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (config.src instanceof Blob) {
|
|
93
|
+
return {
|
|
94
|
+
data: new Uint8Array(await config.src.arrayBuffer()),
|
|
95
|
+
disableRange: true,
|
|
96
|
+
disableStream: true,
|
|
97
|
+
disableAutoFetch: true,
|
|
98
|
+
stopAtErrors: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
data: new Uint8Array(config.src),
|
|
103
|
+
disableRange: true,
|
|
104
|
+
disableStream: true,
|
|
105
|
+
disableAutoFetch: true,
|
|
106
|
+
stopAtErrors: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function createBlobFromSource(config) {
|
|
110
|
+
if (config.src instanceof Blob) {
|
|
111
|
+
return config.src;
|
|
112
|
+
}
|
|
113
|
+
if (config.src instanceof Uint8Array) {
|
|
114
|
+
return new Blob([new Uint8Array(config.src)], { type: "application/pdf" });
|
|
115
|
+
}
|
|
116
|
+
const href = typeof config.src === "string"
|
|
117
|
+
? config.src
|
|
118
|
+
: config.src instanceof URL
|
|
119
|
+
? config.src.toString()
|
|
120
|
+
: null;
|
|
121
|
+
if (!href || typeof fetch === "undefined") {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const response = await fetch(href, {
|
|
125
|
+
headers: config.httpHeaders,
|
|
126
|
+
credentials: config.withCredentials ? "include" : "same-origin",
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return response.blob();
|
|
132
|
+
}
|
|
133
|
+
function emitViewerEvent(element, type, detail) {
|
|
134
|
+
element.dispatchEvent(new CustomEvent(`genus-${type}`, {
|
|
135
|
+
bubbles: true,
|
|
136
|
+
detail,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
export class GenusPdfViewerElement extends HTMLElement {
|
|
140
|
+
static get observedAttributes() {
|
|
141
|
+
return ["src", "title", "page", "zoom", "fit", "continuous", "toolbar", "download"];
|
|
142
|
+
}
|
|
143
|
+
#config = null;
|
|
144
|
+
#document = null;
|
|
145
|
+
#loadingTask = null;
|
|
146
|
+
#pageCount = 0;
|
|
147
|
+
#loadVersion = 0;
|
|
148
|
+
#renderVersion = 0;
|
|
149
|
+
#shadow = this.attachShadow({ mode: "open" });
|
|
150
|
+
#toolbar = document.createElement("div");
|
|
151
|
+
#title = document.createElement("div");
|
|
152
|
+
#navGroup = document.createElement("div");
|
|
153
|
+
#zoomGroup = document.createElement("div");
|
|
154
|
+
#actionGroup = document.createElement("div");
|
|
155
|
+
#pageValue = document.createElement("span");
|
|
156
|
+
#zoomValue = document.createElement("span");
|
|
157
|
+
#prevButton = document.createElement("button");
|
|
158
|
+
#nextButton = document.createElement("button");
|
|
159
|
+
#zoomOutButton = document.createElement("button");
|
|
160
|
+
#zoomInButton = document.createElement("button");
|
|
161
|
+
#resetZoomButton = document.createElement("button");
|
|
162
|
+
#downloadButton = document.createElement("button");
|
|
163
|
+
#stage = document.createElement("div");
|
|
164
|
+
#status = document.createElement("div");
|
|
165
|
+
#handleResize = () => {
|
|
166
|
+
if (!this.#document || !this.#config) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (this.#config.fit === "none") {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
void this.#render();
|
|
173
|
+
};
|
|
174
|
+
#handleScroll = () => {
|
|
175
|
+
if (!this.#config?.continuous) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const page = this.#detectVisiblePage();
|
|
179
|
+
if (!page || page === this.#config.page) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
this.#config = { ...this.#config, page };
|
|
183
|
+
this.#syncToolbarState();
|
|
184
|
+
emitViewerEvent(this, "pagechange", {
|
|
185
|
+
page,
|
|
186
|
+
pageCount: this.#pageCount,
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
constructor() {
|
|
190
|
+
super();
|
|
191
|
+
const style = document.createElement("style");
|
|
192
|
+
style.textContent = VIEWER_STYLES;
|
|
193
|
+
const shell = document.createElement("div");
|
|
194
|
+
shell.className = "shell";
|
|
195
|
+
this.#toolbar.className = "toolbar";
|
|
196
|
+
this.#title.className = "title";
|
|
197
|
+
this.#navGroup.className = "toolbar-group toolbar-group-nav";
|
|
198
|
+
this.#zoomGroup.className = "toolbar-group toolbar-group-zoom";
|
|
199
|
+
this.#actionGroup.className = "toolbar-group toolbar-group-actions";
|
|
200
|
+
this.#pageValue.className = "value";
|
|
201
|
+
this.#zoomValue.className = "value";
|
|
202
|
+
this.#prevButton.type = "button";
|
|
203
|
+
this.#prevButton.textContent = "Prev";
|
|
204
|
+
this.#prevButton.addEventListener("click", () => {
|
|
205
|
+
void this.prevPage();
|
|
206
|
+
});
|
|
207
|
+
this.#nextButton.type = "button";
|
|
208
|
+
this.#nextButton.textContent = "Next";
|
|
209
|
+
this.#nextButton.addEventListener("click", () => {
|
|
210
|
+
void this.nextPage();
|
|
211
|
+
});
|
|
212
|
+
this.#zoomOutButton.type = "button";
|
|
213
|
+
this.#zoomOutButton.textContent = "−";
|
|
214
|
+
this.#zoomOutButton.addEventListener("click", () => {
|
|
215
|
+
void this.zoomOut();
|
|
216
|
+
});
|
|
217
|
+
this.#zoomInButton.type = "button";
|
|
218
|
+
this.#zoomInButton.textContent = "+";
|
|
219
|
+
this.#zoomInButton.addEventListener("click", () => {
|
|
220
|
+
void this.zoomIn();
|
|
221
|
+
});
|
|
222
|
+
this.#resetZoomButton.type = "button";
|
|
223
|
+
this.#resetZoomButton.textContent = "100%";
|
|
224
|
+
this.#resetZoomButton.addEventListener("click", () => {
|
|
225
|
+
void this.setZoom(1);
|
|
226
|
+
});
|
|
227
|
+
this.#downloadButton.type = "button";
|
|
228
|
+
this.#downloadButton.textContent = "Download";
|
|
229
|
+
this.#downloadButton.className = "primary";
|
|
230
|
+
this.#downloadButton.addEventListener("click", () => {
|
|
231
|
+
void this.download();
|
|
232
|
+
});
|
|
233
|
+
this.#navGroup.append(this.#prevButton, this.#pageValue, this.#nextButton);
|
|
234
|
+
this.#zoomGroup.append(this.#zoomOutButton, this.#zoomValue, this.#zoomInButton, this.#resetZoomButton);
|
|
235
|
+
this.#actionGroup.append(this.#downloadButton);
|
|
236
|
+
this.#toolbar.append(this.#title, this.#navGroup, this.#zoomGroup, this.#actionGroup);
|
|
237
|
+
this.#stage.className = "stage single";
|
|
238
|
+
this.#stage.addEventListener("scroll", this.#handleScroll);
|
|
239
|
+
this.#status.className = "status";
|
|
240
|
+
this.#stage.appendChild(this.#status);
|
|
241
|
+
shell.append(this.#toolbar, this.#stage);
|
|
242
|
+
this.#shadow.append(style, shell);
|
|
243
|
+
}
|
|
244
|
+
connectedCallback() {
|
|
245
|
+
window.addEventListener("resize", this.#handleResize);
|
|
246
|
+
if (!this.#config) {
|
|
247
|
+
const attributeConfig = this.#configFromAttributes();
|
|
248
|
+
if (attributeConfig) {
|
|
249
|
+
this.configure(attributeConfig);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (this.#config) {
|
|
254
|
+
void this.#loadAndRender();
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
this.#setStatus("Set a PDF source to begin.");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
disconnectedCallback() {
|
|
261
|
+
window.removeEventListener("resize", this.#handleResize);
|
|
262
|
+
void this.#teardownDocument();
|
|
263
|
+
}
|
|
264
|
+
attributeChangedCallback() {
|
|
265
|
+
const attributeConfig = this.#configFromAttributes();
|
|
266
|
+
if (attributeConfig) {
|
|
267
|
+
this.configure(attributeConfig);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
configure(config) {
|
|
271
|
+
const nextZoom = config.zoom ?? DEFAULT_CONFIG.zoom;
|
|
272
|
+
const nextConfig = {
|
|
273
|
+
...DEFAULT_CONFIG,
|
|
274
|
+
...config,
|
|
275
|
+
src: cloneSource(config.src),
|
|
276
|
+
httpHeaders: config.httpHeaders ? { ...config.httpHeaders } : undefined,
|
|
277
|
+
page: config.page ?? DEFAULT_CONFIG.page,
|
|
278
|
+
zoom: nextZoom,
|
|
279
|
+
};
|
|
280
|
+
nextConfig.zoom = clampZoom(nextZoom, nextConfig.minZoom ?? DEFAULT_CONFIG.minZoom, nextConfig.maxZoom ?? DEFAULT_CONFIG.maxZoom);
|
|
281
|
+
const previous = this.#config;
|
|
282
|
+
this.#config = nextConfig;
|
|
283
|
+
this.#syncToolbarState();
|
|
284
|
+
if (!this.isConnected) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const sourceChanged = !previous ||
|
|
288
|
+
!isSameSource(previous.src, nextConfig.src) ||
|
|
289
|
+
previous.workerSrc !== nextConfig.workerSrc ||
|
|
290
|
+
previous.withCredentials !== nextConfig.withCredentials ||
|
|
291
|
+
!sameHeaders(previous.httpHeaders, nextConfig.httpHeaders);
|
|
292
|
+
if (sourceChanged) {
|
|
293
|
+
void this.#loadAndRender();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const layoutChanged = previous.continuous !== nextConfig.continuous ||
|
|
297
|
+
previous.fit !== nextConfig.fit ||
|
|
298
|
+
previous.zoom !== nextConfig.zoom ||
|
|
299
|
+
previous.minZoom !== nextConfig.minZoom ||
|
|
300
|
+
previous.maxZoom !== nextConfig.maxZoom;
|
|
301
|
+
if (layoutChanged) {
|
|
302
|
+
void this.#render();
|
|
303
|
+
emitViewerEvent(this, "zoomchange", { zoom: nextConfig.zoom ?? DEFAULT_CONFIG.zoom });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (previous.page !== nextConfig.page) {
|
|
307
|
+
if (nextConfig.continuous) {
|
|
308
|
+
this.#scrollToPage(nextConfig.page ?? DEFAULT_CONFIG.page, true);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
void this.#render();
|
|
312
|
+
}
|
|
313
|
+
emitViewerEvent(this, "pagechange", {
|
|
314
|
+
page: nextConfig.page ?? DEFAULT_CONFIG.page,
|
|
315
|
+
pageCount: this.#pageCount,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async reload() {
|
|
320
|
+
await this.#loadAndRender();
|
|
321
|
+
}
|
|
322
|
+
async goToPage(page) {
|
|
323
|
+
if (!this.#config) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const nextPage = clampPage(page, this.#pageCount || Number.MAX_SAFE_INTEGER);
|
|
327
|
+
this.#config = { ...this.#config, page: nextPage };
|
|
328
|
+
this.#syncToolbarState();
|
|
329
|
+
if (this.#config.continuous) {
|
|
330
|
+
this.#scrollToPage(nextPage, true);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
await this.#render();
|
|
334
|
+
}
|
|
335
|
+
emitViewerEvent(this, "pagechange", {
|
|
336
|
+
page: nextPage,
|
|
337
|
+
pageCount: this.#pageCount,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
async nextPage() {
|
|
341
|
+
if (!this.#config) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
await this.goToPage((this.#config.page ?? DEFAULT_CONFIG.page) + 1);
|
|
345
|
+
}
|
|
346
|
+
async prevPage() {
|
|
347
|
+
if (!this.#config) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
await this.goToPage((this.#config.page ?? DEFAULT_CONFIG.page) - 1);
|
|
351
|
+
}
|
|
352
|
+
async setZoom(zoom) {
|
|
353
|
+
if (!this.#config) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const nextZoom = clampZoom(zoom, this.#config.minZoom ?? DEFAULT_CONFIG.minZoom, this.#config.maxZoom ?? DEFAULT_CONFIG.maxZoom);
|
|
357
|
+
this.#config = { ...this.#config, zoom: nextZoom };
|
|
358
|
+
this.#syncToolbarState();
|
|
359
|
+
await this.#render();
|
|
360
|
+
emitViewerEvent(this, "zoomchange", { zoom: nextZoom });
|
|
361
|
+
}
|
|
362
|
+
async zoomIn() {
|
|
363
|
+
if (!this.#config) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
await this.setZoom((this.#config.zoom ?? DEFAULT_CONFIG.zoom) +
|
|
367
|
+
(this.#config.zoomStep ?? DEFAULT_CONFIG.zoomStep));
|
|
368
|
+
}
|
|
369
|
+
async zoomOut() {
|
|
370
|
+
if (!this.#config) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
await this.setZoom((this.#config.zoom ?? DEFAULT_CONFIG.zoom) -
|
|
374
|
+
(this.#config.zoomStep ?? DEFAULT_CONFIG.zoomStep));
|
|
375
|
+
}
|
|
376
|
+
async download() {
|
|
377
|
+
if (!this.#config?.allowDownload) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const link = document.createElement("a");
|
|
381
|
+
const blob = await createBlobFromSource(this.#config);
|
|
382
|
+
if (blob) {
|
|
383
|
+
const href = URL.createObjectURL(blob);
|
|
384
|
+
link.href = href;
|
|
385
|
+
link.download = resolveTitle(this.#config).endsWith(".pdf")
|
|
386
|
+
? resolveTitle(this.#config)
|
|
387
|
+
: `${resolveTitle(this.#config)}.pdf`;
|
|
388
|
+
document.body.appendChild(link);
|
|
389
|
+
link.click();
|
|
390
|
+
link.remove();
|
|
391
|
+
URL.revokeObjectURL(href);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (typeof this.#config.src === "string") {
|
|
395
|
+
window.open(this.#config.src, "_blank", "noopener,noreferrer");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (this.#config.src instanceof URL) {
|
|
399
|
+
window.open(this.#config.src.toString(), "_blank", "noopener,noreferrer");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async destroy() {
|
|
403
|
+
await this.#teardownDocument();
|
|
404
|
+
this.#stage.replaceChildren(this.#status);
|
|
405
|
+
this.#setStatus("Viewer destroyed.");
|
|
406
|
+
}
|
|
407
|
+
#configFromAttributes() {
|
|
408
|
+
const src = this.getAttribute("src");
|
|
409
|
+
if (!src) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
src,
|
|
414
|
+
title: this.getAttribute("title") ?? undefined,
|
|
415
|
+
page: parseNumberAttribute(this.getAttribute("page"), DEFAULT_CONFIG.page),
|
|
416
|
+
zoom: parseNumberAttribute(this.getAttribute("zoom"), DEFAULT_CONFIG.zoom),
|
|
417
|
+
fit: this.getAttribute("fit") ?? DEFAULT_CONFIG.fit,
|
|
418
|
+
continuous: parseBooleanAttribute(this.getAttribute("continuous"), DEFAULT_CONFIG.continuous),
|
|
419
|
+
showToolbar: parseBooleanAttribute(this.getAttribute("toolbar"), DEFAULT_CONFIG.showToolbar),
|
|
420
|
+
allowDownload: parseBooleanAttribute(this.getAttribute("download"), DEFAULT_CONFIG.allowDownload),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
async #loadAndRender() {
|
|
424
|
+
const config = this.#config;
|
|
425
|
+
if (!config) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const loadVersion = ++this.#loadVersion;
|
|
429
|
+
this.#setStatus("Loading PDF...");
|
|
430
|
+
await this.#teardownDocument();
|
|
431
|
+
try {
|
|
432
|
+
const globalWorkerOptions = pdfjsLib.GlobalWorkerOptions;
|
|
433
|
+
globalWorkerOptions.workerPort = null;
|
|
434
|
+
globalWorkerOptions.workerSrc =
|
|
435
|
+
config.workerSrc ?? new URL("./pdf.worker.min.mjs", import.meta.url).toString();
|
|
436
|
+
const params = await toDocumentParameters(config);
|
|
437
|
+
const task = pdfjsLib.getDocument(params);
|
|
438
|
+
this.#loadingTask = task;
|
|
439
|
+
const documentProxy = await task.promise;
|
|
440
|
+
if (loadVersion !== this.#loadVersion) {
|
|
441
|
+
await documentProxy.destroy();
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
this.#document = documentProxy;
|
|
445
|
+
this.#pageCount = documentProxy.numPages;
|
|
446
|
+
this.#config = {
|
|
447
|
+
...config,
|
|
448
|
+
page: clampPage(config.page ?? DEFAULT_CONFIG.page, documentProxy.numPages),
|
|
449
|
+
};
|
|
450
|
+
this.#syncToolbarState();
|
|
451
|
+
await this.#render();
|
|
452
|
+
const detail = {
|
|
453
|
+
page: this.#config.page ?? DEFAULT_CONFIG.page,
|
|
454
|
+
pageCount: this.#pageCount,
|
|
455
|
+
zoom: this.#config.zoom ?? DEFAULT_CONFIG.zoom,
|
|
456
|
+
};
|
|
457
|
+
emitViewerEvent(this, "ready", detail);
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
const detail = {
|
|
461
|
+
error,
|
|
462
|
+
message: this.#toErrorMessage(error),
|
|
463
|
+
};
|
|
464
|
+
this.#setStatus(detail.message);
|
|
465
|
+
emitViewerEvent(this, "error", detail);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async #teardownDocument() {
|
|
469
|
+
this.#renderVersion += 1;
|
|
470
|
+
const loadingTask = this.#loadingTask;
|
|
471
|
+
this.#loadingTask = null;
|
|
472
|
+
if (loadingTask) {
|
|
473
|
+
try {
|
|
474
|
+
await loadingTask.destroy();
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// Ignore teardown failures.
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const documentProxy = this.#document;
|
|
481
|
+
this.#document = null;
|
|
482
|
+
this.#pageCount = 0;
|
|
483
|
+
if (documentProxy) {
|
|
484
|
+
try {
|
|
485
|
+
await documentProxy.destroy();
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// Ignore teardown failures.
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async #render() {
|
|
493
|
+
const config = this.#config;
|
|
494
|
+
const documentProxy = this.#document;
|
|
495
|
+
if (!config || !documentProxy) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const renderVersion = ++this.#renderVersion;
|
|
499
|
+
const scrollSnapshot = config.continuous ? this.#captureScrollSnapshot() : null;
|
|
500
|
+
this.#setStatus(null);
|
|
501
|
+
this.#stage.className = config.continuous ? "stage" : "stage single";
|
|
502
|
+
const stageChildren = Array.from(this.#stage.children);
|
|
503
|
+
for (const child of stageChildren) {
|
|
504
|
+
if (child !== this.#status) {
|
|
505
|
+
child.remove();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (config.continuous) {
|
|
509
|
+
const stack = document.createElement("div");
|
|
510
|
+
stack.className = "page-stack";
|
|
511
|
+
this.#stage.insertBefore(stack, this.#status);
|
|
512
|
+
for (let index = 1; index <= documentProxy.numPages; index += 1) {
|
|
513
|
+
if (renderVersion !== this.#renderVersion) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const page = await documentProxy.getPage(index);
|
|
517
|
+
const wrapper = await this.#renderPage(page, config, index);
|
|
518
|
+
stack.appendChild(wrapper);
|
|
519
|
+
this.#animatePageEntry(wrapper);
|
|
520
|
+
}
|
|
521
|
+
this.#restoreScrollSnapshot(scrollSnapshot ?? {
|
|
522
|
+
page: config.page ?? DEFAULT_CONFIG.page,
|
|
523
|
+
offsetRatio: 0,
|
|
524
|
+
}, false);
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
const pageNumber = clampPage(config.page ?? DEFAULT_CONFIG.page, documentProxy.numPages);
|
|
528
|
+
const page = await documentProxy.getPage(pageNumber);
|
|
529
|
+
const wrapper = await this.#renderPage(page, config, pageNumber);
|
|
530
|
+
this.#stage.insertBefore(wrapper, this.#status);
|
|
531
|
+
this.#animatePageEntry(wrapper);
|
|
532
|
+
}
|
|
533
|
+
this.#syncToolbarState();
|
|
534
|
+
}
|
|
535
|
+
async #renderPage(page, config, pageNumber) {
|
|
536
|
+
const wrapper = document.createElement("div");
|
|
537
|
+
wrapper.className = "page";
|
|
538
|
+
wrapper.dataset.page = String(pageNumber);
|
|
539
|
+
const canvas = document.createElement("canvas");
|
|
540
|
+
const context = canvas.getContext("2d");
|
|
541
|
+
if (!context) {
|
|
542
|
+
throw new Error("Canvas rendering context is unavailable.");
|
|
543
|
+
}
|
|
544
|
+
const scale = this.#computeScale(page, config);
|
|
545
|
+
const viewport = page.getViewport({ scale });
|
|
546
|
+
const dpr = window.devicePixelRatio || 1;
|
|
547
|
+
canvas.width = Math.floor(viewport.width * dpr);
|
|
548
|
+
canvas.height = Math.floor(viewport.height * dpr);
|
|
549
|
+
canvas.style.width = `${Math.floor(viewport.width)}px`;
|
|
550
|
+
canvas.style.height = `${Math.floor(viewport.height)}px`;
|
|
551
|
+
wrapper.style.width = canvas.style.width;
|
|
552
|
+
wrapper.style.minHeight = canvas.style.height;
|
|
553
|
+
wrapper.appendChild(canvas);
|
|
554
|
+
const renderContext = {
|
|
555
|
+
canvas,
|
|
556
|
+
canvasContext: context,
|
|
557
|
+
viewport,
|
|
558
|
+
};
|
|
559
|
+
if (dpr !== 1) {
|
|
560
|
+
renderContext.transform = [dpr, 0, 0, dpr, 0, 0];
|
|
561
|
+
}
|
|
562
|
+
await page.render(renderContext).promise;
|
|
563
|
+
return wrapper;
|
|
564
|
+
}
|
|
565
|
+
#computeScale(page, config) {
|
|
566
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
567
|
+
const availableWidth = Math.max(320, this.#stage.clientWidth || 960) - 32;
|
|
568
|
+
const availableHeight = Math.max(320, this.#stage.clientHeight || 720) - 32;
|
|
569
|
+
let baseScale = 1;
|
|
570
|
+
if (config.fit === "width") {
|
|
571
|
+
baseScale = availableWidth / viewport.width;
|
|
572
|
+
}
|
|
573
|
+
else if (config.fit === "page") {
|
|
574
|
+
baseScale = Math.min(availableWidth / viewport.width, availableHeight / viewport.height);
|
|
575
|
+
}
|
|
576
|
+
return clampZoom(baseScale * (config.zoom ?? DEFAULT_CONFIG.zoom), config.minZoom ?? DEFAULT_CONFIG.minZoom, config.maxZoom ?? DEFAULT_CONFIG.maxZoom);
|
|
577
|
+
}
|
|
578
|
+
#syncToolbarState() {
|
|
579
|
+
const config = this.#config;
|
|
580
|
+
const page = config?.page ?? DEFAULT_CONFIG.page;
|
|
581
|
+
const zoom = config?.zoom ?? DEFAULT_CONFIG.zoom;
|
|
582
|
+
this.#toolbar.hidden = !(config?.showToolbar ?? DEFAULT_CONFIG.showToolbar);
|
|
583
|
+
this.#title.textContent = config ? resolveTitle(config) : "PDF viewer";
|
|
584
|
+
this.#pageValue.textContent = `${page} / ${this.#pageCount || 0}`;
|
|
585
|
+
this.#zoomValue.textContent = `${Math.round(zoom * 100)}%`;
|
|
586
|
+
this.#prevButton.disabled = page <= 1;
|
|
587
|
+
this.#nextButton.disabled = this.#pageCount > 0 ? page >= this.#pageCount : true;
|
|
588
|
+
this.#downloadButton.hidden = !(config?.allowDownload ?? DEFAULT_CONFIG.allowDownload);
|
|
589
|
+
}
|
|
590
|
+
#setStatus(message) {
|
|
591
|
+
if (!message) {
|
|
592
|
+
this.#status.className = "status";
|
|
593
|
+
this.#status.textContent = "";
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
this.#status.className = "status visible";
|
|
597
|
+
this.#status.textContent = message;
|
|
598
|
+
}
|
|
599
|
+
#scrollToPage(page, smooth) {
|
|
600
|
+
const target = this.#stage.querySelector(`.page[data-page="${page}"]`);
|
|
601
|
+
if (!target) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
target.scrollIntoView({
|
|
605
|
+
block: "start",
|
|
606
|
+
behavior: smooth ? "smooth" : "auto",
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
#captureScrollSnapshot() {
|
|
610
|
+
const currentPage = this.#detectVisiblePage();
|
|
611
|
+
if (!currentPage) {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
const target = this.#stage.querySelector(`.page[data-page="${currentPage}"]`);
|
|
615
|
+
if (!target) {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
const offsetWithinPage = Math.max(0, this.#stage.scrollTop - target.offsetTop);
|
|
619
|
+
const offsetRatio = target.offsetHeight > 0 ? Math.min(1, offsetWithinPage / target.offsetHeight) : 0;
|
|
620
|
+
return {
|
|
621
|
+
page: currentPage,
|
|
622
|
+
offsetRatio,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
#restoreScrollSnapshot(snapshot, smooth) {
|
|
626
|
+
const target = this.#stage.querySelector(`.page[data-page="${snapshot.page}"]`);
|
|
627
|
+
if (!target) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const top = target.offsetTop + target.offsetHeight * snapshot.offsetRatio;
|
|
631
|
+
this.#stage.scrollTo({
|
|
632
|
+
top,
|
|
633
|
+
behavior: smooth ? "smooth" : "auto",
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
#animatePageEntry(wrapper) {
|
|
637
|
+
wrapper.classList.add("entering");
|
|
638
|
+
requestAnimationFrame(() => {
|
|
639
|
+
wrapper.classList.remove("entering");
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
#detectVisiblePage() {
|
|
643
|
+
const pages = Array.from(this.#stage.querySelectorAll(".page"));
|
|
644
|
+
if (pages.length === 0) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
const midpoint = this.#stage.scrollTop + this.#stage.clientHeight / 2;
|
|
648
|
+
const visible = pages.find((page) => {
|
|
649
|
+
const top = page.offsetTop;
|
|
650
|
+
const bottom = top + page.offsetHeight;
|
|
651
|
+
return midpoint >= top && midpoint <= bottom;
|
|
652
|
+
}) ?? pages[0];
|
|
653
|
+
const parsed = Number(visible.dataset.page);
|
|
654
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
655
|
+
}
|
|
656
|
+
#toErrorMessage(error) {
|
|
657
|
+
if (error instanceof Error && error.message) {
|
|
658
|
+
return error.message;
|
|
659
|
+
}
|
|
660
|
+
if (typeof error === "string") {
|
|
661
|
+
return error;
|
|
662
|
+
}
|
|
663
|
+
return "Failed to load PDF.";
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
export function defineGenusPdfViewerElement(tagName = GENUS_PDF_VIEWER_TAG) {
|
|
667
|
+
if (!customElements.get(tagName)) {
|
|
668
|
+
customElements.define(tagName, GenusPdfViewerElement);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
export function createGenusPdfViewer(container, config) {
|
|
672
|
+
defineGenusPdfViewerElement();
|
|
673
|
+
const element = document.createElement(GENUS_PDF_VIEWER_TAG);
|
|
674
|
+
element.configure(config);
|
|
675
|
+
container.appendChild(element);
|
|
676
|
+
return element;
|
|
677
|
+
}
|
|
678
|
+
//# sourceMappingURL=viewer.js.map
|