@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.
@@ -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
  }
@@ -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
  }
@@ -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.76",
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.1.1",
48
- "@mui/material": "^9.1.2",
49
- "@mui/x-data-grid": "^9.7.0",
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
+ }
@@ -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
+ }