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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6fe6b668cbffa8acaf9ade9ef15614aad4d7677e3dc505bebe4065ba8ccdb726
4
- data.tar.gz: f8f51d4e310e36beb6dd51efc9dd3501daaa15067b2a853912d074427a04b9f7
3
+ metadata.gz: 94c69ce661ffbfcc99fa65676d97183b65252cb525c69ac4ef4d0dd128a6ba48
4
+ data.tar.gz: c51c519b51d76b496afa78ffa68703bf7eb17e2f147516de224e75bf6c834bfe
5
5
  SHA512:
6
- metadata.gz: ad37d63e9f0c26f373b293737818fd85121bf78945f8e97ab06371f75d2aa3ac6b8b4b9976a27cb5553b5cb3f738b51258a82541761bb78531116e34fa27e90f
7
- data.tar.gz: 41b654cae115f12ef9a8f33c80d5d0645657cbccc7b70ee94651dd557b2ea416b38ace501b7a0e43cf9387dc8b16237c2f7e442915d338e1c03dd0ee3108ad45
6
+ metadata.gz: db307a6dc1e632f7d4e6ddb1c5fec962c421e4981a0e713e71b33686d5b82bf9f3a16ac297049c50bd217424254e863aaca8d63320d695c6feadea5be7bbad6b
7
+ data.tar.gz: d01c3a7398acf4b25054685c87258bf1dc9d24718905c014c5b41410d0bcb0f878c9495ec7f13e639d2071adb3737625e88e793da97424014fe0a43ae82f4733
data/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.3.2] - 2026-05-13
6
+
7
+ ### Added
8
+ - Updated stimulus-pdf-viewer to 0.3.2
9
+
5
10
  ## [0.3.0] - 2026-04-01
6
11
 
7
12
  ### Added
@@ -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
- const loadingTask = pdfjsLib.getDocument(url);
404
- this.pdfDocument = await loadingTask.promise;
405
- this.pageCount = this.pdfDocument.numPages;
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
- // Clear any existing content
408
- this.container.innerHTML = "";
409
- this.pages.clear();
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
- // Set initial display scale on container
412
- this.container.style.setProperty("--display-scale", String(this.displayScale));
443
+ // Clear any existing content
444
+ this.container.innerHTML = "";
445
+ this.pages.clear();
413
446
 
414
- // Create page placeholders for all pages
415
- await this._createPagePlaceholders();
447
+ // Set initial display scale on container
448
+ this.container.style.setProperty("--display-scale", String(this.displayScale));
416
449
 
417
- // Dispatch loaded event
418
- this.eventBus.dispatch(ViewerEvents.DOCUMENT_LOADED, {
419
- pageCount: this.pageCount,
420
- pdfDocument: this.pdfDocument
421
- });
450
+ // Create page placeholders for all pages
451
+ await this._createPagePlaceholders();
422
452
 
423
- // Trigger initial render of visible pages
424
- this._renderingQueue.renderHighestPriority(this.getVisiblePages());
453
+ // Dispatch loaded event
454
+ this.eventBus.dispatch(ViewerEvents.DOCUMENT_LOADED, {
455
+ pageCount: this.pageCount,
456
+ pdfDocument: this.pdfDocument
457
+ });
425
458
 
426
- return this.pdfDocument
427
- } catch (error) {
428
- console.error("Error loading PDF:", error);
429
- this.eventBus.dispatch(ViewerEvents.DOCUMENT_LOAD_ERROR, { error });
430
- throw error
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
- document.addEventListener("click", () => {
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._showError("Failed to load PDF document");
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
- const loadingTask = pdfjsLib__namespace.getDocument(url);
424
- this.pdfDocument = await loadingTask.promise;
425
- this.pageCount = this.pdfDocument.numPages;
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
- // Clear any existing content
428
- this.container.innerHTML = "";
429
- this.pages.clear();
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
- // Set initial display scale on container
432
- this.container.style.setProperty("--display-scale", String(this.displayScale));
463
+ // Clear any existing content
464
+ this.container.innerHTML = "";
465
+ this.pages.clear();
433
466
 
434
- // Create page placeholders for all pages
435
- await this._createPagePlaceholders();
467
+ // Set initial display scale on container
468
+ this.container.style.setProperty("--display-scale", String(this.displayScale));
436
469
 
437
- // Dispatch loaded event
438
- this.eventBus.dispatch(ViewerEvents.DOCUMENT_LOADED, {
439
- pageCount: this.pageCount,
440
- pdfDocument: this.pdfDocument
441
- });
470
+ // Create page placeholders for all pages
471
+ await this._createPagePlaceholders();
442
472
 
443
- // Trigger initial render of visible pages
444
- this._renderingQueue.renderHighestPriority(this.getVisiblePages());
473
+ // Dispatch loaded event
474
+ this.eventBus.dispatch(ViewerEvents.DOCUMENT_LOADED, {
475
+ pageCount: this.pageCount,
476
+ pdfDocument: this.pdfDocument
477
+ });
445
478
 
446
- return this.pdfDocument
447
- } catch (error) {
448
- console.error("Error loading PDF:", error);
449
- this.eventBus.dispatch(ViewerEvents.DOCUMENT_LOAD_ERROR, { error });
450
- throw error
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
- document.addEventListener("click", () => {
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._showError("Failed to load PDF document");
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;
@@ -1,7 +1,7 @@
1
1
  module StimulusPdfViewer
2
2
  module Rails
3
- VERSION = "0.3.1"
3
+ VERSION = "0.3.2"
4
4
  # This should match the npm package version
5
- STIMULUS_PDF_VIEWER_VERSION = "0.3.1"
5
+ STIMULUS_PDF_VIEWER_VERSION = "0.3.2"
6
6
  end
7
7
  end
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.1
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-04-04 00:00:00.000000000 Z
11
+ date: 2026-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties