area-selection-js
Version:
Simple and easy area selection library for image/video cropping
1,600 lines (1,322 loc) • 48 kB
JavaScript
'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;