UNPKG

@etsoo/materialui

Version:

TypeScript Material-UI Implementation

175 lines (174 loc) 9.07 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UserAvatarEditor = UserAvatarEditor; const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = __importDefault(require("react")); const RotateLeft_1 = __importDefault(require("@mui/icons-material/RotateLeft")); const RotateRight_1 = __importDefault(require("@mui/icons-material/RotateRight")); const ClearAll_1 = __importDefault(require("@mui/icons-material/ClearAll")); const Image_1 = __importDefault(require("@mui/icons-material/Image")); const Done_1 = __importDefault(require("@mui/icons-material/Done")); const Remove_1 = __importDefault(require("@mui/icons-material/Remove")); const Add_1 = __importDefault(require("@mui/icons-material/Add")); const Labels_1 = require("./app/Labels"); const FileUploadButton_1 = require("./FileUploadButton"); const Stack_1 = __importDefault(require("@mui/material/Stack")); const Skeleton_1 = __importDefault(require("@mui/material/Skeleton")); const ButtonGroup_1 = __importDefault(require("@mui/material/ButtonGroup")); const Button_1 = __importDefault(require("@mui/material/Button")); const IconButton_1 = __importDefault(require("@mui/material/IconButton")); const Slider_1 = __importDefault(require("@mui/material/Slider")); const defaultSize = 300; const defaultState = { scale: 1, rotate: 0 }; /** * User avatar editor * https://github.com/mosch/react-avatar-editor * @param props Props * @returns Component */ function UserAvatarEditor(props) { // Labels const labels = Labels_1.Labels.UserAvatarEditor; // Destruct const { border = 30, image, maxWidth, onDone, scaledResult = false, width = defaultSize, height = defaultSize, range = [0.1, 2, 0.1], selectFileLabel = labels.selectFile + "..." } = props; // Container width const containerWidth = width + 2 * border + 44 + 4; // Calculated max width const maxWidthCalculated = maxWidth == null || maxWidth < defaultSize ? 2 * width : maxWidth; // Ref const ref = react_1.default.createRef(); // Image type const type = react_1.default.useRef("image/jpeg"); // Button ref const buttonRef = react_1.default.createRef(); // Preview image state const [previewImage, setPreviewImage] = react_1.default.useState(image); react_1.default.useEffect(() => setPreviewImage(image), [image]); // Is ready state const [ready, setReady] = react_1.default.useState(false); // Editor states const [editorState, setEditorState] = react_1.default.useState(defaultState); // Height // noHeight: height is not set and will be updated dynamically const noHeight = height <= 0; const [localHeight, setHeight] = react_1.default.useState(noHeight ? defaultSize : height); // Range const [min, max, step] = range; const marks = [ { value: min, label: min.toString() }, { value: max, label: max.toString() } ]; if (min < 1) { marks.splice(1, 0, { value: 1, label: "1" }); } // Handle zoom const handleZoom = (_event, value, _activeThumb) => { const scale = typeof value === "number" ? value : value[0]; setScale(scale); }; const setScale = (scale) => { const newState = { ...editorState, scale }; setEditorState(newState); }; const adjustScale = (isAdd) => { setScale(editorState.scale + (isAdd ? step : -step)); }; // Handle image load const handleLoad = (imageInfo) => { // Ignore too small images if (imageInfo.resource.width < 10 || imageInfo.resource.height < 10) return; if (noHeight) { setHeight((imageInfo.height * width) / imageInfo.width); } setReady(true); }; // Handle file upload const handleFileUpload = (files) => { // Reset all settings handleReset(); // Set new preview image const file = files[0]; type.current = file.type; setPreviewImage(file); // Set ready state setReady(false); // Make the submit button visible buttonRef.current?.scrollIntoView(false); }; // Handle reset const handleReset = () => { setEditorState({ ...defaultState }); }; const resetUI = () => { setReady(false); setPreviewImage(undefined); handleReset(); }; // Handle rotate const handleRotate = (r) => { let rotate = editorState.rotate + r; if (rotate >= 360 || rotate <= -360) rotate = 0; const newState = { ...editorState, rotate }; setEditorState(newState); }; // Handle done const handleDone = async () => { // Data var data = scaledResult ? ref.current?.getImageScaledToCanvas() : ref.current?.getImage(); if (data == null) return; // pica const pica = (await import("pica")).default; const picaInstance = pica(); // toBlob helper // Convenience method, similar to canvas.toBlob(), but with promise interface & polyfill for old browsers. const toBlob = (canvas, mimeType = type.current, quality = 1) => { return picaInstance.toBlob(canvas, mimeType, quality); }; if (data.width > maxWidthCalculated) { // Target height const heightCalculated = (localHeight * maxWidthCalculated) / width; // Target const to = document.createElement("canvas"); to.width = maxWidthCalculated; to.height = heightCalculated; // Large photo, resize it // https://github.com/nodeca/pica const canvas = await picaInstance.resize(data, to, { unsharpAmount: 160, unsharpRadius: 0.6, unsharpThreshold: 1 }); const result = await onDone(canvas, toBlob, type.current); if (result) { resetUI(); } } else { const result = await onDone(data, toBlob, type.current); if (result) { resetUI(); } } }; // Load the component const AE = react_1.default.lazy(() => import("react-avatar-editor")); return ((0, jsx_runtime_1.jsxs)(Stack_1.default, { direction: "column", spacing: 0.5, 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)(AE, { ref: ref, border: border, width: width, height: localHeight, onLoadSuccess: handleLoad, image: previewImage ?? "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: { paddingBottom: 2 }, alignItems: "center", 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 })] })); }