├── .gitignore ├── .gitattributes ├── src ├── images │ └── bg.png ├── js │ ├── template.js │ ├── constants.js │ ├── defaults.js │ ├── events.js │ ├── preview.js │ ├── handlers.js │ ├── cropper.js │ ├── change.js │ └── render.js └── css │ └── cropper.css ├── docs ├── images │ ├── data.jpg │ ├── layers.jpg │ ├── picture.jpg │ ├── picture-2.jpg │ └── picture-3.jpg ├── css │ ├── main.css │ └── cropper.css └── js │ └── main.js ├── .travis.yml ├── test ├── css │ └── main.css ├── options │ ├── ready.js │ ├── minContainerWidth.js │ ├── minContainerHeight.js │ ├── minCanvasWidth.js │ ├── minCanvasHeight.js │ ├── minCropBoxWidth.js │ ├── minCropBoxHeight.js │ ├── crop.js │ ├── cropend.js │ ├── cropmove.js │ ├── autoCrop.js │ ├── aspectRatio.js │ ├── modal.js │ ├── background.js │ ├── rotatable.js │ ├── data.js │ ├── center.js │ ├── zoomOnWheel.js │ ├── highlight.js │ ├── checkOrientation.js │ ├── cropBoxMovable.js │ ├── scalable.js │ ├── zoom.js │ ├── guides.js │ ├── toggleDragModeOnDblclick.js │ ├── movable.js │ ├── dragMode.js │ ├── zoomable.js │ ├── cropstart.js │ ├── checkCrossOrigin.js │ ├── cropBoxResizable.js │ └── viewMode.js ├── events │ ├── ready.js │ ├── crop.js │ ├── cropend.js │ ├── cropmove.js │ ├── zoom.js │ └── cropstart.js ├── methods │ ├── scaleX.js │ ├── scaleY.js │ ├── moveTo.js │ ├── scale.js │ ├── getContainerData.js │ ├── setAspectRatio.js │ ├── rotate.js │ ├── zoom.js │ ├── move.js │ ├── destroy.js │ ├── crop.js │ ├── replace.js │ ├── getCropBoxData.js │ ├── enable.js │ ├── rotateTo.js │ ├── zoomTo.js │ ├── getCanvasData.js │ ├── clear.js │ ├── setDragMode.js │ ├── reset.js │ ├── getImageData.js │ ├── getCroppedCanvas.js │ ├── setData.js │ ├── setCanvasData.js │ ├── getData.js │ ├── setCropBoxData.js │ └── disable.js ├── js │ └── main.js └── index.html ├── .babelrc ├── .editorconfig ├── .eslintrc ├── postcss.config.js ├── rollup.config.js ├── ISSUE_TEMPLATE.md ├── LICENSE ├── examples ├── multiple-croppers.html ├── full-crop-box.html ├── fixed-crop-box.html ├── responsive-container.html ├── crop-on-canvas.html ├── a-range-of-aspect-ratio.html ├── cropper-in-modal.html ├── crop-a-round-image.html ├── customize-preview.html └── mask-an-image.html ├── package.json ├── CONTRIBUTING.md ├── dist ├── cropper.min.css └── cropper.css └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.map 3 | _* 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/cropperjs/master/src/images/bg.png -------------------------------------------------------------------------------- /docs/images/data.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/cropperjs/master/docs/images/data.jpg -------------------------------------------------------------------------------- /docs/images/layers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/cropperjs/master/docs/images/layers.jpg -------------------------------------------------------------------------------- /docs/images/picture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/cropperjs/master/docs/images/picture.jpg -------------------------------------------------------------------------------- /docs/images/picture-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/cropperjs/master/docs/images/picture-2.jpg -------------------------------------------------------------------------------- /docs/images/picture-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polydice/cropperjs/master/docs/images/picture-3.jpg -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | script: 5 | - npm run lint 6 | - npm run build 7 | - npm test 8 | -------------------------------------------------------------------------------- /test/css/main.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 640px; 3 | max-height: 360px; 4 | margin: 20px auto; 5 | } 6 | 7 | .container > img { 8 | max-width: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["last 2 versions", "ie >= 9"] 7 | } 8 | }] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "browser": true 5 | }, 6 | "rules": { 7 | "no-param-reassign": "off", 8 | "no-restricted-properties": "off", 9 | "valid-jsdoc": ["error", { 10 | "requireReturn": false 11 | }] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const rollupConfig = require('./rollup.config'); 2 | 3 | module.exports = { 4 | plugins: { 5 | 'postcss-cssnext': {}, 6 | 'postcss-url': { 7 | url: 'inline', 8 | }, 9 | 'postcss-header': { 10 | header: rollupConfig.banner, 11 | }, 12 | stylefmt: {}, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /test/options/ready.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#ready', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | assert.ok(true); 11 | 12 | done(); 13 | } 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/events/ready.js: -------------------------------------------------------------------------------- 1 | QUnit.test('events#ready', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | image.addEventListener('ready', function () { 9 | assert.ok(true); 10 | 11 | done(); 12 | }); 13 | 14 | return new Cropper(image); 15 | }); 16 | -------------------------------------------------------------------------------- /test/methods/scaleX.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#scaleX', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var imageData = cropper.scaleX(-1).getImageData(); 12 | 13 | assert.strictEqual(imageData.scaleX, -1); 14 | 15 | done(); 16 | } 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/methods/scaleY.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#scaleY', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var imageData = cropper.scaleY(-1).getImageData(); 12 | 13 | assert.strictEqual(imageData.scaleY, -1); 14 | 15 | done(); 16 | } 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/methods/moveTo.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#moveTo', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(2); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var canvasData = cropper.moveTo(0, 0).getCanvasData(); 12 | 13 | assert.strictEqual(canvasData.left, 0); 14 | assert.strictEqual(canvasData.top, 0); 15 | 16 | done(); 17 | } 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/methods/scale.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#scale', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(2); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var imageData = cropper.scale(-1, -1).getImageData(); 12 | 13 | assert.strictEqual(imageData.scaleX, -1); 14 | assert.strictEqual(imageData.scaleY, -1); 15 | 16 | done(); 17 | } 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/methods/getContainerData.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#getContainerData', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(2); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var containerData = cropper.getContainerData(); 12 | 13 | assert.ok(util.isNumber(containerData.width)); 14 | assert.ok(util.isNumber(containerData.height)); 15 | 16 | done(); 17 | } 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/methods/setAspectRatio.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#setAspectRatio', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(2); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var options = cropper.options; 12 | 13 | assert.ok(isNaN(options.aspectRatio)); 14 | cropper.setAspectRatio(1); 15 | assert.strictEqual(options.aspectRatio, 1); 16 | 17 | done(); 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/methods/rotate.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#rotate', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(3); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | 12 | assert.strictEqual(cropper.rotate(360).getImageData().rotate, 0); 13 | assert.strictEqual(cropper.rotate(90).getImageData().rotate, 90); 14 | assert.strictEqual(cropper.rotate(-180).getImageData().rotate, -90); 15 | 16 | done(); 17 | } 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/methods/zoom.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#zoom', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(2); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var canvasData = cropper.getCanvasData(); 12 | var changedCanvasData = cropper.zoom(0.1).getCanvasData(); 13 | 14 | assert.ok(changedCanvasData.width > canvasData.width); 15 | assert.ok(changedCanvasData.height > canvasData.height); 16 | 17 | done(); 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/options/minContainerWidth.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#minContainerWidth', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | var minContainerWidth = 641; 6 | 7 | assert.expect(1); 8 | 9 | return new Cropper(image, { 10 | minContainerWidth: minContainerWidth, 11 | 12 | ready: function () { 13 | var cropper = this.cropper; 14 | var containerData = cropper.getContainerData(); 15 | 16 | assert.strictEqual(Math.round(containerData.width), minContainerWidth); 17 | 18 | done(); 19 | } 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/options/minContainerHeight.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#minContainerHeight', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | var minContainerHeight = 361; 6 | 7 | assert.expect(1); 8 | 9 | return new Cropper(image, { 10 | minContainerHeight: minContainerHeight, 11 | 12 | ready: function () { 13 | var cropper = this.cropper; 14 | var containerData = cropper.getContainerData(); 15 | 16 | assert.strictEqual(Math.round(containerData.height), minContainerHeight); 17 | 18 | done(); 19 | } 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/methods/move.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#move', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(2); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var canvasData = cropper.getCanvasData(); 12 | var changedCanvasData = cropper.move(1, 1).getCanvasData(); 13 | 14 | assert.strictEqual(changedCanvasData.left, canvasData.left + 1); 15 | assert.strictEqual(changedCanvasData.top, canvasData.top + 1); 16 | 17 | done(); 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/methods/destroy.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#destroy', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(4); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | 12 | assert.ok(typeof cropper === 'object'); 13 | assert.ok(util.hasClass(image, 'cropper-hidden')); 14 | 15 | cropper.destroy(); 16 | assert.ok(typeof this.cropper === 'undefined'); 17 | assert.notOk(util.hasClass(image, 'cropper-hidden')); 18 | 19 | done(); 20 | } 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/methods/crop.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#crop', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(4); 7 | 8 | return new Cropper(image, { 9 | autoCrop: false, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | 14 | assert.notOk(cropper.cropped); 15 | assert.ok(util.hasClass(cropper.cropBox, 'cropper-hidden')); 16 | 17 | cropper.crop(); 18 | assert.ok(cropper.cropped); 19 | assert.notOk(util.hasClass(cropper.cropBox, 'cropper-hidden')); 20 | 21 | done(); 22 | } 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/methods/replace.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#replace', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(2); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | 12 | cropper.options.ready = function () { 13 | assert.notOk(cropper.cropped); 14 | cropper.crop(); 15 | assert.ok(cropper.cropped); 16 | done(); 17 | }; 18 | 19 | cropper.options.autoCrop = false; 20 | cropper.replace('../docs/images/picture-2.jpg'); 21 | } 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/options/minCanvasWidth.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#minCanvasWidth', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | var minCanvasWidth = 480; 6 | 7 | assert.expect(1); 8 | 9 | return new Cropper(image, { 10 | minCanvasWidth: minCanvasWidth, 11 | 12 | ready: function () { 13 | var cropper = this.cropper; 14 | var canvasData = cropper.setCanvasData({ 15 | width: 320 16 | }).getCanvasData(); 17 | 18 | assert.strictEqual(Math.round(canvasData.width), minCanvasWidth); 19 | 20 | done(); 21 | } 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/methods/getCropBoxData.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#getCropBoxData', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(4); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var cropBoxData = cropper.getCropBoxData(); 12 | 13 | assert.ok(util.isNumber(cropBoxData.left)); 14 | assert.ok(util.isNumber(cropBoxData.top)); 15 | assert.ok(util.isNumber(cropBoxData.width)); 16 | assert.ok(util.isNumber(cropBoxData.height)); 17 | 18 | done(); 19 | } 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/methods/enable.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#enable', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(4); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | 12 | cropper.disable(); 13 | assert.ok(cropper.disabled); 14 | assert.ok(util.hasClass(cropper.cropper, 'cropper-disabled')); 15 | 16 | cropper.enable(); 17 | assert.notOk(cropper.disabled); 18 | assert.notOk(util.hasClass(cropper.cropper, 'cropper-disabled')); 19 | 20 | done(); 21 | } 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/options/minCanvasHeight.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#minCanvasHeight', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | var minCanvasHeight = 270; 6 | 7 | assert.expect(1); 8 | 9 | return new Cropper(image, { 10 | minCanvasHeight: minCanvasHeight, 11 | 12 | ready: function () { 13 | var cropper = this.cropper; 14 | var canvasData = cropper.setCanvasData({ 15 | height: 180 16 | }).getCanvasData(); 17 | 18 | assert.strictEqual(Math.round(canvasData.height), minCanvasHeight); 19 | 20 | done(); 21 | } 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/options/minCropBoxWidth.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#minCropBoxWidth', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | var minCropBoxWidth = 300; 6 | 7 | assert.expect(1); 8 | 9 | return new Cropper(image, { 10 | minCropBoxWidth: minCropBoxWidth, 11 | 12 | ready: function () { 13 | var cropper = this.cropper; 14 | var cropBoxData = cropper.setCropBoxData({ 15 | width: 200 16 | }).getCropBoxData(); 17 | 18 | assert.strictEqual(Math.round(cropBoxData.width), minCropBoxWidth); 19 | 20 | done(); 21 | } 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/options/minCropBoxHeight.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#minCropBoxHeight', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | var minCropBoxHeight = 150; 6 | 7 | assert.expect(1); 8 | 9 | return new Cropper(image, { 10 | minCropBoxHeight: minCropBoxHeight, 11 | 12 | ready: function () { 13 | var cropper = this.cropper; 14 | var cropBoxData = cropper.setCropBoxData({ 15 | height: 100 16 | }).getCropBoxData(); 17 | 18 | assert.strictEqual(Math.round(cropBoxData.height), minCropBoxHeight); 19 | 20 | done(); 21 | } 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/events/crop.js: -------------------------------------------------------------------------------- 1 | QUnit.test('events#crop', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(7); 7 | 8 | image.addEventListener('crop', function (e) { 9 | assert.ok(util.isNumber(e.detail.x)); 10 | assert.ok(util.isNumber(e.detail.y)); 11 | assert.ok(util.isNumber(e.detail.width)); 12 | assert.ok(util.isNumber(e.detail.height)); 13 | assert.ok(util.isNumber(e.detail.rotate)); 14 | assert.ok(util.isNumber(e.detail.scaleX)); 15 | assert.ok(util.isNumber(e.detail.scaleY)); 16 | 17 | done(); 18 | }); 19 | 20 | return new Cropper(image); 21 | }); 22 | -------------------------------------------------------------------------------- /test/options/crop.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#crop', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(7); 7 | 8 | return new Cropper(image, { 9 | crop: function (e) { 10 | assert.ok(util.isNumber(e.detail.x)); 11 | assert.ok(util.isNumber(e.detail.y)); 12 | assert.ok(util.isNumber(e.detail.width)); 13 | assert.ok(util.isNumber(e.detail.height)); 14 | assert.ok(util.isNumber(e.detail.rotate)); 15 | assert.ok(util.isNumber(e.detail.scaleX)); 16 | assert.ok(util.isNumber(e.detail.scaleY)); 17 | 18 | done(); 19 | } 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/methods/rotateTo.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#rotateTo', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(4); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | 12 | assert.strictEqual(cropper.rotateTo(360).getImageData().rotate, 0); 13 | assert.strictEqual(cropper.rotateTo(90).getImageData().rotate, 90); 14 | assert.strictEqual(cropper.rotateTo(0).getImageData().rotate, 0); 15 | assert.strictEqual(cropper.rotateTo(-180).getImageData().rotate, -180); 16 | 17 | done(); 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/methods/zoomTo.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#zoomTo', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(3); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var imageData = cropper.zoomTo(1).getImageData(); 12 | var canvasData = cropper.getCanvasData(); 13 | 14 | assert.strictEqual(imageData.width, imageData.naturalWidth); 15 | assert.strictEqual(canvasData.width, canvasData.naturalWidth); 16 | assert.strictEqual(canvasData.naturalWidth, imageData.naturalWidth); 17 | 18 | done(); 19 | } 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/methods/getCanvasData.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#getCanvasData', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(6); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var canvasData = cropper.getCanvasData(); 12 | 13 | assert.ok(util.isNumber(canvasData.left)); 14 | assert.ok(util.isNumber(canvasData.top)); 15 | assert.ok(util.isNumber(canvasData.width)); 16 | assert.ok(util.isNumber(canvasData.height)); 17 | assert.ok(util.isNumber(canvasData.naturalWidth)); 18 | assert.ok(util.isNumber(canvasData.naturalHeight)); 19 | 20 | done(); 21 | } 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/methods/clear.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#clear', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(4); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | 12 | cropper.clear(); 13 | assert.notOk(cropper.cropped); 14 | assert.deepEqual(cropper.getData(), { 15 | x: 0, 16 | y: 0, 17 | width: 0, 18 | height: 0, 19 | rotate: 0, 20 | scaleX: 1, 21 | scaleY: 1 22 | }); 23 | assert.deepEqual(cropper.getCropBoxData(), {}); 24 | assert.ok(util.hasClass(cropper.cropBox, 'cropper-hidden')); 25 | 26 | done(); 27 | } 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/options/cropend.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#cropend', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var PointerEvent = window.PointerEvent; 11 | var cropper = this.cropper; 12 | 13 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerdown' : 'mousedown'); 14 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointermove' : 'mousemove'); 15 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerup' : 'mouseup'); 16 | 17 | done(); 18 | }, 19 | 20 | cropend: function (e) { 21 | assert.strictEqual(e.detail.action, 'crop'); 22 | } 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/options/cropmove.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#cropmove', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var PointerEvent = window.PointerEvent; 11 | var cropper = this.cropper; 12 | 13 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerdown' : 'mousedown'); 14 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointermove' : 'mousemove'); 15 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerup' : 'mouseup'); 16 | 17 | done(); 18 | }, 19 | 20 | cropmove: function (e) { 21 | assert.strictEqual(e.detail.action, 'crop'); 22 | } 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/methods/setDragMode.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#setDragMode', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(4); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var dragBox = cropper.dragBox; 12 | 13 | assert.strictEqual(dragBox.dataset.action, 'crop'); 14 | 15 | cropper.setDragMode('move'); 16 | assert.strictEqual(dragBox.dataset.action, 'move'); 17 | 18 | cropper.setDragMode('crop'); 19 | assert.strictEqual(dragBox.dataset.action, 'crop'); 20 | 21 | cropper.setDragMode('none'); 22 | assert.strictEqual(dragBox.dataset.action, 'none'); 23 | 24 | done(); 25 | } 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/events/cropend.js: -------------------------------------------------------------------------------- 1 | QUnit.test('events#cropend', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | image.addEventListener('ready', function () { 9 | var PointerEvent = window.PointerEvent; 10 | var cropper = this.cropper; 11 | 12 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerdown' : 'mousedown'); 13 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointermove' : 'mousemove'); 14 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerup' : 'mouseup'); 15 | 16 | done(); 17 | }); 18 | 19 | image.addEventListener('cropend', function (e) { 20 | assert.strictEqual(e.detail.action, 'crop'); 21 | }); 22 | 23 | return new Cropper(image); 24 | }); 25 | -------------------------------------------------------------------------------- /test/events/cropmove.js: -------------------------------------------------------------------------------- 1 | QUnit.test('events#cropmove', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | image.addEventListener('ready', function () { 9 | var PointerEvent = window.PointerEvent; 10 | var cropper = this.cropper; 11 | 12 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerdown' : 'mousedown'); 13 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointermove' : 'mousemove'); 14 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerup' : 'mouseup'); 15 | 16 | done(); 17 | }); 18 | 19 | image.addEventListener('cropmove', function (e) { 20 | assert.strictEqual(e.detail.action, 'crop'); 21 | }); 22 | 23 | return new Cropper(image); 24 | }); 25 | -------------------------------------------------------------------------------- /test/options/autoCrop.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#autoCrop: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | // autoCrop: true, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | 14 | assert.ok(cropper.cropped); 15 | 16 | done(); 17 | } 18 | }); 19 | }); 20 | 21 | QUnit.test('options#autoCrop: false', function (assert) { 22 | var done = assert.async(); 23 | var util = window.Util; 24 | var image = util.createImage(); 25 | 26 | assert.expect(1); 27 | 28 | return new Cropper(image, { 29 | autoCrop: false, 30 | 31 | ready: function () { 32 | var cropper = this.cropper; 33 | 34 | assert.notOk(cropper.cropped); 35 | 36 | done(); 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/options/aspectRatio.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#aspectRatio: NaN', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | // aspectRatio: NaN, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | 14 | assert.ok(isNaN(cropper.options.aspectRatio)); 15 | 16 | done(); 17 | } 18 | }); 19 | }); 20 | 21 | QUnit.test('options#aspectRatio: 1', function (assert) { 22 | var done = assert.async(); 23 | var util = window.Util; 24 | var image = util.createImage(); 25 | 26 | assert.expect(1); 27 | 28 | return new Cropper(image, { 29 | aspectRatio: 1, 30 | 31 | ready: function () { 32 | var cropper = this.cropper; 33 | 34 | assert.strictEqual(cropper.options.aspectRatio, 1); 35 | 36 | done(); 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/options/modal.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#modal: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | // modal: true, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | 14 | assert.ok(util.hasClass(cropper.dragBox, 'cropper-modal')); 15 | 16 | done(); 17 | } 18 | }); 19 | }); 20 | 21 | QUnit.test('options#modal: false', function (assert) { 22 | var done = assert.async(); 23 | var util = window.Util; 24 | var image = util.createImage(); 25 | 26 | assert.expect(1); 27 | 28 | return new Cropper(image, { 29 | modal: false, 30 | 31 | ready: function () { 32 | var cropper = this.cropper; 33 | 34 | assert.notOk(util.hasClass(cropper.dragBox, 'cropper-modal')); 35 | 36 | done(); 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/options/background.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#background: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | // background: true, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | 14 | assert.ok(util.hasClass(cropper.cropper, 'cropper-bg')); 15 | 16 | done(); 17 | } 18 | }); 19 | }); 20 | 21 | QUnit.test('options#background: false', function (assert) { 22 | var done = assert.async(); 23 | var util = window.Util; 24 | var image = util.createImage(); 25 | 26 | assert.expect(1); 27 | 28 | return new Cropper(image, { 29 | background: false, 30 | 31 | ready: function () { 32 | var cropper = this.cropper; 33 | 34 | assert.notOk(util.hasClass(cropper.cropper, 'cropper-bg')); 35 | 36 | done(); 37 | } 38 | }); 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const babel = require('rollup-plugin-babel'); 2 | const pkg = require('./package'); 3 | 4 | const now = new Date(); 5 | 6 | module.exports = { 7 | input: 'src/js/cropper.js', 8 | output: [ 9 | { 10 | file: 'dist/cropper.js', 11 | format: 'umd', 12 | }, 13 | { 14 | file: 'dist/cropper.common.js', 15 | format: 'cjs', 16 | }, 17 | { 18 | file: 'dist/cropper.esm.js', 19 | format: 'es', 20 | }, 21 | { 22 | file: 'docs/js/cropper.js', 23 | format: 'umd', 24 | }, 25 | ], 26 | name: 'Cropper', 27 | plugins: [ 28 | babel({ 29 | exclude: 'node_modules/**', 30 | }), 31 | ], 32 | banner: `/*! 33 | * Cropper.js v${pkg.version} 34 | * https://github.com/${pkg.repository} 35 | * 36 | * Copyright (c) 2015-${now.getFullYear()} ${pkg.author.name} 37 | * Released under the ${pkg.license} license 38 | * 39 | * Date: ${now.toISOString()} 40 | */ 41 | `, 42 | }; 43 | -------------------------------------------------------------------------------- /test/options/rotatable.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#rotatable: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | // rotatable: true, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | 14 | assert.strictEqual(cropper.rotate(90).getImageData().rotate, 90); 15 | 16 | done(); 17 | } 18 | }); 19 | }); 20 | 21 | QUnit.test('options#rotatable: false', function (assert) { 22 | var done = assert.async(); 23 | var util = window.Util; 24 | var image = util.createImage(); 25 | 26 | assert.expect(1); 27 | 28 | return new Cropper(image, { 29 | rotatable: false, 30 | 31 | ready: function () { 32 | var cropper = this.cropper; 33 | 34 | assert.strictEqual(cropper.rotate(90).getImageData().rotate, undefined); 35 | 36 | done(); 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/methods/reset.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#reset', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(4); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var canvasData = cropper.getCanvasData(); 12 | var cropBoxData = cropper.getCropBoxData(); 13 | 14 | cropper.setCanvasData({ 15 | top: canvasData.top + 10, 16 | width: canvasData.width - 10 17 | }); 18 | 19 | assert.notDeepEqual(cropper.getCanvasData(), canvasData); 20 | 21 | cropper.setCropBoxData({ 22 | left: cropBoxData.left + 10, 23 | height: cropBoxData.height - 10 24 | }); 25 | 26 | assert.notDeepEqual(cropper.getCropBoxData(), cropBoxData); 27 | 28 | cropper.reset(); 29 | assert.deepEqual(cropper.getCanvasData(), canvasData); 30 | assert.deepEqual(cropper.getCropBoxData(), cropBoxData); 31 | 32 | done(); 33 | } 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/options/data.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#data', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | var initialData = { 6 | x: 360, 7 | y: 450, 8 | width: 640, 9 | height: 360, 10 | rotate: 45, 11 | scaleX: -1, 12 | scaleY: -1 13 | }; 14 | 15 | assert.expect(7); 16 | 17 | return new Cropper(image, { 18 | data: initialData, 19 | 20 | ready: function () { 21 | var cropper = this.cropper; 22 | var data = cropper.getData(true); 23 | 24 | assert.strictEqual(data.x, initialData.x); 25 | assert.strictEqual(data.y, initialData.y); 26 | assert.strictEqual(data.width, initialData.width); 27 | assert.strictEqual(data.height, initialData.height); 28 | assert.strictEqual(data.rotate, initialData.rotate); 29 | assert.strictEqual(data.scaleX, initialData.scaleX); 30 | assert.strictEqual(data.scaleY, initialData.scaleY); 31 | 32 | done(); 33 | } 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/options/center.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#center: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | // center: true, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | var center = util.getByClass(cropper.cropBox, 'cropper-center'); 14 | 15 | assert.notOk(util.hasClass(center[0], 'cropper-hidden')); 16 | 17 | done(); 18 | } 19 | }); 20 | }); 21 | 22 | QUnit.test('options#center: false', function (assert) { 23 | var done = assert.async(); 24 | var util = window.Util; 25 | var image = util.createImage(); 26 | 27 | assert.expect(1); 28 | 29 | return new Cropper(image, { 30 | center: false, 31 | 32 | ready: function () { 33 | var cropper = this.cropper; 34 | var center = util.getByClass(cropper.cropBox, 'cropper-center'); 35 | 36 | assert.ok(util.hasClass(center[0], 'cropper-hidden')); 37 | 38 | done(); 39 | } 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/options/zoomOnWheel.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#zoomOnWheel: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | // zoomOnWheel: true, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | 14 | util.dispatchEvent(cropper.cropper, 'wheel'); 15 | 16 | done(); 17 | }, 18 | 19 | zoom: function () { 20 | assert.ok(true); 21 | } 22 | }); 23 | }); 24 | 25 | QUnit.test('options#zoomOnWheel: false', function (assert) { 26 | var done = assert.async(); 27 | var util = window.Util; 28 | var image = util.createImage(); 29 | 30 | assert.expect(0); 31 | 32 | return new Cropper(image, { 33 | zoomOnWheel: false, 34 | 35 | ready: function () { 36 | var cropper = this.cropper; 37 | 38 | util.dispatchEvent(cropper.cropper, 'wheel'); 39 | 40 | done(); 41 | }, 42 | 43 | zoom: function () { 44 | assert.ok(false); 45 | } 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/options/highlight.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#highlight: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | // highlight: true, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | var face = util.getByClass(cropper.cropBox, 'cropper-face'); 14 | 15 | assert.notOk(util.hasClass(face[0], 'cropper-invisible')); 16 | 17 | done(); 18 | } 19 | }); 20 | }); 21 | 22 | QUnit.test('options#highlight: false', function (assert) { 23 | var done = assert.async(); 24 | var util = window.Util; 25 | var image = util.createImage(); 26 | 27 | assert.expect(1); 28 | 29 | return new Cropper(image, { 30 | highlight: false, 31 | 32 | ready: function () { 33 | var cropper = this.cropper; 34 | var face = util.getByClass(cropper.cropBox, 'cropper-face'); 35 | 36 | assert.ok(util.hasClass(face[0], 'cropper-invisible')); 37 | 38 | done(); 39 | } 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/options/checkOrientation.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#checkOrientation: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage({ 5 | src: '../docs/images/picture-3.jpg' 6 | }); 7 | 8 | assert.expect(1); 9 | 10 | return new Cropper(image, { 11 | // checkOrientation: true, 12 | 13 | ready: function () { 14 | var cropper = this.cropper; 15 | 16 | assert.notStrictEqual(cropper.getData().rotate, 0); 17 | 18 | done(); 19 | } 20 | }); 21 | }); 22 | 23 | QUnit.test('options#checkOrientation: false', function (assert) { 24 | var done = assert.async(); 25 | var util = window.Util; 26 | var image = util.createImage({ 27 | src: '../docs/images/picture-3.jpg' 28 | }); 29 | 30 | assert.expect(1); 31 | 32 | return new Cropper(image, { 33 | checkOrientation: false, 34 | 35 | ready: function () { 36 | var cropper = this.cropper; 37 | 38 | assert.strictEqual(cropper.getData().rotate, 0); 39 | 40 | done(); 41 | } 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/methods/getImageData.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#getImageData', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(10); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var imageData = cropper.getImageData(); 12 | 13 | assert.ok(util.isNumber(imageData.naturalWidth)); 14 | assert.ok(util.isNumber(imageData.naturalHeight)); 15 | assert.ok(util.isNumber(imageData.aspectRatio)); 16 | assert.ok(util.isNumber(imageData.left)); 17 | assert.ok(util.isNumber(imageData.top)); 18 | assert.ok(util.isNumber(imageData.width)); 19 | assert.ok(util.isNumber(imageData.height)); 20 | 21 | imageData = cropper.rotateTo(45).getImageData(); 22 | assert.strictEqual(imageData.rotate, 45); 23 | 24 | imageData = cropper.scale(-1, -1).getImageData(); 25 | assert.strictEqual(imageData.scaleX, -1); 26 | assert.strictEqual(imageData.scaleY, -1); 27 | 28 | done(); 29 | } 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/options/cropBoxMovable.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#cropBoxMovable: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | // cropBoxMovable: true, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | var face = util.getByClass(cropper.cropBox, 'cropper-face'); 14 | 15 | assert.strictEqual(face[0].dataset.action, 'all'); 16 | 17 | done(); 18 | } 19 | }); 20 | }); 21 | 22 | QUnit.test('options#cropBoxMovable: false', function (assert) { 23 | var done = assert.async(); 24 | var util = window.Util; 25 | var image = util.createImage(); 26 | 27 | assert.expect(1); 28 | 29 | return new Cropper(image, { 30 | cropBoxMovable: false, 31 | 32 | ready: function () { 33 | var cropper = this.cropper; 34 | var face = util.getByClass(cropper.cropBox, 'cropper-face'); 35 | 36 | assert.strictEqual(face[0].dataset.action, cropper.options.dragMode); 37 | 38 | done(); 39 | } 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Before opening an issue: 2 | 3 | - [Search for duplicate or closed issues](https://github.com/fengyuanchen/cropperjs/issues?utf8=%E2%9C%93&q=is%3Aissue) 4 | - Prepare a [reduced test case](https://css-tricks.com/reduced-test-cases/) for any bugs 5 | - Read the [contributing guidelines](https://github.com/fengyuanchen/cropperjs/blob/master/CONTRIBUTING.md) 6 | 7 | When asking general "how to" questions: 8 | 9 | - Please do not open an issue here 10 | - Instead, ask for help on [StackOverflow](http://stackoverflow.com/) 11 | 12 | When reporting a bug, include: 13 | 14 | - Operating system and version (Windows, Mac OS X, Android, iOS, Win10 Mobile) 15 | - Browser and version (Chrome, Firefox, Safari, IE, MS Edge, Opera 15+, Android Browser) 16 | - Reduced test cases and potential fixes using [JS Bin](https://jsbin.com), [JSFiddle](https://jsfiddle.net/) or [CodePen](https://codepen.io/) 17 | 18 | When suggesting a feature, include: 19 | 20 | - As much detail as possible for what we should add and why it's important to Cropper.js 21 | - Relevant links to prior art, screenshots, or live demos whenever possible 22 | -------------------------------------------------------------------------------- /test/methods/getCroppedCanvas.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#getCroppedCanvas', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(7); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var canvas = cropper.getCroppedCanvas({ 12 | width: 160, 13 | height: 90 14 | }); 15 | var pixelData; 16 | 17 | assert.ok(canvas instanceof HTMLCanvasElement); 18 | assert.strictEqual(canvas.width, 160); 19 | assert.strictEqual(canvas.height, 90); 20 | 21 | canvas = cropper.rotate(90).getCroppedCanvas({ 22 | fillColor: '#010101' 23 | }); 24 | pixelData = canvas.getContext('2d').getImageData(0, 0, 1, 1).data; 25 | assert.strictEqual(pixelData[0], 1, 'red is 1'); 26 | assert.strictEqual(pixelData[1], 1, 'green is 1'); 27 | assert.strictEqual(pixelData[2], 1, 'blue is 1'); 28 | assert.strictEqual(pixelData[3], 255, 'color is opaque'); 29 | 30 | done(); 31 | } 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/options/scalable.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#scalable: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(2); 7 | 8 | return new Cropper(image, { 9 | // scalable: true, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | var imageData = cropper.scale(-1, -1).getImageData(); 14 | 15 | assert.strictEqual(imageData.scaleX, -1); 16 | assert.strictEqual(imageData.scaleY, -1); 17 | 18 | done(); 19 | } 20 | }); 21 | }); 22 | 23 | QUnit.test('options#scalable: false', function (assert) { 24 | var done = assert.async(); 25 | var util = window.Util; 26 | var image = util.createImage(); 27 | 28 | assert.expect(2); 29 | 30 | return new Cropper(image, { 31 | scalable: false, 32 | 33 | ready: function () { 34 | var cropper = this.cropper; 35 | var imageData = cropper.scale(-1, -1).getImageData(); 36 | 37 | assert.strictEqual(imageData.scaleX, undefined); 38 | assert.strictEqual(imageData.scaleY, undefined); 39 | 40 | done(); 41 | } 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2017 Chen Fengyuan 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/methods/setData.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#setData', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(8); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var data = cropper.getData(); 12 | var changedData = cropper.setData({ 13 | x: 16, 14 | y: 9 15 | }).getData(); 16 | 17 | assert.notStrictEqual(changedData.x, data.x); 18 | assert.notStrictEqual(changedData.y, data.y); 19 | assert.strictEqual(changedData.width, data.width); 20 | assert.strictEqual(changedData.height, data.height); 21 | 22 | data = cropper.getData(); 23 | changedData = cropper.setData({ 24 | width: 320, 25 | height: 180 26 | }).getData(); 27 | 28 | assert.strictEqual(changedData.x, data.x); 29 | assert.strictEqual(changedData.y, data.y); 30 | assert.notStrictEqual(changedData.width, data.width); 31 | assert.notStrictEqual(changedData.height, data.height); 32 | 33 | done(); 34 | } 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/options/zoom.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#zoom', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(3); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | 12 | cropper.zoom(0.1); 13 | 14 | done(); 15 | }, 16 | 17 | zoom: function (e) { 18 | assert.ok(e.detail.ratio > 0); 19 | assert.ok(e.detail.oldRatio > 0); 20 | assert.ok(e.detail.ratio > e.detail.oldRatio); 21 | } 22 | }); 23 | }); 24 | 25 | QUnit.test('options#zoom: default prevented', function (assert) { 26 | var done = assert.async(); 27 | var util = window.Util; 28 | var image = util.createImage(); 29 | 30 | assert.expect(1); 31 | 32 | return new Cropper(image, { 33 | ready: function () { 34 | var cropper = this.cropper; 35 | var canvasData = cropper.getCanvasData(); 36 | 37 | assert.deepEqual(cropper.zoom(0.1).getCanvasData(), canvasData); 38 | 39 | done(); 40 | }, 41 | 42 | zoom: function (e) { 43 | e.preventDefault(); 44 | } 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/events/zoom.js: -------------------------------------------------------------------------------- 1 | QUnit.test('events#zoom', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(3); 7 | 8 | image.addEventListener('ready', function () { 9 | var cropper = this.cropper; 10 | 11 | cropper.zoom(0.1); 12 | 13 | done(); 14 | }); 15 | 16 | image.addEventListener('zoom', function (e) { 17 | assert.ok(e.detail.ratio > 0); 18 | assert.ok(e.detail.oldRatio > 0); 19 | assert.ok(e.detail.ratio > e.detail.oldRatio); 20 | }); 21 | 22 | return new Cropper(image); 23 | }); 24 | 25 | QUnit.test('events#zoom: default prevented', function (assert) { 26 | var done = assert.async(); 27 | var util = window.Util; 28 | var image = util.createImage(); 29 | 30 | assert.expect(1); 31 | 32 | image.addEventListener('ready', function () { 33 | var cropper = this.cropper; 34 | var canvasData = cropper.getCanvasData(); 35 | 36 | assert.deepEqual(cropper.zoom(0.1).getCanvasData(), canvasData); 37 | 38 | done(); 39 | }); 40 | 41 | image.addEventListener('zoom', function (e) { 42 | e.preventDefault(); 43 | }); 44 | 45 | return new Cropper(image); 46 | }); 47 | -------------------------------------------------------------------------------- /test/methods/setCanvasData.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#setCanvasData', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(6); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var canvasData = cropper.getCanvasData(); 12 | var changedCanvasData = cropper.setCanvasData({ 13 | left: 16, 14 | top: 9 15 | }).getCanvasData(); 16 | 17 | assert.notStrictEqual(changedCanvasData.left, canvasData.left); 18 | assert.notStrictEqual(changedCanvasData.top, canvasData.top); 19 | assert.strictEqual(changedCanvasData.width, canvasData.width); 20 | assert.strictEqual(changedCanvasData.height, canvasData.height); 21 | 22 | canvasData = cropper.getCanvasData(); 23 | changedCanvasData = cropper.setCanvasData({ 24 | width: 320, 25 | height: 180 26 | }).getCanvasData(); 27 | 28 | assert.notStrictEqual(changedCanvasData.width, canvasData.width); 29 | assert.notStrictEqual(changedCanvasData.height, canvasData.height); 30 | 31 | done(); 32 | } 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/options/guides.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#guides: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(2); 7 | 8 | return new Cropper(image, { 9 | // guides: true, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | var dashed = util.getByClass(cropper.cropBox, 'cropper-dashed'); 14 | 15 | assert.notOk(util.hasClass(dashed[0], 'cropper-hidden')); 16 | assert.notOk(util.hasClass(dashed[1], 'cropper-hidden')); 17 | 18 | done(); 19 | } 20 | }); 21 | }); 22 | 23 | QUnit.test('options#guides: false', function (assert) { 24 | var done = assert.async(); 25 | var util = window.Util; 26 | var image = util.createImage(); 27 | 28 | assert.expect(2); 29 | 30 | return new Cropper(image, { 31 | guides: false, 32 | 33 | ready: function () { 34 | var cropper = this.cropper; 35 | var dashed = util.getByClass(cropper.cropBox, 'cropper-dashed'); 36 | 37 | assert.ok(util.hasClass(dashed[0], 'cropper-hidden')); 38 | assert.ok(util.hasClass(dashed[1], 'cropper-hidden')); 39 | 40 | done(); 41 | } 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /examples/multiple-croppers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cropper.js 8 | 9 | 19 | 20 | 21 | 22 |
23 |

Multiple Croppers

24 |

Cropper

25 |
26 | Picture 27 |
28 |

Another

29 |
30 | Picture 31 |
32 |
33 | 34 | 35 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /test/methods/getData.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#getData', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(7); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var data = cropper.getData(); 12 | 13 | assert.ok(util.isNumber(data.x)); 14 | assert.ok(util.isNumber(data.y)); 15 | assert.ok(util.isNumber(data.width)); 16 | assert.ok(util.isNumber(data.height)); 17 | assert.ok(util.isNumber(data.rotate)); 18 | assert.ok(util.isNumber(data.scaleX)); 19 | assert.ok(util.isNumber(data.scaleY)); 20 | 21 | done(); 22 | } 23 | }); 24 | }); 25 | 26 | QUnit.test('methods#getData: rounded', function (assert) { 27 | var done = assert.async(); 28 | var util = window.Util; 29 | var image = util.createImage(); 30 | 31 | assert.expect(4); 32 | 33 | return new Cropper(image, { 34 | ready: function () { 35 | var cropper = this.cropper; 36 | var data = cropper.getData(true); 37 | 38 | assert.ok(data.x % 1 === 0); 39 | assert.ok(data.y % 1 === 0); 40 | assert.ok(data.width % 1 === 0); 41 | assert.ok(data.height % 1 === 0); 42 | 43 | done(); 44 | } 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/options/toggleDragModeOnDblclick.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#toggleDragModeOnDblclick: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(2); 7 | 8 | return new Cropper(image, { 9 | // toggleDragModeOnDblclick: true, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | var dragBox = cropper.dragBox; 14 | 15 | util.dispatchEvent(dragBox, 'dblclick'); 16 | assert.ok(util.hasClass(dragBox, 'cropper-move')); 17 | assert.strictEqual(dragBox.dataset.action, 'move'); 18 | 19 | done(); 20 | } 21 | }); 22 | }); 23 | 24 | QUnit.test('options#toggleDragModeOnDblclick: false', function (assert) { 25 | var done = assert.async(); 26 | var util = window.Util; 27 | var image = util.createImage(); 28 | 29 | assert.expect(2); 30 | 31 | return new Cropper(image, { 32 | toggleDragModeOnDblclick: false, 33 | 34 | ready: function () { 35 | var cropper = this.cropper; 36 | var dragBox = cropper.dragBox; 37 | 38 | util.dispatchEvent(dragBox, 'dblclick'); 39 | assert.ok(util.hasClass(dragBox, 'cropper-crop')); 40 | assert.strictEqual(dragBox.dataset.action, 'crop'); 41 | 42 | done(); 43 | } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/options/movable.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#movable: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(2); 7 | 8 | return new Cropper(image, { 9 | // movable: true, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | var canvasData = cropper.getCanvasData(); 14 | var changedCanvasData = cropper.move(10, 10).getCanvasData(); 15 | 16 | assert.strictEqual(changedCanvasData.left, canvasData.left + 10); 17 | assert.strictEqual(changedCanvasData.top, canvasData.top + 10); 18 | 19 | done(); 20 | } 21 | }); 22 | }); 23 | 24 | QUnit.test('options#movable: false', function (assert) { 25 | var done = assert.async(); 26 | var util = window.Util; 27 | var image = util.createImage(); 28 | 29 | assert.expect(2); 30 | 31 | return new Cropper(image, { 32 | movable: false, 33 | 34 | ready: function () { 35 | var cropper = this.cropper; 36 | var canvasData = cropper.getCanvasData(); 37 | var changedCanvasData = cropper.move(10, 10).getCanvasData(); 38 | 39 | assert.strictEqual(changedCanvasData.left, canvasData.left); 40 | assert.strictEqual(changedCanvasData.top, canvasData.top); 41 | 42 | done(); 43 | } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /examples/full-crop-box.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cropper.js 8 | 9 | 19 | 20 | 21 | 22 |
23 |

Cropper with full crop box

24 |
25 | Picture 26 |
27 |
28 | 29 | 30 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/fixed-crop-box.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cropper.js 8 | 9 | 19 | 20 | 21 | 22 |
23 |

Cropper with fixed crop box

24 |
25 | Picture 26 |
27 |
28 | 29 | 30 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /test/methods/setCropBoxData.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#setCropBoxData', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(8); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var cropBoxData = cropper.getCropBoxData(); 12 | var changedCropBoxData = cropper.setCropBoxData({ 13 | left: 16, 14 | top: 9 15 | }).getCropBoxData(); 16 | 17 | assert.notStrictEqual(changedCropBoxData.left, cropBoxData.left); 18 | assert.notStrictEqual(changedCropBoxData.top, cropBoxData.top); 19 | assert.strictEqual(changedCropBoxData.width, cropBoxData.width); 20 | assert.strictEqual(changedCropBoxData.height, cropBoxData.height); 21 | 22 | cropBoxData = cropper.getCropBoxData(); 23 | changedCropBoxData = cropper.setCropBoxData({ 24 | width: 320, 25 | height: 180 26 | }).getCropBoxData(); 27 | 28 | assert.strictEqual(changedCropBoxData.left, cropBoxData.left); 29 | assert.strictEqual(changedCropBoxData.top, cropBoxData.top); 30 | assert.notStrictEqual(changedCropBoxData.width, cropBoxData.width); 31 | assert.notStrictEqual(changedCropBoxData.height, cropBoxData.height); 32 | 33 | done(); 34 | } 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /examples/responsive-container.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cropper.js 8 | 9 | 19 | 20 | 21 | 22 |
23 |

Cropper with responsive container

24 |

25 | 26 |

27 |
28 | Picture 29 |
30 |
31 | 32 | 33 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/options/dragMode.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#dragMode: crop', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | // dragMode: 'crop', 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | 14 | assert.strictEqual(cropper.dragBox.dataset.action, 'crop'); 15 | 16 | done(); 17 | } 18 | }); 19 | }); 20 | 21 | QUnit.test('options#dragMode: move', function (assert) { 22 | var done = assert.async(); 23 | var util = window.Util; 24 | var image = util.createImage(); 25 | 26 | assert.expect(1); 27 | 28 | return new Cropper(image, { 29 | dragMode: 'move', 30 | 31 | ready: function () { 32 | var cropper = this.cropper; 33 | 34 | assert.strictEqual(cropper.dragBox.dataset.action, 'move'); 35 | 36 | done(); 37 | } 38 | }); 39 | }); 40 | 41 | 42 | QUnit.test('options#dragMode: none', function (assert) { 43 | var done = assert.async(); 44 | var util = window.Util; 45 | var image = util.createImage(); 46 | 47 | assert.expect(1); 48 | 49 | return new Cropper(image, { 50 | dragMode: 'none', 51 | 52 | ready: function () { 53 | var cropper = this.cropper; 54 | 55 | assert.strictEqual(cropper.dragBox.dataset.action, 'none'); 56 | 57 | done(); 58 | } 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/options/zoomable.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#zoomable: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(3); 7 | 8 | return new Cropper(image, { 9 | // zoomable: true, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | var canvasData = cropper.getCanvasData(); 14 | var changedCanvasData = cropper.zoom(0.1).getCanvasData(); 15 | 16 | assert.ok(changedCanvasData.width > canvasData.width); 17 | assert.ok(changedCanvasData.height > canvasData.height); 18 | 19 | done(); 20 | }, 21 | zoom: function () { 22 | assert.ok(true); 23 | } 24 | }); 25 | }); 26 | 27 | QUnit.test('options#zoomable: false', function (assert) { 28 | var done = assert.async(); 29 | var util = window.Util; 30 | var image = util.createImage(); 31 | 32 | assert.expect(2); 33 | 34 | return new Cropper(image, { 35 | zoomable: false, 36 | 37 | ready: function () { 38 | var cropper = this.cropper; 39 | var canvasData = cropper.getCanvasData(); 40 | var changedCanvasData = cropper.zoom(0.1).getCanvasData(); 41 | 42 | assert.ok(changedCanvasData.width === canvasData.width); 43 | assert.ok(changedCanvasData.height === canvasData.height); 44 | 45 | done(); 46 | }, 47 | zoom: function () { 48 | assert.ok(false); 49 | } 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/js/template.js: -------------------------------------------------------------------------------- 1 | export default ( 2 | '
' + 3 | '
' + 4 | '
' + 5 | '
' + 6 | '
' + 7 | '
' + 8 | '' + 9 | '' + 10 | '' + 11 | '' + 12 | '' + 13 | '' + 14 | '' + 15 | '' + 16 | '' + 17 | '' + 18 | '' + 19 | '' + 20 | '' + 21 | '' + 22 | '' + 23 | '' + 24 | '' + 25 | '
' + 26 | '
' 27 | ); 28 | -------------------------------------------------------------------------------- /test/options/cropstart.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#cropstart', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var PointerEvent = window.PointerEvent; 11 | var cropper = this.cropper; 12 | 13 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerdown' : 'mousedown'); 14 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerup' : 'mouseup'); 15 | 16 | done(); 17 | }, 18 | 19 | cropstart: function (e) { 20 | assert.strictEqual(e.detail.action, 'crop'); 21 | } 22 | }); 23 | }); 24 | 25 | QUnit.test('options#cropstart: default prevented', function (assert) { 26 | var done = assert.async(); 27 | var util = window.Util; 28 | var image = util.createImage(); 29 | 30 | assert.expect(0); 31 | 32 | return new Cropper(image, { 33 | ready: function () { 34 | var PointerEvent = window.PointerEvent; 35 | var cropper = this.cropper; 36 | 37 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerdown' : 'mousedown'); 38 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointermove' : 'mousemove'); 39 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerup' : 'mouseup'); 40 | 41 | done(); 42 | }, 43 | 44 | cropstart: function (e) { 45 | e.preventDefault(); 46 | }, 47 | 48 | cropmove: function () { 49 | assert.ok(false); 50 | }, 51 | 52 | cropend: function () { 53 | assert.ok(false); 54 | } 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/events/cropstart.js: -------------------------------------------------------------------------------- 1 | QUnit.test('events#cropstart', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(1); 7 | 8 | image.addEventListener('ready', function () { 9 | var PointerEvent = window.PointerEvent; 10 | var cropper = this.cropper; 11 | 12 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerdown' : 'mousedown'); 13 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerup' : 'mouseup'); 14 | 15 | done(); 16 | }); 17 | 18 | image.addEventListener('cropstart', function (e) { 19 | assert.strictEqual(e.detail.action, 'crop'); 20 | }); 21 | 22 | return new Cropper(image); 23 | }); 24 | 25 | QUnit.test('events#cropstart: default prevented', function (assert) { 26 | var done = assert.async(); 27 | var util = window.Util; 28 | var image = util.createImage(); 29 | 30 | assert.expect(0); 31 | 32 | image.addEventListener('ready', function () { 33 | var PointerEvent = window.PointerEvent; 34 | var cropper = this.cropper; 35 | 36 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerdown' : 'mousedown'); 37 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointermove' : 'mousemove'); 38 | util.dispatchEvent(cropper.dragBox, PointerEvent ? 'pointerup' : 'mouseup'); 39 | 40 | done(); 41 | }); 42 | 43 | image.addEventListener('cropstart', function (e) { 44 | e.preventDefault(); 45 | }); 46 | 47 | image.addEventListener('cropmove', function () { 48 | assert.ok(false); 49 | }); 50 | 51 | image.addEventListener('cropend', function () { 52 | assert.ok(false); 53 | }); 54 | 55 | return new Cropper(image); 56 | }); 57 | -------------------------------------------------------------------------------- /examples/crop-on-canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cropper.js 8 | 9 | 19 | 20 | 21 | 22 |
23 |

Crop on canvas with Cropper

24 |

Image

25 |
26 | Picture 27 |
28 |

Canvas

29 |
30 | 31 |
32 |
33 | 34 | 35 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /test/options/checkCrossOrigin.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#checkCrossOrigin: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage({ 5 | src: 'https://fengyuanchen.github.io/cropperjs/images/picture.jpg' 6 | }); 7 | 8 | assert.expect(2); 9 | 10 | return new Cropper(image, { 11 | // checkCrossOrigin: true, 12 | 13 | ready: function () { 14 | var cropper = this.cropper; 15 | 16 | assert.strictEqual(cropper.image.crossOrigin, 'anonymous'); 17 | assert.ok(cropper.image.src.indexOf('timestamp') >= 0); 18 | 19 | done(); 20 | } 21 | }); 22 | }); 23 | 24 | QUnit.test('options#checkCrossOrigin: false', function (assert) { 25 | var done = assert.async(); 26 | var util = window.Util; 27 | var image = util.createImage({ 28 | src: 'https://fengyuanchen.github.io/cropperjs/images/picture.jpg' 29 | }); 30 | 31 | assert.expect(2); 32 | 33 | return new Cropper(image, { 34 | checkCrossOrigin: false, 35 | 36 | ready: function () { 37 | var cropper = this.cropper; 38 | 39 | assert.notStrictEqual(cropper.image.crossOrigin, 'anonymous'); 40 | assert.ok(cropper.image.src.indexOf('timestamp') < 0); 41 | 42 | done(); 43 | } 44 | }); 45 | }); 46 | 47 | QUnit.test('options#checkCrossOrigin: exists crossOrigin attribute', function (assert) { 48 | var done = assert.async(); 49 | var util = window.Util; 50 | var image = util.createImage({ 51 | src: 'https://fengyuanchen.github.io/cropperjs/images/picture.jpg', 52 | crossOrigin: 'anonymous' 53 | }); 54 | 55 | assert.expect(2); 56 | 57 | return new Cropper(image, { 58 | ready: function () { 59 | var cropper = this.cropper; 60 | 61 | assert.strictEqual(cropper.image.crossOrigin, 'anonymous'); 62 | assert.ok(cropper.image.src.indexOf('timestamp') < 0); 63 | 64 | done(); 65 | } 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/methods/disable.js: -------------------------------------------------------------------------------- 1 | QUnit.test('methods#disable', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(11); 7 | 8 | return new Cropper(image, { 9 | ready: function () { 10 | var cropper = this.cropper; 11 | var options = cropper.options; 12 | var cropBoxData; 13 | var canvasData; 14 | var imageData; 15 | var action; 16 | 17 | cropper.disable(); 18 | assert.ok(cropper.disabled); 19 | assert.ok(util.hasClass(cropper.cropper, 'cropper-disabled')); 20 | 21 | cropBoxData = cropper.getCropBoxData(); 22 | cropper.clear(); 23 | assert.deepEqual(cropper.getCropBoxData(), cropBoxData); 24 | 25 | imageData = cropper.getImageData(); 26 | cropper.move(10, 10); 27 | assert.deepEqual(cropper.getImageData(), imageData); 28 | 29 | cropper.zoom(0.5); 30 | assert.strictEqual(cropper.getImageData().ratio, imageData.ratio); 31 | 32 | cropper.rotate(15); 33 | assert.strictEqual(cropper.getImageData().rotate, imageData.rotate); 34 | 35 | cropper.scale(-1); 36 | assert.strictEqual(cropper.getImageData().scaleX, imageData.scaleX); 37 | 38 | canvasData = cropper.getCanvasData(); 39 | cropper.setCanvasData({ 40 | width: canvasData.width - 160 41 | }); 42 | assert.deepEqual(cropper.getCanvasData(), canvasData); 43 | 44 | cropBoxData = cropper.getCropBoxData(); 45 | cropper.setCropBoxData({ 46 | height: cropBoxData.height - 90 47 | }); 48 | assert.deepEqual(cropper.getCropBoxData(), cropBoxData); 49 | 50 | cropper.setAspectRatio(0.618); 51 | assert.ok(isNaN(options.aspectRatio)); 52 | 53 | action = cropper.dragBox.dataset.action; 54 | cropper.setDragMode('none'); 55 | assert.strictEqual(cropper.dragBox.dataset.action, action); 56 | 57 | done(); 58 | } 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/js/main.js: -------------------------------------------------------------------------------- 1 | window.Util = { 2 | isNumber: function (n) { 3 | return typeof n === 'number' && !isNaN(n); 4 | }, 5 | isFunction: function (fn) { 6 | return typeof fn === 'function'; 7 | }, 8 | hasClass: function (element, className) { 9 | return element.classList.contains(className); 10 | }, 11 | getByClass: function (element, className) { 12 | return element.getElementsByClassName ? 13 | element.getElementsByClassName(className) : 14 | element.querySelectorAll('.' + className); 15 | }, 16 | createImage: function (attrs) { 17 | var container = document.createElement('div'); 18 | var image = new Image(); 19 | var attr; 20 | 21 | if (typeof attrs !== 'object') { 22 | attrs = {}; 23 | } 24 | 25 | if (!attrs.src) { 26 | attrs.src = '../docs/images/picture.jpg'; 27 | } 28 | 29 | for (attr in attrs) { 30 | if (attrs.hasOwnProperty(attr)) { 31 | image[attr] = attrs[attr]; 32 | } 33 | } 34 | 35 | container.className = 'container'; 36 | container.appendChild(image); 37 | document.body.appendChild(container); 38 | 39 | return image; 40 | }, 41 | dispatchEvent: function (element, type, data) { 42 | var event; 43 | 44 | if (element.dispatchEvent) { 45 | // Event and CustomEvent on IE9-11 are global objects, not constructors 46 | if (typeof Event === 'function' && typeof CustomEvent === 'function') { 47 | if (!data) { 48 | event = new Event(type, { 49 | bubbles: true, 50 | cancelable: true 51 | }); 52 | } else { 53 | event = new CustomEvent(type, { 54 | detail: data, 55 | bubbles: true, 56 | cancelable: true 57 | }); 58 | } 59 | } else if (!data) { 60 | event = document.createEvent('Event'); 61 | event.initEvent(type, true, true); 62 | } else { 63 | event = document.createEvent('CustomEvent'); 64 | event.initCustomEvent(type, true, true, data); 65 | } 66 | 67 | // IE9+ 68 | return element.dispatchEvent(event); 69 | } else if (element.fireEvent) { 70 | // IE6-10 (native events only) 71 | return element.fireEvent('on' + type); 72 | } 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /src/js/constants.js: -------------------------------------------------------------------------------- 1 | export const WINDOW = typeof window !== 'undefined' ? window : {}; 2 | export const NAMESPACE = 'cropper'; 3 | 4 | // Actions 5 | export const ACTION_ALL = 'all'; 6 | export const ACTION_CROP = 'crop'; 7 | export const ACTION_MOVE = 'move'; 8 | export const ACTION_ZOOM = 'zoom'; 9 | export const ACTION_EAST = 'e'; 10 | export const ACTION_WEST = 'w'; 11 | export const ACTION_SOUTH = 's'; 12 | export const ACTION_NORTH = 'n'; 13 | export const ACTION_NORTH_EAST = 'ne'; 14 | export const ACTION_NORTH_WEST = 'nw'; 15 | export const ACTION_SOUTH_EAST = 'se'; 16 | export const ACTION_SOUTH_WEST = 'sw'; 17 | 18 | // Classes 19 | export const CLASS_CROP = `${NAMESPACE}-crop`; 20 | export const CLASS_DISABLED = `${NAMESPACE}-disabled`; 21 | export const CLASS_HIDDEN = `${NAMESPACE}-hidden`; 22 | export const CLASS_HIDE = `${NAMESPACE}-hide`; 23 | export const CLASS_INVISIBLE = `${NAMESPACE}-invisible`; 24 | export const CLASS_MODAL = `${NAMESPACE}-modal`; 25 | export const CLASS_MOVE = `${NAMESPACE}-move`; 26 | 27 | // Data keys 28 | export const DATA_ACTION = 'action'; 29 | export const DATA_PREVIEW = 'preview'; 30 | 31 | // Drag modes 32 | export const DRAG_MODE_CROP = 'crop'; 33 | export const DRAG_MODE_MOVE = 'move'; 34 | export const DRAG_MODE_NONE = 'none'; 35 | 36 | // Events 37 | export const EVENT_CROP = 'crop'; 38 | export const EVENT_CROP_END = 'cropend'; 39 | export const EVENT_CROP_MOVE = 'cropmove'; 40 | export const EVENT_CROP_START = 'cropstart'; 41 | export const EVENT_DBLCLICK = 'dblclick'; 42 | export const EVENT_ERROR = 'error'; 43 | export const EVENT_LOAD = 'load'; 44 | export const EVENT_POINTER_DOWN = WINDOW.PointerEvent ? 'pointerdown' : 'touchstart mousedown'; 45 | export const EVENT_POINTER_MOVE = WINDOW.PointerEvent ? 'pointermove' : 'touchmove mousemove'; 46 | export const EVENT_POINTER_UP = WINDOW.PointerEvent ? ' pointerup pointercancel' : 'touchend touchcancel mouseup'; 47 | export const EVENT_READY = 'ready'; 48 | export const EVENT_RESIZE = 'resize'; 49 | export const EVENT_WHEEL = 'wheel mousewheel DOMMouseScroll'; 50 | export const EVENT_ZOOM = 'zoom'; 51 | 52 | // RegExps 53 | export const REGEXP_ACTIONS = /^(e|w|s|n|se|sw|ne|nw|all|crop|move|zoom)$/; 54 | export const REGEXP_DATA_URL = /^data:/; 55 | export const REGEXP_DATA_URL_JPEG = /^data:image\/jpeg;base64,/; 56 | export const REGEXP_TAG_NAME = /^(img|canvas)$/i; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cropperjs", 3 | "description": "JavaScript image cropper.", 4 | "version": "1.1.3", 5 | "main": "dist/cropper.common.js", 6 | "module": "dist/cropper.esm.js", 7 | "browser": "dist/cropper.js", 8 | "license": "MIT", 9 | "repository": "fengyuanchen/cropperjs", 10 | "homepage": "https://fengyuanchen.github.io/cropperjs", 11 | "author": { 12 | "name": "Chen Fengyuan", 13 | "url": "http://chenfengyuan.com" 14 | }, 15 | "files": [ 16 | "src", 17 | "dist" 18 | ], 19 | "keywords": [ 20 | "image", 21 | "crop", 22 | "cropping", 23 | "move", 24 | "zoom", 25 | "rotate", 26 | "scale", 27 | "cropper", 28 | "cropperjs", 29 | "cropper.js", 30 | "html", 31 | "css", 32 | "javascript", 33 | "front-end", 34 | "web", 35 | "development" 36 | ], 37 | "scripts": { 38 | "build": "npm run build:css && npm run build:js", 39 | "build:css": "postcss src/css/cropper.css -o dist/cropper.css -m", 40 | "build:js": "rollup -c", 41 | "compress": "npm run compress:css && npm run compress:js", 42 | "compress:css": "postcss dist/cropper.css -u cssnano -o dist/cropper.min.css -m", 43 | "compress:js": "uglifyjs dist/cropper.js -o dist/cropper.min.js -c -m --comments /^!/", 44 | "copy": "cpy dist/cropper.css docs/css", 45 | "lint": "eslint src/js --fix", 46 | "release": "npm run lint && npm run build && npm run compress && npm run copy && npm test", 47 | "start": "npm-run-all --parallel watch:*", 48 | "test": "node-qunit-phantomjs test/index.html --timeout 10", 49 | "watch:css": "postcss src/css/cropper.css -o docs/css/cropper.css -m -w", 50 | "watch:js": "rollup -c -m -w" 51 | }, 52 | "devDependencies": { 53 | "babel-core": "^6.26.0", 54 | "babel-preset-env": "^1.6.1", 55 | "cpy-cli": "^1.0.1", 56 | "cssnano": "^3.10.0", 57 | "eslint": "^4.9.0", 58 | "eslint-config-airbnb-base": "^12.1.0", 59 | "eslint-plugin-import": "^2.8.0", 60 | "node-qunit-phantomjs": "^1.5.1", 61 | "npm-run-all": "^4.1.1", 62 | "postcss-cli": "^4.1.1", 63 | "postcss-cssnext": "^3.0.2", 64 | "postcss-header": "^1.0.0", 65 | "postcss-url": "^7.1.2", 66 | "rollup": "^0.50.0", 67 | "rollup-plugin-babel": "^3.0.2", 68 | "rollup-watch": "^4.3.1", 69 | "stylefmt": "^6.0.0", 70 | "uglify-js": "^3.1.4" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Cropper 2 | 3 | ## How to report bugs 4 | 5 | ### Make sure it is a Cropper bug 6 | 7 | Most bugs reported to our bug tracker are actually bugs in user code, not in Cropper code. Keep in mind that just because your code throws an error inside of Cropper, this does *not* mean the bug is a Cropper bug. 8 | 9 | Ask for help first in a discussion forum like [Stack Overflow](http://stackoverflow.com/). You will get much quicker support, and you will help avoid tying up the Cropper team with invalid bug reports. 10 | 11 | ### Disable browser extensions 12 | 13 | Make sure you have reproduced the bug with all browser extensions and add-ons disabled, as these can sometimes cause things to break in interesting and unpredictable ways. Try using incognito, stealth or anonymous browsing modes. 14 | 15 | ### Try the latest version of Cropper 16 | 17 | Bugs in old versions of Cropper may have already been fixed. In order to avoid reporting known issues, make sure you are always testing against the [latest release](https://github.com/fengyuanchen/cropperjs/releases/latest). We cannot fix bugs in older released files, if a bug has been fixed in a subsequent version of Cropper the site should upgrade. 18 | 19 | ### Simplify the test case 20 | 21 | When experiencing a problem, [reduce your code](http://webkit.org/quality/reduction.html) to the bare minimum required to reproduce the issue. This makes it *much* easier to isolate and fix the offending code. Bugs reported without reduced test cases take on average 9001% longer to fix than bugs that are submitted with them, so you really should try to do this if at all possible. 22 | 23 | ### Search for related or duplicate issues 24 | 25 | Go to the [Cropper issue tracker](https://github.com/fengyuanchen/cropperjs/issues) and make sure the problem hasn't already been reported. If not, create a new issue there and include your test case. 26 | 27 | ### Browser support 28 | 29 | Remember that Cropper supports multiple browsers and their versions; any contributed code must work in all of them. You can refer to the [browser support page](https://github.com/fengyuanchen/cropperjs/blob/master/README.md#browser-support) for the current list of supported browsers. 30 | 31 | ## Notes for pull request 32 | 33 | - Run the test suites in the `test` directory first. 34 | - Don't modify any files in the `dist` directory. 35 | - Follow the same code style as the library. 36 | -------------------------------------------------------------------------------- /examples/a-range-of-aspect-ratio.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cropper.js 8 | 9 | 19 | 20 | 21 | 22 |
23 |

Cropper with a range of aspect ratio

24 |
25 | Picture 26 |
27 |
28 | 29 | 30 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /test/options/cropBoxResizable.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#cropBoxResizable: true', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(12); 7 | 8 | return new Cropper(image, { 9 | // cropBoxResizable: true, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | var line = util.getByClass(cropper.cropBox, 'cropper-line'); 14 | var point = util.getByClass(cropper.cropBox, 'cropper-point'); 15 | 16 | assert.notOk(util.hasClass(line[0], 'cropper-hidden')); 17 | assert.notOk(util.hasClass(line[1], 'cropper-hidden')); 18 | assert.notOk(util.hasClass(line[2], 'cropper-hidden')); 19 | assert.notOk(util.hasClass(line[3], 'cropper-hidden')); 20 | assert.notOk(util.hasClass(point[0], 'cropper-hidden')); 21 | assert.notOk(util.hasClass(point[1], 'cropper-hidden')); 22 | assert.notOk(util.hasClass(point[2], 'cropper-hidden')); 23 | assert.notOk(util.hasClass(point[3], 'cropper-hidden')); 24 | assert.notOk(util.hasClass(point[4], 'cropper-hidden')); 25 | assert.notOk(util.hasClass(point[5], 'cropper-hidden')); 26 | assert.notOk(util.hasClass(point[6], 'cropper-hidden')); 27 | assert.notOk(util.hasClass(point[7], 'cropper-hidden')); 28 | 29 | done(); 30 | } 31 | }); 32 | }); 33 | 34 | QUnit.test('options#cropBoxResizable: false', function (assert) { 35 | var done = assert.async(); 36 | var util = window.Util; 37 | var image = util.createImage(); 38 | 39 | assert.expect(12); 40 | 41 | return new Cropper(image, { 42 | cropBoxResizable: false, 43 | 44 | ready: function () { 45 | var cropper = this.cropper; 46 | var line = util.getByClass(cropper.cropBox, 'cropper-line'); 47 | var point = util.getByClass(cropper.cropBox, 'cropper-point'); 48 | 49 | assert.ok(util.hasClass(line[0], 'cropper-hidden')); 50 | assert.ok(util.hasClass(line[1], 'cropper-hidden')); 51 | assert.ok(util.hasClass(line[2], 'cropper-hidden')); 52 | assert.ok(util.hasClass(line[3], 'cropper-hidden')); 53 | assert.ok(util.hasClass(point[0], 'cropper-hidden')); 54 | assert.ok(util.hasClass(point[1], 'cropper-hidden')); 55 | assert.ok(util.hasClass(point[2], 'cropper-hidden')); 56 | assert.ok(util.hasClass(point[3], 'cropper-hidden')); 57 | assert.ok(util.hasClass(point[4], 'cropper-hidden')); 58 | assert.ok(util.hasClass(point[5], 'cropper-hidden')); 59 | assert.ok(util.hasClass(point[6], 'cropper-hidden')); 60 | assert.ok(util.hasClass(point[7], 'cropper-hidden')); 61 | 62 | done(); 63 | } 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/js/defaults.js: -------------------------------------------------------------------------------- 1 | import { 2 | DRAG_MODE_CROP, 3 | } from './constants'; 4 | 5 | export default { 6 | // Define the view mode of the cropper 7 | viewMode: 0, // 0, 1, 2, 3 8 | 9 | // Define the dragging mode of the cropper 10 | dragMode: DRAG_MODE_CROP, // 'crop', 'move' or 'none' 11 | 12 | // Define the aspect ratio of the crop box 13 | aspectRatio: NaN, 14 | 15 | // An object with the previous cropping result data 16 | data: null, 17 | 18 | // A selector for adding extra containers to preview 19 | preview: '', 20 | 21 | // Re-render the cropper when resize the window 22 | responsive: true, 23 | 24 | // Restore the cropped area after resize the window 25 | restore: true, 26 | 27 | // Check if the current image is a cross-origin image 28 | checkCrossOrigin: true, 29 | 30 | // Check the current image's Exif Orientation information 31 | checkOrientation: true, 32 | 33 | // Show the black modal 34 | modal: true, 35 | 36 | // Show the dashed lines for guiding 37 | guides: true, 38 | 39 | // Show the center indicator for guiding 40 | center: true, 41 | 42 | // Show the white modal to highlight the crop box 43 | highlight: true, 44 | 45 | // Show the grid background 46 | background: true, 47 | 48 | // Enable to crop the image automatically when initialize 49 | autoCrop: true, 50 | 51 | // Define the percentage of automatic cropping area when initializes 52 | autoCropArea: 0.8, 53 | 54 | // Enable to move the image 55 | movable: true, 56 | 57 | // Enable to rotate the image 58 | rotatable: true, 59 | 60 | // Enable to scale the image 61 | scalable: true, 62 | 63 | // Enable to zoom the image 64 | zoomable: true, 65 | 66 | // Enable to zoom the image by dragging touch 67 | zoomOnTouch: true, 68 | 69 | // Enable to zoom the image by wheeling mouse 70 | zoomOnWheel: true, 71 | 72 | // Define zoom ratio when zoom the image by wheeling mouse 73 | wheelZoomRatio: 0.1, 74 | 75 | // Enable to move the crop box 76 | cropBoxMovable: true, 77 | 78 | // Enable to resize the crop box 79 | cropBoxResizable: true, 80 | 81 | // Toggle drag mode between "crop" and "move" when click twice on the cropper 82 | toggleDragModeOnDblclick: true, 83 | 84 | // Size limitation 85 | minCanvasWidth: 0, 86 | minCanvasHeight: 0, 87 | minCropBoxWidth: 0, 88 | minCropBoxHeight: 0, 89 | minContainerWidth: 200, 90 | minContainerHeight: 100, 91 | 92 | // Shortcuts of events 93 | ready: null, 94 | cropstart: null, 95 | cropmove: null, 96 | cropend: null, 97 | crop: null, 98 | zoom: null, 99 | }; 100 | -------------------------------------------------------------------------------- /examples/cropper-in-modal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cropper.js 8 | 9 | 10 | 15 | 16 | 17 | 18 |
19 |

Cropper in a Bootstrap modal

20 | 21 | 22 | 25 | 26 | 27 | 47 |
48 | 49 | 50 | 51 | 52 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /test/options/viewMode.js: -------------------------------------------------------------------------------- 1 | QUnit.test('options#viewMode: 0', function (assert) { 2 | var done = assert.async(); 3 | var util = window.Util; 4 | var image = util.createImage(); 5 | 6 | assert.expect(4); 7 | 8 | return new Cropper(image, { 9 | // viewMode: 0, 10 | 11 | ready: function () { 12 | var cropper = this.cropper; 13 | var canvasData = { 14 | left: 100, 15 | top: 100, 16 | width: 160, 17 | height: 90 18 | }; 19 | var changedCanvasData = cropper.setCanvasData(canvasData).getCanvasData(); 20 | 21 | assert.strictEqual(changedCanvasData.left, canvasData.left); 22 | assert.strictEqual(changedCanvasData.top, canvasData.top); 23 | assert.strictEqual(changedCanvasData.width, canvasData.width); 24 | assert.strictEqual(changedCanvasData.height, canvasData.height); 25 | 26 | done(); 27 | } 28 | }); 29 | }); 30 | 31 | QUnit.test('options#viewMode: 1', function (assert) { 32 | var done = assert.async(); 33 | var util = window.Util; 34 | var image = util.createImage(); 35 | 36 | assert.expect(2); 37 | 38 | return new Cropper(image, { 39 | viewMode: 1, 40 | 41 | ready: function () { 42 | var cropper = this.cropper; 43 | var canvasData = cropper.zoom(-0.5).getCanvasData(); // Zoom out 44 | var cropBoxData = cropper.getCropBoxData(); 45 | 46 | assert.ok(canvasData.width >= cropBoxData.width); 47 | assert.ok(canvasData.height >= cropBoxData.height); 48 | 49 | done(); 50 | } 51 | }); 52 | }); 53 | 54 | QUnit.test('options#viewMode: 2', function (assert) { 55 | var done = assert.async(); 56 | var util = window.Util; 57 | var image = util.createImage(); 58 | 59 | assert.expect(1); 60 | 61 | return new Cropper(image, { 62 | viewMode: 2, 63 | 64 | ready: function () { 65 | var cropper = this.cropper; 66 | var canvasData = cropper.zoom(-0.5).getCanvasData(); // Zoom out 67 | var containerData = cropper.getContainerData(); 68 | 69 | assert.ok(canvasData.width >= containerData.width || 70 | canvasData.height >= containerData.height); 71 | 72 | done(); 73 | } 74 | }); 75 | }); 76 | 77 | QUnit.test('options#viewMode: 3', function (assert) { 78 | var done = assert.async(); 79 | var util = window.Util; 80 | var image = util.createImage(); 81 | 82 | assert.expect(4); 83 | 84 | return new Cropper(image, { 85 | viewMode: 3, 86 | 87 | ready: function () { 88 | var cropper = this.cropper; 89 | var canvasData = cropper.zoom(-0.5).getCanvasData(); // Zoom out 90 | var containerData = cropper.getContainerData(); 91 | 92 | assert.ok(canvasData.left <= 0); 93 | assert.ok(canvasData.top <= 0); 94 | assert.ok(canvasData.width >= containerData.width); 95 | assert.ok(canvasData.height >= containerData.height); 96 | 97 | done(); 98 | } 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /examples/crop-a-round-image.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cropper.js 8 | 9 | 24 | 25 | 26 | 27 |
28 |

Crop a round image

29 |

Image

30 |
31 | Picture 32 |
33 |

Result

34 |

35 | 36 |

37 |
38 |
39 | 40 | 41 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/js/events.js: -------------------------------------------------------------------------------- 1 | import { 2 | EVENT_CROP, 3 | EVENT_CROP_END, 4 | EVENT_CROP_MOVE, 5 | EVENT_CROP_START, 6 | EVENT_DBLCLICK, 7 | EVENT_POINTER_DOWN, 8 | EVENT_POINTER_MOVE, 9 | EVENT_POINTER_UP, 10 | EVENT_RESIZE, 11 | EVENT_WHEEL, 12 | EVENT_ZOOM, 13 | } from './constants'; 14 | import { 15 | addListener, 16 | isFunction, 17 | proxy, 18 | removeListener, 19 | } from './utilities'; 20 | 21 | export default { 22 | bind() { 23 | const { element, options, cropper } = this; 24 | 25 | if (isFunction(options.cropstart)) { 26 | addListener(element, EVENT_CROP_START, options.cropstart); 27 | } 28 | 29 | if (isFunction(options.cropmove)) { 30 | addListener(element, EVENT_CROP_MOVE, options.cropmove); 31 | } 32 | 33 | if (isFunction(options.cropend)) { 34 | addListener(element, EVENT_CROP_END, options.cropend); 35 | } 36 | 37 | if (isFunction(options.crop)) { 38 | addListener(element, EVENT_CROP, options.crop); 39 | } 40 | 41 | if (isFunction(options.zoom)) { 42 | addListener(element, EVENT_ZOOM, options.zoom); 43 | } 44 | 45 | addListener(cropper, EVENT_POINTER_DOWN, (this.onCropStart = proxy(this.cropStart, this))); 46 | 47 | if (options.zoomable && options.zoomOnWheel) { 48 | addListener(cropper, EVENT_WHEEL, (this.onWheel = proxy(this.wheel, this))); 49 | } 50 | 51 | if (options.toggleDragModeOnDblclick) { 52 | addListener(cropper, EVENT_DBLCLICK, (this.onDblclick = proxy(this.dblclick, this))); 53 | } 54 | 55 | addListener(element.ownerDocument, EVENT_POINTER_MOVE, (this.onCropMove = proxy(this.cropMove, this))); 56 | addListener(element.ownerDocument, EVENT_POINTER_UP, (this.onCropEnd = proxy(this.cropEnd, this))); 57 | 58 | if (options.responsive) { 59 | addListener(window, EVENT_RESIZE, (this.onResize = proxy(this.resize, this))); 60 | } 61 | }, 62 | 63 | unbind() { 64 | const { element, options, cropper } = this; 65 | 66 | if (isFunction(options.cropstart)) { 67 | removeListener(element, EVENT_CROP_START, options.cropstart); 68 | } 69 | 70 | if (isFunction(options.cropmove)) { 71 | removeListener(element, EVENT_CROP_MOVE, options.cropmove); 72 | } 73 | 74 | if (isFunction(options.cropend)) { 75 | removeListener(element, EVENT_CROP_END, options.cropend); 76 | } 77 | 78 | if (isFunction(options.crop)) { 79 | removeListener(element, EVENT_CROP, options.crop); 80 | } 81 | 82 | if (isFunction(options.zoom)) { 83 | removeListener(element, EVENT_ZOOM, options.zoom); 84 | } 85 | 86 | removeListener(cropper, EVENT_POINTER_DOWN, this.onCropStart); 87 | 88 | if (options.zoomable && options.zoomOnWheel) { 89 | removeListener(cropper, EVENT_WHEEL, this.onWheel); 90 | } 91 | 92 | if (options.toggleDragModeOnDblclick) { 93 | removeListener(cropper, EVENT_DBLCLICK, this.onDblclick); 94 | } 95 | 96 | removeListener(element.ownerDocument, EVENT_POINTER_MOVE, this.onCropMove); 97 | removeListener(element.ownerDocument, EVENT_POINTER_UP, this.onCropEnd); 98 | 99 | if (options.responsive) { 100 | removeListener(window, EVENT_RESIZE, this.onResize); 101 | } 102 | }, 103 | }; 104 | -------------------------------------------------------------------------------- /dist/cropper.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Cropper.js v1.1.3 3 | * https://github.com/fengyuanchen/cropperjs 4 | * 5 | * Copyright (c) 2015-2017 Chen Fengyuan 6 | * Released under the MIT license 7 | * 8 | * Date: 2017-10-21T09:27:29.883Z 9 | */.cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal,.cropper-wrap-box{bottom:0;left:0;position:absolute;right:0;top:0}.cropper-canvas,.cropper-wrap-box{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline-color:rgba(51,153,255,.75);outline:1px solid #39f;overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.33333%;left:0;top:33.33333%;width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.33333%;top:0;width:33.33333%}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:after,.cropper-center:before{background-color:#eee;content:" ";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:e-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:n-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:w-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:s-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:e-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:n-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:w-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:ne-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nw-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:sw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:se-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width:768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width:992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width:1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:" ";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC")}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed} 10 | /*# sourceMappingURL=cropper.min.css.map */ -------------------------------------------------------------------------------- /examples/customize-preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cropper.js 8 | 9 | 44 | 45 | 46 | 47 |
48 |

Customize preview for Cropper

49 |
50 |
51 | Picture 52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /examples/mask-an-image.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Cropper.js 9 | 10 | 27 | 28 | 29 | 30 | 31 |
32 |

Mask an image (Redaction)

33 |

Image

34 |
35 | Picture 36 |
37 |

Result

38 |

39 |
40 |
41 | 42 | 43 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cropper.js 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/js/preview.js: -------------------------------------------------------------------------------- 1 | import { 2 | DATA_PREVIEW, 3 | } from './constants'; 4 | import { 5 | each, 6 | empty, 7 | extend, 8 | getData, 9 | getTransforms, 10 | removeData, 11 | setData, 12 | setStyle, 13 | } from './utilities'; 14 | 15 | export default { 16 | initPreview() { 17 | const { crossOrigin } = this; 18 | const { preview } = this.options; 19 | const url = crossOrigin ? this.crossOriginUrl : this.url; 20 | const image = document.createElement('img'); 21 | 22 | if (crossOrigin) { 23 | image.crossOrigin = crossOrigin; 24 | } 25 | 26 | image.src = url; 27 | this.viewBox.appendChild(image); 28 | this.image2 = image; 29 | 30 | if (!preview) { 31 | return; 32 | } 33 | 34 | const previews = preview.querySelector ? [preview] : document.querySelectorAll(preview); 35 | 36 | this.previews = previews; 37 | 38 | each(previews, (element) => { 39 | const img = document.createElement('img'); 40 | 41 | // Save the original size for recover 42 | setData(element, DATA_PREVIEW, { 43 | width: element.offsetWidth, 44 | height: element.offsetHeight, 45 | html: element.innerHTML, 46 | }); 47 | 48 | if (crossOrigin) { 49 | img.crossOrigin = crossOrigin; 50 | } 51 | 52 | img.src = url; 53 | 54 | /** 55 | * Override img element styles 56 | * Add `display:block` to avoid margin top issue 57 | * Add `height:auto` to override `height` attribute on IE8 58 | * (Occur only when margin-top <= -height) 59 | */ 60 | img.style.cssText = ( 61 | 'display:block;' + 62 | 'width:100%;' + 63 | 'height:auto;' + 64 | 'min-width:0!important;' + 65 | 'min-height:0!important;' + 66 | 'max-width:none!important;' + 67 | 'max-height:none!important;' + 68 | 'image-orientation:0deg!important;"' 69 | ); 70 | 71 | empty(element); 72 | element.appendChild(img); 73 | }); 74 | }, 75 | 76 | resetPreview() { 77 | each(this.previews, (element) => { 78 | const data = getData(element, DATA_PREVIEW); 79 | 80 | setStyle(element, { 81 | width: data.width, 82 | height: data.height, 83 | }); 84 | 85 | element.innerHTML = data.html; 86 | removeData(element, DATA_PREVIEW); 87 | }); 88 | }, 89 | 90 | preview() { 91 | const { imageData, canvasData, cropBoxData } = this; 92 | const { width: cropBoxWidth, height: cropBoxHeight } = cropBoxData; 93 | const { width, height } = imageData; 94 | const left = cropBoxData.left - canvasData.left - imageData.left; 95 | const top = cropBoxData.top - canvasData.top - imageData.top; 96 | 97 | if (!this.cropped || this.disabled) { 98 | return; 99 | } 100 | 101 | setStyle(this.image2, extend({ 102 | width, 103 | height, 104 | }, getTransforms(extend({ 105 | translateX: -left, 106 | translateY: -top, 107 | }, imageData)))); 108 | 109 | each(this.previews, (element) => { 110 | const data = getData(element, DATA_PREVIEW); 111 | const originalWidth = data.width; 112 | const originalHeight = data.height; 113 | let newWidth = originalWidth; 114 | let newHeight = originalHeight; 115 | let ratio = 1; 116 | 117 | if (cropBoxWidth) { 118 | ratio = originalWidth / cropBoxWidth; 119 | newHeight = cropBoxHeight * ratio; 120 | } 121 | 122 | if (cropBoxHeight && newHeight > originalHeight) { 123 | ratio = originalHeight / cropBoxHeight; 124 | newWidth = cropBoxWidth * ratio; 125 | newHeight = originalHeight; 126 | } 127 | 128 | setStyle(element, { 129 | width: newWidth, 130 | height: newHeight, 131 | }); 132 | 133 | setStyle(element.getElementsByTagName('img')[0], extend({ 134 | width: width * ratio, 135 | height: height * ratio, 136 | }, getTransforms(extend({ 137 | translateX: -left * ratio, 138 | translateY: -top * ratio, 139 | }, imageData)))); 140 | }); 141 | }, 142 | }; 143 | -------------------------------------------------------------------------------- /docs/css/main.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | padding-left: .75rem; 3 | padding-right: .75rem; 4 | } 5 | 6 | label.btn { 7 | margin-bottom: 0; 8 | } 9 | 10 | .d-flex > .btn { 11 | flex: 1; 12 | } 13 | 14 | .carbonads { 15 | border-radius: .25rem; 16 | border: 1px solid #ccc; 17 | font-size: .875rem; 18 | overflow: hidden; 19 | padding: 1rem; 20 | } 21 | 22 | .carbon-wrap { 23 | overflow: hidden; 24 | } 25 | 26 | .carbon-img { 27 | clear: left; 28 | display: block; 29 | float: left; 30 | } 31 | 32 | .carbon-text, 33 | .carbon-poweredby { 34 | display: block; 35 | margin-left: 140px; 36 | } 37 | 38 | .carbon-text, 39 | .carbon-text:hover, 40 | .carbon-text:focus { 41 | color: #fff; 42 | text-decoration: none; 43 | } 44 | 45 | .carbon-poweredby, 46 | .carbon-poweredby:hover, 47 | .carbon-poweredby:focus { 48 | color: #ddd; 49 | text-decoration: none; 50 | } 51 | 52 | @media (min-width: 768px) { 53 | .carbonads { 54 | float: right; 55 | margin-bottom: -1rem; 56 | margin-top: -1rem; 57 | max-width: 360px; 58 | } 59 | } 60 | 61 | .footer { 62 | font-size: .875rem; 63 | } 64 | 65 | .heart { 66 | color: #ddd; 67 | display: block; 68 | height: 2rem; 69 | line-height: 2rem; 70 | margin-bottom: 0; 71 | margin-top: 1rem; 72 | position: relative; 73 | text-align: center; 74 | width: 100%; 75 | } 76 | 77 | .heart:hover { 78 | color: #ff4136; 79 | } 80 | 81 | .heart::before { 82 | border-top: 1px solid #eee; 83 | content: " "; 84 | display: block; 85 | height: 0; 86 | left: 0; 87 | position: absolute; 88 | right: 0; 89 | top: 50%; 90 | } 91 | 92 | .heart::after { 93 | background-color: #fff; 94 | content: "♥"; 95 | padding-left: .5rem; 96 | padding-right: .5rem; 97 | position: relative; 98 | z-index: 1; 99 | } 100 | 101 | .img-container, 102 | .img-preview { 103 | background-color: #f7f7f7; 104 | text-align: center; 105 | width: 100%; 106 | } 107 | 108 | .img-container { 109 | margin-bottom: 1rem; 110 | max-height: 497px; 111 | min-height: 200px; 112 | } 113 | 114 | @media (min-width: 768px) { 115 | .img-container { 116 | min-height: 497px; 117 | } 118 | } 119 | 120 | .img-container > img { 121 | max-width: 100%; 122 | } 123 | 124 | .docs-preview { 125 | margin-right: -1rem; 126 | } 127 | 128 | .img-preview { 129 | float: left; 130 | margin-bottom: .5rem; 131 | margin-right: .5rem; 132 | overflow: hidden; 133 | } 134 | 135 | .img-preview > img { 136 | max-width: 100%; 137 | } 138 | 139 | .preview-lg { 140 | height: 9rem; 141 | width: 16rem; 142 | } 143 | 144 | .preview-md { 145 | height: 4.5rem; 146 | width: 8rem; 147 | } 148 | 149 | .preview-sm { 150 | height: 2.25rem; 151 | width: 4rem; 152 | } 153 | 154 | .preview-xs { 155 | height: 1.125rem; 156 | margin-right: 0; 157 | width: 2rem; 158 | } 159 | 160 | .docs-data > .input-group { 161 | margin-bottom: .5rem; 162 | } 163 | 164 | .docs-data > .input-group > label { 165 | justify-content: center; 166 | min-width: 5rem; 167 | } 168 | 169 | .docs-data > .input-group > span { 170 | justify-content: center; 171 | min-width: 3rem; 172 | } 173 | 174 | .docs-buttons > .btn, 175 | .docs-buttons > .btn-group, 176 | .docs-buttons > .form-control { 177 | margin-bottom: .5rem; 178 | margin-right: .25rem; 179 | } 180 | 181 | .docs-toggles > .btn, 182 | .docs-toggles > .btn-group, 183 | .docs-toggles > .dropdown { 184 | margin-bottom: .5rem; 185 | } 186 | 187 | .docs-tooltip { 188 | display: block; 189 | margin: -.5rem -.75rem; 190 | padding: .5rem .75rem; 191 | } 192 | 193 | .docs-tooltip > .icon { 194 | margin: 0 -.25rem; 195 | vertical-align: top; 196 | } 197 | 198 | .tooltip-inner { 199 | white-space: normal; 200 | } 201 | 202 | .btn-upload .tooltip-inner, 203 | .btn-toggle .tooltip-inner { 204 | white-space: nowrap; 205 | } 206 | 207 | .btn-toggle { 208 | padding: .5rem; 209 | } 210 | 211 | .btn-toggle > .docs-tooltip { 212 | margin: -.5rem; 213 | padding: .5rem; 214 | } 215 | 216 | @media (max-width: 400px) { 217 | .btn-group-crop { 218 | margin-right: -1rem!important; 219 | } 220 | 221 | .btn-group-crop > .btn { 222 | padding-left: .5rem; 223 | padding-right: .5rem; 224 | } 225 | 226 | .btn-group-crop .docs-tooltip { 227 | margin-left: -.5rem; 228 | margin-right: -.5rem; 229 | padding-left: .5rem; 230 | padding-right: .5rem; 231 | } 232 | } 233 | 234 | .docs-options .dropdown-menu { 235 | width: 100%; 236 | } 237 | 238 | .docs-options .dropdown-menu > li { 239 | font-size: .875rem; 240 | padding-left: 1rem; 241 | padding-right: 1rem; 242 | } 243 | 244 | .docs-options .dropdown-menu > li:hover { 245 | background-color: #f7f7f7; 246 | } 247 | 248 | .docs-options .dropdown-menu > li > label { 249 | display: block; 250 | } 251 | 252 | .docs-cropped .modal-body { 253 | text-align: center; 254 | } 255 | 256 | .docs-cropped .modal-body > img, 257 | .docs-cropped .modal-body > canvas { 258 | max-width: 100%; 259 | } 260 | -------------------------------------------------------------------------------- /src/js/handlers.js: -------------------------------------------------------------------------------- 1 | import { 2 | ACTION_CROP, 3 | ACTION_ZOOM, 4 | CLASS_CROP, 5 | CLASS_MODAL, 6 | DATA_ACTION, 7 | DRAG_MODE_CROP, 8 | DRAG_MODE_MOVE, 9 | DRAG_MODE_NONE, 10 | EVENT_CROP_END, 11 | EVENT_CROP_MOVE, 12 | EVENT_CROP_START, 13 | REGEXP_ACTIONS, 14 | } from './constants'; 15 | import { 16 | addClass, 17 | dispatchEvent, 18 | each, 19 | extend, 20 | getData, 21 | getPointer, 22 | hasClass, 23 | toggleClass, 24 | } from './utilities'; 25 | 26 | export default { 27 | resize() { 28 | const { options, container, containerData } = this; 29 | const minContainerWidth = Number(options.minContainerWidth) || 200; 30 | const minContainerHeight = Number(options.minContainerHeight) || 100; 31 | 32 | if (this.disabled || containerData.width <= minContainerWidth || 33 | containerData.height <= minContainerHeight) { 34 | return; 35 | } 36 | 37 | const ratio = container.offsetWidth / containerData.width; 38 | 39 | // Resize when width changed or height changed 40 | if (ratio !== 1 || container.offsetHeight !== containerData.height) { 41 | let canvasData; 42 | let cropBoxData; 43 | 44 | if (options.restore) { 45 | canvasData = this.getCanvasData(); 46 | cropBoxData = this.getCropBoxData(); 47 | } 48 | 49 | this.render(); 50 | 51 | if (options.restore) { 52 | this.setCanvasData(each(canvasData, (n, i) => { 53 | canvasData[i] = n * ratio; 54 | })); 55 | this.setCropBoxData(each(cropBoxData, (n, i) => { 56 | cropBoxData[i] = n * ratio; 57 | })); 58 | } 59 | } 60 | }, 61 | 62 | dblclick() { 63 | if (this.disabled || this.options.dragMode === DRAG_MODE_NONE) { 64 | return; 65 | } 66 | 67 | this.setDragMode(hasClass(this.dragBox, CLASS_CROP) ? DRAG_MODE_MOVE : DRAG_MODE_CROP); 68 | }, 69 | 70 | wheel(e) { 71 | const ratio = Number(this.options.wheelZoomRatio) || 0.1; 72 | let delta = 1; 73 | 74 | if (this.disabled) { 75 | return; 76 | } 77 | 78 | e.preventDefault(); 79 | 80 | // Limit wheel speed to prevent zoom too fast (#21) 81 | if (this.wheeling) { 82 | return; 83 | } 84 | 85 | this.wheeling = true; 86 | 87 | setTimeout(() => { 88 | this.wheeling = false; 89 | }, 50); 90 | 91 | if (e.deltaY) { 92 | delta = e.deltaY > 0 ? 1 : -1; 93 | } else if (e.wheelDelta) { 94 | delta = -e.wheelDelta / 120; 95 | } else if (e.detail) { 96 | delta = e.detail > 0 ? 1 : -1; 97 | } 98 | 99 | this.zoom(-delta * ratio, e); 100 | }, 101 | 102 | cropStart(e) { 103 | if (this.disabled) { 104 | return; 105 | } 106 | 107 | const { options, pointers } = this; 108 | let action; 109 | 110 | if (e.changedTouches) { 111 | // Handle touch event 112 | each(e.changedTouches, (touch) => { 113 | pointers[touch.identifier] = getPointer(touch); 114 | }); 115 | } else { 116 | // Handle mouse event and pointer event 117 | pointers[e.pointerId || 0] = getPointer(e); 118 | } 119 | 120 | if (Object.keys(pointers).length > 1 && options.zoomable && options.zoomOnTouch) { 121 | action = ACTION_ZOOM; 122 | } else { 123 | action = getData(e.target, DATA_ACTION); 124 | } 125 | 126 | if (!REGEXP_ACTIONS.test(action)) { 127 | return; 128 | } 129 | 130 | if (dispatchEvent(this.element, EVENT_CROP_START, { 131 | originalEvent: e, 132 | action, 133 | }) === false) { 134 | return; 135 | } 136 | 137 | e.preventDefault(); 138 | 139 | this.action = action; 140 | this.cropping = false; 141 | 142 | if (action === ACTION_CROP) { 143 | this.cropping = true; 144 | addClass(this.dragBox, CLASS_MODAL); 145 | } 146 | }, 147 | 148 | cropMove(e) { 149 | const { action } = this; 150 | 151 | if (this.disabled || !action) { 152 | return; 153 | } 154 | 155 | const { pointers } = this; 156 | 157 | e.preventDefault(); 158 | 159 | if (dispatchEvent(this.element, EVENT_CROP_MOVE, { 160 | originalEvent: e, 161 | action, 162 | }) === false) { 163 | return; 164 | } 165 | 166 | if (e.changedTouches) { 167 | each(e.changedTouches, (touch) => { 168 | extend(pointers[touch.identifier], getPointer(touch, true)); 169 | }); 170 | } else { 171 | extend(pointers[e.pointerId || 0], getPointer(e, true)); 172 | } 173 | 174 | this.change(e); 175 | }, 176 | 177 | cropEnd(e) { 178 | if (this.disabled) { 179 | return; 180 | } 181 | 182 | const { action, pointers } = this; 183 | 184 | if (e.changedTouches) { 185 | each(e.changedTouches, (touch) => { 186 | delete pointers[touch.identifier]; 187 | }); 188 | } else { 189 | delete pointers[e.pointerId || 0]; 190 | } 191 | 192 | if (!action) { 193 | return; 194 | } 195 | 196 | e.preventDefault(); 197 | 198 | if (!Object.keys(pointers).length) { 199 | this.action = ''; 200 | } 201 | 202 | if (this.cropping) { 203 | this.cropping = false; 204 | toggleClass(this.dragBox, CLASS_MODAL, this.cropped && this.options.modal); 205 | } 206 | 207 | dispatchEvent(this.element, EVENT_CROP_END, { 208 | originalEvent: e, 209 | action, 210 | }); 211 | }, 212 | }; 213 | -------------------------------------------------------------------------------- /src/css/cropper.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --blue: #39f; 3 | } 4 | 5 | .cropper-container { 6 | direction: ltr; 7 | font-size: 0; 8 | line-height: 0; 9 | position: relative; 10 | touch-action: none; 11 | user-select: none; 12 | 13 | & img { 14 | /* Avoid margin top issue (Occur only when margin-top <= -height) */ 15 | display: block; 16 | height: 100%; 17 | image-orientation: 0deg; 18 | max-height: none !important; 19 | max-width: none !important; 20 | min-height: 0 !important; 21 | min-width: 0 !important; 22 | width: 100%; 23 | } 24 | } 25 | 26 | .cropper-wrap-box, 27 | .cropper-canvas, 28 | .cropper-drag-box, 29 | .cropper-crop-box, 30 | .cropper-modal { 31 | bottom: 0; 32 | left: 0; 33 | position: absolute; 34 | right: 0; 35 | top: 0; 36 | } 37 | 38 | .cropper-wrap-box, 39 | .cropper-canvas { 40 | overflow: hidden; 41 | } 42 | 43 | .cropper-drag-box { 44 | background-color: #fff; 45 | opacity: 0; 46 | } 47 | 48 | .cropper-modal { 49 | background-color: #000; 50 | opacity: .5; 51 | } 52 | 53 | .cropper-view-box { 54 | display: block; 55 | height: 100%; 56 | outline-color: color(var(--blue) alpha(75%)); 57 | outline: 1px solid var(--blue); 58 | overflow: hidden; 59 | width: 100%; 60 | } 61 | 62 | .cropper-dashed { 63 | border: 0 dashed #eee; 64 | display: block; 65 | opacity: .5; 66 | position: absolute; 67 | 68 | &.dashed-h { 69 | border-bottom-width: 1px; 70 | border-top-width: 1px; 71 | height: calc(100 / 3)%; 72 | left: 0; 73 | top: calc(100 / 3)%; 74 | width: 100%; 75 | } 76 | 77 | &.dashed-v { 78 | border-left-width: 1px; 79 | border-right-width: 1px; 80 | height: 100%; 81 | left: calc(100 / 3)%; 82 | top: 0; 83 | width: calc(100 / 3)%; 84 | } 85 | } 86 | 87 | .cropper-center { 88 | display: block; 89 | height: 0; 90 | left: 50%; 91 | opacity: .75; 92 | position: absolute; 93 | top: 50%; 94 | width: 0; 95 | 96 | &:before, 97 | &:after { 98 | background-color: #eee; 99 | content: ' '; 100 | display: block; 101 | position: absolute; 102 | } 103 | 104 | &:before { 105 | height: 1px; 106 | left: -3px; 107 | top: 0; 108 | width: 7px; 109 | } 110 | 111 | &:after { 112 | height: 7px; 113 | left: 0; 114 | top: -3px; 115 | width: 1px; 116 | } 117 | } 118 | 119 | .cropper-face, 120 | .cropper-line, 121 | .cropper-point { 122 | display: block; 123 | height: 100%; 124 | opacity: .1; 125 | position: absolute; 126 | width: 100%; 127 | } 128 | 129 | .cropper-face { 130 | background-color: #fff; 131 | left: 0; 132 | top: 0; 133 | } 134 | 135 | .cropper-line { 136 | background-color: var(--blue); 137 | 138 | &.line-e { 139 | cursor: e-resize; 140 | right: -3px; 141 | top: 0; 142 | width: 5px; 143 | } 144 | 145 | &.line-n { 146 | cursor: n-resize; 147 | height: 5px; 148 | left: 0; 149 | top: -3px; 150 | } 151 | 152 | &.line-w { 153 | cursor: w-resize; 154 | left: -3px; 155 | top: 0; 156 | width: 5px; 157 | } 158 | 159 | &.line-s { 160 | bottom: -3px; 161 | cursor: s-resize; 162 | height: 5px; 163 | left: 0; 164 | } 165 | } 166 | 167 | .cropper-point { 168 | background-color: var(--blue); 169 | height: 5px; 170 | opacity: .75; 171 | width: 5px; 172 | 173 | &.point-e { 174 | cursor: e-resize; 175 | margin-top: -3px; 176 | right: -3px; 177 | top: 50%; 178 | } 179 | 180 | &.point-n { 181 | cursor: n-resize; 182 | left: 50%; 183 | margin-left: -3px; 184 | top: -3px; 185 | } 186 | 187 | &.point-w { 188 | cursor: w-resize; 189 | left: -3px; 190 | margin-top: -3px; 191 | top: 50%; 192 | } 193 | 194 | &.point-s { 195 | bottom: -3px; 196 | cursor: s-resize; 197 | left: 50%; 198 | margin-left: -3px; 199 | } 200 | 201 | &.point-ne { 202 | cursor: ne-resize; 203 | right: -3px; 204 | top: -3px; 205 | } 206 | 207 | &.point-nw { 208 | cursor: nw-resize; 209 | left: -3px; 210 | top: -3px; 211 | } 212 | 213 | &.point-sw { 214 | bottom: -3px; 215 | cursor: sw-resize; 216 | left: -3px; 217 | } 218 | 219 | &.point-se { 220 | bottom: -3px; 221 | cursor: se-resize; 222 | height: 20px; 223 | opacity: 1; 224 | right: -3px; 225 | width: 20px; 226 | 227 | @media (min-width: 768px) { 228 | height: 15px; 229 | width: 15px; 230 | } 231 | 232 | @media (min-width: 992px) { 233 | height: 10px; 234 | width: 10px; 235 | } 236 | 237 | @media (min-width: 1200px) { 238 | height: 5px; 239 | opacity: .75; 240 | width: 5px; 241 | } 242 | } 243 | 244 | &.point-se:before { 245 | background-color: var(--blue); 246 | bottom: -50%; 247 | content: ' '; 248 | display: block; 249 | height: 200%; 250 | opacity: 0; 251 | position: absolute; 252 | right: -50%; 253 | width: 200%; 254 | } 255 | } 256 | 257 | .cropper-invisible { 258 | opacity: 0; 259 | } 260 | 261 | .cropper-bg { 262 | background-image: url('../images/bg.png'); 263 | } 264 | 265 | .cropper-hide { 266 | display: block; 267 | height: 0; 268 | position: absolute; 269 | width: 0; 270 | } 271 | 272 | .cropper-hidden { 273 | display: none !important; 274 | } 275 | 276 | .cropper-move { 277 | cursor: move; 278 | } 279 | 280 | .cropper-crop { 281 | cursor: crosshair; 282 | } 283 | 284 | .cropper-disabled .cropper-drag-box, 285 | .cropper-disabled .cropper-face, 286 | .cropper-disabled .cropper-line, 287 | .cropper-disabled .cropper-point { 288 | cursor: not-allowed; 289 | } 290 | 291 | -------------------------------------------------------------------------------- /dist/cropper.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Cropper.js v1.1.3 3 | * https://github.com/fengyuanchen/cropperjs 4 | * 5 | * Copyright (c) 2015-2017 Chen Fengyuan 6 | * Released under the MIT license 7 | * 8 | * Date: 2017-10-21T09:27:29.883Z 9 | */ 10 | 11 | .cropper-container { 12 | direction: ltr; 13 | font-size: 0; 14 | line-height: 0; 15 | position: relative; 16 | -ms-touch-action: none; 17 | touch-action: none; 18 | -webkit-user-select: none; 19 | -moz-user-select: none; 20 | -ms-user-select: none; 21 | user-select: none; 22 | } 23 | 24 | .cropper-container img {/*Avoid margin top issue (Occur only when margin-top <= -height) 25 | */ 26 | display: block; 27 | height: 100%; 28 | image-orientation: 0deg; 29 | max-height: none !important; 30 | max-width: none !important; 31 | min-height: 0 !important; 32 | min-width: 0 !important; 33 | width: 100%; 34 | } 35 | 36 | .cropper-wrap-box, 37 | .cropper-canvas, 38 | .cropper-drag-box, 39 | .cropper-crop-box, 40 | .cropper-modal { 41 | bottom: 0; 42 | left: 0; 43 | position: absolute; 44 | right: 0; 45 | top: 0; 46 | } 47 | 48 | .cropper-wrap-box, 49 | .cropper-canvas { 50 | overflow: hidden; 51 | } 52 | 53 | .cropper-drag-box { 54 | background-color: #fff; 55 | opacity: 0; 56 | } 57 | 58 | .cropper-modal { 59 | background-color: #000; 60 | opacity: .5; 61 | } 62 | 63 | .cropper-view-box { 64 | display: block; 65 | height: 100%; 66 | outline-color: rgba(51, 153, 255, 0.75); 67 | outline: 1px solid #39f; 68 | overflow: hidden; 69 | width: 100%; 70 | } 71 | 72 | .cropper-dashed { 73 | border: 0 dashed #eee; 74 | display: block; 75 | opacity: .5; 76 | position: absolute; 77 | } 78 | 79 | .cropper-dashed.dashed-h { 80 | border-bottom-width: 1px; 81 | border-top-width: 1px; 82 | height: 33.33333%; 83 | left: 0; 84 | top: 33.33333%; 85 | width: 100%; 86 | } 87 | 88 | .cropper-dashed.dashed-v { 89 | border-left-width: 1px; 90 | border-right-width: 1px; 91 | height: 100%; 92 | left: 33.33333%; 93 | top: 0; 94 | width: 33.33333%; 95 | } 96 | 97 | .cropper-center { 98 | display: block; 99 | height: 0; 100 | left: 50%; 101 | opacity: .75; 102 | position: absolute; 103 | top: 50%; 104 | width: 0; 105 | } 106 | 107 | .cropper-center:before, 108 | .cropper-center:after { 109 | background-color: #eee; 110 | content: ' '; 111 | display: block; 112 | position: absolute; 113 | } 114 | 115 | .cropper-center:before { 116 | height: 1px; 117 | left: -3px; 118 | top: 0; 119 | width: 7px; 120 | } 121 | 122 | .cropper-center:after { 123 | height: 7px; 124 | left: 0; 125 | top: -3px; 126 | width: 1px; 127 | } 128 | 129 | .cropper-face, 130 | .cropper-line, 131 | .cropper-point { 132 | display: block; 133 | height: 100%; 134 | opacity: .1; 135 | position: absolute; 136 | width: 100%; 137 | } 138 | 139 | .cropper-face { 140 | background-color: #fff; 141 | left: 0; 142 | top: 0; 143 | } 144 | 145 | .cropper-line { 146 | background-color: #39f; 147 | } 148 | 149 | .cropper-line.line-e { 150 | cursor: e-resize; 151 | right: -3px; 152 | top: 0; 153 | width: 5px; 154 | } 155 | 156 | .cropper-line.line-n { 157 | cursor: n-resize; 158 | height: 5px; 159 | left: 0; 160 | top: -3px; 161 | } 162 | 163 | .cropper-line.line-w { 164 | cursor: w-resize; 165 | left: -3px; 166 | top: 0; 167 | width: 5px; 168 | } 169 | 170 | .cropper-line.line-s { 171 | bottom: -3px; 172 | cursor: s-resize; 173 | height: 5px; 174 | left: 0; 175 | } 176 | 177 | .cropper-point { 178 | background-color: #39f; 179 | height: 5px; 180 | opacity: .75; 181 | width: 5px; 182 | } 183 | 184 | .cropper-point.point-e { 185 | cursor: e-resize; 186 | margin-top: -3px; 187 | right: -3px; 188 | top: 50%; 189 | } 190 | 191 | .cropper-point.point-n { 192 | cursor: n-resize; 193 | left: 50%; 194 | margin-left: -3px; 195 | top: -3px; 196 | } 197 | 198 | .cropper-point.point-w { 199 | cursor: w-resize; 200 | left: -3px; 201 | margin-top: -3px; 202 | top: 50%; 203 | } 204 | 205 | .cropper-point.point-s { 206 | bottom: -3px; 207 | cursor: s-resize; 208 | left: 50%; 209 | margin-left: -3px; 210 | } 211 | 212 | .cropper-point.point-ne { 213 | cursor: ne-resize; 214 | right: -3px; 215 | top: -3px; 216 | } 217 | 218 | .cropper-point.point-nw { 219 | cursor: nw-resize; 220 | left: -3px; 221 | top: -3px; 222 | } 223 | 224 | .cropper-point.point-sw { 225 | bottom: -3px; 226 | cursor: sw-resize; 227 | left: -3px; 228 | } 229 | 230 | .cropper-point.point-se { 231 | bottom: -3px; 232 | cursor: se-resize; 233 | height: 20px; 234 | opacity: 1; 235 | right: -3px; 236 | width: 20px; 237 | } 238 | 239 | @media (min-width: 768px) { 240 | .cropper-point.point-se { 241 | height: 15px; 242 | width: 15px; 243 | } 244 | } 245 | 246 | @media (min-width: 992px) { 247 | .cropper-point.point-se { 248 | height: 10px; 249 | width: 10px; 250 | } 251 | } 252 | 253 | @media (min-width: 1200px) { 254 | .cropper-point.point-se { 255 | height: 5px; 256 | opacity: .75; 257 | width: 5px; 258 | } 259 | } 260 | 261 | .cropper-point.point-se:before { 262 | background-color: #39f; 263 | bottom: -50%; 264 | content: ' '; 265 | display: block; 266 | height: 200%; 267 | opacity: 0; 268 | position: absolute; 269 | right: -50%; 270 | width: 200%; 271 | } 272 | 273 | .cropper-invisible { 274 | opacity: 0; 275 | } 276 | 277 | .cropper-bg { 278 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC'); 279 | } 280 | 281 | .cropper-hide { 282 | display: block; 283 | height: 0; 284 | position: absolute; 285 | width: 0; 286 | } 287 | 288 | .cropper-hidden { 289 | display: none !important; 290 | } 291 | 292 | .cropper-move { 293 | cursor: move; 294 | } 295 | 296 | .cropper-crop { 297 | cursor: crosshair; 298 | } 299 | 300 | .cropper-disabled .cropper-drag-box, 301 | .cropper-disabled .cropper-face, 302 | .cropper-disabled .cropper-line, 303 | .cropper-disabled .cropper-point { 304 | cursor: not-allowed; 305 | } 306 | 307 | -------------------------------------------------------------------------------- /docs/css/cropper.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Cropper.js v1.1.3 3 | * https://github.com/fengyuanchen/cropperjs 4 | * 5 | * Copyright (c) 2015-2017 Chen Fengyuan 6 | * Released under the MIT license 7 | * 8 | * Date: 2017-10-21T09:27:29.883Z 9 | */ 10 | 11 | .cropper-container { 12 | direction: ltr; 13 | font-size: 0; 14 | line-height: 0; 15 | position: relative; 16 | -ms-touch-action: none; 17 | touch-action: none; 18 | -webkit-user-select: none; 19 | -moz-user-select: none; 20 | -ms-user-select: none; 21 | user-select: none; 22 | } 23 | 24 | .cropper-container img {/*Avoid margin top issue (Occur only when margin-top <= -height) 25 | */ 26 | display: block; 27 | height: 100%; 28 | image-orientation: 0deg; 29 | max-height: none !important; 30 | max-width: none !important; 31 | min-height: 0 !important; 32 | min-width: 0 !important; 33 | width: 100%; 34 | } 35 | 36 | .cropper-wrap-box, 37 | .cropper-canvas, 38 | .cropper-drag-box, 39 | .cropper-crop-box, 40 | .cropper-modal { 41 | bottom: 0; 42 | left: 0; 43 | position: absolute; 44 | right: 0; 45 | top: 0; 46 | } 47 | 48 | .cropper-wrap-box, 49 | .cropper-canvas { 50 | overflow: hidden; 51 | } 52 | 53 | .cropper-drag-box { 54 | background-color: #fff; 55 | opacity: 0; 56 | } 57 | 58 | .cropper-modal { 59 | background-color: #000; 60 | opacity: .5; 61 | } 62 | 63 | .cropper-view-box { 64 | display: block; 65 | height: 100%; 66 | outline-color: rgba(51, 153, 255, 0.75); 67 | outline: 1px solid #39f; 68 | overflow: hidden; 69 | width: 100%; 70 | } 71 | 72 | .cropper-dashed { 73 | border: 0 dashed #eee; 74 | display: block; 75 | opacity: .5; 76 | position: absolute; 77 | } 78 | 79 | .cropper-dashed.dashed-h { 80 | border-bottom-width: 1px; 81 | border-top-width: 1px; 82 | height: 33.33333%; 83 | left: 0; 84 | top: 33.33333%; 85 | width: 100%; 86 | } 87 | 88 | .cropper-dashed.dashed-v { 89 | border-left-width: 1px; 90 | border-right-width: 1px; 91 | height: 100%; 92 | left: 33.33333%; 93 | top: 0; 94 | width: 33.33333%; 95 | } 96 | 97 | .cropper-center { 98 | display: block; 99 | height: 0; 100 | left: 50%; 101 | opacity: .75; 102 | position: absolute; 103 | top: 50%; 104 | width: 0; 105 | } 106 | 107 | .cropper-center:before, 108 | .cropper-center:after { 109 | background-color: #eee; 110 | content: ' '; 111 | display: block; 112 | position: absolute; 113 | } 114 | 115 | .cropper-center:before { 116 | height: 1px; 117 | left: -3px; 118 | top: 0; 119 | width: 7px; 120 | } 121 | 122 | .cropper-center:after { 123 | height: 7px; 124 | left: 0; 125 | top: -3px; 126 | width: 1px; 127 | } 128 | 129 | .cropper-face, 130 | .cropper-line, 131 | .cropper-point { 132 | display: block; 133 | height: 100%; 134 | opacity: .1; 135 | position: absolute; 136 | width: 100%; 137 | } 138 | 139 | .cropper-face { 140 | background-color: #fff; 141 | left: 0; 142 | top: 0; 143 | } 144 | 145 | .cropper-line { 146 | background-color: #39f; 147 | } 148 | 149 | .cropper-line.line-e { 150 | cursor: e-resize; 151 | right: -3px; 152 | top: 0; 153 | width: 5px; 154 | } 155 | 156 | .cropper-line.line-n { 157 | cursor: n-resize; 158 | height: 5px; 159 | left: 0; 160 | top: -3px; 161 | } 162 | 163 | .cropper-line.line-w { 164 | cursor: w-resize; 165 | left: -3px; 166 | top: 0; 167 | width: 5px; 168 | } 169 | 170 | .cropper-line.line-s { 171 | bottom: -3px; 172 | cursor: s-resize; 173 | height: 5px; 174 | left: 0; 175 | } 176 | 177 | .cropper-point { 178 | background-color: #39f; 179 | height: 5px; 180 | opacity: .75; 181 | width: 5px; 182 | } 183 | 184 | .cropper-point.point-e { 185 | cursor: e-resize; 186 | margin-top: -3px; 187 | right: -3px; 188 | top: 50%; 189 | } 190 | 191 | .cropper-point.point-n { 192 | cursor: n-resize; 193 | left: 50%; 194 | margin-left: -3px; 195 | top: -3px; 196 | } 197 | 198 | .cropper-point.point-w { 199 | cursor: w-resize; 200 | left: -3px; 201 | margin-top: -3px; 202 | top: 50%; 203 | } 204 | 205 | .cropper-point.point-s { 206 | bottom: -3px; 207 | cursor: s-resize; 208 | left: 50%; 209 | margin-left: -3px; 210 | } 211 | 212 | .cropper-point.point-ne { 213 | cursor: ne-resize; 214 | right: -3px; 215 | top: -3px; 216 | } 217 | 218 | .cropper-point.point-nw { 219 | cursor: nw-resize; 220 | left: -3px; 221 | top: -3px; 222 | } 223 | 224 | .cropper-point.point-sw { 225 | bottom: -3px; 226 | cursor: sw-resize; 227 | left: -3px; 228 | } 229 | 230 | .cropper-point.point-se { 231 | bottom: -3px; 232 | cursor: se-resize; 233 | height: 20px; 234 | opacity: 1; 235 | right: -3px; 236 | width: 20px; 237 | } 238 | 239 | @media (min-width: 768px) { 240 | .cropper-point.point-se { 241 | height: 15px; 242 | width: 15px; 243 | } 244 | } 245 | 246 | @media (min-width: 992px) { 247 | .cropper-point.point-se { 248 | height: 10px; 249 | width: 10px; 250 | } 251 | } 252 | 253 | @media (min-width: 1200px) { 254 | .cropper-point.point-se { 255 | height: 5px; 256 | opacity: .75; 257 | width: 5px; 258 | } 259 | } 260 | 261 | .cropper-point.point-se:before { 262 | background-color: #39f; 263 | bottom: -50%; 264 | content: ' '; 265 | display: block; 266 | height: 200%; 267 | opacity: 0; 268 | position: absolute; 269 | right: -50%; 270 | width: 200%; 271 | } 272 | 273 | .cropper-invisible { 274 | opacity: 0; 275 | } 276 | 277 | .cropper-bg { 278 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC'); 279 | } 280 | 281 | .cropper-hide { 282 | display: block; 283 | height: 0; 284 | position: absolute; 285 | width: 0; 286 | } 287 | 288 | .cropper-hidden { 289 | display: none !important; 290 | } 291 | 292 | .cropper-move { 293 | cursor: move; 294 | } 295 | 296 | .cropper-crop { 297 | cursor: crosshair; 298 | } 299 | 300 | .cropper-disabled .cropper-drag-box, 301 | .cropper-disabled .cropper-face, 302 | .cropper-disabled .cropper-line, 303 | .cropper-disabled .cropper-point { 304 | cursor: not-allowed; 305 | } 306 | 307 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.1.3 (Oct 21, 2017) 4 | 5 | - Fixed a bug of render when disable one of `rotatable` and `scalable` options (#241). 6 | 7 | ## 1.1.2 (Oct 18, 2017) 8 | 9 | - Normalize related decimal numbers when crop an image with canvas. 10 | 11 | ## 1.1.1 (Oct 11, 2017) 12 | 13 | - Supports to load in node environment (#237). 14 | - Fixed a bug of event binding (#238). 15 | 16 | ## 1.1.0 (Oct 8, 2017) 17 | 18 | - Added 4 new options to `getCroppedCanvas` method: `minWidth`, `minHeight`, `maxWidth` and `maxHeight`. 19 | - Enhanced image scaling: the `scaleX` and `scaleY` values should only be `1` or `-1` before, but now they can be any numbers. 20 | - Improved crop box resizing behaviour in the northeast, northwest, southeast and southwest directions. (#222). 21 | 22 | ## 1.0.0 (Sep 3, 2017) 23 | 24 | - Fixed a bug of zoom out after cleared the crop box in view mode 1, 2 and 3 (#209). 25 | - Improve crop box resizing behaviour in the east, west, south and north directions (#222). 26 | 27 | ## 1.0.0-rc.3 (Jul 7, 2017) 28 | 29 | - Added two new options (`imageSmoothingEnabled` and `imageSmoothingQuality`) to `getCroppedCanvas` method. 30 | - Fixed a bug of RegExp using (#195 by @arusakov). 31 | 32 | ## 1.0.0-rc.2 (May 30, 2017) 33 | 34 | - Fixed the issue of canvas box initialization (#179). 35 | 36 | ## 1.0.0-rc.1 (Apr 30, 2017) 37 | 38 | - Change the `main` field value from `dist/cropper.js` (UMD) to `dist/cropper.common.js` (CommonJS). 39 | - Added `module` and `browser` fields to `package.json`. 40 | 41 | ## 1.0.0-rc (Mar 25, 2017) 42 | 43 | - Fixed the bug of touch zoom (#161). 44 | - Fixed the bug of window resize (#162). 45 | - Improve the `toggleDragModeOnDblclick` option (only available when the `dragMode` option is set to `crop` or `move`) 46 | 47 | ## 1.0.0-beta.2 (Feb 25, 2017) 48 | 49 | - Fixed the bug of rotate square image lead image shrink (#155). 50 | - Improved RegExps for DataURL processing (#156). 51 | 52 | ## 1.0.0-beta.1 (Jan 21, 2017) 53 | 54 | - Use CSS3 2D Transforms instead of `left` and `top` for better performance (#138). 55 | - Set `withCredentials` attribute when read the image data by XMLHttpRequest (#141). 56 | 57 | ## 1.0.0-beta (Jan 1, 2017) 58 | 59 | - Supports to set an element for preview (#113). 60 | - Improved event handler for Pointer Events (#127). 61 | 62 | ## 1.0.0-alpha (Dec 4, 2016) 63 | 64 | - Built JavaScript with Rollup. 65 | - Build CSS with PostCSS. 66 | - Fixed a bug of auto crop when replace the image (#83). 67 | 68 | ## 0.8.1 (Sep 3, 2016) 69 | 70 | - Fixed the bug of cropping (#80). 71 | - Fixed the bug of calling `ready` event twice when call `replace` method (#81). 72 | - Fixed the bug of `getCroppedCanvas` when set `scalable` or `rotatable` to `false` (#82). 73 | 74 | ## 0.8.0 (Aug 18, 2016) 75 | 76 | - Removed `build` event. 77 | - Renamed `built` event to `ready`. 78 | - Fixed the error of orientation transform. 79 | - Ported code to ECMAScript 6. 80 | 81 | ## 0.7.2 (Jun 8, 2016) 82 | 83 | - Fixed a bug of `data-*` attributes setting and getting. 84 | - Fixed the calling order of `scale` and `rotate`. 85 | 86 | ## 0.7.1 (May 28, 2016) 87 | 88 | - Improved the rotate and scale transform behaviour. 89 | - Improved the `getCroppedCanvas` method (returns the whole canvas if it is not cropped). 90 | - Check cross origin setting when load image by XMLHTTPRequest. 91 | 92 | ## 0.7.0 (Mar 20, 2016) 93 | 94 | - Supports 7 custom events: `build`, `built`, `cropstart`, `cropmove`, `cropend`, `crop` and `zoom`. 95 | - The original callback options become shortcuts of these events now. 96 | - IE8 is no longer supported after added these custom events. 97 | 98 | ## 0.6.0 (Feb 22, 2016) 99 | 100 | - Added a new parameter to the `replace` method for applying filters. 101 | - Improved the image initializing for Safari. 102 | - Fixed incorrect size limitation of the crop box (#30). 103 | - Fixed incorrect cropped canvas when scaleX or scaleY great than 1. 104 | 105 | ## 0.5.6 (Jan 18, 2016) 106 | 107 | - Fixed crossOriginUrl undefined error when exists the `crossOrigin` property. 108 | - Fixed the issue in the "destroy" method (#24). 109 | - Optimized tests. 110 | 111 | ## 0.5.5 (Jan 1, 2016) 112 | 113 | - Fixed a dimension bug in the "getCroppedCanvas" method. 114 | - Added an example for cropping round image. 115 | 116 | ## 0.5.4 (Dec 28, 2015) 117 | 118 | - Supports to zoom from event triggering point. 119 | 120 | ## 0.5.3 (Dec 24, 2015) 121 | 122 | - Limit wheel speed to prevent zoom too fast (#21) 123 | - Improve the `setCropBoxData` method (#22) 124 | 125 | ## 0.5.2 (Dec 15, 2015) 126 | 127 | - Fix event handlers 128 | 129 | ## 0.5.1 (Dec 12, 2015) 130 | 131 | - Handle Data URL (avoid to use XMLHttpRequest to open a Data URL) 132 | - Handle ajax error when load ArrayBuffer 133 | - Not to transform the image to base64 when Orientation equals to `1` 134 | - Fix some typos 135 | 136 | ## 0.5.0 (Dec 5, 2015) 137 | 138 | - Added a new option: `checkOrientation` 139 | - Added a timestamp to the url of preview image 140 | 141 | ## 0.4.0 (Dec 2, 2015) 142 | 143 | - Added a new option: `restore` 144 | - Fixed #12: Added vendor prefixes to CSS `transform` 145 | 146 | ## 0.3.3 (Nov 30, 2015) 147 | 148 | - Floor the numerical parameters for `CanvasRenderingContext2D.drawImage` 149 | 150 | ## 0.3.2 (Nov 18, 2015) 151 | 152 | - Fixed #10: improve new crop box creating 153 | 154 | ## 0.3.1 (Nov 11, 2015) 155 | 156 | - Fixed #7: reset the `crossOrigin` when call the `replace` method 157 | 158 | ## 0.3.0 (Oct 28, 2015) 159 | 160 | - Supports four view modes 161 | - Supports three drag modes 162 | - Makes the crop box's borders and handlers visible when overflow 163 | - Added some examples 164 | - Fixed some issues 165 | 166 | ### Options 167 | 168 | - Added `viewMode` 169 | - Added `dragMode` 170 | - Renamed `touchDragZoom` to `zoomOnTouch` 171 | - Renamed `mouseWheelZoom` to `zoomOnWheel` 172 | - Renamed `doubleClickToggle` to `toggleDragModeOnDblclick` 173 | - Renamed `checkImageOrigin` to `checkCrossOrigin` 174 | - Removed `strict` (supported by `viewMode: 1`) 175 | - Removed `dragCrop` (supported by `dragMode: 'crop'`) 176 | 177 | ## 0.2.1 (Oct 28, 2015) 178 | 179 | - Fix the error jQuery reference on the `setCanvasData` method 180 | - Fix typo on the `destroy` method 181 | 182 | ## 0.2.0 (Oct 25, 2015) 183 | 184 | - Added 5 new methods: `moveTo`, `zoomTo`, `rotateTo`, `scaleX` and `scaleY` 185 | - Improved 4 methods: `move`, `zoom`, `rotate` and `getCanvasData` 186 | - Improved cropping 187 | 188 | ## 0.1.1 (Oct 10, 2015) 189 | 190 | - Improved canvas limitation 191 | - Improved crop box limitation 192 | - Improved preview for cross origin image 193 | 194 | ## 0.1.0 (Sep 25, 2015) 195 | 196 | - Supports touch (mobile) 197 | - Supports zoom 198 | - Supports rotation 199 | - Supports scale (flip) 200 | - Supports canvas 201 | - Supports multiple croppers 202 | - Cross-browser support 203 | - Supports 37 options: `aspectRatio`, `data`, `preview`, `strict`, `responsive`, `checkImageOrigin`, `modal`, `guides`, `center`, `highlight`, `background`, `autoCrop`, `autoCropArea`, `dragCrop`, `movable`, `rotatable`, `scalable`, `zoomable`, `mouseWheelZoom`, `wheelZoomRatio`, `touchDragZoom`, `cropBoxMovable`, `cropBoxResizable`, `doubleClickToggle`, `minCanvasWidth`, `minCanvasHeight`, `minCropBoxWidth`, `minCropBoxHeight`, `minContainerWidth`, `minContainerHeight`, `build`, `built`, `cropstart`, `cropmove`, `cropend`, `crop`, `zoom`. 204 | - Support 22 methods: `crop`, `reset`, `clear`, `replace`, `enable`, `disable`, `destroy`, `move`, `zoom`, `rotate`, `scale`, `getData`, `setData`, `getContainerData`, `getImageData`, `getCanvasData`, `setCanvasData`, `getCropBoxData`, `setCropBoxData`, `getCroppedCanvas`, `setAspectRatio`, `setDragMode`. 205 | -------------------------------------------------------------------------------- /docs/js/main.js: -------------------------------------------------------------------------------- 1 | window.onload = function () { 2 | 3 | 'use strict'; 4 | 5 | var Cropper = window.Cropper; 6 | var URL = window.URL || window.webkitURL; 7 | var container = document.querySelector('.img-container'); 8 | var image = container.getElementsByTagName('img').item(0); 9 | var download = document.getElementById('download'); 10 | var actions = document.getElementById('actions'); 11 | var dataX = document.getElementById('dataX'); 12 | var dataY = document.getElementById('dataY'); 13 | var dataHeight = document.getElementById('dataHeight'); 14 | var dataWidth = document.getElementById('dataWidth'); 15 | var dataRotate = document.getElementById('dataRotate'); 16 | var dataScaleX = document.getElementById('dataScaleX'); 17 | var dataScaleY = document.getElementById('dataScaleY'); 18 | var options = { 19 | aspectRatio: 321 / 180, 20 | preview: '.img-preview', 21 | ready: function (e) { 22 | console.log(e.type); 23 | }, 24 | cropstart: function (e) { 25 | console.log(e.type, e.detail.action); 26 | }, 27 | cropmove: function (e) { 28 | console.log(e.type, e.detail.action); 29 | }, 30 | cropend: function (e) { 31 | console.log(e.type, e.detail.action); 32 | }, 33 | crop: function (e) { 34 | var data = e.detail; 35 | 36 | console.log(e.type); 37 | dataX.value = Math.round(data.x); 38 | dataY.value = Math.round(data.y); 39 | dataHeight.value = Math.round(data.height); 40 | dataWidth.value = Math.round(data.width); 41 | dataRotate.value = typeof data.rotate !== 'undefined' ? data.rotate : ''; 42 | dataScaleX.value = typeof data.scaleX !== 'undefined' ? data.scaleX : ''; 43 | dataScaleY.value = typeof data.scaleY !== 'undefined' ? data.scaleY : ''; 44 | }, 45 | zoom: function (e) { 46 | console.log(e.type, e.detail.ratio); 47 | } 48 | }; 49 | var cropper = new Cropper(image, options); 50 | var originalImageURL = image.src; 51 | var uploadedImageType = 'image/jpeg'; 52 | var uploadedImageURL; 53 | 54 | // Tooltip 55 | $('[data-toggle="tooltip"]').tooltip(); 56 | 57 | // Buttons 58 | if (!document.createElement('canvas').getContext) { 59 | $('button[data-method="getCroppedCanvas"]').prop('disabled', true); 60 | } 61 | 62 | if (typeof document.createElement('cropper').style.transition === 'undefined') { 63 | $('button[data-method="rotate"]').prop('disabled', true); 64 | $('button[data-method="scale"]').prop('disabled', true); 65 | } 66 | 67 | // Download 68 | if (typeof download.download === 'undefined') { 69 | download.className += ' disabled'; 70 | } 71 | 72 | // Options 73 | actions.querySelector('.docs-toggles').onchange = function (event) { 74 | var e = event || window.event; 75 | var target = e.target || e.srcElement; 76 | var cropBoxData; 77 | var canvasData; 78 | var isCheckbox; 79 | var isRadio; 80 | 81 | if (!cropper) { 82 | return; 83 | } 84 | 85 | if (target.tagName.toLowerCase() === 'label') { 86 | target = target.querySelector('input'); 87 | } 88 | 89 | isCheckbox = target.type === 'checkbox'; 90 | isRadio = target.type === 'radio'; 91 | 92 | if (isCheckbox || isRadio) { 93 | if (isCheckbox) { 94 | options[target.name] = target.checked; 95 | cropBoxData = cropper.getCropBoxData(); 96 | canvasData = cropper.getCanvasData(); 97 | 98 | options.ready = function () { 99 | console.log('ready'); 100 | cropper.setCropBoxData(cropBoxData).setCanvasData(canvasData); 101 | }; 102 | } else { 103 | options[target.name] = target.value; 104 | options.ready = function () { 105 | console.log('ready'); 106 | }; 107 | } 108 | 109 | // Restart 110 | cropper.destroy(); 111 | cropper = new Cropper(image, options); 112 | } 113 | }; 114 | 115 | // Methods 116 | actions.querySelector('.docs-buttons').onclick = function (event) { 117 | var e = event || window.event; 118 | var target = e.target || e.srcElement; 119 | var cropped; 120 | var result; 121 | var input; 122 | var data; 123 | 124 | if (!cropper) { 125 | return; 126 | } 127 | 128 | while (target !== this) { 129 | if (target.getAttribute('data-method')) { 130 | break; 131 | } 132 | 133 | target = target.parentNode; 134 | } 135 | 136 | if (target === this || target.disabled || target.className.indexOf('disabled') > -1) { 137 | return; 138 | } 139 | 140 | data = { 141 | method: target.getAttribute('data-method'), 142 | target: target.getAttribute('data-target'), 143 | option: target.getAttribute('data-option') || undefined, 144 | secondOption: target.getAttribute('data-second-option') || undefined 145 | }; 146 | 147 | cropped = cropper.cropped; 148 | 149 | if (data.method) { 150 | if (typeof data.target !== 'undefined') { 151 | input = document.querySelector(data.target); 152 | 153 | if (!target.hasAttribute('data-option') && data.target && input) { 154 | try { 155 | data.option = JSON.parse(input.value); 156 | } catch (e) { 157 | console.log(e.message); 158 | } 159 | } 160 | } 161 | 162 | switch (data.method) { 163 | case 'rotate': 164 | if (cropped) { 165 | cropper.clear(); 166 | } 167 | 168 | break; 169 | 170 | case 'getCroppedCanvas': 171 | try { 172 | data.option = JSON.parse(data.option); 173 | } catch (e) { 174 | console.log(e.message); 175 | } 176 | 177 | if (uploadedImageType === 'image/jpeg') { 178 | if (!data.option) { 179 | data.option = {}; 180 | } 181 | 182 | data.option.fillColor = '#fff'; 183 | } 184 | 185 | break; 186 | } 187 | 188 | result = cropper[data.method](data.option, data.secondOption); 189 | 190 | switch (data.method) { 191 | case 'rotate': 192 | if (cropped) { 193 | cropper.crop(); 194 | } 195 | 196 | break; 197 | 198 | case 'scaleX': 199 | case 'scaleY': 200 | target.setAttribute('data-option', -data.option); 201 | break; 202 | 203 | case 'getCroppedCanvas': 204 | if (result) { 205 | // Bootstrap's Modal 206 | $('#getCroppedCanvasModal').modal().find('.modal-body').html(result); 207 | 208 | if (!download.disabled) { 209 | download.href = result.toDataURL(uploadedImageType); 210 | } 211 | } 212 | 213 | break; 214 | 215 | case 'destroy': 216 | cropper = null; 217 | 218 | if (uploadedImageURL) { 219 | URL.revokeObjectURL(uploadedImageURL); 220 | uploadedImageURL = ''; 221 | image.src = originalImageURL; 222 | } 223 | 224 | break; 225 | } 226 | 227 | if (typeof result === 'object' && result !== cropper && input) { 228 | try { 229 | input.value = JSON.stringify(result); 230 | } catch (e) { 231 | console.log(e.message); 232 | } 233 | } 234 | } 235 | }; 236 | 237 | document.body.onkeydown = function (event) { 238 | var e = event || window.event; 239 | 240 | if (!cropper || this.scrollTop > 300) { 241 | return; 242 | } 243 | 244 | switch (e.keyCode) { 245 | case 37: 246 | e.preventDefault(); 247 | cropper.move(-1, 0); 248 | break; 249 | 250 | case 38: 251 | e.preventDefault(); 252 | cropper.move(0, -1); 253 | break; 254 | 255 | case 39: 256 | e.preventDefault(); 257 | cropper.move(1, 0); 258 | break; 259 | 260 | case 40: 261 | e.preventDefault(); 262 | cropper.move(0, 1); 263 | break; 264 | } 265 | }; 266 | 267 | // Import image 268 | var inputImage = document.getElementById('inputImage'); 269 | 270 | if (URL) { 271 | inputImage.onchange = function () { 272 | var files = this.files; 273 | var file; 274 | 275 | if (cropper && files && files.length) { 276 | file = files[0]; 277 | 278 | if (/^image\/\w+/.test(file.type)) { 279 | uploadedImageType = file.type; 280 | 281 | if (uploadedImageURL) { 282 | URL.revokeObjectURL(uploadedImageURL); 283 | } 284 | 285 | image.src = uploadedImageURL = URL.createObjectURL(file); 286 | cropper.destroy(); 287 | cropper = new Cropper(image, options); 288 | inputImage.value = null; 289 | } else { 290 | window.alert('Please choose an image file.'); 291 | } 292 | } 293 | }; 294 | } else { 295 | inputImage.disabled = true; 296 | inputImage.parentNode.className += ' disabled'; 297 | } 298 | }; 299 | -------------------------------------------------------------------------------- /src/js/cropper.js: -------------------------------------------------------------------------------- 1 | import DEFAULTS from './defaults'; 2 | import TEMPLATE from './template'; 3 | import render from './render'; 4 | import preview from './preview'; 5 | import events from './events'; 6 | import handlers from './handlers'; 7 | import change from './change'; 8 | import methods from './methods'; 9 | import { 10 | ACTION_ALL, 11 | CLASS_HIDDEN, 12 | CLASS_HIDE, 13 | CLASS_INVISIBLE, 14 | CLASS_MODAL, 15 | CLASS_MOVE, 16 | DATA_ACTION, 17 | EVENT_CROP, 18 | EVENT_ERROR, 19 | EVENT_LOAD, 20 | EVENT_READY, 21 | NAMESPACE, 22 | REGEXP_DATA_URL, 23 | REGEXP_DATA_URL_JPEG, 24 | REGEXP_TAG_NAME, 25 | WINDOW, 26 | } from './constants'; 27 | import { 28 | addClass, 29 | addListener, 30 | addTimestamp, 31 | arrayBufferToDataURL, 32 | dataURLToArrayBuffer, 33 | dispatchEvent, 34 | extend, 35 | getData, 36 | getImageNaturalSizes, 37 | getOrientation, 38 | isCrossOriginURL, 39 | isFunction, 40 | isPlainObject, 41 | parseOrientation, 42 | proxy, 43 | removeClass, 44 | removeListener, 45 | setData, 46 | } from './utilities'; 47 | 48 | const AnotherCropper = WINDOW.Cropper; 49 | 50 | class Cropper { 51 | /** 52 | * Create a new Cropper. 53 | * @param {Element} element - The target element for cropping. 54 | * @param {Object} [options={}] - The configuration options. 55 | */ 56 | constructor(element, options = {}) { 57 | if (!element || !REGEXP_TAG_NAME.test(element.tagName)) { 58 | throw new Error('The first argument is required and must be an or element.'); 59 | } 60 | 61 | this.element = element; 62 | this.options = extend({}, DEFAULTS, isPlainObject(options) && options); 63 | this.complete = false; 64 | this.cropped = false; 65 | this.disabled = false; 66 | this.isImg = false; 67 | this.limited = false; 68 | this.loaded = false; 69 | this.ready = false; 70 | this.replaced = false; 71 | this.wheeling = false; 72 | this.originalUrl = ''; 73 | this.canvasData = null; 74 | this.cropBoxData = null; 75 | this.previews = null; 76 | this.pointers = {}; 77 | this.init(); 78 | } 79 | 80 | init() { 81 | const { element } = this; 82 | const tagName = element.tagName.toLowerCase(); 83 | let url; 84 | 85 | if (getData(element, NAMESPACE)) { 86 | return; 87 | } 88 | 89 | setData(element, NAMESPACE, this); 90 | 91 | if (tagName === 'img') { 92 | this.isImg = true; 93 | 94 | // e.g.: "img/picture.jpg" 95 | url = element.getAttribute('src') || ''; 96 | this.originalUrl = url; 97 | 98 | // Stop when it's a blank image 99 | if (!url) { 100 | return; 101 | } 102 | 103 | // e.g.: "http://example.com/img/picture.jpg" 104 | url = element.src; 105 | } else if (tagName === 'canvas' && window.HTMLCanvasElement) { 106 | url = element.toDataURL(); 107 | } 108 | 109 | this.load(url); 110 | } 111 | 112 | load(url) { 113 | if (!url) { 114 | return; 115 | } 116 | 117 | this.url = url; 118 | this.imageData = {}; 119 | 120 | const { element, options } = this; 121 | 122 | if (!options.checkOrientation || !window.ArrayBuffer) { 123 | this.clone(); 124 | return; 125 | } 126 | 127 | // XMLHttpRequest disallows to open a Data URL in some browsers like IE11 and Safari 128 | if (REGEXP_DATA_URL.test(url)) { 129 | if (REGEXP_DATA_URL_JPEG.test(url)) { 130 | this.read(dataURLToArrayBuffer(url)); 131 | } else { 132 | this.clone(); 133 | } 134 | 135 | return; 136 | } 137 | 138 | const xhr = new XMLHttpRequest(); 139 | 140 | xhr.onerror = () => { 141 | this.clone(); 142 | }; 143 | 144 | xhr.onload = () => { 145 | this.read(xhr.response); 146 | }; 147 | 148 | if (options.checkCrossOrigin && isCrossOriginURL(url) && element.crossOrigin) { 149 | url = addTimestamp(url); 150 | } 151 | 152 | xhr.open('get', url); 153 | xhr.responseType = 'arraybuffer'; 154 | xhr.withCredentials = element.crossOrigin === 'use-credentials'; 155 | xhr.send(); 156 | } 157 | 158 | read(arrayBuffer) { 159 | const { options, imageData } = this; 160 | const orientation = getOrientation(arrayBuffer); 161 | let rotate = 0; 162 | let scaleX = 1; 163 | let scaleY = 1; 164 | 165 | if (orientation > 1) { 166 | this.url = arrayBufferToDataURL(arrayBuffer, 'image/jpeg'); 167 | ({ rotate, scaleX, scaleY } = parseOrientation(orientation)); 168 | } 169 | 170 | if (options.rotatable) { 171 | imageData.rotate = rotate; 172 | } 173 | 174 | if (options.scalable) { 175 | imageData.scaleX = scaleX; 176 | imageData.scaleY = scaleY; 177 | } 178 | 179 | this.clone(); 180 | } 181 | 182 | clone() { 183 | const { element, url } = this; 184 | let crossOrigin; 185 | let crossOriginUrl; 186 | 187 | if (this.options.checkCrossOrigin && isCrossOriginURL(url)) { 188 | ({ crossOrigin } = element); 189 | 190 | if (crossOrigin) { 191 | crossOriginUrl = url; 192 | } else { 193 | crossOrigin = 'anonymous'; 194 | 195 | // Bust cache when there is not a "crossOrigin" property 196 | crossOriginUrl = addTimestamp(url); 197 | } 198 | } 199 | 200 | this.crossOrigin = crossOrigin; 201 | this.crossOriginUrl = crossOriginUrl; 202 | 203 | const image = document.createElement('img'); 204 | 205 | if (crossOrigin) { 206 | image.crossOrigin = crossOrigin; 207 | } 208 | 209 | image.src = crossOriginUrl || url; 210 | 211 | const start = proxy(this.start, this); 212 | const stop = proxy(this.stop, this); 213 | 214 | this.image = image; 215 | this.onStart = start; 216 | this.onStop = stop; 217 | 218 | if (this.isImg) { 219 | if (element.complete) { 220 | this.start(); 221 | } else { 222 | addListener(element, EVENT_LOAD, start); 223 | } 224 | } else { 225 | addListener(image, EVENT_LOAD, start); 226 | addListener(image, EVENT_ERROR, stop); 227 | addClass(image, CLASS_HIDE); 228 | element.parentNode.insertBefore(image, element.nextSibling); 229 | } 230 | } 231 | 232 | start(event) { 233 | const image = this.isImg ? this.element : this.image; 234 | 235 | if (event) { 236 | removeListener(image, EVENT_LOAD, this.onStart); 237 | removeListener(image, EVENT_ERROR, this.onStop); 238 | } 239 | 240 | getImageNaturalSizes(image, (naturalWidth, naturalHeight) => { 241 | extend(this.imageData, { 242 | naturalWidth, 243 | naturalHeight, 244 | aspectRatio: naturalWidth / naturalHeight, 245 | }); 246 | this.loaded = true; 247 | this.build(); 248 | }); 249 | } 250 | 251 | stop() { 252 | const { image } = this; 253 | 254 | removeListener(image, EVENT_LOAD, this.onStart); 255 | removeListener(image, EVENT_ERROR, this.onStop); 256 | image.parentNode.removeChild(image); 257 | this.image = null; 258 | } 259 | 260 | build() { 261 | if (!this.loaded) { 262 | return; 263 | } 264 | 265 | // Unbuild first when replace 266 | if (this.ready) { 267 | this.unbuild(); 268 | } 269 | 270 | const { element, options, image } = this; 271 | 272 | // Create cropper elements 273 | const container = element.parentNode; 274 | const template = document.createElement('div'); 275 | 276 | template.innerHTML = TEMPLATE; 277 | 278 | const cropper = template.querySelector(`.${NAMESPACE}-container`); 279 | const canvas = cropper.querySelector(`.${NAMESPACE}-canvas`); 280 | const dragBox = cropper.querySelector(`.${NAMESPACE}-drag-box`); 281 | const cropBox = cropper.querySelector(`.${NAMESPACE}-crop-box`); 282 | const face = cropBox.querySelector(`.${NAMESPACE}-face`); 283 | 284 | this.container = container; 285 | this.cropper = cropper; 286 | this.canvas = canvas; 287 | this.dragBox = dragBox; 288 | this.cropBox = cropBox; 289 | this.viewBox = cropper.querySelector(`.${NAMESPACE}-view-box`); 290 | this.face = face; 291 | 292 | canvas.appendChild(image); 293 | 294 | // Hide the original image 295 | addClass(element, CLASS_HIDDEN); 296 | 297 | // Inserts the cropper after to the current image 298 | container.insertBefore(cropper, element.nextSibling); 299 | 300 | // Show the image if is hidden 301 | if (!this.isImg) { 302 | removeClass(image, CLASS_HIDE); 303 | } 304 | 305 | this.initPreview(); 306 | this.bind(); 307 | 308 | options.aspectRatio = Math.max(0, options.aspectRatio) || NaN; 309 | options.viewMode = Math.max(0, Math.min(3, Math.round(options.viewMode))) || 0; 310 | 311 | this.cropped = options.autoCrop; 312 | 313 | if (options.autoCrop) { 314 | if (options.modal) { 315 | addClass(dragBox, CLASS_MODAL); 316 | } 317 | } else { 318 | addClass(cropBox, CLASS_HIDDEN); 319 | } 320 | 321 | if (!options.guides) { 322 | addClass(cropBox.getElementsByClassName(`${NAMESPACE}-dashed`), CLASS_HIDDEN); 323 | } 324 | 325 | if (!options.center) { 326 | addClass(cropBox.getElementsByClassName(`${NAMESPACE}-center`), CLASS_HIDDEN); 327 | } 328 | 329 | if (options.background) { 330 | addClass(cropper, `${NAMESPACE}-bg`); 331 | } 332 | 333 | if (!options.highlight) { 334 | addClass(face, CLASS_INVISIBLE); 335 | } 336 | 337 | if (options.cropBoxMovable) { 338 | addClass(face, CLASS_MOVE); 339 | setData(face, DATA_ACTION, ACTION_ALL); 340 | } 341 | 342 | if (!options.cropBoxResizable) { 343 | addClass(cropBox.getElementsByClassName(`${NAMESPACE}-line`), CLASS_HIDDEN); 344 | addClass(cropBox.getElementsByClassName(`${NAMESPACE}-point`), CLASS_HIDDEN); 345 | } 346 | 347 | this.setDragMode(options.dragMode); 348 | this.render(); 349 | this.ready = true; 350 | this.setData(options.data); 351 | 352 | // Call the "ready" option asynchronously to keep "image.cropper" is defined 353 | this.completing = setTimeout(() => { 354 | if (isFunction(options.ready)) { 355 | addListener(element, EVENT_READY, options.ready, { 356 | once: true, 357 | }); 358 | } 359 | 360 | dispatchEvent(element, EVENT_READY); 361 | dispatchEvent(element, EVENT_CROP, this.getData()); 362 | 363 | this.complete = true; 364 | }, 0); 365 | } 366 | 367 | unbuild() { 368 | if (!this.ready) { 369 | return; 370 | } 371 | 372 | if (!this.complete) { 373 | clearTimeout(this.completing); 374 | } 375 | 376 | this.ready = false; 377 | this.complete = false; 378 | this.initialImageData = null; 379 | 380 | // Clear `initialCanvasData` is necessary when replace 381 | this.initialCanvasData = null; 382 | this.initialCropBoxData = null; 383 | this.containerData = null; 384 | this.canvasData = null; 385 | 386 | // Clear `cropBoxData` is necessary when replace 387 | this.cropBoxData = null; 388 | this.unbind(); 389 | this.resetPreview(); 390 | this.previews = null; 391 | this.viewBox = null; 392 | this.cropBox = null; 393 | this.dragBox = null; 394 | this.canvas = null; 395 | this.container = null; 396 | this.cropper.parentNode.removeChild(this.cropper); 397 | this.cropper = null; 398 | } 399 | 400 | /** 401 | * Get the no conflict cropper class. 402 | * @returns {Cropper} The cropper class. 403 | */ 404 | static noConflict() { 405 | window.Cropper = AnotherCropper; 406 | return Cropper; 407 | } 408 | 409 | /** 410 | * Change the default options. 411 | * @param {Object} options - The new default options. 412 | */ 413 | static setDefaults(options) { 414 | extend(DEFAULTS, isPlainObject(options) && options); 415 | } 416 | } 417 | 418 | extend(Cropper.prototype, render, preview, events, handlers, change, methods); 419 | 420 | export default Cropper; 421 | -------------------------------------------------------------------------------- /src/js/change.js: -------------------------------------------------------------------------------- 1 | import { 2 | ACTION_ALL, 3 | ACTION_CROP, 4 | ACTION_EAST, 5 | ACTION_MOVE, 6 | ACTION_NORTH, 7 | ACTION_NORTH_EAST, 8 | ACTION_NORTH_WEST, 9 | ACTION_SOUTH, 10 | ACTION_SOUTH_EAST, 11 | ACTION_SOUTH_WEST, 12 | ACTION_WEST, 13 | ACTION_ZOOM, 14 | CLASS_HIDDEN, 15 | } from './constants'; 16 | import { 17 | each, 18 | getMaxZoomRatio, 19 | getOffset, 20 | removeClass, 21 | } from './utilities'; 22 | 23 | export default { 24 | change(e) { 25 | const { 26 | options, 27 | canvasData, 28 | containerData, 29 | cropBoxData, 30 | pointers, 31 | } = this; 32 | let { action } = this; 33 | let { aspectRatio } = options; 34 | let { 35 | left, 36 | top, 37 | width, 38 | height, 39 | } = cropBoxData; 40 | const right = left + width; 41 | const bottom = top + height; 42 | let minLeft = 0; 43 | let minTop = 0; 44 | let maxWidth = containerData.width; 45 | let maxHeight = containerData.height; 46 | let renderable = true; 47 | let offset; 48 | 49 | // Locking aspect ratio in "free mode" by holding shift key 50 | if (!aspectRatio && e.shiftKey) { 51 | aspectRatio = width && height ? width / height : 1; 52 | } 53 | 54 | if (this.limited) { 55 | ({ minLeft, minTop } = cropBoxData); 56 | maxWidth = minLeft + Math.min( 57 | containerData.width, 58 | canvasData.width, 59 | canvasData.left + canvasData.width, 60 | ); 61 | maxHeight = minTop + Math.min( 62 | containerData.height, 63 | canvasData.height, 64 | canvasData.top + canvasData.height, 65 | ); 66 | } 67 | 68 | const pointer = pointers[Object.keys(pointers)[0]]; 69 | const range = { 70 | x: pointer.endX - pointer.startX, 71 | y: pointer.endY - pointer.startY, 72 | }; 73 | const check = (side) => { 74 | switch (side) { 75 | case ACTION_EAST: 76 | if (right + range.x > maxWidth) { 77 | range.x = maxWidth - right; 78 | } 79 | 80 | break; 81 | 82 | case ACTION_WEST: 83 | if (left + range.x < minLeft) { 84 | range.x = minLeft - left; 85 | } 86 | 87 | break; 88 | 89 | case ACTION_NORTH: 90 | if (top + range.y < minTop) { 91 | range.y = minTop - top; 92 | } 93 | 94 | break; 95 | 96 | case ACTION_SOUTH: 97 | if (bottom + range.y > maxHeight) { 98 | range.y = maxHeight - bottom; 99 | } 100 | 101 | break; 102 | 103 | default: 104 | } 105 | }; 106 | 107 | switch (action) { 108 | // Move crop box 109 | case ACTION_ALL: 110 | left += range.x; 111 | top += range.y; 112 | break; 113 | 114 | // Resize crop box 115 | case ACTION_EAST: 116 | if (range.x >= 0 && (right >= maxWidth || (aspectRatio && 117 | (top <= minTop || bottom >= maxHeight)))) { 118 | renderable = false; 119 | break; 120 | } 121 | 122 | check(ACTION_EAST); 123 | width += range.x; 124 | 125 | if (aspectRatio) { 126 | height = width / aspectRatio; 127 | top -= (range.x / aspectRatio) / 2; 128 | } 129 | 130 | if (width < 0) { 131 | action = ACTION_WEST; 132 | width = 0; 133 | } 134 | 135 | break; 136 | 137 | case ACTION_NORTH: 138 | if (range.y <= 0 && (top <= minTop || (aspectRatio && 139 | (left <= minLeft || right >= maxWidth)))) { 140 | renderable = false; 141 | break; 142 | } 143 | 144 | check(ACTION_NORTH); 145 | height -= range.y; 146 | top += range.y; 147 | 148 | if (aspectRatio) { 149 | width = height * aspectRatio; 150 | left += (range.y * aspectRatio) / 2; 151 | } 152 | 153 | if (height < 0) { 154 | action = ACTION_SOUTH; 155 | height = 0; 156 | } 157 | 158 | break; 159 | 160 | case ACTION_WEST: 161 | if (range.x <= 0 && (left <= minLeft || (aspectRatio && 162 | (top <= minTop || bottom >= maxHeight)))) { 163 | renderable = false; 164 | break; 165 | } 166 | 167 | check(ACTION_WEST); 168 | width -= range.x; 169 | left += range.x; 170 | 171 | if (aspectRatio) { 172 | height = width / aspectRatio; 173 | top += (range.x / aspectRatio) / 2; 174 | } 175 | 176 | if (width < 0) { 177 | action = ACTION_EAST; 178 | width = 0; 179 | } 180 | 181 | break; 182 | 183 | case ACTION_SOUTH: 184 | if (range.y >= 0 && (bottom >= maxHeight || (aspectRatio && 185 | (left <= minLeft || right >= maxWidth)))) { 186 | renderable = false; 187 | break; 188 | } 189 | 190 | check(ACTION_SOUTH); 191 | height += range.y; 192 | 193 | if (aspectRatio) { 194 | width = height * aspectRatio; 195 | left -= (range.y * aspectRatio) / 2; 196 | } 197 | 198 | if (height < 0) { 199 | action = ACTION_NORTH; 200 | height = 0; 201 | } 202 | 203 | break; 204 | 205 | case ACTION_NORTH_EAST: 206 | if (aspectRatio) { 207 | if (range.y <= 0 && (top <= minTop || right >= maxWidth)) { 208 | renderable = false; 209 | break; 210 | } 211 | 212 | check(ACTION_NORTH); 213 | height -= range.y; 214 | top += range.y; 215 | width = height * aspectRatio; 216 | } else { 217 | check(ACTION_NORTH); 218 | check(ACTION_EAST); 219 | 220 | if (range.x >= 0) { 221 | if (right < maxWidth) { 222 | width += range.x; 223 | } else if (range.y <= 0 && top <= minTop) { 224 | renderable = false; 225 | } 226 | } else { 227 | width += range.x; 228 | } 229 | 230 | if (range.y <= 0) { 231 | if (top > minTop) { 232 | height -= range.y; 233 | top += range.y; 234 | } 235 | } else { 236 | height -= range.y; 237 | top += range.y; 238 | } 239 | } 240 | 241 | if (width < 0 && height < 0) { 242 | action = ACTION_SOUTH_WEST; 243 | height = 0; 244 | width = 0; 245 | } else if (width < 0) { 246 | action = ACTION_NORTH_WEST; 247 | width = 0; 248 | } else if (height < 0) { 249 | action = ACTION_SOUTH_EAST; 250 | height = 0; 251 | } 252 | 253 | break; 254 | 255 | case ACTION_NORTH_WEST: 256 | if (aspectRatio) { 257 | if (range.y <= 0 && (top <= minTop || left <= minLeft)) { 258 | renderable = false; 259 | break; 260 | } 261 | 262 | check(ACTION_NORTH); 263 | height -= range.y; 264 | top += range.y; 265 | width = height * aspectRatio; 266 | left += range.y * aspectRatio; 267 | } else { 268 | check(ACTION_NORTH); 269 | check(ACTION_WEST); 270 | 271 | if (range.x <= 0) { 272 | if (left > minLeft) { 273 | width -= range.x; 274 | left += range.x; 275 | } else if (range.y <= 0 && top <= minTop) { 276 | renderable = false; 277 | } 278 | } else { 279 | width -= range.x; 280 | left += range.x; 281 | } 282 | 283 | if (range.y <= 0) { 284 | if (top > minTop) { 285 | height -= range.y; 286 | top += range.y; 287 | } 288 | } else { 289 | height -= range.y; 290 | top += range.y; 291 | } 292 | } 293 | 294 | if (width < 0 && height < 0) { 295 | action = ACTION_SOUTH_EAST; 296 | height = 0; 297 | width = 0; 298 | } else if (width < 0) { 299 | action = ACTION_NORTH_EAST; 300 | width = 0; 301 | } else if (height < 0) { 302 | action = ACTION_SOUTH_WEST; 303 | height = 0; 304 | } 305 | 306 | break; 307 | 308 | case ACTION_SOUTH_WEST: 309 | if (aspectRatio) { 310 | if (range.x <= 0 && (left <= minLeft || bottom >= maxHeight)) { 311 | renderable = false; 312 | break; 313 | } 314 | 315 | check(ACTION_WEST); 316 | width -= range.x; 317 | left += range.x; 318 | height = width / aspectRatio; 319 | } else { 320 | check(ACTION_SOUTH); 321 | check(ACTION_WEST); 322 | 323 | if (range.x <= 0) { 324 | if (left > minLeft) { 325 | width -= range.x; 326 | left += range.x; 327 | } else if (range.y >= 0 && bottom >= maxHeight) { 328 | renderable = false; 329 | } 330 | } else { 331 | width -= range.x; 332 | left += range.x; 333 | } 334 | 335 | if (range.y >= 0) { 336 | if (bottom < maxHeight) { 337 | height += range.y; 338 | } 339 | } else { 340 | height += range.y; 341 | } 342 | } 343 | 344 | if (width < 0 && height < 0) { 345 | action = ACTION_NORTH_EAST; 346 | height = 0; 347 | width = 0; 348 | } else if (width < 0) { 349 | action = ACTION_SOUTH_EAST; 350 | width = 0; 351 | } else if (height < 0) { 352 | action = ACTION_NORTH_WEST; 353 | height = 0; 354 | } 355 | 356 | break; 357 | 358 | case ACTION_SOUTH_EAST: 359 | if (aspectRatio) { 360 | if (range.x >= 0 && (right >= maxWidth || bottom >= maxHeight)) { 361 | renderable = false; 362 | break; 363 | } 364 | 365 | check(ACTION_EAST); 366 | width += range.x; 367 | height = width / aspectRatio; 368 | } else { 369 | check(ACTION_SOUTH); 370 | check(ACTION_EAST); 371 | 372 | if (range.x >= 0) { 373 | if (right < maxWidth) { 374 | width += range.x; 375 | } else if (range.y >= 0 && bottom >= maxHeight) { 376 | renderable = false; 377 | } 378 | } else { 379 | width += range.x; 380 | } 381 | 382 | if (range.y >= 0) { 383 | if (bottom < maxHeight) { 384 | height += range.y; 385 | } 386 | } else { 387 | height += range.y; 388 | } 389 | } 390 | 391 | if (width < 0 && height < 0) { 392 | action = ACTION_NORTH_WEST; 393 | height = 0; 394 | width = 0; 395 | } else if (width < 0) { 396 | action = ACTION_SOUTH_WEST; 397 | width = 0; 398 | } else if (height < 0) { 399 | action = ACTION_NORTH_EAST; 400 | height = 0; 401 | } 402 | 403 | break; 404 | 405 | // Move canvas 406 | case ACTION_MOVE: 407 | this.move(range.x, range.y); 408 | renderable = false; 409 | break; 410 | 411 | // Zoom canvas 412 | case ACTION_ZOOM: 413 | this.zoom(getMaxZoomRatio(pointers), e); 414 | renderable = false; 415 | break; 416 | 417 | // Create crop box 418 | case ACTION_CROP: 419 | if (!range.x || !range.y) { 420 | renderable = false; 421 | break; 422 | } 423 | 424 | offset = getOffset(this.cropper); 425 | left = pointer.startX - offset.left; 426 | top = pointer.startY - offset.top; 427 | width = cropBoxData.minWidth; 428 | height = cropBoxData.minHeight; 429 | 430 | if (range.x > 0) { 431 | action = range.y > 0 ? ACTION_SOUTH_EAST : ACTION_NORTH_EAST; 432 | } else if (range.x < 0) { 433 | left -= width; 434 | action = range.y > 0 ? ACTION_SOUTH_WEST : ACTION_NORTH_WEST; 435 | } 436 | 437 | if (range.y < 0) { 438 | top -= height; 439 | } 440 | 441 | // Show the crop box if is hidden 442 | if (!this.cropped) { 443 | removeClass(this.cropBox, CLASS_HIDDEN); 444 | this.cropped = true; 445 | 446 | if (this.limited) { 447 | this.limitCropBox(true, true); 448 | } 449 | } 450 | 451 | break; 452 | 453 | default: 454 | } 455 | 456 | if (renderable) { 457 | cropBoxData.width = width; 458 | cropBoxData.height = height; 459 | cropBoxData.left = left; 460 | cropBoxData.top = top; 461 | this.action = action; 462 | this.renderCropBox(); 463 | } 464 | 465 | // Override 466 | each(pointers, (p) => { 467 | p.startX = p.endX; 468 | p.startY = p.endY; 469 | }); 470 | }, 471 | }; 472 | -------------------------------------------------------------------------------- /src/js/render.js: -------------------------------------------------------------------------------- 1 | import { 2 | ACTION_ALL, 3 | ACTION_MOVE, 4 | CLASS_HIDDEN, 5 | DATA_ACTION, 6 | EVENT_CROP, 7 | } from './constants'; 8 | import { 9 | addClass, 10 | dispatchEvent, 11 | extend, 12 | getContainSizes, 13 | getRotatedSizes, 14 | getTransforms, 15 | removeClass, 16 | setData, 17 | setStyle, 18 | } from './utilities'; 19 | 20 | export default { 21 | render() { 22 | this.initContainer(); 23 | this.initCanvas(); 24 | this.initCropBox(); 25 | this.renderCanvas(); 26 | 27 | if (this.cropped) { 28 | this.renderCropBox(); 29 | } 30 | }, 31 | 32 | initContainer() { 33 | const { 34 | element, 35 | options, 36 | container, 37 | cropper, 38 | } = this; 39 | 40 | addClass(cropper, CLASS_HIDDEN); 41 | removeClass(element, CLASS_HIDDEN); 42 | 43 | const containerData = { 44 | width: Math.max( 45 | container.offsetWidth, 46 | Number(options.minContainerWidth) || 200, 47 | ), 48 | height: Math.max( 49 | container.offsetHeight, 50 | Number(options.minContainerHeight) || 100, 51 | ), 52 | }; 53 | 54 | this.containerData = containerData; 55 | 56 | setStyle(cropper, { 57 | width: containerData.width, 58 | height: containerData.height, 59 | }); 60 | 61 | addClass(element, CLASS_HIDDEN); 62 | removeClass(cropper, CLASS_HIDDEN); 63 | }, 64 | 65 | // Canvas (image wrapper) 66 | initCanvas() { 67 | const { containerData, imageData } = this; 68 | const { viewMode } = this.options; 69 | const rotated = Math.abs(imageData.rotate) % 180 === 90; 70 | const naturalWidth = rotated ? imageData.naturalHeight : imageData.naturalWidth; 71 | const naturalHeight = rotated ? imageData.naturalWidth : imageData.naturalHeight; 72 | const aspectRatio = naturalWidth / naturalHeight; 73 | let canvasWidth = containerData.width; 74 | let canvasHeight = containerData.height; 75 | 76 | if (containerData.height * aspectRatio > containerData.width) { 77 | if (viewMode === 3) { 78 | canvasWidth = containerData.height * aspectRatio; 79 | } else { 80 | canvasHeight = containerData.width / aspectRatio; 81 | } 82 | } else if (viewMode === 3) { 83 | canvasHeight = containerData.width / aspectRatio; 84 | } else { 85 | canvasWidth = containerData.height * aspectRatio; 86 | } 87 | 88 | const canvasData = { 89 | aspectRatio, 90 | naturalWidth, 91 | naturalHeight, 92 | width: canvasWidth, 93 | height: canvasHeight, 94 | }; 95 | 96 | canvasData.left = (containerData.width - canvasWidth) / 2; 97 | canvasData.top = (containerData.height - canvasHeight) / 2; 98 | canvasData.oldLeft = canvasData.left; 99 | canvasData.oldTop = canvasData.top; 100 | 101 | this.canvasData = canvasData; 102 | this.limited = (viewMode === 1 || viewMode === 2); 103 | this.limitCanvas(true, true); 104 | this.initialImageData = extend({}, imageData); 105 | this.initialCanvasData = extend({}, canvasData); 106 | }, 107 | 108 | limitCanvas(sizeLimited, positionLimited) { 109 | const { 110 | options, 111 | containerData, 112 | canvasData, 113 | cropBoxData, 114 | } = this; 115 | const { viewMode } = options; 116 | const { aspectRatio } = canvasData; 117 | const cropped = this.cropped && cropBoxData; 118 | 119 | if (sizeLimited) { 120 | let minCanvasWidth = Number(options.minCanvasWidth) || 0; 121 | let minCanvasHeight = Number(options.minCanvasHeight) || 0; 122 | 123 | if (viewMode > 1) { 124 | minCanvasWidth = Math.max(minCanvasWidth, containerData.width); 125 | minCanvasHeight = Math.max(minCanvasHeight, containerData.height); 126 | 127 | if (viewMode === 3) { 128 | if (minCanvasHeight * aspectRatio > minCanvasWidth) { 129 | minCanvasWidth = minCanvasHeight * aspectRatio; 130 | } else { 131 | minCanvasHeight = minCanvasWidth / aspectRatio; 132 | } 133 | } 134 | } else if (viewMode > 0) { 135 | if (minCanvasWidth) { 136 | minCanvasWidth = Math.max( 137 | minCanvasWidth, 138 | cropped ? cropBoxData.width : 0, 139 | ); 140 | } else if (minCanvasHeight) { 141 | minCanvasHeight = Math.max( 142 | minCanvasHeight, 143 | cropped ? cropBoxData.height : 0, 144 | ); 145 | } else if (cropped) { 146 | minCanvasWidth = cropBoxData.width; 147 | minCanvasHeight = cropBoxData.height; 148 | 149 | if (minCanvasHeight * aspectRatio > minCanvasWidth) { 150 | minCanvasWidth = minCanvasHeight * aspectRatio; 151 | } else { 152 | minCanvasHeight = minCanvasWidth / aspectRatio; 153 | } 154 | } 155 | } 156 | 157 | ({ width: minCanvasWidth, height: minCanvasHeight } = getContainSizes({ 158 | aspectRatio, 159 | width: minCanvasWidth, 160 | height: minCanvasHeight, 161 | })); 162 | 163 | canvasData.minWidth = minCanvasWidth; 164 | canvasData.minHeight = minCanvasHeight; 165 | canvasData.maxWidth = Infinity; 166 | canvasData.maxHeight = Infinity; 167 | } 168 | 169 | if (positionLimited) { 170 | if (viewMode) { 171 | const newCanvasLeft = containerData.width - canvasData.width; 172 | const newCanvasTop = containerData.height - canvasData.height; 173 | 174 | canvasData.minLeft = Math.min(0, newCanvasLeft); 175 | canvasData.minTop = Math.min(0, newCanvasTop); 176 | canvasData.maxLeft = Math.max(0, newCanvasLeft); 177 | canvasData.maxTop = Math.max(0, newCanvasTop); 178 | 179 | if (cropped && this.limited) { 180 | canvasData.minLeft = Math.min( 181 | cropBoxData.left, 182 | cropBoxData.left + (cropBoxData.width - canvasData.width), 183 | ); 184 | canvasData.minTop = Math.min( 185 | cropBoxData.top, 186 | cropBoxData.top + (cropBoxData.height - canvasData.height), 187 | ); 188 | canvasData.maxLeft = cropBoxData.left; 189 | canvasData.maxTop = cropBoxData.top; 190 | 191 | if (viewMode === 2) { 192 | if (canvasData.width >= containerData.width) { 193 | canvasData.minLeft = Math.min(0, newCanvasLeft); 194 | canvasData.maxLeft = Math.max(0, newCanvasLeft); 195 | } 196 | 197 | if (canvasData.height >= containerData.height) { 198 | canvasData.minTop = Math.min(0, newCanvasTop); 199 | canvasData.maxTop = Math.max(0, newCanvasTop); 200 | } 201 | } 202 | } 203 | } else { 204 | canvasData.minLeft = -canvasData.width; 205 | canvasData.minTop = -canvasData.height; 206 | canvasData.maxLeft = containerData.width; 207 | canvasData.maxTop = containerData.height; 208 | } 209 | } 210 | }, 211 | 212 | renderCanvas(changed, transformed) { 213 | const { canvasData, imageData } = this; 214 | 215 | if (transformed) { 216 | const { width: naturalWidth, height: naturalHeight } = getRotatedSizes({ 217 | width: imageData.naturalWidth * Math.abs(imageData.scaleX || 1), 218 | height: imageData.naturalHeight * Math.abs(imageData.scaleY || 1), 219 | degree: imageData.rotate || 0, 220 | }); 221 | const width = canvasData.width * (naturalWidth / canvasData.naturalWidth); 222 | const height = canvasData.height * (naturalHeight / canvasData.naturalHeight); 223 | 224 | canvasData.left -= (width - canvasData.width) / 2; 225 | canvasData.top -= (height - canvasData.height) / 2; 226 | canvasData.width = width; 227 | canvasData.height = height; 228 | canvasData.aspectRatio = naturalWidth / naturalHeight; 229 | canvasData.naturalWidth = naturalWidth; 230 | canvasData.naturalHeight = naturalHeight; 231 | this.limitCanvas(true, false); 232 | } 233 | 234 | if (canvasData.width > canvasData.maxWidth || 235 | canvasData.width < canvasData.minWidth) { 236 | canvasData.left = canvasData.oldLeft; 237 | } 238 | 239 | if (canvasData.height > canvasData.maxHeight || 240 | canvasData.height < canvasData.minHeight) { 241 | canvasData.top = canvasData.oldTop; 242 | } 243 | 244 | canvasData.width = Math.min( 245 | Math.max(canvasData.width, canvasData.minWidth), 246 | canvasData.maxWidth, 247 | ); 248 | canvasData.height = Math.min( 249 | Math.max(canvasData.height, canvasData.minHeight), 250 | canvasData.maxHeight, 251 | ); 252 | 253 | this.limitCanvas(false, true); 254 | 255 | canvasData.left = Math.min( 256 | Math.max(canvasData.left, canvasData.minLeft), 257 | canvasData.maxLeft, 258 | ); 259 | canvasData.top = Math.min( 260 | Math.max(canvasData.top, canvasData.minTop), 261 | canvasData.maxTop, 262 | ); 263 | canvasData.oldLeft = canvasData.left; 264 | canvasData.oldTop = canvasData.top; 265 | 266 | setStyle(this.canvas, extend({ 267 | width: canvasData.width, 268 | height: canvasData.height, 269 | }, getTransforms({ 270 | translateX: canvasData.left, 271 | translateY: canvasData.top, 272 | }))); 273 | 274 | this.renderImage(changed); 275 | 276 | if (this.cropped && this.limited) { 277 | this.limitCropBox(true, true); 278 | } 279 | }, 280 | 281 | renderImage(changed) { 282 | const { canvasData, imageData } = this; 283 | const width = imageData.naturalWidth * (canvasData.width / canvasData.naturalWidth); 284 | const height = imageData.naturalHeight * (canvasData.height / canvasData.naturalHeight); 285 | 286 | extend(imageData, { 287 | width, 288 | height, 289 | left: (canvasData.width - width) / 2, 290 | top: (canvasData.height - height) / 2, 291 | }); 292 | setStyle(this.image, extend({ 293 | width: imageData.width, 294 | height: imageData.height, 295 | }, getTransforms(extend({ 296 | translateX: imageData.left, 297 | translateY: imageData.top, 298 | }, imageData)))); 299 | 300 | if (changed) { 301 | this.output(); 302 | } 303 | }, 304 | 305 | initCropBox() { 306 | const { options, canvasData } = this; 307 | const { aspectRatio } = options; 308 | const autoCropArea = Number(options.autoCropArea) || 0.8; 309 | const cropBoxData = { 310 | width: canvasData.width, 311 | height: canvasData.height, 312 | }; 313 | 314 | if (aspectRatio) { 315 | if (canvasData.height * aspectRatio > canvasData.width) { 316 | cropBoxData.height = cropBoxData.width / aspectRatio; 317 | } else { 318 | cropBoxData.width = cropBoxData.height * aspectRatio; 319 | } 320 | } 321 | 322 | this.cropBoxData = cropBoxData; 323 | this.limitCropBox(true, true); 324 | 325 | // Initialize auto crop area 326 | cropBoxData.width = Math.min( 327 | Math.max(cropBoxData.width, cropBoxData.minWidth), 328 | cropBoxData.maxWidth, 329 | ); 330 | cropBoxData.height = Math.min( 331 | Math.max(cropBoxData.height, cropBoxData.minHeight), 332 | cropBoxData.maxHeight, 333 | ); 334 | 335 | // The width/height of auto crop area must large than "minWidth/Height" 336 | cropBoxData.width = Math.max( 337 | cropBoxData.minWidth, 338 | cropBoxData.width * autoCropArea, 339 | ); 340 | cropBoxData.height = Math.max( 341 | cropBoxData.minHeight, 342 | cropBoxData.height * autoCropArea, 343 | ); 344 | cropBoxData.left = ( 345 | canvasData.left + ((canvasData.width - cropBoxData.width) / 2) 346 | ); 347 | cropBoxData.top = ( 348 | canvasData.top + ((canvasData.height - cropBoxData.height) / 2) 349 | ); 350 | cropBoxData.oldLeft = cropBoxData.left; 351 | cropBoxData.oldTop = cropBoxData.top; 352 | 353 | this.initialCropBoxData = extend({}, cropBoxData); 354 | }, 355 | 356 | limitCropBox(sizeLimited, positionLimited) { 357 | const { 358 | options, 359 | containerData, 360 | canvasData, 361 | cropBoxData, 362 | limited, 363 | } = this; 364 | const { aspectRatio } = options; 365 | 366 | if (sizeLimited) { 367 | let minCropBoxWidth = Number(options.minCropBoxWidth) || 0; 368 | let minCropBoxHeight = Number(options.minCropBoxHeight) || 0; 369 | let maxCropBoxWidth = Math.min( 370 | containerData.width, 371 | limited ? canvasData.width : containerData.width, 372 | ); 373 | let maxCropBoxHeight = Math.min( 374 | containerData.height, 375 | limited ? canvasData.height : containerData.height, 376 | ); 377 | 378 | // The min/maxCropBoxWidth/Height must be less than container's width/height 379 | minCropBoxWidth = Math.min(minCropBoxWidth, containerData.width); 380 | minCropBoxHeight = Math.min(minCropBoxHeight, containerData.height); 381 | 382 | if (aspectRatio) { 383 | if (minCropBoxWidth && minCropBoxHeight) { 384 | if (minCropBoxHeight * aspectRatio > minCropBoxWidth) { 385 | minCropBoxHeight = minCropBoxWidth / aspectRatio; 386 | } else { 387 | minCropBoxWidth = minCropBoxHeight * aspectRatio; 388 | } 389 | } else if (minCropBoxWidth) { 390 | minCropBoxHeight = minCropBoxWidth / aspectRatio; 391 | } else if (minCropBoxHeight) { 392 | minCropBoxWidth = minCropBoxHeight * aspectRatio; 393 | } 394 | 395 | if (maxCropBoxHeight * aspectRatio > maxCropBoxWidth) { 396 | maxCropBoxHeight = maxCropBoxWidth / aspectRatio; 397 | } else { 398 | maxCropBoxWidth = maxCropBoxHeight * aspectRatio; 399 | } 400 | } 401 | 402 | // The minWidth/Height must be less than maxWidth/Height 403 | cropBoxData.minWidth = Math.min(minCropBoxWidth, maxCropBoxWidth); 404 | cropBoxData.minHeight = Math.min(minCropBoxHeight, maxCropBoxHeight); 405 | cropBoxData.maxWidth = maxCropBoxWidth; 406 | cropBoxData.maxHeight = maxCropBoxHeight; 407 | } 408 | 409 | if (positionLimited) { 410 | if (limited) { 411 | cropBoxData.minLeft = Math.max(0, canvasData.left); 412 | cropBoxData.minTop = Math.max(0, canvasData.top); 413 | cropBoxData.maxLeft = Math.min( 414 | containerData.width, 415 | canvasData.left + canvasData.width, 416 | ) - cropBoxData.width; 417 | cropBoxData.maxTop = Math.min( 418 | containerData.height, 419 | canvasData.top + canvasData.height, 420 | ) - cropBoxData.height; 421 | } else { 422 | cropBoxData.minLeft = 0; 423 | cropBoxData.minTop = 0; 424 | cropBoxData.maxLeft = containerData.width - cropBoxData.width; 425 | cropBoxData.maxTop = containerData.height - cropBoxData.height; 426 | } 427 | } 428 | }, 429 | 430 | renderCropBox() { 431 | const { options, containerData, cropBoxData } = this; 432 | 433 | if (cropBoxData.width > cropBoxData.maxWidth || 434 | cropBoxData.width < cropBoxData.minWidth) { 435 | cropBoxData.left = cropBoxData.oldLeft; 436 | } 437 | 438 | if (cropBoxData.height > cropBoxData.maxHeight || 439 | cropBoxData.height < cropBoxData.minHeight) { 440 | cropBoxData.top = cropBoxData.oldTop; 441 | } 442 | 443 | cropBoxData.width = Math.min( 444 | Math.max(cropBoxData.width, cropBoxData.minWidth), 445 | cropBoxData.maxWidth, 446 | ); 447 | cropBoxData.height = Math.min( 448 | Math.max(cropBoxData.height, cropBoxData.minHeight), 449 | cropBoxData.maxHeight, 450 | ); 451 | 452 | this.limitCropBox(false, true); 453 | 454 | cropBoxData.left = Math.min( 455 | Math.max(cropBoxData.left, cropBoxData.minLeft), 456 | cropBoxData.maxLeft, 457 | ); 458 | cropBoxData.top = Math.min( 459 | Math.max(cropBoxData.top, cropBoxData.minTop), 460 | cropBoxData.maxTop, 461 | ); 462 | cropBoxData.oldLeft = cropBoxData.left; 463 | cropBoxData.oldTop = cropBoxData.top; 464 | 465 | if (options.movable && options.cropBoxMovable) { 466 | // Turn to move the canvas when the crop box is equal to the container 467 | setData(this.face, DATA_ACTION, cropBoxData.width >= containerData.width && 468 | cropBoxData.height >= containerData.height ? ACTION_MOVE : ACTION_ALL); 469 | } 470 | 471 | setStyle(this.cropBox, extend({ 472 | width: cropBoxData.width, 473 | height: cropBoxData.height, 474 | }, getTransforms({ 475 | translateX: cropBoxData.left, 476 | translateY: cropBoxData.top, 477 | }))); 478 | 479 | if (this.cropped && this.limited) { 480 | this.limitCanvas(true, true); 481 | } 482 | 483 | if (!this.disabled) { 484 | this.output(); 485 | } 486 | }, 487 | 488 | output() { 489 | this.preview(); 490 | 491 | if (this.complete) { 492 | dispatchEvent(this.element, EVENT_CROP, this.getData()); 493 | } 494 | }, 495 | }; 496 | --------------------------------------------------------------------------------