UNPKG

@bigfishtv/cockpit

Version:

327 lines (289 loc) 9.53 kB
import PropTypes from 'prop-types' import React, { Component } from 'react' import ReactDOM from 'react-dom' import throttle from 'lodash/throttle' import { isShiftKeyPressed } from '../../utils/selectKeyUtils' import Icon from '../Icon' const ApplyButton = props => ( <button className="button-secondary button-icon" onClick={props.onApply}> <Icon name="tick" /> </button> ) const defaultCrop = { top: 0, left: 0, width: 1, height: 1 } export default class Cropper extends Component { static propTypes = { /** Image width */ width: PropTypes.number, /** Image height */ height: PropTypes.number, /** Image ratio, either null for free-crop or a ratio converted to a decimal (e.g. 3:2 = 1.5) */ fixedRatio: PropTypes.number, /** Called on live resize */ onChange: PropTypes.func, /** Only called when apply button is pressed */ onApply: PropTypes.func, /** Object containing number perchanges for: top, left, width, height. Ranges from 0 to 1 */ defaultCrop: PropTypes.object, /** React component for the Apply button */ ApplyButton: PropTypes.func, } static defaultProps = { width: 640, height: 480, fixedRatio: null, onChange: () => false, onApply: () => false, defaultCrop, ApplyButton, } constructor(props) { super() const crop = props.defaultCrop || defaultCrop this.state = { dragging: false, resizing: false, cropL: crop.left * props.width, cropT: crop.top * props.height, cropW: crop.width * props.width, cropH: crop.height * props.height, } this.cropType = null this.offsetTop = this.offsetLeft = this.clientX = this.clientY = this.dragX = this.dragY = this.origCropL = this.origCropT = 0 this.origCropW = props.width this.origCropH = props.height this.resizeRatio = 1 this.handleMouseUp = this.handleMouseUp.bind(this) this.handleMouseMove = this.handleMouseMove.bind(this) this.handleMouseMove = throttle(this.handleMouseMove, 1000 / 60) this.handleResize = this.handleResize.bind(this) this.handleResize = throttle(this.handleResize, 200) } componentDidMount() { this.refs.omg.setAttribute('fill-rule', 'evenodd') // wtf react window.addEventListener('mouseup', this.handleMouseUp) window.addEventListener('mousemove', this.handleMouseMove) window.addEventListener('resize', this.handleResize) this.handleResize() } componentWillUnmount() { window.removeEventListener('mouseup', this.handleMouseUp) window.removeEventListener('mousemove', this.handleMouseMove) window.removeEventListener('resize', this.handleResize) } componentWillReceiveProps(nextProps) { if (nextProps.fixedRatio !== this.props.fixedRatio && nextProps.fixedRatio !== null) { this.updateCrop(nextProps, true) } } /** * called on window resize to position correctly against 'BoundingClientRect' */ handleResize = () => { const offset = ReactDOM.findDOMNode(this.refs.cropper).getBoundingClientRect() this.offsetTop = offset.top this.offsetLeft = offset.left } /** * called by onMouseDown of any cropping edge/corner * @param {MouseEvent} event passed from onMouseDown of cropping edge/corner * @param {String} cropType type of crop, any of the following: TL, TM, TR, ML, MR, BL, BM, BR */ cropStart(event, cropType) { event.stopPropagation() const { cropW, cropH, cropL, cropT } = this.state this.cropType = cropType this.resizeRatio = cropW / cropH this.origCropW = cropW this.origCropH = cropH this.origCropL = cropL this.origCropT = cropT this.setState({ resizing: true }) } /** * called by onMouseDown of the cropping area * @param {MouseEvent} event passed on from onMouseDown of cropping area */ moveStart = event => { const { cropL, cropT } = this.state this.dragX = this.clientX - this.offsetLeft - cropL this.dragY = this.clientY - this.offsetTop - cropT this.setState({ dragging: true }) } /** * sets dragging and resizing state to false */ handleMouseUp() { this.setState({ dragging: false, resizing: false }) } /** * called onMouseMove, updates mouse position vars (is throttled in constructor) * @param {MouseEvent} event */ handleMouseMove(event) { this.clientX = event.clientX this.clientY = event.clientY this.updateCrop() } /** * called onClick of Apply button, calls onChange prop with new crop object */ handleApply = () => { const { width, height } = this.props const { cropL, cropT, cropW, cropH } = this.state this.props.onChange({ left: cropL / width, top: cropT / height, width: cropW / width, height: cropH / height, }) this.props.onApply() } /** * called either from handleMouseMove or from componentWillReceiveProps (if fixed ratio has changed) * @param {Object} props * @param {Number} props.width image width * @param {Number} props.height image height * @param {Number} props.fixedRatio fixed ratio, either null or a ratio converted to decimal (e.g 3:2 = 1.5) * @param {Boolean} forceResize determines if a resize needs to be forced, typically on fixed ratio change */ updateCrop(props = this.props, forceResize = false) { const { dragging, resizing } = this.state const { width, height, fixedRatio } = props let mouseX = this.clientX - this.offsetLeft let mouseY = this.clientY - this.offsetTop mouseX = mouseX < 0 ? 0 : mouseX > width ? width : mouseX mouseY = mouseY < 0 ? 0 : mouseY > height ? height : mouseY if (dragging) { const { cropW, cropH } = this.state let cropL = mouseX - this.dragX let cropT = mouseY - this.dragY cropL = cropL < 0 ? 0 : cropL > width - cropW ? width - cropW : cropL cropT = cropT < 0 ? 0 : cropT > height - cropH ? height - cropH : cropT this.setState({ cropL, cropT }) this.props.onChange({ left: cropL / width, top: cropT / height, width: this.state.cropW / width, height: this.state.cropH / height, }) } else if (resizing || forceResize) { let { cropW, cropH, cropL, cropT } = this.state const cmd = this.cropType || 'TL' if (isShiftKeyPressed() || fixedRatio) { const ratio = fixedRatio || this.resizeRatio if (cmd[0] !== 'M') { let diffX = 0 let diffY = 0 if (cmd[0] == 'T') { diffX = cropL + cropW - mouseX diffY = cropT + cropH - mouseY } else { diffX = mouseX - cropL diffY = mouseY - cropT } if (diffX > diffY) { cropH = diffX / ratio cropW = diffX } else { cropW = diffY * ratio cropH = diffY } if (cmd[0] == 'T') { cropT = this.origCropT - (cropH - this.origCropH) if (mouseY > cropT + cropH) this.cropType = 'B' + cmd[1] } else { if (mouseY < cropT) this.cropType = 'T' + cmd[1] } if (cmd[1] == 'L') { cropL = this.origCropL - (cropW - this.origCropW) if (mouseX > cropL + cropW) this.cropType = cmd[0] + 'R' } else { if (mouseX < cropL) this.cropType = cmd[0] + 'L' } if (cropT < 0) cropT = 0 if (cropL < 0) cropL = 0 if (cropT + cropH > height) { cropH = height - cropT cropW = cropH * ratio } if (cropL + cropW > width) { cropW = width - cropL cropH = cropW / ratio } } } else { if (cmd[1] == 'L') { cropW = cropW + (cropL - mouseX) cropL = mouseX } else if (cmd[1] == 'R') { cropW = mouseX - cropL } if (cmd[0] == 'T') { cropH = cropH + (cropT - mouseY) cropT = mouseY } else if (cmd[0] == 'B') { cropH = mouseY - cropT } } // check if cropping is going back on itself and adjust accordingly if (cropW < 0) { cropL += cropW cropW = Math.abs(cropW) this.cropType = cmd[0] + (cmd[1] == 'L' ? 'R' : 'L') } if (cropH < 0) { cropT += cropH cropH = Math.abs(cropH) this.cropType = (cmd[0] == 'T' ? 'B' : 'T') + cmd[1] } this.setState({ cropL, cropT, cropW, cropH }) this.props.onChange({ left: cropL / width, top: cropT / height, width: cropW / width, height: cropH / height, }) } } render() { const { width, height, ApplyButton } = this.props const { cropL, cropT, cropW, cropH } = this.state const path = 'M0 0 H ' + width + ' V ' + height + ' H 0 ZM' + cropL + ' ' + cropT + ' H ' + (cropL + cropW) + ' V ' + (cropT + cropH) + ' H ' + cropL + ' V ' + cropH + 'Z' const cropStyle = { left: cropL, top: cropT, width: cropW, height: cropH } return ( <div className="cropper" style={{ width, height }} ref="cropper"> <div className="crop-zone" style={cropStyle} onMouseDown={this.moveStart}> <div className="crop-handle TL" onMouseDown={e => this.cropStart(e, 'TL')} /> <div className="crop-handle TM" onMouseDown={e => this.cropStart(e, 'TM')} /> <div className="crop-handle TR" onMouseDown={e => this.cropStart(e, 'TR')} /> <div className="crop-handle ML" onMouseDown={e => this.cropStart(e, 'ML')} /> <div className="crop-handle MR" onMouseDown={e => this.cropStart(e, 'MR')} /> <div className="crop-handle BL" onMouseDown={e => this.cropStart(e, 'BL')} /> <div className="crop-handle BM" onMouseDown={e => this.cropStart(e, 'BM')} /> <div className="crop-handle BR" onMouseDown={e => this.cropStart(e, 'BR')} /> {ApplyButton && <ApplyButton onApply={this.handleApply} />} </div> <svg width={width} height={height} viewBox={'0 0 ' + width + ' ' + height}> <path d={path} fill="rgba(0,0,0,0.5)" ref="omg" /> </svg> </div> ) } }