mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Improve annotations (#5919)
* Text box/notes movement improvements ✅ * Fix the issue where hiding, then showing annotations looses progress ✅ * Fix the issue where hidig/showing annotations jumps you back up to the top of your open document ✅ * Support ctrl+c and ctrl+v and backspace to delete ✅ * Better handling when moving to different tool from annotate ✅ * Added a color picker eyedropper button ✅ * Auto-switch to Select after note/text placement, so users can quickly place and type ✅
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { createPluginRegistration } from '@embedpdf/core';
|
||||
import type { PluginRegistry } from '@embedpdf/core';
|
||||
import { EmbedPDF } from '@embedpdf/core/react';
|
||||
import { usePdfiumEngine } from '@embedpdf/engines/react';
|
||||
import { PrivateContent } from '@app/components/shared/PrivateContent';
|
||||
@@ -24,7 +25,9 @@ import { AttachmentPluginPackage } from '@embedpdf/plugin-attachment/react';
|
||||
import { PrintPluginPackage } from '@embedpdf/plugin-print/react';
|
||||
import { HistoryPluginPackage } from '@embedpdf/plugin-history/react';
|
||||
import { AnnotationLayer, AnnotationPluginPackage } from '@embedpdf/plugin-annotation/react';
|
||||
import type { AnnotationTool, AnnotationEvent } from '@embedpdf/plugin-annotation';
|
||||
import { PdfAnnotationSubtype } from '@embedpdf/models';
|
||||
import type { PdfAnnotationObject, Rect } from '@embedpdf/models';
|
||||
import { RedactionPluginPackage, RedactionLayer } from '@embedpdf/plugin-redaction/react';
|
||||
import { CustomSearchLayer } from '@app/components/viewer/CustomSearchLayer';
|
||||
import { ZoomAPIBridge } from '@app/components/viewer/ZoomAPIBridge';
|
||||
@@ -68,7 +71,7 @@ interface LocalEmbedPDFProps {
|
||||
enableFormFill?: boolean;
|
||||
isManualRedactionMode?: boolean;
|
||||
showBakedAnnotations?: boolean;
|
||||
onSignatureAdded?: (annotation: any) => void;
|
||||
onSignatureAdded?: (annotation: PdfAnnotationObject) => void;
|
||||
signatureApiRef?: React.RefObject<SignatureAPI>;
|
||||
annotationApiRef?: React.RefObject<AnnotationAPI>;
|
||||
historyApiRef?: React.RefObject<HistoryAPI>;
|
||||
@@ -80,7 +83,7 @@ interface LocalEmbedPDFProps {
|
||||
export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false, enableRedaction = false, enableFormFill = false, isManualRedactionMode = false, showBakedAnnotations = true, onSignatureAdded, signatureApiRef, annotationApiRef, historyApiRef, redactionTrackerRef, fileId }: LocalEmbedPDFProps) {
|
||||
const { t } = useTranslation();
|
||||
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
||||
const [, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: any}>>([]);
|
||||
const [, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: Rect}>>([]);
|
||||
|
||||
// Convert File to URL if needed
|
||||
useEffect(() => {
|
||||
@@ -125,7 +128,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
createPluginRegistration(ScrollPluginPackage),
|
||||
createPluginRegistration(RenderPluginPackage, {
|
||||
withForms: !enableFormFill,
|
||||
withAnnotations: showBakedAnnotations && !enableAnnotations, // Show baked annotations only when: visibility is ON and annotation layer is OFF
|
||||
withAnnotations: !enableAnnotations, // Show baked annotations only when annotation layer is OFF; live layer visibility is controlled via CSS
|
||||
}),
|
||||
|
||||
// Register interaction manager (required for zoom and selection features)
|
||||
@@ -203,7 +206,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
// Register print plugin for printing PDFs
|
||||
createPluginRegistration(PrintPluginPackage),
|
||||
];
|
||||
}, [pdfUrl, enableAnnotations, showBakedAnnotations, fileName, file, url]);
|
||||
}, [pdfUrl, enableAnnotations, fileName, file, url]);
|
||||
|
||||
// Initialize the engine with the React hook - use local WASM for offline support
|
||||
const { engine, isLoading, error } = usePdfiumEngine({
|
||||
@@ -280,7 +283,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
<EmbedPDF
|
||||
engine={engine}
|
||||
plugins={plugins}
|
||||
onInitialized={async (registry: any) => {
|
||||
onInitialized={async (registry: PluginRegistry) => {
|
||||
// v2.0: Use registry.getPlugin() to access plugin APIs
|
||||
const annotationPlugin = registry.getPlugin('annotation');
|
||||
if (!annotationPlugin || !annotationPlugin.provides) return;
|
||||
@@ -289,10 +292,22 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
if (!annotationApi) return;
|
||||
|
||||
if (enableAnnotations) {
|
||||
const ensureTool = (tool: any) => {
|
||||
// LooseAnnotationTool bypasses strict Partial<T> defaults typing from the library —
|
||||
// EmbedPDF accepts extra runtime properties (borderWidth, textColor, finishOnDoubleClick,
|
||||
// etc.) that aren't reflected in the TypeScript model types.
|
||||
type LooseAnnotationTool = {
|
||||
id: string;
|
||||
name: string;
|
||||
interaction?: { exclusive: boolean; cursor: string; textSelection?: boolean; isRotatable?: boolean };
|
||||
matchScore?: (annotation: PdfAnnotationObject) => number;
|
||||
defaults?: Record<string, unknown>;
|
||||
clickBehavior?: Record<string, unknown>;
|
||||
behavior?: { deactivateToolAfterCreate?: boolean; selectAfterCreate?: boolean };
|
||||
};
|
||||
const ensureTool = (tool: LooseAnnotationTool) => {
|
||||
const existing = annotationApi.getTool?.(tool.id);
|
||||
if (!existing) {
|
||||
annotationApi.addTool(tool);
|
||||
annotationApi.addTool(tool as unknown as AnnotationTool);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -300,7 +315,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
id: 'highlight',
|
||||
name: 'Highlight',
|
||||
interaction: { exclusive: true, cursor: 'text', textSelection: true },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.HIGHLIGHT ? 10 : 0),
|
||||
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.HIGHLIGHT ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.HIGHLIGHT,
|
||||
strokeColor: '#ffd54f',
|
||||
@@ -317,7 +332,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
id: 'underline',
|
||||
name: 'Underline',
|
||||
interaction: { exclusive: true, cursor: 'text', textSelection: true },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.UNDERLINE ? 10 : 0),
|
||||
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.UNDERLINE ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.UNDERLINE,
|
||||
strokeColor: '#ffb300',
|
||||
@@ -334,7 +349,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
id: 'strikeout',
|
||||
name: 'Strikeout',
|
||||
interaction: { exclusive: true, cursor: 'text', textSelection: true },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.STRIKEOUT ? 10 : 0),
|
||||
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.STRIKEOUT ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.STRIKEOUT,
|
||||
strokeColor: '#e53935',
|
||||
@@ -351,7 +366,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
id: 'squiggly',
|
||||
name: 'Squiggly',
|
||||
interaction: { exclusive: true, cursor: 'text', textSelection: true },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.SQUIGGLY ? 10 : 0),
|
||||
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.SQUIGGLY ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.SQUIGGLY,
|
||||
strokeColor: '#00acc1',
|
||||
@@ -368,7 +383,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
id: 'ink',
|
||||
name: 'Pen',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.INK ? 10 : 0),
|
||||
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.INK ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.INK,
|
||||
strokeColor: '#1f2933',
|
||||
@@ -388,7 +403,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
id: 'inkHighlighter',
|
||||
name: 'Ink Highlighter',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.INK && (annotation.strokeColor === '#ffd54f' || annotation.color === '#ffd54f') ? 8 : 0),
|
||||
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.INK && (annotation.strokeColor === '#ffd54f' || annotation.color === '#ffd54f') ? 8 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.INK,
|
||||
strokeColor: '#ffd54f',
|
||||
@@ -408,7 +423,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
id: 'square',
|
||||
name: 'Square',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.SQUARE ? 10 : 0),
|
||||
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.SQUARE ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.SQUARE,
|
||||
color: '#0000ff', // fill color (blue)
|
||||
@@ -432,7 +447,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
id: 'circle',
|
||||
name: 'Circle',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.CIRCLE ? 10 : 0),
|
||||
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.CIRCLE ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.CIRCLE,
|
||||
color: '#0000ff', // fill color (blue)
|
||||
@@ -456,7 +471,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
id: 'line',
|
||||
name: 'Line',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.LINE ? 10 : 0),
|
||||
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.LINE ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.LINE,
|
||||
color: '#1565c0',
|
||||
@@ -480,7 +495,12 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
id: 'lineArrow',
|
||||
name: 'Arrow',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.LINE && (annotation.endStyle === 'ClosedArrow' || annotation.lineEndingStyles?.end === 'ClosedArrow') ? 9 : 0),
|
||||
matchScore: (annotation: PdfAnnotationObject) => {
|
||||
if (annotation.type !== PdfAnnotationSubtype.LINE) return 0;
|
||||
// EmbedPDF stores endStyle/lineEndingStyles at runtime; library types use lineEndings
|
||||
const ann = annotation as PdfAnnotationObject & { endStyle?: string; lineEndingStyles?: { end?: string } };
|
||||
return (ann.endStyle === 'ClosedArrow' || ann.lineEndingStyles?.end === 'ClosedArrow') ? 9 : 0;
|
||||
},
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.LINE,
|
||||
color: '#1565c0',
|
||||
@@ -505,7 +525,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
id: 'polyline',
|
||||
name: 'Polyline',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.POLYLINE ? 10 : 0),
|
||||
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.POLYLINE ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.POLYLINE,
|
||||
color: '#1565c0',
|
||||
@@ -526,7 +546,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
id: 'polygon',
|
||||
name: 'Polygon',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.POLYGON ? 10 : 0),
|
||||
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.POLYGON ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.POLYGON,
|
||||
color: '#0000ff', // fill color (blue)
|
||||
@@ -548,8 +568,8 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
ensureTool({
|
||||
id: 'text',
|
||||
name: 'Text',
|
||||
interaction: { exclusive: true, cursor: 'text' },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.FREETEXT ? 10 : 0),
|
||||
interaction: { exclusive: true, cursor: 'text', isRotatable: false },
|
||||
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.FREETEXT ? 10 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.FREETEXT,
|
||||
textColor: '#111111',
|
||||
@@ -560,7 +580,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
contents: 'Text',
|
||||
},
|
||||
behavior: {
|
||||
deactivateToolAfterCreate: false,
|
||||
deactivateToolAfterCreate: true,
|
||||
selectAfterCreate: true,
|
||||
},
|
||||
});
|
||||
@@ -568,8 +588,8 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
ensureTool({
|
||||
id: 'note',
|
||||
name: 'Note',
|
||||
interaction: { exclusive: true, cursor: 'pointer' },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.FREETEXT ? 8 : 0),
|
||||
interaction: { exclusive: true, cursor: 'pointer', isRotatable: false },
|
||||
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.FREETEXT ? 8 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.FREETEXT,
|
||||
textColor: '#1b1b1b',
|
||||
@@ -584,7 +604,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
defaultSize: { width: 160, height: 100 },
|
||||
},
|
||||
behavior: {
|
||||
deactivateToolAfterCreate: false,
|
||||
deactivateToolAfterCreate: true,
|
||||
selectAfterCreate: true,
|
||||
},
|
||||
});
|
||||
@@ -593,7 +613,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
id: 'stamp',
|
||||
name: 'Image Stamp',
|
||||
interaction: { exclusive: false, cursor: 'copy' },
|
||||
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.STAMP ? 5 : 0),
|
||||
matchScore: (annotation: PdfAnnotationObject) => (annotation.type === PdfAnnotationSubtype.STAMP ? 5 : 0),
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.STAMP,
|
||||
},
|
||||
@@ -627,7 +647,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
},
|
||||
});
|
||||
|
||||
annotationApi.onAnnotationEvent((event: any) => {
|
||||
annotationApi.onAnnotationEvent((event: AnnotationEvent) => {
|
||||
if (event.type === 'create' && event.committed) {
|
||||
setAnnotations(prev => [...prev, {
|
||||
id: event.annotation.id,
|
||||
@@ -635,19 +655,11 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
rect: event.annotation.rect
|
||||
}]);
|
||||
|
||||
|
||||
if (onSignatureAdded) {
|
||||
onSignatureAdded(event.annotation);
|
||||
}
|
||||
} else if (event.type === 'delete' && event.committed) {
|
||||
setAnnotations(prev => prev.filter(ann => ann.id !== event.annotation.id));
|
||||
} else if (event.type === 'loaded') {
|
||||
const loadedAnnotations = event.annotations || [];
|
||||
setAnnotations(loadedAnnotations.map((ann: any) => ({
|
||||
id: ann.id,
|
||||
pageIndex: ann.pageIndex || 0,
|
||||
rect: ann.rect
|
||||
})));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -750,8 +762,9 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
<AnnotationLayer
|
||||
documentId={documentId}
|
||||
pageIndex={pageIndex}
|
||||
selectionOutlineColor="#007ACC"
|
||||
selectionOutline={{ color: "#007ACC" }}
|
||||
selectionMenu={(props) => <AnnotationSelectionMenu {...props} />}
|
||||
style={!showBakedAnnotations ? { opacity: 0, pointerEvents: 'none' } : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user