@kopjra/pdf-sentinel
v1.11.4
Published
A React and Preact component library for embedding and interacting with PDF documents using EmbedPDF.
Downloads
207
Maintainers
Readme
pdf-sentinel Documentation
A powerful React/Preact library for viewing and interacting with PDF forms. Built on top of embed-pdf-viewer, pdf-sentinel provides a complete solution for rendering PDF documents with form fields, custom toolbars, internationalization, annotation management, and signature handling.
Table of Contents
Installation
npm install @kopjra/pdf-sentinelQuick Start
import { usePdfSentinel } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";
function MyPdfViewer() {
const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
pdf: "/path/to/document.pdf",
name: "My Document",
usage: { type: "plain" },
});
return (
<div style={{ height: "100vh" }}>
<PdfSentinelContainer>
<Toolbar />
<PdfSentinel />
</PdfSentinelContainer>
</div>
);
}Features
Core Functionality
- 📄 PDF Viewing — Render PDF documents with zoom, scroll, and page navigation
- 📋 Form Fields — Support for text inputs, checkboxes, radio buttons, and signature fields
- ✍️ Form Validation — Built-in validation with error highlighting and auto-scroll to first error
- 🎨 Toolbar — Customizable toolbar with zoom, page navigation, undo/redo, and download
- 🌍 Internationalization — Multi-language support (en, it, es, fr, de) with custom overrides
- 🔧 Annotations — Full annotation management (add, edit, remove, select) with multiple field types
- 📝 Signatures — Native conditional signature support with automatic group coloring
- ↩️ Undo/Redo — History plugin support for annotation operations
- 📱 Pan Support — Touch device pan support with automatic detection
- ⬇️ PDF Download — Built-in download capability
Three Usage Modes
plain— View-only PDF renderingform— Interactive form filling with initial values, readonly fields, conditional signaturesannotation— Freeform annotation placement (email, fullname, phone, date, signature, custom)
API Reference
usePdfSentinel Hook
The main hook for initializing and controlling the PDF viewer.
Signature
function usePdfSentinel(options: PdfSentinelOptions): PdfSentinelResultOptions
interface PdfSentinelOptions {
/** PDF document URL or ArrayBuffer */
pdf: string | ArrayBuffer;
/** Document name/identifier */
name: string;
/** Locale code (en, it, es, fr, de) */
locale?: LocaleType;
/** Custom translations for any locale */
translations?: Record<string, Record<string, string>>;
/** Hide the default toolbar */
hideToolbar?: boolean;
/** Callback when document finishes loading */
onDocumentLoaded?: () => void;
/** Usage mode — determines how the PDF is rendered and interacted with */
usage: PlainUsage | FormUsage | AnnotationUsage;
}Usage Modes
PlainUsage
View-only rendering with no form or annotation interactivity.
interface PlainUsage {
type: "plain";
}FormUsage
Interactive form filling mode.
interface FormUsage {
type: "form";
/** Initial form field values keyed by field name */
initialValues?: InitialValuesType;
/** Conditional signature group configurations */
conditionalSignatures?: ConditionalSignature[];
/** Field names to treat as signature fields */
signatureFieldNames?: string[];
}AnnotationUsage
Freeform annotation placement mode.
interface AnnotationUsage {
type: "annotation";
/** Initial annotations to place, or "readFromDocument" to read existing ones */
initialAnnotations?: AnnotationField[] | "readFromDocument";
/** Whether to assign the user's email to created annotations */
assignEmailToAnnotations?: boolean;
/** Default size for newly created annotations */
defaultSize?: { width: number; height: number };
/** Default annotation field type for new annotations */
defaultType?: AnnotationFieldType;
/** Whether to auto-select annotations on creation */
selectOnCreation?: boolean;
}Return Value
interface PdfSentinelResult {
/** Container component that wraps the PDF viewer and toolbar */
PdfSentinelContainer: React.ComponentType<{ children: React.ReactNode }>;
/** Toolbar component — accepts optional toolbar customization props */
Toolbar: React.ComponentType<{
toolbarButtons?: ToolbarItem[];
removeButtons?: DefaultButtons[];
}>;
/** Main PDF viewer component */
PdfSentinel: React.ComponentType<{}>;
/** API access for form and annotation operations */
api: {
form: UsePdfFormsResult | null;
annotation: AnnotationsApi | null;
};
/** Current page number (1-indexed) */
currentPage: number | null;
/** Total number of pages */
totalPages: number | null;
/** Whether PDF is loading */
loadingPdf: boolean;
/** Whether there was a loading error */
loadingError: boolean;
/** The initialized PDF document buffer (available after load) */
initializedDocument: ArrayBuffer | null;
/** PDF engine instance */
engine: PdfEngine<Blob> | null;
/** Whether PDF has been closed */
closedPdf: boolean;
/** Helper methods for controlling the viewer programmatically */
helpers: {
zoomIn: () => void;
zoomOut: () => void;
resetZoom: () => void;
scrollToPage: (pageNumber: number) => void;
download: () => void;
};
}Types
InitialValuesType
type InitialValuesType = Record<string, {
value?: string | boolean;
readonly?: boolean;
}>;Initial form field values. Each key is a field name.
Example:
const initialValues: InitialValuesType = {
"fullName": { value: "John Doe", readonly: false },
"signature": { value: true, readonly: true },
"agreeToTerms": { value: true },
};ConditionalSignature
interface ConditionalSignature {
index: string;
fieldNames: string[];
}Defines a group of signature fields that are conditional on the same clause.
Example:
const conditionalSignatures: ConditionalSignature[] = [
{ index: "1", fieldNames: ["signature1", "signature2"] },
{ index: "2", fieldNames: ["signature3", "signature4"] },
];AnnotationFieldType
enum AnnotationFieldType {
EMAIL = "EMAIL",
FULLNAME = "FULLNAME",
PHONE = "PHONE",
DATE = "DATE",
SIGNATURE = "SIGNATURE",
CONDITIONAL_SIGNATURE = "CONDITIONAL_SIGNATURE",
CUSTOM = "CUSTOM",
}AnnotationField
interface AnnotationField {
type: AnnotationFieldType;
/** Unique annotation identifier */
id: string;
originalId?: string;
/** Display name */
name?: string;
/** Description (used for conditional signatures) */
description?: string;
/** Whether the annotation is required */
required?: boolean;
/** Page number (1-based) */
page: number;
/** Bounding rectangle in pixels */
rect: { x: number; y: number; width: number; height: number };
/** Assigned email address (if applicable) */
assignedEmail?: string;
}DefaultButtons
type DefaultButtons = "zoomIn" | "zoomOut" | "fitWidth" | "download" | "undo" | "redo";Used to selectively remove built-in toolbar buttons.
Form API
Returned via api.form when using FormUsage.
interface UsePdfFormsResult {
/** All form fields in the document */
fields: Field[];
/** Current field values keyed by field name */
values: Record<string, string | boolean>;
/** Set a field value */
setValue: (name: string, value: string | boolean) => void;
/** Validate all required fields — returns validity and errors */
validate: () => { valid: boolean; errors: Record<string, string> };
/** Reload form fields from the document */
reload: () => void;
/** Current validation errors (if any) */
errors?: Record<string, string>;
/** Whether errors should be displayed */
showErrors: boolean;
/** Scroll to a conditional signature group by index */
scrollToConditionalSignature: (index: string) => Generator;
}Field
interface Field {
name: string;
type: "text" | "checkbox" | "signature" | "radiobutton";
pageIndex: number;
bbox: { x: number; y: number; width: number; height: number };
radioValue?: string;
readonly?: boolean;
required?: boolean;
conditionalIndex?: string;
}Annotation API
Returned via api.annotation when using AnnotationUsage.
interface AnnotationsApi {
/** Get all current annotations */
getAnnotations: () => Promise<AnnotationField[]>;
/** Get annotation by ID */
getAnnotationById: (id: string) => AnnotationField | undefined;
/** Add a new annotation */
addAnnotation: (annotation: Partial<AnnotationField>) => void;
/** Update an existing annotation */
updateAnnotation: (annotation: AnnotationField) => void;
/** Remove annotations */
removeAnnotations: (annotations: AnnotationField[]) => void;
/** Get annotations for a specific page (1-based) */
getAnnotationsByPage: (page: number) => Promise<AnnotationField[]>;
/** Subscribe to annotation changes — returns unsubscribe function */
subscribe: (listener: (event: SubscribeEvent) => void) => () => void;
/** Select annotations by ID (null to deselect all) */
selectAnnotations: (ids: string[] | null) => void;
}Subscribe events:
// Fired on create, update, or delete
interface SubscribeEventModificationsOptions {
type: "update" | "create" | "delete";
annotations: AnnotationField[];
}
// Fired on selection change
interface SubscribeEventSelectionOptions {
type: "selection";
selectedAnnotationsId: string[] | null;
}Toolbar Customization
The Toolbar component accepts optional props for extending or trimming the default toolbar.
<Toolbar
toolbarButtons={customButtons}
removeButtons={["download", "redo"]}
/>ToolbarItem
type ToolbarItem = ToolbarButton | ToolbarSpacer;ToolbarButton
interface ToolbarButton {
id: string;
label?: string;
icon?: string; // Font Awesome icon class
title?: string; // Tooltip text
onClick: () => void;
area: "left" | "right";
disabled?: boolean;
type: "button";
primary?: boolean;
}ToolbarSpacer
interface ToolbarSpacer {
id: string;
type: "spacer";
area: "left" | "right";
}Internationalization
Supported Locales
en— English (default)it— Italianes— Spanishfr— Frenchde— German
Default Translation Keys
{
zoomIn: string;
zoomOut: string;
resetZoom: string;
download: string;
pageNumberTitle: string;
signature: string;
conditional1: string; // "By accepting clause %s"
conditional2: string; // "your signature will be placed here"
}Custom Translations
Override or add translations for any locale:
const translations = {
en: {
zoomIn: "Enlarge",
download: "Download PDF",
customKey: "Custom value",
},
it: {
customKey: "Valore personalizzato",
},
};Examples
Basic PDF Viewer (Plain Mode)
import { usePdfSentinel } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";
export function BasicViewer() {
const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
pdf: "https://example.com/document.pdf",
name: "Document",
usage: { type: "plain" },
});
return (
<div style={{ height: "600px" }}>
<PdfSentinelContainer>
<Toolbar />
<PdfSentinel />
</PdfSentinelContainer>
</div>
);
}PDF with Form Fields
import { usePdfSentinel } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";
export function FormViewer() {
const { PdfSentinelContainer, Toolbar, PdfSentinel, api } = usePdfSentinel({
pdf: "/document.pdf",
name: "Form Document",
usage: {
type: "form",
initialValues: {
"firstName": { value: "John" },
"lastName": { value: "Doe" },
},
},
});
const handleValidate = () => {
const result = api.form?.validate();
if (result?.valid) {
console.log("Form is valid, values:", api.form?.values);
} else {
console.log("Errors:", result?.errors);
}
};
return (
<>
<div style={{ height: "600px" }}>
<PdfSentinelContainer>
<Toolbar />
<PdfSentinel />
</PdfSentinelContainer>
</div>
<button onClick={handleValidate}>Validate</button>
</>
);
}Conditional Signatures
import { usePdfSentinel } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";
export function SignatureViewer() {
const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
pdf: "/contract.pdf",
name: "Contract",
usage: {
type: "form",
conditionalSignatures: [
{ index: "1", fieldNames: ["authorizedBy", "authorizedDate"] },
{ index: "2", fieldNames: ["approvedBy", "approvedDate"] },
],
},
});
return (
<div style={{ height: "600px" }}>
<PdfSentinelContainer>
<Toolbar />
<PdfSentinel />
</PdfSentinelContainer>
</div>
);
}Annotation Mode
import { usePdfSentinel, AnnotationFieldType } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";
export function AnnotationEditor() {
const { PdfSentinelContainer, Toolbar, PdfSentinel, api } = usePdfSentinel({
pdf: "/document.pdf",
name: "Annotate",
usage: {
type: "annotation",
initialAnnotations: "readFromDocument",
defaultType: AnnotationFieldType.SIGNATURE,
defaultSize: { width: 200, height: 60 },
selectOnCreation: true,
},
});
const handleExport = async () => {
const annotations = await api.annotation?.getAnnotations();
console.log("Annotations:", annotations);
};
// Subscribe to annotation changes
api.annotation?.subscribe((event) => {
if (event.type === "create") {
console.log("New annotations:", event.annotations);
}
});
return (
<>
<div style={{ height: "600px" }}>
<PdfSentinelContainer>
<Toolbar />
<PdfSentinel />
</PdfSentinelContainer>
</div>
<button onClick={handleExport}>Export Annotations</button>
</>
);
}Custom Toolbar Buttons
import { usePdfSentinel, ToolbarItem } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";
export function CustomToolbar() {
const customButtons: ToolbarItem[] = [
{
id: "print-btn",
label: "Print",
icon: "fal fa-print",
title: "Print document",
area: "left",
type: "button",
onClick: () => window.print(),
},
{
id: "spacer-1",
type: "spacer",
area: "left",
},
{
id: "share-btn",
label: "Share",
icon: "fal fa-share",
title: "Share document",
area: "right",
type: "button",
onClick: () => console.log("Share clicked"),
},
];
const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
pdf: "/document.pdf",
name: "Document",
usage: { type: "plain" },
});
return (
<div style={{ height: "600px" }}>
<PdfSentinelContainer>
<Toolbar
toolbarButtons={customButtons}
removeButtons={["undo", "redo"]}
/>
<PdfSentinel />
</PdfSentinelContainer>
</div>
);
}Multi-Language Support
import { usePdfSentinel } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";
import { useState } from "react";
export function MultiLanguageViewer() {
const [locale, setLocale] = useState<"en" | "it" | "es" | "fr" | "de">("en");
const customTranslations = {
en: { download: "Download PDF" },
it: { download: "Scarica PDF" },
};
const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
pdf: "/document.pdf",
name: "Document",
locale,
translations: customTranslations,
usage: { type: "plain" },
});
return (
<div>
<select onChange={(e) => setLocale(e.target.value as any)} value={locale}>
<option value="en">English</option>
<option value="it">Italiano</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
</select>
<div style={{ height: "600px", marginTop: "10px" }}>
<PdfSentinelContainer>
<Toolbar />
<PdfSentinel />
</PdfSentinelContainer>
</div>
</div>
);
}External Toolbar (Custom UI)
import { usePdfSentinel } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";
export function ExternalToolbarViewer() {
const {
PdfSentinelContainer,
PdfSentinel,
currentPage,
totalPages,
helpers,
} = usePdfSentinel({
pdf: "/document.pdf",
name: "Document",
hideToolbar: true,
usage: { type: "plain" },
});
return (
<div>
{/* Custom toolbar outside the container */}
<div style={{ display: "flex", gap: "8px", padding: "8px" }}>
<button onClick={helpers.zoomIn}>+</button>
<button onClick={helpers.zoomOut}>−</button>
<button onClick={helpers.resetZoom}>Fit</button>
<span>{currentPage} / {totalPages}</span>
<button onClick={helpers.download}>Download</button>
</div>
<div style={{ height: "600px" }}>
<PdfSentinelContainer>
<PdfSentinel />
</PdfSentinelContainer>
</div>
</div>
);
}Using React and Preact Variants
To use the Preact variant, import from @kopjra/pdf-sentinel/preact instead of @kopjra/pdf-sentinel.
The React variant is the default; you can also use @kopjra/pdf-sentinel/react for clarity.
// React variant (default)
import { usePdfSentinel } from "@kopjra/pdf-sentinel";
// React variant (explicit)
import { usePdfSentinel } from "@kopjra/pdf-sentinel/react";
// Preact variant
import { usePdfSentinel } from "@kopjra/pdf-sentinel/preact";The same code works for both React and Preact — the library automatically uses the correct framework variant based on the import path.
Browser Support
- Chrome/Chromium (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
Contributing
Contributions are welcome! Please ensure:
- TypeScript types are correctly defined
- All code follows the project style guide
npm run typecheckpasses- Changes are tested in both React and Preact variants
License
ISC
