const actions = require('./base');
const utils = require('../utils');
const browser = require('../utils/browser');
const InteractEvent = require('../InteractEvent');
/** @lends Interactable */
const Interactable = require('../Interactable');
const Interaction = require('../Interaction');
const defaultOptions = require('../defaultOptions');
// Less Precision with touch input
const defaultMargin = browser.supportsTouch || browser.supportsPointerEvent? 20: 10;
const resize = {
defaults: {
enabled : false,
mouseButtons: null,
origin : null,
snap : null,
restrict : null,
inertia : null,
autoScroll: null,
square: false,
preserveAspectRatio: false,
axis: 'xy',
// use default margin
margin: NaN,
// object with props left, right, top, bottom which are
// true/false values to resize when the pointer is over that edge,
// CSS selectors to match the handles for each direction
// or the Elements for each handle
edges: null,
// a value of 'none' will limit the resize rect to a minimum of 0x0
// 'negate' will alow the rect to have negative width/height
// 'reposition' will keep the width/height positive by swapping
// the top and bottom edges and/or swapping the left and right edges
invert: 'none',
},
checker: function (pointer, event, interactable, element, interaction, rect) {
if (!rect) { return null; }
const page = utils.extend({}, interaction.curCoords.page);
const options = interactable.options;
if (options.resize.enabled) {
const resizeOptions = options.resize;
const resizeEdges = { left: false, right: false, top: false, bottom: false };
// if using resize.edges
if (utils.is.object(resizeOptions.edges)) {
for (const edge in resizeEdges) {
resizeEdges[edge] = checkResizeEdge(edge,
resizeOptions.edges[edge],
page,
interaction._eventTarget,
element,
rect,
resizeOptions.margin || defaultMargin);
}
resizeEdges.left = resizeEdges.left && !resizeEdges.right;
resizeEdges.top = resizeEdges.top && !resizeEdges.bottom;
if (resizeEdges.left || resizeEdges.right || resizeEdges.top || resizeEdges.bottom) {
return {
name: 'resize',
edges: resizeEdges,
};
}
}
else {
const right = options.resize.axis !== 'y' && page.x > (rect.right - defaultMargin);
const bottom = options.resize.axis !== 'x' && page.y > (rect.bottom - defaultMargin);
if (right || bottom) {
return {
name: 'resize',
axes: (right? 'x' : '') + (bottom? 'y' : ''),
};
}
}
}
return null;
},
cursors: (browser.isIe9 ? {
x : 'e-resize',
y : 's-resize',
xy: 'se-resize',
top : 'n-resize',
left : 'w-resize',
bottom : 's-resize',
right : 'e-resize',
topleft : 'se-resize',
bottomright: 'se-resize',
topright : 'ne-resize',
bottomleft : 'ne-resize',
} : {
x : 'ew-resize',
y : 'ns-resize',
xy: 'nwse-resize',
top : 'ns-resize',
left : 'ew-resize',
bottom : 'ns-resize',
right : 'ew-resize',
topleft : 'nwse-resize',
bottomright: 'nwse-resize',
topright : 'nesw-resize',
bottomleft : 'nesw-resize',
}),
getCursor: function (action) {
if (action.axis) {
return resize.cursors[action.name + action.axis];
}
else if (action.edges) {
let cursorKey = '';
const edgeNames = ['top', 'bottom', 'left', 'right'];
for (let i = 0; i < 4; i++) {
if (action.edges[edgeNames[i]]) {
cursorKey += edgeNames[i];
}
}
return resize.cursors[cursorKey];
}
},
};
// resizestart
InteractEvent.signals.on('new', function ({ iEvent, interaction }) {
if (iEvent.type !== 'resizestart' || !interaction.prepared.edges) {
return;
}
const startRect = interaction.target.getRect(interaction.element);
const resizeOptions = interaction.target.options.resize;
/*
* When using the `resizable.square` or `resizable.preserveAspectRatio` options, resizing from one edge
* will affect another. E.g. with `resizable.square`, resizing to make the right edge larger will make
* the bottom edge larger by the same amount. We call these 'linked' edges. Any linked edges will depend
* on the active edges and the edge being interacted with.
*/
if (resizeOptions.square || resizeOptions.preserveAspectRatio) {
const linkedEdges = utils.extend({}, interaction.prepared.edges);
linkedEdges.top = linkedEdges.top || (linkedEdges.left && !linkedEdges.bottom);
linkedEdges.left = linkedEdges.left || (linkedEdges.top && !linkedEdges.right );
linkedEdges.bottom = linkedEdges.bottom || (linkedEdges.right && !linkedEdges.top );
linkedEdges.right = linkedEdges.right || (linkedEdges.bottom && !linkedEdges.left );
interaction.prepared._linkedEdges = linkedEdges;
}
else {
interaction.prepared._linkedEdges = null;
}
// if using `resizable.preserveAspectRatio` option, record aspect ratio at the start of the resize
if (resizeOptions.preserveAspectRatio) {
interaction.resizeStartAspectRatio = startRect.width / startRect.height;
}
interaction.resizeRects = {
start : startRect,
current : utils.extend({}, startRect),
inverted : utils.extend({}, startRect),
previous : utils.extend({}, startRect),
delta : {
left: 0, right : 0, width : 0,
top : 0, bottom: 0, height: 0,
},
};
iEvent.rect = interaction.resizeRects.inverted;
iEvent.deltaRect = interaction.resizeRects.delta;
});
// resizemove
InteractEvent.signals.on('new', function ({ iEvent, phase, interaction }) {
if (phase !== 'move' || !interaction.prepared.edges) { return; }
const resizeOptions = interaction.target.options.resize;
const invert = resizeOptions.invert;
const invertible = invert === 'reposition' || invert === 'negate';
let edges = interaction.prepared.edges;
const start = interaction.resizeRects.start;
const current = interaction.resizeRects.current;
const inverted = interaction.resizeRects.inverted;
const delta = interaction.resizeRects.delta;
const previous = utils.extend(interaction.resizeRects.previous, inverted);
const originalEdges = edges;
let dx = iEvent.dx;
let dy = iEvent.dy;
if (resizeOptions.preserveAspectRatio || resizeOptions.square) {
// `resize.preserveAspectRatio` takes precedence over `resize.square`
const startAspectRatio = resizeOptions.preserveAspectRatio
? interaction.resizeStartAspectRatio
: 1;
edges = interaction.prepared._linkedEdges;
if ((originalEdges.left && originalEdges.bottom)
|| (originalEdges.right && originalEdges.top)) {
dy = -dx / startAspectRatio;
}
else if (originalEdges.left || originalEdges.right ) { dy = dx / startAspectRatio; }
else if (originalEdges.top || originalEdges.bottom) { dx = dy * startAspectRatio; }
}
// update the 'current' rect without modifications
if (edges.top ) { current.top += dy; }
if (edges.bottom) { current.bottom += dy; }
if (edges.left ) { current.left += dx; }
if (edges.right ) { current.right += dx; }
if (invertible) {
// if invertible, copy the current rect
utils.extend(inverted, current);
if (invert === 'reposition') {
// swap edge values if necessary to keep width/height positive
let swap;
if (inverted.top > inverted.bottom) {
swap = inverted.top;
inverted.top = inverted.bottom;
inverted.bottom = swap;
}
if (inverted.left > inverted.right) {
swap = inverted.left;
inverted.left = inverted.right;
inverted.right = swap;
}
}
}
else {
// if not invertible, restrict to minimum of 0x0 rect
inverted.top = Math.min(current.top, start.bottom);
inverted.bottom = Math.max(current.bottom, start.top);
inverted.left = Math.min(current.left, start.right);
inverted.right = Math.max(current.right, start.left);
}
inverted.width = inverted.right - inverted.left;
inverted.height = inverted.bottom - inverted.top ;
for (const edge in inverted) {
delta[edge] = inverted[edge] - previous[edge];
}
iEvent.edges = interaction.prepared.edges;
iEvent.rect = inverted;
iEvent.deltaRect = delta;
});
/**
* ```js
* interact(element).resizable({
* onstart: function (event) {},
* onmove : function (event) {},
* onend : function (event) {},
*
* edges: {
* top : true, // Use pointer coords to check for resize.
* left : false, // Disable resizing from left edge.
* bottom: '.resize-s',// Resize if pointer target matches selector
* right : handleEl // Resize if pointer target is the given Element
* },
*
* // Width and height can be adjusted independently. When `true`, width and
* // height are adjusted at a 1:1 ratio.
* square: false,
*
* // Width and height can be adjusted independently. When `true`, width and
* // height maintain the aspect ratio they had when resizing started.
* preserveAspectRatio: false,
*
* // a value of 'none' will limit the resize rect to a minimum of 0x0
* // 'negate' will allow the rect to have negative width/height
* // 'reposition' will keep the width/height positive by swapping
* // the top and bottom edges and/or swapping the left and right edges
* invert: 'none' || 'negate' || 'reposition'
*
* // limit multiple resizes.
* // See the explanation in the {@link Interactable.draggable} example
* max: Infinity,
* maxPerElement: 1,
* });
*
* var isResizeable = interact(element).resizable();
* ```
*
* Gets or sets whether resize actions can be performed on the target
*
* @param {boolean | object} [options] true/false or An object with event
* listeners to be fired on resize events (object makes the Interactable
* resizable)
* @return {boolean | Interactable} A boolean indicating if this can be the
* target of resize elements, or this Interactable
*/
Interactable.prototype.resizable = function (options) {
if (utils.is.object(options)) {
this.options.resize.enabled = options.enabled === false? false: true;
this.setPerAction('resize', options);
this.setOnEvents('resize', options);
if (/^x$|^y$|^xy$/.test(options.axis)) {
this.options.resize.axis = options.axis;
}
else if (options.axis === null) {
this.options.resize.axis = defaultOptions.resize.axis;
}
if (utils.is.bool(options.preserveAspectRatio)) {
this.options.resize.preserveAspectRatio = options.preserveAspectRatio;
}
else if (utils.is.bool(options.square)) {
this.options.resize.square = options.square;
}
return this;
}
if (utils.is.bool(options)) {
this.options.resize.enabled = options;
if (!options) {
this.onresizestart = this.onresizestart = this.onresizeend = null;
}
return this;
}
return this.options.resize;
};
function checkResizeEdge (name, value, page, element, interactableElement, rect, margin) {
// false, '', undefined, null
if (!value) { return false; }
// true value, use pointer coords and element rect
if (value === true) {
// if dimensions are negative, "switch" edges
const width = utils.is.number(rect.width )? rect.width : rect.right - rect.left;
const height = utils.is.number(rect.height)? rect.height : rect.bottom - rect.top ;
if (width < 0) {
if (name === 'left' ) { name = 'right'; }
else if (name === 'right') { name = 'left' ; }
}
if (height < 0) {
if (name === 'top' ) { name = 'bottom'; }
else if (name === 'bottom') { name = 'top' ; }
}
if (name === 'left' ) { return page.x < ((width >= 0? rect.left: rect.right ) + margin); }
if (name === 'top' ) { return page.y < ((height >= 0? rect.top : rect.bottom) + margin); }
if (name === 'right' ) { return page.x > ((width >= 0? rect.right : rect.left) - margin); }
if (name === 'bottom') { return page.y > ((height >= 0? rect.bottom: rect.top ) - margin); }
}
// the remaining checks require an element
if (!utils.is.element(element)) { return false; }
return utils.is.element(value)
// the value is an element to use as a resize handle
? value === element
// otherwise check if element matches value as selector
: utils.matchesUpTo(element, value, interactableElement);
}
Interaction.signals.on('new', function (interaction) {
interaction.resizeAxes = 'xy';
});
InteractEvent.signals.on('set-delta', function ({ interaction, iEvent, action }) {
if (action !== 'resize' || !interaction.resizeAxes) { return; }
const options = interaction.target.options;
if (options.resize.square) {
if (interaction.resizeAxes === 'y') {
iEvent.dx = iEvent.dy;
}
else {
iEvent.dy = iEvent.dx;
}
iEvent.axes = 'xy';
}
else {
iEvent.axes = interaction.resizeAxes;
if (interaction.resizeAxes === 'x') {
iEvent.dy = 0;
}
else if (interaction.resizeAxes === 'y') {
iEvent.dx = 0;
}
}
});
actions.resize = resize;
actions.names.push('resize');
utils.merge(Interactable.eventTypes, [
'resizestart',
'resizemove',
'resizeinertiastart',
'resizeinertiaresume',
'resizeend',
]);
actions.methodDict.resize = 'resizable';
defaultOptions.resize = resize.defaults;
module.exports = resize;