fix: undo operation on signature pad (#868)

fixes: #864
This commit is contained in:
Lucas Smith
2024-02-16 22:57:14 +11:00
committed by GitHub
2 changed files with 32 additions and 6 deletions

View File

@ -0,0 +1,13 @@
import type { EffectCallback } from 'react';
import { useEffect } from 'react';
/**
* Dangerously runs an effect "once" by ignoring the depedencies of a given effect.
*
* DANGER: The effect will run twice in concurrent react and development environments.
*/
export const unsafe_useEffectOnce = (callback: EffectCallback) => {
// Intentionally avoiding exhaustive deps and rule of hooks here
// eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/rules-of-hooks
return useEffect(callback, []);
};

View File

@ -7,6 +7,8 @@ import { Undo2 } from 'lucide-react';
import type { StrokeOptions } from 'perfect-freehand'; import type { StrokeOptions } from 'perfect-freehand';
import { getStroke } from 'perfect-freehand'; import { getStroke } from 'perfect-freehand';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import { getSvgPathFromStroke } from './helper'; import { getSvgPathFromStroke } from './helper';
import { Point } from './point'; import { Point } from './point';
@ -28,6 +30,7 @@ export const SignaturePad = ({
...props ...props
}: SignaturePadProps) => { }: SignaturePadProps) => {
const $el = useRef<HTMLCanvasElement>(null); const $el = useRef<HTMLCanvasElement>(null);
const $imageData = useRef<ImageData | null>(null);
const [isPressed, setIsPressed] = useState(false); const [isPressed, setIsPressed] = useState(false);
const [lines, setLines] = useState<Point[][]>([]); const [lines, setLines] = useState<Point[][]>([]);
@ -134,7 +137,6 @@ export const SignaturePad = ({
}); });
onChange?.($el.current.toDataURL()); onChange?.($el.current.toDataURL());
ctx.save(); ctx.save();
} }
} }
@ -163,6 +165,7 @@ export const SignaturePad = ({
const ctx = $el.current.getContext('2d'); const ctx = $el.current.getContext('2d');
ctx?.clearRect(0, 0, $el.current.width, $el.current.height); ctx?.clearRect(0, 0, $el.current.width, $el.current.height);
$imageData.current = null;
} }
onChange?.(null); onChange?.(null);
@ -176,19 +179,25 @@ export const SignaturePad = ({
return; return;
} }
const newLines = [...lines]; const newLines = lines.slice(0, -1);
newLines.pop(); // Remove the last line
setLines(newLines); setLines(newLines);
// Clear the canvas // Clear the canvas
if ($el.current) { if ($el.current) {
const ctx = $el.current.getContext('2d'); const ctx = $el.current.getContext('2d');
ctx?.clearRect(0, 0, $el.current.width, $el.current.height); const { width, height } = $el.current;
ctx?.clearRect(0, 0, width, height);
if (typeof defaultValue === 'string' && $imageData.current) {
ctx?.putImageData($imageData.current, 0, 0);
}
newLines.forEach((line) => { newLines.forEach((line) => {
const pathData = new Path2D(getSvgPathFromStroke(getStroke(line, perfectFreehandOptions))); const pathData = new Path2D(getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)));
ctx?.fill(pathData); ctx?.fill(pathData);
}); });
onChange?.($el.current.toDataURL());
} }
}; };
@ -199,7 +208,7 @@ export const SignaturePad = ({
} }
}, []); }, []);
useEffect(() => { unsafe_useEffectOnce(() => {
if ($el.current && typeof defaultValue === 'string') { if ($el.current && typeof defaultValue === 'string') {
const ctx = $el.current.getContext('2d'); const ctx = $el.current.getContext('2d');
@ -209,11 +218,15 @@ export const SignaturePad = ({
img.onload = () => { img.onload = () => {
ctx?.drawImage(img, 0, 0, Math.min(width, img.width), Math.min(height, img.height)); ctx?.drawImage(img, 0, 0, Math.min(width, img.width), Math.min(height, img.height));
const defaultImageData = ctx?.getImageData(0, 0, width, height) || null;
$imageData.current = defaultImageData;
}; };
img.src = defaultValue; img.src = defaultValue;
} }
}, [defaultValue]); });
return ( return (
<div <div