feat: Add UI and logic to handle password-protected PDF documents.
This commit is contained in:
@@ -10,6 +10,7 @@ import React, {
|
|||||||
import type { CSSProperties, JSX, MutableRefObject, RefObject } from 'react';
|
import type { CSSProperties, JSX, MutableRefObject, RefObject } from 'react';
|
||||||
import {
|
import {
|
||||||
GlobalWorkerOptions,
|
GlobalWorkerOptions,
|
||||||
|
PasswordResponses,
|
||||||
getDocument,
|
getDocument,
|
||||||
} from 'pdfjs-dist';
|
} from 'pdfjs-dist';
|
||||||
import type {
|
import type {
|
||||||
@@ -106,6 +107,10 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
|||||||
velocity: 0,
|
velocity: 0,
|
||||||
});
|
});
|
||||||
const scrollElementRef = useRef<HTMLElement | null>(null);
|
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 requestQueueFlush = useCallback(() => {
|
||||||
const queue = renderQueueRef.current;
|
const queue = renderQueueRef.current;
|
||||||
@@ -283,6 +288,14 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
|||||||
url: src,
|
url: src,
|
||||||
wasmUrl,
|
wasmUrl,
|
||||||
});
|
});
|
||||||
|
loadingTask.onPassword = (callback, reason) => {
|
||||||
|
setIsEncrypted(true);
|
||||||
|
setPasswordCallback(() => callback);
|
||||||
|
if (reason === PasswordResponses.INCORRECT_PASSWORD) {
|
||||||
|
setPasswordError(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const pdf = await loadingTask.promise;
|
const pdf = await loadingTask.promise;
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
await pdf.destroy();
|
await pdf.destroy();
|
||||||
@@ -342,6 +355,13 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
|||||||
};
|
};
|
||||||
}, [ensureWasmUrl, renderWidth, src]);
|
}, [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]
|
const viewerClasses = ['document-viewer__object', 'document-viewer__object--pdf', className]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
@@ -553,6 +573,37 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={viewerClasses}>
|
<div className={viewerClasses}>
|
||||||
|
{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>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={`pdf-viewer__canvas-stack pdf-viewer__canvas-stack--${viewMode}`}
|
className={`pdf-viewer__canvas-stack pdf-viewer__canvas-stack--${viewMode}`}
|
||||||
@@ -569,6 +620,8 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -182,6 +182,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
grid-area: details;
|
grid-area: details;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-viewer__tabs-wrapper {
|
.document-viewer__tabs-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -564,11 +565,69 @@
|
|||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-viewer__summary-tab-content {
|
.document-viewer__summary-tab-content {
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-viewer__page-wrapper--contain .pdf-viewer__page {
|
.pdf-viewer__page-wrapper--contain .pdf-viewer__page {
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
max-height: 100vh;
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user