mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
Restructure and bug fix
This commit is contained in:
parent
aa5333dcd9
commit
c94ee388fc
@ -0,0 +1,91 @@
|
|||||||
|
import React, { createContext, useContext, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface PDFAnnotationContextValue {
|
||||||
|
// Drawing mode management
|
||||||
|
activateDrawMode: () => void;
|
||||||
|
deactivateDrawMode: () => void;
|
||||||
|
activateSignaturePlacementMode: () => void;
|
||||||
|
activateDeleteMode: () => void;
|
||||||
|
|
||||||
|
// Drawing settings
|
||||||
|
updateDrawSettings: (color: string, size: number) => void;
|
||||||
|
|
||||||
|
// History operations
|
||||||
|
undo: () => void;
|
||||||
|
redo: () => void;
|
||||||
|
|
||||||
|
// Image data management
|
||||||
|
storeImageData: (id: string, data: string) => void;
|
||||||
|
getImageData: (id: string) => string | undefined;
|
||||||
|
|
||||||
|
// Placement state
|
||||||
|
isPlacementMode: boolean;
|
||||||
|
|
||||||
|
// Signature configuration
|
||||||
|
signatureConfig: any | null;
|
||||||
|
setSignatureConfig: (config: any | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PDFAnnotationContext = createContext<PDFAnnotationContextValue | undefined>(undefined);
|
||||||
|
|
||||||
|
interface PDFAnnotationProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
// These would come from the signature context
|
||||||
|
activateDrawMode: () => void;
|
||||||
|
deactivateDrawMode: () => void;
|
||||||
|
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;
|
||||||
|
isPlacementMode: boolean;
|
||||||
|
signatureConfig: any | null;
|
||||||
|
setSignatureConfig: (config: any | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PDFAnnotationProvider: React.FC<PDFAnnotationProviderProps> = ({
|
||||||
|
children,
|
||||||
|
activateDrawMode,
|
||||||
|
deactivateDrawMode,
|
||||||
|
activateSignaturePlacementMode,
|
||||||
|
activateDeleteMode,
|
||||||
|
updateDrawSettings,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
storeImageData,
|
||||||
|
getImageData,
|
||||||
|
isPlacementMode,
|
||||||
|
signatureConfig,
|
||||||
|
setSignatureConfig
|
||||||
|
}) => {
|
||||||
|
const contextValue: PDFAnnotationContextValue = {
|
||||||
|
activateDrawMode,
|
||||||
|
deactivateDrawMode,
|
||||||
|
activateSignaturePlacementMode,
|
||||||
|
activateDeleteMode,
|
||||||
|
updateDrawSettings,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
storeImageData,
|
||||||
|
getImageData,
|
||||||
|
isPlacementMode,
|
||||||
|
signatureConfig,
|
||||||
|
setSignatureConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PDFAnnotationContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</PDFAnnotationContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePDFAnnotation = (): PDFAnnotationContextValue => {
|
||||||
|
const context = useContext(PDFAnnotationContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('usePDFAnnotation must be used within a PDFAnnotationProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
@ -0,0 +1,91 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Stack, Tabs, Alert, Text } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { DrawingControls } from './DrawingControls';
|
||||||
|
import { ColorPicker } from './ColorPicker';
|
||||||
|
import { usePDFAnnotation } from '../providers/PDFAnnotationProvider';
|
||||||
|
|
||||||
|
export interface AnnotationToolConfig {
|
||||||
|
enableDrawing?: boolean;
|
||||||
|
enableImageUpload?: boolean;
|
||||||
|
enableTextInput?: boolean;
|
||||||
|
showPlaceButton?: boolean;
|
||||||
|
placeButtonText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseAnnotationToolProps {
|
||||||
|
config: AnnotationToolConfig;
|
||||||
|
children: React.ReactNode;
|
||||||
|
onSignatureDataChange?: (data: string | null) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BaseAnnotationTool: React.FC<BaseAnnotationToolProps> = ({
|
||||||
|
config,
|
||||||
|
children,
|
||||||
|
onSignatureDataChange,
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
activateSignaturePlacementMode,
|
||||||
|
deactivateDrawMode,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
isPlacementMode
|
||||||
|
} = usePDFAnnotation();
|
||||||
|
|
||||||
|
const [selectedColor, setSelectedColor] = useState('#000000');
|
||||||
|
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||||
|
const [signatureData, setSignatureData] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleSignatureDataChange = (data: string | null) => {
|
||||||
|
setSignatureData(data);
|
||||||
|
onSignatureDataChange?.(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlaceSignature = () => {
|
||||||
|
if (activateSignaturePlacementMode) {
|
||||||
|
activateSignaturePlacementMode();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Drawing Controls (Undo/Redo/Place) */}
|
||||||
|
<DrawingControls
|
||||||
|
onUndo={undo}
|
||||||
|
onRedo={redo}
|
||||||
|
onPlaceSignature={config.showPlaceButton ? handlePlaceSignature : undefined}
|
||||||
|
hasSignatureData={!!signatureData}
|
||||||
|
disabled={disabled}
|
||||||
|
showPlaceButton={config.showPlaceButton}
|
||||||
|
placeButtonText={config.placeButtonText}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tool Content */}
|
||||||
|
{React.cloneElement(children as React.ReactElement<any>, {
|
||||||
|
selectedColor,
|
||||||
|
signatureData,
|
||||||
|
onSignatureDataChange: handleSignatureDataChange,
|
||||||
|
onColorSwatchClick: () => setIsColorPickerOpen(true),
|
||||||
|
disabled
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Instructions for placing signature */}
|
||||||
|
<Alert color="blue" title={t('sign.instructions.title', 'How to add signature')}>
|
||||||
|
<Text size="sm">
|
||||||
|
Click anywhere on the PDF to place your annotation.
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{/* Color Picker Modal */}
|
||||||
|
<ColorPicker
|
||||||
|
isOpen={isColorPickerOpen}
|
||||||
|
onClose={() => setIsColorPickerOpen(false)}
|
||||||
|
selectedColor={selectedColor}
|
||||||
|
onColorChange={setSelectedColor}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
67
frontend/src/components/annotation/shared/ColorPicker.tsx
Normal file
67
frontend/src/components/annotation/shared/ColorPicker.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Modal, Stack, ColorPicker as MantineColorPicker, Group, Button, ColorSwatch } from '@mantine/core';
|
||||||
|
|
||||||
|
interface ColorPickerProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
selectedColor: string;
|
||||||
|
onColorChange: (color: string) => void;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ColorPicker: React.FC<ColorPickerProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
selectedColor,
|
||||||
|
onColorChange,
|
||||||
|
title = "Choose Color"
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
title={title}
|
||||||
|
size="sm"
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<MantineColorPicker
|
||||||
|
format="hex"
|
||||||
|
value={selectedColor}
|
||||||
|
onChange={onColorChange}
|
||||||
|
swatches={['#000000', '#0066cc', '#cc0000', '#cc6600', '#009900', '#6600cc']}
|
||||||
|
swatchesPerRow={6}
|
||||||
|
size="lg"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<Button onClick={onClose}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ColorSwatchButtonProps {
|
||||||
|
color: string;
|
||||||
|
onClick: () => void;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ColorSwatchButton: React.FC<ColorSwatchButtonProps> = ({
|
||||||
|
color,
|
||||||
|
onClick,
|
||||||
|
size = 24
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ColorSwatch
|
||||||
|
color={color}
|
||||||
|
size={size}
|
||||||
|
radius={0}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
443
frontend/src/components/annotation/shared/DrawingCanvas.tsx
Normal file
443
frontend/src/components/annotation/shared/DrawingCanvas.tsx
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
import React, { useRef, useState, useCallback } from 'react';
|
||||||
|
import { Paper, Group, Button, Modal, Stack, Text } from '@mantine/core';
|
||||||
|
import { ColorSwatchButton } from './ColorPicker';
|
||||||
|
import PenSizeSelector from '../../tools/sign/PenSizeSelector';
|
||||||
|
|
||||||
|
interface DrawingCanvasProps {
|
||||||
|
selectedColor: string;
|
||||||
|
penSize: number;
|
||||||
|
penSizeInput: string;
|
||||||
|
onColorSwatchClick: () => void;
|
||||||
|
onPenSizeChange: (size: number) => void;
|
||||||
|
onPenSizeInputChange: (input: string) => void;
|
||||||
|
onSignatureDataChange: (data: string | null) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
modalWidth?: number;
|
||||||
|
modalHeight?: number;
|
||||||
|
additionalButtons?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DrawingCanvas: React.FC<DrawingCanvasProps> = ({
|
||||||
|
selectedColor,
|
||||||
|
penSize,
|
||||||
|
penSizeInput,
|
||||||
|
onColorSwatchClick,
|
||||||
|
onPenSizeChange,
|
||||||
|
onPenSizeInputChange,
|
||||||
|
onSignatureDataChange,
|
||||||
|
disabled = false,
|
||||||
|
width = 400,
|
||||||
|
height = 150,
|
||||||
|
modalWidth = 800,
|
||||||
|
modalHeight = 400,
|
||||||
|
additionalButtons
|
||||||
|
}) => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const modalCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const visibleModalCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
const [isDrawing, setIsDrawing] = useState(false);
|
||||||
|
const [isModalDrawing, setIsModalDrawing] = useState(false);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [signatureData, setSignatureData] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Drawing functions for main canvas
|
||||||
|
const startDrawing = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
if (!canvasRef.current || disabled) return;
|
||||||
|
|
||||||
|
setIsDrawing(true);
|
||||||
|
const rect = canvasRef.current.getBoundingClientRect();
|
||||||
|
const scaleX = canvasRef.current.width / rect.width;
|
||||||
|
const scaleY = canvasRef.current.height / rect.height;
|
||||||
|
const x = (e.clientX - rect.left) * scaleX;
|
||||||
|
const y = (e.clientY - rect.top) * scaleY;
|
||||||
|
|
||||||
|
const ctx = canvasRef.current.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.strokeStyle = selectedColor;
|
||||||
|
ctx.lineWidth = penSize;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
}
|
||||||
|
}, [disabled, selectedColor, penSize]);
|
||||||
|
|
||||||
|
const draw = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
if (!isDrawing || !canvasRef.current || disabled) return;
|
||||||
|
|
||||||
|
const rect = canvasRef.current.getBoundingClientRect();
|
||||||
|
const scaleX = canvasRef.current.width / rect.width;
|
||||||
|
const scaleY = canvasRef.current.height / rect.height;
|
||||||
|
const x = (e.clientX - rect.left) * scaleX;
|
||||||
|
const y = (e.clientY - rect.top) * scaleY;
|
||||||
|
|
||||||
|
const ctx = canvasRef.current.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}, [isDrawing, disabled]);
|
||||||
|
|
||||||
|
const stopDrawing = useCallback(() => {
|
||||||
|
if (!isDrawing || disabled) return;
|
||||||
|
|
||||||
|
setIsDrawing(false);
|
||||||
|
|
||||||
|
// Save canvas as signature data
|
||||||
|
if (canvasRef.current) {
|
||||||
|
const dataURL = canvasRef.current.toDataURL('image/png');
|
||||||
|
setSignatureData(dataURL);
|
||||||
|
onSignatureDataChange(dataURL);
|
||||||
|
}
|
||||||
|
}, [isDrawing, disabled, onSignatureDataChange]);
|
||||||
|
|
||||||
|
// Modal canvas drawing functions
|
||||||
|
const startModalDrawing = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
if (!visibleModalCanvasRef.current || !modalCanvasRef.current) return;
|
||||||
|
|
||||||
|
setIsModalDrawing(true);
|
||||||
|
const rect = visibleModalCanvasRef.current.getBoundingClientRect();
|
||||||
|
const scaleX = visibleModalCanvasRef.current.width / rect.width;
|
||||||
|
const scaleY = visibleModalCanvasRef.current.height / rect.height;
|
||||||
|
const x = (e.clientX - rect.left) * scaleX;
|
||||||
|
const y = (e.clientY - rect.top) * scaleY;
|
||||||
|
|
||||||
|
// Draw on both the visible modal canvas and hidden canvas
|
||||||
|
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
||||||
|
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
||||||
|
|
||||||
|
[visibleCtx, hiddenCtx].forEach(ctx => {
|
||||||
|
if (ctx) {
|
||||||
|
ctx.strokeStyle = selectedColor;
|
||||||
|
ctx.lineWidth = penSize;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [selectedColor, penSize]);
|
||||||
|
|
||||||
|
const drawModal = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
if (!isModalDrawing || !visibleModalCanvasRef.current || !modalCanvasRef.current) return;
|
||||||
|
|
||||||
|
const rect = visibleModalCanvasRef.current.getBoundingClientRect();
|
||||||
|
const scaleX = visibleModalCanvasRef.current.width / rect.width;
|
||||||
|
const scaleY = visibleModalCanvasRef.current.height / rect.height;
|
||||||
|
const x = (e.clientX - rect.left) * scaleX;
|
||||||
|
const y = (e.clientY - rect.top) * scaleY;
|
||||||
|
|
||||||
|
// Draw on both canvases
|
||||||
|
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
||||||
|
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
||||||
|
|
||||||
|
[visibleCtx, hiddenCtx].forEach(ctx => {
|
||||||
|
if (ctx) {
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [isModalDrawing]);
|
||||||
|
|
||||||
|
const stopModalDrawing = useCallback(() => {
|
||||||
|
if (!isModalDrawing) return;
|
||||||
|
setIsModalDrawing(false);
|
||||||
|
|
||||||
|
// Sync the canvases and update signature data (only when drawing stops)
|
||||||
|
if (modalCanvasRef.current) {
|
||||||
|
const dataURL = modalCanvasRef.current.toDataURL('image/png');
|
||||||
|
setSignatureData(dataURL);
|
||||||
|
onSignatureDataChange(dataURL);
|
||||||
|
|
||||||
|
// Also update the small canvas display
|
||||||
|
if (canvasRef.current) {
|
||||||
|
const smallCtx = canvasRef.current.getContext('2d');
|
||||||
|
if (smallCtx) {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
smallCtx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
||||||
|
smallCtx.drawImage(img, 0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
||||||
|
};
|
||||||
|
img.src = dataURL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isModalDrawing]);
|
||||||
|
|
||||||
|
// Clear canvas functions
|
||||||
|
const clearCanvas = useCallback(() => {
|
||||||
|
if (!canvasRef.current || disabled) return;
|
||||||
|
|
||||||
|
const ctx = canvasRef.current.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
||||||
|
|
||||||
|
// Also clear the modal canvas if it exists
|
||||||
|
if (modalCanvasRef.current) {
|
||||||
|
const modalCtx = modalCanvasRef.current.getContext('2d');
|
||||||
|
if (modalCtx) {
|
||||||
|
modalCtx.clearRect(0, 0, modalCanvasRef.current.width, modalCanvasRef.current.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSignatureData(null);
|
||||||
|
onSignatureDataChange(null);
|
||||||
|
}
|
||||||
|
}, [disabled]);
|
||||||
|
|
||||||
|
const clearModalCanvas = useCallback(() => {
|
||||||
|
// Clear both modal canvases (visible and hidden)
|
||||||
|
if (modalCanvasRef.current) {
|
||||||
|
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
||||||
|
if (hiddenCtx) {
|
||||||
|
hiddenCtx.clearRect(0, 0, modalCanvasRef.current.width, modalCanvasRef.current.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibleModalCanvasRef.current) {
|
||||||
|
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
||||||
|
if (visibleCtx) {
|
||||||
|
visibleCtx.clearRect(0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also clear the main canvas and signature data
|
||||||
|
if (canvasRef.current) {
|
||||||
|
const mainCtx = canvasRef.current.getContext('2d');
|
||||||
|
if (mainCtx) {
|
||||||
|
mainCtx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSignatureData(null);
|
||||||
|
onSignatureDataChange(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveModalSignature = useCallback(() => {
|
||||||
|
if (!modalCanvasRef.current) return;
|
||||||
|
|
||||||
|
const dataURL = modalCanvasRef.current.toDataURL('image/png');
|
||||||
|
setSignatureData(dataURL);
|
||||||
|
onSignatureDataChange(dataURL);
|
||||||
|
|
||||||
|
// Copy to small canvas for display
|
||||||
|
if (canvasRef.current) {
|
||||||
|
const ctx = canvasRef.current.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
ctx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
||||||
|
ctx.drawImage(img, 0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
||||||
|
};
|
||||||
|
img.src = dataURL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openModal = useCallback(() => {
|
||||||
|
setIsModalOpen(true);
|
||||||
|
// Copy content to modal canvas after a brief delay
|
||||||
|
setTimeout(() => {
|
||||||
|
if (visibleModalCanvasRef.current && modalCanvasRef.current) {
|
||||||
|
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
||||||
|
if (visibleCtx) {
|
||||||
|
visibleCtx.strokeStyle = selectedColor;
|
||||||
|
visibleCtx.lineWidth = penSize;
|
||||||
|
visibleCtx.lineCap = 'round';
|
||||||
|
visibleCtx.lineJoin = 'round';
|
||||||
|
visibleCtx.clearRect(0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
||||||
|
visibleCtx.drawImage(modalCanvasRef.current, 0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, [selectedColor, penSize]);
|
||||||
|
|
||||||
|
// Initialize canvas settings whenever color or pen size changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
const updateCanvas = (canvas: HTMLCanvasElement | null) => {
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.strokeStyle = selectedColor;
|
||||||
|
ctx.lineWidth = penSize;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCanvas(canvasRef.current);
|
||||||
|
updateCanvas(modalCanvasRef.current);
|
||||||
|
updateCanvas(visibleModalCanvasRef.current);
|
||||||
|
}, [selectedColor, penSize]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Paper withBorder p="md">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={500}>Draw your signature</Text>
|
||||||
|
<Group gap="lg">
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500} mb="xs" ta="center">Color</Text>
|
||||||
|
<Group justify="center">
|
||||||
|
<ColorSwatchButton
|
||||||
|
color={selectedColor}
|
||||||
|
onClick={onColorSwatchClick}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500} mb="xs">Pen Size</Text>
|
||||||
|
<PenSizeSelector
|
||||||
|
value={penSize}
|
||||||
|
inputValue={penSizeInput}
|
||||||
|
onValueChange={onPenSizeChange}
|
||||||
|
onInputChange={onPenSizeInputChange}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder="Size"
|
||||||
|
size="compact-sm"
|
||||||
|
style={{ width: '60px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ paddingTop: '24px' }}>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
size="compact-sm"
|
||||||
|
onClick={openModal}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
Expand
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
style={{
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: disabled ? 'default' : 'crosshair',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
onMouseDown={startDrawing}
|
||||||
|
onMouseMove={draw}
|
||||||
|
onMouseUp={stopDrawing}
|
||||||
|
onMouseLeave={stopDrawing}
|
||||||
|
/>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<div>
|
||||||
|
{additionalButtons}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
size="compact-sm"
|
||||||
|
onClick={clearCanvas}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Hidden canvas for modal synchronization */}
|
||||||
|
<canvas
|
||||||
|
ref={modalCanvasRef}
|
||||||
|
width={modalWidth}
|
||||||
|
height={modalHeight}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal for larger signature canvas */}
|
||||||
|
<Modal
|
||||||
|
opened={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
title="Draw Your Signature"
|
||||||
|
size="xl"
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Color and Pen Size picker */}
|
||||||
|
<Paper withBorder p="sm">
|
||||||
|
<Group gap="lg" align="flex-end">
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500} mb="xs">Color</Text>
|
||||||
|
<ColorSwatchButton
|
||||||
|
color={selectedColor}
|
||||||
|
onClick={onColorSwatchClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500} mb="xs">Pen Size</Text>
|
||||||
|
<PenSizeSelector
|
||||||
|
value={penSize}
|
||||||
|
inputValue={penSizeInput}
|
||||||
|
onValueChange={onPenSizeChange}
|
||||||
|
onInputChange={onPenSizeInputChange}
|
||||||
|
placeholder="Size"
|
||||||
|
size="compact-sm"
|
||||||
|
style={{ width: '60px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Paper withBorder p="md">
|
||||||
|
<canvas
|
||||||
|
ref={visibleModalCanvasRef}
|
||||||
|
width={modalWidth}
|
||||||
|
height={modalHeight}
|
||||||
|
style={{
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'crosshair',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: `${modalWidth}px`,
|
||||||
|
height: 'auto',
|
||||||
|
}}
|
||||||
|
onMouseDown={startModalDrawing}
|
||||||
|
onMouseMove={drawModal}
|
||||||
|
onMouseUp={stopModalDrawing}
|
||||||
|
onMouseLeave={stopModalDrawing}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
onClick={clearModalCanvas}
|
||||||
|
>
|
||||||
|
Clear Canvas
|
||||||
|
</Button>
|
||||||
|
<Group gap="sm">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={saveModalSignature}
|
||||||
|
>
|
||||||
|
Save Signature
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DrawingCanvas;
|
@ -0,0 +1,60 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Group, Button } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface DrawingControlsProps {
|
||||||
|
onUndo?: () => void;
|
||||||
|
onRedo?: () => void;
|
||||||
|
onPlaceSignature?: () => void;
|
||||||
|
hasSignatureData?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
showPlaceButton?: boolean;
|
||||||
|
placeButtonText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DrawingControls: React.FC<DrawingControlsProps> = ({
|
||||||
|
onUndo,
|
||||||
|
onRedo,
|
||||||
|
onPlaceSignature,
|
||||||
|
hasSignatureData = false,
|
||||||
|
disabled = false,
|
||||||
|
showPlaceButton = true,
|
||||||
|
placeButtonText = "Update and Place"
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group gap="sm">
|
||||||
|
{/* Undo/Redo Controls */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onUndo}
|
||||||
|
disabled={disabled}
|
||||||
|
flex={1}
|
||||||
|
>
|
||||||
|
{t('sign.undo', 'Undo')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onRedo}
|
||||||
|
disabled={disabled}
|
||||||
|
flex={1}
|
||||||
|
>
|
||||||
|
{t('sign.redo', 'Redo')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Place Signature Button */}
|
||||||
|
{showPlaceButton && onPlaceSignature && (
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
color="blue"
|
||||||
|
onClick={onPlaceSignature}
|
||||||
|
disabled={disabled || !hasSignatureData}
|
||||||
|
flex={1}
|
||||||
|
>
|
||||||
|
{placeButtonText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
55
frontend/src/components/annotation/shared/ImageUploader.tsx
Normal file
55
frontend/src/components/annotation/shared/ImageUploader.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FileInput, Text, Stack } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface ImageUploaderProps {
|
||||||
|
onImageChange: (file: File | null) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
hint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImageUploader: React.FC<ImageUploaderProps> = ({
|
||||||
|
onImageChange,
|
||||||
|
disabled = false,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
hint
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleImageChange = async (file: File | null) => {
|
||||||
|
if (file && !disabled) {
|
||||||
|
try {
|
||||||
|
// Validate that it's actually an image file
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
console.error('Selected file is not an image');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onImageChange(file);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing image file:', error);
|
||||||
|
}
|
||||||
|
} else if (!file) {
|
||||||
|
// Clear image data when no file is selected
|
||||||
|
onImageChange(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<FileInput
|
||||||
|
label={label || t('sign.image.label', 'Upload signature image')}
|
||||||
|
placeholder={placeholder || t('sign.image.placeholder', 'Select image file')}
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{hint || t('sign.image.hint', 'Upload a PNG or JPG image of your signature')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
126
frontend/src/components/annotation/shared/TextInputWithFont.tsx
Normal file
126
frontend/src/components/annotation/shared/TextInputWithFont.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Stack, TextInput, Select, Combobox, useCombobox } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface TextInputWithFontProps {
|
||||||
|
text: string;
|
||||||
|
onTextChange: (text: string) => void;
|
||||||
|
fontSize: number;
|
||||||
|
onFontSizeChange: (size: number) => void;
|
||||||
|
fontFamily: string;
|
||||||
|
onFontFamilyChange: (family: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextInputWithFont: React.FC<TextInputWithFontProps> = ({
|
||||||
|
text,
|
||||||
|
onTextChange,
|
||||||
|
fontSize,
|
||||||
|
onFontSizeChange,
|
||||||
|
fontFamily,
|
||||||
|
onFontFamilyChange,
|
||||||
|
disabled = false,
|
||||||
|
label,
|
||||||
|
placeholder
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [fontSizeInput, setFontSizeInput] = useState(fontSize.toString());
|
||||||
|
const fontSizeCombobox = useCombobox();
|
||||||
|
|
||||||
|
// Sync font size input with prop changes
|
||||||
|
useEffect(() => {
|
||||||
|
setFontSizeInput(fontSize.toString());
|
||||||
|
}, [fontSize]);
|
||||||
|
|
||||||
|
const fontOptions = [
|
||||||
|
{ value: 'Helvetica', label: 'Helvetica' },
|
||||||
|
{ value: 'Times-Roman', label: 'Times' },
|
||||||
|
{ value: 'Courier', label: 'Courier' },
|
||||||
|
{ value: 'Arial', label: 'Arial' },
|
||||||
|
{ value: 'Georgia', label: 'Georgia' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const fontSizeOptions = ['8', '12', '16', '20', '24', '28', '32', '36', '40', '48'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<TextInput
|
||||||
|
label={label || t('sign.text.name', 'Signer Name')}
|
||||||
|
placeholder={placeholder || t('sign.text.placeholder', 'Enter your full name')}
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => onTextChange(e.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Font Selection */}
|
||||||
|
<Select
|
||||||
|
label="Font"
|
||||||
|
value={fontFamily}
|
||||||
|
onChange={(value) => onFontFamilyChange(value || 'Helvetica')}
|
||||||
|
data={fontOptions}
|
||||||
|
disabled={disabled}
|
||||||
|
searchable
|
||||||
|
allowDeselect={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Font Size */}
|
||||||
|
<Combobox
|
||||||
|
onOptionSubmit={(optionValue) => {
|
||||||
|
setFontSizeInput(optionValue);
|
||||||
|
const size = parseInt(optionValue);
|
||||||
|
if (!isNaN(size)) {
|
||||||
|
onFontSizeChange(size);
|
||||||
|
}
|
||||||
|
fontSizeCombobox.closeDropdown();
|
||||||
|
}}
|
||||||
|
store={fontSizeCombobox}
|
||||||
|
withinPortal={false}
|
||||||
|
>
|
||||||
|
<Combobox.Target>
|
||||||
|
<TextInput
|
||||||
|
label="Font Size"
|
||||||
|
placeholder="Type or select font size (8-72)"
|
||||||
|
value={fontSizeInput}
|
||||||
|
onChange={(event) => {
|
||||||
|
const value = event.currentTarget.value;
|
||||||
|
setFontSizeInput(value);
|
||||||
|
|
||||||
|
// Parse and validate the typed value in real-time
|
||||||
|
const size = parseInt(value);
|
||||||
|
if (!isNaN(size) && size >= 8 && size <= 72) {
|
||||||
|
onFontSizeChange(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
fontSizeCombobox.openDropdown();
|
||||||
|
fontSizeCombobox.updateSelectedOptionIndex();
|
||||||
|
}}
|
||||||
|
onClick={() => fontSizeCombobox.openDropdown()}
|
||||||
|
onFocus={() => fontSizeCombobox.openDropdown()}
|
||||||
|
onBlur={() => {
|
||||||
|
fontSizeCombobox.closeDropdown();
|
||||||
|
// Clean up invalid values on blur
|
||||||
|
const size = parseInt(fontSizeInput);
|
||||||
|
if (isNaN(size) || size < 8 || size > 72) {
|
||||||
|
setFontSizeInput(fontSize.toString());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Combobox.Target>
|
||||||
|
|
||||||
|
<Combobox.Dropdown>
|
||||||
|
<Combobox.Options>
|
||||||
|
{fontSizeOptions.map((size) => (
|
||||||
|
<Combobox.Option value={size} key={size}>
|
||||||
|
{size}px
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
</Combobox.Dropdown>
|
||||||
|
</Combobox>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
47
frontend/src/components/annotation/tools/DrawingTool.tsx
Normal file
47
frontend/src/components/annotation/tools/DrawingTool.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Stack, Paper, Text, Group } from '@mantine/core';
|
||||||
|
import { BaseAnnotationTool } from '../shared/BaseAnnotationTool';
|
||||||
|
import { DrawingCanvas } from '../shared/DrawingCanvas';
|
||||||
|
import { ColorSwatchButton } from '../shared/ColorPicker';
|
||||||
|
import PenSizeSelector from '../../tools/sign/PenSizeSelector';
|
||||||
|
|
||||||
|
interface DrawingToolProps {
|
||||||
|
onDrawingChange?: (data: string | null) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DrawingTool: React.FC<DrawingToolProps> = ({
|
||||||
|
onDrawingChange,
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
|
const [selectedColor, setSelectedColor] = useState('#000000');
|
||||||
|
const [penSize, setPenSize] = useState(2);
|
||||||
|
const [penSizeInput, setPenSizeInput] = useState('2');
|
||||||
|
|
||||||
|
const toolConfig = {
|
||||||
|
enableDrawing: true,
|
||||||
|
showPlaceButton: true,
|
||||||
|
placeButtonText: "Place Drawing"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseAnnotationTool
|
||||||
|
config={toolConfig}
|
||||||
|
onSignatureDataChange={onDrawingChange}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<Stack gap="sm">
|
||||||
|
<DrawingCanvas
|
||||||
|
selectedColor={selectedColor}
|
||||||
|
penSize={penSize}
|
||||||
|
penSizeInput={penSizeInput}
|
||||||
|
onColorSwatchClick={() => {}} // Color picker handled by BaseAnnotationTool
|
||||||
|
onPenSizeChange={setPenSize}
|
||||||
|
onPenSizeInputChange={setPenSizeInput}
|
||||||
|
onSignatureDataChange={onDrawingChange || (() => {})}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</BaseAnnotationTool>
|
||||||
|
);
|
||||||
|
};
|
67
frontend/src/components/annotation/tools/ImageTool.tsx
Normal file
67
frontend/src/components/annotation/tools/ImageTool.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Stack } from '@mantine/core';
|
||||||
|
import { BaseAnnotationTool } from '../shared/BaseAnnotationTool';
|
||||||
|
import { ImageUploader } from '../shared/ImageUploader';
|
||||||
|
|
||||||
|
interface ImageToolProps {
|
||||||
|
onImageChange?: (data: string | null) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImageTool: React.FC<ImageToolProps> = ({
|
||||||
|
onImageChange,
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
|
const [imageData, setImageData] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleImageUpload = async (file: File | null) => {
|
||||||
|
if (file && !disabled) {
|
||||||
|
try {
|
||||||
|
const result = await new Promise<string>((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
if (e.target?.result) {
|
||||||
|
resolve(e.target.result as string);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Failed to read file'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = () => reject(reader.error);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
setImageData(result);
|
||||||
|
onImageChange?.(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading file:', error);
|
||||||
|
}
|
||||||
|
} else if (!file) {
|
||||||
|
setImageData(null);
|
||||||
|
onImageChange?.(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toolConfig = {
|
||||||
|
enableImageUpload: true,
|
||||||
|
showPlaceButton: true,
|
||||||
|
placeButtonText: "Place Image"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseAnnotationTool
|
||||||
|
config={toolConfig}
|
||||||
|
onSignatureDataChange={onImageChange}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<Stack gap="sm">
|
||||||
|
<ImageUploader
|
||||||
|
onImageChange={handleImageUpload}
|
||||||
|
disabled={disabled}
|
||||||
|
label="Upload Image"
|
||||||
|
placeholder="Select image file"
|
||||||
|
hint="Upload a PNG, JPG, or other image file to place on the PDF"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</BaseAnnotationTool>
|
||||||
|
);
|
||||||
|
};
|
57
frontend/src/components/annotation/tools/TextTool.tsx
Normal file
57
frontend/src/components/annotation/tools/TextTool.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Stack } from '@mantine/core';
|
||||||
|
import { BaseAnnotationTool } from '../shared/BaseAnnotationTool';
|
||||||
|
import { TextInputWithFont } from '../shared/TextInputWithFont';
|
||||||
|
|
||||||
|
interface TextToolProps {
|
||||||
|
onTextChange?: (text: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextTool: React.FC<TextToolProps> = ({
|
||||||
|
onTextChange,
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
|
const [text, setText] = useState('');
|
||||||
|
const [fontSize, setFontSize] = useState(16);
|
||||||
|
const [fontFamily, setFontFamily] = useState('Helvetica');
|
||||||
|
|
||||||
|
const handleTextChange = (newText: string) => {
|
||||||
|
setText(newText);
|
||||||
|
onTextChange?.(newText);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSignatureDataChange = (data: string | null) => {
|
||||||
|
if (data) {
|
||||||
|
onTextChange?.(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toolConfig = {
|
||||||
|
enableTextInput: true,
|
||||||
|
showPlaceButton: true,
|
||||||
|
placeButtonText: "Place Text"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseAnnotationTool
|
||||||
|
config={toolConfig}
|
||||||
|
onSignatureDataChange={handleSignatureDataChange}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<Stack gap="sm">
|
||||||
|
<TextInputWithFont
|
||||||
|
text={text}
|
||||||
|
onTextChange={handleTextChange}
|
||||||
|
fontSize={fontSize}
|
||||||
|
onFontSizeChange={setFontSize}
|
||||||
|
fontFamily={fontFamily}
|
||||||
|
onFontFamilyChange={setFontFamily}
|
||||||
|
disabled={disabled}
|
||||||
|
label="Text Content"
|
||||||
|
placeholder="Enter text to place on the PDF"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</BaseAnnotationTool>
|
||||||
|
);
|
||||||
|
};
|
@ -1,10 +1,15 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
import React, { useState, useEffect } 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, Tabs } from '@mantine/core';
|
import { Stack, Button, Text, Alert, Tabs, Group, Paper } from '@mantine/core';
|
||||||
import ButtonSelector from "../../shared/ButtonSelector";
|
|
||||||
import { SignParameters } from "../../../hooks/tools/sign/useSignParameters";
|
import { SignParameters } from "../../../hooks/tools/sign/useSignParameters";
|
||||||
import { SuggestedToolsSection } from "../shared/SuggestedToolsSection";
|
import { SuggestedToolsSection } from "../shared/SuggestedToolsSection";
|
||||||
import PenSizeSelector from "./PenSizeSelector";
|
|
||||||
|
// Import the new reusable components
|
||||||
|
import { DrawingCanvas } from "../../annotation/shared/DrawingCanvas";
|
||||||
|
import { DrawingControls } from "../../annotation/shared/DrawingControls";
|
||||||
|
import { ImageUploader } from "../../annotation/shared/ImageUploader";
|
||||||
|
import { TextInputWithFont } from "../../annotation/shared/TextInputWithFont";
|
||||||
|
import { ColorPicker } from "../../annotation/shared/ColorPicker";
|
||||||
|
|
||||||
interface SignSettingsProps {
|
interface SignSettingsProps {
|
||||||
parameters: SignParameters;
|
parameters: SignParameters;
|
||||||
@ -19,241 +24,32 @@ interface SignSettingsProps {
|
|||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SignSettings = ({ parameters, onParameterChange, disabled = false, onActivateDrawMode, onActivateSignaturePlacement, onDeactivateSignature, onUpdateDrawSettings, onUndo, onRedo, onSave }: SignSettingsProps) => {
|
const SignSettings = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
disabled = false,
|
||||||
|
onActivateDrawMode,
|
||||||
|
onActivateSignaturePlacement,
|
||||||
|
onDeactivateSignature,
|
||||||
|
onUpdateDrawSettings,
|
||||||
|
onUndo,
|
||||||
|
onRedo,
|
||||||
|
onSave
|
||||||
|
}: SignSettingsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
const [isDrawing, setIsDrawing] = useState(false);
|
// State for drawing
|
||||||
const [canvasSignatureData, setCanvasSignatureData] = useState<string | null>(null);
|
|
||||||
const [imageSignatureData, setImageSignatureData] = useState<string | null>(null);
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
|
||||||
const modalCanvasRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
const visibleModalCanvasRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
const [isModalDrawing, setIsModalDrawing] = useState(false);
|
|
||||||
const [selectedColor, setSelectedColor] = useState('#000000');
|
const [selectedColor, setSelectedColor] = useState('#000000');
|
||||||
const [penSize, setPenSize] = useState(2);
|
const [penSize, setPenSize] = useState(2);
|
||||||
const [penSizeInput, setPenSizeInput] = useState('2');
|
const [penSizeInput, setPenSizeInput] = useState('2');
|
||||||
const [fontSizeInput, setFontSizeInput] = useState((parameters.fontSize || 16).toString());
|
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||||
const fontSizeCombobox = useCombobox();
|
|
||||||
|
|
||||||
|
// State for different signature types
|
||||||
|
const [canvasSignatureData, setCanvasSignatureData] = useState<string | null>(null);
|
||||||
|
const [imageSignatureData, setImageSignatureData] = useState<string | null>(null);
|
||||||
|
|
||||||
// Drawing functions for signature canvas
|
// Handle image upload
|
||||||
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
const handleImageChange = async (file: File | null) => {
|
||||||
if (!canvasRef.current || disabled) return;
|
|
||||||
|
|
||||||
setIsDrawing(true);
|
|
||||||
const rect = canvasRef.current.getBoundingClientRect();
|
|
||||||
const scaleX = canvasRef.current.width / rect.width;
|
|
||||||
const scaleY = canvasRef.current.height / rect.height;
|
|
||||||
const x = (e.clientX - rect.left) * scaleX;
|
|
||||||
const y = (e.clientY - rect.top) * scaleY;
|
|
||||||
|
|
||||||
|
|
||||||
const ctx = canvasRef.current.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
ctx.strokeStyle = selectedColor;
|
|
||||||
ctx.lineWidth = penSize;
|
|
||||||
ctx.lineCap = 'round';
|
|
||||||
ctx.lineJoin = 'round';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, y);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const draw = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
||||||
if (!isDrawing || !canvasRef.current || disabled) return;
|
|
||||||
|
|
||||||
const rect = canvasRef.current.getBoundingClientRect();
|
|
||||||
const scaleX = canvasRef.current.width / rect.width;
|
|
||||||
const scaleY = canvasRef.current.height / rect.height;
|
|
||||||
const x = (e.clientX - rect.left) * scaleX;
|
|
||||||
const y = (e.clientY - rect.top) * scaleY;
|
|
||||||
|
|
||||||
const ctx = canvasRef.current.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// Don't update signature data during drawing - too expensive
|
|
||||||
// This will happen in stopDrawing instead
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopDrawing = () => {
|
|
||||||
if (!isDrawing || disabled) return;
|
|
||||||
|
|
||||||
setIsDrawing(false);
|
|
||||||
|
|
||||||
// Save canvas as signature data
|
|
||||||
if (canvasRef.current) {
|
|
||||||
const dataURL = canvasRef.current.toDataURL('image/png');
|
|
||||||
setCanvasSignatureData(dataURL);
|
|
||||||
onParameterChange('signatureData', dataURL);
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearCanvas = () => {
|
|
||||||
if (!canvasRef.current || disabled) return;
|
|
||||||
|
|
||||||
const ctx = canvasRef.current.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
|
||||||
|
|
||||||
// Also clear the modal canvas if it exists
|
|
||||||
if (modalCanvasRef.current) {
|
|
||||||
const modalCtx = modalCanvasRef.current.getContext('2d');
|
|
||||||
if (modalCtx) {
|
|
||||||
modalCtx.clearRect(0, 0, modalCanvasRef.current.width, modalCanvasRef.current.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setCanvasSignatureData(null);
|
|
||||||
onParameterChange('signatureData', undefined);
|
|
||||||
|
|
||||||
// Deactivate signature placement when cleared
|
|
||||||
if (onDeactivateSignature) {
|
|
||||||
onDeactivateSignature();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Modal canvas drawing functions
|
|
||||||
const startModalDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
||||||
if (!visibleModalCanvasRef.current || !modalCanvasRef.current) return;
|
|
||||||
|
|
||||||
setIsModalDrawing(true);
|
|
||||||
const rect = visibleModalCanvasRef.current.getBoundingClientRect();
|
|
||||||
const scaleX = visibleModalCanvasRef.current.width / rect.width;
|
|
||||||
const scaleY = visibleModalCanvasRef.current.height / rect.height;
|
|
||||||
const x = (e.clientX - rect.left) * scaleX;
|
|
||||||
const y = (e.clientY - rect.top) * scaleY;
|
|
||||||
|
|
||||||
|
|
||||||
// Draw on both the visible modal canvas and hidden canvas
|
|
||||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
|
||||||
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
|
||||||
|
|
||||||
[visibleCtx, hiddenCtx].forEach(ctx => {
|
|
||||||
if (ctx) {
|
|
||||||
ctx.strokeStyle = selectedColor;
|
|
||||||
ctx.lineWidth = penSize;
|
|
||||||
ctx.lineCap = 'round';
|
|
||||||
ctx.lineJoin = 'round';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, y);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawModal = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
||||||
if (!isModalDrawing || !visibleModalCanvasRef.current || !modalCanvasRef.current) return;
|
|
||||||
|
|
||||||
const rect = visibleModalCanvasRef.current.getBoundingClientRect();
|
|
||||||
const scaleX = visibleModalCanvasRef.current.width / rect.width;
|
|
||||||
const scaleY = visibleModalCanvasRef.current.height / rect.height;
|
|
||||||
const x = (e.clientX - rect.left) * scaleX;
|
|
||||||
const y = (e.clientY - rect.top) * scaleY;
|
|
||||||
|
|
||||||
// Draw on both canvases
|
|
||||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
|
||||||
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
|
||||||
|
|
||||||
[visibleCtx, hiddenCtx].forEach(ctx => {
|
|
||||||
if (ctx) {
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Don't update signature data or sync canvases during drawing - too expensive
|
|
||||||
// This will happen in stopModalDrawing instead
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopModalDrawing = () => {
|
|
||||||
if (!isModalDrawing) return;
|
|
||||||
setIsModalDrawing(false);
|
|
||||||
|
|
||||||
// Now sync the canvases and update signature data (only when drawing stops)
|
|
||||||
if (modalCanvasRef.current) {
|
|
||||||
const dataURL = modalCanvasRef.current.toDataURL('image/png');
|
|
||||||
setCanvasSignatureData(dataURL);
|
|
||||||
onParameterChange('signatureData', dataURL);
|
|
||||||
|
|
||||||
// Also update the small canvas display
|
|
||||||
if (canvasRef.current) {
|
|
||||||
const smallCtx = canvasRef.current.getContext('2d');
|
|
||||||
if (smallCtx) {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
smallCtx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
|
||||||
smallCtx.drawImage(img, 0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
|
||||||
};
|
|
||||||
img.src = dataURL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearModalCanvas = () => {
|
|
||||||
// Clear both modal canvases (visible and hidden)
|
|
||||||
if (modalCanvasRef.current) {
|
|
||||||
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
|
||||||
if (hiddenCtx) {
|
|
||||||
hiddenCtx.clearRect(0, 0, modalCanvasRef.current.width, modalCanvasRef.current.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visibleModalCanvasRef.current) {
|
|
||||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
|
||||||
if (visibleCtx) {
|
|
||||||
visibleCtx.clearRect(0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also clear the main canvas and signature data
|
|
||||||
if (canvasRef.current) {
|
|
||||||
const mainCtx = canvasRef.current.getContext('2d');
|
|
||||||
if (mainCtx) {
|
|
||||||
mainCtx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setCanvasSignatureData(null);
|
|
||||||
onParameterChange('signatureData', undefined);
|
|
||||||
|
|
||||||
// Deactivate signature placement when cleared
|
|
||||||
if (onDeactivateSignature) {
|
|
||||||
onDeactivateSignature();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveModalSignature = () => {
|
|
||||||
if (!modalCanvasRef.current) return;
|
|
||||||
|
|
||||||
const dataURL = modalCanvasRef.current.toDataURL('image/png');
|
|
||||||
setCanvasSignatureData(dataURL);
|
|
||||||
onParameterChange('signatureData', dataURL);
|
|
||||||
|
|
||||||
// Copy to small canvas for display
|
|
||||||
if (canvasRef.current) {
|
|
||||||
const ctx = canvasRef.current.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
ctx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
|
||||||
ctx.drawImage(img, 0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
|
||||||
};
|
|
||||||
img.src = dataURL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsModalOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle signature image upload
|
|
||||||
const handleSignatureImageChange = async (file: File | null) => {
|
|
||||||
console.log('Image file selected:', file);
|
|
||||||
if (file && !disabled) {
|
if (file && !disabled) {
|
||||||
try {
|
try {
|
||||||
const result = await new Promise<string>((resolve, reject) => {
|
const result = await new Promise<string>((resolve, reject) => {
|
||||||
@ -271,131 +67,41 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv
|
|||||||
|
|
||||||
// Clear any existing canvas signatures when uploading image
|
// Clear any existing canvas signatures when uploading image
|
||||||
setCanvasSignatureData(null);
|
setCanvasSignatureData(null);
|
||||||
|
|
||||||
// Set as active signature immediately
|
|
||||||
setImageSignatureData(result);
|
setImageSignatureData(result);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reading file:', error);
|
console.error('Error reading file:', error);
|
||||||
}
|
}
|
||||||
} else if (!file) {
|
} else if (!file) {
|
||||||
// Clear image data when no file is selected
|
|
||||||
setImageSignatureData(null);
|
setImageSignatureData(null);
|
||||||
// Deactivate signature placement when image is removed
|
|
||||||
if (onDeactivateSignature) {
|
if (onDeactivateSignature) {
|
||||||
onDeactivateSignature();
|
onDeactivateSignature();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize canvas
|
// Handle signature data changes
|
||||||
React.useEffect(() => {
|
const handleCanvasSignatureChange = (data: string | null) => {
|
||||||
if (canvasRef.current && parameters.signatureType === 'canvas') {
|
setCanvasSignatureData(prev => {
|
||||||
const ctx = canvasRef.current.getContext('2d');
|
if (prev === data) return prev; // Prevent unnecessary updates
|
||||||
if (ctx) {
|
return data;
|
||||||
ctx.strokeStyle = selectedColor;
|
});
|
||||||
ctx.lineWidth = penSize;
|
if (data) {
|
||||||
ctx.lineCap = 'round';
|
// Clear image data when canvas is used
|
||||||
ctx.lineJoin = 'round';
|
setImageSignatureData(prev => prev ? null : prev);
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [parameters.signatureType, selectedColor, penSize]);
|
|
||||||
|
|
||||||
// Initialize both canvases - hidden one always exists, main one when in canvas mode
|
|
||||||
React.useEffect(() => {
|
|
||||||
const initCanvas = (canvas: HTMLCanvasElement | null) => {
|
|
||||||
if (!canvas) return;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
ctx.strokeStyle = selectedColor;
|
|
||||||
ctx.lineWidth = penSize;
|
|
||||||
ctx.lineCap = 'round';
|
|
||||||
ctx.lineJoin = 'round';
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (parameters.signatureType === 'canvas') {
|
// Handle drawing mode activation
|
||||||
initCanvas(canvasRef.current);
|
useEffect(() => {
|
||||||
initCanvas(modalCanvasRef.current); // Hidden canvas always available
|
if (parameters.signatureType === 'draw' && onActivateDrawMode) {
|
||||||
}
|
|
||||||
}, [parameters.signatureType, selectedColor, penSize]);
|
|
||||||
|
|
||||||
// Copy main canvas content to hidden modal canvas whenever signature data changes
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (modalCanvasRef.current && canvasSignatureData) {
|
|
||||||
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
|
||||||
if (hiddenCtx) {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
if (modalCanvasRef.current) {
|
|
||||||
hiddenCtx.clearRect(0, 0, modalCanvasRef.current.width, modalCanvasRef.current.height);
|
|
||||||
hiddenCtx.drawImage(img, 0, 0, modalCanvasRef.current.width, modalCanvasRef.current.height);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
img.src = canvasSignatureData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [canvasSignatureData]);
|
|
||||||
|
|
||||||
|
|
||||||
// Switch signature data based on mode
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (parameters.signatureType === 'canvas') {
|
|
||||||
if (canvasSignatureData) {
|
|
||||||
onParameterChange('signatureData', canvasSignatureData);
|
|
||||||
} else {
|
|
||||||
onParameterChange('signatureData', undefined);
|
|
||||||
}
|
|
||||||
} else if (parameters.signatureType === 'image') {
|
|
||||||
if (imageSignatureData) {
|
|
||||||
onParameterChange('signatureData', imageSignatureData);
|
|
||||||
// Activate signature placement mode when image is ready
|
|
||||||
if (onActivateSignaturePlacement) {
|
|
||||||
onActivateSignaturePlacement();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onParameterChange('signatureData', undefined);
|
|
||||||
}
|
|
||||||
} else if (parameters.signatureType === 'text') {
|
|
||||||
// For text mode, we don't use signatureData - we use signerName directly
|
|
||||||
onParameterChange('signatureData', undefined);
|
|
||||||
} else {
|
|
||||||
// For draw mode, clear signature data
|
|
||||||
onParameterChange('signatureData', undefined);
|
|
||||||
}
|
|
||||||
}, [parameters.signatureType, canvasSignatureData, imageSignatureData]);
|
|
||||||
|
|
||||||
// Initialize draw mode on mount
|
|
||||||
React.useEffect(() => {
|
|
||||||
// Use a ref to track if we've already initialized
|
|
||||||
let isInitialized = false;
|
|
||||||
|
|
||||||
if (parameters.signatureType === 'draw' && onActivateDrawMode && !isInitialized) {
|
|
||||||
// Delay to ensure viewer is ready
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
onActivateDrawMode();
|
onActivateDrawMode();
|
||||||
isInitialized = true;
|
} else if (parameters.signatureType !== 'draw' && onDeactivateSignature) {
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, []); // Empty dependency - only run on mount
|
|
||||||
|
|
||||||
// Auto-activate draw mode when draw type is selected
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (parameters.signatureType === 'draw') {
|
|
||||||
if (onActivateDrawMode) {
|
|
||||||
onActivateDrawMode();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (onDeactivateSignature) {
|
|
||||||
onDeactivateSignature();
|
onDeactivateSignature();
|
||||||
}
|
}
|
||||||
}
|
}, [parameters.signatureType]);
|
||||||
}, [parameters.signatureType]); // Only depend on signatureType to avoid loops
|
|
||||||
|
|
||||||
// Auto-activate text signature placement when signer name is entered
|
// Handle text signature activation
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (parameters.signatureType === 'text' && parameters.signerName && parameters.signerName.trim() !== '') {
|
if (parameters.signatureType === 'text' && parameters.signerName && parameters.signerName.trim() !== '') {
|
||||||
if (onActivateSignaturePlacement) {
|
if (onActivateSignaturePlacement) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -407,31 +113,39 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv
|
|||||||
onDeactivateSignature();
|
onDeactivateSignature();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [parameters.signatureType, parameters.signerName]); // Remove function dependencies to prevent loops
|
}, [parameters.signatureType, parameters.signerName]);
|
||||||
|
|
||||||
// Update draw settings when color or pen size changes
|
// Handle signature data updates
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (parameters.signatureType === 'draw' && onUpdateDrawSettings) {
|
let newSignatureData: string | undefined = undefined;
|
||||||
onUpdateDrawSettings(selectedColor, penSize);
|
|
||||||
|
if (parameters.signatureType === 'image' && imageSignatureData) {
|
||||||
|
newSignatureData = imageSignatureData;
|
||||||
|
} else if (parameters.signatureType === 'canvas' && canvasSignatureData) {
|
||||||
|
newSignatureData = canvasSignatureData;
|
||||||
}
|
}
|
||||||
}, [selectedColor, penSize, parameters.signatureType]); // Remove function dependency to prevent loops
|
|
||||||
|
|
||||||
// Sync font size input with parameter changes
|
// Only update if the signature data has actually changed
|
||||||
React.useEffect(() => {
|
if (parameters.signatureData !== newSignatureData) {
|
||||||
setFontSizeInput((parameters.fontSize || 16).toString());
|
onParameterChange('signatureData', newSignatureData);
|
||||||
}, [parameters.fontSize]);
|
}
|
||||||
|
}, [parameters.signatureType, parameters.signatureData, canvasSignatureData, imageSignatureData]);
|
||||||
|
|
||||||
// Update signature config when font settings change
|
// Handle image signature activation - activate when image data syncs with parameters
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (parameters.signatureType === 'text' && (parameters.fontFamily || parameters.fontSize)) {
|
if (parameters.signatureType === 'image' && imageSignatureData && parameters.signatureData === imageSignatureData && onActivateSignaturePlacement) {
|
||||||
// Trigger re-activation of signature placement to apply new font settings
|
|
||||||
if (parameters.signerName && parameters.signerName.trim() !== '' && onActivateSignaturePlacement) {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onActivateSignaturePlacement();
|
onActivateSignaturePlacement();
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
}, [parameters.signatureType, parameters.signatureData, imageSignatureData]);
|
||||||
|
|
||||||
|
// Update draw settings when color or pen size changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (parameters.signatureType === 'draw' && onUpdateDrawSettings) {
|
||||||
|
onUpdateDrawSettings(selectedColor, penSize);
|
||||||
}
|
}
|
||||||
}, [parameters.fontFamily, parameters.fontSize, parameters.signatureType, parameters.signerName]); // Remove function dependency to prevent loops
|
}, [selectedColor, penSize, parameters.signatureType]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
@ -456,228 +170,66 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv
|
|||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{/* Undo/Redo Controls */}
|
{/* Drawing Controls */}
|
||||||
<Group justify="space-between" grow>
|
<DrawingControls
|
||||||
<Button
|
onUndo={onUndo}
|
||||||
variant="outline"
|
onRedo={onRedo}
|
||||||
onClick={onUndo}
|
onPlaceSignature={() => {
|
||||||
|
if (onActivateSignaturePlacement) {
|
||||||
|
onActivateSignaturePlacement();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
hasSignatureData={!!(canvasSignatureData || imageSignatureData || (parameters.signerName && parameters.signerName.trim() !== ''))}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
showPlaceButton={false}
|
||||||
{t('sign.undo', 'Undo')}
|
placeButtonText="Update and Place"
|
||||||
</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' && (
|
||||||
<Paper withBorder p="md">
|
<DrawingCanvas
|
||||||
<Stack gap="sm">
|
selectedColor={selectedColor}
|
||||||
<Group justify="space-between">
|
penSize={penSize}
|
||||||
<Text fw={500}>{t('sign.draw.title', 'Draw your signature')}</Text>
|
penSizeInput={penSizeInput}
|
||||||
<Group gap="lg">
|
onColorSwatchClick={() => setIsColorPickerOpen(true)}
|
||||||
<div>
|
onPenSizeChange={setPenSize}
|
||||||
<Text size="sm" fw={500} mb="xs" ta="center">Color</Text>
|
onPenSizeInputChange={setPenSizeInput}
|
||||||
<Group justify="center">
|
onSignatureDataChange={handleCanvasSignatureChange}
|
||||||
<ColorSwatch
|
|
||||||
color={selectedColor}
|
|
||||||
size={24}
|
|
||||||
radius={0}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
onClick={() => setIsColorPickerOpen(true)}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Text size="sm" fw={500} mb="xs">Pen Size</Text>
|
|
||||||
<PenSizeSelector
|
|
||||||
value={penSize}
|
|
||||||
inputValue={penSizeInput}
|
|
||||||
onValueChange={setPenSize}
|
|
||||||
onInputChange={setPenSizeInput}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder="Size"
|
additionalButtons={
|
||||||
size="compact-sm"
|
|
||||||
style={{ width: '60px' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ paddingTop: '24px' }}>
|
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
|
||||||
size="compact-sm"
|
|
||||||
onClick={() => {
|
|
||||||
setIsModalOpen(true);
|
|
||||||
// Copy content to modal canvas after a brief delay
|
|
||||||
setTimeout(() => {
|
|
||||||
if (visibleModalCanvasRef.current && modalCanvasRef.current) {
|
|
||||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
|
||||||
if (visibleCtx) {
|
|
||||||
visibleCtx.strokeStyle = selectedColor;
|
|
||||||
visibleCtx.lineWidth = penSize;
|
|
||||||
visibleCtx.lineCap = 'round';
|
|
||||||
visibleCtx.lineJoin = 'round';
|
|
||||||
visibleCtx.clearRect(0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
|
||||||
visibleCtx.drawImage(modalCanvasRef.current, 0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
Expand
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
<canvas
|
|
||||||
ref={canvasRef}
|
|
||||||
width={400}
|
|
||||||
height={150}
|
|
||||||
style={{
|
|
||||||
border: '1px solid #ccc',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: disabled ? 'default' : 'crosshair',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
onMouseDown={startDrawing}
|
|
||||||
onMouseMove={draw}
|
|
||||||
onMouseUp={stopDrawing}
|
|
||||||
onMouseLeave={stopDrawing}
|
|
||||||
/>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Button
|
|
||||||
variant="filled"
|
|
||||||
color="blue"
|
|
||||||
size="compact-sm"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (onActivateSignaturePlacement) {
|
if (onActivateSignaturePlacement) {
|
||||||
onActivateSignaturePlacement();
|
onActivateSignaturePlacement();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
color="blue"
|
||||||
|
variant="filled"
|
||||||
disabled={disabled || !canvasSignatureData}
|
disabled={disabled || !canvasSignatureData}
|
||||||
>
|
>
|
||||||
Update and Place
|
Update and Place
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
}
|
||||||
variant="subtle"
|
/>
|
||||||
color="red"
|
|
||||||
size="compact-sm"
|
|
||||||
onClick={clearCanvas}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{t('sign.draw.clear', 'Clear')}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{parameters.signatureType === 'image' && (
|
{parameters.signatureType === 'image' && (
|
||||||
<Stack gap="sm">
|
<ImageUploader
|
||||||
<FileInput
|
onImageChange={handleImageChange}
|
||||||
label={t('sign.image.label', 'Upload signature image')}
|
|
||||||
placeholder={t('sign.image.placeholder', 'Select image file')}
|
|
||||||
accept="image/*"
|
|
||||||
onChange={handleSignatureImageChange}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{t('sign.image.hint', 'Upload a PNG or JPG image of your signature')}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{parameters.signatureType === 'text' && (
|
{parameters.signatureType === 'text' && (
|
||||||
<Stack gap="sm">
|
<TextInputWithFont
|
||||||
<TextInput
|
text={parameters.signerName || ''}
|
||||||
label={t('sign.text.name', 'Signer Name')}
|
onTextChange={(text) => onParameterChange('signerName', text)}
|
||||||
placeholder={t('sign.text.placeholder', 'Enter your full name')}
|
fontSize={parameters.fontSize || 16}
|
||||||
value={parameters.signerName || ''}
|
onFontSizeChange={(size) => onParameterChange('fontSize', size)}
|
||||||
onChange={(e) => onParameterChange('signerName', e.target.value)}
|
fontFamily={parameters.fontFamily || 'Helvetica'}
|
||||||
disabled={disabled}
|
onFontFamilyChange={(family) => onParameterChange('fontFamily', family)}
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Font Selection */}
|
|
||||||
<Select
|
|
||||||
label="Font"
|
|
||||||
value={parameters.fontFamily || 'Helvetica'}
|
|
||||||
onChange={(value) => onParameterChange('fontFamily', value || 'Helvetica')}
|
|
||||||
data={[
|
|
||||||
{ value: 'Helvetica', label: 'Helvetica' },
|
|
||||||
{ value: 'Times-Roman', label: 'Times' },
|
|
||||||
{ value: 'Courier', label: 'Courier' },
|
|
||||||
{ value: 'Arial', label: 'Arial' },
|
|
||||||
{ value: 'Georgia', label: 'Georgia' },
|
|
||||||
]}
|
|
||||||
disabled={disabled}
|
|
||||||
searchable
|
|
||||||
allowDeselect={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Font Size */}
|
|
||||||
<Combobox
|
|
||||||
onOptionSubmit={(optionValue) => {
|
|
||||||
setFontSizeInput(optionValue);
|
|
||||||
const size = parseInt(optionValue);
|
|
||||||
if (!isNaN(size)) {
|
|
||||||
onParameterChange('fontSize', size);
|
|
||||||
}
|
|
||||||
fontSizeCombobox.closeDropdown();
|
|
||||||
}}
|
|
||||||
store={fontSizeCombobox}
|
|
||||||
withinPortal={false}
|
|
||||||
>
|
|
||||||
<Combobox.Target>
|
|
||||||
<TextInput
|
|
||||||
label="Font Size"
|
|
||||||
placeholder="Type or select font size (8-72)"
|
|
||||||
value={fontSizeInput}
|
|
||||||
onChange={(event) => {
|
|
||||||
const value = event.currentTarget.value;
|
|
||||||
setFontSizeInput(value);
|
|
||||||
|
|
||||||
// Parse and validate the typed value in real-time
|
|
||||||
const size = parseInt(value);
|
|
||||||
if (!isNaN(size) && size >= 8 && size <= 72) {
|
|
||||||
onParameterChange('fontSize', size);
|
|
||||||
}
|
|
||||||
|
|
||||||
fontSizeCombobox.openDropdown();
|
|
||||||
fontSizeCombobox.updateSelectedOptionIndex();
|
|
||||||
}}
|
|
||||||
onClick={() => fontSizeCombobox.openDropdown()}
|
|
||||||
onFocus={() => fontSizeCombobox.openDropdown()}
|
|
||||||
onBlur={() => {
|
|
||||||
fontSizeCombobox.closeDropdown();
|
|
||||||
// Clean up invalid values on blur
|
|
||||||
const size = parseInt(fontSizeInput);
|
|
||||||
if (isNaN(size) || size < 8 || size > 72) {
|
|
||||||
setFontSizeInput((parameters.fontSize || 16).toString());
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Combobox.Target>
|
|
||||||
|
|
||||||
<Combobox.Dropdown>
|
|
||||||
<Combobox.Options>
|
|
||||||
{['8', '12', '16', '20', '24', '28', '32', '36', '40', '48'].map((size) => (
|
|
||||||
<Combobox.Option value={size} key={size}>
|
|
||||||
{size}px
|
|
||||||
</Combobox.Option>
|
|
||||||
))}
|
|
||||||
</Combobox.Options>
|
|
||||||
</Combobox.Dropdown>
|
|
||||||
</Combobox>
|
|
||||||
</Stack>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Direct PDF Drawing */}
|
{/* Direct PDF Drawing */}
|
||||||
@ -692,23 +244,37 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv
|
|||||||
{/* Drawing Controls */}
|
{/* Drawing Controls */}
|
||||||
<Group gap="md" align="flex-end">
|
<Group gap="md" align="flex-end">
|
||||||
{/* Color Picker */}
|
{/* Color Picker */}
|
||||||
<ColorSwatch
|
<div>
|
||||||
color={selectedColor}
|
<Text size="sm" fw={500} mb="xs">Color</Text>
|
||||||
size={24}
|
<div
|
||||||
radius={0}
|
style={{
|
||||||
style={{ cursor: 'pointer' }}
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
backgroundColor: selectedColor,
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: 0,
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
onClick={() => setIsColorPickerOpen(true)}
|
onClick={() => setIsColorPickerOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Pen Size */}
|
{/* Pen Size */}
|
||||||
<div style={{ flexGrow: 1, maxWidth: '200px' }}>
|
<div style={{ flexGrow: 1, maxWidth: '200px' }}>
|
||||||
<Text size="sm" fw={500} mb="xs">Pen Size</Text>
|
<Text size="sm" fw={500} mb="xs">Pen Size</Text>
|
||||||
<PenSizeSelector
|
<input
|
||||||
|
type="number"
|
||||||
value={penSize}
|
value={penSize}
|
||||||
inputValue={penSizeInput}
|
onChange={(e) => {
|
||||||
onValueChange={setPenSize}
|
const size = parseInt(e.target.value);
|
||||||
onInputChange={setPenSizeInput}
|
if (!isNaN(size) && size >= 1 && size <= 200) {
|
||||||
|
setPenSize(size);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
min={1}
|
||||||
|
max={200}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
style={{ width: '100%', padding: '4px 8px' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
@ -716,134 +282,24 @@ const SignSettings = ({ parameters, onParameterChange, disabled = false, onActiv
|
|||||||
</Paper>
|
</Paper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Instructions for placing signature */}
|
{/* Instructions for placing signature */}
|
||||||
{(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' && 'After drawing your signature in the canvas above, click anywhere on the PDF to place it.'}
|
{parameters.signatureType === 'canvas' && 'After drawing your signature in the canvas above, click "Update and Place" then 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 === '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.'}
|
{parameters.signatureType === 'text' && 'After entering your name above, click anywhere on the PDF to place your signature.'}
|
||||||
</Text>
|
</Text>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hidden canvas for modal synchronization - always exists */}
|
|
||||||
<canvas
|
|
||||||
ref={modalCanvasRef}
|
|
||||||
width={800}
|
|
||||||
height={400}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Modal for larger signature canvas */}
|
|
||||||
<Modal
|
|
||||||
opened={isModalOpen}
|
|
||||||
onClose={() => setIsModalOpen(false)}
|
|
||||||
title="Draw Your Signature"
|
|
||||||
size="xl"
|
|
||||||
centered
|
|
||||||
>
|
|
||||||
<Stack gap="md">
|
|
||||||
{/* Color and Pen Size picker */}
|
|
||||||
<Paper withBorder p="sm">
|
|
||||||
<Group gap="lg" align="flex-end">
|
|
||||||
<div>
|
|
||||||
<Text size="sm" fw={500} mb="xs">Color</Text>
|
|
||||||
<ColorSwatch
|
|
||||||
color={selectedColor}
|
|
||||||
size={24}
|
|
||||||
radius={0}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
onClick={() => setIsColorPickerOpen(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Text size="sm" fw={500} mb="xs">Pen Size</Text>
|
|
||||||
<PenSizeSelector
|
|
||||||
value={penSize}
|
|
||||||
inputValue={penSizeInput}
|
|
||||||
onValueChange={setPenSize}
|
|
||||||
onInputChange={setPenSizeInput}
|
|
||||||
placeholder="Size"
|
|
||||||
size="compact-sm"
|
|
||||||
style={{ width: '60px' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Group>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Paper withBorder p="md">
|
|
||||||
<canvas
|
|
||||||
ref={visibleModalCanvasRef}
|
|
||||||
width={800}
|
|
||||||
height={400}
|
|
||||||
style={{
|
|
||||||
border: '1px solid #ccc',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: 'crosshair',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: '800px',
|
|
||||||
height: 'auto',
|
|
||||||
}}
|
|
||||||
onMouseDown={startModalDrawing}
|
|
||||||
onMouseMove={drawModal}
|
|
||||||
onMouseUp={stopModalDrawing}
|
|
||||||
onMouseLeave={stopModalDrawing}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
color="red"
|
|
||||||
onClick={clearModalCanvas}
|
|
||||||
>
|
|
||||||
Clear Canvas
|
|
||||||
</Button>
|
|
||||||
<Group gap="sm">
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={saveModalSignature}
|
|
||||||
>
|
|
||||||
Save Signature
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* Color Picker Modal */}
|
{/* Color Picker Modal */}
|
||||||
<Modal
|
|
||||||
opened={isColorPickerOpen}
|
|
||||||
onClose={() => setIsColorPickerOpen(false)}
|
|
||||||
title="Choose Color"
|
|
||||||
size="sm"
|
|
||||||
centered
|
|
||||||
>
|
|
||||||
<Stack gap="md">
|
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
format="hex"
|
isOpen={isColorPickerOpen}
|
||||||
value={selectedColor}
|
onClose={() => setIsColorPickerOpen(false)}
|
||||||
onChange={setSelectedColor}
|
selectedColor={selectedColor}
|
||||||
swatches={['#000000', '#0066cc', '#cc0000', '#cc6600', '#009900', '#6600cc']}
|
onColorChange={setSelectedColor}
|
||||||
swatchesPerRow={6}
|
|
||||||
size="lg"
|
|
||||||
fullWidth
|
|
||||||
/>
|
/>
|
||||||
<Group justify="flex-end">
|
|
||||||
<Button onClick={() => setIsColorPickerOpen(false)}>
|
|
||||||
Done
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* Save Button */}
|
{/* Save Button */}
|
||||||
{onSave && (
|
{onSave && (
|
||||||
|
@ -82,7 +82,7 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children
|
|||||||
signatureApiRef.current.activateSignaturePlacementMode();
|
signatureApiRef.current.activateSignaturePlacementMode();
|
||||||
setPlacementMode(true);
|
setPlacementMode(true);
|
||||||
}
|
}
|
||||||
}, [state.signatureConfig, setPlacementMode]);
|
}, [setPlacementMode]);
|
||||||
|
|
||||||
const activateDeleteMode = useCallback(() => {
|
const activateDeleteMode = useCallback(() => {
|
||||||
if (signatureApiRef.current) {
|
if (signatureApiRef.current) {
|
||||||
|
@ -25,17 +25,9 @@ const Sign = (props: BaseToolProps) => {
|
|||||||
// Track which signature mode was active for reactivation after save
|
// Track which signature mode was active for reactivation after save
|
||||||
const activeModeRef = useRef<'draw' | 'placement' | null>(null);
|
const activeModeRef = useRef<'draw' | 'placement' | null>(null);
|
||||||
|
|
||||||
// Manual sync function
|
// Single handler that activates placement mode
|
||||||
const syncSignatureConfig = () => {
|
|
||||||
setSignatureConfig(base.params.parameters);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Single handler that syncs first
|
|
||||||
const handleSignaturePlacement = () => {
|
const handleSignaturePlacement = () => {
|
||||||
syncSignatureConfig();
|
|
||||||
setTimeout(() => {
|
|
||||||
activateSignaturePlacementMode();
|
activateSignaturePlacementMode();
|
||||||
}, 100);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const base = useBaseTool(
|
const base = useBaseTool(
|
||||||
|
Loading…
Reference in New Issue
Block a user