├── .gitignore ├── .jshintrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bower.json ├── cropit.jquery.json ├── demo ├── basic.html ├── form.html ├── image-background.html └── pica.html ├── dist └── jquery.cropit.js ├── package.json ├── src ├── constants.js ├── cropit.js ├── options.js ├── plugin.js ├── utils.js └── zoomer.js ├── test ├── cropit.spec.js ├── cropit_view.spec.js ├── fixtures │ ├── basic.html │ └── image-background.html └── zoomer.spec.js ├── update_version.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | bower_components 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "browser": true, 4 | "bitwise": true, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "indent": 2, 8 | "latedef": true, 9 | "noarg": true, 10 | "undef": true, 11 | "unused": true, 12 | "globals": { 13 | "jQuery": true, 14 | "jest": true, 15 | "jasmine": true, 16 | "describe": false, 17 | "xdescribe": false, 18 | "ddescribe": false, 19 | "it": false, 20 | "xit": false, 21 | "iit": false, 22 | "beforeEach": false, 23 | "afterEach": false, 24 | "expect": false, 25 | "pending": false, 26 | "spyOn": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.5.1 (February 27, 2016) 2 | 3 | ### Bug fixes 4 | 5 | * Fixed wrong image offset when rotation is negative. 6 | * Fixed bug where image background can be dragged and moved. 7 | 8 | 9 | ## 0.5.0 (February 27, 2016) 10 | 11 | ### Migration guide 12 | 13 | Markup in v0.4: 14 | 15 | ```html 16 |
17 | 18 |
19 |
20 |
21 | 22 |
23 | ``` 24 | 25 | New markup in v0.5: 26 | 27 | ```html 28 |
29 |
30 | 31 |
32 | ``` 33 | 34 | Note that `.cropit-image-preview-container` element is no longer needed, and all you need is a `.cropit-preview` (previously `.cropit-image-preview`) whether or not you want image background that goes beyond the preview area. New markup structure (after cropit is initialized) is as follows: 35 | 36 | ```jade 37 | .cropit-preview 38 | .cropit-preview-background-container 39 | img.cropit-preview-background 40 | .cropit-preview-image-container 41 | img.cropit-preview-image 42 | ``` 43 | 44 | Note the class name changes: 45 | 46 | ``` 47 | .cropit-image-preview => .cropit-preview 48 | .cropit-image-background-container => .cropit-preview-background-container 49 | .cropit-image-background => .cropit-preview-background 50 | ``` 51 | 52 | Make sure to update class names in your selectors. 53 | 54 | ### Breaking changes 55 | 56 | * Markup structure and class name changes. See migration guide above for details. 57 | 58 | ### New features 59 | 60 | * Added rotation APIs `rotateCW` and `rotateCCW`, which rotates the image by 90 degrees clockwise/counterclockwise. If, after rotated by 90 degrees, the dimension of the image no longer meets the requirements, it would be rotated by 180 degrees. 61 | * Render image using CSS transformation, which drastically improved performance. 62 | 63 | ### Bug fixes 64 | 65 | * Now remote images are loaded through AJAX and rendered as data URI strings, which addresses CORS issues. `allowCrossOrigin` option is no longer necessary and therefore removed. 66 | 67 | 68 | ## 0.4.5 (September 27, 2015) 69 | 70 | ### Bug fixes 71 | 72 | * Fixed an issue where cropit exports blank images on Safari. Removed progressive resizing, which may degrade cropped image quality. For high quality resizing, using a server-side tool is recommended. 73 | 74 | 75 | ## 0.4.4 (September 12, 2015) 76 | 77 | ### New features 78 | 79 | * Added getters and setters for `initialZoom`, `exportZoom`, `minZoom` and `maxZoom` 80 | * Added `onOffsetChange` and `onZoomChange` callback 81 | * onFileChange now passes back the event object 82 | 83 | ### Bug fixes 84 | 85 | * Fixed bug where `image-loaded` class is removed if a small image is loaded and rejected 86 | 87 | 88 | ## 0.4.1 (August 2, 2015) 89 | 90 | ### Bug fixes 91 | 92 | * Fixed crossOrigin preventing image from loading in Safari and Firefox. 93 | 94 | 95 | ## 0.4.0 (July 7, 2015) 96 | 97 | ### New features 98 | 99 | * Added option to allow small image to be either zoomed down its original size or stretch to fill/fit container 100 | 101 | ### Breaking changes 102 | 103 | * Replaced `rejectSmallImage` option with `smallImage`. `rejectSmallImage: true` is now `smallImage: 'reject'`, and `rejectSmallImage: false` is now `smallImage: 'allow'`. 104 | 105 | 106 | ## 0.3.2 (July 3, 2015) 107 | 108 | ### New features 109 | 110 | * Added back `allowCrossOrigin` option 111 | 112 | 113 | ## 0.3.1 (June 30, 2015) 114 | 115 | ### Bug fixes 116 | 117 | * Fixed jQuery import in AMD and CommonJS. 118 | 119 | 120 | ## 0.3.0 (June 21, 2015) 121 | 122 | ### New features 123 | 124 | * Center image when uploaded 125 | * Added `maxZoom`, `minZoom`, `initialZoom` options 126 | * Added `rejectSmallImage` option 127 | * By default if image is smaller than preview, it won't be loaded and the old image would be preserved 128 | * Added `onFileReaderError` callback 129 | 130 | ### Breaking changes 131 | 132 | * Removed `allowCrossOrigin` option 133 | 134 | ### Development 135 | 136 | * Major refactor -- rewrote in ES6! No more CoffeeScript. 137 | * Now build with Webpack and removed Grunt 138 | 139 | 140 | ## 0.2.0 (December 16, 2014) 141 | 142 | ### New features 143 | 144 | * Added drag & drop support via `allowDragNDrop` option, default to true 145 | * Added free move support via `freeMove` option, default to false 146 | * Added CommonJS support 147 | 148 | ### Breaking changes 149 | 150 | * Renamed option `freeImageMove` -> `freeMove` 151 | 152 | 153 | ## 0.1.9 (October 19, 2014) 154 | 155 | ### New features 156 | 157 | * Added touch support 158 | * Added disable and reenable APIs 159 | * Support varying backgroung image border size 160 | * Added white background in jpeg format exports 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Scott Cheng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cropit 2 | [![CDNJS](https://img.shields.io/cdnjs/v/cropit.svg)](https://cdnjs.com/libraries/cropit) 3 | [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/cropit/badge?style=rounded)](https://www.jsdelivr.com/package/npm/cropit) 4 | 5 | Customizable crop and zoom. 6 | 7 | See demos and docs [here](http://scottcheng.github.io/cropit/). 8 | 9 | Built on top of [Yufei Liu's Image Editor](https://github.com/yufeiliu/simple_image_uploader). 10 | 11 | 12 | ## Installation 13 | 14 | ```bash 15 | # Install cropit with bower 16 | $ bower install cropit 17 | 18 | # or with npm 19 | $ npm install cropit 20 | ``` 21 | 22 | 23 | ## Migrating to v0.5 24 | 25 | v0.5 [introduced](https://github.com/scottcheng/cropit/blob/master/CHANGELOG.md#user-content-050-february-27-2016) rotation feature and improved performance, as well as a breaking changes in markup structure and class names. 26 | 27 | Markup in v0.4: 28 | 29 | ```html 30 |
31 | 32 |
33 |
34 |
35 | 36 |
37 | ``` 38 | 39 | New markup in v0.5: 40 | 41 | ```html 42 |
43 |
44 | 45 |
46 | ``` 47 | 48 | Note that `.cropit-image-preview-container` element is no longer needed, and all you need is a `.cropit-preview` (previously `.cropit-image-preview`) whether or not you want image background that goes beyond the preview area. New markup structure (after cropit is initialized) is as follows: 49 | 50 | ```jade 51 | .cropit-preview 52 | .cropit-preview-background-container 53 | img.cropit-preview-background 54 | .cropit-preview-image-container 55 | img.cropit-preview-image 56 | ``` 57 | 58 | Note the class name changes: 59 | 60 | ``` 61 | .cropit-image-preview => .cropit-preview 62 | .cropit-image-background-container => .cropit-preview-background-container 63 | .cropit-image-background => .cropit-preview-background 64 | ``` 65 | 66 | Make sure to update class names in your selectors. 67 | 68 | 69 | ## Development 70 | 71 | * Build: `webpack` 72 | * Watch for changes and rebuild: `webpack -w` 73 | * Test: `npm test` 74 | * Test specific file: `jest ` 75 | * Lint: `npm run jshint -s` 76 | 77 | 78 | ## License 79 | 80 | MIT 81 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cropit", 3 | "description": "Customizable crop and zoom.", 4 | "version": "0.5.1", 5 | "authors": [ 6 | "Scott Cheng ", 7 | "Yufei Liu " 8 | ], 9 | "main": "dist/jquery.cropit.js", 10 | "keywords": [ 11 | "crop", 12 | "zoom", 13 | "image editor" 14 | ], 15 | "license": "MIT", 16 | "homepage": "http://scottcheng.github.io/cropit", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "test" 21 | ], 22 | "dependencies": { 23 | "jquery": ">=1.9" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cropit.jquery.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cropit", 3 | "title": "cropit", 4 | "description": "Customizable crop and zoom.", 5 | "keywords": [ 6 | "image", 7 | "edit", 8 | "crop", 9 | "zoom" 10 | ], 11 | "version": "0.5.1", 12 | "author": { 13 | "name": "Scott Cheng", 14 | "email": "me@scottcheng.com", 15 | "url": "https://github.com/scottcheng" 16 | }, 17 | "maintainers": [ 18 | { 19 | "name": "Scott Cheng", 20 | "email": "me@scottcheng.com", 21 | "url": "https://github.com/scottcheng" 22 | } 23 | ], 24 | "licenses": [ 25 | { 26 | "type": "MIT", 27 | "url": "http://zenorocha.mit-license.org/" 28 | } 29 | ], 30 | "bugs": "https://github.com/scottcheng/cropit/issues", 31 | "homepage": "https://github.com/scottcheng/cropit", 32 | "docs": "https://github.com/scottcheng/cropit#readme", 33 | "download": "https://github.com/scottcheng/cropit/archive/master.zip", 34 | "dependencies": { 35 | "jquery": ">=1.9" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /demo/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cropit 5 | 6 | 7 | 8 | 35 | 36 | 37 |
38 | 39 |
40 |
41 | Resize image 42 |
43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /demo/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cropit 5 | 6 | 7 | 8 | 48 | 49 | 50 |
51 |
52 | 53 |
54 |
55 | Resize image 56 |
57 | 58 | 59 | 60 |
61 |
62 | 63 |
64 | $form.serialize() = 65 | 66 |
67 | 68 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /demo/image-background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cropit 5 | 6 | 7 | 8 | 43 | 44 | 45 |
46 | 47 |
48 |
49 | Resize image 50 |
51 | 52 | 53 | 54 | 55 | 56 |
57 | 58 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /demo/pica.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | cropit 4 | 5 | 6 | 7 | 8 | 35 | 36 | 37 |
38 | 39 |
40 |
41 | Resize image 42 |
43 | 44 | 45 |
46 | 47 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /dist/jquery.cropit.js: -------------------------------------------------------------------------------- 1 | /*! cropit - v0.5.1 */ 2 | (function webpackUniversalModuleDefinition(root, factory) { 3 | if(typeof exports === 'object' && typeof module === 'object') 4 | module.exports = factory(require("jquery")); 5 | else if(typeof define === 'function' && define.amd) 6 | define(["jquery"], factory); 7 | else if(typeof exports === 'object') 8 | exports["cropit"] = factory(require("jquery")); 9 | else 10 | root["cropit"] = factory(root["jQuery"]); 11 | })(this, function(__WEBPACK_EXTERNAL_MODULE_1__) { 12 | return /******/ (function(modules) { // webpackBootstrap 13 | /******/ // The module cache 14 | /******/ var installedModules = {}; 15 | 16 | /******/ // The require function 17 | /******/ function __webpack_require__(moduleId) { 18 | 19 | /******/ // Check if module is in cache 20 | /******/ if(installedModules[moduleId]) 21 | /******/ return installedModules[moduleId].exports; 22 | 23 | /******/ // Create a new module (and put it into the cache) 24 | /******/ var module = installedModules[moduleId] = { 25 | /******/ exports: {}, 26 | /******/ id: moduleId, 27 | /******/ loaded: false 28 | /******/ }; 29 | 30 | /******/ // Execute the module function 31 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 32 | 33 | /******/ // Flag the module as loaded 34 | /******/ module.loaded = true; 35 | 36 | /******/ // Return the exports of the module 37 | /******/ return module.exports; 38 | /******/ } 39 | 40 | 41 | /******/ // expose the modules object (__webpack_modules__) 42 | /******/ __webpack_require__.m = modules; 43 | 44 | /******/ // expose the module cache 45 | /******/ __webpack_require__.c = installedModules; 46 | 47 | /******/ // __webpack_public_path__ 48 | /******/ __webpack_require__.p = ""; 49 | 50 | /******/ // Load entry module and return exports 51 | /******/ return __webpack_require__(0); 52 | /******/ }) 53 | /************************************************************************/ 54 | /******/ ([ 55 | /* 0 */ 56 | /***/ function(module, exports, __webpack_require__) { 57 | 58 | var _slice = Array.prototype.slice; 59 | 60 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 61 | 62 | var _jquery = __webpack_require__(1); 63 | 64 | var _jquery2 = _interopRequireDefault(_jquery); 65 | 66 | var _cropit = __webpack_require__(2); 67 | 68 | var _cropit2 = _interopRequireDefault(_cropit); 69 | 70 | var _constants = __webpack_require__(4); 71 | 72 | var _utils = __webpack_require__(6); 73 | 74 | var applyOnEach = function applyOnEach($el, callback) { 75 | return $el.each(function () { 76 | var cropit = _jquery2['default'].data(this, _constants.PLUGIN_KEY); 77 | 78 | if (!cropit) { 79 | return; 80 | } 81 | callback(cropit); 82 | }); 83 | }; 84 | 85 | var callOnFirst = function callOnFirst($el, method, options) { 86 | var cropit = $el.first().data(_constants.PLUGIN_KEY); 87 | 88 | if (!cropit || !_jquery2['default'].isFunction(cropit[method])) { 89 | return null; 90 | } 91 | return cropit[method](options); 92 | }; 93 | 94 | var methods = { 95 | init: function init(options) { 96 | return this.each(function () { 97 | // Only instantiate once per element 98 | if (_jquery2['default'].data(this, _constants.PLUGIN_KEY)) { 99 | return; 100 | } 101 | 102 | var cropit = new _cropit2['default'](_jquery2['default'], this, options); 103 | _jquery2['default'].data(this, _constants.PLUGIN_KEY, cropit); 104 | }); 105 | }, 106 | 107 | destroy: function destroy() { 108 | return this.each(function () { 109 | _jquery2['default'].removeData(this, _constants.PLUGIN_KEY); 110 | }); 111 | }, 112 | 113 | isZoomable: function isZoomable() { 114 | return callOnFirst(this, 'isZoomable'); 115 | }, 116 | 117 | 'export': function _export(options) { 118 | return callOnFirst(this, 'getCroppedImageData', options); 119 | } 120 | }; 121 | 122 | var delegate = function delegate($el, fnName) { 123 | return applyOnEach($el, function (cropit) { 124 | cropit[fnName](); 125 | }); 126 | }; 127 | 128 | var prop = function prop($el, name, value) { 129 | if ((0, _utils.exists)(value)) { 130 | return applyOnEach($el, function (cropit) { 131 | cropit[name] = value; 132 | }); 133 | } else { 134 | var cropit = $el.first().data(_constants.PLUGIN_KEY); 135 | return cropit[name]; 136 | } 137 | }; 138 | 139 | _jquery2['default'].fn.cropit = function (method) { 140 | if (methods[method]) { 141 | return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 142 | } else if (['imageState', 'imageSrc', 'offset', 'previewSize', 'imageSize', 'zoom', 'initialZoom', 'exportZoom', 'minZoom', 'maxZoom'].indexOf(method) >= 0) { 143 | return prop.apply(undefined, [this].concat(_slice.call(arguments))); 144 | } else if (['rotateCW', 'rotateCCW', 'disable', 'reenable'].indexOf(method) >= 0) { 145 | return delegate.apply(undefined, [this].concat(_slice.call(arguments))); 146 | } else { 147 | return methods.init.apply(this, arguments); 148 | } 149 | }; 150 | 151 | /***/ }, 152 | /* 1 */ 153 | /***/ function(module, exports) { 154 | 155 | module.exports = __WEBPACK_EXTERNAL_MODULE_1__; 156 | 157 | /***/ }, 158 | /* 2 */ 159 | /***/ function(module, exports, __webpack_require__) { 160 | 161 | Object.defineProperty(exports, '__esModule', { 162 | value: true 163 | }); 164 | 165 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 166 | 167 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 168 | 169 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 170 | 171 | var _jquery = __webpack_require__(1); 172 | 173 | var _jquery2 = _interopRequireDefault(_jquery); 174 | 175 | var _Zoomer = __webpack_require__(3); 176 | 177 | var _Zoomer2 = _interopRequireDefault(_Zoomer); 178 | 179 | var _constants = __webpack_require__(4); 180 | 181 | var _options = __webpack_require__(5); 182 | 183 | var _utils = __webpack_require__(6); 184 | 185 | var Cropit = (function () { 186 | function Cropit(jQuery, element, options) { 187 | _classCallCheck(this, Cropit); 188 | 189 | this.$el = (0, _jquery2['default'])(element); 190 | 191 | var defaults = (0, _options.loadDefaults)(this.$el); 192 | this.options = _jquery2['default'].extend({}, defaults, options); 193 | 194 | this.init(); 195 | } 196 | 197 | _createClass(Cropit, [{ 198 | key: 'init', 199 | value: function init() { 200 | var _this = this; 201 | 202 | this.image = new Image(); 203 | this.preImage = new Image(); 204 | this.image.onload = this.onImageLoaded.bind(this); 205 | this.preImage.onload = this.onPreImageLoaded.bind(this); 206 | this.image.onerror = this.preImage.onerror = function () { 207 | _this.onImageError.call(_this, _constants.ERRORS.IMAGE_FAILED_TO_LOAD); 208 | }; 209 | 210 | this.$preview = this.options.$preview.css('position', 'relative'); 211 | this.$fileInput = this.options.$fileInput.attr({ accept: 'image/*' }); 212 | this.$zoomSlider = this.options.$zoomSlider.attr({ min: 0, max: 1, step: 0.01 }); 213 | 214 | this.previewSize = { 215 | width: this.options.width || this.$preview.innerWidth(), 216 | height: this.options.height || this.$preview.innerHeight() 217 | }; 218 | 219 | this.$image = (0, _jquery2['default'])('').addClass(_constants.CLASS_NAMES.PREVIEW_IMAGE).attr('alt', '').css({ 220 | transformOrigin: 'top left', 221 | webkitTransformOrigin: 'top left', 222 | willChange: 'transform' 223 | }); 224 | this.$imageContainer = (0, _jquery2['default'])('
').addClass(_constants.CLASS_NAMES.PREVIEW_IMAGE_CONTAINER).css({ 225 | position: 'absolute', 226 | overflow: 'hidden', 227 | left: 0, 228 | top: 0, 229 | width: '100%', 230 | height: '100%' 231 | }).append(this.$image); 232 | this.$preview.append(this.$imageContainer); 233 | 234 | if (this.options.imageBackground) { 235 | if (_jquery2['default'].isArray(this.options.imageBackgroundBorderWidth)) { 236 | this.bgBorderWidthArray = this.options.imageBackgroundBorderWidth; 237 | } else { 238 | this.bgBorderWidthArray = [0, 1, 2, 3].map(function () { 239 | return _this.options.imageBackgroundBorderWidth; 240 | }); 241 | } 242 | 243 | this.$bg = (0, _jquery2['default'])('').addClass(_constants.CLASS_NAMES.PREVIEW_BACKGROUND).attr('alt', '').css({ 244 | position: 'relative', 245 | left: this.bgBorderWidthArray[3], 246 | top: this.bgBorderWidthArray[0], 247 | transformOrigin: 'top left', 248 | webkitTransformOrigin: 'top left', 249 | willChange: 'transform' 250 | }); 251 | this.$bgContainer = (0, _jquery2['default'])('
').addClass(_constants.CLASS_NAMES.PREVIEW_BACKGROUND_CONTAINER).css({ 252 | position: 'absolute', 253 | zIndex: 0, 254 | top: -this.bgBorderWidthArray[0], 255 | right: -this.bgBorderWidthArray[1], 256 | bottom: -this.bgBorderWidthArray[2], 257 | left: -this.bgBorderWidthArray[3] 258 | }).append(this.$bg); 259 | if (this.bgBorderWidthArray[0] > 0) { 260 | this.$bgContainer.css('overflow', 'hidden'); 261 | } 262 | this.$preview.prepend(this.$bgContainer); 263 | } 264 | 265 | this.initialZoom = this.options.initialZoom; 266 | 267 | this.imageLoaded = false; 268 | 269 | this.moveContinue = false; 270 | 271 | this.zoomer = new _Zoomer2['default'](); 272 | 273 | if (this.options.allowDragNDrop) { 274 | _jquery2['default'].event.props.push('dataTransfer'); 275 | } 276 | 277 | this.bindListeners(); 278 | 279 | if (this.options.imageState && this.options.imageState.src) { 280 | this.loadImage(this.options.imageState.src); 281 | } 282 | } 283 | }, { 284 | key: 'bindListeners', 285 | value: function bindListeners() { 286 | this.$fileInput.on('change.cropit', this.onFileChange.bind(this)); 287 | this.$imageContainer.on(_constants.EVENTS.PREVIEW, this.onPreviewEvent.bind(this)); 288 | this.$zoomSlider.on(_constants.EVENTS.ZOOM_INPUT, this.onZoomSliderChange.bind(this)); 289 | 290 | if (this.options.allowDragNDrop) { 291 | this.$imageContainer.on('dragover.cropit dragleave.cropit', this.onDragOver.bind(this)); 292 | this.$imageContainer.on('drop.cropit', this.onDrop.bind(this)); 293 | } 294 | } 295 | }, { 296 | key: 'unbindListeners', 297 | value: function unbindListeners() { 298 | this.$fileInput.off('change.cropit'); 299 | this.$imageContainer.off(_constants.EVENTS.PREVIEW); 300 | this.$imageContainer.off('dragover.cropit dragleave.cropit drop.cropit'); 301 | this.$zoomSlider.off(_constants.EVENTS.ZOOM_INPUT); 302 | } 303 | }, { 304 | key: 'onFileChange', 305 | value: function onFileChange(e) { 306 | this.options.onFileChange(e); 307 | 308 | if (this.$fileInput.get(0).files) { 309 | this.loadFile(this.$fileInput.get(0).files[0]); 310 | } 311 | } 312 | }, { 313 | key: 'loadFile', 314 | value: function loadFile(file) { 315 | var fileReader = new FileReader(); 316 | if (file && file.type.match('image')) { 317 | fileReader.readAsDataURL(file); 318 | fileReader.onload = this.onFileReaderLoaded.bind(this); 319 | fileReader.onerror = this.onFileReaderError.bind(this); 320 | } else if (file) { 321 | this.onFileReaderError(); 322 | } 323 | } 324 | }, { 325 | key: 'onFileReaderLoaded', 326 | value: function onFileReaderLoaded(e) { 327 | this.loadImage(e.target.result); 328 | } 329 | }, { 330 | key: 'onFileReaderError', 331 | value: function onFileReaderError() { 332 | this.options.onFileReaderError(); 333 | } 334 | }, { 335 | key: 'onDragOver', 336 | value: function onDragOver(e) { 337 | e.preventDefault(); 338 | e.dataTransfer.dropEffect = 'copy'; 339 | this.$preview.toggleClass(_constants.CLASS_NAMES.DRAG_HOVERED, e.type === 'dragover'); 340 | } 341 | }, { 342 | key: 'onDrop', 343 | value: function onDrop(e) { 344 | var _this2 = this; 345 | 346 | e.preventDefault(); 347 | e.stopPropagation(); 348 | 349 | var files = Array.prototype.slice.call(e.dataTransfer.files, 0); 350 | files.some(function (file) { 351 | if (!file.type.match('image')) { 352 | return false; 353 | } 354 | 355 | _this2.loadFile(file); 356 | return true; 357 | }); 358 | 359 | this.$preview.removeClass(_constants.CLASS_NAMES.DRAG_HOVERED); 360 | } 361 | }, { 362 | key: 'loadImage', 363 | value: function loadImage(imageSrc) { 364 | var _this3 = this; 365 | 366 | if (!imageSrc) { 367 | return; 368 | } 369 | 370 | this.options.onImageLoading(); 371 | this.setImageLoadingClass(); 372 | 373 | if (imageSrc.indexOf('data') === 0) { 374 | this.preImage.src = imageSrc; 375 | } else { 376 | var xhr = new XMLHttpRequest(); 377 | xhr.onload = function (e) { 378 | if (e.target.status >= 300) { 379 | _this3.onImageError.call(_this3, _constants.ERRORS.IMAGE_FAILED_TO_LOAD); 380 | return; 381 | } 382 | 383 | _this3.loadFile(e.target.response); 384 | }; 385 | xhr.open('GET', imageSrc); 386 | xhr.responseType = 'blob'; 387 | xhr.send(); 388 | } 389 | } 390 | }, { 391 | key: 'onPreImageLoaded', 392 | value: function onPreImageLoaded() { 393 | if (this.shouldRejectImage({ 394 | imageWidth: this.preImage.width, 395 | imageHeight: this.preImage.height, 396 | previewSize: this.previewSize, 397 | maxZoom: this.options.maxZoom, 398 | exportZoom: this.options.exportZoom, 399 | smallImage: this.options.smallImage 400 | })) { 401 | this.onImageError(_constants.ERRORS.SMALL_IMAGE); 402 | if (this.image.src) { 403 | this.setImageLoadedClass(); 404 | } 405 | return; 406 | } 407 | 408 | this.image.src = this.preImage.src; 409 | } 410 | }, { 411 | key: 'onImageLoaded', 412 | value: function onImageLoaded() { 413 | this.rotation = 0; 414 | this.setupZoomer(this.options.imageState && this.options.imageState.zoom || this._initialZoom); 415 | if (this.options.imageState && this.options.imageState.offset) { 416 | this.offset = this.options.imageState.offset; 417 | } else { 418 | this.centerImage(); 419 | } 420 | 421 | this.options.imageState = {}; 422 | 423 | this.$image.attr('src', this.image.src); 424 | if (this.options.imageBackground) { 425 | this.$bg.attr('src', this.image.src); 426 | } 427 | 428 | this.setImageLoadedClass(); 429 | 430 | this.imageLoaded = true; 431 | 432 | this.options.onImageLoaded(); 433 | } 434 | }, { 435 | key: 'onImageError', 436 | value: function onImageError() { 437 | this.options.onImageError.apply(this, arguments); 438 | this.removeImageLoadingClass(); 439 | } 440 | }, { 441 | key: 'setImageLoadingClass', 442 | value: function setImageLoadingClass() { 443 | this.$preview.removeClass(_constants.CLASS_NAMES.IMAGE_LOADED).addClass(_constants.CLASS_NAMES.IMAGE_LOADING); 444 | } 445 | }, { 446 | key: 'setImageLoadedClass', 447 | value: function setImageLoadedClass() { 448 | this.$preview.removeClass(_constants.CLASS_NAMES.IMAGE_LOADING).addClass(_constants.CLASS_NAMES.IMAGE_LOADED); 449 | } 450 | }, { 451 | key: 'removeImageLoadingClass', 452 | value: function removeImageLoadingClass() { 453 | this.$preview.removeClass(_constants.CLASS_NAMES.IMAGE_LOADING); 454 | } 455 | }, { 456 | key: 'getEventPosition', 457 | value: function getEventPosition(e) { 458 | if (e.originalEvent && e.originalEvent.touches && e.originalEvent.touches[0]) { 459 | e = e.originalEvent.touches[0]; 460 | } 461 | if (e.clientX && e.clientY) { 462 | return { x: e.clientX, y: e.clientY }; 463 | } 464 | } 465 | }, { 466 | key: 'onPreviewEvent', 467 | value: function onPreviewEvent(e) { 468 | if (!this.imageLoaded) { 469 | return; 470 | } 471 | 472 | this.moveContinue = false; 473 | this.$imageContainer.off(_constants.EVENTS.PREVIEW_MOVE); 474 | 475 | if (e.type === 'mousedown' || e.type === 'touchstart') { 476 | this.origin = this.getEventPosition(e); 477 | this.moveContinue = true; 478 | this.$imageContainer.on(_constants.EVENTS.PREVIEW_MOVE, this.onMove.bind(this)); 479 | } else { 480 | (0, _jquery2['default'])(document.body).focus(); 481 | } 482 | 483 | e.stopPropagation(); 484 | return false; 485 | } 486 | }, { 487 | key: 'onMove', 488 | value: function onMove(e) { 489 | var eventPosition = this.getEventPosition(e); 490 | 491 | if (this.moveContinue && eventPosition) { 492 | this.offset = { 493 | x: this.offset.x + eventPosition.x - this.origin.x, 494 | y: this.offset.y + eventPosition.y - this.origin.y 495 | }; 496 | } 497 | 498 | this.origin = eventPosition; 499 | 500 | e.stopPropagation(); 501 | return false; 502 | } 503 | }, { 504 | key: 'fixOffset', 505 | value: function fixOffset(offset) { 506 | if (!this.imageLoaded) { 507 | return offset; 508 | } 509 | 510 | var ret = { x: offset.x, y: offset.y }; 511 | 512 | if (!this.options.freeMove) { 513 | if (this.imageWidth * this.zoom >= this.previewSize.width) { 514 | ret.x = Math.min(0, Math.max(ret.x, this.previewSize.width - this.imageWidth * this.zoom)); 515 | } else { 516 | ret.x = Math.max(0, Math.min(ret.x, this.previewSize.width - this.imageWidth * this.zoom)); 517 | } 518 | 519 | if (this.imageHeight * this.zoom >= this.previewSize.height) { 520 | ret.y = Math.min(0, Math.max(ret.y, this.previewSize.height - this.imageHeight * this.zoom)); 521 | } else { 522 | ret.y = Math.max(0, Math.min(ret.y, this.previewSize.height - this.imageHeight * this.zoom)); 523 | } 524 | } 525 | 526 | ret.x = (0, _utils.round)(ret.x); 527 | ret.y = (0, _utils.round)(ret.y); 528 | 529 | return ret; 530 | } 531 | }, { 532 | key: 'centerImage', 533 | value: function centerImage() { 534 | if (!this.image.width || !this.image.height || !this.zoom) { 535 | return; 536 | } 537 | 538 | this.offset = { 539 | x: (this.previewSize.width - this.imageWidth * this.zoom) / 2, 540 | y: (this.previewSize.height - this.imageHeight * this.zoom) / 2 541 | }; 542 | } 543 | }, { 544 | key: 'onZoomSliderChange', 545 | value: function onZoomSliderChange() { 546 | if (!this.imageLoaded) { 547 | return; 548 | } 549 | 550 | this.zoomSliderPos = Number(this.$zoomSlider.val()); 551 | var newZoom = this.zoomer.getZoom(this.zoomSliderPos); 552 | if (newZoom === this.zoom) { 553 | return; 554 | } 555 | this.zoom = newZoom; 556 | } 557 | }, { 558 | key: 'enableZoomSlider', 559 | value: function enableZoomSlider() { 560 | this.$zoomSlider.removeAttr('disabled'); 561 | this.options.onZoomEnabled(); 562 | } 563 | }, { 564 | key: 'disableZoomSlider', 565 | value: function disableZoomSlider() { 566 | this.$zoomSlider.attr('disabled', true); 567 | this.options.onZoomDisabled(); 568 | } 569 | }, { 570 | key: 'setupZoomer', 571 | value: function setupZoomer(zoom) { 572 | this.zoomer.setup({ 573 | imageSize: this.imageSize, 574 | previewSize: this.previewSize, 575 | exportZoom: this.options.exportZoom, 576 | maxZoom: this.options.maxZoom, 577 | minZoom: this.options.minZoom, 578 | smallImage: this.options.smallImage 579 | }); 580 | this.zoom = (0, _utils.exists)(zoom) ? zoom : this._zoom; 581 | 582 | if (this.isZoomable()) { 583 | this.enableZoomSlider(); 584 | } else { 585 | this.disableZoomSlider(); 586 | } 587 | } 588 | }, { 589 | key: 'fixZoom', 590 | value: function fixZoom(zoom) { 591 | return this.zoomer.fixZoom(zoom); 592 | } 593 | }, { 594 | key: 'isZoomable', 595 | value: function isZoomable() { 596 | return this.zoomer.isZoomable(); 597 | } 598 | }, { 599 | key: 'renderImage', 600 | value: function renderImage() { 601 | var transformation = '\n translate(' + this.rotatedOffset.x + 'px, ' + this.rotatedOffset.y + 'px)\n scale(' + this.zoom + ')\n rotate(' + this.rotation + 'deg)'; 602 | 603 | this.$image.css({ 604 | transform: transformation, 605 | webkitTransform: transformation 606 | }); 607 | if (this.options.imageBackground) { 608 | this.$bg.css({ 609 | transform: transformation, 610 | webkitTransform: transformation 611 | }); 612 | } 613 | } 614 | }, { 615 | key: 'rotateCW', 616 | value: function rotateCW() { 617 | if (this.shouldRejectImage({ 618 | imageWidth: this.image.height, 619 | imageHeight: this.image.width, 620 | previewSize: this.previewSize, 621 | maxZoom: this.options.maxZoom, 622 | exportZoom: this.options.exportZoom, 623 | smallImage: this.options.smallImage 624 | })) { 625 | this.rotation = (this.rotation + 180) % 360; 626 | } else { 627 | this.rotation = (this.rotation + 90) % 360; 628 | } 629 | } 630 | }, { 631 | key: 'rotateCCW', 632 | value: function rotateCCW() { 633 | if (this.shouldRejectImage({ 634 | imageWidth: this.image.height, 635 | imageHeight: this.image.width, 636 | previewSize: this.previewSize, 637 | maxZoom: this.options.maxZoom, 638 | exportZoom: this.options.exportZoom, 639 | smallImage: this.options.smallImage 640 | })) { 641 | this.rotation = (this.rotation + 180) % 360; 642 | } else { 643 | this.rotation = (this.rotation + 270) % 360; 644 | } 645 | } 646 | }, { 647 | key: 'shouldRejectImage', 648 | value: function shouldRejectImage(_ref) { 649 | var imageWidth = _ref.imageWidth; 650 | var imageHeight = _ref.imageHeight; 651 | var previewSize = _ref.previewSize; 652 | var maxZoom = _ref.maxZoom; 653 | var exportZoom = _ref.exportZoom; 654 | var smallImage = _ref.smallImage; 655 | 656 | if (smallImage !== 'reject') { 657 | return false; 658 | } 659 | 660 | return imageWidth * maxZoom < previewSize.width * exportZoom || imageHeight * maxZoom < previewSize.height * exportZoom; 661 | } 662 | }, { 663 | key: 'getCroppedImageData', 664 | value: function getCroppedImageData(exportOptions) { 665 | if (!this.image.src) { 666 | return; 667 | } 668 | 669 | var exportDefaults = { 670 | type: 'image/png', 671 | quality: 0.75, 672 | originalSize: false, 673 | fillBg: '#fff' 674 | }; 675 | exportOptions = _jquery2['default'].extend({}, exportDefaults, exportOptions); 676 | 677 | var exportZoom = exportOptions.originalSize ? 1 / this.zoom : this.options.exportZoom; 678 | 679 | var zoomedSize = { 680 | width: this.zoom * exportZoom * this.image.width, 681 | height: this.zoom * exportZoom * this.image.height 682 | }; 683 | 684 | var canvas = (0, _jquery2['default'])('').attr({ 685 | width: this.previewSize.width * exportZoom, 686 | height: this.previewSize.height * exportZoom 687 | }).get(0); 688 | var canvasContext = canvas.getContext('2d'); 689 | 690 | if (exportOptions.type === 'image/jpeg') { 691 | canvasContext.fillStyle = exportOptions.fillBg; 692 | canvasContext.fillRect(0, 0, canvas.width, canvas.height); 693 | } 694 | 695 | canvasContext.translate(this.rotatedOffset.x * exportZoom, this.rotatedOffset.y * exportZoom); 696 | canvasContext.rotate(this.rotation * Math.PI / 180); 697 | canvasContext.drawImage(this.image, 0, 0, zoomedSize.width, zoomedSize.height); 698 | 699 | return canvas.toDataURL(exportOptions.type, exportOptions.quality); 700 | } 701 | }, { 702 | key: 'disable', 703 | value: function disable() { 704 | this.unbindListeners(); 705 | this.disableZoomSlider(); 706 | this.$el.addClass(_constants.CLASS_NAMES.DISABLED); 707 | } 708 | }, { 709 | key: 'reenable', 710 | value: function reenable() { 711 | this.bindListeners(); 712 | this.enableZoomSlider(); 713 | this.$el.removeClass(_constants.CLASS_NAMES.DISABLED); 714 | } 715 | }, { 716 | key: '$', 717 | value: function $(selector) { 718 | if (!this.$el) { 719 | return null; 720 | } 721 | return this.$el.find(selector); 722 | } 723 | }, { 724 | key: 'offset', 725 | set: function (position) { 726 | if (!position || !(0, _utils.exists)(position.x) || !(0, _utils.exists)(position.y)) { 727 | return; 728 | } 729 | 730 | this._offset = this.fixOffset(position); 731 | this.renderImage(); 732 | 733 | this.options.onOffsetChange(position); 734 | }, 735 | get: function () { 736 | return this._offset; 737 | } 738 | }, { 739 | key: 'zoom', 740 | set: function (newZoom) { 741 | newZoom = this.fixZoom(newZoom); 742 | 743 | if (this.imageLoaded) { 744 | var oldZoom = this.zoom; 745 | 746 | var newX = this.previewSize.width / 2 - (this.previewSize.width / 2 - this.offset.x) * newZoom / oldZoom; 747 | var newY = this.previewSize.height / 2 - (this.previewSize.height / 2 - this.offset.y) * newZoom / oldZoom; 748 | 749 | this._zoom = newZoom; 750 | this.offset = { x: newX, y: newY }; // Triggers renderImage() 751 | } else { 752 | this._zoom = newZoom; 753 | } 754 | 755 | this.zoomSliderPos = this.zoomer.getSliderPos(this.zoom); 756 | this.$zoomSlider.val(this.zoomSliderPos); 757 | 758 | this.options.onZoomChange(newZoom); 759 | }, 760 | get: function () { 761 | return this._zoom; 762 | } 763 | }, { 764 | key: 'rotatedOffset', 765 | get: function () { 766 | return { 767 | x: this.offset.x + (this.rotation === 90 ? this.image.height * this.zoom : 0) + (this.rotation === 180 ? this.image.width * this.zoom : 0), 768 | y: this.offset.y + (this.rotation === 180 ? this.image.height * this.zoom : 0) + (this.rotation === 270 ? this.image.width * this.zoom : 0) 769 | }; 770 | } 771 | }, { 772 | key: 'rotation', 773 | set: function (newRotation) { 774 | this._rotation = newRotation; 775 | 776 | if (this.imageLoaded) { 777 | // Change in image size may lead to change in zoom range 778 | this.setupZoomer(); 779 | } 780 | }, 781 | get: function () { 782 | return this._rotation; 783 | } 784 | }, { 785 | key: 'imageState', 786 | get: function () { 787 | return { 788 | src: this.image.src, 789 | offset: this.offset, 790 | zoom: this.zoom 791 | }; 792 | } 793 | }, { 794 | key: 'imageSrc', 795 | get: function () { 796 | return this.image.src; 797 | }, 798 | set: function (imageSrc) { 799 | this.loadImage(imageSrc); 800 | } 801 | }, { 802 | key: 'imageWidth', 803 | get: function () { 804 | return this.rotation % 180 === 0 ? this.image.width : this.image.height; 805 | } 806 | }, { 807 | key: 'imageHeight', 808 | get: function () { 809 | return this.rotation % 180 === 0 ? this.image.height : this.image.width; 810 | } 811 | }, { 812 | key: 'imageSize', 813 | get: function () { 814 | return { 815 | width: this.imageWidth, 816 | height: this.imageHeight 817 | }; 818 | } 819 | }, { 820 | key: 'initialZoom', 821 | get: function () { 822 | return this.options.initialZoom; 823 | }, 824 | set: function (initialZoomOption) { 825 | this.options.initialZoom = initialZoomOption; 826 | if (initialZoomOption === 'min') { 827 | this._initialZoom = 0; // Will be fixed when image loads 828 | } else if (initialZoomOption === 'image') { 829 | this._initialZoom = 1; 830 | } else { 831 | this._initialZoom = 0; 832 | } 833 | } 834 | }, { 835 | key: 'exportZoom', 836 | get: function () { 837 | return this.options.exportZoom; 838 | }, 839 | set: function (exportZoom) { 840 | this.options.exportZoom = exportZoom; 841 | this.setupZoomer(); 842 | } 843 | }, { 844 | key: 'minZoom', 845 | get: function () { 846 | return this.options.minZoom; 847 | }, 848 | set: function (minZoom) { 849 | this.options.minZoom = minZoom; 850 | this.setupZoomer(); 851 | } 852 | }, { 853 | key: 'maxZoom', 854 | get: function () { 855 | return this.options.maxZoom; 856 | }, 857 | set: function (maxZoom) { 858 | this.options.maxZoom = maxZoom; 859 | this.setupZoomer(); 860 | } 861 | }, { 862 | key: 'previewSize', 863 | get: function () { 864 | return this._previewSize; 865 | }, 866 | set: function (size) { 867 | if (!size || size.width <= 0 || size.height <= 0) { 868 | return; 869 | } 870 | 871 | this._previewSize = { 872 | width: size.width, 873 | height: size.height 874 | }; 875 | this.$preview.innerWidth(this.previewSize.width).innerHeight(this.previewSize.height); 876 | 877 | if (this.imageLoaded) { 878 | this.setupZoomer(); 879 | } 880 | } 881 | }]); 882 | 883 | return Cropit; 884 | })(); 885 | 886 | exports['default'] = Cropit; 887 | module.exports = exports['default']; 888 | 889 | /***/ }, 890 | /* 3 */ 891 | /***/ function(module, exports) { 892 | 893 | Object.defineProperty(exports, '__esModule', { 894 | value: true 895 | }); 896 | 897 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 898 | 899 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 900 | 901 | var Zoomer = (function () { 902 | function Zoomer() { 903 | _classCallCheck(this, Zoomer); 904 | 905 | this.minZoom = this.maxZoom = 1; 906 | } 907 | 908 | _createClass(Zoomer, [{ 909 | key: 'setup', 910 | value: function setup(_ref) { 911 | var imageSize = _ref.imageSize; 912 | var previewSize = _ref.previewSize; 913 | var exportZoom = _ref.exportZoom; 914 | var maxZoom = _ref.maxZoom; 915 | var minZoom = _ref.minZoom; 916 | var smallImage = _ref.smallImage; 917 | 918 | var widthRatio = previewSize.width / imageSize.width; 919 | var heightRatio = previewSize.height / imageSize.height; 920 | 921 | if (minZoom === 'fit') { 922 | this.minZoom = Math.min(widthRatio, heightRatio); 923 | } else { 924 | this.minZoom = Math.max(widthRatio, heightRatio); 925 | } 926 | 927 | if (smallImage === 'allow') { 928 | this.minZoom = Math.min(this.minZoom, 1); 929 | } 930 | 931 | this.maxZoom = Math.max(this.minZoom, maxZoom / exportZoom); 932 | } 933 | }, { 934 | key: 'getZoom', 935 | value: function getZoom(sliderPos) { 936 | if (!this.minZoom || !this.maxZoom) { 937 | return null; 938 | } 939 | 940 | return sliderPos * (this.maxZoom - this.minZoom) + this.minZoom; 941 | } 942 | }, { 943 | key: 'getSliderPos', 944 | value: function getSliderPos(zoom) { 945 | if (!this.minZoom || !this.maxZoom) { 946 | return null; 947 | } 948 | 949 | if (this.minZoom === this.maxZoom) { 950 | return 0; 951 | } else { 952 | return (zoom - this.minZoom) / (this.maxZoom - this.minZoom); 953 | } 954 | } 955 | }, { 956 | key: 'isZoomable', 957 | value: function isZoomable() { 958 | if (!this.minZoom || !this.maxZoom) { 959 | return null; 960 | } 961 | 962 | return this.minZoom !== this.maxZoom; 963 | } 964 | }, { 965 | key: 'fixZoom', 966 | value: function fixZoom(zoom) { 967 | return Math.max(this.minZoom, Math.min(this.maxZoom, zoom)); 968 | } 969 | }]); 970 | 971 | return Zoomer; 972 | })(); 973 | 974 | exports['default'] = Zoomer; 975 | module.exports = exports['default']; 976 | 977 | /***/ }, 978 | /* 4 */ 979 | /***/ function(module, exports) { 980 | 981 | Object.defineProperty(exports, '__esModule', { 982 | value: true 983 | }); 984 | var PLUGIN_KEY = 'cropit'; 985 | 986 | exports.PLUGIN_KEY = PLUGIN_KEY; 987 | var CLASS_NAMES = { 988 | PREVIEW: 'cropit-preview', 989 | PREVIEW_IMAGE_CONTAINER: 'cropit-preview-image-container', 990 | PREVIEW_IMAGE: 'cropit-preview-image', 991 | PREVIEW_BACKGROUND_CONTAINER: 'cropit-preview-background-container', 992 | PREVIEW_BACKGROUND: 'cropit-preview-background', 993 | FILE_INPUT: 'cropit-image-input', 994 | ZOOM_SLIDER: 'cropit-image-zoom-input', 995 | 996 | DRAG_HOVERED: 'cropit-drag-hovered', 997 | IMAGE_LOADING: 'cropit-image-loading', 998 | IMAGE_LOADED: 'cropit-image-loaded', 999 | DISABLED: 'cropit-disabled' 1000 | }; 1001 | 1002 | exports.CLASS_NAMES = CLASS_NAMES; 1003 | var ERRORS = { 1004 | IMAGE_FAILED_TO_LOAD: { code: 0, message: 'Image failed to load.' }, 1005 | SMALL_IMAGE: { code: 1, message: 'Image is too small.' } 1006 | }; 1007 | 1008 | exports.ERRORS = ERRORS; 1009 | var eventName = function eventName(events) { 1010 | return events.map(function (e) { 1011 | return '' + e + '.cropit'; 1012 | }).join(' '); 1013 | }; 1014 | var EVENTS = { 1015 | PREVIEW: eventName(['mousedown', 'mouseup', 'mouseleave', 'touchstart', 'touchend', 'touchcancel', 'touchleave']), 1016 | PREVIEW_MOVE: eventName(['mousemove', 'touchmove']), 1017 | ZOOM_INPUT: eventName(['mousemove', 'touchmove', 'change']) 1018 | }; 1019 | exports.EVENTS = EVENTS; 1020 | 1021 | /***/ }, 1022 | /* 5 */ 1023 | /***/ function(module, exports, __webpack_require__) { 1024 | 1025 | Object.defineProperty(exports, '__esModule', { 1026 | value: true 1027 | }); 1028 | 1029 | var _constants = __webpack_require__(4); 1030 | 1031 | var options = { 1032 | elements: [{ 1033 | name: '$preview', 1034 | description: 'The HTML element that displays image preview.', 1035 | defaultSelector: '.' + _constants.CLASS_NAMES.PREVIEW 1036 | }, { 1037 | name: '$fileInput', 1038 | description: 'File input element.', 1039 | defaultSelector: 'input.' + _constants.CLASS_NAMES.FILE_INPUT 1040 | }, { 1041 | name: '$zoomSlider', 1042 | description: 'Range input element that controls image zoom.', 1043 | defaultSelector: 'input.' + _constants.CLASS_NAMES.ZOOM_SLIDER 1044 | }].map(function (o) { 1045 | o.type = 'jQuery element'; 1046 | o['default'] = '$imageCropper.find(\'' + o.defaultSelector + '\')'; 1047 | return o; 1048 | }), 1049 | 1050 | values: [{ 1051 | name: 'width', 1052 | type: 'number', 1053 | description: 'Width of image preview in pixels. If set, it will override the CSS property.', 1054 | 'default': null 1055 | }, { 1056 | name: 'height', 1057 | type: 'number', 1058 | description: 'Height of image preview in pixels. If set, it will override the CSS property.', 1059 | 'default': null 1060 | }, { 1061 | name: 'imageBackground', 1062 | type: 'boolean', 1063 | description: 'Whether or not to display the background image beyond the preview area.', 1064 | 'default': false 1065 | }, { 1066 | name: 'imageBackgroundBorderWidth', 1067 | type: 'array or number', 1068 | description: 'Width of background image border in pixels.\n The four array elements specify the width of background image width on the top, right, bottom, left side respectively.\n The background image beyond the width will be hidden.\n If specified as a number, border with uniform width on all sides will be applied.', 1069 | 'default': [0, 0, 0, 0] 1070 | }, { 1071 | name: 'exportZoom', 1072 | type: 'number', 1073 | description: 'The ratio between the desired image size to export and the preview size.\n For example, if the preview size is `300px * 200px`, and `exportZoom = 2`, then\n the exported image size will be `600px * 400px`.\n This also affects the maximum zoom level, since the exported image cannot be zoomed to larger than its original size.', 1074 | 'default': 1 1075 | }, { 1076 | name: 'allowDragNDrop', 1077 | type: 'boolean', 1078 | description: 'When set to true, you can load an image by dragging it from local file browser onto the preview area.', 1079 | 'default': true 1080 | }, { 1081 | name: 'minZoom', 1082 | type: 'string', 1083 | description: 'This options decides the minimal zoom level of the image.\n If set to `\'fill\'`, the image has to fill the preview area, i.e. both width and height must not go smaller than the preview area.\n If set to `\'fit\'`, the image can shrink further to fit the preview area, i.e. at least one of its edges must not go smaller than the preview area.', 1084 | 'default': 'fill' 1085 | }, { 1086 | name: 'maxZoom', 1087 | type: 'number', 1088 | description: 'Determines how big the image can be zoomed. E.g. if set to 1.5, the image can be zoomed to 150% of its original size.', 1089 | 'default': 1 1090 | }, { 1091 | name: 'initialZoom', 1092 | type: 'string', 1093 | description: 'Determines the zoom when an image is loaded.\n When set to `\'min\'`, image is zoomed to the smallest when loaded.\n When set to `\'image\'`, image is zoomed to 100% when loaded.', 1094 | 'default': 'min' 1095 | }, { 1096 | name: 'freeMove', 1097 | type: 'boolean', 1098 | description: 'When set to true, you can freely move the image instead of being bound to the container borders', 1099 | 'default': false 1100 | }, { 1101 | name: 'smallImage', 1102 | type: 'string', 1103 | description: 'When set to `\'reject\'`, `onImageError` would be called when cropit loads an image that is smaller than the container.\n When set to `\'allow\'`, images smaller than the container can be zoomed down to its original size, overiding `minZoom` option.\n When set to `\'stretch\'`, the minimum zoom of small images would follow `minZoom` option.', 1104 | 'default': 'reject' 1105 | }], 1106 | 1107 | callbacks: [{ 1108 | name: 'onFileChange', 1109 | description: 'Called when user selects a file in the select file input.', 1110 | params: [{ 1111 | name: 'event', 1112 | type: 'object', 1113 | description: 'File change event object' 1114 | }] 1115 | }, { 1116 | name: 'onFileReaderError', 1117 | description: 'Called when `FileReader` encounters an error while loading the image file.' 1118 | }, { 1119 | name: 'onImageLoading', 1120 | description: 'Called when image starts to be loaded.' 1121 | }, { 1122 | name: 'onImageLoaded', 1123 | description: 'Called when image is loaded.' 1124 | }, { 1125 | name: 'onImageError', 1126 | description: 'Called when image cannot be loaded.', 1127 | params: [{ 1128 | name: 'error', 1129 | type: 'object', 1130 | description: 'Error object.' 1131 | }, { 1132 | name: 'error.code', 1133 | type: 'number', 1134 | description: 'Error code. `0` means generic image loading failure. `1` means image is too small.' 1135 | }, { 1136 | name: 'error.message', 1137 | type: 'string', 1138 | description: 'A message explaining the error.' 1139 | }] 1140 | }, { 1141 | name: 'onZoomEnabled', 1142 | description: 'Called when image the zoom slider is enabled.' 1143 | }, { 1144 | name: 'onZoomDisabled', 1145 | description: 'Called when image the zoom slider is disabled.' 1146 | }, { 1147 | name: 'onZoomChange', 1148 | description: 'Called when zoom changes.', 1149 | params: [{ 1150 | name: 'zoom', 1151 | type: 'number', 1152 | description: 'New zoom.' 1153 | }] 1154 | }, { 1155 | name: 'onOffsetChange', 1156 | description: 'Called when image offset changes.', 1157 | params: [{ 1158 | name: 'offset', 1159 | type: 'object', 1160 | description: 'New offset, with `x` and `y` values.' 1161 | }] 1162 | }].map(function (o) { 1163 | o.type = 'function';return o; 1164 | }) 1165 | }; 1166 | 1167 | var loadDefaults = function loadDefaults($el) { 1168 | var defaults = {}; 1169 | if ($el) { 1170 | options.elements.forEach(function (o) { 1171 | defaults[o.name] = $el.find(o.defaultSelector); 1172 | }); 1173 | } 1174 | options.values.forEach(function (o) { 1175 | defaults[o.name] = o['default']; 1176 | }); 1177 | options.callbacks.forEach(function (o) { 1178 | defaults[o.name] = function () {}; 1179 | }); 1180 | 1181 | return defaults; 1182 | }; 1183 | 1184 | exports.loadDefaults = loadDefaults; 1185 | exports['default'] = options; 1186 | 1187 | /***/ }, 1188 | /* 6 */ 1189 | /***/ function(module, exports) { 1190 | 1191 | Object.defineProperty(exports, '__esModule', { 1192 | value: true 1193 | }); 1194 | var exists = function exists(v) { 1195 | return typeof v !== 'undefined'; 1196 | }; 1197 | 1198 | exports.exists = exists; 1199 | var round = function round(x) { 1200 | return +(Math.round(x * 100) + 'e-2'); 1201 | }; 1202 | exports.round = round; 1203 | 1204 | /***/ } 1205 | /******/ ]) 1206 | }); 1207 | ; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cropit", 3 | "main": "dist/jquery.cropit.js", 4 | "description": "Customizable crop and zoom.", 5 | "version": "0.5.1", 6 | "author": { 7 | "name": "Scott Cheng", 8 | "email": "me@scottcheng.com", 9 | "url": "http://scottcheng.com" 10 | }, 11 | "keywords": [ 12 | "crop", 13 | "zoom", 14 | "image editor" 15 | ], 16 | "homepage": "http://scottcheng.github.io/cropit", 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/scottcheng/cropit.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/scottcheng/cropit/issues" 23 | }, 24 | "license": "MIT", 25 | "scripts": { 26 | "test": "jest", 27 | "jshint": "jshint src test --reporter=node_modules/jshint-stylish" 28 | }, 29 | "jest": { 30 | "scriptPreprocessor": "/node_modules/babel-jest", 31 | "testFileExtensions": [ 32 | "js" 33 | ], 34 | "moduleFileExtensions": [ 35 | "js", 36 | "json" 37 | ], 38 | "testDirectoryName": "test", 39 | "unmockedModulePathPatterns": [ 40 | "constants", 41 | "zoomer", 42 | "cropit", 43 | "plugin", 44 | "jquery", 45 | "lodash", 46 | "fs" 47 | ] 48 | }, 49 | "dependencies": { 50 | "jquery": ">=1.9" 51 | }, 52 | "devDependencies": { 53 | "babel-core": "~5.5.8", 54 | "babel-jest": "~5.3.0", 55 | "babel-loader": "~5.1.4", 56 | "jest-cli": "^0.8.2", 57 | "jshint-stylish": "~2.0.0", 58 | "lodash": "~3.9.3", 59 | "node-libs-browser": "~0.5.2", 60 | "pica": "^1.0.7", 61 | "webpack": "~1.9.11", 62 | "webpack-dev-server": "~1.9.0" 63 | }, 64 | "engine": "node >= 0.4.1", 65 | "readmeFilename": "README.md" 66 | } 67 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const PLUGIN_KEY = 'cropit'; 2 | 3 | export const CLASS_NAMES = { 4 | PREVIEW: 'cropit-preview', 5 | PREVIEW_IMAGE_CONTAINER: 'cropit-preview-image-container', 6 | PREVIEW_IMAGE: 'cropit-preview-image', 7 | PREVIEW_BACKGROUND_CONTAINER: 'cropit-preview-background-container', 8 | PREVIEW_BACKGROUND: 'cropit-preview-background', 9 | FILE_INPUT: 'cropit-image-input', 10 | ZOOM_SLIDER: 'cropit-image-zoom-input', 11 | 12 | DRAG_HOVERED: 'cropit-drag-hovered', 13 | IMAGE_LOADING: 'cropit-image-loading', 14 | IMAGE_LOADED: 'cropit-image-loaded', 15 | DISABLED: 'cropit-disabled', 16 | }; 17 | 18 | export const ERRORS = { 19 | IMAGE_FAILED_TO_LOAD: { code: 0, message: 'Image failed to load.' }, 20 | SMALL_IMAGE: { code: 1, message: 'Image is too small.' }, 21 | }; 22 | 23 | const eventName = (events) => events.map((e) => `${e}.cropit`).join(' '); 24 | export const EVENTS = { 25 | PREVIEW: eventName([ 26 | 'mousedown', 'mouseup', 'mouseleave', 27 | 'touchstart', 'touchend', 'touchcancel', 'touchleave', 28 | ]), 29 | PREVIEW_MOVE: eventName(['mousemove', 'touchmove']), 30 | ZOOM_INPUT: eventName(['mousemove', 'touchmove', 'change']), 31 | }; 32 | -------------------------------------------------------------------------------- /src/cropit.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | import Zoomer from './Zoomer'; 4 | import { CLASS_NAMES, ERRORS, EVENTS } from './constants'; 5 | import { loadDefaults } from './options'; 6 | import { exists, round } from './utils'; 7 | 8 | class Cropit { 9 | constructor(jQuery, element, options) { 10 | this.$el = $(element); 11 | 12 | const defaults = loadDefaults(this.$el); 13 | this.options = $.extend({}, defaults, options); 14 | 15 | this.init(); 16 | } 17 | 18 | init() { 19 | this.image = new Image(); 20 | this.preImage = new Image(); 21 | this.image.onload = this.onImageLoaded.bind(this); 22 | this.preImage.onload = this.onPreImageLoaded.bind(this); 23 | this.image.onerror = this.preImage.onerror = () => { 24 | this.onImageError.call(this, ERRORS.IMAGE_FAILED_TO_LOAD); 25 | }; 26 | 27 | this.$preview = this.options.$preview.css('position', 'relative'); 28 | this.$fileInput = this.options.$fileInput.attr({ accept: 'image/*' }); 29 | this.$zoomSlider = this.options.$zoomSlider.attr({ min: 0, max: 1, step: 0.01 }); 30 | 31 | this.previewSize = { 32 | width: this.options.width || this.$preview.innerWidth(), 33 | height: this.options.height || this.$preview.innerHeight(), 34 | }; 35 | 36 | this.$image = $('') 37 | .addClass(CLASS_NAMES.PREVIEW_IMAGE) 38 | .attr('alt', '') 39 | .css({ 40 | transformOrigin: 'top left', 41 | webkitTransformOrigin: 'top left', 42 | willChange: 'transform', 43 | }); 44 | this.$imageContainer = $('
') 45 | .addClass(CLASS_NAMES.PREVIEW_IMAGE_CONTAINER) 46 | .css({ 47 | position: 'absolute', 48 | overflow: 'hidden', 49 | left: 0, 50 | top: 0, 51 | width: '100%', 52 | height: '100%', 53 | }) 54 | .append(this.$image); 55 | this.$preview.append(this.$imageContainer); 56 | 57 | if (this.options.imageBackground) { 58 | if ($.isArray(this.options.imageBackgroundBorderWidth)) { 59 | this.bgBorderWidthArray = this.options.imageBackgroundBorderWidth; 60 | } 61 | else { 62 | this.bgBorderWidthArray = [0, 1, 2, 3].map(() => this.options.imageBackgroundBorderWidth); 63 | } 64 | 65 | this.$bg = $('') 66 | .addClass(CLASS_NAMES.PREVIEW_BACKGROUND) 67 | .attr('alt', '') 68 | .css({ 69 | position: 'relative', 70 | left: this.bgBorderWidthArray[3], 71 | top: this.bgBorderWidthArray[0], 72 | transformOrigin: 'top left', 73 | webkitTransformOrigin: 'top left', 74 | willChange: 'transform', 75 | }); 76 | this.$bgContainer = $('
') 77 | .addClass(CLASS_NAMES.PREVIEW_BACKGROUND_CONTAINER) 78 | .css({ 79 | position: 'absolute', 80 | zIndex: 0, 81 | top: -this.bgBorderWidthArray[0], 82 | right: -this.bgBorderWidthArray[1], 83 | bottom: -this.bgBorderWidthArray[2], 84 | left: -this.bgBorderWidthArray[3], 85 | }) 86 | .append(this.$bg); 87 | if (this.bgBorderWidthArray[0] > 0) { 88 | this.$bgContainer.css('overflow', 'hidden'); 89 | } 90 | this.$preview.prepend(this.$bgContainer); 91 | } 92 | 93 | this.initialZoom = this.options.initialZoom; 94 | 95 | this.imageLoaded = false; 96 | 97 | this.moveContinue = false; 98 | 99 | this.zoomer = new Zoomer(); 100 | 101 | if (this.options.allowDragNDrop) { 102 | $.event.props.push('dataTransfer'); 103 | } 104 | 105 | this.bindListeners(); 106 | 107 | if (this.options.imageState && this.options.imageState.src) { 108 | this.loadImage(this.options.imageState.src); 109 | } 110 | } 111 | 112 | bindListeners() { 113 | this.$fileInput.on('change.cropit', this.onFileChange.bind(this)); 114 | this.$imageContainer.on(EVENTS.PREVIEW, this.onPreviewEvent.bind(this)); 115 | this.$zoomSlider.on(EVENTS.ZOOM_INPUT, this.onZoomSliderChange.bind(this)); 116 | 117 | if (this.options.allowDragNDrop) { 118 | this.$imageContainer.on('dragover.cropit dragleave.cropit', this.onDragOver.bind(this)); 119 | this.$imageContainer.on('drop.cropit', this.onDrop.bind(this)); 120 | } 121 | } 122 | 123 | unbindListeners() { 124 | this.$fileInput.off('change.cropit'); 125 | this.$imageContainer.off(EVENTS.PREVIEW); 126 | this.$imageContainer.off('dragover.cropit dragleave.cropit drop.cropit'); 127 | this.$zoomSlider.off(EVENTS.ZOOM_INPUT); 128 | } 129 | 130 | onFileChange(e) { 131 | this.options.onFileChange(e); 132 | 133 | if (this.$fileInput.get(0).files) { 134 | this.loadFile(this.$fileInput.get(0).files[0]); 135 | } 136 | } 137 | 138 | loadFile(file) { 139 | const fileReader = new FileReader(); 140 | if (file && file.type.match('image')) { 141 | fileReader.readAsDataURL(file); 142 | fileReader.onload = this.onFileReaderLoaded.bind(this); 143 | fileReader.onerror = this.onFileReaderError.bind(this); 144 | } 145 | else if (file) { 146 | this.onFileReaderError(); 147 | } 148 | } 149 | 150 | onFileReaderLoaded(e) { 151 | this.loadImage(e.target.result); 152 | } 153 | 154 | onFileReaderError() { 155 | this.options.onFileReaderError(); 156 | } 157 | 158 | onDragOver(e) { 159 | e.preventDefault(); 160 | e.dataTransfer.dropEffect = 'copy'; 161 | this.$preview.toggleClass(CLASS_NAMES.DRAG_HOVERED, e.type === 'dragover'); 162 | } 163 | 164 | onDrop(e) { 165 | e.preventDefault(); 166 | e.stopPropagation(); 167 | 168 | const files = Array.prototype.slice.call(e.dataTransfer.files, 0); 169 | files.some((file) => { 170 | if (!file.type.match('image')) { return false; } 171 | 172 | this.loadFile(file); 173 | return true; 174 | }); 175 | 176 | this.$preview.removeClass(CLASS_NAMES.DRAG_HOVERED); 177 | } 178 | 179 | loadImage(imageSrc) { 180 | if (!imageSrc) { return; } 181 | 182 | this.options.onImageLoading(); 183 | this.setImageLoadingClass(); 184 | 185 | if (imageSrc.indexOf('data') === 0) { 186 | this.preImage.src = imageSrc; 187 | } 188 | else { 189 | const xhr = new XMLHttpRequest(); 190 | xhr.onload = (e) => { 191 | if (e.target.status >= 300) { 192 | this.onImageError.call(this, ERRORS.IMAGE_FAILED_TO_LOAD); 193 | return; 194 | } 195 | 196 | this.loadFile(e.target.response); 197 | }; 198 | xhr.open('GET', imageSrc); 199 | xhr.responseType = 'blob'; 200 | xhr.send(); 201 | } 202 | } 203 | 204 | onPreImageLoaded() { 205 | if (this.shouldRejectImage({ 206 | imageWidth: this.preImage.width, 207 | imageHeight: this.preImage.height, 208 | previewSize: this.previewSize, 209 | maxZoom: this.options.maxZoom, 210 | exportZoom: this.options.exportZoom, 211 | smallImage: this.options.smallImage, 212 | })) { 213 | this.onImageError(ERRORS.SMALL_IMAGE); 214 | if (this.image.src) { this.setImageLoadedClass(); } 215 | return; 216 | } 217 | 218 | this.image.src = this.preImage.src; 219 | } 220 | 221 | onImageLoaded() { 222 | this.rotation = 0; 223 | this.setupZoomer(this.options.imageState && this.options.imageState.zoom || this._initialZoom); 224 | if (this.options.imageState && this.options.imageState.offset) { 225 | this.offset = this.options.imageState.offset; 226 | } 227 | else { 228 | this.centerImage(); 229 | } 230 | 231 | this.options.imageState = {}; 232 | 233 | this.$image.attr('src', this.image.src); 234 | if (this.options.imageBackground) { 235 | this.$bg.attr('src', this.image.src); 236 | } 237 | 238 | this.setImageLoadedClass(); 239 | 240 | this.imageLoaded = true; 241 | 242 | this.options.onImageLoaded(); 243 | } 244 | 245 | onImageError() { 246 | this.options.onImageError.apply(this, arguments); 247 | this.removeImageLoadingClass(); 248 | } 249 | 250 | setImageLoadingClass() { 251 | this.$preview 252 | .removeClass(CLASS_NAMES.IMAGE_LOADED) 253 | .addClass(CLASS_NAMES.IMAGE_LOADING); 254 | } 255 | 256 | setImageLoadedClass() { 257 | this.$preview 258 | .removeClass(CLASS_NAMES.IMAGE_LOADING) 259 | .addClass(CLASS_NAMES.IMAGE_LOADED); 260 | } 261 | 262 | removeImageLoadingClass() { 263 | this.$preview 264 | .removeClass(CLASS_NAMES.IMAGE_LOADING); 265 | } 266 | 267 | getEventPosition(e) { 268 | if (e.originalEvent && e.originalEvent.touches && e.originalEvent.touches[0]) { 269 | e = e.originalEvent.touches[0]; 270 | } 271 | if (e.clientX && e.clientY) { 272 | return { x: e.clientX, y: e.clientY }; 273 | } 274 | } 275 | 276 | onPreviewEvent(e) { 277 | if (!this.imageLoaded) { return; } 278 | 279 | this.moveContinue = false; 280 | this.$imageContainer.off(EVENTS.PREVIEW_MOVE); 281 | 282 | if (e.type === 'mousedown' || e.type === 'touchstart') { 283 | this.origin = this.getEventPosition(e); 284 | this.moveContinue = true; 285 | this.$imageContainer.on(EVENTS.PREVIEW_MOVE, this.onMove.bind(this)); 286 | } 287 | else { 288 | $(document.body).focus(); 289 | } 290 | 291 | e.stopPropagation(); 292 | return false; 293 | } 294 | 295 | onMove(e) { 296 | const eventPosition = this.getEventPosition(e); 297 | 298 | if (this.moveContinue && eventPosition) { 299 | this.offset = { 300 | x: this.offset.x + eventPosition.x - this.origin.x, 301 | y: this.offset.y + eventPosition.y - this.origin.y, 302 | }; 303 | } 304 | 305 | this.origin = eventPosition; 306 | 307 | e.stopPropagation(); 308 | return false; 309 | } 310 | 311 | set offset(position) { 312 | if (!position || !exists(position.x) || !exists(position.y)) { return; } 313 | 314 | this._offset = this.fixOffset(position); 315 | this.renderImage(); 316 | 317 | this.options.onOffsetChange(position); 318 | } 319 | 320 | fixOffset(offset) { 321 | if (!this.imageLoaded) { return offset; } 322 | 323 | const ret = { x: offset.x, y: offset.y }; 324 | 325 | if (!this.options.freeMove) { 326 | if (this.imageWidth * this.zoom >= this.previewSize.width) { 327 | ret.x = Math.min(0, Math.max(ret.x, 328 | this.previewSize.width - this.imageWidth * this.zoom)); 329 | } 330 | else { 331 | ret.x = Math.max(0, Math.min(ret.x, 332 | this.previewSize.width - this.imageWidth * this.zoom)); 333 | } 334 | 335 | if (this.imageHeight * this.zoom >= this.previewSize.height) { 336 | ret.y = Math.min(0, Math.max(ret.y, 337 | this.previewSize.height - this.imageHeight * this.zoom)); 338 | } 339 | else { 340 | ret.y = Math.max(0, Math.min(ret.y, 341 | this.previewSize.height - this.imageHeight * this.zoom)); 342 | } 343 | } 344 | 345 | ret.x = round(ret.x); 346 | ret.y = round(ret.y); 347 | 348 | return ret; 349 | } 350 | 351 | centerImage() { 352 | if (!this.image.width || !this.image.height || !this.zoom) { return; } 353 | 354 | this.offset = { 355 | x: (this.previewSize.width - this.imageWidth * this.zoom) / 2, 356 | y: (this.previewSize.height - this.imageHeight * this.zoom) / 2, 357 | }; 358 | } 359 | 360 | onZoomSliderChange() { 361 | if (!this.imageLoaded) { return; } 362 | 363 | this.zoomSliderPos = Number(this.$zoomSlider.val()); 364 | const newZoom = this.zoomer.getZoom(this.zoomSliderPos); 365 | if (newZoom === this.zoom) { return; } 366 | this.zoom = newZoom; 367 | } 368 | 369 | enableZoomSlider() { 370 | this.$zoomSlider.removeAttr('disabled'); 371 | this.options.onZoomEnabled(); 372 | } 373 | 374 | disableZoomSlider() { 375 | this.$zoomSlider.attr('disabled', true); 376 | this.options.onZoomDisabled(); 377 | } 378 | 379 | setupZoomer(zoom) { 380 | this.zoomer.setup({ 381 | imageSize: this.imageSize, 382 | previewSize: this.previewSize, 383 | exportZoom: this.options.exportZoom, 384 | maxZoom: this.options.maxZoom, 385 | minZoom: this.options.minZoom, 386 | smallImage: this.options.smallImage, 387 | }); 388 | this.zoom = exists(zoom) ? zoom : this._zoom; 389 | 390 | if (this.isZoomable()) { 391 | this.enableZoomSlider(); 392 | } 393 | else { 394 | this.disableZoomSlider(); 395 | } 396 | } 397 | 398 | set zoom(newZoom) { 399 | newZoom = this.fixZoom(newZoom); 400 | 401 | if (this.imageLoaded) { 402 | const oldZoom = this.zoom; 403 | 404 | const newX = this.previewSize.width / 2 - (this.previewSize.width / 2 - this.offset.x) * newZoom / oldZoom; 405 | const newY = this.previewSize.height / 2 - (this.previewSize.height / 2 - this.offset.y) * newZoom / oldZoom; 406 | 407 | this._zoom = newZoom; 408 | this.offset = { x: newX, y: newY }; // Triggers renderImage() 409 | } 410 | else { 411 | this._zoom = newZoom; 412 | } 413 | 414 | this.zoomSliderPos = this.zoomer.getSliderPos(this.zoom); 415 | this.$zoomSlider.val(this.zoomSliderPos); 416 | 417 | this.options.onZoomChange(newZoom); 418 | } 419 | 420 | fixZoom(zoom) { 421 | return this.zoomer.fixZoom(zoom); 422 | } 423 | 424 | isZoomable() { 425 | return this.zoomer.isZoomable(); 426 | } 427 | 428 | get rotatedOffset() { 429 | return { 430 | x: this.offset.x + 431 | (this.rotation === 90 ? this.image.height * this.zoom : 0) + 432 | (this.rotation === 180 ? this.image.width * this.zoom : 0), 433 | y: this.offset.y + 434 | (this.rotation === 180 ? this.image.height * this.zoom : 0) + 435 | (this.rotation === 270 ? this.image.width * this.zoom : 0), 436 | }; 437 | } 438 | 439 | renderImage() { 440 | const transformation = ` 441 | translate(${this.rotatedOffset.x}px, ${this.rotatedOffset.y}px) 442 | scale(${this.zoom}) 443 | rotate(${this.rotation}deg)`; 444 | 445 | this.$image.css({ 446 | transform: transformation, 447 | webkitTransform: transformation, 448 | }); 449 | if (this.options.imageBackground) { 450 | this.$bg.css({ 451 | transform: transformation, 452 | webkitTransform: transformation, 453 | }); 454 | } 455 | } 456 | 457 | set rotation(newRotation) { 458 | this._rotation = newRotation; 459 | 460 | if (this.imageLoaded) { 461 | // Change in image size may lead to change in zoom range 462 | this.setupZoomer(); 463 | } 464 | } 465 | 466 | get rotation() { 467 | return this._rotation; 468 | } 469 | 470 | rotateCW() { 471 | if (this.shouldRejectImage({ 472 | imageWidth: this.image.height, 473 | imageHeight: this.image.width, 474 | previewSize: this.previewSize, 475 | maxZoom: this.options.maxZoom, 476 | exportZoom: this.options.exportZoom, 477 | smallImage: this.options.smallImage, 478 | })) { 479 | this.rotation = (this.rotation + 180) % 360; 480 | } 481 | else { 482 | this.rotation = (this.rotation + 90) % 360; 483 | } 484 | } 485 | 486 | rotateCCW() { 487 | if (this.shouldRejectImage({ 488 | imageWidth: this.image.height, 489 | imageHeight: this.image.width, 490 | previewSize: this.previewSize, 491 | maxZoom: this.options.maxZoom, 492 | exportZoom: this.options.exportZoom, 493 | smallImage: this.options.smallImage, 494 | })) { 495 | this.rotation = (this.rotation + 180) % 360; 496 | } 497 | else { 498 | this.rotation = (this.rotation + 270) % 360; 499 | } 500 | } 501 | 502 | shouldRejectImage({ imageWidth, imageHeight, previewSize, maxZoom, exportZoom, smallImage }) { 503 | if (smallImage !== 'reject') { return false; } 504 | 505 | return imageWidth * maxZoom < previewSize.width * exportZoom || 506 | imageHeight * maxZoom < previewSize.height * exportZoom; 507 | } 508 | 509 | getCroppedImageData(exportOptions) { 510 | if (!this.image.src) { return; } 511 | 512 | const exportDefaults = { 513 | type: 'image/png', 514 | quality: 0.75, 515 | originalSize: false, 516 | fillBg: '#fff', 517 | }; 518 | exportOptions = $.extend({}, exportDefaults, exportOptions); 519 | 520 | const exportZoom = exportOptions.originalSize ? 1 / this.zoom : this.options.exportZoom; 521 | 522 | const zoomedSize = { 523 | width: this.zoom * exportZoom * this.image.width, 524 | height: this.zoom * exportZoom * this.image.height, 525 | }; 526 | 527 | const canvas = $('') 528 | .attr({ 529 | width: this.previewSize.width * exportZoom, 530 | height: this.previewSize.height * exportZoom, 531 | }) 532 | .get(0); 533 | const canvasContext = canvas.getContext('2d'); 534 | 535 | if (exportOptions.type === 'image/jpeg') { 536 | canvasContext.fillStyle = exportOptions.fillBg; 537 | canvasContext.fillRect(0, 0, canvas.width, canvas.height); 538 | } 539 | 540 | canvasContext.translate( 541 | this.rotatedOffset.x * exportZoom, 542 | this.rotatedOffset.y * exportZoom); 543 | canvasContext.rotate(this.rotation * Math.PI / 180); 544 | canvasContext.drawImage(this.image, 545 | 0, 0, 546 | zoomedSize.width, 547 | zoomedSize.height); 548 | 549 | return canvas.toDataURL(exportOptions.type, exportOptions.quality); 550 | } 551 | 552 | get imageState() { 553 | return { 554 | src: this.image.src, 555 | offset: this.offset, 556 | zoom: this.zoom, 557 | }; 558 | } 559 | 560 | get imageSrc() { 561 | return this.image.src; 562 | } 563 | 564 | set imageSrc(imageSrc) { 565 | this.loadImage(imageSrc); 566 | } 567 | 568 | get offset() { 569 | return this._offset; 570 | } 571 | 572 | get zoom() { 573 | return this._zoom; 574 | } 575 | 576 | get imageWidth() { 577 | return this.rotation % 180 === 0 ? this.image.width : this.image.height; 578 | } 579 | 580 | get imageHeight() { 581 | return this.rotation % 180 === 0 ? this.image.height : this.image.width; 582 | } 583 | 584 | get imageSize() { 585 | return { 586 | width: this.imageWidth, 587 | height: this.imageHeight, 588 | }; 589 | } 590 | 591 | get initialZoom() { 592 | return this.options.initialZoom; 593 | } 594 | 595 | set initialZoom(initialZoomOption) { 596 | this.options.initialZoom = initialZoomOption; 597 | if (initialZoomOption === 'min') { 598 | this._initialZoom = 0; // Will be fixed when image loads 599 | } 600 | else if (initialZoomOption === 'image') { 601 | this._initialZoom = 1; 602 | } 603 | else { 604 | this._initialZoom = 0; 605 | } 606 | } 607 | 608 | get exportZoom() { 609 | return this.options.exportZoom; 610 | } 611 | 612 | set exportZoom(exportZoom) { 613 | this.options.exportZoom = exportZoom; 614 | this.setupZoomer(); 615 | } 616 | 617 | get minZoom() { 618 | return this.options.minZoom; 619 | } 620 | 621 | set minZoom(minZoom) { 622 | this.options.minZoom = minZoom; 623 | this.setupZoomer(); 624 | } 625 | 626 | get maxZoom() { 627 | return this.options.maxZoom; 628 | } 629 | 630 | set maxZoom(maxZoom) { 631 | this.options.maxZoom = maxZoom; 632 | this.setupZoomer(); 633 | } 634 | 635 | get previewSize() { 636 | return this._previewSize; 637 | } 638 | 639 | set previewSize(size) { 640 | if (!size || size.width <= 0 || size.height <= 0) { return; } 641 | 642 | this._previewSize = { 643 | width: size.width, 644 | height: size.height, 645 | }; 646 | this.$preview 647 | .innerWidth(this.previewSize.width) 648 | .innerHeight(this.previewSize.height); 649 | 650 | if (this.imageLoaded) { 651 | this.setupZoomer(); 652 | } 653 | } 654 | 655 | disable() { 656 | this.unbindListeners(); 657 | this.disableZoomSlider(); 658 | this.$el.addClass(CLASS_NAMES.DISABLED); 659 | } 660 | 661 | reenable() { 662 | this.bindListeners(); 663 | this.enableZoomSlider(); 664 | this.$el.removeClass(CLASS_NAMES.DISABLED); 665 | } 666 | 667 | $(selector) { 668 | if (!this.$el) { return null; } 669 | return this.$el.find(selector); 670 | } 671 | } 672 | 673 | export default Cropit; 674 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | import { CLASS_NAMES } from './constants'; 2 | 3 | const options = { 4 | elements: [ 5 | { 6 | name: '$preview', 7 | description: 'The HTML element that displays image preview.', 8 | defaultSelector: `.${CLASS_NAMES.PREVIEW}`, 9 | }, 10 | { 11 | name: '$fileInput', 12 | description: 'File input element.', 13 | defaultSelector: `input.${CLASS_NAMES.FILE_INPUT}`, 14 | }, 15 | { 16 | name: '$zoomSlider', 17 | description: 'Range input element that controls image zoom.', 18 | defaultSelector: `input.${CLASS_NAMES.ZOOM_SLIDER}`, 19 | }, 20 | ].map((o) => { 21 | o.type = 'jQuery element'; 22 | o.default = `$imageCropper.find('${o.defaultSelector}')`; 23 | return o; 24 | }), 25 | 26 | values: [ 27 | { 28 | name: 'width', 29 | type: 'number', 30 | description: 'Width of image preview in pixels. If set, it will override the CSS property.', 31 | default: null, 32 | }, 33 | { 34 | name: 'height', 35 | type: 'number', 36 | description: 'Height of image preview in pixels. If set, it will override the CSS property.', 37 | default: null, 38 | }, 39 | { 40 | name: 'imageBackground', 41 | type: 'boolean', 42 | description: 'Whether or not to display the background image beyond the preview area.', 43 | default: false, 44 | }, 45 | { 46 | name: 'imageBackgroundBorderWidth', 47 | type: 'array or number', 48 | description: `Width of background image border in pixels. 49 | The four array elements specify the width of background image width on the top, right, bottom, left side respectively. 50 | The background image beyond the width will be hidden. 51 | If specified as a number, border with uniform width on all sides will be applied.`, 52 | default: [0, 0, 0, 0], 53 | }, 54 | { 55 | name: 'exportZoom', 56 | type: 'number', 57 | description: `The ratio between the desired image size to export and the preview size. 58 | For example, if the preview size is \`300px * 200px\`, and \`exportZoom = 2\`, then 59 | the exported image size will be \`600px * 400px\`. 60 | This also affects the maximum zoom level, since the exported image cannot be zoomed to larger than its original size.`, 61 | default: 1, 62 | }, 63 | { 64 | name: 'allowDragNDrop', 65 | type: 'boolean', 66 | description: 'When set to true, you can load an image by dragging it from local file browser onto the preview area.', 67 | default: true, 68 | }, 69 | { 70 | name: 'minZoom', 71 | type: 'string', 72 | description: `This options decides the minimal zoom level of the image. 73 | If set to \`'fill'\`, the image has to fill the preview area, i.e. both width and height must not go smaller than the preview area. 74 | If set to \`'fit'\`, the image can shrink further to fit the preview area, i.e. at least one of its edges must not go smaller than the preview area.`, 75 | default: 'fill', 76 | }, 77 | { 78 | name: 'maxZoom', 79 | type: 'number', 80 | description: 'Determines how big the image can be zoomed. E.g. if set to 1.5, the image can be zoomed to 150% of its original size.', 81 | default: 1, 82 | }, 83 | { 84 | name: 'initialZoom', 85 | type: 'string', 86 | description: `Determines the zoom when an image is loaded. 87 | When set to \`'min'\`, image is zoomed to the smallest when loaded. 88 | When set to \`'image'\`, image is zoomed to 100% when loaded.`, 89 | default: 'min', 90 | }, 91 | { 92 | name: 'freeMove', 93 | type: 'boolean', 94 | description: 'When set to true, you can freely move the image instead of being bound to the container borders', 95 | default: false, 96 | }, 97 | { 98 | name: 'smallImage', 99 | type: 'string', 100 | description: `When set to \`'reject'\`, \`onImageError\` would be called when cropit loads an image that is smaller than the container. 101 | When set to \`'allow'\`, images smaller than the container can be zoomed down to its original size, overiding \`minZoom\` option. 102 | When set to \`'stretch'\`, the minimum zoom of small images would follow \`minZoom\` option.`, 103 | default: 'reject', 104 | }, 105 | ], 106 | 107 | callbacks: [ 108 | { 109 | name: 'onFileChange', 110 | description: 'Called when user selects a file in the select file input.', 111 | params: [ 112 | { 113 | name: 'event', 114 | type: 'object', 115 | description: 'File change event object', 116 | }, 117 | ], 118 | }, 119 | { 120 | name: 'onFileReaderError', 121 | description: 'Called when `FileReader` encounters an error while loading the image file.', 122 | }, 123 | { 124 | name: 'onImageLoading', 125 | description: 'Called when image starts to be loaded.', 126 | }, 127 | { 128 | name: 'onImageLoaded', 129 | description: 'Called when image is loaded.', 130 | }, 131 | { 132 | name: 'onImageError', 133 | description: 'Called when image cannot be loaded.', 134 | params: [ 135 | { 136 | name: 'error', 137 | type: 'object', 138 | description: 'Error object.', 139 | }, 140 | { 141 | name: 'error.code', 142 | type: 'number', 143 | description: 'Error code. `0` means generic image loading failure. `1` means image is too small.', 144 | }, 145 | { 146 | name: 'error.message', 147 | type: 'string', 148 | description: 'A message explaining the error.', 149 | }, 150 | ], 151 | }, 152 | { 153 | name: 'onZoomEnabled', 154 | description: 'Called when image the zoom slider is enabled.', 155 | }, 156 | { 157 | name: 'onZoomDisabled', 158 | description: 'Called when image the zoom slider is disabled.', 159 | }, 160 | { 161 | name: 'onZoomChange', 162 | description: 'Called when zoom changes.', 163 | params: [ 164 | { 165 | name: 'zoom', 166 | type: 'number', 167 | description: 'New zoom.', 168 | }, 169 | ], 170 | }, 171 | { 172 | name: 'onOffsetChange', 173 | description: 'Called when image offset changes.', 174 | params: [ 175 | { 176 | name: 'offset', 177 | type: 'object', 178 | description: 'New offset, with `x` and `y` values.', 179 | }, 180 | ], 181 | }, 182 | ].map((o) => { o.type = 'function'; return o; }), 183 | }; 184 | 185 | export const loadDefaults = ($el) => { 186 | const defaults = {}; 187 | if ($el) { 188 | options.elements.forEach((o) => { 189 | defaults[o.name] = $el.find(o.defaultSelector); 190 | }); 191 | } 192 | options.values.forEach((o) => { 193 | defaults[o.name] = o.default; 194 | }); 195 | options.callbacks.forEach((o) => { 196 | defaults[o.name] = () => {}; 197 | }); 198 | 199 | return defaults; 200 | }; 201 | 202 | export default options; 203 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | import Cropit from './cropit'; 4 | import { PLUGIN_KEY } from './constants'; 5 | import { exists } from './utils'; 6 | 7 | const applyOnEach = ($el, callback) => { 8 | return $el.each(function() { 9 | const cropit = $.data(this, PLUGIN_KEY); 10 | 11 | if (!cropit) { return; } 12 | callback(cropit); 13 | }); 14 | }; 15 | 16 | const callOnFirst = ($el, method, options) => { 17 | const cropit = $el.first().data(PLUGIN_KEY); 18 | 19 | if (!cropit || !$.isFunction(cropit[method])) { return null; } 20 | return cropit[method](options); 21 | }; 22 | 23 | const methods = { 24 | init(options) { 25 | return this.each(function() { 26 | // Only instantiate once per element 27 | if ($.data(this, PLUGIN_KEY)) { return; } 28 | 29 | const cropit = new Cropit($, this, options); 30 | $.data(this, PLUGIN_KEY, cropit); 31 | }); 32 | }, 33 | 34 | destroy() { 35 | return this.each(function() { 36 | $.removeData(this, PLUGIN_KEY); 37 | }); 38 | }, 39 | 40 | isZoomable() { 41 | return callOnFirst(this, 'isZoomable'); 42 | }, 43 | 44 | export(options) { 45 | return callOnFirst(this, 'getCroppedImageData', options); 46 | }, 47 | }; 48 | 49 | const delegate = ($el, fnName) => { 50 | return applyOnEach($el, (cropit) => { 51 | cropit[fnName](); 52 | }); 53 | }; 54 | 55 | const prop = ($el, name, value) => { 56 | if (exists(value)) { 57 | return applyOnEach($el, (cropit) => { 58 | cropit[name] = value; 59 | }); 60 | } 61 | else { 62 | const cropit = $el.first().data(PLUGIN_KEY); 63 | return cropit[name]; 64 | } 65 | }; 66 | 67 | $.fn.cropit = function(method) { 68 | if (methods[method]) { 69 | return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 70 | } 71 | else if (['imageState', 'imageSrc', 'offset', 'previewSize', 'imageSize', 'zoom', 72 | 'initialZoom', 'exportZoom', 'minZoom', 'maxZoom'].indexOf(method) >= 0) { 73 | return prop(this, ...arguments); 74 | } 75 | else if (['rotateCW', 'rotateCCW', 'disable', 'reenable'].indexOf(method) >= 0) { 76 | return delegate(this, ...arguments); 77 | } 78 | else { 79 | return methods.init.apply(this, arguments); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const exists = (v) => typeof v !== 'undefined'; 2 | 3 | export const round = (x) => +(Math.round(x * 1e2) + 'e-2'); 4 | -------------------------------------------------------------------------------- /src/zoomer.js: -------------------------------------------------------------------------------- 1 | class Zoomer { 2 | constructor() { 3 | this.minZoom = this.maxZoom = 1; 4 | } 5 | 6 | setup({ imageSize, previewSize, exportZoom, maxZoom, minZoom, smallImage }) { 7 | const widthRatio = previewSize.width / imageSize.width; 8 | const heightRatio = previewSize.height / imageSize.height; 9 | 10 | if (minZoom === 'fit') { 11 | this.minZoom = Math.min(widthRatio, heightRatio); 12 | } 13 | else { 14 | this.minZoom = Math.max(widthRatio, heightRatio); 15 | } 16 | 17 | if (smallImage === 'allow') { 18 | this.minZoom = Math.min(this.minZoom, 1); 19 | } 20 | 21 | this.maxZoom = Math.max(this.minZoom, maxZoom / exportZoom); 22 | } 23 | 24 | getZoom(sliderPos) { 25 | if (!this.minZoom || !this.maxZoom) { return null; } 26 | 27 | return sliderPos * (this.maxZoom - this.minZoom) + this.minZoom; 28 | } 29 | 30 | getSliderPos(zoom) { 31 | if (!this.minZoom || !this.maxZoom) { return null; } 32 | 33 | if (this.minZoom === this.maxZoom) { 34 | return 0; 35 | } 36 | else { 37 | return (zoom - this.minZoom) / (this.maxZoom - this.minZoom); 38 | } 39 | } 40 | 41 | isZoomable() { 42 | if (!this.minZoom || !this.maxZoom) { return null; } 43 | 44 | return this.minZoom !== this.maxZoom; 45 | } 46 | 47 | fixZoom(zoom) { 48 | return Math.max(this.minZoom, Math.min(this.maxZoom, zoom)); 49 | } 50 | } 51 | 52 | export default Zoomer; 53 | -------------------------------------------------------------------------------- /test/cropit.spec.js: -------------------------------------------------------------------------------- 1 | jest 2 | .dontMock('jquery') 3 | .dontMock('../src/constants') 4 | .dontMock('../src/options') 5 | .dontMock('../src/cropit'); 6 | 7 | import $ from 'jquery'; 8 | import { ERRORS } from '../src/constants'; 9 | import options from '../src/options'; 10 | import Cropit from '../src/cropit'; 11 | 12 | const IMAGE_URL = 'http://example.com/image.jpg'; 13 | const IMAGE_DATA = 'data:image/png;base64,image-data...'; 14 | 15 | const newCropit = (options) => { 16 | return new Cropit($, null, options); 17 | }; 18 | 19 | describe('Cropit', () => { 20 | let cropit = null; 21 | 22 | it('sets default options', () => { 23 | cropit = newCropit(); 24 | 25 | options.values.forEach((o) => { 26 | expect(cropit.options[o.name]).toBe(o.default); 27 | }); 28 | }); 29 | 30 | describe('#init', () => { 31 | it('calls loadImage if image source is present', () => { 32 | cropit = newCropit({ imageState: { src: IMAGE_URL } }); 33 | spyOn(cropit, 'loadImage'); 34 | 35 | cropit.init(); 36 | expect(cropit.loadImage).toHaveBeenCalled(); 37 | }); 38 | }); 39 | 40 | describe('#onFileReaderLoaded', () => { 41 | beforeEach(() => { 42 | cropit = newCropit(); 43 | }); 44 | 45 | it('calls loadImage', () => { 46 | spyOn(cropit, 'loadImage'); 47 | 48 | cropit.onFileReaderLoaded({ target: { result: IMAGE_DATA } }); 49 | expect(cropit.loadImage).toHaveBeenCalled(); 50 | }); 51 | }); 52 | 53 | describe('#loadImage', () => { 54 | beforeEach(() => { 55 | cropit = newCropit(); 56 | }); 57 | 58 | it('sets test image source', () => { 59 | expect(cropit.preImage.src).not.toBe(IMAGE_DATA); 60 | 61 | cropit.loadImage(IMAGE_DATA); 62 | expect(cropit.preImage.src).toBe(IMAGE_DATA); 63 | }); 64 | }); 65 | 66 | describe('#onPreImageLoaded', () => { 67 | describe('reject small images', () => { 68 | beforeEach(() => { 69 | cropit = newCropit({ width: 2, height: 2, smallImage: 'reject' }); 70 | }); 71 | 72 | it('rejects image where image width is smaller than preview width', () => { 73 | spyOn(cropit, 'onImageError'); 74 | cropit.preImage = { width: 1, height: 3 }; 75 | cropit.onPreImageLoaded(); 76 | expect(cropit.onImageError).toHaveBeenCalled(); 77 | }); 78 | 79 | it('rejects image where image height is smaller than preview height', () => { 80 | spyOn(cropit, 'onImageError'); 81 | cropit.preImage = { width: 3, height: 1 }; 82 | cropit.onPreImageLoaded(); 83 | expect(cropit.onImageError).toHaveBeenCalledWith(ERRORS.SMALL_IMAGE); 84 | }); 85 | 86 | it('does not reject image if it is larger than preview', () => { 87 | spyOn(cropit, 'onImageError'); 88 | cropit.preImage = { width: 3, height: 3 }; 89 | cropit.onPreImageLoaded(); 90 | expect(cropit.onImageError).not.toHaveBeenCalledWith(ERRORS.SMALL_IMAGE); 91 | }); 92 | }); 93 | 94 | describe('reject small images and exportZoom is not 1', () => { 95 | beforeEach(() => { 96 | cropit = newCropit({ width: 2, height: 2, smallImage: 'reject', exportZoom: 2 }); 97 | }); 98 | 99 | it('rejects image if image is smaller than preview after applying exportZoom', () => { 100 | spyOn(cropit, 'onImageError'); 101 | cropit.preImage = { width: 3, height: 3 }; 102 | cropit.onPreImageLoaded(); 103 | expect(cropit.onImageError).toHaveBeenCalledWith(ERRORS.SMALL_IMAGE); 104 | }); 105 | }); 106 | 107 | describe('reject small images and maxZoom is not 1', () => { 108 | beforeEach(() => { 109 | cropit = newCropit({ width: 4, height: 4, smallImage: 'reject', maxZoom: 2 }); 110 | }); 111 | 112 | it('does not reject image if maxZoom allows image to be zoomed beyond preview', () => { 113 | spyOn(cropit, 'onImageError'); 114 | cropit.preImage = { width: 3, height: 3 }; 115 | cropit.onPreImageLoaded(); 116 | expect(cropit.onImageError).not.toHaveBeenCalledWith(ERRORS.SMALL_IMAGE); 117 | }); 118 | }); 119 | 120 | describe('allow small images', () => { 121 | beforeEach(() => { 122 | cropit = newCropit({ width: 2, height: 2, smallImage: 'allow' }); 123 | }); 124 | 125 | it('does not reject small image', () => { 126 | spyOn(cropit, 'onImageError'); 127 | cropit.preImage = { width: 1, height: 1 }; 128 | cropit.onPreImageLoaded(); 129 | expect(cropit.onImageError).not.toHaveBeenCalled(); 130 | }); 131 | }); 132 | 133 | it('sets image.src if everything passes', () => { 134 | cropit = newCropit({ width: 1, height: 1 }); 135 | cropit.preImage = { src: IMAGE_DATA }; 136 | expect(cropit.image.src).not.toBe(IMAGE_DATA); 137 | 138 | cropit.onPreImageLoaded(); 139 | expect(cropit.image.src).toBe(IMAGE_DATA); 140 | }); 141 | }); 142 | 143 | describe('#onImageLoaded', () => { 144 | it('centers image', () => { 145 | cropit = newCropit({ width: 1, height: 1 }); 146 | spyOn(cropit, 'centerImage'); 147 | cropit.onImageLoaded(); 148 | expect(cropit.centerImage).toHaveBeenCalled(); 149 | }); 150 | 151 | it('sets zoom to 1 if initialZoom is image', () => { 152 | cropit = newCropit({ width: 1, height: 1, initialZoom: 'image' }); 153 | expect(cropit.zoom).not.toBe(1); 154 | 155 | cropit.image = { width: 2, height: 2 }; 156 | cropit.onImageLoaded(); 157 | expect(cropit.zoom).toBe(1); 158 | }); 159 | }); 160 | 161 | describe('#onPreviewEvent', () => { 162 | describe('mouse event', () => { 163 | const previewEvent = { 164 | type: 'mousedown', 165 | clientX: 1, 166 | clientY: 1, 167 | stopPropagation: () => {}, 168 | }; 169 | 170 | beforeEach(() => { 171 | cropit = newCropit(); 172 | }); 173 | 174 | it('sets origin coordinates on mousedown', () => { 175 | expect(cropit.origin).not.toEqual({ x: 1, y: 1 }); 176 | 177 | cropit.imageLoaded = true; 178 | cropit.onPreviewEvent(previewEvent); 179 | expect(cropit.origin).toEqual({ x: 1, y: 1 }); 180 | }); 181 | 182 | it('calls stopPropagation', () => { 183 | spyOn(previewEvent, 'stopPropagation'); 184 | cropit.imageLoaded = true; 185 | cropit.onPreviewEvent(previewEvent); 186 | expect(previewEvent.stopPropagation).toHaveBeenCalled(); 187 | }); 188 | 189 | it('does nothing before loading image', () => { 190 | spyOn(previewEvent, 'stopPropagation'); 191 | cropit.onPreviewEvent(previewEvent); 192 | expect(cropit.origin).not.toEqual({ x: 1, y: 1 }); 193 | expect(previewEvent.stopPropagation).not.toHaveBeenCalled(); 194 | }); 195 | }); 196 | 197 | describe('touch event', () => { 198 | const previewEvent = { 199 | type: 'touchstart', 200 | originalEvent: { touches: [{ clientX: 1, clientY: 1 }] }, 201 | stopPropagation: () => {}, 202 | }; 203 | 204 | beforeEach(() => { 205 | cropit = newCropit(); 206 | }); 207 | 208 | it('sets origin coordinates on mousedown', () => { 209 | expect(cropit.origin).not.toEqual({ x: 1, y: 1 }); 210 | 211 | cropit.imageLoaded = true; 212 | cropit.onPreviewEvent(previewEvent); 213 | expect(cropit.origin).toEqual({ x: 1, y: 1 }); 214 | }); 215 | 216 | it('calls stopPropagation', () => { 217 | spyOn(previewEvent, 'stopPropagation'); 218 | cropit.imageLoaded = true; 219 | cropit.onPreviewEvent(previewEvent); 220 | expect(previewEvent.stopPropagation).toHaveBeenCalled(); 221 | }); 222 | 223 | it('does nothing before loading image', () => { 224 | spyOn(previewEvent, 'stopPropagation'); 225 | cropit.onPreviewEvent(previewEvent); 226 | expect(cropit.origin).not.toEqual({ x: 1, y: 1 }); 227 | expect(previewEvent.stopPropagation).not.toHaveBeenCalled(); 228 | }); 229 | }); 230 | }); 231 | 232 | describe('#fixOffset', () => { 233 | beforeEach(() => { 234 | cropit = newCropit({ width: 1, height: 1 }); 235 | cropit.imageLoaded = true; 236 | cropit._rotation = 0; 237 | }); 238 | 239 | describe('fixes x', () => { 240 | it('fits image to left if image width is less than preview', () => { 241 | cropit.image = { width: 1 }; 242 | cropit._zoom = 0.5; 243 | const offset = cropit.fixOffset({ x: -1 }); 244 | expect(offset.x).toBe(0); 245 | }); 246 | 247 | it('fits image to left', () => { 248 | cropit.image = { width: 4 }; 249 | cropit._zoom = 0.5; 250 | const offset = cropit.fixOffset({ x: 1 }); 251 | expect(offset.x).toBe(0); 252 | }); 253 | 254 | it('fits image to right', () => { 255 | cropit.image = { width: 4 }; 256 | cropit._zoom = 0.5; 257 | const offset = cropit.fixOffset({ x: -2 }); 258 | expect(offset.x).toBe(-1); 259 | }); 260 | 261 | it('rounds x', () => { 262 | cropit.image = { width: 4 }; 263 | cropit._zoom = 0.5; 264 | const offset = cropit.fixOffset({ x: -0.12121 }); 265 | expect(offset.x).toBe(-0.12); 266 | }); 267 | }); 268 | 269 | describe('fixes y', () => { 270 | it('fits image to top if image height is less than preview', () => { 271 | cropit.image = { height: 1 }; 272 | cropit._zoom = 0.5; 273 | const offset = cropit.fixOffset({ y: -1 }); 274 | expect(offset.y).toBe(0); 275 | }); 276 | 277 | it('fits image to top', () => { 278 | cropit.image = { height: 4 }; 279 | cropit._zoom = 0.5; 280 | const offset = cropit.fixOffset({ y: 1 }); 281 | expect(offset.y).toBe(0); 282 | }); 283 | 284 | it('fits image to bottom', () => { 285 | cropit.image = { height: 4 }; 286 | cropit._zoom = 0.5; 287 | const offset = cropit.fixOffset({ y: -2 }); 288 | expect(offset.y).toBe(-1); 289 | }); 290 | 291 | it('rounds y', () => { 292 | cropit.image = { height: 4 }; 293 | cropit._zoom = 0.5; 294 | const offset = cropit.fixOffset({ y: -0.12121 }); 295 | expect(offset.y).toBe(-0.12); 296 | }); 297 | }); 298 | 299 | it('takes rotation into account', () => { 300 | cropit.image = { width: 2, height: 1 }; 301 | cropit._zoom = 1; 302 | cropit._rotation = 90; 303 | 304 | const offset = cropit.fixOffset({ x: -0.5, y: -0.5 }); 305 | expect(offset).toEqual({ x: 0, y: -0.5 }); 306 | }); 307 | }); 308 | 309 | describe('#centerImage', () => { 310 | it('should center image', () => { 311 | cropit = newCropit({ width: 4, height: 2 }); 312 | cropit.imageLoaded = true; 313 | cropit.image = { width: 12, height: 8 }; 314 | cropit._zoom = 0.5; 315 | cropit._rotation = 0; 316 | 317 | cropit.offset = { x: 0, y: 1 }; 318 | expect(cropit.offset).not.toEqual({ x: -1, y: -1 }); 319 | 320 | cropit.centerImage(); 321 | expect(cropit.offset).toEqual({ x: -1, y: -1 }); 322 | }); 323 | }); 324 | 325 | describe('#fixZoom', () => { 326 | it('returns zoomer.fixZoom()', () => { 327 | cropit = newCropit(); 328 | 329 | cropit.zoomer = { fixZoom: () => 0.1 }; 330 | expect(cropit.fixZoom()).toBe(0.1); 331 | 332 | cropit.zoomer = { fixZoom: () => 0.5 }; 333 | expect(cropit.fixZoom()).toBe(0.5); 334 | 335 | cropit.zoomer = { fixZoom: () => 1 }; 336 | expect(cropit.fixZoom()).toBe(1); 337 | }); 338 | }); 339 | 340 | describe('#isZoomable', () => { 341 | it('returns zoomer.isZoomable', () => { 342 | cropit = newCropit(); 343 | 344 | cropit.zoomer = { isZoomable: () => true }; 345 | expect(cropit.isZoomable()).toBe(true); 346 | 347 | cropit.zoomer = { isZoomable: () => false }; 348 | expect(cropit.isZoomable()).toBe(false); 349 | }); 350 | }); 351 | 352 | describe('#get imageState', () => { 353 | it('returns image state', () => { 354 | cropit = newCropit(); 355 | cropit.image = { src: IMAGE_DATA }; 356 | cropit.offset = { x: -1, y: -1 }; 357 | cropit._zoom = 0.5; 358 | const imageState = cropit.imageState; 359 | expect(imageState.src).toBe(IMAGE_DATA); 360 | expect(imageState.offset).toEqual({ x: -1, y: -1 }); 361 | expect(imageState.zoom).toBe(0.5); 362 | }); 363 | }); 364 | 365 | describe('#set previewSize', () => { 366 | it('updates zoomer if image is loaded', () => { 367 | cropit = newCropit(); 368 | cropit.imageLoaded = true; 369 | cropit.image = { width: 2, height: 2 }; 370 | cropit._offset = { x: 0, y: 0 }; 371 | spyOn(cropit.zoomer, 'setup'); 372 | cropit.previewSize = { width: 1, height: 1 }; 373 | expect(cropit.zoomer.setup).toHaveBeenCalled(); 374 | }); 375 | }); 376 | }); 377 | -------------------------------------------------------------------------------- /test/cropit_view.spec.js: -------------------------------------------------------------------------------- 1 | jest 2 | .dontMock('fs') 3 | .dontMock('jquery') 4 | .dontMock('../src/constants') 5 | .dontMock('../src/cropit') 6 | .dontMock('../src/plugin'); 7 | 8 | import fs from 'fs'; 9 | import $ from 'jquery'; 10 | 11 | import { CLASS_NAMES, PLUGIN_KEY } from '../src/constants'; 12 | import Cropit from '../src/cropit'; 13 | import '../src/plugin'; 14 | 15 | const IMAGE_DATA = 'data:image/png;base64,image-data...'; 16 | 17 | const FIXTURES = { 18 | BASIC: fs.readFileSync('./test/fixtures/basic.html').toString(), 19 | IMAGE_BACKGROUND: fs.readFileSync('./test/fixtures/image-background.html').toString(), 20 | }; 21 | 22 | let $el = null; 23 | let cropit = null; 24 | 25 | describe('Cropit View', () => { 26 | beforeEach(() => { 27 | document.documentElement.innerHTML = FIXTURES.BASIC; 28 | $el = $('.image-editor'); 29 | }); 30 | 31 | describe('basic', () => { 32 | describe('#init', () => { 33 | it('sets preview size from options', () => { 34 | const $preview = $el.find(`.${CLASS_NAMES.PREVIEW}`); 35 | $preview.css({ width: 1, height: 1 }); 36 | expect($preview.width()).not.toBe(2); 37 | expect($preview.height()).not.toBe(2); 38 | 39 | $el.cropit({ width: 2, height: 2 }); 40 | expect($preview.width()).toBe(2); 41 | expect($preview.height()).toBe(2); 42 | }); 43 | 44 | it('sets min, max and step attributes on zoom slider', () => { 45 | const $zoomSlider = $el.find(`.${CLASS_NAMES.ZOOM_SLIDER}`); 46 | $zoomSlider.attr({ min: 2, max: 3, step: 0.5 }); 47 | expect($zoomSlider.attr('min')).not.toBe('0'); 48 | expect($zoomSlider.attr('max')).not.toBe('1'); 49 | expect($zoomSlider.attr('step')).not.toBe('0.01'); 50 | 51 | $el.cropit(); 52 | expect($zoomSlider.attr('min')).toBe('0'); 53 | expect($zoomSlider.attr('max')).toBe('1'); 54 | expect($zoomSlider.attr('step')).toBe('0.01'); 55 | }); 56 | }); 57 | 58 | describe('#onFileChange', () => { 59 | it('is invoked when file input changes', () => { 60 | spyOn(Cropit.prototype, 'onFileChange'); 61 | $el.cropit(); 62 | const $fileInput = $el.find(`.${CLASS_NAMES.FILE_INPUT}`); 63 | 64 | $fileInput.trigger('change'); 65 | expect(Cropit.prototype.onFileChange).toHaveBeenCalled(); 66 | }); 67 | 68 | it('calls options.onFileChange', () => { 69 | const onFileChangeCallback = jasmine.createSpy('onFileChange callback'); 70 | $el.cropit({ onFileChange: onFileChangeCallback }); 71 | cropit = $el.data(PLUGIN_KEY); 72 | 73 | cropit.onFileChange(); 74 | expect(onFileChangeCallback).toHaveBeenCalled(); 75 | }); 76 | }); 77 | 78 | describe('#loadImage', () => { 79 | it('calls options.onImageLoading', () => { 80 | const onImageLoadingCallback = jasmine.createSpy('onImageLoading callback'); 81 | $el.cropit({ onImageLoading: onImageLoadingCallback }); 82 | cropit = $el.data(PLUGIN_KEY); 83 | 84 | cropit.loadImage(IMAGE_DATA); 85 | expect(onImageLoadingCallback).toHaveBeenCalled(); 86 | }); 87 | }); 88 | 89 | describe('#onImageLoaded', () => { 90 | describe('with imageSrc', () => { 91 | beforeEach(() => { 92 | $el.cropit({ width: 1, height: 1 }); 93 | cropit = $el.data(PLUGIN_KEY); 94 | cropit.image = { src: IMAGE_DATA }; 95 | }); 96 | 97 | it('sets preview image', () => { 98 | const $image = $el.find(`.${CLASS_NAMES.PREVIEW_IMAGE}`); 99 | expect($image.attr('src')).not.toBe(IMAGE_DATA); 100 | 101 | cropit.onImageLoaded(); 102 | expect($image.attr('src')).toBe(IMAGE_DATA); 103 | }); 104 | 105 | it('sets up zoomer', () => { 106 | spyOn(cropit.zoomer, 'setup'); 107 | 108 | cropit.onImageLoaded(); 109 | expect(cropit.zoomer.setup).toHaveBeenCalled(); 110 | }); 111 | 112 | it('updates zoom slider', () => { 113 | const $zoomSlider = $el.find(`.${CLASS_NAMES.ZOOM_SLIDER}`); 114 | $zoomSlider.val(1); 115 | cropit.zoomer.getSliderPos = () => 0.5; 116 | expect(Number($zoomSlider.val())).not.toBe(0.5); 117 | 118 | cropit.onImageLoaded(); 119 | expect(Number($zoomSlider.val())).toBe(0.5); 120 | }); 121 | }); 122 | 123 | it('calls options.onImageLoaded', () => { 124 | const onImageLoadedCallback = jasmine.createSpy('onImageLoaded callback'); 125 | $el.cropit({ width: 1, height: 1, onImageLoaded: onImageLoadedCallback }); 126 | cropit = $el.data(PLUGIN_KEY); 127 | 128 | cropit.onImageLoaded(); 129 | expect(onImageLoadedCallback).toHaveBeenCalled(); 130 | }); 131 | }); 132 | 133 | describe('#onImageError', () => { 134 | it('calls options.onImageError', () => { 135 | const onImageError = jasmine.createSpy('onImageLoaded callback'); 136 | $el.cropit({ onImageError: onImageError }); 137 | cropit = $el.data(PLUGIN_KEY); 138 | 139 | cropit.onImageError(); 140 | expect(onImageError).toHaveBeenCalled(); 141 | }); 142 | }); 143 | 144 | describe('#onPreviewEvent', () => { 145 | describe('mouse event', () => { 146 | it('is invoked on mousedown on image container', () => { 147 | spyOn(Cropit.prototype, 'onPreviewEvent'); 148 | $el.cropit(); 149 | const $imageContainer = $el.find(`.${CLASS_NAMES.PREVIEW_IMAGE_CONTAINER}`); 150 | 151 | $imageContainer.trigger('mousedown'); 152 | expect(Cropit.prototype.onPreviewEvent).toHaveBeenCalled(); 153 | }); 154 | 155 | it('is invoked on mouseup on image container', () => { 156 | spyOn(Cropit.prototype, 'onPreviewEvent'); 157 | $el.cropit(); 158 | const $imageContainer = $el.find(`.${CLASS_NAMES.PREVIEW_IMAGE_CONTAINER}`); 159 | 160 | $imageContainer.trigger('mouseup'); 161 | expect(Cropit.prototype.onPreviewEvent).toHaveBeenCalled(); 162 | }); 163 | 164 | it('is invoked on mouseleave on image container', () => { 165 | spyOn(Cropit.prototype, 'onPreviewEvent'); 166 | $el.cropit(); 167 | const $imageContainer = $el.find(`.${CLASS_NAMES.PREVIEW_IMAGE_CONTAINER}`); 168 | 169 | $imageContainer.trigger('mouseleave'); 170 | expect(Cropit.prototype.onPreviewEvent).toHaveBeenCalled(); 171 | }); 172 | 173 | it('binds onMove on mousedown', () => { 174 | $el.cropit({ width: 2, height: 2 }); 175 | cropit = $el.data(PLUGIN_KEY); 176 | cropit.imageLoaded = true; 177 | cropit.image = { width: 8, height: 6 }; 178 | 179 | const $imageContainer = $el.find(`.${CLASS_NAMES.PREVIEW_IMAGE_CONTAINER}`); 180 | 181 | spyOn(Cropit.prototype, 'onMove'); 182 | cropit.onPreviewEvent({ 183 | type: 'mousedown', 184 | stopPropagation: () => {}, 185 | }); 186 | 187 | $imageContainer.trigger('mousemove'); 188 | expect(Cropit.prototype.onMove).toHaveBeenCalled(); 189 | }); 190 | 191 | it('moves image by dragging', () => { 192 | $el.cropit({ width: 2, height: 2 }); 193 | cropit = $el.data(PLUGIN_KEY); 194 | cropit.imageLoaded = true; 195 | cropit.image = { width: 8, height: 6 }; 196 | cropit._zoom = 1; 197 | cropit._offset = { x: 0, y: 0 }; 198 | 199 | cropit.onPreviewEvent({ 200 | type: 'touchstart', 201 | clientX: -1, 202 | clientY: -1, 203 | stopPropagation: () => {}, 204 | }); 205 | 206 | cropit.onMove({ 207 | type: 'touchmove', 208 | clientX: -3, 209 | clientY: -2, 210 | stopPropagation: () => {}, 211 | }); 212 | 213 | expect(cropit.offset).toEqual({ x: -2, y: -1 }); 214 | }); 215 | }); 216 | 217 | describe('touch event', () => { 218 | it('is invoked on touchstart on image container', () => { 219 | spyOn(Cropit.prototype, 'onPreviewEvent'); 220 | $el.cropit(); 221 | const $imageContainer = $el.find(`.${CLASS_NAMES.PREVIEW_IMAGE_CONTAINER}`); 222 | 223 | $imageContainer.trigger('touchstart'); 224 | expect(Cropit.prototype.onPreviewEvent).toHaveBeenCalled(); 225 | }); 226 | 227 | it('is invoked on touchend on image container', () => { 228 | spyOn(Cropit.prototype, 'onPreviewEvent'); 229 | $el.cropit(); 230 | const $imageContainer = $el.find(`.${CLASS_NAMES.PREVIEW_IMAGE_CONTAINER}`); 231 | 232 | $imageContainer.trigger('touchend'); 233 | expect(Cropit.prototype.onPreviewEvent).toHaveBeenCalled(); 234 | }); 235 | 236 | it('is invoked on touchcancel on image container', () => { 237 | spyOn(Cropit.prototype, 'onPreviewEvent'); 238 | $el.cropit(); 239 | const $imageContainer = $el.find(`.${CLASS_NAMES.PREVIEW_IMAGE_CONTAINER}`); 240 | 241 | $imageContainer.trigger('touchcancel'); 242 | expect(Cropit.prototype.onPreviewEvent).toHaveBeenCalled(); 243 | }); 244 | 245 | it('is invoked on touchleave on image container', () => { 246 | spyOn(Cropit.prototype, 'onPreviewEvent'); 247 | $el.cropit(); 248 | const $imageContainer = $el.find(`.${CLASS_NAMES.PREVIEW_IMAGE_CONTAINER}`); 249 | 250 | $imageContainer.trigger('touchleave'); 251 | expect(Cropit.prototype.onPreviewEvent).toHaveBeenCalled(); 252 | }); 253 | 254 | it('binds onMove on touchstart', () => { 255 | $el.cropit({ width: 2, height: 2 }); 256 | cropit = $el.data(PLUGIN_KEY); 257 | cropit.imageLoaded = true; 258 | cropit.image = { width: 8, height: 6 }; 259 | 260 | const $imageContainer = $el.find(`.${CLASS_NAMES.PREVIEW_IMAGE_CONTAINER}`); 261 | 262 | spyOn(Cropit.prototype, 'onMove'); 263 | cropit.onPreviewEvent({ 264 | type: 'touchstart', 265 | stopPropagation: () => {}, 266 | }); 267 | 268 | $imageContainer.trigger('touchmove'); 269 | expect(Cropit.prototype.onMove).toHaveBeenCalled(); 270 | }); 271 | 272 | it('moves image by dragging', () => { 273 | $el.cropit({ width: 2, height: 2 }); 274 | cropit = $el.data(PLUGIN_KEY); 275 | cropit.imageLoaded = true; 276 | cropit.image = { width: 8, height: 6 }; 277 | cropit._zoom = 1; 278 | cropit._offset = { x: 0, y: 0 }; 279 | 280 | cropit.onPreviewEvent({ 281 | type: 'touchstart', 282 | clientX: -1, 283 | clientY: -1, 284 | stopPropagation: () => {}, 285 | }); 286 | 287 | cropit.onMove({ 288 | type: 'touchmove', 289 | clientX: -3, 290 | clientY: -2, 291 | stopPropagation: () => {}, 292 | }); 293 | 294 | expect(cropit.offset).toEqual({ x: -2, y: -1 }); 295 | }); 296 | }); 297 | }); 298 | 299 | describe('#onZoomSliderChange', () => { 300 | it('is invoked mousemove on zoom slider', () => { 301 | spyOn(Cropit.prototype, 'onZoomSliderChange'); 302 | $el.cropit(); 303 | const $zoomSlider = $el.find(`.${CLASS_NAMES.ZOOM_SLIDER}`); 304 | 305 | $zoomSlider.trigger('mousemove'); 306 | expect(Cropit.prototype.onZoomSliderChange).toHaveBeenCalled(); 307 | }); 308 | 309 | it('is invoked touchmove on zoom slider', () => { 310 | spyOn(Cropit.prototype, 'onZoomSliderChange'); 311 | $el.cropit(); 312 | const $zoomSlider = $el.find(`.${CLASS_NAMES.ZOOM_SLIDER}`); 313 | 314 | $zoomSlider.trigger('touchmove'); 315 | expect(Cropit.prototype.onZoomSliderChange).toHaveBeenCalled(); 316 | }); 317 | 318 | it('is invoked change on zoom slider', () => { 319 | spyOn(Cropit.prototype, 'onZoomSliderChange'); 320 | $el.cropit(); 321 | const $zoomSlider = $el.find(`.${CLASS_NAMES.ZOOM_SLIDER}`); 322 | 323 | $zoomSlider.trigger('change'); 324 | expect(Cropit.prototype.onZoomSliderChange).toHaveBeenCalled(); 325 | }); 326 | 327 | describe('when invoked', () => { 328 | beforeEach(() => { 329 | $el.cropit({ width: 2, height: 2 }); 330 | cropit = $el.data(PLUGIN_KEY); 331 | cropit.image = { width: 8, height: 6 }; 332 | cropit._zoom = 1; 333 | cropit.imageLoaded = true; 334 | cropit.setZoom = () => {}; 335 | }); 336 | 337 | it('updates zoomSliderPos', () => { 338 | cropit.zoomSliderPos = 0; 339 | expect(cropit.zoomSliderPos).not.toBe(1); 340 | 341 | const $zoomSlider = $el.find(`.${CLASS_NAMES.ZOOM_SLIDER}`); 342 | $zoomSlider.val(1); 343 | cropit.onZoomSliderChange(); 344 | expect(cropit.zoomSliderPos).toBe(1); 345 | }); 346 | }); 347 | }); 348 | }); 349 | 350 | describe('with background image', () => { 351 | beforeEach(() => { 352 | document.documentElement.innerHTML = FIXTURES.IMAGE_BACKGROUND; 353 | $el = $('.image-editor'); 354 | }); 355 | 356 | describe('#init', () => { 357 | it('inserts background image', () => { 358 | $el.cropit({ width: 1, height: 1, imageBackground: true }); 359 | const $imageBg = $el.find(`.${CLASS_NAMES.PREVIEW_BACKGROUND}`); 360 | expect($imageBg.length).toBeTruthy(); 361 | }); 362 | 363 | it('inserts background image container', () => { 364 | $el.cropit({ width: 1, height: 1, imageBackground: true }); 365 | const $imageBgContainer = $el.find(`.${CLASS_NAMES.PREVIEW_BACKGROUND_CONTAINER}`); 366 | expect($imageBgContainer.length).toBeTruthy(); 367 | expect($imageBgContainer.css('position')).toBe('absolute'); 368 | }); 369 | }); 370 | 371 | describe('#onImageLoaded', () => { 372 | it('updates background image source', () => { 373 | $el.cropit({ width: 1, height: 1, imageBackground: true }); 374 | cropit = $el.data(PLUGIN_KEY); 375 | cropit.image = { src: IMAGE_DATA }; 376 | const $imageBg = $el.find(`.${CLASS_NAMES.PREVIEW_BACKGROUND}`); 377 | expect($imageBg.attr('src')).not.toBe(IMAGE_DATA); 378 | 379 | cropit.onImageLoaded(); 380 | expect($imageBg.attr('src')).toBe(IMAGE_DATA); 381 | }); 382 | }); 383 | }); 384 | }); 385 | -------------------------------------------------------------------------------- /test/fixtures/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cropit 5 | 6 | 7 |
8 | 9 |
10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /test/fixtures/image-background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cropit 5 | 6 | 7 |
8 | 9 |
10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /test/zoomer.spec.js: -------------------------------------------------------------------------------- 1 | jest 2 | .dontMock('lodash') 3 | .dontMock('../src/options') 4 | .dontMock('../src/zoomer'); 5 | 6 | import _ from 'lodash'; 7 | 8 | import { loadDefaults } from '../src/options'; 9 | import Zoomer from '../src/zoomer'; 10 | 11 | const defaults = loadDefaults(); 12 | 13 | describe('Zoomer', () => { 14 | let zoomer = null; 15 | 16 | const setup = (options) => { 17 | zoomer.setup(_.extend({}, defaults, options)); 18 | }; 19 | 20 | beforeEach(() => { 21 | zoomer = new Zoomer(); 22 | }); 23 | 24 | describe('#setup', () => { 25 | it('sets minZoom to the larger of widthRatio and heightRatio in `fill` minZoom mode', () => { 26 | setup({ 27 | imageSize: { width: 4, height: 2 }, 28 | previewSize: { width: 1, height: 1 }, 29 | minZoom: 'fill', 30 | }); 31 | expect(zoomer.minZoom).toBe(0.5); 32 | 33 | setup({ 34 | imageSize: { width: 2, height: 4 }, 35 | previewSize: { width: 1, height: 1 }, 36 | minZoom: 'fill', 37 | }); 38 | expect(zoomer.minZoom).toBe(0.5); 39 | 40 | setup({ 41 | imageSize: { width: 2, height: 2 }, 42 | previewSize: { width: 1, height: 1 }, 43 | minZoom: 'fill', 44 | }); 45 | expect(zoomer.minZoom).toBe(0.5); 46 | }); 47 | 48 | it('sets minZoom to the smaller of widthRatio and heightRatio `fit` minZoom mode', () => { 49 | setup({ 50 | imageSize: { width: 4, height: 2 }, 51 | previewSize: { width: 1, height: 1 }, 52 | minZoom: 'fit', 53 | }); 54 | expect(zoomer.minZoom).toBe(0.25); 55 | 56 | setup({ 57 | imageSize: { width: 2, height: 4 }, 58 | previewSize: { width: 1, height: 1 }, 59 | minZoom: 'fit', 60 | }); 61 | expect(zoomer.minZoom).toBe(0.25); 62 | 63 | setup({ 64 | imageSize: { width: 2, height: 2 }, 65 | previewSize: { width: 1, height: 1 }, 66 | minZoom: 'fit', 67 | }); 68 | expect(zoomer.minZoom).toBe(0.5); 69 | }); 70 | 71 | it('sets minZoom to 1 if image is small and smallImage is allow', () => { 72 | setup({ 73 | imageSize: { width: 1, height: 1 }, 74 | previewSize: { width: 2, height: 2 }, 75 | smallImage: 'allow', 76 | }); 77 | expect(zoomer.minZoom).toBe(1); 78 | 79 | setup({ 80 | imageSize: { width: 1, height: 3 }, 81 | previewSize: { width: 2, height: 2 }, 82 | minZoom: 'fill', 83 | smallImage: 'allow', 84 | }); 85 | expect(zoomer.minZoom).toBe(1); 86 | }); 87 | 88 | it('follows minZoom option for small images if smallImage is stretch', () => { 89 | setup({ 90 | imageSize: { width: 2, height: 1 }, 91 | previewSize: { width: 4, height: 4 }, 92 | minZoom: 'fill', 93 | smallImage: 'stretch', 94 | }); 95 | expect(zoomer.minZoom).toBe(4); 96 | 97 | setup({ 98 | imageSize: { width: 2, height: 1 }, 99 | previewSize: { width: 4, height: 4 }, 100 | minZoom: 'fit', 101 | smallImage: 'stretch', 102 | }); 103 | expect(zoomer.minZoom).toBe(2); 104 | }); 105 | 106 | it('sets maxZoom to minZoom if image is smaller than preview', () => { 107 | setup({ 108 | imageSize: { width: 4, height: 2 }, 109 | previewSize: { width: 5, height: 5 }, 110 | }); 111 | expect(zoomer.maxZoom).toBe(zoomer.minZoom); 112 | }); 113 | 114 | it('sets maxZoom to 1 if image is larger than preview', () => { 115 | setup({ 116 | imageSize: { width: 4, height: 2 }, 117 | previewSize: { width: 1, height: 1 }, 118 | }); 119 | expect(zoomer.maxZoom).toBe(1); 120 | }); 121 | 122 | it('sets maxZoom to customized value', () => { 123 | setup({ 124 | imageSize: { width: 4, height: 2 }, 125 | previewSize: { width: 1, height: 1 }, 126 | maxZoom: 1.5, 127 | }); 128 | expect(zoomer.maxZoom).toBe(1.5); 129 | }); 130 | 131 | it('scales maxZoom in inverse proportion to exportZoom', () => { 132 | setup({ 133 | imageSize: { width: 8, height: 4 }, 134 | previewSize: { width: 1, height: 1 }, 135 | exportZoom: 2, 136 | }); 137 | expect(zoomer.maxZoom).toBe(0.5); 138 | 139 | setup({ 140 | imageSize: { width: 8, height: 4 }, 141 | previewSize: { width: 1, height: 1 }, 142 | exportZoom: 2, 143 | maxZoom: 1.5, 144 | }); 145 | expect(zoomer.maxZoom).toBe(0.75); 146 | }); 147 | }); 148 | 149 | describe('#getZoom', () => { 150 | it('returns proper zoom level', () => { 151 | zoomer.minZoom = 0.5; 152 | zoomer.maxZoom = 1; 153 | 154 | expect(zoomer.getZoom(0)).toBe(0.5); 155 | expect(zoomer.getZoom(0.5)).toBe(0.75); 156 | expect(zoomer.getZoom(1)).toBe(1); 157 | }); 158 | }); 159 | 160 | describe('#getSliderPos', () => { 161 | it('returns proper slider pos', () => { 162 | zoomer.minZoom = 0.5; 163 | zoomer.maxZoom = 1; 164 | 165 | expect(zoomer.getSliderPos(0.5)).toBe(0); 166 | expect(zoomer.getSliderPos(0.75)).toBe(0.5); 167 | expect(zoomer.getSliderPos(1)).toBe(1); 168 | }); 169 | 170 | it('returns 0 when minZoom and maxZoom are the same', () => { 171 | zoomer.minZoom = 2; 172 | zoomer.maxZoom = 2; 173 | 174 | expect(zoomer.getSliderPos(1)).toBe(0); 175 | expect(zoomer.getSliderPos(2)).toBe(0); 176 | expect(zoomer.getSliderPos(3)).toBe(0); 177 | }); 178 | 179 | it('is inverse to getZoom', () => { 180 | zoomer.minZoom = Math.random(); 181 | zoomer.maxZoom = Math.random() + zoomer.minZoom; 182 | _.range(10).map((x) => x / 10).forEach((sliderPos) => { 183 | const zoom = zoomer.getZoom(sliderPos); 184 | const calculatedSliderPos = zoomer.getSliderPos(zoom); 185 | expect(calculatedSliderPos).toBeGreaterThan(sliderPos - 0.0001); 186 | expect(calculatedSliderPos).toBeLessThan(sliderPos + 0.0001); 187 | }); 188 | }); 189 | }); 190 | 191 | describe('#isZoomable', () => { 192 | it('returns true when image is bigger than preview', () => { 193 | setup({ 194 | imageSize: { width: 2, height: 2 }, 195 | previewSize: { width: 1, height: 1 }, 196 | }); 197 | expect(zoomer.isZoomable()).toBe(true); 198 | }); 199 | 200 | it('returns false when image is the same size as preview', () => { 201 | setup({ 202 | imageSize: { width: 1, height: 1 }, 203 | previewSize: { width: 1, height: 1 }, 204 | }); 205 | expect(zoomer.isZoomable()).toBe(false); 206 | }); 207 | 208 | it('returns false when image has the same width as preview', () => { 209 | setup({ 210 | imageSize: { width: 1, height: 2 }, 211 | previewSize: { width: 1, height: 1 }, 212 | }); 213 | expect(zoomer.isZoomable()).toBe(false); 214 | }); 215 | 216 | it('returns false when image has the same height as preview', () => { 217 | setup({ 218 | imageSize: { width: 2, height: 1 }, 219 | previewSize: { width: 1, height: 1 }, 220 | }); 221 | expect(zoomer.isZoomable()).toBe(false); 222 | }); 223 | 224 | it('returns false when image is smaller than preview', () => { 225 | setup({ 226 | imageSize: { width: 1, height: 1 }, 227 | previewSize: { width: 2, height: 2 }, 228 | }); 229 | expect(zoomer.isZoomable()).toBe(false); 230 | }); 231 | }); 232 | 233 | describe('fixZoom()', () => { 234 | beforeEach(() => { 235 | zoomer.minZoom = 0.5; 236 | zoomer.maxZoom = 1; 237 | }); 238 | 239 | it('fixes zoom when it is too small', () => { 240 | expect(zoomer.fixZoom(0)).toBe(0.5); 241 | expect(zoomer.fixZoom(0.25)).toBe(0.5); 242 | expect(zoomer.fixZoom(0.49)).toBe(0.5); 243 | }); 244 | 245 | it('fixes zoom when it is too large', () => { 246 | expect(zoomer.fixZoom(1.5)).toBe(1); 247 | expect(zoomer.fixZoom(1.1)).toBe(1); 248 | }); 249 | 250 | it('keeps zoom when it is right', () => { 251 | expect(zoomer.fixZoom(0.5)).toBe(0.5); 252 | expect(zoomer.fixZoom(0.75)).toBe(0.75); 253 | expect(zoomer.fixZoom(1)).toBe(1); 254 | }); 255 | }); 256 | }); 257 | -------------------------------------------------------------------------------- /update_version.js: -------------------------------------------------------------------------------- 1 | #! /usr/local/bin/node 2 | 3 | var sys = require('sys') 4 | var exec = require('child_process').exec; 5 | var fs = require('fs'); 6 | 7 | var version = require('./cropit.jquery.json').version; 8 | 9 | sys.puts('Version = ' + version); 10 | 11 | ['package', 'bower'].forEach(function(f) { 12 | var meta = require('./' + f + '.json'); 13 | meta.version = version; 14 | 15 | fs.writeFileSync(f + '.json', JSON.stringify(meta, null, ' ') + '\n'); 16 | sys.puts('Finished ' + f + '.json'); 17 | }); 18 | 19 | exec('webpack', function (error, stdout, stderr) { sys.puts(stdout) }); 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | var pkg = require('./cropit.jquery.json'); 5 | 6 | var paths = { 7 | src: path.join(__dirname, 'src'), 8 | dist: path.join(__dirname, 'dist'), 9 | }; 10 | 11 | module.exports = { 12 | entry: paths.src + '/plugin.js', 13 | output: { 14 | path: paths.dist, 15 | filename: 'jquery.cropit.js', 16 | library: 'cropit', 17 | libraryTarget: 'umd', 18 | }, 19 | 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.js$/, 24 | exclude: /node_modules/, 25 | loader: 'babel-loader?blacklist[]=strict', 26 | }, 27 | ], 28 | }, 29 | 30 | externals: { 31 | jquery: { 32 | root: 'jQuery', 33 | commonjs: 'jquery', 34 | commonjs2: 'jquery', 35 | amd: 'jquery', 36 | }, 37 | }, 38 | 39 | plugins: [ 40 | new webpack.BannerPlugin(pkg.name + ' - v' + pkg.version + 41 | ' <' +pkg.homepage + '>'), 42 | ], 43 | }; 44 | --------------------------------------------------------------------------------