diff --git a/lib/Modeler.js b/lib/Modeler.js index aeb3fec5..a4f5d13b 100644 --- a/lib/Modeler.js +++ b/lib/Modeler.js @@ -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') ]; diff --git a/lib/features/modeling/behavior/MoveStartEventBehavior.js b/lib/features/modeling/behavior/MoveStartEventBehavior.js index b769f80a..77e639e5 100644 --- a/lib/features/modeling/behavior/MoveStartEventBehavior.js +++ b/lib/features/modeling/behavior/MoveStartEventBehavior.js @@ -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); diff --git a/lib/features/modeling/rules/BpmnRules.js b/lib/features/modeling/rules/BpmnRules.js index 8683fb9a..ff4407a5 100644 --- a/lib/features/modeling/rules/BpmnRules.js +++ b/lib/features/modeling/rules/BpmnRules.js @@ -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 diff --git a/lib/features/replace-preview/BpmnReplacePreview.js b/lib/features/replace-preview/BpmnReplacePreview.js new file mode 100644 index 00000000..105fce65 --- /dev/null +++ b/lib/features/replace-preview/BpmnReplacePreview.js @@ -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; diff --git a/lib/features/replace-preview/index.js b/lib/features/replace-preview/index.js new file mode 100644 index 00000000..f8ffc9b9 --- /dev/null +++ b/lib/features/replace-preview/index.js @@ -0,0 +1,5 @@ +module.exports = { + __depends__: [ require('diagram-js/lib/features/move') ], + __init__: ['bpmnReplacePreview'], + bpmnReplacePreview: [ 'type', require('./BpmnReplacePreview') ] +}; diff --git a/test/spec/features/replace-preview/BpmnReplacePreview.bpmn b/test/spec/features/replace-preview/BpmnReplacePreview.bpmn new file mode 100644 index 00000000..940788e5 --- /dev/null +++ b/test/spec/features/replace-preview/BpmnReplacePreview.bpmn @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/features/replace-preview/BpmnReplacePreviewSpec.js b/test/spec/features/replace-preview/BpmnReplacePreviewSpec.js new file mode 100644 index 00000000..be87bc48 --- /dev/null +++ b/test/spec/features/replace-preview/BpmnReplacePreviewSpec.js @@ -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()); + + })); + +}); diff --git a/test/spec/features/replace/BpmnReplaceSpec.js b/test/spec/features/replace/BpmnReplaceSpec.js index d03503d8..ec689366 100644 --- a/test/spec/features/replace/BpmnReplaceSpec.js +++ b/test/spec/features/replace/BpmnReplaceSpec.js @@ -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); }));