From c76b03a86dd16e5ea397c04c034eddfe301654a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=B3=E4=B9=99=E5=B1=B1?= Date: Wed, 21 Dec 2022 15:21:43 +0800 Subject: [PATCH] feat: export getScaleFromCropRect --- packages/imageeditor/src/ImageEditor.js | 265 ++++++++++++++++++ .../{index.test.js => ImageEditor.test.js} | 6 +- packages/imageeditor/src/index.js | 265 +----------------- ...e.test.js => getScaleFromCropRect.test.js} | 8 +- ...etInitScale.js => getScaleFromCropRect.js} | 4 +- 5 files changed, 277 insertions(+), 271 deletions(-) create mode 100644 packages/imageeditor/src/ImageEditor.js rename packages/imageeditor/src/__tests__/{index.test.js => ImageEditor.test.js} (98%) rename packages/imageeditor/src/utils/__tests__/{getInitScale.test.js => getScaleFromCropRect.test.js} (68%) rename packages/imageeditor/src/utils/{getInitScale.js => getScaleFromCropRect.js} (91%) diff --git a/packages/imageeditor/src/ImageEditor.js b/packages/imageeditor/src/ImageEditor.js new file mode 100644 index 00000000..a8e12d6f --- /dev/null +++ b/packages/imageeditor/src/ImageEditor.js @@ -0,0 +1,265 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import icBEM from '@ichef/gypcrete/lib/utils/icBEM'; +import prefixClass from '@ichef/gypcrete/lib/utils/prefixClass'; + +import AvatarEditor from 'react-avatar-editor'; +import EditorPlaceholder from './EditorPlaceholder'; + +import getScaleFromCropRect from './utils/getScaleFromCropRect'; +import getInitPosition from './utils/getInitPosition'; + +import './styles/index.scss'; + +export const DEFAULT_SCALE = 1; +export const DEFAULT_POSITION = { x: 0.5, y: 0.5 }; + +export const COMPONENT_NAME = prefixClass('image-editor'); +const ROOT_BEM = icBEM(COMPONENT_NAME); +export const BEM = { + root: ROOT_BEM, + canvas: ROOT_BEM.element('canvas'), + control: ROOT_BEM.element('control'), + slider: ROOT_BEM.element('slider'), + placeholder: ROOT_BEM.element('placeholder'), +}; + +export { AvatarEditor }; + +/** + * This component is built upon `mosch/react-avatar-editor`, offering pre-configured + * scale slider, no-image placeholder and a loading indicator mode. It also supports + * setting an initial cropping rectangle via `initCropRect` prop. + * + * This component passes unknown props to the inner ``. + * Please refer to: https://git.io/vxhT8, for a full list of supported props. + * + * ### Appearance configs + * - `control`: toggles the scale slider + * - `autoMargin`: adds `auto` to both left and right CSS margin. + * - `readOnly`: prevent the editor from modifing the crop. + * - `loading`: put the editor into a loading indicator mode. + * + * ### Event callbacks + * - `onCropChange`: called with cropping rect when not read-only and `onImageChange` fires. + * - `onLoadSuccess`: called with `imgInfo` and `cropRect` when image finishes loading + * + */ +class ImageEditor extends PureComponent { + static propTypes = { + scale: PropTypes.number, + minScale: PropTypes.number, + maxScale: PropTypes.number, + initCropRect: PropTypes.shape({ + x: PropTypes.number, + y: PropTypes.number, + width: PropTypes.number, + height: PropTypes.number, + }), + onScaleChange: PropTypes.func, + onCropChange: PropTypes.func, + onLoadSuccess: PropTypes.func, + // appearance configs + control: PropTypes.bool, + autoMargin: PropTypes.bool, + readOnly: PropTypes.bool, + loading: PropTypes.bool, + // props for + image: AvatarEditor.propTypes.image, + width: AvatarEditor.propTypes.width, + height: AvatarEditor.propTypes.height, + onImageChange: AvatarEditor.propTypes.onImageChange, + onPositionChange: AvatarEditor.propTypes.onPositionChange, + }; + + static defaultProps = { + scale: undefined, + minScale: 0.5, + maxScale: 5, + initCropRect: undefined, + onScaleChange: () => {}, + onCropChange: () => {}, + onLoadSuccess: () => {}, + // appearance configs + control: false, + autoMargin: false, + readOnly: false, + loading: false, + // props for + image: AvatarEditor.defaultProps.image, + width: AvatarEditor.defaultProps.width, + height: AvatarEditor.defaultProps.height, + onImageChange: AvatarEditor.defaultProps.onImageChange, + onPositionChange: AvatarEditor.defaultProps.onPositionChange, + }; + + state = { + scale: getScaleFromCropRect(this.props.initCropRect) || DEFAULT_SCALE, + position: getInitPosition(this.props.initCropRect) || DEFAULT_POSITION, + }; + + editorRef = React.createRef(); + + // eslint-disable-next-line react/no-deprecated + componentWillReceiveProps(nextProps) { + // Consider current `scale`, `position` and `initCropRect` outdated when image changes + if (nextProps.image !== this.props.image) { + this.setState({ + scale: DEFAULT_SCALE, + position: DEFAULT_POSITION, + }); + } + } + + getImageCanvas = ({ originalSize = false } = {}) => { + if (originalSize) { + return this.editorRef.current.getImage(); + } + return this.editorRef.current.getImageScaledToCanvas(); + } + + getCroppingRect = () => { + if (!this.editorRef.current) { + return null; + } + + return this.editorRef.current.getCroppingRect(); + } + + handleSliderChange = (event) => { + const newScale = Number(event.target.value); + + this.setState({ scale: newScale }); + this.props.onScaleChange(newScale); + } + + handleCanvasPosChange = (newPos) => { + if (!this.props.readOnly) { + this.setState({ position: newPos }); + } + + // Proxies original `onPositionChange()` prop for `` + this.props.onPositionChange(newPos); + } + + handleCanvasImageChange = () => { + if (!this.props.readOnly) { + const newCropRect = this.editorRef.current && this.editorRef.current.getCroppingRect(); + this.props.onCropChange(newCropRect); + } + + // Proxies original `onImageChange()` prop for `` + this.props.onImageChange(); + } + + handleCanvasLoadSuccess = (imgInfo) => { + const cropRect = this.editorRef.current && this.editorRef.current.getCroppingRect(); + + this.props.onLoadSuccess(imgInfo, cropRect); + } + + renderControl() { + if (!this.props.control) { + return null; + } + + const { + scale, + minScale, + maxScale, + image, + readOnly, + loading, + } = this.props; + const shouldDisable = readOnly || !image || loading; + + return ( +
+ +
+ ); + } + + render() { + const { + minScale, + maxScale, + initCropRect, + onCropChange, + onLoadSuccess, + // appearance configs + control, + autoMargin, + readOnly, + loading, + // props for + scale, + image, + width, + height, + onImageChange, + onPositionChange, + onScaleChange, + // react props + className, + style, + ...avatarEditorProps + } = this.props; + + const wraperBEM = BEM.root + .modifier('auto-margin', autoMargin) + .modifier('readonly', readOnly) + .toString(); + + const wrapperClass = classNames(className, wraperBEM); + const wrapperStyle = { ...style, width }; + + const shouldShowPlaceholder = (!image || loading); + + const placeholder = ( + + ); + const canvas = ( + + ); + + return ( +
+
+ {shouldShowPlaceholder ? placeholder : canvas} +
+ + {this.renderControl()} +
+ ); + } +} + +export default ImageEditor; diff --git a/packages/imageeditor/src/__tests__/index.test.js b/packages/imageeditor/src/__tests__/ImageEditor.test.js similarity index 98% rename from packages/imageeditor/src/__tests__/index.test.js rename to packages/imageeditor/src/__tests__/ImageEditor.test.js index 0d2fac6d..4258f3db 100644 --- a/packages/imageeditor/src/__tests__/index.test.js +++ b/packages/imageeditor/src/__tests__/ImageEditor.test.js @@ -4,10 +4,10 @@ import { shallow } from 'enzyme'; import AvatarEditor from 'react-avatar-editor'; -import ImageEditor, { DEFAULT_SCALE, DEFAULT_POSITION } from '../index'; +import ImageEditor, { DEFAULT_SCALE, DEFAULT_POSITION } from '../ImageEditor'; import EditorPlaceholder from '../EditorPlaceholder'; -import getInitScale from '../utils/getInitScale'; +import getScaleFromCropRect from '../utils/getScaleFromCropRect'; import getInitPosition from '../utils/getInitPosition'; // from: https://css-tricks.com/snippets/html/base64-encode-of-1x1px-transparent-gif/ @@ -179,7 +179,7 @@ it('takes an initial cropping rect to set scale and position', () => { ); expect(wrapper.find(AvatarEditor).prop('scale')) - .toEqual(getInitScale(cropRect)); + .toEqual(getScaleFromCropRect(cropRect)); expect(wrapper.find(AvatarEditor).prop('position')) .toEqual(getInitPosition(cropRect)); }); diff --git a/packages/imageeditor/src/index.js b/packages/imageeditor/src/index.js index 2e19209f..4d78478b 100644 --- a/packages/imageeditor/src/index.js +++ b/packages/imageeditor/src/index.js @@ -1,265 +1,6 @@ -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; +// eslint-disable-next-line filenames/match-exported +import ImageEditor from './ImageEditor'; -import icBEM from '@ichef/gypcrete/lib/utils/icBEM'; -import prefixClass from '@ichef/gypcrete/lib/utils/prefixClass'; - -import AvatarEditor from 'react-avatar-editor'; -import EditorPlaceholder from './EditorPlaceholder'; - -import getInitScale from './utils/getInitScale'; -import getInitPosition from './utils/getInitPosition'; - -import './styles/index.scss'; - -export const DEFAULT_SCALE = 1; -export const DEFAULT_POSITION = { x: 0.5, y: 0.5 }; - -export const COMPONENT_NAME = prefixClass('image-editor'); -const ROOT_BEM = icBEM(COMPONENT_NAME); -export const BEM = { - root: ROOT_BEM, - canvas: ROOT_BEM.element('canvas'), - control: ROOT_BEM.element('control'), - slider: ROOT_BEM.element('slider'), - placeholder: ROOT_BEM.element('placeholder'), -}; - -export { AvatarEditor }; - -/** - * This component is built upon `mosch/react-avatar-editor`, offering pre-configured - * scale slider, no-image placeholder and a loading indicator mode. It also supports - * setting an initial cropping rectangle via `initCropRect` prop. - * - * This component passes unknown props to the inner ``. - * Please refer to: https://git.io/vxhT8, for a full list of supported props. - * - * ### Appearance configs - * - `control`: toggles the scale slider - * - `autoMargin`: adds `auto` to both left and right CSS margin. - * - `readOnly`: prevent the editor from modifing the crop. - * - `loading`: put the editor into a loading indicator mode. - * - * ### Event callbacks - * - `onCropChange`: called with cropping rect when not read-only and `onImageChange` fires. - * - `onLoadSuccess`: called with `imgInfo` and `cropRect` when image finishes loading - * - */ -class ImageEditor extends PureComponent { - static propTypes = { - scale: PropTypes.number, - minScale: PropTypes.number, - maxScale: PropTypes.number, - initCropRect: PropTypes.shape({ - x: PropTypes.number, - y: PropTypes.number, - width: PropTypes.number, - height: PropTypes.number, - }), - onScaleChange: PropTypes.func, - onCropChange: PropTypes.func, - onLoadSuccess: PropTypes.func, - // appearance configs - control: PropTypes.bool, - autoMargin: PropTypes.bool, - readOnly: PropTypes.bool, - loading: PropTypes.bool, - // props for - image: AvatarEditor.propTypes.image, - width: AvatarEditor.propTypes.width, - height: AvatarEditor.propTypes.height, - onImageChange: AvatarEditor.propTypes.onImageChange, - onPositionChange: AvatarEditor.propTypes.onPositionChange, - }; - - static defaultProps = { - scale: undefined, - minScale: 0.5, - maxScale: 5, - initCropRect: undefined, - onScaleChange: () => {}, - onCropChange: () => {}, - onLoadSuccess: () => {}, - // appearance configs - control: false, - autoMargin: false, - readOnly: false, - loading: false, - // props for - image: AvatarEditor.defaultProps.image, - width: AvatarEditor.defaultProps.width, - height: AvatarEditor.defaultProps.height, - onImageChange: AvatarEditor.defaultProps.onImageChange, - onPositionChange: AvatarEditor.defaultProps.onPositionChange, - }; - - state = { - scale: getInitScale(this.props.initCropRect) || DEFAULT_SCALE, - position: getInitPosition(this.props.initCropRect) || DEFAULT_POSITION, - }; - - editorRef = React.createRef(); - - // eslint-disable-next-line react/no-deprecated - componentWillReceiveProps(nextProps) { - // Consider current `scale`, `position` and `initCropRect` outdated when image changes - if (nextProps.image !== this.props.image) { - this.setState({ - scale: DEFAULT_SCALE, - position: DEFAULT_POSITION, - }); - } - } - - getImageCanvas = ({ originalSize = false } = {}) => { - if (originalSize) { - return this.editorRef.current.getImage(); - } - return this.editorRef.current.getImageScaledToCanvas(); - } - - getCroppingRect = () => { - if (!this.editorRef.current) { - return null; - } - - return this.editorRef.current.getCroppingRect(); - } - - handleSliderChange = (event) => { - const newScale = Number(event.target.value); - - this.setState({ scale: newScale }); - this.props.onScaleChange(newScale); - } - - handleCanvasPosChange = (newPos) => { - if (!this.props.readOnly) { - this.setState({ position: newPos }); - } - - // Proxies original `onPositionChange()` prop for `` - this.props.onPositionChange(newPos); - } - - handleCanvasImageChange = () => { - if (!this.props.readOnly) { - const newCropRect = this.editorRef.current && this.editorRef.current.getCroppingRect(); - this.props.onCropChange(newCropRect); - } - - // Proxies original `onImageChange()` prop for `` - this.props.onImageChange(); - } - - handleCanvasLoadSuccess = (imgInfo) => { - const cropRect = this.editorRef.current && this.editorRef.current.getCroppingRect(); - - this.props.onLoadSuccess(imgInfo, cropRect); - } - - renderControl() { - if (!this.props.control) { - return null; - } - - const { - scale, - minScale, - maxScale, - image, - readOnly, - loading, - } = this.props; - const shouldDisable = readOnly || !image || loading; - - return ( -
- -
- ); - } - - render() { - const { - minScale, - maxScale, - initCropRect, - onCropChange, - onLoadSuccess, - // appearance configs - control, - autoMargin, - readOnly, - loading, - // props for - scale, - image, - width, - height, - onImageChange, - onPositionChange, - onScaleChange, - // react props - className, - style, - ...avatarEditorProps - } = this.props; - - const wraperBEM = BEM.root - .modifier('auto-margin', autoMargin) - .modifier('readonly', readOnly) - .toString(); - - const wrapperClass = classNames(className, wraperBEM); - const wrapperStyle = { ...style, width }; - - const shouldShowPlaceholder = (!image || loading); - - const placeholder = ( - - ); - const canvas = ( - - ); - - return ( -
-
- {shouldShowPlaceholder ? placeholder : canvas} -
- - {this.renderControl()} -
- ); - } -} +export { default as getScaleFromCropRect } from './utils/getScaleFromCropRect'; export default ImageEditor; diff --git a/packages/imageeditor/src/utils/__tests__/getInitScale.test.js b/packages/imageeditor/src/utils/__tests__/getScaleFromCropRect.test.js similarity index 68% rename from packages/imageeditor/src/utils/__tests__/getInitScale.test.js rename to packages/imageeditor/src/utils/__tests__/getScaleFromCropRect.test.js index 951b4d89..58d5d108 100644 --- a/packages/imageeditor/src/utils/__tests__/getInitScale.test.js +++ b/packages/imageeditor/src/utils/__tests__/getScaleFromCropRect.test.js @@ -1,7 +1,7 @@ -import getInitScale from '../getInitScale'; +import getScaleFromCropRect from '../getScaleFromCropRect'; it('infers scale from init cropping rect where width ratio is larger', () => { - expect(getInitScale({ + expect(getScaleFromCropRect({ x: 0, y: 0, width: 0.6, @@ -10,7 +10,7 @@ it('infers scale from init cropping rect where width ratio is larger', () => { }); it('infers scale from init cropping rect where height ratio is larger', () => { - expect(getInitScale({ + expect(getScaleFromCropRect({ x: 0, y: 0, width: 0.2, @@ -19,5 +19,5 @@ it('infers scale from init cropping rect where height ratio is larger', () => { }); it('returns null if cropping rect is broken', () => { - expect(getInitScale({})).toBe(null); + expect(getScaleFromCropRect({})).toBe(null); }); diff --git a/packages/imageeditor/src/utils/getInitScale.js b/packages/imageeditor/src/utils/getScaleFromCropRect.js similarity index 91% rename from packages/imageeditor/src/utils/getInitScale.js rename to packages/imageeditor/src/utils/getScaleFromCropRect.js index bd3cf86d..0862d816 100644 --- a/packages/imageeditor/src/utils/getInitScale.js +++ b/packages/imageeditor/src/utils/getScaleFromCropRect.js @@ -11,7 +11,7 @@ * @param {{ width: number, height: number }} initCropRect * @returns {number?} */ -function getInitScale(initCropRect) { +function getScaleFromCropRect(initCropRect) { if (!initCropRect) return null; const { width: widthRatio, height: heightRatio } = initCropRect; @@ -25,4 +25,4 @@ function getInitScale(initCropRect) { return Number.parseFloat(inferredScale.toFixed(1)); } -export default getInitScale; +export default getScaleFromCropRect;