-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
277 additions
and
271 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.