SME-Web/src/components/setting/Signature/DrawSignatureCanvas.tsx

309 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;