UNPKG

area-selection-js

Version:

Simple and easy area selection library for image/video cropping

1,600 lines (1,322 loc) 48 kB
'use strict'; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var classCallCheck = _classCallCheck; function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } var createClass = _createClass; function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } var getPrototypeOf = createCommonjsModule(function (module) { function _getPrototypeOf(o) { module.exports = _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } module.exports = _getPrototypeOf; }); function _superPropBase(object, property) { while (!Object.prototype.hasOwnProperty.call(object, property)) { object = getPrototypeOf(object); if (object === null) break; } return object; } var superPropBase = _superPropBase; var get = createCommonjsModule(function (module) { function _get(target, property, receiver) { if (typeof Reflect !== "undefined" && Reflect.get) { module.exports = _get = Reflect.get; } else { module.exports = _get = function _get(target, property, receiver) { var base = superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(receiver); } return desc.value; }; } return _get(target, property, receiver || target); } module.exports = _get; }); var setPrototypeOf = createCommonjsModule(function (module) { function _setPrototypeOf(o, p) { module.exports = _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } module.exports = _setPrototypeOf; }); function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) setPrototypeOf(subClass, superClass); } var inherits = _inherits; var _typeof_1 = createCommonjsModule(function (module) { function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { module.exports = _typeof = function _typeof(obj) { return typeof obj; }; } else { module.exports = _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } module.exports = _typeof; }); function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } var assertThisInitialized = _assertThisInitialized; function _possibleConstructorReturn(self, call) { if (call && (_typeof_1(call) === "object" || typeof call === "function")) { return call; } return assertThisInitialized(self); } var possibleConstructorReturn = _possibleConstructorReturn; function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } var arrayWithHoles = _arrayWithHoles; function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } var iterableToArrayLimit = _iterableToArrayLimit; function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } var arrayLikeToArray = _arrayLikeToArray; function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return arrayLikeToArray(o, minLen); } var unsupportedIterableToArray = _unsupportedIterableToArray; function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var nonIterableRest = _nonIterableRest; function _slicedToArray(arr, i) { return arrayWithHoles(arr) || iterableToArrayLimit(arr, i) || unsupportedIterableToArray(arr, i) || nonIterableRest(); } var slicedToArray = _slicedToArray; /** * Box component */ var Box = /*#__PURE__*/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() { 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; } /** * Move the box to the specified coordinates. */ }, { 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': // Grow height only this.resize(this.width(), this.width() / ratio, origin); break; case 'width': // Grow width only this.resize(this.height() * ratio, this.height(), origin); break; default: // Default: Grow height only 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]; // Calculate the maximum sizes for each direction of growth var _this$getAbsolutePoin = this.getAbsolutePoint(origin), _this$getAbsolutePoin2 = slicedToArray(_this$getAbsolutePoin, 2), originX = _this$getAbsolutePoin2[0], originY = _this$getAbsolutePoin2[1]; var maxIfLeft = originX; var maxIfTop = originY; var maxIfRight = boundaryWidth - originX; var 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. var directionX = -2 * origin[0] + 1; var directionY = -2 * origin[1] + 1; // Determine the max size to use according to the direction of growth. 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; } // Resize if the box exceeds the calculated max width/height. 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; // 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) { 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; }(); /** * Handle component */ var 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} direction The direction of this handle. * @param {Element} eventBus The element to dispatch events to. */ function Handle(position, constraints, direction, eventBus) { classCallCheck(this, Handle); var self = this; this.position = position; this.constraints = constraints; this.direction = direction; this.eventBus = eventBus; // Create DOM element this.el = document.createElement('div'); this.el.className = 'area-selection-handle' + " area-selection-handle-".concat(direction); // Attach initial listener this.el.addEventListener('mousedown', onMouseDown); function onMouseDown(e) { e.stopPropagation(); document.addEventListener('mouseup', onMouseUp); document.addEventListener('mousemove', onMouseMove); // Notify parent self.eventBus.dispatchEvent(new CustomEvent('handlestart', { detail: { handle: self } })); } function onMouseUp(e) { e.stopPropagation(); document.removeEventListener('mouseup', onMouseUp); document.removeEventListener('mousemove', onMouseMove); // Notify parent self.eventBus.dispatchEvent(new CustomEvent('handleend', { detail: { handle: self } })); } function onMouseMove(e) { e.stopPropagation(); // Notify parent self.eventBus.dispatchEvent(new CustomEvent('handlemove', { detail: { mouseX: e.clientX, mouseY: e.clientY } })); } }; /** * AreaSelection Touch * Enables support for touch devices by translating touch events to * mouse events. */ /** * Binds an element's touch events to be simulated as mouse events. * @param {Element} element */ function enableTouch(element) { 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 })); } /** * Define a list of handles to create. * * @property {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]. * @property {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]. * @property {String} direction - The direction of this handle. */ var HANDLES = [{ position: [0.0, 0.0], constraints: [1, 0, 0, 1], direction: 'nw' }, { position: [0.5, 0.0], constraints: [1, 0, 0, 0], direction: 'n' }, { position: [1.0, 0.0], constraints: [1, 1, 0, 0], direction: 'ne' }, { position: [1.0, 0.5], constraints: [0, 1, 0, 0], direction: 'e' }, { position: [1.0, 1.0], constraints: [0, 1, 1, 0], direction: 'se' }, { position: [0.5, 1.0], constraints: [0, 0, 1, 0], direction: 's' }, { position: [0.0, 1.0], constraints: [0, 0, 1, 1], direction: 'sw' }, { position: [0.0, 0.5], constraints: [0, 0, 0, 1], direction: 'w' }]; /** * Core class for AreaSelection containing most of its functional logic. */ var Core = /*#__PURE__*/function () { function Core(element, options) { var _this = this; var deferred = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; classCallCheck(this, Core); // Parse options this.options = Core.parseOptions(options || {}); // Get target img element if (!element.nodeName) { element = document.querySelector(element); if (element == null) { throw 'Unable to find element.'; } } // 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 = function () { _this.initialize(element); }; } else { this.initialize(element); } } } /** * Initialize the AreaSelection instance */ createClass(Core, [{ key: "initialize", value: function initialize(element) { // Create DOM elements this.createDOM(element); // Process option values this.options.convertToPixels(this.selectionEl); // Listen for events from children this.attachHandlerEvents(); this.attachRegionEvents(); this.attachOverlayEvents(); // Bootstrap this area selection 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 AreaSelection's DOM elements */ }, { key: "createDOM", value: function createDOM(targetEl) { var _this2 = this; // Create main container and use it as the main event listeners this.containerEl = document.createElement('div'); this.containerEl.className = 'area-selection-container'; this.eventBus = this.containerEl; enableTouch(this.containerEl); // Create selection element this.selectionEl = document.createElement('div'); this.selectionEl.className = 'area-selection'; // Create region box element this.regionEl = document.createElement('div'); this.regionEl.className = 'area-selection-region'; // Create overlay element this.overlayEl = document.createElement('div'); this.overlayEl.className = 'area-selection-overlay'; // Create dashed lines ['h', 'v'].forEach(function (item) { var dashedLine = document.createElement('div'); dashedLine.className = "area-selection-dashed area-selection-dashed-".concat(item); _this2.regionEl.appendChild(dashedLine); }); // Create center crosshair var center = document.createElement('div'); center.className = 'area-selection-center'; this.regionEl.appendChild(center); // Create handles element this.handles = []; for (var i = 0; i < HANDLES.length; i++) { var handle = new Handle(HANDLES[i].position, HANDLES[i].constraints, HANDLES[i].direction, this.eventBus); this.handles.push(handle); this.regionEl.appendChild(handle.el); } // And then we piece it all together! this.targetEl = targetEl; this.selectionEl.appendChild(targetEl); this.selectionEl.appendChild(this.regionEl); this.selectionEl.appendChild(this.overlayEl); this.containerEl.appendChild(this.selectionEl); // And then finally insert it into the document this._restore.parent.appendChild(this.containerEl); } /** * Destroy the AreaSelection instance and replace with the original element. */ }, { 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) { // Create initial box var width = opts.startSize.width; var height = opts.startSize.height; var box = new Box(0, 0, width, height); // Maintain ratio box.constrainToRatio(opts.aspectRatio, [0.5, 0.5]); // Maintain minimum/maximum size var min = opts.minSize; var max = opts.maxSize; box.constrainToSize(max.width, max.height, min.width, min.height, [0.5, 0.5], opts.aspectRatio); // Constrain to boundary var parentWidth = this.selectionEl.offsetWidth; var parentHeight = this.selectionEl.offsetHeight; box.constrainToBoundary(parentWidth, parentHeight, [0.5, 0.5]); // Move to center var x = this.selectionEl.offsetWidth / 2 - box.width() / 2; var y = this.selectionEl.offsetHeight / 2 - box.height() / 2; box.move(x, y); return box; } /** * Draw visuals (border, handles, etc) for the current box. */ }, { key: "redraw", value: function redraw() { var _this3 = this; // Round positional values to prevent subpixel coordinates, which can // result in element that is rendered blurly 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 () { // Update region element _this3.regionEl.style.transform = "translate(".concat(x1, "px, ").concat(y1, "px)"); _this3.regionEl.style.width = width + 'px'; _this3.regionEl.style.height = height + 'px'; }); } /** * Attach listeners for events emitted by the handles. * Enables resizing of the region element. */ }, { 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)); } /** * Attach event listeners for the crop region element. * Enables dragging/moving of the region element. */ }, { key: "attachRegionEvents", value: function attachRegionEvents() { var eventBus = this.eventBus; 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 } })); } 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. */ }, { 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); // Calculate mouse's position in relative to the container var container = self.selectionEl.getBoundingClientRect(); var mouseX = e.clientX - container.left; var 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] } })); } 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 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 when user begins dragging a handle. */ }, { key: "onHandleMoveStart", value: function onHandleMoveStart(e) { var 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. var originPoint = [1 - handle.position[0], 1 - handle.position[1]]; var _this$box$getAbsolute = this.box.getAbsolutePoint(originPoint), _this$box$getAbsolute2 = slicedToArray(_this$box$getAbsolute, 2), originX = _this$box$getAbsolute2[0], originY = _this$box$getAbsolute2[1]; this.activeHandle = { handle: handle, originPoint: originPoint, originX: originX, originY: originY }; // Trigger callback if (this.options.onSelectStart !== null) { this.options.onSelectStart(this.getValue()); } } /** * EVENT HANDLER * Executes on handle move. Main logic to manage the movement of handles. */ }, { key: "onHandleMoveMoving", value: function onHandleMoveMoving(e) { var _e$detail = e.detail, mouseX = _e$detail.mouseX, mouseY = _e$detail.mouseY; // Calculate mouse's position in relative to the container var container = this.selectionEl.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 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); // Apply movement to respective sides according to the handle's // constraint values. 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; // Check if the user dragged past the origin point. If it did, // we set the flipped flag to true. 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 it is flipped, we swap the coordinates and flip the origin point. if (isFlippedX) { var tmp = x1; x1 = x2; x2 = tmp; // Swap x1 and x2 origin[0] = 1 - origin[0]; // Flip origin x point } if (isFlippedY) { var _tmp = y1; y1 = y2; y2 = _tmp; // Swap y1 and y2 origin[1] = 1 - origin[1]; // Flip origin y point } // Create new box object var box = new Box(x1, y1, x2, y2); // Maintain aspect ratio 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); } // Maintain minimum/maximum size var min = this.options.minSize; var max = this.options.maxSize; box.constrainToSize(max.width, max.height, min.width, min.height, origin, this.options.aspectRatio); // Constrain to boundary var parentWidth = this.selectionEl.offsetWidth; var parentHeight = this.selectionEl.offsetHeight; box.constrainToBoundary(parentWidth, parentHeight, origin); // Finally, update the visuals (border, handles, clipped image, etc) this.box = box; this.redraw(); // Trigger callback if (this.options.onSelectMove !== null) { this.options.onSelectMove(this.getValue()); } } /** * EVENT HANDLER * Executes on handle move end. */ }, { key: "onHandleMoveEnd", value: function onHandleMoveEnd(e) { // Trigger callback if (this.options.onSelectEnd !== null) { this.options.onSelectEnd(this.getValue()); } } /** * EVENT HANDLER * Executes when user starts moving the crop region. */ }, { key: "onRegionMoveStart", value: function onRegionMoveStart(e) { var _e$detail2 = e.detail, mouseX = _e$detail2.mouseX, mouseY = _e$detail2.mouseY; // Calculate mouse's position in relative to the container var container = this.selectionEl.getBoundingClientRect(); mouseX = mouseX - container.left; mouseY = mouseY - container.top; this.currentMove = { offsetX: mouseX - this.box.x1, offsetY: mouseY - this.box.y1 }; // Trigger callback if (this.options.onSelectStart !== null) { this.options.onSelectStart(this.getValue()); } } /** * EVENT HANDLER * Executes when user moves the crop region. */ }, { key: "onRegionMoveMoving", value: function onRegionMoveMoving(e) { var _e$detail3 = e.detail, mouseX = _e$detail3.mouseX, mouseY = _e$detail3.mouseY; var _this$currentMove = this.currentMove, offsetX = _this$currentMove.offsetX, offsetY = _this$currentMove.offsetY; // Calculate mouse's position in relative to the container var container = this.selectionEl.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(); // Trigger callback if (this.options.onSelectMove !== null) { this.options.onSelectMove(this.getValue()); } } /** * EVENT HANDLER * Executes when user stops moving the crop region (mouse up). */ }, { key: "onRegionMoveEnd", value: function onRegionMoveEnd(e) { // Trigger callback if (this.options.onSelectEnd !== null) { this.options.onSelectEnd(this.getValue()); } } /** * Calculate the value of the crop region. */ }, { 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.targetEl.naturalWidth || this.targetEl.width; var actualHeight = this.targetEl.naturalHeight || this.targetEl.height; var _this$targetEl$getBou = this.targetEl.getBoundingClientRect(), elementWidth = _this$targetEl$getBou.width, elementHeight = _this$targetEl$getBou.height; var factorX = (actualWidth || elementWidth) / elementWidth; var factorY = (actualHeight || elementHeight) / elementHeight; 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 _this$targetEl$getBou2 = this.targetEl.getBoundingClientRect(), _elementWidth = _this$targetEl$getBou2.width, _elementHeight = _this$targetEl$getBou2.height; 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. */ }, { key: "setOptions", /** * Set user options and redraw */ value: function setOptions(opts) { this.options = Core.parseOptions(Object.assign(this.options, opts || {})); this.box = this.initializeBox(this.options); this.redraw(); } }], [{ key: "parseOptions", value: function parseOptions(opts) { var defaults = { aspectRatio: null, maxSize: { width: null, height: null }, minSize: { width: null, height: null }, startSize: { width: 100, height: 100, unit: '%' }, returnMode: 'real', onInitialize: null, onSelectStart: null, onSelectMove: null, onSelectEnd: null }; // Parse aspect ratio 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]; } } // Parse max width/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' }; } // Parse min width/height 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' }; } // Parse start size 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] || '%' }; } // Parse callbacks var onInitialize = null; if (typeof opts.onInitialize === 'function') { onInitialize = opts.onInitialize; } var onSelectStart = null; if (typeof opts.onSelectStart === 'function') { onSelectStart = opts.onSelectStart; } var onSelectEnd = null; if (typeof opts.onSelectEnd === 'function') { onSelectEnd = opts.onSelectEnd; } var onSelectMove = null; if (typeof opts.onSelectMove === 'function') { onSelectMove = opts.onSelectMove; } // Parse returnMode value 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; } // Create function to convert % values to pixels var convertToPixels = function convertToPixels(container) { var width = container.offsetWidth; var height = container.offsetHeight; // Convert sizes 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.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), onSelectStart: defaultValue(onSelectStart, defaults.onSelectStart), onSelectMove: defaultValue(onSelectMove, defaults.onSelectMove), onSelectEnd: defaultValue(onSelectEnd, defaults.onSelectEnd), convertToPixels: convertToPixels }; } }]); return Core; }(); function round(value, decimals) { return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals); } function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return possibleConstructorReturn(this, result); }; } function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } /** * This class is a wrapper for Core that merely implements the main * interfaces for the AreaSelection instance. Look into Core for all the * main logic. */ var AreaSelection = /*#__PURE__*/function (_Core) { inherits(AreaSelection, _Core); var _super = _createSuper(AreaSelection); /** * @constructor * Calls the Core's constructor. */ function AreaSelection(element, options) { var _deferred = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; classCallCheck(this, AreaSelection); return _super.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(AreaSelection, [{ key: "getValue", value: function getValue(mode) { return get(getPrototypeOf(AreaSelection.prototype), "getValue", this).call(this, mode); } /** * Changes the options. * @param {Object} options */ }, { key: "setOptions", value: function setOptions(options) { return get(getPrototypeOf(AreaSelection.prototype), "setOptions", this).call(this, options); } /** * Destroys the AreaSelection instance */ }, { key: "destroy", value: function destroy() { return get(getPrototypeOf(AreaSelection.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(); // Call the callback if (this.options.onSelectEnd !== null) { this.options.onSelectEnd(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(); // Call the callback if (this.options.onSelectEnd !== null) { this.options.onSelectEnd(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(); // Call the callback if (this.options.onSelectEnd !== null) { this.options.onSelectEnd(this.getValue()); } return this; } /** * Resets the crop region to the initial settings. */ }, { key: "reset", value: function reset() { this.box = this.initializeBox(this.options); this.redraw(); // Call the callback if (this.options.onSelectEnd !== null) { this.options.onSelectEnd(this.getValue()); } return this; } }]); return AreaSelection; }(Core); /** * Babel Starter Kit (https://www.kriasoft.com/babel-starter-kit) * * Copyright © 2015-2016 Kriasoft, LLC. All rights reserved. * * This source code is licensed under the MIT license found in the * LICENSE.txt file in the root directory of this source tree. */ module.exports = AreaSelection;