309 lines
12 KiB
TypeScript
309 lines
12 KiB
TypeScript
import React, { useRef, useState, useEffect } from 'react';
|
||
import { IoMdClose } from 'react-icons/io';
|
||
|
||
type Props = {
|
||
onSignatureChange: (data: { fullSignature: string | null; initials: string | null }) => void;
|
||
initialSignatures?: { fullSignature: string | null; initials: string | null };
|
||
};
|
||
|
||
const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignatures }) => {
|
||
const canvasRef1 = useRef<HTMLCanvasElement>(null);
|
||
const canvasRef2 = useRef<HTMLCanvasElement>(null);
|
||
const [isDrawing, setIsDrawing] = useState(false);
|
||
const [isDrawingType, setIsDrawingType] = useState<{ signatureDrawn: boolean, initialDrawn: boolean }>({ signatureDrawn: false, initialDrawn: false });
|
||
const [paths1, setPaths1] = useState<Array<Array<{ x: number; y: number }>>>([]);
|
||
const [paths2, setPaths2] = useState<Array<Array<{ x: number; y: number }>>>([]);
|
||
const [currentPath, setCurrentPath] = useState<Array<{ x: number; y: number }>>([]);
|
||
const [activeCanvas, setActiveCanvas] = useState<'full' | 'initials' | null>(null);
|
||
const [initialLoaded, setInitialLoaded] = useState(false);
|
||
|
||
// Store initial signature images to avoid reloading
|
||
const [initialImage1, setInitialImage1] = useState<HTMLImageElement | null>(null);
|
||
const [initialImage2, setInitialImage2] = useState<HTMLImageElement | null>(null);
|
||
|
||
// Setup canvas dimensions when component mounts
|
||
useEffect(() => {
|
||
const resizeCanvas = (canvas: HTMLCanvasElement | null) => {
|
||
if (canvas) {
|
||
const rect = canvas.getBoundingClientRect();
|
||
if (rect.width > 0 && rect.height > 0) {
|
||
canvas.width = rect.width;
|
||
canvas.height = rect.height;
|
||
}
|
||
}
|
||
};
|
||
|
||
const observer1 = new ResizeObserver(() => resizeCanvas(canvasRef1.current));
|
||
const observer2 = new ResizeObserver(() => resizeCanvas(canvasRef2.current));
|
||
|
||
if (canvasRef1.current) observer1.observe(canvasRef1.current);
|
||
if (canvasRef2.current) observer2.observe(canvasRef2.current);
|
||
|
||
// initial setup
|
||
resizeCanvas(canvasRef1.current);
|
||
resizeCanvas(canvasRef2.current);
|
||
|
||
return () => {
|
||
observer1.disconnect();
|
||
observer2.disconnect();
|
||
};
|
||
}, []);
|
||
|
||
|
||
const loadInitialSignatures = () => {
|
||
if (!initialSignatures) return;
|
||
|
||
let loadedCount = 0;
|
||
const totalToLoad = (initialSignatures.fullSignature ? 1 : 0) + (initialSignatures.initials ? 1 : 0);
|
||
|
||
const checkComplete = () => {
|
||
loadedCount++;
|
||
if (loadedCount == totalToLoad) {
|
||
setInitialLoaded(true);
|
||
}
|
||
};
|
||
|
||
// Load full signature
|
||
if (initialSignatures.fullSignature && canvasRef1.current) {
|
||
const img1 = new Image();
|
||
img1.onload = () => {
|
||
setInitialImage1(img1);
|
||
const ctx1 = canvasRef1.current?.getContext('2d');
|
||
if (ctx1) {
|
||
ctx1.clearRect(0, 0, canvasRef1.current!.width, canvasRef1.current!.height);
|
||
ctx1.drawImage(img1, 0, 0);
|
||
}
|
||
checkComplete();
|
||
};
|
||
img1.src = initialSignatures.fullSignature;
|
||
}
|
||
|
||
// Load initials
|
||
if (initialSignatures.initials && canvasRef2.current) {
|
||
const img2 = new Image();
|
||
img2.onload = () => {
|
||
setInitialImage2(img2);
|
||
const ctx2 = canvasRef2.current?.getContext('2d');
|
||
if (ctx2) {
|
||
ctx2.clearRect(0, 0, canvasRef2.current!.width, canvasRef2.current!.height);
|
||
ctx2.drawImage(img2, 0, 0);
|
||
}
|
||
checkComplete();
|
||
};
|
||
img2.src = initialSignatures.initials;
|
||
}
|
||
|
||
if (totalToLoad == 0) {
|
||
setInitialLoaded(true);
|
||
}
|
||
};
|
||
|
||
const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||
setIsDrawing(true);
|
||
const rect = e.currentTarget.getBoundingClientRect();
|
||
const point = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
||
setCurrentPath([point]);
|
||
setActiveCanvas(e.currentTarget == canvasRef1.current ? 'full' : 'initials');
|
||
};
|
||
|
||
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>, type: "SIGNATURE" | "INITIAL") => {
|
||
if (!isDrawing) return;
|
||
setIsDrawingType((prev) => {
|
||
return { ...prev, signatureDrawn: type == "SIGNATURE" ? true : prev.signatureDrawn, initialDrawn: type == "INITIAL" ? true : prev.initialDrawn }
|
||
})
|
||
const rect = e.currentTarget.getBoundingClientRect();
|
||
const point = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
||
const newPath = [...currentPath, point];
|
||
setCurrentPath(newPath);
|
||
|
||
// Draw immediately for smooth experience
|
||
drawCurrentPath(e.currentTarget as HTMLCanvasElement, newPath);
|
||
};
|
||
|
||
const drawCurrentPath = (canvas: HTMLCanvasElement, path: Array<{ x: number; y: number }>) => {
|
||
if (!canvas || path.length < 2) return;
|
||
const ctx = canvas.getContext('2d');
|
||
if (!ctx) return;
|
||
|
||
// Only draw the current path segment without clearing the canvas
|
||
ctx.strokeStyle = '#000';
|
||
ctx.lineWidth = 2;
|
||
ctx.lineJoin = 'round';
|
||
ctx.lineCap = 'round';
|
||
|
||
const lastPoint = path[path.length - 2];
|
||
const currentPoint = path[path.length - 1];
|
||
|
||
ctx.beginPath();
|
||
ctx.moveTo(lastPoint.x, lastPoint.y);
|
||
ctx.lineTo(currentPoint.x, currentPoint.y);
|
||
ctx.stroke();
|
||
};
|
||
|
||
const exportCanvasAsImage = (canvas: HTMLCanvasElement | null): string | null => {
|
||
if (!canvas) return null;
|
||
return canvas.toDataURL('image/png');
|
||
};
|
||
|
||
const handleMouseUp = () => {
|
||
setIsDrawing(false);
|
||
|
||
if (currentPath.length == 0 || !activeCanvas) return;
|
||
|
||
if (activeCanvas == 'full') {
|
||
const newPaths = [...paths1, currentPath];
|
||
setPaths1(newPaths);
|
||
} else {
|
||
const newPaths = [...paths2, currentPath];
|
||
setPaths2(newPaths);
|
||
}
|
||
|
||
setCurrentPath([]);
|
||
|
||
// Export both canvases and notify parent
|
||
const fullSignature = exportCanvasAsImage(canvasRef1.current);
|
||
const initials = exportCanvasAsImage(canvasRef2.current);
|
||
onSignatureChange({ fullSignature: (isDrawingType.signatureDrawn) ? fullSignature : null, initials: (isDrawingType.initialDrawn) ? initials : null });
|
||
// onSignatureChange({ fullSignature, initials });
|
||
};
|
||
|
||
const redrawCanvas = (canvas: HTMLCanvasElement, paths: Array<Array<{ x: number; y: number }>>, initialImage: HTMLImageElement | null) => {
|
||
const ctx = canvas.getContext('2d');
|
||
if (!ctx) return;
|
||
|
||
// Clear canvas
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// Draw initial signature if exists
|
||
if (initialImage) {
|
||
ctx.drawImage(initialImage, 0, 0);
|
||
}
|
||
|
||
// Draw all paths
|
||
ctx.strokeStyle = '#000';
|
||
ctx.lineWidth = 2;
|
||
ctx.lineJoin = 'round';
|
||
ctx.lineCap = 'round';
|
||
|
||
paths.forEach((path) => {
|
||
if (path.length < 2) return;
|
||
ctx.beginPath();
|
||
ctx.moveTo(path[0].x, path[0].y);
|
||
for (let i = 1; i < path.length; i++) {
|
||
ctx.lineTo(path[i].x, path[i].y);
|
||
}
|
||
ctx.stroke();
|
||
});
|
||
};
|
||
|
||
const clearCanvas = () => {
|
||
if (canvasRef1.current) {
|
||
const ctx1 = canvasRef1.current.getContext('2d');
|
||
ctx1?.clearRect(0, 0, canvasRef1.current.width, canvasRef1.current.height);
|
||
}
|
||
if (canvasRef2.current) {
|
||
const ctx2 = canvasRef2.current.getContext('2d');
|
||
ctx2?.clearRect(0, 0, canvasRef2.current.width, canvasRef2.current.height);
|
||
}
|
||
|
||
setIsDrawingType({ signatureDrawn: false, initialDrawn: false })
|
||
|
||
setPaths1([]);
|
||
setPaths2([]);
|
||
setInitialImage1(null);
|
||
setInitialImage2(null);
|
||
setInitialLoaded(false);
|
||
onSignatureChange({ fullSignature: null, initials: null });
|
||
};
|
||
|
||
const undoLastPath = () => {
|
||
if (activeCanvas == 'full' && paths1.length > 0) {
|
||
const newPaths = paths1.slice(0, -1);
|
||
setPaths1(newPaths);
|
||
redrawCanvas(canvasRef1.current!, newPaths, initialImage1);
|
||
} else if (activeCanvas == 'initials' && paths2.length > 0) {
|
||
const newPaths = paths2.slice(0, -1);
|
||
setPaths2(newPaths);
|
||
redrawCanvas(canvasRef2.current!, newPaths, initialImage2);
|
||
}
|
||
|
||
// Update parent with new signature data
|
||
setTimeout(() => {
|
||
const fullSignature = exportCanvasAsImage(canvasRef1.current);
|
||
const initials = exportCanvasAsImage(canvasRef2.current);
|
||
onSignatureChange({ fullSignature, initials });
|
||
}, 10);
|
||
};
|
||
|
||
return (
|
||
<div className="lg:px-6 py-4 flex-shrink-0">
|
||
<div className='flex justify-between'>
|
||
{/* <h2 className="text-lg sm:text-xl lg:text-2xl font-semibold text-gray-900 mb-1 sm:mb-2 ">Draw Signature</h2> */}
|
||
<h2 className="mt-4 mb-5 text-sm">Draw your signature. <span className='text-red-600'>*</span> Are required fields.</h2>
|
||
|
||
{/* Controls */}
|
||
<div className="flex gap-2 mt-3">
|
||
<div
|
||
onClick={clearCanvas}
|
||
className='flex justify-center items-center gap-1 font-medium'>
|
||
<IoMdClose className='text-red-500' />
|
||
<button
|
||
className="text-xs text-red-500"
|
||
>
|
||
Clear Signature
|
||
</button>
|
||
</div>
|
||
{/* <button
|
||
className="text-xs"
|
||
onClick={undoLastPath}
|
||
disabled={!activeCanvas || (activeCanvas == 'full' ? paths1.length == 0 : paths2.length == 0)}
|
||
>
|
||
Undo Last
|
||
</button> */}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-y-auto">
|
||
<div className="grid grid-cols-12 gap-4 lg:gap-6 overflow-x-auto">
|
||
{/* Full Signature Canvas */}
|
||
<div className="col-span-8">
|
||
<label className="block text-sm font-medium mb-2">Full Name
|
||
<span className='text-red-600'> *</span>
|
||
</label>
|
||
<div className="border-1 border-gray-300 rounded-lg bg-white hover:bg-gray-50 transition-colors">
|
||
<canvas
|
||
ref={canvasRef1}
|
||
style={{ width: "100%", height: "200px" }}
|
||
className="w-full h-51 cursor-crosshair"
|
||
onPointerDown={handleMouseDown}
|
||
onPointerMove={(e) => handleMouseMove(e, "SIGNATURE")}
|
||
onPointerUp={() => handleMouseUp()}
|
||
onPointerLeave={() => handleMouseUp()}
|
||
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Initials Canvas */}
|
||
<div className="col-span-4">
|
||
<label className="block text-sm font-medium mb-2">Initials
|
||
<span className='text-red-600'> *</span>
|
||
</label>
|
||
<div className="border-1 border-gray-300 rounded-lg bg-white hover:bg-gray-50 transition-colors">
|
||
<canvas
|
||
ref={canvasRef2}
|
||
className="w-full h-51 cursor-crosshair"
|
||
onPointerDown={handleMouseDown}
|
||
onPointerMove={(e) => handleMouseMove(e, "INITIAL")}
|
||
onPointerUp={() => handleMouseUp()}
|
||
onPointerLeave={() => handleMouseUp()}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default DrawSignatureCanvas; |