diff --git a/assets/diagram-js.css b/assets/diagram-js.css index 72ba58b92..f869ef75b 100644 --- a/assets/diagram-js.css +++ b/assets/diagram-js.css @@ -40,6 +40,7 @@ --context-pad-entry-hover-background-color: var(--color-grey-225-10-95); --element-dragger-color: var(--color-blue-205-100-50); + --element-dragging-color: var(--color-grey-225-10-75); --element-hover-outline-fill-color: var(--color-blue-205-100-45); --element-selected-outline-stroke-color: var(--color-blue-205-100-50); --element-selected-outline-secondary-stroke-color: var(--color-blue-205-100-70); @@ -280,10 +281,6 @@ marker.djs-dragger tspan { stroke: var(--element-dragger-color) !important; } -.djs-dragging { - opacity: 0.3; -} - .djs-dragging, .djs-dragging > * { pointer-events: none !important; @@ -294,6 +291,32 @@ marker.djs-dragger tspan { display: none !important; } +.djs-dragging * { + fill: none !important; + stroke: var(--element-dragging-color) !important; +} + +.djs-dragging tspan, +.djs-dragging text { + fill: var(--element-dragging-color) !important; + stroke: none !important; +} + +marker.djs-dragging circle, +marker.djs-dragging path, +marker.djs-dragging polygon, +marker.djs-dragging polyline, +marker.djs-dragging rect { + fill: var(--element-dragging-color) !important; + stroke: none !important; +} + +marker.djs-dragging text, +marker.djs-dragging tspan { + fill: none !important; + stroke: var(--element-dragging-color) !important; +} + /** * no pointer events for visual */ diff --git a/lib/core/GraphicsFactory.js b/lib/core/GraphicsFactory.js index d0eaef347..d520c3df6 100644 --- a/lib/core/GraphicsFactory.js +++ b/lib/core/GraphicsFactory.js @@ -202,13 +202,14 @@ GraphicsFactory.prototype.updateContainments = function(elements) { * * @param {SVGElement} visual The graphical element. * @param {ShapeLike} element The shape. + * @param {Object} attrs Optional attributes. * * @return {SVGElement} */ -GraphicsFactory.prototype.drawShape = function(visual, element) { +GraphicsFactory.prototype.drawShape = function(visual, element, attrs = {}) { var eventBus = this._eventBus; - return eventBus.fire('render.shape', { gfx: visual, element: element }); + return eventBus.fire('render.shape', { gfx: visual, element, attrs }); }; /** @@ -229,13 +230,14 @@ GraphicsFactory.prototype.getShapePath = function(element) { * * @param {SVGElement} visual The graphical element. * @param {ConnectionLike} element The connection. + * @param {Object} attrs Optional attributes. * * @return {SVGElement} */ -GraphicsFactory.prototype.drawConnection = function(visual, element) { +GraphicsFactory.prototype.drawConnection = function(visual, element, attrs = {}) { var eventBus = this._eventBus; - return eventBus.fire('render.connection', { gfx: visual, element: element }); + return eventBus.fire('render.connection', { gfx: visual, element, attrs }); }; /** diff --git a/lib/features/bendpoints/BendpointMovePreview.js b/lib/features/bendpoints/BendpointMovePreview.js index 35b0f319a..636493985 100644 --- a/lib/features/bendpoints/BendpointMovePreview.js +++ b/lib/features/bendpoints/BendpointMovePreview.js @@ -25,7 +25,7 @@ var MARKER_OK = 'connect-ok', MARKER_NOT_OK = 'connect-not-ok', MARKER_CONNECT_HOVER = 'connect-hover', MARKER_CONNECT_UPDATING = 'djs-updating', - MARKER_ELEMENT_HIDDEN = 'djs-element-hidden'; + MARKER_DRAGGER = 'djs-dragging'; var HIGH_PRIORITY = 1100; @@ -65,7 +65,7 @@ export default function BendpointMovePreview(bendpointMove, injector, eventBus, svgClasses(draggerGfx).add('djs-dragging'); - canvas.addMarker(connection, MARKER_ELEMENT_HIDDEN); + canvas.addMarker(connection, MARKER_DRAGGER); canvas.addMarker(connection, MARKER_CONNECT_UPDATING); }); @@ -195,7 +195,7 @@ export default function BendpointMovePreview(bendpointMove, injector, eventBus, svgRemove(draggerGfx); canvas.removeMarker(connection, MARKER_CONNECT_UPDATING); - canvas.removeMarker(connection, MARKER_ELEMENT_HIDDEN); + canvas.removeMarker(connection, MARKER_DRAGGER); if (hover) { canvas.removeMarker(hover, MARKER_OK); 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/connection-preview/ConnectionPreview.js b/lib/features/connection-preview/ConnectionPreview.js index d5ddb0e84..544baef7a 100644 --- a/lib/features/connection-preview/ConnectionPreview.js +++ b/lib/features/connection-preview/ConnectionPreview.js @@ -34,7 +34,7 @@ import { * @typedef {import('../../core/GraphicsFactory').default} GraphicsFactory */ -var MARKER_CONNECTION_PREVIEW = 'djs-connection-preview'; +var MARKER_CONNECTION_PREVIEW = 'djs-dragger'; /** * Draws connection preview. Optionally, this can use layouter and connection docking to draw @@ -150,7 +150,9 @@ ConnectionPreview.prototype.drawPreview = function(context, canConnect, hints) { connection.waypoints = this._connectionDocking.getCroppedWaypoints(connection, source, target); } - this._graphicsFactory.drawConnection(connectionPreviewGfx, connection); + this._graphicsFactory.drawConnection(connectionPreviewGfx, connection, { + stroke: 'var(--element-dragger-color)' + }); }; /** diff --git a/lib/features/context-pad/ContextPad.js b/lib/features/context-pad/ContextPad.js index a86d9074a..18ee112eb 100644 --- a/lib/features/context-pad/ContextPad.js +++ b/lib/features/context-pad/ContextPad.js @@ -55,6 +55,7 @@ var entrySelector = '.entry'; var DEFAULT_PRIORITY = 1000; var CONTEXT_PAD_PADDING = 12; +var HOVER_DELAY = 300; /** * A context pad that displays element specific, contextual actions next @@ -229,11 +230,25 @@ ContextPad.prototype.trigger = function(action, event, autoActivate) { entry = domAttr(button, 'data-action'); originalEvent = event.originalEvent || event; + if (action === 'mouseover') { + this._timeout = setTimeout(() => { + this._mouseout = this.triggerEntry(entry, 'hover', originalEvent, autoActivate); + }, HOVER_DELAY); + } else if (action === 'mouseout') { + clearTimeout(this._timeout); + + if (this._mouseout) { + this._mouseout(); + + this._mouseout = null; + } + } + return this.triggerEntry(entry, action, originalEvent, autoActivate); }; /** - * Trigger context pad entry entry. + * Trigger action on context pad entry entry, e.g. click, mouseover or mouseout. * * @param {string} entryId * @param {string} action @@ -391,6 +406,14 @@ ContextPad.prototype.getPad = function(target) { self.trigger('dragstart', event); }); + domDelegate.bind(html, entrySelector, 'mouseover', function(event) { + self.trigger('mouseover', event); + }); + + domDelegate.bind(html, entrySelector, 'mouseout', function(event) { + self.trigger('mouseout', event); + }); + // stop propagation of mouse events domEvent.bind(html, 'mousedown', function(event) { event.stopPropagation(); diff --git a/lib/features/copy-paste/CopyPaste.js b/lib/features/copy-paste/CopyPaste.js index d6cca387d..caaab18d2 100644 --- a/lib/features/copy-paste/CopyPaste.js +++ b/lib/features/copy-paste/CopyPaste.js @@ -211,7 +211,7 @@ CopyPaste.prototype.copy = function(elements) { * * @param {Object} [context] * @param {Shape} [context.element] The optional parent. - * @param {Point} [context.point] The optional osition. + * @param {Point} [context.point] The optional position. * @param {Object} [context.hints] The optional hints. */ CopyPaste.prototype.paste = function(context) { diff --git a/lib/features/preview-support/PreviewSupport.js b/lib/features/preview-support/PreviewSupport.js index 6f7477a8e..889e4c64e 100644 --- a/lib/features/preview-support/PreviewSupport.js +++ b/lib/features/preview-support/PreviewSupport.js @@ -61,11 +61,7 @@ export default function PreviewSupport(elementRegistry, eventBus, canvas, styles var self = this; eventBus.on('drag.cleanup', function() { - forEach(self._clonedMarkers, function(clonedMarker) { - svgRemove(clonedMarker); - }); - - self._clonedMarkers = {}; + self.cleanUp(); }); } @@ -76,6 +72,15 @@ PreviewSupport.$inject = [ 'styles' ]; +PreviewSupport.prototype.cleanUp = function() { + var self = this; + + forEach(self._clonedMarkers, function(clonedMarker) { + svgRemove(clonedMarker); + }); + + self._clonedMarkers = {}; +}; /** * Returns graphics of an element. @@ -94,24 +99,27 @@ PreviewSupport.prototype.getGfx = function(element) { * @param {Element} element The element to be moved. * @param {SVGElement} group The SVG group to add the preview to. * @param {SVGElement} [gfx] The optional graphical element of the element. + * @param {string} [className="djs-dragger"] The optional class name to add to the preview. * * @return {SVGElement} The preview. */ -PreviewSupport.prototype.addDragger = function(element, group, gfx) { +PreviewSupport.prototype.addDragger = function(element, group, gfx, className = 'djs-dragger') { gfx = gfx || this.getGfx(element); var dragger = svgClone(gfx); var bbox = gfx.getBoundingClientRect(); - this._cloneMarkers(getVisual(dragger)); + this._cloneMarkers(getVisual(dragger), className); - svgAttr(dragger, this._styles.cls('djs-dragger', [], { + svgAttr(dragger, this._styles.cls(className, [], { x: bbox.top, y: bbox.left })); svgAppend(group, dragger); + svgAttr(dragger, 'data-preview-support-element-id', element.id); + return dragger; }; @@ -135,6 +143,8 @@ PreviewSupport.prototype.addFrame = function(shape, group) { svgAppend(group, frame); + svgAttr(frame, 'data-preview-support-element-id', shape.id); + return frame; }; @@ -142,8 +152,9 @@ PreviewSupport.prototype.addFrame = function(shape, group) { * Clone all markers referenced by a node and its child nodes. * * @param {SVGElement} gfx + * @param {string} [className="djs-dragger"] */ -PreviewSupport.prototype._cloneMarkers = function(gfx) { +PreviewSupport.prototype._cloneMarkers = function(gfx, className = 'djs-dragger') { var self = this; if (gfx.childNodes) { @@ -152,7 +163,7 @@ PreviewSupport.prototype._cloneMarkers = function(gfx) { for (var i = 0; i < gfx.childNodes.length; i++) { // recursively clone markers of child nodes - self._cloneMarkers(gfx.childNodes[ i ]); + self._cloneMarkers(gfx.childNodes[ i ], className); } } @@ -164,7 +175,7 @@ PreviewSupport.prototype._cloneMarkers = function(gfx) { if (svgAttr(gfx, markerType)) { var marker = getMarker(gfx, markerType, self._canvas.getContainer()); - self._cloneMarker(gfx, marker, markerType); + self._cloneMarker(gfx, marker, markerType, className); } }); }; @@ -175,9 +186,10 @@ PreviewSupport.prototype._cloneMarkers = function(gfx) { * @param {SVGElement} gfx * @param {SVGElement} marker * @param {string} markerType + * @param {string} [className="djs-dragger"] */ -PreviewSupport.prototype._cloneMarker = function(gfx, marker, markerType) { - var markerId = marker.id; +PreviewSupport.prototype._cloneMarker = function(gfx, marker, markerType, className = 'djs-dragger') { + var markerId = marker.id + '-' + className; var clonedMarker = this._clonedMarkers[ markerId ]; @@ -188,9 +200,7 @@ PreviewSupport.prototype._cloneMarker = function(gfx, marker, markerType) { clonedMarker.id = clonedMarkerId; - svgClasses(clonedMarker) - .add('djs-dragger') - .add('djs-dragger-marker'); + svgClasses(clonedMarker).add(className); this._clonedMarkers[ markerId ] = clonedMarker; diff --git a/test/spec/connection-preview/ConnectionPreviewSpec.js b/test/spec/connection-preview/ConnectionPreviewSpec.js index 70e04eda6..76b6b3b29 100755 --- a/test/spec/connection-preview/ConnectionPreviewSpec.js +++ b/test/spec/connection-preview/ConnectionPreviewSpec.js @@ -90,7 +90,7 @@ describe('features/connection-preview', function() { // when connectionPreview.drawPreview(context, true, hints); - var preview = domQuery('.djs-connection-preview', testContainer); + var preview = domQuery('.djs-dragger', testContainer); // then expect(preview).to.exist; @@ -110,7 +110,7 @@ describe('features/connection-preview', function() { // when connectionPreview.drawPreview(context, true, hints); - var preview = domQuery('.djs-connection-preview', testContainer); + var preview = domQuery('.djs-dragger', testContainer); // then expect(preview).to.exist; @@ -131,7 +131,7 @@ describe('features/connection-preview', function() { connectionPreview.drawPreview(context, true, hints); connectionPreview.cleanUp(context); - var preview = domQuery('.djs-connection-preview', testContainer); + var preview = domQuery('.djs-dragger', testContainer); // then expect(preview).not.to.exist; @@ -154,7 +154,7 @@ describe('features/connection-preview', function() { // when connectionPreview.drawPreview(context, false, hints); - var preview = domQuery('.djs-connection-preview', testContainer); + var preview = domQuery('.djs-dragger', testContainer); // then expect(preview).to.exist; @@ -175,7 +175,7 @@ describe('features/connection-preview', function() { // when connectionPreview.drawPreview(context, false, hints); - var preview = domQuery('.djs-connection-preview', testContainer); + var preview = domQuery('.djs-dragger', testContainer); // then expect(preview).to.exist; diff --git a/test/spec/core/GraphicsFactorySpec.js b/test/spec/core/GraphicsFactorySpec.js index b298d552b..7e39e931f 100644 --- a/test/spec/core/GraphicsFactorySpec.js +++ b/test/spec/core/GraphicsFactorySpec.js @@ -47,4 +47,68 @@ describe('GraphicsFactory', function() { expect(svgClasses(gfx).has('djs-frame')).to.equal(true); })); + + it('should propagate additional attributes when drawing shape', inject( + function(canvas, elementFactory, eventBus, graphicsFactory) { + + // given + var root = canvas.getRootElement(); + + var element = elementFactory.createShape({ + id: 'shape', + parent: root + }); + + var gfx = graphicsFactory.create('shape', element); + + var spy = sinon.spy(); + + eventBus.on('render.shape', spy); + + // when + graphicsFactory.drawShape(gfx, element, { foo: 'bar' }); + + // then + expect(spy).to.have.been.calledWith(sinon.match({ + attrs: { foo: 'bar' } + })); + })); + + + it('should propagate additional attributes when drawing connection', inject( + function(canvas, elementFactory, eventBus, graphicsFactory) { + + // given + var root = canvas.getRootElement(); + + var element = elementFactory.createConnection({ + id: 'connection', + parent: root, + waypoints: [ + { + x: 0, + y: 0 + }, + { + x: 0, + y: 100 + } + ] + }); + + var gfx = graphicsFactory.create('connection', element); + + var spy = sinon.spy(); + + eventBus.on('render.connection', spy); + + // when + graphicsFactory.drawConnection(gfx, element, { foo: 'bar' }); + + // then + expect(spy).to.have.been.calledWith(sinon.match({ + attrs: { foo: 'bar' } + })); + })); + }); diff --git a/test/spec/features/complex-preview/ComplexPreviewSpec.js b/test/spec/features/complex-preview/ComplexPreviewSpec.js new file mode 100644 index 000000000..e509dff0c --- /dev/null +++ b/test/spec/features/complex-preview/ComplexPreviewSpec.js @@ -0,0 +1,249 @@ +import { + bootstrapDiagram, + inject +} from 'test/TestHelper'; + +import complexPreviewModule from 'lib/features/complex-preview'; +import modelingModule from 'lib/features/modeling'; +import rendererModule from '../preview-support/renderer'; + +import { + query as domQuery, + queryAll as domQueryAll +} from 'min-dom'; + +var testModules = [ + complexPreviewModule, + modelingModule, + rendererModule +]; + + +describe('features/complex-preview', function() { + + var root, + shape1, + shape2, + connection, + newShape, + newConnection; + + 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); + + connection = elementFactory.createConnection({ + id: 'connection', + source: shape1, + target: shape2, + waypoints: [ + { x: 100, y: 50 }, + { x: 200, y: 50 } + ], + marker: { + start: true, + end: true + } + }); + + canvas.addConnection(connection, root); + + newShape = elementFactory.createShape({ + id: 'newShape', + x: 400, + y: 0, + width: 100, + height: 100 + }); + + newConnection = elementFactory.createConnection({ + id: 'newConnection', + source: shape2, + target: newShape, + waypoints: [ + { x: 300, y: 50 }, + { x: 400, y: 50 } + ], + marker: { + start: true, + end: true + } + }); + } + + + beforeEach(bootstrapDiagram({ + modules: testModules + })); + + beforeEach(inject(setupDiagram)); + + + it('should create preview for created shapes and connections', inject(function(canvas, complexPreview) { + + // when + complexPreview.create({ + created: [ + newConnection, + newShape + ] + }); + + // then + const layer = canvas.getLayer('complex-preview'); + + expect(layer).to.exist; + + expect(queryPreview('newConnection', layer)).to.have.length(1); + expect(queryPreview('newShape', layer)).to.have.length(1); + })); + + + it('should create preview for moved shapes and connections', inject(function(canvas, complexPreview) { + + // when + complexPreview.create({ + moved: [ + { + element: shape1, + delta: { x: 100, y: 100 } + }, + { + element: shape2, + delta: { x: 100, y: 100 } + }, + { + element: connection, + delta: { x: 100, y: 100 } + } + ] + }); + + // then + const layer = canvas.getLayer('complex-preview'); + + expect(layer).to.exist; + + expect(queryPreview('connection', layer)).to.have.length(2); + expect(queryPreview('shape1', layer)).to.have.length(2); + expect(queryPreview('shape2', layer)).to.have.length(2); + })); + + + it('should create preview for removed shapes and connections', inject(function(canvas, complexPreview) { + + // when + complexPreview.create({ + removed: [ + shape2, + connection + ] + }); + + // then + const layer = canvas.getLayer('complex-preview'); + + expect(layer).to.exist; + + expect(queryPreview('connection', layer)).to.have.length(1); + expect(queryPreview('shape2', layer)).to.have.length(1); + })); + + + it('should create preview for resized shapes', inject(function(canvas, complexPreview) { + + // when + complexPreview.create({ + resized: [ + { + shape: shape2, + bounds: { x: 200, y: 0, width: 200, height: 200 } + } + ] + }); + + // then + const layer = canvas.getLayer('complex-preview'); + + expect(layer).to.exist; + + expect(queryPreview('shape2', layer)).to.have.length(2); + })); + + + it('should clone markers', inject(function(canvas, complexPreview) { + + // when + complexPreview.create({ + moved: [ + { + element: shape1, + delta: { x: 100, y: 100 } + }, + { + element: shape2, + delta: { x: 100, y: 100 } + }, + { + element: connection, + delta: { x: 100, y: 100 } + } + ] + }); + + // then + expect(domQueryAll('marker.djs-dragging', canvas.getContainer())).to.have.length(2); + + expect(domQuery('marker#marker-start-djs-dragging-clone', canvas.getContainer())).to.exist; + expect(domQuery('marker#marker-end-djs-dragging-clone', canvas.getContainer())).to.exist; + })); + + + it('should clean up preview', inject(function(canvas, complexPreview) { + + // given + complexPreview.create({ + created: [ + newShape + ] + }); + + // 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-preview-support-element-id="' + id + '"]', layer); +} \ No newline at end of file diff --git a/test/spec/features/context-pad/ContextPadProvider.js b/test/spec/features/context-pad/ContextPadProvider.js index e1d1a64f7..1a407f9f7 100755 --- a/test/spec/features/context-pad/ContextPadProvider.js +++ b/test/spec/features/context-pad/ContextPadProvider.js @@ -42,6 +42,19 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { } } }; + } else if (element.type === 'hover') { + return { + 'action.hover': { + className: 'hover', + action: { + hover: function(e) { + e.__handled = true; + + return 'action.hover'; + } + } + } + }; } else if (element.type === 'bigImage') { return { 'action.c': { diff --git a/test/spec/features/context-pad/ContextPadSpec.js b/test/spec/features/context-pad/ContextPadSpec.js index 9fbea91be..5ce23f9c6 100755 --- a/test/spec/features/context-pad/ContextPadSpec.js +++ b/test/spec/features/context-pad/ContextPadSpec.js @@ -776,6 +776,16 @@ describe('features/context-pad', function() { beforeEach(bootstrapDiagram({ modules: [ contextPadModule, providerModule ] })); + var clock; + + beforeEach(function() { + clock = sinon.useFakeTimers(); + }); + + afterEach(function() { + clock.restore(); + }); + it('should handle click event', inject(function(canvas, contextPad) { @@ -798,6 +808,36 @@ describe('features/context-pad', function() { })); + it('should handle hover event', inject(function(canvas, contextPad) { + + // given + var shape = canvas.addShape({ + id: 's1', + width: 100, height: 100, + x: 10, y: 10, + type: 'hover' + }); + + contextPad.open(shape); + + var pad = contextPad.getPad(shape), + html = pad.html, + target = domQuery('[data-action="action.hover"]', html); + + var event = globalEvent(target, { x: 0, y: 0 }); + + // when + contextPad.trigger('mouseover', event); + + expect(event.__handled).not.to.exist; + + clock.tick(500); + + // then + expect(event.__handled).to.be.true; + })); + + it('should prevent unhandled events', inject(function(canvas, contextPad) { // given @@ -1274,4 +1314,5 @@ describe('features/context-pad', function() { expect(injected).not.to.exist; })); }); + }); diff --git a/test/spec/features/move/MovePreviewSpec.js b/test/spec/features/move/MovePreviewSpec.js index 472b4c67c..d924b0860 100644 --- a/test/spec/features/move/MovePreviewSpec.js +++ b/test/spec/features/move/MovePreviewSpec.js @@ -11,7 +11,7 @@ import modelingModule from 'lib/features/modeling'; import moveModule from 'lib/features/move'; import attachSupportModule from 'lib/features/attach-support'; import rulesModule from './rules'; -import rendererModule from './renderer'; +import rendererModule from '../preview-support/renderer'; import { query as domQuery, @@ -584,13 +584,13 @@ describe('features/move - MovePreview', function() { // then var container = canvas.getContainer(); - var clonedMarkers = domQueryAll('marker.djs-dragger-marker', container); + var clonedMarkers = domQueryAll('marker.djs-dragger', container); expect(clonedMarkers).to.have.length(3); - var markerStartClone = domQuery('marker#marker-start-clone', container), - markerMidClone = domQuery('marker#marker-mid-clone', container), - markerEndClone = domQuery('marker#marker-end-clone', container); + var markerStartClone = domQuery('marker#marker-start-djs-dragger-clone', container), + markerMidClone = domQuery('marker#marker-mid-djs-dragger-clone', container), + markerEndClone = domQuery('marker#marker-end-djs-dragger-clone', container); expect(markerStartClone).to.exist; expect(markerMidClone).to.exist; diff --git a/test/spec/features/move/renderer/MarkerRenderer.js b/test/spec/features/preview-support/renderer/MarkerRenderer.js similarity index 94% rename from test/spec/features/move/renderer/MarkerRenderer.js rename to test/spec/features/preview-support/renderer/MarkerRenderer.js index 8f45816a1..95b6d3bf2 100644 --- a/test/spec/features/move/renderer/MarkerRenderer.js +++ b/test/spec/features/preview-support/renderer/MarkerRenderer.js @@ -1,141 +1,141 @@ -import inherits from 'inherits-browser'; - -import { - append as svgAppend, - attr as svgAttr, - create as svgCreate -} from 'tiny-svg'; - -import { - query as domQuery -} from 'min-dom'; - -import DefaultRenderer from 'lib/draw/DefaultRenderer'; - -import { - createLine -} from 'lib//util/RenderUtil'; - -/** - * @typedef {import('../../model').Connection} Connection - */ - -var HIGH_PRIORITY = 3000; - -var CONNECTION_STYLE = { - fill: 'none', - stroke: 'fuchsia', - strokeWidth: 5 -}; - -var MARKER_TYPES = [ - 'marker-start', - 'marker-mid', - 'marker-end' -]; - -/** - * A renderer that can render markers. - */ -export default function MarkerRenderer(canvas, eventBus, styles) { - DefaultRenderer.call(this, eventBus, styles, HIGH_PRIORITY); - - this._canvas = canvas; - - this._markers = {}; -} - -inherits(MarkerRenderer, DefaultRenderer); - -MarkerRenderer.$inject = [ - 'canvas', - 'eventBus', - 'styles' -]; - -MarkerRenderer.prototype.canRender = function() { - return true; -}; - -MarkerRenderer.prototype.drawConnection = function(parentGfx, connection) { - var line = createLine(connection.waypoints, CONNECTION_STYLE); - - svgAppend(parentGfx, line); - - var self = this; - - MARKER_TYPES.forEach(function(markerType) { - if (hasMarker(connection, markerType)) { - self.addMarker(line, markerType); - } - }); - - return line; -}; - -MarkerRenderer.prototype.addMarker = function(gfx, markerType) { - var marker = this._markers[ markerType ], - defs; - - if (!marker) { - marker = this._markers[ markerType ] = svgCreate('marker'); - - marker.id = markerType; - - svgAttr(marker, { - refX: 5, - refY: 5, - viewBox: '0 0 10 10' - }); - - var circle = svgCreate('circle'); - - svgAttr(circle, { - cx: 5, - cy: 5, - fill: 'fuchsia', - r: 5 - }); - - svgAppend(marker, circle); - - defs = domQuery('defs', this._canvas._svg); - - if (!defs) { - defs = svgCreate('defs'); - - svgAppend(this._canvas._svg, defs); - } - - svgAppend(defs, marker); - } - - var reference = idToReference(marker.id); - - svgAttr(gfx, markerType, reference); -}; - -// helpers ////////// - -/** - * Get functional IRI reference for given ID of fragment within current document. - * - * @param {string} id - * - * @return {string} - */ -function idToReference(id) { - return 'url(#' + id + ')'; -} - -/** - * Check wether given connection has marker of given type. - * - * @param {Connection} connection - * @param {string} markerType - * - * @return {boolean} - */ -function hasMarker(connection, markerType) { - return connection.marker && connection.marker[ markerType.split('-').pop() ]; +import inherits from 'inherits-browser'; + +import { + append as svgAppend, + attr as svgAttr, + create as svgCreate +} from 'tiny-svg'; + +import { + query as domQuery +} from 'min-dom'; + +import DefaultRenderer from 'lib/draw/DefaultRenderer'; + +import { + createLine +} from 'lib//util/RenderUtil'; + +/** + * @typedef {import('../../model').Connection} Connection + */ + +var HIGH_PRIORITY = 3000; + +var CONNECTION_STYLE = { + fill: 'none', + stroke: 'fuchsia', + strokeWidth: 5 +}; + +var MARKER_TYPES = [ + 'marker-start', + 'marker-mid', + 'marker-end' +]; + +/** + * A renderer that can render markers. + */ +export default function MarkerRenderer(canvas, eventBus, styles) { + DefaultRenderer.call(this, eventBus, styles, HIGH_PRIORITY); + + this._canvas = canvas; + + this._markers = {}; +} + +inherits(MarkerRenderer, DefaultRenderer); + +MarkerRenderer.$inject = [ + 'canvas', + 'eventBus', + 'styles' +]; + +MarkerRenderer.prototype.canRender = function() { + return true; +}; + +MarkerRenderer.prototype.drawConnection = function(parentGfx, connection) { + var line = createLine(connection.waypoints, CONNECTION_STYLE); + + svgAppend(parentGfx, line); + + var self = this; + + MARKER_TYPES.forEach(function(markerType) { + if (hasMarker(connection, markerType)) { + self.addMarker(line, markerType); + } + }); + + return line; +}; + +MarkerRenderer.prototype.addMarker = function(gfx, markerType) { + var marker = this._markers[ markerType ], + defs; + + if (!marker) { + marker = this._markers[ markerType ] = svgCreate('marker'); + + marker.id = markerType; + + svgAttr(marker, { + refX: 5, + refY: 5, + viewBox: '0 0 10 10' + }); + + var circle = svgCreate('circle'); + + svgAttr(circle, { + cx: 5, + cy: 5, + fill: 'fuchsia', + r: 5 + }); + + svgAppend(marker, circle); + + defs = domQuery('defs', this._canvas._svg); + + if (!defs) { + defs = svgCreate('defs'); + + svgAppend(this._canvas._svg, defs); + } + + svgAppend(defs, marker); + } + + var reference = idToReference(marker.id); + + svgAttr(gfx, markerType, reference); +}; + +// helpers ////////// + +/** + * Get functional IRI reference for given ID of fragment within current document. + * + * @param {string} id + * + * @return {string} + */ +function idToReference(id) { + return 'url(#' + id + ')'; +} + +/** + * Check wether given connection has marker of given type. + * + * @param {Connection} connection + * @param {string} markerType + * + * @return {boolean} + */ +function hasMarker(connection, markerType) { + return connection.marker && connection.marker[ markerType.split('-').pop() ]; } \ No newline at end of file diff --git a/test/spec/features/move/renderer/index.js b/test/spec/features/preview-support/renderer/index.js similarity index 96% rename from test/spec/features/move/renderer/index.js rename to test/spec/features/preview-support/renderer/index.js index 79c5a6842..80b3cb1d8 100644 --- a/test/spec/features/move/renderer/index.js +++ b/test/spec/features/preview-support/renderer/index.js @@ -1,6 +1,6 @@ -import MarkerRenderer from './MarkerRenderer'; - -export default { - __init__: [ 'defaultRenderer' ], - defaultRenderer: [ 'type', MarkerRenderer ] +import MarkerRenderer from './MarkerRenderer'; + +export default { + __init__: [ 'defaultRenderer' ], + defaultRenderer: [ 'type', MarkerRenderer ] }; \ No newline at end of file