feat(rules): support attach from context-menu

* take target attach and parent hints on `connection.create`
  rule into account to implement create from context-menu

Related to bpmn-io/diagram-js#242

Closes #742
This commit is contained in:
Nico Rehwaldt 2017-12-20 10:34:45 +01:00
parent 11354e951c
commit 367399d509
2 changed files with 204 additions and 104 deletions

View File

@ -22,6 +22,7 @@ var RuleProvider = require('diagram-js/lib/features/rules/RuleProvider');
var isBoundaryAttachment = require('../snapping/BpmnSnappingUtil').getBoundaryAttachment;
/**
* BPMN specific modeling rule
*/
@ -39,9 +40,32 @@ BpmnRules.prototype.init = function() {
this.addRule('connection.create', function(context) {
var source = context.source,
target = context.target;
target = context.target,
hints = context.hints || {},
targetParent = hints.targetParent,
targetAttach = hints.targetAttach;
return canConnect(source, target);
// don't allow incoming connections on
// newly created boundary events
// to boundary events
if (targetAttach) {
return false;
}
// temporarily set target parent for scoping
// checks to work
if (targetParent) {
target.parent = targetParent;
}
try {
return canConnect(source, target);
} finally {
// unset temporary target parent
if (targetParent) {
target.parent = null;
}
}
});
this.addRule('connection.reconnectStart', function(context) {
@ -87,13 +111,23 @@ BpmnRules.prototype.init = function() {
canInsert(shapes, target, position);
});
this.addRule([ 'shape.create', 'shape.append' ], function(context) {
var target = context.target,
shape = context.shape,
source = context.source,
position = context.position;
this.addRule('shape.create', function(context) {
return canCreate(
context.shape,
context.target,
context.source,
context.position
);
});
return canAttach([ shape ], target, source, position) || canCreate(shape, target, source, position);
this.addRule('shape.attach', function(context) {
return canAttach(
context.shape,
context.target,
null,
context.position
);
});
this.addRule('element.copy', function(context) {
@ -173,17 +207,16 @@ function isSame(a, b) {
function getOrganizationalParent(element) {
var bo = getBusinessObject(element);
while (bo && !is(bo, 'bpmn:Process')) {
if (is(bo, 'bpmn:Participant')) {
return bo.processRef || bo;
do {
if (is(element, 'bpmn:Process')) {
return getBusinessObject(element);
}
bo = bo.$parent;
}
if (is(element, 'bpmn:Participant')) {
return getBusinessObject(element).processRef;
}
} while ((element = element.parent));
return bo;
}
function isTextAnnotation(element) {
@ -228,21 +261,20 @@ function isMessageFlowTarget(element) {
function getScopeParent(element) {
var bo = getBusinessObject(element);
var parent = element;
if (is(bo, 'bpmn:Participant')) {
return null;
}
while ((parent = parent.parent)) {
while (bo) {
bo = bo.$parent;
if (is(parent, 'bpmn:FlowElementsContainer')) {
return getBusinessObject(parent);
}
if (is(bo, 'bpmn:FlowElementsContainer')) {
return bo;
if (is(parent, 'bpmn:Participant')) {
return getBusinessObject(parent).processRef;
}
}
return bo;
return null;
}
function isSameScope(a, b) {

View File

@ -1,6 +1,6 @@
'use strict';
var TestHelper = require('../../../TestHelper');
var getBpmnJS = require('../../../TestHelper').getBpmnJS;
var TestContainer = require('mocha-test-container-support');
@ -171,7 +171,7 @@ describe('features - context-pad', function() {
function expectContextPadEntries(elementOrId, expectedEntries) {
TestHelper.getBpmnJS().invoke(function(elementRegistry, contextPad) {
getBpmnJS().invoke(function(elementRegistry, contextPad) {
var element = typeof elementOrId === 'string' ? elementRegistry.get(elementOrId) : elementOrId;
@ -279,11 +279,68 @@ describe('features - context-pad', function() {
});
describe('create', function() {
var diagramXML = require('../../../fixtures/bpmn/simple.bpmn');
beforeEach(bootstrapModeler(diagramXML, {
modules: testModules
}));
it('should attach boundary event', inject(function(dragging, contextPad, elementRegistry) {
// given
var task = elementRegistry.get('Task_1');
// when
contextPad.open(task);
contextPad.trigger('dragstart', padEvent('append.intermediate-event'));
dragging.move(canvasEvent({ x: task.x, y: task.y }));
dragging.hover({ element: task });
dragging.move(canvasEvent({ x: task.x + 80, y: task.y + 70 }));
dragging.end();
// then
expect(task.attachers).to.have.length(1);
}));
it('should attach boundary event to other target', inject(
function(dragging, contextPad, elementRegistry) {
// given
var task = elementRegistry.get('Task_1');
var subProcess = elementRegistry.get('SubProcess_1');
// when
contextPad.open(task);
contextPad.trigger('dragstart', padEvent('append.intermediate-event'));
dragging.move(canvasEvent({ x: subProcess.x, y: subProcess.y }));
dragging.hover({ element: subProcess });
dragging.move(canvasEvent({ x: subProcess.x + 80, y: subProcess.y + 5 }));
dragging.end();
// then
expect(subProcess.attachers).to.have.length(1);
})
);
});
describe('replace', function() {
var diagramXML = require('../../../fixtures/bpmn/simple.bpmn');
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
beforeEach(bootstrapModeler(diagramXML, {
modules: testModules
}));
var container;
@ -297,20 +354,15 @@ describe('features - context-pad', function() {
// given
var element = elementRegistry.get('StartEvent_1'),
padding = 5,
replaceMenuRect,
padMenuRect;
padMenuRect,
replaceMenuRect;
contextPad.open(element);
padMenuRect = contextPad.getPad(element).html.getBoundingClientRect();
// mock event
var event = {
target: padEntry(container, 'replace'),
preventDefault: function() {}
};
// when
contextPad.trigger('click', event);
contextPad.trigger('click', padEvent('replace'));
padMenuRect = contextPad.getPad(element).html.getBoundingClientRect();
replaceMenuRect = domQuery('.bpmn-replace', container).getBoundingClientRect();
// then
@ -319,7 +371,7 @@ describe('features - context-pad', function() {
}));
it('should not include control if replacement is disallowed',
it('should hide wrench if replacement is disallowed',
inject(function(elementRegistry, contextPad, customRules) {
// given
@ -337,10 +389,11 @@ describe('features - context-pad', function() {
// then
expect(padEntry(padNode, 'replace')).not.to.exist;
}));
}
));
it('should include control if replacement is allowed',
it('should show wrench if replacement is allowed',
inject(function(elementRegistry, contextPad, customRules) {
// given
@ -358,78 +411,88 @@ describe('features - context-pad', function() {
// then
expect(padEntry(padNode, 'replace')).to.exist;
}));
}
));
it('should open the replace menu after an element is created if it has modifier key',
inject(function(create, dragging, canvas, elementFactory) {
// given
var rootShape = canvas.getRootElement(),
startEvent = elementFactory.createShape({ type: 'bpmn:StartEvent' }),
replaceMenu;
describe('create + <CTRL>', function() {
// when
create.start(canvasEvent({ x: 0, y: 0 }), startEvent);
it('should open replace',
inject(function(create, dragging, canvas, elementFactory) {
dragging.move(canvasEvent({ x: 50, y: 50 }));
dragging.hover({ element: rootShape });
dragging.move(canvasEvent({ x: 75, y: 75 }));
// given
var rootShape = canvas.getRootElement(),
startEvent = elementFactory.createShape({ type: 'bpmn:StartEvent' }),
replaceMenu;
dragging.end(canvasEvent({ x: 75, y: 75 }, { ctrlKey: true, metaKey: true }));
// when
create.start(canvasEvent({ x: 0, y: 0 }), startEvent);
replaceMenu = domQuery('.bpmn-replace', container);
dragging.move(canvasEvent({ x: 50, y: 50 }));
dragging.hover({ element: rootShape });
dragging.move(canvasEvent({ x: 75, y: 75 }));
// then
expect(replaceMenu).to.exist;
}));
dragging.end(canvasEvent({ x: 75, y: 75 }, { ctrlKey: true, metaKey: true }));
replaceMenu = domQuery('.bpmn-replace', container);
// then
expect(replaceMenu).to.exist;
}
));
it('should open boundary event replace menu after an element is created if it has modifier key',
inject(function(create, dragging, canvas, elementFactory, modeling, popupMenu) {
// given
var rootShape = canvas.getRootElement();
var task = elementFactory.createShape({ type: 'bpmn:Task' });
var intermediateEvent = elementFactory.createShape({ type: 'bpmn:IntermediateThrowEvent' });
it('should open boundary event replace menu',
inject(function(create, dragging, canvas, elementFactory, modeling, popupMenu) {
modeling.createShape(task, { x: 100, y: 100 }, rootShape);
// given
var rootShape = canvas.getRootElement();
var task = elementFactory.createShape({ type: 'bpmn:Task' });
var intermediateEvent = elementFactory.createShape({ type: 'bpmn:IntermediateThrowEvent' });
// when
create.start(canvasEvent({ x: 0, y: 0 }), intermediateEvent);
modeling.createShape(task, { x: 100, y: 100 }, rootShape);
dragging.move(canvasEvent({ x: 50, y: 50 }));
dragging.hover({ element: task });
dragging.move(canvasEvent({ x: 50, y: 65 }));
// when
create.start(canvasEvent({ x: 0, y: 0 }), intermediateEvent);
dragging.end(canvasEvent({ x: 50, y: 65 }, { ctrlKey: true, metaKey: true }));
dragging.move(canvasEvent({ x: 50, y: 50 }));
dragging.hover({ element: task });
dragging.move(canvasEvent({ x: 50, y: 65 }));
// then
var replaceMenu = domQuery.all('[data-id$="-boundary"]', popupMenu._current.container);
expect(replaceMenu).to.exist;
expect(replaceMenu.length).to.eql(13);
}));
dragging.end(canvasEvent({ x: 50, y: 65 }, { ctrlKey: true, metaKey: true }));
// then
var replaceMenu = domQuery.all('[data-id$="-boundary"]', popupMenu._current.container);
expect(replaceMenu).to.exist;
expect(replaceMenu.length).to.eql(13);
}
));
it('should not open the replace menu after an element is created when there is none',
inject(function(create, dragging, canvas, elementFactory) {
// given
var rootShape = canvas.getRootElement(),
dataObject = elementFactory.createShape({ type: 'bpmn:DataObjectReference' }),
replaceMenu;
it('should not open non-existing replace menu',
inject(function(create, dragging, canvas, elementFactory) {
// given
var rootShape = canvas.getRootElement(),
dataObject = elementFactory.createShape({ type: 'bpmn:DataObjectReference' }),
replaceMenu;
// when
create.start(canvasEvent({ x: 0, y: 0 }), dataObject);
// when
create.start(canvasEvent({ x: 0, y: 0 }), dataObject);
dragging.move(canvasEvent({ x: 50, y: 50 }));
dragging.hover({ element: rootShape });
dragging.move(canvasEvent({ x: 75, y: 75 }));
dragging.move(canvasEvent({ x: 50, y: 50 }));
dragging.hover({ element: rootShape });
dragging.move(canvasEvent({ x: 75, y: 75 }));
dragging.end(canvasEvent({ x: 75, y: 75 }, { ctrlKey: true, metaKey: true }));
dragging.end(canvasEvent({ x: 75, y: 75 }, { ctrlKey: true, metaKey: true }));
replaceMenu = domQuery('.bpmn-replace', container);
replaceMenu = domQuery('.bpmn-replace', container);
// then
expect(replaceMenu).to.not.exist;
}));
// then
expect(replaceMenu).to.not.exist;
}
));
});
});
@ -442,12 +505,6 @@ describe('features - context-pad', function() {
modules: testModules.concat(autoPlaceModule)
}));
var container;
beforeEach(function() {
container = TestContainer.get(this);
});
it('should trigger', inject(function(elementRegistry, contextPad) {
@ -457,12 +514,7 @@ describe('features - context-pad', function() {
contextPad.open(element);
// mock event
var event = {
clientX: 100,
clientY: 100,
target: padEntry(container, 'append.gateway'),
preventDefault: function() {}
};
var event = padEvent('append.gateway');
// when
contextPad.trigger('click', event);
@ -522,3 +574,19 @@ describe('features - context-pad', function() {
function padEntry(element, name) {
return domQuery('[data-action="' + name + '"]', element);
}
function padEvent(entry) {
return getBpmnJS().invoke(function(overlays) {
var target = padEntry(overlays._overlayRoot, entry);
return {
target: target,
preventDefault: function() {},
clientX: 100,
clientY: 100
};
});
}