feat(replace-preview): Add visual feedback during shape movement

Closes #325
This commit is contained in:
pedesen 2015-08-17 16:43:15 +02:00
parent e1876fa3e8
commit 4196ae8b00
8 changed files with 502 additions and 36 deletions

View File

@ -138,7 +138,8 @@ Modeler.prototype._modelingModules = [
require('./features/snapping'),
require('./features/modeling'),
require('./features/context-pad'),
require('./features/palette')
require('./features/palette'),
require('./features/replace-preview')
];

View File

@ -4,58 +4,43 @@ var inherits = require('inherits');
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor');
var forEach = require('lodash/collection').forEach;
var isInterrupting = require('../../../util/DiUtil').isInterrupting,
isEventSubProcess = require('../../../util/DiUtil').isEventSubProcess,
is = require('../../../util/ModelUtil').is;
var forEach = require('lodash/collection/forEach');
var isEventSubProcess = require('../../../util/DiUtil').isEventSubProcess;
/**
* Defines the behavior when a start event is moved
*/
function MoveStartEventBehavior(eventBus, bpmnReplace) {
function MoveStartEventBehavior(eventBus, bpmnReplace, bpmnRules, elementRegistry) {
CommandInterceptor.call(this, eventBus);
/**
* Replaces non-interrupting StartEvents by blank interrupting StartEvents,
* if the target is not an event sub process.
*/
function replaceElement(element, target) {
if (!isEventSubProcess(target)) {
if (is(element, 'bpmn:StartEvent') && !isInterrupting(element) && element.type !== 'label') {
bpmnReplace.replaceElement(element, { type: 'bpmn:StartEvent' });
}
}
}
this.postExecuted([ 'elements.move' ], function(event) {
var target = event.context.newParent;
var target = event.context.newParent,
elements = [];
forEach(event.context.closure.topLevel, function(topLevelElements) {
if(isEventSubProcess(topLevelElements)) {
forEach(topLevelElements.children, function(element) {
replaceElement(element, target);
});
if (isEventSubProcess(topLevelElements)) {
elements = elements.concat(topLevelElements.children);
} else {
forEach(topLevelElements, function(element) {
replaceElement(element, target);
});
elements = elements.concat(topLevelElements);
}
});
});
var canReplace = bpmnRules.canReplace(elements, target);
forEach(canReplace.replace, function(newElementData) {
var newElement = {
type: newElementData.type
};
bpmnReplace.replaceElement(elementRegistry.get(newElementData.id), newElement);
});
});
}
MoveStartEventBehavior.$inject = [ 'eventBus', 'bpmnReplace' ];
MoveStartEventBehavior.$inject = [ 'eventBus', 'bpmnReplace', 'bpmnRules', 'elementRegistry' ];
inherits(MoveStartEventBehavior, CommandInterceptor);

View File

@ -4,13 +4,15 @@ var groupBy = require('lodash/collection/groupBy'),
size = require('lodash/collection/size'),
find = require('lodash/collection/find'),
any = require('lodash/collection/any'),
forEach = require('lodash/collection/forEach'),
inherits = require('inherits');
var getParents = require('../ModelingUtil').getParents,
is = require('../../../util/ModelUtil').is,
getBusinessObject = require('../../../util/ModelUtil').getBusinessObject,
isExpanded = require('../../../util/DiUtil').isExpanded,
isEventSubProcess = require('../../../util/DiUtil').isEventSubProcess;
isEventSubProcess = require('../../../util/DiUtil').isEventSubProcess,
isInterrupting = require('../../../util/DiUtil').isInterrupting;
var RuleProvider = require('diagram-js/lib/features/rules/RuleProvider');
@ -76,7 +78,9 @@ BpmnRules.prototype.init = function() {
shapes = context.shapes,
position = context.position;
return canAttach(shapes, target, null, position) || canMove(shapes, target, position);
return canAttach(shapes, target, null, position) ||
canReplace(shapes, target) ||
canMove(shapes, target, position);
});
this.addRule([ 'shape.create', 'shape.append' ], function(context) {
@ -100,6 +104,8 @@ BpmnRules.prototype.canMove = canMove;
BpmnRules.prototype.canAttach = canAttach;
BpmnRules.prototype.canReplace = canReplace;
BpmnRules.prototype.canDrop = canDrop;
BpmnRules.prototype.canInsert = canInsert;
@ -383,6 +389,58 @@ function canAttach(elements, target, source, position) {
return 'attach';
}
/**
* Defines how to replace elements for a given target.
*
* Returns an array containing all elements which will be replaced.
*
* @example
*
* [{ id: 'IntermediateEvent_2',
* type: 'bpmn:StartEvent'
* },
* { id: 'IntermediateEvent_5',
* type: 'bpmn:EndEvent'
* }]
*
* @param {Array} elements
* @param {Object} target
*
* @return {Object} an object containing all elements which have to be replaced
*/
function canReplace(elements, target) {
if (!target) {
return false;
}
var canExecute = {
replace: []
};
forEach(elements, function(element) {
// replace a non-interrupting start event by a blank interrupting start event
// when the target is not an event sub process
if (!isEventSubProcess(target)) {
if (is(element, 'bpmn:StartEvent') &&
!isInterrupting(element) &&
element.type !== 'label' &&
canDrop(element, target)) {
canExecute.replace.push({
id: element.id,
type: 'bpmn:StartEvent'
});
}
}
});
return canExecute.replace.length ? canExecute : false;
}
function canMove(elements, target) {
// only move if they have the same parent

View File

@ -0,0 +1,116 @@
'use strict';
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor');
var inherits = require('inherits');
var assign = require('lodash/object/assign'),
forEach = require('lodash/collection/forEach'),
includes = require('lodash/collection/includes');
var LOW_PRIORITY = 250;
function BpmnReplacePreview(eventBus, elementRegistry, elementFactory, canvas, moveVisuals) {
CommandInterceptor.call(this, eventBus);
/**
* Replace the visuals of all elements in the context which can be replaced
*
* @param {Object} context
*/
function replaceVisual(context) {
var replacements = context.canExecute.replace;
forEach(replacements, function(newElementData) {
var id = newElementData.id;
var newElement = {
type: newElementData.type
};
// if the visual of the element is already replaced
if (includes(context.visualReplacements, id)) {
return;
}
var element = elementRegistry.get(id);
assign(newElement, { x: element.x, y: element.y });
// create a temporary shape
var tempShape = elementFactory.createShape(newElement);
canvas.addShape(tempShape, element.parent);
// select the original SVG element related to the element and hide it
var gfx = context.dragGroup.select('[data-element-id=' + element.id + ']');
if (gfx) {
gfx.attr({ display: 'none' });
}
// clone the gfx of the temporary shape and add it to the drag group
var dragger = moveVisuals.addDragger(context, tempShape);
context.visualReplacements.push({
id: id,
dragger: dragger
});
canvas.removeShape(tempShape);
});
}
/**
* Restore the original visuals of the previously replaced elements
*
* @param {Object} context
*/
function restoreVisual(context) {
var visualReplacements = context.visualReplacements;
forEach(visualReplacements, function(element) {
var originalGfx = context.dragGroup.select('[data-element-id=' + element.id + ']'),
idx;
if (originalGfx) {
originalGfx.attr({ display: 'inline' });
}
element.dragger.remove();
idx = visualReplacements.indexOf(element.id);
if (idx !== -1) {
visualReplacements.splice(idx, 1);
}
});
}
eventBus.on('shape.move.move', LOW_PRIORITY, function(event) {
var context = event.context,
canExecute = context.canExecute;
if (!context.visualReplacements) {
context.visualReplacements = [];
}
if (canExecute.replace) {
replaceVisual(context);
} else {
restoreVisual(context);
}
});
}
BpmnReplacePreview.$inject = [ 'eventBus', 'elementRegistry', 'elementFactory', 'canvas', 'moveVisuals' ];
inherits(BpmnReplacePreview, CommandInterceptor);
module.exports = BpmnReplacePreview;

View File

@ -0,0 +1,5 @@
module.exports = {
__depends__: [ require('diagram-js/lib/features/move') ],
__init__: ['bpmnReplacePreview'],
bpmnReplacePreview: [ 'type', require('./BpmnReplacePreview') ]
};

View File

@ -0,0 +1,47 @@
<?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:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn:process id="Process_1" isExecutable="false">
<bpmn:subProcess id="SubProcess_1" triggeredByEvent="true">
<bpmn:startEvent id="StartEvent_1" isInterrupting="false">
<bpmn:messageEventDefinition />
</bpmn:startEvent>
<bpmn:startEvent id="StartEvent_2" />
<bpmn:startEvent id="StartEvent_3" isInterrupting="false">
<bpmn:timerEventDefinition />
</bpmn:startEvent>
</bpmn:subProcess>
<bpmn:subProcess id="SubProcess_2" triggeredByEvent="true" />
<bpmn:subProcess id="SubProcess_3" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="SubProcess_1_di" bpmnElement="SubProcess_1" isExpanded="true">
<dc:Bounds x="92" y="46" width="149" height="138" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1">
<dc:Bounds x="119" y="75" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="92" y="111" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_2_di" bpmnElement="StartEvent_2">
<dc:Bounds x="119" y="127" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="92" y="163" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_3_di" bpmnElement="StartEvent_3">
<dc:Bounds x="180" y="75" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="153" y="111" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="SubProcess_2_di" bpmnElement="SubProcess_2" isExpanded="true">
<dc:Bounds x="299" y="46" width="144" height="138" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="SubProcess_3_di" bpmnElement="SubProcess_3" isExpanded="true">
<dc:Bounds x="503" y="46" width="161" height="137" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,249 @@
'use strict';
var TestHelper = require('../../../TestHelper');
/* global bootstrapModeler, inject */
var replacePreviewModule = require('../../../../lib/features/replace-preview'),
modelingModule = require('../../../../lib/features/modeling'),
coreModule = require('../../../../lib/core');
var Events = require('diagram-js/test/util/Events');
var assign = require('lodash/object/assign');
describe('features/replace-preview', function() {
var testModules = [ replacePreviewModule, modelingModule, coreModule ];
var diagramXML = require('./BpmnReplacePreview.bpmn');
var startEvent_1,
rootElement,
Event;
var getGfx,
moveShape;
beforeEach(bootstrapModeler(diagramXML, { modules: testModules }));
beforeEach(inject(function(canvas, elementRegistry, elementFactory, move, dragging) {
Event = Events.target(canvas._svg);
startEvent_1 = elementRegistry.get('StartEvent_1');
rootElement = canvas.getRootElement();
/**
* returns the gfx representation of an element type
*
* @param {Object} elementData
*
* @return {Object}
*/
getGfx = function(elementData) {
assign(elementData, { x: 0, y: 0 });
var tempShape = elementFactory.createShape(elementData);
canvas.addShape(tempShape, rootElement);
var gfx = elementRegistry.getGraphics(tempShape).clone();
canvas.removeShape(tempShape);
return gfx;
};
moveShape = function(shape, target, position) {
var startPosition = { x: shape.x + 10 + (shape.width / 2), y: shape.y + 30 + (shape.height / 2) };
move.start(Event.create(startPosition), shape);
dragging.hover({
element: target,
gfx: elementRegistry.getGraphics(target)
});
dragging.move(Event.create(position));
};
}));
it('should replace visuals at the same position as the replaced visual', inject(function(dragging) {
// when
moveShape(startEvent_1, rootElement, { x: 280, y: 120 });
// then
var dragGroup = dragging.active().data.context.dragGroup;
dragGroup[0].attr('display', 'inline');
expect(dragGroup[0].getBBox()).to.eql(dragGroup[1].getBBox());
}));
it('should hide the replaced visual',
inject(function(dragging) {
// when
moveShape(startEvent_1, rootElement, { x: 280, y: 120 });
// then
var dragGroup = dragging.active().data.context.dragGroup;
expect(dragGroup[0].attr('display')).to.equal('none');
}));
it('should not replace non-interrupting start event while hover over same event sub process',
inject(function(dragging, elementRegistry) {
// given
var subProcess_1 = elementRegistry.get('SubProcess_1');
// when
moveShape(startEvent_1, subProcess_1, { x: 210, y: 180 });
var context = dragging.active().data.context;
// then
// check if the visual representation remains a non interrupting message start event
var startEventGfx = getGfx({
type: 'bpmn:StartEvent',
isInterrupting: false,
_eventDefinitionType: 'bpmn:MessageEventDefinition'
});
expect(context.dragGroup[0].innerSVG()).to.equal(startEventGfx.innerSVG());
}));
it('should replace non-interrupting start event while hover over root element',
inject(function(dragging, elementRegistry) {
// when
moveShape(startEvent_1, rootElement, { x: 280, y: 120 });
var context = dragging.active().data.context;
// then
// check if the visual replacement is a blank interrupting start event
var startEventGfx = getGfx({ type: 'bpmn:StartEvent' });
expect(context.dragGroup[1].innerSVG()).to.equal(startEventGfx.innerSVG());
}));
it('should not replace non-interrupting start event while hover over another event sub process',
inject(function(dragging, elementRegistry) {
// given
var subProcess_2 = elementRegistry.get('SubProcess_2');
// when
moveShape(startEvent_1, subProcess_2, { x: 350, y: 120 });
var context = dragging.active().data.context;
// then
// check if the visual representation remains a non interrupting message start event
var startEventGfx = getGfx({
type: 'bpmn:StartEvent',
isInterrupting: false,
_eventDefinitionType: 'bpmn:MessageEventDefinition'
});
expect(context.dragGroup[0].innerSVG()).to.equal(startEventGfx.innerSVG());
}));
it('should replace non-interrupting start event while hover over regular sub process',
inject(function(dragging, elementRegistry) {
// given
var subProcess_3 = elementRegistry.get('SubProcess_3');
// when
moveShape(startEvent_1, subProcess_3, { x: 600, y: 120 });
var context = dragging.active().data.context;
// then
// check if the visual representation remains a non interrupting message start event
var startEventGfx = getGfx({ type: 'bpmn:StartEvent' });
expect(context.dragGroup[1].innerSVG()).to.equal(startEventGfx.innerSVG());
}));
it('should replace all non-interrupting start events in a selection of multiple elements',
inject(function(move, dragging, elementRegistry, selection) {
// given
var startEvent_2 = elementRegistry.get('StartEvent_2'),
startEvent_3 = elementRegistry.get('StartEvent_3');
// when
selection.select([ startEvent_1, startEvent_2, startEvent_3 ]);
moveShape(startEvent_1, rootElement, { x: 150, y: 250 });
var context = dragging.active().data.context;
// then
// check if the visual replacements are blank interrupting start events
var startEventGfx = getGfx({ type: 'bpmn:StartEvent' });
expect(context.dragGroup[1].innerSVG()).to.equal(startEventGfx.innerSVG());
expect(context.dragGroup[3].innerSVG()).to.equal(startEventGfx.innerSVG());
expect(context.dragGroup[4].innerSVG()).to.equal(startEventGfx.innerSVG());
}));
it('should not replace any non-interrupting start events in a selection of multiple elements',
inject(function(move, dragging, elementRegistry, selection) {
// given
var startEvent_2 = elementRegistry.get('StartEvent_2'),
startEvent_3 = elementRegistry.get('StartEvent_3'),
subProcess_2 = elementRegistry.get('SubProcess_2');
var messageStartEventGfx = getGfx({
type: 'bpmn:StartEvent',
isInterrupting: false,
_eventDefinitionType: 'bpmn:MessageEventDefinition'
});
var timerStartEventGfx = getGfx({
type: 'bpmn:StartEvent',
isInterrupting: false,
_eventDefinitionType: 'bpmn:TimerEventDefinition'
});
var startEventGfx = getGfx({ type: 'bpmn:StartEvent' });
// when
selection.select([ startEvent_1, startEvent_2, startEvent_3 ]);
moveShape(startEvent_1, subProcess_2, { x: 350, y: 120 });
var context = dragging.active().data.context;
// then
expect(context.dragGroup[0].innerSVG()).to.equal(messageStartEventGfx.innerSVG());
expect(context.dragGroup[1].innerSVG()).to.equal(startEventGfx.innerSVG());
expect(context.dragGroup[2].innerSVG()).to.equal(timerStartEventGfx.innerSVG());
}));
});

View File

@ -740,6 +740,7 @@ describe('features/replace', function() {
// then
expect(isInterrupting(startEventAfter)).to.be.true;
expect(startEventAfter.parent).to.equal(root);
}));
@ -760,6 +761,7 @@ describe('features/replace', function() {
// then
expect(isInterrupting(startEventAfter)).to.be.true;
expect(startEventAfter.parent).to.equal(subProcess);
}));
@ -786,6 +788,7 @@ describe('features/replace', function() {
// then
expect(startEvent.id).to.equal(startEventAfter.id);
expect(startEventAfter.parent).to.equal(eventSubProcess);
}));
@ -808,6 +811,7 @@ describe('features/replace', function() {
// then
expect(startEventAfter).to.equal(interruptingStartEvent);
expect(startEventAfter.parent).to.equal(root);
}));
@ -827,6 +831,7 @@ describe('features/replace', function() {
})[0];
expect(isInterrupting(replacedStartEvent)).to.be.true;
expect(replacedStartEvent.parent).to.equal(subProcess);
}));