mirror of
https://github.com/sartography/bpmn-js.git
synced 2025-01-17 20:41:32 +00:00
ae96f3714d
Elements will automatically be created at appropriate positions when context menu create entries are being clicked (rather than dragged). This marks a major step forward for mobile modeling, too as dragging, especially dragging out from very small controls is very cumbersome to do. Things we take into account: * for bpmn:FlowNodes, we try to compute the current distance between elements on the flow based on connections going in and out of the flow nodes source element * for bpmn:TextAnnotation we assume placement of the element top right of the source shape * for bpmn:DataObject and friends we assume a placement bottom right of the source shape * for all elements, we try not to place elements on top of each other; i.e. new elements will be pushed up or down accordingly, if an element at a chosen position does already exist Integration into other services: * context pad provider works with autoPlace, if available and defaults to drag start without * auto placed elements are selected and direct editing may conditionally be activated based on element type (LabelEditingProvider knows the rules) Users can out out of autoPlace by specifying the configuration property `config.contextPad.autoPlace = false`. Closes #563 BREAKING CHANGE: * This breaks the default interaction from the context pad; if you rely on clicking to start the drag you can opt out of autoPlace: ``` new BpmnJS({ contextPad: { autoPlace: false } }); ```
525 lines
13 KiB
JavaScript
525 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
var TestHelper = require('../../../TestHelper');
|
|
|
|
var TestContainer = require('mocha-test-container-support');
|
|
|
|
var domQuery = require('min-dom/lib/query');
|
|
|
|
var is = require('../../../../lib/util/ModelUtil').is;
|
|
|
|
var canvasEvent = require('../../../util/MockEvents').createCanvasEvent;
|
|
|
|
|
|
/* global bootstrapModeler, inject */
|
|
|
|
|
|
var contextPadModule = require('../../../../lib/features/context-pad'),
|
|
coreModule = require('../../../../lib/core'),
|
|
modelingModule = require('../../../../lib/features/modeling'),
|
|
replaceMenuModule = require('../../../../lib/features/popup-menu'),
|
|
createModule = require('diagram-js/lib/features/create'),
|
|
customRulesModule = require('../../../util/custom-rules'),
|
|
autoPlaceModule = require('../../../../lib/features/auto-place');
|
|
|
|
|
|
describe('features - context-pad', function() {
|
|
|
|
var testModules = [
|
|
coreModule,
|
|
modelingModule,
|
|
contextPadModule,
|
|
replaceMenuModule,
|
|
customRulesModule,
|
|
createModule
|
|
];
|
|
|
|
|
|
describe('remove action rules', function() {
|
|
|
|
var diagramXML = require('../../../fixtures/bpmn/simple.bpmn');
|
|
|
|
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
|
|
|
|
|
|
var deleteAction;
|
|
|
|
beforeEach(inject(function(contextPad) {
|
|
|
|
deleteAction = function(element) {
|
|
return padEntry(contextPad.getPad(element).html, 'delete');
|
|
};
|
|
}));
|
|
|
|
|
|
it('should add delete action by default', inject(function(elementRegistry, contextPad) {
|
|
|
|
// given
|
|
var element = elementRegistry.get('StartEvent_1');
|
|
|
|
// when
|
|
contextPad.open(element);
|
|
|
|
// then
|
|
expect(deleteAction(element)).to.exist;
|
|
}));
|
|
|
|
|
|
it('should include delete action when rule returns true',
|
|
inject(function(elementRegistry, contextPad, customRules) {
|
|
|
|
// given
|
|
customRules.addRule('elements.delete', 1500, function() {
|
|
return true;
|
|
});
|
|
|
|
var element = elementRegistry.get('StartEvent_1');
|
|
|
|
// when
|
|
contextPad.open(element);
|
|
|
|
// then
|
|
expect(deleteAction(element)).to.exist;
|
|
})
|
|
);
|
|
|
|
|
|
it('should NOT include delete action when rule returns false',
|
|
inject(function(elementRegistry, contextPad, customRules) {
|
|
|
|
// given
|
|
customRules.addRule('elements.delete', 1500, function() {
|
|
return false;
|
|
});
|
|
|
|
var element = elementRegistry.get('StartEvent_1');
|
|
|
|
// when
|
|
contextPad.open(element);
|
|
|
|
// then
|
|
expect(deleteAction(element)).to.not.exist;
|
|
})
|
|
);
|
|
|
|
|
|
it('should call rules with [ element ]', inject(function(elementRegistry, contextPad, customRules) {
|
|
|
|
// given
|
|
var element = elementRegistry.get('StartEvent_1');
|
|
|
|
customRules.addRule('elements.delete', 1500, function(context) {
|
|
|
|
// element array is actually passed
|
|
expect(context.elements).to.eql([ element ]);
|
|
|
|
return true;
|
|
});
|
|
|
|
// then
|
|
expect(function() {
|
|
contextPad.open(element);
|
|
}).not.to.throw;
|
|
}));
|
|
|
|
|
|
it('should include delete action when [ element ] is returned from rule',
|
|
inject(function(elementRegistry, contextPad, customRules) {
|
|
|
|
// given
|
|
customRules.addRule('elements.delete', 1500, function(context) {
|
|
return context.elements;
|
|
});
|
|
|
|
var element = elementRegistry.get('StartEvent_1');
|
|
|
|
// when
|
|
contextPad.open(element);
|
|
|
|
// then
|
|
expect(deleteAction(element)).to.exist;
|
|
})
|
|
);
|
|
|
|
|
|
it('should NOT include delete action when [ ] is returned from rule',
|
|
inject(function(elementRegistry, contextPad, customRules) {
|
|
|
|
// given
|
|
customRules.addRule('elements.delete', 1500, function() {
|
|
return [];
|
|
});
|
|
|
|
var element = elementRegistry.get('StartEvent_1');
|
|
|
|
// when
|
|
contextPad.open(element);
|
|
|
|
// then
|
|
expect(deleteAction(element)).to.not.exist;
|
|
})
|
|
);
|
|
|
|
});
|
|
|
|
|
|
describe('available entries', function() {
|
|
|
|
var diagramXML = require('./ContextPad.activation.bpmn');
|
|
|
|
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
|
|
|
|
function expectContextPadEntries(elementOrId, expectedEntries) {
|
|
|
|
TestHelper.getBpmnJS().invoke(function(elementRegistry, contextPad) {
|
|
|
|
var element = typeof elementOrId === 'string' ? elementRegistry.get(elementOrId) : elementOrId;
|
|
|
|
contextPad.open(element, true);
|
|
|
|
var entries = contextPad._current.entries;
|
|
|
|
expectedEntries.forEach(function(name) {
|
|
|
|
if (name.charAt(0) === '!') {
|
|
name = name.substring(1);
|
|
|
|
expect(entries).not.to.have.property(name);
|
|
} else {
|
|
expect(entries).to.have.property(name);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
it('should provide Task entries', inject(function() {
|
|
|
|
expectContextPadEntries('Task_1', [
|
|
'connect',
|
|
'replace',
|
|
'append.end-event',
|
|
'append.gateway',
|
|
'append.append-task',
|
|
'append.intermediate-event',
|
|
'append.text-annotation'
|
|
]);
|
|
}));
|
|
|
|
|
|
it('should provide EventBasedGateway entries', inject(function() {
|
|
|
|
expectContextPadEntries('EventBasedGateway_1', [
|
|
'connect',
|
|
'replace',
|
|
'append.receive-task',
|
|
'append.message-intermediate-event',
|
|
'append.timer-intermediate-event',
|
|
'append.condtion-intermediate-event',
|
|
'append.signal-intermediate-event',
|
|
'append.text-annotation',
|
|
'!append.task'
|
|
]);
|
|
}));
|
|
|
|
|
|
it('should provide EndEvent entries', inject(function() {
|
|
|
|
expectContextPadEntries('EndEvent_1', [
|
|
'connect',
|
|
'replace',
|
|
'!append.task'
|
|
]);
|
|
}));
|
|
|
|
|
|
it('should provide Compensation Activity entries', inject(function() {
|
|
|
|
expectContextPadEntries('Task_2', [
|
|
'connect',
|
|
'replace',
|
|
'!append.end-event',
|
|
'append.text-annotation'
|
|
]);
|
|
}));
|
|
|
|
|
|
it('should provide Compensate Boundary entries', inject(function() {
|
|
|
|
expectContextPadEntries('BoundaryEvent_1', [
|
|
'connect',
|
|
'replace',
|
|
'append.compensation-activity',
|
|
'!append.end-event'
|
|
]);
|
|
}));
|
|
|
|
|
|
it('should provide DataStoreReference entries', inject(function() {
|
|
|
|
expectContextPadEntries('DataStoreReference', [
|
|
'connect',
|
|
'append.text-annotation',
|
|
'!replace',
|
|
'!append.end-event'
|
|
]);
|
|
}));
|
|
|
|
|
|
it('should provide DataObjectReference entries', inject(function() {
|
|
|
|
expectContextPadEntries('DataObjectReference', [
|
|
'connect',
|
|
'append.text-annotation',
|
|
'!replace',
|
|
'!append.end-event'
|
|
]);
|
|
}));
|
|
|
|
});
|
|
|
|
|
|
describe('replace', function() {
|
|
|
|
var diagramXML = require('../../../fixtures/bpmn/simple.bpmn');
|
|
|
|
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
|
|
|
|
var container;
|
|
|
|
beforeEach(function() {
|
|
container = TestContainer.get(this);
|
|
});
|
|
|
|
|
|
it('should show popup menu in the correct position', inject(function(elementRegistry, contextPad) {
|
|
|
|
// given
|
|
var element = elementRegistry.get('StartEvent_1'),
|
|
padding = 5,
|
|
replaceMenuRect,
|
|
padMenuRect;
|
|
|
|
contextPad.open(element);
|
|
padMenuRect = contextPad.getPad(element).html.getBoundingClientRect();
|
|
|
|
// mock event
|
|
var event = {
|
|
target: padEntry(container, 'replace'),
|
|
preventDefault: function() {}
|
|
};
|
|
|
|
// when
|
|
contextPad.trigger('click', event);
|
|
replaceMenuRect = domQuery('.bpmn-replace', container).getBoundingClientRect();
|
|
|
|
// then
|
|
expect(replaceMenuRect.left).to.be.at.most(padMenuRect.left);
|
|
expect(replaceMenuRect.top).to.be.at.most(padMenuRect.bottom + padding);
|
|
}));
|
|
|
|
|
|
it('should not include control if replacement is disallowed',
|
|
inject(function(elementRegistry, contextPad, customRules) {
|
|
|
|
// given
|
|
var element = elementRegistry.get('StartEvent_1');
|
|
|
|
// disallow replacement
|
|
customRules.addRule('shape.replace', function(context) {
|
|
return !is(context.element, 'bpmn:StartEvent');
|
|
});
|
|
|
|
// when
|
|
contextPad.open(element);
|
|
|
|
var padNode = contextPad.getPad(element).html;
|
|
|
|
// then
|
|
expect(padEntry(padNode, 'replace')).not.to.exist;
|
|
}));
|
|
|
|
|
|
it('should include control if replacement is allowed',
|
|
inject(function(elementRegistry, contextPad, customRules) {
|
|
|
|
// given
|
|
var element = elementRegistry.get('EndEvent_1');
|
|
|
|
// disallow replacement
|
|
customRules.addRule('shape.replace', function(context) {
|
|
return !is(context.element, 'bpmn:StartEvent');
|
|
});
|
|
|
|
// when
|
|
contextPad.open(element);
|
|
|
|
var padNode = contextPad.getPad(element).html;
|
|
|
|
// 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;
|
|
|
|
// when
|
|
create.start(canvasEvent({ x: 0, y: 0 }), startEvent);
|
|
|
|
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 }));
|
|
|
|
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' });
|
|
|
|
modeling.createShape(task, { x: 100, y: 100 }, rootShape);
|
|
|
|
// when
|
|
create.start(canvasEvent({ x: 0, y: 0 }), intermediateEvent);
|
|
|
|
dragging.move(canvasEvent({ x: 50, y: 50 }));
|
|
dragging.hover({ element: task });
|
|
dragging.move(canvasEvent({ x: 50, y: 65 }));
|
|
|
|
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;
|
|
|
|
// 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.end(canvasEvent({ x: 75, y: 75 }, { ctrlKey: true, metaKey: true }));
|
|
|
|
replaceMenu = domQuery('.bpmn-replace', container);
|
|
|
|
// then
|
|
expect(replaceMenu).to.not.exist;
|
|
}));
|
|
|
|
});
|
|
|
|
|
|
describe('auto place', function() {
|
|
|
|
var diagramXML = require('../../../fixtures/bpmn/simple.bpmn');
|
|
|
|
beforeEach(bootstrapModeler(diagramXML, {
|
|
modules: testModules.concat(autoPlaceModule)
|
|
}));
|
|
|
|
var container;
|
|
|
|
beforeEach(function() {
|
|
container = TestContainer.get(this);
|
|
});
|
|
|
|
|
|
it('should trigger', inject(function(elementRegistry, contextPad) {
|
|
|
|
// given
|
|
var element = elementRegistry.get('Task_1');
|
|
|
|
contextPad.open(element);
|
|
|
|
// mock event
|
|
var event = {
|
|
clientX: 100,
|
|
clientY: 100,
|
|
target: padEntry(container, 'append.gateway'),
|
|
preventDefault: function() {}
|
|
};
|
|
|
|
// when
|
|
contextPad.trigger('click', event);
|
|
|
|
// then
|
|
expect(element.outgoing).to.have.length(1);
|
|
}));
|
|
|
|
});
|
|
|
|
|
|
describe('disabled auto-place', function() {
|
|
|
|
var diagramXML = require('../../../fixtures/bpmn/simple.bpmn');
|
|
|
|
beforeEach(bootstrapModeler(diagramXML, {
|
|
modules: testModules.concat(autoPlaceModule),
|
|
contextPad: {
|
|
autoPlace: false
|
|
}
|
|
}));
|
|
|
|
var container;
|
|
|
|
beforeEach(function() {
|
|
container = TestContainer.get(this);
|
|
});
|
|
|
|
|
|
it('should default to drag start', inject(function(elementRegistry, contextPad, dragging) {
|
|
|
|
// given
|
|
var element = elementRegistry.get('Task_1');
|
|
|
|
contextPad.open(element);
|
|
|
|
// mock event
|
|
var event = {
|
|
clientX: 100,
|
|
clientY: 100,
|
|
target: padEntry(container, 'append.gateway'),
|
|
preventDefault: function() {}
|
|
};
|
|
|
|
// when
|
|
contextPad.trigger('click', event);
|
|
|
|
// then
|
|
expect(dragging.context()).to.exist;
|
|
}));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
function padEntry(element, name) {
|
|
return domQuery('[data-action="' + name + '"]', element);
|
|
}
|