stimulus-pdf-viewer-rails 0.3.1 → 0.3.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/app/assets/javascripts/stimulus-pdf-viewer.esm.js +103 -27
- data/app/assets/javascripts/stimulus-pdf-viewer.js +103 -27
- data/lib/stimulus_pdf_viewer/rails/version.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 94c69ce661ffbfcc99fa65676d97183b65252cb525c69ac4ef4d0dd128a6ba48
|
|
4
|
+
data.tar.gz: c51c519b51d76b496afa78ffa68703bf7eb17e2f147516de224e75bf6c834bfe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db307a6dc1e632f7d4e6ddb1c5fec962c421e4981a0e713e71b33686d5b82bf9f3a16ac297049c50bd217424254e863aaca8d63320d695c6feadea5be7bbad6b
|
|
7
|
+
data.tar.gz: d01c3a7398acf4b25054685c87258bf1dc9d24718905c014c5b41410d0bcb0f878c9495ec7f13e639d2071adb3737625e88e793da97424014fe0a43ae82f4733
|
data/CHANGELOG.md
CHANGED
|
@@ -395,40 +395,87 @@ class CoreViewer {
|
|
|
395
395
|
|
|
396
396
|
/**
|
|
397
397
|
* Load a PDF document from a URL.
|
|
398
|
+
*
|
|
399
|
+
* PDF.js loads PDFs via streamed HTTP Range requests by default, which some
|
|
400
|
+
* corporate antivirus / web-filter products block while letting plain GETs
|
|
401
|
+
* through. If the initial streamed load fails with a network-shaped error,
|
|
402
|
+
* we retry once by fetching the entire PDF as an ArrayBuffer and handing the
|
|
403
|
+
* bytes to PDF.js directly.
|
|
404
|
+
*
|
|
398
405
|
* @param {string} url - The PDF URL
|
|
399
406
|
* @returns {Promise<PDFDocumentProxy>}
|
|
400
407
|
*/
|
|
401
408
|
async load(url) {
|
|
402
409
|
try {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
410
|
+
return await this._loadDocument(url)
|
|
411
|
+
} catch (error) {
|
|
412
|
+
if (typeof url === "string" && this._shouldRetryAsBlob(error)) {
|
|
413
|
+
console.warn("PDF streamed load failed, retrying as full fetch:", error);
|
|
414
|
+
try {
|
|
415
|
+
const response = await fetch(url, { credentials: "same-origin" });
|
|
416
|
+
if (!response.ok) {
|
|
417
|
+
throw new Error(`HTTP ${response.status} fetching PDF`)
|
|
418
|
+
}
|
|
419
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
420
|
+
return await this._loadDocument({ data })
|
|
421
|
+
} catch (retryError) {
|
|
422
|
+
console.error("PDF blob fallback also failed:", retryError);
|
|
423
|
+
this.eventBus.dispatch(ViewerEvents.DOCUMENT_LOAD_ERROR, { error: retryError });
|
|
424
|
+
throw retryError
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
console.error("Error loading PDF:", error);
|
|
428
|
+
this.eventBus.dispatch(ViewerEvents.DOCUMENT_LOAD_ERROR, { error });
|
|
429
|
+
throw error
|
|
430
|
+
}
|
|
431
|
+
}
|
|
406
432
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
433
|
+
/**
|
|
434
|
+
* Load a PDF document from either a URL or pre-fetched bytes.
|
|
435
|
+
* @param {string|{data: Uint8Array}} source
|
|
436
|
+
* @returns {Promise<PDFDocumentProxy>}
|
|
437
|
+
*/
|
|
438
|
+
async _loadDocument(source) {
|
|
439
|
+
const loadingTask = pdfjsLib.getDocument(source);
|
|
440
|
+
this.pdfDocument = await loadingTask.promise;
|
|
441
|
+
this.pageCount = this.pdfDocument.numPages;
|
|
410
442
|
|
|
411
|
-
|
|
412
|
-
|
|
443
|
+
// Clear any existing content
|
|
444
|
+
this.container.innerHTML = "";
|
|
445
|
+
this.pages.clear();
|
|
413
446
|
|
|
414
|
-
|
|
415
|
-
|
|
447
|
+
// Set initial display scale on container
|
|
448
|
+
this.container.style.setProperty("--display-scale", String(this.displayScale));
|
|
416
449
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
pageCount: this.pageCount,
|
|
420
|
-
pdfDocument: this.pdfDocument
|
|
421
|
-
});
|
|
450
|
+
// Create page placeholders for all pages
|
|
451
|
+
await this._createPagePlaceholders();
|
|
422
452
|
|
|
423
|
-
|
|
424
|
-
|
|
453
|
+
// Dispatch loaded event
|
|
454
|
+
this.eventBus.dispatch(ViewerEvents.DOCUMENT_LOADED, {
|
|
455
|
+
pageCount: this.pageCount,
|
|
456
|
+
pdfDocument: this.pdfDocument
|
|
457
|
+
});
|
|
425
458
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
459
|
+
// Trigger initial render of visible pages
|
|
460
|
+
this._renderingQueue.renderHighestPriority(this.getVisiblePages());
|
|
461
|
+
|
|
462
|
+
return this.pdfDocument
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Decide whether a load failure is worth retrying via full-fetch.
|
|
467
|
+
* Skips errors where a different transport won't help: password-protected
|
|
468
|
+
* PDFs, corrupt files, and auth failures.
|
|
469
|
+
*/
|
|
470
|
+
_shouldRetryAsBlob(error) {
|
|
471
|
+
const message = String(error?.message || error || "");
|
|
472
|
+
const name = String(error?.name || "");
|
|
473
|
+
|
|
474
|
+
if (/Password|InvalidPDF/i.test(name)) return false
|
|
475
|
+
if (/\b40[0-9]\b/.test(message)) return false
|
|
476
|
+
if (/password|invalid pdf|encrypted/i.test(message)) return false
|
|
477
|
+
|
|
478
|
+
return true
|
|
432
479
|
}
|
|
433
480
|
|
|
434
481
|
/**
|
|
@@ -2435,11 +2482,12 @@ class ColorPicker {
|
|
|
2435
2482
|
});
|
|
2436
2483
|
|
|
2437
2484
|
// Close on outside click
|
|
2438
|
-
|
|
2485
|
+
this._documentClickHandler = () => {
|
|
2439
2486
|
if (this.isOpen) {
|
|
2440
2487
|
this._closeDropdown();
|
|
2441
2488
|
}
|
|
2442
|
-
}
|
|
2489
|
+
};
|
|
2490
|
+
document.addEventListener("click", this._documentClickHandler);
|
|
2443
2491
|
}
|
|
2444
2492
|
|
|
2445
2493
|
_closeDropdown() {
|
|
@@ -2472,6 +2520,15 @@ class ColorPicker {
|
|
|
2472
2520
|
getColor() {
|
|
2473
2521
|
return this.currentColor
|
|
2474
2522
|
}
|
|
2523
|
+
|
|
2524
|
+
destroy() {
|
|
2525
|
+
if (this._documentClickHandler) {
|
|
2526
|
+
document.removeEventListener("click", this._documentClickHandler);
|
|
2527
|
+
this._documentClickHandler = null;
|
|
2528
|
+
}
|
|
2529
|
+
this.element?.remove();
|
|
2530
|
+
this.element = null;
|
|
2531
|
+
}
|
|
2475
2532
|
}
|
|
2476
2533
|
|
|
2477
2534
|
class AnnotationEditToolbar {
|
|
@@ -8388,6 +8445,7 @@ class PdfViewer {
|
|
|
8388
8445
|
this.annotationSidebar?.destroy();
|
|
8389
8446
|
this.findController?.destroy();
|
|
8390
8447
|
this.findBar?.destroy();
|
|
8448
|
+
this.colorPicker?.destroy();
|
|
8391
8449
|
|
|
8392
8450
|
Object.values(this.tools).forEach(tool => tool.destroy?.());
|
|
8393
8451
|
|
|
@@ -8410,7 +8468,8 @@ class pdf_viewer_controller extends Controller {
|
|
|
8410
8468
|
initialPage: Number,
|
|
8411
8469
|
initialAnnotation: String,
|
|
8412
8470
|
autoHeight: { type: Boolean, default: true },
|
|
8413
|
-
detailPanel: { type: Boolean, default: false }
|
|
8471
|
+
detailPanel: { type: Boolean, default: false },
|
|
8472
|
+
errorMessage: String
|
|
8414
8473
|
}
|
|
8415
8474
|
|
|
8416
8475
|
initialize() {
|
|
@@ -8446,10 +8505,27 @@ class pdf_viewer_controller extends Controller {
|
|
|
8446
8505
|
await this.pdfViewer.load();
|
|
8447
8506
|
} catch (error) {
|
|
8448
8507
|
console.error("Failed to load PDF:", error);
|
|
8449
|
-
this.
|
|
8508
|
+
this._handleLoadFailure(error);
|
|
8450
8509
|
}
|
|
8451
8510
|
}
|
|
8452
8511
|
|
|
8512
|
+
// Surface a final load failure: hide the spinner, show a host-customizable
|
|
8513
|
+
// message, and dispatch a DOM event so the host app can wire telemetry.
|
|
8514
|
+
_handleLoadFailure(error) {
|
|
8515
|
+
if (this.hasLoadingOverlayTarget) {
|
|
8516
|
+
this.loadingOverlayTarget.classList.add("hidden");
|
|
8517
|
+
}
|
|
8518
|
+
const message = this.errorMessageValue || "Failed to load PDF document";
|
|
8519
|
+
this._showError(message);
|
|
8520
|
+
this.containerTarget.dispatchEvent(new CustomEvent("pdf-viewer:load-failed", {
|
|
8521
|
+
bubbles: true,
|
|
8522
|
+
detail: {
|
|
8523
|
+
message: String(error?.message || error || ""),
|
|
8524
|
+
name: String(error?.name || "")
|
|
8525
|
+
}
|
|
8526
|
+
}));
|
|
8527
|
+
}
|
|
8528
|
+
|
|
8453
8529
|
_setupErrorListener() {
|
|
8454
8530
|
this._errorHandler = (e) => {
|
|
8455
8531
|
const { message } = e.detail;
|
|
@@ -415,40 +415,87 @@
|
|
|
415
415
|
|
|
416
416
|
/**
|
|
417
417
|
* Load a PDF document from a URL.
|
|
418
|
+
*
|
|
419
|
+
* PDF.js loads PDFs via streamed HTTP Range requests by default, which some
|
|
420
|
+
* corporate antivirus / web-filter products block while letting plain GETs
|
|
421
|
+
* through. If the initial streamed load fails with a network-shaped error,
|
|
422
|
+
* we retry once by fetching the entire PDF as an ArrayBuffer and handing the
|
|
423
|
+
* bytes to PDF.js directly.
|
|
424
|
+
*
|
|
418
425
|
* @param {string} url - The PDF URL
|
|
419
426
|
* @returns {Promise<PDFDocumentProxy>}
|
|
420
427
|
*/
|
|
421
428
|
async load(url) {
|
|
422
429
|
try {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
430
|
+
return await this._loadDocument(url)
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (typeof url === "string" && this._shouldRetryAsBlob(error)) {
|
|
433
|
+
console.warn("PDF streamed load failed, retrying as full fetch:", error);
|
|
434
|
+
try {
|
|
435
|
+
const response = await fetch(url, { credentials: "same-origin" });
|
|
436
|
+
if (!response.ok) {
|
|
437
|
+
throw new Error(`HTTP ${response.status} fetching PDF`)
|
|
438
|
+
}
|
|
439
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
440
|
+
return await this._loadDocument({ data })
|
|
441
|
+
} catch (retryError) {
|
|
442
|
+
console.error("PDF blob fallback also failed:", retryError);
|
|
443
|
+
this.eventBus.dispatch(ViewerEvents.DOCUMENT_LOAD_ERROR, { error: retryError });
|
|
444
|
+
throw retryError
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
console.error("Error loading PDF:", error);
|
|
448
|
+
this.eventBus.dispatch(ViewerEvents.DOCUMENT_LOAD_ERROR, { error });
|
|
449
|
+
throw error
|
|
450
|
+
}
|
|
451
|
+
}
|
|
426
452
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Load a PDF document from either a URL or pre-fetched bytes.
|
|
455
|
+
* @param {string|{data: Uint8Array}} source
|
|
456
|
+
* @returns {Promise<PDFDocumentProxy>}
|
|
457
|
+
*/
|
|
458
|
+
async _loadDocument(source) {
|
|
459
|
+
const loadingTask = pdfjsLib__namespace.getDocument(source);
|
|
460
|
+
this.pdfDocument = await loadingTask.promise;
|
|
461
|
+
this.pageCount = this.pdfDocument.numPages;
|
|
430
462
|
|
|
431
|
-
|
|
432
|
-
|
|
463
|
+
// Clear any existing content
|
|
464
|
+
this.container.innerHTML = "";
|
|
465
|
+
this.pages.clear();
|
|
433
466
|
|
|
434
|
-
|
|
435
|
-
|
|
467
|
+
// Set initial display scale on container
|
|
468
|
+
this.container.style.setProperty("--display-scale", String(this.displayScale));
|
|
436
469
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
pageCount: this.pageCount,
|
|
440
|
-
pdfDocument: this.pdfDocument
|
|
441
|
-
});
|
|
470
|
+
// Create page placeholders for all pages
|
|
471
|
+
await this._createPagePlaceholders();
|
|
442
472
|
|
|
443
|
-
|
|
444
|
-
|
|
473
|
+
// Dispatch loaded event
|
|
474
|
+
this.eventBus.dispatch(ViewerEvents.DOCUMENT_LOADED, {
|
|
475
|
+
pageCount: this.pageCount,
|
|
476
|
+
pdfDocument: this.pdfDocument
|
|
477
|
+
});
|
|
445
478
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
479
|
+
// Trigger initial render of visible pages
|
|
480
|
+
this._renderingQueue.renderHighestPriority(this.getVisiblePages());
|
|
481
|
+
|
|
482
|
+
return this.pdfDocument
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Decide whether a load failure is worth retrying via full-fetch.
|
|
487
|
+
* Skips errors where a different transport won't help: password-protected
|
|
488
|
+
* PDFs, corrupt files, and auth failures.
|
|
489
|
+
*/
|
|
490
|
+
_shouldRetryAsBlob(error) {
|
|
491
|
+
const message = String(error?.message || error || "");
|
|
492
|
+
const name = String(error?.name || "");
|
|
493
|
+
|
|
494
|
+
if (/Password|InvalidPDF/i.test(name)) return false
|
|
495
|
+
if (/\b40[0-9]\b/.test(message)) return false
|
|
496
|
+
if (/password|invalid pdf|encrypted/i.test(message)) return false
|
|
497
|
+
|
|
498
|
+
return true
|
|
452
499
|
}
|
|
453
500
|
|
|
454
501
|
/**
|
|
@@ -2455,11 +2502,12 @@
|
|
|
2455
2502
|
});
|
|
2456
2503
|
|
|
2457
2504
|
// Close on outside click
|
|
2458
|
-
|
|
2505
|
+
this._documentClickHandler = () => {
|
|
2459
2506
|
if (this.isOpen) {
|
|
2460
2507
|
this._closeDropdown();
|
|
2461
2508
|
}
|
|
2462
|
-
}
|
|
2509
|
+
};
|
|
2510
|
+
document.addEventListener("click", this._documentClickHandler);
|
|
2463
2511
|
}
|
|
2464
2512
|
|
|
2465
2513
|
_closeDropdown() {
|
|
@@ -2492,6 +2540,15 @@
|
|
|
2492
2540
|
getColor() {
|
|
2493
2541
|
return this.currentColor
|
|
2494
2542
|
}
|
|
2543
|
+
|
|
2544
|
+
destroy() {
|
|
2545
|
+
if (this._documentClickHandler) {
|
|
2546
|
+
document.removeEventListener("click", this._documentClickHandler);
|
|
2547
|
+
this._documentClickHandler = null;
|
|
2548
|
+
}
|
|
2549
|
+
this.element?.remove();
|
|
2550
|
+
this.element = null;
|
|
2551
|
+
}
|
|
2495
2552
|
}
|
|
2496
2553
|
|
|
2497
2554
|
class AnnotationEditToolbar {
|
|
@@ -8408,6 +8465,7 @@
|
|
|
8408
8465
|
this.annotationSidebar?.destroy();
|
|
8409
8466
|
this.findController?.destroy();
|
|
8410
8467
|
this.findBar?.destroy();
|
|
8468
|
+
this.colorPicker?.destroy();
|
|
8411
8469
|
|
|
8412
8470
|
Object.values(this.tools).forEach(tool => tool.destroy?.());
|
|
8413
8471
|
|
|
@@ -8430,7 +8488,8 @@
|
|
|
8430
8488
|
initialPage: Number,
|
|
8431
8489
|
initialAnnotation: String,
|
|
8432
8490
|
autoHeight: { type: Boolean, default: true },
|
|
8433
|
-
detailPanel: { type: Boolean, default: false }
|
|
8491
|
+
detailPanel: { type: Boolean, default: false },
|
|
8492
|
+
errorMessage: String
|
|
8434
8493
|
}
|
|
8435
8494
|
|
|
8436
8495
|
initialize() {
|
|
@@ -8466,10 +8525,27 @@
|
|
|
8466
8525
|
await this.pdfViewer.load();
|
|
8467
8526
|
} catch (error) {
|
|
8468
8527
|
console.error("Failed to load PDF:", error);
|
|
8469
|
-
this.
|
|
8528
|
+
this._handleLoadFailure(error);
|
|
8470
8529
|
}
|
|
8471
8530
|
}
|
|
8472
8531
|
|
|
8532
|
+
// Surface a final load failure: hide the spinner, show a host-customizable
|
|
8533
|
+
// message, and dispatch a DOM event so the host app can wire telemetry.
|
|
8534
|
+
_handleLoadFailure(error) {
|
|
8535
|
+
if (this.hasLoadingOverlayTarget) {
|
|
8536
|
+
this.loadingOverlayTarget.classList.add("hidden");
|
|
8537
|
+
}
|
|
8538
|
+
const message = this.errorMessageValue || "Failed to load PDF document";
|
|
8539
|
+
this._showError(message);
|
|
8540
|
+
this.containerTarget.dispatchEvent(new CustomEvent("pdf-viewer:load-failed", {
|
|
8541
|
+
bubbles: true,
|
|
8542
|
+
detail: {
|
|
8543
|
+
message: String(error?.message || error || ""),
|
|
8544
|
+
name: String(error?.name || "")
|
|
8545
|
+
}
|
|
8546
|
+
}));
|
|
8547
|
+
}
|
|
8548
|
+
|
|
8473
8549
|
_setupErrorListener() {
|
|
8474
8550
|
this._errorHandler = (e) => {
|
|
8475
8551
|
const { message } = e.detail;
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stimulus-pdf-viewer-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jeremy Baker
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|