genus-pdf-viewer 0.2.3 → 0.2.10
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 +34 -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.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 +6 -0
- package/dist/viewer.d.ts.map +1 -1
- package/dist/viewer.js +640 -85
- package/dist/viewer.js.map +1 -1
- package/package.json +3 -5
package/dist/viewer.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import * as pdfjsLib from "pdfjs-dist";
|
|
1
|
+
import * as pdfjsLib from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
2
2
|
import { VIEWER_STYLES } from "./styles.js";
|
|
3
3
|
const GENUS_PDF_VIEWER_TAG = "genus-pdf-viewer";
|
|
4
|
+
export const PDF_WORKER_BOOTSTRAP_ASSET_PATH = "assets/pdf.worker.bootstrap.mjs";
|
|
5
|
+
const IOS_CANVAS_PIXEL_LIMIT = 16_777_216;
|
|
4
6
|
const DEFAULT_CONFIG = {
|
|
5
7
|
page: 1,
|
|
6
8
|
zoom: 1,
|
|
@@ -72,21 +74,232 @@ function resolveTitle(config) {
|
|
|
72
74
|
}
|
|
73
75
|
return "PDF viewer";
|
|
74
76
|
}
|
|
75
|
-
|
|
77
|
+
function resolveSourceHref(source) {
|
|
78
|
+
if (typeof source === "string") {
|
|
79
|
+
return source;
|
|
80
|
+
}
|
|
81
|
+
if (source instanceof URL) {
|
|
82
|
+
return source.toString();
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function resolveProxyEndpoint(config, sourceHref) {
|
|
87
|
+
if (config.proxyUrl === false) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (typeof config.proxyUrl === "string" && config.proxyUrl) {
|
|
91
|
+
return config.proxyUrl;
|
|
92
|
+
}
|
|
93
|
+
return isCrossOriginSource(sourceHref) ? "/__proxy" : null;
|
|
94
|
+
}
|
|
95
|
+
function resolveEffectiveSourceHref(config, proxyPreference = "auto") {
|
|
96
|
+
const sourceHref = resolveSourceHref(config.src);
|
|
97
|
+
if (!sourceHref) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (proxyPreference === "never") {
|
|
101
|
+
return sourceHref;
|
|
102
|
+
}
|
|
103
|
+
const proxyEndpoint = resolveProxyEndpoint(config, sourceHref);
|
|
104
|
+
if (!proxyEndpoint) {
|
|
105
|
+
return sourceHref;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const proxyUrl = new URL(proxyEndpoint, typeof window === "undefined" ? "http://localhost" : window.location.href);
|
|
109
|
+
proxyUrl.searchParams.set("url", sourceHref);
|
|
110
|
+
return proxyUrl.toString();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
const separator = proxyEndpoint.includes("?") ? "&" : "?";
|
|
114
|
+
return `${proxyEndpoint}${separator}url=${encodeURIComponent(sourceHref)}`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function isAppleMobileBrowser() {
|
|
118
|
+
if (typeof navigator === "undefined") {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const userAgent = navigator.userAgent ?? "";
|
|
122
|
+
const platform = navigator.platform ?? "";
|
|
123
|
+
return (/iPad|iPhone|iPod/i.test(userAgent) ||
|
|
124
|
+
(/Mac/i.test(platform) && (navigator.maxTouchPoints ?? 0) > 1));
|
|
125
|
+
}
|
|
126
|
+
function getPdfJsRuntimeOptions() {
|
|
127
|
+
if (!isAppleMobileBrowser()) {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
isOffscreenCanvasSupported: false,
|
|
132
|
+
isImageDecoderSupported: false,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function getCanvasOutputScale(width, height) {
|
|
136
|
+
const devicePixelRatio = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
|
|
137
|
+
if (!isAppleMobileBrowser()) {
|
|
138
|
+
return devicePixelRatio;
|
|
139
|
+
}
|
|
140
|
+
const pixelArea = Math.max(width * height, 1);
|
|
141
|
+
const maxScale = Math.sqrt(IOS_CANVAS_PIXEL_LIMIT / pixelArea);
|
|
142
|
+
return Math.min(devicePixelRatio, maxScale);
|
|
143
|
+
}
|
|
144
|
+
function iconLabelMarkup(svg, label) {
|
|
145
|
+
return `<span class="button-icon" aria-hidden="true">${svg}</span><span class="button-label">${label}</span>`;
|
|
146
|
+
}
|
|
147
|
+
function openUrl(url) {
|
|
148
|
+
const openedWindow = window.open(url, "_blank", "noopener,noreferrer");
|
|
149
|
+
if (!openedWindow) {
|
|
150
|
+
window.location.href = url;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function releaseObjectUrl(href) {
|
|
154
|
+
window.setTimeout(() => {
|
|
155
|
+
URL.revokeObjectURL(href);
|
|
156
|
+
}, 1_000);
|
|
157
|
+
}
|
|
158
|
+
function triggerDownload(href, filename) {
|
|
159
|
+
const link = document.createElement("a");
|
|
160
|
+
link.href = href;
|
|
161
|
+
link.download = filename;
|
|
162
|
+
link.rel = "noopener";
|
|
163
|
+
document.body.appendChild(link);
|
|
164
|
+
link.click();
|
|
165
|
+
link.remove();
|
|
166
|
+
}
|
|
167
|
+
async function tryNativeFileDownload(blob, filename) {
|
|
168
|
+
if (typeof navigator === "undefined" || typeof File === "undefined") {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (typeof navigator.share !== "function") {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const file = new File([blob], filename, { type: blob.type || "application/pdf" });
|
|
176
|
+
if (typeof navigator.canShare === "function" && !navigator.canShare({ files: [file] })) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
await navigator.share({
|
|
180
|
+
title: filename,
|
|
181
|
+
files: [file],
|
|
182
|
+
});
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function shouldUseCompactToolbarUi() {
|
|
190
|
+
return isAppleMobileBrowser();
|
|
191
|
+
}
|
|
192
|
+
function ensurePromiseCompat() {
|
|
193
|
+
const promiseCtor = Promise;
|
|
194
|
+
if (typeof promiseCtor.try !== "function") {
|
|
195
|
+
promiseCtor.try = (callback, ...args) => new Promise((resolve, reject) => {
|
|
196
|
+
try {
|
|
197
|
+
resolve(callback(...args));
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
reject(error);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (typeof promiseCtor.withResolvers !== "function") {
|
|
205
|
+
promiseCtor.withResolvers = () => {
|
|
206
|
+
let resolve;
|
|
207
|
+
let reject;
|
|
208
|
+
const promise = new Promise((promiseResolve, promiseReject) => {
|
|
209
|
+
resolve = promiseResolve;
|
|
210
|
+
reject = promiseReject;
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
promise,
|
|
214
|
+
resolve,
|
|
215
|
+
reject,
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function ensureUrlParseCompat() {
|
|
221
|
+
const urlCtor = URL;
|
|
222
|
+
if (typeof URL === "undefined" || typeof urlCtor.parse === "function") {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
urlCtor.parse = (input, base) => {
|
|
226
|
+
try {
|
|
227
|
+
if (typeof base === "undefined") {
|
|
228
|
+
return new URL(input);
|
|
229
|
+
}
|
|
230
|
+
return new URL(input, base);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function ensurePdfJsCompat() {
|
|
238
|
+
ensurePromiseCompat();
|
|
239
|
+
ensureUrlParseCompat();
|
|
240
|
+
}
|
|
241
|
+
function isUnbundledPackageModuleUrl(moduleUrl) {
|
|
242
|
+
try {
|
|
243
|
+
const { pathname } = new URL(moduleUrl);
|
|
244
|
+
return /\/(?:index|viewer)\.js$/.test(pathname);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
export function resolveDefaultPdfWorkerSrc(options = {}) {
|
|
251
|
+
const moduleUrl = options.moduleUrl ?? import.meta.url;
|
|
252
|
+
const packageRelativeWorkerUrl = new URL("./pdf.worker.bootstrap.mjs", moduleUrl).toString();
|
|
253
|
+
if (isUnbundledPackageModuleUrl(moduleUrl)) {
|
|
254
|
+
return packageRelativeWorkerUrl;
|
|
255
|
+
}
|
|
256
|
+
const baseUri = options.baseUri ??
|
|
257
|
+
(typeof document !== "undefined" ? document.baseURI : undefined) ??
|
|
258
|
+
(typeof location !== "undefined" ? location.href : undefined);
|
|
259
|
+
return baseUri
|
|
260
|
+
? new URL(PDF_WORKER_BOOTSTRAP_ASSET_PATH, baseUri).toString()
|
|
261
|
+
: packageRelativeWorkerUrl;
|
|
262
|
+
}
|
|
263
|
+
async function createDocumentData(config) {
|
|
264
|
+
if (config.src instanceof Blob) {
|
|
265
|
+
return new Uint8Array(await config.src.arrayBuffer());
|
|
266
|
+
}
|
|
267
|
+
if (config.src instanceof Uint8Array) {
|
|
268
|
+
return new Uint8Array(config.src);
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
async function toDocumentParameters(config, proxyPreference = "auto") {
|
|
273
|
+
const runtimeOptions = getPdfJsRuntimeOptions();
|
|
274
|
+
const data = await createDocumentData(config);
|
|
275
|
+
if (data) {
|
|
276
|
+
return {
|
|
277
|
+
data,
|
|
278
|
+
disableRange: true,
|
|
279
|
+
disableStream: true,
|
|
280
|
+
disableAutoFetch: true,
|
|
281
|
+
stopAtErrors: true,
|
|
282
|
+
...runtimeOptions,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
76
285
|
if (typeof config.src === "string") {
|
|
286
|
+
const url = resolveEffectiveSourceHref(config, proxyPreference);
|
|
77
287
|
return {
|
|
78
|
-
url: config.src,
|
|
288
|
+
url: url ?? config.src,
|
|
79
289
|
withCredentials: config.withCredentials,
|
|
80
290
|
httpHeaders: config.httpHeaders,
|
|
81
291
|
stopAtErrors: true,
|
|
292
|
+
...runtimeOptions,
|
|
82
293
|
};
|
|
83
294
|
}
|
|
84
295
|
if (config.src instanceof URL) {
|
|
296
|
+
const url = resolveEffectiveSourceHref(config, proxyPreference);
|
|
85
297
|
return {
|
|
86
|
-
url: config.src.toString(),
|
|
298
|
+
url: url ?? config.src.toString(),
|
|
87
299
|
withCredentials: config.withCredentials,
|
|
88
300
|
httpHeaders: config.httpHeaders,
|
|
89
301
|
stopAtErrors: true,
|
|
302
|
+
...runtimeOptions,
|
|
90
303
|
};
|
|
91
304
|
}
|
|
92
305
|
if (config.src instanceof Blob) {
|
|
@@ -96,6 +309,7 @@ async function toDocumentParameters(config) {
|
|
|
96
309
|
disableStream: true,
|
|
97
310
|
disableAutoFetch: true,
|
|
98
311
|
stopAtErrors: true,
|
|
312
|
+
...runtimeOptions,
|
|
99
313
|
};
|
|
100
314
|
}
|
|
101
315
|
return {
|
|
@@ -104,6 +318,7 @@ async function toDocumentParameters(config) {
|
|
|
104
318
|
disableStream: true,
|
|
105
319
|
disableAutoFetch: true,
|
|
106
320
|
stopAtErrors: true,
|
|
321
|
+
...runtimeOptions,
|
|
107
322
|
};
|
|
108
323
|
}
|
|
109
324
|
async function createBlobFromSource(config) {
|
|
@@ -113,32 +328,183 @@ async function createBlobFromSource(config) {
|
|
|
113
328
|
if (config.src instanceof Uint8Array) {
|
|
114
329
|
return new Blob([new Uint8Array(config.src)], { type: "application/pdf" });
|
|
115
330
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
331
|
+
const response = await fetchSourceResponse(config);
|
|
332
|
+
if (!response) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
return response.blob();
|
|
336
|
+
}
|
|
337
|
+
function emitViewerEvent(element, type, detail) {
|
|
338
|
+
element.dispatchEvent(new CustomEvent(`genus-${type}`, {
|
|
339
|
+
bubbles: true,
|
|
340
|
+
detail,
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
function getErrorName(error) {
|
|
344
|
+
if (typeof error === "object" &&
|
|
345
|
+
error !== null &&
|
|
346
|
+
"name" in error &&
|
|
347
|
+
typeof error.name === "string") {
|
|
348
|
+
return error.name;
|
|
349
|
+
}
|
|
350
|
+
return "";
|
|
351
|
+
}
|
|
352
|
+
function getRawErrorMessage(error) {
|
|
353
|
+
if (error instanceof Error && error.message) {
|
|
354
|
+
return error.message;
|
|
355
|
+
}
|
|
356
|
+
if (typeof error === "object" &&
|
|
357
|
+
error !== null &&
|
|
358
|
+
"message" in error &&
|
|
359
|
+
typeof error.message === "string") {
|
|
360
|
+
return error.message;
|
|
361
|
+
}
|
|
362
|
+
if (typeof error === "string") {
|
|
363
|
+
return error;
|
|
364
|
+
}
|
|
365
|
+
return "";
|
|
366
|
+
}
|
|
367
|
+
function isLikelyCorsError(error) {
|
|
368
|
+
const message = getRawErrorMessage(error).toLowerCase();
|
|
369
|
+
const name = getErrorName(error).toLowerCase();
|
|
370
|
+
return (message.includes("access-control") ||
|
|
371
|
+
message.includes("cross-origin") ||
|
|
372
|
+
message.includes("cors") ||
|
|
373
|
+
message.includes("credential is not supported") ||
|
|
374
|
+
message.includes("origin ") ||
|
|
375
|
+
name.includes("cors"));
|
|
376
|
+
}
|
|
377
|
+
function isLikelyNetworkLoadError(error) {
|
|
378
|
+
const name = getErrorName(error).toLowerCase();
|
|
379
|
+
const message = getRawErrorMessage(error).toLowerCase();
|
|
380
|
+
return (name.includes("network") ||
|
|
381
|
+
name === "unknownerrorexception" ||
|
|
382
|
+
message.includes("load failed") ||
|
|
383
|
+
message.includes("failed to fetch") ||
|
|
384
|
+
message.includes("networkerror") ||
|
|
385
|
+
message.includes("unexpected server response (0)") ||
|
|
386
|
+
message.includes("fetch"));
|
|
387
|
+
}
|
|
388
|
+
function isCrossOriginSource(href) {
|
|
389
|
+
if (typeof window === "undefined") {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
return new URL(href, window.location.href).origin !== window.location.origin;
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function resolveFetchCredentials(href, withCredentials) {
|
|
400
|
+
if (withCredentials) {
|
|
401
|
+
return "include";
|
|
402
|
+
}
|
|
403
|
+
return isCrossOriginSource(href) ? "omit" : "same-origin";
|
|
404
|
+
}
|
|
405
|
+
function resolveFetchMode(href) {
|
|
406
|
+
return isCrossOriginSource(href) ? "cors" : "same-origin";
|
|
407
|
+
}
|
|
408
|
+
async function fetchSourceResponse(config, proxyPreference = "auto") {
|
|
409
|
+
const href = resolveEffectiveSourceHref(config, proxyPreference);
|
|
121
410
|
if (!href || typeof fetch === "undefined") {
|
|
122
411
|
return null;
|
|
123
412
|
}
|
|
124
413
|
const response = await fetch(href, {
|
|
125
414
|
headers: config.httpHeaders,
|
|
126
|
-
credentials: config.withCredentials
|
|
415
|
+
credentials: resolveFetchCredentials(href, config.withCredentials),
|
|
416
|
+
mode: resolveFetchMode(href),
|
|
127
417
|
});
|
|
128
418
|
if (!response.ok) {
|
|
129
419
|
return null;
|
|
130
420
|
}
|
|
131
|
-
return response
|
|
421
|
+
return response;
|
|
132
422
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
423
|
+
async function fetchDocumentDataViaXhr(config, proxyPreference = "auto") {
|
|
424
|
+
const href = resolveEffectiveSourceHref(config, proxyPreference);
|
|
425
|
+
if (!href || typeof XMLHttpRequest === "undefined") {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
return new Promise((resolve, reject) => {
|
|
429
|
+
const xhr = new XMLHttpRequest();
|
|
430
|
+
xhr.open("GET", href, true);
|
|
431
|
+
xhr.responseType = "arraybuffer";
|
|
432
|
+
xhr.withCredentials = Boolean(config.withCredentials);
|
|
433
|
+
for (const [key, value] of Object.entries(config.httpHeaders ?? {})) {
|
|
434
|
+
xhr.setRequestHeader(key, value);
|
|
435
|
+
}
|
|
436
|
+
xhr.onload = () => {
|
|
437
|
+
if ((xhr.status === 200 || xhr.status === 206) && xhr.response instanceof ArrayBuffer) {
|
|
438
|
+
resolve(new Uint8Array(xhr.response));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
resolve(null);
|
|
442
|
+
};
|
|
443
|
+
xhr.onerror = () => {
|
|
444
|
+
reject(new TypeError("Load failed"));
|
|
445
|
+
};
|
|
446
|
+
xhr.onabort = () => {
|
|
447
|
+
reject(new TypeError("Load failed"));
|
|
448
|
+
};
|
|
449
|
+
xhr.send();
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
async function fetchDocumentDataFromSource(config, proxyPreference = "auto") {
|
|
453
|
+
try {
|
|
454
|
+
const response = await fetchSourceResponse(config, proxyPreference);
|
|
455
|
+
if (response) {
|
|
456
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
// Fall through to the XHR loader, which is often more reliable on Safari.
|
|
461
|
+
}
|
|
462
|
+
return fetchDocumentDataViaXhr(config, proxyPreference);
|
|
463
|
+
}
|
|
464
|
+
function createErrorDetail(config, error) {
|
|
465
|
+
const sourceUrl = resolveSourceHref(config.src) ?? undefined;
|
|
466
|
+
const rawMessage = getRawErrorMessage(error);
|
|
467
|
+
if (sourceUrl && isLikelyNetworkLoadError(error)) {
|
|
468
|
+
if (isCrossOriginSource(sourceUrl) && isLikelyCorsError(error)) {
|
|
469
|
+
return {
|
|
470
|
+
error,
|
|
471
|
+
code: "cors",
|
|
472
|
+
sourceUrl,
|
|
473
|
+
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.`,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
error,
|
|
478
|
+
code: "network",
|
|
479
|
+
sourceUrl,
|
|
480
|
+
message: `Failed to load PDF from ${sourceUrl}. Check the URL, auth headers, and network access.`,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
error,
|
|
485
|
+
code: "unknown",
|
|
486
|
+
sourceUrl,
|
|
487
|
+
message: rawMessage || "Failed to load PDF.",
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function shouldAttemptNativeUrlFallback(config, error) {
|
|
491
|
+
return Boolean(isAppleMobileBrowser() &&
|
|
492
|
+
resolveSourceHref(config.src) &&
|
|
493
|
+
(isLikelyNetworkLoadError(error) || isLikelyCorsError(error)));
|
|
138
494
|
}
|
|
139
495
|
export class GenusPdfViewerElement extends HTMLElement {
|
|
140
496
|
static get observedAttributes() {
|
|
141
|
-
return [
|
|
497
|
+
return [
|
|
498
|
+
"src",
|
|
499
|
+
"title",
|
|
500
|
+
"page",
|
|
501
|
+
"zoom",
|
|
502
|
+
"fit",
|
|
503
|
+
"continuous",
|
|
504
|
+
"toolbar",
|
|
505
|
+
"download",
|
|
506
|
+
"proxy-url",
|
|
507
|
+
];
|
|
142
508
|
}
|
|
143
509
|
#config = null;
|
|
144
510
|
#document = null;
|
|
@@ -146,6 +512,8 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
146
512
|
#pageCount = 0;
|
|
147
513
|
#loadVersion = 0;
|
|
148
514
|
#renderVersion = 0;
|
|
515
|
+
#isUsingNativeViewer = false;
|
|
516
|
+
#resizeObserver = null;
|
|
149
517
|
#shadow = this.attachShadow({ mode: "open" });
|
|
150
518
|
#toolbar = document.createElement("div");
|
|
151
519
|
#navGroup = document.createElement("div");
|
|
@@ -162,6 +530,8 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
162
530
|
#stage = document.createElement("div");
|
|
163
531
|
#status = document.createElement("div");
|
|
164
532
|
#handleResize = () => {
|
|
533
|
+
this.#syncInteractionMode();
|
|
534
|
+
this.#syncContainerSize();
|
|
165
535
|
if (!this.#document || !this.#config) {
|
|
166
536
|
return;
|
|
167
537
|
}
|
|
@@ -187,6 +557,11 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
187
557
|
};
|
|
188
558
|
constructor() {
|
|
189
559
|
super();
|
|
560
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
561
|
+
this.#resizeObserver = new ResizeObserver(() => {
|
|
562
|
+
this.#handleResize();
|
|
563
|
+
});
|
|
564
|
+
}
|
|
190
565
|
const style = document.createElement("style");
|
|
191
566
|
style.textContent = VIEWER_STYLES;
|
|
192
567
|
const shell = document.createElement("div");
|
|
@@ -198,33 +573,49 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
198
573
|
this.#pageValue.className = "value";
|
|
199
574
|
this.#zoomValue.className = "value";
|
|
200
575
|
this.#prevButton.type = "button";
|
|
201
|
-
this.#prevButton.
|
|
576
|
+
this.#prevButton.className = "compact";
|
|
577
|
+
this.#prevButton.setAttribute("aria-label", "Previous page");
|
|
578
|
+
this.#prevButton.title = "Previous page";
|
|
579
|
+
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
580
|
this.#prevButton.addEventListener("click", () => {
|
|
203
581
|
void this.prevPage();
|
|
204
582
|
});
|
|
205
583
|
this.#nextButton.type = "button";
|
|
206
|
-
this.#nextButton.
|
|
584
|
+
this.#nextButton.className = "compact";
|
|
585
|
+
this.#nextButton.setAttribute("aria-label", "Next page");
|
|
586
|
+
this.#nextButton.title = "Next page";
|
|
587
|
+
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
588
|
this.#nextButton.addEventListener("click", () => {
|
|
208
589
|
void this.nextPage();
|
|
209
590
|
});
|
|
210
591
|
this.#zoomOutButton.type = "button";
|
|
211
592
|
this.#zoomOutButton.textContent = "−";
|
|
593
|
+
this.#zoomOutButton.setAttribute("aria-label", "Zoom out");
|
|
594
|
+
this.#zoomOutButton.title = "Zoom out";
|
|
212
595
|
this.#zoomOutButton.addEventListener("click", () => {
|
|
213
596
|
void this.zoomOut();
|
|
214
597
|
});
|
|
215
598
|
this.#zoomInButton.type = "button";
|
|
216
599
|
this.#zoomInButton.textContent = "+";
|
|
600
|
+
this.#zoomInButton.setAttribute("aria-label", "Zoom in");
|
|
601
|
+
this.#zoomInButton.title = "Zoom in";
|
|
217
602
|
this.#zoomInButton.addEventListener("click", () => {
|
|
218
603
|
void this.zoomIn();
|
|
219
604
|
});
|
|
220
605
|
this.#resetZoomButton.type = "button";
|
|
221
|
-
this.#resetZoomButton.
|
|
606
|
+
this.#resetZoomButton.className = "compact";
|
|
607
|
+
this.#resetZoomButton.setAttribute("aria-label", "Reset zoom");
|
|
608
|
+
this.#resetZoomButton.title = "Reset zoom";
|
|
609
|
+
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
610
|
this.#resetZoomButton.addEventListener("click", () => {
|
|
223
611
|
void this.setZoom(1);
|
|
224
612
|
});
|
|
225
613
|
this.#downloadButton.type = "button";
|
|
226
|
-
this.#downloadButton.
|
|
227
|
-
this.#downloadButton.
|
|
614
|
+
this.#downloadButton.className = "primary icon-button";
|
|
615
|
+
this.#downloadButton.setAttribute("aria-label", "Download PDF");
|
|
616
|
+
this.#downloadButton.title = "Download PDF";
|
|
617
|
+
this.#downloadButton.innerHTML =
|
|
618
|
+
'<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
619
|
this.#downloadButton.addEventListener("click", () => {
|
|
229
620
|
void this.download();
|
|
230
621
|
});
|
|
@@ -241,6 +632,9 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
241
632
|
}
|
|
242
633
|
connectedCallback() {
|
|
243
634
|
window.addEventListener("resize", this.#handleResize);
|
|
635
|
+
this.#observeResizeTargets();
|
|
636
|
+
this.#syncInteractionMode();
|
|
637
|
+
this.#syncContainerSize();
|
|
244
638
|
if (!this.#config) {
|
|
245
639
|
const attributeConfig = this.#configFromAttributes();
|
|
246
640
|
if (attributeConfig) {
|
|
@@ -257,6 +651,7 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
257
651
|
}
|
|
258
652
|
disconnectedCallback() {
|
|
259
653
|
window.removeEventListener("resize", this.#handleResize);
|
|
654
|
+
this.#resizeObserver?.disconnect();
|
|
260
655
|
void this.#teardownDocument();
|
|
261
656
|
}
|
|
262
657
|
attributeChangedCallback() {
|
|
@@ -285,6 +680,7 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
285
680
|
const sourceChanged = !previous ||
|
|
286
681
|
!isSameSource(previous.src, nextConfig.src) ||
|
|
287
682
|
previous.workerSrc !== nextConfig.workerSrc ||
|
|
683
|
+
previous.proxyUrl !== nextConfig.proxyUrl ||
|
|
288
684
|
previous.withCredentials !== nextConfig.withCredentials ||
|
|
289
685
|
!sameHeaders(previous.httpHeaders, nextConfig.httpHeaders);
|
|
290
686
|
if (sourceChanged) {
|
|
@@ -318,7 +714,7 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
318
714
|
await this.#loadAndRender();
|
|
319
715
|
}
|
|
320
716
|
async goToPage(page) {
|
|
321
|
-
if (!this.#config) {
|
|
717
|
+
if (!this.#config || this.#isUsingNativeViewer) {
|
|
322
718
|
return;
|
|
323
719
|
}
|
|
324
720
|
const nextPage = clampPage(page, this.#pageCount || Number.MAX_SAFE_INTEGER);
|
|
@@ -342,13 +738,13 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
342
738
|
await this.goToPage((this.#config.page ?? DEFAULT_CONFIG.page) + 1);
|
|
343
739
|
}
|
|
344
740
|
async prevPage() {
|
|
345
|
-
if (!this.#config) {
|
|
741
|
+
if (!this.#config || this.#isUsingNativeViewer) {
|
|
346
742
|
return;
|
|
347
743
|
}
|
|
348
744
|
await this.goToPage((this.#config.page ?? DEFAULT_CONFIG.page) - 1);
|
|
349
745
|
}
|
|
350
746
|
async setZoom(zoom) {
|
|
351
|
-
if (!this.#config) {
|
|
747
|
+
if (!this.#config || this.#isUsingNativeViewer) {
|
|
352
748
|
return;
|
|
353
749
|
}
|
|
354
750
|
const nextZoom = clampZoom(zoom, this.#config.minZoom ?? DEFAULT_CONFIG.minZoom, this.#config.maxZoom ?? DEFAULT_CONFIG.maxZoom);
|
|
@@ -358,14 +754,14 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
358
754
|
emitViewerEvent(this, "zoomchange", { zoom: nextZoom });
|
|
359
755
|
}
|
|
360
756
|
async zoomIn() {
|
|
361
|
-
if (!this.#config) {
|
|
757
|
+
if (!this.#config || this.#isUsingNativeViewer) {
|
|
362
758
|
return;
|
|
363
759
|
}
|
|
364
760
|
await this.setZoom((this.#config.zoom ?? DEFAULT_CONFIG.zoom) +
|
|
365
761
|
(this.#config.zoomStep ?? DEFAULT_CONFIG.zoomStep));
|
|
366
762
|
}
|
|
367
763
|
async zoomOut() {
|
|
368
|
-
if (!this.#config) {
|
|
764
|
+
if (!this.#config || this.#isUsingNativeViewer) {
|
|
369
765
|
return;
|
|
370
766
|
}
|
|
371
767
|
await this.setZoom((this.#config.zoom ?? DEFAULT_CONFIG.zoom) -
|
|
@@ -375,26 +771,29 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
375
771
|
if (!this.#config?.allowDownload) {
|
|
376
772
|
return;
|
|
377
773
|
}
|
|
378
|
-
const link = document.createElement("a");
|
|
379
774
|
const blob = await createBlobFromSource(this.#config);
|
|
380
775
|
if (blob) {
|
|
381
776
|
const href = URL.createObjectURL(blob);
|
|
382
|
-
|
|
383
|
-
link.download = resolveTitle(this.#config).endsWith(".pdf")
|
|
777
|
+
const filename = resolveTitle(this.#config).endsWith(".pdf")
|
|
384
778
|
? resolveTitle(this.#config)
|
|
385
779
|
: `${resolveTitle(this.#config)}.pdf`;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
780
|
+
try {
|
|
781
|
+
if (isAppleMobileBrowser() && (await tryNativeFileDownload(blob, filename))) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
triggerDownload(href, filename);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
finally {
|
|
788
|
+
releaseObjectUrl(href);
|
|
789
|
+
}
|
|
391
790
|
}
|
|
392
791
|
if (typeof this.#config.src === "string") {
|
|
393
|
-
|
|
792
|
+
openUrl(this.#config.src);
|
|
394
793
|
return;
|
|
395
794
|
}
|
|
396
795
|
if (this.#config.src instanceof URL) {
|
|
397
|
-
|
|
796
|
+
openUrl(this.#config.src.toString());
|
|
398
797
|
}
|
|
399
798
|
}
|
|
400
799
|
async destroy() {
|
|
@@ -416,6 +815,13 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
416
815
|
continuous: parseBooleanAttribute(this.getAttribute("continuous"), DEFAULT_CONFIG.continuous),
|
|
417
816
|
showToolbar: parseBooleanAttribute(this.getAttribute("toolbar"), DEFAULT_CONFIG.showToolbar),
|
|
418
817
|
allowDownload: parseBooleanAttribute(this.getAttribute("download"), DEFAULT_CONFIG.allowDownload),
|
|
818
|
+
proxyUrl: (() => {
|
|
819
|
+
const value = this.getAttribute("proxy-url");
|
|
820
|
+
if (value === null) {
|
|
821
|
+
return undefined;
|
|
822
|
+
}
|
|
823
|
+
return value === "false" ? false : value;
|
|
824
|
+
})(),
|
|
419
825
|
};
|
|
420
826
|
}
|
|
421
827
|
async #loadAndRender() {
|
|
@@ -424,19 +830,16 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
424
830
|
return;
|
|
425
831
|
}
|
|
426
832
|
const loadVersion = ++this.#loadVersion;
|
|
833
|
+
this.#isUsingNativeViewer = false;
|
|
427
834
|
this.#setStatus("Loading PDF...");
|
|
428
835
|
await this.#teardownDocument();
|
|
429
836
|
try {
|
|
430
837
|
const globalWorkerOptions = pdfjsLib.GlobalWorkerOptions;
|
|
431
838
|
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();
|
|
839
|
+
ensurePdfJsCompat();
|
|
840
|
+
globalWorkerOptions.workerSrc = config.workerSrc ?? resolveDefaultPdfWorkerSrc();
|
|
841
|
+
const documentProxy = await this.#loadDocumentProxy(config, loadVersion);
|
|
842
|
+
if (!documentProxy) {
|
|
440
843
|
return;
|
|
441
844
|
}
|
|
442
845
|
this.#document = documentProxy;
|
|
@@ -455,16 +858,79 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
455
858
|
emitViewerEvent(this, "ready", detail);
|
|
456
859
|
}
|
|
457
860
|
catch (error) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
861
|
+
if (this.#tryRenderNativeUrlFallback(config, error)) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const detail = createErrorDetail(config, error);
|
|
462
865
|
this.#setStatus(detail.message);
|
|
463
866
|
emitViewerEvent(this, "error", detail);
|
|
464
867
|
}
|
|
465
868
|
}
|
|
869
|
+
async #loadDocumentProxy(config, loadVersion) {
|
|
870
|
+
try {
|
|
871
|
+
return await this.#loadDocumentProxyFromParams(await toDocumentParameters(config, "auto"), loadVersion);
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
let currentError = error;
|
|
875
|
+
const directRetryParams = await this.#createDirectUrlRetryParameters(config);
|
|
876
|
+
if (directRetryParams) {
|
|
877
|
+
try {
|
|
878
|
+
return await this.#loadDocumentProxyFromParams(directRetryParams, loadVersion);
|
|
879
|
+
}
|
|
880
|
+
catch (directError) {
|
|
881
|
+
currentError = directError;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const fallbackParams = await this.#createFetchedDataFallbackParameters(config, currentError, directRetryParams ? "never" : "auto");
|
|
885
|
+
if (!fallbackParams) {
|
|
886
|
+
throw currentError;
|
|
887
|
+
}
|
|
888
|
+
return this.#loadDocumentProxyFromParams(fallbackParams, loadVersion);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
async #loadDocumentProxyFromParams(params, loadVersion) {
|
|
892
|
+
const task = pdfjsLib.getDocument(params);
|
|
893
|
+
this.#loadingTask = task;
|
|
894
|
+
const documentProxy = await task.promise;
|
|
895
|
+
if (loadVersion !== this.#loadVersion) {
|
|
896
|
+
await documentProxy.destroy();
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
return documentProxy;
|
|
900
|
+
}
|
|
901
|
+
async #createFetchedDataFallbackParameters(config, error, proxyPreference) {
|
|
902
|
+
if (!resolveSourceHref(config.src) || !isLikelyNetworkLoadError(error)) {
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
try {
|
|
906
|
+
const data = await fetchDocumentDataFromSource(config, proxyPreference);
|
|
907
|
+
if (!data) {
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
data,
|
|
912
|
+
disableRange: true,
|
|
913
|
+
disableStream: true,
|
|
914
|
+
disableAutoFetch: true,
|
|
915
|
+
stopAtErrors: true,
|
|
916
|
+
...getPdfJsRuntimeOptions(),
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
catch {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
async #createDirectUrlRetryParameters(config) {
|
|
924
|
+
const proxiedHref = resolveEffectiveSourceHref(config, "auto");
|
|
925
|
+
const directHref = resolveEffectiveSourceHref(config, "never");
|
|
926
|
+
if (!proxiedHref || !directHref || proxiedHref === directHref) {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
return toDocumentParameters(config, "never");
|
|
930
|
+
}
|
|
466
931
|
async #teardownDocument() {
|
|
467
932
|
this.#renderVersion += 1;
|
|
933
|
+
this.#isUsingNativeViewer = false;
|
|
468
934
|
const loadingTask = this.#loadingTask;
|
|
469
935
|
this.#loadingTask = null;
|
|
470
936
|
if (loadingTask) {
|
|
@@ -497,16 +963,10 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
497
963
|
const scrollSnapshot = config.continuous ? this.#captureScrollSnapshot() : null;
|
|
498
964
|
this.#setStatus(null);
|
|
499
965
|
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
|
-
}
|
|
966
|
+
const stageChildren = Array.from(this.#stage.children).filter((child) => child !== this.#status);
|
|
506
967
|
if (config.continuous) {
|
|
507
968
|
const stack = document.createElement("div");
|
|
508
969
|
stack.className = "page-stack";
|
|
509
|
-
this.#stage.insertBefore(stack, this.#status);
|
|
510
970
|
for (let index = 1; index <= documentProxy.numPages; index += 1) {
|
|
511
971
|
if (renderVersion !== this.#renderVersion) {
|
|
512
972
|
return;
|
|
@@ -516,6 +976,10 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
516
976
|
stack.appendChild(wrapper);
|
|
517
977
|
this.#animatePageEntry(wrapper);
|
|
518
978
|
}
|
|
979
|
+
for (const child of stageChildren) {
|
|
980
|
+
child.remove();
|
|
981
|
+
}
|
|
982
|
+
this.#stage.insertBefore(stack, this.#status);
|
|
519
983
|
this.#restoreScrollSnapshot(scrollSnapshot ?? {
|
|
520
984
|
page: config.page ?? DEFAULT_CONFIG.page,
|
|
521
985
|
offsetRatio: 0,
|
|
@@ -525,6 +989,9 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
525
989
|
const pageNumber = clampPage(config.page ?? DEFAULT_CONFIG.page, documentProxy.numPages);
|
|
526
990
|
const page = await documentProxy.getPage(pageNumber);
|
|
527
991
|
const wrapper = await this.#renderPage(page, config, pageNumber);
|
|
992
|
+
for (const child of stageChildren) {
|
|
993
|
+
child.remove();
|
|
994
|
+
}
|
|
528
995
|
this.#stage.insertBefore(wrapper, this.#status);
|
|
529
996
|
this.#animatePageEntry(wrapper);
|
|
530
997
|
}
|
|
@@ -541,9 +1008,9 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
541
1008
|
}
|
|
542
1009
|
const scale = this.#computeScale(page, config);
|
|
543
1010
|
const viewport = page.getViewport({ scale });
|
|
544
|
-
const
|
|
545
|
-
canvas.width = Math.floor(viewport.width *
|
|
546
|
-
canvas.height = Math.floor(viewport.height *
|
|
1011
|
+
const outputScale = getCanvasOutputScale(viewport.width, viewport.height);
|
|
1012
|
+
canvas.width = Math.max(1, Math.floor(viewport.width * outputScale));
|
|
1013
|
+
canvas.height = Math.max(1, Math.floor(viewport.height * outputScale));
|
|
547
1014
|
canvas.style.width = `${Math.floor(viewport.width)}px`;
|
|
548
1015
|
canvas.style.height = `${Math.floor(viewport.height)}px`;
|
|
549
1016
|
wrapper.style.width = canvas.style.width;
|
|
@@ -554,16 +1021,17 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
554
1021
|
canvasContext: context,
|
|
555
1022
|
viewport,
|
|
556
1023
|
};
|
|
557
|
-
if (
|
|
558
|
-
renderContext.transform = [
|
|
1024
|
+
if (outputScale !== 1) {
|
|
1025
|
+
renderContext.transform = [outputScale, 0, 0, outputScale, 0, 0];
|
|
559
1026
|
}
|
|
560
1027
|
await page.render(renderContext).promise;
|
|
561
1028
|
return wrapper;
|
|
562
1029
|
}
|
|
563
1030
|
#computeScale(page, config) {
|
|
564
1031
|
const viewport = page.getViewport({ scale: 1 });
|
|
565
|
-
const
|
|
566
|
-
const
|
|
1032
|
+
const { width, height } = this.#getAvailableViewportSize();
|
|
1033
|
+
const availableWidth = Math.max(320, width) - 32;
|
|
1034
|
+
const availableHeight = Math.max(320, height) - 32;
|
|
567
1035
|
let baseScale = 1;
|
|
568
1036
|
if (config.fit === "width") {
|
|
569
1037
|
baseScale = availableWidth / viewport.width;
|
|
@@ -577,11 +1045,20 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
577
1045
|
const config = this.#config;
|
|
578
1046
|
const page = config?.page ?? DEFAULT_CONFIG.page;
|
|
579
1047
|
const zoom = config?.zoom ?? DEFAULT_CONFIG.zoom;
|
|
1048
|
+
const isCompactUi = this.hasAttribute("compact-ui");
|
|
1049
|
+
const isNativeViewer = this.#isUsingNativeViewer;
|
|
580
1050
|
this.#toolbar.hidden = !(config?.showToolbar ?? DEFAULT_CONFIG.showToolbar);
|
|
581
|
-
this.#pageValue.textContent =
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1051
|
+
this.#pageValue.textContent = isNativeViewer
|
|
1052
|
+
? "Browser"
|
|
1053
|
+
: isCompactUi
|
|
1054
|
+
? `${page}/${this.#pageCount || 0}`
|
|
1055
|
+
: `${page} / ${this.#pageCount || 0}`;
|
|
1056
|
+
this.#zoomValue.textContent = isNativeViewer ? "Auto" : `${Math.round(zoom * 100)}%`;
|
|
1057
|
+
this.#prevButton.disabled = isNativeViewer || page <= 1;
|
|
1058
|
+
this.#nextButton.disabled = isNativeViewer || (this.#pageCount > 0 ? page >= this.#pageCount : true);
|
|
1059
|
+
this.#zoomOutButton.disabled = isNativeViewer;
|
|
1060
|
+
this.#zoomInButton.disabled = isNativeViewer;
|
|
1061
|
+
this.#resetZoomButton.disabled = isNativeViewer;
|
|
585
1062
|
this.#downloadButton.hidden = !(config?.allowDownload ?? DEFAULT_CONFIG.allowDownload);
|
|
586
1063
|
}
|
|
587
1064
|
#setStatus(message) {
|
|
@@ -594,17 +1071,13 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
594
1071
|
this.#status.textContent = message;
|
|
595
1072
|
}
|
|
596
1073
|
#scrollToPage(page, smooth) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
target.scrollIntoView({
|
|
602
|
-
block: "start",
|
|
603
|
-
behavior: smooth ? "smooth" : "auto",
|
|
604
|
-
});
|
|
1074
|
+
this.#restoreScrollSnapshot({
|
|
1075
|
+
page,
|
|
1076
|
+
offsetRatio: 0,
|
|
1077
|
+
}, smooth);
|
|
605
1078
|
}
|
|
606
1079
|
#captureScrollSnapshot() {
|
|
607
|
-
const currentPage = this.#detectVisiblePage();
|
|
1080
|
+
const currentPage = this.#config?.page ?? this.#detectVisiblePage();
|
|
608
1081
|
if (!currentPage) {
|
|
609
1082
|
return null;
|
|
610
1083
|
}
|
|
@@ -636,6 +1109,97 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
636
1109
|
wrapper.classList.remove("entering");
|
|
637
1110
|
});
|
|
638
1111
|
}
|
|
1112
|
+
#observeResizeTargets() {
|
|
1113
|
+
if (!this.#resizeObserver) {
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
this.#resizeObserver.disconnect();
|
|
1117
|
+
this.#resizeObserver.observe(this);
|
|
1118
|
+
if (this.parentElement) {
|
|
1119
|
+
this.#resizeObserver.observe(this.parentElement);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
#syncInteractionMode() {
|
|
1123
|
+
if (shouldUseCompactToolbarUi()) {
|
|
1124
|
+
this.setAttribute("compact-ui", "");
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
this.removeAttribute("compact-ui");
|
|
1128
|
+
}
|
|
1129
|
+
#tryRenderNativeUrlFallback(config, error) {
|
|
1130
|
+
if (!shouldAttemptNativeUrlFallback(config, error)) {
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
const sourceUrl = resolveSourceHref(config.src);
|
|
1134
|
+
if (!sourceUrl) {
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1137
|
+
this.#isUsingNativeViewer = true;
|
|
1138
|
+
this.#pageCount = 0;
|
|
1139
|
+
this.#setStatus(null);
|
|
1140
|
+
this.#stage.className = "stage native-stage";
|
|
1141
|
+
const stageChildren = Array.from(this.#stage.children).filter((child) => child !== this.#status);
|
|
1142
|
+
for (const child of stageChildren) {
|
|
1143
|
+
child.remove();
|
|
1144
|
+
}
|
|
1145
|
+
const wrapper = document.createElement("div");
|
|
1146
|
+
wrapper.className = "page native-page";
|
|
1147
|
+
wrapper.dataset.page = "1";
|
|
1148
|
+
const frame = document.createElement("iframe");
|
|
1149
|
+
frame.className = "native-frame";
|
|
1150
|
+
frame.src = sourceUrl;
|
|
1151
|
+
frame.title = resolveTitle(config);
|
|
1152
|
+
const actions = document.createElement("div");
|
|
1153
|
+
actions.className = "native-actions";
|
|
1154
|
+
const note = document.createElement("p");
|
|
1155
|
+
note.className = "native-note";
|
|
1156
|
+
note.textContent = "Using the iOS browser PDF viewer because direct PDF fetching failed.";
|
|
1157
|
+
const link = document.createElement("a");
|
|
1158
|
+
link.className = "native-link";
|
|
1159
|
+
link.href = sourceUrl;
|
|
1160
|
+
link.target = "_blank";
|
|
1161
|
+
link.rel = "noopener noreferrer";
|
|
1162
|
+
link.textContent = "Open PDF in a new tab";
|
|
1163
|
+
actions.append(note, link);
|
|
1164
|
+
wrapper.append(frame, actions);
|
|
1165
|
+
this.#stage.insertBefore(wrapper, this.#status);
|
|
1166
|
+
this.#syncToolbarState();
|
|
1167
|
+
const detail = {
|
|
1168
|
+
page: 1,
|
|
1169
|
+
pageCount: 0,
|
|
1170
|
+
zoom: config.zoom ?? DEFAULT_CONFIG.zoom,
|
|
1171
|
+
};
|
|
1172
|
+
emitViewerEvent(this, "ready", detail);
|
|
1173
|
+
return true;
|
|
1174
|
+
}
|
|
1175
|
+
#syncContainerSize() {
|
|
1176
|
+
const parent = this.parentElement;
|
|
1177
|
+
if (!parent) {
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const parentWidth = parent.clientWidth || Math.round(parent.getBoundingClientRect().width);
|
|
1181
|
+
const parentHeight = parent.clientHeight || Math.round(parent.getBoundingClientRect().height);
|
|
1182
|
+
this.style.width = parentWidth > 0 ? `${parentWidth}px` : "100%";
|
|
1183
|
+
if (parentHeight > 0) {
|
|
1184
|
+
this.style.height = `${parentHeight}px`;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
#getAvailableViewportSize() {
|
|
1188
|
+
const toolbarHeight = this.#toolbar.hidden ? 0 : this.#toolbar.offsetHeight;
|
|
1189
|
+
const stageWidth = this.#stage.clientWidth;
|
|
1190
|
+
const stageHeight = this.#stage.clientHeight;
|
|
1191
|
+
const hostWidth = this.clientWidth;
|
|
1192
|
+
const hostHeight = this.clientHeight;
|
|
1193
|
+
const parentWidth = this.parentElement?.clientWidth ?? 0;
|
|
1194
|
+
const parentHeight = this.parentElement?.clientHeight ?? 0;
|
|
1195
|
+
return {
|
|
1196
|
+
width: stageWidth || hostWidth || parentWidth || 960,
|
|
1197
|
+
height: stageHeight ||
|
|
1198
|
+
Math.max(hostHeight - toolbarHeight, 0) ||
|
|
1199
|
+
Math.max(parentHeight - toolbarHeight, 0) ||
|
|
1200
|
+
720,
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
639
1203
|
#detectVisiblePage() {
|
|
640
1204
|
const pages = Array.from(this.#stage.querySelectorAll(".page"));
|
|
641
1205
|
if (pages.length === 0) {
|
|
@@ -650,15 +1214,6 @@ export class GenusPdfViewerElement extends HTMLElement {
|
|
|
650
1214
|
const parsed = Number(visible.dataset.page);
|
|
651
1215
|
return Number.isFinite(parsed) ? parsed : null;
|
|
652
1216
|
}
|
|
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
1217
|
}
|
|
663
1218
|
export function defineGenusPdfViewerElement(tagName = GENUS_PDF_VIEWER_TAG) {
|
|
664
1219
|
if (!customElements.get(tagName)) {
|