Add undo/redo functionality and refactor signature settings UI

- Introduced HistoryAPIBridge for managing undo/redo actions.
- Updated SignSettings component to include undo/redo buttons.
- Refactored signature type selection to use Tabs for better UI.
- Enhanced SignatureAPIBridge to store image data for annotations.
- Integrated history management into SignatureContext for state handling.
This commit is contained in:
Reece Browne 2025-09-24 14:58:10 +01:00
parent bac61c7e9e
commit a12e457577
7 changed files with 242 additions and 46 deletions

View File

@ -1,6 +1,6 @@
import React, { useRef, useState } from 'react';
import { useTranslation } from "react-i18next";
import { Stack, TextInput, FileInput, Paper, Group, Button, Text, Alert, Modal, ColorSwatch, Menu, ActionIcon, Slider, Select, Combobox, useCombobox, ColorPicker } from '@mantine/core';
import { Stack, TextInput, FileInput, Paper, Group, Button, Text, Alert, Modal, ColorSwatch, Menu, ActionIcon, Slider, Select, Combobox, useCombobox, ColorPicker, Tabs } from '@mantine/core';
import ButtonSelector from "../../shared/ButtonSelector";
import { SignParameters } from "../../../hooks/tools/sign/useSignParameters";
@ -12,9 +12,11 @@ interface SignSettingsProps {
onActivateSignaturePlacement?: () => void;
onDeactivateSignature?: () => void;
onUpdateDrawSettings?: (color: string, size: number) => void;
onUndo?: () => void;
onRedo?: () => void;
}
const SignSettings = ({ parameters, onParameterChange, disabled = false, onActivateDrawMode, onActivateSignaturePlacement, onDeactivateSignature, onUpdateDrawSettings }: SignSettingsProps) => {
const SignSettings = ({ parameters, onParameterChange, disabled = false, onActivateDrawMode, onActivateSignaturePlacement, onDeactivateSignature, onUpdateDrawSettings, onUndo, onRedo }: SignSettingsProps) => {
const { t } = useTranslation();
const canvasRef = useRef<HTMLCanvasElement>(null);
const [isDrawing, setIsDrawing] = useState(false);
@ -432,35 +434,43 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv
return (
<Stack gap="md">
{/* Signature Type Selection */}
<div>
<Text size="sm" fw={500} mb="xs">
{t('sign.type.title', 'Signature Type')}
</Text>
<ButtonSelector
value={parameters.signatureType}
onChange={(value) => onParameterChange('signatureType', value as 'image' | 'text' | 'draw' | 'canvas')}
options={[
{
value: 'draw',
label: t('sign.type.draw', 'Draw'),
},
{
value: 'canvas',
label: t('sign.type.canvas', 'Canvas'),
},
{
value: 'image',
label: t('sign.type.image', 'Image'),
},
{
value: 'text',
label: t('sign.type.text', 'Text'),
},
]}
disabled={disabled}
/>
</div>
<Tabs
value={parameters.signatureType}
onChange={(value) => onParameterChange('signatureType', value as 'image' | 'text' | 'draw' | 'canvas')}
>
<Tabs.List grow>
<Tabs.Tab value="draw" style={{ fontSize: '0.8rem' }}>
{t('sign.type.draw', 'Draw')}
</Tabs.Tab>
<Tabs.Tab value="canvas" style={{ fontSize: '0.8rem' }}>
{t('sign.type.canvas', 'Canvas')}
</Tabs.Tab>
<Tabs.Tab value="image" style={{ fontSize: '0.8rem' }}>
{t('sign.type.image', 'Image')}
</Tabs.Tab>
<Tabs.Tab value="text" style={{ fontSize: '0.8rem' }}>
{t('sign.type.text', 'Text')}
</Tabs.Tab>
</Tabs.List>
</Tabs>
{/* Undo/Redo Controls */}
<Group justify="space-between" grow>
<Button
variant="outline"
onClick={onUndo}
disabled={disabled}
>
{t('sign.undo', 'Undo')}
</Button>
<Button
variant="outline"
onClick={onRedo}
disabled={disabled}
>
{t('sign.redo', 'Redo')}
</Button>
</Group>
{/* Signature Creation based on type */}
{parameters.signatureType === 'canvas' && (
@ -782,9 +792,9 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv
{(parameters.signatureType === 'canvas' || parameters.signatureType === 'image' || parameters.signatureType === 'text') && (
<Alert color="blue" title={t('sign.instructions.title', 'How to add signature')}>
<Text size="sm">
{parameters.signatureType === 'canvas' && 'Draw your signature in the canvas above. Placement mode will activate automatically when you finish drawing.'}
{parameters.signatureType === 'image' && 'Upload your signature image above. Placement mode will activate automatically when the image is loaded.'}
{parameters.signatureType === 'text' && 'Enter your name above. Placement mode will activate automatically when you type your name.'}
{parameters.signatureType === 'canvas' && 'After drawing your signature in the canvas above, click anywhere on the PDF to place it.'}
{parameters.signatureType === 'image' && 'After uploading your signature image above, click anywhere on the PDF to place it.'}
{parameters.signatureType === 'text' && 'After entering your name above, click anywhere on the PDF to place your signature.'}
</Text>
</Alert>
)}

View File

@ -40,7 +40,7 @@ const EmbedPdfViewerContent = ({
const isSignatureMode = selectedTool === 'sign';
// Get signature context
const { signatureApiRef } = useSignature();
const { signatureApiRef, historyApiRef } = useSignature();
// Get current file from FileContext
@ -189,8 +189,17 @@ const EmbedPdfViewerContent = ({
url={effectiveFile.url}
enableSignature={isSignatureMode}
signatureApiRef={signatureApiRef as React.RefObject<any>}
historyApiRef={historyApiRef as React.RefObject<any>}
onSignatureAdded={(annotation) => {
console.log('Signature added:', annotation);
if (annotation.type === 13) {
console.log('- imageSrc:', !!annotation.imageSrc, annotation.imageSrc?.length);
console.log('- contents:', !!annotation.contents, annotation.contents?.length);
console.log('- data:', !!annotation.data, annotation.data?.length);
console.log('- imageData:', !!annotation.imageData, annotation.imageData?.length);
console.log('- appearance:', !!annotation.appearance, typeof annotation.appearance);
console.log('- All keys:', Object.keys(annotation));
}
// Future: Handle signature completion
}}
/>

View File

@ -0,0 +1,86 @@
import React, { useImperativeHandle, forwardRef, useEffect } from 'react';
import { useHistoryCapability } from '@embedpdf/plugin-history/react';
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
import { useSignature } from '../../contexts/SignatureContext';
export interface HistoryAPI {
undo: () => void;
redo: () => void;
canUndo: () => boolean;
canRedo: () => boolean;
}
export interface HistoryAPIBridgeProps {}
export const HistoryAPIBridge = forwardRef<HistoryAPI, HistoryAPIBridgeProps>((props, ref) => {
const { provides: historyApi } = useHistoryCapability();
const { provides: annotationApi } = useAnnotationCapability();
const { getImageData } = useSignature();
useImperativeHandle(ref, () => ({
undo: () => {
if (historyApi) {
historyApi.undo();
// Restore image data for STAMP annotations after undo
setTimeout(() => {
if (!annotationApi) return;
for (let pageIndex = 0; pageIndex < 10; pageIndex++) {
const pageAnnotationsTask = annotationApi.getPageAnnotations?.({ pageIndex });
if (pageAnnotationsTask) {
pageAnnotationsTask.toPromise().then((pageAnnotations: any) => {
if (pageAnnotations) {
pageAnnotations.forEach((ann: any) => {
if (ann.type === 13) {
const storedImageData = getImageData(ann.id);
if (storedImageData && (!ann.imageSrc || ann.imageSrc !== storedImageData)) {
const originalData = {
type: ann.type,
rect: ann.rect,
author: ann.author || 'Digital Signature',
subject: ann.subject || 'Digital Signature',
pageIndex: pageIndex,
id: ann.id,
created: ann.created || new Date(),
imageSrc: storedImageData
};
annotationApi.deleteAnnotation(pageIndex, ann.id);
setTimeout(() => {
annotationApi.createAnnotation(pageIndex, originalData);
}, 50);
}
}
});
}
}).catch((error: any) => {
console.error(`Failed to get annotations for page ${pageIndex}:`, error);
});
}
}
}, 200);
}
},
redo: () => {
if (historyApi) {
historyApi.redo();
}
},
canUndo: () => {
return historyApi ? historyApi.canUndo() : false;
},
canRedo: () => {
return historyApi ? historyApi.canRedo() : false;
},
}), [historyApi]);
return null; // This is a bridge component with no UI
});
HistoryAPIBridge.displayName = 'HistoryAPIBridge';

View File

@ -35,6 +35,7 @@ import { SearchAPIBridge } from './SearchAPIBridge';
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
import { RotateAPIBridge } from './RotateAPIBridge';
import { SignatureAPIBridge, SignatureAPI } from './SignatureAPIBridge';
import { HistoryAPIBridge, HistoryAPI } from './HistoryAPIBridge';
interface LocalEmbedPDFProps {
file?: File | Blob;
@ -42,9 +43,10 @@ interface LocalEmbedPDFProps {
enableSignature?: boolean;
onSignatureAdded?: (annotation: any) => void;
signatureApiRef?: React.RefObject<SignatureAPI>;
historyApiRef?: React.RefObject<HistoryAPI>;
}
export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureAdded, signatureApiRef }: LocalEmbedPDFProps) {
export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) {
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [annotations, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: any}>>([]);
@ -257,6 +259,7 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
<ThumbnailAPIBridge />
<RotateAPIBridge />
{enableSignature && <SignatureAPIBridge ref={signatureApiRef} />}
{enableSignature && <HistoryAPIBridge ref={historyApiRef} />}
<GlobalPointerProvider>
<Viewport
style={{

View File

@ -20,7 +20,8 @@ export interface SignatureAPIBridgeProps {}
export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgeProps>((props, ref) => {
const { provides: annotationApi } = useAnnotationCapability();
const { signatureConfig } = useSignature();
const { signatureConfig, storeImageData } = useSignature();
// Enable keyboard deletion of selected annotations
useEffect(() => {
@ -31,13 +32,30 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
const selectedAnnotation = annotationApi.getSelectedAnnotation?.();
if (selectedAnnotation) {
const annotation = selectedAnnotation as any;
const pageIndex = annotation.object?.pageIndex || 0;
const id = annotation.object?.id;
// First, let EmbedPDF try to handle this natively
// Only intervene if needed
setTimeout(() => {
// Check if the annotation is still selected after a brief delay
// If EmbedPDF handled it natively, it should be gone
const stillSelected = annotationApi.getSelectedAnnotation?.();
if (id) {
annotationApi.deleteAnnotation(pageIndex, id);
}
if (stillSelected) {
// EmbedPDF didn't handle it, so we need to delete manually
const annotation = stillSelected as any;
const pageIndex = annotation.object?.pageIndex || 0;
const id = annotation.object?.id;
if (id) {
// Try deleteSelected method first (should integrate with history)
if ((annotationApi as any).deleteSelected) {
(annotationApi as any).deleteSelected();
} else {
// Fallback to direct deletion
annotationApi.deleteAnnotation(pageIndex, id);
}
}
}
}, 10);
}
}
};
@ -50,7 +68,14 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => {
if (!annotationApi) return;
// Create image stamp annotation
// Create image stamp annotation with proper image data
console.log('Creating image annotation with data length:', signatureData?.length);
const annotationId = uuidV4();
// Store image data in our persistent store
storeImageData(annotationId, signatureData);
annotationApi.createAnnotation(pageIndex, {
type: PdfAnnotationSubtype.STAMP,
rect: {
@ -60,8 +85,14 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
author: 'Digital Signature',
subject: 'Digital Signature',
pageIndex: pageIndex,
id: uuidV4(),
id: annotationId,
created: new Date(),
// Store image data in multiple places to ensure history captures it
imageSrc: signatureData,
contents: signatureData, // Some annotation systems use contents
data: signatureData, // Try data field
imageData: signatureData, // Try imageData field
appearance: signatureData // Try appearance field
});
},
@ -86,6 +117,10 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
pageIndex: pageIndex,
id: uuidV4(),
created: new Date(),
customData: {
signatureText: text,
signatureType: 'text'
}
});
},
@ -235,6 +270,11 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
switch (params.signatureType) {
case 'image':
if (params.signatureData) {
const annotationId = uuidV4();
// Store image data in our persistent store
storeImageData(annotationId, params.signatureData);
annotationApi.createAnnotation(page, {
type: PdfAnnotationSubtype.STAMP,
rect: {
@ -244,8 +284,14 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
author: 'Digital Signature',
subject: `Digital Signature - ${params.reason || 'Document signing'}`,
pageIndex: page,
id: uuidV4(),
id: annotationId,
created: new Date(),
// Store image data in multiple places to ensure history captures it
imageSrc: params.signatureData,
contents: params.signatureData, // Some annotation systems use contents
data: params.signatureData, // Try data field
imageData: params.signatureData, // Try imageData field
appearance: params.signatureData // Try appearance field
});
// Switch to select mode after placing signature so it can be easily deleted
@ -274,6 +320,10 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
pageIndex: page,
id: uuidV4(),
created: new Date(),
customData: {
signatureText: params.signerName,
signatureType: 'text'
}
});
// Switch to select mode after placing signature so it can be easily deleted

View File

@ -1,6 +1,7 @@
import React, { createContext, useContext, useState, ReactNode, useCallback, useRef, useEffect } from 'react';
import { SignParameters } from '../hooks/tools/sign/useSignParameters';
import { SignatureAPI } from '../components/viewer/SignatureAPIBridge';
import { HistoryAPI } from '../components/viewer/HistoryAPIBridge';
// Signature state interface
interface SignatureState {
@ -19,11 +20,16 @@ interface SignatureActions {
activateSignaturePlacementMode: () => void;
activateDeleteMode: () => void;
updateDrawSettings: (color: string, size: number) => void;
undo: () => void;
redo: () => void;
storeImageData: (id: string, data: string) => void;
getImageData: (id: string) => string | undefined;
}
// Combined context interface
interface SignatureContextValue extends SignatureState, SignatureActions {
signatureApiRef: React.RefObject<SignatureAPI | null>;
historyApiRef: React.RefObject<HistoryAPI | null>;
}
// Create context
@ -39,6 +45,8 @@ const initialState: SignatureState = {
export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [state, setState] = useState<SignatureState>(initialState);
const signatureApiRef = useRef<SignatureAPI>(null);
const historyApiRef = useRef<HistoryAPI>(null);
const imageDataStore = useRef<Map<string, string>>(new Map());
// Actions
const setSignatureConfig = useCallback((config: SignParameters | null) => {
@ -100,12 +108,36 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children
}
}, []);
const undo = useCallback(() => {
if (historyApiRef.current) {
historyApiRef.current.undo();
}
}, []);
const redo = useCallback(() => {
if (historyApiRef.current) {
historyApiRef.current.redo();
}
}, []);
const storeImageData = useCallback((id: string, data: string) => {
console.log('Storing image data for annotation:', id, data.length, 'chars');
imageDataStore.current.set(id, data);
}, []);
const getImageData = useCallback((id: string) => {
const data = imageDataStore.current.get(id);
console.log('Retrieving image data for annotation:', id, data?.length, 'chars');
return data;
}, []);
// No auto-activation - all modes use manual buttons
const contextValue: SignatureContextValue = {
...state,
signatureApiRef,
historyApiRef,
setSignatureConfig,
setPlacementMode,
activateDrawMode,
@ -113,6 +145,10 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children
activateSignaturePlacementMode,
activateDeleteMode,
updateDrawSettings,
undo,
redo,
storeImageData,
getImageData,
};
return (

View File

@ -12,7 +12,7 @@ import { useSignature } from "../contexts/SignatureContext";
const Sign = (props: BaseToolProps) => {
const { t } = useTranslation();
const { setWorkbench } = useNavigation();
const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode, updateDrawSettings } = useSignature();
const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode, updateDrawSettings, undo, redo } = useSignature();
// Manual sync function
const syncSignatureConfig = () => {
@ -65,6 +65,8 @@ const Sign = (props: BaseToolProps) => {
onActivateSignaturePlacement={handleSignaturePlacement}
onDeactivateSignature={deactivateDrawMode}
onUpdateDrawSettings={updateDrawSettings}
onUndo={undo}
onRedo={redo}
/>
),
});