diff --git a/lib/features/complex-preview/ComplexPreview.js b/lib/features/complex-preview/ComplexPreview.js new file mode 100644 index 000000000..b93e74c27 --- /dev/null +++ b/lib/features/complex-preview/ComplexPreview.js @@ -0,0 +1,155 @@ +import { + clear as svgClear, + create as svgCreate +} from 'tiny-svg'; + +import { getVisual } from '../../util/GraphicsUtil'; + +import { isConnection } from '../../util/ModelUtil'; + +import { translate } from '../../util/SvgTransformUtil'; + +/** + * @typedef {import('../../model/Types').Element} Element + * @typedef {import('../../model/Types').Shape} Shape + * @typedef {import('../../util/Types').Point} Point + * @typedef {import('../../util/Types').Rect} Rect + * + * @typedef { { element: Element, delta: Point } } MovedOption + * @typedef { { shape: Shape, bounds: Rect } } ResizedOption + * + * @typedef { { + * created?: Element[], + * removed?: Element[], + * moved?: MovedOption[], + * resized?: ResizedOption[] + * } } CreateOptions + */ + +const LAYER_NAME = 'complex-preview'; + +/** + * Complex preview for shapes and connections. + */ +export default class ComplexPreview { + constructor(canvas, graphicsFactory, previewSupport) { + this._canvas = canvas; + this._graphicsFactory = graphicsFactory; + this._previewSupport = previewSupport; + + this._markers = []; + } + + /** + * Create complex preview. + * + * @param {CreateOptions} options + */ + create(options) { + + // there can only be one complex preview at a time + this.cleanUp(); + + const { + created = [], + moved = [], + removed = [], + resized = [] + } = options; + + const layer = this._canvas.getLayer(LAYER_NAME); + + // shapes and connections to be created + created.filter(element => !isHidden(element)).forEach(element => { + let gfx; + + if (isConnection(element)) { + gfx = this._graphicsFactory._createContainer('connection', svgCreate('g')); + + this._graphicsFactory.drawConnection(getVisual(gfx), element); + } else { + gfx = this._graphicsFactory._createContainer('shape', svgCreate('g')); + + this._graphicsFactory.drawShape(getVisual(gfx), element); + + translate(gfx, element.x, element.y); + } + + this._previewSupport.addDragger(element, layer, gfx); + }); + + // elements to be moved + moved.forEach(({ element, delta }) => { + this._previewSupport.addDragger(element, layer, undefined, 'djs-dragging'); + + this._canvas.addMarker(element, 'djs-element-hidden'); + + this._markers.push([ element, 'djs-element-hidden' ]); + + const dragger = this._previewSupport.addDragger(element, layer); + + if (isConnection(element)) { + translate(dragger, delta.x, delta.y); + } else { + translate(dragger, element.x + delta.x, element.y + delta.y); + } + }); + + // elements to be removed + removed.forEach(element => { + this._previewSupport.addDragger(element, layer, undefined, 'djs-dragging'); + + this._canvas.addMarker(element, 'djs-element-hidden'); + + this._markers.push([ element, 'djs-element-hidden' ]); + }); + + // elements to be resized + resized.forEach(({ shape, bounds }) => { + this._canvas.addMarker(shape, 'djs-hidden'); + + this._markers.push([ shape, 'djs-hidden' ]); + + this._previewSupport.addDragger(shape, layer, undefined, 'djs-dragging'); + + const gfx = this._graphicsFactory._createContainer('shape', svgCreate('g')); + + this._graphicsFactory.drawShape(getVisual(gfx), shape, { + width: bounds.width, + height: bounds.height + }); + + translate(gfx, bounds.x, bounds.y); + + this._previewSupport.addDragger(shape, layer, gfx); + }); + } + + cleanUp() { + svgClear(this._canvas.getLayer(LAYER_NAME)); + + this._markers.forEach(([ element, marker ]) => this._canvas.removeMarker(element, marker)); + + this._markers = []; + + this._previewSupport.cleanUp(); + } + + show() { + this._canvas.showLayer(LAYER_NAME); + } + + hide() { + this._canvas.hideLayer(LAYER_NAME); + } +} + +ComplexPreview.$inject = [ + 'canvas', + 'graphicsFactory', + 'previewSupport' +]; + +function isHidden(element) { + return element.hidden; +} \ No newline at end of file diff --git a/lib/features/complex-preview/index.js b/lib/features/complex-preview/index.js new file mode 100644 index 000000000..8d6dbe5b0 --- /dev/null +++ b/lib/features/complex-preview/index.js @@ -0,0 +1,12 @@ +import PreviewSupportModule from '../preview-support'; + +import ComplexPreview from './ComplexPreview'; + +/** + * @type { import('didi').ModuleDeclaration } + */ +export default { + __depends__: [ PreviewSupportModule ], + __init__: [ 'complexPreview' ], + complexPreview: [ 'type', ComplexPreview ] +}; \ No newline at end of file diff --git a/lib/features/preview-support/PreviewSupport.js b/lib/features/preview-support/PreviewSupport.js index 4355bde95..3a9a0e978 100644 --- a/lib/features/preview-support/PreviewSupport.js +++ b/lib/features/preview-support/PreviewSupport.js @@ -118,6 +118,8 @@ PreviewSupport.prototype.addDragger = function(element, group, gfx, className = svgAppend(group, dragger); + svgAttr(dragger, 'data-complex-preview-element-id', element.id); + return dragger; }; @@ -141,6 +143,8 @@ PreviewSupport.prototype.addFrame = function(shape, group) { svgAppend(group, frame); + svgAttr(frame, 'data-complex-preview-element-id', shape.id); + return frame; }; diff --git a/test/spec/features/complex-preview/ComplexPreviewSpec.js b/test/spec/features/complex-preview/ComplexPreviewSpec.js new file mode 100644 index 000000000..5e8fa0767 --- /dev/null +++ b/test/spec/features/complex-preview/ComplexPreviewSpec.js @@ -0,0 +1,168 @@ +import { + bootstrapDiagram, + inject +} from 'test/TestHelper'; + +import complexPreviewModule from 'lib/features/complex-preview'; +import modelingModule from 'lib/features/modeling'; + +import { queryAll as domQueryAll } from 'min-dom'; + +var testModules = [ + complexPreviewModule, + modelingModule +]; + + +describe('features/complex-preview - ComplexPreviewSpec', function() { + + var root, + shape1, + shape2, + shape3, + connection, + newShape; + + function setupDiagram(elementFactory, canvas) { + root = elementFactory.createRoot({ + id: 'root' + }); + + canvas.setRootElement(root); + + shape1 = elementFactory.createShape({ + id: 'shape1', + x: 0, + y: 0, + width: 100, + height: 100 + }); + + canvas.addShape(shape1, root); + + shape2 = elementFactory.createShape({ + id: 'shape2', + x: 200, + y: 0, + width: 100, + height: 100 + }); + + canvas.addShape(shape2, root); + + shape3 = elementFactory.createShape({ + id: 'shape3', + x: 0, + y: 200, + width: 100, + height: 100 + }); + + canvas.addShape(shape3, root); + + connection = elementFactory.createConnection({ + id: 'connection', + source: shape1, + target: shape2, + waypoints: [ + { x: 100, y: 50 }, + { x: 200, y: 50 } + ] + }); + + canvas.addConnection(connection, root); + + newShape = elementFactory.createShape({ + id: 'newShape', + x: 400, + y: 0, + width: 100, + height: 100 + }); + } + + + beforeEach(bootstrapDiagram({ + modules: testModules + })); + + beforeEach(inject(setupDiagram)); + + + beforeEach(inject(function(complexPreview) { + complexPreview.create({ + created: [ + newShape + ], + removed: [ + shape1, + connection + ], + moved: [ + { + element: shape2, + delta: { + x: 100, + y: 100 + } + } + ], + resized: [ + { + shape: shape3, + bounds: { + x: 0, + y: 200, + width: 200, + height: 200 + } + } + ] + }); + })); + + + it('should create preview', inject(function(canvas) { + + // given + + // when + + // then + const layer = canvas.getLayer('complex-preview'); + + expect(layer).to.exist; + + // created or removed (1 preview) + expect(queryPreview('newShape', layer)).to.have.length(1); + expect(queryPreview('connection', layer)).to.have.length(1); + expect(queryPreview('shape1', layer)).to.have.length(1); + + // moved (2 previews) + expect(queryPreview('shape2', layer)).to.have.length(2); + + // resized (2 previews) + expect(queryPreview('shape3', layer)).to.have.length(2); + })); + + + it('should clean up preview', inject(function(canvas, complexPreview) { + + // given + + // when + complexPreview.cleanUp(); + + // then + const layer = canvas.getLayer('complex-preview'); + + expect(layer).to.exist; + + expect(layer.childNodes).to.have.length(0); + })); + +}); + +function queryPreview(id, layer) { + return domQueryAll('[data-complex-preview-element-id="' + id + '"]', layer); +} \ No newline at end of file