import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import renderHtmlAttributes from "../../utils/renderHtmlAttributes"; import useFormSubmitSuccess from "../../hooks/useFormSubmitSuccess"; import type {PointGroup} from "signature_pad"; import SignaturePad from "signature_pad"; // @ts-ignore import trimCanvas from 'trim-canvas' import type {FreemiusProps} from "../../types/freemius"; import UpgradeOverlay from "../../components/UpgradeOverlay"; type FieldProps = { itemId: string formId: string attrs: Record showPlaceholder: boolean showColorsList: boolean clearCanvas: boolean includeBgColor: boolean trimCanvas: boolean showUndoButton: boolean showRedoButton: boolean placeholderIcon: string colorsListLabel: string undoButtonText: string redoButtonText: string clearButtonText: string backgroundColor: string sendFiles: 'link' | 'attach' | 'both' penColor: string fileName: string colorsList: { title: string; color: string }[] custom_id: string buttonAnimation: string } & FreemiusProps const Field = (props: FieldProps) => { const inputRef = useRef(null); const inputHiddenRef = useRef(null); const canvasRef = useRef(null); const padRef = useRef(null); const attrs = useMemo(() => renderHtmlAttributes(props.attrs), [props.attrs]); const [undoData, setUndoData] = useState([]); const [isBeginStroke, setBeginStroke] = useState(false); const [isEmpty, setIsEmpty] = useState(true); // Reset canvas const reset: () => void = useCallback(() => { const signaturePad = padRef.current as SignaturePad; signaturePad.clear(); setIsEmpty(true); setBeginStroke(false); (inputRef.current as HTMLInputElement).value = ''; (inputHiddenRef.current as HTMLInputElement).value = ''; }, []); // Reset after form submit useFormSubmitSuccess(inputRef, () => { reset(); }, [reset]); const placeholderIcon = useMemo(() => decodeURIComponent(props.placeholderIcon), [props.placeholderIcon]); // Get data url with background color const getTempCanvasDataUrl: (bgColor: string) => string = useCallback((bgColor: string) => { const canvas = canvasRef.current as HTMLCanvasElement; const signaturePad = padRef.current as SignaturePad; const tempCanvas = document.createElement('canvas'); tempCanvas.width = canvas.width; tempCanvas.height = canvas.height; const ratio = Math.max(window.devicePixelRatio || 1, 1); tempCanvas.getContext('2d')?.scale(ratio, ratio); const tempSignaturePad = new SignaturePad(tempCanvas, { penColor: signaturePad.penColor, backgroundColor: props.includeBgColor ? bgColor : 'rgba(0,0,0,0)', minWidth: signaturePad.minWidth, maxWidth: signaturePad.maxWidth, throttle: signaturePad.throttle, dotSize: signaturePad.dotSize, minDistance: signaturePad.minDistance, velocityFilterWeight: signaturePad.velocityFilterWeight }); tempSignaturePad.fromData(signaturePad.toData()); if (props.trimCanvas) { trimCanvas(tempCanvas); } const url = tempSignaturePad.toDataURL(); tempSignaturePad.off(); tempCanvas.remove(); return url; }, [props.includeBgColor, props.trimCanvas]); // Create image const createImage: () => Promise = useCallback(async () => { const dataUrl = getTempCanvasDataUrl(props.backgroundColor); const imgResponse = await fetch(dataUrl); const imgBlob = await imgResponse.blob(); // Create a new file const fileName = `${props.fileName}.png`; const file = new File([imgBlob], fileName); // Add file const fileList = new DataTransfer(); fileList.items.add(file); (inputRef.current as HTMLInputElement).files = fileList.files; (inputHiddenRef.current as HTMLInputElement).value = fileName; }, [getTempCanvasDataUrl, props.backgroundColor, props.fileName]); const isUndoData = useMemo(() => undoData.length > 0, [undoData.length]); useEffect(() => { const canvas = canvasRef.current as HTMLCanvasElement; if (!canvas) { return; } const signaturePad = new SignaturePad(canvas, { penColor: props.penColor, backgroundColor: 'rgba(0,0,0,0)', // Transparent minWidth: 0.5, maxWidth: 2.5, throttle: 16, dotSize: 0, minDistance: 0, velocityFilterWeight: 0.7, }); padRef.current = signaturePad; /** * Adjust canvas coordinate space taking into account pixel ratio, * to make it look crisp on mobile devices. * This also causes canvas to be cleared. */ function resizeCanvas() { // When zoomed out to less than 100%, for some very strange reason, // some browsers report devicePixelRatio as less than 1 // and only part of the canvas is cleared then. const ratio = Math.max(window.devicePixelRatio || 1, 1); // This part causes the canvas to be cleared canvas.width = canvas.offsetWidth * ratio; canvas.height = canvas.offsetHeight * ratio; canvas.getContext('2d')?.scale(ratio, ratio); // signaturePad.clear(); // If you want to keep the drawing on resize instead of clearing it you can reset the data. signaturePad.fromData(signaturePad.toData()); } // Triggered before stroke begins function beginStroke() { setBeginStroke(true); } // Triggered after stroke ends const endStroke = async () => { // Clear undoData when new data is added setUndoData([]); setIsEmpty(signaturePad.isEmpty()); await createImage(); }; // Bind events window.addEventListener('resize', resizeCanvas, false); signaturePad.addEventListener("beginStroke", beginStroke, false); signaturePad.addEventListener('endStroke', endStroke, false); resizeCanvas(); return () => { window.removeEventListener('resize', resizeCanvas, false); signaturePad.removeEventListener('beginStroke', endStroke, false); signaturePad.removeEventListener('endStroke', endStroke, false); signaturePad?.off(); signaturePad?.clear(); } }, [createImage, props.penColor]); useEffect(() => { if (isEmpty) { setBeginStroke(false); } else { setBeginStroke(true); } }, [isEmpty]); // Undo button async function handleUndo(e: React.MouseEvent) { e.preventDefault(); if (!props.freemius.can_use_premium_code) { return; } const signaturePad = padRef.current as SignaturePad; const data = signaturePad.toData(); if (!(data && data.length > 0)) { return; } // Remove the last dot or line const removed = data.pop(); if (removed) { setUndoData(prevState => ([ ...prevState, removed ])); } signaturePad.fromData(data); setIsEmpty(signaturePad.isEmpty()); // Reset if pad is empty if (signaturePad.isEmpty()) { reset(); } } // Redo Button async function handleRedo(e: React.MouseEvent) { e.preventDefault(); if (!props.freemius.can_use_premium_code) { return; } if (undoData.length <= 0) { return; } const signaturePad = padRef.current as SignaturePad; const data = signaturePad.toData(); data.push(undoData.pop() as PointGroup); signaturePad.fromData(data); if (undoData.length <= 0) { setUndoData([]); } setIsEmpty(signaturePad.isEmpty()); await createImage(); } // Clear button function handleClear(e: React.MouseEvent) { e.preventDefault(); if (!props.freemius.can_use_premium_code) { return; } setUndoData([]); reset(); } // Select color function handleColor(e: React.ChangeEvent) { if (!props.freemius.can_use_premium_code) { return; } const value = e.target.value.trim().toLowerCase(); const signaturePad = padRef.current as SignaturePad; signaturePad.penColor = value; // Clear canvas if (props.clearCanvas) { setUndoData([]); reset(); } } return ( <> {!props.freemius.can_use_premium_code && }
{props.showPlaceholder && (
)}
{(props.showColorsList && props.colorsList.length > 0) && (
{props.colorsList.map(({title, color}, index) => ( ))}
)}
{props.showUndoButton && ( )} {(props.showUndoButton && props.showRedoButton) && ( )} {props.clearButtonText && ( )}
); }; export default Field;