Merge branch 'develop'

This commit is contained in:
Philipp Fromme 2020-04-28 14:02:54 +02:00
commit 0a377250bb
53 changed files with 910 additions and 794 deletions

View File

@ -6,6 +6,13 @@ All notable changes to [bpmn-js](https://github.com/bpmn-io/bpmn-js) are documen
___Note:__ Yet to be released changes appear here._ ___Note:__ Yet to be released changes appear here._
## 6.5.0
* `FEAT`: prefer straight layout for sub-process connections ([#1309](https://github.com/bpmn-io/bpmn-js/pull/1309))
* `FEAT`: move common auto-place feature to diagram-js, add BPMN-specific auto-place feature ([#1284](https://github.com/bpmn-io/bpmn-js/pull/1284))
* `CHORE`: make bpmn-font a development dependency ([`63045bdf`](https://github.com/bpmn-io/bpmn-js/commit/63045bdfa87b9f1989a2a7a509facbeb4616acda))
* `CHORE`: bump to `diagram-js@6.6.1`
## 6.4.2 ## 6.4.2
* `CHORE`: bump to `bpmn-moddle@6.0.5` * `CHORE`: bump to `bpmn-moddle@6.0.5`

View File

@ -12,8 +12,8 @@ import BaseViewer from './BaseViewer';
* *
* @param {Object} [options] configuration options to pass to the viewer * @param {Object} [options] configuration options to pass to the viewer
* @param {DOMElement} [options.container] the container to render the viewer in, defaults to body. * @param {DOMElement} [options.container] the container to render the viewer in, defaults to body.
* @param {String|Number} [options.width] the width of the viewer * @param {string|number} [options.width] the width of the viewer
* @param {String|Number} [options.height] the height of the viewer * @param {string|number} [options.height] the height of the viewer
* @param {Object} [options.moddleExtensions] extension packages to provide * @param {Object} [options.moddleExtensions] extension packages to provide
* @param {Array<didi.Module>} [options.modules] a list of modules to override the default modules * @param {Array<didi.Module>} [options.modules] a list of modules to override the default modules
* @param {Array<didi.Module>} [options.additionalModules] a list of modules to use with the default modules * @param {Array<didi.Module>} [options.additionalModules] a list of modules to use with the default modules

View File

@ -40,8 +40,8 @@ import {
* *
* @param {Object} [options] configuration options to pass to the viewer * @param {Object} [options] configuration options to pass to the viewer
* @param {DOMElement} [options.container] the container to render the viewer in, defaults to body. * @param {DOMElement} [options.container] the container to render the viewer in, defaults to body.
* @param {String|Number} [options.width] the width of the viewer * @param {string|number} [options.width] the width of the viewer
* @param {String|Number} [options.height] the height of the viewer * @param {string|number} [options.height] the height of the viewer
* @param {Object} [options.moddleExtensions] extension packages to provide * @param {Object} [options.moddleExtensions] extension packages to provide
* @param {Array<didi.Module>} [options.modules] a list of modules to override the default modules * @param {Array<didi.Module>} [options.modules] a list of modules to override the default modules
* @param {Array<didi.Module>} [options.additionalModules] a list of modules to use with the default modules * @param {Array<didi.Module>} [options.additionalModules] a list of modules to use with the default modules
@ -84,8 +84,8 @@ inherits(BaseViewer, Diagram);
* *
* You can use these events to hook into the life-cycle. * You can use these events to hook into the life-cycle.
* *
* @param {String} xml the BPMN 2.0 xml * @param {string} xml the BPMN 2.0 xml
* @param {ModdleElement<BPMNDiagram>|String} [bpmnDiagram] BPMN diagram or id of diagram to render (if not provided, the first one will be rendered) * @param {ModdleElement<BPMNDiagram>|string} [bpmnDiagram] BPMN diagram or id of diagram to render (if not provided, the first one will be rendered)
* @param {Function} [done] invoked with (err, warnings=[]) * @param {Function} [done] invoked with (err, warnings=[])
*/ */
BaseViewer.prototype.importXML = function(xml, bpmnDiagram, done) { BaseViewer.prototype.importXML = function(xml, bpmnDiagram, done) {
@ -150,7 +150,7 @@ BaseViewer.prototype.importXML = function(xml, bpmnDiagram, done) {
* You can use these events to hook into the life-cycle. * You can use these events to hook into the life-cycle.
* *
* @param {ModdleElement<Definitions>} definitions parsed BPMN 2.0 definitions * @param {ModdleElement<Definitions>} definitions parsed BPMN 2.0 definitions
* @param {ModdleElement<BPMNDiagram>|String} [bpmnDiagram] BPMN diagram or id of diagram to render (if not provided, the first one will be rendered) * @param {ModdleElement<BPMNDiagram>|string} [bpmnDiagram] BPMN diagram or id of diagram to render (if not provided, the first one will be rendered)
* @param {Function} [done] invoked with (err, warnings=[]) * @param {Function} [done] invoked with (err, warnings=[])
*/ */
BaseViewer.prototype.importDefinitions = function(definitions, bpmnDiagram, done) { BaseViewer.prototype.importDefinitions = function(definitions, bpmnDiagram, done) {
@ -183,7 +183,7 @@ BaseViewer.prototype.importDefinitions = function(definitions, bpmnDiagram, done
* *
* You can use these events to hook into the life-cycle. * You can use these events to hook into the life-cycle.
* *
* @param {String|ModdleElement<BPMNDiagram>} [bpmnDiagramOrId] id or the diagram to open * @param {string|ModdleElement<BPMNDiagram>} [bpmnDiagramOrId] id or the diagram to open
* @param {Function} [done] invoked with (err, warnings=[]) * @param {Function} [done] invoked with (err, warnings=[])
*/ */
BaseViewer.prototype.open = function(bpmnDiagramOrId, done) { BaseViewer.prototype.open = function(bpmnDiagramOrId, done) {
@ -238,8 +238,8 @@ BaseViewer.prototype.open = function(bpmnDiagramOrId, done) {
* You can use these events to hook into the life-cycle. * You can use these events to hook into the life-cycle.
* *
* @param {Object} [options] export options * @param {Object} [options] export options
* @param {Boolean} [options.format=false] output formatted XML * @param {boolean} [options.format=false] output formatted XML
* @param {Boolean} [options.preamble=true] output preamble * @param {boolean} [options.preamble=true] output preamble
* *
* @param {Function} done invoked with (err, xml) * @param {Function} done invoked with (err, xml)
*/ */
@ -350,7 +350,7 @@ BaseViewer.prototype.saveSVG = function(options, done) {
* var elementRegistry = viewer.get('elementRegistry'); * var elementRegistry = viewer.get('elementRegistry');
* var startEventShape = elementRegistry.get('StartEvent_1'); * var startEventShape = elementRegistry.get('StartEvent_1');
* *
* @param {String} name * @param {string} name
* *
* @return {Object} diagram service instance * @return {Object} diagram service instance
* *
@ -432,8 +432,8 @@ BaseViewer.prototype.destroy = function() {
* *
* Remove a previously added listener via {@link #off(event, callback)}. * Remove a previously added listener via {@link #off(event, callback)}.
* *
* @param {String} event * @param {string} event
* @param {Number} [priority] * @param {number} [priority]
* @param {Function} callback * @param {Function} callback
* @param {Object} [that] * @param {Object} [that]
*/ */
@ -444,7 +444,7 @@ BaseViewer.prototype.on = function(event, priority, callback, target) {
/** /**
* De-register an event listener * De-register an event listener
* *
* @param {String} event * @param {string} event
* @param {Function} callback * @param {Function} callback
*/ */
BaseViewer.prototype.off = function(event, callback) { BaseViewer.prototype.off = function(event, callback) {
@ -524,7 +524,7 @@ BaseViewer.prototype._init = function(container, moddle, options) {
/** /**
* Emit an event on the underlying {@link EventBus} * Emit an event on the underlying {@link EventBus}
* *
* @param {String} type * @param {string} type
* @param {Object} event * @param {Object} event
* *
* @return {Object} event processing result (if any) * @return {Object} event processing result (if any)
@ -593,7 +593,7 @@ function ensureUnit(val) {
* Find BPMNDiagram in definitions by ID * Find BPMNDiagram in definitions by ID
* *
* @param {ModdleElement<Definitions>} definitions * @param {ModdleElement<Definitions>} definitions
* @param {String} diagramId * @param {string} diagramId
* *
* @return {ModdleElement<BPMNDiagram>|null} * @return {ModdleElement<BPMNDiagram>|null}
*/ */

View File

@ -123,8 +123,8 @@ var initialDiagram =
* *
* @param {Object} [options] configuration options to pass to the viewer * @param {Object} [options] configuration options to pass to the viewer
* @param {DOMElement} [options.container] the container to render the viewer in, defaults to body. * @param {DOMElement} [options.container] the container to render the viewer in, defaults to body.
* @param {String|Number} [options.width] the width of the viewer * @param {string|number} [options.width] the width of the viewer
* @param {String|Number} [options.height] the height of the viewer * @param {string|number} [options.height] the height of the viewer
* @param {Object} [options.moddleExtensions] extension packages to provide * @param {Object} [options.moddleExtensions] extension packages to provide
* @param {Array<didi.Module>} [options.modules] a list of modules to override the default modules * @param {Array<didi.Module>} [options.modules] a list of modules to override the default modules
* @param {Array<didi.Module>} [options.additionalModules] a list of modules to use with the default modules * @param {Array<didi.Module>} [options.additionalModules] a list of modules to use with the default modules

View File

@ -49,8 +49,8 @@ import BaseViewer from './BaseViewer';
* *
* @param {Object} [options] configuration options to pass to the viewer * @param {Object} [options] configuration options to pass to the viewer
* @param {DOMElement} [options.container] the container to render the viewer in, defaults to body. * @param {DOMElement} [options.container] the container to render the viewer in, defaults to body.
* @param {String|Number} [options.width] the width of the viewer * @param {string|number} [options.width] the width of the viewer
* @param {String|Number} [options.height] the height of the viewer * @param {string|number} [options.height] the height of the viewer
* @param {Object} [options.moddleExtensions] extension packages to provide * @param {Object} [options.moddleExtensions] extension packages to provide
* @param {Array<didi.Module>} [options.modules] a list of modules to override the default modules * @param {Array<didi.Module>} [options.modules] a list of modules to override the default modules
* @param {Array<didi.Module>} [options.additionalModules] a list of modules to use with the default modules * @param {Array<didi.Module>} [options.additionalModules] a list of modules to use with the default modules

View File

@ -362,7 +362,7 @@ export default function PathMap() {
* Also there are use cases where only some parts of a path should be * Also there are use cases where only some parts of a path should be
* scaled.</p> * scaled.</p>
* *
* @param {String} pathId The ID of the path. * @param {string} pathId The ID of the path.
* @param {Object} param <p> * @param {Object} param <p>
* Example param object scales the path to 60% size of the container (data.width, data.height). * Example param object scales the path to 60% size of the container (data.width, data.height).
* <pre> * <pre>

View File

@ -32,7 +32,7 @@ export default function TextRenderer(config) {
* layouted label. * layouted label.
* *
* @param {Bounds} bounds * @param {Bounds} bounds
* @param {String} text * @param {string} text
* *
* @return {Bounds} * @return {Bounds}
*/ */
@ -62,7 +62,7 @@ export default function TextRenderer(config) {
* Get the new bounds of text annotation. * Get the new bounds of text annotation.
* *
* @param {Bounds} bounds * @param {Bounds} bounds
* @param {String} text * @param {string} text
* *
* @return {Bounds} * @return {Bounds}
*/ */
@ -86,7 +86,7 @@ export default function TextRenderer(config) {
/** /**
* Create a layouted text element. * Create a layouted text element.
* *
* @param {String} text * @param {string} text
* @param {Object} [options] * @param {Object} [options]
* *
* @return {SVGElement} rendered text * @return {SVGElement} rendered text

View File

@ -1,58 +0,0 @@
import { getNewShapePosition } from './AutoPlaceUtil';
/**
* A service that places elements connected to existing ones
* to an appropriate position in an _automated_ fashion.
*
* @param {EventBus} eventBus
* @param {Modeling} modeling
*/
export default 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) {
emit('autoPlace.start', {
source: source,
shape: 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);
emit('autoPlace.end', {
source: source,
shape: newShape
});
return newShape;
};
}
AutoPlace.$inject = [
'eventBus',
'modeling'
];

View File

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

View File

@ -1,430 +0,0 @@
import { is } from '../../util/ModelUtil';
import { isAny } from '../modeling/util/ModelingUtil';
import {
getMid,
asTRBL,
getOrientation
} from 'diagram-js/lib/layout/LayoutUtil';
import {
find,
reduce
} from 'min-dash';
var DEFAULT_HORIZONTAL_DISTANCE = 50;
var MAX_HORIZONTAL_DISTANCE = 250;
// padding to detect element placement
var PLACEMENT_DETECTION_PAD = 10;
/**
* Find the new position for the target element to
* connect to source.
*
* @param {djs.model.Shape} source
* @param {djs.model.Shape} element
*
* @return {Point}
*/
export 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);
}
/**
* Always try to place element right of source;
* compute actual distance from previous nodes in flow.
*/
export function getFlowNodePosition(source, element) {
var sourceTrbl = asTRBL(source);
var sourceMid = getMid(source);
var horizontalDistance = getFlowNodeDistance(source, element);
var orientation = 'left',
rowSize = 80,
margin = 30;
if (is(source, 'bpmn:BoundaryEvent')) {
orientation = getOrientation(source, source.host, -25);
if (orientation.indexOf('top') !== -1) {
margin *= -1;
}
}
function getVerticalDistance(orient) {
if (orient.indexOf('top') != -1) {
return -1 * rowSize;
} else if (orient.indexOf('bottom') != -1) {
return rowSize;
} else {
return 0;
}
}
var position = {
x: sourceTrbl.right + horizontalDistance + element.width / 2,
y: sourceMid.y + getVerticalDistance(orientation)
};
var escapeDirection = {
y: {
margin: margin,
rowSize: rowSize
}
};
return deconflictPosition(source, element, position, escapeDirection);
}
/**
* 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
*/
export function getFlowNodeDistance(source, element) {
var sourceTrbl = asTRBL(source);
// is connection a reference to consider?
function isReference(c) {
return is(c, 'bpmn:SequenceFlow');
}
function toTargetNode(weight) {
return function(shape) {
return {
shape: shape,
weight: weight,
distanceTo: function(shape) {
var shapeTrbl = asTRBL(shape);
return shapeTrbl.left - sourceTrbl.right;
}
};
};
}
function toSourceNode(weight) {
return function(shape) {
return {
shape: shape,
weight: weight,
distanceTo: function(shape) {
var shapeTrbl = asTRBL(shape);
return sourceTrbl.left - shapeTrbl.right;
}
};
};
}
// we create a list of nodes to take into consideration
// for calculating the optimal flow node distance
//
// * weight existing target nodes higher than source nodes
// * only take into account individual nodes once
//
var nodes = reduce([].concat(
getTargets(source, isReference).map(toTargetNode(5)),
getSources(source, isReference).map(toSourceNode(1))
), function(nodes, node) {
// filter out shapes connected twice via source or target
nodes[node.shape.id + '__weight_' + node.weight] = node;
return nodes;
}, {});
// 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;
}
}
/**
* Always try to place text annotations top right of source.
*/
export function getTextAnnotationPosition(source, element) {
var sourceTrbl = asTRBL(source);
var position = {
x: sourceTrbl.right + element.width / 2,
y: sourceTrbl.top - 50 - element.height / 2
};
var escapeDirection = {
y: {
margin: -30,
rowSize: 20
}
};
return deconflictPosition(source, element, position, escapeDirection);
}
/**
* Always put element bottom right of source.
*/
export function getDataElementPosition(source, element) {
var sourceTrbl = asTRBL(source);
var position = {
x: sourceTrbl.right - 10 + element.width / 2,
y: sourceTrbl.bottom + 40 + element.width / 2
};
var escapeDirection = {
x: {
margin: 30,
rowSize: 30
}
};
return deconflictPosition(source, element, position, escapeDirection);
}
/**
* Always put element right of source per default.
*/
export 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
};
}
/**
* Returns all connected elements around the given source.
*
* This includes:
*
* - connected elements
* - host connected elements
* - attachers connected elements
*
* @param {djs.model.Shape} source
* @param {djs.model.Shape} element
*
* @return {Array<djs.model.Shape>}
*/
function getAutoPlaceClosure(source, element) {
var allConnected = getConnected(source);
if (source.host) {
allConnected = allConnected.concat(getConnected(source.host));
}
if (source.attachers) {
allConnected = allConnected.concat(source.attachers.reduce(function(shapes, attacher) {
return shapes.concat(getConnected(attacher));
}, []));
}
return allConnected;
}
/**
* Return target at given position, if defined.
*
* This takes connected elements from host and attachers
* into account, too.
*/
export 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 closure = getAutoPlaceClosure(source, element);
return find(closure, function(target) {
if (target === element) {
return false;
}
var orientation = getOrientation(target, bounds, PLACEMENT_DETECTION_PAD);
return orientation === 'intersect';
});
}
/**
* Returns a new, position for the given element
* based on the given element that is not occupied
* by some element connected to source.
*
* Take into account the escapeDirection (where to move
* on positioning clashes) in the computation.
*
* @param {djs.model.Shape} source
* @param {djs.model.Shape} element
* @param {Point} position
* @param {Object} escapeDelta
*
* @return {Point}
*/
export function deconflictPosition(source, element, position, escapeDelta) {
function nextPosition(existingElement) {
var newPosition = {
x: position.x,
y: position.y
};
[ 'x', 'y' ].forEach(function(axis) {
var axisDelta = escapeDelta[axis];
if (!axisDelta) {
return;
}
var dimension = axis === 'x' ? 'width' : 'height';
var margin = axisDelta.margin,
rowSize = axisDelta.rowSize;
if (margin < 0) {
newPosition[axis] = Math.min(
existingElement[axis] + margin - element[dimension] / 2,
position[axis] - rowSize + margin
);
} else {
newPosition[axis] = Math.max(
existingTarget[axis] + existingTarget[dimension] + margin + element[dimension] / 2,
position[axis] + rowSize + margin
);
}
});
return newPosition;
}
var existingTarget;
// deconflict position until free slot is found
while ((existingTarget = getConnectedAtPosition(source, position, element))) {
position = nextPosition(existingTarget);
}
return position;
}
// helpers //////////////////////
function noneFilter() {
return true;
}
function getConnected(element, connectionFilter) {
return [].concat(
getTargets(element, connectionFilter),
getSources(element, connectionFilter)
);
}
function getSources(shape, connectionFilter) {
if (!connectionFilter) {
connectionFilter = noneFilter;
}
return shape.incoming.filter(connectionFilter).map(function(c) {
return c.source;
});
}
function getTargets(shape, connectionFilter) {
if (!connectionFilter) {
connectionFilter = noneFilter;
}
return shape.outgoing.filter(connectionFilter).map(function(c) {
return c.target;
});
}

View File

@ -0,0 +1,18 @@
import { getNewShapePosition } from './BpmnAutoPlaceUtil';
/**
* BPMN auto-place behavior.
*
* @param {EventBus} eventBus
*/
export default function AutoPlace(eventBus) {
eventBus.on('autoPlace', function(context) {
var shape = context.shape,
source = context.source;
return getNewShapePosition(source, shape);
});
}
AutoPlace.$inject = [ 'eventBus' ];

View File

@ -0,0 +1,138 @@
import { is } from '../../util/ModelUtil';
import { isAny } from '../modeling/util/ModelingUtil';
import {
getMid,
asTRBL,
getOrientation
} from 'diagram-js/lib/layout/LayoutUtil';
import {
findFreePosition,
generateGetNextPosition,
getConnectedDistance
} from 'diagram-js/lib/features/auto-place/AutoPlaceUtil';
/**
* Find the new position for the target element to
* connect to source.
*
* @param {djs.model.Shape} source
* @param {djs.model.Shape} element
*
* @return {Point}
*/
export 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);
}
}
/**
* Always try to place element right of source;
* compute actual distance from previous nodes in flow.
*/
export function getFlowNodePosition(source, element) {
var sourceTrbl = asTRBL(source);
var sourceMid = getMid(source);
var horizontalDistance = getConnectedDistance(source, {
filter: function(connection) {
return is(connection, 'bpmn:SequenceFlow');
}
});
var margin = 30,
minDistance = 80,
orientation = 'left';
if (is(source, 'bpmn:BoundaryEvent')) {
orientation = getOrientation(source, source.host, -25);
if (orientation.indexOf('top') !== -1) {
margin *= -1;
}
}
var position = {
x: sourceTrbl.right + horizontalDistance + element.width / 2,
y: sourceMid.y + getVerticalDistance(orientation, minDistance)
};
var nextPositionDirection = {
y: {
margin: margin,
minDistance: minDistance
}
};
return findFreePosition(source, element, position, generateGetNextPosition(nextPositionDirection));
}
function getVerticalDistance(orientation, minDistance) {
if (orientation.indexOf('top') != -1) {
return -1 * minDistance;
} else if (orientation.indexOf('bottom') != -1) {
return minDistance;
} else {
return 0;
}
}
/**
* Always try to place text annotations top right of source.
*/
export function getTextAnnotationPosition(source, element) {
var sourceTrbl = asTRBL(source);
var position = {
x: sourceTrbl.right + element.width / 2,
y: sourceTrbl.top - 50 - element.height / 2
};
var nextPositionDirection = {
y: {
margin: -30,
minDistance: 20
}
};
return findFreePosition(source, element, position, generateGetNextPosition(nextPositionDirection));
}
/**
* Always put element bottom right of source.
*/
export function getDataElementPosition(source, element) {
var sourceTrbl = asTRBL(source);
var position = {
x: sourceTrbl.right - 10 + element.width / 2,
y: sourceTrbl.bottom + 40 + element.width / 2
};
var nextPositionDirection = {
x: {
margin: 30,
minDistance: 30
}
};
return findFreePosition(source, element, position, generateGetNextPosition(nextPositionDirection));
}

View File

@ -1,8 +1,9 @@
import AutoPlace from './AutoPlace'; import AutoPlaceModule from 'diagram-js/lib/features/auto-place';
import AutoPlaceSelectionBehavior from './AutoPlaceSelectionBehavior';
import BpmnAutoPlace from './BpmnAutoPlace';
export default { export default {
__init__: [ 'autoPlaceSelectionBehavior' ], __depends__: [ AutoPlaceModule ],
autoPlace: [ 'type', AutoPlace ], __init__: [ 'bpmnAutoPlace' ],
autoPlaceSelectionBehavior: [ 'type', AutoPlaceSelectionBehavior ] bpmnAutoPlace: [ 'type', BpmnAutoPlace ]
}; };

View File

@ -142,9 +142,9 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) {
/** /**
* Create an append action * Create an append action
* *
* @param {String} type * @param {string} type
* @param {String} className * @param {string} className
* @param {String} [title] * @param {string} [title]
* @param {Object} [options] * @param {Object} [options]
* *
* @return {Object} descriptor * @return {Object} descriptor

View File

@ -1,11 +1,13 @@
import { getNewShapePosition } from '../../auto-place/AutoPlaceUtil'; import { getNewShapePosition } from '../../auto-place/BpmnAutoPlaceUtil';
import { getMid } from 'diagram-js/lib/layout/LayoutUtil'; import { getMid } from 'diagram-js/lib/layout/LayoutUtil';
import { is } from '../../../util/ModelUtil'; import { is } from '../../../util/ModelUtil';
var HIGH_PRIORITY = 2000;
export default function AutoPlaceBehavior(eventBus, gridSnapping) { export default function AutoPlaceBehavior(eventBus, gridSnapping) {
eventBus.on('autoPlace', function(context) { eventBus.on('autoPlace', HIGH_PRIORITY, function(context) {
var source = context.source, var source = context.source,
sourceMid = getMid(source), sourceMid = getMid(source),
shape = context.shape; shape = context.shape;

View File

@ -34,7 +34,7 @@ BpmnKeyboardBindings.prototype.registerBindings = function(keyboard, editorActio
* Add keyboard binding if respective editor action * Add keyboard binding if respective editor action
* is registered. * is registered.
* *
* @param {String} action name * @param {string} action name
* @param {Function} fn that implements the key binding * @param {Function} fn that implements the key binding
*/ */
function addListener(action, fn) { function addListener(action, fn) {

View File

@ -31,7 +31,7 @@ export default function UpdateLabelHandler(modeling, textRenderer) {
* Element parameter can be label itself or connection (i.e. sequence flow). * Element parameter can be label itself or connection (i.e. sequence flow).
* *
* @param {djs.model.Base} element * @param {djs.model.Base} element
* @param {String} text * @param {string} text
*/ */
function setText(element, text) { function setText(element, text) {

View File

@ -98,6 +98,8 @@ BpmnLayouter.prototype.layoutConnection = function(connection, hints) {
manhattanOptions = { manhattanOptions = {
preferredLayouts: getBoundaryEventPreferredLayouts(source, target, connectionEnd) preferredLayouts: getBoundaryEventPreferredLayouts(source, target, connectionEnd)
}; };
} else if (isExpandedSubProcess(source) || isExpandedSubProcess(target)) {
manhattanOptions = getSubProcessManhattanOptions(source);
} else if (is(source, 'bpmn:Gateway')) { } else if (is(source, 'bpmn:Gateway')) {
manhattanOptions = { manhattanOptions = {
preferredLayouts: [ 'v:h' ] preferredLayouts: [ 'v:h' ]
@ -177,6 +179,17 @@ function getMessageFlowPreserveDocking(source, target) {
return null; return null;
} }
function getSubProcessManhattanOptions(source) {
return {
preferredLayouts: [ 'straight', 'h:h' ],
preserveDocking: getSubProcessPreserveDocking(source)
};
}
function getSubProcessPreserveDocking(source) {
return isExpandedSubProcess(source) ? 'target' : 'source';
}
function getConnectionDocking(point, shape) { function getConnectionDocking(point, shape) {
return point ? (point.original || point) : getMid(shape); return point ? (point.original || point) : getMid(shape);
} }

View File

@ -229,7 +229,7 @@ ElementFactory.prototype.createParticipantShape = function(attrs) {
* *
* @param {Base} element * @param {Base} element
* @param {Object} attrs (in/out map of attributes) * @param {Object} attrs (in/out map of attributes)
* @param {Array<String>} attributeNames name of attributes to apply * @param {Array<string>} attributeNames name of attributes to apply
*/ */
function applyAttributes(element, attrs, attributeNames) { function applyAttributes(element, attrs, attributeNames) {
@ -246,7 +246,7 @@ function applyAttributes(element, attrs, attributeNames) {
* *
* @param {Base} element * @param {Base} element
* @param {Object} attrs (in/out map of attributes) * @param {Object} attrs (in/out map of attributes)
* @param {String} attributeName to apply * @param {string} attributeName to apply
*/ */
function applyAttribute(element, attrs, attributeName) { function applyAttribute(element, attrs, attributeName) {
element[attributeName] = attrs[attributeName]; element[attributeName] = attrs[attributeName];

View File

@ -173,7 +173,7 @@ AdaptiveLabelPositioningBehavior.$inject = [
* *
* @param {Shape} element * @param {Shape} element
* *
* @return {Array<String>} * @return {Array<string>}
*/ */
function getTakenHostAlignments(element) { function getTakenHostAlignments(element) {
@ -204,7 +204,7 @@ function getTakenHostAlignments(element) {
* *
* @param {Shape} element * @param {Shape} element
* *
* @return {Array<String>} * @return {Array<string>}
*/ */
function getTakenConnectionAlignments(element) { function getTakenConnectionAlignments(element) {
@ -230,7 +230,7 @@ function getTakenConnectionAlignments(element) {
* *
* @param {Shape} element * @param {Shape} element
* *
* @return {String} positioning identifier * @return {string} positioning identifier
*/ */
function getOptimalPosition(element) { function getOptimalPosition(element) {

View File

@ -100,7 +100,7 @@ export default function GroupBehavior(
* *
* @param {Array<djs.model.shape>} elements * @param {Array<djs.model.shape>} elements
* @param {ModdleElement} categoryValue * @param {ModdleElement} categoryValue
* @return {Boolean} * @return {boolean}
*/ */
function isReferenced(elements, categoryValue) { function isReferenced(elements, categoryValue) {
return elements.some(function(e) { return elements.some(function(e) {

View File

@ -6,6 +6,8 @@ import { getParticipantResizeConstraints } from './util/ResizeUtil';
var HIGH_PRIORITY = 1500; var HIGH_PRIORITY = 1500;
export var LANE_MIN_DIMENSIONS = { width: 300, height: 60 };
export var PARTICIPANT_MIN_DIMENSIONS = { width: 300, height: 150 }; export var PARTICIPANT_MIN_DIMENSIONS = { width: 300, height: 150 };
export var SUB_PROCESS_MIN_DIMENSIONS = { width: 140, height: 120 }; export var SUB_PROCESS_MIN_DIMENSIONS = { width: 140, height: 120 };

View File

@ -5,21 +5,38 @@ import { is } from '../../../util/ModelUtil';
import { isExpanded } from '../../../util/DiUtil'; import { isExpanded } from '../../../util/DiUtil';
import { import {
LANE_MIN_DIMENSIONS,
PARTICIPANT_MIN_DIMENSIONS, PARTICIPANT_MIN_DIMENSIONS,
SUB_PROCESS_MIN_DIMENSIONS, SUB_PROCESS_MIN_DIMENSIONS,
TEXT_ANNOTATION_MIN_DIMENSIONS TEXT_ANNOTATION_MIN_DIMENSIONS
} from './ResizeBehavior'; } from './ResizeBehavior';
import { getChildLanes } from '../util/LaneUtil';
var max = Math.max;
export default function SpaceToolBehavior(eventBus) { export default function SpaceToolBehavior(eventBus) {
eventBus.on('spaceTool.getMinDimensions', function(context) { eventBus.on('spaceTool.getMinDimensions', function(context) {
var shapes = context.shapes, var shapes = context.shapes,
axis = context.axis,
start = context.start,
minDimensions = {}; minDimensions = {};
forEach(shapes, function(shape) { forEach(shapes, function(shape) {
var id = shape.id; var id = shape.id;
if (is(shape, 'bpmn:Participant')) { if (is(shape, 'bpmn:Participant')) {
if (isHorizontal(axis)) {
minDimensions[ id ] = PARTICIPANT_MIN_DIMENSIONS; minDimensions[ id ] = PARTICIPANT_MIN_DIMENSIONS;
} else {
minDimensions[ id ] = {
width: PARTICIPANT_MIN_DIMENSIONS.width,
height: getParticipantMinHeight(shape, start)
};
}
} }
if (is(shape, 'bpmn:SubProcess') && isExpanded(shape)) { if (is(shape, 'bpmn:SubProcess') && isExpanded(shape)) {
@ -36,3 +53,72 @@ export default function SpaceToolBehavior(eventBus) {
} }
SpaceToolBehavior.$inject = [ 'eventBus' ]; SpaceToolBehavior.$inject = [ 'eventBus' ];
// helpers //////////
function isHorizontal(axis) {
return axis === 'x';
}
/**
* Get minimum height for participant taking lanes into account.
*
* @param {<djs.model.Shape>} participant
* @param {number} start
*
* @returns {Object}
*/
function getParticipantMinHeight(participant, start) {
var lanesMinHeight;
if (!hasChildLanes(participant)) {
return PARTICIPANT_MIN_DIMENSIONS.height;
}
lanesMinHeight = getLanesMinHeight(participant, start);
return max(PARTICIPANT_MIN_DIMENSIONS.height, lanesMinHeight);
}
function hasChildLanes(element) {
return !!getChildLanes(element).length;
}
function getLanesMinHeight(participant, resizeStart) {
var lanes = getChildLanes(participant),
resizedLane;
// find the nested lane which is currently resized
resizedLane = findResizedLane(lanes, resizeStart);
// resized lane cannot shrink below the minimum height
// but remaining lanes' dimensions are kept intact
return participant.height - resizedLane.height + LANE_MIN_DIMENSIONS.height;
}
/**
* Find nested lane which is currently resized.
*
* @param {Array<djs.model.Shape>} lanes
* @param {number} resizeStart
*/
function findResizedLane(lanes, resizeStart) {
var i, lane, childLanes;
for (i = 0; i < lanes.length; i++) {
lane = lanes[i];
// resizing current lane or a lane nested
if (resizeStart >= lane.y && resizeStart <= lane.y + lane.height) {
childLanes = getChildLanes(lane);
// a nested lane is resized
if (childLanes.length) {
return findResizedLane(childLanes, resizeStart);
}
// current lane is the resized one
return lane;
}
}
}

View File

@ -6,9 +6,9 @@ var sqrt = Math.sqrt,
/** /**
* Calculate the square (power to two) of a number. * Calculate the square (power to two) of a number.
* *
* @param {Number} n * @param {number} n
* *
* @return {Number} * @return {number}
*/ */
function sq(n) { function sq(n) {
return Math.pow(n, 2); return Math.pow(n, 2);
@ -20,7 +20,7 @@ function sq(n) {
* @param {Point} p1 * @param {Point} p1
* @param {Point} p2 * @param {Point} p2
* *
* @return {Number} * @return {number}
*/ */
function getDistance(p1, p2) { function getDistance(p1, p2) {
return sqrt(sq(p1.x - p2.x) + sq(p1.y - p2.y)); return sqrt(sq(p1.x - p2.x) + sq(p1.y - p2.y));
@ -127,7 +127,7 @@ export function getAttachment(point, line) {
* @param {Point} s1 segment start * @param {Point} s1 segment start
* @param {Point} s2 segment end * @param {Point} s2 segment end
* @param {Point} cc circle center * @param {Point} cc circle center
* @param {Number} cr circle radius * @param {number} cr circle radius
* *
* @return {Array<Point>} intersections * @return {Array<Point>} intersections
*/ */

View File

@ -9,6 +9,8 @@ import {
getLanesRoot getLanesRoot
} from '../../../modeling/util/LaneUtil'; } from '../../../modeling/util/LaneUtil';
import { LANE_MIN_DIMENSIONS } from '../ResizeBehavior';
var abs = Math.abs, var abs = Math.abs,
min = Math.min, min = Math.min,
max = Math.max; max = Math.max;
@ -31,9 +33,7 @@ function addMax(trbl, attr, value) {
return addToTrbl(trbl, attr, value, max); return addToTrbl(trbl, attr, value, max);
} }
var LANE_MIN_HEIGHT = 60, var LANE_RIGHT_PADDING = 20,
LANE_MIN_WIDTH = 300,
LANE_RIGHT_PADDING = 20,
LANE_LEFT_PADDING = 50, LANE_LEFT_PADDING = 50,
LANE_TOP_PADDING = 20, LANE_TOP_PADDING = 20,
LANE_BOTTOM_PADDING = 20; LANE_BOTTOM_PADDING = 20;
@ -54,10 +54,10 @@ export function getParticipantResizeConstraints(laneShape, resizeDirection, bala
minTrbl = {}; minTrbl = {};
if (/e/.test(resizeDirection)) { if (/e/.test(resizeDirection)) {
minTrbl.right = laneTrbl.left + LANE_MIN_WIDTH; minTrbl.right = laneTrbl.left + LANE_MIN_DIMENSIONS.width;
} else } else
if (/w/.test(resizeDirection)) { if (/w/.test(resizeDirection)) {
minTrbl.left = laneTrbl.right - LANE_MIN_WIDTH; minTrbl.left = laneTrbl.right - LANE_MIN_DIMENSIONS.width;
} }
allLanes.forEach(function(other) { allLanes.forEach(function(other) {
@ -72,12 +72,12 @@ export function getParticipantResizeConstraints(laneShape, resizeDirection, bala
// max top size (based on next element) // max top size (based on next element)
if (balanced && abs(laneTrbl.top - otherTrbl.bottom) < 10) { if (balanced && abs(laneTrbl.top - otherTrbl.bottom) < 10) {
addMax(maxTrbl, 'top', otherTrbl.top + LANE_MIN_HEIGHT); addMax(maxTrbl, 'top', otherTrbl.top + LANE_MIN_DIMENSIONS.height);
} }
// min top size (based on self or nested element) // min top size (based on self or nested element)
if (abs(laneTrbl.top - otherTrbl.top) < 5) { if (abs(laneTrbl.top - otherTrbl.top) < 5) {
addMin(minTrbl, 'top', otherTrbl.bottom - LANE_MIN_HEIGHT); addMin(minTrbl, 'top', otherTrbl.bottom - LANE_MIN_DIMENSIONS.height);
} }
} }
@ -89,12 +89,12 @@ export function getParticipantResizeConstraints(laneShape, resizeDirection, bala
// max bottom size (based on previous element) // max bottom size (based on previous element)
if (balanced && abs(laneTrbl.bottom - otherTrbl.top) < 10) { if (balanced && abs(laneTrbl.bottom - otherTrbl.top) < 10) {
addMin(maxTrbl, 'bottom', otherTrbl.bottom - LANE_MIN_HEIGHT); addMin(maxTrbl, 'bottom', otherTrbl.bottom - LANE_MIN_DIMENSIONS.height);
} }
// min bottom size (based on self or nested element) // min bottom size (based on self or nested element)
if (abs(laneTrbl.bottom - otherTrbl.bottom) < 5) { if (abs(laneTrbl.bottom - otherTrbl.bottom) < 5) {
addMax(minTrbl, 'bottom', otherTrbl.top + LANE_MIN_HEIGHT); addMax(minTrbl, 'bottom', otherTrbl.top + LANE_MIN_DIMENSIONS.height);
} }
} }
}); });

View File

@ -9,9 +9,9 @@ import { is } from '../../../util/ModelUtil';
* Return true if element has any of the given types. * Return true if element has any of the given types.
* *
* @param {djs.model.Base} element * @param {djs.model.Base} element
* @param {Array<String>} types * @param {Array<string>} types
* *
* @return {Boolean} * @return {boolean}
*/ */
export function isAny(element, types) { export function isAny(element, types) {
return some(types, function(t) { return some(types, function(t) {
@ -24,7 +24,7 @@ export function isAny(element, types) {
* Return the parent of the element with any of the given types. * Return the parent of the element with any of the given types.
* *
* @param {djs.model.Base} element * @param {djs.model.Base} element
* @param {String|Array<String>} anyType * @param {string|Array<string>} anyType
* *
* @return {djs.model.Base} * @return {djs.model.Base}
*/ */

View File

@ -14,7 +14,7 @@ import {
* *
* @param {djs.model.Base} element * @param {djs.model.Base} element
* *
* @return {Boolean} * @return {boolean}
*/ */
export function isDifferentType(element) { export function isDifferentType(element) {

View File

@ -201,7 +201,7 @@ BpmnRules.prototype.canCopy = canCopy;
* Checks if given element can be used for starting connection. * Checks if given element can be used for starting connection.
* *
* @param {Element} source * @param {Element} source
* @return {Boolean} * @return {boolean}
*/ */
function canStartConnection(element) { function canStartConnection(element) {
if (nonExistingOrLabel(element)) { if (nonExistingOrLabel(element)) {
@ -435,7 +435,7 @@ function canConnect(source, target, connection) {
/** /**
* Can an element be dropped into the target element * Can an element be dropped into the target element
* *
* @return {Boolean} * @return {boolean}
*/ */
function canDrop(element, target, position) { function canDrop(element, target, position) {

View File

@ -39,10 +39,10 @@ BpmnSearchProvider.$inject = [
* *
* <Token> : * <Token> :
* { * {
* normal|matched: <String> * normal|matched: <string>
* } * }
* *
* @param {String} pattern * @param {string} pattern
* @return {Array<Result>} * @return {Array<Result>}
*/ */
BpmnSearchProvider.prototype.find = function(pattern) { BpmnSearchProvider.prototype.find = function(pattern) {

View File

@ -19,9 +19,9 @@ var diRefs = new Refs(
* Returns true if an element has the given meta-model type * Returns true if an element has the given meta-model type
* *
* @param {ModdleElement} element * @param {ModdleElement} element
* @param {String} type * @param {string} type
* *
* @return {Boolean} * @return {boolean}
*/ */
function is(element, type) { function is(element, type) {
return element.$instanceOf(type); return element.$instanceOf(type);

View File

@ -17,7 +17,7 @@ export var FLOW_LABEL_INDENT = 15;
* Returns true if the given semantic has an external label * Returns true if the given semantic has an external label
* *
* @param {BpmnElement} semantic * @param {BpmnElement} semantic
* @return {Boolean} true if has label * @return {boolean} true if has label
*/ */
export function isLabelExternal(semantic) { export function isLabelExternal(semantic) {
return is(semantic, 'bpmn:Event') || return is(semantic, 'bpmn:Event') ||
@ -35,7 +35,7 @@ export function isLabelExternal(semantic) {
* Returns true if the given element has an external label * Returns true if the given element has an external label
* *
* @param {djs.model.shape} element * @param {djs.model.shape} element
* @return {Boolean} true if has label * @return {boolean} true if has label
*/ */
export function hasExternalLabel(element) { export function hasExternalLabel(element) {
return isLabel(element.label); return isLabel(element.label);

View File

@ -2,9 +2,9 @@
* Is an element of the given BPMN type? * Is an element of the given BPMN type?
* *
* @param {djs.model.Base|ModdleElement} element * @param {djs.model.Base|ModdleElement} element
* @param {String} type * @param {string} type
* *
* @return {Boolean} * @return {boolean}
*/ */
export function is(element, type) { export function is(element, type) {
var bo = getBusinessObject(element); var bo = getBusinessObject(element);

11
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "bpmn-js", "name": "bpmn-js",
"version": "6.4.2", "version": "6.5.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -1103,7 +1103,8 @@
"bpmn-font": { "bpmn-font": {
"version": "0.9.3", "version": "0.9.3",
"resolved": "https://registry.npmjs.org/bpmn-font/-/bpmn-font-0.9.3.tgz", "resolved": "https://registry.npmjs.org/bpmn-font/-/bpmn-font-0.9.3.tgz",
"integrity": "sha512-kzRGXGLzTROLRNCSskkOyj/+SbtTAn2unKfgB9tNt7RWJFybg/Wbe9YjK2ALotI3b64wwlCTkAalXiTiskP6dg==" "integrity": "sha512-kzRGXGLzTROLRNCSskkOyj/+SbtTAn2unKfgB9tNt7RWJFybg/Wbe9YjK2ALotI3b64wwlCTkAalXiTiskP6dg==",
"dev": true
}, },
"bpmn-moddle": { "bpmn-moddle": {
"version": "6.0.6", "version": "6.0.6",
@ -2026,9 +2027,9 @@
"dev": true "dev": true
}, },
"diagram-js": { "diagram-js": {
"version": "6.4.1", "version": "6.6.1",
"resolved": "https://registry.npmjs.org/diagram-js/-/diagram-js-6.4.1.tgz", "resolved": "https://registry.npmjs.org/diagram-js/-/diagram-js-6.6.1.tgz",
"integrity": "sha512-VqzydNl6RmnWuHT1B82VId7mH+TG+yZYkgEDp5BizaVDJyhl2F9Z1/q+X9FUQaBq08ZMW1NQP/Bljn56nGHjKw==", "integrity": "sha512-3SlXwT2ieXCZkQn8dVZWfNry9+6d4R+0Q57Oz9t/SfIyNIrRPg0c9IlsaTHpGUhPE3fossXPDjmvqjJD0lmBLw==",
"requires": { "requires": {
"css.escape": "^1.5.1", "css.escape": "^1.5.1",
"didi": "^4.0.0", "didi": "^4.0.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "bpmn-js", "name": "bpmn-js",
"version": "6.4.2", "version": "6.5.0",
"description": "A bpmn 2.0 toolkit and web modeler", "description": "A bpmn 2.0 toolkit and web modeler",
"scripts": { "scripts": {
"all": "run-s lint test distro test:distro", "all": "run-s lint test distro test:distro",
@ -39,6 +39,7 @@
"*.css" "*.css"
], ],
"devDependencies": { "devDependencies": {
"bpmn-font": "^0.9.3",
"camunda-bpmn-moddle": "^4.0.1", "camunda-bpmn-moddle": "^4.0.1",
"chai": "^4.1.2", "chai": "^4.1.2",
"chai-match": "^1.1.1", "chai-match": "^1.1.1",
@ -79,10 +80,9 @@
"webpack": "^4.35.3" "webpack": "^4.35.3"
}, },
"dependencies": { "dependencies": {
"bpmn-font": "^0.9.3",
"bpmn-moddle": "^6.0.6", "bpmn-moddle": "^6.0.6",
"css.escape": "^1.5.1", "css.escape": "^1.5.1",
"diagram-js": "^6.4.1", "diagram-js": "^6.6.1",
"diagram-js-direct-editing": "^1.6.1", "diagram-js-direct-editing": "^1.6.1",
"ids": "^1.0.0", "ids": "^1.0.0",
"inherits": "^2.0.1", "inherits": "^2.0.1",

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="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="_tkxkICCPEeWwcL3w8i7dvw" targetNamespace="http://activiti.org/bpmn" exporter="camunda modeler" exporterVersion="2.7.0" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd"> <bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="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="_tkxkICCPEeWwcL3w8i7dvw" targetNamespace="http://activiti.org/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.1" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd">
<bpmn2:process id="Process_1" isExecutable="false"> <bpmn2:process id="Process_1" isExecutable="false">
<bpmn2:subProcess id="SubProcess_1"> <bpmn2:subProcess id="SubProcess_1">
<bpmn2:startEvent id="StartEvent_1"> <bpmn2:startEvent id="StartEvent_1">
@ -15,43 +15,32 @@
<bpmn2:boundaryEvent id="BoundaryEvent_2" name="superman" attachedToRef="Task_2" /> <bpmn2:boundaryEvent id="BoundaryEvent_2" name="superman" attachedToRef="Task_2" />
<bpmn2:task id="CompensationTask" isForCompensation="true" /> <bpmn2:task id="CompensationTask" isForCompensation="true" />
<bpmn2:intermediateThrowEvent id="IntermediateThrowEvent_1" name="joker" /> <bpmn2:intermediateThrowEvent id="IntermediateThrowEvent_1" name="joker" />
<bpmn2:boundaryEvent id="BoundaryEvent_3" name="wonder woman" attachedToRef="SubProcess_1" />
</bpmn2:process> </bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1"> <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_SubProcess_2" bpmnElement="SubProcess_1" isExpanded="true">
<dc:Bounds x="204" y="78" width="457" height="289" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="BPMNEdge_SequenceFlow_1" bpmnElement="SequenceFlow_1" sourceElement="_BPMNShape_StartEvent_2" targetElement="_BPMNShape_Task_2">
<di:waypoint x="311" y="232" />
<di:waypoint x="431" y="232" />
<bpmndi:BPMNLabel>
<dc:Bounds x="332" y="236" width="6" height="6" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1"> <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="275" y="214" width="36" height="36" /> <dc:Bounds x="275" y="214" width="36" height="36" />
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="293" y="255" width="0" height="0" /> <dc:Bounds x="293" y="255" width="0" height="0" />
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_SubProcess_2" bpmnElement="SubProcess_1" isExpanded="true">
<dc:Bounds x="204" y="78" width="457" height="289" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_Task_2" bpmnElement="Task_1"> <bpmndi:BPMNShape id="_BPMNShape_Task_2" bpmnElement="Task_1">
<dc:Bounds x="431" y="192" width="100" height="80" /> <dc:Bounds x="431" y="192" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="BPMNEdge_SequenceFlow_1" bpmnElement="SequenceFlow_1" sourceElement="_BPMNShape_StartEvent_2" targetElement="_BPMNShape_Task_2">
<di:waypoint xsi:type="dc:Point" x="311" y="232" />
<di:waypoint xsi:type="dc:Point" x="431" y="232" />
<bpmndi:BPMNLabel>
<dc:Bounds x="332" y="236" width="6" height="6" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="BoundaryEvent_1_di" bpmnElement="BoundaryEvent_1">
<dc:Bounds x="185" y="349" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="155" y="390" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_2_di" bpmnElement="Task_2"> <bpmndi:BPMNShape id="Task_2_di" bpmnElement="Task_2">
<dc:Bounds x="795" y="75" width="100" height="80" /> <dc:Bounds x="795" y="75" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_2_di" bpmnElement="BoundaryEvent_2">
<dc:Bounds x="781" y="140" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="754" y="176" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="CompensationTask_di" bpmnElement="CompensationTask"> <bpmndi:BPMNShape id="CompensationTask_di" bpmnElement="CompensationTask">
<dc:Bounds x="795" y="249" width="100" height="80" /> <dc:Bounds x="795" y="249" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
@ -61,6 +50,24 @@
<dc:Bounds x="191" y="525" width="24" height="14" /> <dc:Bounds x="191" y="525" width="24" height="14" />
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_2_di" bpmnElement="BoundaryEvent_2">
<dc:Bounds x="781" y="140" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="754" y="176" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BoundaryEvent_1_di" bpmnElement="BoundaryEvent_1">
<dc:Bounds x="185" y="349" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="155" y="390" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1kn7msk_di" bpmnElement="BoundaryEvent_3">
<dc:Bounds x="412" y="349" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="393" y="392" width="75" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane> </bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram> </bpmndi:BPMNDiagram>
</bpmn2:definitions> </bpmn2:definitions>

View File

@ -142,7 +142,7 @@ export function bootstrapBpmnJS(BpmnJS, diagram, options, locals) {
* *
* }); * });
* *
* @param {String} xml document to display * @param {string} xml document to display
* @param {Object} (options) optional options to be passed to the diagram upon instantiation * @param {Object} (options) optional options to be passed to the diagram upon instantiation
* @param {Object|Function} locals the local overrides to be used by the diagram or a function that produces them * @param {Object|Function} locals the local overrides to be used by the diagram or a function that produces them
* @return {Function} a function to be passed to beforeEach * @return {Function} a function to be passed to beforeEach
@ -170,7 +170,7 @@ export function bootstrapModeler(diagram, options, locals) {
* *
* }); * });
* *
* @param {String} xml document to display * @param {string} xml document to display
* @param {Object} (options) optional options to be passed to the diagram upon instantiation * @param {Object} (options) optional options to be passed to the diagram upon instantiation
* @param {Object|Function} locals the local overrides to be used by the diagram or a function that produces them * @param {Object|Function} locals the local overrides to be used by the diagram or a function that produces them
* @return {Function} a function to be passed to beforeEach * @return {Function} a function to be passed to beforeEach

View File

@ -61,7 +61,7 @@ CustomElementFactory.$inject = [ 'injector' ];
* return shapes[type]; * return shapes[type];
* *
* *
* @param {String} type * @param {string} type
* *
* @return {Bounds} { width, height} * @return {Bounds} { width, height}
*/ */

View File

@ -267,6 +267,21 @@ describe('Viewer', function() {
}); });
}); });
it('should throw error due to missing diagram', function(done) {
var xml = require('../fixtures/bpmn/empty-definitions.bpmn');
// when
createViewer(xml, function(err, warnings) {
// then
expect(err.message).to.eql('no diagram to display');
done();
});
});
}); });
@ -549,25 +564,6 @@ describe('Viewer', function() {
}); });
it('should throw error due to missing diagram', function(done) {
var xml = require('../fixtures/bpmn/empty-definitions.bpmn');
// given
viewer = new Viewer({ container: container, additionalModules: testModules });
// when
viewer.importXML(xml, function(err) {
// then
expect(err.message).to.eql('no diagram to display');
done();
});
});
}); });

View File

@ -416,8 +416,8 @@ describe('draw - bpmn renderer', function() {
* *
* @param {djs.model.base} element - Element. * @param {djs.model.base} element - Element.
* @param {SVG} gfx - Graphics of element. * @param {SVG} gfx - Graphics of element.
* @param {String} fillColor - Fill color to expect. * @param {string} fillColor - Fill color to expect.
* @param {String} strokeColor - Stroke color to expect. * @param {string} strokeColor - Stroke color to expect.
*/ */
function expectColors(element, gfx, fillColor, strokeColor) { function expectColors(element, gfx, fillColor, strokeColor) {
var djsVisual = domQuery('.djs-visual', gfx); var djsVisual = domQuery('.djs-visual', gfx);

View File

@ -11,14 +11,12 @@ import selectionModule from 'diagram-js/lib/features/selection';
import { getBusinessObject } from '../../../../lib/util/ModelUtil'; import { getBusinessObject } from '../../../../lib/util/ModelUtil';
import { getMid } from 'diagram-js/lib/layout/LayoutUtil';
describe('features/auto-place', function() { describe('features/auto-place', function() {
describe('element placement', function() { describe('element placement', function() {
var diagramXML = require('./AutoPlace.bpmn'); var diagramXML = require('./BpmnAutoPlace.bpmn');
before(bootstrapModeler(diagramXML, { before(bootstrapModeler(diagramXML, {
modules: [ modules: [
@ -116,7 +114,7 @@ describe('features/auto-place', function() {
describe('integration', function() { describe('integration', function() {
var diagramXML = require('./AutoPlace.bpmn'); var diagramXML = require('./BpmnAutoPlace.bpmn');
before(bootstrapModeler(diagramXML, { before(bootstrapModeler(diagramXML, {
modules: [ modules: [
@ -174,7 +172,7 @@ describe('features/auto-place', function() {
describe('multi connection handling', function() { describe('multi connection handling', function() {
var diagramXML = require('./AutoPlace.multi-connection.bpmn'); var diagramXML = require('./BpmnAutoPlace.multi-connection.bpmn');
before(bootstrapModeler(diagramXML, { before(bootstrapModeler(diagramXML, {
modules: [ modules: [
@ -209,7 +207,7 @@ describe('features/auto-place', function() {
describe('boundary event connection handling', function() { describe('boundary event connection handling', function() {
var diagramXML = require('./AutoPlace.boundary-events.bpmn'); var diagramXML = require('./BpmnAutoPlace.boundary-events.bpmn');
before(bootstrapModeler(diagramXML, { before(bootstrapModeler(diagramXML, {
modules: [ modules: [
@ -241,12 +239,14 @@ describe('features/auto-place', function() {
expectedBounds: { x: 242, y: -27, width: 100, height: 80 } expectedBounds: { x: 242, y: -27, width: 100, height: 80 }
})); }));
it('should place top right of BOUNDARY_TOP_RIGHT without infinite loop', autoPlace({ it('should place top right of BOUNDARY_TOP_RIGHT without infinite loop', autoPlace({
element: 'bpmn:Task', element: 'bpmn:Task',
behind: 'BOUNDARY_TOP_RIGHT', behind: 'BOUNDARY_TOP_RIGHT',
expectedBounds: { x: 473, y: -27, width: 100, height: 80 } expectedBounds: { x: 473, y: -27, width: 100, height: 80 }
})); }));
it('should place top right of BOUNDARY_SUBPROCESS_TOP', autoPlace({ it('should place top right of BOUNDARY_SUBPROCESS_TOP', autoPlace({
element: 'bpmn:Task', element: 'bpmn:Task',
behind: 'BOUNDARY_SUBPROCESS_TOP', behind: 'BOUNDARY_SUBPROCESS_TOP',
@ -255,114 +255,10 @@ describe('features/auto-place', function() {
}); });
describe('eventbus integration', function() {
var diagramXML = require('./AutoPlace.bpmn');
beforeEach(bootstrapModeler(diagramXML, {
modules: [
autoPlaceModule,
coreModule,
labelEditingModule,
modelingModule,
selectionModule
]
}));
it('<autoPlace.start>', inject(
function(autoPlace, elementFactory, elementRegistry, eventBus) {
// given
var element = elementFactory.createShape({ type: 'bpmn:Task' });
var source = elementRegistry.get('TASK_2');
var listener = sinon.spy(function(event) {
// then
expect(event.shape).to.equal(element);
expect(event.source).to.equal(source);
});
eventBus.on('autoPlace.start', listener);
// when
autoPlace.append(source, element);
expect(listener).to.have.been.called;
}
));
it('<autoPlace>', inject(
function(autoPlace, elementFactory, elementRegistry, eventBus) {
// given
var element = elementFactory.createShape({ type: 'bpmn:Task' });
var source = elementRegistry.get('TASK_2');
var listener = sinon.spy(function(event) {
// then
expect(event.shape).to.equal(element);
expect(event.source).to.equal(source);
return {
x: 0,
y: 0
};
});
eventBus.on('autoPlace', listener);
// when
autoPlace.append(source, element);
expect(listener).to.have.been.called;
expect(getMid(element)).to.eql({
x: 0,
y: 0
});
}
));
it('<autoPlace.end>', inject(
function(autoPlace, elementFactory, elementRegistry, eventBus) {
// given
var element = elementFactory.createShape({ type: 'bpmn:Task' });
var source = elementRegistry.get('TASK_2');
var listener = sinon.spy(function(event) {
// then
expect(event.shape).to.equal(element);
expect(event.source).to.equal(source);
});
eventBus.on('autoPlace.end', listener);
// when
autoPlace.append(source, element);
expect(listener).to.have.been.called;
}
));
});
}); });
// helpers //////////
// helpers //////////////////////
function autoPlace(cfg) { function autoPlace(cfg) {

View File

@ -834,7 +834,7 @@ describe('features/copy-paste', function() {
/** /**
* Integration test involving copying, pasting, moving, undoing and redoing. * Integration test involving copying, pasting, moving, undoing and redoing.
* *
* @param {String|Array<String>} elementIds * @param {string|Array<string>} elementIds
*/ */
function integrationTest(elementIds) { function integrationTest(elementIds) {
if (!isArray(elementIds)) { if (!isArray(elementIds)) {
@ -1000,7 +1000,7 @@ function _findDescriptorsInTree(elements, tree, depth) {
/** /**
* Copy elements. * Copy elements.
* *
* @param {Array<String|djs.model.Base} elements * @param {Array<string|djs.model.Base} elements
* *
* @returns {Object} * @returns {Object}
*/ */

View File

@ -3,6 +3,8 @@ import {
inject inject
} from 'test/TestHelper'; } from 'test/TestHelper';
import { pick } from 'min-dash';
import { import {
getBusinessObject getBusinessObject
} from 'lib/util/ModelUtil'; } from 'lib/util/ModelUtil';
@ -123,4 +125,35 @@ describe('features/modeling - resize shape', function() {
}); });
describe('integration', function() {
var diagramXML = require('../../../fixtures/bpmn/boundary-events.bpmn');
var testModules = [ coreModule, modelingModule ];
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should not move Boundary Event if unnecessary', inject(function(elementRegistry, modeling) {
// given
var boundaryEvent = elementRegistry.get('BoundaryEvent_3'),
originalPosition = getPosition(boundaryEvent),
subProcessElement = elementRegistry.get('SubProcess_1');
// when
modeling.resizeShape(subProcessElement, { x: 204, y: 28, width: 400, height: 339 });
// then
expect(getPosition(boundaryEvent)).to.jsonEqual(originalPosition);
}));
}); });
});
// helper /////
function getPosition(shape) {
return pick(shape, [ 'x', 'y' ]);
}

View File

@ -13,7 +13,11 @@ import {
createCanvasEvent as canvasEvent createCanvasEvent as canvasEvent
} from '../../../../util/MockEvents'; } from '../../../../util/MockEvents';
import { SUB_PROCESS_MIN_DIMENSIONS } from 'lib/features/modeling/behavior/ResizeBehavior'; import {
LANE_MIN_DIMENSIONS,
PARTICIPANT_MIN_DIMENSIONS,
SUB_PROCESS_MIN_DIMENSIONS
} from 'lib/features/modeling/behavior/ResizeBehavior';
var testModules = [ var testModules = [
coreModule, coreModule,
@ -26,16 +30,16 @@ var testModules = [
describe('features/modeling - space tool behavior', function() { describe('features/modeling - space tool behavior', function() {
describe('participant', function() { describe('subprocess', function() {
describe('minimum dimensions', function() { describe('minimum dimensions', function() {
var diagramXML = require('./SpaceToolBehaviorSpec.bpmn'); var diagramXML = require('./SpaceToolBehaviorSpec.subprocess.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: testModules })); beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should ensure minimum dimensions', inject( it('should ensure subprocess minimum dimensions', inject(
function(dragging, elementRegistry, spaceTool) { function(dragging, elementRegistry, spaceTool) {
// given // given
@ -57,4 +61,92 @@ describe('features/modeling - space tool behavior', function() {
}); });
describe('participant', function() {
describe('minimum dimensions', function() {
var diagramXML = require('./SpaceToolBehaviorSpec.participant.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should ensure participant minimum width', inject(
function(dragging, elementRegistry, spaceTool) {
// given
var participant = elementRegistry.get('Participant_1');
// when
spaceTool.activateMakeSpace(canvasEvent({ x: 300, y: 0 }));
dragging.move(canvasEvent({ x: -200, y: 0 }));
dragging.end();
// then
expect(participant.width).to.equal(PARTICIPANT_MIN_DIMENSIONS.width);
})
);
it('should ensure participant minimum height', inject(
function(dragging, elementRegistry, spaceTool) {
// given
var participant = elementRegistry.get('Participant_1');
// when
spaceTool.activateMakeSpace(canvasEvent({ x: 0, y: 100 }));
dragging.move(canvasEvent({ x: 0, y: -400 }));
dragging.end();
// then
expect(participant.height).to.equal(PARTICIPANT_MIN_DIMENSIONS.height);
})
);
it('should ensure lane minimum height', inject(
function(dragging, elementRegistry, spaceTool) {
// given
var lane = elementRegistry.get('Lane_1');
// when
spaceTool.activateMakeSpace(canvasEvent({ x: 0, y: 400 }));
dragging.move(canvasEvent({ x: 0, y: 0 }));
dragging.end();
// then
expect(lane.height).to.equal(LANE_MIN_DIMENSIONS.height);
})
);
it('should ensure nested lane minimum height', inject(
function(dragging, elementRegistry, spaceTool) {
// given
var lane = elementRegistry.get('Lane_6');
// when
spaceTool.activateMakeSpace(canvasEvent({ x: 0, y: 925 }));
dragging.move(canvasEvent({ x: 0, y: 0 }));
dragging.end();
// then
expect(lane.height).to.equal(LANE_MIN_DIMENSIONS.height);
})
);
});
});
}); });

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions 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" id="Definitions_0mdr1un" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:collaboration id="Collaboration_1">
<bpmn:participant id="Participant_1" name="Participant_1" processRef="Process_1" />
<bpmn:participant id="Participant_2" name="Participant_2" processRef="Process_2" />
<bpmn:participant id="Participant_3" name="Participant_3" processRef="Process_3" />
</bpmn:collaboration>
<bpmn:process id="Process_1" isExecutable="true" />
<bpmn:process id="Process_2" isExecutable="false">
<bpmn:laneSet id="LaneSet_16fkurg">
<bpmn:lane id="Lane_1" name="Lane_1" />
<bpmn:lane id="Lane_2" name="Lane_2" />
</bpmn:laneSet>
</bpmn:process>
<bpmn:process id="Process_3" isExecutable="false">
<bpmn:laneSet id="LaneSet_04c016w">
<bpmn:lane id="Lane_3" name="Lane_3" />
<bpmn:lane id="Lane_4" name="Lane_4">
<bpmn:childLaneSet id="LaneSet_1eow6b9">
<bpmn:lane id="Lane_5" name="Lane_5" />
<bpmn:lane id="Lane_6" name="Lane_6" />
</bpmn:childLaneSet>
</bpmn:lane>
</bpmn:laneSet>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_1">
<bpmndi:BPMNShape id="Participant_06757og_di" bpmnElement="Participant_1" isHorizontal="true">
<dc:Bounds x="0" y="0" width="600" height="250" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Participant_0ae1cne_di" bpmnElement="Participant_2" isHorizontal="true">
<dc:Bounds x="700" y="350" width="600" height="250" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_030yoo3_di" bpmnElement="Lane_1" isHorizontal="true">
<dc:Bounds x="730" y="350" width="570" height="130" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1bwlv2s_di" bpmnElement="Lane_2" isHorizontal="true">
<dc:Bounds x="730" y="480" width="570" height="120" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Participant_01wmlzw_di" bpmnElement="Participant_3" isHorizontal="true">
<dc:Bounds x="1400" y="700" width="600" height="250" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_0jco8kr_di" bpmnElement="Lane_3" isHorizontal="true">
<dc:Bounds x="1430" y="700" width="570" height="125" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_0zojmuw_di" bpmnElement="Lane_4" isHorizontal="true">
<dc:Bounds x="1430" y="825" width="570" height="125" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1xlpjnj_di" bpmnElement="Lane_5" isHorizontal="true">
<dc:Bounds x="1460" y="825" width="540" height="63" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_0k03ea4_di" bpmnElement="Lane_6" isHorizontal="true">
<dc:Bounds x="1460" y="888" width="540" height="62" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -5,6 +5,7 @@ import {
} from 'test/TestHelper'; } from 'test/TestHelper';
import { import {
map,
pick pick
} from 'min-dash'; } from 'min-dash';
@ -270,6 +271,45 @@ describe('features/modeling - add Lane', function() {
}); });
describe('flow node handling', function() {
var diagramXML = require('./lanes.bpmn');
beforeEach(bootstrapModeler(diagramXML, {
modules: testModules
}));
it('should move flow nodes and sequence flows', inject(function(elementRegistry, modeling) {
// given
var laneShape = elementRegistry.get('Nested_Lane_B'),
task_Boundary = elementRegistry.get('Task_Boundary'),
boundary = elementRegistry.get('Boundary'),
sequenceFlow = elementRegistry.get('SequenceFlow'),
sequenceFlow_From_Boundary = elementRegistry.get('SequenceFlow_From_Boundary');
// when
var newLane = modeling.addLane(laneShape, 'top');
// then
expect(task_Boundary).to.have.position({ x: 264, y: -57 });
expect(boundary).to.have.position({ x: 311, y: 5 });
expect(sequenceFlow_From_Boundary).to.have.waypoints([
{ x: 329, y: 161 - newLane.height },
{ x: 329, y: 188 - newLane.height },
{ x: 482, y: 188 - newLane.height },
{ x: 482, y: 143 - newLane.height }
]);
expect(sequenceFlow).to.have.waypoints([
{ x: 364, y: 103 - newLane.height },
{ x: 432, y: 103 - newLane.height }
]);
}));
});
describe('flow node handling', function() { describe('flow node handling', function() {
var diagramXML = require('./lanes-flow-nodes.bpmn'); var diagramXML = require('./lanes-flow-nodes.bpmn');
@ -278,6 +318,7 @@ describe('features/modeling - add Lane', function() {
modules: testModules modules: testModules
})); }));
function addLaneAbove(laneId) { function addLaneAbove(laneId) {
return getBpmnJS().invoke(function(elementRegistry, modeling) { return getBpmnJS().invoke(function(elementRegistry, modeling) {
@ -289,19 +330,32 @@ describe('features/modeling - add Lane', function() {
}); });
} }
function addLaneBelow(laneId) {
return getBpmnJS().invoke(function(elementRegistry, modeling) {
var existingLane = elementRegistry.get(laneId);
expect(existingLane).to.exist;
return modeling.addLane(existingLane, 'bottom');
});
}
it('should move flow nodes', inject(function(elementRegistry, modeling) { it('should move flow nodes', inject(function(elementRegistry, modeling) {
// given // given
var task_Boundary = elementRegistry.get('Task_Boundary'), var task_Boundary = elementRegistry.get('Task_Boundary'),
boundary = elementRegistry.get('Boundary'); taskPosition = getPosition(task_Boundary),
boundary = elementRegistry.get('Boundary'),
boundaryPosition = getPosition(boundary);
// when // when
addLaneAbove('Nested_Lane_B'); addLaneAbove('Nested_Lane_B');
// then // then
expect(task_Boundary).to.have.position({ x: 344, y: -7 }); expect(task_Boundary).to.have.position({ x: taskPosition.x, y: taskPosition.y - 120 });
expect(boundary).to.have.position({ x: 391, y: 55 }); expect(boundary).to.have.position({ x: boundaryPosition.x, y: boundaryPosition.y - 120 });
})); }));
@ -309,22 +363,54 @@ describe('features/modeling - add Lane', function() {
// given // given
var sequenceFlow = elementRegistry.get('SequenceFlow'), var sequenceFlow = elementRegistry.get('SequenceFlow'),
sequenceFlow_From_Boundary = elementRegistry.get('SequenceFlow_From_Boundary'); sequenceFlowWaypoints = sequenceFlow.waypoints,
sequenceFlow_From_Boundary = elementRegistry.get('SequenceFlow_From_Boundary'),
sequenceFlow_From_BoundaryWaypoints = sequenceFlow_From_Boundary.waypoints;
// when // when
addLaneAbove('Nested_Lane_B'); addLaneAbove('Nested_Lane_B');
// then // then
expect(sequenceFlow_From_Boundary).to.have.waypoints([ expect(sequenceFlow_From_Boundary).to.have.waypoints(
{ x: 409, y: 91 }, moveWaypoints(sequenceFlow_From_BoundaryWaypoints, 0, -120)
{ x: 409, y: 118 }, );
{ x: 562, y: 118 },
{ x: 562, y: 73 }
]);
expect(sequenceFlow).to.have.waypoints([ expect(sequenceFlow).to.have.waypoints(
{ x: 444, y: 33 }, moveWaypoints(sequenceFlowWaypoints, 0, -120)
{ x: 512, y: 33 } );
}));
it('should move message flows when lane added above', inject(function(elementRegistry) {
// given
var messageFlow = elementRegistry.get('MessageFlowAbove'),
messageFlowWaypoints = messageFlow.waypoints;
// when
addLaneAbove('Nested_Lane_B');
// then
expect(messageFlow).to.have.waypoints([
movePosition(messageFlowWaypoints[0], 0, -120),
messageFlowWaypoints[1]
]);
}));
it('should move message flows when lane added below', inject(function(elementRegistry) {
// given
var messageFlow = elementRegistry.get('MessageFlowBelow'),
messageFlowWaypoints = messageFlow.waypoints;
// when
addLaneBelow('Nested_Lane_B');
// then
expect(messageFlow).to.have.waypoints([
messageFlowWaypoints[0],
movePosition(messageFlowWaypoints[1], 0, 120)
]); ]);
})); }));
@ -333,7 +419,8 @@ describe('features/modeling - add Lane', function() {
// given // given
var event = elementRegistry.get('Event'), var event = elementRegistry.get('Event'),
label = event.label; label = event.label,
labelPosition = getPosition(label);
// TODO(nikku): consolidate import + editing behavior => not consistent right now // TODO(nikku): consolidate import + editing behavior => not consistent right now
@ -344,7 +431,10 @@ describe('features/modeling - add Lane', function() {
addLaneAbove('Nested_Lane_B'); addLaneAbove('Nested_Lane_B');
// then // then
expect(label.y).to.eql(58); expect(label).to.have.position({
x: labelPosition.x,
y: labelPosition.y - 120
});
})); }));
}); });
@ -408,3 +498,23 @@ function padEvent(entry) {
}; };
}); });
} }
function getPosition(element) {
return {
x: element.x,
y: element.y
};
}
function moveWaypoints(waypoints, deltaX, deltaY) {
return map(waypoints, function(waypoint) {
return movePosition(waypoint, deltaX, deltaY);
});
}
function movePosition(point, deltaX, deltaY) {
return {
x: point.x + deltaX,
y: point.y + deltaY
};
}

View File

@ -1,7 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="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="_4bAZoD9WEeWLcNBL4nCk1A" targetNamespace="http://camunda.org/schema/1.0/bpmn" exporter="bpmn-js (https://demo.bpmn.io)" exporterVersion="6.3.1" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd"> <bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="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="_4bAZoD9WEeWLcNBL4nCk1A" targetNamespace="http://camunda.org/schema/1.0/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.1" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd">
<bpmn2:collaboration id="_Collaboration_2"> <bpmn2:collaboration id="_Collaboration_2">
<bpmn2:participant id="Participant_Lane" name="Participant_Lane" processRef="Process_Lane" /> <bpmn2:participant id="Participant_Lane" name="Participant_Lane" processRef="Process_Lane" />
<bpmn2:participant id="ParticipantBelow" name="ParticipantBelow" processRef="Process_0kzdcfx" />
<bpmn2:participant id="ParticipantAbove" name="ParticipantAbove" processRef="Process_1nej7ck" />
<bpmn2:messageFlow id="MessageFlowBelow" sourceRef="ParticipantBelow" targetRef="Participant_Lane" />
<bpmn2:messageFlow id="MessageFlowAbove" name="MessageFlowAbove" sourceRef="Participant_Lane" targetRef="ParticipantAbove" />
</bpmn2:collaboration> </bpmn2:collaboration>
<bpmn2:process id="Process_Lane" isExecutable="false"> <bpmn2:process id="Process_Lane" isExecutable="false">
<bpmn2:laneSet id="LaneSet_1" name="Lane Set 1"> <bpmn2:laneSet id="LaneSet_1" name="Lane Set 1">
@ -11,13 +15,13 @@
<bpmn2:flowNodeRef>Event</bpmn2:flowNodeRef> <bpmn2:flowNodeRef>Event</bpmn2:flowNodeRef>
<bpmn2:flowNodeRef>Boundary</bpmn2:flowNodeRef> <bpmn2:flowNodeRef>Boundary</bpmn2:flowNodeRef>
<bpmn2:childLaneSet id="LaneSet_2"> <bpmn2:childLaneSet id="LaneSet_2">
<bpmn2:lane id="Nested_Lane_B" name="Nested_Lane_B" />
<bpmn2:lane id="Nested_Lane_A" name="Nested_Lane_A"> <bpmn2:lane id="Nested_Lane_A" name="Nested_Lane_A">
<bpmn2:flowNodeRef>Task_Boundary</bpmn2:flowNodeRef> <bpmn2:flowNodeRef>Task_Boundary</bpmn2:flowNodeRef>
<bpmn2:flowNodeRef>Task</bpmn2:flowNodeRef> <bpmn2:flowNodeRef>Task</bpmn2:flowNodeRef>
<bpmn2:flowNodeRef>Event</bpmn2:flowNodeRef> <bpmn2:flowNodeRef>Event</bpmn2:flowNodeRef>
<bpmn2:flowNodeRef>Boundary</bpmn2:flowNodeRef> <bpmn2:flowNodeRef>Boundary</bpmn2:flowNodeRef>
</bpmn2:lane> </bpmn2:lane>
<bpmn2:lane id="Nested_Lane_B" name="Nested_Lane_B" />
</bpmn2:childLaneSet> </bpmn2:childLaneSet>
</bpmn2:lane> </bpmn2:lane>
</bpmn2:laneSet> </bpmn2:laneSet>
@ -32,57 +36,76 @@
<bpmn2:boundaryEvent id="Boundary" name="Boundary" attachedToRef="Task_Boundary"> <bpmn2:boundaryEvent id="Boundary" name="Boundary" attachedToRef="Task_Boundary">
<bpmn2:outgoing>SequenceFlow_From_Boundary</bpmn2:outgoing> <bpmn2:outgoing>SequenceFlow_From_Boundary</bpmn2:outgoing>
</bpmn2:boundaryEvent> </bpmn2:boundaryEvent>
<bpmn2:sequenceFlow id="SequenceFlow" name="Flow" sourceRef="Task_Boundary" targetRef="Task" />
<bpmn2:sequenceFlow id="SequenceFlow_From_Boundary" name="" sourceRef="Boundary" targetRef="Task" /> <bpmn2:sequenceFlow id="SequenceFlow_From_Boundary" name="" sourceRef="Boundary" targetRef="Task" />
<bpmn2:sequenceFlow id="SequenceFlow" name="Flow" sourceRef="Task_Boundary" targetRef="Task" />
</bpmn2:process> </bpmn2:process>
<bpmn2:process id="Process_0kzdcfx" isExecutable="false" />
<bpmn2:process id="Process_1nej7ck" isExecutable="false" />
<bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="_Collaboration_2"> <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="_Collaboration_2">
<bpmndi:BPMNShape id="_BPMNShape_Participant_2" bpmnElement="Participant_Lane" isHorizontal="true"> <bpmndi:BPMNShape id="_BPMNShape_Participant_2" bpmnElement="Participant_Lane" isHorizontal="true">
<dc:Bounds x="152" y="83" width="540" height="537" /> <dc:Bounds x="152" y="633" width="540" height="537" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_Task_2" bpmnElement="Task_Boundary"> <bpmndi:BPMNShape id="_BPMNShape_Lane_3" bpmnElement="Lane_A" isHorizontal="true">
<dc:Bounds x="344" y="113" width="100" height="80" /> <dc:Bounds x="182" y="633" width="510" height="537" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_BoundaryEvent_2" bpmnElement="Boundary"> <bpmndi:BPMNShape id="_BPMNShape_Lane_4" bpmnElement="Nested_Lane_A" isHorizontal="true">
<dc:Bounds x="391" y="175" width="36" height="36" /> <dc:Bounds x="212" y="633" width="480" height="180" />
<bpmndi:BPMNLabel>
<dc:Bounds x="339" y="210" width="48" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_Task_3" bpmnElement="Task"> <bpmndi:BPMNShape id="_BPMNShape_Lane_5" bpmnElement="Nested_Lane_B" isHorizontal="true">
<dc:Bounds x="512" y="113" width="100" height="80" /> <dc:Bounds x="212" y="813" width="480" height="357" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="BPMNEdge_SequenceFlow_1" bpmnElement="SequenceFlow_From_Boundary" sourceElement="_BPMNShape_BoundaryEvent_2" targetElement="_BPMNShape_Task_3"> <bpmndi:BPMNEdge id="BPMNEdge_SequenceFlow_1" bpmnElement="SequenceFlow_From_Boundary" sourceElement="_BPMNShape_BoundaryEvent_2" targetElement="_BPMNShape_Task_3">
<di:waypoint x="409" y="211" /> <di:waypoint x="409" y="761" />
<di:waypoint x="409" y="238" /> <di:waypoint x="409" y="788" />
<di:waypoint x="562" y="238" /> <di:waypoint x="562" y="788" />
<di:waypoint x="562" y="193" /> <di:waypoint x="562" y="743" />
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="377" y="188" width="6" height="6" /> <dc:Bounds x="377" y="188" width="6" height="6" />
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="BPMNEdge_SequenceFlow_2" bpmnElement="SequenceFlow" sourceElement="_BPMNShape_Task_2" targetElement="_BPMNShape_Task_3"> <bpmndi:BPMNEdge id="BPMNEdge_SequenceFlow_2" bpmnElement="SequenceFlow" sourceElement="_BPMNShape_Task_2" targetElement="_BPMNShape_Task_3">
<di:waypoint x="444" y="153" /> <di:waypoint x="444" y="703" />
<di:waypoint x="512" y="153" /> <di:waypoint x="512" y="703" />
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="466" y="135" width="25" height="14" /> <dc:Bounds x="467" y="685" width="24" height="14" />
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_Lane_3" bpmnElement="Lane_A" isHorizontal="true"> <bpmndi:BPMNShape id="_BPMNShape_Task_2" bpmnElement="Task_Boundary">
<dc:Bounds x="182" y="83" width="510" height="537" /> <dc:Bounds x="344" y="663" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_Lane_4" bpmnElement="Nested_Lane_A" isHorizontal="true"> <bpmndi:BPMNShape id="_BPMNShape_Task_3" bpmnElement="Task">
<dc:Bounds x="212" y="83" width="480" height="180" /> <dc:Bounds x="512" y="663" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_Lane_5" bpmnElement="Nested_Lane_B" isHorizontal="true">
<dc:Bounds x="212" y="263" width="480" height="357" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_di" bpmnElement="Event"> <bpmndi:BPMNShape id="Event_di" bpmnElement="Event">
<dc:Bounds x="262" y="135" width="36" height="36" /> <dc:Bounds x="262" y="685" width="36" height="36" />
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="268" y="178" width="24" height="14" /> <dc:Bounds x="268" y="728" width="24" height="14" />
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_BoundaryEvent_2" bpmnElement="Boundary">
<dc:Bounds x="391" y="725" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="339" y="760" width="48" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Participant_1nh1wgv_di" bpmnElement="ParticipantBelow" isHorizontal="true">
<dc:Bounds x="152" y="1500" width="600" height="250" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Participant_05yp9aj_di" bpmnElement="ParticipantAbove" isHorizontal="true">
<dc:Bounds x="152" y="80" width="600" height="250" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1g3bwbi_di" bpmnElement="MessageFlowBelow">
<di:waypoint x="452" y="1500" />
<di:waypoint x="452" y="1170" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0n2j5uv_di" bpmnElement="MessageFlowAbove">
<di:waypoint x="422" y="633" />
<di:waypoint x="422" y="330" />
<bpmndi:BPMNLabel>
<dc:Bounds x="393" y="479" width="88" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane> </bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram> </bpmndi:BPMNDiagram>
</bpmn2:definitions> </bpmn2:definitions>

View File

@ -552,4 +552,119 @@ describe('features/modeling - layout', function() {
}); });
describe('subProcess', function() {
var diagramXML = require('./LayoutSequenceFlowSpec.subProcess.bpmn');
var testModules = [ coreModule, modelingModule ];
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
it('should layout straight between subProcesses (top -> bottom)', function() {
// when
var connection = connect('SubProcess_Center', 'SubProcess_Bottom'),
source = connection.source,
target = connection.target;
var expectedX = getMid(target).x;
// then
expect(connection).to.have.waypoints([
{ x: expectedX, y: source.y + source.height },
{ x: expectedX, y: target.y }
]);
});
it('should layout straight between subProcesses (bottom -> top)', function() {
// when
var connection = connect('SubProcess_Bottom', 'SubProcess_Center'),
source = connection.source,
target = connection.target;
var expectedX = getMid(target).x;
// then
expect(connection).to.have.waypoints([
{ x: expectedX, y: source.y },
{ x: expectedX, y: target.y + target.height }
]);
});
it('should layout straight between subProcess and task next to it (subProcess -> task)',
function() {
// when
var connection = connect('SubProcess_Center', 'Task_Right'),
source = connection.source,
target = connection.target;
var expectedY = getMid(target).y;
// then
expect(connection).to.have.waypoints([
{ x: source.x + source.width, y: expectedY },
{ x: target.x, y: expectedY }
]);
}
);
it('should layout straight between subProcess and task next to it (task -> subProcess)',
function() {
// when
var connection = connect('Task_Right', 'SubProcess_Center'),
source = connection.source,
target = connection.target;
var expectedY = getMid(source).y;
// then
expect(connection).to.have.waypoints([
{ x: source.x, y: expectedY },
{ x: target.x + target.width, y: expectedY }
]);
}
);
it('should layout straight between subProcess and task above (subProcess -> task)', function() {
// when
var connection = connect('SubProcess_Center', 'Task_Top'),
source = connection.source,
target = connection.target;
var expectedX = getMid(target).x;
// then
expect(connection).to.have.waypoints([
{ x: expectedX, y: source.y },
{ x: expectedX, y: target.y + target.height }
]);
});
it('should layout straight between subProcess and task above (task -> subProcess)', function() {
// when
var connection = connect('Task_Top', 'SubProcess_Center'),
source = connection.source,
target = connection.target;
var expectedX = getMid(source).x;
// then
expect(connection).to.have.waypoints([
{ x: expectedX, y: source.y + source.height },
{ x: expectedX, y: target.y }
]);
});
});
}); });

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions 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" id="Definitions_00dfyhw" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.1">
<bpmn:process id="Process_00i7uqd" isExecutable="true">
<bpmn:subProcess id="SubProcess_Center" />
<bpmn:subProcess id="SubProcess_Bottom" />
<bpmn:task id="Task_Right" />
<bpmn:task id="Task_Top" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_00i7uqd">
<bpmndi:BPMNShape id="Activity_0r2w32m_di" bpmnElement="SubProcess_Center" isExpanded="true">
<dc:Bounds x="270" y="270" width="350" height="200" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0tbriov_di" bpmnElement="SubProcess_Bottom" isExpanded="true">
<dc:Bounds x="160" y="600" width="350" height="200" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0fm36cc_di" bpmnElement="Task_Right">
<dc:Bounds x="780" y="270" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_16fbgsj_di" bpmnElement="Task_Top">
<dc:Bounds x="520" y="110" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -6,7 +6,7 @@ import {
/** /**
* Create a fake key event for testing purposes. * Create a fake key event for testing purposes.
* *
* @param {String|Number} key the key or keyCode/charCode * @param {string|number} key the key or keyCode/charCode
* @param {Object} [attrs] * @param {Object} [attrs]
* *
* @return {Event} * @return {Event}