@kwiz/fluentui 1.0.73 → 1.0.75
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/.github/workflows/npm-publish.yml +24 -24
- package/LICENSE +21 -21
- package/README.md +53 -53
- package/dist/@types/forwardRef.d.ts +0 -0
- package/dist/@types/forwardRef.js +1 -0
- package/dist/@types/forwardRef.js.map +1 -0
- package/dist/controls/error-boundary copy.d.ts +23 -0
- package/dist/controls/error-boundary copy.js +33 -0
- package/dist/controls/error-boundary copy.js.map +1 -0
- package/dist/controls/menu.js +2 -2
- package/dist/controls/menu.js.map +1 -1
- package/dist/controls/search.js +19 -11
- package/dist/controls/search.js.map +1 -1
- package/dist/controls/svg.js +21 -21
- package/dist/controls/svg.js.map +1 -1
- package/dist/helpers/common.d.ts +4 -0
- package/dist/helpers/common.js +2 -0
- package/dist/helpers/common.js.map +1 -0
- package/dist/helpers/context.d.ts +26 -0
- package/dist/helpers/context.js +15 -0
- package/dist/helpers/context.js.map +1 -0
- package/dist/helpers/drag-drop/exports.d.ts +12 -0
- package/dist/helpers/drag-drop/exports.js +3 -0
- package/dist/helpers/drag-drop/exports.js.map +1 -0
- package/dist/helpers/exports.d.ts +7 -0
- package/dist/helpers/exports.js +8 -0
- package/dist/helpers/exports.js.map +1 -0
- package/dist/helpers/use-editable-control.d.ts +1 -1
- package/dist/helpers/use-editable-control.js.map +1 -1
- package/package.json +85 -84
- package/src/_modules/config.ts +9 -9
- package/src/_modules/constants.ts +3 -3
- package/src/controls/ColorPickerDialog.tsx +83 -83
- package/src/controls/accordion.tsx +62 -62
- package/src/controls/button.tsx +180 -180
- package/src/controls/canvas/CustomEventTargetBase.ts +32 -32
- package/src/controls/canvas/DrawPad.tsx +296 -296
- package/src/controls/canvas/DrawPadManager.ts +694 -694
- package/src/controls/canvas/bezier.ts +109 -109
- package/src/controls/canvas/point.ts +44 -44
- package/src/controls/card-list.tsx +31 -31
- package/src/controls/card.tsx +77 -77
- package/src/controls/centered.tsx +14 -14
- package/src/controls/date.tsx +87 -87
- package/src/controls/diagram-picker.tsx +96 -96
- package/src/controls/divider.tsx +15 -15
- package/src/controls/dropdown.tsx +66 -66
- package/src/controls/error-boundary.tsx +41 -41
- package/src/controls/field-editor.tsx +42 -42
- package/src/controls/file-upload.tsx +155 -155
- package/src/controls/horizontal.tsx +48 -48
- package/src/controls/html-editor/editor.tsx +182 -182
- package/src/controls/index.ts +33 -33
- package/src/controls/input.tsx +160 -160
- package/src/controls/kwizoverflow.tsx +106 -106
- package/src/controls/list.tsx +119 -119
- package/src/controls/loading.tsx +10 -10
- package/src/controls/menu.tsx +173 -173
- package/src/controls/merge-text.tsx +126 -126
- package/src/controls/please-wait.tsx +32 -32
- package/src/controls/progress-bar.tsx +109 -109
- package/src/controls/prompt.tsx +121 -121
- package/src/controls/qrcode.tsx +36 -36
- package/src/controls/search.tsx +71 -61
- package/src/controls/section.tsx +133 -133
- package/src/controls/svg.tsx +138 -138
- package/src/controls/toolbar.tsx +46 -46
- package/src/controls/vertical-content.tsx +49 -49
- package/src/controls/vertical.tsx +42 -42
- package/src/helpers/block-nav.tsx +88 -88
- package/src/helpers/context-const.ts +29 -29
- package/src/helpers/context-export.tsx +77 -77
- package/src/helpers/context-internal.ts +13 -13
- package/src/helpers/drag-drop/drag-drop-container.tsx +53 -53
- package/src/helpers/drag-drop/drag-drop-context-internal.tsx +9 -9
- package/src/helpers/drag-drop/drag-drop-context.tsx +61 -61
- package/src/helpers/drag-drop/drag-drop.types.ts +21 -21
- package/src/helpers/drag-drop/index.ts +12 -12
- package/src/helpers/drag-drop/readme.md +75 -75
- package/src/helpers/drag-drop/use-draggable.ts +47 -47
- package/src/helpers/drag-drop/use-droppable.ts +38 -38
- package/src/helpers/forwardRef.ts +7 -7
- package/src/helpers/hooks-events.ts +149 -149
- package/src/helpers/hooks.tsx +141 -141
- package/src/helpers/index.ts +8 -8
- package/src/helpers/use-alerts.tsx +74 -74
- package/src/helpers/use-editable-control.tsx +37 -37
- package/src/helpers/use-toast.tsx +29 -29
- package/src/index.ts +2 -2
- package/src/styles/index.ts +1 -1
- package/src/styles/styles.ts +104 -104
- package/src/styles/theme.ts +90 -90
@@ -1,297 +1,297 @@
|
|
1
|
-
import { Field, tokens } from "@fluentui/react-components";
|
2
|
-
import { ArrowMaximizeRegular, ArrowMinimizeRegular, ArrowUploadRegular, CalligraphyPenRegular, DismissRegular } from "@fluentui/react-icons";
|
3
|
-
import { debounce, getCSSVariableValue, ImageFileTypes, isElement, isNotEmptyString, isNullOrEmptyArray, isNullOrEmptyString, isNullOrUndefined } from "@kwiz/common";
|
4
|
-
import * as React from "react";
|
5
|
-
import { useAlerts, useElementSize, useStateEX } from "../../helpers";
|
6
|
-
import { ButtonEX } from "../button";
|
7
|
-
import { ColorPickerEx } from "../ColorPickerDialog";
|
8
|
-
import { FileUpload } from "../file-upload";
|
9
|
-
import { Horizontal } from "../horizontal";
|
10
|
-
import { InputEx } from "../input";
|
11
|
-
import { Vertical } from "../vertical";
|
12
|
-
import DrawPadManager from "./DrawPadManager";
|
13
|
-
|
14
|
-
interface iProps {
|
15
|
-
BackgroundColor?: string;
|
16
|
-
BorderColor?: string;
|
17
|
-
LineColor?: string;
|
18
|
-
minWidth?: number;
|
19
|
-
minHeight?: number;
|
20
|
-
/** url or base64 image data:image/png;base64,.... */
|
21
|
-
Value?: string;
|
22
|
-
/** when user hits clear, it will reset to this value
|
23
|
-
* url or base64 image data:image/png;base64,....
|
24
|
-
*/
|
25
|
-
DefaultBackdrop?: string;
|
26
|
-
OnChange?: (newValue: string) => void;
|
27
|
-
ReadOnly?: boolean;
|
28
|
-
HideUpload?: boolean;
|
29
|
-
HideClear?: boolean;
|
30
|
-
//HideUndo?: boolean;
|
31
|
-
HideColorPicker?: boolean;
|
32
|
-
disabled?: boolean;
|
33
|
-
/** true - will prompt user for his name, string will just sign as that string */
|
34
|
-
allowSigning?: boolean | string;
|
35
|
-
allowFullscreen?: boolean;
|
36
|
-
}
|
37
|
-
var _userName: string = null;
|
38
|
-
export const DrawPadUserName = {
|
39
|
-
get: () => { return _userName },
|
40
|
-
set: (userName: string) => { _userName = userName }
|
41
|
-
};
|
42
|
-
|
43
|
-
const fontName = "Dancing Script";
|
44
|
-
let fontLoading: Promise<boolean> = null;
|
45
|
-
let fontReady = false;
|
46
|
-
export const DrawPad: React.FunctionComponent<iProps> = (props) => {
|
47
|
-
const [LineColor, setLineColor] = useStateEX<string>(props.LineColor || tokens.colorBrandForeground1);
|
48
|
-
const [manager, setmanager] = useStateEX<DrawPadManager>(null);
|
49
|
-
const [canUndo, setcanUndo] = useStateEX<boolean>(false, { skipUpdateIfSame: true });
|
50
|
-
const [signed, setSigned] = useStateEX<boolean>(false);
|
51
|
-
const [fullscreen, setFullscreen] = useStateEX<boolean>(false);
|
52
|
-
const onChangeRef = React.useRef(props.OnChange);
|
53
|
-
const alerts = useAlerts();
|
54
|
-
const canvasArea: React.RefObject<HTMLCanvasElement> = React.useRef();
|
55
|
-
const canvasContainerDiv = React.useRef<HTMLDivElement>();
|
56
|
-
|
57
|
-
//keep onChange up to date
|
58
|
-
React.useEffect(() => {
|
59
|
-
onChangeRef.current = props.OnChange;
|
60
|
-
}, [props.OnChange]);
|
61
|
-
//if user name provided - keep it
|
62
|
-
React.useEffect(() => {
|
63
|
-
if (isNotEmptyString(props.allowSigning)) {
|
64
|
-
DrawPadUserName.set(props.allowSigning);
|
65
|
-
}
|
66
|
-
}, [props.allowSigning]);
|
67
|
-
|
68
|
-
//load font for sign as text, if needed
|
69
|
-
React.useEffect(() => {
|
70
|
-
if (props.allowSigning && !fontLoading) {
|
71
|
-
let DancingScriptFont = new FontFace(
|
72
|
-
fontName,
|
73
|
-
"url(https://fonts.gstatic.com/s/dancingscript/v25/If2RXTr6YS-zF4S-kcSWSVi_szLgiuE.woff2) format('woff2')",
|
74
|
-
{
|
75
|
-
style: "normal",
|
76
|
-
weight: "400 700",
|
77
|
-
display: "swap",
|
78
|
-
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
79
|
-
}
|
80
|
-
);
|
81
|
-
|
82
|
-
fontLoading = DancingScriptFont.load().then(async loadedFont => {
|
83
|
-
document.fonts.add(loadedFont);
|
84
|
-
await document.fonts.ready;
|
85
|
-
fontReady = true;
|
86
|
-
return true;
|
87
|
-
});
|
88
|
-
}
|
89
|
-
}, [props.allowSigning]);
|
90
|
-
|
91
|
-
//setup manager
|
92
|
-
React.useEffect(() => {
|
93
|
-
if (!props.ReadOnly && canvasArea.current && !canvasArea.current["canvas_initialized"]) {
|
94
|
-
canvasArea.current["canvas_initialized"] = true;//for some reason this still fires twice on the same div
|
95
|
-
//this gets called after each render...
|
96
|
-
let manager = new DrawPadManager(canvasArea.current);
|
97
|
-
setmanager(manager);
|
98
|
-
manager.addEventListener("endStroke", () => {
|
99
|
-
//because this is used in an addEventListener - need to have a ref to get the most up to date onChange
|
100
|
-
if (onChangeRef.current) {
|
101
|
-
let canvasValue = manager.toPng();
|
102
|
-
setcanUndo(true);
|
103
|
-
onChangeRef.current(canvasValue);
|
104
|
-
}
|
105
|
-
});
|
106
|
-
}
|
107
|
-
}, [canvasArea]);
|
108
|
-
//update line color selection
|
109
|
-
React.useEffect(() => {
|
110
|
-
if (manager) {
|
111
|
-
manager.penColor.value = getCSSVariableValue(LineColor, canvasArea.current);
|
112
|
-
}
|
113
|
-
}, [manager, LineColor]);
|
114
|
-
|
115
|
-
//Set props value to canvas on initial load or when owner element changes the prop
|
116
|
-
React.useEffect(() => {
|
117
|
-
if (manager) {
|
118
|
-
let canvasValue = manager.toPng();
|
119
|
-
let neededValue = isNotEmptyString(props.Value)
|
120
|
-
? props.Value
|
121
|
-
: isNotEmptyString(props.DefaultBackdrop)
|
122
|
-
? props.DefaultBackdrop
|
123
|
-
: "";
|
124
|
-
if (canvasValue !== neededValue ||
|
125
|
-
(
|
126
|
-
isNullOrEmptyString(canvasValue) &&
|
127
|
-
isNullOrEmptyString(neededValue)
|
128
|
-
)
|
129
|
-
) {
|
130
|
-
UpdateCanvas(neededValue);//if called repeatedly or too fast - may not load correctly
|
131
|
-
}
|
132
|
-
}
|
133
|
-
});//cant use those - canvas loses value on drag so must run this every time: [props.Value, props.DefaultBackdrop, manager]);
|
134
|
-
|
135
|
-
//set value to canvas
|
136
|
-
const UpdateCanvas = React.useCallback(debounce((valueToSet: string) => {
|
137
|
-
if (valueToSet === "") manager.clear();
|
138
|
-
else manager.fromDataURL(valueToSet);
|
139
|
-
}, 200, this), [manager]);
|
140
|
-
|
141
|
-
//enable/disable canvas manager
|
142
|
-
React.useEffect(() => {
|
143
|
-
if (manager) {
|
144
|
-
if (props.disabled) manager.off();//stop accepting strokes, but still allow to set a default value
|
145
|
-
else manager.on();
|
146
|
-
}
|
147
|
-
}, [manager, props.disabled]);
|
148
|
-
|
149
|
-
const sign = React.useCallback((name: string) => {
|
150
|
-
let canvas = canvasArea.current;
|
151
|
-
if (!isElement(canvas)) {
|
152
|
-
return;
|
153
|
-
}
|
154
|
-
setSigned(true);
|
155
|
-
|
156
|
-
let height = canvas.clientHeight;
|
157
|
-
let width = canvas.clientWidth;
|
158
|
-
|
159
|
-
let ctx = canvas.getContext("2d");
|
160
|
-
ctx.fillStyle = getCSSVariableValue(LineColor, canvasArea.current);
|
161
|
-
|
162
|
-
let fontSize = 0.6 * height;
|
163
|
-
ctx.font = `${fontSize}px ${fontName}`;
|
164
|
-
let textMeasurement = ctx.measureText(name);
|
165
|
-
let textWidth = textMeasurement.width;
|
166
|
-
let maxWidth = 0.9 * width;
|
167
|
-
|
168
|
-
while (textWidth > maxWidth && fontSize > 1) {
|
169
|
-
fontSize = fontSize - 1;
|
170
|
-
ctx.font = `${fontSize}px ${fontName}`;
|
171
|
-
textMeasurement = ctx.measureText(name);
|
172
|
-
textWidth = textMeasurement.width;
|
173
|
-
}
|
174
|
-
|
175
|
-
let x = (width - textWidth) / 2;
|
176
|
-
let y = 0.6 * height; //baseline not starting point
|
177
|
-
ctx.fillText(name, x, y, width);
|
178
|
-
let url = canvas.toDataURL("image/png");
|
179
|
-
onChangeRef.current?.(url);
|
180
|
-
}, [canvasArea, LineColor]);
|
181
|
-
|
182
|
-
const onSignAs = React.useCallback(async () => {
|
183
|
-
if (isNullOrUndefined(DrawPadUserName.get())) {
|
184
|
-
//prompt user to type his name - then continue
|
185
|
-
alerts.promptEX({
|
186
|
-
//mountNode: canvasContainerDiv.current, this lets other content on the form cover the dialog
|
187
|
-
title: "Sign as name",
|
188
|
-
children: <Field label="Signing as" hint="Please type in your name" required>
|
189
|
-
<InputEx onChange={(e, data) => DrawPadUserName.set(data.value)} />
|
190
|
-
</Field>,
|
191
|
-
onCancel: () => {
|
192
|
-
DrawPadUserName.set(null);//get rid of anything they typed while dialog was open
|
193
|
-
},
|
194
|
-
onOK: () => {
|
195
|
-
if (!isNullOrEmptyString(DrawPadUserName.get()))//need to test current since this won't be updated when state changes
|
196
|
-
{
|
197
|
-
sign(DrawPadUserName.get());
|
198
|
-
}
|
199
|
-
},
|
200
|
-
});
|
201
|
-
}
|
202
|
-
else sign(DrawPadUserName.get());
|
203
|
-
}, [canvasArea, LineColor]);
|
204
|
-
|
205
|
-
const HideButtons = props.HideClear && props.HideColorPicker && props.HideUpload;
|
206
|
-
|
207
|
-
const sizer = useElementSize(canvasContainerDiv.current);
|
208
|
-
const [size, setSize] = useStateEX<{ width?: number; height?: number }>({});
|
209
|
-
//handle canvas resizing
|
210
|
-
React.useEffect(() => {
|
211
|
-
if (canvasContainerDiv.current) {
|
212
|
-
setSize({
|
213
|
-
width: canvasContainerDiv.current.clientWidth,
|
214
|
-
height: canvasContainerDiv.current.clientHeight,
|
215
|
-
});
|
216
|
-
if (manager) manager.resizeCanvas();
|
217
|
-
}
|
218
|
-
}, [canvasContainerDiv, sizer, manager]);
|
219
|
-
|
220
|
-
return <Horizontal nogap fullscreen={fullscreen}>
|
221
|
-
{alerts.alertPrompt}
|
222
|
-
<div ref={canvasContainerDiv}
|
223
|
-
style={{
|
224
|
-
flexGrow: 1,
|
225
|
-
position: "relative",
|
226
|
-
minWidth: props.minWidth,
|
227
|
-
minHeight: props.minHeight,
|
228
|
-
backgroundColor: props.BackgroundColor,
|
229
|
-
border: `1px solid ${props.BorderColor || tokens.colorNeutralStroke1}`
|
230
|
-
}}>
|
231
|
-
{props.ReadOnly
|
232
|
-
? <img src={isNotEmptyString(props.Value) ? props.Value : props.DefaultBackdrop} style={{ position: "absolute", left: 0, top: 0, width: size.width, height: size.height }} />
|
233
|
-
:
|
234
|
-
<div style={{ position: "absolute", left: 0, top: 0, width: size.width, height: size.height }}>
|
235
|
-
<canvas
|
236
|
-
ref={canvasArea}
|
237
|
-
style={{
|
238
|
-
touchAction: "none",
|
239
|
-
userSelect: "none",
|
240
|
-
position: "absolute",
|
241
|
-
left: 0,
|
242
|
-
top: 0,
|
243
|
-
width: size.width,
|
244
|
-
height: size.height,
|
245
|
-
border: tokens.colorBrandStroke1
|
246
|
-
}} />
|
247
|
-
{!signed
|
248
|
-
&& !isNullOrEmptyString(props.allowSigning)
|
249
|
-
&& !isNullOrEmptyArray(fontReady)
|
250
|
-
&& <ButtonEX
|
251
|
-
style={{
|
252
|
-
position: "absolute",
|
253
|
-
bottom: 0,
|
254
|
-
border: 0,
|
255
|
-
margin: 0,
|
256
|
-
right: 0,
|
257
|
-
height: 16
|
258
|
-
}}
|
259
|
-
disabled={props.disabled}
|
260
|
-
icon={<CalligraphyPenRegular />}
|
261
|
-
title={`Sign as ${props.allowSigning === true ? "..." : props.allowSigning}`}
|
262
|
-
onClick={() => {
|
263
|
-
onSignAs();
|
264
|
-
}}
|
265
|
-
/>}
|
266
|
-
</div>
|
267
|
-
}
|
268
|
-
</div>
|
269
|
-
{!props.ReadOnly && !HideButtons && <Vertical nogap>
|
270
|
-
{props.HideColorPicker || <ColorPickerEx //mountNode={canvasContainerDiv.current} this lets other content on the form cover the dialog
|
271
|
-
disabled={props.disabled} buttonOnly value={props.LineColor} onChange={newColor => {
|
272
|
-
setLineColor(newColor);
|
273
|
-
}} />}
|
274
|
-
{/* todo: undo isn't working properly
|
275
|
-
{props.HideUndo || <ButtonEX disabled={!canUndo} title="Undo" icon={<ArrowUndoRegular />} onClick={() => {
|
276
|
-
manager.undoLast();
|
277
|
-
setcanUndo(manager.canUndo());
|
278
|
-
}} />} */}
|
279
|
-
{props.HideClear || <ButtonEX disabled={props.disabled || isNullOrEmptyString(props.Value)} title="Clear" icon={<DismissRegular />} onClick={() => {
|
280
|
-
//can call clear on the canvas, or can call the onchange which will cause a re-draw
|
281
|
-
setSigned(false);
|
282
|
-
onChangeRef.current?.("");
|
283
|
-
}} />}
|
284
|
-
{props.HideUpload || <FileUpload disabled={props.disabled} title="Load background image" icon={<ArrowUploadRegular />} limitFileTypes={ImageFileTypes} asBase64={base64 => {
|
285
|
-
if (onChangeRef.current)
|
286
|
-
onChangeRef.current?.(base64[0].base64);//this will trigger a change and state update
|
287
|
-
else
|
288
|
-
manager?.fromDataURL(base64[0].base64);//this will just set the image to the canvas but won't trigger a change event for the caller
|
289
|
-
}} />}
|
290
|
-
{props.allowFullscreen && <ButtonEX title="Full screen" disabled={props.disabled} icon={fullscreen ? <ArrowMinimizeRegular /> : <ArrowMaximizeRegular />} onClick={async () => {
|
291
|
-
//can call clear on the canvas, or can call the onchange which will cause a re-draw
|
292
|
-
await setFullscreen(!fullscreen);
|
293
|
-
if (manager) manager.resizeCanvas();
|
294
|
-
}} />}
|
295
|
-
</Vertical>}
|
296
|
-
</Horizontal>;
|
1
|
+
import { Field, tokens } from "@fluentui/react-components";
|
2
|
+
import { ArrowMaximizeRegular, ArrowMinimizeRegular, ArrowUploadRegular, CalligraphyPenRegular, DismissRegular } from "@fluentui/react-icons";
|
3
|
+
import { debounce, getCSSVariableValue, ImageFileTypes, isElement, isNotEmptyString, isNullOrEmptyArray, isNullOrEmptyString, isNullOrUndefined } from "@kwiz/common";
|
4
|
+
import * as React from "react";
|
5
|
+
import { useAlerts, useElementSize, useStateEX } from "../../helpers";
|
6
|
+
import { ButtonEX } from "../button";
|
7
|
+
import { ColorPickerEx } from "../ColorPickerDialog";
|
8
|
+
import { FileUpload } from "../file-upload";
|
9
|
+
import { Horizontal } from "../horizontal";
|
10
|
+
import { InputEx } from "../input";
|
11
|
+
import { Vertical } from "../vertical";
|
12
|
+
import DrawPadManager from "./DrawPadManager";
|
13
|
+
|
14
|
+
interface iProps {
|
15
|
+
BackgroundColor?: string;
|
16
|
+
BorderColor?: string;
|
17
|
+
LineColor?: string;
|
18
|
+
minWidth?: number;
|
19
|
+
minHeight?: number;
|
20
|
+
/** url or base64 image data:image/png;base64,.... */
|
21
|
+
Value?: string;
|
22
|
+
/** when user hits clear, it will reset to this value
|
23
|
+
* url or base64 image data:image/png;base64,....
|
24
|
+
*/
|
25
|
+
DefaultBackdrop?: string;
|
26
|
+
OnChange?: (newValue: string) => void;
|
27
|
+
ReadOnly?: boolean;
|
28
|
+
HideUpload?: boolean;
|
29
|
+
HideClear?: boolean;
|
30
|
+
//HideUndo?: boolean;
|
31
|
+
HideColorPicker?: boolean;
|
32
|
+
disabled?: boolean;
|
33
|
+
/** true - will prompt user for his name, string will just sign as that string */
|
34
|
+
allowSigning?: boolean | string;
|
35
|
+
allowFullscreen?: boolean;
|
36
|
+
}
|
37
|
+
var _userName: string = null;
|
38
|
+
export const DrawPadUserName = {
|
39
|
+
get: () => { return _userName },
|
40
|
+
set: (userName: string) => { _userName = userName }
|
41
|
+
};
|
42
|
+
|
43
|
+
const fontName = "Dancing Script";
|
44
|
+
let fontLoading: Promise<boolean> = null;
|
45
|
+
let fontReady = false;
|
46
|
+
export const DrawPad: React.FunctionComponent<iProps> = (props) => {
|
47
|
+
const [LineColor, setLineColor] = useStateEX<string>(props.LineColor || tokens.colorBrandForeground1);
|
48
|
+
const [manager, setmanager] = useStateEX<DrawPadManager>(null);
|
49
|
+
const [canUndo, setcanUndo] = useStateEX<boolean>(false, { skipUpdateIfSame: true });
|
50
|
+
const [signed, setSigned] = useStateEX<boolean>(false);
|
51
|
+
const [fullscreen, setFullscreen] = useStateEX<boolean>(false);
|
52
|
+
const onChangeRef = React.useRef(props.OnChange);
|
53
|
+
const alerts = useAlerts();
|
54
|
+
const canvasArea: React.RefObject<HTMLCanvasElement> = React.useRef();
|
55
|
+
const canvasContainerDiv = React.useRef<HTMLDivElement>();
|
56
|
+
|
57
|
+
//keep onChange up to date
|
58
|
+
React.useEffect(() => {
|
59
|
+
onChangeRef.current = props.OnChange;
|
60
|
+
}, [props.OnChange]);
|
61
|
+
//if user name provided - keep it
|
62
|
+
React.useEffect(() => {
|
63
|
+
if (isNotEmptyString(props.allowSigning)) {
|
64
|
+
DrawPadUserName.set(props.allowSigning);
|
65
|
+
}
|
66
|
+
}, [props.allowSigning]);
|
67
|
+
|
68
|
+
//load font for sign as text, if needed
|
69
|
+
React.useEffect(() => {
|
70
|
+
if (props.allowSigning && !fontLoading) {
|
71
|
+
let DancingScriptFont = new FontFace(
|
72
|
+
fontName,
|
73
|
+
"url(https://fonts.gstatic.com/s/dancingscript/v25/If2RXTr6YS-zF4S-kcSWSVi_szLgiuE.woff2) format('woff2')",
|
74
|
+
{
|
75
|
+
style: "normal",
|
76
|
+
weight: "400 700",
|
77
|
+
display: "swap",
|
78
|
+
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
79
|
+
}
|
80
|
+
);
|
81
|
+
|
82
|
+
fontLoading = DancingScriptFont.load().then(async loadedFont => {
|
83
|
+
document.fonts.add(loadedFont);
|
84
|
+
await document.fonts.ready;
|
85
|
+
fontReady = true;
|
86
|
+
return true;
|
87
|
+
});
|
88
|
+
}
|
89
|
+
}, [props.allowSigning]);
|
90
|
+
|
91
|
+
//setup manager
|
92
|
+
React.useEffect(() => {
|
93
|
+
if (!props.ReadOnly && canvasArea.current && !canvasArea.current["canvas_initialized"]) {
|
94
|
+
canvasArea.current["canvas_initialized"] = true;//for some reason this still fires twice on the same div
|
95
|
+
//this gets called after each render...
|
96
|
+
let manager = new DrawPadManager(canvasArea.current);
|
97
|
+
setmanager(manager);
|
98
|
+
manager.addEventListener("endStroke", () => {
|
99
|
+
//because this is used in an addEventListener - need to have a ref to get the most up to date onChange
|
100
|
+
if (onChangeRef.current) {
|
101
|
+
let canvasValue = manager.toPng();
|
102
|
+
setcanUndo(true);
|
103
|
+
onChangeRef.current(canvasValue);
|
104
|
+
}
|
105
|
+
});
|
106
|
+
}
|
107
|
+
}, [canvasArea]);
|
108
|
+
//update line color selection
|
109
|
+
React.useEffect(() => {
|
110
|
+
if (manager) {
|
111
|
+
manager.penColor.value = getCSSVariableValue(LineColor, canvasArea.current);
|
112
|
+
}
|
113
|
+
}, [manager, LineColor]);
|
114
|
+
|
115
|
+
//Set props value to canvas on initial load or when owner element changes the prop
|
116
|
+
React.useEffect(() => {
|
117
|
+
if (manager) {
|
118
|
+
let canvasValue = manager.toPng();
|
119
|
+
let neededValue = isNotEmptyString(props.Value)
|
120
|
+
? props.Value
|
121
|
+
: isNotEmptyString(props.DefaultBackdrop)
|
122
|
+
? props.DefaultBackdrop
|
123
|
+
: "";
|
124
|
+
if (canvasValue !== neededValue ||
|
125
|
+
(
|
126
|
+
isNullOrEmptyString(canvasValue) &&
|
127
|
+
isNullOrEmptyString(neededValue)
|
128
|
+
)
|
129
|
+
) {
|
130
|
+
UpdateCanvas(neededValue);//if called repeatedly or too fast - may not load correctly
|
131
|
+
}
|
132
|
+
}
|
133
|
+
});//cant use those - canvas loses value on drag so must run this every time: [props.Value, props.DefaultBackdrop, manager]);
|
134
|
+
|
135
|
+
//set value to canvas
|
136
|
+
const UpdateCanvas = React.useCallback(debounce((valueToSet: string) => {
|
137
|
+
if (valueToSet === "") manager.clear();
|
138
|
+
else manager.fromDataURL(valueToSet);
|
139
|
+
}, 200, this), [manager]);
|
140
|
+
|
141
|
+
//enable/disable canvas manager
|
142
|
+
React.useEffect(() => {
|
143
|
+
if (manager) {
|
144
|
+
if (props.disabled) manager.off();//stop accepting strokes, but still allow to set a default value
|
145
|
+
else manager.on();
|
146
|
+
}
|
147
|
+
}, [manager, props.disabled]);
|
148
|
+
|
149
|
+
const sign = React.useCallback((name: string) => {
|
150
|
+
let canvas = canvasArea.current;
|
151
|
+
if (!isElement(canvas)) {
|
152
|
+
return;
|
153
|
+
}
|
154
|
+
setSigned(true);
|
155
|
+
|
156
|
+
let height = canvas.clientHeight;
|
157
|
+
let width = canvas.clientWidth;
|
158
|
+
|
159
|
+
let ctx = canvas.getContext("2d");
|
160
|
+
ctx.fillStyle = getCSSVariableValue(LineColor, canvasArea.current);
|
161
|
+
|
162
|
+
let fontSize = 0.6 * height;
|
163
|
+
ctx.font = `${fontSize}px ${fontName}`;
|
164
|
+
let textMeasurement = ctx.measureText(name);
|
165
|
+
let textWidth = textMeasurement.width;
|
166
|
+
let maxWidth = 0.9 * width;
|
167
|
+
|
168
|
+
while (textWidth > maxWidth && fontSize > 1) {
|
169
|
+
fontSize = fontSize - 1;
|
170
|
+
ctx.font = `${fontSize}px ${fontName}`;
|
171
|
+
textMeasurement = ctx.measureText(name);
|
172
|
+
textWidth = textMeasurement.width;
|
173
|
+
}
|
174
|
+
|
175
|
+
let x = (width - textWidth) / 2;
|
176
|
+
let y = 0.6 * height; //baseline not starting point
|
177
|
+
ctx.fillText(name, x, y, width);
|
178
|
+
let url = canvas.toDataURL("image/png");
|
179
|
+
onChangeRef.current?.(url);
|
180
|
+
}, [canvasArea, LineColor]);
|
181
|
+
|
182
|
+
const onSignAs = React.useCallback(async () => {
|
183
|
+
if (isNullOrUndefined(DrawPadUserName.get())) {
|
184
|
+
//prompt user to type his name - then continue
|
185
|
+
alerts.promptEX({
|
186
|
+
//mountNode: canvasContainerDiv.current, this lets other content on the form cover the dialog
|
187
|
+
title: "Sign as name",
|
188
|
+
children: <Field label="Signing as" hint="Please type in your name" required>
|
189
|
+
<InputEx onChange={(e, data) => DrawPadUserName.set(data.value)} />
|
190
|
+
</Field>,
|
191
|
+
onCancel: () => {
|
192
|
+
DrawPadUserName.set(null);//get rid of anything they typed while dialog was open
|
193
|
+
},
|
194
|
+
onOK: () => {
|
195
|
+
if (!isNullOrEmptyString(DrawPadUserName.get()))//need to test current since this won't be updated when state changes
|
196
|
+
{
|
197
|
+
sign(DrawPadUserName.get());
|
198
|
+
}
|
199
|
+
},
|
200
|
+
});
|
201
|
+
}
|
202
|
+
else sign(DrawPadUserName.get());
|
203
|
+
}, [canvasArea, LineColor]);
|
204
|
+
|
205
|
+
const HideButtons = props.HideClear && props.HideColorPicker && props.HideUpload;
|
206
|
+
|
207
|
+
const sizer = useElementSize(canvasContainerDiv.current);
|
208
|
+
const [size, setSize] = useStateEX<{ width?: number; height?: number }>({});
|
209
|
+
//handle canvas resizing
|
210
|
+
React.useEffect(() => {
|
211
|
+
if (canvasContainerDiv.current) {
|
212
|
+
setSize({
|
213
|
+
width: canvasContainerDiv.current.clientWidth,
|
214
|
+
height: canvasContainerDiv.current.clientHeight,
|
215
|
+
});
|
216
|
+
if (manager) manager.resizeCanvas();
|
217
|
+
}
|
218
|
+
}, [canvasContainerDiv, sizer, manager]);
|
219
|
+
|
220
|
+
return <Horizontal nogap fullscreen={fullscreen}>
|
221
|
+
{alerts.alertPrompt}
|
222
|
+
<div ref={canvasContainerDiv}
|
223
|
+
style={{
|
224
|
+
flexGrow: 1,
|
225
|
+
position: "relative",
|
226
|
+
minWidth: props.minWidth,
|
227
|
+
minHeight: props.minHeight,
|
228
|
+
backgroundColor: props.BackgroundColor,
|
229
|
+
border: `1px solid ${props.BorderColor || tokens.colorNeutralStroke1}`
|
230
|
+
}}>
|
231
|
+
{props.ReadOnly
|
232
|
+
? <img src={isNotEmptyString(props.Value) ? props.Value : props.DefaultBackdrop} style={{ position: "absolute", left: 0, top: 0, width: size.width, height: size.height }} />
|
233
|
+
:
|
234
|
+
<div style={{ position: "absolute", left: 0, top: 0, width: size.width, height: size.height }}>
|
235
|
+
<canvas
|
236
|
+
ref={canvasArea}
|
237
|
+
style={{
|
238
|
+
touchAction: "none",
|
239
|
+
userSelect: "none",
|
240
|
+
position: "absolute",
|
241
|
+
left: 0,
|
242
|
+
top: 0,
|
243
|
+
width: size.width,
|
244
|
+
height: size.height,
|
245
|
+
border: tokens.colorBrandStroke1
|
246
|
+
}} />
|
247
|
+
{!signed
|
248
|
+
&& !isNullOrEmptyString(props.allowSigning)
|
249
|
+
&& !isNullOrEmptyArray(fontReady)
|
250
|
+
&& <ButtonEX
|
251
|
+
style={{
|
252
|
+
position: "absolute",
|
253
|
+
bottom: 0,
|
254
|
+
border: 0,
|
255
|
+
margin: 0,
|
256
|
+
right: 0,
|
257
|
+
height: 16
|
258
|
+
}}
|
259
|
+
disabled={props.disabled}
|
260
|
+
icon={<CalligraphyPenRegular />}
|
261
|
+
title={`Sign as ${props.allowSigning === true ? "..." : props.allowSigning}`}
|
262
|
+
onClick={() => {
|
263
|
+
onSignAs();
|
264
|
+
}}
|
265
|
+
/>}
|
266
|
+
</div>
|
267
|
+
}
|
268
|
+
</div>
|
269
|
+
{!props.ReadOnly && !HideButtons && <Vertical nogap>
|
270
|
+
{props.HideColorPicker || <ColorPickerEx //mountNode={canvasContainerDiv.current} this lets other content on the form cover the dialog
|
271
|
+
disabled={props.disabled} buttonOnly value={props.LineColor} onChange={newColor => {
|
272
|
+
setLineColor(newColor);
|
273
|
+
}} />}
|
274
|
+
{/* todo: undo isn't working properly
|
275
|
+
{props.HideUndo || <ButtonEX disabled={!canUndo} title="Undo" icon={<ArrowUndoRegular />} onClick={() => {
|
276
|
+
manager.undoLast();
|
277
|
+
setcanUndo(manager.canUndo());
|
278
|
+
}} />} */}
|
279
|
+
{props.HideClear || <ButtonEX disabled={props.disabled || isNullOrEmptyString(props.Value)} title="Clear" icon={<DismissRegular />} onClick={() => {
|
280
|
+
//can call clear on the canvas, or can call the onchange which will cause a re-draw
|
281
|
+
setSigned(false);
|
282
|
+
onChangeRef.current?.("");
|
283
|
+
}} />}
|
284
|
+
{props.HideUpload || <FileUpload disabled={props.disabled} title="Load background image" icon={<ArrowUploadRegular />} limitFileTypes={ImageFileTypes} asBase64={base64 => {
|
285
|
+
if (onChangeRef.current)
|
286
|
+
onChangeRef.current?.(base64[0].base64);//this will trigger a change and state update
|
287
|
+
else
|
288
|
+
manager?.fromDataURL(base64[0].base64);//this will just set the image to the canvas but won't trigger a change event for the caller
|
289
|
+
}} />}
|
290
|
+
{props.allowFullscreen && <ButtonEX title="Full screen" disabled={props.disabled} icon={fullscreen ? <ArrowMinimizeRegular /> : <ArrowMaximizeRegular />} onClick={async () => {
|
291
|
+
//can call clear on the canvas, or can call the onchange which will cause a re-draw
|
292
|
+
await setFullscreen(!fullscreen);
|
293
|
+
if (manager) manager.resizeCanvas();
|
294
|
+
}} />}
|
295
|
+
</Vertical>}
|
296
|
+
</Horizontal>;
|
297
297
|
}
|