mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 03:20:46 +02:00
Feature/sign placement UI (#4891)
Show signature preview on hover Place signature smaller (mkatch preview size) Retain signature in modal on close/open Clean up ui generally Small bug fixes
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Stack, Button, Text, Alert, Tabs, SegmentedControl } from '@mantine/core';
|
||||
import { Stack, Button, Text, Alert, SegmentedControl, Divider, ActionIcon, Tooltip, Group, Box } from '@mantine/core';
|
||||
import { SignParameters } from "@app/hooks/tools/sign/useSignParameters";
|
||||
import { SuggestedToolsSection } from "@app/components/tools/shared/SuggestedToolsSection";
|
||||
import { useSignature } from "@app/contexts/SignatureContext";
|
||||
import { useViewer } from "@app/contexts/ViewerContext";
|
||||
import { PLACEMENT_ACTIVATION_DELAY, FILE_SWITCH_ACTIVATION_DELAY } from '@app/constants/signConstants';
|
||||
|
||||
// Import the new reusable components
|
||||
import { DrawingCanvas } from "@app/components/annotation/shared/DrawingCanvas";
|
||||
@@ -11,6 +13,18 @@ import { DrawingControls } from "@app/components/annotation/shared/DrawingContro
|
||||
import { ImageUploader } from "@app/components/annotation/shared/ImageUploader";
|
||||
import { TextInputWithFont } from "@app/components/annotation/shared/TextInputWithFont";
|
||||
import { ColorPicker } from "@app/components/annotation/shared/ColorPicker";
|
||||
import { LocalIcon } from "@app/components/shared/LocalIcon";
|
||||
|
||||
type SignatureDrafts = {
|
||||
canvas?: string;
|
||||
image?: string;
|
||||
text?: {
|
||||
signerName: string;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
textColor: string;
|
||||
};
|
||||
};
|
||||
|
||||
interface SignSettingsProps {
|
||||
parameters: SignParameters;
|
||||
@@ -31,23 +45,64 @@ const SignSettings = ({
|
||||
disabled = false,
|
||||
onActivateSignaturePlacement,
|
||||
onDeactivateSignature,
|
||||
onUpdateDrawSettings,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onSave
|
||||
}: SignSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { isPlacementMode } = useSignature();
|
||||
const { isPlacementMode, signaturesApplied, historyApiRef } = useSignature();
|
||||
const { activeFileIndex } = useViewer();
|
||||
const [historyAvailability, setHistoryAvailability] = useState({ canUndo: false, canRedo: false });
|
||||
const historyApiInstance = historyApiRef.current;
|
||||
|
||||
// State for drawing
|
||||
const [selectedColor, setSelectedColor] = useState('#000000');
|
||||
const [penSize, setPenSize] = useState(2);
|
||||
const [penSizeInput, setPenSizeInput] = useState('2');
|
||||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||
const [interactionMode, setInteractionMode] = useState<'move' | 'place'>('move');
|
||||
const [isPlacementManuallyPaused, setPlacementManuallyPaused] = useState(false);
|
||||
|
||||
// State for different signature types
|
||||
const [canvasSignatureData, setCanvasSignatureData] = useState<string | null>(null);
|
||||
const [imageSignatureData, setImageSignatureData] = useState<string | null>(null);
|
||||
const [canvasSignatureData, setCanvasSignatureData] = useState<string | undefined>();
|
||||
const [imageSignatureData, setImageSignatureData] = useState<string | undefined>();
|
||||
const [signatureDrafts, setSignatureDrafts] = useState<SignatureDrafts>({});
|
||||
const lastSyncedTextDraft = useRef<SignatureDrafts['text'] | null>(null);
|
||||
const lastAppliedPlacementKey = useRef<string | null>(null);
|
||||
const previousFileIndexRef = useRef(activeFileIndex);
|
||||
|
||||
useEffect(() => {
|
||||
if (!disabled) {
|
||||
onUpdateDrawSettings?.(selectedColor, penSize);
|
||||
}
|
||||
}, [selectedColor, penSize, disabled, onUpdateDrawSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (signaturesApplied) {
|
||||
setPlacementManuallyPaused(false);
|
||||
}
|
||||
}, [signaturesApplied]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!historyApiInstance) {
|
||||
setHistoryAvailability({ canUndo: false, canRedo: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const updateAvailability = () => {
|
||||
setHistoryAvailability({
|
||||
canUndo: historyApiInstance.canUndo?.() ?? false,
|
||||
canRedo: historyApiInstance.canRedo?.() ?? false,
|
||||
});
|
||||
};
|
||||
|
||||
const unsubscribe = historyApiInstance.subscribe?.(updateAvailability);
|
||||
updateAvailability();
|
||||
|
||||
return () => {
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [historyApiInstance]);
|
||||
|
||||
// Handle image upload
|
||||
const handleImageChange = async (file: File | null) => {
|
||||
@@ -66,64 +121,209 @@ const SignSettings = ({
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
// Clear any existing canvas signatures when uploading image
|
||||
setCanvasSignatureData(null);
|
||||
setImageSignatureData(result);
|
||||
} catch (error) {
|
||||
console.error('Error reading file:', error);
|
||||
}
|
||||
} else if (!file) {
|
||||
setImageSignatureData(null);
|
||||
if (onDeactivateSignature) {
|
||||
onDeactivateSignature();
|
||||
}
|
||||
setImageSignatureData(undefined);
|
||||
onDeactivateSignature?.();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle signature data changes
|
||||
const handleCanvasSignatureChange = (data: string | null) => {
|
||||
const nextValue = data ?? undefined;
|
||||
setCanvasSignatureData(prev => {
|
||||
if (prev === data) return prev; // Prevent unnecessary updates
|
||||
return data;
|
||||
if (prev === nextValue) {
|
||||
return prev;
|
||||
}
|
||||
return nextValue;
|
||||
});
|
||||
if (data) {
|
||||
// Clear image data when canvas is used
|
||||
setImageSignatureData(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle signature mode deactivation when switching types
|
||||
useEffect(() => {
|
||||
if (parameters.signatureType !== 'text' && onDeactivateSignature) {
|
||||
onDeactivateSignature();
|
||||
const hasCanvasSignature = useMemo(() => Boolean(canvasSignatureData), [canvasSignatureData]);
|
||||
const hasImageSignature = useMemo(() => Boolean(imageSignatureData), [imageSignatureData]);
|
||||
const hasTextSignature = useMemo(
|
||||
() => Boolean(parameters.signerName && parameters.signerName.trim() !== ''),
|
||||
[parameters.signerName]
|
||||
);
|
||||
|
||||
const hasAnySignature = hasCanvasSignature || hasImageSignature || hasTextSignature;
|
||||
|
||||
const isCurrentTypeReady = useMemo(() => {
|
||||
switch (parameters.signatureType) {
|
||||
case 'canvas':
|
||||
return hasCanvasSignature;
|
||||
case 'image':
|
||||
return hasImageSignature;
|
||||
case 'text':
|
||||
return hasTextSignature;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}, [parameters.signatureType]);
|
||||
}, [parameters.signatureType, hasCanvasSignature, hasImageSignature, hasTextSignature]);
|
||||
|
||||
const placementSignatureKey = useMemo(() => {
|
||||
if (!isCurrentTypeReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (parameters.signatureType) {
|
||||
case 'canvas':
|
||||
return canvasSignatureData ?? null;
|
||||
case 'image':
|
||||
return imageSignatureData ?? null;
|
||||
case 'text':
|
||||
return JSON.stringify({
|
||||
signerName: (parameters.signerName ?? '').trim(),
|
||||
fontSize: parameters.fontSize ?? 16,
|
||||
fontFamily: parameters.fontFamily ?? 'Helvetica',
|
||||
textColor: parameters.textColor ?? '#000000',
|
||||
});
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [
|
||||
isCurrentTypeReady,
|
||||
parameters.signatureType,
|
||||
canvasSignatureData,
|
||||
imageSignatureData,
|
||||
parameters.signerName,
|
||||
parameters.fontSize,
|
||||
parameters.fontFamily,
|
||||
parameters.textColor,
|
||||
]);
|
||||
|
||||
const shouldEnablePlacement = useMemo(() => {
|
||||
if (disabled) return false;
|
||||
return isCurrentTypeReady;
|
||||
}, [disabled, isCurrentTypeReady]);
|
||||
|
||||
const shouldAutoActivate = shouldEnablePlacement && !isPlacementManuallyPaused && !signaturesApplied;
|
||||
|
||||
// Handle text signature activation (including fontSize and fontFamily changes)
|
||||
useEffect(() => {
|
||||
if (parameters.signatureType === 'text' && parameters.signerName && parameters.signerName.trim() !== '') {
|
||||
if (onActivateSignaturePlacement) {
|
||||
setInteractionMode('place');
|
||||
setTimeout(() => {
|
||||
onActivateSignaturePlacement();
|
||||
}, 100);
|
||||
setSignatureDrafts(prev => {
|
||||
if (canvasSignatureData) {
|
||||
if (prev.canvas === canvasSignatureData) {
|
||||
return prev;
|
||||
}
|
||||
return { ...prev, canvas: canvasSignatureData };
|
||||
}
|
||||
} else if (parameters.signatureType === 'text' && (!parameters.signerName || parameters.signerName.trim() === '')) {
|
||||
if (onDeactivateSignature) {
|
||||
setInteractionMode('move');
|
||||
onDeactivateSignature();
|
||||
|
||||
if (prev.canvas !== undefined) {
|
||||
const next = { ...prev };
|
||||
delete next.canvas;
|
||||
return next;
|
||||
}
|
||||
}
|
||||
}, [parameters.signatureType, parameters.signerName, parameters.fontSize, parameters.fontFamily, onActivateSignaturePlacement, onDeactivateSignature]);
|
||||
|
||||
// Reset to move mode when placement mode is deactivated
|
||||
return prev;
|
||||
});
|
||||
}, [canvasSignatureData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlacementMode && interactionMode === 'place') {
|
||||
setInteractionMode('move');
|
||||
setSignatureDrafts(prev => {
|
||||
if (imageSignatureData) {
|
||||
if (prev.image === imageSignatureData) {
|
||||
return prev;
|
||||
}
|
||||
return { ...prev, image: imageSignatureData };
|
||||
}
|
||||
|
||||
if (prev.image !== undefined) {
|
||||
const next = { ...prev };
|
||||
delete next.image;
|
||||
return next;
|
||||
}
|
||||
|
||||
return prev;
|
||||
});
|
||||
}, [imageSignatureData]);
|
||||
|
||||
useEffect(() => {
|
||||
const nextDraft = {
|
||||
signerName: parameters.signerName || '',
|
||||
fontSize: parameters.fontSize || 16,
|
||||
fontFamily: parameters.fontFamily || 'Helvetica',
|
||||
textColor: parameters.textColor || '#000000',
|
||||
};
|
||||
|
||||
setSignatureDrafts(prev => {
|
||||
const prevDraft = prev.text;
|
||||
if (
|
||||
prevDraft &&
|
||||
prevDraft.signerName === nextDraft.signerName &&
|
||||
prevDraft.fontSize === nextDraft.fontSize &&
|
||||
prevDraft.fontFamily === nextDraft.fontFamily &&
|
||||
prevDraft.textColor === nextDraft.textColor
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return { ...prev, text: nextDraft };
|
||||
});
|
||||
}, [parameters.signerName, parameters.fontSize, parameters.fontFamily, parameters.textColor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (parameters.signatureType === 'text') {
|
||||
const draft = signatureDrafts.text;
|
||||
if (!draft) {
|
||||
lastSyncedTextDraft.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSignerName = parameters.signerName ?? '';
|
||||
const currentFontSize = parameters.fontSize ?? 16;
|
||||
const currentFontFamily = parameters.fontFamily ?? 'Helvetica';
|
||||
const currentTextColor = parameters.textColor ?? '#000000';
|
||||
|
||||
const isSynced =
|
||||
draft.signerName === currentSignerName &&
|
||||
draft.fontSize === currentFontSize &&
|
||||
draft.fontFamily === currentFontFamily &&
|
||||
draft.textColor === currentTextColor;
|
||||
|
||||
if (isSynced) {
|
||||
lastSyncedTextDraft.current = draft;
|
||||
return;
|
||||
}
|
||||
|
||||
const lastSynced = lastSyncedTextDraft.current;
|
||||
const alreadyAttempted =
|
||||
lastSynced &&
|
||||
lastSynced.signerName === draft.signerName &&
|
||||
lastSynced.fontSize === draft.fontSize &&
|
||||
lastSynced.fontFamily === draft.fontFamily &&
|
||||
lastSynced.textColor === draft.textColor;
|
||||
|
||||
if (!alreadyAttempted) {
|
||||
lastSyncedTextDraft.current = draft;
|
||||
if (draft.signerName !== currentSignerName) {
|
||||
onParameterChange('signerName', draft.signerName);
|
||||
}
|
||||
if (draft.fontSize !== currentFontSize) {
|
||||
onParameterChange('fontSize', draft.fontSize);
|
||||
}
|
||||
if (draft.fontFamily !== currentFontFamily) {
|
||||
onParameterChange('fontFamily', draft.fontFamily);
|
||||
}
|
||||
if (draft.textColor !== currentTextColor) {
|
||||
onParameterChange('textColor', draft.textColor);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lastSyncedTextDraft.current = null;
|
||||
}
|
||||
}, [isPlacementMode, interactionMode]);
|
||||
}, [
|
||||
parameters.signatureType,
|
||||
parameters.signerName,
|
||||
parameters.fontSize,
|
||||
parameters.fontFamily,
|
||||
parameters.textColor,
|
||||
signatureDrafts.text,
|
||||
onParameterChange,
|
||||
]);
|
||||
|
||||
// Handle signature data updates
|
||||
useEffect(() => {
|
||||
let newSignatureData: string | undefined = undefined;
|
||||
|
||||
@@ -133,71 +333,99 @@ const SignSettings = ({
|
||||
newSignatureData = canvasSignatureData;
|
||||
}
|
||||
|
||||
// Only update if the signature data has actually changed
|
||||
if (parameters.signatureData !== newSignatureData) {
|
||||
onParameterChange('signatureData', newSignatureData);
|
||||
}
|
||||
}, [parameters.signatureType, parameters.signatureData, canvasSignatureData, imageSignatureData]);
|
||||
}, [parameters.signatureType, parameters.signatureData, canvasSignatureData, imageSignatureData, onParameterChange]);
|
||||
|
||||
// Handle image signature activation - activate when image data syncs with parameters
|
||||
useEffect(() => {
|
||||
if (parameters.signatureType === 'image' && imageSignatureData && parameters.signatureData === imageSignatureData && onActivateSignaturePlacement) {
|
||||
setInteractionMode('place');
|
||||
setTimeout(() => {
|
||||
onActivateSignaturePlacement();
|
||||
}, 100);
|
||||
if (!shouldEnablePlacement) {
|
||||
if (isPlacementMode) {
|
||||
onDeactivateSignature?.();
|
||||
}
|
||||
if (isPlacementManuallyPaused) {
|
||||
setPlacementManuallyPaused(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}, [parameters.signatureType, parameters.signatureData, imageSignatureData]);
|
||||
|
||||
// Handle canvas signature activation - activate when canvas data syncs with parameters
|
||||
if (!shouldAutoActivate || isPlacementMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const timer = window.setTimeout(() => {
|
||||
onActivateSignaturePlacement?.();
|
||||
}, PLACEMENT_ACTIVATION_DELAY);
|
||||
return () => window.clearTimeout(timer);
|
||||
}
|
||||
|
||||
onActivateSignaturePlacement?.();
|
||||
}, [
|
||||
shouldEnablePlacement,
|
||||
shouldAutoActivate,
|
||||
isPlacementMode,
|
||||
isPlacementManuallyPaused,
|
||||
onActivateSignaturePlacement,
|
||||
onDeactivateSignature,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (parameters.signatureType === 'canvas' && canvasSignatureData && parameters.signatureData === canvasSignatureData && onActivateSignaturePlacement) {
|
||||
setInteractionMode('place');
|
||||
setTimeout(() => {
|
||||
onActivateSignaturePlacement();
|
||||
}, 100);
|
||||
if (!shouldAutoActivate || !placementSignatureKey) {
|
||||
if (!shouldEnablePlacement || !shouldAutoActivate) {
|
||||
lastAppliedPlacementKey.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}, [parameters.signatureType, parameters.signatureData, canvasSignatureData]);
|
||||
|
||||
// Draw settings are no longer needed since draw mode is removed
|
||||
if (!isPlacementMode) {
|
||||
lastAppliedPlacementKey.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Signature Type Selection */}
|
||||
<Tabs
|
||||
value={parameters.signatureType}
|
||||
onChange={(value) => onParameterChange('signatureType', value as 'image' | 'text' | 'canvas')}
|
||||
>
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab value="canvas" style={{ fontSize: '0.8rem' }}>
|
||||
{t('sign.type.canvas', 'Canvas')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="image" style={{ fontSize: '0.8rem' }}>
|
||||
{t('sign.type.image', 'Image')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="text" style={{ fontSize: '0.8rem' }}>
|
||||
{t('sign.type.text', 'Text')}
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
if (lastAppliedPlacementKey.current === placementSignatureKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
{/* Drawing Controls */}
|
||||
<DrawingControls
|
||||
onUndo={onUndo}
|
||||
onRedo={onRedo}
|
||||
onPlaceSignature={() => {
|
||||
if (onActivateSignaturePlacement) {
|
||||
onActivateSignaturePlacement();
|
||||
}
|
||||
}}
|
||||
hasSignatureData={!!(canvasSignatureData || imageSignatureData || (parameters.signerName && parameters.signerName.trim() !== ''))}
|
||||
disabled={disabled}
|
||||
showPlaceButton={false}
|
||||
placeButtonText={t('sign.updateAndPlace', 'Update and Place')}
|
||||
/>
|
||||
const trigger = () => {
|
||||
onActivateSignaturePlacement?.();
|
||||
lastAppliedPlacementKey.current = placementSignatureKey;
|
||||
};
|
||||
|
||||
{/* Signature Creation based on type */}
|
||||
{parameters.signatureType === 'canvas' && (
|
||||
if (typeof window !== 'undefined') {
|
||||
const timer = window.setTimeout(trigger, PLACEMENT_ACTIVATION_DELAY);
|
||||
return () => window.clearTimeout(timer);
|
||||
}
|
||||
|
||||
trigger();
|
||||
}, [placementSignatureKey, shouldAutoActivate, shouldEnablePlacement, isPlacementMode, onActivateSignaturePlacement]);
|
||||
useEffect(() => {
|
||||
if (activeFileIndex === previousFileIndexRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
previousFileIndexRef.current = activeFileIndex;
|
||||
|
||||
if (!shouldEnablePlacement || signaturesApplied) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPlacementManuallyPaused(false);
|
||||
lastAppliedPlacementKey.current = null;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const timer = window.setTimeout(() => {
|
||||
onActivateSignaturePlacement?.();
|
||||
}, FILE_SWITCH_ACTIVATION_DELAY);
|
||||
return () => window.clearTimeout(timer);
|
||||
}
|
||||
|
||||
onActivateSignaturePlacement?.();
|
||||
}, [activeFileIndex, shouldEnablePlacement, signaturesApplied, onActivateSignaturePlacement]);
|
||||
|
||||
const renderSignatureBuilder = () => {
|
||||
if (parameters.signatureType === 'canvas') {
|
||||
return (
|
||||
<DrawingCanvas
|
||||
selectedColor={selectedColor}
|
||||
penSize={penSize}
|
||||
@@ -207,92 +435,204 @@ const SignSettings = ({
|
||||
onPenSizeInputChange={setPenSizeInput}
|
||||
onSignatureDataChange={handleCanvasSignatureChange}
|
||||
onDrawingComplete={() => {
|
||||
if (onActivateSignaturePlacement) {
|
||||
onActivateSignaturePlacement();
|
||||
}
|
||||
onActivateSignaturePlacement?.();
|
||||
}}
|
||||
disabled={disabled}
|
||||
additionalButtons={
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (onActivateSignaturePlacement) {
|
||||
onActivateSignaturePlacement();
|
||||
}
|
||||
}}
|
||||
color="blue"
|
||||
variant="filled"
|
||||
disabled={disabled || !canvasSignatureData}
|
||||
>
|
||||
{t('sign.updateAndPlace', 'Update and Place')}
|
||||
</Button>
|
||||
}
|
||||
initialSignatureData={canvasSignatureData}
|
||||
/>
|
||||
)}
|
||||
);
|
||||
}
|
||||
|
||||
{parameters.signatureType === 'image' && (
|
||||
if (parameters.signatureType === 'image') {
|
||||
return (
|
||||
<ImageUploader
|
||||
onImageChange={handleImageChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
);
|
||||
}
|
||||
|
||||
{parameters.signatureType === 'text' && (
|
||||
<TextInputWithFont
|
||||
text={parameters.signerName || ''}
|
||||
onTextChange={(text) => onParameterChange('signerName', text)}
|
||||
fontSize={parameters.fontSize || 16}
|
||||
onFontSizeChange={(size) => onParameterChange('fontSize', size)}
|
||||
fontFamily={parameters.fontFamily || 'Helvetica'}
|
||||
onFontFamilyChange={(family) => onParameterChange('fontFamily', family)}
|
||||
textColor={parameters.textColor || '#000000'}
|
||||
onTextColorChange={(color) => onParameterChange('textColor', color)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
return (
|
||||
<TextInputWithFont
|
||||
text={parameters.signerName || ''}
|
||||
onTextChange={(text) => onParameterChange('signerName', text)}
|
||||
fontSize={parameters.fontSize || 16}
|
||||
onFontSizeChange={(size) => onParameterChange('fontSize', size)}
|
||||
fontFamily={parameters.fontFamily || 'Helvetica'}
|
||||
onFontFamilyChange={(family) => onParameterChange('fontFamily', family)}
|
||||
textColor={parameters.textColor || '#000000'}
|
||||
onTextColorChange={(color) => onParameterChange('textColor', color)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const placementInstructions = () => {
|
||||
if (parameters.signatureType === 'canvas') {
|
||||
return t('sign.instructions.canvas', 'After drawing your signature and closing the canvas, click anywhere on the PDF to place it.');
|
||||
}
|
||||
if (parameters.signatureType === 'image') {
|
||||
return t('sign.instructions.image', 'After uploading your signature image, click anywhere on the PDF to place it.');
|
||||
}
|
||||
return t('sign.instructions.text', 'After entering your name above, click anywhere on the PDF to place your signature.');
|
||||
};
|
||||
|
||||
{/* Interaction Mode Toggle */}
|
||||
{(canvasSignatureData || imageSignatureData || (parameters.signerName && parameters.signerName.trim() !== '')) && (
|
||||
<SegmentedControl
|
||||
value={interactionMode}
|
||||
onChange={(value) => {
|
||||
setInteractionMode(value as 'move' | 'place');
|
||||
if (value === 'place') {
|
||||
if (onActivateSignaturePlacement) {
|
||||
onActivateSignaturePlacement();
|
||||
}
|
||||
} else {
|
||||
if (onDeactivateSignature) {
|
||||
onDeactivateSignature();
|
||||
}
|
||||
}
|
||||
}}
|
||||
data={[
|
||||
{ label: t('sign.mode.move', 'Move Signature'), value: 'move' },
|
||||
{ label: t('sign.mode.place', 'Place Signature'), value: 'place' }
|
||||
]}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
const placementAlert = isCurrentTypeReady
|
||||
? {
|
||||
color: isPlacementMode ? 'blue' : 'teal',
|
||||
title: isPlacementMode
|
||||
? t('sign.instructions.title', 'How to add your signature')
|
||||
: t('sign.instructions.paused', 'Placement paused'),
|
||||
message: isPlacementMode
|
||||
? placementInstructions()
|
||||
: t('sign.instructions.resumeHint', 'Resume placement to click and add your signature.'),
|
||||
}
|
||||
: {
|
||||
color: 'yellow',
|
||||
title: t('sign.instructions.title', 'How to add your signature'),
|
||||
message: t('sign.instructions.noSignature', 'Create a signature above to enable placement tools.'),
|
||||
};
|
||||
|
||||
{/* Instructions for placing signature */}
|
||||
<Alert color="blue" title={t('sign.instructions.title', 'How to add signature')}>
|
||||
<Text size="sm">
|
||||
{parameters.signatureType === 'canvas' && t('sign.instructions.canvas', 'After drawing your signature in the canvas, close the modal then click anywhere on the PDF to place it.')}
|
||||
{parameters.signatureType === 'image' && t('sign.instructions.image', 'After uploading your signature image above, click anywhere on the PDF to place it.')}
|
||||
{parameters.signatureType === 'text' && t('sign.instructions.text', 'After entering your name above, click anywhere on the PDF to place your signature.')}
|
||||
const handlePausePlacement = () => {
|
||||
setPlacementManuallyPaused(true);
|
||||
onDeactivateSignature?.();
|
||||
};
|
||||
|
||||
const handleResumePlacement = () => {
|
||||
setPlacementManuallyPaused(false);
|
||||
onActivateSignaturePlacement?.();
|
||||
};
|
||||
|
||||
// Handle Escape key to toggle pause/resume
|
||||
useEffect(() => {
|
||||
if (!isCurrentTypeReady) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
if (isPlacementMode) {
|
||||
handlePausePlacement();
|
||||
} else if (isPlacementManuallyPaused) {
|
||||
handleResumePlacement();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isCurrentTypeReady, isPlacementMode, isPlacementManuallyPaused]);
|
||||
|
||||
const placementToggleControl =
|
||||
onActivateSignaturePlacement || onDeactivateSignature
|
||||
? isPlacementMode
|
||||
? (
|
||||
<Tooltip label={t('sign.mode.pause', 'Pause placement')}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label={t('sign.mode.pause', 'Pause placement')}
|
||||
onClick={handlePausePlacement}
|
||||
disabled={disabled || !onDeactivateSignature}
|
||||
style={{
|
||||
width: 'auto',
|
||||
paddingInline: '0.75rem',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.4rem',
|
||||
}}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:pause-rounded" width={20} height={20} />
|
||||
<Text component="span" size="sm" fw={500}>
|
||||
{t('sign.mode.pause', 'Pause placement')}
|
||||
</Text>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)
|
||||
: (
|
||||
<Tooltip label={t('sign.mode.resume', 'Resume placement')}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label={t('sign.mode.resume', 'Resume placement')}
|
||||
onClick={handleResumePlacement}
|
||||
disabled={disabled || !isCurrentTypeReady || !onActivateSignaturePlacement}
|
||||
style={{
|
||||
width: 'auto',
|
||||
paddingInline: '0.75rem',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.4rem',
|
||||
}}
|
||||
>
|
||||
<LocalIcon icon="material-symbols:play-arrow-rounded" width={20} height={20} />
|
||||
<Text component="span" size="sm" fw={500}>
|
||||
{t('sign.mode.resume', 'Resume placement')}
|
||||
</Text>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('sign.step.createDesc', 'Choose how you want to create the signature')}
|
||||
</Text>
|
||||
</Alert>
|
||||
<SegmentedControl
|
||||
value={parameters.signatureType}
|
||||
fullWidth
|
||||
onChange={(value) => onParameterChange('signatureType', value as 'image' | 'text' | 'canvas')}
|
||||
data={[
|
||||
{ label: t('sign.type.canvas', 'Draw'), value: 'canvas' },
|
||||
{ label: t('sign.type.image', 'Upload'), value: 'image' },
|
||||
{ label: t('sign.type.text', 'Type'), value: 'text' },
|
||||
]}
|
||||
/>
|
||||
{renderSignatureBuilder()}
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack gap="sm">
|
||||
<Text fw={600} size="md">
|
||||
{t('sign.step.place', 'Place & save')}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('sign.step.placeDesc', 'Position the signature on your PDF')}
|
||||
</Text>
|
||||
|
||||
<Group gap="xs" wrap="nowrap" align="center">
|
||||
<DrawingControls
|
||||
onUndo={onUndo}
|
||||
onRedo={onRedo}
|
||||
canUndo={historyAvailability.canUndo}
|
||||
canRedo={historyAvailability.canRedo}
|
||||
hasSignatureData={hasAnySignature}
|
||||
disabled={disabled}
|
||||
showPlaceButton={false}
|
||||
/>
|
||||
<Box style={{ marginLeft: 'auto' }}>
|
||||
{placementToggleControl}
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
<Alert color={placementAlert.color} title={placementAlert.title}>
|
||||
<Text size="sm">
|
||||
{placementAlert.message}
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
</Stack>
|
||||
|
||||
{/* Color Picker Modal */}
|
||||
<ColorPicker
|
||||
isOpen={isColorPickerOpen}
|
||||
onClose={() => setIsColorPickerOpen(false)}
|
||||
selectedColor={selectedColor}
|
||||
onColorChange={setSelectedColor}
|
||||
title={t('sign.canvas.colorPickerTitle', 'Choose stroke colour')}
|
||||
/>
|
||||
|
||||
{/* Apply Signatures Button */}
|
||||
{onSave && (
|
||||
<Button
|
||||
onClick={onSave}
|
||||
@@ -304,7 +644,6 @@ const SignSettings = ({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Suggested Tools Section */}
|
||||
<SuggestedToolsSection />
|
||||
</Stack>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user