feat: Add UI and logic to handle password-protected PDF documents.

This commit is contained in:
2025-11-28 08:37:04 +01:00
parent 268488d93d
commit 0b4a9441d0
2 changed files with 129 additions and 17 deletions
+68 -15
View File
@@ -10,6 +10,7 @@ import React, {
import type { CSSProperties, JSX, MutableRefObject, RefObject } from 'react';
import {
GlobalWorkerOptions,
PasswordResponses,
getDocument,
} from 'pdfjs-dist';
import type {
@@ -106,6 +107,10 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
velocity: 0,
});
const scrollElementRef = useRef<HTMLElement | null>(null);
const [isEncrypted, setIsEncrypted] = useState(false);
const [passwordError, setPasswordError] = useState(false);
const [passwordCallback, setPasswordCallback] = useState<((password: string) => void) | null>(null);
const passwordInputRef = useRef<HTMLInputElement>(null);
const requestQueueFlush = useCallback(() => {
const queue = renderQueueRef.current;
@@ -283,6 +288,14 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
url: src,
wasmUrl,
});
loadingTask.onPassword = (callback, reason) => {
setIsEncrypted(true);
setPasswordCallback(() => callback);
if (reason === PasswordResponses.INCORRECT_PASSWORD) {
setPasswordError(true);
}
};
const pdf = await loadingTask.promise;
if (cancelled) {
await pdf.destroy();
@@ -342,6 +355,13 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
};
}, [ensureWasmUrl, renderWidth, src]);
const handlePasswordSubmit = useCallback((event: React.FormEvent) => {
event.preventDefault();
if (passwordCallback && passwordInputRef.current) {
passwordCallback(passwordInputRef.current.value);
}
}, [passwordCallback]);
const viewerClasses = ['document-viewer__object', 'document-viewer__object--pdf', className]
.filter(Boolean)
.join(' ');
@@ -553,22 +573,55 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
return (
<div className={viewerClasses}>
<div
ref={containerRef}
className={`pdf-viewer__canvas-stack pdf-viewer__canvas-stack--${viewMode}`}
role="document"
aria-label={title || 'PDF document'}
style={stackStyle}
>
{pageElements}
</div>
{showStatus ? (
<div className={statusClasses}>
<div className="document-viewer__message">
{statusMessage}
</div>
{isEncrypted && !pdfRef.current ? (
<div
className="pdf-viewer__password-container"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<form onSubmit={handlePasswordSubmit} className="pdf-viewer__password-form">
<div className="pdf-viewer__password-message">
This document is password protected.
</div>
<div className="pdf-viewer__password-input-group">
<input
ref={passwordInputRef}
type="password"
className="pdf-viewer__password-input"
placeholder="Enter password"
autoFocus
/>
<button type="submit" className="button button--primary">
Unlock
</button>
</div>
{passwordError ? (
<div className="pdf-viewer__password-error">
Incorrect password. Please try again.
</div>
) : null}
</form>
</div>
) : null}
) : (
<>
<div
ref={containerRef}
className={`pdf-viewer__canvas-stack pdf-viewer__canvas-stack--${viewMode}`}
role="document"
aria-label={title || 'PDF document'}
style={stackStyle}
>
{pageElements}
</div>
{showStatus ? (
<div className={statusClasses}>
<div className="document-viewer__message">
{statusMessage}
</div>
</div>
) : null}
</>
)}
</div>
);
};
+61 -2
View File
@@ -22,8 +22,8 @@
min-height: auto;
}
.document-viewer-panel__body > .document-viewer,
.document-viewer-panel__body > .document-viewer--loading {
.document-viewer-panel__body>.document-viewer,
.document-viewer-panel__body>.document-viewer--loading {
flex: 1;
}
@@ -182,6 +182,7 @@
flex: 1;
grid-area: details;
}
.document-viewer__tabs-wrapper {
display: flex;
flex-direction: column;
@@ -564,11 +565,69 @@
width: 1rem;
height: 1rem;
}
.document-viewer__summary-tab-content {
padding-top: 0.5rem;
}
.pdf-viewer__page-wrapper--contain .pdf-viewer__page {
width: auto;
max-width: 100vw;
max-height: 100vh;
}
.pdf-viewer__password-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
background: var(--surface-subtle);
}
.pdf-viewer__password-form {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 2rem;
background: var(--surface);
box-shadow: var(--shadow-pop);
max-width: 400px;
margin: 1rem;
box-sizing: border-box;
}
.pdf-viewer__password-message {
font-size: 1rem;
font-weight: 500;
color: var(--fg);
text-align: center;
}
.pdf-viewer__password-input-group {
display: flex;
gap: 0.5rem;
width: 100%;
box-sizing: border-box;
}
.pdf-viewer__password-input-group .button {
flex-shrink: 0;
}
.pdf-viewer__password-input {
flex: 1;
min-width: 0;
padding: 0.5rem;
border: 1px solid var(--outline);
border-radius: 0.25rem;
font-size: 1rem;
box-sizing: border-box;
}
.pdf-viewer__password-error {
color: var(--danger);
font-size: 0.9rem;
}