stimulus-pdf-viewer-rails 0.4.1 → 0.5.0
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 838e9d480a11d8bbf5a590bad2ab618624c1ec95284a1b7aa28b2c40468447ab
|
|
4
|
+
data.tar.gz: 7296bfffca896a43fb16a50a3d271a790eab5541d3744aef8df90ae336e2767a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32804a93259414ccee6b961564c6587204c6deadcbf5bf07de69d1872751eb125c89099ec5e69125934f7a4ca85974b9bdf5b2b620912725bccb599ce27e082e
|
|
7
|
+
data.tar.gz: '099ea9e6b131bfba5f48b4a63818c03841e05b5da782f51adca84418bc9ee3538b8cc47982e169e76386acaa7c125278bb3794f53ac15a021efd152c1bbf9c37'
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.0] - 2026-08-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Updated stimulus-pdf-viewer to 0.5.0
|
|
9
|
+
|
|
10
|
+
Adds support for password-protected PDFs: the viewer prompts for the password
|
|
11
|
+
(with retry on incorrect entry) and opens encrypted documents in read-only
|
|
12
|
+
mode — annotation tools are disabled and the download button delivers the
|
|
13
|
+
original file. The vendored stylesheet includes new rules for the password
|
|
14
|
+
prompt and read-only mode, so recompile your assets after upgrading. See the
|
|
15
|
+
upstream
|
|
16
|
+
[0.5.0 changelog](https://github.com/jhubert/stimulus-pdf-viewer/blob/main/CHANGELOG.md)
|
|
17
|
+
for full details.
|
|
18
|
+
|
|
5
19
|
## [0.4.1] - 2026-08-10
|
|
6
20
|
|
|
7
21
|
### Fixed
|
|
@@ -369,11 +369,21 @@ class CoreViewer {
|
|
|
369
369
|
this.container = container;
|
|
370
370
|
this.eventBus = options.eventBus || new EventBus();
|
|
371
371
|
|
|
372
|
+
// Called when the document needs a password: ({ retry }) => Promise<string>.
|
|
373
|
+
// Resolve with the password, reject to abort the load. When absent,
|
|
374
|
+
// password-protected documents fail to load with a PasswordException.
|
|
375
|
+
this.onPasswordRequest = options.onPasswordRequest || null;
|
|
376
|
+
|
|
372
377
|
// PDF.js document reference
|
|
373
378
|
this.pdfDocument = null;
|
|
374
379
|
this._loadingTask = null;
|
|
375
380
|
this.pageCount = 0;
|
|
376
381
|
|
|
382
|
+
// Whether the loaded document is encrypted (user- or owner-password).
|
|
383
|
+
// Valid after load() resolves.
|
|
384
|
+
this.isEncrypted = false;
|
|
385
|
+
this._lastPassword = null;
|
|
386
|
+
|
|
377
387
|
// Page data storage: pageNumber -> PageData
|
|
378
388
|
this.pages = new Map();
|
|
379
389
|
|
|
@@ -434,6 +444,8 @@ class CoreViewer {
|
|
|
434
444
|
* @returns {Promise<PDFDocumentProxy>}
|
|
435
445
|
*/
|
|
436
446
|
async load(url) {
|
|
447
|
+
this._lastPassword = null;
|
|
448
|
+
this.isEncrypted = false;
|
|
437
449
|
try {
|
|
438
450
|
return await this._loadDocument(url)
|
|
439
451
|
} catch (error) {
|
|
@@ -445,7 +457,10 @@ class CoreViewer {
|
|
|
445
457
|
throw new Error(`HTTP ${response.status} fetching PDF`)
|
|
446
458
|
}
|
|
447
459
|
const data = new Uint8Array(await response.arrayBuffer());
|
|
448
|
-
|
|
460
|
+
// Reuse a password entered during the streamed attempt so the
|
|
461
|
+
// fallback doesn't prompt the user a second time
|
|
462
|
+
const source = this._lastPassword ? { data, password: this._lastPassword } : { data };
|
|
463
|
+
return await this._loadDocument(source)
|
|
449
464
|
} catch (retryError) {
|
|
450
465
|
console.error("PDF blob fallback also failed:", retryError);
|
|
451
466
|
this.eventBus.dispatch(ViewerEvents.DOCUMENT_LOAD_ERROR, { error: retryError });
|
|
@@ -471,9 +486,32 @@ class CoreViewer {
|
|
|
471
486
|
await this._teardownDocument();
|
|
472
487
|
|
|
473
488
|
this._loadingTask = pdfjsLib.getDocument(source);
|
|
489
|
+
|
|
490
|
+
// Prompt for a password instead of failing with PasswordException. PDF.js
|
|
491
|
+
// calls onPassword again with INCORRECT_PASSWORD after a wrong attempt;
|
|
492
|
+
// passing an Error to updatePassword aborts the load.
|
|
493
|
+
if (this.onPasswordRequest) {
|
|
494
|
+
this._loadingTask.onPassword = (updatePassword, reason) => {
|
|
495
|
+
const retry = reason === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD;
|
|
496
|
+
Promise.resolve(this.onPasswordRequest({ retry }))
|
|
497
|
+
.then(password => {
|
|
498
|
+
this._lastPassword = password;
|
|
499
|
+
updatePassword(password);
|
|
500
|
+
})
|
|
501
|
+
.catch(error => {
|
|
502
|
+
updatePassword(error instanceof Error ? error : new Error("Password entry cancelled"));
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
474
507
|
this.pdfDocument = await this._loadingTask.promise;
|
|
475
508
|
this.pageCount = this.pdfDocument.numPages;
|
|
476
509
|
|
|
510
|
+
// getPermissions() returns null for unencrypted documents. Any encryption
|
|
511
|
+
// (user- or owner-password) makes the document read-only downstream:
|
|
512
|
+
// pdf-lib can't open encrypted files to embed annotations on download.
|
|
513
|
+
this.isEncrypted = (await this.pdfDocument.getPermissions()) !== null;
|
|
514
|
+
|
|
477
515
|
// Set initial display scale on container
|
|
478
516
|
this.container.style.setProperty("--display-scale", String(this.displayScale));
|
|
479
517
|
|
|
@@ -1988,6 +2026,16 @@ class DownloadManager {
|
|
|
1988
2026
|
this._triggerDownload(pdfBytes, filename);
|
|
1989
2027
|
}
|
|
1990
2028
|
|
|
2029
|
+
// Download the original file untouched. Used for encrypted documents, which
|
|
2030
|
+
// pdf-lib can't open to embed annotations or the watermark.
|
|
2031
|
+
async downloadOriginal() {
|
|
2032
|
+
const request = new FetchRequest("get", this.documentUrl, { responseKind: "blob" });
|
|
2033
|
+
const response = await request.perform();
|
|
2034
|
+
const pdfBytes = await response.response.arrayBuffer();
|
|
2035
|
+
const filename = this._sanitizeFilename(this.documentName || "document");
|
|
2036
|
+
this._triggerDownload(pdfBytes, filename);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
1991
2039
|
_applyWatermarkToPage(page, font, width, height) {
|
|
1992
2040
|
if (!this.userName) return
|
|
1993
2041
|
|
|
@@ -5608,6 +5656,122 @@ class FindBar {
|
|
|
5608
5656
|
}
|
|
5609
5657
|
}
|
|
5610
5658
|
|
|
5659
|
+
/**
|
|
5660
|
+
* PasswordPrompt - Modal dialog for unlocking password-protected PDFs.
|
|
5661
|
+
*
|
|
5662
|
+
* Shown over the viewer when PDF.js requests a password during document
|
|
5663
|
+
* load. Each request() call returns a promise that resolves with the
|
|
5664
|
+
* entered password, or rejects when the user cancels.
|
|
5665
|
+
*/
|
|
5666
|
+
|
|
5667
|
+
class PasswordPrompt {
|
|
5668
|
+
constructor(options = {}) {
|
|
5669
|
+
this.container = options.container;
|
|
5670
|
+
|
|
5671
|
+
this.element = null;
|
|
5672
|
+
this.inputElement = null;
|
|
5673
|
+
this.errorElement = null;
|
|
5674
|
+
|
|
5675
|
+
this._pending = null; // { resolve, reject } for the in-flight request
|
|
5676
|
+
|
|
5677
|
+
this._createUI();
|
|
5678
|
+
this._setupEventListeners();
|
|
5679
|
+
}
|
|
5680
|
+
|
|
5681
|
+
_createUI() {
|
|
5682
|
+
this.element = document.createElement("div");
|
|
5683
|
+
this.element.className = "pdf-password-prompt hidden";
|
|
5684
|
+
this.element.innerHTML = `
|
|
5685
|
+
<div class="pdf-password-prompt-dialog" role="dialog" aria-modal="true" aria-labelledby="pdf-password-prompt-title">
|
|
5686
|
+
<h2 class="pdf-password-prompt-title" id="pdf-password-prompt-title">Password required</h2>
|
|
5687
|
+
<p class="pdf-password-prompt-message">This document is protected. Enter the password to open it.</p>
|
|
5688
|
+
<p class="pdf-password-prompt-error hidden">Incorrect password. Please try again.</p>
|
|
5689
|
+
<input type="password" class="pdf-password-prompt-input" autocomplete="off" aria-label="Document password">
|
|
5690
|
+
<div class="pdf-password-prompt-buttons">
|
|
5691
|
+
<button type="button" class="pdf-password-prompt-btn pdf-password-prompt-cancel">Cancel</button>
|
|
5692
|
+
<button type="button" class="pdf-password-prompt-btn pdf-password-prompt-submit">Open</button>
|
|
5693
|
+
</div>
|
|
5694
|
+
</div>
|
|
5695
|
+
`;
|
|
5696
|
+
|
|
5697
|
+
this.inputElement = this.element.querySelector(".pdf-password-prompt-input");
|
|
5698
|
+
this.errorElement = this.element.querySelector(".pdf-password-prompt-error");
|
|
5699
|
+
this.submitButton = this.element.querySelector(".pdf-password-prompt-submit");
|
|
5700
|
+
this.cancelButton = this.element.querySelector(".pdf-password-prompt-cancel");
|
|
5701
|
+
|
|
5702
|
+
this.container.appendChild(this.element);
|
|
5703
|
+
}
|
|
5704
|
+
|
|
5705
|
+
_setupEventListeners() {
|
|
5706
|
+
this.submitButton.addEventListener("click", () => this._submit());
|
|
5707
|
+
this.cancelButton.addEventListener("click", () => this._cancel());
|
|
5708
|
+
|
|
5709
|
+
this.element.addEventListener("keydown", (e) => {
|
|
5710
|
+
if (e.key === "Enter") {
|
|
5711
|
+
e.preventDefault();
|
|
5712
|
+
this._submit();
|
|
5713
|
+
} else if (e.key === "Escape") {
|
|
5714
|
+
e.preventDefault();
|
|
5715
|
+
this._cancel();
|
|
5716
|
+
}
|
|
5717
|
+
});
|
|
5718
|
+
}
|
|
5719
|
+
|
|
5720
|
+
/**
|
|
5721
|
+
* Ask the user for the document password.
|
|
5722
|
+
* @param {Object} options
|
|
5723
|
+
* @param {boolean} options.retry - Whether a previous attempt was incorrect
|
|
5724
|
+
* @returns {Promise<string>} Resolves with the password, rejects on cancel
|
|
5725
|
+
*/
|
|
5726
|
+
request({ retry = false } = {}) {
|
|
5727
|
+
return new Promise((resolve, reject) => {
|
|
5728
|
+
// A request should never overlap another, but if it does, cancel the old one
|
|
5729
|
+
this._pending?.reject(new Error("Password entry cancelled"));
|
|
5730
|
+
this._pending = { resolve, reject };
|
|
5731
|
+
|
|
5732
|
+
this.errorElement.classList.toggle("hidden", !retry);
|
|
5733
|
+
this.inputElement.value = "";
|
|
5734
|
+
this.element.classList.remove("hidden");
|
|
5735
|
+
this.inputElement.focus({ preventScroll: true });
|
|
5736
|
+
})
|
|
5737
|
+
}
|
|
5738
|
+
|
|
5739
|
+
_submit() {
|
|
5740
|
+
const password = this.inputElement.value;
|
|
5741
|
+
if (!password) {
|
|
5742
|
+
this.inputElement.focus({ preventScroll: true });
|
|
5743
|
+
return
|
|
5744
|
+
}
|
|
5745
|
+
|
|
5746
|
+
const pending = this._pending;
|
|
5747
|
+
this._pending = null;
|
|
5748
|
+
this._hide();
|
|
5749
|
+
pending?.resolve(password);
|
|
5750
|
+
}
|
|
5751
|
+
|
|
5752
|
+
_cancel() {
|
|
5753
|
+
const pending = this._pending;
|
|
5754
|
+
this._pending = null;
|
|
5755
|
+
this._hide();
|
|
5756
|
+
pending?.reject(new Error("Password entry cancelled"));
|
|
5757
|
+
}
|
|
5758
|
+
|
|
5759
|
+
_hide() {
|
|
5760
|
+
this.element.classList.add("hidden");
|
|
5761
|
+
this.inputElement.value = "";
|
|
5762
|
+
}
|
|
5763
|
+
|
|
5764
|
+
/**
|
|
5765
|
+
* Clean up. Safe to call multiple times.
|
|
5766
|
+
*/
|
|
5767
|
+
destroy() {
|
|
5768
|
+
this._pending?.reject(new Error("Password entry cancelled"));
|
|
5769
|
+
this._pending = null;
|
|
5770
|
+
this.element?.remove();
|
|
5771
|
+
this.element = null;
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5774
|
+
|
|
5611
5775
|
class BaseTool {
|
|
5612
5776
|
constructor(pdfViewer) {
|
|
5613
5777
|
this.pdfViewer = pdfViewer;
|
|
@@ -7233,6 +7397,9 @@ class PdfViewer {
|
|
|
7233
7397
|
|
|
7234
7398
|
this.currentTool = null;
|
|
7235
7399
|
this.currentMode = ToolMode.SELECT;
|
|
7400
|
+
// True once an encrypted document is loaded: annotations and annotated
|
|
7401
|
+
// download are disabled (pdf-lib can't open encrypted files)
|
|
7402
|
+
this.readOnly = false;
|
|
7236
7403
|
this.selectedAnnotation = null;
|
|
7237
7404
|
this.selectedAnnotationElement = null;
|
|
7238
7405
|
this.pendingAnnotationSelection = null; // Annotation ID to select when rendered
|
|
@@ -7274,9 +7441,13 @@ class PdfViewer {
|
|
|
7274
7441
|
return
|
|
7275
7442
|
}
|
|
7276
7443
|
|
|
7444
|
+
// Modal shown when a document needs a password to open
|
|
7445
|
+
this.passwordPrompt = new PasswordPrompt({ container: this.container });
|
|
7446
|
+
|
|
7277
7447
|
// Core viewer (PDF.js wrapper with lazy rendering and events)
|
|
7278
7448
|
this.viewer = new CoreViewer(this.pagesContainer, {
|
|
7279
|
-
initialScale: 1.0
|
|
7449
|
+
initialScale: 1.0,
|
|
7450
|
+
onPasswordRequest: ({ retry }) => this.passwordPrompt.request({ retry })
|
|
7280
7451
|
});
|
|
7281
7452
|
|
|
7282
7453
|
// Subscribe to core viewer events
|
|
@@ -7420,9 +7591,11 @@ class PdfViewer {
|
|
|
7420
7591
|
*/
|
|
7421
7592
|
_onDocumentLoaded(pageCount) {
|
|
7422
7593
|
this._currentPage = 1;
|
|
7594
|
+
this.readOnly = this.viewer.isEncrypted;
|
|
7423
7595
|
this._dispatchEvent("pdf-viewer:ready", {
|
|
7424
7596
|
pageCount,
|
|
7425
|
-
currentPage: 1
|
|
7597
|
+
currentPage: 1,
|
|
7598
|
+
readOnly: this.readOnly
|
|
7426
7599
|
});
|
|
7427
7600
|
}
|
|
7428
7601
|
|
|
@@ -7519,11 +7692,12 @@ class PdfViewer {
|
|
|
7519
7692
|
await this.thumbnailSidebar.setDocument(this.viewer.pdfDocument);
|
|
7520
7693
|
}
|
|
7521
7694
|
|
|
7522
|
-
// Load existing annotations from store
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7695
|
+
// Load existing annotations from store and render them on all rendered
|
|
7696
|
+
// pages. Skipped for encrypted documents, which are view-only.
|
|
7697
|
+
if (!this.readOnly) {
|
|
7698
|
+
await this.annotationManager.loadAnnotations();
|
|
7699
|
+
this._renderAnnotations();
|
|
7700
|
+
}
|
|
7527
7701
|
|
|
7528
7702
|
const annotations = this.annotationManager.getAllAnnotations();
|
|
7529
7703
|
this.container.dispatchEvent(new CustomEvent("pdf-viewer:annotations-loaded", {
|
|
@@ -7554,6 +7728,11 @@ class PdfViewer {
|
|
|
7554
7728
|
}
|
|
7555
7729
|
|
|
7556
7730
|
setTool(mode) {
|
|
7731
|
+
// Encrypted documents are view-only: only the select tool is allowed
|
|
7732
|
+
if (this.readOnly && mode !== ToolMode.SELECT) {
|
|
7733
|
+
return
|
|
7734
|
+
}
|
|
7735
|
+
|
|
7557
7736
|
// Deactivate current tool
|
|
7558
7737
|
if (this.currentTool) {
|
|
7559
7738
|
this.currentTool.deactivate();
|
|
@@ -8556,10 +8735,15 @@ class PdfViewer {
|
|
|
8556
8735
|
});
|
|
8557
8736
|
}
|
|
8558
8737
|
|
|
8559
|
-
// Download with annotations
|
|
8738
|
+
// Download with annotations (or the original file for encrypted documents,
|
|
8739
|
+
// which pdf-lib can't open to embed annotations)
|
|
8560
8740
|
async download() {
|
|
8561
8741
|
try {
|
|
8562
|
-
|
|
8742
|
+
if (this.readOnly) {
|
|
8743
|
+
await this.downloadManager.downloadOriginal();
|
|
8744
|
+
} else {
|
|
8745
|
+
await this.downloadManager.downloadWithAnnotations();
|
|
8746
|
+
}
|
|
8563
8747
|
} catch (error) {
|
|
8564
8748
|
console.error("Failed to download PDF:", error);
|
|
8565
8749
|
throw error
|
|
@@ -8588,6 +8772,7 @@ class PdfViewer {
|
|
|
8588
8772
|
this.findController?.destroy();
|
|
8589
8773
|
this.findBar?.destroy();
|
|
8590
8774
|
this.colorPicker?.destroy();
|
|
8775
|
+
this.passwordPrompt?.destroy();
|
|
8591
8776
|
|
|
8592
8777
|
Object.values(this.tools || {}).forEach(tool => tool.destroy?.());
|
|
8593
8778
|
|
|
@@ -8657,7 +8842,9 @@ class pdf_viewer_controller extends Controller {
|
|
|
8657
8842
|
if (this.hasLoadingOverlayTarget) {
|
|
8658
8843
|
this.loadingOverlayTarget.classList.add("hidden");
|
|
8659
8844
|
}
|
|
8660
|
-
const
|
|
8845
|
+
const isPasswordError = /password/i.test(String(error?.message || "")) || /password/i.test(String(error?.name || ""));
|
|
8846
|
+
const message = this.errorMessageValue ||
|
|
8847
|
+
(isPasswordError ? "A password is required to open this document" : "Failed to load PDF document");
|
|
8661
8848
|
this._showError(message);
|
|
8662
8849
|
this.containerTarget.dispatchEvent(new CustomEvent("pdf-viewer:load-failed", {
|
|
8663
8850
|
bubbles: true,
|
|
@@ -8838,6 +9025,10 @@ class pdf_viewer_controller extends Controller {
|
|
|
8838
9025
|
}
|
|
8839
9026
|
|
|
8840
9027
|
_activateTool(toolName) {
|
|
9028
|
+
// View-only for encrypted documents; the buttons are disabled, but guard
|
|
9029
|
+
// against keyboard/programmatic activation too
|
|
9030
|
+
if (this._readOnly && toolName !== "select") return
|
|
9031
|
+
|
|
8841
9032
|
// Tool map for name -> mode conversion
|
|
8842
9033
|
const toolMap = {
|
|
8843
9034
|
select: ToolMode.SELECT,
|
|
@@ -9023,8 +9214,8 @@ class pdf_viewer_controller extends Controller {
|
|
|
9023
9214
|
_setupPageNavigationListeners() {
|
|
9024
9215
|
// Listen for ready event from PdfViewer
|
|
9025
9216
|
this._readyHandler = (e) => {
|
|
9026
|
-
const { pageCount, currentPage } = e.detail;
|
|
9027
|
-
this._onViewerReady(pageCount, currentPage);
|
|
9217
|
+
const { pageCount, currentPage, readOnly } = e.detail;
|
|
9218
|
+
this._onViewerReady(pageCount, currentPage, readOnly);
|
|
9028
9219
|
};
|
|
9029
9220
|
this.containerTarget.addEventListener("pdf-viewer:ready", this._readyHandler);
|
|
9030
9221
|
|
|
@@ -9036,12 +9227,16 @@ class pdf_viewer_controller extends Controller {
|
|
|
9036
9227
|
this.containerTarget.addEventListener("pdf-viewer:page-changed", this._pageChangedHandler);
|
|
9037
9228
|
}
|
|
9038
9229
|
|
|
9039
|
-
_onViewerReady(pageCount, currentPage) {
|
|
9230
|
+
_onViewerReady(pageCount, currentPage, readOnly) {
|
|
9040
9231
|
// Hide the loading overlay
|
|
9041
9232
|
if (this.hasLoadingOverlayTarget) {
|
|
9042
9233
|
this.loadingOverlayTarget.classList.add("hidden");
|
|
9043
9234
|
}
|
|
9044
9235
|
|
|
9236
|
+
if (readOnly) {
|
|
9237
|
+
this._enterReadOnlyMode();
|
|
9238
|
+
}
|
|
9239
|
+
|
|
9045
9240
|
if (this.hasPageCountTarget) {
|
|
9046
9241
|
this.pageCountTarget.textContent = pageCount;
|
|
9047
9242
|
}
|
|
@@ -9062,6 +9257,16 @@ class pdf_viewer_controller extends Controller {
|
|
|
9062
9257
|
this._setZoomPreset("auto");
|
|
9063
9258
|
}
|
|
9064
9259
|
|
|
9260
|
+
// Encrypted documents are view-only: disable the annotation tools and hide
|
|
9261
|
+
// the color picker. Download stays enabled but delivers the original file.
|
|
9262
|
+
_enterReadOnlyMode() {
|
|
9263
|
+
this._readOnly = true;
|
|
9264
|
+
this.containerTarget.classList.add("pdf-viewer-read-only");
|
|
9265
|
+
this.containerTarget
|
|
9266
|
+
.querySelectorAll('.pdf-tool-btn[data-tool]:not([data-tool="select"]), .pdf-overflow-tool-btn[data-tool]:not([data-tool="select"])')
|
|
9267
|
+
.forEach(btn => { btn.disabled = true; });
|
|
9268
|
+
}
|
|
9269
|
+
|
|
9065
9270
|
_onPageChanged(currentPage, pageCount) {
|
|
9066
9271
|
if (this.hasPageInputTarget && document.activeElement !== this.pageInputTarget) {
|
|
9067
9272
|
this.pageInputTarget.value = currentPage;
|
|
@@ -389,11 +389,21 @@
|
|
|
389
389
|
this.container = container;
|
|
390
390
|
this.eventBus = options.eventBus || new EventBus();
|
|
391
391
|
|
|
392
|
+
// Called when the document needs a password: ({ retry }) => Promise<string>.
|
|
393
|
+
// Resolve with the password, reject to abort the load. When absent,
|
|
394
|
+
// password-protected documents fail to load with a PasswordException.
|
|
395
|
+
this.onPasswordRequest = options.onPasswordRequest || null;
|
|
396
|
+
|
|
392
397
|
// PDF.js document reference
|
|
393
398
|
this.pdfDocument = null;
|
|
394
399
|
this._loadingTask = null;
|
|
395
400
|
this.pageCount = 0;
|
|
396
401
|
|
|
402
|
+
// Whether the loaded document is encrypted (user- or owner-password).
|
|
403
|
+
// Valid after load() resolves.
|
|
404
|
+
this.isEncrypted = false;
|
|
405
|
+
this._lastPassword = null;
|
|
406
|
+
|
|
397
407
|
// Page data storage: pageNumber -> PageData
|
|
398
408
|
this.pages = new Map();
|
|
399
409
|
|
|
@@ -454,6 +464,8 @@
|
|
|
454
464
|
* @returns {Promise<PDFDocumentProxy>}
|
|
455
465
|
*/
|
|
456
466
|
async load(url) {
|
|
467
|
+
this._lastPassword = null;
|
|
468
|
+
this.isEncrypted = false;
|
|
457
469
|
try {
|
|
458
470
|
return await this._loadDocument(url)
|
|
459
471
|
} catch (error) {
|
|
@@ -465,7 +477,10 @@
|
|
|
465
477
|
throw new Error(`HTTP ${response.status} fetching PDF`)
|
|
466
478
|
}
|
|
467
479
|
const data = new Uint8Array(await response.arrayBuffer());
|
|
468
|
-
|
|
480
|
+
// Reuse a password entered during the streamed attempt so the
|
|
481
|
+
// fallback doesn't prompt the user a second time
|
|
482
|
+
const source = this._lastPassword ? { data, password: this._lastPassword } : { data };
|
|
483
|
+
return await this._loadDocument(source)
|
|
469
484
|
} catch (retryError) {
|
|
470
485
|
console.error("PDF blob fallback also failed:", retryError);
|
|
471
486
|
this.eventBus.dispatch(ViewerEvents.DOCUMENT_LOAD_ERROR, { error: retryError });
|
|
@@ -491,9 +506,32 @@
|
|
|
491
506
|
await this._teardownDocument();
|
|
492
507
|
|
|
493
508
|
this._loadingTask = pdfjsLib__namespace.getDocument(source);
|
|
509
|
+
|
|
510
|
+
// Prompt for a password instead of failing with PasswordException. PDF.js
|
|
511
|
+
// calls onPassword again with INCORRECT_PASSWORD after a wrong attempt;
|
|
512
|
+
// passing an Error to updatePassword aborts the load.
|
|
513
|
+
if (this.onPasswordRequest) {
|
|
514
|
+
this._loadingTask.onPassword = (updatePassword, reason) => {
|
|
515
|
+
const retry = reason === pdfjsLib__namespace.PasswordResponses.INCORRECT_PASSWORD;
|
|
516
|
+
Promise.resolve(this.onPasswordRequest({ retry }))
|
|
517
|
+
.then(password => {
|
|
518
|
+
this._lastPassword = password;
|
|
519
|
+
updatePassword(password);
|
|
520
|
+
})
|
|
521
|
+
.catch(error => {
|
|
522
|
+
updatePassword(error instanceof Error ? error : new Error("Password entry cancelled"));
|
|
523
|
+
});
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
494
527
|
this.pdfDocument = await this._loadingTask.promise;
|
|
495
528
|
this.pageCount = this.pdfDocument.numPages;
|
|
496
529
|
|
|
530
|
+
// getPermissions() returns null for unencrypted documents. Any encryption
|
|
531
|
+
// (user- or owner-password) makes the document read-only downstream:
|
|
532
|
+
// pdf-lib can't open encrypted files to embed annotations on download.
|
|
533
|
+
this.isEncrypted = (await this.pdfDocument.getPermissions()) !== null;
|
|
534
|
+
|
|
497
535
|
// Set initial display scale on container
|
|
498
536
|
this.container.style.setProperty("--display-scale", String(this.displayScale));
|
|
499
537
|
|
|
@@ -2008,6 +2046,16 @@
|
|
|
2008
2046
|
this._triggerDownload(pdfBytes, filename);
|
|
2009
2047
|
}
|
|
2010
2048
|
|
|
2049
|
+
// Download the original file untouched. Used for encrypted documents, which
|
|
2050
|
+
// pdf-lib can't open to embed annotations or the watermark.
|
|
2051
|
+
async downloadOriginal() {
|
|
2052
|
+
const request = new request_js.FetchRequest("get", this.documentUrl, { responseKind: "blob" });
|
|
2053
|
+
const response = await request.perform();
|
|
2054
|
+
const pdfBytes = await response.response.arrayBuffer();
|
|
2055
|
+
const filename = this._sanitizeFilename(this.documentName || "document");
|
|
2056
|
+
this._triggerDownload(pdfBytes, filename);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2011
2059
|
_applyWatermarkToPage(page, font, width, height) {
|
|
2012
2060
|
if (!this.userName) return
|
|
2013
2061
|
|
|
@@ -5628,6 +5676,122 @@
|
|
|
5628
5676
|
}
|
|
5629
5677
|
}
|
|
5630
5678
|
|
|
5679
|
+
/**
|
|
5680
|
+
* PasswordPrompt - Modal dialog for unlocking password-protected PDFs.
|
|
5681
|
+
*
|
|
5682
|
+
* Shown over the viewer when PDF.js requests a password during document
|
|
5683
|
+
* load. Each request() call returns a promise that resolves with the
|
|
5684
|
+
* entered password, or rejects when the user cancels.
|
|
5685
|
+
*/
|
|
5686
|
+
|
|
5687
|
+
class PasswordPrompt {
|
|
5688
|
+
constructor(options = {}) {
|
|
5689
|
+
this.container = options.container;
|
|
5690
|
+
|
|
5691
|
+
this.element = null;
|
|
5692
|
+
this.inputElement = null;
|
|
5693
|
+
this.errorElement = null;
|
|
5694
|
+
|
|
5695
|
+
this._pending = null; // { resolve, reject } for the in-flight request
|
|
5696
|
+
|
|
5697
|
+
this._createUI();
|
|
5698
|
+
this._setupEventListeners();
|
|
5699
|
+
}
|
|
5700
|
+
|
|
5701
|
+
_createUI() {
|
|
5702
|
+
this.element = document.createElement("div");
|
|
5703
|
+
this.element.className = "pdf-password-prompt hidden";
|
|
5704
|
+
this.element.innerHTML = `
|
|
5705
|
+
<div class="pdf-password-prompt-dialog" role="dialog" aria-modal="true" aria-labelledby="pdf-password-prompt-title">
|
|
5706
|
+
<h2 class="pdf-password-prompt-title" id="pdf-password-prompt-title">Password required</h2>
|
|
5707
|
+
<p class="pdf-password-prompt-message">This document is protected. Enter the password to open it.</p>
|
|
5708
|
+
<p class="pdf-password-prompt-error hidden">Incorrect password. Please try again.</p>
|
|
5709
|
+
<input type="password" class="pdf-password-prompt-input" autocomplete="off" aria-label="Document password">
|
|
5710
|
+
<div class="pdf-password-prompt-buttons">
|
|
5711
|
+
<button type="button" class="pdf-password-prompt-btn pdf-password-prompt-cancel">Cancel</button>
|
|
5712
|
+
<button type="button" class="pdf-password-prompt-btn pdf-password-prompt-submit">Open</button>
|
|
5713
|
+
</div>
|
|
5714
|
+
</div>
|
|
5715
|
+
`;
|
|
5716
|
+
|
|
5717
|
+
this.inputElement = this.element.querySelector(".pdf-password-prompt-input");
|
|
5718
|
+
this.errorElement = this.element.querySelector(".pdf-password-prompt-error");
|
|
5719
|
+
this.submitButton = this.element.querySelector(".pdf-password-prompt-submit");
|
|
5720
|
+
this.cancelButton = this.element.querySelector(".pdf-password-prompt-cancel");
|
|
5721
|
+
|
|
5722
|
+
this.container.appendChild(this.element);
|
|
5723
|
+
}
|
|
5724
|
+
|
|
5725
|
+
_setupEventListeners() {
|
|
5726
|
+
this.submitButton.addEventListener("click", () => this._submit());
|
|
5727
|
+
this.cancelButton.addEventListener("click", () => this._cancel());
|
|
5728
|
+
|
|
5729
|
+
this.element.addEventListener("keydown", (e) => {
|
|
5730
|
+
if (e.key === "Enter") {
|
|
5731
|
+
e.preventDefault();
|
|
5732
|
+
this._submit();
|
|
5733
|
+
} else if (e.key === "Escape") {
|
|
5734
|
+
e.preventDefault();
|
|
5735
|
+
this._cancel();
|
|
5736
|
+
}
|
|
5737
|
+
});
|
|
5738
|
+
}
|
|
5739
|
+
|
|
5740
|
+
/**
|
|
5741
|
+
* Ask the user for the document password.
|
|
5742
|
+
* @param {Object} options
|
|
5743
|
+
* @param {boolean} options.retry - Whether a previous attempt was incorrect
|
|
5744
|
+
* @returns {Promise<string>} Resolves with the password, rejects on cancel
|
|
5745
|
+
*/
|
|
5746
|
+
request({ retry = false } = {}) {
|
|
5747
|
+
return new Promise((resolve, reject) => {
|
|
5748
|
+
// A request should never overlap another, but if it does, cancel the old one
|
|
5749
|
+
this._pending?.reject(new Error("Password entry cancelled"));
|
|
5750
|
+
this._pending = { resolve, reject };
|
|
5751
|
+
|
|
5752
|
+
this.errorElement.classList.toggle("hidden", !retry);
|
|
5753
|
+
this.inputElement.value = "";
|
|
5754
|
+
this.element.classList.remove("hidden");
|
|
5755
|
+
this.inputElement.focus({ preventScroll: true });
|
|
5756
|
+
})
|
|
5757
|
+
}
|
|
5758
|
+
|
|
5759
|
+
_submit() {
|
|
5760
|
+
const password = this.inputElement.value;
|
|
5761
|
+
if (!password) {
|
|
5762
|
+
this.inputElement.focus({ preventScroll: true });
|
|
5763
|
+
return
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5766
|
+
const pending = this._pending;
|
|
5767
|
+
this._pending = null;
|
|
5768
|
+
this._hide();
|
|
5769
|
+
pending?.resolve(password);
|
|
5770
|
+
}
|
|
5771
|
+
|
|
5772
|
+
_cancel() {
|
|
5773
|
+
const pending = this._pending;
|
|
5774
|
+
this._pending = null;
|
|
5775
|
+
this._hide();
|
|
5776
|
+
pending?.reject(new Error("Password entry cancelled"));
|
|
5777
|
+
}
|
|
5778
|
+
|
|
5779
|
+
_hide() {
|
|
5780
|
+
this.element.classList.add("hidden");
|
|
5781
|
+
this.inputElement.value = "";
|
|
5782
|
+
}
|
|
5783
|
+
|
|
5784
|
+
/**
|
|
5785
|
+
* Clean up. Safe to call multiple times.
|
|
5786
|
+
*/
|
|
5787
|
+
destroy() {
|
|
5788
|
+
this._pending?.reject(new Error("Password entry cancelled"));
|
|
5789
|
+
this._pending = null;
|
|
5790
|
+
this.element?.remove();
|
|
5791
|
+
this.element = null;
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5794
|
+
|
|
5631
5795
|
class BaseTool {
|
|
5632
5796
|
constructor(pdfViewer) {
|
|
5633
5797
|
this.pdfViewer = pdfViewer;
|
|
@@ -7253,6 +7417,9 @@
|
|
|
7253
7417
|
|
|
7254
7418
|
this.currentTool = null;
|
|
7255
7419
|
this.currentMode = ToolMode.SELECT;
|
|
7420
|
+
// True once an encrypted document is loaded: annotations and annotated
|
|
7421
|
+
// download are disabled (pdf-lib can't open encrypted files)
|
|
7422
|
+
this.readOnly = false;
|
|
7256
7423
|
this.selectedAnnotation = null;
|
|
7257
7424
|
this.selectedAnnotationElement = null;
|
|
7258
7425
|
this.pendingAnnotationSelection = null; // Annotation ID to select when rendered
|
|
@@ -7294,9 +7461,13 @@
|
|
|
7294
7461
|
return
|
|
7295
7462
|
}
|
|
7296
7463
|
|
|
7464
|
+
// Modal shown when a document needs a password to open
|
|
7465
|
+
this.passwordPrompt = new PasswordPrompt({ container: this.container });
|
|
7466
|
+
|
|
7297
7467
|
// Core viewer (PDF.js wrapper with lazy rendering and events)
|
|
7298
7468
|
this.viewer = new CoreViewer(this.pagesContainer, {
|
|
7299
|
-
initialScale: 1.0
|
|
7469
|
+
initialScale: 1.0,
|
|
7470
|
+
onPasswordRequest: ({ retry }) => this.passwordPrompt.request({ retry })
|
|
7300
7471
|
});
|
|
7301
7472
|
|
|
7302
7473
|
// Subscribe to core viewer events
|
|
@@ -7440,9 +7611,11 @@
|
|
|
7440
7611
|
*/
|
|
7441
7612
|
_onDocumentLoaded(pageCount) {
|
|
7442
7613
|
this._currentPage = 1;
|
|
7614
|
+
this.readOnly = this.viewer.isEncrypted;
|
|
7443
7615
|
this._dispatchEvent("pdf-viewer:ready", {
|
|
7444
7616
|
pageCount,
|
|
7445
|
-
currentPage: 1
|
|
7617
|
+
currentPage: 1,
|
|
7618
|
+
readOnly: this.readOnly
|
|
7446
7619
|
});
|
|
7447
7620
|
}
|
|
7448
7621
|
|
|
@@ -7539,11 +7712,12 @@
|
|
|
7539
7712
|
await this.thumbnailSidebar.setDocument(this.viewer.pdfDocument);
|
|
7540
7713
|
}
|
|
7541
7714
|
|
|
7542
|
-
// Load existing annotations from store
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7715
|
+
// Load existing annotations from store and render them on all rendered
|
|
7716
|
+
// pages. Skipped for encrypted documents, which are view-only.
|
|
7717
|
+
if (!this.readOnly) {
|
|
7718
|
+
await this.annotationManager.loadAnnotations();
|
|
7719
|
+
this._renderAnnotations();
|
|
7720
|
+
}
|
|
7547
7721
|
|
|
7548
7722
|
const annotations = this.annotationManager.getAllAnnotations();
|
|
7549
7723
|
this.container.dispatchEvent(new CustomEvent("pdf-viewer:annotations-loaded", {
|
|
@@ -7574,6 +7748,11 @@
|
|
|
7574
7748
|
}
|
|
7575
7749
|
|
|
7576
7750
|
setTool(mode) {
|
|
7751
|
+
// Encrypted documents are view-only: only the select tool is allowed
|
|
7752
|
+
if (this.readOnly && mode !== ToolMode.SELECT) {
|
|
7753
|
+
return
|
|
7754
|
+
}
|
|
7755
|
+
|
|
7577
7756
|
// Deactivate current tool
|
|
7578
7757
|
if (this.currentTool) {
|
|
7579
7758
|
this.currentTool.deactivate();
|
|
@@ -8576,10 +8755,15 @@
|
|
|
8576
8755
|
});
|
|
8577
8756
|
}
|
|
8578
8757
|
|
|
8579
|
-
// Download with annotations
|
|
8758
|
+
// Download with annotations (or the original file for encrypted documents,
|
|
8759
|
+
// which pdf-lib can't open to embed annotations)
|
|
8580
8760
|
async download() {
|
|
8581
8761
|
try {
|
|
8582
|
-
|
|
8762
|
+
if (this.readOnly) {
|
|
8763
|
+
await this.downloadManager.downloadOriginal();
|
|
8764
|
+
} else {
|
|
8765
|
+
await this.downloadManager.downloadWithAnnotations();
|
|
8766
|
+
}
|
|
8583
8767
|
} catch (error) {
|
|
8584
8768
|
console.error("Failed to download PDF:", error);
|
|
8585
8769
|
throw error
|
|
@@ -8608,6 +8792,7 @@
|
|
|
8608
8792
|
this.findController?.destroy();
|
|
8609
8793
|
this.findBar?.destroy();
|
|
8610
8794
|
this.colorPicker?.destroy();
|
|
8795
|
+
this.passwordPrompt?.destroy();
|
|
8611
8796
|
|
|
8612
8797
|
Object.values(this.tools || {}).forEach(tool => tool.destroy?.());
|
|
8613
8798
|
|
|
@@ -8677,7 +8862,9 @@
|
|
|
8677
8862
|
if (this.hasLoadingOverlayTarget) {
|
|
8678
8863
|
this.loadingOverlayTarget.classList.add("hidden");
|
|
8679
8864
|
}
|
|
8680
|
-
const
|
|
8865
|
+
const isPasswordError = /password/i.test(String(error?.message || "")) || /password/i.test(String(error?.name || ""));
|
|
8866
|
+
const message = this.errorMessageValue ||
|
|
8867
|
+
(isPasswordError ? "A password is required to open this document" : "Failed to load PDF document");
|
|
8681
8868
|
this._showError(message);
|
|
8682
8869
|
this.containerTarget.dispatchEvent(new CustomEvent("pdf-viewer:load-failed", {
|
|
8683
8870
|
bubbles: true,
|
|
@@ -8858,6 +9045,10 @@
|
|
|
8858
9045
|
}
|
|
8859
9046
|
|
|
8860
9047
|
_activateTool(toolName) {
|
|
9048
|
+
// View-only for encrypted documents; the buttons are disabled, but guard
|
|
9049
|
+
// against keyboard/programmatic activation too
|
|
9050
|
+
if (this._readOnly && toolName !== "select") return
|
|
9051
|
+
|
|
8861
9052
|
// Tool map for name -> mode conversion
|
|
8862
9053
|
const toolMap = {
|
|
8863
9054
|
select: ToolMode.SELECT,
|
|
@@ -9043,8 +9234,8 @@
|
|
|
9043
9234
|
_setupPageNavigationListeners() {
|
|
9044
9235
|
// Listen for ready event from PdfViewer
|
|
9045
9236
|
this._readyHandler = (e) => {
|
|
9046
|
-
const { pageCount, currentPage } = e.detail;
|
|
9047
|
-
this._onViewerReady(pageCount, currentPage);
|
|
9237
|
+
const { pageCount, currentPage, readOnly } = e.detail;
|
|
9238
|
+
this._onViewerReady(pageCount, currentPage, readOnly);
|
|
9048
9239
|
};
|
|
9049
9240
|
this.containerTarget.addEventListener("pdf-viewer:ready", this._readyHandler);
|
|
9050
9241
|
|
|
@@ -9056,12 +9247,16 @@
|
|
|
9056
9247
|
this.containerTarget.addEventListener("pdf-viewer:page-changed", this._pageChangedHandler);
|
|
9057
9248
|
}
|
|
9058
9249
|
|
|
9059
|
-
_onViewerReady(pageCount, currentPage) {
|
|
9250
|
+
_onViewerReady(pageCount, currentPage, readOnly) {
|
|
9060
9251
|
// Hide the loading overlay
|
|
9061
9252
|
if (this.hasLoadingOverlayTarget) {
|
|
9062
9253
|
this.loadingOverlayTarget.classList.add("hidden");
|
|
9063
9254
|
}
|
|
9064
9255
|
|
|
9256
|
+
if (readOnly) {
|
|
9257
|
+
this._enterReadOnlyMode();
|
|
9258
|
+
}
|
|
9259
|
+
|
|
9065
9260
|
if (this.hasPageCountTarget) {
|
|
9066
9261
|
this.pageCountTarget.textContent = pageCount;
|
|
9067
9262
|
}
|
|
@@ -9082,6 +9277,16 @@
|
|
|
9082
9277
|
this._setZoomPreset("auto");
|
|
9083
9278
|
}
|
|
9084
9279
|
|
|
9280
|
+
// Encrypted documents are view-only: disable the annotation tools and hide
|
|
9281
|
+
// the color picker. Download stays enabled but delivers the original file.
|
|
9282
|
+
_enterReadOnlyMode() {
|
|
9283
|
+
this._readOnly = true;
|
|
9284
|
+
this.containerTarget.classList.add("pdf-viewer-read-only");
|
|
9285
|
+
this.containerTarget
|
|
9286
|
+
.querySelectorAll('.pdf-tool-btn[data-tool]:not([data-tool="select"]), .pdf-overflow-tool-btn[data-tool]:not([data-tool="select"])')
|
|
9287
|
+
.forEach(btn => { btn.disabled = true; });
|
|
9288
|
+
}
|
|
9289
|
+
|
|
9085
9290
|
_onPageChanged(currentPage, pageCount) {
|
|
9086
9291
|
if (this.hasPageInputTarget && document.activeElement !== this.pageInputTarget) {
|
|
9087
9292
|
this.pageInputTarget.value = currentPage;
|
|
@@ -2467,6 +2467,112 @@
|
|
|
2467
2467
|
}
|
|
2468
2468
|
}
|
|
2469
2469
|
|
|
2470
|
+
// Password prompt for encrypted documents.
|
|
2471
|
+
// Sits above the loading overlay (z-index 100) and toasts (1001) so it is
|
|
2472
|
+
// reachable while the host app's spinner is still up.
|
|
2473
|
+
.pdf-password-prompt {
|
|
2474
|
+
position: absolute;
|
|
2475
|
+
top: 0;
|
|
2476
|
+
left: 0;
|
|
2477
|
+
right: 0;
|
|
2478
|
+
bottom: 0;
|
|
2479
|
+
display: flex;
|
|
2480
|
+
align-items: center;
|
|
2481
|
+
justify-content: center;
|
|
2482
|
+
background: rgba(0, 0, 0, 0.4);
|
|
2483
|
+
z-index: 1002;
|
|
2484
|
+
|
|
2485
|
+
&.hidden {
|
|
2486
|
+
display: none;
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
.pdf-password-prompt-dialog {
|
|
2491
|
+
width: min(360px, calc(100% - 32px));
|
|
2492
|
+
padding: 24px;
|
|
2493
|
+
background: #fff;
|
|
2494
|
+
border-radius: 8px;
|
|
2495
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
.pdf-password-prompt-title {
|
|
2499
|
+
margin: 0 0 8px;
|
|
2500
|
+
font-size: 16px;
|
|
2501
|
+
font-weight: 600;
|
|
2502
|
+
color: #333;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
.pdf-password-prompt-message {
|
|
2506
|
+
margin: 0 0 12px;
|
|
2507
|
+
font-size: 14px;
|
|
2508
|
+
color: #666;
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
.pdf-password-prompt-error {
|
|
2512
|
+
margin: 0 0 12px;
|
|
2513
|
+
font-size: 13px;
|
|
2514
|
+
color: #c62828;
|
|
2515
|
+
|
|
2516
|
+
&.hidden {
|
|
2517
|
+
display: none;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
.pdf-password-prompt-input {
|
|
2522
|
+
width: 100%;
|
|
2523
|
+
box-sizing: border-box;
|
|
2524
|
+
padding: 8px 10px;
|
|
2525
|
+
border: 1px solid #ccc;
|
|
2526
|
+
border-radius: 6px;
|
|
2527
|
+
font-size: 14px;
|
|
2528
|
+
|
|
2529
|
+
&:focus {
|
|
2530
|
+
outline: none;
|
|
2531
|
+
border-color: #0060df;
|
|
2532
|
+
box-shadow: 0 0 0 2px rgba(0, 96, 223, 0.2);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
.pdf-password-prompt-buttons {
|
|
2537
|
+
display: flex;
|
|
2538
|
+
justify-content: flex-end;
|
|
2539
|
+
gap: 8px;
|
|
2540
|
+
margin-top: 16px;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
.pdf-password-prompt-btn {
|
|
2544
|
+
padding: 6px 16px;
|
|
2545
|
+
border: 1px solid #ccc;
|
|
2546
|
+
border-radius: 6px;
|
|
2547
|
+
background: #fff;
|
|
2548
|
+
color: #333;
|
|
2549
|
+
font-size: 13px;
|
|
2550
|
+
font-weight: 500;
|
|
2551
|
+
cursor: pointer;
|
|
2552
|
+
|
|
2553
|
+
&:hover {
|
|
2554
|
+
background: #f5f5f5;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
&.pdf-password-prompt-submit {
|
|
2558
|
+
background: #0060df;
|
|
2559
|
+
border-color: #0060df;
|
|
2560
|
+
color: #fff;
|
|
2561
|
+
|
|
2562
|
+
&:hover {
|
|
2563
|
+
background: #0050bf;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// Read-only mode for encrypted documents: annotation tool buttons are
|
|
2569
|
+
// disabled in JS; the color picker has no use, so hide it entirely.
|
|
2570
|
+
.pdf-viewer-read-only {
|
|
2571
|
+
.pdf-toolbar-colors {
|
|
2572
|
+
display: none;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2470
2576
|
// Screen reader only class for aria-live regions
|
|
2471
2577
|
// Visually hidden but accessible to screen readers
|
|
2472
2578
|
.pdf-viewer-announcer {
|
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.
|
|
4
|
+
version: 0.5.0
|
|
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-08-
|
|
11
|
+
date: 2026-08-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|