627 lines
16 KiB
JavaScript
627 lines
16 KiB
JavaScript
import {
|
|
assign,
|
|
filter,
|
|
forEach,
|
|
isNumber
|
|
} from 'min-dash';
|
|
|
|
import {
|
|
asTRBL,
|
|
getMid
|
|
} from '../../layout/LayoutUtil';
|
|
|
|
import { getBBox } from '../../util/Elements';
|
|
|
|
import { getDirection } from './SpaceUtil';
|
|
|
|
import { hasPrimaryModifier } from '../../util/Mouse';
|
|
|
|
import { set as setCursor } from '../../util/Cursor';
|
|
|
|
import { selfAndAllChildren } from '../../util/Elements';
|
|
|
|
/**
|
|
* @typedef {import('../../model').Shape} Shape
|
|
*
|
|
* @typedef {import('../../core/Canvas').default} Canvas
|
|
* @typedef {import('../dragging/Dragging').default} Dragging
|
|
* @typedef {import('../../core/EventBus').default} EventBus
|
|
* @typedef {import('../modeling/Modeling').default} Modeling
|
|
* @typedef {import('../mouse/Mouse').default} Mouse
|
|
* @typedef {import('../rules/Rules').default} Rules
|
|
* @typedef {import('../tool-manager/ToolManager').default} ToolManager
|
|
*/
|
|
|
|
var abs = Math.abs,
|
|
round = Math.round;
|
|
|
|
var AXIS_TO_DIMENSION = {
|
|
x: 'width',
|
|
y: 'height'
|
|
};
|
|
|
|
var CURSOR_CROSSHAIR = 'crosshair';
|
|
|
|
var DIRECTION_TO_TRBL = {
|
|
n: 'top',
|
|
w: 'left',
|
|
s: 'bottom',
|
|
e: 'right'
|
|
};
|
|
|
|
var HIGH_PRIORITY = 1500;
|
|
|
|
var DIRECTION_TO_OPPOSITE = {
|
|
n: 's',
|
|
w: 'e',
|
|
s: 'n',
|
|
e: 'w'
|
|
};
|
|
|
|
var PADDING = 20;
|
|
|
|
|
|
/**
|
|
* Add or remove space by moving and resizing elements.
|
|
*
|
|
* @param {Canvas} canvas
|
|
* @param {Dragging} dragging
|
|
* @param {EventBus} eventBus
|
|
* @param {Modeling} modeling
|
|
* @param {Rules} rules
|
|
* @param {ToolManager} toolManager
|
|
* @param {Mouse} mouse
|
|
*/
|
|
export default function SpaceTool(
|
|
canvas, dragging, eventBus,
|
|
modeling, rules, toolManager,
|
|
mouse) {
|
|
|
|
this._canvas = canvas;
|
|
this._dragging = dragging;
|
|
this._eventBus = eventBus;
|
|
this._modeling = modeling;
|
|
this._rules = rules;
|
|
this._toolManager = toolManager;
|
|
this._mouse = mouse;
|
|
|
|
var self = this;
|
|
|
|
toolManager.registerTool('space', {
|
|
tool: 'spaceTool.selection',
|
|
dragging: 'spaceTool'
|
|
});
|
|
|
|
eventBus.on('spaceTool.selection.end', function(event) {
|
|
eventBus.once('spaceTool.selection.ended', function() {
|
|
self.activateMakeSpace(event.originalEvent);
|
|
});
|
|
});
|
|
|
|
eventBus.on('spaceTool.move', HIGH_PRIORITY , function(event) {
|
|
var context = event.context,
|
|
initialized = context.initialized;
|
|
|
|
if (!initialized) {
|
|
initialized = context.initialized = self.init(event, context);
|
|
}
|
|
|
|
if (initialized) {
|
|
ensureConstraints(event);
|
|
}
|
|
});
|
|
|
|
eventBus.on('spaceTool.end', function(event) {
|
|
var context = event.context,
|
|
axis = context.axis,
|
|
direction = context.direction,
|
|
movingShapes = context.movingShapes,
|
|
resizingShapes = context.resizingShapes,
|
|
start = context.start;
|
|
|
|
if (!context.initialized) {
|
|
return;
|
|
}
|
|
|
|
ensureConstraints(event);
|
|
|
|
var delta = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
|
|
delta[ axis ] = round(event[ 'd' + axis ]);
|
|
|
|
self.makeSpace(movingShapes, resizingShapes, delta, direction, start);
|
|
|
|
eventBus.once('spaceTool.ended', function(event) {
|
|
|
|
// activate space tool selection after make space
|
|
self.activateSelection(event.originalEvent, true, true);
|
|
});
|
|
});
|
|
}
|
|
|
|
SpaceTool.$inject = [
|
|
'canvas',
|
|
'dragging',
|
|
'eventBus',
|
|
'modeling',
|
|
'rules',
|
|
'toolManager',
|
|
'mouse'
|
|
];
|
|
|
|
/**
|
|
* Activate space tool selection.
|
|
*
|
|
* @param {Object} event
|
|
* @param {boolean} autoActivate
|
|
*/
|
|
SpaceTool.prototype.activateSelection = function(event, autoActivate, reactivate) {
|
|
this._dragging.init(event, 'spaceTool.selection', {
|
|
autoActivate: autoActivate,
|
|
cursor: CURSOR_CROSSHAIR,
|
|
data: {
|
|
context: {
|
|
reactivate: reactivate
|
|
}
|
|
},
|
|
trapClick: false
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Activate space tool make space.
|
|
*
|
|
* @param {MouseEvent|TouchEvent} event
|
|
*/
|
|
SpaceTool.prototype.activateMakeSpace = function(event) {
|
|
this._dragging.init(event, 'spaceTool', {
|
|
autoActivate: true,
|
|
cursor: CURSOR_CROSSHAIR,
|
|
data: {
|
|
context: {}
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Make space.
|
|
*
|
|
* @param {Array<Shape>} movingShapes
|
|
* @param {Array<Shape>} resizingShapes
|
|
* @param {Object} delta
|
|
* @param {number} delta.x
|
|
* @param {number} delta.y
|
|
* @param {string} direction
|
|
* @param {number} start
|
|
*/
|
|
SpaceTool.prototype.makeSpace = function(movingShapes, resizingShapes, delta, direction, start) {
|
|
return this._modeling.createSpace(movingShapes, resizingShapes, delta, direction, start);
|
|
};
|
|
|
|
/**
|
|
* Initialize make space and return true if that was successful.
|
|
*
|
|
* @param {Object} event
|
|
* @param {Object} context
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
SpaceTool.prototype.init = function(event, context) {
|
|
var axis = abs(event.dx) > abs(event.dy) ? 'x' : 'y',
|
|
delta = event[ 'd' + axis ],
|
|
start = event[ axis ] - delta;
|
|
|
|
if (abs(delta) < 5) {
|
|
return false;
|
|
}
|
|
|
|
// invert delta to remove space when moving left
|
|
if (delta < 0) {
|
|
delta *= -1;
|
|
}
|
|
|
|
// invert delta to add/remove space when removing/adding space if modifier key is pressed
|
|
if (hasPrimaryModifier(event)) {
|
|
delta *= -1;
|
|
}
|
|
|
|
var direction = getDirection(axis, delta);
|
|
|
|
var root = this._canvas.getRootElement();
|
|
|
|
var children = selfAndAllChildren(root, true);
|
|
|
|
var elements = this.calculateAdjustments(children, axis, delta, start);
|
|
|
|
var minDimensions = this._eventBus.fire('spaceTool.getMinDimensions', {
|
|
axis: axis,
|
|
direction: direction,
|
|
shapes: elements.resizingShapes,
|
|
start: start
|
|
});
|
|
|
|
var spaceToolConstraints = getSpaceToolConstraints(elements, axis, direction, start, minDimensions);
|
|
|
|
assign(
|
|
context,
|
|
elements,
|
|
{
|
|
axis: axis,
|
|
direction: direction,
|
|
spaceToolConstraints: spaceToolConstraints,
|
|
start: start
|
|
}
|
|
);
|
|
|
|
setCursor('resize-' + (axis === 'x' ? 'ew' : 'ns'));
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Get elements to be moved and resized.
|
|
*
|
|
* @param {Array<Shape>} elements
|
|
* @param {string} axis
|
|
* @param {number} delta
|
|
* @param {number} start
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
SpaceTool.prototype.calculateAdjustments = function(elements, axis, delta, start) {
|
|
var rules = this._rules;
|
|
|
|
var movingShapes = [],
|
|
resizingShapes = [];
|
|
|
|
var attachers = [],
|
|
connections = [];
|
|
|
|
function moveShape(shape) {
|
|
if (!movingShapes.includes(shape)) {
|
|
movingShapes.push(shape);
|
|
}
|
|
|
|
var label = shape.label;
|
|
|
|
// move external label if its label target is moving
|
|
if (label && !movingShapes.includes(label)) {
|
|
movingShapes.push(label);
|
|
}
|
|
}
|
|
|
|
function resizeShape(shape) {
|
|
if (!resizingShapes.includes(shape)) {
|
|
resizingShapes.push(shape);
|
|
}
|
|
}
|
|
|
|
forEach(elements, function(element) {
|
|
if (!element.parent || isLabel(element)) {
|
|
return;
|
|
}
|
|
|
|
// handle connections separately
|
|
if (isConnection(element)) {
|
|
connections.push(element);
|
|
|
|
return;
|
|
}
|
|
|
|
var shapeStart = element[ axis ],
|
|
shapeEnd = shapeStart + element[ AXIS_TO_DIMENSION[ axis ] ];
|
|
|
|
// handle attachers separately
|
|
if (isAttacher(element)
|
|
&& ((delta > 0 && getMid(element)[ axis ] > start)
|
|
|| (delta < 0 && getMid(element)[ axis ] < start))) {
|
|
attachers.push(element);
|
|
|
|
return;
|
|
}
|
|
|
|
// move shape if its start is after space tool
|
|
if ((delta > 0 && shapeStart > start)
|
|
|| (delta < 0 && shapeEnd < start)) {
|
|
moveShape(element);
|
|
|
|
return;
|
|
}
|
|
|
|
// resize shape if it's resizable and its start is before and its end is after space tool
|
|
if (shapeStart < start
|
|
&& shapeEnd > start
|
|
&& rules.allowed('shape.resize', { shape: element })
|
|
) {
|
|
resizeShape(element);
|
|
|
|
return;
|
|
}
|
|
});
|
|
|
|
// move attacher if its host is moving
|
|
forEach(movingShapes, function(shape) {
|
|
var attachers = shape.attachers;
|
|
|
|
if (attachers) {
|
|
forEach(attachers, function(attacher) {
|
|
moveShape(attacher);
|
|
});
|
|
}
|
|
});
|
|
|
|
var allShapes = movingShapes.concat(resizingShapes);
|
|
|
|
// move attacher if its mid is after space tool and its host is moving or resizing
|
|
forEach(attachers, function(attacher) {
|
|
var host = attacher.host;
|
|
|
|
if (includes(allShapes, host)) {
|
|
moveShape(attacher);
|
|
}
|
|
});
|
|
|
|
allShapes = movingShapes.concat(resizingShapes);
|
|
|
|
// move external label if its label target's (connection) source and target are moving
|
|
forEach(connections, function(connection) {
|
|
var source = connection.source,
|
|
target = connection.target,
|
|
label = connection.label;
|
|
|
|
if (includes(allShapes, source)
|
|
&& includes(allShapes, target)
|
|
&& label) {
|
|
moveShape(label);
|
|
}
|
|
});
|
|
|
|
return {
|
|
movingShapes: movingShapes,
|
|
resizingShapes: resizingShapes
|
|
};
|
|
};
|
|
|
|
SpaceTool.prototype.toggle = function() {
|
|
|
|
if (this.isActive()) {
|
|
return this._dragging.cancel();
|
|
}
|
|
|
|
var mouseEvent = this._mouse.getLastMoveEvent();
|
|
|
|
this.activateSelection(mouseEvent, !!mouseEvent);
|
|
};
|
|
|
|
SpaceTool.prototype.isActive = function() {
|
|
var context = this._dragging.context();
|
|
|
|
if (context) {
|
|
return /^spaceTool/.test(context.prefix);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// helpers //////////
|
|
|
|
function addPadding(trbl) {
|
|
return {
|
|
top: trbl.top - PADDING,
|
|
right: trbl.right + PADDING,
|
|
bottom: trbl.bottom + PADDING,
|
|
left: trbl.left - PADDING
|
|
};
|
|
}
|
|
|
|
function ensureConstraints(event) {
|
|
var context = event.context,
|
|
spaceToolConstraints = context.spaceToolConstraints;
|
|
|
|
if (!spaceToolConstraints) {
|
|
return;
|
|
}
|
|
|
|
var x, y;
|
|
|
|
if (isNumber(spaceToolConstraints.left)) {
|
|
x = Math.max(event.x, spaceToolConstraints.left);
|
|
|
|
event.dx = event.dx + x - event.x;
|
|
event.x = x;
|
|
}
|
|
|
|
if (isNumber(spaceToolConstraints.right)) {
|
|
x = Math.min(event.x, spaceToolConstraints.right);
|
|
|
|
event.dx = event.dx + x - event.x;
|
|
event.x = x;
|
|
}
|
|
|
|
if (isNumber(spaceToolConstraints.top)) {
|
|
y = Math.max(event.y, spaceToolConstraints.top);
|
|
|
|
event.dy = event.dy + y - event.y;
|
|
event.y = y;
|
|
}
|
|
|
|
if (isNumber(spaceToolConstraints.bottom)) {
|
|
y = Math.min(event.y, spaceToolConstraints.bottom);
|
|
|
|
event.dy = event.dy + y - event.y;
|
|
event.y = y;
|
|
}
|
|
}
|
|
|
|
function getSpaceToolConstraints(elements, axis, direction, start, minDimensions) {
|
|
var movingShapes = elements.movingShapes,
|
|
resizingShapes = elements.resizingShapes;
|
|
|
|
if (!resizingShapes.length) {
|
|
return;
|
|
}
|
|
|
|
var spaceToolConstraints = {},
|
|
min,
|
|
max;
|
|
|
|
forEach(resizingShapes, function(resizingShape) {
|
|
var attachers = resizingShape.attachers,
|
|
children = resizingShape.children;
|
|
|
|
var resizingShapeBBox = asTRBL(resizingShape);
|
|
|
|
// find children that are not moving or resizing
|
|
var nonMovingResizingChildren = filter(children, function(child) {
|
|
return !isConnection(child) &&
|
|
!isLabel(child) &&
|
|
!includes(movingShapes, child) &&
|
|
!includes(resizingShapes, child);
|
|
});
|
|
|
|
// find children that are moving
|
|
var movingChildren = filter(children, function(child) {
|
|
return !isConnection(child) && !isLabel(child) && includes(movingShapes, child);
|
|
});
|
|
|
|
var minOrMax,
|
|
nonMovingResizingChildrenBBox,
|
|
movingChildrenBBox,
|
|
movingAttachers = [],
|
|
nonMovingAttachers = [],
|
|
movingAttachersBBox,
|
|
movingAttachersConstraint,
|
|
nonMovingAttachersBBox,
|
|
nonMovingAttachersConstraint;
|
|
|
|
if (nonMovingResizingChildren.length) {
|
|
nonMovingResizingChildrenBBox = addPadding(asTRBL(getBBox(nonMovingResizingChildren)));
|
|
|
|
minOrMax = start -
|
|
resizingShapeBBox[ DIRECTION_TO_TRBL[ direction ] ] +
|
|
nonMovingResizingChildrenBBox[ DIRECTION_TO_TRBL[ direction ] ];
|
|
|
|
if (direction === 'n') {
|
|
spaceToolConstraints.bottom = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
|
|
} else if (direction === 'w') {
|
|
spaceToolConstraints.right = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
|
|
} else if (direction === 's') {
|
|
spaceToolConstraints.top = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
|
|
} else if (direction === 'e') {
|
|
spaceToolConstraints.left = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
|
|
}
|
|
}
|
|
|
|
if (movingChildren.length) {
|
|
movingChildrenBBox = addPadding(asTRBL(getBBox(movingChildren)));
|
|
|
|
minOrMax = start -
|
|
movingChildrenBBox[ DIRECTION_TO_TRBL[ DIRECTION_TO_OPPOSITE[ direction ] ] ] +
|
|
resizingShapeBBox[ DIRECTION_TO_TRBL[ DIRECTION_TO_OPPOSITE[ direction ] ] ];
|
|
|
|
if (direction === 'n') {
|
|
spaceToolConstraints.bottom = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
|
|
} else if (direction === 'w') {
|
|
spaceToolConstraints.right = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
|
|
} else if (direction === 's') {
|
|
spaceToolConstraints.top = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
|
|
} else if (direction === 'e') {
|
|
spaceToolConstraints.left = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
|
|
}
|
|
}
|
|
|
|
if (attachers && attachers.length) {
|
|
attachers.forEach(function(attacher) {
|
|
if (includes(movingShapes, attacher)) {
|
|
movingAttachers.push(attacher);
|
|
} else {
|
|
nonMovingAttachers.push(attacher);
|
|
}
|
|
});
|
|
|
|
if (movingAttachers.length) {
|
|
movingAttachersBBox = asTRBL(getBBox(movingAttachers.map(getMid)));
|
|
|
|
movingAttachersConstraint = resizingShapeBBox[ DIRECTION_TO_TRBL[ DIRECTION_TO_OPPOSITE[ direction ] ] ]
|
|
- (movingAttachersBBox[ DIRECTION_TO_TRBL[ DIRECTION_TO_OPPOSITE[ direction ] ] ] - start);
|
|
}
|
|
|
|
if (nonMovingAttachers.length) {
|
|
nonMovingAttachersBBox = asTRBL(getBBox(nonMovingAttachers.map(getMid)));
|
|
|
|
nonMovingAttachersConstraint = nonMovingAttachersBBox[ DIRECTION_TO_TRBL[ direction ] ]
|
|
- (resizingShapeBBox[ DIRECTION_TO_TRBL[ direction ] ] - start);
|
|
}
|
|
|
|
if (direction === 'n') {
|
|
minOrMax = Math.min(movingAttachersConstraint || Infinity, nonMovingAttachersConstraint || Infinity);
|
|
|
|
spaceToolConstraints.bottom = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
|
|
} else if (direction === 'w') {
|
|
minOrMax = Math.min(movingAttachersConstraint || Infinity, nonMovingAttachersConstraint || Infinity);
|
|
|
|
spaceToolConstraints.right = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
|
|
} else if (direction === 's') {
|
|
minOrMax = Math.max(movingAttachersConstraint || -Infinity, nonMovingAttachersConstraint || -Infinity);
|
|
|
|
spaceToolConstraints.top = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
|
|
} else if (direction === 'e') {
|
|
minOrMax = Math.max(movingAttachersConstraint || -Infinity, nonMovingAttachersConstraint || -Infinity);
|
|
|
|
spaceToolConstraints.left = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
|
|
}
|
|
}
|
|
|
|
var resizingShapeMinDimensions = minDimensions && minDimensions[ resizingShape.id ];
|
|
|
|
if (resizingShapeMinDimensions) {
|
|
|
|
if (direction === 'n') {
|
|
minOrMax = start +
|
|
resizingShape[ AXIS_TO_DIMENSION [ axis ] ] -
|
|
resizingShapeMinDimensions[ AXIS_TO_DIMENSION[ axis ] ];
|
|
|
|
spaceToolConstraints.bottom = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
|
|
} else if (direction === 'w') {
|
|
minOrMax = start +
|
|
resizingShape[ AXIS_TO_DIMENSION [ axis ] ] -
|
|
resizingShapeMinDimensions[ AXIS_TO_DIMENSION[ axis ] ];
|
|
|
|
spaceToolConstraints.right = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
|
|
} else if (direction === 's') {
|
|
minOrMax = start -
|
|
resizingShape[ AXIS_TO_DIMENSION [ axis ] ] +
|
|
resizingShapeMinDimensions[ AXIS_TO_DIMENSION[ axis ] ];
|
|
|
|
spaceToolConstraints.top = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
|
|
} else if (direction === 'e') {
|
|
minOrMax = start -
|
|
resizingShape[ AXIS_TO_DIMENSION [ axis ] ] +
|
|
resizingShapeMinDimensions[ AXIS_TO_DIMENSION[ axis ] ];
|
|
|
|
spaceToolConstraints.left = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
|
|
}
|
|
}
|
|
});
|
|
|
|
return spaceToolConstraints;
|
|
}
|
|
|
|
function includes(array, item) {
|
|
return array.indexOf(item) !== -1;
|
|
}
|
|
|
|
function isAttacher(element) {
|
|
return !!element.host;
|
|
}
|
|
|
|
function isConnection(element) {
|
|
return !!element.waypoints;
|
|
}
|
|
|
|
function isLabel(element) {
|
|
return !!element.labelTarget;
|
|
} |