feat(modeling): add auto placement from context menu

Elements will automatically be created at appropriate
positions when context menu create entries are being
clicked (rather than dragged).

This marks a major step forward for mobile modeling,
too as dragging, especially dragging out from very small
controls is very cumbersome to do.

Things we take into account:

* for bpmn:FlowNodes, we try to compute the current
  distance between elements on the flow based on
  connections going in and out of the flow nodes
  source element
* for bpmn:TextAnnotation we assume placement of the
  element top right of the source shape
* for bpmn:DataObject and friends we assume a
  placement bottom right of the source shape
* for all elements, we try not to place elements on
  top of each other; i.e. new elements will be pushed
  up or down accordingly, if an element at a chosen
  position does already exist

Integration into other services:

* context pad provider works with autoPlace, if
  available and defaults to drag start without
* auto placed elements are selected and direct editing
  may conditionally be activated based on element type
  (LabelEditingProvider knows the rules)

Users can out out of autoPlace by specifying the configuration
property `config.contextPad.autoPlace = false`.

Closes #563

BREAKING CHANGE:

* This breaks the default interaction from the context
  pad; if you rely on clicking to start the drag
  you can opt out of autoPlace:

  ```
  new BpmnJS({ contextPad: { autoPlace: false } });
  ```
This commit is contained in:
Nico Rehwaldt 2017-12-08 21:06:08 +01:00
parent 6b5277b936
commit ae96f3714d
10 changed files with 867 additions and 7 deletions

View File

@ -185,6 +185,7 @@ Modeler.prototype._modelingModules = [
require('diagram-js/lib/features/move'), require('diagram-js/lib/features/move'),
require('diagram-js/lib/features/resize'), require('diagram-js/lib/features/resize'),
require('./features/auto-resize'), require('./features/auto-resize'),
require('./features/auto-place'),
require('./features/editor-actions'), require('./features/editor-actions'),
require('./features/context-pad'), require('./features/context-pad'),
require('./features/keyboard'), require('./features/keyboard'),

View File

@ -0,0 +1,91 @@
'use strict';
var is = require('../../util/ModelUtil').is;
var isAny = require('../modeling/util/ModelingUtil').isAny;
var getTextAnnotationPosition = require('./AutoPlaceUtil').getTextAnnotationPosition,
getDataElementPosition = require('./AutoPlaceUtil').getDataElementPosition,
getFlowNodePosition = require('./AutoPlaceUtil').getFlowNodePosition,
getDefaultPosition = require('./AutoPlaceUtil').getDefaultPosition;
/**
* A service that places elements connected to existing ones
* to an appropriate position in an _automated_ fashion.
*
* @param {EventBus} eventBus
* @param {Modeling} modeling
*/
function AutoPlace(eventBus, modeling) {
function emit(event, payload) {
return eventBus.fire(event, payload);
}
/**
* Append shape to source at appropriate position.
*
* @param {djs.model.Shape} source
* @param {djs.model.Shape} shape
*
* @return {djs.model.Shape} appended shape
*/
this.append = function(source, shape) {
// allow others to provide the position
var position = emit('autoPlace', {
source: source,
shape: shape
});
if (!position) {
position = getNewShapePosition(source, shape);
}
var newShape = modeling.appendShape(source, shape, position, source.parent);
// notify interested parties on new shape placed
emit('autoPlace.end', {
shape: newShape
});
return newShape;
};
}
AutoPlace.$inject = [
'eventBus',
'modeling'
];
module.exports = AutoPlace;
/////////// helpers /////////////////////////////////////
/**
* Find the new position for the target element to
* connect to source.
*
* @param {djs.model.Shape} source
* @param {djs.model.Shape} element
*
* @return {Point}
*/
function getNewShapePosition(source, element) {
if (is(element, 'bpmn:TextAnnotation')) {
return getTextAnnotationPosition(source, element);
}
if (isAny(element, [ 'bpmn:DataObjectReference', 'bpmn:DataStoreReference' ])) {
return getDataElementPosition(source, element);
}
if (is(element, 'bpmn:FlowNode')) {
return getFlowNodePosition(source, element);
}
return getDefaultPosition(source, element);
}

View File

@ -0,0 +1,22 @@
'use strict';
/**
* Select element after auto placement.
*
* @param {EventBus} eventBus
* @param {Selection} selection
*/
function AutoPlaceSelectionBehavior(eventBus, selection) {
eventBus.on('autoPlace.end', 500, function(e) {
selection.select(e.shape);
});
}
AutoPlaceSelectionBehavior.$inject = [
'eventBus',
'selection'
];
module.exports = AutoPlaceSelectionBehavior;

View File

@ -0,0 +1,262 @@
'use strict';
var is = require('../../util/ModelUtil').is;
var getMid = require('diagram-js/lib/layout/LayoutUtil').getMid,
asTRBL = require('diagram-js/lib/layout/LayoutUtil').asTRBL,
getOrientation = require('diagram-js/lib/layout/LayoutUtil').getOrientation;
var find = require('lodash/collection/find'),
reduce = require('lodash/collection/reduce'),
filter = require('lodash/collection/filter');
var DEFAULT_HORIZONTAL_DISTANCE = 50;
var MAX_HORIZONTAL_DISTANCE = 250;
// padding to detect element placement
var PLACEMENT_DETECTION_PAD = 10;
/**
* Always try to place element right of source;
* compute actual distance from previous nodes in flow.
*/
function getFlowNodePosition(source, element) {
var sourceTrbl = asTRBL(source);
var sourceMid = getMid(source);
var horizontalDistance = getFlowNodeDistance(source, element);
var position = {
x: sourceTrbl.right + horizontalDistance + element.width / 2,
y: sourceMid.y
};
var existingTarget;
// make sure we don't place targets a
while ((existingTarget = getConnectedAtPosition(source, position, element))) {
position = {
x: position.x,
y: Math.max(
existingTarget.y + existingTarget.height + 30 + element.height / 2,
position.y + 80
)
};
}
return position;
}
module.exports.getFlowNodePosition = getFlowNodePosition;
/**
* Compute best distance between source and target,
* based on existing connections to and from source.
*
* @param {djs.model.Shape} source
* @param {djs.model.Shape} element
*
* @return {Number} distance
*/
function getFlowNodeDistance(source, element) {
var sourceTrbl = asTRBL(source);
// is connection a reference to consider?
var isReference = function(c) {
return is(c, 'bpmn:SequenceFlow');
};
var nodes = [].concat(
filter(source.outgoing, isReference).map(function(c) {
return {
shape: c.target,
weight: 5,
distanceTo: function(shape) {
var shapeTrbl = asTRBL(shape);
return shapeTrbl.left - sourceTrbl.right;
}
};
}),
filter(source.incoming, isReference).map(function(c) {
return {
shape: c.source,
weight: 1,
distanceTo: function(shape) {
var shapeTrbl = asTRBL(shape);
return sourceTrbl.left - shapeTrbl.right;
}
};
})
);
// compute distances between source and incoming nodes;
// group at the same time by distance and expose the
// favourite distance as { fav: { count, value } }.
var distancesGrouped = reduce(nodes, function(result, node) {
var shape = node.shape,
weight = node.weight,
distanceTo = node.distanceTo;
var fav = result.fav,
currentDistance,
currentDistanceCount,
currentDistanceEntry;
currentDistance = distanceTo(shape);
// ignore too far away peers
// or non-left to right modeled nodes
if (currentDistance < 0 || currentDistance > MAX_HORIZONTAL_DISTANCE) {
return result;
}
currentDistanceEntry = result[String(currentDistance)] =
result[String(currentDistance)] || {
value: currentDistance,
count: 0
};
// inc diff count
currentDistanceCount = currentDistanceEntry.count += 1 * weight;
if (!fav || fav.count < currentDistanceCount) {
result.fav = currentDistanceEntry;
}
return result;
}, { });
if (distancesGrouped.fav) {
return distancesGrouped.fav.value;
} else {
return DEFAULT_HORIZONTAL_DISTANCE;
}
}
module.exports.getFlowNodeDistance = getFlowNodeDistance;
/**
* Always try to place text annotations top right of source.
*/
function getTextAnnotationPosition(source, element) {
var sourceTrbl = asTRBL(source);
// todo: adaptive
var position = {
x: sourceTrbl.right + 30 + element.width / 2,
y: sourceTrbl.top - 50 - element.height / 2
};
var existingTarget;
while ((existingTarget = getConnectedAtPosition(source, position, element))) {
// escape to top
position = {
x: position.x,
y: Math.min(
existingTarget.y - 30 - element.height / 2,
position.y - 30
)
};
}
return position;
}
module.exports.getTextAnnotationPosition = getTextAnnotationPosition;
/**
* Always put element bottom right of source.
*/
function getDataElementPosition(source, element) {
var sourceTrbl = asTRBL(source);
var position = {
x: sourceTrbl.right + 50 + element.width / 2,
y: sourceTrbl.bottom + 40 + element.width / 2
};
var existingTarget;
while ((existingTarget = getConnectedAtPosition(source, position, element))) {
// escape to right
position = {
x: Math.min(
existingTarget.x + existingTarget.width + 30 + element.width / 2,
position.x + 80
),
y: position.y
};
}
return position;
}
module.exports.getDataElementPosition = getDataElementPosition;
/**
* Always put element right of source per default.
*/
function getDefaultPosition(source, element) {
var sourceTrbl = asTRBL(source);
var sourceMid = getMid(source);
// simply put element right next to source
return {
x: sourceTrbl.right + DEFAULT_HORIZONTAL_DISTANCE + element.width / 2,
y: sourceMid.y
};
}
module.exports.getDefaultPosition = getDefaultPosition;
/**
* Return target at given position, if defined.
*/
function getConnectedAtPosition(source, position, element) {
var bounds = {
x: position.x - (element.width / 2),
y: position.y - (element.height / 2),
width: element.width,
height: element.height
};
var targets = source.outgoing.map(function(c) {
return c.target;
});
var sources = source.incoming.map(function(c) {
return c.source;
});
var allConnected = [].concat(targets, sources);
return find(allConnected, function(target) {
var orientation = getOrientation(target, bounds, PLACEMENT_DETECTION_PAD);
return orientation === 'intersect';
});
}
module.exports.getConnectedAtPosition = getConnectedAtPosition;

View File

@ -0,0 +1,5 @@
module.exports = {
__init__: [ 'autoPlaceSelectionBehavior' ],
autoPlace: [ 'type', require('./AutoPlace') ],
autoPlaceSelectionBehavior: [ 'type', require('./AutoPlaceSelectionBehavior') ]
};

View File

@ -14,10 +14,12 @@ var assign = require('lodash/object/assign'),
/** /**
* A provider for BPMN 2.0 elements context pad * A provider for BPMN 2.0 elements context pad
*/ */
function ContextPadProvider(eventBus, contextPad, modeling, elementFactory, function ContextPadProvider(config, injector, eventBus, contextPad, modeling,
connect, create, popupMenu, elementFactory, connect, create, popupMenu,
canvas, rules, translate) { canvas, rules, translate) {
config = config || {};
contextPad.registerProvider(this); contextPad.registerProvider(this);
this._contextPad = contextPad; this._contextPad = contextPad;
@ -32,6 +34,9 @@ function ContextPadProvider(eventBus, contextPad, modeling, elementFactory,
this._rules = rules; this._rules = rules;
this._translate = translate; this._translate = translate;
if (config.autoPlace !== false) {
this._autoPlace = injector.get('autoPlace', false);
}
eventBus.on('create.end', 250, function(event) { eventBus.on('create.end', 250, function(event) {
var shape = event.context.shape; var shape = event.context.shape;
@ -49,6 +54,8 @@ function ContextPadProvider(eventBus, contextPad, modeling, elementFactory,
} }
ContextPadProvider.$inject = [ ContextPadProvider.$inject = [
'config.contextPad',
'injector',
'eventBus', 'eventBus',
'contextPad', 'contextPad',
'modeling', 'modeling',
@ -75,7 +82,7 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) {
popupMenu = this._popupMenu, popupMenu = this._popupMenu,
canvas = this._canvas, canvas = this._canvas,
rules = this._rules, rules = this._rules,
autoPlace = this._autoPlace,
translate = this._translate; translate = this._translate;
var actions = {}; var actions = {};
@ -137,19 +144,27 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) {
title = translate('Append {type}', { type: type.replace(/^bpmn\:/, '') }); title = translate('Append {type}', { type: type.replace(/^bpmn\:/, '') });
} }
function appendListener(event, element) { function appendStart(event, element) {
var shape = elementFactory.createShape(assign({ type: type }, options)); var shape = elementFactory.createShape(assign({ type: type }, options));
create.start(event, shape, element); create.start(event, shape, element);
} }
var append = autoPlace ? function(event, element) {
var shape = elementFactory.createShape(assign({ type: type }, options));
autoPlace.append(element, shape);
} : appendStart;
return { return {
group: 'model', group: 'model',
className: className, className: className,
title: title, title: title,
action: { action: {
dragstart: appendListener, dragstart: appendStart,
click: appendListener click: append
} }
}; };
} }

View File

@ -67,6 +67,10 @@ function LabelEditingProvider(eventBus, canvas, directEditing, commandStack, res
activateDirectEdit(element); activateDirectEdit(element);
}); });
eventBus.on('autoPlace.end', 500, function(event) {
activateDirectEdit(event.shape);
});
function activateDirectEdit(element, force) { function activateDirectEdit(element, force) {
if (force || if (force ||

View File

@ -0,0 +1,206 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn:process id="Process_1" isExecutable="false">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_16tlpj7</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="TASK_0" name="TASK_0">
<bpmn:incoming>SequenceFlow_16tlpj7</bpmn:incoming>
<bpmn:incoming>SequenceFlow_19p2kv6</bpmn:incoming>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_16tlpj7" sourceRef="StartEvent_1" targetRef="TASK_0" />
<bpmn:task id="TASK_1" name="TASK_1">
<bpmn:incoming>SequenceFlow_0s1mty3</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0b5s2a7</bpmn:outgoing>
</bpmn:task>
<bpmn:startEvent id="StartEvent_0f2fdfi">
<bpmn:outgoing>SequenceFlow_0s1mty3</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_0s1mty3" sourceRef="StartEvent_0f2fdfi" targetRef="TASK_1" />
<bpmn:task id="TASK_2" name="TASK_2">
<bpmn:incoming>SequenceFlow_0b5s2a7</bpmn:incoming>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_0b5s2a7" sourceRef="TASK_1" targetRef="TASK_2" />
<bpmn:task id="TASK_3" name="TASK_3">
<bpmn:outgoing>SequenceFlow_18dnq8n</bpmn:outgoing>
<bpmn:dataOutputAssociation id="DataOutputAssociation_16lcc1g">
<bpmn:targetRef>DataStoreReference_0r0lie7</bpmn:targetRef>
</bpmn:dataOutputAssociation>
</bpmn:task>
<bpmn:task id="TASK_4" name="TASK_4">
<bpmn:incoming>SequenceFlow_18dnq8n</bpmn:incoming>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_18dnq8n" sourceRef="TASK_3" targetRef="TASK_4" />
<bpmn:task id="TASK_5" name="TASK_5">
<bpmn:incoming>SequenceFlow_0n4l6q7</bpmn:incoming>
<bpmn:incoming>SequenceFlow_10nwqsy</bpmn:incoming>
<bpmn:incoming>SequenceFlow_13ubee5</bpmn:incoming>
</bpmn:task>
<bpmn:startEvent id="StartEvent_0wxeenz">
<bpmn:outgoing>SequenceFlow_0n4l6q7</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:startEvent id="StartEvent_1m0lwft">
<bpmn:outgoing>SequenceFlow_10nwqsy</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:startEvent id="StartEvent_06jwo6i">
<bpmn:outgoing>SequenceFlow_13ubee5</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_0n4l6q7" sourceRef="StartEvent_0wxeenz" targetRef="TASK_5" />
<bpmn:sequenceFlow id="SequenceFlow_10nwqsy" sourceRef="StartEvent_1m0lwft" targetRef="TASK_5" />
<bpmn:sequenceFlow id="SequenceFlow_13ubee5" sourceRef="StartEvent_06jwo6i" targetRef="TASK_5" />
<bpmn:startEvent id="START_EVENT_1" name="START_EVENT_1" />
<bpmn:intermediateThrowEvent id="IntermediateThrowEvent_0yy98gf">
<bpmn:outgoing>SequenceFlow_19p2kv6</bpmn:outgoing>
</bpmn:intermediateThrowEvent>
<bpmn:sequenceFlow id="SequenceFlow_19p2kv6" sourceRef="IntermediateThrowEvent_0yy98gf" targetRef="TASK_0" />
<bpmn:dataStoreReference id="DataStoreReference_0r0lie7" />
<bpmn:subProcess id="SUBPROCESS_1" name="SUBPROCESS_1" />
<bpmn:textAnnotation id="TextAnnotation_0czqc1j" />
<bpmn:association id="Association_1ebqqnb" sourceRef="TASK_3" targetRef="TextAnnotation_0czqc1j" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="44" y="76" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="17" y="112" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TASK_0_di" bpmnElement="TASK_0">
<dc:Bounds x="121" y="54" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_16tlpj7_di" bpmnElement="SequenceFlow_16tlpj7">
<di:waypoint xsi:type="dc:Point" x="80" y="94" />
<di:waypoint xsi:type="dc:Point" x="121" y="94" />
<bpmndi:BPMNLabel>
<dc:Bounds x="100.5" y="73" width="0" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="TASK_1_di" bpmnElement="TASK_1">
<dc:Bounds x="121" y="183" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_0f2fdfi_di" bpmnElement="StartEvent_0f2fdfi">
<dc:Bounds x="44" y="205" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="17" y="241" width="0" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0s1mty3_di" bpmnElement="SequenceFlow_0s1mty3">
<di:waypoint xsi:type="dc:Point" x="80" y="223" />
<di:waypoint xsi:type="dc:Point" x="121" y="223" />
<bpmndi:BPMNLabel>
<dc:Bounds x="100.5" y="202" width="0" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="TASK_2_di" bpmnElement="TASK_2">
<dc:Bounds x="279" y="183" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0b5s2a7_di" bpmnElement="SequenceFlow_0b5s2a7">
<di:waypoint xsi:type="dc:Point" x="221" y="223" />
<di:waypoint xsi:type="dc:Point" x="279" y="223" />
<bpmndi:BPMNLabel>
<dc:Bounds x="250" y="202" width="0" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="TASK_3_di" bpmnElement="TASK_3">
<dc:Bounds x="596" y="127" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_0czqc1j_di" bpmnElement="TextAnnotation_0czqc1j">
<dc:Bounds x="698" y="56" width="100" height="30" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_1ebqqnb_di" bpmnElement="Association_1ebqqnb">
<di:waypoint xsi:type="dc:Point" x="687" y="128" />
<di:waypoint xsi:type="dc:Point" x="732" y="86" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="TASK_4_di" bpmnElement="TASK_4">
<dc:Bounds x="1030" y="127" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_18dnq8n_di" bpmnElement="SequenceFlow_18dnq8n">
<di:waypoint xsi:type="dc:Point" x="696" y="167" />
<di:waypoint xsi:type="dc:Point" x="1030" y="167" />
<bpmndi:BPMNLabel>
<dc:Bounds x="818" y="146" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="TASK_5_di" bpmnElement="TASK_5">
<dc:Bounds x="121" y="390" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_0wxeenz_di" bpmnElement="StartEvent_0wxeenz">
<dc:Bounds x="10" y="353" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="-17" y="393" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_1m0lwft_di" bpmnElement="StartEvent_1m0lwft">
<dc:Bounds x="44" y="412" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="17" y="452" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_06jwo6i_di" bpmnElement="StartEvent_06jwo6i">
<dc:Bounds x="10" y="479" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="-17" y="519" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0n4l6q7_di" bpmnElement="SequenceFlow_0n4l6q7">
<di:waypoint xsi:type="dc:Point" x="46" y="371" />
<di:waypoint xsi:type="dc:Point" x="84" y="371" />
<di:waypoint xsi:type="dc:Point" x="84" y="408" />
<di:waypoint xsi:type="dc:Point" x="121" y="408" />
<bpmndi:BPMNLabel>
<dc:Bounds x="54" y="384" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_10nwqsy_di" bpmnElement="SequenceFlow_10nwqsy">
<di:waypoint xsi:type="dc:Point" x="80" y="430" />
<di:waypoint xsi:type="dc:Point" x="121" y="430" />
<bpmndi:BPMNLabel>
<dc:Bounds x="56" y="409" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_13ubee5_di" bpmnElement="SequenceFlow_13ubee5">
<di:waypoint xsi:type="dc:Point" x="46" y="497" />
<di:waypoint xsi:type="dc:Point" x="84" y="497" />
<di:waypoint xsi:type="dc:Point" x="84" y="449" />
<di:waypoint xsi:type="dc:Point" x="121" y="449" />
<bpmndi:BPMNLabel>
<dc:Bounds x="54" y="467" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="START_EVENT_1_di" bpmnElement="START_EVENT_1">
<dc:Bounds x="966" y="246" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="941" y="286" width="87" height="24" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="IntermediateThrowEvent_0yy98gf_di" bpmnElement="IntermediateThrowEvent_0yy98gf">
<dc:Bounds x="219" y="5" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="192" y="45" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_19p2kv6_di" bpmnElement="SequenceFlow_19p2kv6">
<di:waypoint xsi:type="dc:Point" x="219" y="23" />
<di:waypoint xsi:type="dc:Point" x="171" y="23" />
<di:waypoint xsi:type="dc:Point" x="171" y="54" />
<bpmndi:BPMNLabel>
<dc:Bounds x="150" y="2" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="DataStoreReference_0r0lie7_di" bpmnElement="DataStoreReference_0r0lie7">
<dc:Bounds x="689" y="245" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="669" y="299" width="90" height="12" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="DataOutputAssociation_16lcc1g_di" bpmnElement="DataOutputAssociation_16lcc1g">
<di:waypoint xsi:type="dc:Point" x="671" y="207" />
<di:waypoint xsi:type="dc:Point" x="695" y="245" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="SUBPROCESS_1_di" bpmnElement="SUBPROCESS_1" isExpanded="true">
<dc:Bounds x="525" y="308" width="350" height="200" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,170 @@
'use strict';
require('../../../TestHelper');
/* global bootstrapModeler, inject */
var autoPlaceModule = require('../../../../lib/features/auto-place'),
modelingModule = require('../../../../lib/features/modeling'),
selectionModule = require('diagram-js/lib/features/selection'),
labelEditingModule = require('../../../../lib/features/label-editing'),
coreModule = require('../../../../lib/core');
describe('features/auto-place', function() {
var diagramXML = require('./AutoPlace.bpmn');
describe('element placement', function() {
before(bootstrapModeler(diagramXML, {
modules: [
coreModule,
modelingModule,
autoPlaceModule,
selectionModule
]
}));
function autoPlace(cfg) {
var element = cfg.element,
behind = cfg.behind,
expectedBounds = cfg.expectedBounds;
return inject(function(autoPlace, elementRegistry, elementFactory) {
var sourceEl = elementRegistry.get(behind);
// assume
expect(sourceEl).to.exist;
if (typeof element === 'string') {
element = { type: element };
}
var shape = elementFactory.createShape(element);
// when
var placedShape = autoPlace.append(sourceEl, shape);
// then
expect(placedShape).to.have.bounds(expectedBounds);
});
}
describe('should place bpmn:FlowNode', function() {
it('at default distance after START_EVENT_1', autoPlace({
element: 'bpmn:Task',
behind: 'START_EVENT_1',
expectedBounds: { x: 1052, y: 224, width: 100, height: 80 }
}));
it('at incoming distance after TASK_0', autoPlace({
element: 'bpmn:Task',
behind: 'TASK_0',
expectedBounds: { x: 262, y: 54, width: 100, height: 80 }
}));
it('at incoming distance / quorum after TASK_5', autoPlace({
element: 'bpmn:Task',
behind: 'TASK_5',
expectedBounds: { x: 296, y: 390, width: 100, height: 80 }
}));
it('at existing outgoing / below TASK_2', autoPlace({
element: 'bpmn:Task',
behind: 'TASK_1',
expectedBounds: { x: 279, y: 293, width: 100, height: 80 }
}));
it('ignoring existing, far away outgoing of TASK_3', autoPlace({
element: 'bpmn:Task',
behind: 'TASK_3',
expectedBounds: { x: 746, y: 127, width: 100, height: 80 }
}));
it('behind bpmn:SubProcess', autoPlace({
element: 'bpmn:Task',
behind: 'SUBPROCESS_1',
expectedBounds: { x: 925, y: 368, width: 100, height: 80 }
}));
});
describe('should place bpmn:DataStoreReference', function() {
it('bottom right of source', autoPlace({
element: 'bpmn:DataStoreReference',
behind: 'TASK_3',
expectedBounds: { x: 769, y: 247, width: 50, height: 50 }
}));
});
describe('should place bpmn:TextAnnotation', function() {
it('top right of source', autoPlace({
element: 'bpmn:TextAnnotation',
behind: 'TASK_2',
expectedBounds: { x: 409, y: 103, width: 100, height: 30 }
}));
it('above existing', autoPlace({
element: 'bpmn:TextAnnotation',
behind: 'TASK_3',
expectedBounds: { x: 726, y: -4, width: 100, height: 30 }
}));
});
});
describe('modeling flow', function() {
before(bootstrapModeler(diagramXML, {
modules: [
coreModule,
modelingModule,
autoPlaceModule,
selectionModule,
labelEditingModule
]
}));
it('should select + direct edit on autoPlace', inject(
function(autoPlace, elementRegistry, elementFactory, selection, directEditing) {
// given
var el = elementFactory.createShape({ type: 'bpmn:Task' });
var source = elementRegistry.get('TASK_2');
// when
var newShape = autoPlace.append(source, el);
// then
expect(selection.get()).to.eql([ newShape ]);
expect(directEditing.isActive()).to.be.true;
expect(directEditing._active.element).to.equal(newShape);
}
));
});
});

View File

@ -19,7 +19,8 @@ var contextPadModule = require('../../../../lib/features/context-pad'),
modelingModule = require('../../../../lib/features/modeling'), modelingModule = require('../../../../lib/features/modeling'),
replaceMenuModule = require('../../../../lib/features/popup-menu'), replaceMenuModule = require('../../../../lib/features/popup-menu'),
createModule = require('diagram-js/lib/features/create'), createModule = require('diagram-js/lib/features/create'),
customRulesModule = require('../../../util/custom-rules'); customRulesModule = require('../../../util/custom-rules'),
autoPlaceModule = require('../../../../lib/features/auto-place');
describe('features - context-pad', function() { describe('features - context-pad', function() {
@ -432,6 +433,89 @@ describe('features - context-pad', function() {
}); });
describe('auto place', function() {
var diagramXML = require('../../../fixtures/bpmn/simple.bpmn');
beforeEach(bootstrapModeler(diagramXML, {
modules: testModules.concat(autoPlaceModule)
}));
var container;
beforeEach(function() {
container = TestContainer.get(this);
});
it('should trigger', inject(function(elementRegistry, contextPad) {
// given
var element = elementRegistry.get('Task_1');
contextPad.open(element);
// mock event
var event = {
clientX: 100,
clientY: 100,
target: padEntry(container, 'append.gateway'),
preventDefault: function() {}
};
// when
contextPad.trigger('click', event);
// then
expect(element.outgoing).to.have.length(1);
}));
});
describe('disabled auto-place', function() {
var diagramXML = require('../../../fixtures/bpmn/simple.bpmn');
beforeEach(bootstrapModeler(diagramXML, {
modules: testModules.concat(autoPlaceModule),
contextPad: {
autoPlace: false
}
}));
var container;
beforeEach(function() {
container = TestContainer.get(this);
});
it('should default to drag start', inject(function(elementRegistry, contextPad, dragging) {
// given
var element = elementRegistry.get('Task_1');
contextPad.open(element);
// mock event
var event = {
clientX: 100,
clientY: 100,
target: padEntry(container, 'append.gateway'),
preventDefault: function() {}
};
// when
contextPad.trigger('click', event);
// then
expect(dragging.context()).to.exist;
}));
});
}); });