diff --git a/lib/features/modeling/cmd/SpaceToolHandler.js b/lib/features/modeling/cmd/SpaceToolHandler.js index 685b13ba6..5e7d2a0fe 100644 --- a/lib/features/modeling/cmd/SpaceToolHandler.js +++ b/lib/features/modeling/cmd/SpaceToolHandler.js @@ -1,4 +1,8 @@ -import { forEach } from 'min-dash'; +import { + forEach, + groupBy, + keys +} from 'min-dash'; import { resizeBounds @@ -6,43 +10,140 @@ import { /** - * A handler that implements reversible creating and removing of space. - * - * It executes in two phases: - * - * (1) resize all affected resizeShapes - * (2) move all affected moveElements + * Add or remove space by moving and resizing shapes. */ export default function SpaceToolHandler(modeling) { this._modeling = modeling; } -SpaceToolHandler.$inject = [ 'modeling' ]; - +SpaceToolHandler.$inject = [ + 'modeling' +]; SpaceToolHandler.prototype.preExecute = function(context) { - - // resize - var modeling = this._modeling, + var self = this, + movingShapes = context.movingShapes, resizingShapes = context.resizingShapes, delta = context.delta, direction = context.direction; - forEach(resizingShapes, function(shape) { - var newBounds = resizeBounds(shape, direction, delta); + var addingSpace = isAddingSpace(delta, direction); + + var steps = getSteps(movingShapes, resizingShapes); + + if (!addingSpace) { + steps = steps.reverse(); + } - modeling.resizeShape(shape, newBounds); + steps.forEach(function(step) { + var type = step.type, + shapes = step.shapes; + + if (type === 'resize') { + self.resizeShapes(shapes, delta, direction); + } else if (type === 'move') { + self.moveShapes(shapes, delta); + } }); }; -SpaceToolHandler.prototype.postExecute = function(context) { - // move - var modeling = this._modeling, - movingShapes = context.movingShapes, - delta = context.delta; +SpaceToolHandler.prototype.execute = function() {}; +SpaceToolHandler.prototype.revert = function() {}; - modeling.moveElements(movingShapes, delta, undefined, { autoResize: false, attach: false }); +SpaceToolHandler.prototype.moveShapes = function(shapes, delta) { + this._modeling.moveElements(shapes, delta, null, { + autoResize: false, + recurse: false + }); }; -SpaceToolHandler.prototype.execute = function(context) {}; -SpaceToolHandler.prototype.revert = function(context) {}; +SpaceToolHandler.prototype.resizeShapes = function(shapes, delta, direction) { + var self = this; + + forEach(shapes, function(shape) { + var newBounds = resizeBounds(shape, direction, delta); + + self._modeling.resizeShape(shape, newBounds); + }); +}; + + + +// helpers ////////// + +function isAddingSpace(delta, direction) { + if (direction === 'n') { + return delta.y < 0; + } else if (direction === 'w') { + return delta.x < 0; + } else if (direction === 's') { + return delta.y >= 0; + } else if (direction === 'e') { + return delta.x >= 0; + } +} + +/** + * Get steps for moving and resizing shapes starting with top-level shapes. + * + * @param {Array} movingShapes + * @param {Array} resizingShapes + * + * @returns {Array} + */ +export function getSteps(movingShapes, resizingShapes) { + var steps = []; + + var groupedMovingShapes = groupBy(movingShapes, getIndex), + groupedResizingShapes = groupBy(resizingShapes, getIndex); + + var maxIndex = max(keys(groupedMovingShapes).concat(keys(groupedResizingShapes)).concat(0)); + + var index = 1; + + while (index <= maxIndex) { + if (groupedMovingShapes[ index ]) { + + if (groupedMovingShapes[ index ]) { + steps.push({ + type: 'move', + shapes: groupedMovingShapes[ index ] + }); + } + } + + if (groupedResizingShapes[ index ]) { + steps.push({ + type: 'resize', + shapes: groupedResizingShapes[ index ] + }); + } + + index++; + } + + return steps; +} + +/** + * Get index of a given shape. + * + * @param {djs.model.Shape} shape + * + * @returns {number} + */ +function getIndex(shape) { + var index = 0; + + while (shape.parent) { + index++; + + shape = shape.parent; + } + + return index; +} + +function max(array) { + return Math.max.apply(null, array); +} \ No newline at end of file diff --git a/test/spec/features/space-tool/SpaceToolSpec.js b/test/spec/features/space-tool/SpaceToolSpec.js index 9d931d643..3e51d0565 100644 --- a/test/spec/features/space-tool/SpaceToolSpec.js +++ b/test/spec/features/space-tool/SpaceToolSpec.js @@ -13,6 +13,8 @@ import autoResizeModule from 'lib/features/auto-resize'; import rulesModule from './rules'; import autoResizeProviderModule from './auto-resize'; +import { getSteps } from 'lib/features/modeling/cmd/SpaceToolHandler'; + import { isMac } from 'lib/util/Platform'; var keyModifier = isMac() ? { metaKey: true } : { ctrlKey: true }; @@ -629,6 +631,135 @@ describe('features/space-tool', function() { }); + describe('steps', function() { + + beforeEach(bootstrapDiagram()); + + var level1shape1, + level1shape2, + level1shape2label, + level2shape1, + level3shape1, + level3shape1label, + level3shape2, + level3connection, + level3connectionLabel; + + beforeEach(inject(function(elementFactory, canvas) { + + level1shape1 = elementFactory.createShape({ + id: 'level1shape1', + x: 100, y: 100, + width: 450, height: 200 + }); + + canvas.addShape(level1shape1); + + level1shape2 = elementFactory.createShape({ + id: 'level1shape2', + x: 400, y: 350, + width: 100, height: 50 + }); + + canvas.addShape(level1shape2); + + level1shape2label = elementFactory.createLabel({ + id: 'level1shape2label', + x: 425, y: 425, + width: 50, height: 20, + labelTarget: level1shape2 + }); + + canvas.addShape(level1shape2label); + + level2shape1 = elementFactory.createShape({ + id: 'level2shape1', + x: 125, y: 125, + width: 400, height: 150 + }); + + canvas.addShape(level2shape1, level1shape1); + + level3shape1 = elementFactory.createShape({ + id: 'level3shape1', + x: 150, y: 150, + width: 100, height: 50 + }); + + canvas.addShape(level3shape1, level2shape1); + + level3shape1label = elementFactory.createLabel({ + id: 'level3shape1label', + x: 175, y: 225, + width: 50, height: 20, + labelTarget: level3shape1 + }); + + canvas.addShape(level3shape1label, level2shape1); + + level3shape2 = elementFactory.createShape({ + id: 'level3shape2', + x: 400, y: 150, + width: 100, height: 50 + }); + + canvas.addShape(level3shape2, level2shape1); + + level3connection = elementFactory.createConnection({ + id: 'level3connection', + source: level3shape1, + target: level3shape2, + waypoints: [ + { x: 250, y: 175 }, + { x: 400, y: 175 } + ] + }); + + canvas.addConnection(level3connection, level2shape1); + + level3connectionLabel = elementFactory.createLabel({ + id: 'level3connectionLabel', + x: 300, y: 200, + width: 50, height: 20, + labelTarget: level3shape1 + }); + + canvas.addShape(level3connectionLabel, level2shape1); + })); + + + it('should return steps', function() { + + // given + var movingShapes = [ + level1shape2, + level1shape2label, + level3shape2, + level3connectionLabel + ]; + + var resizingShapes = [ + level1shape1, + level2shape1 + ]; + + // when + var steps = getSteps(movingShapes, resizingShapes); + + // then + expect(steps).to.have.length(4); + + expect(steps).to.eql([ + { type: 'move', shapes: [ level1shape2, level1shape2label ] }, + { type: 'resize', shapes: [ level1shape1 ] }, + { type: 'resize', shapes: [ level2shape1 ] }, + { type: 'move', shapes: [ level3shape2, level3connectionLabel ] } + ]); + }); + + }); + + describe('redo / undo integration', function() { beforeEach(bootstrapDiagram({