UNPKG

@kwiz/fluentui

Version:

KWIZ common controls for FluentUI

238 lines 13.5 kB
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