@kwiz/fluentui
Version:
KWIZ common controls for FluentUI
238 lines • 13.5 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Field, tokens } from "@fluentui/react-components";
import { ArrowMaximizeRegular, ArrowMinimizeRegular, ArrowUploadRegular, CalligraphyPenRegular, DismissRegular } from "@fluentui/react-icons";
import { debounce, getCSSVariableValue, ImageFileTypes, isElement, isNotEmptyString, isNullOrEmptyArray, isNullOrEmptyString, isNullOrUndefined } from "@kwiz/common";
import * as React from "react";
import { useAlerts, useElementSize, useStateEX } from "../../helpers";
import { ButtonEX } from "../button";
import { ColorPickerEx } from "../ColorPickerDialog";
import { FileUpload } from "../file-upload";
import { Horizontal } from "../horizontal";
import { InputEx } from "../input";
import { Vertical } from "../vertical";
import DrawPadManager from "./DrawPadManager";
var _userName = null;
export const DrawPadUserName = {
get: () => { return _userName; },
set: (userName) => { _userName = userName; }
};
const fontName = "Dancing Script";
let fontLoading = null;
let fontReady = false;
export const DrawPad = (props) => {
const [LineColor, setLineColor] = useStateEX(props.LineColor || tokens.colorBrandForeground1);
const [manager, setmanager] = useStateEX(null);
const [canUndo, setcanUndo] = useStateEX(false, { skipUpdateIfSame: true });
const [signed, setSigned] = useStateEX(false);
const [fullscreen, setFullscreen] = useStateEX(false);
const onChangeRef = React.useRef(props.OnChange);
const alerts = useAlerts();
const canvasArea = React.useRef();
const canvasContainerDiv = React.useRef();
//keep onChange up to date
React.useEffect(() => {
onChangeRef.current = props.OnChange;
}, [props.OnChange]);
//if user name provided - keep it
React.useEffect(() => {
if (isNotEmptyString(props.allowSigning)) {
DrawPadUserName.set(props.allowSigning);
}
}, [props.allowSigning]);
//load font for sign as text, if needed
React.useEffect(() => {
if (props.allowSigning && !fontLoading) {
let DancingScriptFont = new FontFace(fontName, "url(https://fonts.gstatic.com/s/dancingscript/v25/If2RXTr6YS-zF4S-kcSWSVi_szLgiuE.woff2) format('woff2')", {
style: "normal",
weight: "400 700",
display: "swap",
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"
});
fontLoading = DancingScriptFont.load().then((loadedFont) => __awaiter(void 0, void 0, void 0, function* () {
document.fonts.add(loadedFont);
yield document.fonts.ready;
fontReady = true;
return true;
}));
}
}, [props.allowSigning]);
//setup manager
React.useEffect(() => {
if (!props.ReadOnly && canvasArea.current && !canvasArea.current["canvas_initialized"]) {
canvasArea.current["canvas_initialized"] = true; //for some reason this still fires twice on the same div
//this gets called after each render...
let manager = new DrawPadManager(canvasArea.current);
setmanager(manager);
manager.addEventListener("endStroke", () => {
//because this is used in an addEventListener - need to have a ref to get the most up to date onChange
if (onChangeRef.current) {
let canvasValue = manager.toPng();
setcanUndo(true);
onChangeRef.current(canvasValue);
}
});
}
}, [canvasArea]);
//update line color selection
React.useEffect(() => {
if (manager) {
manager.penColor.value = getCSSVariableValue(LineColor, canvasArea.current);
}
}, [manager, LineColor]);
//Set props value to canvas on initial load or when owner element changes the prop
React.useEffect(() => {
if (manager) {
let canvasValue = manager.toPng();
let neededValue = isNotEmptyString(props.Value)
? props.Value
: isNotEmptyString(props.DefaultBackdrop)
? props.DefaultBackdrop
: "";
if (canvasValue !== neededValue ||
(isNullOrEmptyString(canvasValue) &&
isNullOrEmptyString(neededValue))) {
UpdateCanvas(neededValue); //if called repeatedly or too fast - may not load correctly
}
}
}); //cant use those - canvas loses value on drag so must run this every time: [props.Value, props.DefaultBackdrop, manager]);
//set value to canvas
const UpdateCanvas = React.useCallback(debounce((valueToSet) => {
if (valueToSet === "")
manager.clear();
else
manager.fromDataURL(valueToSet);
}, 200, this), [manager]);
//enable/disable canvas manager
React.useEffect(() => {
if (manager) {
if (props.disabled)
manager.off(); //stop accepting strokes, but still allow to set a default value
else
manager.on();
}
}, [manager, props.disabled]);
const sign = React.useCallback((name) => {
var _a;
let canvas = canvasArea.current;
if (!isElement(canvas)) {
return;
}
setSigned(true);
let height = canvas.clientHeight;
let width = canvas.clientWidth;
let ctx = canvas.getContext("2d");
ctx.fillStyle = getCSSVariableValue(LineColor, canvasArea.current);
let fontSize = 0.6 * height;
ctx.font = `${fontSize}px ${fontName}`;
let textMeasurement = ctx.measureText(name);
let textWidth = textMeasurement.width;
let maxWidth = 0.9 * width;
while (textWidth > maxWidth && fontSize > 1) {
fontSize = fontSize - 1;
ctx.font = `${fontSize}px ${fontName}`;
textMeasurement = ctx.measureText(name);
textWidth = textMeasurement.width;
}
let x = (width - textWidth) / 2;
let y = 0.6 * height; //baseline not starting point
ctx.fillText(name, x, y, width);
let url = canvas.toDataURL("image/png");
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, url);
}, [canvasArea, LineColor]);
const onSignAs = React.useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
if (isNullOrUndefined(DrawPadUserName.get())) {
//prompt user to type his name - then continue
alerts.promptEX({
//mountNode: canvasContainerDiv.current, this lets other content on the form cover the dialog
title: "Sign as name",
children: _jsx(Field, { label: "Signing as", hint: "Please type in your name", required: true, children: _jsx(InputEx, { onChange: (e, data) => DrawPadUserName.set(data.value) }) }),
onCancel: () => {
DrawPadUserName.set(null); //get rid of anything they typed while dialog was open
},
onOK: () => {
if (!isNullOrEmptyString(DrawPadUserName.get())) //need to test current since this won't be updated when state changes
{
sign(DrawPadUserName.get());
}
},
});
}
else
sign(DrawPadUserName.get());
}), [canvasArea, LineColor]);
const HideButtons = props.HideClear && props.HideColorPicker && props.HideUpload;
const sizer = useElementSize(canvasContainerDiv.current);
const [size, setSize] = useStateEX({});
//handle canvas resizing
React.useEffect(() => {
if (canvasContainerDiv.current) {
setSize({
width: canvasContainerDiv.current.clientWidth,
height: canvasContainerDiv.current.clientHeight,
});
if (manager)
manager.resizeCanvas();
}
}, [canvasContainerDiv, sizer, manager]);
return _jsxs(Horizontal, { nogap: true, fullscreen: fullscreen, children: [alerts.alertPrompt, _jsx("div", { ref: canvasContainerDiv, style: {
flexGrow: 1,
position: "relative",
minWidth: props.minWidth,
minHeight: props.minHeight,
backgroundColor: props.BackgroundColor,
border: `1px solid ${props.BorderColor || tokens.colorNeutralStroke1}`
}, children: props.ReadOnly
? _jsx("img", { src: isNotEmptyString(props.Value) ? props.Value : props.DefaultBackdrop, style: { position: "absolute", left: 0, top: 0, width: size.width, height: size.height } })
:
_jsxs("div", { style: { position: "absolute", left: 0, top: 0, width: size.width, height: size.height }, children: [_jsx("canvas", { ref: canvasArea, style: {
touchAction: "none",
userSelect: "none",
position: "absolute",
left: 0,
top: 0,
width: size.width,
height: size.height,
border: tokens.colorBrandStroke1
} }), !signed
&& !isNullOrEmptyString(props.allowSigning)
&& !isNullOrEmptyArray(fontReady)
&& _jsx(ButtonEX, { style: {
position: "absolute",
bottom: 0,
border: 0,
margin: 0,
right: 0,
height: 16
}, disabled: props.disabled, icon: _jsx(CalligraphyPenRegular, {}), title: `Sign as ${props.allowSigning === true ? "..." : props.allowSigning}`, onClick: () => {
onSignAs();
} })] }) }), !props.ReadOnly && !HideButtons && _jsxs(Vertical, { nogap: true, children: [props.HideColorPicker || _jsx(ColorPickerEx //mountNode={canvasContainerDiv.current} this lets other content on the form cover the dialog
, { disabled: props.disabled, buttonOnly: true, value: props.LineColor, onChange: newColor => {
setLineColor(newColor);
} }), props.HideClear || _jsx(ButtonEX, { disabled: props.disabled || isNullOrEmptyString(props.Value), title: "Clear", icon: _jsx(DismissRegular, {}), onClick: () => {
var _a;
//can call clear on the canvas, or can call the onchange which will cause a re-draw
setSigned(false);
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, "");
} }), props.HideUpload || _jsx(FileUpload, { disabled: props.disabled, title: "Load background image", icon: _jsx(ArrowUploadRegular, {}), limitFileTypes: ImageFileTypes, asBase64: base64 => {
var _a;
if (onChangeRef.current)
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, base64[0].base64); //this will trigger a change and state update
else
manager === null || manager === void 0 ? void 0 : manager.fromDataURL(base64[0].base64); //this will just set the image to the canvas but won't trigger a change event for the caller
} }), props.allowFullscreen && _jsx(ButtonEX, { title: "Full screen", disabled: props.disabled, icon: fullscreen ? _jsx(ArrowMinimizeRegular, {}) : _jsx(ArrowMaximizeRegular, {}), onClick: () => __awaiter(void 0, void 0, void 0, function* () {
//can call clear on the canvas, or can call the onchange which will cause a re-draw
yield setFullscreen(!fullscreen);
if (manager)
manager.resizeCanvas();
}) })] })] });
};
//# sourceMappingURL=DrawPad.js.map