├── .babelrc ├── .gitignore ├── README.md ├── cropper.css ├── dist ├── react-crop.js ├── react-crop.js.map ├── react-crop.min.js └── react-crop.min.js.map ├── docs ├── bundle.js ├── index.html └── index.js ├── draggable-resizable-box.js ├── index.js ├── lib ├── cropper.css ├── draggable-resizable-box.js └── index.src.js ├── package.json ├── webpack.config.js └── webpack.dist.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-1"], 3 | "plugins": ["transform-object-assign"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | npm_debug.log 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #react-crop# 2 | An accessible image cropper where the image is stationary and a resizable, draggable box represents the cropped image 3 | 4 | For example usage check out the docs folder. Demo: http://instructure-react.github.io/react-crop/ 5 | 6 | ###Basic usage### 7 | 8 | ``` javascript 9 | import React, { Component } from 'react'; 10 | 11 | import Cropper from 'react-crop'; 12 | import 'react-crop/css'; 13 | 14 | // You'll need to use async functions 15 | import "babel-core/register"; 16 | import "babel-polyfill"; 17 | 18 | export default class MyComponent extends Component { 19 | constructor() { 20 | super(); 21 | 22 | this.state = { 23 | image: null, 24 | previewImage: null 25 | }; 26 | } 27 | 28 | onChange(evt) { 29 | this.setState({ 30 | image: evt.target.files[0] 31 | }) 32 | } 33 | 34 | async crop() { 35 | let image = await this.refs.crop.cropImage() 36 | this.setState({ 37 | previewUrl: window.URL.createObjectURL(image) 38 | }) 39 | } 40 | 41 | clear() { 42 | this.refs.file.value = null 43 | this.setState({ 44 | previewUrl: null, 45 | image: null 46 | }) 47 | } 48 | 49 | imageLoaded(img) { 50 | if (img.naturalWidth && img.naturalWidth < 262 && 51 | img.naturalHeight && img.naturalHeight < 147) { 52 | this.crop() 53 | } 54 | } 55 | 56 | render() { 57 | return ( 58 |
59 | 60 | 61 | { 62 | 63 | this.state.image && 64 | 65 |
66 | 73 | 74 | 75 | 76 |
77 | 78 | } 79 | 80 | { 81 | this.state.previewUrl && 82 | 83 | 84 | } 85 | 86 |
87 | ); 88 | } 89 | } 90 | ``` 91 | 92 | ###Props### 93 | 94 | ####`width`#### 95 | This is the desired width that you would like the image to be cropped to. The width of the cropper will be scaled to fit this size. This prop also helps determine the minimum width that the cropper can be. 96 | 97 | ####`height`#### 98 | This is the desired height that you would like the image to be cropped to. The height of the cropper will be scaled to fit this size. This prop also helps determine the minimum height that the cropper can be. The width and height aspect ratio will be preserved while resizing the cropper. 99 | 100 | ####`image`#### 101 | A `blob` of the original image that you wish to crop. 102 | 103 | ####`widthLabel`#### 104 | The label to use next to the width input used for keyboard users. This is especially useful if you need to localize the text. The default is "Width". 105 | 106 | ####`heightLabel`#### 107 | The label to use next to the height input used for keyboard users. This is especially useful if you need to localize the text. The default is "Height". 108 | 109 | ####`offsetXLabel`#### 110 | The label to use next to the offset X input used for keyboard users. This is especially useful if you need to localize the text. The default is "Offset X". 111 | 112 | ####`offsetYLabel`#### 113 | The label to use next to the offset Y input used for keyboard users. This is especially useful if you need to localize the text. The default is "Offset Y". 114 | 115 | ###Running the Example### 116 | - Clone the repo 117 | - `npm i` 118 | - `npm run docs` 119 | - Visit `localhost:8080` 120 | -------------------------------------------------------------------------------- /cropper.css: -------------------------------------------------------------------------------- 1 | .Cropper { 2 | position: relative; 3 | display: inline-block; 4 | max-width:100%; 5 | max-height:100%; 6 | } 7 | 8 | .box { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | bottom: 0; 13 | right: 0; 14 | } 15 | 16 | .Cropper-box { 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | bottom: 0; 21 | right: 0; 22 | cursor: move; 23 | border: #fff solid 1px; 24 | } 25 | 26 | .Cropper-canvas { 27 | visibility: hidden; 28 | position: absolute; 29 | } 30 | 31 | .Cropper-image { 32 | vertical-align: middle; 33 | max-width: 100%; 34 | position: relative; 35 | transform: translate(-50%, -50%); 36 | left: 50%; 37 | } 38 | 39 | .resize-handle { 40 | position: absolute; 41 | background-color: #ECEEEF; 42 | border: #8295AB solid 1px; 43 | width: 13px; 44 | height: 13px; 45 | z-index: 1; 46 | } 47 | 48 | .resize-handle-se { 49 | bottom: 0; 50 | right: 0; 51 | cursor: nwse-resize; 52 | transform: translate(50%, 50%); 53 | } 54 | .resize-handle-ne { 55 | right: 0; 56 | top: 0; 57 | cursor: nesw-resize; 58 | transform: translate(50%, -50%); 59 | } 60 | .resize-handle-sw { 61 | bottom: 0; 62 | left: 0; 63 | cursor: nesw-resize; 64 | transform: translate(-50%, 50%); 65 | } 66 | .resize-handle-nw { 67 | top: 0; 68 | bottom: 0; 69 | cursor: nwse-resize; 70 | transform: translate(-50%, -50%); 71 | } 72 | 73 | .DraggableResizable { 74 | position: relative; 75 | width: 100%; 76 | height: 100%; 77 | } 78 | 79 | .DraggableResizable-controls { 80 | border: 0; 81 | clip: rect(0 0 0 0); 82 | height: 1px; 83 | margin: -1px; 84 | overflow: hidden; 85 | padding: 0; 86 | position: absolute; 87 | width: 1px; 88 | } 89 | 90 | .DraggableResizable-top, 91 | .DraggableResizable-left, 92 | .DraggableResizable-bottom, 93 | .DraggableResizable-right { 94 | position: absolute; 95 | background-color: rgba(0,0,0,.7); 96 | } 97 | 98 | .DraggableResizable-top { 99 | top: 0; 100 | left: 0; 101 | right: 0; 102 | } 103 | .DraggableResizable-bottom { 104 | bottom: 0; 105 | left: 0; 106 | right: 0; 107 | } 108 | .DraggableResizable-left { 109 | left: 0; 110 | } 111 | .DraggableResizable-right { 112 | right: 0; 113 | } 114 | -------------------------------------------------------------------------------- /dist/react-crop.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("react")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["react"], factory); 6 | else if(typeof exports === 'object') 7 | exports["ReactCrop"] = factory(require("react")); 8 | else 9 | root["ReactCrop"] = factory(root["react"]); 10 | })(this, function(__WEBPACK_EXTERNAL_MODULE_1__) { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = "/"; 48 | /******/ 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ function(module, exports, __webpack_require__) { 56 | 57 | 'use strict'; 58 | 59 | Object.defineProperty(exports, "__esModule", { 60 | value: true 61 | }); 62 | 63 | var _react = __webpack_require__(1); 64 | 65 | var _react2 = _interopRequireDefault(_react); 66 | 67 | var _draggableResizableBox = __webpack_require__(2); 68 | 69 | var _draggableResizableBox2 = _interopRequireDefault(_draggableResizableBox); 70 | 71 | var _dataUriToBlob = __webpack_require__(3); 72 | 73 | var _dataUriToBlob2 = _interopRequireDefault(_dataUriToBlob); 74 | 75 | __webpack_require__(4); 76 | 77 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 78 | 79 | exports.default = _react2.default.createClass({ 80 | displayName: 'Cropper', 81 | 82 | propTypes: { 83 | width: _react2.default.PropTypes.number.isRequired, 84 | height: _react2.default.PropTypes.number.isRequired, 85 | center: _react2.default.PropTypes.bool, 86 | image: _react2.default.PropTypes.any, 87 | widthLabel: _react2.default.PropTypes.string, 88 | heightLabel: _react2.default.PropTypes.string, 89 | offsetXLabel: _react2.default.PropTypes.string, 90 | offsetYLabel: _react2.default.PropTypes.string, 91 | onImageLoaded: _react2.default.PropTypes.func, 92 | minConstraints: _react2.default.PropTypes.arrayOf(_react2.default.PropTypes.number) 93 | }, 94 | 95 | getDefaultProps: function getDefaultProps() { 96 | return { 97 | center: false, 98 | width: 'Width', 99 | height: 'Height', 100 | offsetXLabel: 'Offset X', 101 | offsetYLabel: 'Offset Y' 102 | }; 103 | }, 104 | getInitialState: function getInitialState() { 105 | return { 106 | imageLoaded: false, 107 | width: this.props.width, 108 | height: this.props.height, 109 | url: window.URL.createObjectURL(this.props.image) 110 | }; 111 | }, 112 | componentWillReceiveProps: function componentWillReceiveProps(nextProps) { 113 | if (this.props.image !== nextProps.image) { 114 | this.setState({ 115 | url: window.URL.createObjectURL(nextProps.image), 116 | imageLoaded: false 117 | }); 118 | } 119 | }, 120 | shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) { 121 | var image = this.props.image; 122 | 123 | return nextProps.image.size !== image.size || nextProps.image.name !== image.name || nextProps.image.type !== image.type || nextState.imageLoaded !== this.state.imageLoaded; 124 | }, 125 | onLoad: function onLoad(evt) { 126 | var _this = this; 127 | 128 | var box = this.refs.box.getBoundingClientRect(); 129 | this.setState({ 130 | imageLoaded: true, 131 | width: box.width, 132 | height: box.height 133 | }, function () { 134 | var img = _this.refs.image; 135 | _this.props.onImageLoaded && _this.props.onImageLoaded(img); 136 | }); 137 | }, 138 | cropImage: function cropImage() { 139 | var _this2 = this; 140 | 141 | return new Promise(function (resolve, reject) { 142 | var img = new Image(); 143 | img.onload = function () { 144 | var canvas = _this2.refs.canvas; 145 | var img = _this2.refs.image; 146 | var ctx = canvas.getContext('2d'); 147 | var xScale = img.naturalWidth / _this2.state.width, 148 | yScale = img.naturalHeight / _this2.state.height; 149 | 150 | 151 | var imageOffsetX = xScale < 1 ? 0 : _this2.state.offset.left * xScale; 152 | var imageOffsetY = yScale < 1 ? 0 : _this2.state.offset.top * yScale; 153 | var imageWidth = xScale < 1 ? img.naturalWidth : _this2.state.dimensions.width * xScale; 154 | var imageHeight = yScale < 1 ? img.naturalHeight : _this2.state.dimensions.height * yScale; 155 | 156 | var canvasOffsetX = xScale < 1 ? Math.floor((_this2.state.dimensions.width - img.naturalWidth) / 2) : 0; 157 | var canvasOffsetY = yScale < 1 ? Math.floor((_this2.state.dimensions.height - img.naturalHeight) / 2) : 0; 158 | var canvasWidth = xScale < 1 ? img.naturalWidth : _this2.props.width; 159 | var canvasHeight = yScale < 1 ? img.naturalHeight : _this2.props.height; 160 | 161 | ctx.clearRect(0, 0, _this2.props.width, _this2.props.height); 162 | ctx.drawImage(img, imageOffsetX, imageOffsetY, imageWidth, imageHeight, canvasOffsetX, canvasOffsetY, canvasWidth, canvasHeight); 163 | resolve((0, _dataUriToBlob2.default)(canvas.toDataURL())); 164 | }; 165 | img.src = window.URL.createObjectURL(_this2.props.image); 166 | }); 167 | }, 168 | onChange: function onChange(offset, dimensions) { 169 | this.setState({ offset: offset, dimensions: dimensions }); 170 | }, 171 | render: function render() { 172 | return _react2.default.createElement( 173 | 'div', 174 | { 175 | ref: 'box', 176 | className: 'Cropper', 177 | style: { 178 | minWidth: this.props.width, 179 | minHeight: this.props.height 180 | } }, 181 | _react2.default.createElement('canvas', { 182 | className: 'Cropper-canvas', 183 | ref: 'canvas', 184 | width: this.props.width, 185 | height: this.props.height }), 186 | _react2.default.createElement('img', { 187 | ref: 'image', 188 | src: this.state.url, 189 | className: 'Cropper-image', 190 | onLoad: this.onLoad, 191 | style: { top: this.state.height / 2 } }), 192 | this.state.imageLoaded && _react2.default.createElement( 193 | 'div', 194 | { className: 'box' }, 195 | _react2.default.createElement( 196 | _draggableResizableBox2.default, 197 | { 198 | aspectRatio: this.props.width / this.props.height, 199 | width: this.state.width, 200 | height: this.state.height, 201 | minConstraints: this.props.minConstraints, 202 | onChange: this.onChange, 203 | widthLabel: this.props.widthLabel, 204 | heightLabel: this.props.heightLabel, 205 | offsetXLabel: this.props.offsetXLabel, 206 | offsetYLabel: this.props.offsetYLabel }, 207 | _react2.default.createElement('div', { className: 'Cropper-box' }) 208 | ) 209 | ) 210 | ); 211 | } 212 | }); 213 | 214 | /***/ }, 215 | /* 1 */ 216 | /***/ function(module, exports) { 217 | 218 | module.exports = __WEBPACK_EXTERNAL_MODULE_1__; 219 | 220 | /***/ }, 221 | /* 2 */ 222 | /***/ function(module, exports, __webpack_require__) { 223 | 224 | 'use strict'; 225 | 226 | Object.defineProperty(exports, "__esModule", { 227 | value: true 228 | }); 229 | 230 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 231 | 232 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 233 | 234 | var _react = __webpack_require__(1); 235 | 236 | var _react2 = _interopRequireDefault(_react); 237 | 238 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 239 | 240 | exports.default = _react2.default.createClass({ 241 | displayName: 'DraggableResizableBox', 242 | 243 | propTypes: { 244 | aspectRatio: _react2.default.PropTypes.number.isRequired, 245 | width: _react2.default.PropTypes.number.isRequired, 246 | height: _react2.default.PropTypes.number.isRequired, 247 | onChange: _react2.default.PropTypes.func, 248 | offset: _react2.default.PropTypes.array, 249 | minConstraints: _react2.default.PropTypes.array, 250 | children: _react2.default.PropTypes.node, 251 | widthLabel: _react2.default.PropTypes.string, 252 | heightLabel: _react2.default.PropTypes.string, 253 | offsetXLabel: _react2.default.PropTypes.string, 254 | offsetYLabel: _react2.default.PropTypes.string 255 | }, 256 | 257 | getDefaultProps: function getDefaultProps() { 258 | return { 259 | widthLabel: 'Width', 260 | heightLabel: 'Height', 261 | offsetXLabel: 'Offset X', 262 | offsetYLabel: 'Offset Y' 263 | }; 264 | }, 265 | getInitialState: function getInitialState() { 266 | var _preserveAspectRatio = this.preserveAspectRatio(this.props.width, this.props.height), 267 | _preserveAspectRatio2 = _slicedToArray(_preserveAspectRatio, 2), 268 | width = _preserveAspectRatio2[0], 269 | height = _preserveAspectRatio2[1]; 270 | 271 | var centerYOffset = (this.props.height - height) / 2; 272 | var centerXOffset = (this.props.width - width) / 2; 273 | return { 274 | top: centerYOffset, 275 | left: centerXOffset, 276 | bottom: centerYOffset, 277 | right: centerXOffset, 278 | width: width, 279 | height: height 280 | }; 281 | }, 282 | componentDidMount: function componentDidMount() { 283 | document.addEventListener('mousemove', this.eventMove); 284 | document.addEventListener('mouseup', this.eventEnd); 285 | document.addEventListener('touchmove', this.eventMove); 286 | document.addEventListener('touchend', this.eventEnd); 287 | document.addEventListener('keydown', this.handleKey); 288 | this.props.onChange({ 289 | top: this.state.top, 290 | left: this.state.left 291 | }, { 292 | width: this.state.width, 293 | height: this.state.height 294 | }); 295 | }, 296 | componentWillUnmount: function componentWillUnmount() { 297 | document.removeEventListener('mousemove', this.eventMove); 298 | document.removeEventListener('mouseup', this.eventEnd); 299 | document.removeEventListener('touchmove', this.eventMove); 300 | document.removeEventListener('touchend', this.eventEnd); 301 | document.removeEventListener('keydown', this.handleKey); 302 | }, 303 | calculateDimensions: function calculateDimensions(_ref) { 304 | var top = _ref.top, 305 | left = _ref.left, 306 | bottom = _ref.bottom, 307 | right = _ref.right; 308 | 309 | return { width: this.props.width - left - right, height: this.props.height - top - bottom }; 310 | }, 311 | 312 | 313 | // If you do this, be careful of constraints 314 | preserveAspectRatio: function preserveAspectRatio(width, height) { 315 | if (this.props.minConstraints) { 316 | width = Math.max(width, this.props.minConstraints[0]); 317 | height = Math.max(height, this.props.minConstraints[1]); 318 | } 319 | var currentAspectRatio = width / height; 320 | 321 | if (currentAspectRatio < this.props.aspectRatio) { 322 | return [width, width / this.props.aspectRatio]; 323 | } else if (currentAspectRatio > this.props.aspectRatio) { 324 | return [height * this.props.aspectRatio, height]; 325 | } else { 326 | return [width, height]; 327 | } 328 | }, 329 | constrainBoundary: function constrainBoundary(side) { 330 | return side < 0 ? 0 : side; 331 | }, 332 | getClientCoordinates: function getClientCoordinates(evt) { 333 | return evt.touches ? { 334 | clientX: evt.touches[0].clientX, 335 | clientY: evt.touches[0].clientY 336 | } : { 337 | clientX: evt.clientX, 338 | clientY: evt.clientY 339 | }; 340 | }, 341 | eventMove: function eventMove(evt) { 342 | if (this.state.resizing) { 343 | this.onResize(evt); 344 | } else if (this.state.moving) { 345 | this.eventMoveBox(evt); 346 | } 347 | }, 348 | eventEnd: function eventEnd(evt) { 349 | if (this.state.resizing) { 350 | this.stopResize(evt); 351 | } else if (this.state.moving) { 352 | this.stopMove(evt); 353 | } 354 | }, 355 | 356 | 357 | // Resize methods 358 | startResize: function startResize(corner, event) { 359 | event.stopPropagation(); 360 | event.preventDefault(); 361 | this.setState({ 362 | resizing: true, 363 | corner: corner 364 | }); 365 | }, 366 | stopResize: function stopResize() { 367 | this.setState({ resizing: false }); 368 | }, 369 | 370 | 371 | // resize strategies 372 | nw: function nw(mousePos, boxPos) { 373 | var pos = _extends({}, this.state, { 374 | top: this.constrainBoundary(mousePos.clientY - boxPos.top), 375 | left: this.constrainBoundary(mousePos.clientX - boxPos.left) 376 | }); 377 | var dimensions = this.calculateDimensions(pos); 378 | 379 | var _preserveAspectRatio3 = this.preserveAspectRatio(dimensions.width, dimensions.height), 380 | _preserveAspectRatio4 = _slicedToArray(_preserveAspectRatio3, 2), 381 | width = _preserveAspectRatio4[0], 382 | height = _preserveAspectRatio4[1]; 383 | 384 | pos.top = this.props.height - pos.bottom - height; 385 | pos.left = this.props.width - pos.right - width; 386 | return pos; 387 | }, 388 | ne: function ne(mousePos, boxPos) { 389 | var pos = _extends({}, this.state, { 390 | top: this.constrainBoundary(mousePos.clientY - boxPos.top), 391 | right: this.constrainBoundary(boxPos.right - mousePos.clientX) 392 | }); 393 | var dimensions = this.calculateDimensions(pos); 394 | 395 | var _preserveAspectRatio5 = this.preserveAspectRatio(dimensions.width, dimensions.height), 396 | _preserveAspectRatio6 = _slicedToArray(_preserveAspectRatio5, 2), 397 | width = _preserveAspectRatio6[0], 398 | height = _preserveAspectRatio6[1]; 399 | 400 | pos.top = this.props.height - pos.bottom - height; 401 | pos.right = this.props.width - pos.left - width; 402 | return pos; 403 | }, 404 | se: function se(mousePos, boxPos) { 405 | var pos = _extends({}, this.state, { 406 | bottom: this.constrainBoundary(boxPos.bottom - mousePos.clientY), 407 | right: this.constrainBoundary(boxPos.right - mousePos.clientX) 408 | }); 409 | var dimensions = this.calculateDimensions(pos); 410 | 411 | var _preserveAspectRatio7 = this.preserveAspectRatio(dimensions.width, dimensions.height), 412 | _preserveAspectRatio8 = _slicedToArray(_preserveAspectRatio7, 2), 413 | width = _preserveAspectRatio8[0], 414 | height = _preserveAspectRatio8[1]; 415 | 416 | pos.bottom = this.props.height - pos.top - height; 417 | pos.right = this.props.width - pos.left - width; 418 | return pos; 419 | }, 420 | sw: function sw(mousePos, boxPos) { 421 | var pos = _extends({}, this.state, { 422 | bottom: this.constrainBoundary(boxPos.bottom - mousePos.clientY), 423 | left: this.constrainBoundary(mousePos.clientX - boxPos.left) 424 | }); 425 | var dimensions = this.calculateDimensions(pos); 426 | 427 | var _preserveAspectRatio9 = this.preserveAspectRatio(dimensions.width, dimensions.height), 428 | _preserveAspectRatio10 = _slicedToArray(_preserveAspectRatio9, 2), 429 | width = _preserveAspectRatio10[0], 430 | height = _preserveAspectRatio10[1]; 431 | 432 | pos.bottom = this.props.height - pos.top - height; 433 | pos.left = this.props.width - pos.right - width; 434 | return pos; 435 | }, 436 | onResize: function onResize(event) { 437 | var box = this.refs.box.parentElement.parentElement.getBoundingClientRect(); 438 | var coordinates = this.getClientCoordinates(event); 439 | var position = this[this.state.corner](coordinates, box); 440 | this.resize(position, coordinates); 441 | }, 442 | controlsResize: function controlsResize(event) { 443 | var box = this.refs.box.parentElement.parentElement.getBoundingClientRect(); 444 | var width = event.target.name === 'width' ? +event.target.value : +event.target.value * this.props.aspectRatio; 445 | var height = event.target.name === 'height' ? +event.target.value : +event.target.value / this.props.aspectRatio; 446 | var dimensions = this.preserveAspectRatio(width, height); 447 | width = dimensions[0]; 448 | height = dimensions[1]; 449 | 450 | if (width > box.width - this.state.left || height > box.height - this.state.top) return; 451 | 452 | var widthDifference = this.state.width - width; 453 | var heightDifference = this.state.height - height; 454 | var pos = _extends({}, this.state, { 455 | right: this.state.right + widthDifference, 456 | bottom: this.state.bottom + heightDifference 457 | }); 458 | var coordinates = { 459 | clientX: box.right - pos.right, 460 | clientY: box.bottom - pos.bottom 461 | }; 462 | 463 | this.resize(pos, coordinates); 464 | }, 465 | resize: function resize(position, coordinates) { 466 | var _this = this; 467 | 468 | var dimensions = this.calculateDimensions(position); 469 | var widthChanged = dimensions.width !== this.state.width, 470 | heightChanged = dimensions.height !== this.state.height; 471 | if (!widthChanged && !heightChanged) return; 472 | 473 | this.setState(_extends({}, coordinates, position, dimensions), function () { 474 | _this.props.onChange({ 475 | top: position.top, 476 | left: position.left 477 | }, dimensions); 478 | }); 479 | }, 480 | 481 | 482 | // Move methods 483 | startMove: function startMove(evt) { 484 | var _getClientCoordinates = this.getClientCoordinates(evt), 485 | clientX = _getClientCoordinates.clientX, 486 | clientY = _getClientCoordinates.clientY; 487 | 488 | this.setState({ 489 | moving: true, 490 | clientX: clientX, 491 | clientY: clientY 492 | }); 493 | }, 494 | stopMove: function stopMove(evt) { 495 | this.setState({ 496 | moving: false 497 | }); 498 | }, 499 | eventMoveBox: function eventMoveBox(evt) { 500 | evt.preventDefault(); 501 | 502 | var _getClientCoordinates2 = this.getClientCoordinates(evt), 503 | clientX = _getClientCoordinates2.clientX, 504 | clientY = _getClientCoordinates2.clientY; 505 | 506 | var movedX = clientX - this.state.clientX; 507 | var movedY = clientY - this.state.clientY; 508 | 509 | this.moveBox(clientX, clientY, movedX, movedY); 510 | }, 511 | controlsMoveBox: function controlsMoveBox(evt) { 512 | var movedX = evt.target.name === 'x' ? evt.target.value - this.state.left : 0; 513 | var movedY = evt.target.name === 'y' ? evt.target.value - this.state.top : 0; 514 | this.moveBox(0, 0, movedX, movedY); 515 | }, 516 | moveBox: function moveBox(clientX, clientY, movedX, movedY) { 517 | var _this2 = this; 518 | 519 | var position = { 520 | top: this.constrainBoundary(this.state.top + movedY), 521 | left: this.constrainBoundary(this.state.left + movedX), 522 | bottom: this.constrainBoundary(this.state.bottom - movedY), 523 | right: this.constrainBoundary(this.state.right - movedX) 524 | }; 525 | 526 | if (!position.top) { 527 | position.bottom = this.props.height - this.state.height; 528 | } 529 | if (!position.bottom) { 530 | position.top = this.props.height - this.state.height; 531 | } 532 | if (!position.left) { 533 | position.right = this.props.width - this.state.width; 534 | } 535 | if (!position.right) { 536 | position.left = this.props.width - this.state.width; 537 | } 538 | 539 | this.setState(_extends({}, { 540 | clientX: clientX, 541 | clientY: clientY 542 | }, position), function () { 543 | _this2.props.onChange({ 544 | top: position.top, 545 | left: position.left 546 | }, _this2.calculateDimensions(position)); 547 | }); 548 | }, 549 | keyboardResize: function keyboardResize(change) { 550 | if (this.state.right - change < 0) { 551 | return; 552 | } 553 | if (this.state.bottom - change < 0) { 554 | return; 555 | } 556 | 557 | var _preserveAspectRatio11 = this.preserveAspectRatio(this.state.width + change, this.state.height + change), 558 | _preserveAspectRatio12 = _slicedToArray(_preserveAspectRatio11, 2), 559 | width = _preserveAspectRatio12[0], 560 | height = _preserveAspectRatio12[1]; 561 | 562 | var widthChange = width - this.state.width; 563 | var heightChange = height - this.state.height; 564 | 565 | this.setState({ 566 | bottom: this.state.bottom - heightChange, 567 | right: this.state.right - widthChange, 568 | width: width, 569 | height: height 570 | }); 571 | }, 572 | handleKey: function handleKey(event) { 573 | // safari doesn't support event.key, so fall back to keyCode 574 | if (event.shiftKey) { 575 | if (event.key === 'ArrowUp' || event.keyCode === 38) { 576 | this.keyboardResize(-10); 577 | event.preventDefault(); 578 | } else if (event.key === 'ArrowDown' || event.keyCode === 40) { 579 | this.keyboardResize(10); 580 | event.preventDefault(); 581 | } else if (event.key === 'ArrowLeft' || event.keyCode === 37) { 582 | this.keyboardResize(-10); 583 | event.preventDefault(); 584 | } else if (event.key === 'ArrowRight' || event.keyCode === 39) { 585 | this.keyboardResize(10); 586 | event.preventDefault(); 587 | } 588 | } else { 589 | if (event.key === 'ArrowUp' || event.keyCode === 38) { 590 | this.moveBox(this.state.clientX, this.state.clientY, 0, -10); 591 | event.preventDefault(); 592 | } else if (event.key === 'ArrowDown' || event.keyCode === 40) { 593 | this.moveBox(this.state.clientX, this.state.clientY, 0, 10); 594 | event.preventDefault(); 595 | } else if (event.key === 'ArrowLeft' || event.keyCode === 37) { 596 | this.moveBox(this.state.clientX, this.state.clientY, -10, 0); 597 | event.preventDefault(); 598 | } else if (event.key === 'ArrowRight' || event.keyCode === 39) { 599 | this.moveBox(this.state.clientX, this.state.clientY, 10, 0); 600 | event.preventDefault(); 601 | } 602 | } 603 | }, 604 | render: function render() { 605 | var style = { 606 | position: 'absolute', 607 | top: this.state.top, 608 | left: this.state.left, 609 | right: this.state.right, 610 | bottom: this.state.bottom 611 | }; 612 | 613 | var _calculateDimensions = this.calculateDimensions(this.state), 614 | width = _calculateDimensions.width, 615 | height = _calculateDimensions.height; 616 | 617 | var topStyle = { 618 | height: this.state.top 619 | }; 620 | var bottomStyle = { 621 | height: this.state.bottom 622 | }; 623 | var leftStyle = { 624 | top: this.state.top, 625 | right: width + this.state.right, 626 | bottom: this.state.bottom 627 | }; 628 | var rightStyle = { 629 | top: this.state.top, 630 | left: width + this.state.left, 631 | bottom: this.state.bottom 632 | }; 633 | 634 | return _react2.default.createElement( 635 | 'div', 636 | { ref: 'box', className: 'DraggableResizable' }, 637 | _react2.default.createElement( 638 | 'div', 639 | { className: 'DraggableResizable-controls' }, 640 | _react2.default.createElement( 641 | 'label', 642 | null, 643 | this.props.offsetXLabel, 644 | _react2.default.createElement('input', { 645 | name: 'x', 646 | value: Math.round(this.state.left), 647 | onChange: this.controlsMoveBox, 648 | tabIndex: '-1', 649 | type: 'number' }) 650 | ), 651 | _react2.default.createElement( 652 | 'label', 653 | null, 654 | this.props.offsetYLabel, 655 | _react2.default.createElement('input', { 656 | name: 'y', 657 | value: Math.round(this.state.top), 658 | onChange: this.controlsMoveBox, 659 | tabIndex: '-1', 660 | type: 'number' }) 661 | ), 662 | _react2.default.createElement( 663 | 'label', 664 | null, 665 | this.props.widthLabel, 666 | _react2.default.createElement('input', { 667 | name: 'width', 668 | value: Math.round(width), 669 | type: 'number', 670 | tabIndex: '-1', 671 | onChange: this.controlsResize }) 672 | ), 673 | _react2.default.createElement( 674 | 'label', 675 | null, 676 | this.props.heightLabel, 677 | _react2.default.createElement('input', { 678 | value: Math.round(height), 679 | type: 'number', 680 | name: 'height', 681 | tabIndex: '-1', 682 | onChange: this.controlsResize }) 683 | ) 684 | ), 685 | _react2.default.createElement('div', { className: 'DraggableResizable-top', style: topStyle }), 686 | _react2.default.createElement('div', { className: 'DraggableResizable-left', style: leftStyle }), 687 | _react2.default.createElement( 688 | 'div', 689 | { style: style, onMouseDown: this.startMove, onTouchStart: this.startMove }, 690 | this.props.children, 691 | _react2.default.createElement('div', { className: 'resize-handle resize-handle-se', 692 | onMouseDown: this.startResize.bind(null, 'se'), 693 | onTouchStart: this.startResize.bind(null, 'se') }), 694 | _react2.default.createElement('div', { className: 'resize-handle resize-handle-ne', 695 | onMouseDown: this.startResize.bind(null, 'ne'), 696 | onTouchStart: this.startResize.bind(null, 'ne') }), 697 | _react2.default.createElement('div', { className: 'resize-handle resize-handle-sw', 698 | onMouseDown: this.startResize.bind(null, 'sw'), 699 | onTouchStart: this.startResize.bind(null, 'sw') }), 700 | _react2.default.createElement('div', { className: 'resize-handle resize-handle-nw', 701 | onMouseDown: this.startResize.bind(null, 'nw'), 702 | onTouchStart: this.startResize.bind(null, 'nw') }) 703 | ), 704 | _react2.default.createElement('div', { className: 'DraggableResizable-right', style: rightStyle }), 705 | _react2.default.createElement('div', { className: 'DraggableResizable-bottom', style: bottomStyle }) 706 | ); 707 | } 708 | }); 709 | 710 | /***/ }, 711 | /* 3 */ 712 | /***/ function(module, exports) { 713 | 714 | 715 | /** 716 | * Blob constructor. 717 | */ 718 | 719 | var Blob = window.Blob; 720 | 721 | /** 722 | * ArrayBufferView support. 723 | */ 724 | 725 | var hasArrayBufferView = new Blob([new Uint8Array(100)]).size == 100; 726 | 727 | /** 728 | * Return a `Blob` for the given data `uri`. 729 | * 730 | * @param {String} uri 731 | * @return {Blob} 732 | * @api public 733 | */ 734 | 735 | module.exports = function(uri){ 736 | var data = uri.split(',')[1]; 737 | var bytes = atob(data); 738 | var buf = new ArrayBuffer(bytes.length); 739 | var arr = new Uint8Array(buf); 740 | for (var i = 0; i < bytes.length; i++) { 741 | arr[i] = bytes.charCodeAt(i); 742 | } 743 | 744 | if (!hasArrayBufferView) arr = buf; 745 | var blob = new Blob([arr], { type: mime(uri) }); 746 | blob.slice = blob.slice || blob.webkitSlice; 747 | return blob; 748 | }; 749 | 750 | /** 751 | * Return data uri mime type. 752 | */ 753 | 754 | function mime(uri) { 755 | return uri.split(';')[0].slice(5); 756 | } 757 | 758 | 759 | /***/ }, 760 | /* 4 */ 761 | /***/ function(module, exports, __webpack_require__) { 762 | 763 | // style-loader: Adds some css to the DOM by adding a 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Cropper from '../lib/index.src.js' 4 | 5 | const WIDTH = 262; 6 | const HEIGHT = 147; 7 | 8 | let Wrapper = React.createClass({ 9 | displayName: 'Wrapper', 10 | 11 | getInitialState () { 12 | return { 13 | image: null, 14 | previewUrl: null 15 | } 16 | }, 17 | 18 | onChange (evt) { 19 | this.setState({ 20 | image: evt.target.files[0] 21 | }) 22 | }, 23 | 24 | crop () { 25 | this.refs.crop.cropImage().then((image) => { 26 | this.setState({ 27 | previewUrl: window.URL.createObjectURL(image) 28 | }) 29 | }) 30 | }, 31 | 32 | clear () { 33 | this.refs.file.value = null 34 | this.setState({ 35 | previewUrl: null, 36 | image: null 37 | }) 38 | }, 39 | 40 | imageLoaded (img) { 41 | if (img.naturalWidth && img.naturalWidth < WIDTH && 42 | img.naturalHeight && img.naturalHeight < HEIGHT) { 43 | this.crop() 44 | } 45 | }, 46 | 47 | render () { 48 | return ( 49 |
50 | 51 |
52 | {this.state.image && 53 |
54 | 60 |
} 61 |
62 |
63 | 64 | 65 |
66 | {this.state.previewUrl && 67 | } 68 |
69 | ) 70 | } 71 | }) 72 | 73 | ReactDOM.render(, document.querySelector('#view')) 74 | -------------------------------------------------------------------------------- /draggable-resizable-box.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | exports.default = _react2.default.createClass({ 18 | displayName: 'DraggableResizableBox', 19 | 20 | propTypes: { 21 | aspectRatio: _react2.default.PropTypes.number.isRequired, 22 | width: _react2.default.PropTypes.number.isRequired, 23 | height: _react2.default.PropTypes.number.isRequired, 24 | onChange: _react2.default.PropTypes.func, 25 | offset: _react2.default.PropTypes.array, 26 | minConstraints: _react2.default.PropTypes.array, 27 | children: _react2.default.PropTypes.node, 28 | widthLabel: _react2.default.PropTypes.string, 29 | heightLabel: _react2.default.PropTypes.string, 30 | offsetXLabel: _react2.default.PropTypes.string, 31 | offsetYLabel: _react2.default.PropTypes.string 32 | }, 33 | 34 | getDefaultProps: function getDefaultProps() { 35 | return { 36 | widthLabel: 'Width', 37 | heightLabel: 'Height', 38 | offsetXLabel: 'Offset X', 39 | offsetYLabel: 'Offset Y' 40 | }; 41 | }, 42 | getInitialState: function getInitialState() { 43 | var _preserveAspectRatio = this.preserveAspectRatio(this.props.width, this.props.height), 44 | _preserveAspectRatio2 = _slicedToArray(_preserveAspectRatio, 2), 45 | width = _preserveAspectRatio2[0], 46 | height = _preserveAspectRatio2[1]; 47 | 48 | var centerYOffset = (this.props.height - height) / 2; 49 | var centerXOffset = (this.props.width - width) / 2; 50 | return { 51 | top: centerYOffset, 52 | left: centerXOffset, 53 | bottom: centerYOffset, 54 | right: centerXOffset, 55 | width: width, 56 | height: height 57 | }; 58 | }, 59 | componentDidMount: function componentDidMount() { 60 | document.addEventListener('mousemove', this.eventMove); 61 | document.addEventListener('mouseup', this.eventEnd); 62 | document.addEventListener('touchmove', this.eventMove); 63 | document.addEventListener('touchend', this.eventEnd); 64 | document.addEventListener('keydown', this.handleKey); 65 | this.props.onChange({ 66 | top: this.state.top, 67 | left: this.state.left 68 | }, { 69 | width: this.state.width, 70 | height: this.state.height 71 | }); 72 | }, 73 | componentWillUnmount: function componentWillUnmount() { 74 | document.removeEventListener('mousemove', this.eventMove); 75 | document.removeEventListener('mouseup', this.eventEnd); 76 | document.removeEventListener('touchmove', this.eventMove); 77 | document.removeEventListener('touchend', this.eventEnd); 78 | document.removeEventListener('keydown', this.handleKey); 79 | }, 80 | calculateDimensions: function calculateDimensions(_ref) { 81 | var top = _ref.top, 82 | left = _ref.left, 83 | bottom = _ref.bottom, 84 | right = _ref.right; 85 | 86 | return { width: this.props.width - left - right, height: this.props.height - top - bottom }; 87 | }, 88 | 89 | 90 | // If you do this, be careful of constraints 91 | preserveAspectRatio: function preserveAspectRatio(width, height) { 92 | if (this.props.minConstraints) { 93 | width = Math.max(width, this.props.minConstraints[0]); 94 | height = Math.max(height, this.props.minConstraints[1]); 95 | } 96 | var currentAspectRatio = width / height; 97 | 98 | if (currentAspectRatio < this.props.aspectRatio) { 99 | return [width, width / this.props.aspectRatio]; 100 | } else if (currentAspectRatio > this.props.aspectRatio) { 101 | return [height * this.props.aspectRatio, height]; 102 | } else { 103 | return [width, height]; 104 | } 105 | }, 106 | constrainBoundary: function constrainBoundary(side) { 107 | return side < 0 ? 0 : side; 108 | }, 109 | getClientCoordinates: function getClientCoordinates(evt) { 110 | return evt.touches ? { 111 | clientX: evt.touches[0].clientX, 112 | clientY: evt.touches[0].clientY 113 | } : { 114 | clientX: evt.clientX, 115 | clientY: evt.clientY 116 | }; 117 | }, 118 | eventMove: function eventMove(evt) { 119 | if (this.state.resizing) { 120 | this.onResize(evt); 121 | } else if (this.state.moving) { 122 | this.eventMoveBox(evt); 123 | } 124 | }, 125 | eventEnd: function eventEnd(evt) { 126 | if (this.state.resizing) { 127 | this.stopResize(evt); 128 | } else if (this.state.moving) { 129 | this.stopMove(evt); 130 | } 131 | }, 132 | 133 | 134 | // Resize methods 135 | startResize: function startResize(corner, event) { 136 | event.stopPropagation(); 137 | event.preventDefault(); 138 | this.setState({ 139 | resizing: true, 140 | corner: corner 141 | }); 142 | }, 143 | stopResize: function stopResize() { 144 | this.setState({ resizing: false }); 145 | }, 146 | 147 | 148 | // resize strategies 149 | nw: function nw(mousePos, boxPos) { 150 | var pos = _extends({}, this.state, { 151 | top: this.constrainBoundary(mousePos.clientY - boxPos.top), 152 | left: this.constrainBoundary(mousePos.clientX - boxPos.left) 153 | }); 154 | var dimensions = this.calculateDimensions(pos); 155 | 156 | var _preserveAspectRatio3 = this.preserveAspectRatio(dimensions.width, dimensions.height), 157 | _preserveAspectRatio4 = _slicedToArray(_preserveAspectRatio3, 2), 158 | width = _preserveAspectRatio4[0], 159 | height = _preserveAspectRatio4[1]; 160 | 161 | pos.top = this.props.height - pos.bottom - height; 162 | pos.left = this.props.width - pos.right - width; 163 | return pos; 164 | }, 165 | ne: function ne(mousePos, boxPos) { 166 | var pos = _extends({}, this.state, { 167 | top: this.constrainBoundary(mousePos.clientY - boxPos.top), 168 | right: this.constrainBoundary(boxPos.right - mousePos.clientX) 169 | }); 170 | var dimensions = this.calculateDimensions(pos); 171 | 172 | var _preserveAspectRatio5 = this.preserveAspectRatio(dimensions.width, dimensions.height), 173 | _preserveAspectRatio6 = _slicedToArray(_preserveAspectRatio5, 2), 174 | width = _preserveAspectRatio6[0], 175 | height = _preserveAspectRatio6[1]; 176 | 177 | pos.top = this.props.height - pos.bottom - height; 178 | pos.right = this.props.width - pos.left - width; 179 | return pos; 180 | }, 181 | se: function se(mousePos, boxPos) { 182 | var pos = _extends({}, this.state, { 183 | bottom: this.constrainBoundary(boxPos.bottom - mousePos.clientY), 184 | right: this.constrainBoundary(boxPos.right - mousePos.clientX) 185 | }); 186 | var dimensions = this.calculateDimensions(pos); 187 | 188 | var _preserveAspectRatio7 = this.preserveAspectRatio(dimensions.width, dimensions.height), 189 | _preserveAspectRatio8 = _slicedToArray(_preserveAspectRatio7, 2), 190 | width = _preserveAspectRatio8[0], 191 | height = _preserveAspectRatio8[1]; 192 | 193 | pos.bottom = this.props.height - pos.top - height; 194 | pos.right = this.props.width - pos.left - width; 195 | return pos; 196 | }, 197 | sw: function sw(mousePos, boxPos) { 198 | var pos = _extends({}, this.state, { 199 | bottom: this.constrainBoundary(boxPos.bottom - mousePos.clientY), 200 | left: this.constrainBoundary(mousePos.clientX - boxPos.left) 201 | }); 202 | var dimensions = this.calculateDimensions(pos); 203 | 204 | var _preserveAspectRatio9 = this.preserveAspectRatio(dimensions.width, dimensions.height), 205 | _preserveAspectRatio10 = _slicedToArray(_preserveAspectRatio9, 2), 206 | width = _preserveAspectRatio10[0], 207 | height = _preserveAspectRatio10[1]; 208 | 209 | pos.bottom = this.props.height - pos.top - height; 210 | pos.left = this.props.width - pos.right - width; 211 | return pos; 212 | }, 213 | onResize: function onResize(event) { 214 | var box = this.refs.box.parentElement.parentElement.getBoundingClientRect(); 215 | var coordinates = this.getClientCoordinates(event); 216 | var position = this[this.state.corner](coordinates, box); 217 | this.resize(position, coordinates); 218 | }, 219 | controlsResize: function controlsResize(event) { 220 | var box = this.refs.box.parentElement.parentElement.getBoundingClientRect(); 221 | var width = event.target.name === 'width' ? +event.target.value : +event.target.value * this.props.aspectRatio; 222 | var height = event.target.name === 'height' ? +event.target.value : +event.target.value / this.props.aspectRatio; 223 | var dimensions = this.preserveAspectRatio(width, height); 224 | width = dimensions[0]; 225 | height = dimensions[1]; 226 | 227 | if (width > box.width - this.state.left || height > box.height - this.state.top) return; 228 | 229 | var widthDifference = this.state.width - width; 230 | var heightDifference = this.state.height - height; 231 | var pos = _extends({}, this.state, { 232 | right: this.state.right + widthDifference, 233 | bottom: this.state.bottom + heightDifference 234 | }); 235 | var coordinates = { 236 | clientX: box.right - pos.right, 237 | clientY: box.bottom - pos.bottom 238 | }; 239 | 240 | this.resize(pos, coordinates); 241 | }, 242 | resize: function resize(position, coordinates) { 243 | var _this = this; 244 | 245 | var dimensions = this.calculateDimensions(position); 246 | var widthChanged = dimensions.width !== this.state.width, 247 | heightChanged = dimensions.height !== this.state.height; 248 | if (!widthChanged && !heightChanged) return; 249 | 250 | this.setState(_extends({}, coordinates, position, dimensions), function () { 251 | _this.props.onChange({ 252 | top: position.top, 253 | left: position.left 254 | }, dimensions); 255 | }); 256 | }, 257 | 258 | 259 | // Move methods 260 | startMove: function startMove(evt) { 261 | var _getClientCoordinates = this.getClientCoordinates(evt), 262 | clientX = _getClientCoordinates.clientX, 263 | clientY = _getClientCoordinates.clientY; 264 | 265 | this.setState({ 266 | moving: true, 267 | clientX: clientX, 268 | clientY: clientY 269 | }); 270 | }, 271 | stopMove: function stopMove(evt) { 272 | this.setState({ 273 | moving: false 274 | }); 275 | }, 276 | eventMoveBox: function eventMoveBox(evt) { 277 | evt.preventDefault(); 278 | 279 | var _getClientCoordinates2 = this.getClientCoordinates(evt), 280 | clientX = _getClientCoordinates2.clientX, 281 | clientY = _getClientCoordinates2.clientY; 282 | 283 | var movedX = clientX - this.state.clientX; 284 | var movedY = clientY - this.state.clientY; 285 | 286 | this.moveBox(clientX, clientY, movedX, movedY); 287 | }, 288 | controlsMoveBox: function controlsMoveBox(evt) { 289 | var movedX = evt.target.name === 'x' ? evt.target.value - this.state.left : 0; 290 | var movedY = evt.target.name === 'y' ? evt.target.value - this.state.top : 0; 291 | this.moveBox(0, 0, movedX, movedY); 292 | }, 293 | moveBox: function moveBox(clientX, clientY, movedX, movedY) { 294 | var _this2 = this; 295 | 296 | var position = { 297 | top: this.constrainBoundary(this.state.top + movedY), 298 | left: this.constrainBoundary(this.state.left + movedX), 299 | bottom: this.constrainBoundary(this.state.bottom - movedY), 300 | right: this.constrainBoundary(this.state.right - movedX) 301 | }; 302 | 303 | if (!position.top) { 304 | position.bottom = this.props.height - this.state.height; 305 | } 306 | if (!position.bottom) { 307 | position.top = this.props.height - this.state.height; 308 | } 309 | if (!position.left) { 310 | position.right = this.props.width - this.state.width; 311 | } 312 | if (!position.right) { 313 | position.left = this.props.width - this.state.width; 314 | } 315 | 316 | this.setState(_extends({}, { 317 | clientX: clientX, 318 | clientY: clientY 319 | }, position), function () { 320 | _this2.props.onChange({ 321 | top: position.top, 322 | left: position.left 323 | }, _this2.calculateDimensions(position)); 324 | }); 325 | }, 326 | keyboardResize: function keyboardResize(change) { 327 | if (this.state.right - change < 0) { 328 | return; 329 | } 330 | if (this.state.bottom - change < 0) { 331 | return; 332 | } 333 | 334 | var _preserveAspectRatio11 = this.preserveAspectRatio(this.state.width + change, this.state.height + change), 335 | _preserveAspectRatio12 = _slicedToArray(_preserveAspectRatio11, 2), 336 | width = _preserveAspectRatio12[0], 337 | height = _preserveAspectRatio12[1]; 338 | 339 | var widthChange = width - this.state.width; 340 | var heightChange = height - this.state.height; 341 | 342 | this.setState({ 343 | bottom: this.state.bottom - heightChange, 344 | right: this.state.right - widthChange, 345 | width: width, 346 | height: height 347 | }); 348 | }, 349 | handleKey: function handleKey(event) { 350 | // safari doesn't support event.key, so fall back to keyCode 351 | if (event.shiftKey) { 352 | if (event.key === 'ArrowUp' || event.keyCode === 38) { 353 | this.keyboardResize(-10); 354 | event.preventDefault(); 355 | } else if (event.key === 'ArrowDown' || event.keyCode === 40) { 356 | this.keyboardResize(10); 357 | event.preventDefault(); 358 | } else if (event.key === 'ArrowLeft' || event.keyCode === 37) { 359 | this.keyboardResize(-10); 360 | event.preventDefault(); 361 | } else if (event.key === 'ArrowRight' || event.keyCode === 39) { 362 | this.keyboardResize(10); 363 | event.preventDefault(); 364 | } 365 | } else { 366 | if (event.key === 'ArrowUp' || event.keyCode === 38) { 367 | this.moveBox(this.state.clientX, this.state.clientY, 0, -10); 368 | event.preventDefault(); 369 | } else if (event.key === 'ArrowDown' || event.keyCode === 40) { 370 | this.moveBox(this.state.clientX, this.state.clientY, 0, 10); 371 | event.preventDefault(); 372 | } else if (event.key === 'ArrowLeft' || event.keyCode === 37) { 373 | this.moveBox(this.state.clientX, this.state.clientY, -10, 0); 374 | event.preventDefault(); 375 | } else if (event.key === 'ArrowRight' || event.keyCode === 39) { 376 | this.moveBox(this.state.clientX, this.state.clientY, 10, 0); 377 | event.preventDefault(); 378 | } 379 | } 380 | }, 381 | render: function render() { 382 | var style = { 383 | position: 'absolute', 384 | top: this.state.top, 385 | left: this.state.left, 386 | right: this.state.right, 387 | bottom: this.state.bottom 388 | }; 389 | 390 | var _calculateDimensions = this.calculateDimensions(this.state), 391 | width = _calculateDimensions.width, 392 | height = _calculateDimensions.height; 393 | 394 | var topStyle = { 395 | height: this.state.top 396 | }; 397 | var bottomStyle = { 398 | height: this.state.bottom 399 | }; 400 | var leftStyle = { 401 | top: this.state.top, 402 | right: width + this.state.right, 403 | bottom: this.state.bottom 404 | }; 405 | var rightStyle = { 406 | top: this.state.top, 407 | left: width + this.state.left, 408 | bottom: this.state.bottom 409 | }; 410 | 411 | return _react2.default.createElement( 412 | 'div', 413 | { ref: 'box', className: 'DraggableResizable' }, 414 | _react2.default.createElement( 415 | 'div', 416 | { className: 'DraggableResizable-controls' }, 417 | _react2.default.createElement( 418 | 'label', 419 | null, 420 | this.props.offsetXLabel, 421 | _react2.default.createElement('input', { 422 | name: 'x', 423 | value: Math.round(this.state.left), 424 | onChange: this.controlsMoveBox, 425 | tabIndex: '-1', 426 | type: 'number' }) 427 | ), 428 | _react2.default.createElement( 429 | 'label', 430 | null, 431 | this.props.offsetYLabel, 432 | _react2.default.createElement('input', { 433 | name: 'y', 434 | value: Math.round(this.state.top), 435 | onChange: this.controlsMoveBox, 436 | tabIndex: '-1', 437 | type: 'number' }) 438 | ), 439 | _react2.default.createElement( 440 | 'label', 441 | null, 442 | this.props.widthLabel, 443 | _react2.default.createElement('input', { 444 | name: 'width', 445 | value: Math.round(width), 446 | type: 'number', 447 | tabIndex: '-1', 448 | onChange: this.controlsResize }) 449 | ), 450 | _react2.default.createElement( 451 | 'label', 452 | null, 453 | this.props.heightLabel, 454 | _react2.default.createElement('input', { 455 | value: Math.round(height), 456 | type: 'number', 457 | name: 'height', 458 | tabIndex: '-1', 459 | onChange: this.controlsResize }) 460 | ) 461 | ), 462 | _react2.default.createElement('div', { className: 'DraggableResizable-top', style: topStyle }), 463 | _react2.default.createElement('div', { className: 'DraggableResizable-left', style: leftStyle }), 464 | _react2.default.createElement( 465 | 'div', 466 | { style: style, onMouseDown: this.startMove, onTouchStart: this.startMove }, 467 | this.props.children, 468 | _react2.default.createElement('div', { className: 'resize-handle resize-handle-se', 469 | onMouseDown: this.startResize.bind(null, 'se'), 470 | onTouchStart: this.startResize.bind(null, 'se') }), 471 | _react2.default.createElement('div', { className: 'resize-handle resize-handle-ne', 472 | onMouseDown: this.startResize.bind(null, 'ne'), 473 | onTouchStart: this.startResize.bind(null, 'ne') }), 474 | _react2.default.createElement('div', { className: 'resize-handle resize-handle-sw', 475 | onMouseDown: this.startResize.bind(null, 'sw'), 476 | onTouchStart: this.startResize.bind(null, 'sw') }), 477 | _react2.default.createElement('div', { className: 'resize-handle resize-handle-nw', 478 | onMouseDown: this.startResize.bind(null, 'nw'), 479 | onTouchStart: this.startResize.bind(null, 'nw') }) 480 | ), 481 | _react2.default.createElement('div', { className: 'DraggableResizable-right', style: rightStyle }), 482 | _react2.default.createElement('div', { className: 'DraggableResizable-bottom', style: bottomStyle }) 483 | ); 484 | } 485 | }); 486 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _react = require('react'); 8 | 9 | var _react2 = _interopRequireDefault(_react); 10 | 11 | var _draggableResizableBox = require('./draggable-resizable-box'); 12 | 13 | var _draggableResizableBox2 = _interopRequireDefault(_draggableResizableBox); 14 | 15 | var _dataUriToBlob = require('data-uri-to-blob'); 16 | 17 | var _dataUriToBlob2 = _interopRequireDefault(_dataUriToBlob); 18 | 19 | require('./cropper.css'); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | exports.default = _react2.default.createClass({ 24 | displayName: 'Cropper', 25 | 26 | propTypes: { 27 | width: _react2.default.PropTypes.number.isRequired, 28 | height: _react2.default.PropTypes.number.isRequired, 29 | center: _react2.default.PropTypes.bool, 30 | image: _react2.default.PropTypes.any, 31 | widthLabel: _react2.default.PropTypes.string, 32 | heightLabel: _react2.default.PropTypes.string, 33 | offsetXLabel: _react2.default.PropTypes.string, 34 | offsetYLabel: _react2.default.PropTypes.string, 35 | onImageLoaded: _react2.default.PropTypes.func, 36 | minConstraints: _react2.default.PropTypes.arrayOf(_react2.default.PropTypes.number) 37 | }, 38 | 39 | getDefaultProps: function getDefaultProps() { 40 | return { 41 | center: false, 42 | width: 'Width', 43 | height: 'Height', 44 | offsetXLabel: 'Offset X', 45 | offsetYLabel: 'Offset Y' 46 | }; 47 | }, 48 | getInitialState: function getInitialState() { 49 | return { 50 | imageLoaded: false, 51 | width: this.props.width, 52 | height: this.props.height, 53 | url: window.URL.createObjectURL(this.props.image) 54 | }; 55 | }, 56 | componentWillReceiveProps: function componentWillReceiveProps(nextProps) { 57 | if (this.props.image !== nextProps.image) { 58 | this.setState({ 59 | url: window.URL.createObjectURL(nextProps.image), 60 | imageLoaded: false 61 | }); 62 | } 63 | }, 64 | shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) { 65 | var image = this.props.image; 66 | 67 | return nextProps.image.size !== image.size || nextProps.image.name !== image.name || nextProps.image.type !== image.type || nextState.imageLoaded !== this.state.imageLoaded; 68 | }, 69 | onLoad: function onLoad(evt) { 70 | var _this = this; 71 | 72 | var box = this.refs.box.getBoundingClientRect(); 73 | this.setState({ 74 | imageLoaded: true, 75 | width: box.width, 76 | height: box.height 77 | }, function () { 78 | var img = _this.refs.image; 79 | _this.props.onImageLoaded && _this.props.onImageLoaded(img); 80 | }); 81 | }, 82 | cropImage: function cropImage() { 83 | var _this2 = this; 84 | 85 | return new Promise(function (resolve, reject) { 86 | var img = new Image(); 87 | img.onload = function () { 88 | var canvas = _this2.refs.canvas; 89 | var img = _this2.refs.image; 90 | var ctx = canvas.getContext('2d'); 91 | var xScale = img.naturalWidth / _this2.state.width, 92 | yScale = img.naturalHeight / _this2.state.height; 93 | 94 | 95 | var imageOffsetX = xScale < 1 ? 0 : _this2.state.offset.left * xScale; 96 | var imageOffsetY = yScale < 1 ? 0 : _this2.state.offset.top * yScale; 97 | var imageWidth = xScale < 1 ? img.naturalWidth : _this2.state.dimensions.width * xScale; 98 | var imageHeight = yScale < 1 ? img.naturalHeight : _this2.state.dimensions.height * yScale; 99 | 100 | var canvasOffsetX = xScale < 1 ? Math.floor((_this2.state.dimensions.width - img.naturalWidth) / 2) : 0; 101 | var canvasOffsetY = yScale < 1 ? Math.floor((_this2.state.dimensions.height - img.naturalHeight) / 2) : 0; 102 | var canvasWidth = xScale < 1 ? img.naturalWidth : _this2.props.width; 103 | var canvasHeight = yScale < 1 ? img.naturalHeight : _this2.props.height; 104 | 105 | ctx.clearRect(0, 0, _this2.props.width, _this2.props.height); 106 | ctx.drawImage(img, imageOffsetX, imageOffsetY, imageWidth, imageHeight, canvasOffsetX, canvasOffsetY, canvasWidth, canvasHeight); 107 | resolve((0, _dataUriToBlob2.default)(canvas.toDataURL())); 108 | }; 109 | img.src = window.URL.createObjectURL(_this2.props.image); 110 | }); 111 | }, 112 | onChange: function onChange(offset, dimensions) { 113 | this.setState({ offset: offset, dimensions: dimensions }); 114 | }, 115 | render: function render() { 116 | return _react2.default.createElement( 117 | 'div', 118 | { 119 | ref: 'box', 120 | className: 'Cropper', 121 | style: { 122 | minWidth: this.props.width, 123 | minHeight: this.props.height 124 | } }, 125 | _react2.default.createElement('canvas', { 126 | className: 'Cropper-canvas', 127 | ref: 'canvas', 128 | width: this.props.width, 129 | height: this.props.height }), 130 | _react2.default.createElement('img', { 131 | ref: 'image', 132 | src: this.state.url, 133 | className: 'Cropper-image', 134 | onLoad: this.onLoad, 135 | style: { top: this.state.height / 2 } }), 136 | this.state.imageLoaded && _react2.default.createElement( 137 | 'div', 138 | { className: 'box' }, 139 | _react2.default.createElement( 140 | _draggableResizableBox2.default, 141 | { 142 | aspectRatio: this.props.width / this.props.height, 143 | width: this.state.width, 144 | height: this.state.height, 145 | minConstraints: this.props.minConstraints, 146 | onChange: this.onChange, 147 | widthLabel: this.props.widthLabel, 148 | heightLabel: this.props.heightLabel, 149 | offsetXLabel: this.props.offsetXLabel, 150 | offsetYLabel: this.props.offsetYLabel }, 151 | _react2.default.createElement('div', { className: 'Cropper-box' }) 152 | ) 153 | ) 154 | ); 155 | } 156 | }); 157 | -------------------------------------------------------------------------------- /lib/cropper.css: -------------------------------------------------------------------------------- 1 | .Cropper { 2 | position: relative; 3 | display: inline-block; 4 | max-width:100%; 5 | max-height:100%; 6 | } 7 | 8 | .box { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | bottom: 0; 13 | right: 0; 14 | } 15 | 16 | .Cropper-box { 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | bottom: 0; 21 | right: 0; 22 | cursor: move; 23 | border: #fff solid 1px; 24 | } 25 | 26 | .Cropper-canvas { 27 | visibility: hidden; 28 | position: absolute; 29 | } 30 | 31 | .Cropper-image { 32 | vertical-align: middle; 33 | max-width: 100%; 34 | position: relative; 35 | transform: translate(-50%, -50%); 36 | left: 50%; 37 | } 38 | 39 | .resize-handle { 40 | position: absolute; 41 | background-color: #ECEEEF; 42 | border: #8295AB solid 1px; 43 | width: 13px; 44 | height: 13px; 45 | z-index: 1; 46 | } 47 | 48 | .resize-handle-se { 49 | bottom: 0; 50 | right: 0; 51 | cursor: nwse-resize; 52 | transform: translate(50%, 50%); 53 | } 54 | .resize-handle-ne { 55 | right: 0; 56 | top: 0; 57 | cursor: nesw-resize; 58 | transform: translate(50%, -50%); 59 | } 60 | .resize-handle-sw { 61 | bottom: 0; 62 | left: 0; 63 | cursor: nesw-resize; 64 | transform: translate(-50%, 50%); 65 | } 66 | .resize-handle-nw { 67 | top: 0; 68 | bottom: 0; 69 | cursor: nwse-resize; 70 | transform: translate(-50%, -50%); 71 | } 72 | 73 | .DraggableResizable { 74 | position: relative; 75 | width: 100%; 76 | height: 100%; 77 | } 78 | 79 | .DraggableResizable-controls { 80 | border: 0; 81 | clip: rect(0 0 0 0); 82 | height: 1px; 83 | margin: -1px; 84 | overflow: hidden; 85 | padding: 0; 86 | position: absolute; 87 | width: 1px; 88 | } 89 | 90 | .DraggableResizable-top, 91 | .DraggableResizable-left, 92 | .DraggableResizable-bottom, 93 | .DraggableResizable-right { 94 | position: absolute; 95 | background-color: rgba(0,0,0,.7); 96 | } 97 | 98 | .DraggableResizable-top { 99 | top: 0; 100 | left: 0; 101 | right: 0; 102 | } 103 | .DraggableResizable-bottom { 104 | bottom: 0; 105 | left: 0; 106 | right: 0; 107 | } 108 | .DraggableResizable-left { 109 | left: 0; 110 | } 111 | .DraggableResizable-right { 112 | right: 0; 113 | } 114 | -------------------------------------------------------------------------------- /lib/draggable-resizable-box.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default React.createClass({ 4 | displayName: 'DraggableResizableBox', 5 | 6 | propTypes: { 7 | aspectRatio: React.PropTypes.number.isRequired, 8 | width: React.PropTypes.number.isRequired, 9 | height: React.PropTypes.number.isRequired, 10 | onChange: React.PropTypes.func, 11 | offset: React.PropTypes.array, 12 | minConstraints: React.PropTypes.array, 13 | children: React.PropTypes.node, 14 | widthLabel: React.PropTypes.string, 15 | heightLabel: React.PropTypes.string, 16 | offsetXLabel: React.PropTypes.string, 17 | offsetYLabel: React.PropTypes.string 18 | }, 19 | 20 | getDefaultProps () { 21 | return { 22 | widthLabel: 'Width', 23 | heightLabel: 'Height', 24 | offsetXLabel: 'Offset X', 25 | offsetYLabel: 'Offset Y' 26 | } 27 | }, 28 | 29 | getInitialState () { 30 | let [width, height] = this.preserveAspectRatio(this.props.width, this.props.height) 31 | let centerYOffset = (this.props.height - height) / 2 32 | let centerXOffset = (this.props.width - width) / 2 33 | return { 34 | top: centerYOffset, 35 | left: centerXOffset, 36 | bottom: centerYOffset, 37 | right: centerXOffset, 38 | width: width, 39 | height: height 40 | } 41 | }, 42 | 43 | componentDidMount () { 44 | document.addEventListener('mousemove', this.eventMove) 45 | document.addEventListener('mouseup', this.eventEnd) 46 | document.addEventListener('touchmove', this.eventMove) 47 | document.addEventListener('touchend', this.eventEnd) 48 | document.addEventListener('keydown', this.handleKey) 49 | this.props.onChange({ 50 | top: this.state.top, 51 | left: this.state.left 52 | }, { 53 | width: this.state.width, 54 | height: this.state.height 55 | }) 56 | }, 57 | 58 | componentWillUnmount () { 59 | document.removeEventListener('mousemove', this.eventMove) 60 | document.removeEventListener('mouseup', this.eventEnd) 61 | document.removeEventListener('touchmove', this.eventMove) 62 | document.removeEventListener('touchend', this.eventEnd) 63 | document.removeEventListener('keydown', this.handleKey) 64 | }, 65 | 66 | calculateDimensions ({top, left, bottom, right}) { 67 | return {width: this.props.width - left - right, height: this.props.height - top - bottom} 68 | }, 69 | 70 | // If you do this, be careful of constraints 71 | preserveAspectRatio (width, height) { 72 | if(this.props.minConstraints) { 73 | width = Math.max(width, this.props.minConstraints[0]) 74 | height = Math.max(height, this.props.minConstraints[1]) 75 | } 76 | const currentAspectRatio = width / height 77 | 78 | if (currentAspectRatio < this.props.aspectRatio) { 79 | return [width, width / this.props.aspectRatio] 80 | } else if (currentAspectRatio > this.props.aspectRatio) { 81 | return [height * this.props.aspectRatio, height] 82 | } else { 83 | return [width, height] 84 | } 85 | }, 86 | 87 | constrainBoundary (side) { 88 | return side < 0 ? 0 : side 89 | }, 90 | 91 | getClientCoordinates (evt) { 92 | return evt.touches ? { 93 | clientX: evt.touches[0].clientX, 94 | clientY: evt.touches[0].clientY 95 | } : 96 | { 97 | clientX: evt.clientX, 98 | clientY: evt.clientY 99 | } 100 | }, 101 | 102 | eventMove (evt) { 103 | if (this.state.resizing) { 104 | this.onResize(evt) 105 | } else if (this.state.moving) { 106 | this.eventMoveBox(evt) 107 | } 108 | }, 109 | 110 | eventEnd (evt) { 111 | if (this.state.resizing) { 112 | this.stopResize(evt) 113 | } else if (this.state.moving) { 114 | this.stopMove(evt) 115 | } 116 | }, 117 | 118 | // Resize methods 119 | startResize (corner, event) { 120 | event.stopPropagation() 121 | event.preventDefault() 122 | this.setState({ 123 | resizing: true, 124 | corner 125 | }) 126 | }, 127 | 128 | stopResize () { 129 | this.setState({resizing: false}) 130 | }, 131 | 132 | // resize strategies 133 | nw (mousePos, boxPos) { 134 | let pos = Object.assign({}, this.state, { 135 | top: this.constrainBoundary(mousePos.clientY - boxPos.top), 136 | left: this.constrainBoundary(mousePos.clientX - boxPos.left) 137 | }) 138 | let dimensions = this.calculateDimensions(pos) 139 | let [width, height] = this.preserveAspectRatio(dimensions.width, dimensions.height) 140 | pos.top = this.props.height - pos.bottom - height 141 | pos.left = this.props.width - pos.right - width 142 | return pos 143 | }, 144 | ne (mousePos, boxPos) { 145 | let pos = Object.assign({}, this.state, { 146 | top: this.constrainBoundary(mousePos.clientY - boxPos.top), 147 | right: this.constrainBoundary(boxPos.right - mousePos.clientX) 148 | }) 149 | let dimensions = this.calculateDimensions(pos) 150 | let [width, height] = this.preserveAspectRatio(dimensions.width, dimensions.height) 151 | pos.top = this.props.height - pos.bottom - height 152 | pos.right = this.props.width - pos.left - width 153 | return pos 154 | }, 155 | se (mousePos, boxPos) { 156 | let pos = Object.assign({}, this.state, { 157 | bottom: this.constrainBoundary(boxPos.bottom - mousePos.clientY), 158 | right: this.constrainBoundary(boxPos.right - mousePos.clientX) 159 | }) 160 | let dimensions = this.calculateDimensions(pos) 161 | let [width, height] = this.preserveAspectRatio(dimensions.width, dimensions.height) 162 | pos.bottom = this.props.height - pos.top - height 163 | pos.right = this.props.width - pos.left - width 164 | return pos 165 | }, 166 | sw (mousePos, boxPos) { 167 | let pos = Object.assign({}, this.state, { 168 | bottom: this.constrainBoundary(boxPos.bottom - mousePos.clientY), 169 | left: this.constrainBoundary(mousePos.clientX - boxPos.left) 170 | }) 171 | let dimensions = this.calculateDimensions(pos) 172 | let [width, height] = this.preserveAspectRatio(dimensions.width, dimensions.height) 173 | pos.bottom = this.props.height - pos.top - height 174 | pos.left = this.props.width - pos.right - width 175 | return pos 176 | }, 177 | 178 | onResize (event) { 179 | let box = this.refs.box.parentElement.parentElement.getBoundingClientRect() 180 | let coordinates = this.getClientCoordinates(event) 181 | let position = this[this.state.corner](coordinates, box) 182 | this.resize(position, coordinates) 183 | }, 184 | 185 | controlsResize (event) { 186 | let box = this.refs.box.parentElement.parentElement.getBoundingClientRect() 187 | let width = event.target.name === 'width' ? +event.target.value : +event.target.value * this.props.aspectRatio 188 | let height = event.target.name === 'height' ? +event.target.value : +event.target.value / this.props.aspectRatio 189 | let dimensions = this.preserveAspectRatio(width, height) 190 | width = dimensions[0] 191 | height = dimensions[1] 192 | 193 | if (width > box.width - this.state.left || 194 | height > box.height - this.state.top) return 195 | 196 | let widthDifference = this.state.width - width 197 | let heightDifference = this.state.height - height 198 | let pos = Object.assign({}, this.state, { 199 | right: this.state.right + widthDifference, 200 | bottom: this.state.bottom + heightDifference 201 | }) 202 | let coordinates = { 203 | clientX: box.right - pos.right, 204 | clientY: box.bottom - pos.bottom 205 | } 206 | 207 | this.resize(pos, coordinates) 208 | }, 209 | 210 | resize (position, coordinates) { 211 | let dimensions = this.calculateDimensions(position) 212 | var widthChanged = dimensions.width !== this.state.width, heightChanged = dimensions.height !== this.state.height 213 | if (!widthChanged && !heightChanged) return 214 | 215 | this.setState(Object.assign({}, coordinates, position, dimensions), () => { 216 | this.props.onChange({ 217 | top: position.top, 218 | left: position.left 219 | }, dimensions) 220 | }) 221 | }, 222 | 223 | // Move methods 224 | startMove (evt) { 225 | let {clientX, clientY} = this.getClientCoordinates(evt) 226 | this.setState({ 227 | moving: true, 228 | clientX: clientX, 229 | clientY: clientY 230 | }) 231 | }, 232 | 233 | stopMove (evt) { 234 | this.setState({ 235 | moving: false 236 | }) 237 | }, 238 | 239 | eventMoveBox (evt) { 240 | evt.preventDefault() 241 | let {clientX, clientY} = this.getClientCoordinates(evt) 242 | let movedX = clientX - this.state.clientX 243 | let movedY = clientY - this.state.clientY 244 | 245 | this.moveBox(clientX, clientY, movedX, movedY) 246 | }, 247 | 248 | controlsMoveBox (evt) { 249 | let movedX = evt.target.name === 'x' ? evt.target.value - this.state.left : 0 250 | let movedY = evt.target.name === 'y' ? evt.target.value - this.state.top : 0 251 | this.moveBox(0, 0, movedX, movedY) 252 | }, 253 | 254 | moveBox (clientX, clientY, movedX, movedY) { 255 | let position = { 256 | top: this.constrainBoundary(this.state.top + movedY), 257 | left: this.constrainBoundary(this.state.left + movedX), 258 | bottom: this.constrainBoundary(this.state.bottom - movedY), 259 | right: this.constrainBoundary(this.state.right - movedX) 260 | } 261 | 262 | if (!position.top) { 263 | position.bottom = this.props.height - this.state.height 264 | } 265 | if (!position.bottom) { 266 | position.top = this.props.height - this.state.height 267 | } 268 | if (!position.left) { 269 | position.right = this.props.width - this.state.width 270 | } 271 | if (!position.right) { 272 | position.left = this.props.width - this.state.width 273 | } 274 | 275 | this.setState(Object.assign({}, { 276 | clientX: clientX, 277 | clientY: clientY 278 | }, position), () => { 279 | this.props.onChange({ 280 | top: position.top, 281 | left: position.left 282 | }, this.calculateDimensions(position)) 283 | }) 284 | }, 285 | 286 | keyboardResize (change) { 287 | if (this.state.right - change < 0) { return } 288 | if (this.state.bottom - change < 0) { return } 289 | 290 | const [width, height] = this.preserveAspectRatio( 291 | this.state.width + change, 292 | this.state.height + change 293 | ) 294 | const widthChange = width - this.state.width 295 | const heightChange = height - this.state.height 296 | 297 | this.setState({ 298 | bottom: this.state.bottom - heightChange, 299 | right: this.state.right - widthChange, 300 | width, 301 | height 302 | }) 303 | }, 304 | 305 | handleKey (event) { 306 | // safari doesn't support event.key, so fall back to keyCode 307 | if (event.shiftKey) { 308 | if (event.key === 'ArrowUp' || event.keyCode === 38) { 309 | this.keyboardResize(-10) 310 | event.preventDefault() 311 | } else if (event.key === 'ArrowDown' || event.keyCode === 40) { 312 | this.keyboardResize(10) 313 | event.preventDefault() 314 | } else if (event.key === 'ArrowLeft' || event.keyCode === 37) { 315 | this.keyboardResize(-10) 316 | event.preventDefault() 317 | } else if (event.key === 'ArrowRight' || event.keyCode === 39) { 318 | this.keyboardResize(10) 319 | event.preventDefault() 320 | } 321 | } else { 322 | if (event.key === 'ArrowUp' || event.keyCode === 38) { 323 | this.moveBox(this.state.clientX, this.state.clientY, 0, -10) 324 | event.preventDefault() 325 | } else if (event.key === 'ArrowDown' || event.keyCode === 40) { 326 | this.moveBox(this.state.clientX, this.state.clientY, 0, 10) 327 | event.preventDefault() 328 | } else if (event.key === 'ArrowLeft' || event.keyCode === 37) { 329 | this.moveBox(this.state.clientX, this.state.clientY, -10, 0) 330 | event.preventDefault() 331 | } else if (event.key === 'ArrowRight' || event.keyCode === 39) { 332 | this.moveBox(this.state.clientX, this.state.clientY, 10, 0) 333 | event.preventDefault() 334 | } 335 | } 336 | }, 337 | 338 | render () { 339 | let style = { 340 | position: 'absolute', 341 | top: this.state.top, 342 | left: this.state.left, 343 | right: this.state.right, 344 | bottom: this.state.bottom 345 | } 346 | let {width, height} = this.calculateDimensions(this.state) 347 | let topStyle = { 348 | height: this.state.top 349 | } 350 | let bottomStyle = { 351 | height: this.state.bottom 352 | } 353 | let leftStyle = { 354 | top: this.state.top, 355 | right: width + this.state.right, 356 | bottom: this.state.bottom 357 | } 358 | let rightStyle = { 359 | top: this.state.top, 360 | left: width + this.state.left, 361 | bottom: this.state.bottom 362 | } 363 | 364 | return ( 365 |
366 |
367 | 376 | 385 | 394 | 403 |
404 |
405 |
406 |
407 | {this.props.children} 408 |
411 |
412 |
415 |
416 |
419 |
420 |
423 |
424 |
425 |
426 |
427 |
428 | ) 429 | } 430 | }) 431 | -------------------------------------------------------------------------------- /lib/index.src.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import DraggableResizableBox from './draggable-resizable-box' 3 | import toBlob from 'data-uri-to-blob' 4 | import './cropper.css' 5 | 6 | export default React.createClass({ 7 | displayName: 'Cropper', 8 | 9 | propTypes: { 10 | width: React.PropTypes.number.isRequired, 11 | height: React.PropTypes.number.isRequired, 12 | center: React.PropTypes.bool, 13 | image: React.PropTypes.any, 14 | widthLabel: React.PropTypes.string, 15 | heightLabel: React.PropTypes.string, 16 | offsetXLabel: React.PropTypes.string, 17 | offsetYLabel: React.PropTypes.string, 18 | onImageLoaded: React.PropTypes.func, 19 | minConstraints: React.PropTypes.arrayOf(React.PropTypes.number) 20 | }, 21 | 22 | getDefaultProps () { 23 | return { 24 | center: false, 25 | width: 'Width', 26 | height: 'Height', 27 | offsetXLabel: 'Offset X', 28 | offsetYLabel: 'Offset Y' 29 | } 30 | }, 31 | 32 | getInitialState () { 33 | return { 34 | imageLoaded: false, 35 | width: this.props.width, 36 | height: this.props.height, 37 | url: window.URL.createObjectURL(this.props.image) 38 | } 39 | }, 40 | 41 | componentWillReceiveProps (nextProps) { 42 | if (this.props.image !== nextProps.image) { 43 | this.setState({ 44 | url: window.URL.createObjectURL(nextProps.image), 45 | imageLoaded: false 46 | }) 47 | } 48 | }, 49 | 50 | shouldComponentUpdate (nextProps, nextState) { 51 | let {image} = this.props 52 | return nextProps.image.size !== image.size || 53 | nextProps.image.name !== image.name || 54 | nextProps.image.type !== image.type || 55 | nextState.imageLoaded !== this.state.imageLoaded 56 | }, 57 | 58 | onLoad (evt) { 59 | let box = this.refs.box.getBoundingClientRect() 60 | this.setState({ 61 | imageLoaded: true, 62 | width: box.width, 63 | height: box.height 64 | }, () => { 65 | let img = this.refs.image 66 | this.props.onImageLoaded && this.props.onImageLoaded(img) 67 | }) 68 | }, 69 | 70 | cropImage () { 71 | return new Promise((resolve, reject) => { 72 | let img = new Image() 73 | img.onload = () => { 74 | let canvas = this.refs.canvas 75 | let img = this.refs.image 76 | let ctx = canvas.getContext('2d') 77 | let [xScale, yScale] = [img.naturalWidth / this.state.width, 78 | img.naturalHeight / this.state.height] 79 | 80 | let imageOffsetX = xScale < 1 ? 0 : this.state.offset.left * xScale 81 | let imageOffsetY = yScale < 1 ? 0 : this.state.offset.top * yScale 82 | let imageWidth = xScale < 1 ? img.naturalWidth : this.state.dimensions.width * xScale 83 | let imageHeight = yScale < 1 ? img.naturalHeight : this.state.dimensions.height * yScale 84 | 85 | let canvasOffsetX = xScale < 1 ? Math.floor((this.state.dimensions.width - img.naturalWidth) / 2) : 0 86 | let canvasOffsetY = yScale < 1 ? Math.floor((this.state.dimensions.height - img.naturalHeight) / 2) : 0 87 | let canvasWidth = xScale < 1 ? img.naturalWidth : this.props.width 88 | let canvasHeight = yScale < 1 ? img.naturalHeight : this.props.height 89 | 90 | ctx.clearRect(0, 0, this.props.width, this.props.height) 91 | ctx.drawImage(img, imageOffsetX, imageOffsetY, imageWidth, imageHeight, canvasOffsetX, canvasOffsetY, canvasWidth, canvasHeight) 92 | resolve(toBlob(canvas.toDataURL())) 93 | } 94 | img.src = window.URL.createObjectURL(this.props.image) 95 | }) 96 | }, 97 | 98 | onChange (offset, dimensions) { 99 | this.setState({offset, dimensions}) 100 | }, 101 | 102 | render () { 103 | return ( 104 |
111 | 116 | 117 | 123 | {this.state.imageLoaded && 124 |
125 | 135 |
136 |
137 |
} 138 |
139 | ) 140 | } 141 | }) 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-crop", 3 | "version": "4.0.2", 4 | "description": "An image cropper that moves the cropper rather than the image", 5 | "repository": "https://github.com/instructure-react/react-crop", 6 | "main": "index.js", 7 | "scripts": { 8 | "build": "NODE_ENV=production webpack --config webpack.dist.config.js", 9 | "compile-index": "cp lib/cropper.css . && babel lib/index.src.js --out-file index.js", 10 | "compile-box": "babel lib/draggable-resizable-box.js --out-file draggable-resizable-box.js", 11 | "compile": "npm run compile-index && npm run compile-box && webpack", 12 | "docs": "webpack-dev-server --content-base docs/", 13 | "prepublish": "npm run compile && npm run build" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "image", 18 | "crop", 19 | "react-component" 20 | ], 21 | "author": "Matthew Sessions (http://www.matthewsessions.com/)", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "babel-cli": "^6.9.0", 25 | "babel-core": "^6.9.0", 26 | "babel-loader": "^6.2.4", 27 | "babel-plugin-transform-object-assign": "^6.8.0", 28 | "babel-polyfill": "^6.20.0", 29 | "babel-preset-es2015": "^6.9.0", 30 | "babel-preset-react": "^6.5.0", 31 | "babel-preset-stage-1": "^6.5.0", 32 | "css-loader": "^0.26.1", 33 | "node-libs-browser": "0.5.2", 34 | "react": "^15.4.1", 35 | "react-dom": "15.4.1", 36 | "style-loader": "^0.13.1", 37 | "webpack": "1.12.0", 38 | "webpack-dev-server": "1.10.1" 39 | }, 40 | "dependencies": { 41 | "autoprefixer": "5.2.0", 42 | "data-uri-to-blob": "0.0.4", 43 | "postcss-cli": "2.0.0" 44 | }, 45 | "peerDependencies": { 46 | "react": ">=0.14.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | entry: ['./docs/index.js', 'babel-polyfill'], 5 | output: { 6 | path: './docs', 7 | filename: 'bundle.js' 8 | }, 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.js?$/, 13 | exclude: /node_modules/, 14 | loader: 'babel' 15 | }, 16 | { 17 | test: /\.css$/, 18 | loader: 'style-loader!css-loader' 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /webpack.dist.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin; 3 | 4 | module.exports = { 5 | 6 | entry: { 7 | 'react-crop': './lib/index.src.js', 8 | 'react-crop.min': './lib/index.src.js' 9 | }, 10 | 11 | externals: [ 12 | 'react', 13 | 'react-dom' 14 | ], 15 | 16 | output: { 17 | filename: '[name].js', 18 | chunkFilename: '[id].chunk.js', 19 | path: 'dist', 20 | publicPath: '/', 21 | libraryTarget: 'umd', 22 | library: 'ReactCrop' 23 | }, 24 | 25 | devtool: 'source-map', 26 | 27 | plugins: [ 28 | new webpack.DefinePlugin({ 29 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 30 | }), 31 | new UglifyJsPlugin({ 32 | include: /\.min\.js$/, 33 | minimize: true, 34 | compress: { 35 | warnings: false 36 | } 37 | }) 38 | ], 39 | 40 | module: { 41 | loaders: [ 42 | { test: /\.js?$/, exclude: /node_modules/, loader: 'babel'}, 43 | { 44 | test: /\.css$/, 45 | loader: 'style-loader!css-loader' 46 | } 47 | ] 48 | } 49 | 50 | }; 51 | --------------------------------------------------------------------------------