react-esign
Version:
react-esign is a lightweight, dependency free React component built for capturing handwritten signatures. It provides a simple and responsive signature pad, perfect for e-signatures, form authentication, or user confirmations in React applications
338 lines (328 loc) • 16.4 kB
JavaScript
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var react = require('react');
function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css_248z = ".signature-input-container{align-items:flex-start;display:flex;flex-direction:column;height:100%;position:relative}.drawing-canvas{box-sizing:border-box;display:block}.signature-input-container .button{align-self:flex-start;border:none;border-radius:6px;box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);color:#fff;cursor:pointer;font-size:.875rem;font-weight:500;letter-spacing:.02857em;line-height:1.75;margin:6px 0;padding:6px 16px;transition:background-color .25s cubic-bezier(.4,0,.2,1) 0ms,box-shadow .25s cubic-bezier(.4,0,.2,1) 0ms}.signature-input-container .button:hover{cursor:pointer}.signature-input-container .text-button{background-color:transparent;box-shadow:none;color:#000;padding:2px 6px 2px 0}.signature-input-container .text-button:hover{cursor:pointer}.signature-input-container .text-button:disabled{box-shadow:none;color:rgba(0,0,0,.26);cursor:default}.signature-input-container .button:hover{box-shadow:0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12)}.signature-input-container .button:disabled{background-color:rgba(0,0,0,.12);box-shadow:none;color:rgba(0,0,0,.26);cursor:default}.drawing-canvas{background-color:#fff;border:2px solid rgba(0,0,0,.1);border-radius:6px;box-shadow:0 2px 4px rgba(0,0,0,.1);cursor:crosshair}.drawing-canvas.is-drawing{border-color:#1976d2}.drawing-canvas.is-disabled{border-color:#ccc}.drawing-canvas.is-error{border-color:#f44336;cursor:default}@media (max-width:600px){.drawing-canvas{display:block}}";
styleInject(css_248z);
const Button = ({ children, onClick, disabled, style }) => {
return (jsxRuntime.jsx("button", { onClick: onClick, disabled: disabled, style: disabled ? undefined : style, className: "button", children: children }));
};
const TextButton = ({ children, onClick, disabled, style }) => {
return (jsxRuntime.jsx("button", { onClick: onClick, disabled: disabled, style: style, className: "text-button", children: children }));
};
const SignatureInput = ({ onChange, isDisabled = false, isError = false, width = 450, height = 150, themeColor = "#1976d2", strokeWidth = 2, inputMode = "draw", buttonType = "button", download = false, clear = true, style, }) => {
const [isDrawing, setIsDrawing] = react.useState(false);
const [hasStrokes, setHasStrokes] = react.useState(false);
const signaturePadRef = react.useRef(null);
const ctxRef = react.useRef(null);
const strokesRef = react.useRef([]);
const currentStrokeRef = react.useRef([]);
const [typedSignature, setTypedSignature] = react.useState("");
const scaleRef = react.useRef({ x: 1, y: 1 });
const displaySizeRef = react.useRef({
width,
height,
});
const initializeCanvas = react.useCallback(() => {
if (!signaturePadRef.current)
return;
const canvas = signaturePadRef.current;
const ctx = canvas.getContext("2d");
if (!ctx)
return;
// Get container width
const container = canvas.parentElement;
if (!container)
return;
const containerWidth = container.clientWidth;
// Calculate the scaled dimensions while maintaining aspect ratio
const scale = Math.min(containerWidth / width, containerWidth / (width * (height / width)));
const scaledWidth = width * scale;
const scaledHeight = height * scale;
// Update display size ref
displaySizeRef.current = { width: scaledWidth, height: scaledHeight };
// Set the display size
canvas.style.width = `${scaledWidth}px`;
canvas.style.height = `${scaledHeight}px`;
// Set the internal canvas size (for consistent image export)
canvas.width = width;
canvas.height = height;
// Calculate scale factors
scaleRef.current = {
x: width / scaledWidth,
y: height / scaledHeight,
};
ctx.lineWidth = strokeWidth;
ctx.lineJoin = "round";
ctx.strokeStyle = "#000";
ctxRef.current = ctx;
}, [width, height, strokeWidth]);
const redrawCanvasWithSmoothing = react.useCallback(() => {
if (!ctxRef.current)
return;
// Clear the entire canvas
ctxRef.current.clearRect(0, 0, ctxRef.current.canvas.width, ctxRef.current.canvas.height);
// Reset the drawing style for each redraw
ctxRef.current.lineWidth = strokeWidth;
ctxRef.current.lineJoin = "round";
ctxRef.current.strokeStyle = "#000";
// Draw each stroke
strokesRef.current.forEach((stroke) => {
if (!ctxRef.current)
return;
const smoothedStroke = smoothStroke(stroke);
ctxRef.current.beginPath();
ctxRef.current.moveTo(smoothedStroke[0].x, smoothedStroke[0].y);
for (let i = 1; i < smoothedStroke.length; i++) {
ctxRef.current.lineTo(smoothedStroke[i].x, smoothedStroke[i].y);
}
ctxRef.current.stroke();
});
// Draw typed signature after strokes
if (typedSignature) {
if (!ctxRef.current)
return;
ctxRef.current.font = `italic ${strokeWidth * 12}px "Dancing Script", cursive`;
ctxRef.current.textAlign = "center";
ctxRef.current.fillStyle = "#000";
ctxRef.current.fillText(typedSignature, ctxRef.current.canvas.width / (2 * window.devicePixelRatio), ctxRef.current.canvas.height / (2 * window.devicePixelRatio) + 10);
}
}, [strokeWidth, typedSignature]);
// Add resize observer
react.useEffect(() => {
var _a;
const observer = new ResizeObserver(() => {
initializeCanvas();
redrawCanvasWithSmoothing();
});
if ((_a = signaturePadRef.current) === null || _a === void 0 ? void 0 : _a.parentElement) {
observer.observe(signaturePadRef.current.parentElement);
}
return () => observer.disconnect();
}, [initializeCanvas, redrawCanvasWithSmoothing]);
const handleClear = react.useCallback(() => {
if (!ctxRef.current || isDisabled)
return;
ctxRef.current.clearRect(0, 0, ctxRef.current.canvas.width, ctxRef.current.canvas.height);
strokesRef.current = [];
currentStrokeRef.current = [];
setTypedSignature("");
setHasStrokes(false);
onChange(undefined);
}, [onChange]);
const handlePointerDown = react.useCallback((event) => {
if (!isDisabled &&
(inputMode === "auto" || inputMode === "draw") &&
ctxRef.current &&
signaturePadRef.current) {
setIsDrawing(true);
const rect = signaturePadRef.current.getBoundingClientRect();
const x = (event.clientX - rect.left) * scaleRef.current.x;
const y = (event.clientY - rect.top) * scaleRef.current.y;
ctxRef.current.beginPath();
ctxRef.current.moveTo(x, y);
currentStrokeRef.current = [{ x, y }];
}
}, [inputMode]);
const handlePointerMove = react.useCallback((event) => {
if (isDisabled)
return;
if ((inputMode !== "draw" && inputMode !== "auto") ||
!isDrawing ||
!ctxRef.current ||
!signaturePadRef.current)
return;
const rect = signaturePadRef.current.getBoundingClientRect();
const x = (event.clientX - rect.left) * scaleRef.current.x;
const y = (event.clientY - rect.top) * scaleRef.current.y;
ctxRef.current.lineTo(x, y);
ctxRef.current.stroke();
currentStrokeRef.current.push({ x, y });
}, [inputMode, isDrawing]);
const handlePointerUp = react.useCallback((event) => {
var _a;
setIsDrawing(false);
// Save the stroke
if (currentStrokeRef.current.length > 1) {
strokesRef.current.push([...currentStrokeRef.current]);
setHasStrokes(true);
// Smooth strokes and redraw
setTimeout(() => {
redrawCanvasWithSmoothing();
}, 10);
}
(_a = ctxRef.current) === null || _a === void 0 ? void 0 : _a.beginPath();
// Convert to File
if (strokesRef.current.length > 0 && signaturePadRef.current) {
const rect = signaturePadRef.current.getBoundingClientRect();
const isWithinBounds = event.clientX >= rect.left &&
event.clientX <= rect.right &&
event.clientY >= rect.top &&
event.clientY <= rect.bottom;
if (isWithinBounds) {
signaturePadRef.current.toBlob((blob) => {
if (blob) {
const file = new File([blob], "signature.png", {
type: "image/png",
});
onChange(file);
}
});
}
}
}, [onChange]);
const handleTouchStart = react.useCallback((event) => {
if (isDisabled)
return;
event.preventDefault(); // Prevent scrolling when drawing starts
}, [isDisabled]);
react.useEffect(() => {
if (!signaturePadRef.current)
return;
const canvas = signaturePadRef.current;
const attachListeners = () => {
canvas.addEventListener("touchstart", handleTouchStart, {
passive: false,
});
canvas.addEventListener("pointerdown", handlePointerDown);
canvas.addEventListener("pointermove", handlePointerMove);
document.addEventListener("pointerup", handlePointerUp);
};
const detachListeners = () => {
canvas.removeEventListener("touchstart", handleTouchStart);
canvas.removeEventListener("pointerdown", handlePointerDown);
canvas.removeEventListener("pointermove", handlePointerMove);
document.removeEventListener("pointerup", handlePointerUp);
};
if (!isDisabled) {
attachListeners();
}
else {
detachListeners();
}
return () => {
detachListeners();
};
}, [
handlePointerDown,
handlePointerMove,
handlePointerUp,
handleTouchStart,
isDisabled,
]); // ✅ Dependency on isDisabled
/**
* Catmull-Rom Splines Algorithm: Smooths the strokes
*/
const smoothStroke = (points, numSegments = 10 // Controls smoothness
) => {
if (points.length < 4)
return points; // Need at least 4 points for Catmull-Rom to work
let smoothed = [];
smoothed.push(points[0]); // Keep the first point
for (let i = 0; i < points.length - 1; i++) {
const p0 = points[Math.max(0, i - 1)];
const p1 = points[i];
const p2 = points[Math.min(i + 1, points.length - 1)];
const p3 = points[Math.min(i + 2, points.length - 1)];
for (let t = 0; t <= 1; t += 1 / numSegments) {
const t2 = t * t;
const t3 = t2 * t;
// Catmull-Rom spline formula
const x = 0.5 *
(2 * p1.x +
(-p0.x + p2.x) * t +
(2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 +
(-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3);
const y = 0.5 *
(2 * p1.y +
(-p0.y + p2.y) * t +
(2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
(-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3);
smoothed.push({ x, y });
}
}
smoothed.push(points[points.length - 1]); // Keep last point
return smoothed;
};
/**
* Listen for user keyboard input in "type" mode or "auto" mode
*/
react.useEffect(() => {
if (inputMode !== "type" && (inputMode !== "auto" || isDrawing))
return;
const handleKeyPress = (event) => {
if (event.key.length === 1) {
setTypedSignature((prev) => prev + event.key);
}
else if (event.key === "Backspace") {
if (typedSignature.length === 1) {
handleClear();
}
else {
setTypedSignature((prev) => prev.slice(0, -1));
}
}
};
window.addEventListener("keydown", handleKeyPress);
return () => window.removeEventListener("keydown", handleKeyPress);
}, [inputMode, typedSignature, handleClear, isDrawing]);
const handleDownload = react.useCallback(() => {
if (!signaturePadRef.current || strokesRef.current.length === 0)
return;
// Create a temporary link element
const link = document.createElement("a");
link.download = "signature.png";
// Get the canvas data as a URL
const dataUrl = signaturePadRef.current.toDataURL("image/png");
link.href = dataUrl;
// Programmatically click the link to trigger download
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}, []);
// Update canvas when typed signature changes
react.useEffect(() => {
redrawCanvasWithSmoothing();
}, [typedSignature, redrawCanvasWithSmoothing]);
const borderStyles = () => {
if (isDrawing && !isDisabled)
return { borderColor: themeColor };
if (isDisabled)
return { borderColor: "#ccc" };
if (isError)
return { borderColor: "#f44336" };
return {};
};
return (jsxRuntime.jsxs("div", { className: "signature-input-container", style: Object.assign({ width: "100%", maxWidth: `${width}px`, minWidth: `${Math.min(width, 100)}px` }, style), children: [jsxRuntime.jsx("canvas", { className: `drawing-canvas`, ref: signaturePadRef, style: Object.assign({ touchAction: "none", width: "100%", height: "100%", maxWidth: `${width}px`, aspectRatio: `${width} / ${height}`, cursor: isDisabled
? "not-allowed"
: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z'/%3E%3C/svg%3E") 0 24, pointer` }, borderStyles()) }), jsxRuntime.jsxs("div", { style: {
display: "flex",
gap: 1,
justifyContent: "space-between",
flexDirection: "column",
marginTop: "10px",
}, children: [clear && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [buttonType === "button" && (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(Button, { onClick: handleClear, disabled: !hasStrokes || isDisabled, style: { backgroundColor: themeColor }, children: "Clear" }) })), buttonType === "text" && (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(TextButton, { onClick: handleClear, disabled: !hasStrokes || isDisabled, style: { color: themeColor, opacity: !hasStrokes ? 0.5 : 1 }, children: "Clear" }) }))] })), download && (jsxRuntime.jsx(Button, { onClick: handleDownload, disabled: !hasStrokes, style: { backgroundColor: themeColor }, children: "Download" }))] })] }));
};
exports.SignatureInput = SignatureInput;
//# sourceMappingURL=index.cjs.js.map