Skip to content

Commit

Permalink
feat: export getScaleFromCropRect
Browse files Browse the repository at this point in the history
  • Loading branch information
chenesan committed Dec 22, 2022
1 parent a33b299 commit c76b03a
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 271 deletions.
265 changes: 265 additions & 0 deletions packages/imageeditor/src/ImageEditor.js
Original file line number Diff line number Diff line change
@@ -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 `<AvatarEditor>`.
* 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 <AvatarEditor>
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 <AvatarEditor>
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 `<AvatarEditor>`
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 `<AvatarEditor>`
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 (
<div className={BEM.control.toString()}>
<input
type="range"
value={scale || this.state.scale}
className={BEM.slider.toString()}
disabled={shouldDisable}
step="0.1"
min={minScale}
max={maxScale}
onChange={this.handleSliderChange}
/>
</div>
);
}

render() {
const {
minScale,
maxScale,
initCropRect,
onCropChange,
onLoadSuccess,
// appearance configs
control,
autoMargin,
readOnly,
loading,
// props for <AvatarEditor>
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 = (
<EditorPlaceholder
loading={loading}
className={BEM.placeholder.toString()}
canvasHeight={height}
/>
);
const canvas = (
<AvatarEditor
ref={this.editorRef}
image={image}
width={width}
height={height}
scale={scale || this.state.scale}
position={this.state.position}
onImageChange={this.handleCanvasImageChange}
onPositionChange={this.handleCanvasPosChange}
onLoadSuccess={this.handleCanvasLoadSuccess}
border={0}
{...avatarEditorProps}
/>
);

return (
<div className={wrapperClass} style={wrapperStyle}>
<div className={BEM.canvas.toString()}>
{shouldShowPlaceholder ? placeholder : canvas}
</div>

{this.renderControl()}
</div>
);
}
}

export default ImageEditor;
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down Expand Up @@ -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));
});
Expand Down
Loading

0 comments on commit c76b03a

Please sign in to comment.