mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-04 01:31:28 +00:00
feat: Précablage du formulaire dynamique [N3WTS-17]
This commit is contained in:
346
Front-End/src/components/Form/SignatureField.js
Normal file
346
Front-End/src/components/Form/SignatureField.js
Normal file
@ -0,0 +1,346 @@
|
||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
|
||||
const SignatureField = ({
|
||||
label = 'Signature',
|
||||
required = false,
|
||||
onChange,
|
||||
value,
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
backgroundColor = '#ffffff',
|
||||
penColor = '#000000',
|
||||
penWidth = 2,
|
||||
}) => {
|
||||
const canvasRef = useRef(null);
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
const [lastPosition, setLastPosition] = useState({ x: 0, y: 0 });
|
||||
const [isEmpty, setIsEmpty] = useState(true);
|
||||
const [svgPaths, setSvgPaths] = useState([]);
|
||||
const [currentPath, setCurrentPath] = useState('');
|
||||
const [smoothingPoints, setSmoothingPoints] = useState([]);
|
||||
|
||||
// Initialiser le canvas
|
||||
const initializeCanvas = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
// Support High DPI / Retina displays
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
const displayWidth = 400;
|
||||
const displayHeight = 200;
|
||||
|
||||
// Ajuster la taille physique du canvas pour la haute résolution
|
||||
canvas.width = displayWidth * devicePixelRatio;
|
||||
canvas.height = displayHeight * devicePixelRatio;
|
||||
|
||||
// Maintenir la taille d'affichage
|
||||
canvas.style.width = displayWidth + 'px';
|
||||
canvas.style.height = displayHeight + 'px';
|
||||
|
||||
// Adapter le contexte à la densité de pixels
|
||||
context.scale(devicePixelRatio, devicePixelRatio);
|
||||
|
||||
// Améliorer l'anti-aliasing et le rendu
|
||||
context.imageSmoothingEnabled = true;
|
||||
context.imageSmoothingQuality = 'high';
|
||||
context.textRenderingOptimization = 'optimizeQuality';
|
||||
|
||||
// Configuration du style de dessin
|
||||
context.fillStyle = backgroundColor;
|
||||
context.fillRect(0, 0, displayWidth, displayHeight);
|
||||
context.strokeStyle = penColor;
|
||||
context.lineWidth = penWidth;
|
||||
context.lineCap = 'round';
|
||||
context.lineJoin = 'round';
|
||||
context.globalCompositeOperation = 'source-over';
|
||||
}, [backgroundColor, penColor, penWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
initializeCanvas();
|
||||
|
||||
// Si une valeur est fournie (signature existante), la charger
|
||||
if (value && value !== '') {
|
||||
const canvas = canvasRef.current;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (value.includes('svg+xml')) {
|
||||
// Charger une signature SVG
|
||||
const svgData = atob(value.split(',')[1]);
|
||||
const img = new Image();
|
||||
const svg = new Blob([svgData], {
|
||||
type: 'image/svg+xml;charset=utf-8',
|
||||
});
|
||||
const url = URL.createObjectURL(svg);
|
||||
|
||||
img.onload = () => {
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.fillStyle = backgroundColor;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
context.drawImage(img, 0, 0);
|
||||
setIsEmpty(false);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
img.src = url;
|
||||
} else {
|
||||
// Charger une image classique
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.fillStyle = backgroundColor;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
context.drawImage(img, 0, 0);
|
||||
setIsEmpty(false);
|
||||
};
|
||||
img.src = value;
|
||||
}
|
||||
}
|
||||
}, [value, initializeCanvas, backgroundColor]);
|
||||
|
||||
// Obtenir les coordonnées relatives au canvas
|
||||
const getCanvasPosition = (e) => {
|
||||
const canvas = canvasRef.current;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = canvas.style.width
|
||||
? parseFloat(canvas.style.width) / rect.width
|
||||
: 1;
|
||||
const scaleY = canvas.style.height
|
||||
? parseFloat(canvas.style.height) / rect.height
|
||||
: 1;
|
||||
|
||||
return {
|
||||
x: (e.clientX - rect.left) * scaleX,
|
||||
y: (e.clientY - rect.top) * scaleY,
|
||||
};
|
||||
};
|
||||
|
||||
// Obtenir les coordonnées pour les événements tactiles
|
||||
const getTouchPosition = (e) => {
|
||||
const canvas = canvasRef.current;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = canvas.style.width
|
||||
? parseFloat(canvas.style.width) / rect.width
|
||||
: 1;
|
||||
const scaleY = canvas.style.height
|
||||
? parseFloat(canvas.style.height) / rect.height
|
||||
: 1;
|
||||
|
||||
return {
|
||||
x: (e.touches[0].clientX - rect.left) * scaleX,
|
||||
y: (e.touches[0].clientY - rect.top) * scaleY,
|
||||
};
|
||||
};
|
||||
|
||||
// Commencer le dessin
|
||||
const startDrawing = useCallback(
|
||||
(e) => {
|
||||
if (disabled || readOnly) return;
|
||||
|
||||
e.preventDefault();
|
||||
setIsDrawing(true);
|
||||
|
||||
const pos = e.type.includes('touch')
|
||||
? getTouchPosition(e)
|
||||
: getCanvasPosition(e);
|
||||
setLastPosition(pos);
|
||||
|
||||
// Commencer un nouveau path SVG
|
||||
setCurrentPath(`M ${pos.x},${pos.y}`);
|
||||
setSmoothingPoints([pos]);
|
||||
},
|
||||
[disabled, readOnly]
|
||||
);
|
||||
|
||||
// Dessiner
|
||||
const draw = useCallback(
|
||||
(e) => {
|
||||
if (!isDrawing || disabled || readOnly) return;
|
||||
|
||||
e.preventDefault();
|
||||
const canvas = canvasRef.current;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
const currentPos = e.type.includes('touch')
|
||||
? getTouchPosition(e)
|
||||
: getCanvasPosition(e);
|
||||
|
||||
// Calculer la distance pour déterminer si on doit interpoler
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(currentPos.x - lastPosition.x, 2) +
|
||||
Math.pow(currentPos.y - lastPosition.y, 2)
|
||||
);
|
||||
|
||||
// Si la distance est grande, interpoler pour un tracé plus lisse
|
||||
if (distance > 2) {
|
||||
const midPoint = {
|
||||
x: (lastPosition.x + currentPos.x) / 2,
|
||||
y: (lastPosition.y + currentPos.y) / 2,
|
||||
};
|
||||
|
||||
// Utiliser une courbe quadratique pour un tracé plus lisse
|
||||
context.beginPath();
|
||||
context.moveTo(lastPosition.x, lastPosition.y);
|
||||
context.quadraticCurveTo(
|
||||
lastPosition.x,
|
||||
lastPosition.y,
|
||||
midPoint.x,
|
||||
midPoint.y
|
||||
);
|
||||
context.stroke();
|
||||
|
||||
setLastPosition(midPoint);
|
||||
setCurrentPath(
|
||||
(prev) =>
|
||||
prev +
|
||||
` Q ${lastPosition.x},${lastPosition.y} ${midPoint.x},${midPoint.y}`
|
||||
);
|
||||
} else {
|
||||
// Tracé direct pour les mouvements lents
|
||||
context.beginPath();
|
||||
context.moveTo(lastPosition.x, lastPosition.y);
|
||||
context.lineTo(currentPos.x, currentPos.y);
|
||||
context.stroke();
|
||||
|
||||
setLastPosition(currentPos);
|
||||
setCurrentPath((prev) => prev + ` L ${currentPos.x},${currentPos.y}`);
|
||||
}
|
||||
|
||||
setIsEmpty(false);
|
||||
},
|
||||
[isDrawing, lastPosition, disabled]
|
||||
);
|
||||
|
||||
// Arrêter le dessin
|
||||
const stopDrawing = useCallback(
|
||||
(e) => {
|
||||
if (!isDrawing) return;
|
||||
|
||||
e.preventDefault();
|
||||
setIsDrawing(false);
|
||||
|
||||
// Ajouter le path terminé aux paths SVG
|
||||
if (currentPath) {
|
||||
setSvgPaths((prev) => [...prev, currentPath]);
|
||||
setCurrentPath('');
|
||||
}
|
||||
|
||||
// Notifier le parent du changement avec SVG
|
||||
if (onChange) {
|
||||
const newPaths = [...svgPaths, currentPath].filter((p) => p.length > 0);
|
||||
const svgData = generateSVG(newPaths);
|
||||
onChange(svgData);
|
||||
}
|
||||
},
|
||||
[isDrawing, onChange, svgPaths, currentPath]
|
||||
);
|
||||
|
||||
// Générer le SVG à partir des paths
|
||||
const generateSVG = (paths) => {
|
||||
const svgContent = `<svg width="400" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100%" height="100%" fill="${backgroundColor}"/>
|
||||
${paths
|
||||
.map(
|
||||
(path) =>
|
||||
`<path d="${path}" stroke="${penColor}" stroke-width="${penWidth}" stroke-linecap="round" stroke-linejoin="round" fill="none"/>`
|
||||
)
|
||||
.join('\n ')}
|
||||
</svg>`;
|
||||
|
||||
return `data:image/svg+xml;base64,${btoa(svgContent)}`;
|
||||
};
|
||||
|
||||
// Effacer la signature
|
||||
const clearSignature = () => {
|
||||
const canvas = canvasRef.current;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
// Effacer en tenant compte des dimensions d'affichage
|
||||
const displayWidth = 400;
|
||||
const displayHeight = 200;
|
||||
|
||||
context.clearRect(0, 0, displayWidth, displayHeight);
|
||||
context.fillStyle = backgroundColor;
|
||||
context.fillRect(0, 0, displayWidth, displayHeight);
|
||||
setIsEmpty(true);
|
||||
setSvgPaths([]);
|
||||
setCurrentPath('');
|
||||
setSmoothingPoints([]);
|
||||
|
||||
if (onChange) {
|
||||
onChange('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="signature-field">
|
||||
{label && (
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div className="border border-gray-300 rounded-lg p-4 bg-gray-50">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={`border border-gray-200 bg-white rounded touch-none ${
|
||||
readOnly ? 'cursor-default' : 'cursor-crosshair'
|
||||
}`}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
height: 'auto',
|
||||
opacity: disabled || readOnly ? 0.7 : 1,
|
||||
cursor: disabled
|
||||
? 'not-allowed'
|
||||
: readOnly
|
||||
? 'default'
|
||||
: 'crosshair',
|
||||
}}
|
||||
onMouseDown={readOnly ? undefined : startDrawing}
|
||||
onMouseMove={readOnly ? undefined : draw}
|
||||
onMouseUp={readOnly ? undefined : stopDrawing}
|
||||
onMouseLeave={readOnly ? undefined : stopDrawing}
|
||||
onTouchStart={readOnly ? undefined : startDrawing}
|
||||
onTouchMove={readOnly ? undefined : draw}
|
||||
onTouchEnd={readOnly ? undefined : stopDrawing}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mt-3">
|
||||
<div className="text-xs text-gray-500">
|
||||
{readOnly
|
||||
? isEmpty
|
||||
? 'Aucune signature'
|
||||
: 'Signature'
|
||||
: isEmpty
|
||||
? 'Signez dans la zone ci-dessus'
|
||||
: 'Signature capturée'}
|
||||
</div>
|
||||
|
||||
{!readOnly && (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearSignature}
|
||||
disabled={disabled || isEmpty}
|
||||
className="flex items-center gap-1 px-3 py-1 text-xs bg-red-100 text-red-600 rounded hover:bg-red-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<RotateCcw size={12} />
|
||||
Effacer
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{required && isEmpty && (
|
||||
<div className="text-xs text-red-500 mt-1">
|
||||
La signature est obligatoire
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignatureField;
|
||||
Reference in New Issue
Block a user