diff --git a/dist/croppr.css b/dist/croppr.css index e21630c..19eb2f7 100644 --- a/dist/croppr.css +++ b/dist/croppr.css @@ -1,58 +1,58 @@ .croppr-container * { - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - box-sizing: border-box; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; } .croppr-container img { - vertical-align: middle; - max-width: 100%; + vertical-align: middle; + max-width: 100%; } .croppr { - position: relative; - display: inline-block; + position: relative; + display: inline-block; } .croppr-overlay { - background: rgba(0,0,0,0.5); - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1; - cursor: crosshair; + background: rgba(0,0,0,0.5); + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + cursor: crosshair; } .croppr-region { - border: 1px dashed rgba(0, 0, 0, 0.5); - position: absolute; - z-index: 3; - cursor: move; - top: 0; + border: 1px dashed rgba(0, 0, 0, 0.5); + position: absolute; + z-index: 3; + cursor: move; + top: 0; } .croppr-imageClipped { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 2; - pointer-events: none; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + pointer-events: none; } .croppr-handle { - border: 1px solid black; - background-color: white; - width: 10px; - height: 10px; - position: absolute; - z-index: 4; - top: 0; + border: 1px solid black; + background-color: white; + width: 10px; + height: 10px; + position: absolute; + z-index: 4; + top: 0; } \ No newline at end of file diff --git a/dist/croppr.js b/dist/croppr.js index c8fe647..c5e6c47 100644 --- a/dist/croppr.js +++ b/dist/croppr.js @@ -205,300 +205,300 @@ var Handle = * @param {Element} eventBus The element to dispatch events to. */ function Handle(position, constraints, cursor, eventBus) { - classCallCheck(this, Handle); - var self = this; - this.position = position; - this.constraints = constraints; - this.cursor = cursor; - this.eventBus = eventBus; - this.el = document.createElement('div'); - this.el.className = 'croppr-handle'; - this.el.style.cursor = cursor; - this.el.addEventListener('mousedown', onMouseDown); - function onMouseDown(e) { - e.stopPropagation(); - document.addEventListener('mouseup', onMouseUp); - document.addEventListener('mousemove', onMouseMove); - self.eventBus.dispatchEvent(new CustomEvent('handlestart', { - detail: { handle: self } - })); - } - function onMouseUp(e) { - e.stopPropagation(); - document.removeEventListener('mouseup', onMouseUp); - document.removeEventListener('mousemove', onMouseMove); - self.eventBus.dispatchEvent(new CustomEvent('handleend', { - detail: { handle: self } - })); - } - function onMouseMove(e) { - e.stopPropagation(); - self.eventBus.dispatchEvent(new CustomEvent('handlemove', { - detail: { mouseX: e.clientX, mouseY: e.clientY } - })); - } + classCallCheck(this, Handle); + var self = this; + this.position = position; + this.constraints = constraints; + this.cursor = cursor; + this.eventBus = eventBus; + this.el = document.createElement('div'); + this.el.className = 'croppr-handle'; + this.el.style.cursor = cursor; + this.el.addEventListener('mousedown', onMouseDown); + function onMouseDown(e) { + e.stopPropagation(); + document.addEventListener('mouseup', onMouseUp); + document.addEventListener('mousemove', onMouseMove); + self.eventBus.dispatchEvent(new CustomEvent('handlestart', { + detail: { handle: self } + })); + } + function onMouseUp(e) { + e.stopPropagation(); + document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener('mousemove', onMouseMove); + self.eventBus.dispatchEvent(new CustomEvent('handleend', { + detail: { handle: self } + })); + } + function onMouseMove(e) { + e.stopPropagation(); + self.eventBus.dispatchEvent(new CustomEvent('handlemove', { + detail: { mouseX: e.clientX, mouseY: e.clientY } + })); + } }; var Box = function () { + /** + * Creates a new Box instance. + * @constructor + * @param {Number} x1 + * @param {Number} y1 + * @param {Number} x2 + * @param {Number} y2 + */ + function Box(x1, y1, x2, y2) { + classCallCheck(this, Box); + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + } + /** + * Sets the new dimensions of the box. + * @param {Number} x1 + * @param {Number} y1 + * @param {Number} x2 + * @param {Number} y2 + */ + createClass(Box, [{ + key: 'set', + value: function set$$1() { + var x1 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var y1 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + var x2 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + var y2 = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; + this.x1 = x1 == null ? this.x1 : x1; + this.y1 = y1 == null ? this.y1 : y1; + this.x2 = x2 == null ? this.x2 : x2; + this.y2 = y2 == null ? this.y2 : y2; + return this; + } /** - * Creates a new Box instance. - * @constructor - * @param {Number} x1 - * @param {Number} y1 - * @param {Number} x2 - * @param {Number} y2 + * Calculates the width of the box. + * @returns {Number} */ - function Box(x1, y1, x2, y2) { - classCallCheck(this, Box); - this.x1 = x1; - this.y1 = y1; - this.x2 = x2; - this.y2 = y2; + }, { + key: 'width', + value: function width() { + return Math.abs(this.x2 - this.x1); } /** - * Sets the new dimensions of the box. - * @param {Number} x1 - * @param {Number} y1 - * @param {Number} x2 - * @param {Number} y2 + * Calculates the height of the box. + * @returns {Number} */ - createClass(Box, [{ - key: 'set', - value: function set$$1() { - var x1 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; - var y1 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; - var x2 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; - var y2 = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; - this.x1 = x1 == null ? this.x1 : x1; - this.y1 = y1 == null ? this.y1 : y1; - this.x2 = x2 == null ? this.x2 : x2; - this.y2 = y2 == null ? this.y2 : y2; - return this; - } - /** - * Calculates the width of the box. - * @returns {Number} - */ - }, { - key: 'width', - value: function width() { - return Math.abs(this.x2 - this.x1); - } - /** - * Calculates the height of the box. - * @returns {Number} - */ - }, { - key: 'height', - value: function height() { - return Math.abs(this.y2 - this.y1); - } - /** - * Resizes the box to a new size. - * @param {Number} newWidth - * @param {Number} newHeight - * @param {Array} [origin] The origin point to resize from. - * Defaults to [0, 0] (top left). - */ - }, { - key: 'resize', - value: function resize(newWidth, newHeight) { - var origin = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [0, 0]; - var fromX = this.x1 + this.width() * origin[0]; - var fromY = this.y1 + this.height() * origin[1]; - this.x1 = fromX - newWidth * origin[0]; - this.y1 = fromY - newHeight * origin[1]; - this.x2 = this.x1 + newWidth; - this.y2 = this.y1 + newHeight; - return this; - } - /** - * Scale the box by a factor. - * @param {Number} factor - * @param {Array} [origin] The origin point to resize from. - * Defaults to [0, 0] (top left). - */ - }, { - key: 'scale', - value: function scale(factor) { - var origin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [0, 0]; - var newWidth = this.width() * factor; - var newHeight = this.height() * factor; - this.resize(newWidth, newHeight, origin); - return this; - } - }, { - key: 'move', - value: function move() { - var x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; - var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; - var width = this.width(); - var height = this.height(); - x = x === null ? this.x1 : x; - y = y === null ? this.y1 : y; - this.x1 = x; - this.y1 = y; - this.x2 = x + width; - this.y2 = y + height; - return this; - } - /** - * Get relative x and y coordinates of a given point within the box. - * @param {Array} point The x and y ratio position within the box. - * @returns {Array} The x and y coordinates [x, y]. - */ - }, { - key: 'getRelativePoint', - value: function getRelativePoint() { - var point = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0]; - var x = this.width() * point[0]; - var y = this.height() * point[1]; - return [x, y]; - } - /** - * Get absolute x and y coordinates of a given point within the box. - * @param {Array} point The x and y ratio position within the box. - * @returns {Array} The x and y coordinates [x, y]. - */ - }, { - key: 'getAbsolutePoint', - value: function getAbsolutePoint() { - var point = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0]; - var x = this.x1 + this.width() * point[0]; - var y = this.y1 + this.height() * point[1]; - return [x, y]; - } - /** - * Constrain the box to a fixed ratio. - * @param {Number} ratio - * @param {Array} [origin] The origin point to resize from. - * Defaults to [0, 0] (top left). - * @param {String} [grow] The axis to grow to maintain the ratio. - * Defaults to 'height'. - */ - }, { - key: 'constrainToRatio', - value: function constrainToRatio(ratio) { - var origin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [0, 0]; - var grow = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'height'; - if (ratio === null) { - return; - } - var width = this.width(); - var height = this.height(); - switch (grow) { - case 'height': - this.resize(this.width(), this.width() * ratio, origin); - break; - case 'width': - this.resize(this.height() * 1 / ratio, this.height(), origin); - break; - default: - this.resize(this.width(), this.width() * ratio, origin); - } - return this; - } - /** - * Constrain the box within a boundary. - * @param {Number} boundaryWidth - * @param {Number} boundaryHeight - * @param {Array} [origin] The origin point to resize from. - * Defaults to [0, 0] (top left). - */ - }, { - key: 'constrainToBoundary', - value: function constrainToBoundary(boundaryWidth, boundaryHeight) { - var origin = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [0, 0]; - var _getAbsolutePoint = this.getAbsolutePoint(origin), - _getAbsolutePoint2 = slicedToArray(_getAbsolutePoint, 2), - originX = _getAbsolutePoint2[0], - originY = _getAbsolutePoint2[1]; - var maxIfLeft = originX; - var maxIfTop = originY; - var maxIfRight = boundaryWidth - originX; - var maxIfBottom = boundaryHeight - originY; - var directionX = -2 * origin[0] + 1; - var directionY = -2 * origin[1] + 1; - var maxWidth = null, - maxHeight = null; - switch (directionX) { - case -1: - maxWidth = maxIfLeft;break; - case 0: - maxWidth = Math.min(maxIfLeft, maxIfRight) * 2;break; - case +1: - maxWidth = maxIfRight;break; - } - switch (directionY) { - case -1: - maxHeight = maxIfTop;break; - case 0: - maxHeight = Math.min(maxIfTop, maxIfBottom) * 2;break; - case +1: - maxHeight = maxIfBottom;break; - } - if (this.width() > maxWidth) { - var factor = maxWidth / this.width(); - this.scale(factor, origin); - } - if (this.height() > maxHeight) { - var _factor = maxHeight / this.height(); - this.scale(_factor, origin); - } - return this; - } - /** - * Constrain the box to a maximum/minimum size. - * @param {Number} [maxWidth] - * @param {Number} [maxHeight] - * @param {Number} [minWidth] - * @param {Number} [minHeight] - * @param {Array} [origin] The origin point to resize from. - * Defaults to [0, 0] (top left). - * @param {Number} [ratio] Ratio to maintain. - */ - }, { - key: 'constrainToSize', - value: function constrainToSize() { - var maxWidth = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; - var maxHeight = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; - var minWidth = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; - var minHeight = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; - var origin = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : [0, 0]; - var ratio = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null; - if (ratio) { - if (ratio > 1) { - maxWidth = maxHeight * 1 / ratio; - minHeight = minHeight * ratio; - } else if (ratio < 1) { - maxHeight = maxWidth * ratio; - minWidth = minHeight * 1 / ratio; - } - } - if (maxWidth && this.width() > maxWidth) { - var newWidth = maxWidth, - newHeight = ratio === null ? this.height() : maxHeight; - this.resize(newWidth, newHeight, origin); - } - if (maxHeight && this.height() > maxHeight) { - var _newWidth = ratio === null ? this.width() : maxWidth, - _newHeight = maxHeight; - this.resize(_newWidth, _newHeight, origin); - } - if (minWidth && this.width() < minWidth) { - var _newWidth2 = minWidth, - _newHeight2 = ratio === null ? this.height() : minHeight; - this.resize(_newWidth2, _newHeight2, origin); - } - if (minHeight && this.height() < minHeight) { - var _newWidth3 = ratio === null ? this.width() : minWidth, - _newHeight3 = minHeight; - this.resize(_newWidth3, _newHeight3, origin); - } - return this; + }, { + key: 'height', + value: function height() { + return Math.abs(this.y2 - this.y1); + } + /** + * Resizes the box to a new size. + * @param {Number} newWidth + * @param {Number} newHeight + * @param {Array} [origin] The origin point to resize from. + * Defaults to [0, 0] (top left). + */ + }, { + key: 'resize', + value: function resize(newWidth, newHeight) { + var origin = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [0, 0]; + var fromX = this.x1 + this.width() * origin[0]; + var fromY = this.y1 + this.height() * origin[1]; + this.x1 = fromX - newWidth * origin[0]; + this.y1 = fromY - newHeight * origin[1]; + this.x2 = this.x1 + newWidth; + this.y2 = this.y1 + newHeight; + return this; + } + /** + * Scale the box by a factor. + * @param {Number} factor + * @param {Array} [origin] The origin point to resize from. + * Defaults to [0, 0] (top left). + */ + }, { + key: 'scale', + value: function scale(factor) { + var origin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [0, 0]; + var newWidth = this.width() * factor; + var newHeight = this.height() * factor; + this.resize(newWidth, newHeight, origin); + return this; + } + }, { + key: 'move', + value: function move() { + var x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + var width = this.width(); + var height = this.height(); + x = x === null ? this.x1 : x; + y = y === null ? this.y1 : y; + this.x1 = x; + this.y1 = y; + this.x2 = x + width; + this.y2 = y + height; + return this; + } + /** + * Get relative x and y coordinates of a given point within the box. + * @param {Array} point The x and y ratio position within the box. + * @returns {Array} The x and y coordinates [x, y]. + */ + }, { + key: 'getRelativePoint', + value: function getRelativePoint() { + var point = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0]; + var x = this.width() * point[0]; + var y = this.height() * point[1]; + return [x, y]; + } + /** + * Get absolute x and y coordinates of a given point within the box. + * @param {Array} point The x and y ratio position within the box. + * @returns {Array} The x and y coordinates [x, y]. + */ + }, { + key: 'getAbsolutePoint', + value: function getAbsolutePoint() { + var point = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0]; + var x = this.x1 + this.width() * point[0]; + var y = this.y1 + this.height() * point[1]; + return [x, y]; + } + /** + * Constrain the box to a fixed ratio. + * @param {Number} ratio + * @param {Array} [origin] The origin point to resize from. + * Defaults to [0, 0] (top left). + * @param {String} [grow] The axis to grow to maintain the ratio. + * Defaults to 'height'. + */ + }, { + key: 'constrainToRatio', + value: function constrainToRatio(ratio) { + var origin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [0, 0]; + var grow = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'height'; + if (ratio === null) { + return; + } + var width = this.width(); + var height = this.height(); + switch (grow) { + case 'height': + this.resize(this.width(), this.width() * ratio, origin); + break; + case 'width': + this.resize(this.height() * 1 / ratio, this.height(), origin); + break; + default: + this.resize(this.width(), this.width() * ratio, origin); + } + return this; + } + /** + * Constrain the box within a boundary. + * @param {Number} boundaryWidth + * @param {Number} boundaryHeight + * @param {Array} [origin] The origin point to resize from. + * Defaults to [0, 0] (top left). + */ + }, { + key: 'constrainToBoundary', + value: function constrainToBoundary(boundaryWidth, boundaryHeight) { + var origin = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [0, 0]; + var _getAbsolutePoint = this.getAbsolutePoint(origin), + _getAbsolutePoint2 = slicedToArray(_getAbsolutePoint, 2), + originX = _getAbsolutePoint2[0], + originY = _getAbsolutePoint2[1]; + var maxIfLeft = originX; + var maxIfTop = originY; + var maxIfRight = boundaryWidth - originX; + var maxIfBottom = boundaryHeight - originY; + var directionX = -2 * origin[0] + 1; + var directionY = -2 * origin[1] + 1; + var maxWidth = null, + maxHeight = null; + switch (directionX) { + case -1: + maxWidth = maxIfLeft;break; + case 0: + maxWidth = Math.min(maxIfLeft, maxIfRight) * 2;break; + case +1: + maxWidth = maxIfRight;break; + } + switch (directionY) { + case -1: + maxHeight = maxIfTop;break; + case 0: + maxHeight = Math.min(maxIfTop, maxIfBottom) * 2;break; + case +1: + maxHeight = maxIfBottom;break; + } + if (this.width() > maxWidth) { + var factor = maxWidth / this.width(); + this.scale(factor, origin); + } + if (this.height() > maxHeight) { + var _factor = maxHeight / this.height(); + this.scale(_factor, origin); + } + return this; + } + /** + * Constrain the box to a maximum/minimum size. + * @param {Number} [maxWidth] + * @param {Number} [maxHeight] + * @param {Number} [minWidth] + * @param {Number} [minHeight] + * @param {Array} [origin] The origin point to resize from. + * Defaults to [0, 0] (top left). + * @param {Number} [ratio] Ratio to maintain. + */ + }, { + key: 'constrainToSize', + value: function constrainToSize() { + var maxWidth = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var maxHeight = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + var minWidth = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + var minHeight = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; + var origin = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : [0, 0]; + var ratio = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null; + if (ratio) { + if (ratio > 1) { + maxWidth = maxHeight * 1 / ratio; + minHeight = minHeight * ratio; + } else if (ratio < 1) { + maxHeight = maxWidth * ratio; + minWidth = minHeight * 1 / ratio; } - }]); - return Box; + } + if (maxWidth && this.width() > maxWidth) { + var newWidth = maxWidth, + newHeight = ratio === null ? this.height() : maxHeight; + this.resize(newWidth, newHeight, origin); + } + if (maxHeight && this.height() > maxHeight) { + var _newWidth = ratio === null ? this.width() : maxWidth, + _newHeight = maxHeight; + this.resize(_newWidth, _newHeight, origin); + } + if (minWidth && this.width() < minWidth) { + var _newWidth2 = minWidth, + _newHeight2 = ratio === null ? this.height() : minHeight; + this.resize(_newWidth2, _newHeight2, origin); + } + if (minHeight && this.height() < minHeight) { + var _newWidth3 = ratio === null ? this.width() : minWidth, + _newHeight3 = minHeight; + this.resize(_newWidth3, _newHeight3, origin); + } + return this; + } + }]); + return Box; }(); /** @@ -506,31 +506,31 @@ var Box = function () { * @param {Element} element */ function enableTouch(element) { - element.addEventListener('touchstart', simulateMouseEvent); - element.addEventListener('touchend', simulateMouseEvent); - element.addEventListener('touchmove', simulateMouseEvent); + element.addEventListener('touchstart', simulateMouseEvent); + element.addEventListener('touchend', simulateMouseEvent); + element.addEventListener('touchmove', simulateMouseEvent); } /** * Translates a touch event to a mouse event. * @param {Event} e */ function simulateMouseEvent(e) { - e.preventDefault(); - var touch = e.changedTouches[0]; - var eventMap = { - 'touchstart': 'mousedown', - 'touchmove': 'mousemove', - 'touchend': 'mouseup' - }; - touch.target.dispatchEvent(new MouseEvent(eventMap[e.type], { - bubbles: true, - cancelable: true, - view: window, - clientX: touch.clientX, - clientY: touch.clientY, - screenX: touch.screenX, - screenY: touch.screenY - })); + e.preventDefault(); + var touch = e.changedTouches[0]; + var eventMap = { + 'touchstart': 'mousedown', + 'touchmove': 'mousemove', + 'touchend': 'mouseup' + }; + touch.target.dispatchEvent(new MouseEvent(eventMap[e.type], { + bubbles: true, + cancelable: true, + view: window, + clientX: touch.clientX, + clientY: touch.clientY, + screenX: touch.screenX, + screenY: touch.screenY + })); } /** @@ -545,614 +545,639 @@ function simulateMouseEvent(e) { */ var HANDLES = [{ position: [0.0, 0.0], constraints: [1, 0, 0, 1], cursor: 'nw-resize' }, { position: [0.5, 0.0], constraints: [1, 0, 0, 0], cursor: 'n-resize' }, { position: [1.0, 0.0], constraints: [1, 1, 0, 0], cursor: 'ne-resize' }, { position: [1.0, 0.5], constraints: [0, 1, 0, 0], cursor: 'e-resize' }, { position: [1.0, 1.0], constraints: [0, 1, 1, 0], cursor: 'se-resize' }, { position: [0.5, 1.0], constraints: [0, 0, 1, 0], cursor: 's-resize' }, { position: [0.0, 1.0], constraints: [0, 0, 1, 1], cursor: 'sw-resize' }, { position: [0.0, 0.5], constraints: [0, 0, 0, 1], cursor: 'w-resize' }]; var CropprCore = function () { - function CropprCore(element, options) { - var _this = this; - var deferred = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; - classCallCheck(this, CropprCore); - this.options = CropprCore.parseOptions(options || {}); - if (!element.nodeName) { - element = document.querySelector(element); - if (element == null) { - throw 'Unable to find element.'; - } - } - if (!element.getAttribute('src')) { - throw 'Image src not provided.'; - } - this._initialized = false; - this._restore = { - parent: element.parentNode, - element: element + function CropprCore(element, options) { + var _this = this; + var deferred = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + classCallCheck(this, CropprCore); + this.options = CropprCore.parseOptions(options || {}); + if (!element.nodeName) { + element = document.querySelector(element); + if (element == null) { + throw 'Unable to find element.'; + } + } + if (!element.getAttribute('src')) { + throw 'Image src not provided.'; + } + this._initialized = false; + this._restore = { + parent: element.parentNode, + element: element + }; + if (!deferred) { + if (element.width === 0 || element.height === 0) { + element.onload = function () { + _this.initialize(element); }; - if (!deferred) { - if (element.width === 0 || element.height === 0) { - element.onload = function () { - _this.initialize(element); - }; - } else { - this.initialize(element); - } - } + } else { + this.initialize(element); + } } - createClass(CropprCore, [{ - key: 'initialize', - value: function initialize(element) { - this.createDOM(element); - this.options.convertToPixels(this.cropperEl); - this.attachHandlerEvents(); - this.attachRegionEvents(); - this.attachOverlayEvents(); - this.box = this.initializeBox(this.options); - this.redraw(); - this._initialized = true; - if (this.options.onInitialize !== null) { - this.options.onInitialize(this); - } - } - }, { - key: 'createDOM', - value: function createDOM(targetEl) { - this.containerEl = document.createElement('div'); - this.containerEl.className = 'croppr-container'; - this.eventBus = this.containerEl; - enableTouch(this.containerEl); - this.cropperEl = document.createElement('div'); - this.cropperEl.className = 'croppr'; - this.imageEl = document.createElement('img'); - this.imageEl.setAttribute('src', targetEl.getAttribute('src')); - this.imageEl.setAttribute('alt', targetEl.getAttribute('alt')); - this.imageEl.className = 'croppr-image'; - this.imageClippedEl = this.imageEl.cloneNode(); - this.imageClippedEl.className = 'croppr-imageClipped'; - this.regionEl = document.createElement('div'); - this.regionEl.className = 'croppr-region'; - this.overlayEl = document.createElement('div'); - this.overlayEl.className = 'croppr-overlay'; - var handleContainerEl = document.createElement('div'); - handleContainerEl.className = 'croppr-handleContainer'; - this.handles = []; - for (var i = 0; i < HANDLES.length; i++) { - var handle = new Handle(HANDLES[i].position, HANDLES[i].constraints, HANDLES[i].cursor, this.eventBus); - this.handles.push(handle); - handleContainerEl.appendChild(handle.el); - } - this.cropperEl.appendChild(this.imageEl); - this.cropperEl.appendChild(this.imageClippedEl); - this.cropperEl.appendChild(this.regionEl); - this.cropperEl.appendChild(this.overlayEl); - this.cropperEl.appendChild(handleContainerEl); - this.containerEl.appendChild(this.cropperEl); - targetEl.parentElement.replaceChild(this.containerEl, targetEl); - } - }, { - key: 'destroy', - value: function destroy() { - this._restore.parent.replaceChild(this._restore.element, this.containerEl); - } - /** - * Create a new box region with a set of options. - * @param {Object} opts The options. - * @returns {Box} - */ - }, { - key: 'initializeBox', - value: function initializeBox(opts) { - var width = opts.startSize.width; - var height = opts.startSize.height; - var box = new Box(0, 0, width, height); - box.constrainToRatio(opts.aspectRatio, [0.5, 0.5]); - var min = opts.minSize; - var max = opts.maxSize; - box.constrainToSize(max.width, max.height, min.width, min.height, [0.5, 0.5], opts.aspectRatio); - var parentWidth = this.cropperEl.offsetWidth; - var parentHeight = this.cropperEl.offsetHeight; - box.constrainToBoundary(parentWidth, parentHeight, [0.5, 0.5]); - var x = this.cropperEl.offsetWidth / 2 - box.width() / 2; - var y = this.cropperEl.offsetHeight / 2 - box.height() / 2; - box.move(x, y); - return box; - } - }, { - key: 'redraw', - value: function redraw() { - var _this2 = this; - var width = Math.round(this.box.width()), - height = Math.round(this.box.height()), - x1 = Math.round(this.box.x1), - y1 = Math.round(this.box.y1), - x2 = Math.round(this.box.x2), - y2 = Math.round(this.box.y2); - window.requestAnimationFrame(function () { - _this2.regionEl.style.transform = 'translate(' + x1 + 'px, ' + y1 + 'px)'; - _this2.regionEl.style.width = width + 'px'; - _this2.regionEl.style.height = height + 'px'; - _this2.imageClippedEl.style.clip = 'rect(' + y1 + 'px, ' + x2 + 'px, ' + y2 + 'px, ' + x1 + 'px)'; - var center = _this2.box.getAbsolutePoint([.5, .5]); - var xSign = center[0] - _this2.cropperEl.offsetWidth / 2 >> 31, - ySign = center[1] - _this2.cropperEl.offsetHeight / 2 >> 31; - var quadrant = (xSign ^ ySign) + ySign + ySign + 4; - var foregroundHandleIndex = -2 * quadrant + 8; - for (var i = 0; i < _this2.handles.length; i++) { - var handle = _this2.handles[i]; - var handleWidth = handle.el.offsetWidth; - var handleHeight = handle.el.offsetHeight; - var left = x1 + width * handle.position[0] - handleWidth / 2; - var top = y1 + height * handle.position[1] - handleHeight / 2; - handle.el.style.transform = 'translate(' + Math.round(left) + 'px, ' + Math.round(top) + 'px)'; - handle.el.style.zIndex = foregroundHandleIndex == i ? 5 : 4; - } - }); - } - }, { - key: 'attachHandlerEvents', - value: function attachHandlerEvents() { - var eventBus = this.eventBus; - eventBus.addEventListener('handlestart', this.onHandleMoveStart.bind(this)); - eventBus.addEventListener('handlemove', this.onHandleMoveMoving.bind(this)); - eventBus.addEventListener('handleend', this.onHandleMoveEnd.bind(this)); - } - }, { - key: 'attachRegionEvents', - value: function attachRegionEvents() { - var eventBus = this.eventBus; - var self = this; - this.regionEl.addEventListener('mousedown', onMouseDown); - eventBus.addEventListener('regionstart', this.onRegionMoveStart.bind(this)); - eventBus.addEventListener('regionmove', this.onRegionMoveMoving.bind(this)); - eventBus.addEventListener('regionend', this.onRegionMoveEnd.bind(this)); - function onMouseDown(e) { - e.stopPropagation(); - document.addEventListener('mouseup', onMouseUp); - document.addEventListener('mousemove', onMouseMove); - eventBus.dispatchEvent(new CustomEvent('regionstart', { - detail: { mouseX: e.clientX, mouseY: e.clientY } - })); - } - function onMouseMove(e) { - e.stopPropagation(); - eventBus.dispatchEvent(new CustomEvent('regionmove', { - detail: { mouseX: e.clientX, mouseY: e.clientY } - })); - } - function onMouseUp(e) { - e.stopPropagation(); - document.removeEventListener('mouseup', onMouseUp); - document.removeEventListener('mousemove', onMouseMove); - eventBus.dispatchEvent(new CustomEvent('regionend', { - detail: { mouseX: e.clientX, mouseY: e.clientY } - })); - } - } - }, { - key: 'attachOverlayEvents', - value: function attachOverlayEvents() { - var SOUTHEAST_HANDLE_IDX = 4; - var self = this; - var tmpBox = null; - this.overlayEl.addEventListener('mousedown', onMouseDown); - function onMouseDown(e) { - e.stopPropagation(); - document.addEventListener('mouseup', onMouseUp); - document.addEventListener('mousemove', onMouseMove); - var container = self.cropperEl.getBoundingClientRect(); - var mouseX = e.clientX - container.left; - var mouseY = e.clientY - container.top; - tmpBox = self.box; - self.box = new Box(mouseX, mouseY, mouseX + 1, mouseY + 1); - self.eventBus.dispatchEvent(new CustomEvent('handlestart', { - detail: { handle: self.handles[SOUTHEAST_HANDLE_IDX] } - })); - } - function onMouseMove(e) { - e.stopPropagation(); - self.eventBus.dispatchEvent(new CustomEvent('handlemove', { - detail: { mouseX: e.clientX, mouseY: e.clientY } - })); - } - function onMouseUp(e) { - e.stopPropagation(); - document.removeEventListener('mouseup', onMouseUp); - document.removeEventListener('mousemove', onMouseMove); - if (self.box.width() === 1 && self.box.height() === 1) { - self.box = tmpBox; - return; - } - self.eventBus.dispatchEvent(new CustomEvent('handleend', { - detail: { mouseX: e.clientX, mouseY: e.clientY } - })); - } - } - }, { - key: 'onHandleMoveStart', - value: function onHandleMoveStart(e) { - var handle = e.detail.handle; - var originPoint = [1 - handle.position[0], 1 - handle.position[1]]; - var _box$getAbsolutePoint = this.box.getAbsolutePoint(originPoint), - _box$getAbsolutePoint2 = slicedToArray(_box$getAbsolutePoint, 2), - originX = _box$getAbsolutePoint2[0], - originY = _box$getAbsolutePoint2[1]; - this.activeHandle = { handle: handle, originPoint: originPoint, originX: originX, originY: originY }; - if (this.options.onCropStart !== null) { - this.options.onCropStart(this.getValue()); - } - } - }, { - key: 'onHandleMoveMoving', - value: function onHandleMoveMoving(e) { - var _e$detail = e.detail, - mouseX = _e$detail.mouseX, - mouseY = _e$detail.mouseY; - var container = this.cropperEl.getBoundingClientRect(); - mouseX = mouseX - container.left; - mouseY = mouseY - container.top; - if (mouseX < 0) { - mouseX = 0; - } else if (mouseX > container.width) { - mouseX = container.width; - } - if (mouseY < 0) { - mouseY = 0; - } else if (mouseY > container.height) { - mouseY = container.height; - } - var origin = this.activeHandle.originPoint.slice(); - var originX = this.activeHandle.originX; - var originY = this.activeHandle.originY; - var handle = this.activeHandle.handle; - var TOP_MOVABLE = handle.constraints[0] === 1; - var RIGHT_MOVABLE = handle.constraints[1] === 1; - var BOTTOM_MOVABLE = handle.constraints[2] === 1; - var LEFT_MOVABLE = handle.constraints[3] === 1; - var MULTI_AXIS = (LEFT_MOVABLE || RIGHT_MOVABLE) && (TOP_MOVABLE || BOTTOM_MOVABLE); - var x1 = LEFT_MOVABLE || RIGHT_MOVABLE ? originX : this.box.x1; - var x2 = LEFT_MOVABLE || RIGHT_MOVABLE ? originX : this.box.x2; - var y1 = TOP_MOVABLE || BOTTOM_MOVABLE ? originY : this.box.y1; - var y2 = TOP_MOVABLE || BOTTOM_MOVABLE ? originY : this.box.y2; - x1 = LEFT_MOVABLE ? mouseX : x1; - x2 = RIGHT_MOVABLE ? mouseX : x2; - y1 = TOP_MOVABLE ? mouseY : y1; - y2 = BOTTOM_MOVABLE ? mouseY : y2; - var isFlippedX = false, - isFlippedY = false; - if (LEFT_MOVABLE || RIGHT_MOVABLE) { - isFlippedX = LEFT_MOVABLE ? mouseX > originX : mouseX < originX; - } - if (TOP_MOVABLE || BOTTOM_MOVABLE) { - isFlippedY = TOP_MOVABLE ? mouseY > originY : mouseY < originY; - } - if (isFlippedX) { - var tmp = x1;x1 = x2;x2 = tmp; - origin[0] = 1 - origin[0]; - } - if (isFlippedY) { - var _tmp = y1;y1 = y2;y2 = _tmp; - origin[1] = 1 - origin[1]; - } - var box = new Box(x1, y1, x2, y2); - if (this.options.aspectRatio) { - var ratio = this.options.aspectRatio; - var isVerticalMovement = false; - if (MULTI_AXIS) { - isVerticalMovement = mouseY > box.y1 + ratio * box.width() || mouseY < box.y2 - ratio * box.width(); - } else if (TOP_MOVABLE || BOTTOM_MOVABLE) { - isVerticalMovement = true; - } - var ratioMode = isVerticalMovement ? 'width' : 'height'; - box.constrainToRatio(ratio, origin, ratioMode); - } - var min = this.options.minSize; - var max = this.options.maxSize; - box.constrainToSize(max.width, max.height, min.width, min.height, origin, this.options.aspectRatio); - var parentWidth = this.cropperEl.offsetWidth; - var parentHeight = this.cropperEl.offsetHeight; - box.constrainToBoundary(parentWidth, parentHeight, origin); - this.box = box; - this.redraw(); - if (this.options.onCropMove !== null) { - this.options.onCropMove(this.getValue()); - } - } - }, { - key: 'onHandleMoveEnd', - value: function onHandleMoveEnd(e) { - if (this.options.onCropEnd !== null) { - this.options.onCropEnd(this.getValue()); - } + } + createClass(CropprCore, [{ + key: 'initialize', + value: function initialize(element) { + this.createDOM(element); + this.options.convertToPixels(this.cropperEl); + this.attachHandlerEvents(); + this.attachRegionEvents(); + this.attachOverlayEvents(); + this.box = this.initializeBox(this.options); + this.redraw(); + this._initialized = true; + if (this.options.onInitialize !== null) { + this.options.onInitialize(this); + } + } + }, { + key: 'createDOM', + value: function createDOM(targetEl) { + this.containerEl = document.createElement('div'); + this.containerEl.className = 'croppr-container'; + this.eventBus = this.containerEl; + enableTouch(this.containerEl); + this.cropperEl = document.createElement('div'); + this.cropperEl.className = 'croppr'; + this.imageEl = document.createElement('img'); + this.imageEl.setAttribute('src', targetEl.getAttribute('src')); + this.imageEl.setAttribute('alt', targetEl.getAttribute('alt')); + this.imageEl.className = 'croppr-image'; + this.imageClippedEl = this.imageEl.cloneNode(); + this.imageClippedEl.className = 'croppr-imageClipped'; + this.regionEl = document.createElement('div'); + this.regionEl.className = 'croppr-region'; + this.overlayEl = document.createElement('div'); + this.overlayEl.className = 'croppr-overlay'; + var handleContainerEl = document.createElement('div'); + handleContainerEl.className = 'croppr-handleContainer'; + this.handles = []; + for (var i = 0; i < HANDLES.length; i++) { + var handle = new Handle(HANDLES[i].position, HANDLES[i].constraints, HANDLES[i].cursor, this.eventBus); + this.handles.push(handle); + handleContainerEl.appendChild(handle.el); + } + this.cropperEl.appendChild(this.imageEl); + this.cropperEl.appendChild(this.imageClippedEl); + this.cropperEl.appendChild(this.regionEl); + this.cropperEl.appendChild(this.overlayEl); + this.cropperEl.appendChild(handleContainerEl); + this.containerEl.appendChild(this.cropperEl); + targetEl.parentElement.replaceChild(this.containerEl, targetEl); + } + /** + * Changes the image src. + * @param {String} src + */ + }, { + key: 'setImage', + value: function setImage(src) { + var _this2 = this; + this.imageEl.onload = function () { + _this2.box = _this2.initializeBox(_this2.options); + _this2.redraw(); + }; + this.imageEl.src = src; + this.imageClippedEl.src = src; + return this; + } + }, { + key: 'destroy', + value: function destroy() { + this._restore.parent.replaceChild(this._restore.element, this.containerEl); + } + /** + * Create a new box region with a set of options. + * @param {Object} opts The options. + * @returns {Box} + */ + }, { + key: 'initializeBox', + value: function initializeBox(opts) { + var width = opts.startSize.width; + var height = opts.startSize.height; + var box = new Box(0, 0, width, height); + box.constrainToRatio(opts.aspectRatio, [0.5, 0.5]); + var min = opts.minSize; + var max = opts.maxSize; + box.constrainToSize(max.width, max.height, min.width, min.height, [0.5, 0.5], opts.aspectRatio); + var parentWidth = this.cropperEl.offsetWidth; + var parentHeight = this.cropperEl.offsetHeight; + box.constrainToBoundary(parentWidth, parentHeight, [0.5, 0.5]); + var x = this.cropperEl.offsetWidth / 2 - box.width() / 2; + var y = this.cropperEl.offsetHeight / 2 - box.height() / 2; + box.move(x, y); + return box; + } + }, { + key: 'redraw', + value: function redraw() { + var _this3 = this; + var width = Math.round(this.box.width()), + height = Math.round(this.box.height()), + x1 = Math.round(this.box.x1), + y1 = Math.round(this.box.y1), + x2 = Math.round(this.box.x2), + y2 = Math.round(this.box.y2); + window.requestAnimationFrame(function () { + _this3.regionEl.style.transform = 'translate(' + x1 + 'px, ' + y1 + 'px)'; + _this3.regionEl.style.width = width + 'px'; + _this3.regionEl.style.height = height + 'px'; + _this3.imageClippedEl.style.clip = 'rect(' + y1 + 'px, ' + x2 + 'px, ' + y2 + 'px, ' + x1 + 'px)'; + var center = _this3.box.getAbsolutePoint([.5, .5]); + var xSign = center[0] - _this3.cropperEl.offsetWidth / 2 >> 31; + var ySign = center[1] - _this3.cropperEl.offsetHeight / 2 >> 31; + var quadrant = (xSign ^ ySign) + ySign + ySign + 4; + var foregroundHandleIndex = -2 * quadrant + 8; + for (var i = 0; i < _this3.handles.length; i++) { + var handle = _this3.handles[i]; + var handleWidth = handle.el.offsetWidth; + var handleHeight = handle.el.offsetHeight; + var left = x1 + width * handle.position[0] - handleWidth / 2; + var top = y1 + height * handle.position[1] - handleHeight / 2; + handle.el.style.transform = 'translate(' + Math.round(left) + 'px, ' + Math.round(top) + 'px)'; + handle.el.style.zIndex = foregroundHandleIndex == i ? 5 : 4; } - }, { - key: 'onRegionMoveStart', - value: function onRegionMoveStart(e) { - var _e$detail2 = e.detail, - mouseX = _e$detail2.mouseX, - mouseY = _e$detail2.mouseY; - var container = this.cropperEl.getBoundingClientRect(); - mouseX = mouseX - container.left; - mouseY = mouseY - container.top; - this.currentMove = { - offsetX: mouseX - this.box.x1, - offsetY: mouseY - this.box.y1 - }; - if (this.options.onCropStart !== null) { - this.options.onCropStart(this.getValue()); - } + }); + } + }, { + key: 'attachHandlerEvents', + value: function attachHandlerEvents() { + var eventBus = this.eventBus; + eventBus.addEventListener('handlestart', this.onHandleMoveStart.bind(this)); + eventBus.addEventListener('handlemove', this.onHandleMoveMoving.bind(this)); + eventBus.addEventListener('handleend', this.onHandleMoveEnd.bind(this)); + } + }, { + key: 'attachRegionEvents', + value: function attachRegionEvents() { + var eventBus = this.eventBus; + var self = this; + this.regionEl.addEventListener('mousedown', onMouseDown); + eventBus.addEventListener('regionstart', this.onRegionMoveStart.bind(this)); + eventBus.addEventListener('regionmove', this.onRegionMoveMoving.bind(this)); + eventBus.addEventListener('regionend', this.onRegionMoveEnd.bind(this)); + function onMouseDown(e) { + e.stopPropagation(); + document.addEventListener('mouseup', onMouseUp); + document.addEventListener('mousemove', onMouseMove); + eventBus.dispatchEvent(new CustomEvent('regionstart', { + detail: { mouseX: e.clientX, mouseY: e.clientY } + })); + } + function onMouseMove(e) { + e.stopPropagation(); + eventBus.dispatchEvent(new CustomEvent('regionmove', { + detail: { mouseX: e.clientX, mouseY: e.clientY } + })); + } + function onMouseUp(e) { + e.stopPropagation(); + document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener('mousemove', onMouseMove); + eventBus.dispatchEvent(new CustomEvent('regionend', { + detail: { mouseX: e.clientX, mouseY: e.clientY } + })); + } + } + }, { + key: 'attachOverlayEvents', + value: function attachOverlayEvents() { + var SOUTHEAST_HANDLE_IDX = 4; + var self = this; + var tmpBox = null; + this.overlayEl.addEventListener('mousedown', onMouseDown); + function onMouseDown(e) { + e.stopPropagation(); + document.addEventListener('mouseup', onMouseUp); + document.addEventListener('mousemove', onMouseMove); + var container = self.cropperEl.getBoundingClientRect(); + var mouseX = e.clientX - container.left; + var mouseY = e.clientY - container.top; + tmpBox = self.box; + self.box = new Box(mouseX, mouseY, mouseX + 1, mouseY + 1); + self.eventBus.dispatchEvent(new CustomEvent('handlestart', { + detail: { handle: self.handles[SOUTHEAST_HANDLE_IDX] } + })); + } + function onMouseMove(e) { + e.stopPropagation(); + self.eventBus.dispatchEvent(new CustomEvent('handlemove', { + detail: { mouseX: e.clientX, mouseY: e.clientY } + })); + } + function onMouseUp(e) { + e.stopPropagation(); + document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener('mousemove', onMouseMove); + if (self.box.width() === 1 && self.box.height() === 1) { + self.box = tmpBox; + return; } - }, { - key: 'onRegionMoveMoving', - value: function onRegionMoveMoving(e) { - var _e$detail3 = e.detail, - mouseX = _e$detail3.mouseX, - mouseY = _e$detail3.mouseY; - var _currentMove = this.currentMove, - offsetX = _currentMove.offsetX, - offsetY = _currentMove.offsetY; - var container = this.cropperEl.getBoundingClientRect(); - mouseX = mouseX - container.left; - mouseY = mouseY - container.top; - this.box.move(mouseX - offsetX, mouseY - offsetY); - if (this.box.x1 < 0) { - this.box.move(0, null); - } - if (this.box.x2 > container.width) { - this.box.move(container.width - this.box.width(), null); - } - if (this.box.y1 < 0) { - this.box.move(null, 0); - } - if (this.box.y2 > container.height) { - this.box.move(null, container.height - this.box.height()); - } - this.redraw(); - if (this.options.onCropMove !== null) { - this.options.onCropMove(this.getValue()); - } + self.eventBus.dispatchEvent(new CustomEvent('handleend', { + detail: { mouseX: e.clientX, mouseY: e.clientY } + })); + } + } + }, { + key: 'onHandleMoveStart', + value: function onHandleMoveStart(e) { + var handle = e.detail.handle; + var originPoint = [1 - handle.position[0], 1 - handle.position[1]]; + var _box$getAbsolutePoint = this.box.getAbsolutePoint(originPoint), + _box$getAbsolutePoint2 = slicedToArray(_box$getAbsolutePoint, 2), + originX = _box$getAbsolutePoint2[0], + originY = _box$getAbsolutePoint2[1]; + this.activeHandle = { handle: handle, originPoint: originPoint, originX: originX, originY: originY }; + if (this.options.onCropStart !== null) { + this.options.onCropStart(this.getValue()); + } + } + }, { + key: 'onHandleMoveMoving', + value: function onHandleMoveMoving(e) { + var _e$detail = e.detail, + mouseX = _e$detail.mouseX, + mouseY = _e$detail.mouseY; + var container = this.cropperEl.getBoundingClientRect(); + mouseX = mouseX - container.left; + mouseY = mouseY - container.top; + if (mouseX < 0) { + mouseX = 0; + } else if (mouseX > container.width) { + mouseX = container.width; + } + if (mouseY < 0) { + mouseY = 0; + } else if (mouseY > container.height) { + mouseY = container.height; + } + var origin = this.activeHandle.originPoint.slice(); + var originX = this.activeHandle.originX; + var originY = this.activeHandle.originY; + var handle = this.activeHandle.handle; + var TOP_MOVABLE = handle.constraints[0] === 1; + var RIGHT_MOVABLE = handle.constraints[1] === 1; + var BOTTOM_MOVABLE = handle.constraints[2] === 1; + var LEFT_MOVABLE = handle.constraints[3] === 1; + var MULTI_AXIS = (LEFT_MOVABLE || RIGHT_MOVABLE) && (TOP_MOVABLE || BOTTOM_MOVABLE); + var x1 = LEFT_MOVABLE || RIGHT_MOVABLE ? originX : this.box.x1; + var x2 = LEFT_MOVABLE || RIGHT_MOVABLE ? originX : this.box.x2; + var y1 = TOP_MOVABLE || BOTTOM_MOVABLE ? originY : this.box.y1; + var y2 = TOP_MOVABLE || BOTTOM_MOVABLE ? originY : this.box.y2; + x1 = LEFT_MOVABLE ? mouseX : x1; + x2 = RIGHT_MOVABLE ? mouseX : x2; + y1 = TOP_MOVABLE ? mouseY : y1; + y2 = BOTTOM_MOVABLE ? mouseY : y2; + var isFlippedX = false, + isFlippedY = false; + if (LEFT_MOVABLE || RIGHT_MOVABLE) { + isFlippedX = LEFT_MOVABLE ? mouseX > originX : mouseX < originX; + } + if (TOP_MOVABLE || BOTTOM_MOVABLE) { + isFlippedY = TOP_MOVABLE ? mouseY > originY : mouseY < originY; + } + if (isFlippedX) { + var tmp = x1;x1 = x2;x2 = tmp; + origin[0] = 1 - origin[0]; + } + if (isFlippedY) { + var _tmp = y1;y1 = y2;y2 = _tmp; + origin[1] = 1 - origin[1]; + } + var box = new Box(x1, y1, x2, y2); + if (this.options.aspectRatio) { + var ratio = this.options.aspectRatio; + var isVerticalMovement = false; + if (MULTI_AXIS) { + isVerticalMovement = mouseY > box.y1 + ratio * box.width() || mouseY < box.y2 - ratio * box.width(); + } else if (TOP_MOVABLE || BOTTOM_MOVABLE) { + isVerticalMovement = true; } - }, { - key: 'onRegionMoveEnd', - value: function onRegionMoveEnd(e) { - if (this.options.onCropEnd !== null) { - this.options.onCropEnd(this.getValue()); - } + var ratioMode = isVerticalMovement ? 'width' : 'height'; + box.constrainToRatio(ratio, origin, ratioMode); + } + var min = this.options.minSize; + var max = this.options.maxSize; + box.constrainToSize(max.width, max.height, min.width, min.height, origin, this.options.aspectRatio); + var parentWidth = this.cropperEl.offsetWidth; + var parentHeight = this.cropperEl.offsetHeight; + box.constrainToBoundary(parentWidth, parentHeight, origin); + this.box = box; + this.redraw(); + if (this.options.onCropMove !== null) { + this.options.onCropMove(this.getValue()); + } + } + }, { + key: 'onHandleMoveEnd', + value: function onHandleMoveEnd(e) { + if (this.options.onCropEnd !== null) { + this.options.onCropEnd(this.getValue()); + } + } + }, { + key: 'onRegionMoveStart', + value: function onRegionMoveStart(e) { + var _e$detail2 = e.detail, + mouseX = _e$detail2.mouseX, + mouseY = _e$detail2.mouseY; + var container = this.cropperEl.getBoundingClientRect(); + mouseX = mouseX - container.left; + mouseY = mouseY - container.top; + this.currentMove = { + offsetX: mouseX - this.box.x1, + offsetY: mouseY - this.box.y1 + }; + if (this.options.onCropStart !== null) { + this.options.onCropStart(this.getValue()); + } + } + }, { + key: 'onRegionMoveMoving', + value: function onRegionMoveMoving(e) { + var _e$detail3 = e.detail, + mouseX = _e$detail3.mouseX, + mouseY = _e$detail3.mouseY; + var _currentMove = this.currentMove, + offsetX = _currentMove.offsetX, + offsetY = _currentMove.offsetY; + var container = this.cropperEl.getBoundingClientRect(); + mouseX = mouseX - container.left; + mouseY = mouseY - container.top; + this.box.move(mouseX - offsetX, mouseY - offsetY); + if (this.box.x1 < 0) { + this.box.move(0, null); + } + if (this.box.x2 > container.width) { + this.box.move(container.width - this.box.width(), null); + } + if (this.box.y1 < 0) { + this.box.move(null, 0); + } + if (this.box.y2 > container.height) { + this.box.move(null, container.height - this.box.height()); + } + this.redraw(); + if (this.options.onCropMove !== null) { + this.options.onCropMove(this.getValue()); + } + } + }, { + key: 'onRegionMoveEnd', + value: function onRegionMoveEnd(e) { + if (this.options.onCropEnd !== null) { + this.options.onCropEnd(this.getValue()); + } + } + }, { + key: 'getValue', + value: function getValue() { + var mode = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + if (mode === null) { + mode = this.options.returnMode; + } + if (mode == 'real') { + var actualWidth = this.imageEl.naturalWidth; + var actualHeight = this.imageEl.naturalHeight; + var factorX = actualWidth / this.imageEl.offsetWidth; + var factorY = actualHeight / this.imageEl.offsetHeight; + return { + x: Math.round(this.box.x1 * factorX), + y: Math.round(this.box.y1 * factorY), + width: Math.round(this.box.width() * factorX), + height: Math.round(this.box.height() * factorY) + }; + } else if (mode == 'ratio') { + var elementWidth = this.imageEl.offsetWidth; + var elementHeight = this.imageEl.offsetHeight; + return { + x: round(this.box.x1 / elementWidth, 3), + y: round(this.box.y1 / elementHeight, 3), + width: round(this.box.width() / elementWidth, 3), + height: round(this.box.height() / elementHeight, 3) + }; + } else if (mode == 'raw') { + return { + x: Math.round(this.box.x1), + y: Math.round(this.box.y1), + width: Math.round(this.box.width()), + height: Math.round(this.box.height()) + }; + } + } + }], [{ + key: 'parseOptions', + value: function parseOptions(opts) { + var defaults$$1 = { + aspectRatio: null, + maxSize: { width: null, height: null }, + minSize: { width: null, height: null }, + startSize: { width: 100, height: 100, unit: '%' }, + returnMode: 'real', + onInitialize: null, + onCropStart: null, + onCropMove: null, + onCropEnd: null + }; + var aspectRatio = null; + if (opts.aspectRatio !== undefined) { + if (typeof opts.aspectRatio === 'number') { + aspectRatio = opts.aspectRatio; + } else if (opts.aspectRatio instanceof Array) { + aspectRatio = opts.aspectRatio[1] / opts.aspectRatio[0]; } - }, { - key: 'getValue', - value: function getValue() { - var mode = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; - if (mode === null) { - mode = this.options.returnMode; - } - if (mode == 'real') { - var actualWidth = this.imageEl.naturalWidth; - var actualHeight = this.imageEl.naturalHeight; - var factorX = actualWidth / this.imageEl.offsetWidth; - var factorY = actualHeight / this.imageEl.offsetHeight; - return { - x: Math.round(this.box.x1 * factorX), - y: Math.round(this.box.y1 * factorY), - width: Math.round(this.box.width() * factorX), - height: Math.round(this.box.height() * factorY) - }; - } else if (mode == 'ratio') { - var elementWidth = this.imageEl.offsetWidth; - var elementHeight = this.imageEl.offsetHeight; - return { - x: round(this.box.x1 / elementWidth, 3), - y: round(this.box.y1 / elementHeight, 3), - width: round(this.box.width() / elementWidth, 3), - height: round(this.box.height() / elementHeight, 3) - }; - } else if (mode == 'raw') { - return { - x: Math.round(this.box.x1), - y: Math.round(this.box.y1), - width: Math.round(this.box.width()), - height: Math.round(this.box.height()) - }; - } + } + var maxSize = null; + if (opts.maxSize !== undefined && opts.maxSize !== null) { + maxSize = { + width: opts.maxSize[0] || null, + height: opts.maxSize[1] || null, + unit: opts.maxSize[2] || 'px' + }; + } + var minSize = null; + if (opts.minSize !== undefined && opts.minSize !== null) { + minSize = { + width: opts.minSize[0] || null, + height: opts.minSize[1] || null, + unit: opts.minSize[2] || 'px' + }; + } + var startSize = null; + if (opts.startSize !== undefined && opts.startSize !== null) { + startSize = { + width: opts.startSize[0] || null, + height: opts.startSize[1] || null, + unit: opts.startSize[2] || '%' + }; + } + var onInitialize = null; + if (typeof opts.onInitialize === 'function') { + onInitialize = opts.onInitialize; + } + var onCropStart = null; + if (typeof opts.onCropStart === 'function') { + onCropStart = opts.onCropStart; + } + var onCropEnd = null; + if (typeof opts.onCropEnd === 'function') { + onCropEnd = opts.onCropEnd; + } + var onCropMove = null; + if (typeof opts.onUpdate === 'function') { + console.warn('Croppr.js: `onUpdate` is deprecated and will be removed in the next major release. Please use `onCropMove` or `onCropEnd` instead.'); + onCropMove = opts.onUpdate; + } + if (typeof opts.onCropMove === 'function') { + onCropMove = opts.onCropMove; + } + var returnMode = null; + if (opts.returnMode !== undefined) { + var s = opts.returnMode.toLowerCase(); + if (['real', 'ratio', 'raw'].indexOf(s) === -1) { + throw "Invalid return mode."; } - }], [{ - key: 'parseOptions', - value: function parseOptions(opts) { - var defaults$$1 = { - aspectRatio: null, - maxSize: { width: null, height: null }, - minSize: { width: null, height: null }, - startSize: { width: 100, height: 100, unit: '%' }, - returnMode: 'real', - onInitialize: null, - onCropStart: null, - onCropMove: null, - onCropEnd: null - }; - var aspectRatio = null; - if (opts.aspectRatio !== undefined) { - if (typeof opts.aspectRatio === 'number') { - aspectRatio = opts.aspectRatio; - } else if (opts.aspectRatio instanceof Array) { - aspectRatio = opts.aspectRatio[1] / opts.aspectRatio[0]; - } - } - var maxSize = null; - if (opts.maxSize !== undefined && opts.maxSize !== null) { - maxSize = { - width: opts.maxSize[0] || null, - height: opts.maxSize[1] || null, - unit: opts.maxSize[2] || 'px' - }; - } - var minSize = null; - if (opts.minSize !== undefined && opts.minSize !== null) { - minSize = { - width: opts.minSize[0] || null, - height: opts.minSize[1] || null, - unit: opts.minSize[2] || 'px' - }; - } - var startSize = null; - if (opts.startSize !== undefined && opts.startSize !== null) { - startSize = { - width: opts.startSize[0] || null, - height: opts.startSize[1] || null, - unit: opts.startSize[2] || '%' - }; - } - var onInitialize = null; - if (typeof opts.onInitialize === 'function') { - onInitialize = opts.onInitialize; - } - var onCropStart = null; - if (typeof opts.onCropStart === 'function') { - onCropStart = opts.onCropStart; - } - var onCropEnd = null; - if (typeof opts.onCropEnd === 'function') { - onCropEnd = opts.onCropEnd; - } - var onCropMove = null; - if (typeof opts.onUpdate === 'function') { - console.warn('Croppr.js: `onUpdate` is deprecated and will be removed in the next major release. Please use `onCropMove` or `onCropEnd` instead.'); - onCropMove = opts.onUpdate; - } - if (typeof opts.onCropMove === 'function') { - onCropMove = opts.onCropMove; - } - var returnMode = null; - if (opts.returnMode !== undefined) { - var s = opts.returnMode.toLowerCase(); - if (['real', 'ratio', 'raw'].indexOf(s) === -1) { - throw "Invalid return mode."; - } - returnMode = s; + returnMode = s; + } + var convertToPixels = function convertToPixels(container) { + var width = container.offsetWidth; + var height = container.offsetHeight; + var sizeKeys = ['maxSize', 'minSize', 'startSize']; + for (var i = 0; i < sizeKeys.length; i++) { + var key = sizeKeys[i]; + if (this[key] !== null) { + if (this[key].unit == '%') { + if (this[key].width !== null) { + this[key].width = this[key].width / 100 * width; + } + if (this[key].height !== null) { + this[key].height = this[key].height / 100 * height; + } } - var convertToPixels = function convertToPixels(container) { - var width = container.offsetWidth; - var height = container.offsetHeight; - var sizeKeys = ['maxSize', 'minSize', 'startSize']; - for (var i = 0; i < sizeKeys.length; i++) { - var key = sizeKeys[i]; - if (this[key] !== null) { - if (this[key].unit == '%') { - if (this[key].width !== null) { - this[key].width = this[key].width / 100 * width; - } - if (this[key].height !== null) { - this[key].height = this[key].height / 100 * height; - } - } - delete this[key].unit; - } - } - }; - var defaultValue = function defaultValue(v, d) { - return v !== null ? v : d; - }; - return { - aspectRatio: defaultValue(aspectRatio, defaults$$1.aspectRatio), - maxSize: defaultValue(maxSize, defaults$$1.maxSize), - minSize: defaultValue(minSize, defaults$$1.minSize), - startSize: defaultValue(startSize, defaults$$1.startSize), - returnMode: defaultValue(returnMode, defaults$$1.returnMode), - onInitialize: defaultValue(onInitialize, defaults$$1.onInitialize), - onCropStart: defaultValue(onCropStart, defaults$$1.onCropStart), - onCropMove: defaultValue(onCropMove, defaults$$1.onCropMove), - onCropEnd: defaultValue(onCropEnd, defaults$$1.onCropEnd), - convertToPixels: convertToPixels - }; + delete this[key].unit; + } } - }]); - return CropprCore; + }; + var defaultValue = function defaultValue(v, d) { + return v !== null ? v : d; + }; + return { + aspectRatio: defaultValue(aspectRatio, defaults$$1.aspectRatio), + maxSize: defaultValue(maxSize, defaults$$1.maxSize), + minSize: defaultValue(minSize, defaults$$1.minSize), + startSize: defaultValue(startSize, defaults$$1.startSize), + returnMode: defaultValue(returnMode, defaults$$1.returnMode), + onInitialize: defaultValue(onInitialize, defaults$$1.onInitialize), + onCropStart: defaultValue(onCropStart, defaults$$1.onCropStart), + onCropMove: defaultValue(onCropMove, defaults$$1.onCropMove), + onCropEnd: defaultValue(onCropEnd, defaults$$1.onCropEnd), + convertToPixels: convertToPixels + }; + } + }]); + return CropprCore; }(); function round(value, decimals) { - return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals); + return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals); } var Croppr$1 = function (_CropprCore) { - inherits(Croppr, _CropprCore); + inherits(Croppr, _CropprCore); + /** + * @constructor + * Calls the CropprCore's constructor. + */ + function Croppr(element, options) { + var _deferred = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + classCallCheck(this, Croppr); + return possibleConstructorReturn(this, (Croppr.__proto__ || Object.getPrototypeOf(Croppr)).call(this, element, options, _deferred)); + } + /** + * Gets the value of the crop region. + * @param {String} [mode] Which mode of calculation to use: 'real', 'ratio' or + * 'raw'. + */ + createClass(Croppr, [{ + key: 'getValue', + value: function getValue(mode) { + return get(Croppr.prototype.__proto__ || Object.getPrototypeOf(Croppr.prototype), 'getValue', this).call(this, mode); + } /** - * @constructor - * Calls the CropprCore's constructor. + * Changes the image src. + * @param {String} src */ - function Croppr(element, options) { - var _deferred = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; - classCallCheck(this, Croppr); - return possibleConstructorReturn(this, (Croppr.__proto__ || Object.getPrototypeOf(Croppr)).call(this, element, options, _deferred)); + }, { + key: 'setImage', + value: function setImage(src) { + return get(Croppr.prototype.__proto__ || Object.getPrototypeOf(Croppr.prototype), 'setImage', this).call(this, src); + } + }, { + key: 'destroy', + value: function destroy() { + return get(Croppr.prototype.__proto__ || Object.getPrototypeOf(Croppr.prototype), 'destroy', this).call(this); } /** - * Gets the value of the crop region. - * @param {String} mode Which mode of calculation to use: 'real', 'ratio' or - * 'raw'. + * Moves the crop region to a specified coordinate. + * @param {Number} x + * @param {Number} y */ - createClass(Croppr, [{ - key: 'getValue', - value: function getValue(mode) { - return get(Croppr.prototype.__proto__ || Object.getPrototypeOf(Croppr.prototype), 'getValue', this).call(this, mode); - } - }, { - key: 'destroy', - value: function destroy() { - return get(Croppr.prototype.__proto__ || Object.getPrototypeOf(Croppr.prototype), 'destroy', this).call(this); - } - /** - * Moves the crop region to a specified coordinate. - * @param {Number} x - * @param {Number} y - */ - }, { - key: 'moveTo', - value: function moveTo(x, y) { - this.box.move(x, y); - this.redraw(); - if (this.options.onCropEnd !== null) { - this.options.onCropEnd(this.getValue()); - } - return this; - } - /** - * Resizes the crop region to a specified width and height. - * @param {Number} width - * @param {Number} height - * @param {Array} origin The origin point to resize from. - * Defaults to [0.5, 0.5] (center). - */ - }, { - key: 'resizeTo', - value: function resizeTo(width, height) { - var origin = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [.5, .5]; - this.box.resize(width, height, origin); - this.redraw(); - if (this.options.onCropEnd !== null) { - this.options.onCropEnd(this.getValue()); - } - return this; - } - /** - * Scale the crop region by a factor. - * @param {Number} factor - * @param {Array} origin The origin point to resize from. - * Defaults to [0.5, 0.5] (center). - */ - }, { - key: 'scaleBy', - value: function scaleBy(factor) { - var origin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [.5, .5]; - this.box.scale(factor, origin); - this.redraw(); - if (this.options.onCropEnd !== null) { - this.options.onCropEnd(this.getValue()); - } - return this; - } - }, { - key: 'reset', - value: function reset() { - this.box = this.initializeBox(this.options); - this.redraw(); - if (this.options.onCropEnd !== null) { - this.options.onCropEnd(this.getValue()); - } - return this; - } - }]); - return Croppr; + }, { + key: 'moveTo', + value: function moveTo(x, y) { + this.box.move(x, y); + this.redraw(); + if (this.options.onCropEnd !== null) { + this.options.onCropEnd(this.getValue()); + } + return this; + } + /** + * Resizes the crop region to a specified width and height. + * @param {Number} width + * @param {Number} height + * @param {Array} origin The origin point to resize from. + * Defaults to [0.5, 0.5] (center). + */ + }, { + key: 'resizeTo', + value: function resizeTo(width, height) { + var origin = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [.5, .5]; + this.box.resize(width, height, origin); + this.redraw(); + if (this.options.onCropEnd !== null) { + this.options.onCropEnd(this.getValue()); + } + return this; + } + /** + * Scale the crop region by a factor. + * @param {Number} factor + * @param {Array} origin The origin point to resize from. + * Defaults to [0.5, 0.5] (center). + */ + }, { + key: 'scaleBy', + value: function scaleBy(factor) { + var origin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [.5, .5]; + this.box.scale(factor, origin); + this.redraw(); + if (this.options.onCropEnd !== null) { + this.options.onCropEnd(this.getValue()); + } + return this; + } + }, { + key: 'reset', + value: function reset() { + this.box = this.initializeBox(this.options); + this.redraw(); + if (this.options.onCropEnd !== null) { + this.options.onCropEnd(this.getValue()); + } + return this; + } + }]); + return Croppr; }(CropprCore); return Croppr$1; diff --git a/dist/croppr.min.js b/dist/croppr.min.js index 1dcd167..69439a2 100644 --- a/dist/croppr.min.js +++ b/dist/croppr.min.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Croppr=e()}(this,function(){"use strict";function t(t){t.addEventListener("touchstart",e),t.addEventListener("touchend",e),t.addEventListener("touchmove",e)}function e(t){t.preventDefault();var e=t.changedTouches[0],i={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup"};e.target.dispatchEvent(new MouseEvent(i[t.type],{bubbles:!0,cancelable:!0,view:window,clientX:e.clientX,clientY:e.clientY,screenX:e.screenX,screenY:e.screenY}))}function i(t,e){return Number(Math.round(t+"e"+e)+"e-"+e)}!function(){for(var t=0,e=["ms","moz","webkit","o"],i=0;i0&&void 0!==arguments[0]?arguments[0]:null,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;return this.x1=null==t?this.x1:t,this.y1=null==e?this.y1:e,this.x2=null==i?this.x2:i,this.y2=null==n?this.y2:n,this}},{key:"width",value:function(){return Math.abs(this.x2-this.x1)}},{key:"height",value:function(){return Math.abs(this.y2-this.y1)}},{key:"resize",value:function(t,e){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[0,0],n=this.x1+this.width()*i[0],o=this.y1+this.height()*i[1];return this.x1=n-t*i[0],this.y1=o-e*i[1],this.x2=this.x1+t,this.y2=this.y1+e,this}},{key:"scale",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[0,0],i=this.width()*t,n=this.height()*t;return this.resize(i,n,e),this}},{key:"move",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,i=this.width(),n=this.height();return t=null===t?this.x1:t,e=null===e?this.y1:e,this.x1=t,this.y1=e,this.x2=t+i,this.y2=e+n,this}},{key:"getRelativePoint",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[0,0],e=this.width()*t[0],i=this.height()*t[1];return[e,i]}},{key:"getAbsolutePoint",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[0,0],e=this.x1+this.width()*t[0],i=this.y1+this.height()*t[1];return[e,i]}},{key:"constrainToRatio",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[0,0],i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"height";if(null!==t){this.width(),this.height();switch(i){case"height":this.resize(this.width(),this.width()*t,e);break;case"width":this.resize(1*this.height()/t,this.height(),e);break;default:this.resize(this.width(),this.width()*t,e)}return this}}},{key:"constrainToBoundary",value:function(t,e){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[0,0],n=this.getAbsolutePoint(i),o=h(n,2),s=o[0],r=o[1],a=s,l=r,u=t-s,d=e-r,c=-2*i[0]+1,p=-2*i[1]+1,v=null,m=null;switch(c){case-1:v=a;break;case 0:v=2*Math.min(a,u);break;case 1:v=u}switch(p){case-1:m=l;break;case 0:m=2*Math.min(l,d);break;case 1:m=d}if(this.width()>v){var f=v/this.width();this.scale(f,i)}if(this.height()>m){var g=m/this.height();this.scale(g,i)}return this}},{key:"constrainToSize",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:[0,0],s=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null;if(s&&(s>1?(t=1*e/s,n*=s):s<1&&(e=t*s,i=1*n/s)),t&&this.width()>t){var r=t,a=null===s?this.height():e;this.resize(r,a,o)}if(e&&this.height()>e){var h=null===s?this.width():t,l=e;this.resize(h,l,o)}if(i&&this.width()2&&void 0!==arguments[2]&&arguments[2];if(n(this,e),this.options=e.parseOptions(i||{}),!t.nodeName&&(t=document.querySelector(t),null==t))throw"Unable to find element.";if(!t.getAttribute("src"))throw"Image src not provided.";this._initialized=!1,this._restore={parent:t.parentNode,element:t},s||(0===t.width||0===t.height?t.onload=function(){o.initialize(t)}:this.initialize(t))}return o(e,[{key:"initialize",value:function(t){this.createDOM(t),this.options.convertToPixels(this.cropperEl),this.attachHandlerEvents(),this.attachRegionEvents(),this.attachOverlayEvents(),this.box=this.initializeBox(this.options),this.redraw(),this._initialized=!0,null!==this.options.onInitialize&&this.options.onInitialize(this)}},{key:"createDOM",value:function(e){this.containerEl=document.createElement("div"),this.containerEl.className="croppr-container",this.eventBus=this.containerEl,t(this.containerEl),this.cropperEl=document.createElement("div"),this.cropperEl.className="croppr",this.imageEl=document.createElement("img"),this.imageEl.setAttribute("src",e.getAttribute("src")),this.imageEl.setAttribute("alt",e.getAttribute("alt")),this.imageEl.className="croppr-image",this.imageClippedEl=this.imageEl.cloneNode(),this.imageClippedEl.className="croppr-imageClipped",this.regionEl=document.createElement("div"),this.regionEl.className="croppr-region",this.overlayEl=document.createElement("div"),this.overlayEl.className="croppr-overlay";var i=document.createElement("div");i.className="croppr-handleContainer",this.handles=[];for(var n=0;n>31,l=a[1]-t.cropperEl.offsetHeight/2>>31,u=(h^l)+l+l+4,d=-2*u+8,c=0;co.width&&(i=o.width),n<0?n=0:n>o.height&&(n=o.height);var s=this.activeHandle.originPoint.slice(),r=this.activeHandle.originX,a=this.activeHandle.originY,h=this.activeHandle.handle,l=1===h.constraints[0],d=1===h.constraints[1],c=1===h.constraints[2],p=1===h.constraints[3],v=(p||d)&&(l||c),m=p||d?r:this.box.x1,f=p||d?r:this.box.x2,g=l||c?a:this.box.y1,E=l||c?a:this.box.y2;m=p?i:m,f=d?i:f,g=l?n:g,E=c?n:E;var w=!1,y=!1;if((p||d)&&(w=p?i>r:ia:nC.y1+z*C.width()||na.width&&this.box.move(a.width-this.box.width(),null),this.box.y1<0&&this.box.move(null,0),this.box.y2>a.height&&this.box.move(null,a.height-this.box.height()),this.redraw(),null!==this.options.onCropMove&&this.options.onCropMove(this.getValue())}},{key:"onRegionMoveEnd",value:function(t){null!==this.options.onCropEnd&&this.options.onCropEnd(this.getValue())}},{key:"getValue",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;if(null===t&&(t=this.options.returnMode),"real"==t){var e=this.imageEl.naturalWidth,n=this.imageEl.naturalHeight,o=e/this.imageEl.offsetWidth,s=n/this.imageEl.offsetHeight;return{x:Math.round(this.box.x1*o),y:Math.round(this.box.y1*s),width:Math.round(this.box.width()*o),height:Math.round(this.box.height()*s)}}if("ratio"==t){var r=this.imageEl.offsetWidth,a=this.imageEl.offsetHeight;return{x:i(this.box.x1/r,3),y:i(this.box.y1/a,3),width:i(this.box.width()/r,3),height:i(this.box.height()/a,3)}}if("raw"==t)return{x:Math.round(this.box.x1),y:Math.round(this.box.y1),width:Math.round(this.box.width()),height:Math.round(this.box.height())}}}],[{key:"parseOptions",value:function(t){var e={aspectRatio:null,maxSize:{width:null,height:null},minSize:{width:null,height:null},startSize:{width:100,height:100,unit:"%"},returnMode:"real",onInitialize:null,onCropStart:null,onCropMove:null,onCropEnd:null},i=null;void 0!==t.aspectRatio&&("number"==typeof t.aspectRatio?i=t.aspectRatio:t.aspectRatio instanceof Array&&(i=t.aspectRatio[1]/t.aspectRatio[0]));var n=null;void 0!==t.maxSize&&null!==t.maxSize&&(n={width:t.maxSize[0]||null,height:t.maxSize[1]||null,unit:t.maxSize[2]||"px"});var o=null;void 0!==t.minSize&&null!==t.minSize&&(o={width:t.minSize[0]||null,height:t.minSize[1]||null,unit:t.minSize[2]||"px"});var s=null;void 0!==t.startSize&&null!==t.startSize&&(s={width:t.startSize[0]||null,height:t.startSize[1]||null,unit:t.startSize[2]||"%"});var r=null;"function"==typeof t.onInitialize&&(r=t.onInitialize);var a=null;"function"==typeof t.onCropStart&&(a=t.onCropStart);var h=null;"function"==typeof t.onCropEnd&&(h=t.onCropEnd);var l=null;"function"==typeof t.onUpdate&&(console.warn("Croppr.js: `onUpdate` is deprecated and will be removed in the next major release. Please use `onCropMove` or `onCropEnd` instead."),l=t.onUpdate),"function"==typeof t.onCropMove&&(l=t.onCropMove);var u=null;if(void 0!==t.returnMode){var d=t.returnMode.toLowerCase();if(["real","ratio","raw"].indexOf(d)===-1)throw"Invalid return mode.";u=d}var c=function(t){for(var e=t.offsetWidth,i=t.offsetHeight,n=["maxSize","minSize","startSize"],o=0;o2&&void 0!==arguments[2]&&arguments[2];return n(this,e),a(this,(e.__proto__||Object.getPrototypeOf(e)).call(this,t,i,o))}return r(e,t),o(e,[{key:"getValue",value:function(t){return s(e.prototype.__proto__||Object.getPrototypeOf(e.prototype),"getValue",this).call(this,t)}},{key:"destroy",value:function(){return s(e.prototype.__proto__||Object.getPrototypeOf(e.prototype),"destroy",this).call(this)}},{key:"moveTo",value:function(t,e){return this.box.move(t,e),this.redraw(),null!==this.options.onCropEnd&&this.options.onCropEnd(this.getValue()),this}},{key:"resizeTo",value:function(t,e){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[.5,.5];return this.box.resize(t,e,i),this.redraw(),null!==this.options.onCropEnd&&this.options.onCropEnd(this.getValue()),this}},{key:"scaleBy",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[.5,.5];return this.box.scale(t,e),this.redraw(),null!==this.options.onCropEnd&&this.options.onCropEnd(this.getValue()),this}},{key:"reset",value:function(){return this.box=this.initializeBox(this.options),this.redraw(),null!==this.options.onCropEnd&&this.options.onCropEnd(this.getValue()),this}}]),e}(c);return p}); +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Croppr=e()}(this,function(){"use strict";function t(t){t.addEventListener("touchstart",e),t.addEventListener("touchend",e),t.addEventListener("touchmove",e)}function e(t){t.preventDefault();var e=t.changedTouches[0],i={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup"};e.target.dispatchEvent(new MouseEvent(i[t.type],{bubbles:!0,cancelable:!0,view:window,clientX:e.clientX,clientY:e.clientY,screenX:e.screenX,screenY:e.screenY}))}function i(t,e){return Number(Math.round(t+"e"+e)+"e-"+e)}!function(){for(var t=0,e=["ms","moz","webkit","o"],i=0;i0&&void 0!==arguments[0]?arguments[0]:null,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;return this.x1=null==t?this.x1:t,this.y1=null==e?this.y1:e,this.x2=null==i?this.x2:i,this.y2=null==n?this.y2:n,this}},{key:"width",value:function(){return Math.abs(this.x2-this.x1)}},{key:"height",value:function(){return Math.abs(this.y2-this.y1)}},{key:"resize",value:function(t,e){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[0,0],n=this.x1+this.width()*i[0],o=this.y1+this.height()*i[1];return this.x1=n-t*i[0],this.y1=o-e*i[1],this.x2=this.x1+t,this.y2=this.y1+e,this}},{key:"scale",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[0,0],i=this.width()*t,n=this.height()*t;return this.resize(i,n,e),this}},{key:"move",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,i=this.width(),n=this.height();return t=null===t?this.x1:t,e=null===e?this.y1:e,this.x1=t,this.y1=e,this.x2=t+i,this.y2=e+n,this}},{key:"getRelativePoint",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[0,0],e=this.width()*t[0],i=this.height()*t[1];return[e,i]}},{key:"getAbsolutePoint",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[0,0],e=this.x1+this.width()*t[0],i=this.y1+this.height()*t[1];return[e,i]}},{key:"constrainToRatio",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[0,0],i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"height";if(null!==t){this.width(),this.height();switch(i){case"height":this.resize(this.width(),this.width()*t,e);break;case"width":this.resize(1*this.height()/t,this.height(),e);break;default:this.resize(this.width(),this.width()*t,e)}return this}}},{key:"constrainToBoundary",value:function(t,e){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[0,0],n=this.getAbsolutePoint(i),o=h(n,2),s=o[0],r=o[1],a=s,l=r,u=t-s,d=e-r,c=-2*i[0]+1,p=-2*i[1]+1,v=null,m=null;switch(c){case-1:v=a;break;case 0:v=2*Math.min(a,u);break;case 1:v=u}switch(p){case-1:m=l;break;case 0:m=2*Math.min(l,d);break;case 1:m=d}if(this.width()>v){var f=v/this.width();this.scale(f,i)}if(this.height()>m){var g=m/this.height();this.scale(g,i)}return this}},{key:"constrainToSize",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:[0,0],s=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null;if(s&&(s>1?(t=1*e/s,n*=s):s<1&&(e=t*s,i=1*n/s)),t&&this.width()>t){var r=t,a=null===s?this.height():e;this.resize(r,a,o)}if(e&&this.height()>e){var h=null===s?this.width():t,l=e;this.resize(h,l,o)}if(i&&this.width()2&&void 0!==arguments[2]&&arguments[2];if(n(this,e),this.options=e.parseOptions(i||{}),!t.nodeName&&(t=document.querySelector(t),null==t))throw"Unable to find element.";if(!t.getAttribute("src"))throw"Image src not provided.";this._initialized=!1,this._restore={parent:t.parentNode,element:t},s||(0===t.width||0===t.height?t.onload=function(){o.initialize(t)}:this.initialize(t))}return o(e,[{key:"initialize",value:function(t){this.createDOM(t),this.options.convertToPixels(this.cropperEl),this.attachHandlerEvents(),this.attachRegionEvents(),this.attachOverlayEvents(),this.box=this.initializeBox(this.options),this.redraw(),this._initialized=!0,null!==this.options.onInitialize&&this.options.onInitialize(this)}},{key:"createDOM",value:function(e){this.containerEl=document.createElement("div"),this.containerEl.className="croppr-container",this.eventBus=this.containerEl,t(this.containerEl),this.cropperEl=document.createElement("div"),this.cropperEl.className="croppr",this.imageEl=document.createElement("img"),this.imageEl.setAttribute("src",e.getAttribute("src")),this.imageEl.setAttribute("alt",e.getAttribute("alt")),this.imageEl.className="croppr-image",this.imageClippedEl=this.imageEl.cloneNode(),this.imageClippedEl.className="croppr-imageClipped",this.regionEl=document.createElement("div"),this.regionEl.className="croppr-region",this.overlayEl=document.createElement("div"),this.overlayEl.className="croppr-overlay";var i=document.createElement("div");i.className="croppr-handleContainer",this.handles=[];for(var n=0;n>31,l=a[1]-t.cropperEl.offsetHeight/2>>31,u=(h^l)+l+l+4,d=-2*u+8,c=0;co.width&&(i=o.width),n<0?n=0:n>o.height&&(n=o.height);var s=this.activeHandle.originPoint.slice(),r=this.activeHandle.originX,a=this.activeHandle.originY,h=this.activeHandle.handle,l=1===h.constraints[0],d=1===h.constraints[1],c=1===h.constraints[2],p=1===h.constraints[3],v=(p||d)&&(l||c),m=p||d?r:this.box.x1,f=p||d?r:this.box.x2,g=l||c?a:this.box.y1,E=l||c?a:this.box.y2;m=p?i:m,f=d?i:f,g=l?n:g,E=c?n:E;var w=!1,y=!1;if((p||d)&&(w=p?i>r:ia:nC.y1+z*C.width()||na.width&&this.box.move(a.width-this.box.width(),null),this.box.y1<0&&this.box.move(null,0),this.box.y2>a.height&&this.box.move(null,a.height-this.box.height()),this.redraw(),null!==this.options.onCropMove&&this.options.onCropMove(this.getValue())}},{key:"onRegionMoveEnd",value:function(t){null!==this.options.onCropEnd&&this.options.onCropEnd(this.getValue())}},{key:"getValue",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;if(null===t&&(t=this.options.returnMode),"real"==t){var e=this.imageEl.naturalWidth,n=this.imageEl.naturalHeight,o=e/this.imageEl.offsetWidth,s=n/this.imageEl.offsetHeight;return{x:Math.round(this.box.x1*o),y:Math.round(this.box.y1*s),width:Math.round(this.box.width()*o),height:Math.round(this.box.height()*s)}}if("ratio"==t){var r=this.imageEl.offsetWidth,a=this.imageEl.offsetHeight;return{x:i(this.box.x1/r,3),y:i(this.box.y1/a,3),width:i(this.box.width()/r,3),height:i(this.box.height()/a,3)}}if("raw"==t)return{x:Math.round(this.box.x1),y:Math.round(this.box.y1),width:Math.round(this.box.width()),height:Math.round(this.box.height())}}}],[{key:"parseOptions",value:function(t){var e={aspectRatio:null,maxSize:{width:null,height:null},minSize:{width:null,height:null},startSize:{width:100,height:100,unit:"%"},returnMode:"real",onInitialize:null,onCropStart:null,onCropMove:null,onCropEnd:null},i=null;void 0!==t.aspectRatio&&("number"==typeof t.aspectRatio?i=t.aspectRatio:t.aspectRatio instanceof Array&&(i=t.aspectRatio[1]/t.aspectRatio[0]));var n=null;void 0!==t.maxSize&&null!==t.maxSize&&(n={width:t.maxSize[0]||null,height:t.maxSize[1]||null,unit:t.maxSize[2]||"px"});var o=null;void 0!==t.minSize&&null!==t.minSize&&(o={width:t.minSize[0]||null,height:t.minSize[1]||null,unit:t.minSize[2]||"px"});var s=null;void 0!==t.startSize&&null!==t.startSize&&(s={width:t.startSize[0]||null,height:t.startSize[1]||null,unit:t.startSize[2]||"%"});var r=null;"function"==typeof t.onInitialize&&(r=t.onInitialize);var a=null;"function"==typeof t.onCropStart&&(a=t.onCropStart);var h=null;"function"==typeof t.onCropEnd&&(h=t.onCropEnd);var l=null;"function"==typeof t.onUpdate&&(console.warn("Croppr.js: `onUpdate` is deprecated and will be removed in the next major release. Please use `onCropMove` or `onCropEnd` instead."),l=t.onUpdate),"function"==typeof t.onCropMove&&(l=t.onCropMove);var u=null;if(void 0!==t.returnMode){var d=t.returnMode.toLowerCase();if(["real","ratio","raw"].indexOf(d)===-1)throw"Invalid return mode.";u=d}var c=function(t){for(var e=t.offsetWidth,i=t.offsetHeight,n=["maxSize","minSize","startSize"],o=0;o2&&void 0!==arguments[2]&&arguments[2];return n(this,e),a(this,(e.__proto__||Object.getPrototypeOf(e)).call(this,t,i,o))}return r(e,t),o(e,[{key:"getValue",value:function(t){return s(e.prototype.__proto__||Object.getPrototypeOf(e.prototype),"getValue",this).call(this,t)}},{key:"setImage",value:function(t){return s(e.prototype.__proto__||Object.getPrototypeOf(e.prototype),"setImage",this).call(this,t)}},{key:"destroy",value:function(){return s(e.prototype.__proto__||Object.getPrototypeOf(e.prototype),"destroy",this).call(this)}},{key:"moveTo",value:function(t,e){return this.box.move(t,e),this.redraw(),null!==this.options.onCropEnd&&this.options.onCropEnd(this.getValue()),this}},{key:"resizeTo",value:function(t,e){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[.5,.5];return this.box.resize(t,e,i),this.redraw(),null!==this.options.onCropEnd&&this.options.onCropEnd(this.getValue()),this}},{key:"scaleBy",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[.5,.5];return this.box.scale(t,e),this.redraw(),null!==this.options.onCropEnd&&this.options.onCropEnd(this.getValue()),this}},{key:"reset",value:function(){return this.box=this.initializeBox(this.options),this.redraw(),null!==this.options.onCropEnd&&this.options.onCropEnd(this.getValue()),this}}]),e}(c);return p}); diff --git a/index.d.ts b/index.d.ts index 0b2d1a9..a7bc1b0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -18,6 +18,9 @@ declare class Croppr { /** Gets the value of the crop region */ getValue(mode?: 'real' | 'ratio' | 'raw'): Croppr.CropValue + /** Changes the image src. */ + setImage(src: string): Croppr + /** Destroys the Croppr instance */ destroy(): void @@ -32,6 +35,7 @@ declare class Croppr { /** Resets the crop region to the initial settings */ reset(): Croppr + } /*~ Declare type modules */ diff --git a/src/box.js b/src/box.js index ab13dbf..dde7f2a 100644 --- a/src/box.js +++ b/src/box.js @@ -2,244 +2,244 @@ * Box component */ export default class Box { - /** - * Creates a new Box instance. - * @constructor - * @param {Number} x1 - * @param {Number} y1 - * @param {Number} x2 - * @param {Number} y2 - */ - constructor(x1, y1, x2, y2) { - this.x1 = x1; - this.y1 = y1; - this.x2 = x2; - this.y2 = y2; + /** + * Creates a new Box instance. + * @constructor + * @param {Number} x1 + * @param {Number} y1 + * @param {Number} x2 + * @param {Number} y2 + */ + constructor(x1, y1, x2, y2) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + } + + /** + * Sets the new dimensions of the box. + * @param {Number} x1 + * @param {Number} y1 + * @param {Number} x2 + * @param {Number} y2 + */ + set(x1 = null, y1 = null, x2 = null, y2 = null) { + this.x1 = x1 == null ? this.x1 : x1; + this.y1 = y1 == null ? this.y1 : y1; + this.x2 = x2 == null ? this.x2 : x2; + this.y2 = y2 == null ? this.y2 : y2; + return this; + } + + /** + * Calculates the width of the box. + * @returns {Number} + */ + width() { + return Math.abs(this.x2 - this.x1); + } + + /** + * Calculates the height of the box. + * @returns {Number} + */ + height() { + return Math.abs(this.y2 - this.y1); + } + + /** + * Resizes the box to a new size. + * @param {Number} newWidth + * @param {Number} newHeight + * @param {Array} [origin] The origin point to resize from. + * Defaults to [0, 0] (top left). + */ + resize(newWidth, newHeight, origin = [0, 0]) { + const fromX = this.x1 + (this.width() * origin[0]); + const fromY = this.y1 + (this.height() * origin[1]); + + this.x1 = fromX - (newWidth * origin[0]); + this.y1 = fromY - (newHeight * origin[1]); + this.x2 = this.x1 + newWidth; + this.y2 = this.y1 + newHeight; + + return this; + } + + /** + * Scale the box by a factor. + * @param {Number} factor + * @param {Array} [origin] The origin point to resize from. + * Defaults to [0, 0] (top left). + */ + scale(factor, origin = [0, 0]) { + const newWidth = this.width() * factor; + const newHeight = this.height() * factor; + this.resize(newWidth, newHeight, origin); + return this; + } + + /** + * Move the box to the specified coordinates. + */ + move(x = null, y = null) { + let width = this.width(); + let height = this.height(); + x = x === null ? this.x1 : x; + y = y === null ? this.y1 : y; + + this.x1 = x; + this.y1 = y; + this.x2 = x + width; + this.y2 = y + height; + return this; + } + + /** + * Get relative x and y coordinates of a given point within the box. + * @param {Array} point The x and y ratio position within the box. + * @returns {Array} The x and y coordinates [x, y]. + */ + getRelativePoint(point = [0, 0]) { + const x = this.width() * point[0]; + const y = this.height() * point[1]; + return [x, y]; + } + + /** + * Get absolute x and y coordinates of a given point within the box. + * @param {Array} point The x and y ratio position within the box. + * @returns {Array} The x and y coordinates [x, y]. + */ + getAbsolutePoint(point = [0, 0]) { + const x = this.x1 + this.width() * point[0]; + const y = this.y1 + this.height() * point[1]; + return [x, y]; + } + + /** + * Constrain the box to a fixed ratio. + * @param {Number} ratio + * @param {Array} [origin] The origin point to resize from. + * Defaults to [0, 0] (top left). + * @param {String} [grow] The axis to grow to maintain the ratio. + * Defaults to 'height'. + */ + constrainToRatio(ratio, origin = [0, 0], grow = 'height') { + if (ratio === null) { return; } + const width = this.width(); + const height = this.height(); + switch (grow) { + case 'height': // Grow height only + this.resize(this.width(), this.width() * ratio, origin); + break; + case 'width': // Grow width only + this.resize(this.height() * 1 / ratio, this.height(), origin); + break; + default: // Default: Grow height only + this.resize(this.width(), this.width() * ratio, origin); } - /** - * Sets the new dimensions of the box. - * @param {Number} x1 - * @param {Number} y1 - * @param {Number} x2 - * @param {Number} y2 - */ - set(x1 = null, y1 = null, x2 = null, y2 = null) { - this.x1 = x1 == null ? this.x1 : x1; - this.y1 = y1 == null ? this.y1 : y1; - this.x2 = x2 == null ? this.x2 : x2; - this.y2 = y2 == null ? this.y2 : y2; - return this; + return this; + } + + /** + * Constrain the box within a boundary. + * @param {Number} boundaryWidth + * @param {Number} boundaryHeight + * @param {Array} [origin] The origin point to resize from. + * Defaults to [0, 0] (top left). + */ + constrainToBoundary(boundaryWidth, boundaryHeight, origin = [0, 0]) { + + // Calculate the maximum sizes for each direction of growth + const [originX, originY] = this.getAbsolutePoint(origin); + const maxIfLeft = originX + const maxIfTop = originY + const maxIfRight = boundaryWidth - originX + const maxIfBottom = boundaryHeight - originY + + // Express the direction of growth in terms of left, both, + // and right as -1, 0, and 1 respectively. Ditto for top/both/down. + const directionX = -2 * origin[0] + 1; + const directionY = -2 * origin[1] + 1; + + // Determine the max size to use according to the direction of growth. + let [maxWidth, maxHeight] = [null, null]; + switch (directionX) { + case -1: maxWidth = maxIfLeft; break; + case 0: maxWidth = Math.min(maxIfLeft, maxIfRight) * 2; break; + case +1: maxWidth = maxIfRight; break; } - - /** - * Calculates the width of the box. - * @returns {Number} - */ - width() { - return Math.abs(this.x2 - this.x1); + switch (directionY) { + case -1: maxHeight = maxIfTop; break; + case 0: maxHeight = Math.min(maxIfTop, maxIfBottom) * 2; break; + case +1: maxHeight = maxIfBottom; break; } - /** - * Calculates the height of the box. - * @returns {Number} - */ - height() { - return Math.abs(this.y2 - this.y1); + // Resize if the box exceeds the calculated max width/height. + if (this.width() > maxWidth) { + const factor = maxWidth / this.width(); + this.scale(factor, origin); } - - /** - * Resizes the box to a new size. - * @param {Number} newWidth - * @param {Number} newHeight - * @param {Array} [origin] The origin point to resize from. - * Defaults to [0, 0] (top left). - */ - resize(newWidth, newHeight, origin=[0, 0]) { - const fromX = this.x1 + (this.width() * origin[0]); - const fromY = this.y1 + (this.height() * origin[1]); - - this.x1 = fromX - (newWidth * origin[0]); - this.y1 = fromY - (newHeight * origin[1]); - this.x2 = this.x1 + newWidth; - this.y2 = this.y1 + newHeight; - - return this; + if (this.height() > maxHeight) { + const factor = maxHeight / this.height(); + this.scale(factor, origin); } - /** - * Scale the box by a factor. - * @param {Number} factor - * @param {Array} [origin] The origin point to resize from. - * Defaults to [0, 0] (top left). - */ - scale(factor, origin=[0, 0]) { - const newWidth = this.width() * factor; - const newHeight = this.height() * factor; - this.resize(newWidth, newHeight, origin); - return this; + return this; + } + + /** + * Constrain the box to a maximum/minimum size. + * @param {Number} [maxWidth] + * @param {Number} [maxHeight] + * @param {Number} [minWidth] + * @param {Number} [minHeight] + * @param {Array} [origin] The origin point to resize from. + * Defaults to [0, 0] (top left). + * @param {Number} [ratio] Ratio to maintain. + */ + constrainToSize(maxWidth = null, maxHeight = null, + minWidth = null, minHeight = null, + origin = [0, 0], ratio = null) { + + // Calculate new max/min widths & heights that constrains to the ratio + if (ratio) { + if (ratio > 1) { + maxWidth = maxHeight * 1 / ratio; + minHeight = minHeight * ratio; + } else if (ratio < 1) { + maxHeight = maxWidth * ratio; + minWidth = minHeight * 1 / ratio; + } } - /** - * Move the box to the specified coordinates. - */ - move(x = null, y = null) { - let width = this.width(); - let height = this.height(); - x = x === null ? this.x1 : x; - y = y === null ? this.y1 : y; - - this.x1 = x; - this.y1 = y; - this.x2 = x + width; - this.y2 = y + height; - return this; + if (maxWidth && this.width() > maxWidth) { + const newWidth = maxWidth, + newHeight = ratio === null ? this.height() : maxHeight; + this.resize(newWidth, newHeight, origin); } - /** - * Get relative x and y coordinates of a given point within the box. - * @param {Array} point The x and y ratio position within the box. - * @returns {Array} The x and y coordinates [x, y]. - */ - getRelativePoint(point=[0, 0]) { - const x = this.width() * point[0]; - const y = this.height() * point[1]; - return [x, y]; + if (maxHeight && this.height() > maxHeight) { + const newWidth = ratio === null ? this.width() : maxWidth, + newHeight = maxHeight; + this.resize(newWidth, newHeight, origin); } - /** - * Get absolute x and y coordinates of a given point within the box. - * @param {Array} point The x and y ratio position within the box. - * @returns {Array} The x and y coordinates [x, y]. - */ - getAbsolutePoint(point=[0, 0]) { - const x = this.x1 + this.width() * point[0]; - const y = this.y1 + this.height() * point[1]; - return [x, y]; + if (minWidth && this.width() < minWidth) { + const newWidth = minWidth, + newHeight = ratio === null ? this.height() : minHeight; + this.resize(newWidth, newHeight, origin); } - /** - * Constrain the box to a fixed ratio. - * @param {Number} ratio - * @param {Array} [origin] The origin point to resize from. - * Defaults to [0, 0] (top left). - * @param {String} [grow] The axis to grow to maintain the ratio. - * Defaults to 'height'. - */ - constrainToRatio(ratio, origin=[0, 0], grow='height') { - if (ratio === null) { return; } - const width = this.width(); - const height = this.height(); - switch (grow) { - case 'height': // Grow height only - this.resize(this.width(), this.width() * ratio, origin); - break; - case 'width': // Grow width only - this.resize(this.height() * 1/ratio, this.height(), origin); - break; - default: // Default: Grow height only - this.resize(this.width(), this.width() * ratio, origin); - } - - return this; + if (minHeight && this.height() < minHeight) { + const newWidth = ratio === null ? this.width() : minWidth, + newHeight = minHeight; + this.resize(newWidth, newHeight, origin); } - /** - * Constrain the box within a boundary. - * @param {Number} boundaryWidth - * @param {Number} boundaryHeight - * @param {Array} [origin] The origin point to resize from. - * Defaults to [0, 0] (top left). - */ - constrainToBoundary(boundaryWidth, boundaryHeight, origin=[0, 0]) { - - // Calculate the maximum sizes for each direction of growth - const [originX, originY] = this.getAbsolutePoint(origin); - const maxIfLeft = originX - const maxIfTop = originY - const maxIfRight = boundaryWidth - originX - const maxIfBottom = boundaryHeight - originY - - // Express the direction of growth in terms of left, both, - // and right as -1, 0, and 1 respectively. Ditto for top/both/down. - const directionX = -2 * origin[0] + 1; - const directionY = -2 * origin[1] + 1; - - // Determine the max size to use according to the direction of growth. - let [maxWidth, maxHeight] = [null, null]; - switch (directionX) { - case -1: maxWidth = maxIfLeft; break; - case 0: maxWidth = Math.min(maxIfLeft, maxIfRight) * 2; break; - case +1: maxWidth = maxIfRight; break; - } - switch (directionY) { - case -1: maxHeight = maxIfTop; break; - case 0: maxHeight = Math.min(maxIfTop, maxIfBottom) * 2; break; - case +1: maxHeight = maxIfBottom; break; - } - - // Resize if the box exceeds the calculated max width/height. - if (this.width() > maxWidth) { - const factor = maxWidth / this.width(); - this.scale(factor, origin); - } - if (this.height() > maxHeight) { - const factor = maxHeight / this.height(); - this.scale(factor, origin); - } - - return this; - } - - /** - * Constrain the box to a maximum/minimum size. - * @param {Number} [maxWidth] - * @param {Number} [maxHeight] - * @param {Number} [minWidth] - * @param {Number} [minHeight] - * @param {Array} [origin] The origin point to resize from. - * Defaults to [0, 0] (top left). - * @param {Number} [ratio] Ratio to maintain. - */ - constrainToSize(maxWidth = null, maxHeight = null, - minWidth = null, minHeight = null, - origin=[0, 0], ratio = null) { - - // Calculate new max/min widths & heights that constrains to the ratio - if (ratio) { - if (ratio > 1) { - maxWidth = maxHeight * 1/ratio; - minHeight = minHeight * ratio; - } else if (ratio < 1) { - maxHeight = maxWidth * ratio; - minWidth = minHeight * 1/ratio; - } - } - - if (maxWidth && this.width() > maxWidth) { - const newWidth = maxWidth, - newHeight = ratio === null ? this.height() : maxHeight; - this.resize(newWidth, newHeight, origin); - } - - if (maxHeight && this.height() > maxHeight) { - const newWidth = ratio === null ? this.width() : maxWidth, - newHeight = maxHeight; - this.resize(newWidth, newHeight, origin); - } - - if (minWidth && this.width() < minWidth) { - const newWidth = minWidth, - newHeight = ratio === null ? this.height() : minHeight; - this.resize(newWidth, newHeight, origin); - } - - if (minHeight && this.height() < minHeight) { - const newWidth = ratio === null ? this.width() : minWidth, - newHeight = minHeight; - this.resize(newWidth, newHeight, origin); - } - - return this; - } + return this; + } } diff --git a/src/core.js b/src/core.js index af67a55..5ebf65b 100644 --- a/src/core.js +++ b/src/core.js @@ -18,709 +18,726 @@ import enableTouch from './touch'; * @property {String} cursor - The CSS cursor of this handle. */ const HANDLES = [ - { position: [0.0, 0.0], constraints: [1, 0, 0, 1], cursor: 'nw-resize' }, - { position: [0.5, 0.0], constraints: [1, 0, 0, 0], cursor: 'n-resize' }, - { position: [1.0, 0.0], constraints: [1, 1, 0, 0], cursor: 'ne-resize' }, - { position: [1.0, 0.5], constraints: [0, 1, 0, 0], cursor: 'e-resize' }, - { position: [1.0, 1.0], constraints: [0, 1, 1, 0], cursor: 'se-resize' }, - { position: [0.5, 1.0], constraints: [0, 0, 1, 0], cursor: 's-resize' }, - { position: [0.0, 1.0], constraints: [0, 0, 1, 1], cursor: 'sw-resize' }, - { position: [0.0, 0.5], constraints: [0, 0, 0, 1], cursor: 'w-resize' } + { position: [0.0, 0.0], constraints: [1, 0, 0, 1], cursor: 'nw-resize' }, + { position: [0.5, 0.0], constraints: [1, 0, 0, 0], cursor: 'n-resize' }, + { position: [1.0, 0.0], constraints: [1, 1, 0, 0], cursor: 'ne-resize' }, + { position: [1.0, 0.5], constraints: [0, 1, 0, 0], cursor: 'e-resize' }, + { position: [1.0, 1.0], constraints: [0, 1, 1, 0], cursor: 'se-resize' }, + { position: [0.5, 1.0], constraints: [0, 0, 1, 0], cursor: 's-resize' }, + { position: [0.0, 1.0], constraints: [0, 0, 1, 1], cursor: 'sw-resize' }, + { position: [0.0, 0.5], constraints: [0, 0, 0, 1], cursor: 'w-resize' } ] /** * Core class for Croppr containing most of its functional logic. */ export default class CropprCore { - constructor(element, options, deferred=false) { + constructor(element, options, deferred = false) { - // Parse options - this.options = CropprCore.parseOptions(options || {}); + // Parse options + this.options = CropprCore.parseOptions(options || {}); - // Get target img element - if (!element.nodeName) { - element = document.querySelector(element); - if (element == null) { throw 'Unable to find element.' } - } - if (!element.getAttribute('src')) { - throw 'Image src not provided.' - } - - // Define internal props - this._initialized = false; - this._restore = { - parent: element.parentNode, - element: element - } - - // Wait until image is loaded before proceeding - if (!deferred) { - if (element.width === 0 || element.height === 0) { - element.onload = () => { this.initialize(element); } - } else { - this.initialize(element); - } - } + // Get target img element + if (!element.nodeName) { + element = document.querySelector(element); + if (element == null) { throw 'Unable to find element.' } } - - /** - * Initialize the Croppr instance - */ - initialize(element) { - // Create DOM elements - this.createDOM(element); - - // Process option values - this.options.convertToPixels(this.cropperEl); - - // Listen for events from children - this.attachHandlerEvents(); - this.attachRegionEvents(); - this.attachOverlayEvents(); - - // Bootstrap this cropper instance - this.box = this.initializeBox(this.options); - this.redraw(); - - // Set the initalized flag to true and call the callback - this._initialized = true; - if (this.options.onInitialize !== null) { - this.options.onInitialize(this); - } + if (!element.getAttribute('src')) { + throw 'Image src not provided.' } - /** - * Create Croppr's DOM elements - */ - createDOM(targetEl) { - // Create main container and use it as the main event listeners - this.containerEl = document.createElement('div'); - this.containerEl.className = 'croppr-container'; - this.eventBus = this.containerEl; - enableTouch(this.containerEl); - - // Create cropper element - this.cropperEl = document.createElement('div'); - this.cropperEl.className = 'croppr'; - - // Create image element - this.imageEl = document.createElement('img'); - this.imageEl.setAttribute('src', targetEl.getAttribute('src')); - this.imageEl.setAttribute('alt', targetEl.getAttribute('alt')); - this.imageEl.className = 'croppr-image'; - - // Create clipped image element - this.imageClippedEl = this.imageEl.cloneNode(); - this.imageClippedEl.className = 'croppr-imageClipped'; - - // Create region box element - this.regionEl = document.createElement('div'); - this.regionEl.className = 'croppr-region'; - - // Create overlay element - this.overlayEl = document.createElement('div'); - this.overlayEl.className = 'croppr-overlay'; - - // Create handles element - let handleContainerEl = document.createElement('div'); - handleContainerEl.className = 'croppr-handleContainer'; - this.handles = []; - for (let i = 0; i < HANDLES.length; i++) { - const handle = new Handle(HANDLES[i].position, - HANDLES[i].constraints, - HANDLES[i].cursor, - this.eventBus); - this.handles.push(handle); - handleContainerEl.appendChild(handle.el); - } - - // And then we piece it all together! - this.cropperEl.appendChild(this.imageEl); - this.cropperEl.appendChild(this.imageClippedEl); - this.cropperEl.appendChild(this.regionEl); - this.cropperEl.appendChild(this.overlayEl); - this.cropperEl.appendChild(handleContainerEl); - this.containerEl.appendChild(this.cropperEl); - - // And then finally insert it into the document - targetEl.parentElement.replaceChild(this.containerEl, targetEl); - } - - /** - * Destroy the Croppr instance and replace with the original element. - */ - destroy() { - this._restore.parent.replaceChild(this._restore.element, this.containerEl); - } - - /** - * Create a new box region with a set of options. - * @param {Object} opts The options. - * @returns {Box} - */ - initializeBox(opts) { - // Create initial box - const width = opts.startSize.width; - const height = opts.startSize.height; - let box = new Box(0, 0, width, height) - - // Maintain ratio - box.constrainToRatio(opts.aspectRatio, [0.5, 0.5]); - - // Maintain minimum/maximum size - const min = opts.minSize; - const max = opts.maxSize; - box.constrainToSize(max.width, max.height, min.width, min.height, - [0.5, 0.5], opts.aspectRatio); - - // Constrain to boundary - const parentWidth = this.cropperEl.offsetWidth; - const parentHeight = this.cropperEl.offsetHeight; - box.constrainToBoundary(parentWidth, parentHeight, [0.5, 0.5]); - - // Move to center - const x = (this.cropperEl.offsetWidth / 2) - (box.width() / 2); - const y = (this.cropperEl.offsetHeight / 2) - (box.height() / 2); - box.move(x, y); - - return box; - } - - /** - * Draw visuals (border, handles, etc) for the current box. - */ - redraw() { - // Round positional values to prevent subpixel coordinates, which can - // result in element that is rendered blurly - const width = Math.round(this.box.width()), - height = Math.round(this.box.height()), - x1 = Math.round(this.box.x1), - y1 = Math.round(this.box.y1), - x2 = Math.round(this.box.x2), - y2 = Math.round(this.box.y2); - - window.requestAnimationFrame(() => { - // Update region element - this.regionEl.style.transform = `translate(${x1}px, ${y1}px)` - this.regionEl.style.width = width + 'px'; - this.regionEl.style.height = height + 'px'; - - // Update clipped image element - this.imageClippedEl.style.clip = `rect(${y1}px, ${x2}px, ${y2}px, ${x1}px)`; - - // Determine which handle to bring forward. The following code - // calculates the quadrant the box is in using bitwise operators. - // Reference: https://stackoverflow.com/questions/9718059 - const center = this.box.getAbsolutePoint([.5, .5]); - const xSign = (center[0] - this.cropperEl.offsetWidth / 2) >> 31, - ySign = (center[1] - this.cropperEl.offsetHeight / 2) >> 31; - const quadrant = (xSign ^ ySign) + ySign + ySign + 4; - - // The following equation calculates which handle index to bring - // forward. The equation is derived using algebra (if youre curious) - const foregroundHandleIndex = -2 * quadrant + 8 - - // Update handle positions - for (let i = 0; i < this.handles.length; i++) { - let handle = this.handles[i]; - - // Calculate handle position - const handleWidth = handle.el.offsetWidth; - const handleHeight = handle.el.offsetHeight; - const left = x1 + (width * handle.position[0]) - handleWidth / 2; - const top = y1 + (height * handle.position[1]) - handleHeight / 2; - - // Apply new position. The positional values are rounded to - // prevent subpixel positions which can result in a blurry element - handle.el.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`; - handle.el.style.zIndex = foregroundHandleIndex == i ? 5 : 4; - } - }); - } - - /** - * Attach listeners for events emitted by the handles. - * Enables resizing of the region element. - */ - attachHandlerEvents() { - const eventBus = this.eventBus; - eventBus.addEventListener('handlestart', this.onHandleMoveStart.bind(this)); - eventBus.addEventListener('handlemove', this.onHandleMoveMoving.bind(this)); - eventBus.addEventListener('handleend', this.onHandleMoveEnd.bind(this)); - } - - /** - * Attach event listeners for the crop region element. - * Enables dragging/moving of the region element. - */ - attachRegionEvents() { - const eventBus = this.eventBus; - const self = this; - - this.regionEl.addEventListener('mousedown', onMouseDown); - eventBus.addEventListener('regionstart', this.onRegionMoveStart.bind(this)); - eventBus.addEventListener('regionmove', this.onRegionMoveMoving.bind(this)); - eventBus.addEventListener('regionend', this.onRegionMoveEnd.bind(this)); - - function onMouseDown(e) { - e.stopPropagation(); - document.addEventListener('mouseup', onMouseUp); - document.addEventListener('mousemove', onMouseMove); - - // Notify parent - eventBus.dispatchEvent(new CustomEvent('regionstart', { - detail: {mouseX: e.clientX, mouseY: e.clientY} - })); - } - - function onMouseMove(e) { - e.stopPropagation(); - - // Notify parent - eventBus.dispatchEvent(new CustomEvent('regionmove', { - detail: {mouseX: e.clientX, mouseY: e.clientY} - })); - } + // Define internal props + this._initialized = false; + this._restore = { + parent: element.parentNode, + element: element + } - function onMouseUp(e) { - e.stopPropagation(); - document.removeEventListener('mouseup', onMouseUp); - document.removeEventListener('mousemove', onMouseMove); + // Wait until image is loaded before proceeding + if (!deferred) { + if (element.width === 0 || element.height === 0) { + element.onload = () => { this.initialize(element); } + } else { + this.initialize(element); + } + } + } + + /** + * Initialize the Croppr instance + */ + initialize(element) { + // Create DOM elements + this.createDOM(element); + + // Process option values + this.options.convertToPixels(this.cropperEl); + + // Listen for events from children + this.attachHandlerEvents(); + this.attachRegionEvents(); + this.attachOverlayEvents(); + + // Bootstrap this cropper instance + this.box = this.initializeBox(this.options); + this.redraw(); + + // Set the initalized flag to true and call the callback + this._initialized = true; + if (this.options.onInitialize !== null) { + this.options.onInitialize(this); + } + } + + /** + * Create Croppr's DOM elements + */ + createDOM(targetEl) { + // Create main container and use it as the main event listeners + this.containerEl = document.createElement('div'); + this.containerEl.className = 'croppr-container'; + this.eventBus = this.containerEl; + enableTouch(this.containerEl); + + // Create cropper element + this.cropperEl = document.createElement('div'); + this.cropperEl.className = 'croppr'; + + // Create image element + this.imageEl = document.createElement('img'); + this.imageEl.setAttribute('src', targetEl.getAttribute('src')); + this.imageEl.setAttribute('alt', targetEl.getAttribute('alt')); + this.imageEl.className = 'croppr-image'; + + // Create clipped image element + this.imageClippedEl = this.imageEl.cloneNode(); + this.imageClippedEl.className = 'croppr-imageClipped'; + + // Create region box element + this.regionEl = document.createElement('div'); + this.regionEl.className = 'croppr-region'; + + // Create overlay element + this.overlayEl = document.createElement('div'); + this.overlayEl.className = 'croppr-overlay'; + + // Create handles element + let handleContainerEl = document.createElement('div'); + handleContainerEl.className = 'croppr-handleContainer'; + this.handles = []; + for (let i = 0; i < HANDLES.length; i++) { + const handle = new Handle(HANDLES[i].position, + HANDLES[i].constraints, + HANDLES[i].cursor, + this.eventBus); + this.handles.push(handle); + handleContainerEl.appendChild(handle.el); + } - // Notify parent - eventBus.dispatchEvent(new CustomEvent('regionend', { - detail: {mouseX: e.clientX, mouseY: e.clientY} - })); - } + // And then we piece it all together! + this.cropperEl.appendChild(this.imageEl); + this.cropperEl.appendChild(this.imageClippedEl); + this.cropperEl.appendChild(this.regionEl); + this.cropperEl.appendChild(this.overlayEl); + this.cropperEl.appendChild(handleContainerEl); + this.containerEl.appendChild(this.cropperEl); + + // And then finally insert it into the document + targetEl.parentElement.replaceChild(this.containerEl, targetEl); + } + + /** + * Changes the image src. + * @param {String} src + */ + setImage(src) { + // Add onload listener to reinitialize box + this.imageEl.onload = () => { + this.box = this.initializeBox(this.options); + this.redraw(); } - /** - * Attach event listeners for the overlay element. - * Enables the creation of a new selection by dragging an empty area. - */ - attachOverlayEvents() { - const SOUTHEAST_HANDLE_IDX = 4; - const self = this; - let tmpBox = null; - this.overlayEl.addEventListener('mousedown', onMouseDown); - - function onMouseDown(e) { - e.stopPropagation(); - document.addEventListener('mouseup', onMouseUp); - document.addEventListener('mousemove', onMouseMove); - - // Calculate mouse's position in relative to the container - const container = self.cropperEl.getBoundingClientRect(); - const mouseX = e.clientX - container.left; - const mouseY = e.clientY - container.top; - - // Create new box at mouse position - tmpBox = self.box; - self.box = new Box(mouseX, mouseY, mouseX + 1, mouseY + 1); - - // Activate the bottom right handle - self.eventBus.dispatchEvent(new CustomEvent('handlestart', { - detail: {handle: self.handles[SOUTHEAST_HANDLE_IDX]} - })); - } + // Change image source + this.imageEl.src = src; + this.imageClippedEl.src = src; + return this; + } + + /** + * Destroy the Croppr instance and replace with the original element. + */ + destroy() { + this._restore.parent.replaceChild(this._restore.element, this.containerEl); + } + + /** + * Create a new box region with a set of options. + * @param {Object} opts The options. + * @returns {Box} + */ + initializeBox(opts) { + // Create initial box + const width = opts.startSize.width; + const height = opts.startSize.height; + let box = new Box(0, 0, width, height) + + // Maintain ratio + box.constrainToRatio(opts.aspectRatio, [0.5, 0.5]); + + // Maintain minimum/maximum size + const min = opts.minSize; + const max = opts.maxSize; + box.constrainToSize(max.width, max.height, min.width, min.height, + [0.5, 0.5], opts.aspectRatio); + + // Constrain to boundary + const parentWidth = this.cropperEl.offsetWidth; + const parentHeight = this.cropperEl.offsetHeight; + box.constrainToBoundary(parentWidth, parentHeight, [0.5, 0.5]); + + // Move to center + const x = (this.cropperEl.offsetWidth / 2) - (box.width() / 2); + const y = (this.cropperEl.offsetHeight / 2) - (box.height() / 2); + box.move(x, y); + + return box; + } + + /** + * Draw visuals (border, handles, etc) for the current box. + */ + redraw() { + // Round positional values to prevent subpixel coordinates, which can + // result in element that is rendered blurly + const width = Math.round(this.box.width()), + height = Math.round(this.box.height()), + x1 = Math.round(this.box.x1), + y1 = Math.round(this.box.y1), + x2 = Math.round(this.box.x2), + y2 = Math.round(this.box.y2); + + window.requestAnimationFrame(() => { + // Update region element + this.regionEl.style.transform = `translate(${x1}px, ${y1}px)` + this.regionEl.style.width = width + 'px'; + this.regionEl.style.height = height + 'px'; + + // Update clipped image element + this.imageClippedEl.style.clip = `rect(${y1}px, ${x2}px, ${y2}px, ${x1}px)`; + + // Determine which handle to bring forward. The following code + // calculates the quadrant the box is in using bitwise operators. + // Reference: https://stackoverflow.com/questions/9718059 + const center = this.box.getAbsolutePoint([.5, .5]); + const xSign = (center[0] - this.cropperEl.offsetWidth / 2) >> 31; + const ySign = (center[1] - this.cropperEl.offsetHeight / 2) >> 31; + const quadrant = (xSign ^ ySign) + ySign + ySign + 4; + + // The following equation calculates which handle index to bring + // forward. The equation is derived using algebra (if youre curious) + const foregroundHandleIndex = -2 * quadrant + 8 + + // Update handle positions + for (let i = 0; i < this.handles.length; i++) { + let handle = this.handles[i]; + + // Calculate handle position + const handleWidth = handle.el.offsetWidth; + const handleHeight = handle.el.offsetHeight; + const left = x1 + (width * handle.position[0]) - handleWidth / 2; + const top = y1 + (height * handle.position[1]) - handleHeight / 2; + + // Apply new position. The positional values are rounded to + // prevent subpixel positions which can result in a blurry element + handle.el.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`; + handle.el.style.zIndex = foregroundHandleIndex == i ? 5 : 4; + } + }); + } + + /** + * Attach listeners for events emitted by the handles. + * Enables resizing of the region element. + */ + attachHandlerEvents() { + const eventBus = this.eventBus; + eventBus.addEventListener('handlestart', this.onHandleMoveStart.bind(this)); + eventBus.addEventListener('handlemove', this.onHandleMoveMoving.bind(this)); + eventBus.addEventListener('handleend', this.onHandleMoveEnd.bind(this)); + } + + /** + * Attach event listeners for the crop region element. + * Enables dragging/moving of the region element. + */ + attachRegionEvents() { + const eventBus = this.eventBus; + const self = this; + + this.regionEl.addEventListener('mousedown', onMouseDown); + eventBus.addEventListener('regionstart', this.onRegionMoveStart.bind(this)); + eventBus.addEventListener('regionmove', this.onRegionMoveMoving.bind(this)); + eventBus.addEventListener('regionend', this.onRegionMoveEnd.bind(this)); + + function onMouseDown(e) { + e.stopPropagation(); + document.addEventListener('mouseup', onMouseUp); + document.addEventListener('mousemove', onMouseMove); + + // Notify parent + eventBus.dispatchEvent(new CustomEvent('regionstart', { + detail: { mouseX: e.clientX, mouseY: e.clientY } + })); + } - function onMouseMove(e) { - e.stopPropagation(); - self.eventBus.dispatchEvent(new CustomEvent('handlemove', { - detail: {mouseX: e.clientX, mouseY: e.clientY} - })); - } + function onMouseMove(e) { + e.stopPropagation(); - function onMouseUp(e) { - e.stopPropagation(); - document.removeEventListener('mouseup', onMouseUp); - document.removeEventListener('mousemove', onMouseMove); - - // If the new box has no width and height, it suggests that - // the user had just clicked on an empty area and did not drag - // a new box (ie. an accidental click). In this scenario, we - // simply replace it with the previous box. - if (self.box.width() === 1 && self.box.height() === 1) { - self.box = tmpBox; - return; - } + // Notify parent + eventBus.dispatchEvent(new CustomEvent('regionmove', { + detail: { mouseX: e.clientX, mouseY: e.clientY } + })); + } - self.eventBus.dispatchEvent(new CustomEvent('handleend', { - detail: {mouseX: e.clientX, mouseY: e.clientY} - })); - } + function onMouseUp(e) { + e.stopPropagation(); + document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener('mousemove', onMouseMove); + // Notify parent + eventBus.dispatchEvent(new CustomEvent('regionend', { + detail: { mouseX: e.clientX, mouseY: e.clientY } + })); + } + } + + /** + * Attach event listeners for the overlay element. + * Enables the creation of a new selection by dragging an empty area. + */ + attachOverlayEvents() { + const SOUTHEAST_HANDLE_IDX = 4; + const self = this; + let tmpBox = null; + this.overlayEl.addEventListener('mousedown', onMouseDown); + + function onMouseDown(e) { + e.stopPropagation(); + document.addEventListener('mouseup', onMouseUp); + document.addEventListener('mousemove', onMouseMove); + + // Calculate mouse's position in relative to the container + const container = self.cropperEl.getBoundingClientRect(); + const mouseX = e.clientX - container.left; + const mouseY = e.clientY - container.top; + + // Create new box at mouse position + tmpBox = self.box; + self.box = new Box(mouseX, mouseY, mouseX + 1, mouseY + 1); + + // Activate the bottom right handle + self.eventBus.dispatchEvent(new CustomEvent('handlestart', { + detail: { handle: self.handles[SOUTHEAST_HANDLE_IDX] } + })); } - /** - * EVENT HANDLER - * Executes when user begins dragging a handle. - */ - onHandleMoveStart(e) { - let handle = e.detail.handle; - - // The origin point is the point where the box is scaled from. - // This is usually the opposite side/corner of the active handle. - const originPoint = [1 - handle.position[0], 1 - handle.position[1]]; - let [originX, originY] = this.box.getAbsolutePoint(originPoint); - - this.activeHandle = {handle, originPoint, originX, originY} + function onMouseMove(e) { + e.stopPropagation(); + self.eventBus.dispatchEvent(new CustomEvent('handlemove', { + detail: { mouseX: e.clientX, mouseY: e.clientY } + })); + } - // Trigger callback - if (this.options.onCropStart !== null) { - this.options.onCropStart(this.getValue()); - } + function onMouseUp(e) { + e.stopPropagation(); + document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener('mousemove', onMouseMove); + + // If the new box has no width and height, it suggests that + // the user had just clicked on an empty area and did not drag + // a new box (ie. an accidental click). In this scenario, we + // simply replace it with the previous box. + if (self.box.width() === 1 && self.box.height() === 1) { + self.box = tmpBox; + return; + } + + self.eventBus.dispatchEvent(new CustomEvent('handleend', { + detail: { mouseX: e.clientX, mouseY: e.clientY } + })); } - /** - * EVENT HANDLER - * Executes on handle move. Main logic to manage the movement of handles. - */ - onHandleMoveMoving(e) { - let {mouseX, mouseY} = e.detail; - - // Calculate mouse's position in relative to the container - let container = this.cropperEl.getBoundingClientRect(); - mouseX = mouseX - container.left; - mouseY = mouseY - container.top; - - // Ensure mouse is within the boundaries - if (mouseX < 0) { mouseX = 0; } - else if (mouseX > container.width) { mouseX = container.width; } - - if (mouseY < 0) { mouseY = 0; } - else if (mouseY > container.height) { mouseY = container.height; } - - // Bootstrap helper variables - let origin = this.activeHandle.originPoint.slice(); - const originX = this.activeHandle.originX; - const originY = this.activeHandle.originY; - const handle = this.activeHandle.handle; - const TOP_MOVABLE = handle.constraints[0] === 1; - const RIGHT_MOVABLE = handle.constraints[1] === 1; - const BOTTOM_MOVABLE = handle.constraints[2] === 1; - const LEFT_MOVABLE = handle.constraints[3] === 1; - const MULTI_AXIS = (LEFT_MOVABLE || RIGHT_MOVABLE) && - (TOP_MOVABLE || BOTTOM_MOVABLE); - - // Apply movement to respective sides according to the handle's - // constraint values. - let x1 = LEFT_MOVABLE || RIGHT_MOVABLE ? originX : this.box.x1; - let x2 = LEFT_MOVABLE || RIGHT_MOVABLE ? originX : this.box.x2; - let y1 = TOP_MOVABLE || BOTTOM_MOVABLE ? originY : this.box.y1; - let y2 = TOP_MOVABLE || BOTTOM_MOVABLE ? originY : this.box.y2; - x1 = LEFT_MOVABLE ? mouseX : x1; - x2 = RIGHT_MOVABLE ? mouseX : x2; - y1 = TOP_MOVABLE ? mouseY: y1; - y2 = BOTTOM_MOVABLE ? mouseY: y2; - - // Check if the user dragged past the origin point. If it did, - // we set the flipped flag to true. - let [isFlippedX, isFlippedY] = [false, false]; - if (LEFT_MOVABLE || RIGHT_MOVABLE) { - isFlippedX = LEFT_MOVABLE ? mouseX > originX : mouseX < originX; - } - if (TOP_MOVABLE || BOTTOM_MOVABLE) { - isFlippedY = TOP_MOVABLE ? mouseY > originY : mouseY < originY; - } + } - // If it is flipped, we swap the coordinates and flip the origin point. - if (isFlippedX) { - const tmp = x1; x1 = x2; x2 = tmp; // Swap x1 and x2 - origin[0] = 1 - origin[0]; // Flip origin x point - } - if (isFlippedY) { - const tmp = y1; y1 = y2; y2 = tmp; // Swap y1 and y2 - origin[1] = 1 - origin[1]; // Flip origin y point - } - - // Create new box object - let box = new Box(x1, y1, x2, y2); - - // Maintain aspect ratio - if (this.options.aspectRatio) { - const ratio = this.options.aspectRatio; - let isVerticalMovement = false; - if (MULTI_AXIS) { - isVerticalMovement = (mouseY > box.y1 + ratio * box.width()) || - (mouseY < box.y2 - ratio * box.width()); - } else if (TOP_MOVABLE || BOTTOM_MOVABLE) { - isVerticalMovement = true; - } - const ratioMode = isVerticalMovement ? 'width' : 'height'; - box.constrainToRatio(ratio, origin, ratioMode); - } + /** + * EVENT HANDLER + * Executes when user begins dragging a handle. + */ + onHandleMoveStart(e) { + let handle = e.detail.handle; - // Maintain minimum/maximum size - const min = this.options.minSize; - const max = this.options.maxSize; - box.constrainToSize(max.width, max.height, min.width, - min.height, origin, this.options.aspectRatio); + // The origin point is the point where the box is scaled from. + // This is usually the opposite side/corner of the active handle. + const originPoint = [1 - handle.position[0], 1 - handle.position[1]]; + let [originX, originY] = this.box.getAbsolutePoint(originPoint); - // Constrain to boundary - const parentWidth = this.cropperEl.offsetWidth; - const parentHeight = this.cropperEl.offsetHeight; - box.constrainToBoundary(parentWidth, parentHeight, origin); + this.activeHandle = { handle, originPoint, originX, originY } - // Finally, update the visuals (border, handles, clipped image, etc) - this.box = box; - this.redraw(); + // Trigger callback + if (this.options.onCropStart !== null) { + this.options.onCropStart(this.getValue()); + } + } + + /** + * EVENT HANDLER + * Executes on handle move. Main logic to manage the movement of handles. + */ + onHandleMoveMoving(e) { + let { mouseX, mouseY } = e.detail; + + // Calculate mouse's position in relative to the container + let container = this.cropperEl.getBoundingClientRect(); + mouseX = mouseX - container.left; + mouseY = mouseY - container.top; + + // Ensure mouse is within the boundaries + if (mouseX < 0) { mouseX = 0; } + else if (mouseX > container.width) { mouseX = container.width; } + + if (mouseY < 0) { mouseY = 0; } + else if (mouseY > container.height) { mouseY = container.height; } + + // Bootstrap helper variables + let origin = this.activeHandle.originPoint.slice(); + const originX = this.activeHandle.originX; + const originY = this.activeHandle.originY; + const handle = this.activeHandle.handle; + const TOP_MOVABLE = handle.constraints[0] === 1; + const RIGHT_MOVABLE = handle.constraints[1] === 1; + const BOTTOM_MOVABLE = handle.constraints[2] === 1; + const LEFT_MOVABLE = handle.constraints[3] === 1; + const MULTI_AXIS = (LEFT_MOVABLE || RIGHT_MOVABLE) && + (TOP_MOVABLE || BOTTOM_MOVABLE); + + // Apply movement to respective sides according to the handle's + // constraint values. + let x1 = LEFT_MOVABLE || RIGHT_MOVABLE ? originX : this.box.x1; + let x2 = LEFT_MOVABLE || RIGHT_MOVABLE ? originX : this.box.x2; + let y1 = TOP_MOVABLE || BOTTOM_MOVABLE ? originY : this.box.y1; + let y2 = TOP_MOVABLE || BOTTOM_MOVABLE ? originY : this.box.y2; + x1 = LEFT_MOVABLE ? mouseX : x1; + x2 = RIGHT_MOVABLE ? mouseX : x2; + y1 = TOP_MOVABLE ? mouseY : y1; + y2 = BOTTOM_MOVABLE ? mouseY : y2; + + // Check if the user dragged past the origin point. If it did, + // we set the flipped flag to true. + let [isFlippedX, isFlippedY] = [false, false]; + if (LEFT_MOVABLE || RIGHT_MOVABLE) { + isFlippedX = LEFT_MOVABLE ? mouseX > originX : mouseX < originX; + } + if (TOP_MOVABLE || BOTTOM_MOVABLE) { + isFlippedY = TOP_MOVABLE ? mouseY > originY : mouseY < originY; + } - // Trigger callback - if (this.options.onCropMove !== null) { - this.options.onCropMove(this.getValue()); - } + // If it is flipped, we swap the coordinates and flip the origin point. + if (isFlippedX) { + const tmp = x1; x1 = x2; x2 = tmp; // Swap x1 and x2 + origin[0] = 1 - origin[0]; // Flip origin x point + } + if (isFlippedY) { + const tmp = y1; y1 = y2; y2 = tmp; // Swap y1 and y2 + origin[1] = 1 - origin[1]; // Flip origin y point } - /** - * EVENT HANDLER - * Executes on handle move end. - */ - onHandleMoveEnd(e) { - // Trigger callback - if (this.options.onCropEnd !== null) { - this.options.onCropEnd(this.getValue()); - } + // Create new box object + let box = new Box(x1, y1, x2, y2); + + // Maintain aspect ratio + if (this.options.aspectRatio) { + const ratio = this.options.aspectRatio; + let isVerticalMovement = false; + if (MULTI_AXIS) { + isVerticalMovement = (mouseY > box.y1 + ratio * box.width()) || + (mouseY < box.y2 - ratio * box.width()); + } else if (TOP_MOVABLE || BOTTOM_MOVABLE) { + isVerticalMovement = true; + } + const ratioMode = isVerticalMovement ? 'width' : 'height'; + box.constrainToRatio(ratio, origin, ratioMode); } - /** - * EVENT HANDLER - * Executes when user starts moving the crop region. - */ - onRegionMoveStart(e) { - let {mouseX, mouseY} = e.detail; + // Maintain minimum/maximum size + const min = this.options.minSize; + const max = this.options.maxSize; + box.constrainToSize(max.width, max.height, min.width, + min.height, origin, this.options.aspectRatio); - // Calculate mouse's position in relative to the container - let container = this.cropperEl.getBoundingClientRect(); - mouseX = mouseX - container.left; - mouseY = mouseY - container.top; + // Constrain to boundary + const parentWidth = this.cropperEl.offsetWidth; + const parentHeight = this.cropperEl.offsetHeight; + box.constrainToBoundary(parentWidth, parentHeight, origin); - this.currentMove = { - offsetX: mouseX - this.box.x1, - offsetY: mouseY - this.box.y1 - } + // Finally, update the visuals (border, handles, clipped image, etc) + this.box = box; + this.redraw(); - // Trigger callback - if (this.options.onCropStart !== null) { - this.options.onCropStart(this.getValue()); - } + // Trigger callback + if (this.options.onCropMove !== null) { + this.options.onCropMove(this.getValue()); + } + } + + /** + * EVENT HANDLER + * Executes on handle move end. + */ + onHandleMoveEnd(e) { + // Trigger callback + if (this.options.onCropEnd !== null) { + this.options.onCropEnd(this.getValue()); + } + } + + /** + * EVENT HANDLER + * Executes when user starts moving the crop region. + */ + onRegionMoveStart(e) { + let { mouseX, mouseY } = e.detail; + + // Calculate mouse's position in relative to the container + let container = this.cropperEl.getBoundingClientRect(); + mouseX = mouseX - container.left; + mouseY = mouseY - container.top; + + this.currentMove = { + offsetX: mouseX - this.box.x1, + offsetY: mouseY - this.box.y1 } - /** - * EVENT HANDLER - * Executes when user moves the crop region. - */ - onRegionMoveMoving(e) { - let {mouseX, mouseY} = e.detail; - let {offsetX, offsetY} = this.currentMove; - - // Calculate mouse's position in relative to the container - let container = this.cropperEl.getBoundingClientRect(); - mouseX = mouseX - container.left; - mouseY = mouseY - container.top; - - this.box.move(mouseX - offsetX, mouseY - offsetY); - - // Ensure box is within the boundaries - if (this.box.x1 < 0) { - this.box.move(0, null); - } - if (this.box.x2 > container.width) { - this.box.move(container.width - this.box.width(), null); - } - if (this.box.y1 < 0) { - this.box.move(null, 0); - } - if (this.box.y2 > container.height) { - this.box.move(null, container.height - this.box.height()); - } + // Trigger callback + if (this.options.onCropStart !== null) { + this.options.onCropStart(this.getValue()); + } + } + + /** + * EVENT HANDLER + * Executes when user moves the crop region. + */ + onRegionMoveMoving(e) { + let { mouseX, mouseY } = e.detail; + let { offsetX, offsetY } = this.currentMove; + + // Calculate mouse's position in relative to the container + let container = this.cropperEl.getBoundingClientRect(); + mouseX = mouseX - container.left; + mouseY = mouseY - container.top; + + this.box.move(mouseX - offsetX, mouseY - offsetY); + + // Ensure box is within the boundaries + if (this.box.x1 < 0) { + this.box.move(0, null); + } + if (this.box.x2 > container.width) { + this.box.move(container.width - this.box.width(), null); + } + if (this.box.y1 < 0) { + this.box.move(null, 0); + } + if (this.box.y2 > container.height) { + this.box.move(null, container.height - this.box.height()); + } - // Update visuals - this.redraw(); + // Update visuals + this.redraw(); - // Trigger callback - if (this.options.onCropMove !== null) { - this.options.onCropMove(this.getValue()); - } + // Trigger callback + if (this.options.onCropMove !== null) { + this.options.onCropMove(this.getValue()); } - - /** - * EVENT HANDLER - * Executes when user stops moving the crop region (mouse up). - */ - onRegionMoveEnd(e) { - // Trigger callback - if (this.options.onCropEnd !== null) { - this.options.onCropEnd(this.getValue()); - } + } + + /** + * EVENT HANDLER + * Executes when user stops moving the crop region (mouse up). + */ + onRegionMoveEnd(e) { + // Trigger callback + if (this.options.onCropEnd !== null) { + this.options.onCropEnd(this.getValue()); + } + } + + + /** + * Calculate the value of the crop region. + */ + getValue(mode = null) { + if (mode === null) { mode = this.options.returnMode; } + if (mode == 'real') { + const actualWidth = this.imageEl.naturalWidth; + const actualHeight = this.imageEl.naturalHeight; + const factorX = actualWidth / this.imageEl.offsetWidth; + const factorY = actualHeight / this.imageEl.offsetHeight; + return { + x: Math.round(this.box.x1 * factorX), + y: Math.round(this.box.y1 * factorY), + width: Math.round(this.box.width() * factorX), + height: Math.round(this.box.height() * factorY) + } + } else if (mode == 'ratio') { + const elementWidth = this.imageEl.offsetWidth; + const elementHeight = this.imageEl.offsetHeight; + return { + x: round(this.box.x1 / elementWidth, 3), + y: round(this.box.y1 / elementHeight, 3), + width: round(this.box.width() / elementWidth, 3), + height: round(this.box.height() / elementHeight, 3) + } + } else if (mode == 'raw') { + return { + x: Math.round(this.box.x1), + y: Math.round(this.box.y1), + width: Math.round(this.box.width()), + height: Math.round(this.box.height()) + } + } + } + + /** + * Parse user options and set default values. + */ + static parseOptions(opts) { + const defaults = { + aspectRatio: null, + maxSize: { width: null, height: null }, + minSize: { width: null, height: null }, + startSize: { width: 100, height: 100, unit: '%' }, + returnMode: 'real', + onInitialize: null, + onCropStart: null, + onCropMove: null, + onCropEnd: null, } - - /** - * Calculate the value of the crop region. - */ - getValue(mode = null) { - if (mode === null) { mode = this.options.returnMode; } - if (mode == 'real') { - const actualWidth = this.imageEl.naturalWidth; - const actualHeight = this.imageEl.naturalHeight; - const factorX = actualWidth / this.imageEl.offsetWidth; - const factorY = actualHeight / this.imageEl.offsetHeight; - return { - x: Math.round(this.box.x1 * factorX), - y: Math.round(this.box.y1 * factorY), - width: Math.round(this.box.width() * factorX), - height: Math.round(this.box.height() * factorY) - } - } else if (mode == 'ratio') { - const elementWidth = this.imageEl.offsetWidth; - const elementHeight = this.imageEl.offsetHeight; - return { - x: round(this.box.x1/elementWidth, 3), - y: round(this.box.y1/elementHeight, 3), - width: round(this.box.width()/elementWidth, 3), - height: round(this.box.height()/elementHeight, 3) - } - } else if (mode == 'raw') { - return { - x: Math.round(this.box.x1), - y: Math.round(this.box.y1), - width: Math.round(this.box.width()), - height: Math.round(this.box.height()) - } - } + // Parse aspect ratio + let aspectRatio = null; + if (opts.aspectRatio !== undefined) { + if (typeof (opts.aspectRatio) === 'number') { + aspectRatio = opts.aspectRatio + } else if (opts.aspectRatio instanceof Array) { + aspectRatio = opts.aspectRatio[1] / opts.aspectRatio[0]; + } } - /** - * Parse user options and set default values. - */ - static parseOptions(opts) { - const defaults = { - aspectRatio: null, - maxSize: { width: null, height: null }, - minSize: { width: null, height: null }, - startSize: { width: 100, height: 100, unit: '%' }, - returnMode: 'real', - onInitialize: null, - onCropStart: null, - onCropMove: null, - onCropEnd: null, - } - - // Parse aspect ratio - let aspectRatio = null; - if (opts.aspectRatio !== undefined) { - if (typeof(opts.aspectRatio) === 'number') { - aspectRatio = opts.aspectRatio - } else if (opts.aspectRatio instanceof Array) { - aspectRatio = opts.aspectRatio[1] / opts.aspectRatio[0]; - } - } + // Parse max width/height + let maxSize = null; + if (opts.maxSize !== undefined && opts.maxSize !== null) { + maxSize = { + width: opts.maxSize[0] || null, + height: opts.maxSize[1] || null, + unit: opts.maxSize[2] || 'px' + } + } - // Parse max width/height - let maxSize = null; - if (opts.maxSize !== undefined && opts.maxSize !== null) { - maxSize = { - width: opts.maxSize[0] || null, - height: opts.maxSize[1] || null, - unit: opts.maxSize[2] || 'px' - } - } + // Parse min width/height + let minSize = null; + if (opts.minSize !== undefined && opts.minSize !== null) { + minSize = { + width: opts.minSize[0] || null, + height: opts.minSize[1] || null, + unit: opts.minSize[2] || 'px' + } + } - // Parse min width/height - let minSize = null; - if (opts.minSize !== undefined && opts.minSize !== null) { - minSize = { - width: opts.minSize[0] || null, - height: opts.minSize[1] || null, - unit: opts.minSize[2] || 'px' - } - } + // Parse start size + let startSize = null; + if (opts.startSize !== undefined && opts.startSize !== null) { + startSize = { + width: opts.startSize[0] || null, + height: opts.startSize[1] || null, + unit: opts.startSize[2] || '%' + } + } - // Parse start size - let startSize = null; - if (opts.startSize !== undefined && opts.startSize !== null) { - startSize = { - width: opts.startSize[0] || null, - height: opts.startSize[1] || null, - unit: opts.startSize[2] || '%' - } - } + // Parse callbacks + let onInitialize = null; + if (typeof opts.onInitialize === 'function') { + onInitialize = opts.onInitialize; + } - // Parse callbacks - let onInitialize = null; - if (typeof opts.onInitialize === 'function') { - onInitialize = opts.onInitialize; - } + let onCropStart = null; + if (typeof opts.onCropStart === 'function') { + onCropStart = opts.onCropStart; + } - let onCropStart = null; - if (typeof opts.onCropStart === 'function') { - onCropStart = opts.onCropStart; - } + let onCropEnd = null; + if (typeof opts.onCropEnd === 'function') { + onCropEnd = opts.onCropEnd; + } - let onCropEnd = null; - if (typeof opts.onCropEnd === 'function') { - onCropEnd = opts.onCropEnd; - } + let onCropMove = null; + if (typeof opts.onUpdate === 'function') { + // DEPRECATED: onUpdate is deprecated to create a more uniform + // callback API, such as: onCropStart, onCropMove, onCropEnd + console.warn('Croppr.js: `onUpdate` is deprecated and will be removed in the next major release. Please use `onCropMove` or `onCropEnd` instead.'); + onCropMove = opts.onUpdate; + } + if (typeof opts.onCropMove === 'function') { + onCropMove = opts.onCropMove; + } - let onCropMove = null; - if (typeof opts.onUpdate === 'function') { - // DEPRECATED: onUpdate is deprecated to create a more uniform - // callback API, such as: onCropStart, onCropMove, onCropEnd - console.warn('Croppr.js: `onUpdate` is deprecated and will be removed in the next major release. Please use `onCropMove` or `onCropEnd` instead.'); - onCropMove = opts.onUpdate; - } - if (typeof opts.onCropMove === 'function') { - onCropMove = opts.onCropMove; - } + // Parse returnMode value + let returnMode = null; + if (opts.returnMode !== undefined) { + const s = opts.returnMode.toLowerCase(); + if (['real', 'ratio', 'raw'].indexOf(s) === -1) { + throw "Invalid return mode."; + } + returnMode = s; + } - // Parse returnMode value - let returnMode = null; - if (opts.returnMode !== undefined) { - const s = opts.returnMode.toLowerCase(); - if (['real', 'ratio', 'raw'].indexOf(s) === -1) { - throw "Invalid return mode."; + // Create function to convert % values to pixels + const convertToPixels = function (container) { + const width = container.offsetWidth; + const height = container.offsetHeight; + + // Convert sizes + const sizeKeys = ['maxSize', 'minSize', 'startSize']; + for (let i = 0; i < sizeKeys.length; i++) { + const key = sizeKeys[i]; + if (this[key] !== null) { + if (this[key].unit == '%') { + if (this[key].width !== null) { + this[key].width = (this[key].width / 100) * width; } - returnMode = s; - } - - // Create function to convert % values to pixels - const convertToPixels = function(container) { - const width = container.offsetWidth; - const height = container.offsetHeight; - - // Convert sizes - const sizeKeys = ['maxSize', 'minSize', 'startSize']; - for (let i = 0; i < sizeKeys.length; i++) { - const key = sizeKeys[i]; - if (this[key] !== null) { - if (this[key].unit == '%') { - if (this[key].width !== null) { - this[key].width = (this[key].width/100) * width; - } - if (this[key].height !== null) { - this[key].height = (this[key].height/100) * height; - } - } - delete this[key].unit; - } + if (this[key].height !== null) { + this[key].height = (this[key].height / 100) * height; } + } + delete this[key].unit; } + } + } - const defaultValue = (v, d) => (v !== null ? v : d); - return { - aspectRatio: defaultValue(aspectRatio, defaults.aspectRatio), - maxSize: defaultValue(maxSize, defaults.maxSize), - minSize: defaultValue(minSize, defaults.minSize), - startSize: defaultValue(startSize, defaults.startSize), - returnMode: defaultValue(returnMode, defaults.returnMode), - onInitialize: defaultValue(onInitialize, defaults.onInitialize), - onCropStart: defaultValue(onCropStart, defaults.onCropStart), - onCropMove: defaultValue(onCropMove, defaults.onCropMove), - onCropEnd: defaultValue(onCropEnd, defaults.onCropEnd), - convertToPixels: convertToPixels - } + const defaultValue = (v, d) => (v !== null ? v : d); + return { + aspectRatio: defaultValue(aspectRatio, defaults.aspectRatio), + maxSize: defaultValue(maxSize, defaults.maxSize), + minSize: defaultValue(minSize, defaults.minSize), + startSize: defaultValue(startSize, defaults.startSize), + returnMode: defaultValue(returnMode, defaults.returnMode), + onInitialize: defaultValue(onInitialize, defaults.onInitialize), + onCropStart: defaultValue(onCropStart, defaults.onCropStart), + onCropMove: defaultValue(onCropMove, defaults.onCropMove), + onCropEnd: defaultValue(onCropEnd, defaults.onCropEnd), + convertToPixels: convertToPixels } + } } /** @@ -728,5 +745,5 @@ export default class CropprCore { */ function round(value, decimals) { - return Number(Math.round(value+'e'+decimals)+'e-'+decimals); + return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals); } diff --git a/src/croppr.js b/src/croppr.js index 5ff0b78..db5ca26 100644 --- a/src/croppr.js +++ b/src/croppr.js @@ -1,3 +1,6 @@ +/// @ts-check +/// + /** * Croppr.js * https://github.com/jamesssooi/Croppr.js @@ -16,92 +19,100 @@ import CropprCore from './core'; * main logic. */ export default class Croppr extends CropprCore { - /** - * @constructor - * Calls the CropprCore's constructor. - */ - constructor(element, options, _deferred=false) { - super(element, options, _deferred); - } + /** + * @constructor + * Calls the CropprCore's constructor. + */ + constructor(element, options, _deferred = false) { + super(element, options, _deferred); + } - /** - * Gets the value of the crop region. - * @param {String} mode Which mode of calculation to use: 'real', 'ratio' or - * 'raw'. - */ - getValue(mode) { - return super.getValue(mode); - } + /** + * Gets the value of the crop region. + * @param {String} [mode] Which mode of calculation to use: 'real', 'ratio' or + * 'raw'. + */ + getValue(mode) { + return super.getValue(mode); + } - /** - * Destroys the Croppr instance - */ - destroy() { - return super.destroy(); - } + /** + * Changes the image src. + * @param {String} src + */ + setImage(src) { + return super.setImage(src); + } + + /** + * Destroys the Croppr instance + */ + destroy() { + return super.destroy(); + } + + /** + * Moves the crop region to a specified coordinate. + * @param {Number} x + * @param {Number} y + */ + moveTo(x, y) { + this.box.move(x, y); + this.redraw(); - /** - * Moves the crop region to a specified coordinate. - * @param {Number} x - * @param {Number} y - */ - moveTo(x, y) { - this.box.move(x, y); - this.redraw(); - - // Call the callback - if (this.options.onCropEnd !== null) { - this.options.onCropEnd(this.getValue()); - } - return this; + // Call the callback + if (this.options.onCropEnd !== null) { + this.options.onCropEnd(this.getValue()); } + return this; + } - /** - * Resizes the crop region to a specified width and height. - * @param {Number} width - * @param {Number} height - * @param {Array} origin The origin point to resize from. - * Defaults to [0.5, 0.5] (center). - */ - resizeTo(width, height, origin=[.5, .5]) { - this.box.resize(width, height, origin); - this.redraw(); + /** + * Resizes the crop region to a specified width and height. + * @param {Number} width + * @param {Number} height + * @param {Array} origin The origin point to resize from. + * Defaults to [0.5, 0.5] (center). + */ + resizeTo(width, height, origin = [.5, .5]) { + this.box.resize(width, height, origin); + this.redraw(); - // Call the callback - if (this.options.onCropEnd !== null) { - this.options.onCropEnd(this.getValue()); - } - return this; + // Call the callback + if (this.options.onCropEnd !== null) { + this.options.onCropEnd(this.getValue()); } + return this; + } - /** - * Scale the crop region by a factor. - * @param {Number} factor - * @param {Array} origin The origin point to resize from. - * Defaults to [0.5, 0.5] (center). - */ - scaleBy(factor, origin=[.5, .5]) { - this.box.scale(factor, origin); - this.redraw(); + /** + * Scale the crop region by a factor. + * @param {Number} factor + * @param {Array} origin The origin point to resize from. + * Defaults to [0.5, 0.5] (center). + */ + scaleBy(factor, origin = [.5, .5]) { + this.box.scale(factor, origin); + this.redraw(); - // Call the callback - if (this.options.onCropEnd !== null) { - this.options.onCropEnd(this.getValue()); - } - return this; + // Call the callback + if (this.options.onCropEnd !== null) { + this.options.onCropEnd(this.getValue()); } + return this; + } - /** - * Resets the crop region to the initial settings. - */ - reset() { - this.box = this.initializeBox(this.options); - this.redraw(); + /** + * Resets the crop region to the initial settings. + */ + reset() { + this.box = this.initializeBox(this.options); + this.redraw(); - // Call the callback - if (this.options.onCropEnd !== null) { - this.options.onCropEnd(this.getValue()); - } - return this; + // Call the callback + if (this.options.onCropEnd !== null) { + this.options.onCropEnd(this.getValue()); } + return this; + } } \ No newline at end of file diff --git a/src/css/croppr.css b/src/css/croppr.css index e21630c..19eb2f7 100644 --- a/src/css/croppr.css +++ b/src/css/croppr.css @@ -1,58 +1,58 @@ .croppr-container * { - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - box-sizing: border-box; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; } .croppr-container img { - vertical-align: middle; - max-width: 100%; + vertical-align: middle; + max-width: 100%; } .croppr { - position: relative; - display: inline-block; + position: relative; + display: inline-block; } .croppr-overlay { - background: rgba(0,0,0,0.5); - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1; - cursor: crosshair; + background: rgba(0,0,0,0.5); + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + cursor: crosshair; } .croppr-region { - border: 1px dashed rgba(0, 0, 0, 0.5); - position: absolute; - z-index: 3; - cursor: move; - top: 0; + border: 1px dashed rgba(0, 0, 0, 0.5); + position: absolute; + z-index: 3; + cursor: move; + top: 0; } .croppr-imageClipped { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 2; - pointer-events: none; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + pointer-events: none; } .croppr-handle { - border: 1px solid black; - background-color: white; - width: 10px; - height: 10px; - position: absolute; - z-index: 4; - top: 0; + border: 1px solid black; + background-color: white; + width: 10px; + height: 10px; + position: absolute; + z-index: 4; + top: 0; } \ No newline at end of file diff --git a/src/handle.js b/src/handle.js index ab8910d..5523e89 100644 --- a/src/handle.js +++ b/src/handle.js @@ -3,63 +3,63 @@ */ export default class Handle { - /** - * Creates a new Handle instance. - * @constructor - * @param {Array} position The x and y ratio position of the handle - * within the crop region. Accepts a value between 0 to 1 in the order - * of [X, Y]. - * @param {Array} constraints Define the side of the crop region that - * is to be affected by this handle. Accepts a value of 0 or 1 in the - * order of [TOP, RIGHT, BOTTOM, LEFT]. - * @param {String} cursor The CSS cursor of this handle. - * @param {Element} eventBus The element to dispatch events to. - */ - constructor(position, constraints, cursor, eventBus) { + /** + * Creates a new Handle instance. + * @constructor + * @param {Array} position The x and y ratio position of the handle + * within the crop region. Accepts a value between 0 to 1 in the order + * of [X, Y]. + * @param {Array} constraints Define the side of the crop region that + * is to be affected by this handle. Accepts a value of 0 or 1 in the + * order of [TOP, RIGHT, BOTTOM, LEFT]. + * @param {String} cursor The CSS cursor of this handle. + * @param {Element} eventBus The element to dispatch events to. + */ + constructor(position, constraints, cursor, eventBus) { - var self = this; - this.position = position; - this.constraints = constraints; - this.cursor = cursor; - this.eventBus = eventBus; + var self = this; + this.position = position; + this.constraints = constraints; + this.cursor = cursor; + this.eventBus = eventBus; - // Create DOM element - this.el = document.createElement('div'); - this.el.className = 'croppr-handle'; - this.el.style.cursor = cursor; + // Create DOM element + this.el = document.createElement('div'); + this.el.className = 'croppr-handle'; + this.el.style.cursor = cursor; - // Attach initial listener - this.el.addEventListener('mousedown', onMouseDown); + // Attach initial listener + this.el.addEventListener('mousedown', onMouseDown); - function onMouseDown(e) { - e.stopPropagation(); - document.addEventListener('mouseup', onMouseUp); - document.addEventListener('mousemove', onMouseMove); + function onMouseDown(e) { + e.stopPropagation(); + document.addEventListener('mouseup', onMouseUp); + document.addEventListener('mousemove', onMouseMove); - // Notify parent - self.eventBus.dispatchEvent(new CustomEvent('handlestart', { - detail: {handle: self} - })); - } + // Notify parent + self.eventBus.dispatchEvent(new CustomEvent('handlestart', { + detail: { handle: self } + })); + } - function onMouseUp(e) { - e.stopPropagation(); - document.removeEventListener('mouseup', onMouseUp); - document.removeEventListener('mousemove', onMouseMove); + function onMouseUp(e) { + e.stopPropagation(); + document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener('mousemove', onMouseMove); - // Notify parent - self.eventBus.dispatchEvent(new CustomEvent('handleend', { - detail: {handle: self} - })); - } + // Notify parent + self.eventBus.dispatchEvent(new CustomEvent('handleend', { + detail: { handle: self } + })); + } - function onMouseMove(e) { - e.stopPropagation(); + function onMouseMove(e) { + e.stopPropagation(); - // Notify parent - self.eventBus.dispatchEvent(new CustomEvent('handlemove', { - detail: {mouseX: e.clientX, mouseY: e.clientY} - })); - } + // Notify parent + self.eventBus.dispatchEvent(new CustomEvent('handlemove', { + detail: { mouseX: e.clientX, mouseY: e.clientY } + })); } + } } \ No newline at end of file diff --git a/src/polyfills.js b/src/polyfills.js index 5505424..5b0264c 100644 --- a/src/polyfills.js +++ b/src/polyfills.js @@ -3,42 +3,42 @@ */ // Request Animation Frame polyfill -(function() { - var lastTime = 0; - var vendors = ['ms', 'moz', 'webkit', 'o']; - for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { - window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; - window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] - || window[vendors[x]+'CancelRequestAnimationFrame']; - } - - if (!window.requestAnimationFrame) - window.requestAnimationFrame = function(callback, element) { - var currTime = new Date().getTime(); - var timeToCall = Math.max(0, 16 - (currTime - lastTime)); - var id = window.setTimeout(function() { callback(currTime + timeToCall); }, - timeToCall); - lastTime = currTime + timeToCall; - return id; - }; - - if (!window.cancelAnimationFrame) - window.cancelAnimationFrame = function(id) { - clearTimeout(id); - }; +(function () { + var lastTime = 0; + var vendors = ['ms', 'moz', 'webkit', 'o']; + for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; + window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] + || window[vendors[x] + 'CancelRequestAnimationFrame']; + } + + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function (callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function () { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function (id) { + clearTimeout(id); + }; }()); // CustomEvents polyfill (function () { - if ( typeof window.CustomEvent === "function" ) return false; + if (typeof window.CustomEvent === "function") return false; - function CustomEvent ( event, params ) { + function CustomEvent(event, params) { params = params || { bubbles: false, cancelable: false, detail: undefined }; - var evt = document.createEvent( 'CustomEvent' ); - evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); return evt; - } + } CustomEvent.prototype = window.Event.prototype; diff --git a/src/touch.js b/src/touch.js index e0f810f..f82c523 100644 --- a/src/touch.js +++ b/src/touch.js @@ -9,9 +9,9 @@ * @param {Element} element */ export default function enableTouch(element) { - element.addEventListener('touchstart', simulateMouseEvent); - element.addEventListener('touchend', simulateMouseEvent); - element.addEventListener('touchmove', simulateMouseEvent); + element.addEventListener('touchstart', simulateMouseEvent); + element.addEventListener('touchend', simulateMouseEvent); + element.addEventListener('touchmove', simulateMouseEvent); } /** @@ -19,21 +19,21 @@ export default function enableTouch(element) { * @param {Event} e */ function simulateMouseEvent(e) { - e.preventDefault(); - const touch = e.changedTouches[0]; - const eventMap = { - 'touchstart': 'mousedown', - 'touchmove': 'mousemove', - 'touchend': 'mouseup' - } + e.preventDefault(); + const touch = e.changedTouches[0]; + const eventMap = { + 'touchstart': 'mousedown', + 'touchmove': 'mousemove', + 'touchend': 'mouseup' + } - touch.target.dispatchEvent(new MouseEvent(eventMap[e.type], { - bubbles: true, - cancelable: true, - view: window, - clientX: touch.clientX, - clientY: touch.clientY, - screenX: touch.screenX, - screenY: touch.screenY, - })); + touch.target.dispatchEvent(new MouseEvent(eventMap[e.type], { + bubbles: true, + cancelable: true, + view: window, + clientX: touch.clientX, + clientY: touch.clientY, + screenX: touch.screenX, + screenY: touch.screenY, + })); } \ No newline at end of file