@etsoo/materialui 1.6.76 → 1.6.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/SignaturePadComponent.d.ts +92 -0
- package/lib/cjs/SignaturePadComponent.js +104 -0
- package/lib/cjs/UserAvatarEditor.js +1 -1
- package/lib/cjs/index.d.ts +2 -0
- package/lib/cjs/index.js +2 -0
- package/lib/cjs/utils/CanvasUtils.d.ts +35 -0
- package/lib/cjs/utils/CanvasUtils.js +84 -0
- package/lib/mjs/SignaturePadComponent.d.ts +92 -0
- package/lib/mjs/SignaturePadComponent.js +95 -0
- package/lib/mjs/UserAvatarEditor.js +1 -1
- package/lib/mjs/index.d.ts +2 -0
- package/lib/mjs/index.js +2 -0
- package/lib/mjs/utils/CanvasUtils.d.ts +35 -0
- package/lib/mjs/utils/CanvasUtils.js +81 -0
- package/package.json +6 -5
- package/src/SignaturePadComponent.tsx +230 -0
- package/src/UserAvatarEditor.tsx +1 -1
- package/src/index.ts +3 -0
- package/src/utils/CanvasUtils.ts +103 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import SignaturePad, { Options } from "signature_pad";
|
|
3
|
+
/**
|
|
4
|
+
* Extended SignaturePad class with trim method
|
|
5
|
+
*/
|
|
6
|
+
export declare class SignaturePadEx extends SignaturePad {
|
|
7
|
+
private readonly canvasElement;
|
|
8
|
+
constructor(canvasElement: HTMLCanvasElement, options?: Options);
|
|
9
|
+
/**
|
|
10
|
+
* Trim the canvas to remove empty space around the signature
|
|
11
|
+
* @returns Trimmed canvas
|
|
12
|
+
*/
|
|
13
|
+
trim(): HTMLCanvasElement;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Props for SignaturePadComponent
|
|
17
|
+
*/
|
|
18
|
+
export type SignaturePadComponentProps = {
|
|
19
|
+
/**
|
|
20
|
+
* Width of the canvas
|
|
21
|
+
*/
|
|
22
|
+
width: number | string;
|
|
23
|
+
/**
|
|
24
|
+
* Height of the canvas
|
|
25
|
+
*/
|
|
26
|
+
height: number;
|
|
27
|
+
/**
|
|
28
|
+
* Options for SignaturePad
|
|
29
|
+
*/
|
|
30
|
+
options?: Options;
|
|
31
|
+
/**
|
|
32
|
+
* Ref for the SignaturePadEx instance
|
|
33
|
+
*/
|
|
34
|
+
ref: React.RefObject<SignaturePadEx | null>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* React component for SignaturePad
|
|
38
|
+
* @param props Props
|
|
39
|
+
* @returns Component
|
|
40
|
+
*/
|
|
41
|
+
export declare function SignaturePadComponent(props: SignaturePadComponentProps): React.JSX.Element;
|
|
42
|
+
type Placement = "top" | "right" | "bottom" | "left";
|
|
43
|
+
type Alignment = "flex-start" | "center" | "flex-end";
|
|
44
|
+
/**
|
|
45
|
+
* Methods for SignaturePadFull component
|
|
46
|
+
*/
|
|
47
|
+
export interface SignaturePadFullMethods {
|
|
48
|
+
getInstance(): SignaturePadEx | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Props for SignaturePadFull component
|
|
52
|
+
*/
|
|
53
|
+
export type SignaturePadFullProps = Partial<Omit<SignaturePadComponentProps, "ref">> & {
|
|
54
|
+
/**
|
|
55
|
+
* Label for the clear button
|
|
56
|
+
* @default "Clear"
|
|
57
|
+
*/
|
|
58
|
+
clearLabel?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Label for the save button
|
|
61
|
+
* @default "Save"
|
|
62
|
+
*/
|
|
63
|
+
saveLabel?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Ref for the SignaturePadFull instance
|
|
66
|
+
*/
|
|
67
|
+
mRef?: React.RefObject<SignaturePadFullMethods | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Placement of the buttons relative to the canvas
|
|
70
|
+
* @default ["right", "flex-start"]
|
|
71
|
+
*/
|
|
72
|
+
placement?: [Placement?, alignment?: Alignment];
|
|
73
|
+
/**
|
|
74
|
+
* Callback when the signature is saved
|
|
75
|
+
* @param signaturePad SignaturePadEx instance
|
|
76
|
+
* @returns void or Promise<void>
|
|
77
|
+
*/
|
|
78
|
+
onSave?: (signaturePad: SignaturePadEx) => void | Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Spacing between the canvas and buttons
|
|
81
|
+
* @default 0.5
|
|
82
|
+
*/
|
|
83
|
+
spacing?: number;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Full SignaturePad component with clear and save buttons
|
|
87
|
+
*
|
|
88
|
+
* @param props Props
|
|
89
|
+
* @returns Component
|
|
90
|
+
*/
|
|
91
|
+
export declare function SignaturePadFull(props: SignaturePadFullProps): React.JSX.Element;
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SignaturePadEx = void 0;
|
|
7
|
+
exports.SignaturePadComponent = SignaturePadComponent;
|
|
8
|
+
exports.SignaturePadFull = SignaturePadFull;
|
|
9
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
10
|
+
const Button_1 = __importDefault(require("@mui/material/Button"));
|
|
11
|
+
const Paper_1 = __importDefault(require("@mui/material/Paper"));
|
|
12
|
+
const react_1 = __importDefault(require("react"));
|
|
13
|
+
const signature_pad_1 = __importDefault(require("signature_pad"));
|
|
14
|
+
const Clear_1 = __importDefault(require("@mui/icons-material/Clear"));
|
|
15
|
+
const Save_1 = __importDefault(require("@mui/icons-material/Save"));
|
|
16
|
+
const CanvasUtils_1 = require("./utils/CanvasUtils");
|
|
17
|
+
const Stack_1 = __importDefault(require("@mui/material/Stack"));
|
|
18
|
+
const ButtonGroup_1 = __importDefault(require("@mui/material/ButtonGroup"));
|
|
19
|
+
const ReactApp_1 = require("./app/ReactApp");
|
|
20
|
+
/**
|
|
21
|
+
* Extended SignaturePad class with trim method
|
|
22
|
+
*/
|
|
23
|
+
class SignaturePadEx extends signature_pad_1.default {
|
|
24
|
+
canvasElement;
|
|
25
|
+
constructor(canvasElement, options) {
|
|
26
|
+
super(canvasElement, options);
|
|
27
|
+
this.canvasElement = canvasElement;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Trim the canvas to remove empty space around the signature
|
|
31
|
+
* @returns Trimmed canvas
|
|
32
|
+
*/
|
|
33
|
+
trim() {
|
|
34
|
+
return CanvasUtils_1.CanvasUtils.trim(this.canvasElement);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.SignaturePadEx = SignaturePadEx;
|
|
38
|
+
/**
|
|
39
|
+
* React component for SignaturePad
|
|
40
|
+
* @param props Props
|
|
41
|
+
* @returns Component
|
|
42
|
+
*/
|
|
43
|
+
function SignaturePadComponent(props) {
|
|
44
|
+
// Destruct
|
|
45
|
+
const { width, height, options, ref } = props;
|
|
46
|
+
// Refs
|
|
47
|
+
const canvasRef = react_1.default.useRef(null);
|
|
48
|
+
react_1.default.useEffect(() => {
|
|
49
|
+
const canvas = canvasRef.current;
|
|
50
|
+
if (!canvas)
|
|
51
|
+
return;
|
|
52
|
+
ref.current = new SignaturePadEx(canvas, options);
|
|
53
|
+
const observer = new ResizeObserver((entries) => {
|
|
54
|
+
const { width, height } = entries[0].contentRect;
|
|
55
|
+
canvas.width = width;
|
|
56
|
+
canvas.height = height;
|
|
57
|
+
});
|
|
58
|
+
observer.observe(canvas.parentElement);
|
|
59
|
+
return () => {
|
|
60
|
+
ref.current?.off();
|
|
61
|
+
observer.disconnect();
|
|
62
|
+
};
|
|
63
|
+
}, [options, ref]);
|
|
64
|
+
return (0, jsx_runtime_1.jsx)("canvas", { ref: canvasRef, width: width, height: height });
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Full SignaturePad component with clear and save buttons
|
|
68
|
+
*
|
|
69
|
+
* @param props Props
|
|
70
|
+
* @returns Component
|
|
71
|
+
*/
|
|
72
|
+
function SignaturePadFull(props) {
|
|
73
|
+
// Global app
|
|
74
|
+
const app = (0, ReactApp_1.useAppContext)();
|
|
75
|
+
const { clear, save } = app?.getLabels("clear", "save") ?? {};
|
|
76
|
+
// Destruct
|
|
77
|
+
const { clearLabel = clear || "Clear", saveLabel = save || "Save", width = "100%", height = 300, mRef, onSave, options, placement = ["right", "flex-start"], spacing = 0.5 } = props;
|
|
78
|
+
// Ref
|
|
79
|
+
const signaturePadRef = react_1.default.useRef(null);
|
|
80
|
+
react_1.default.useImperativeHandle(mRef, () => ({
|
|
81
|
+
getInstance: () => signaturePadRef.current
|
|
82
|
+
}));
|
|
83
|
+
function handleClear() {
|
|
84
|
+
signaturePadRef.current?.clear();
|
|
85
|
+
}
|
|
86
|
+
function handleSave() {
|
|
87
|
+
const sp = signaturePadRef.current;
|
|
88
|
+
if (!sp || sp.isEmpty())
|
|
89
|
+
return;
|
|
90
|
+
onSave?.(sp);
|
|
91
|
+
}
|
|
92
|
+
const [p = "right", a = "flex-start"] = placement;
|
|
93
|
+
let stackDirection;
|
|
94
|
+
let buttonGroupOrientation;
|
|
95
|
+
if (p === "top" || p === "bottom") {
|
|
96
|
+
stackDirection = p === "top" ? "column-reverse" : "column";
|
|
97
|
+
buttonGroupOrientation = "horizontal";
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
stackDirection = p === "left" ? "row-reverse" : "row";
|
|
101
|
+
buttonGroupOrientation = "vertical";
|
|
102
|
+
}
|
|
103
|
+
return ((0, jsx_runtime_1.jsxs)(Stack_1.default, { direction: stackDirection, spacing: spacing, sx: { alignItems: a }, children: [(0, jsx_runtime_1.jsx)(Paper_1.default, { sx: { width, height }, children: (0, jsx_runtime_1.jsx)(SignaturePadComponent, { width: width, height: height, options: options, ref: signaturePadRef }) }), (0, jsx_runtime_1.jsxs)(ButtonGroup_1.default, { orientation: buttonGroupOrientation, children: [(0, jsx_runtime_1.jsx)(Button_1.default, { onClick: handleClear, startIcon: (0, jsx_runtime_1.jsx)(Clear_1.default, {}), children: clearLabel }), onSave && ((0, jsx_runtime_1.jsx)(Button_1.default, { variant: "contained", onClick: handleSave, startIcon: (0, jsx_runtime_1.jsx)(Save_1.default, {}), children: saveLabel }))] })] }));
|
|
104
|
+
}
|
|
@@ -175,6 +175,6 @@ function UserAvatarEditor(props) {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
};
|
|
178
|
-
return ((0, jsx_runtime_1.jsxs)(Stack_1.default, { direction: "column", spacing: 0.5, sx: { width: containerWidth }, children: [(0, jsx_runtime_1.jsx)(FileUploadButton_1.FileUploadButton, { variant: "outlined", size: "medium", startIcon: (0, jsx_runtime_1.jsx)(Image_1.default, {}), fullWidth: true, onUploadFiles: handleFileUpload, inputProps: { accept: "image/png, image/jpeg" }, children: selectFileLabel }), (0, jsx_runtime_1.jsxs)(Stack_1.default, { direction: "row", spacing: 0.5, children: [(0, jsx_runtime_1.jsx)(react_1.default.Suspense, { fallback: (0, jsx_runtime_1.jsx)(Skeleton_1.default, { variant: "rounded", width: width, height: localHeight }), children: (0, jsx_runtime_1.jsx)(react_avatar_editor_1.default, { ref: ref, border: border, width: width, height: localHeight, onLoadSuccess: handleLoad, image: previewImage ??
|
|
178
|
+
return ((0, jsx_runtime_1.jsxs)(Stack_1.default, { direction: "column", spacing: 0.5, sx: { width: containerWidth }, children: [(0, jsx_runtime_1.jsx)(FileUploadButton_1.FileUploadButton, { variant: "outlined", size: "medium", startIcon: (0, jsx_runtime_1.jsx)(Image_1.default, {}), fullWidth: true, onUploadFiles: handleFileUpload, inputProps: { accept: "image/png, image/jpeg, image/webp" }, children: selectFileLabel }), (0, jsx_runtime_1.jsxs)(Stack_1.default, { direction: "row", spacing: 0.5, children: [(0, jsx_runtime_1.jsx)(react_1.default.Suspense, { fallback: (0, jsx_runtime_1.jsx)(Skeleton_1.default, { variant: "rounded", width: width, height: localHeight }), children: (0, jsx_runtime_1.jsx)(react_avatar_editor_1.default, { ref: ref, border: border, width: width, height: localHeight, onLoadSuccess: handleLoad, image: previewImage ??
|
|
179
179
|
"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=", scale: editorState.scale, rotate: editorState.rotate }) }), (0, jsx_runtime_1.jsxs)(ButtonGroup_1.default, { size: "small", orientation: "vertical", disabled: !ready, children: [(0, jsx_runtime_1.jsx)(Button_1.default, { onClick: () => handleRotate(90), title: labels.rotateRight, children: (0, jsx_runtime_1.jsx)(RotateRight_1.default, {}) }), (0, jsx_runtime_1.jsx)(Button_1.default, { onClick: () => handleRotate(-90), title: labels.rotateLeft, children: (0, jsx_runtime_1.jsx)(RotateLeft_1.default, {}) }), (0, jsx_runtime_1.jsx)(Button_1.default, { onClick: handleReset, title: labels.reset, children: (0, jsx_runtime_1.jsx)(ClearAll_1.default, {}) })] })] }), (0, jsx_runtime_1.jsxs)(Stack_1.default, { spacing: 0.5, direction: "row", sx: { alignItems: "center", paddingBottom: 2 }, children: [(0, jsx_runtime_1.jsx)(IconButton_1.default, { size: "small", disabled: !ready || editorState.scale <= min, onClick: () => adjustScale(false), children: (0, jsx_runtime_1.jsx)(Remove_1.default, {}) }), (0, jsx_runtime_1.jsx)(Slider_1.default, { title: labels.zoom, disabled: !ready, min: min, max: max, step: step, value: editorState.scale, valueLabelDisplay: "auto", valueLabelFormat: (value) => `${Math.round(100 * value) / 100}`, marks: marks, onChange: handleZoom }), (0, jsx_runtime_1.jsx)(IconButton_1.default, { size: "small", disabled: !ready || editorState.scale >= max, onClick: () => adjustScale(true), children: (0, jsx_runtime_1.jsx)(Add_1.default, {}) })] }), (0, jsx_runtime_1.jsx)(Button_1.default, { ref: buttonRef, variant: "contained", startIcon: (0, jsx_runtime_1.jsx)(Done_1.default, {}), disabled: !ready, onClick: handleDone, children: labels.done })] }));
|
|
180
180
|
}
|
package/lib/cjs/index.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export * from "./pages/ViewPage";
|
|
|
35
35
|
export * from "./texts/DateText";
|
|
36
36
|
export * from "./texts/MoneyText";
|
|
37
37
|
export * from "./texts/NumberText";
|
|
38
|
+
export * from "./utils/CanvasUtils";
|
|
38
39
|
export * from "./AddresSelector";
|
|
39
40
|
export * from "./AuditDisplay";
|
|
40
41
|
export * from "./AutocompleteExtendedProps";
|
|
@@ -122,6 +123,7 @@ export * from "./SearchOptionGroup";
|
|
|
122
123
|
export * from "./SelectBool";
|
|
123
124
|
export * from "./SelectEx";
|
|
124
125
|
export * from "./ShowDataComparison";
|
|
126
|
+
export * from "./SignaturePadComponent";
|
|
125
127
|
export * from "./Switch";
|
|
126
128
|
export * from "./SwitchAnt";
|
|
127
129
|
export * from "./SwitchField";
|
package/lib/cjs/index.js
CHANGED
|
@@ -55,6 +55,7 @@ __exportStar(require("./pages/ViewPage"), exports);
|
|
|
55
55
|
__exportStar(require("./texts/DateText"), exports);
|
|
56
56
|
__exportStar(require("./texts/MoneyText"), exports);
|
|
57
57
|
__exportStar(require("./texts/NumberText"), exports);
|
|
58
|
+
__exportStar(require("./utils/CanvasUtils"), exports);
|
|
58
59
|
__exportStar(require("./AddresSelector"), exports);
|
|
59
60
|
__exportStar(require("./AuditDisplay"), exports);
|
|
60
61
|
__exportStar(require("./AutocompleteExtendedProps"), exports);
|
|
@@ -142,6 +143,7 @@ __exportStar(require("./SearchOptionGroup"), exports);
|
|
|
142
143
|
__exportStar(require("./SelectBool"), exports);
|
|
143
144
|
__exportStar(require("./SelectEx"), exports);
|
|
144
145
|
__exportStar(require("./ShowDataComparison"), exports);
|
|
146
|
+
__exportStar(require("./SignaturePadComponent"), exports);
|
|
145
147
|
__exportStar(require("./Switch"), exports);
|
|
146
148
|
__exportStar(require("./SwitchAnt"), exports);
|
|
147
149
|
__exportStar(require("./SwitchField"), exports);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for canvas operations
|
|
3
|
+
*/
|
|
4
|
+
export declare namespace CanvasUtils {
|
|
5
|
+
/**
|
|
6
|
+
* JPEG content type
|
|
7
|
+
*/
|
|
8
|
+
const JPG = "image/jpeg";
|
|
9
|
+
/**
|
|
10
|
+
* PNG content type
|
|
11
|
+
*/
|
|
12
|
+
const PNG = "image/png";
|
|
13
|
+
/**
|
|
14
|
+
* WEBP content type
|
|
15
|
+
*/
|
|
16
|
+
const WEBP = "image/webp";
|
|
17
|
+
/**
|
|
18
|
+
* SVG content type
|
|
19
|
+
*/
|
|
20
|
+
const SVG = "image/svg+xml";
|
|
21
|
+
/**
|
|
22
|
+
* Convert canvas to Blob
|
|
23
|
+
* @param canvas Canvas element
|
|
24
|
+
* @param type MIME type of the image
|
|
25
|
+
* @param quality Image quality (0 to 1)
|
|
26
|
+
* @returns Promise that resolves to a Blob
|
|
27
|
+
*/
|
|
28
|
+
function toBlob(canvas: HTMLCanvasElement, type?: string, quality?: number): Promise<Blob>;
|
|
29
|
+
/**
|
|
30
|
+
* Trim the canvas to remove empty space around the signature
|
|
31
|
+
* @param canvas Canvas element
|
|
32
|
+
* @returns Trimmed canvas
|
|
33
|
+
*/
|
|
34
|
+
function trim(canvas: HTMLCanvasElement): HTMLCanvasElement;
|
|
35
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CanvasUtils = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Utility functions for canvas operations
|
|
6
|
+
*/
|
|
7
|
+
var CanvasUtils;
|
|
8
|
+
(function (CanvasUtils) {
|
|
9
|
+
/**
|
|
10
|
+
* JPEG content type
|
|
11
|
+
*/
|
|
12
|
+
CanvasUtils.JPG = "image/jpeg";
|
|
13
|
+
/**
|
|
14
|
+
* PNG content type
|
|
15
|
+
*/
|
|
16
|
+
CanvasUtils.PNG = "image/png";
|
|
17
|
+
/**
|
|
18
|
+
* WEBP content type
|
|
19
|
+
*/
|
|
20
|
+
CanvasUtils.WEBP = "image/webp";
|
|
21
|
+
/**
|
|
22
|
+
* SVG content type
|
|
23
|
+
*/
|
|
24
|
+
CanvasUtils.SVG = "image/svg+xml";
|
|
25
|
+
/**
|
|
26
|
+
* Convert canvas to Blob
|
|
27
|
+
* @param canvas Canvas element
|
|
28
|
+
* @param type MIME type of the image
|
|
29
|
+
* @param quality Image quality (0 to 1)
|
|
30
|
+
* @returns Promise that resolves to a Blob
|
|
31
|
+
*/
|
|
32
|
+
function toBlob(canvas, type = "image/png", quality = 1) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
canvas.toBlob((blob) => {
|
|
35
|
+
if (blob) {
|
|
36
|
+
resolve(blob);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
reject(new Error("Failed to create Blob."));
|
|
40
|
+
}
|
|
41
|
+
}, type, quality);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
CanvasUtils.toBlob = toBlob;
|
|
45
|
+
/**
|
|
46
|
+
* Trim the canvas to remove empty space around the signature
|
|
47
|
+
* @param canvas Canvas element
|
|
48
|
+
* @returns Trimmed canvas
|
|
49
|
+
*/
|
|
50
|
+
function trim(canvas) {
|
|
51
|
+
const ctx = canvas.getContext("2d");
|
|
52
|
+
const { width, height } = canvas;
|
|
53
|
+
const { data } = ctx.getImageData(0, 0, width, height);
|
|
54
|
+
let top = height;
|
|
55
|
+
let left = width;
|
|
56
|
+
let right = 0;
|
|
57
|
+
let bottom = 0;
|
|
58
|
+
for (let y = 0; y < height; y++) {
|
|
59
|
+
for (let x = 0; x < width; x++) {
|
|
60
|
+
if (data[(y * width + x) * 4 + 3] === 0)
|
|
61
|
+
continue;
|
|
62
|
+
if (x < left)
|
|
63
|
+
left = x;
|
|
64
|
+
if (x > right)
|
|
65
|
+
right = x;
|
|
66
|
+
if (y < top)
|
|
67
|
+
top = y;
|
|
68
|
+
if (y > bottom)
|
|
69
|
+
bottom = y;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const trimmed = document.createElement("canvas");
|
|
73
|
+
// Empty canvas
|
|
74
|
+
if (right < left || bottom < top)
|
|
75
|
+
return trimmed;
|
|
76
|
+
trimmed.width = right - left + 1;
|
|
77
|
+
trimmed.height = bottom - top + 1;
|
|
78
|
+
trimmed
|
|
79
|
+
.getContext("2d")
|
|
80
|
+
.drawImage(canvas, left, top, trimmed.width, trimmed.height, 0, 0, trimmed.width, trimmed.height);
|
|
81
|
+
return trimmed;
|
|
82
|
+
}
|
|
83
|
+
CanvasUtils.trim = trim;
|
|
84
|
+
})(CanvasUtils || (exports.CanvasUtils = CanvasUtils = {}));
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import SignaturePad, { Options } from "signature_pad";
|
|
3
|
+
/**
|
|
4
|
+
* Extended SignaturePad class with trim method
|
|
5
|
+
*/
|
|
6
|
+
export declare class SignaturePadEx extends SignaturePad {
|
|
7
|
+
private readonly canvasElement;
|
|
8
|
+
constructor(canvasElement: HTMLCanvasElement, options?: Options);
|
|
9
|
+
/**
|
|
10
|
+
* Trim the canvas to remove empty space around the signature
|
|
11
|
+
* @returns Trimmed canvas
|
|
12
|
+
*/
|
|
13
|
+
trim(): HTMLCanvasElement;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Props for SignaturePadComponent
|
|
17
|
+
*/
|
|
18
|
+
export type SignaturePadComponentProps = {
|
|
19
|
+
/**
|
|
20
|
+
* Width of the canvas
|
|
21
|
+
*/
|
|
22
|
+
width: number | string;
|
|
23
|
+
/**
|
|
24
|
+
* Height of the canvas
|
|
25
|
+
*/
|
|
26
|
+
height: number;
|
|
27
|
+
/**
|
|
28
|
+
* Options for SignaturePad
|
|
29
|
+
*/
|
|
30
|
+
options?: Options;
|
|
31
|
+
/**
|
|
32
|
+
* Ref for the SignaturePadEx instance
|
|
33
|
+
*/
|
|
34
|
+
ref: React.RefObject<SignaturePadEx | null>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* React component for SignaturePad
|
|
38
|
+
* @param props Props
|
|
39
|
+
* @returns Component
|
|
40
|
+
*/
|
|
41
|
+
export declare function SignaturePadComponent(props: SignaturePadComponentProps): React.JSX.Element;
|
|
42
|
+
type Placement = "top" | "right" | "bottom" | "left";
|
|
43
|
+
type Alignment = "flex-start" | "center" | "flex-end";
|
|
44
|
+
/**
|
|
45
|
+
* Methods for SignaturePadFull component
|
|
46
|
+
*/
|
|
47
|
+
export interface SignaturePadFullMethods {
|
|
48
|
+
getInstance(): SignaturePadEx | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Props for SignaturePadFull component
|
|
52
|
+
*/
|
|
53
|
+
export type SignaturePadFullProps = Partial<Omit<SignaturePadComponentProps, "ref">> & {
|
|
54
|
+
/**
|
|
55
|
+
* Label for the clear button
|
|
56
|
+
* @default "Clear"
|
|
57
|
+
*/
|
|
58
|
+
clearLabel?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Label for the save button
|
|
61
|
+
* @default "Save"
|
|
62
|
+
*/
|
|
63
|
+
saveLabel?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Ref for the SignaturePadFull instance
|
|
66
|
+
*/
|
|
67
|
+
mRef?: React.RefObject<SignaturePadFullMethods | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Placement of the buttons relative to the canvas
|
|
70
|
+
* @default ["right", "flex-start"]
|
|
71
|
+
*/
|
|
72
|
+
placement?: [Placement?, alignment?: Alignment];
|
|
73
|
+
/**
|
|
74
|
+
* Callback when the signature is saved
|
|
75
|
+
* @param signaturePad SignaturePadEx instance
|
|
76
|
+
* @returns void or Promise<void>
|
|
77
|
+
*/
|
|
78
|
+
onSave?: (signaturePad: SignaturePadEx) => void | Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Spacing between the canvas and buttons
|
|
81
|
+
* @default 0.5
|
|
82
|
+
*/
|
|
83
|
+
spacing?: number;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Full SignaturePad component with clear and save buttons
|
|
87
|
+
*
|
|
88
|
+
* @param props Props
|
|
89
|
+
* @returns Component
|
|
90
|
+
*/
|
|
91
|
+
export declare function SignaturePadFull(props: SignaturePadFullProps): React.JSX.Element;
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Button from "@mui/material/Button";
|
|
3
|
+
import Paper from "@mui/material/Paper";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import SignaturePad from "signature_pad";
|
|
6
|
+
import ClearIcon from "@mui/icons-material/Clear";
|
|
7
|
+
import SaveIcon from "@mui/icons-material/Save";
|
|
8
|
+
import { CanvasUtils } from "./utils/CanvasUtils";
|
|
9
|
+
import Stack from "@mui/material/Stack";
|
|
10
|
+
import ButtonGroup from "@mui/material/ButtonGroup";
|
|
11
|
+
import { useAppContext } from "./app/ReactApp";
|
|
12
|
+
/**
|
|
13
|
+
* Extended SignaturePad class with trim method
|
|
14
|
+
*/
|
|
15
|
+
export class SignaturePadEx extends SignaturePad {
|
|
16
|
+
canvasElement;
|
|
17
|
+
constructor(canvasElement, options) {
|
|
18
|
+
super(canvasElement, options);
|
|
19
|
+
this.canvasElement = canvasElement;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Trim the canvas to remove empty space around the signature
|
|
23
|
+
* @returns Trimmed canvas
|
|
24
|
+
*/
|
|
25
|
+
trim() {
|
|
26
|
+
return CanvasUtils.trim(this.canvasElement);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* React component for SignaturePad
|
|
31
|
+
* @param props Props
|
|
32
|
+
* @returns Component
|
|
33
|
+
*/
|
|
34
|
+
export function SignaturePadComponent(props) {
|
|
35
|
+
// Destruct
|
|
36
|
+
const { width, height, options, ref } = props;
|
|
37
|
+
// Refs
|
|
38
|
+
const canvasRef = React.useRef(null);
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
const canvas = canvasRef.current;
|
|
41
|
+
if (!canvas)
|
|
42
|
+
return;
|
|
43
|
+
ref.current = new SignaturePadEx(canvas, options);
|
|
44
|
+
const observer = new ResizeObserver((entries) => {
|
|
45
|
+
const { width, height } = entries[0].contentRect;
|
|
46
|
+
canvas.width = width;
|
|
47
|
+
canvas.height = height;
|
|
48
|
+
});
|
|
49
|
+
observer.observe(canvas.parentElement);
|
|
50
|
+
return () => {
|
|
51
|
+
ref.current?.off();
|
|
52
|
+
observer.disconnect();
|
|
53
|
+
};
|
|
54
|
+
}, [options, ref]);
|
|
55
|
+
return _jsx("canvas", { ref: canvasRef, width: width, height: height });
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Full SignaturePad component with clear and save buttons
|
|
59
|
+
*
|
|
60
|
+
* @param props Props
|
|
61
|
+
* @returns Component
|
|
62
|
+
*/
|
|
63
|
+
export function SignaturePadFull(props) {
|
|
64
|
+
// Global app
|
|
65
|
+
const app = useAppContext();
|
|
66
|
+
const { clear, save } = app?.getLabels("clear", "save") ?? {};
|
|
67
|
+
// Destruct
|
|
68
|
+
const { clearLabel = clear || "Clear", saveLabel = save || "Save", width = "100%", height = 300, mRef, onSave, options, placement = ["right", "flex-start"], spacing = 0.5 } = props;
|
|
69
|
+
// Ref
|
|
70
|
+
const signaturePadRef = React.useRef(null);
|
|
71
|
+
React.useImperativeHandle(mRef, () => ({
|
|
72
|
+
getInstance: () => signaturePadRef.current
|
|
73
|
+
}));
|
|
74
|
+
function handleClear() {
|
|
75
|
+
signaturePadRef.current?.clear();
|
|
76
|
+
}
|
|
77
|
+
function handleSave() {
|
|
78
|
+
const sp = signaturePadRef.current;
|
|
79
|
+
if (!sp || sp.isEmpty())
|
|
80
|
+
return;
|
|
81
|
+
onSave?.(sp);
|
|
82
|
+
}
|
|
83
|
+
const [p = "right", a = "flex-start"] = placement;
|
|
84
|
+
let stackDirection;
|
|
85
|
+
let buttonGroupOrientation;
|
|
86
|
+
if (p === "top" || p === "bottom") {
|
|
87
|
+
stackDirection = p === "top" ? "column-reverse" : "column";
|
|
88
|
+
buttonGroupOrientation = "horizontal";
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
stackDirection = p === "left" ? "row-reverse" : "row";
|
|
92
|
+
buttonGroupOrientation = "vertical";
|
|
93
|
+
}
|
|
94
|
+
return (_jsxs(Stack, { direction: stackDirection, spacing: spacing, sx: { alignItems: a }, children: [_jsx(Paper, { sx: { width, height }, children: _jsx(SignaturePadComponent, { width: width, height: height, options: options, ref: signaturePadRef }) }), _jsxs(ButtonGroup, { orientation: buttonGroupOrientation, children: [_jsx(Button, { onClick: handleClear, startIcon: _jsx(ClearIcon, {}), children: clearLabel }), onSave && (_jsx(Button, { variant: "contained", onClick: handleSave, startIcon: _jsx(SaveIcon, {}), children: saveLabel }))] })] }));
|
|
95
|
+
}
|
|
@@ -169,6 +169,6 @@ export function UserAvatarEditor(props) {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
};
|
|
172
|
-
return (_jsxs(Stack, { direction: "column", spacing: 0.5, sx: { width: containerWidth }, children: [_jsx(FileUploadButton, { variant: "outlined", size: "medium", startIcon: _jsx(ImageIcon, {}), fullWidth: true, onUploadFiles: handleFileUpload, inputProps: { accept: "image/png, image/jpeg" }, children: selectFileLabel }), _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(React.Suspense, { fallback: _jsx(Skeleton, { variant: "rounded", width: width, height: localHeight }), children: _jsx(AvatarEditor, { ref: ref, border: border, width: width, height: localHeight, onLoadSuccess: handleLoad, image: previewImage ??
|
|
172
|
+
return (_jsxs(Stack, { direction: "column", spacing: 0.5, sx: { width: containerWidth }, children: [_jsx(FileUploadButton, { variant: "outlined", size: "medium", startIcon: _jsx(ImageIcon, {}), fullWidth: true, onUploadFiles: handleFileUpload, inputProps: { accept: "image/png, image/jpeg, image/webp" }, children: selectFileLabel }), _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(React.Suspense, { fallback: _jsx(Skeleton, { variant: "rounded", width: width, height: localHeight }), children: _jsx(AvatarEditor, { ref: ref, border: border, width: width, height: localHeight, onLoadSuccess: handleLoad, image: previewImage ??
|
|
173
173
|
"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=", scale: editorState.scale, rotate: editorState.rotate }) }), _jsxs(ButtonGroup, { size: "small", orientation: "vertical", disabled: !ready, children: [_jsx(Button, { onClick: () => handleRotate(90), title: labels.rotateRight, children: _jsx(RotateRightIcon, {}) }), _jsx(Button, { onClick: () => handleRotate(-90), title: labels.rotateLeft, children: _jsx(RotateLeftIcon, {}) }), _jsx(Button, { onClick: handleReset, title: labels.reset, children: _jsx(ClearAllIcon, {}) })] })] }), _jsxs(Stack, { spacing: 0.5, direction: "row", sx: { alignItems: "center", paddingBottom: 2 }, children: [_jsx(IconButton, { size: "small", disabled: !ready || editorState.scale <= min, onClick: () => adjustScale(false), children: _jsx(RemoveIcon, {}) }), _jsx(Slider, { title: labels.zoom, disabled: !ready, min: min, max: max, step: step, value: editorState.scale, valueLabelDisplay: "auto", valueLabelFormat: (value) => `${Math.round(100 * value) / 100}`, marks: marks, onChange: handleZoom }), _jsx(IconButton, { size: "small", disabled: !ready || editorState.scale >= max, onClick: () => adjustScale(true), children: _jsx(AddIcon, {}) })] }), _jsx(Button, { ref: buttonRef, variant: "contained", startIcon: _jsx(DoneIcon, {}), disabled: !ready, onClick: handleDone, children: labels.done })] }));
|
|
174
174
|
}
|
package/lib/mjs/index.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export * from "./pages/ViewPage";
|
|
|
35
35
|
export * from "./texts/DateText";
|
|
36
36
|
export * from "./texts/MoneyText";
|
|
37
37
|
export * from "./texts/NumberText";
|
|
38
|
+
export * from "./utils/CanvasUtils";
|
|
38
39
|
export * from "./AddresSelector";
|
|
39
40
|
export * from "./AuditDisplay";
|
|
40
41
|
export * from "./AutocompleteExtendedProps";
|
|
@@ -122,6 +123,7 @@ export * from "./SearchOptionGroup";
|
|
|
122
123
|
export * from "./SelectBool";
|
|
123
124
|
export * from "./SelectEx";
|
|
124
125
|
export * from "./ShowDataComparison";
|
|
126
|
+
export * from "./SignaturePadComponent";
|
|
125
127
|
export * from "./Switch";
|
|
126
128
|
export * from "./SwitchAnt";
|
|
127
129
|
export * from "./SwitchField";
|
package/lib/mjs/index.js
CHANGED
|
@@ -35,6 +35,7 @@ export * from "./pages/ViewPage";
|
|
|
35
35
|
export * from "./texts/DateText";
|
|
36
36
|
export * from "./texts/MoneyText";
|
|
37
37
|
export * from "./texts/NumberText";
|
|
38
|
+
export * from "./utils/CanvasUtils";
|
|
38
39
|
export * from "./AddresSelector";
|
|
39
40
|
export * from "./AuditDisplay";
|
|
40
41
|
export * from "./AutocompleteExtendedProps";
|
|
@@ -122,6 +123,7 @@ export * from "./SearchOptionGroup";
|
|
|
122
123
|
export * from "./SelectBool";
|
|
123
124
|
export * from "./SelectEx";
|
|
124
125
|
export * from "./ShowDataComparison";
|
|
126
|
+
export * from "./SignaturePadComponent";
|
|
125
127
|
export * from "./Switch";
|
|
126
128
|
export * from "./SwitchAnt";
|
|
127
129
|
export * from "./SwitchField";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for canvas operations
|
|
3
|
+
*/
|
|
4
|
+
export declare namespace CanvasUtils {
|
|
5
|
+
/**
|
|
6
|
+
* JPEG content type
|
|
7
|
+
*/
|
|
8
|
+
const JPG = "image/jpeg";
|
|
9
|
+
/**
|
|
10
|
+
* PNG content type
|
|
11
|
+
*/
|
|
12
|
+
const PNG = "image/png";
|
|
13
|
+
/**
|
|
14
|
+
* WEBP content type
|
|
15
|
+
*/
|
|
16
|
+
const WEBP = "image/webp";
|
|
17
|
+
/**
|
|
18
|
+
* SVG content type
|
|
19
|
+
*/
|
|
20
|
+
const SVG = "image/svg+xml";
|
|
21
|
+
/**
|
|
22
|
+
* Convert canvas to Blob
|
|
23
|
+
* @param canvas Canvas element
|
|
24
|
+
* @param type MIME type of the image
|
|
25
|
+
* @param quality Image quality (0 to 1)
|
|
26
|
+
* @returns Promise that resolves to a Blob
|
|
27
|
+
*/
|
|
28
|
+
function toBlob(canvas: HTMLCanvasElement, type?: string, quality?: number): Promise<Blob>;
|
|
29
|
+
/**
|
|
30
|
+
* Trim the canvas to remove empty space around the signature
|
|
31
|
+
* @param canvas Canvas element
|
|
32
|
+
* @returns Trimmed canvas
|
|
33
|
+
*/
|
|
34
|
+
function trim(canvas: HTMLCanvasElement): HTMLCanvasElement;
|
|
35
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for canvas operations
|
|
3
|
+
*/
|
|
4
|
+
export var CanvasUtils;
|
|
5
|
+
(function (CanvasUtils) {
|
|
6
|
+
/**
|
|
7
|
+
* JPEG content type
|
|
8
|
+
*/
|
|
9
|
+
CanvasUtils.JPG = "image/jpeg";
|
|
10
|
+
/**
|
|
11
|
+
* PNG content type
|
|
12
|
+
*/
|
|
13
|
+
CanvasUtils.PNG = "image/png";
|
|
14
|
+
/**
|
|
15
|
+
* WEBP content type
|
|
16
|
+
*/
|
|
17
|
+
CanvasUtils.WEBP = "image/webp";
|
|
18
|
+
/**
|
|
19
|
+
* SVG content type
|
|
20
|
+
*/
|
|
21
|
+
CanvasUtils.SVG = "image/svg+xml";
|
|
22
|
+
/**
|
|
23
|
+
* Convert canvas to Blob
|
|
24
|
+
* @param canvas Canvas element
|
|
25
|
+
* @param type MIME type of the image
|
|
26
|
+
* @param quality Image quality (0 to 1)
|
|
27
|
+
* @returns Promise that resolves to a Blob
|
|
28
|
+
*/
|
|
29
|
+
function toBlob(canvas, type = "image/png", quality = 1) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
canvas.toBlob((blob) => {
|
|
32
|
+
if (blob) {
|
|
33
|
+
resolve(blob);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
reject(new Error("Failed to create Blob."));
|
|
37
|
+
}
|
|
38
|
+
}, type, quality);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
CanvasUtils.toBlob = toBlob;
|
|
42
|
+
/**
|
|
43
|
+
* Trim the canvas to remove empty space around the signature
|
|
44
|
+
* @param canvas Canvas element
|
|
45
|
+
* @returns Trimmed canvas
|
|
46
|
+
*/
|
|
47
|
+
function trim(canvas) {
|
|
48
|
+
const ctx = canvas.getContext("2d");
|
|
49
|
+
const { width, height } = canvas;
|
|
50
|
+
const { data } = ctx.getImageData(0, 0, width, height);
|
|
51
|
+
let top = height;
|
|
52
|
+
let left = width;
|
|
53
|
+
let right = 0;
|
|
54
|
+
let bottom = 0;
|
|
55
|
+
for (let y = 0; y < height; y++) {
|
|
56
|
+
for (let x = 0; x < width; x++) {
|
|
57
|
+
if (data[(y * width + x) * 4 + 3] === 0)
|
|
58
|
+
continue;
|
|
59
|
+
if (x < left)
|
|
60
|
+
left = x;
|
|
61
|
+
if (x > right)
|
|
62
|
+
right = x;
|
|
63
|
+
if (y < top)
|
|
64
|
+
top = y;
|
|
65
|
+
if (y > bottom)
|
|
66
|
+
bottom = y;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const trimmed = document.createElement("canvas");
|
|
70
|
+
// Empty canvas
|
|
71
|
+
if (right < left || bottom < top)
|
|
72
|
+
return trimmed;
|
|
73
|
+
trimmed.width = right - left + 1;
|
|
74
|
+
trimmed.height = bottom - top + 1;
|
|
75
|
+
trimmed
|
|
76
|
+
.getContext("2d")
|
|
77
|
+
.drawImage(canvas, left, top, trimmed.width, trimmed.height, 0, 0, trimmed.width, trimmed.height);
|
|
78
|
+
return trimmed;
|
|
79
|
+
}
|
|
80
|
+
CanvasUtils.trim = trim;
|
|
81
|
+
})(CanvasUtils || (CanvasUtils = {}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@etsoo/materialui",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.78",
|
|
4
4
|
"description": "TypeScript Material-UI Implementation",
|
|
5
5
|
"main": "lib/cjs/index.js",
|
|
6
6
|
"module": "lib/mjs/index.js",
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"@etsoo/notificationbase": "^1.1.72",
|
|
45
45
|
"@etsoo/react": "^1.8.94",
|
|
46
46
|
"@etsoo/shared": "^1.2.87",
|
|
47
|
-
"@mui/icons-material": "^9.
|
|
48
|
-
"@mui/material": "^9.
|
|
49
|
-
"@mui/x-data-grid": "^9.
|
|
47
|
+
"@mui/icons-material": "^9.2.0",
|
|
48
|
+
"@mui/material": "^9.2.0",
|
|
49
|
+
"@mui/x-data-grid": "^9.8.0",
|
|
50
50
|
"chart.js": "^4.5.1",
|
|
51
51
|
"chartjs-plugin-datalabels": "^2.2.0",
|
|
52
52
|
"dompurify": "^3.4.11",
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
"react-chartjs-2": "^5.3.1",
|
|
59
59
|
"react-dom": "^19.2.7",
|
|
60
60
|
"react-draggable": "^4.7.0",
|
|
61
|
-
"react-imask": "7.6.1"
|
|
61
|
+
"react-imask": "7.6.1",
|
|
62
|
+
"signature_pad": "^5.1.3"
|
|
62
63
|
},
|
|
63
64
|
"overrides": {
|
|
64
65
|
"react": "$react",
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import Button from "@mui/material/Button";
|
|
2
|
+
import Paper from "@mui/material/Paper";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import SignaturePad, { Options } from "signature_pad";
|
|
5
|
+
import ClearIcon from "@mui/icons-material/Clear";
|
|
6
|
+
import SaveIcon from "@mui/icons-material/Save";
|
|
7
|
+
import { ResponsiveStyleValue } from "@mui/system";
|
|
8
|
+
import { CanvasUtils } from "./utils/CanvasUtils";
|
|
9
|
+
import Stack from "@mui/material/Stack";
|
|
10
|
+
import ButtonGroup from "@mui/material/ButtonGroup";
|
|
11
|
+
import { useAppContext } from "./app/ReactApp";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extended SignaturePad class with trim method
|
|
15
|
+
*/
|
|
16
|
+
export class SignaturePadEx extends SignaturePad {
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly canvasElement: HTMLCanvasElement,
|
|
19
|
+
options?: Options
|
|
20
|
+
) {
|
|
21
|
+
super(canvasElement, options);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Trim the canvas to remove empty space around the signature
|
|
26
|
+
* @returns Trimmed canvas
|
|
27
|
+
*/
|
|
28
|
+
trim(): HTMLCanvasElement {
|
|
29
|
+
return CanvasUtils.trim(this.canvasElement);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Props for SignaturePadComponent
|
|
35
|
+
*/
|
|
36
|
+
export type SignaturePadComponentProps = {
|
|
37
|
+
/**
|
|
38
|
+
* Width of the canvas
|
|
39
|
+
*/
|
|
40
|
+
width: number | string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Height of the canvas
|
|
44
|
+
*/
|
|
45
|
+
height: number;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Options for SignaturePad
|
|
49
|
+
*/
|
|
50
|
+
options?: Options;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Ref for the SignaturePadEx instance
|
|
54
|
+
*/
|
|
55
|
+
ref: React.RefObject<SignaturePadEx | null>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* React component for SignaturePad
|
|
60
|
+
* @param props Props
|
|
61
|
+
* @returns Component
|
|
62
|
+
*/
|
|
63
|
+
export function SignaturePadComponent(props: SignaturePadComponentProps) {
|
|
64
|
+
// Destruct
|
|
65
|
+
const { width, height, options, ref } = props;
|
|
66
|
+
|
|
67
|
+
// Refs
|
|
68
|
+
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
|
69
|
+
|
|
70
|
+
React.useEffect(() => {
|
|
71
|
+
const canvas = canvasRef.current;
|
|
72
|
+
if (!canvas) return;
|
|
73
|
+
|
|
74
|
+
ref.current = new SignaturePadEx(canvas, options);
|
|
75
|
+
|
|
76
|
+
const observer = new ResizeObserver((entries) => {
|
|
77
|
+
const { width, height } = entries[0].contentRect;
|
|
78
|
+
|
|
79
|
+
canvas.width = width;
|
|
80
|
+
canvas.height = height;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
observer.observe(canvas.parentElement!);
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
ref.current?.off();
|
|
87
|
+
observer.disconnect();
|
|
88
|
+
};
|
|
89
|
+
}, [options, ref]);
|
|
90
|
+
|
|
91
|
+
return <canvas ref={canvasRef} width={width} height={height} />;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type Placement = "top" | "right" | "bottom" | "left";
|
|
95
|
+
type Alignment = "flex-start" | "center" | "flex-end";
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Methods for SignaturePadFull component
|
|
99
|
+
*/
|
|
100
|
+
export interface SignaturePadFullMethods {
|
|
101
|
+
getInstance(): SignaturePadEx | null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Props for SignaturePadFull component
|
|
106
|
+
*/
|
|
107
|
+
export type SignaturePadFullProps = Partial<
|
|
108
|
+
Omit<SignaturePadComponentProps, "ref">
|
|
109
|
+
> & {
|
|
110
|
+
/**
|
|
111
|
+
* Label for the clear button
|
|
112
|
+
* @default "Clear"
|
|
113
|
+
*/
|
|
114
|
+
clearLabel?: string;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Label for the save button
|
|
118
|
+
* @default "Save"
|
|
119
|
+
*/
|
|
120
|
+
saveLabel?: string;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Ref for the SignaturePadFull instance
|
|
124
|
+
*/
|
|
125
|
+
mRef?: React.RefObject<SignaturePadFullMethods | null>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Placement of the buttons relative to the canvas
|
|
129
|
+
* @default ["right", "flex-start"]
|
|
130
|
+
*/
|
|
131
|
+
placement?: [Placement?, alignment?: Alignment];
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Callback when the signature is saved
|
|
135
|
+
* @param signaturePad SignaturePadEx instance
|
|
136
|
+
* @returns void or Promise<void>
|
|
137
|
+
*/
|
|
138
|
+
onSave?: (signaturePad: SignaturePadEx) => void | Promise<void>;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Spacing between the canvas and buttons
|
|
142
|
+
* @default 0.5
|
|
143
|
+
*/
|
|
144
|
+
spacing?: number;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Full SignaturePad component with clear and save buttons
|
|
149
|
+
*
|
|
150
|
+
* @param props Props
|
|
151
|
+
* @returns Component
|
|
152
|
+
*/
|
|
153
|
+
export function SignaturePadFull(props: SignaturePadFullProps) {
|
|
154
|
+
// Global app
|
|
155
|
+
const app = useAppContext();
|
|
156
|
+
const { clear, save } = app?.getLabels("clear", "save") ?? {};
|
|
157
|
+
|
|
158
|
+
// Destruct
|
|
159
|
+
const {
|
|
160
|
+
clearLabel = clear || "Clear",
|
|
161
|
+
saveLabel = save || "Save",
|
|
162
|
+
width = "100%",
|
|
163
|
+
height = 300,
|
|
164
|
+
mRef,
|
|
165
|
+
onSave,
|
|
166
|
+
options,
|
|
167
|
+
placement = ["right", "flex-start"],
|
|
168
|
+
spacing = 0.5
|
|
169
|
+
} = props;
|
|
170
|
+
|
|
171
|
+
// Ref
|
|
172
|
+
const signaturePadRef = React.useRef<SignaturePadEx>(null);
|
|
173
|
+
|
|
174
|
+
React.useImperativeHandle(mRef, () => ({
|
|
175
|
+
getInstance: () => signaturePadRef.current
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
function handleClear() {
|
|
179
|
+
signaturePadRef.current?.clear();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function handleSave() {
|
|
183
|
+
const sp = signaturePadRef.current;
|
|
184
|
+
if (!sp || sp.isEmpty()) return;
|
|
185
|
+
|
|
186
|
+
onSave?.(sp);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const [p = "right", a = "flex-start"] = placement;
|
|
190
|
+
|
|
191
|
+
let stackDirection: ResponsiveStyleValue<
|
|
192
|
+
"row" | "row-reverse" | "column" | "column-reverse"
|
|
193
|
+
>;
|
|
194
|
+
let buttonGroupOrientation: "horizontal" | "vertical";
|
|
195
|
+
|
|
196
|
+
if (p === "top" || p === "bottom") {
|
|
197
|
+
stackDirection = p === "top" ? "column-reverse" : "column";
|
|
198
|
+
buttonGroupOrientation = "horizontal";
|
|
199
|
+
} else {
|
|
200
|
+
stackDirection = p === "left" ? "row-reverse" : "row";
|
|
201
|
+
buttonGroupOrientation = "vertical";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<Stack direction={stackDirection} spacing={spacing} sx={{ alignItems: a }}>
|
|
206
|
+
<Paper sx={{ width, height }}>
|
|
207
|
+
<SignaturePadComponent
|
|
208
|
+
width={width}
|
|
209
|
+
height={height}
|
|
210
|
+
options={options}
|
|
211
|
+
ref={signaturePadRef}
|
|
212
|
+
/>
|
|
213
|
+
</Paper>
|
|
214
|
+
<ButtonGroup orientation={buttonGroupOrientation}>
|
|
215
|
+
<Button onClick={handleClear} startIcon={<ClearIcon />}>
|
|
216
|
+
{clearLabel}
|
|
217
|
+
</Button>
|
|
218
|
+
{onSave && (
|
|
219
|
+
<Button
|
|
220
|
+
variant="contained"
|
|
221
|
+
onClick={handleSave}
|
|
222
|
+
startIcon={<SaveIcon />}
|
|
223
|
+
>
|
|
224
|
+
{saveLabel}
|
|
225
|
+
</Button>
|
|
226
|
+
)}
|
|
227
|
+
</ButtonGroup>
|
|
228
|
+
</Stack>
|
|
229
|
+
);
|
|
230
|
+
}
|
package/src/UserAvatarEditor.tsx
CHANGED
|
@@ -314,7 +314,7 @@ export function UserAvatarEditor(props: UserAvatarEditorProps) {
|
|
|
314
314
|
startIcon={<ImageIcon />}
|
|
315
315
|
fullWidth
|
|
316
316
|
onUploadFiles={handleFileUpload}
|
|
317
|
-
inputProps={{ accept: "image/png, image/jpeg" }}
|
|
317
|
+
inputProps={{ accept: "image/png, image/jpeg, image/webp" }}
|
|
318
318
|
>
|
|
319
319
|
{selectFileLabel}
|
|
320
320
|
</FileUploadButton>
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,8 @@ export * from "./texts/DateText";
|
|
|
41
41
|
export * from "./texts/MoneyText";
|
|
42
42
|
export * from "./texts/NumberText";
|
|
43
43
|
|
|
44
|
+
export * from "./utils/CanvasUtils";
|
|
45
|
+
|
|
44
46
|
export * from "./AddresSelector";
|
|
45
47
|
export * from "./AuditDisplay";
|
|
46
48
|
export * from "./AutocompleteExtendedProps";
|
|
@@ -128,6 +130,7 @@ export * from "./SearchOptionGroup";
|
|
|
128
130
|
export * from "./SelectBool";
|
|
129
131
|
export * from "./SelectEx";
|
|
130
132
|
export * from "./ShowDataComparison";
|
|
133
|
+
export * from "./SignaturePadComponent";
|
|
131
134
|
export * from "./Switch";
|
|
132
135
|
export * from "./SwitchAnt";
|
|
133
136
|
export * from "./SwitchField";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for canvas operations
|
|
3
|
+
*/
|
|
4
|
+
export namespace CanvasUtils {
|
|
5
|
+
/**
|
|
6
|
+
* JPEG content type
|
|
7
|
+
*/
|
|
8
|
+
export const JPG = "image/jpeg";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* PNG content type
|
|
12
|
+
*/
|
|
13
|
+
export const PNG = "image/png";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* WEBP content type
|
|
17
|
+
*/
|
|
18
|
+
export const WEBP = "image/webp";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* SVG content type
|
|
22
|
+
*/
|
|
23
|
+
export const SVG = "image/svg+xml";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert canvas to Blob
|
|
27
|
+
* @param canvas Canvas element
|
|
28
|
+
* @param type MIME type of the image
|
|
29
|
+
* @param quality Image quality (0 to 1)
|
|
30
|
+
* @returns Promise that resolves to a Blob
|
|
31
|
+
*/
|
|
32
|
+
export function toBlob(
|
|
33
|
+
canvas: HTMLCanvasElement,
|
|
34
|
+
type = "image/png",
|
|
35
|
+
quality = 1
|
|
36
|
+
): Promise<Blob> {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
canvas.toBlob(
|
|
39
|
+
(blob) => {
|
|
40
|
+
if (blob) {
|
|
41
|
+
resolve(blob);
|
|
42
|
+
} else {
|
|
43
|
+
reject(new Error("Failed to create Blob."));
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
type,
|
|
47
|
+
quality
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Trim the canvas to remove empty space around the signature
|
|
54
|
+
* @param canvas Canvas element
|
|
55
|
+
* @returns Trimmed canvas
|
|
56
|
+
*/
|
|
57
|
+
export function trim(canvas: HTMLCanvasElement): HTMLCanvasElement {
|
|
58
|
+
const ctx = canvas.getContext("2d")!;
|
|
59
|
+
const { width, height } = canvas;
|
|
60
|
+
|
|
61
|
+
const { data } = ctx.getImageData(0, 0, width, height);
|
|
62
|
+
|
|
63
|
+
let top = height;
|
|
64
|
+
let left = width;
|
|
65
|
+
let right = 0;
|
|
66
|
+
let bottom = 0;
|
|
67
|
+
|
|
68
|
+
for (let y = 0; y < height; y++) {
|
|
69
|
+
for (let x = 0; x < width; x++) {
|
|
70
|
+
if (data[(y * width + x) * 4 + 3] === 0) continue;
|
|
71
|
+
|
|
72
|
+
if (x < left) left = x;
|
|
73
|
+
if (x > right) right = x;
|
|
74
|
+
if (y < top) top = y;
|
|
75
|
+
if (y > bottom) bottom = y;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const trimmed = document.createElement("canvas");
|
|
80
|
+
|
|
81
|
+
// Empty canvas
|
|
82
|
+
if (right < left || bottom < top) return trimmed;
|
|
83
|
+
|
|
84
|
+
trimmed.width = right - left + 1;
|
|
85
|
+
trimmed.height = bottom - top + 1;
|
|
86
|
+
|
|
87
|
+
trimmed
|
|
88
|
+
.getContext("2d")!
|
|
89
|
+
.drawImage(
|
|
90
|
+
canvas,
|
|
91
|
+
left,
|
|
92
|
+
top,
|
|
93
|
+
trimmed.width,
|
|
94
|
+
trimmed.height,
|
|
95
|
+
0,
|
|
96
|
+
0,
|
|
97
|
+
trimmed.width,
|
|
98
|
+
trimmed.height
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return trimmed;
|
|
102
|
+
}
|
|
103
|
+
}
|