genus-pdf-viewer 0.2.3 → 0.2.11
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 +50 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pdf-worker-source.generated.d.ts +2 -0
- package/dist/pdf-worker-source.generated.d.ts.map +1 -0
- package/dist/pdf-worker-source.generated.js +4 -0
- package/dist/pdf-worker-source.generated.js.map +1 -0
- package/dist/pdf.worker.bootstrap.mjs +43 -0
- package/dist/pdf.worker.min.mjs +4 -4
- package/dist/styles.css +211 -34
- package/dist/styles.d.ts +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +211 -34
- package/dist/styles.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -0
- package/dist/types.js.map +0 -0
- package/dist/viewer.d.ts +7 -0
- package/dist/viewer.d.ts.map +1 -1
- package/dist/viewer.js +706 -85
- package/dist/viewer.js.map +1 -1
- package/package.json +8 -9
package/dist/viewer.js
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
|
-
import * as pdfjsLib from "pdfjs-dist";
|
|
1
|
+
import * as pdfjsLib from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
2
|
+
import { PDF_WORKER_SOURCE } from "./pdf-worker-source.generated.js";
|
|
2
3
|
import { VIEWER_STYLES } from "./styles.js";
|
|
3
4
|
const GENUS_PDF_VIEWER_TAG = "genus-pdf-viewer";
|
|
5
|
+
export const PDF_WORKER_BOOTSTRAP_ASSET_PATH = "assets/pdf.worker.bootstrap.mjs";
|
|
6
|
+
const PDF_WORKER_BLOB_MIME_TYPE = "text/javascript";
|
|
7
|
+
const PDF_WORKER_COMPAT_SOURCE = `
|
|
8
|
+
if (typeof Promise.try !== "function") {
|
|
9
|
+
Promise.try = (callback, ...args) =>
|
|
10
|
+
new Promise((resolve, reject) => {
|
|
11
|
+
try {
|
|
12
|
+
resolve(callback(...args));
|
|
13
|
+
} catch (error) {
|
|
14
|
+
reject(error);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof Promise.withResolvers !== "function") {
|
|
20
|
+
Promise.withResolvers = () => {
|
|
21
|
+
let resolve;
|
|
22
|
+
let reject;
|
|
23
|
+
const promise = new Promise((promiseResolve, promiseReject) => {
|
|
24
|
+
resolve = promiseResolve;
|
|
25
|
+
reject = promiseReject;
|
|
26
|
+
});
|
|
27
|
+
return { promise, resolve, reject };
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof URL.parse !== "function") {
|
|
32
|
+
URL.parse = (url, base) => {
|
|
33
|
+
try {
|
|
34
|
+
return new URL(url, base);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
const IOS_CANVAS_PIXEL_LIMIT = 16_777_216;
|
|
42
|
+
let defaultPdfWorkerSrc = null;
|
|
4
43
|
const DEFAULT_CONFIG = {
|
|
5
44
|
page: 1,
|
|
6
45
|
zoom: 1,
|
|
@@ -72,21 +111,261 @@ function resolveTitle(config) {
|
|
|
72
111
|
}
|
|
73
112
|
return "PDF viewer";
|
|
74
113
|
}
|
|
75
|
-
|
|
114
|
+
function resolveSourceHref(source) {
|
|
115
|
+
if (typeof source === "string") {
|
|
116
|
+
return source;
|
|
117
|
+
}
|
|
118
|
+
if (source instanceof URL) {
|
|
119
|
+
return source.toString();
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
function resolveProxyEndpoint(config, sourceHref) {
|
|
124
|
+
if (config.proxyUrl === false) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
if (typeof config.proxyUrl === "string" && config.proxyUrl) {
|
|
128
|
+
return config.proxyUrl;
|
|
129
|
+
}
|
|
130
|
+
return isCrossOriginSource(sourceHref) ? "/__proxy" : null;
|
|
131
|
+
}
|
|
132
|
+
function resolveEffectiveSourceHref(config, proxyPreference = "auto") {
|
|
133
|
+
const sourceHref = resolveSourceHref(config.src);
|
|
134
|
+
if (!sourceHref) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (proxyPreference === "never") {
|
|
138
|
+
return sourceHref;
|
|
139
|
+
}
|
|
140
|
+
const proxyEndpoint = resolveProxyEndpoint(config, sourceHref);
|
|
141
|
+
if (!proxyEndpoint) {
|
|
142
|
+
return sourceHref;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const proxyUrl = new URL(proxyEndpoint, typeof window === "undefined" ? "http://localhost" : window.location.href);
|
|
146
|
+
proxyUrl.searchParams.set("url", sourceHref);
|
|
147
|
+
return proxyUrl.toString();
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
const separator = proxyEndpoint.includes("?") ? "&" : "?";
|
|
151
|
+
return `${proxyEndpoint}${separator}url=${encodeURIComponent(sourceHref)}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function isAppleMobileBrowser() {
|
|
155
|
+
if (typeof navigator === "undefined") {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
const userAgent = navigator.userAgent ?? "";
|
|
159
|
+
const platform = navigator.platform ?? "";
|
|
160
|
+
return (/iPad|iPhone|iPod/i.test(userAgent) ||
|
|
161
|
+
(/Mac/i.test(platform) && (navigator.maxTouchPoints ?? 0) > 1));
|
|
162
|
+
}
|
|
163
|
+
function getPdfJsRuntimeOptions() {
|
|
164
|
+
if (!isAppleMobileBrowser()) {
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
isOffscreenCanvasSupported: false,
|
|
169
|
+
isImageDecoderSupported: false,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function getCanvasOutputScale(width, height) {
|
|
173
|
+
const devicePixelRatio = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
|
|
174
|
+
if (!isAppleMobileBrowser()) {
|
|
175
|
+
return devicePixelRatio;
|
|
176
|
+
}
|
|
177
|
+
const pixelArea = Math.max(width * height, 1);
|
|
178
|
+
const maxScale = Math.sqrt(IOS_CANVAS_PIXEL_LIMIT / pixelArea);
|
|
179
|
+
return Math.min(devicePixelRatio, maxScale);
|
|
180
|
+
}
|
|
181
|
+
function iconLabelMarkup(svg, label) {
|
|
182
|
+
return `<span class="button-icon" aria-hidden="true">${svg}</span><span class="button-label">${label}</span>`;
|
|
183
|
+
}
|
|
184
|
+
function openUrl(url) {
|
|
185
|
+
const openedWindow = window.open(url, "_blank", "noopener,noreferrer");
|
|
186
|
+
if (!openedWindow) {
|
|
187
|
+
window.location.href = url;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function releaseObjectUrl(href) {
|
|
191
|
+
window.setTimeout(() => {
|
|
192
|
+
URL.revokeObjectURL(href);
|
|
193
|
+
}, 1_000);
|
|
194
|
+
}
|
|
195
|
+
function triggerDownload(href, filename) {
|
|
196
|
+
const link = document.createElement("a");
|
|
197
|
+
link.href = href;
|
|
198
|
+
link.download = filename;
|
|
199
|
+
link.rel = "noopener";
|
|
200
|
+
document.body.appendChild(link);
|
|
201
|
+
link.click();
|
|
202
|
+
link.remove();
|
|
203
|
+
}
|
|
204
|
+
async function tryNativeFileDownload(blob, filename) {
|
|
205
|
+
if (typeof navigator === "undefined" || typeof File === "undefined") {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
if (typeof navigator.share !== "function") {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const file = new File([blob], filename, { type: blob.type || "application/pdf" });
|
|
213
|
+
if (typeof navigator.canShare === "function" && !navigator.canShare({ files: [file] })) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
await navigator.share({
|
|
217
|
+
title: filename,
|
|
218
|
+
files: [file],
|
|
219
|
+
});
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function shouldUseCompactToolbarUi() {
|
|
227
|
+
return isAppleMobileBrowser();
|
|
228
|
+
}
|
|
229
|
+
function ensurePromiseCompat() {
|
|
230
|
+
const promiseCtor = Promise;
|
|
231
|
+
if (typeof promiseCtor.try !== "function") {
|
|
232
|
+
promiseCtor.try = (callback, ...args) => new Promise((resolve, reject) => {
|
|
233
|
+
try {
|
|
234
|
+
resolve(callback(...args));
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
reject(error);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (typeof promiseCtor.withResolvers !== "function") {
|
|
242
|
+
promiseCtor.withResolvers = () => {
|
|
243
|
+
let resolve;
|
|
244
|
+
let reject;
|
|
245
|
+
const promise = new Promise((promiseResolve, promiseReject) => {
|
|
246
|
+
resolve = promiseResolve;
|
|
247
|
+
reject = promiseReject;
|
|
248
|
+
});
|
|
249
|
+
return {
|
|
250
|
+
promise,
|
|
251
|
+
resolve,
|
|
252
|
+
reject,
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function ensureUrlParseCompat() {
|
|
258
|
+
const urlCtor = URL;
|
|
259
|
+
if (typeof URL === "undefined" || typeof urlCtor.parse === "function") {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
urlCtor.parse = (input, base) => {
|
|
263
|
+
try {
|
|
264
|
+
if (typeof base === "undefined") {
|
|
265
|
+
return new URL(input);
|
|
266
|
+
}
|
|
267
|
+
return new URL(input, base);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function ensurePdfJsCompat() {
|
|
275
|
+
ensurePromiseCompat();
|
|
276
|
+
ensureUrlParseCompat();
|
|
277
|
+
}
|
|
278
|
+
function isUnbundledPackageModuleUrl(moduleUrl) {
|
|
279
|
+
try {
|
|
280
|
+
const { pathname } = new URL(moduleUrl);
|
|
281
|
+
return /\/(?:index|viewer)\.js$/.test(pathname);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function createInlinePdfWorkerSrc() {
|
|
288
|
+
if (defaultPdfWorkerSrc) {
|
|
289
|
+
return defaultPdfWorkerSrc;
|
|
290
|
+
}
|
|
291
|
+
if (typeof Blob === "undefined" ||
|
|
292
|
+
typeof URL === "undefined" ||
|
|
293
|
+
typeof URL.createObjectURL !== "function" ||
|
|
294
|
+
!PDF_WORKER_SOURCE) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const workerBlob = new Blob([PDF_WORKER_COMPAT_SOURCE, "\n", PDF_WORKER_SOURCE], {
|
|
298
|
+
type: PDF_WORKER_BLOB_MIME_TYPE,
|
|
299
|
+
});
|
|
300
|
+
defaultPdfWorkerSrc = URL.createObjectURL(workerBlob);
|
|
301
|
+
return defaultPdfWorkerSrc;
|
|
302
|
+
}
|
|
303
|
+
export function releaseDefaultPdfWorkerSrc() {
|
|
304
|
+
if (!defaultPdfWorkerSrc) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (typeof URL !== "undefined" && typeof URL.revokeObjectURL === "function") {
|
|
308
|
+
URL.revokeObjectURL(defaultPdfWorkerSrc);
|
|
309
|
+
}
|
|
310
|
+
defaultPdfWorkerSrc = null;
|
|
311
|
+
}
|
|
312
|
+
export function resolveDefaultPdfWorkerSrc(options = {}) {
|
|
313
|
+
const moduleUrl = options.moduleUrl ?? import.meta.url;
|
|
314
|
+
const packageRelativeWorkerUrl = new URL("./pdf.worker.bootstrap.mjs", moduleUrl).toString();
|
|
315
|
+
if (isUnbundledPackageModuleUrl(moduleUrl)) {
|
|
316
|
+
return packageRelativeWorkerUrl;
|
|
317
|
+
}
|
|
318
|
+
const inlineWorkerSrc = createInlinePdfWorkerSrc();
|
|
319
|
+
if (inlineWorkerSrc) {
|
|
320
|
+
return inlineWorkerSrc;
|
|
321
|
+
}
|
|
322
|
+
const baseUri = options.baseUri ??
|
|
323
|
+
(typeof document !== "undefined" ? document.baseURI : undefined) ??
|
|
324
|
+
(typeof location !== "undefined" ? location.href : undefined);
|
|
325
|
+
return baseUri
|
|
326
|
+
? new URL(PDF_WORKER_BOOTSTRAP_ASSET_PATH, baseUri).toString()
|
|
327
|
+
: packageRelativeWorkerUrl;
|
|
328
|
+
}
|
|
329
|
+
async function createDocumentData(config) {
|
|
330
|
+
if (config.src instanceof Blob) {
|
|
331
|
+
return new Uint8Array(await config.src.arrayBuffer());
|
|
332
|
+
}
|
|
333
|
+
if (config.src instanceof Uint8Array) {
|
|
334
|
+
return new Uint8Array(config.src);
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
async function toDocumentParameters(config, proxyPreference = "auto") {
|
|
339
|
+
const runtimeOptions = getPdfJsRuntimeOptions();
|
|
340
|
+
const data = await createDocumentData(config);
|
|
341
|
+
if (data) {
|
|
342
|
+
return {
|
|
343
|
+
data,
|
|
344
|
+
disableRange: true,
|
|
345
|
+
disableStream: true,
|
|
346
|
+
disableAutoFetch: true,
|
|
347
|
+
stopAtErrors: true,
|
|
348
|
+
...runtimeOptions,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
76
351
|
if (typeof config.src === "string") {
|
|
352
|
+
const url = resolveEffectiveSourceHref(config, proxyPreference);
|
|
77
353
|
return {
|
|
78
|
-
url: config.src,
|
|
354
|
+
url: url ?? config.src,
|
|
79
355
|
withCredentials: config.withCredentials,
|
|
80
356
|
httpHeaders: config.httpHeaders,
|
|
81
357
|
stopAtErrors: true,
|
|
358
|
+
...runtimeOptions,
|
|
82
359
|
};
|
|
83
360
|
}
|
|
84
361
|
if (config.src instanceof URL) {
|
|
362
|
+
const url = resolveEffectiveSourceHref(config, proxyPreference);
|
|
85
363
|
return {
|
|
86
|
-
url: config.src.toString(),
|
|
364
|
+
url: url ?? config.src.toString(),
|
|
87
365
|
withCredentials: config.withCredentials,
|
|
88
366
|
httpHeaders: config.httpHeaders,
|
|
89
367
|
stopAtErrors: true,
|
|
368
|
+
...runtimeOptions,
|
|
90
369
|
};
|
|
91
370
|
}
|
|
92
371
|
if (config.src instanceof Blob) {
|
|
@@ -96,6 +375,7 @@ async function toDocumentParameters(config) {
|
|
|
96
375
|
disableStream: true,
|
|
97
376
|
disableAutoFetch: true,
|
|
98
377
|
stopAtErrors: true,
|
|
378
|
+
...runtimeOptions,
|
|
99
379
|
};
|
|
100
380
|
}
|
|
101
381
|
return {
|
|
@@ -104,6 +384,7 @@ async function toDocumentParameters(config) {
|
|
|
104
384
|
disableStream: true,
|
|
105
385
|
disableAutoFetch: true,
|
|
106
386
|
stopAtErrors: true,
|
|
387
|
+
...runtimeOptions,
|
|
107
388
|
};
|
|
108
389
|
}
|
|
109
390
|
async function createBlobFromSource(config) {
|
|
@@ -113,32 +394,183 @@ async function createBlobFromSource(config) {
|
|
|
113
394
|
if (config.src instanceof Uint8Array) {
|
|
114
395
|
return new Blob([new Uint8Array(config.src)], { type: "application/pdf" });
|
|
115
396
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
397
|
+
const response = await fetchSourceResponse(config);
|
|
398
|
+
if (!response) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
return response.blob();
|
|
402
|
+
}
|
|
403
|
+
function emitViewerEvent(element, type, detail) {
|
|
404
|
+
element.dispatchEvent(new CustomEvent(`genus-${type}`, {
|
|
405
|
+
bubbles: true,
|
|
406
|
+
detail,
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
function getErrorName(error) {
|
|
410
|
+
if (typeof error === "object" &&
|
|
411
|
+
error !== null &&
|
|
412
|
+
"name" in error &&
|
|
413
|
+
typeof error.name === "string") {
|
|
414
|
+
return error.name;
|
|
415
|
+
}
|
|
416
|
+
return "";
|
|
417
|
+
}
|
|
418
|
+
function getRawErrorMessage(error) {
|
|
419
|
+
if (error instanceof Error && error.message) {
|
|
420
|
+
return error.message;
|
|
421
|
+
}
|
|
422
|
+
if (typeof error === "object" &&
|
|
423
|
+
error !== null &&
|
|
424
|
+
"message" in error &&
|
|
425
|
+
typeof error.message === "string") {
|
|
426
|
+
return error.message;
|
|
427
|
+
}
|
|
428
|
+
if (typeof error === "string") {
|
|
429
|
+
return error;
|
|
430
|
+
}
|
|
431
|
+
return "";
|
|
432
|
+
}
|
|
433
|
+
function isLikelyCorsError(error) {
|
|
434
|
+
const message = getRawErrorMessage(error).toLowerCase();
|
|
435
|
+
const name = getErrorName(error).toLowerCase();
|
|
436
|
+
return (message.includes("access-control") ||
|
|
437
|
+
message.includes("cross-origin") ||
|
|
438
|
+
message.includes("cors") ||
|
|
439
|
+
message.includes("credential is not supported") ||
|
|
440
|
+
message.includes("origin ") ||
|
|
441
|
+
name.includes("cors"));
|
|
442
|
+
}
|
|
443
|
+
function isLikelyNetworkLoadError(error) {
|
|
444
|
+
const name = getErrorName(error).toLowerCase();
|
|
445
|
+
const message = getRawErrorMessage(error).toLowerCase();
|
|
446
|
+
return (name.includes("network") ||
|
|
447
|
+
name === "unknownerrorexception" ||
|
|
448
|
+
message.includes("load failed") ||
|
|
449
|
+
message.includes("failed to fetch") ||
|
|
450
|
+
message.includes("networkerror") ||
|
|
451
|
+
message.includes("unexpected server response (0)") ||
|
|
452
|
+
message.includes("fetch"));
|
|
453
|
+
}
|
|
454
|
+
function isCrossOriginSource(href) {
|
|
455
|
+
if (typeof window === "undefined") {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
return new URL(href, window.location.href).origin !== window.location.origin;
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function resolveFetchCredentials(href, withCredentials) {
|
|
466
|
+
if (withCredentials) {
|
|
467
|
+
return "include";
|
|
468
|
+
}
|
|
469
|
+
return isCrossOriginSource(href) ? "omit" : "same-origin";
|
|
470
|
+
}
|
|
471
|
+
function resolveFetchMode(href) {
|
|
472
|
+
return isCrossOriginSource(href) ? "cors" : "same-origin";
|
|
473
|
+
}
|
|
474
|
+
async function fetchSourceResponse(config, proxyPreference = "auto") {
|
|
475
|
+
const href = resolveEffectiveSourceHref(config, proxyPreference);
|
|
121
476
|
if (!href || typeof fetch === "undefined") {
|
|
122
477
|
return null;
|
|
123
478
|
}
|
|
124
479
|
const response = await fetch(href, {
|
|
125
480
|
headers: config.httpHeaders,
|
|
126
|
-
credentials: config.withCredentials
|
|
481
|
+
credentials: resolveFetchCredentials(href, config.withCredentials),
|
|
482
|
+
mode: resolveFetchMode(href),
|
|
127
483
|
});
|
|
128
484
|
if (!response.ok) {
|
|
129
485
|
return null;
|
|
130
486
|
}
|
|
131
|
-
return response
|
|
487
|
+
return response;
|
|
132
488
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
489
|
+
async function fetchDocumentDataViaXhr(config, proxyPreference = "auto") {
|
|
490
|
+
const href = resolveEffectiveSourceHref(config, proxyPreference);
|
|
491
|
+
if (!href || typeof XMLHttpRequest === "undefined") {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
return new Promise((resolve, reject) => {
|
|
495
|
+
const xhr = new XMLHttpRequest();
|
|
496
|
+
xhr.open("GET", href, true);
|
|
497
|
+
xhr.responseType = "arraybuffer";
|
|
498
|
+
xhr.withCredentials = Boolean(config.withCredentials);
|
|
499
|
+
for (const [key, value] of Object.entries(config.httpHeaders ?? {})) {
|
|
500
|
+
xhr.setRequestHeader(key, value);
|
|
501
|
+
}
|
|
502
|
+
xhr.onload = () => {
|
|
503
|
+
if ((xhr.status === 200 || xhr.status === 206) && xhr.response instanceof ArrayBuffer) {
|
|
504
|
+
resolve(new Uint8Array(xhr.response));
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
resolve(null);
|
|
508
|
+
};
|
|
509
|
+
xhr.onerror = () => {
|
|
510
|
+
reject(new TypeError("Load failed"));
|
|
511
|
+
};
|
|
512
|
+
xhr.onabort = () => {
|
|
513
|
+
reject(new TypeError("Load failed"));
|
|
514
|
+
};
|
|
515
|
+
xhr.send();
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
async function fetchDocumentDataFromSource(config, proxyPreference = "auto") {
|
|
519
|
+
try {
|
|
520
|
+
const response = await fetchSourceResponse(config, proxyPreference);
|
|
521
|
+
if (response) {
|
|
522
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
// Fall through to the XHR loader, which is often more reliable on Safari.
|
|
527
|
+
}
|
|
528
|
+
return fetchDocumentDataViaXhr(config, proxyPreference);
|
|
529
|
+
}
|
|
530
|
+
function createErrorDetail(config, error) {
|
|
531
|
+
const sourceUrl = resolveSourceHref(config.src) ?? undefined;
|
|
532
|
+
const rawMessage = getRawErrorMessage(error);
|
|
533
|
+
if (sourceUrl && isLikelyNetworkLoadError(error)) {
|
|
534
|
+
if (isCrossOriginSource(sourceUrl) && isLikelyCorsError(error)) {
|
|
535
|
+
return {
|
|
536
|
+
error,
|
|
537
|
+
code: "cors",
|
|
538
|
+
sourceUrl,
|
|
539
|
+
message: `Failed to load PDF from ${sourceUrl}. The remote server blocked this origin. Allow CORS for your app origin or pass Blob/Uint8Array PDF data instead of a URL.`,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
error,
|
|
544
|
+
code: "network",
|
|
545
|
+
sourceUrl,
|
|
546
|
+
message: `Failed to load PDF from ${sourceUrl}. Check the URL, auth headers, and network access.`,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
error,
|
|
551
|
+
code: "unknown",
|
|
552
|
+
sourceUrl,
|
|
553
|
+
message: rawMessage || "Failed to load PDF.",
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function shouldAttemptNativeUrlFallback(config, error) {
|
|
557
|
+
return Boolean(isAppleMobileBrowser() &&
|
|
558
|
+
resolveSourceHref(config.src) &&
|
|
559
|
+
(isLikelyNetworkLoadError(error) || isLikelyCorsError(error)));
|
|
138
560
|
}
|
|
139
561
|
export class GenusPdfViewerElement extends HTMLElement {
|
|
140
562
|
static get observedAttributes() {
|
|
141
|
-
return [
|
|
563
|
+
return [
|
|
564
|
+
"src",
|
|
565
|
+
"title",
|
|
566
|
+
"page",
|
|
567
|
+
"zoom",
|
|
568
|
+
"fit",
|
|
569
|
+
"continuous",
|
|
570
|
+
"toolbar",
|
|
571
|
+
"download",
|
|
572
|
+
"proxy-url",
|
|
573
|
+
];
|
|
142
574
|
}
|
|
143
575
|
#config = null;
|
|
144
576
|
#document = null;
|
|
@@ -146,6 +578,8 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
146
578
|
#pageCount = 0;
|
|
147
579
|
#loadVersion = 0;
|
|
148
580
|
#renderVersion = 0;
|
|
581
|
+
#isUsingNativeViewer = false;
|
|
582
|
+
#resizeObserver = null;
|
|
149
583
|
#shadow = this.attachShadow({ mode: "open" });
|
|
150
584
|
#toolbar = document.createElement("div");
|
|
151
585
|
#navGroup = document.createElement("div");
|
|
@@ -162,6 +596,8 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
162
596
|
#stage = document.createElement("div");
|
|
163
597
|
#status = document.createElement("div");
|
|
164
598
|
#handleResize = () => {
|
|
599
|
+
this.#syncInteractionMode();
|
|
600
|
+
this.#syncContainerSize();
|
|
165
601
|
if (!this.#document || !this.#config) {
|
|
166
602
|
return;
|
|
167
603
|
}
|
|
@@ -187,6 +623,11 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
187
623
|
};
|
|
188
624
|
constructor() {
|
|
189
625
|
super();
|
|
626
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
627
|
+
this.#resizeObserver = new ResizeObserver(() => {
|
|
628
|
+
this.#handleResize();
|
|
629
|
+
});
|
|
630
|
+
}
|
|
190
631
|
const style = document.createElement("style");
|
|
191
632
|
style.textContent = VIEWER_STYLES;
|
|
192
633
|
const shell = document.createElement("div");
|
|
@@ -198,33 +639,49 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
198
639
|
this.#pageValue.className = "value";
|
|
199
640
|
this.#zoomValue.className = "value";
|
|
200
641
|
this.#prevButton.type = "button";
|
|
201
|
-
this.#prevButton.
|
|
642
|
+
this.#prevButton.className = "compact";
|
|
643
|
+
this.#prevButton.setAttribute("aria-label", "Previous page");
|
|
644
|
+
this.#prevButton.title = "Previous page";
|
|
645
|
+
this.#prevButton.innerHTML = iconLabelMarkup('<svg viewBox="0 0 24 24" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"></path></svg>', "Prev");
|
|
202
646
|
this.#prevButton.addEventListener("click", () => {
|
|
203
647
|
void this.prevPage();
|
|
204
648
|
});
|
|
205
649
|
this.#nextButton.type = "button";
|
|
206
|
-
this.#nextButton.
|
|
650
|
+
this.#nextButton.className = "compact";
|
|
651
|
+
this.#nextButton.setAttribute("aria-label", "Next page");
|
|
652
|
+
this.#nextButton.title = "Next page";
|
|
653
|
+
this.#nextButton.innerHTML = iconLabelMarkup('<svg viewBox="0 0 24 24" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"></path></svg>', "Next");
|
|
207
654
|
this.#nextButton.addEventListener("click", () => {
|
|
208
655
|
void this.nextPage();
|
|
209
656
|
});
|
|
210
657
|
this.#zoomOutButton.type = "button";
|
|
211
658
|
this.#zoomOutButton.textContent = "−";
|
|
659
|
+
this.#zoomOutButton.setAttribute("aria-label", "Zoom out");
|
|
660
|
+
this.#zoomOutButton.title = "Zoom out";
|
|
212
661
|
this.#zoomOutButton.addEventListener("click", () => {
|
|
213
662
|
void this.zoomOut();
|
|
214
663
|
});
|
|
215
664
|
this.#zoomInButton.type = "button";
|
|
216
665
|
this.#zoomInButton.textContent = "+";
|
|
666
|
+
this.#zoomInButton.setAttribute("aria-label", "Zoom in");
|
|
667
|
+
this.#zoomInButton.title = "Zoom in";
|
|
217
668
|
this.#zoomInButton.addEventListener("click", () => {
|
|
218
669
|
void this.zoomIn();
|
|
219
670
|
});
|
|
220
671
|
this.#resetZoomButton.type = "button";
|
|
221
|
-
this.#resetZoomButton.
|
|
672
|
+
this.#resetZoomButton.className = "compact";
|
|
673
|
+
this.#resetZoomButton.setAttribute("aria-label", "Reset zoom");
|
|
674
|
+
this.#resetZoomButton.title = "Reset zoom";
|
|
675
|
+
this.#resetZoomButton.innerHTML = iconLabelMarkup('<svg viewBox="0 0 24 24" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-3.2-6.9"></path><path d="M21 3v6h-6"></path></svg>', "100%");
|
|
222
676
|
this.#resetZoomButton.addEventListener("click", () => {
|
|
223
677
|
void this.setZoom(1);
|
|
224
678
|
});
|
|
225
679
|
this.#downloadButton.type = "button";
|
|
226
|
-
this.#downloadButton.
|
|
227
|
-
this.#downloadButton.
|
|
680
|
+
this.#downloadButton.className = "primary icon-button";
|
|
681
|
+
this.#downloadButton.setAttribute("aria-label", "Download PDF");
|
|
682
|
+
this.#downloadButton.title = "Download PDF";
|
|
683
|
+
this.#downloadButton.innerHTML =
|
|
684
|
+
'<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v11"></path><path d="m7 11 5 5 5-5"></path><path d="M5 21h14"></path></svg>';
|
|
228
685
|
this.#downloadButton.addEventListener("click", () => {
|
|
229
686
|
void this.download();
|
|
230
687
|
});
|
|
@@ -241,6 +698,9 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
241
698
|
}
|
|
242
699
|
connectedCallback() {
|
|
243
700
|
window.addEventListener("resize", this.#handleResize);
|
|
701
|
+
this.#observeResizeTargets();
|
|
702
|
+
this.#syncInteractionMode();
|
|
703
|
+
this.#syncContainerSize();
|
|
244
704
|
if (!this.#config) {
|
|
245
705
|
const attributeConfig = this.#configFromAttributes();
|
|
246
706
|
if (attributeConfig) {
|
|
@@ -257,6 +717,7 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
257
717
|
}
|
|
258
718
|
disconnectedCallback() {
|
|
259
719
|
window.removeEventListener("resize", this.#handleResize);
|
|
720
|
+
this.#resizeObserver?.disconnect();
|
|
260
721
|
void this.#teardownDocument();
|
|
261
722
|
}
|
|
262
723
|
attributeChangedCallback() {
|
|
@@ -285,6 +746,7 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
285
746
|
const sourceChanged = !previous ||
|
|
286
747
|
!isSameSource(previous.src, nextConfig.src) ||
|
|
287
748
|
previous.workerSrc !== nextConfig.workerSrc ||
|
|
749
|
+
previous.proxyUrl !== nextConfig.proxyUrl ||
|
|
288
750
|
previous.withCredentials !== nextConfig.withCredentials ||
|
|
289
751
|
!sameHeaders(previous.httpHeaders, nextConfig.httpHeaders);
|
|
290
752
|
if (sourceChanged) {
|
|
@@ -318,7 +780,7 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
318
780
|
await this.#loadAndRender();
|
|
319
781
|
}
|
|
320
782
|
async goToPage(page) {
|
|
321
|
-
if (!this.#config) {
|
|
783
|
+
if (!this.#config || this.#isUsingNativeViewer) {
|
|
322
784
|
return;
|
|
323
785
|
}
|
|
324
786
|
const nextPage = clampPage(page, this.#pageCount || Number.MAX_SAFE_INTEGER);
|
|
@@ -342,13 +804,13 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
342
804
|
await this.goToPage((this.#config.page ?? DEFAULT_CONFIG.page) + 1);
|
|
343
805
|
}
|
|
344
806
|
async prevPage() {
|
|
345
|
-
if (!this.#config) {
|
|
807
|
+
if (!this.#config || this.#isUsingNativeViewer) {
|
|
346
808
|
return;
|
|
347
809
|
}
|
|
348
810
|
await this.goToPage((this.#config.page ?? DEFAULT_CONFIG.page) - 1);
|
|
349
811
|
}
|
|
350
812
|
async setZoom(zoom) {
|
|
351
|
-
if (!this.#config) {
|
|
813
|
+
if (!this.#config || this.#isUsingNativeViewer) {
|
|
352
814
|
return;
|
|
353
815
|
}
|
|
354
816
|
const nextZoom = clampZoom(zoom, this.#config.minZoom ?? DEFAULT_CONFIG.minZoom, this.#config.maxZoom ?? DEFAULT_CONFIG.maxZoom);
|
|
@@ -358,14 +820,14 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
358
820
|
emitViewerEvent(this, "zoomchange", { zoom: nextZoom });
|
|
359
821
|
}
|
|
360
822
|
async zoomIn() {
|
|
361
|
-
if (!this.#config) {
|
|
823
|
+
if (!this.#config || this.#isUsingNativeViewer) {
|
|
362
824
|
return;
|
|
363
825
|
}
|
|
364
826
|
await this.setZoom((this.#config.zoom ?? DEFAULT_CONFIG.zoom) +
|
|
365
827
|
(this.#config.zoomStep ?? DEFAULT_CONFIG.zoomStep));
|
|
366
828
|
}
|
|
367
829
|
async zoomOut() {
|
|
368
|
-
if (!this.#config) {
|
|
830
|
+
if (!this.#config || this.#isUsingNativeViewer) {
|
|
369
831
|
return;
|
|
370
832
|
}
|
|
371
833
|
await this.setZoom((this.#config.zoom ?? DEFAULT_CONFIG.zoom) -
|
|
@@ -375,26 +837,29 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
375
837
|
if (!this.#config?.allowDownload) {
|
|
376
838
|
return;
|
|
377
839
|
}
|
|
378
|
-
const link = document.createElement("a");
|
|
379
840
|
const blob = await createBlobFromSource(this.#config);
|
|
380
841
|
if (blob) {
|
|
381
842
|
const href = URL.createObjectURL(blob);
|
|
382
|
-
|
|
383
|
-
link.download = resolveTitle(this.#config).endsWith(".pdf")
|
|
843
|
+
const filename = resolveTitle(this.#config).endsWith(".pdf")
|
|
384
844
|
? resolveTitle(this.#config)
|
|
385
845
|
: `${resolveTitle(this.#config)}.pdf`;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
846
|
+
try {
|
|
847
|
+
if (isAppleMobileBrowser() && (await tryNativeFileDownload(blob, filename))) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
triggerDownload(href, filename);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
finally {
|
|
854
|
+
releaseObjectUrl(href);
|
|
855
|
+
}
|
|
391
856
|
}
|
|
392
857
|
if (typeof this.#config.src === "string") {
|
|
393
|
-
|
|
858
|
+
openUrl(this.#config.src);
|
|
394
859
|
return;
|
|
395
860
|
}
|
|
396
861
|
if (this.#config.src instanceof URL) {
|
|
397
|
-
|
|
862
|
+
openUrl(this.#config.src.toString());
|
|
398
863
|
}
|
|
399
864
|
}
|
|
400
865
|
async destroy() {
|
|
@@ -416,6 +881,13 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
416
881
|
continuous: parseBooleanAttribute(this.getAttribute("continuous"), DEFAULT_CONFIG.continuous),
|
|
417
882
|
showToolbar: parseBooleanAttribute(this.getAttribute("toolbar"), DEFAULT_CONFIG.showToolbar),
|
|
418
883
|
allowDownload: parseBooleanAttribute(this.getAttribute("download"), DEFAULT_CONFIG.allowDownload),
|
|
884
|
+
proxyUrl: (() => {
|
|
885
|
+
const value = this.getAttribute("proxy-url");
|
|
886
|
+
if (value === null) {
|
|
887
|
+
return undefined;
|
|
888
|
+
}
|
|
889
|
+
return value === "false" ? false : value;
|
|
890
|
+
})(),
|
|
419
891
|
};
|
|
420
892
|
}
|
|
421
893
|
async #loadAndRender() {
|
|
@@ -424,19 +896,16 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
424
896
|
return;
|
|
425
897
|
}
|
|
426
898
|
const loadVersion = ++this.#loadVersion;
|
|
899
|
+
this.#isUsingNativeViewer = false;
|
|
427
900
|
this.#setStatus("Loading PDF...");
|
|
428
901
|
await this.#teardownDocument();
|
|
429
902
|
try {
|
|
430
903
|
const globalWorkerOptions = pdfjsLib.GlobalWorkerOptions;
|
|
431
904
|
globalWorkerOptions.workerPort = null;
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
this.#loadingTask = task;
|
|
437
|
-
const documentProxy = await task.promise;
|
|
438
|
-
if (loadVersion !== this.#loadVersion) {
|
|
439
|
-
await documentProxy.destroy();
|
|
905
|
+
ensurePdfJsCompat();
|
|
906
|
+
globalWorkerOptions.workerSrc = config.workerSrc ?? resolveDefaultPdfWorkerSrc();
|
|
907
|
+
const documentProxy = await this.#loadDocumentProxy(config, loadVersion);
|
|
908
|
+
if (!documentProxy) {
|
|
440
909
|
return;
|
|
441
910
|
}
|
|
442
911
|
this.#document = documentProxy;
|
|
@@ -455,16 +924,79 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
455
924
|
emitViewerEvent(this, "ready", detail);
|
|
456
925
|
}
|
|
457
926
|
catch (error) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
927
|
+
if (this.#tryRenderNativeUrlFallback(config, error)) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
const detail = createErrorDetail(config, error);
|
|
462
931
|
this.#setStatus(detail.message);
|
|
463
932
|
emitViewerEvent(this, "error", detail);
|
|
464
933
|
}
|
|
465
934
|
}
|
|
935
|
+
async #loadDocumentProxy(config, loadVersion) {
|
|
936
|
+
try {
|
|
937
|
+
return await this.#loadDocumentProxyFromParams(await toDocumentParameters(config, "auto"), loadVersion);
|
|
938
|
+
}
|
|
939
|
+
catch (error) {
|
|
940
|
+
let currentError = error;
|
|
941
|
+
const directRetryParams = await this.#createDirectUrlRetryParameters(config);
|
|
942
|
+
if (directRetryParams) {
|
|
943
|
+
try {
|
|
944
|
+
return await this.#loadDocumentProxyFromParams(directRetryParams, loadVersion);
|
|
945
|
+
}
|
|
946
|
+
catch (directError) {
|
|
947
|
+
currentError = directError;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
const fallbackParams = await this.#createFetchedDataFallbackParameters(config, currentError, directRetryParams ? "never" : "auto");
|
|
951
|
+
if (!fallbackParams) {
|
|
952
|
+
throw currentError;
|
|
953
|
+
}
|
|
954
|
+
return this.#loadDocumentProxyFromParams(fallbackParams, loadVersion);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
async #loadDocumentProxyFromParams(params, loadVersion) {
|
|
958
|
+
const task = pdfjsLib.getDocument(params);
|
|
959
|
+
this.#loadingTask = task;
|
|
960
|
+
const documentProxy = await task.promise;
|
|
961
|
+
if (loadVersion !== this.#loadVersion) {
|
|
962
|
+
await documentProxy.destroy();
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
return documentProxy;
|
|
966
|
+
}
|
|
967
|
+
async #createFetchedDataFallbackParameters(config, error, proxyPreference) {
|
|
968
|
+
if (!resolveSourceHref(config.src) || !isLikelyNetworkLoadError(error)) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
try {
|
|
972
|
+
const data = await fetchDocumentDataFromSource(config, proxyPreference);
|
|
973
|
+
if (!data) {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
data,
|
|
978
|
+
disableRange: true,
|
|
979
|
+
disableStream: true,
|
|
980
|
+
disableAutoFetch: true,
|
|
981
|
+
stopAtErrors: true,
|
|
982
|
+
...getPdfJsRuntimeOptions(),
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
async #createDirectUrlRetryParameters(config) {
|
|
990
|
+
const proxiedHref = resolveEffectiveSourceHref(config, "auto");
|
|
991
|
+
const directHref = resolveEffectiveSourceHref(config, "never");
|
|
992
|
+
if (!proxiedHref || !directHref || proxiedHref === directHref) {
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
return toDocumentParameters(config, "never");
|
|
996
|
+
}
|
|
466
997
|
async #teardownDocument() {
|
|
467
998
|
this.#renderVersion += 1;
|
|
999
|
+
this.#isUsingNativeViewer = false;
|
|
468
1000
|
const loadingTask = this.#loadingTask;
|
|
469
1001
|
this.#loadingTask = null;
|
|
470
1002
|
if (loadingTask) {
|
|
@@ -497,16 +1029,10 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
497
1029
|
const scrollSnapshot = config.continuous ? this.#captureScrollSnapshot() : null;
|
|
498
1030
|
this.#setStatus(null);
|
|
499
1031
|
this.#stage.className = config.continuous ? "stage" : "stage single";
|
|
500
|
-
const stageChildren = Array.from(this.#stage.children);
|
|
501
|
-
for (const child of stageChildren) {
|
|
502
|
-
if (child !== this.#status) {
|
|
503
|
-
child.remove();
|
|
504
|
-
}
|
|
505
|
-
}
|
|
1032
|
+
const stageChildren = Array.from(this.#stage.children).filter((child) => child !== this.#status);
|
|
506
1033
|
if (config.continuous) {
|
|
507
1034
|
const stack = document.createElement("div");
|
|
508
1035
|
stack.className = "page-stack";
|
|
509
|
-
this.#stage.insertBefore(stack, this.#status);
|
|
510
1036
|
for (let index = 1; index <= documentProxy.numPages; index += 1) {
|
|
511
1037
|
if (renderVersion !== this.#renderVersion) {
|
|
512
1038
|
return;
|
|
@@ -516,6 +1042,10 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
516
1042
|
stack.appendChild(wrapper);
|
|
517
1043
|
this.#animatePageEntry(wrapper);
|
|
518
1044
|
}
|
|
1045
|
+
for (const child of stageChildren) {
|
|
1046
|
+
child.remove();
|
|
1047
|
+
}
|
|
1048
|
+
this.#stage.insertBefore(stack, this.#status);
|
|
519
1049
|
this.#restoreScrollSnapshot(scrollSnapshot ?? {
|
|
520
1050
|
page: config.page ?? DEFAULT_CONFIG.page,
|
|
521
1051
|
offsetRatio: 0,
|
|
@@ -525,6 +1055,9 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
525
1055
|
const pageNumber = clampPage(config.page ?? DEFAULT_CONFIG.page, documentProxy.numPages);
|
|
526
1056
|
const page = await documentProxy.getPage(pageNumber);
|
|
527
1057
|
const wrapper = await this.#renderPage(page, config, pageNumber);
|
|
1058
|
+
for (const child of stageChildren) {
|
|
1059
|
+
child.remove();
|
|
1060
|
+
}
|
|
528
1061
|
this.#stage.insertBefore(wrapper, this.#status);
|
|
529
1062
|
this.#animatePageEntry(wrapper);
|
|
530
1063
|
}
|
|
@@ -541,9 +1074,9 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
541
1074
|
}
|
|
542
1075
|
const scale = this.#computeScale(page, config);
|
|
543
1076
|
const viewport = page.getViewport({ scale });
|
|
544
|
-
const
|
|
545
|
-
canvas.width = Math.floor(viewport.width *
|
|
546
|
-
canvas.height = Math.floor(viewport.height *
|
|
1077
|
+
const outputScale = getCanvasOutputScale(viewport.width, viewport.height);
|
|
1078
|
+
canvas.width = Math.max(1, Math.floor(viewport.width * outputScale));
|
|
1079
|
+
canvas.height = Math.max(1, Math.floor(viewport.height * outputScale));
|
|
547
1080
|
canvas.style.width = `${Math.floor(viewport.width)}px`;
|
|
548
1081
|
canvas.style.height = `${Math.floor(viewport.height)}px`;
|
|
549
1082
|
wrapper.style.width = canvas.style.width;
|
|
@@ -554,16 +1087,17 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
554
1087
|
canvasContext: context,
|
|
555
1088
|
viewport,
|
|
556
1089
|
};
|
|
557
|
-
if (
|
|
558
|
-
renderContext.transform = [
|
|
1090
|
+
if (outputScale !== 1) {
|
|
1091
|
+
renderContext.transform = [outputScale, 0, 0, outputScale, 0, 0];
|
|
559
1092
|
}
|
|
560
1093
|
await page.render(renderContext).promise;
|
|
561
1094
|
return wrapper;
|
|
562
1095
|
}
|
|
563
1096
|
#computeScale(page, config) {
|
|
564
1097
|
const viewport = page.getViewport({ scale: 1 });
|
|
565
|
-
const
|
|
566
|
-
const
|
|
1098
|
+
const { width, height } = this.#getAvailableViewportSize();
|
|
1099
|
+
const availableWidth = Math.max(320, width) - 32;
|
|
1100
|
+
const availableHeight = Math.max(320, height) - 32;
|
|
567
1101
|
let baseScale = 1;
|
|
568
1102
|
if (config.fit === "width") {
|
|
569
1103
|
baseScale = availableWidth / viewport.width;
|
|
@@ -577,11 +1111,20 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
577
1111
|
const config = this.#config;
|
|
578
1112
|
const page = config?.page ?? DEFAULT_CONFIG.page;
|
|
579
1113
|
const zoom = config?.zoom ?? DEFAULT_CONFIG.zoom;
|
|
1114
|
+
const isCompactUi = this.hasAttribute("compact-ui");
|
|
1115
|
+
const isNativeViewer = this.#isUsingNativeViewer;
|
|
580
1116
|
this.#toolbar.hidden = !(config?.showToolbar ?? DEFAULT_CONFIG.showToolbar);
|
|
581
|
-
this.#pageValue.textContent =
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1117
|
+
this.#pageValue.textContent = isNativeViewer
|
|
1118
|
+
? "Browser"
|
|
1119
|
+
: isCompactUi
|
|
1120
|
+
? `${page}/${this.#pageCount || 0}`
|
|
1121
|
+
: `${page} / ${this.#pageCount || 0}`;
|
|
1122
|
+
this.#zoomValue.textContent = isNativeViewer ? "Auto" : `${Math.round(zoom * 100)}%`;
|
|
1123
|
+
this.#prevButton.disabled = isNativeViewer || page <= 1;
|
|
1124
|
+
this.#nextButton.disabled = isNativeViewer || (this.#pageCount > 0 ? page >= this.#pageCount : true);
|
|
1125
|
+
this.#zoomOutButton.disabled = isNativeViewer;
|
|
1126
|
+
this.#zoomInButton.disabled = isNativeViewer;
|
|
1127
|
+
this.#resetZoomButton.disabled = isNativeViewer;
|
|
585
1128
|
this.#downloadButton.hidden = !(config?.allowDownload ?? DEFAULT_CONFIG.allowDownload);
|
|
586
1129
|
}
|
|
587
1130
|
#setStatus(message) {
|
|
@@ -594,17 +1137,13 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
594
1137
|
this.#status.textContent = message;
|
|
595
1138
|
}
|
|
596
1139
|
#scrollToPage(page, smooth) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
target.scrollIntoView({
|
|
602
|
-
block: "start",
|
|
603
|
-
behavior: smooth ? "smooth" : "auto",
|
|
604
|
-
});
|
|
1140
|
+
this.#restoreScrollSnapshot({
|
|
1141
|
+
page,
|
|
1142
|
+
offsetRatio: 0,
|
|
1143
|
+
}, smooth);
|
|
605
1144
|
}
|
|
606
1145
|
#captureScrollSnapshot() {
|
|
607
|
-
const currentPage = this.#detectVisiblePage();
|
|
1146
|
+
const currentPage = this.#config?.page ?? this.#detectVisiblePage();
|
|
608
1147
|
if (!currentPage) {
|
|
609
1148
|
return null;
|
|
610
1149
|
}
|
|
@@ -636,6 +1175,97 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
636
1175
|
wrapper.classList.remove("entering");
|
|
637
1176
|
});
|
|
638
1177
|
}
|
|
1178
|
+
#observeResizeTargets() {
|
|
1179
|
+
if (!this.#resizeObserver) {
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
this.#resizeObserver.disconnect();
|
|
1183
|
+
this.#resizeObserver.observe(this);
|
|
1184
|
+
if (this.parentElement) {
|
|
1185
|
+
this.#resizeObserver.observe(this.parentElement);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
#syncInteractionMode() {
|
|
1189
|
+
if (shouldUseCompactToolbarUi()) {
|
|
1190
|
+
this.setAttribute("compact-ui", "");
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
this.removeAttribute("compact-ui");
|
|
1194
|
+
}
|
|
1195
|
+
#tryRenderNativeUrlFallback(config, error) {
|
|
1196
|
+
if (!shouldAttemptNativeUrlFallback(config, error)) {
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
const sourceUrl = resolveSourceHref(config.src);
|
|
1200
|
+
if (!sourceUrl) {
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
this.#isUsingNativeViewer = true;
|
|
1204
|
+
this.#pageCount = 0;
|
|
1205
|
+
this.#setStatus(null);
|
|
1206
|
+
this.#stage.className = "stage native-stage";
|
|
1207
|
+
const stageChildren = Array.from(this.#stage.children).filter((child) => child !== this.#status);
|
|
1208
|
+
for (const child of stageChildren) {
|
|
1209
|
+
child.remove();
|
|
1210
|
+
}
|
|
1211
|
+
const wrapper = document.createElement("div");
|
|
1212
|
+
wrapper.className = "page native-page";
|
|
1213
|
+
wrapper.dataset.page = "1";
|
|
1214
|
+
const frame = document.createElement("iframe");
|
|
1215
|
+
frame.className = "native-frame";
|
|
1216
|
+
frame.src = sourceUrl;
|
|
1217
|
+
frame.title = resolveTitle(config);
|
|
1218
|
+
const actions = document.createElement("div");
|
|
1219
|
+
actions.className = "native-actions";
|
|
1220
|
+
const note = document.createElement("p");
|
|
1221
|
+
note.className = "native-note";
|
|
1222
|
+
note.textContent = "Using the iOS browser PDF viewer because direct PDF fetching failed.";
|
|
1223
|
+
const link = document.createElement("a");
|
|
1224
|
+
link.className = "native-link";
|
|
1225
|
+
link.href = sourceUrl;
|
|
1226
|
+
link.target = "_blank";
|
|
1227
|
+
link.rel = "noopener noreferrer";
|
|
1228
|
+
link.textContent = "Open PDF in a new tab";
|
|
1229
|
+
actions.append(note, link);
|
|
1230
|
+
wrapper.append(frame, actions);
|
|
1231
|
+
this.#stage.insertBefore(wrapper, this.#status);
|
|
1232
|
+
this.#syncToolbarState();
|
|
1233
|
+
const detail = {
|
|
1234
|
+
page: 1,
|
|
1235
|
+
pageCount: 0,
|
|
1236
|
+
zoom: config.zoom ?? DEFAULT_CONFIG.zoom,
|
|
1237
|
+
};
|
|
1238
|
+
emitViewerEvent(this, "ready", detail);
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
#syncContainerSize() {
|
|
1242
|
+
const parent = this.parentElement;
|
|
1243
|
+
if (!parent) {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const parentWidth = parent.clientWidth || Math.round(parent.getBoundingClientRect().width);
|
|
1247
|
+
const parentHeight = parent.clientHeight || Math.round(parent.getBoundingClientRect().height);
|
|
1248
|
+
this.style.width = parentWidth > 0 ? `${parentWidth}px` : "100%";
|
|
1249
|
+
if (parentHeight > 0) {
|
|
1250
|
+
this.style.height = `${parentHeight}px`;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
#getAvailableViewportSize() {
|
|
1254
|
+
const toolbarHeight = this.#toolbar.hidden ? 0 : this.#toolbar.offsetHeight;
|
|
1255
|
+
const stageWidth = this.#stage.clientWidth;
|
|
1256
|
+
const stageHeight = this.#stage.clientHeight;
|
|
1257
|
+
const hostWidth = this.clientWidth;
|
|
1258
|
+
const hostHeight = this.clientHeight;
|
|
1259
|
+
const parentWidth = this.parentElement?.clientWidth ?? 0;
|
|
1260
|
+
const parentHeight = this.parentElement?.clientHeight ?? 0;
|
|
1261
|
+
return {
|
|
1262
|
+
width: stageWidth || hostWidth || parentWidth || 960,
|
|
1263
|
+
height: stageHeight ||
|
|
1264
|
+
Math.max(hostHeight - toolbarHeight, 0) ||
|
|
1265
|
+
Math.max(parentHeight - toolbarHeight, 0) ||
|
|
1266
|
+
720,
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
639
1269
|
#detectVisiblePage() {
|
|
640
1270
|
const pages = Array.from(this.#stage.querySelectorAll(".page"));
|
|
641
1271
|
if (pages.length === 0) {
|
|
@@ -650,15 +1280,6 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
650
1280
|
const parsed = Number(visible.dataset.page);
|
|
651
1281
|
return Number.isFinite(parsed) ? parsed : null;
|
|
652
1282
|
}
|
|
653
|
-
#toErrorMessage(error) {
|
|
654
|
-
if (error instanceof Error && error.message) {
|
|
655
|
-
return error.message;
|
|
656
|
-
}
|
|
657
|
-
if (typeof error === "string") {
|
|
658
|
-
return error;
|
|
659
|
-
}
|
|
660
|
-
return "Failed to load PDF.";
|
|
661
|
-
}
|
|
662
1283
|
}
|
|
663
1284
|
export function defineGenusPdfViewerElement(tagName = GENUS_PDF_VIEWER_TAG) {
|
|
664
1285
|
if (!customElements.get(tagName)) {
|