diff --git a/lib/features/create/CreateConnectPreview.js b/lib/features/create/CreateConnectPreview.js new file mode 100644 index 000000000..8f2e1d141 --- /dev/null +++ b/lib/features/create/CreateConnectPreview.js @@ -0,0 +1,152 @@ +import { + append as svgAppend, + attr as svgAttr, + classes as svgClasses, + create as svgCreate, + remove as svgRemove, + clear as svgClear +} from 'tiny-svg'; + + +var LOW_PRIORITY = 740; + +/** + * Shows a connection preview during element creation. + * + * @param {didi.Injector} injector + * @param {EventBus} eventBus + * @param {Canvas} canvas + * @param {GraphicsFactory} graphicsFactory + * @param {ElementFactory} elementFactory + */ +export default function CreateConnectPreview(injector, eventBus, canvas, graphicsFactory, elementFactory) { + + // optional components + + var connectionDocking = injector.get('connectionDocking', false); + var layouter = injector.get('layouter', false); + + + // visual helpers + + function createConnectVisual() { + + var visual = svgCreate('g'); + svgAttr(visual, { + 'pointer-events': 'none' + }); + + svgClasses(visual).add('djs-dragger'); + + svgAppend(canvas.getDefaultLayer(), visual); + + return visual; + } + + // move integration + + eventBus.on('create.move', LOW_PRIORITY, function(event) { + + var context = event.context, + source = context.source, + shape = context.shape, + connectVisual = context.connectVisual, + canExecute = context.canExecute, + connect = canExecute && canExecute.connect, + getConnection = context.getConnection, + connection; + + + if (!getConnection) { + getConnection = context.getConnection = Cacher(function(attrs) { + return elementFactory.create('connection', attrs); + }); + } + + if (!connectVisual) { + connectVisual = context.connectVisual = createConnectVisual(); + } + + svgClear(connectVisual); + + if (!connect) { + return; + } + + connection = getConnection(connectionAttrs(connect)); + + // monkey patch shape position for intersection to work + shape.x = Math.round(event.x - shape.width / 2); + shape.y = Math.round(event.y - shape.height / 2); + + if (layouter) { + connection.waypoints = []; + + connection.waypoints = layouter.layoutConnection(connection, { + source: source, + target: shape + }); + } else { + connection.waypoints = [ + { x: source.x + source.width / 2, y: source.y + source.height / 2 }, + { x: event.x, y: event.y } + ]; + } + + if (connectionDocking) { + connection.waypoints = connectionDocking.getCroppedWaypoints(connection, source, shape); + } + + graphicsFactory.drawConnection(connectVisual, connection); + }); + + + eventBus.on('create.cleanup', function(event) { + var context = event.context; + + if (context.connectVisual) { + svgRemove(context.connectVisual); + } + }); + +} + +CreateConnectPreview.$inject = [ + 'injector', + 'eventBus', + 'canvas', + 'graphicsFactory', + 'elementFactory' +]; + + + +// helpers ////////////// + +function connectionAttrs(connect) { + + if (typeof connect === 'boolean') { + return {}; + } else { + return connect; + } +} + + +function Cacher(createFn) { + + var entries = {}; + + return function(attrs) { + + var key = JSON.stringify(attrs); + + var e = entries[key]; + + if (!e) { + e = entries[key] = createFn(attrs); + } + + return e; + }; +} \ No newline at end of file diff --git a/lib/features/create/index.js b/lib/features/create/index.js index 1cdccef61..ca61c3276 100644 --- a/lib/features/create/index.js +++ b/lib/features/create/index.js @@ -3,6 +3,7 @@ import SelectionModule from '../selection'; import RulesModule from '../rules'; import Create from './Create'; +import CreateConnectPreview from './CreateConnectPreview'; export default { @@ -11,5 +12,9 @@ export default { SelectionModule, RulesModule ], - create: [ 'type', Create ] + __init__: [ + 'createConnectPreview' + ], + create: [ 'type', Create ], + createConnectPreview: [ 'type', CreateConnectPreview ] }; diff --git a/test/spec/features/create/CreateSpec.js b/test/spec/features/create/CreateSpec.js index eca1178d0..f7449570e 100644 --- a/test/spec/features/create/CreateSpec.js +++ b/test/spec/features/create/CreateSpec.js @@ -224,6 +224,97 @@ describe('features/create - Create', function() { }); + describe('display', function() { + + it('should display connection preview', inject(function(create, elementRegistry, dragging) { + + // given + var parentGfx = elementRegistry.getGraphics('parentShape'); + + // when + create.start(canvasEvent({ x: 0, y: 0 }), newShape, childShape); + + dragging.move(canvasEvent({ x: 175, y: 175 })); + dragging.hover({ element: parentShape, gfx: parentGfx }); + dragging.move(canvasEvent({ x: 400, y: 200 })); + + var ctx = dragging.context(); + + // then + expect(ctx.data.context.connectVisual).to.exist; + expect(svgClasses(ctx.data.context.connectVisual).has('djs-dragger')).to.be.true; + })); + + + it('should not display preview if connection is disallowed', + inject(function(create, elementRegistry, dragging, createRules) { + + // given + createRules.addRule('connection.create', 8000, function() { + return false; + }); + + var parentGfx = elementRegistry.getGraphics('parentShape'); + + // when + create.start(canvasEvent({ x: 0, y: 0 }), newShape, childShape); + + dragging.move(canvasEvent({ x: 175, y: 175 })); + dragging.hover({ element: parentShape, gfx: parentGfx }); + dragging.move(canvasEvent({ x: 400, y: 200 })); + + var ctx = dragging.context(); + + // then + expect(ctx.data.context.connectVisual.childNodes).to.be.have.lengthOf(0); + }) + ); + }); + + describe('cleanup', function() { + + it('should remove connection preview on dragging end', inject(function(create, elementRegistry, dragging) { + + // given + var parentGfx = elementRegistry.getGraphics('parentShape'); + + // when + create.start(canvasEvent({ x: 0, y: 0 }), newShape, childShape); + + dragging.move(canvasEvent({ x: 175, y: 175 })); + dragging.hover({ element: parentShape, gfx: parentGfx }); + dragging.move(canvasEvent({ x: 400, y: 200 })); + + var ctx = dragging.context(); + + dragging.end(); + + // then + expect(ctx.data.context.connectVisual.parentNode).not.to.exist; + })); + + + it('should remove connection preview on dragging cancel', inject(function(create, elementRegistry, dragging) { + + // given + var parentGfx = elementRegistry.getGraphics('parentShape'); + + // when + create.start(canvasEvent({ x: 0, y: 0 }), newShape, childShape); + + dragging.move(canvasEvent({ x: 175, y: 175 })); + dragging.hover({ element: parentShape, gfx: parentGfx }); + dragging.move(canvasEvent({ x: 400, y: 200 })); + + var ctx = dragging.context(); + + dragging.cancel(); + + // then + expect(ctx.data.context.connectVisual.parentNode).not.to.exist; + })); + }); + describe('rules', function() {