@etsoo/materialui
Version:
TypeScript Material-UI Implementation
175 lines (174 loc) • 9.07 kB
JavaScript
"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 })] }));
}