├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── component └── Cropper.js ├── demo ├── demo.js └── demo.less ├── dist ├── app.js └── image │ └── demo.jpg ├── index.html ├── index.js ├── lib └── Cropper.js ├── package-lock.json ├── package.json ├── server.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", 'react'] 3 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": [ 3 | "standard", 4 | "plugin:react/recommended" 5 | ], 6 | "rules": { 7 | "react/no-find-dom-node": "warn" 8 | } 9 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .log 3 | npm-debug.log* 4 | test 5 | .DS_Store -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "asi": false, 4 | "boss": false, 5 | "camelcase": false, 6 | "curly": false, 7 | "eqeqeq": false, 8 | "eqnull": false, 9 | "es5": false, 10 | "evil": false, 11 | "expr": true, 12 | "forin": false, 13 | "funcscope": false, 14 | "immed": false, 15 | "indent": 2, 16 | "latedef": false, 17 | "loopfunc": false, 18 | "maxerr": 7, 19 | "newcap": false, 20 | "node": true, 21 | "nonew": false, 22 | "plusplus": false, 23 | "quotmark": "false", 24 | "regexdash": false, 25 | "shadow": false, 26 | "strict": false, 27 | "sub": false, 28 | "supernew": false, 29 | "trailing": false, 30 | "undef": false, 31 | "unused": false, 32 | "white": false 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 jerry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # collaborators wanted 3 | I have barely no time work on improve this project, needs collaborators for project maintenance. 4 | 5 | ## React Image Cropper 6 | 7 | [![Downloads](https://img.shields.io/npm/dt/react-image-cropper.svg)](https://www.npmjs.com/package/react-image-cropper) 8 | [![Version](https://img.shields.io/npm/v/react-image-cropper.svg)](https://www.npmjs.com/package/react-image-cropper) 9 | 10 | A React.JS Image Cropper 11 | Touch supported 12 | 13 | **[See the demo](http://braavos.me/react-image-cropper/)** 14 | 15 | Custom: 16 | 17 | + initial cropper frame position 18 | + frame width, height, ratio 19 | + crop event 20 | 21 | ### Hot to Use 22 | 23 | + `import {Cropper} from 'react-image-cropper'` 24 | 25 | + styles are all inline 26 | 27 | + define Cropper with src, and ref to execute crop method 28 | 29 | ``` 30 | { this.cropper = ref }} 33 | /> 34 | ``` 35 | 36 | + crop and get image url 37 | 38 | `image.src = this.cropper.crop()` 39 | 40 | + get crop values: 41 | 42 | `const values = this.cropper.values()` 43 | 44 | values: 45 | 46 | ``` 47 | { 48 | // display values 49 | display: { 50 | width, // frame width 51 | height, // frame height 52 | x, // original x position 53 | y, // original y position 54 | imgWidth, // img width 55 | imgHeight, // img height 56 | }, 57 | // original values 58 | original: { 59 | width, // frame width 60 | height, // frame height 61 | x, // original x position 62 | y, // original y position 63 | imgWidth, // img width 64 | imgHeight, // img height 65 | } 66 | } 67 | ``` 68 | 69 | 70 | + onChange for preview 71 | 72 | (values) => onChange(values) 73 | 74 | + custom use 75 | 76 | | prop | value | 77 | |:-------:|:--------| 78 | | ratio | width / height | 79 | | width | cropper frame width | 80 | | height | cropper frame height | 81 | | originX | cropper original position(x axis), accroding to image left| 82 | | originY | cropper original position(Y axis), accroding to image top| 83 | | fixedRatio | turn on/off fixed ratio (bool default true) | 84 | | allowNewSelection | allow user to create a new selection instead of reusing initial selection (bool default true) | 85 | | styles | specify styles to override inline styles | 86 | | onImgLoad | specify fuction callback to run when the image completed loading | 87 | | beforeImgload | specify function callback to run when the image size value is ready but image is not completed loading | 88 | | onChange | triggred when dragging stop, get values of cropper | 89 | 90 | 91 | **[See the demo](http://braavos.me/react-image-cropper/)** 92 | 93 | -------------------------------------------------------------------------------- /component/Cropper.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { Component } = React 3 | const ReactDOM = require('react-dom') 4 | const deepExtend = require('deep-extend') 5 | const PropTypes = require('prop-types') 6 | const { findDOMNode } = ReactDOM 7 | 8 | class Cropper extends Component { 9 | constructor (props) { 10 | super(props) 11 | const { 12 | originX, 13 | originY, 14 | width, 15 | height, 16 | fixedRatio, 17 | ratio, 18 | styles, 19 | src 20 | } = props 21 | 22 | this.state = { 23 | // image and clone image src 24 | src, 25 | // background image width 26 | imgWidth: '100%', 27 | // background image height 28 | imgHeight: 'auto', 29 | // cropper width, drag trigger changing 30 | frameWidth4Style: width, 31 | // cropper height, drag trigger changing 32 | frameHeight4Style: fixedRatio ? (width / ratio) : height, 33 | // cropper height, drag trigger changing 34 | toImgTop4Style: 0, 35 | toImgLeft4Style: 0, 36 | // cropper original position(x axis), accroding to image left 37 | originX, 38 | // cropper original position(y axis), accroding to image top 39 | originY, 40 | // dragging start, position's pageX and pageY 41 | startPageX: 0, 42 | startPageY: 0, 43 | // frame width, change only dragging stop 44 | frameWidth: width, 45 | // frame height, change only dragging stop 46 | frameHeight: fixedRatio ? (width / ratio) : height, 47 | dragging: false, 48 | maxLeft: 0, 49 | maxTop: 0, 50 | action: null, 51 | imgLoaded: false, 52 | styles: deepExtend({}, defaultStyles, styles) 53 | } 54 | } 55 | 56 | // initialize style, component did mount or component updated. 57 | initStyles () { 58 | const container = findDOMNode(this.container) 59 | this.setState({ 60 | imgWidth: container.offsetWidth 61 | }, () => { 62 | // calc frame width height 63 | let { 64 | originX, 65 | originY, 66 | disabled 67 | } = this.props 68 | 69 | if (disabled) return 70 | 71 | const { 72 | imgWidth, 73 | imgHeight 74 | } = this.state 75 | let { 76 | frameWidth, 77 | frameHeight 78 | } = this.state 79 | 80 | const maxLeft = imgWidth - frameWidth 81 | const maxTop = imgHeight - frameHeight 82 | 83 | if (originX + frameWidth >= imgWidth) { 84 | originX = imgWidth - frameWidth 85 | this.setState({ 86 | originX 87 | }) 88 | } 89 | if (originY + frameHeight >= imgHeight) { 90 | originY = imgHeight - frameHeight 91 | this.setState({ 92 | originY 93 | }) 94 | } 95 | 96 | this.setState({ 97 | maxLeft, 98 | maxTop 99 | }) 100 | // calc clone position 101 | this.calcPosition(frameWidth, frameHeight, originX, originY, () => { 102 | const { 103 | frameWidth4Style, 104 | frameHeight4Style, 105 | toImgTop4Style, 106 | toImgLeft4Style 107 | } = this.state 108 | 109 | this.setState({ 110 | frameWidth: frameWidth4Style, 111 | frameHeight: frameHeight4Style, 112 | originX: toImgLeft4Style, 113 | originY: toImgTop4Style 114 | }) 115 | }) 116 | }) 117 | } 118 | 119 | componentDidMount () { 120 | // event 121 | document.addEventListener('mousemove', this.handleDrag.bind(this)) 122 | document.addEventListener('touchmove', this.handleDrag.bind(this)) 123 | document.addEventListener('mouseup', this.handleDragStop.bind(this)) 124 | document.addEventListener('touchend', this.handleDragStop.bind(this)) 125 | this.imgGetSizeBeforeLoad() 126 | } 127 | 128 | componentWillUnmount () { 129 | // remove event 130 | document.removeEventListener('mousemove', this.handleDrag.bind(this)) 131 | document.removeEventListener('touchmove', this.handleDrag.bind(this)) 132 | document.removeEventListener('mouseup', this.handleDragStop.bind(this)) 133 | document.removeEventListener('touchend', this.handleDragStop.bind(this)) 134 | } 135 | 136 | // props change to update frame 137 | componentWillReceiveProps (newProps) { 138 | const { 139 | width, 140 | height, 141 | originX, 142 | originY 143 | } = this.props 144 | 145 | // img src changed 146 | if (this.props.src !== newProps.src) { 147 | return this.setState({ 148 | src: newProps.src 149 | }, this.imgGetSizeBeforeLoad) 150 | } 151 | 152 | if (width !== newProps.width || 153 | height !== newProps.height || 154 | originX !== newProps.originX || 155 | originY !== newProps.originY) { 156 | // update frame 157 | this.setState({ 158 | frameWidth: newProps.width, 159 | frameHeight: newProps.height, 160 | originX: newProps.originX, 161 | originY: newProps.originY 162 | }, () => this.initStyles()) 163 | } 164 | } 165 | 166 | // image onloaded hook 167 | imgOnLoad () { 168 | this.props.onImgLoad() 169 | } 170 | 171 | // adjust image height when image size scaleing change, also initialize styles 172 | imgGetSizeBeforeLoad () { 173 | // trick way to get natural width of image after component did mount 174 | setTimeout(() => { 175 | const img = findDOMNode(this.img) 176 | if (img && img.naturalWidth) { 177 | // image scaleing 178 | const imgHeight = parseInt(img.offsetWidth / img.naturalWidth * img.naturalHeight) 179 | // resize imgHeight 180 | this.setState({ 181 | imgHeight, 182 | imgLoaded: true 183 | }, this.initStyles) 184 | // before image loaded hook 185 | this.props.beforeImgLoad() 186 | } else if (img) { 187 | // catch if image natural width is 0 188 | this.imgGetSizeBeforeLoad() 189 | } 190 | }, 0) 191 | } 192 | 193 | // frame width, frame height, position left, position top 194 | calcPosition (width, height, left, top, callback) { 195 | const { 196 | imgWidth, 197 | imgHeight 198 | } = this.state 199 | const { 200 | ratio, 201 | fixedRatio 202 | } = this.props 203 | // width < 0 or height < 0, frame invalid 204 | if (width < 0 || height < 0) return false 205 | // if ratio is fixed 206 | if (fixedRatio) { 207 | // adjust by width 208 | if (width / imgWidth > height / imgHeight) { 209 | if (width > imgWidth) { 210 | width = imgWidth 211 | left = 0 212 | height = width / ratio 213 | } 214 | } else { 215 | // adjust by height 216 | if (height > imgHeight) { 217 | height = imgHeight 218 | top = 0 219 | width = height * ratio 220 | } 221 | } 222 | } 223 | // frame width plus offset left, larger than img's width 224 | if (width + left > imgWidth) { 225 | if (fixedRatio) { 226 | // if fixed ratio, adjust left with width 227 | left = imgWidth - width 228 | } else { 229 | // resize width with left 230 | width = width - ((width + left) - imgWidth) 231 | } 232 | } 233 | // frame heigth plust offset top, larger than img's height 234 | if (height + top > imgHeight) { 235 | if (fixedRatio) { 236 | // if fixed ratio, adjust top with height 237 | top = imgHeight - height 238 | } else { 239 | // resize height with top 240 | height = height - ((height + top) - imgHeight) 241 | } 242 | } 243 | // left is invalid 244 | if (left < 0) { 245 | left = 0 246 | } 247 | // top is invalid 248 | if (top < 0) { 249 | top = 0 250 | } 251 | // if frame width larger than img width 252 | if (width > imgWidth) { 253 | width = imgWidth 254 | } 255 | // if frame height larger than img height 256 | if (height > imgHeight) { 257 | height = imgHeight 258 | } 259 | this.setState({ 260 | toImgLeft4Style: left, 261 | toImgTop4Style: top, 262 | frameWidth4Style: width, 263 | frameHeight4Style: height 264 | }, () => { 265 | if (callback) callback() 266 | }) 267 | } 268 | 269 | // create a new frame, and drag, so frame width and height is became larger. 270 | createNewFrame (e) { 271 | if (this.state.dragging) { 272 | // click or touch event 273 | const { 274 | pageX, 275 | pageY 276 | } = e.pageX ? e : e.targetTouches[0] 277 | 278 | const { 279 | ratio, 280 | fixedRatio 281 | } = this.props 282 | 283 | const { 284 | frameWidth, 285 | frameHeight, 286 | startPageX, 287 | startPageY, 288 | originX, 289 | originY 290 | } = this.state 291 | // click or touch point's offset from source image top 292 | const _x = pageX - startPageX 293 | const _y = pageY - startPageY 294 | 295 | // frame new width, height, left, top 296 | const _width = frameWidth + Math.abs(_x) 297 | const _height = fixedRatio ? (frameWidth + Math.abs(_x)) / ratio : frameHeight + Math.abs(_y) 298 | let _left = originX 299 | let _top = originY 300 | 301 | if (_y < 0) { 302 | // drag and resize to top, top changing 303 | _top = fixedRatio ? originY - Math.abs(_x) / ratio : originY - Math.abs(_y) 304 | } 305 | 306 | if (_x < 0) { 307 | // drag and resize, go to left, left changing 308 | _left = originX + _x 309 | } 310 | // calc position 311 | return this.calcPosition(_width, _height, _left, _top) 312 | } 313 | } 314 | 315 | // frame move handler 316 | frameMove (e) { 317 | const { 318 | originX, 319 | originY, 320 | startPageX, 321 | startPageY, 322 | frameWidth, 323 | frameHeight, 324 | maxLeft, 325 | maxTop 326 | } = this.state 327 | 328 | const { 329 | pageX, 330 | pageY 331 | } = e.pageX ? e : e.targetTouches[0] 332 | 333 | let _x = pageX - startPageX + originX 334 | let _y = pageY - startPageY + originY 335 | if (pageX < 0 || pageY < 0) return false 336 | 337 | if (_x > maxLeft) _x = maxLeft 338 | if (_y > maxTop) _y = maxTop 339 | // frame width, frame height not change, top and left changing 340 | this.calcPosition(frameWidth, frameHeight, _x, _y) 341 | } 342 | 343 | // drag dot to different direction 344 | frameDotMove (dir, e) { 345 | const { 346 | pageX, 347 | pageY 348 | } = e.pageX ? e : e.targetTouches[0] 349 | 350 | const { 351 | ratio, 352 | fixedRatio 353 | } = this.props 354 | 355 | const { 356 | startPageX, 357 | startPageY, 358 | originX, 359 | originY, 360 | frameWidth4Style, 361 | frameHeight4Style, 362 | frameWidth, 363 | frameHeight, 364 | imgWidth, 365 | imgHeight 366 | } = this.state 367 | 368 | if (pageY !== 0 && pageX !== 0) { 369 | // current drag position offset x and y to first drag start position 370 | const _x = pageX - startPageX 371 | const _y = pageY - startPageY 372 | 373 | let _width = 0 374 | let _height = 0 375 | let _top = 0 376 | let _left = 0 377 | // just calc width, height, left, top in each direction 378 | switch (dir) { 379 | case 'ne': 380 | _width = frameWidth + _x 381 | _height = fixedRatio ? _width / ratio : frameHeight - _y 382 | _left = originX 383 | _top = fixedRatio ? (originY - _x / ratio) : originY + _y 384 | break 385 | case 'e': 386 | _width = frameWidth + _x 387 | _height = fixedRatio ? _width / ratio : frameHeight 388 | _left = originX 389 | _top = fixedRatio ? originY - _x / ratio * 0.5 : originY 390 | break 391 | case 'se': 392 | _width = frameWidth + _x 393 | _height = fixedRatio ? _width / ratio : frameHeight + _y 394 | _left = originX 395 | _top = originY 396 | break 397 | case 'n': 398 | _height = frameHeight - _y 399 | _width = fixedRatio ? _height * ratio : frameWidth 400 | _left = fixedRatio ? originX + _y * ratio * 0.5 : originX 401 | _top = originY + _y 402 | break 403 | case 'nw': 404 | _width = frameWidth - _x 405 | _height = fixedRatio ? _width / ratio : frameHeight - _y 406 | _left = originX + _x 407 | _top = fixedRatio ? originY + _x / ratio : originY + _y 408 | break 409 | case 'w': 410 | _width = frameWidth - _x 411 | _height = fixedRatio ? _width / ratio : frameHeight 412 | _left = originX + _x 413 | _top = fixedRatio ? originY + _x / ratio * 0.5 : originY 414 | break 415 | case 'sw': 416 | _width = frameWidth - _x 417 | _height = fixedRatio ? _width / ratio : frameHeight + _y 418 | _left = originX + _x 419 | _top = originY 420 | break 421 | case 's': 422 | _height = frameHeight + _y 423 | _width = fixedRatio ? _height * ratio : frameWidth 424 | _left = fixedRatio ? originX - _y * ratio * 0.5 : originX 425 | _top = originY 426 | break 427 | default: 428 | break 429 | } 430 | 431 | if (_width > imgWidth || _height > imgHeight) { 432 | if (frameWidth4Style >= imgWidth || frameHeight4Style >= imgHeight) { 433 | return false 434 | } 435 | } 436 | 437 | return this.calcPosition(_width, _height, _left, _top) 438 | } 439 | } 440 | 441 | // judge whether to create new frame, frame or frame dot move acroding to action 442 | handleDrag (e) { 443 | if (this.state.dragging) { 444 | e.preventDefault() 445 | const { 446 | action 447 | } = this.state 448 | 449 | if (!action) return this.createNewFrame(e) 450 | if (action === 'move') return this.frameMove(e) 451 | this.frameDotMove(action, e) 452 | } 453 | } 454 | 455 | // starting dragging 456 | handleDragStart (e) { 457 | const { 458 | allowNewSelection 459 | } = this.props 460 | 461 | const action = e.target.getAttribute('data-action') 462 | ? e.target.getAttribute('data-action') 463 | : e.target.parentNode.getAttribute('data-action') 464 | 465 | const { 466 | pageX, 467 | pageY 468 | } = e.pageX ? e : e.targetTouches[0] 469 | 470 | // if drag or move or allow new selection, change startPageX, startPageY, dragging state 471 | if (action || allowNewSelection) { 472 | e.preventDefault() 473 | // drag start, set startPageX, startPageY for dragging start point 474 | this.setState({ 475 | startPageX: pageX, 476 | startPageY: pageY, 477 | dragging: true, 478 | action 479 | }) 480 | } 481 | // if no action and allowNewSelection, then create a new frame 482 | if (!action && allowNewSelection) { 483 | const container = findDOMNode(this.container) 484 | const { 485 | offsetLeft, 486 | offsetTop 487 | } = container 488 | 489 | this.setState({ 490 | // set offset left and top of new frame 491 | originX: pageX - offsetLeft, 492 | originY: pageY - offsetTop, 493 | frameWidth: 2, 494 | frameHeight: 2 495 | }, () => this.calcPosition(2, 2, pageX - offsetLeft, pageY - offsetTop)) 496 | } 497 | } 498 | 499 | // crop image 500 | crop () { 501 | const img = findDOMNode(this.img) 502 | let canvas = document.createElement('canvas') 503 | const { 504 | x, 505 | y, 506 | width, 507 | height 508 | } = this.values().original 509 | 510 | canvas.width = width 511 | canvas.height = height 512 | canvas.getContext('2d').drawImage(img, x, y, width, height, 0, 0, width, height) 513 | return canvas.toDataURL() 514 | } 515 | 516 | // get current values 517 | values () { 518 | const img = findDOMNode(this.img) 519 | const { 520 | frameWidth, 521 | frameHeight, 522 | originX, 523 | originY, 524 | imgWidth, 525 | imgHeight 526 | } = this.state 527 | 528 | // crop accroding image's natural width 529 | const _scale = img.naturalWidth / imgWidth 530 | const realFrameWidth = frameWidth * _scale 531 | const realFrameHeight = frameHeight * _scale 532 | const realOriginX = originX * _scale 533 | const realOriginY = originY * _scale 534 | 535 | return { 536 | display: { 537 | width: frameWidth, 538 | height: frameHeight, 539 | x: originX, 540 | y: originY, 541 | imgWidth, 542 | imgHeight 543 | }, 544 | original: { 545 | width: realFrameWidth, 546 | height: realFrameHeight, 547 | x: realOriginX, 548 | y: realOriginY, 549 | imgWidth: img.naturalWidth, 550 | imgHeight: img.naturalHeight 551 | } 552 | } 553 | } 554 | 555 | // stop dragging 556 | handleDragStop (e) { 557 | if (this.state.dragging) { 558 | e.preventDefault() 559 | 560 | const { 561 | offsetLeft, 562 | offsetTop, 563 | offsetWidth, 564 | offsetHeight 565 | } = findDOMNode(this.frameNode) 566 | 567 | const { 568 | imgWidth, 569 | imgHeight 570 | } = this.state 571 | 572 | this.setState({ 573 | originX: offsetLeft, 574 | originY: offsetTop, 575 | dragging: false, 576 | frameWidth: offsetWidth, 577 | frameHeight: offsetHeight, 578 | maxLeft: imgWidth - offsetWidth, 579 | maxTop: imgHeight - offsetHeight, 580 | action: null 581 | }, () => { 582 | const { onChange } = this.props 583 | if (onChange) onChange(this.values()) 584 | }) 585 | } 586 | } 587 | 588 | render () { 589 | const { 590 | dragging, 591 | imgHeight, 592 | imgWidth, 593 | imgLoaded, 594 | styles, 595 | src 596 | } = this.state 597 | 598 | const { disabled } = this.props 599 | 600 | const imageNode = ( 601 |
{ this.sourceNode = ref } 605 | } 606 | > 607 | { this.img = ref } 614 | } 615 | style = { 616 | deepExtend({}, styles.img, styles.source_img) 617 | } 618 | onLoad={this.imgOnLoad.bind(this)} 619 | /> 620 |
621 | ) 622 | // disabled cropper 623 | if (disabled) { 624 | return ( 625 |
{ this.container = ref } 637 | } 638 | > 639 | {imageNode} 640 |
643 |
644 |
645 | ) 646 | } 647 | 648 | return ( 649 |
{ this.container = ref } 662 | } 663 | > 664 | {imageNode} 665 | { 666 | imgLoaded 667 | ?
668 |
671 |
672 | {/* frame container */} 673 |
{ this.frameNode = ref } 688 | } 689 | > 690 | {/* clone img */} 691 |
694 | { this.cloneImg = ref } 709 | } 710 | /> 711 |
712 | 713 | {/* move element */} 714 | 718 | 719 | {/* move center element */} 720 | 726 | 731 | 732 | 737 | 738 | 739 | 740 | {/* frame dot elements */} 741 | 747 | 752 | 753 | 754 | 760 | 765 | 766 | 767 | 773 | 778 | 779 | 780 | 786 | 791 | 792 | 793 | 799 | 804 | 805 | 806 | 812 | 817 | 818 | 819 | 825 | 830 | 831 | 832 | 838 | 843 | 844 | 845 | 846 | {/* frame line elements */} 847 | 853 | 854 | 860 | 861 | 867 | 868 | 874 | 875 |
876 |
877 | : null 878 | } 879 |
880 | ) 881 | } 882 | } 883 | 884 | Cropper.propTypes = { 885 | src: PropTypes.string.isRequired, 886 | originX: PropTypes.number, 887 | originY: PropTypes.number, 888 | ratio: PropTypes.number, 889 | width: PropTypes.number, 890 | height: PropTypes.number, 891 | fixedRatio: PropTypes.bool, 892 | allowNewSelection: PropTypes.bool, 893 | disabled: PropTypes.bool, 894 | styles: PropTypes.object, 895 | onImgLoad: PropTypes.func, 896 | beforeImgLoad: PropTypes.func, 897 | onChange: PropTypes.func 898 | } 899 | 900 | Cropper.defaultProps = { 901 | width: 200, 902 | height: 200, 903 | fixedRatio: true, 904 | allowNewSelection: true, 905 | ratio: 1, 906 | originX: 0, 907 | originY: 0, 908 | styles: {}, 909 | onImgLoad: function () {}, 910 | beforeImgLoad: function () {} 911 | } 912 | 913 | /* 914 | default inline styles 915 | */ 916 | const defaultStyles = { 917 | container: {}, 918 | img: { 919 | userDrag: 'none', 920 | userSelect: 'none', 921 | MozUserSelect: 'none', 922 | WebkitUserDrag: 'none', 923 | WebkitUserSelect: 'none', 924 | WebkitTransform: 'translateZ(0)', 925 | WebkitPerspective: 1000, 926 | WebkitBackfaceVisibility: 'hidden' 927 | }, 928 | 929 | clone: { 930 | width: '100%', 931 | height: '100%', 932 | overflow: 'hidden', 933 | position: 'absolute', 934 | left: 0, 935 | top: 0 936 | }, 937 | 938 | frame: { 939 | position: 'absolute', 940 | left: 0, 941 | top: 0, 942 | bottom: 0, 943 | right: 0, 944 | display: 'none' 945 | }, 946 | 947 | dragging_frame: { 948 | opacity: 0.8 949 | }, 950 | 951 | source: { 952 | overflow: 'hidden' 953 | }, 954 | 955 | source_img: { 956 | float: 'left' 957 | }, 958 | 959 | modal: { 960 | position: 'absolute', 961 | left: 0, 962 | top: 0, 963 | bottom: 0, 964 | right: 0, 965 | opacity: 0.4, 966 | backgroundColor: '#000' 967 | }, 968 | modal_disabled: { 969 | backgroundColor: '#666', 970 | opacity: 0.7, 971 | cursor: 'not-allowed' 972 | }, 973 | move: { 974 | position: 'absolute', 975 | left: 0, 976 | top: 0, 977 | bottom: 0, 978 | right: 0, 979 | cursor: 'move', 980 | outline: '1px dashed #88f', 981 | backgroundColor: 'transparent' 982 | }, 983 | 984 | dot: { 985 | zIndex: 10 986 | }, 987 | dotN: { 988 | cursor: 'n-resize' 989 | }, 990 | dotS: { 991 | cursor: 's-resize' 992 | }, 993 | dotE: { 994 | cursor: 'e-resize' 995 | }, 996 | dotW: { 997 | cursor: 'w-resize' 998 | }, 999 | dotNW: { 1000 | cursor: 'nw-resize' 1001 | }, 1002 | dotNE: { 1003 | cursor: 'ne-resize' 1004 | }, 1005 | dotSW: { 1006 | cursor: 'sw-resize' 1007 | }, 1008 | dotSE: { 1009 | cursor: 'se-resize' 1010 | }, 1011 | dotCenter: { 1012 | backgroundColor: 'transparent', 1013 | cursor: 'move' 1014 | }, 1015 | 1016 | dotInner: { 1017 | border: '1px solid #88f', 1018 | background: '#fff', 1019 | display: 'block', 1020 | width: 6, 1021 | height: 6, 1022 | padding: 0, 1023 | margin: 0, 1024 | position: 'absolute' 1025 | }, 1026 | 1027 | dotInnerN: { 1028 | top: -4, 1029 | left: '50%', 1030 | marginLeft: -4 1031 | }, 1032 | dotInnerS: { 1033 | bottom: -4, 1034 | left: '50%', 1035 | marginLeft: -4 1036 | }, 1037 | dotInnerE: { 1038 | right: -4, 1039 | top: '50%', 1040 | marginTop: -4 1041 | }, 1042 | dotInnerW: { 1043 | left: -4, 1044 | top: '50%', 1045 | marginTop: -4 1046 | }, 1047 | dotInnerNE: { 1048 | top: -4, 1049 | right: -4 1050 | }, 1051 | dotInnerSE: { 1052 | bottom: -4, 1053 | right: -4 1054 | }, 1055 | dotInnerNW: { 1056 | top: -4, 1057 | left: -4 1058 | }, 1059 | dotInnerSW: { 1060 | bottom: -4, 1061 | left: -4 1062 | }, 1063 | dotInnerCenterVertical: { 1064 | position: 'absolute', 1065 | border: 'none', 1066 | width: 2, 1067 | height: 8, 1068 | backgroundColor: '#88f', 1069 | top: '50%', 1070 | left: '50%', 1071 | marginLeft: -1, 1072 | marginTop: -4 1073 | }, 1074 | dotInnerCenterHorizontal: { 1075 | position: 'absolute', 1076 | border: 'none', 1077 | width: 8, 1078 | height: 2, 1079 | backgroundColor: '#88f', 1080 | top: '50%', 1081 | left: '50%', 1082 | marginLeft: -4, 1083 | marginTop: -1 1084 | }, 1085 | 1086 | line: { 1087 | position: 'absolute', 1088 | display: 'block', 1089 | zIndex: 100 1090 | }, 1091 | 1092 | lineS: { 1093 | cursor: 's-resize', 1094 | bottom: 0, 1095 | left: 0, 1096 | width: '100%', 1097 | height: 4, 1098 | background: 'transparent' 1099 | }, 1100 | lineN: { 1101 | cursor: 'n-resize', 1102 | top: 0, 1103 | left: 0, 1104 | width: '100%', 1105 | height: 4, 1106 | background: 'transparent' 1107 | }, 1108 | lineE: { 1109 | cursor: 'e-resize', 1110 | right: 0, 1111 | top: 0, 1112 | width: 4, 1113 | height: '100%', 1114 | background: 'transparent' 1115 | }, 1116 | lineW: { 1117 | cursor: 'w-resize', 1118 | left: 0, 1119 | top: 0, 1120 | width: 4, 1121 | height: '100%', 1122 | background: 'transparent' 1123 | } 1124 | } 1125 | 1126 | module.exports = Cropper 1127 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './demo.less' 4 | import Cropper from '../component/Cropper' 5 | 6 | const DemoImg = 'https://braavos.me/images/posts/gr/8.jpg' 7 | 8 | class ImageCropDemo extends Component { 9 | constructor (props) { 10 | super(props) 11 | this.state = { 12 | imgSrc: DemoImg, 13 | image: '', 14 | imageLoaded: false, 15 | image1: '', 16 | imageL1oaded: false, 17 | image2: '', 18 | image2Loaded: false, 19 | image3: '', 20 | image3Loaded: false, 21 | image4: '', 22 | image4Loaded: false, 23 | image4BeforeLoaded: false, 24 | image4Values: '' 25 | } 26 | } 27 | 28 | handleImageLoaded (state) { 29 | this.setState({ 30 | [state + 'Loaded']: true 31 | }) 32 | } 33 | 34 | handleBeforeImageLoad (state) { 35 | this.setState({ 36 | [state + 'BeforeLoaded']: true 37 | }) 38 | } 39 | 40 | handleClick (state) { 41 | let node = this[state] 42 | this.setState({ 43 | [state]: node.crop() 44 | }) 45 | } 46 | 47 | handleChange (state, values) { 48 | this.setState({ 49 | [state + 'Values']: values 50 | }) 51 | } 52 | 53 | handleGetValues (state) { 54 | let node = this[state] 55 | this.setState({ 56 | [state + 'Values']: node.values() 57 | }) 58 | } 59 | 60 | render () { 61 | return ( 62 | 239 | ) 240 | } 241 | } 242 | 243 | if (module.hot) { 244 | module.hot.accept() 245 | } 246 | 247 | ReactDOM.render(, document.getElementById('root')) 248 | -------------------------------------------------------------------------------- /demo/demo.less: -------------------------------------------------------------------------------- 1 | 2 | .container { 3 | width: 600px; 4 | margin: 0 auto; 5 | } 6 | 7 | .after-img { 8 | width: 300px; 9 | } -------------------------------------------------------------------------------- /dist/image/demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flyfloor/react-image-cropper/90d07aa44263807ae1cc4ce8f168415ed9892e26/dist/image/demo.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | react image crop 6 | 7 | 8 |
9 |

crop demo

10 |
11 |
12 | React image cropper 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Cropper = require('./lib/Cropper'); 4 | 5 | module.exports = { 6 | Cropper: Cropper 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /lib/Cropper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 8 | 9 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 10 | 11 | var React = require('react'); 12 | var Component = React.Component; 13 | 14 | var ReactDOM = require('react-dom'); 15 | var deepExtend = require('deep-extend'); 16 | var PropTypes = require('prop-types'); 17 | var findDOMNode = ReactDOM.findDOMNode; 18 | 19 | var Cropper = function (_Component) { 20 | _inherits(Cropper, _Component); 21 | 22 | function Cropper(props) { 23 | _classCallCheck(this, Cropper); 24 | 25 | var _this = _possibleConstructorReturn(this, (Cropper.__proto__ || Object.getPrototypeOf(Cropper)).call(this, props)); 26 | 27 | var originX = props.originX, 28 | originY = props.originY, 29 | width = props.width, 30 | height = props.height, 31 | fixedRatio = props.fixedRatio, 32 | ratio = props.ratio, 33 | styles = props.styles, 34 | src = props.src; 35 | 36 | 37 | _this.state = { 38 | // image and clone image src 39 | src: src, 40 | // background image width 41 | imgWidth: '100%', 42 | // background image height 43 | imgHeight: 'auto', 44 | // cropper width, drag trigger changing 45 | frameWidth4Style: width, 46 | // cropper height, drag trigger changing 47 | frameHeight4Style: fixedRatio ? width / ratio : height, 48 | // cropper height, drag trigger changing 49 | toImgTop4Style: 0, 50 | toImgLeft4Style: 0, 51 | // cropper original position(x axis), accroding to image left 52 | originX: originX, 53 | // cropper original position(y axis), accroding to image top 54 | originY: originY, 55 | // dragging start, position's pageX and pageY 56 | startPageX: 0, 57 | startPageY: 0, 58 | // frame width, change only dragging stop 59 | frameWidth: width, 60 | // frame height, change only dragging stop 61 | frameHeight: fixedRatio ? width / ratio : height, 62 | dragging: false, 63 | maxLeft: 0, 64 | maxTop: 0, 65 | action: null, 66 | imgLoaded: false, 67 | styles: deepExtend({}, defaultStyles, styles) 68 | }; 69 | return _this; 70 | } 71 | 72 | // initialize style, component did mount or component updated. 73 | 74 | 75 | _createClass(Cropper, [{ 76 | key: 'initStyles', 77 | value: function initStyles() { 78 | var _this2 = this; 79 | 80 | var container = findDOMNode(this.container); 81 | this.setState({ 82 | imgWidth: container.offsetWidth 83 | }, function () { 84 | // calc frame width height 85 | var _props = _this2.props, 86 | originX = _props.originX, 87 | originY = _props.originY, 88 | disabled = _props.disabled; 89 | 90 | 91 | if (disabled) return; 92 | 93 | var _state = _this2.state, 94 | imgWidth = _state.imgWidth, 95 | imgHeight = _state.imgHeight; 96 | var _state2 = _this2.state, 97 | frameWidth = _state2.frameWidth, 98 | frameHeight = _state2.frameHeight; 99 | 100 | 101 | var maxLeft = imgWidth - frameWidth; 102 | var maxTop = imgHeight - frameHeight; 103 | 104 | if (originX + frameWidth >= imgWidth) { 105 | originX = imgWidth - frameWidth; 106 | _this2.setState({ 107 | originX: originX 108 | }); 109 | } 110 | if (originY + frameHeight >= imgHeight) { 111 | originY = imgHeight - frameHeight; 112 | _this2.setState({ 113 | originY: originY 114 | }); 115 | } 116 | 117 | _this2.setState({ 118 | maxLeft: maxLeft, 119 | maxTop: maxTop 120 | }); 121 | // calc clone position 122 | _this2.calcPosition(frameWidth, frameHeight, originX, originY, function () { 123 | var _state3 = _this2.state, 124 | frameWidth4Style = _state3.frameWidth4Style, 125 | frameHeight4Style = _state3.frameHeight4Style, 126 | toImgTop4Style = _state3.toImgTop4Style, 127 | toImgLeft4Style = _state3.toImgLeft4Style; 128 | 129 | 130 | _this2.setState({ 131 | frameWidth: frameWidth4Style, 132 | frameHeight: frameHeight4Style, 133 | originX: toImgLeft4Style, 134 | originY: toImgTop4Style 135 | }); 136 | }); 137 | }); 138 | } 139 | }, { 140 | key: 'componentDidMount', 141 | value: function componentDidMount() { 142 | // event 143 | document.addEventListener('mousemove', this.handleDrag.bind(this)); 144 | document.addEventListener('touchmove', this.handleDrag.bind(this)); 145 | document.addEventListener('mouseup', this.handleDragStop.bind(this)); 146 | document.addEventListener('touchend', this.handleDragStop.bind(this)); 147 | this.imgGetSizeBeforeLoad(); 148 | } 149 | }, { 150 | key: 'componentWillUnmount', 151 | value: function componentWillUnmount() { 152 | // remove event 153 | document.removeEventListener('mousemove', this.handleDrag.bind(this)); 154 | document.removeEventListener('touchmove', this.handleDrag.bind(this)); 155 | document.removeEventListener('mouseup', this.handleDragStop.bind(this)); 156 | document.removeEventListener('touchend', this.handleDragStop.bind(this)); 157 | } 158 | 159 | // props change to update frame 160 | 161 | }, { 162 | key: 'componentWillReceiveProps', 163 | value: function componentWillReceiveProps(newProps) { 164 | var _this3 = this; 165 | 166 | var _props2 = this.props, 167 | width = _props2.width, 168 | height = _props2.height, 169 | originX = _props2.originX, 170 | originY = _props2.originY; 171 | 172 | // img src changed 173 | 174 | if (this.props.src !== newProps.src) { 175 | return this.setState({ 176 | src: newProps.src 177 | }, this.imgGetSizeBeforeLoad); 178 | } 179 | 180 | if (width !== newProps.width || height !== newProps.height || originX !== newProps.originX || originY !== newProps.originY) { 181 | // update frame 182 | this.setState({ 183 | frameWidth: newProps.width, 184 | frameHeight: newProps.height, 185 | originX: newProps.originX, 186 | originY: newProps.originY 187 | }, function () { 188 | return _this3.initStyles(); 189 | }); 190 | } 191 | } 192 | 193 | // image onloaded hook 194 | 195 | }, { 196 | key: 'imgOnLoad', 197 | value: function imgOnLoad() { 198 | this.props.onImgLoad(); 199 | } 200 | 201 | // adjust image height when image size scaleing change, also initialize styles 202 | 203 | }, { 204 | key: 'imgGetSizeBeforeLoad', 205 | value: function imgGetSizeBeforeLoad() { 206 | var _this4 = this; 207 | 208 | // trick way to get natural width of image after component did mount 209 | setTimeout(function () { 210 | var img = findDOMNode(_this4.img); 211 | if (img && img.naturalWidth) { 212 | // image scaleing 213 | var imgHeight = parseInt(img.offsetWidth / img.naturalWidth * img.naturalHeight); 214 | // resize imgHeight 215 | _this4.setState({ 216 | imgHeight: imgHeight, 217 | imgLoaded: true 218 | }, _this4.initStyles); 219 | // before image loaded hook 220 | _this4.props.beforeImgLoad(); 221 | } else if (img) { 222 | // catch if image natural width is 0 223 | _this4.imgGetSizeBeforeLoad(); 224 | } 225 | }, 0); 226 | } 227 | 228 | // frame width, frame height, position left, position top 229 | 230 | }, { 231 | key: 'calcPosition', 232 | value: function calcPosition(width, height, left, top, callback) { 233 | var _state4 = this.state, 234 | imgWidth = _state4.imgWidth, 235 | imgHeight = _state4.imgHeight; 236 | var _props3 = this.props, 237 | ratio = _props3.ratio, 238 | fixedRatio = _props3.fixedRatio; 239 | // width < 0 or height < 0, frame invalid 240 | 241 | if (width < 0 || height < 0) return false; 242 | // if ratio is fixed 243 | if (fixedRatio) { 244 | // adjust by width 245 | if (width / imgWidth > height / imgHeight) { 246 | if (width > imgWidth) { 247 | width = imgWidth; 248 | left = 0; 249 | height = width / ratio; 250 | } 251 | } else { 252 | // adjust by height 253 | if (height > imgHeight) { 254 | height = imgHeight; 255 | top = 0; 256 | width = height * ratio; 257 | } 258 | } 259 | } 260 | // frame width plus offset left, larger than img's width 261 | if (width + left > imgWidth) { 262 | if (fixedRatio) { 263 | // if fixed ratio, adjust left with width 264 | left = imgWidth - width; 265 | } else { 266 | // resize width with left 267 | width = width - (width + left - imgWidth); 268 | } 269 | } 270 | // frame heigth plust offset top, larger than img's height 271 | if (height + top > imgHeight) { 272 | if (fixedRatio) { 273 | // if fixed ratio, adjust top with height 274 | top = imgHeight - height; 275 | } else { 276 | // resize height with top 277 | height = height - (height + top - imgHeight); 278 | } 279 | } 280 | // left is invalid 281 | if (left < 0) { 282 | left = 0; 283 | } 284 | // top is invalid 285 | if (top < 0) { 286 | top = 0; 287 | } 288 | // if frame width larger than img width 289 | if (width > imgWidth) { 290 | width = imgWidth; 291 | } 292 | // if frame height larger than img height 293 | if (height > imgHeight) { 294 | height = imgHeight; 295 | } 296 | this.setState({ 297 | toImgLeft4Style: left, 298 | toImgTop4Style: top, 299 | frameWidth4Style: width, 300 | frameHeight4Style: height 301 | }, function () { 302 | if (callback) callback(); 303 | }); 304 | } 305 | 306 | // create a new frame, and drag, so frame width and height is became larger. 307 | 308 | }, { 309 | key: 'createNewFrame', 310 | value: function createNewFrame(e) { 311 | if (this.state.dragging) { 312 | // click or touch event 313 | var _ref = e.pageX ? e : e.targetTouches[0], 314 | pageX = _ref.pageX, 315 | pageY = _ref.pageY; 316 | 317 | var _props4 = this.props, 318 | ratio = _props4.ratio, 319 | fixedRatio = _props4.fixedRatio; 320 | var _state5 = this.state, 321 | frameWidth = _state5.frameWidth, 322 | frameHeight = _state5.frameHeight, 323 | startPageX = _state5.startPageX, 324 | startPageY = _state5.startPageY, 325 | originX = _state5.originX, 326 | originY = _state5.originY; 327 | // click or touch point's offset from source image top 328 | 329 | var _x = pageX - startPageX; 330 | var _y = pageY - startPageY; 331 | 332 | // frame new width, height, left, top 333 | var _width = frameWidth + Math.abs(_x); 334 | var _height = fixedRatio ? (frameWidth + Math.abs(_x)) / ratio : frameHeight + Math.abs(_y); 335 | var _left = originX; 336 | var _top = originY; 337 | 338 | if (_y < 0) { 339 | // drag and resize to top, top changing 340 | _top = fixedRatio ? originY - Math.abs(_x) / ratio : originY - Math.abs(_y); 341 | } 342 | 343 | if (_x < 0) { 344 | // drag and resize, go to left, left changing 345 | _left = originX + _x; 346 | } 347 | // calc position 348 | return this.calcPosition(_width, _height, _left, _top); 349 | } 350 | } 351 | 352 | // frame move handler 353 | 354 | }, { 355 | key: 'frameMove', 356 | value: function frameMove(e) { 357 | var _state6 = this.state, 358 | originX = _state6.originX, 359 | originY = _state6.originY, 360 | startPageX = _state6.startPageX, 361 | startPageY = _state6.startPageY, 362 | frameWidth = _state6.frameWidth, 363 | frameHeight = _state6.frameHeight, 364 | maxLeft = _state6.maxLeft, 365 | maxTop = _state6.maxTop; 366 | 367 | var _ref2 = e.pageX ? e : e.targetTouches[0], 368 | pageX = _ref2.pageX, 369 | pageY = _ref2.pageY; 370 | 371 | var _x = pageX - startPageX + originX; 372 | var _y = pageY - startPageY + originY; 373 | if (pageX < 0 || pageY < 0) return false; 374 | 375 | if (_x > maxLeft) _x = maxLeft; 376 | if (_y > maxTop) _y = maxTop; 377 | // frame width, frame height not change, top and left changing 378 | this.calcPosition(frameWidth, frameHeight, _x, _y); 379 | } 380 | 381 | // drag dot to different direction 382 | 383 | }, { 384 | key: 'frameDotMove', 385 | value: function frameDotMove(dir, e) { 386 | var _ref3 = e.pageX ? e : e.targetTouches[0], 387 | pageX = _ref3.pageX, 388 | pageY = _ref3.pageY; 389 | 390 | var _props5 = this.props, 391 | ratio = _props5.ratio, 392 | fixedRatio = _props5.fixedRatio; 393 | var _state7 = this.state, 394 | startPageX = _state7.startPageX, 395 | startPageY = _state7.startPageY, 396 | originX = _state7.originX, 397 | originY = _state7.originY, 398 | frameWidth4Style = _state7.frameWidth4Style, 399 | frameHeight4Style = _state7.frameHeight4Style, 400 | frameWidth = _state7.frameWidth, 401 | frameHeight = _state7.frameHeight, 402 | imgWidth = _state7.imgWidth, 403 | imgHeight = _state7.imgHeight; 404 | 405 | 406 | if (pageY !== 0 && pageX !== 0) { 407 | // current drag position offset x and y to first drag start position 408 | var _x = pageX - startPageX; 409 | var _y = pageY - startPageY; 410 | 411 | var _width = 0; 412 | var _height = 0; 413 | var _top = 0; 414 | var _left = 0; 415 | // just calc width, height, left, top in each direction 416 | switch (dir) { 417 | case 'ne': 418 | _width = frameWidth + _x; 419 | _height = fixedRatio ? _width / ratio : frameHeight - _y; 420 | _left = originX; 421 | _top = fixedRatio ? originY - _x / ratio : originY + _y; 422 | break; 423 | case 'e': 424 | _width = frameWidth + _x; 425 | _height = fixedRatio ? _width / ratio : frameHeight; 426 | _left = originX; 427 | _top = fixedRatio ? originY - _x / ratio * 0.5 : originY; 428 | break; 429 | case 'se': 430 | _width = frameWidth + _x; 431 | _height = fixedRatio ? _width / ratio : frameHeight + _y; 432 | _left = originX; 433 | _top = originY; 434 | break; 435 | case 'n': 436 | _height = frameHeight - _y; 437 | _width = fixedRatio ? _height * ratio : frameWidth; 438 | _left = fixedRatio ? originX + _y * ratio * 0.5 : originX; 439 | _top = originY + _y; 440 | break; 441 | case 'nw': 442 | _width = frameWidth - _x; 443 | _height = fixedRatio ? _width / ratio : frameHeight - _y; 444 | _left = originX + _x; 445 | _top = fixedRatio ? originY + _x / ratio : originY + _y; 446 | break; 447 | case 'w': 448 | _width = frameWidth - _x; 449 | _height = fixedRatio ? _width / ratio : frameHeight; 450 | _left = originX + _x; 451 | _top = fixedRatio ? originY + _x / ratio * 0.5 : originY; 452 | break; 453 | case 'sw': 454 | _width = frameWidth - _x; 455 | _height = fixedRatio ? _width / ratio : frameHeight + _y; 456 | _left = originX + _x; 457 | _top = originY; 458 | break; 459 | case 's': 460 | _height = frameHeight + _y; 461 | _width = fixedRatio ? _height * ratio : frameWidth; 462 | _left = fixedRatio ? originX - _y * ratio * 0.5 : originX; 463 | _top = originY; 464 | break; 465 | default: 466 | break; 467 | } 468 | 469 | if (_width > imgWidth || _height > imgHeight) { 470 | if (frameWidth4Style >= imgWidth || frameHeight4Style >= imgHeight) { 471 | return false; 472 | } 473 | } 474 | 475 | return this.calcPosition(_width, _height, _left, _top); 476 | } 477 | } 478 | 479 | // judge whether to create new frame, frame or frame dot move acroding to action 480 | 481 | }, { 482 | key: 'handleDrag', 483 | value: function handleDrag(e) { 484 | if (this.state.dragging) { 485 | e.preventDefault(); 486 | var action = this.state.action; 487 | 488 | 489 | if (!action) return this.createNewFrame(e); 490 | if (action === 'move') return this.frameMove(e); 491 | this.frameDotMove(action, e); 492 | } 493 | } 494 | 495 | // starting dragging 496 | 497 | }, { 498 | key: 'handleDragStart', 499 | value: function handleDragStart(e) { 500 | var _this5 = this; 501 | 502 | var allowNewSelection = this.props.allowNewSelection; 503 | 504 | 505 | var action = e.target.getAttribute('data-action') ? e.target.getAttribute('data-action') : e.target.parentNode.getAttribute('data-action'); 506 | 507 | var _ref4 = e.pageX ? e : e.targetTouches[0], 508 | pageX = _ref4.pageX, 509 | pageY = _ref4.pageY; 510 | 511 | // if drag or move or allow new selection, change startPageX, startPageY, dragging state 512 | 513 | 514 | if (action || allowNewSelection) { 515 | e.preventDefault(); 516 | // drag start, set startPageX, startPageY for dragging start point 517 | this.setState({ 518 | startPageX: pageX, 519 | startPageY: pageY, 520 | dragging: true, 521 | action: action 522 | }); 523 | } 524 | // if no action and allowNewSelection, then create a new frame 525 | if (!action && allowNewSelection) { 526 | var container = findDOMNode(this.container); 527 | var offsetLeft = container.offsetLeft, 528 | offsetTop = container.offsetTop; 529 | 530 | 531 | this.setState({ 532 | // set offset left and top of new frame 533 | originX: pageX - offsetLeft, 534 | originY: pageY - offsetTop, 535 | frameWidth: 2, 536 | frameHeight: 2 537 | }, function () { 538 | return _this5.calcPosition(2, 2, pageX - offsetLeft, pageY - offsetTop); 539 | }); 540 | } 541 | } 542 | 543 | // crop image 544 | 545 | }, { 546 | key: 'crop', 547 | value: function crop() { 548 | var img = findDOMNode(this.img); 549 | var canvas = document.createElement('canvas'); 550 | var _values$original = this.values().original, 551 | x = _values$original.x, 552 | y = _values$original.y, 553 | width = _values$original.width, 554 | height = _values$original.height; 555 | 556 | 557 | canvas.width = width; 558 | canvas.height = height; 559 | canvas.getContext('2d').drawImage(img, x, y, width, height, 0, 0, width, height); 560 | return canvas.toDataURL(); 561 | } 562 | 563 | // get current values 564 | 565 | }, { 566 | key: 'values', 567 | value: function values() { 568 | var img = findDOMNode(this.img); 569 | var _state8 = this.state, 570 | frameWidth = _state8.frameWidth, 571 | frameHeight = _state8.frameHeight, 572 | originX = _state8.originX, 573 | originY = _state8.originY, 574 | imgWidth = _state8.imgWidth, 575 | imgHeight = _state8.imgHeight; 576 | 577 | // crop accroding image's natural width 578 | 579 | var _scale = img.naturalWidth / imgWidth; 580 | var realFrameWidth = frameWidth * _scale; 581 | var realFrameHeight = frameHeight * _scale; 582 | var realOriginX = originX * _scale; 583 | var realOriginY = originY * _scale; 584 | 585 | return { 586 | display: { 587 | width: frameWidth, 588 | height: frameHeight, 589 | x: originX, 590 | y: originY, 591 | imgWidth: imgWidth, 592 | imgHeight: imgHeight 593 | }, 594 | original: { 595 | width: realFrameWidth, 596 | height: realFrameHeight, 597 | x: realOriginX, 598 | y: realOriginY, 599 | imgWidth: img.naturalWidth, 600 | imgHeight: img.naturalHeight 601 | } 602 | }; 603 | } 604 | 605 | // stop dragging 606 | 607 | }, { 608 | key: 'handleDragStop', 609 | value: function handleDragStop(e) { 610 | var _this6 = this; 611 | 612 | if (this.state.dragging) { 613 | e.preventDefault(); 614 | 615 | var _findDOMNode = findDOMNode(this.frameNode), 616 | offsetLeft = _findDOMNode.offsetLeft, 617 | offsetTop = _findDOMNode.offsetTop, 618 | offsetWidth = _findDOMNode.offsetWidth, 619 | offsetHeight = _findDOMNode.offsetHeight; 620 | 621 | var _state9 = this.state, 622 | imgWidth = _state9.imgWidth, 623 | imgHeight = _state9.imgHeight; 624 | 625 | 626 | this.setState({ 627 | originX: offsetLeft, 628 | originY: offsetTop, 629 | dragging: false, 630 | frameWidth: offsetWidth, 631 | frameHeight: offsetHeight, 632 | maxLeft: imgWidth - offsetWidth, 633 | maxTop: imgHeight - offsetHeight, 634 | action: null 635 | }, function () { 636 | var onChange = _this6.props.onChange; 637 | 638 | if (onChange) onChange(_this6.values()); 639 | }); 640 | } 641 | } 642 | }, { 643 | key: 'render', 644 | value: function render() { 645 | var _this7 = this; 646 | 647 | var _state10 = this.state, 648 | dragging = _state10.dragging, 649 | imgHeight = _state10.imgHeight, 650 | imgWidth = _state10.imgWidth, 651 | imgLoaded = _state10.imgLoaded, 652 | styles = _state10.styles, 653 | src = _state10.src; 654 | var disabled = this.props.disabled; 655 | 656 | 657 | var imageNode = React.createElement( 658 | 'div', 659 | { 660 | style: styles.source, 661 | ref: function ref(_ref6) { 662 | _this7.sourceNode = _ref6; 663 | } 664 | }, 665 | React.createElement('img', { 666 | crossOrigin: 'anonymous', 667 | src: src, 668 | width: imgWidth, 669 | height: imgHeight, 670 | ref: function ref(_ref5) { 671 | _this7.img = _ref5; 672 | }, 673 | style: deepExtend({}, styles.img, styles.source_img), 674 | onLoad: this.imgOnLoad.bind(this) 675 | }) 676 | ); 677 | // disabled cropper 678 | if (disabled) { 679 | return React.createElement( 680 | 'div', 681 | { 682 | style: deepExtend({}, styles.container, { 683 | 'position': 'relative', 684 | 'height': imgHeight 685 | }), 686 | ref: function ref(_ref7) { 687 | _this7.container = _ref7; 688 | } 689 | }, 690 | imageNode, 691 | React.createElement('div', { 692 | style: deepExtend({}, styles.modal, styles.modal_disabled) 693 | }) 694 | ); 695 | } 696 | 697 | return React.createElement( 698 | 'div', 699 | { 700 | onMouseDown: this.handleDragStart.bind(this), 701 | onTouchStart: this.handleDragStart.bind(this), 702 | style: deepExtend({}, styles.container, { 703 | 'position': 'relative', 704 | 'height': imgHeight 705 | }), 706 | ref: function ref(_ref10) { 707 | _this7.container = _ref10; 708 | } 709 | }, 710 | imageNode, 711 | imgLoaded ? React.createElement( 712 | 'div', 713 | null, 714 | React.createElement('div', { 715 | style: styles.modal 716 | }), 717 | React.createElement( 718 | 'div', 719 | { 720 | style: deepExtend({}, styles.frame, dragging ? styles.dragging_frame : {}, { 721 | display: 'block', 722 | left: this.state.toImgLeft4Style, 723 | top: this.state.toImgTop4Style, 724 | width: this.state.frameWidth4Style, 725 | height: this.state.frameHeight4Style 726 | }), 727 | ref: function ref(_ref9) { 728 | _this7.frameNode = _ref9; 729 | } 730 | }, 731 | React.createElement( 732 | 'div', 733 | { 734 | style: styles.clone 735 | }, 736 | React.createElement('img', { 737 | src: src, 738 | crossOrigin: 'anonymous', 739 | width: imgWidth, 740 | height: imgHeight, 741 | style: deepExtend({}, styles.img, { 742 | marginLeft: -1 * this.state.toImgLeft4Style, 743 | marginTop: -1 * this.state.toImgTop4Style 744 | }), 745 | ref: function ref(_ref8) { 746 | _this7.cloneImg = _ref8; 747 | } 748 | }) 749 | ), 750 | React.createElement('span', { 751 | 'data-action': 'move', 752 | style: styles.move 753 | }), 754 | React.createElement( 755 | 'span', 756 | { 757 | 'data-action': 'move', 758 | style: deepExtend({}, styles.dot, styles.dotCenter) 759 | }, 760 | React.createElement('span', { 761 | style: deepExtend({}, styles.dotInner, styles.dotInnerCenterVertical) 762 | }), 763 | React.createElement('span', { 764 | style: deepExtend({}, styles.dotInner, styles.dotInnerCenterHorizontal) 765 | }) 766 | ), 767 | React.createElement( 768 | 'span', 769 | { 770 | 'data-action': 'ne', 771 | style: deepExtend({}, styles.dot, styles.dotNE) 772 | }, 773 | React.createElement('span', { 774 | style: deepExtend({}, styles.dotInner, styles.dotInnerNE) 775 | }) 776 | ), 777 | React.createElement( 778 | 'span', 779 | { 780 | 'data-action': 'n', 781 | style: deepExtend({}, styles.dot, styles.dotN) 782 | }, 783 | React.createElement('span', { 784 | style: deepExtend({}, styles.dotInner, styles.dotInnerN) 785 | }) 786 | ), 787 | React.createElement( 788 | 'span', 789 | { 790 | 'data-action': 'nw', 791 | style: deepExtend({}, styles.dot, styles.dotNW) 792 | }, 793 | React.createElement('span', { 794 | style: deepExtend({}, styles.dotInner, styles.dotInnerNW) 795 | }) 796 | ), 797 | React.createElement( 798 | 'span', 799 | { 800 | 'data-action': 'e', 801 | style: deepExtend({}, styles.dot, styles.dotE) 802 | }, 803 | React.createElement('span', { 804 | style: deepExtend({}, styles.dotInner, styles.dotInnerE) 805 | }) 806 | ), 807 | React.createElement( 808 | 'span', 809 | { 810 | 'data-action': 'w', 811 | style: deepExtend({}, styles.dot, styles.dotW) 812 | }, 813 | React.createElement('span', { 814 | style: deepExtend({}, styles.dotInner, styles.dotInnerW) 815 | }) 816 | ), 817 | React.createElement( 818 | 'span', 819 | { 820 | 'data-action': 'se', 821 | style: deepExtend({}, styles.dot, styles.dotSE) 822 | }, 823 | React.createElement('span', { 824 | style: deepExtend({}, styles.dotInner, styles.dotInnerSE) 825 | }) 826 | ), 827 | React.createElement( 828 | 'span', 829 | { 830 | 'data-action': 's', 831 | style: deepExtend({}, styles.dot, styles.dotS) 832 | }, 833 | React.createElement('span', { 834 | style: deepExtend({}, styles.dotInner, styles.dotInnerS) 835 | }) 836 | ), 837 | React.createElement( 838 | 'span', 839 | { 840 | 'data-action': 'sw', 841 | style: deepExtend({}, styles.dot, styles.dotSW) 842 | }, 843 | React.createElement('span', { 844 | style: deepExtend({}, styles.dotInner, styles.dotInnerSW) 845 | }) 846 | ), 847 | React.createElement('span', { 848 | 'data-action': 'n', 849 | style: deepExtend({}, styles.line, styles.lineN) 850 | }), 851 | React.createElement('span', { 852 | 'data-action': 's', 853 | style: deepExtend({}, styles.line, styles.lineS) 854 | }), 855 | React.createElement('span', { 856 | 'data-action': 'w', 857 | style: deepExtend({}, styles.line, styles.lineW) 858 | }), 859 | React.createElement('span', { 860 | 'data-action': 'e', 861 | style: deepExtend({}, styles.line, styles.lineE) 862 | }) 863 | ) 864 | ) : null 865 | ); 866 | } 867 | }]); 868 | 869 | return Cropper; 870 | }(Component); 871 | 872 | Cropper.propTypes = { 873 | src: PropTypes.string.isRequired, 874 | originX: PropTypes.number, 875 | originY: PropTypes.number, 876 | ratio: PropTypes.number, 877 | width: PropTypes.number, 878 | height: PropTypes.number, 879 | fixedRatio: PropTypes.bool, 880 | allowNewSelection: PropTypes.bool, 881 | disabled: PropTypes.bool, 882 | styles: PropTypes.object, 883 | onImgLoad: PropTypes.func, 884 | beforeImgLoad: PropTypes.func, 885 | onChange: PropTypes.func 886 | }; 887 | 888 | Cropper.defaultProps = { 889 | width: 200, 890 | height: 200, 891 | fixedRatio: true, 892 | allowNewSelection: true, 893 | ratio: 1, 894 | originX: 0, 895 | originY: 0, 896 | styles: {}, 897 | onImgLoad: function onImgLoad() {}, 898 | beforeImgLoad: function beforeImgLoad() {} 899 | 900 | /* 901 | default inline styles 902 | */ 903 | };var defaultStyles = { 904 | container: {}, 905 | img: { 906 | userDrag: 'none', 907 | userSelect: 'none', 908 | MozUserSelect: 'none', 909 | WebkitUserDrag: 'none', 910 | WebkitUserSelect: 'none', 911 | WebkitTransform: 'translateZ(0)', 912 | WebkitPerspective: 1000, 913 | WebkitBackfaceVisibility: 'hidden' 914 | }, 915 | 916 | clone: { 917 | width: '100%', 918 | height: '100%', 919 | overflow: 'hidden', 920 | position: 'absolute', 921 | left: 0, 922 | top: 0 923 | }, 924 | 925 | frame: { 926 | position: 'absolute', 927 | left: 0, 928 | top: 0, 929 | bottom: 0, 930 | right: 0, 931 | display: 'none' 932 | }, 933 | 934 | dragging_frame: { 935 | opacity: 0.8 936 | }, 937 | 938 | source: { 939 | overflow: 'hidden' 940 | }, 941 | 942 | source_img: { 943 | float: 'left' 944 | }, 945 | 946 | modal: { 947 | position: 'absolute', 948 | left: 0, 949 | top: 0, 950 | bottom: 0, 951 | right: 0, 952 | opacity: 0.4, 953 | backgroundColor: '#000' 954 | }, 955 | modal_disabled: { 956 | backgroundColor: '#666', 957 | opacity: 0.7, 958 | cursor: 'not-allowed' 959 | }, 960 | move: { 961 | position: 'absolute', 962 | left: 0, 963 | top: 0, 964 | bottom: 0, 965 | right: 0, 966 | cursor: 'move', 967 | outline: '1px dashed #88f', 968 | backgroundColor: 'transparent' 969 | }, 970 | 971 | dot: { 972 | zIndex: 10 973 | }, 974 | dotN: { 975 | cursor: 'n-resize' 976 | }, 977 | dotS: { 978 | cursor: 's-resize' 979 | }, 980 | dotE: { 981 | cursor: 'e-resize' 982 | }, 983 | dotW: { 984 | cursor: 'w-resize' 985 | }, 986 | dotNW: { 987 | cursor: 'nw-resize' 988 | }, 989 | dotNE: { 990 | cursor: 'ne-resize' 991 | }, 992 | dotSW: { 993 | cursor: 'sw-resize' 994 | }, 995 | dotSE: { 996 | cursor: 'se-resize' 997 | }, 998 | dotCenter: { 999 | backgroundColor: 'transparent', 1000 | cursor: 'move' 1001 | }, 1002 | 1003 | dotInner: { 1004 | border: '1px solid #88f', 1005 | background: '#fff', 1006 | display: 'block', 1007 | width: 6, 1008 | height: 6, 1009 | padding: 0, 1010 | margin: 0, 1011 | position: 'absolute' 1012 | }, 1013 | 1014 | dotInnerN: { 1015 | top: -4, 1016 | left: '50%', 1017 | marginLeft: -4 1018 | }, 1019 | dotInnerS: { 1020 | bottom: -4, 1021 | left: '50%', 1022 | marginLeft: -4 1023 | }, 1024 | dotInnerE: { 1025 | right: -4, 1026 | top: '50%', 1027 | marginTop: -4 1028 | }, 1029 | dotInnerW: { 1030 | left: -4, 1031 | top: '50%', 1032 | marginTop: -4 1033 | }, 1034 | dotInnerNE: { 1035 | top: -4, 1036 | right: -4 1037 | }, 1038 | dotInnerSE: { 1039 | bottom: -4, 1040 | right: -4 1041 | }, 1042 | dotInnerNW: { 1043 | top: -4, 1044 | left: -4 1045 | }, 1046 | dotInnerSW: { 1047 | bottom: -4, 1048 | left: -4 1049 | }, 1050 | dotInnerCenterVertical: { 1051 | position: 'absolute', 1052 | border: 'none', 1053 | width: 2, 1054 | height: 8, 1055 | backgroundColor: '#88f', 1056 | top: '50%', 1057 | left: '50%', 1058 | marginLeft: -1, 1059 | marginTop: -4 1060 | }, 1061 | dotInnerCenterHorizontal: { 1062 | position: 'absolute', 1063 | border: 'none', 1064 | width: 8, 1065 | height: 2, 1066 | backgroundColor: '#88f', 1067 | top: '50%', 1068 | left: '50%', 1069 | marginLeft: -4, 1070 | marginTop: -1 1071 | }, 1072 | 1073 | line: { 1074 | position: 'absolute', 1075 | display: 'block', 1076 | zIndex: 100 1077 | }, 1078 | 1079 | lineS: { 1080 | cursor: 's-resize', 1081 | bottom: 0, 1082 | left: 0, 1083 | width: '100%', 1084 | height: 4, 1085 | background: 'transparent' 1086 | }, 1087 | lineN: { 1088 | cursor: 'n-resize', 1089 | top: 0, 1090 | left: 0, 1091 | width: '100%', 1092 | height: 4, 1093 | background: 'transparent' 1094 | }, 1095 | lineE: { 1096 | cursor: 'e-resize', 1097 | right: 0, 1098 | top: 0, 1099 | width: 4, 1100 | height: '100%', 1101 | background: 'transparent' 1102 | }, 1103 | lineW: { 1104 | cursor: 'w-resize', 1105 | left: 0, 1106 | top: 0, 1107 | width: 4, 1108 | height: '100%', 1109 | background: 'transparent' 1110 | } 1111 | }; 1112 | 1113 | module.exports = Cropper; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-image-cropper", 3 | "version": "1.3.0", 4 | "description": "react image crop", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "npm-run-all --parallel babel server", 8 | "server": "cross-env NODE_ENV=dev node server.js", 9 | "babel": "babel --presets es2015,react --watch component/ --out-dir lib/", 10 | "build": "webpack -p", 11 | "lint": "./node_modules/.bin/eslint ./component" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/jerryshew/react-image-cropper" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "image crop", 20 | "front crop", 21 | "canvas", 22 | "react component" 23 | ], 24 | "author": "jerryshew", 25 | "license": "MIT", 26 | "homepage": "https://github.com", 27 | "dependencies": { 28 | "deep-extend": "^0.4.1", 29 | "npm-run-all": "^4.1.1", 30 | "prop-types": "^15.6.0", 31 | "react": "^16.2.0", 32 | "react-dom": "^16.2.0" 33 | }, 34 | "pre-commit": [ 35 | "lint" 36 | ], 37 | "devDependencies": { 38 | "autoprefixer": "^7.1.1", 39 | "babel-cli": "^6.18.0", 40 | "babel-core": "^6.18.2", 41 | "babel-loader": "^6.0.0", 42 | "babel-plugin-react-transform": "^1.1.1", 43 | "babel-polyfill": "^6.23.0", 44 | "babel-preset-es2015": "^6.6.0", 45 | "babel-preset-react": "^6.5.0", 46 | "babel-runtime": "^6.0.0", 47 | "cross-env": "^3.1.3", 48 | "css-loader": "^0.28.4", 49 | "eslint": "^4.16.0", 50 | "eslint-config-standard": "^11.0.0-beta.0", 51 | "eslint-plugin-import": "^2.8.0", 52 | "eslint-plugin-node": "^5.2.1", 53 | "eslint-plugin-promise": "^3.6.0", 54 | "eslint-plugin-react": "^7.6.1", 55 | "eslint-plugin-standard": "^3.0.1", 56 | "express": "^4.13.3", 57 | "extract-text-webpack-plugin": "^2.1.2", 58 | "file-loader": "^0.8.5", 59 | "force-case-sensitivity-webpack-plugin": "^0.1.1", 60 | "html-webpack-plugin": "^1.6.1", 61 | "less": "^2.7.2", 62 | "less-loader": "^4.0.5", 63 | "postcss-loader": "^2.0.6", 64 | "pre-commit": "^1.2.2", 65 | "progress-bar-webpack-plugin": "^1.9.3", 66 | "react-transform-hmr": "^1.0.0", 67 | "style-loader": "^0.12.3", 68 | "uglify-loader": "^1.2.0", 69 | "url-loader": "^0.5.7", 70 | "webpack": "^2.6.1", 71 | "webpack-dev-middleware": "^1.2.0", 72 | "webpack-hot-middleware": "^2.5.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var webpackDevMiddleware = require('webpack-dev-middleware'); 3 | var webpackHotMiddleware = require('webpack-hot-middleware'); 4 | var config = require('./webpack.config'); 5 | var express = require('express'); 6 | 7 | var app = new (express)(); 8 | var port = 3000; 9 | 10 | var compiler = webpack(config); 11 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); 12 | app.use(webpackHotMiddleware(compiler)); 13 | 14 | app.use(express.static('dist')); 15 | 16 | app.get("/", function(req, res) { 17 | res.sendFile(__dirname + '/index.html'); 18 | }); 19 | 20 | app.listen(port, function(error) { 21 | if (error) { 22 | console.error(error); 23 | } else { 24 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port); 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var ProgressBarPlugin = require('progress-bar-webpack-plugin'); 4 | var ForceCaseSensitivityPlugin = require('force-case-sensitivity-webpack-plugin'); 5 | var NODE_ENV = process.env.NODE_ENV 6 | var publicPath = '/dist/'; 7 | 8 | module.exports = { 9 | plugins: [ 10 | new ProgressBarPlugin(), 11 | new webpack.optimize.OccurrenceOrderPlugin(), 12 | new webpack.HotModuleReplacementPlugin(), 13 | new webpack.NoEmitOnErrorsPlugin(), 14 | new ForceCaseSensitivityPlugin() 15 | ], 16 | resolve: { 17 | alias: { 18 | "react": path.resolve('./node_modules/react'), 19 | "react-router-dom": path.resolve('./node_modules/react-router-dom') 20 | }, 21 | }, 22 | entry: { 23 | app: NODE_ENV === 'dev' ? 24 | [ 25 | "./demo/demo.js", 26 | "webpack-hot-middleware/client?path=/__webpack_hmr&timeout=2000&overlay=false" 27 | ] 28 | : './demo/demo.js', 29 | }, 30 | output: { 31 | path: path.join(__dirname, 'dist'), 32 | filename: 'app.js', 33 | publicPath: publicPath, 34 | }, 35 | 36 | module: { 37 | rules: [ 38 | { 39 | test: /\.css$/, 40 | use: [ 41 | 'style-loader', 42 | 'less-loader', 43 | { 44 | loader: 'postcss-loader', 45 | options: { 46 | plugins: [require('autoprefixer')] 47 | } 48 | } 49 | ], 50 | }, 51 | { 52 | test: /\.less$/, 53 | use: [ 54 | 'style-loader', 55 | 'css-loader', 56 | { 57 | loader: 'postcss-loader', 58 | options: { 59 | plugins: [require('autoprefixer')] 60 | } 61 | }, 62 | 'less-loader', 63 | ], 64 | }, 65 | { 66 | test: /\.(png|jpg)$/, 67 | use: 'url-loader?limit=8192&name=./image/[name].[ext]' 68 | }, { 69 | test: /\.js$/, 70 | exclude: /(node_modules|bower_components)/, 71 | use: "babel-loader", 72 | }, { 73 | test: /\.jsx?$/, 74 | exclude: /(node_modules|bower_components)/, 75 | use: 'babel-loader', 76 | }, { 77 | test: /\.(ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, 78 | use: 'url-loader?limit=10000&name=./font/[name].[ext]' 79 | } 80 | ], 81 | } 82 | }; --------------------------------------------------------------------------------