bpmn-js/lib/features/snapping/BpmnSnapping.js

458 lines
12 KiB
JavaScript

'use strict';
var inherits = require('inherits');
var forEach = require('lodash/collection/forEach');
var getBoundingBox = require('diagram-js/lib/util/Elements').getBBox;
var is = require('../../util/ModelUtil').is,
isAny = require('../modeling/util/ModelingUtil').isAny,
isExpanded = require('../../util/DiUtil').isExpanded;
var Snapping = require('diagram-js/lib/features/snapping/Snapping'),
SnapUtil = require('diagram-js/lib/features/snapping/SnapUtil');
var asTRBL = require('diagram-js/lib/layout/LayoutUtil').asTRBL;
var round = Math.round;
var mid = SnapUtil.mid,
topLeft = SnapUtil.topLeft,
bottomRight = SnapUtil.bottomRight,
isSnapped = SnapUtil.isSnapped,
setSnapped = SnapUtil.setSnapped;
var getBoundaryAttachment = require('./BpmnSnappingUtil').getBoundaryAttachment,
getParticipantSizeConstraints = require('./BpmnSnappingUtil').getParticipantSizeConstraints,
getLanesRoot = require('../modeling/util/LaneUtil').getLanesRoot;
/**
* BPMN specific snapping functionality
*
* * snap on process elements if a pool is created inside a
* process diagram
*
* @param {EventBus} eventBus
* @param {Canvas} canvas
*/
function BpmnSnapping(eventBus, canvas, bpmnRules, elementRegistry) {
// instantiate super
Snapping.call(this, eventBus, canvas);
/**
* Drop participant on process <> process elements snapping
*/
eventBus.on('create.start', function(event) {
var context = event.context,
shape = context.shape,
rootElement = canvas.getRootElement();
// snap participant around existing elements (if any)
if (is(shape, 'bpmn:Participant') && is(rootElement, 'bpmn:Process')) {
initParticipantSnapping(context, shape, rootElement.children);
}
});
eventBus.on([ 'create.move', 'create.end' ], 1500, function(event) {
var context = event.context,
shape = context.shape,
participantSnapBox = context.participantSnapBox;
if (!isSnapped(event) && participantSnapBox) {
snapParticipant(participantSnapBox, shape, event);
}
});
eventBus.on('shape.move.start', function(event) {
var context = event.context,
shape = context.shape,
rootElement = canvas.getRootElement();
// snap participant around existing elements (if any)
if (is(shape, 'bpmn:Participant') && is(rootElement, 'bpmn:Process')) {
initParticipantSnapping(context, shape, rootElement.children);
}
});
function canAttach(shape, target, position) {
return bpmnRules.canAttach([ shape ], target, null, position) === 'attach';
}
/**
* Snap boundary events to elements border
*/
eventBus.on([
'create.move',
'create.end',
'shape.move.move',
'shape.move.end'
], 1500, function(event) {
var context = event.context,
target = context.target,
shape = context.shape;
if (target && !isSnapped(event) && canAttach(shape, target, event)) {
snapBoundaryEvent(event, shape, target);
}
});
/**
* Adjust parent for flowElements to the target participant
* when droping onto lanes.
*/
eventBus.on([
'shape.move.hover',
'shape.move.move',
'shape.move.end',
'create.hover',
'create.move',
'create.end'
], 1500, function(event) {
var context = event.context,
shape = context.shape,
hover = event.hover;
if (is(hover, 'bpmn:Lane') && !isAny(shape, [ 'bpmn:Lane', 'bpmn:Participant' ])) {
event.hover = getLanesRoot(hover);
event.hoverGfx = elementRegistry.getGraphics(event.hover);
}
});
var abs = Math.abs;
var filter = require('lodash/collection/filter'),
assign = require('lodash/object/assign');
eventBus.on([
'create.move',
'shape.move.move'
], function(event) {
var context = event.context,
shape = context.shape,
target = context.target;
var threshold = 30;
if (is(shape, 'bpmn:Lane')) {
if (isAny(target, [ 'bpmn:Lane', 'bpmn:Participant' ])) {
var childLanes = filter(target.children, function(c) {
return is(c, 'bpmn:Lane');
});
var y = event.y,
targetTrbl;
var insert = childLanes.reduce(function(insert, l) {
var laneTrbl = asTRBL(l);
if (abs(laneTrbl.top - y) < threshold) {
insert = assign(insert || {}, { before: { element: l, y: laneTrbl.top } });
} else
if (abs(laneTrbl.bottom - y) < threshold) {
insert = assign(insert || {}, { after: { element: l, y: laneTrbl.bottom } });
} else
if (laneTrbl.top < y && laneTrbl.bottom > y) {
if (abs(laneTrbl.top - y) > abs(laneTrbl.bottom - y)) {
insert = assign(insert || {}, { after: { element: l, y: laneTrbl.bottom } });
} else {
insert = assign(insert || {}, { before: { element: l, y: laneTrbl.top } });
}
}
return insert;
}, false);
if (!insert) {
targetTrbl = asTRBL(target);
if (abs(targetTrbl.top - y) < threshold) {
insert = { before: { element: target, y: targetTrbl.top } };
} else
if (abs(targetTrbl.bottom - y) < threshold) {
insert = { after: { element: target, y: targetTrbl.bottom } };
} else {
insert = { into: { element: target, y: (targetTrbl.top + targetTrbl.bottom) / 2 } };
}
}
if (insert.before && insert.after) {
console.log('insert between', insert.before.element.id, 'and', insert.after.element.id);
setSnapped(event, 'x', insert.before.element.x + insert.before.element.width / 2);
setSnapped(event, 'y', insert.before.y);
} else
if (insert.after) {
console.log('insert after', insert.after.element.id);
setSnapped(event, 'x', insert.after.element.x + insert.after.element.width / 2);
setSnapped(event, 'y', insert.after.y);
} else
if (insert.before) {
console.log('insert before', insert.before.element.id);
setSnapped(event, 'x', insert.before.element.x + insert.before.element.width / 2);
setSnapped(event, 'y', insert.before.y);
} else
if (insert.into) {
console.log('insert into', insert.into.element.id);
setSnapped(event, 'x', insert.into.element.x + insert.into.element.width / 2);
setSnapped(event, 'y', insert.into.y);
}
}
}
});
eventBus.on('resize.start', 1500, function(event) {
var context = event.context,
shape = context.shape;
if (is(shape, 'bpmn:SubProcess') && isExpanded(shape)) {
context.minDimensions = { width: 140, height: 120 };
}
if (is(shape, 'bpmn:Participant')) {
context.minDimensions = { width: 300, height: 150 };
}
if (is(shape, 'bpmn:Lane') || is(shape, 'bpmn:Participant')) {
context.resizeConstraints = getParticipantSizeConstraints(shape, context.direction, context.balanced);
}
if (is(shape, 'bpmn:TextAnnotation')) {
context.minDimensions = { width: 50, height: 50 };
}
});
}
inherits(BpmnSnapping, Snapping);
BpmnSnapping.$inject = [ 'eventBus', 'canvas', 'bpmnRules', 'elementRegistry' ];
module.exports = BpmnSnapping;
BpmnSnapping.prototype.initSnap = function(event) {
var context = event.context,
shape = event.shape,
shapeMid,
shapeBounds,
shapeTopLeft,
shapeBottomRight,
snapContext;
snapContext = Snapping.prototype.initSnap.call(this, event);
if (is(shape, 'bpmn:Participant')) {
// assign higher priority for outer snaps on participants
snapContext.setSnapLocations([ 'top-left', 'bottom-right', 'mid' ]);
}
if (shape) {
shapeMid = mid(shape, event);
shapeBounds = {
width: shape.width,
height: shape.height,
x: isNaN(shape.x) ? round(shapeMid.x - shape.width / 2) : shape.x,
y: isNaN(shape.y) ? round(shapeMid.y - shape.height / 2) : shape.y,
};
shapeTopLeft = topLeft(shapeBounds);
shapeBottomRight = bottomRight(shapeBounds);
snapContext.setSnapOrigin('top-left', {
x: shapeTopLeft.x - event.x,
y: shapeTopLeft.y - event.y
});
snapContext.setSnapOrigin('bottom-right', {
x: shapeBottomRight.x - event.x,
y: shapeBottomRight.y - event.y
});
forEach(shape.outgoing, function(c) {
var docking = c.waypoints[0];
docking = docking.original || docking;
snapContext.setSnapOrigin(c.id + '-docking', {
x: docking.x - event.x,
y: docking.y - event.y
});
});
forEach(shape.incoming, function(c) {
var docking = c.waypoints[c.waypoints.length - 1];
docking = docking.original || docking;
snapContext.setSnapOrigin(c.id + '-docking', {
x: docking.x - event.x,
y: docking.y - event.y
});
});
}
var source = context.source;
if (source) {
snapContext.addDefaultSnap('mid', mid(source));
}
};
BpmnSnapping.prototype.addTargetSnaps = function(snapPoints, shape, target) {
// use target parent as snap target
if (is(shape, 'bpmn:BoundaryEvent') && shape.type !== 'label') {
target = target.parent;
}
// add sequence flow parents as snap targets
if (is(target, 'bpmn:SequenceFlow')) {
this.addTargetSnaps(snapPoints, shape, target.parent);
}
var siblings = this.getSiblings(shape, target) || [];
forEach(siblings, function(s) {
// do not snap to lanes
if (is(s, 'bpmn:Lane')) {
return;
}
snapPoints.add('mid', mid(s));
if (is(s, 'bpmn:Participant')) {
snapPoints.add('top-left', topLeft(s));
snapPoints.add('bottom-right', bottomRight(s));
}
});
forEach(shape.incoming, function(c) {
if (siblings.indexOf(c.source) === -1) {
snapPoints.add('mid', mid(c.source));
}
var docking = c.waypoints[0];
snapPoints.add(c.id + '-docking', docking.original || docking);
});
forEach(shape.outgoing, function(c) {
if (siblings.indexOf(c.target) === -1) {
snapPoints.add('mid', mid(c.target));
}
var docking = c.waypoints[c.waypoints.length - 1];
snapPoints.add(c.id + '-docking', docking.original || docking);
});
};
/////// participant snapping //////////////////
function initParticipantSnapping(context, shape, elements) {
if (!elements.length) {
return;
}
var snapBox = getBoundingBox(elements.filter(function(e) {
return !e.labelTarget && !e.waypoints;
}));
snapBox.x -= 50;
snapBox.y -= 20;
snapBox.width += 70;
snapBox.height += 40;
// adjust shape height to include bounding box
shape.width = Math.max(shape.width, snapBox.width);
shape.height = Math.max(shape.height, snapBox.height);
context.participantSnapBox = snapBox;
}
function snapParticipant(snapBox, shape, event, offset) {
offset = offset || 0;
var shapeHalfWidth = shape.width / 2 - offset,
shapeHalfHeight = shape.height / 2;
var currentTopLeft = {
x: event.x - shapeHalfWidth - offset,
y: event.y - shapeHalfHeight
};
var currentBottomRight = {
x: event.x + shapeHalfWidth + offset,
y: event.y + shapeHalfHeight
};
var snapTopLeft = snapBox,
snapBottomRight = bottomRight(snapBox);
if (currentTopLeft.x >= snapTopLeft.x) {
setSnapped(event, 'x', snapTopLeft.x + offset + shapeHalfWidth);
} else
if (currentBottomRight.x <= snapBottomRight.x) {
setSnapped(event, 'x', snapBottomRight.x - offset - shapeHalfWidth);
}
if (currentTopLeft.y >= snapTopLeft.y) {
setSnapped(event, 'y', snapTopLeft.y + shapeHalfHeight);
} else
if (currentBottomRight.y <= snapBottomRight.y) {
setSnapped(event, 'y', snapBottomRight.y - shapeHalfHeight);
}
}
/////// boundary event snapping /////////////////////////
function snapBoundaryEvent(event, shape, target) {
var targetTRBL = asTRBL(target);
var direction = getBoundaryAttachment(event, target);
if (/top/.test(direction)) {
setSnapped(event, 'y', targetTRBL.top);
} else
if (/bottom/.test(direction)) {
setSnapped(event, 'y', targetTRBL.bottom);
}
if (/left/.test(direction)) {
setSnapped(event, 'x', targetTRBL.left);
} else
if (/right/.test(direction)) {
setSnapped(event, 'x', targetTRBL.right);
}
}