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 React, { useRef, useState } from 'react';
import { useTranslation } from "react-i18next"; 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 ButtonSelector from "../../shared/ButtonSelector";
import { SignParameters } from "../../../hooks/tools/sign/useSignParameters"; import { SignParameters } from "../../../hooks/tools/sign/useSignParameters";
@ -12,9 +12,11 @@ interface SignSettingsProps {
onActivateSignaturePlacement?: () => void; onActivateSignaturePlacement?: () => void;
onDeactivateSignature?: () => void; onDeactivateSignature?: () => void;
onUpdateDrawSettings?: (color: string, size: number) => 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 { t } = useTranslation();
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const [isDrawing, setIsDrawing] = useState(false); const [isDrawing, setIsDrawing] = useState(false);
@ -432,35 +434,43 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv
return ( return (
<Stack gap="md"> <Stack gap="md">
{/* Signature Type Selection */} {/* Signature Type Selection */}
<div> <Tabs
<Text size="sm" fw={500} mb="xs">
{t('sign.type.title', 'Signature Type')}
</Text>
<ButtonSelector
value={parameters.signatureType} value={parameters.signatureType}
onChange={(value) => onParameterChange('signatureType', value as 'image' | 'text' | 'draw' | 'canvas')} onChange={(value) => onParameterChange('signatureType', value as 'image' | 'text' | 'draw' | 'canvas')}
options={[ >
{ <Tabs.List grow>
value: 'draw', <Tabs.Tab value="draw" style={{ fontSize: '0.8rem' }}>
label: t('sign.type.draw', 'Draw'), {t('sign.type.draw', 'Draw')}
}, </Tabs.Tab>
{ <Tabs.Tab value="canvas" style={{ fontSize: '0.8rem' }}>
value: 'canvas', {t('sign.type.canvas', 'Canvas')}
label: t('sign.type.canvas', 'Canvas'), </Tabs.Tab>
}, <Tabs.Tab value="image" style={{ fontSize: '0.8rem' }}>
{ {t('sign.type.image', 'Image')}
value: 'image', </Tabs.Tab>
label: t('sign.type.image', 'Image'), <Tabs.Tab value="text" style={{ fontSize: '0.8rem' }}>
}, {t('sign.type.text', 'Text')}
{ </Tabs.Tab>
value: 'text', </Tabs.List>
label: t('sign.type.text', 'Text'), </Tabs>
},
]}
disabled={disabled}
/>
</div>
{/* 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 */} {/* Signature Creation based on type */}
{parameters.signatureType === 'canvas' && ( {parameters.signatureType === 'canvas' && (
@ -782,9 +792,9 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv
{(parameters.signatureType === 'canvas' || parameters.signatureType === 'image' || parameters.signatureType === 'text') && ( {(parameters.signatureType === 'canvas' || parameters.signatureType === 'image' || parameters.signatureType === 'text') && (
<Alert color="blue" title={t('sign.instructions.title', 'How to add signature')}> <Alert color="blue" title={t('sign.instructions.title', 'How to add signature')}>
<Text size="sm"> <Text size="sm">
{parameters.signatureType === 'canvas' && 'Draw your signature in the canvas above. Placement mode will activate automatically when you finish drawing.'} {parameters.signatureType === 'canvas' && 'After drawing your signature in the canvas above, click anywhere on the PDF to place it.'}
{parameters.signatureType === 'image' && 'Upload your signature image above. Placement mode will activate automatically when the image is loaded.'} {parameters.signatureType === 'image' && 'After uploading your signature image above, click anywhere on the PDF to place it.'}
{parameters.signatureType === 'text' && 'Enter your name above. Placement mode will activate automatically when you type your name.'} {parameters.signatureType === 'text' && 'After entering your name above, click anywhere on the PDF to place your signature.'}
</Text> </Text>
</Alert> </Alert>
)} )}

View File

@ -40,7 +40,7 @@ const EmbedPdfViewerContent = ({
const isSignatureMode = selectedTool === 'sign'; const isSignatureMode = selectedTool === 'sign';
// Get signature context // Get signature context
const { signatureApiRef } = useSignature(); const { signatureApiRef, historyApiRef } = useSignature();
// Get current file from FileContext // Get current file from FileContext
@ -189,8 +189,17 @@ const EmbedPdfViewerContent = ({
url={effectiveFile.url} url={effectiveFile.url}
enableSignature={isSignatureMode} enableSignature={isSignatureMode}
signatureApiRef={signatureApiRef as React.RefObject<any>} signatureApiRef={signatureApiRef as React.RefObject<any>}
historyApiRef={historyApiRef as React.RefObject<any>}
onSignatureAdded={(annotation) => { onSignatureAdded={(annotation) => {
console.log('Signature added:', 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 // 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 { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
import { RotateAPIBridge } from './RotateAPIBridge'; import { RotateAPIBridge } from './RotateAPIBridge';
import { SignatureAPIBridge, SignatureAPI } from './SignatureAPIBridge'; import { SignatureAPIBridge, SignatureAPI } from './SignatureAPIBridge';
import { HistoryAPIBridge, HistoryAPI } from './HistoryAPIBridge';
interface LocalEmbedPDFProps { interface LocalEmbedPDFProps {
file?: File | Blob; file?: File | Blob;
@ -42,9 +43,10 @@ interface LocalEmbedPDFProps {
enableSignature?: boolean; enableSignature?: boolean;
onSignatureAdded?: (annotation: any) => void; onSignatureAdded?: (annotation: any) => void;
signatureApiRef?: React.RefObject<SignatureAPI>; 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 [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [annotations, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: any}>>([]); const [annotations, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: any}>>([]);
@ -257,6 +259,7 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
<ThumbnailAPIBridge /> <ThumbnailAPIBridge />
<RotateAPIBridge /> <RotateAPIBridge />
{enableSignature && <SignatureAPIBridge ref={signatureApiRef} />} {enableSignature && <SignatureAPIBridge ref={signatureApiRef} />}
{enableSignature && <HistoryAPIBridge ref={historyApiRef} />}
<GlobalPointerProvider> <GlobalPointerProvider>
<Viewport <Viewport
style={{ style={{

View File

@ -20,7 +20,8 @@ export interface SignatureAPIBridgeProps {}
export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgeProps>((props, ref) => { export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgeProps>((props, ref) => {
const { provides: annotationApi } = useAnnotationCapability(); const { provides: annotationApi } = useAnnotationCapability();
const { signatureConfig } = useSignature(); const { signatureConfig, storeImageData } = useSignature();
// Enable keyboard deletion of selected annotations // Enable keyboard deletion of selected annotations
useEffect(() => { useEffect(() => {
@ -31,15 +32,32 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
const selectedAnnotation = annotationApi.getSelectedAnnotation?.(); const selectedAnnotation = annotationApi.getSelectedAnnotation?.();
if (selectedAnnotation) { if (selectedAnnotation) {
const annotation = selectedAnnotation as any; // 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 (stillSelected) {
// EmbedPDF didn't handle it, so we need to delete manually
const annotation = stillSelected as any;
const pageIndex = annotation.object?.pageIndex || 0; const pageIndex = annotation.object?.pageIndex || 0;
const id = annotation.object?.id; const id = annotation.object?.id;
if (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); annotationApi.deleteAnnotation(pageIndex, id);
} }
} }
} }
}, 10);
}
}
}; };
document.addEventListener('keydown', handleKeyDown); document.addEventListener('keydown', handleKeyDown);
@ -50,7 +68,14 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => { addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => {
if (!annotationApi) return; 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, { annotationApi.createAnnotation(pageIndex, {
type: PdfAnnotationSubtype.STAMP, type: PdfAnnotationSubtype.STAMP,
rect: { rect: {
@ -60,8 +85,14 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
author: 'Digital Signature', author: 'Digital Signature',
subject: 'Digital Signature', subject: 'Digital Signature',
pageIndex: pageIndex, pageIndex: pageIndex,
id: uuidV4(), id: annotationId,
created: new Date(), 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, pageIndex: pageIndex,
id: uuidV4(), id: uuidV4(),
created: new Date(), created: new Date(),
customData: {
signatureText: text,
signatureType: 'text'
}
}); });
}, },
@ -235,6 +270,11 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
switch (params.signatureType) { switch (params.signatureType) {
case 'image': case 'image':
if (params.signatureData) { if (params.signatureData) {
const annotationId = uuidV4();
// Store image data in our persistent store
storeImageData(annotationId, params.signatureData);
annotationApi.createAnnotation(page, { annotationApi.createAnnotation(page, {
type: PdfAnnotationSubtype.STAMP, type: PdfAnnotationSubtype.STAMP,
rect: { rect: {
@ -244,8 +284,14 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI, SignatureAPIBridgePro
author: 'Digital Signature', author: 'Digital Signature',
subject: `Digital Signature - ${params.reason || 'Document signing'}`, subject: `Digital Signature - ${params.reason || 'Document signing'}`,
pageIndex: page, pageIndex: page,
id: uuidV4(), id: annotationId,
created: new Date(), 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 // 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, pageIndex: page,
id: uuidV4(), id: uuidV4(),
created: new Date(), created: new Date(),
customData: {
signatureText: params.signerName,
signatureType: 'text'
}
}); });
// Switch to select mode after placing signature so it can be easily deleted // 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 React, { createContext, useContext, useState, ReactNode, useCallback, useRef, useEffect } from 'react';
import { SignParameters } from '../hooks/tools/sign/useSignParameters'; import { SignParameters } from '../hooks/tools/sign/useSignParameters';
import { SignatureAPI } from '../components/viewer/SignatureAPIBridge'; import { SignatureAPI } from '../components/viewer/SignatureAPIBridge';
import { HistoryAPI } from '../components/viewer/HistoryAPIBridge';
// Signature state interface // Signature state interface
interface SignatureState { interface SignatureState {
@ -19,11 +20,16 @@ interface SignatureActions {
activateSignaturePlacementMode: () => void; activateSignaturePlacementMode: () => void;
activateDeleteMode: () => void; activateDeleteMode: () => void;
updateDrawSettings: (color: string, size: number) => 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 // Combined context interface
interface SignatureContextValue extends SignatureState, SignatureActions { interface SignatureContextValue extends SignatureState, SignatureActions {
signatureApiRef: React.RefObject<SignatureAPI | null>; signatureApiRef: React.RefObject<SignatureAPI | null>;
historyApiRef: React.RefObject<HistoryAPI | null>;
} }
// Create context // Create context
@ -39,6 +45,8 @@ const initialState: SignatureState = {
export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children }) => { export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [state, setState] = useState<SignatureState>(initialState); const [state, setState] = useState<SignatureState>(initialState);
const signatureApiRef = useRef<SignatureAPI>(null); const signatureApiRef = useRef<SignatureAPI>(null);
const historyApiRef = useRef<HistoryAPI>(null);
const imageDataStore = useRef<Map<string, string>>(new Map());
// Actions // Actions
const setSignatureConfig = useCallback((config: SignParameters | null) => { 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 // No auto-activation - all modes use manual buttons
const contextValue: SignatureContextValue = { const contextValue: SignatureContextValue = {
...state, ...state,
signatureApiRef, signatureApiRef,
historyApiRef,
setSignatureConfig, setSignatureConfig,
setPlacementMode, setPlacementMode,
activateDrawMode, activateDrawMode,
@ -113,6 +145,10 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children
activateSignaturePlacementMode, activateSignaturePlacementMode,
activateDeleteMode, activateDeleteMode,
updateDrawSettings, updateDrawSettings,
undo,
redo,
storeImageData,
getImageData,
}; };
return ( return (

View File

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