diff --git a/html/admin/js/graphViewer/graph/edgeShaper.js b/html/admin/js/graphViewer/graph/edgeShaper.js index 53daeede91..1bd0a48fd6 100644 --- a/html/admin/js/graphViewer/graph/edgeShaper.js +++ b/html/admin/js/graphViewer/graph/edgeShaper.js @@ -50,7 +50,8 @@ function EdgeShaper(parent, flags, idfunc) { edges = [], toplevelSVG, visibleLabels = true, - + followEdge = {}, + followEdgeG, idFunction = function(d) { return d.source._id + "-" + d.target._id; }, @@ -319,6 +320,8 @@ function EdgeShaper(parent, flags, idfunc) { idFunction = idfunc; } + followEdgeG = toplevelSVG.append("g"); + ///////////////////////////////////////////////////////// /// Public functions @@ -351,6 +354,24 @@ function EdgeShaper(parent, flags, idfunc) { } shapeEdges(); }; + + self.addAnEdgeFollowingTheCursor = function(x, y) { + followEdge = followEdgeG.append("line"); + followEdge.attr("stroke", "black") + .attr("id", "connectionLine") + .attr("x1", x) + .attr("y1", y) + .attr("x2", x) + .attr("y2", y); + return function(x, y) { + followEdge.attr("x2", x).attr("y2", y); + }; + }; + + self.removeCursorFollowingEdge = function() { + followEdge.remove(); + followEdge = {}; + }; } EdgeShaper.shapes = Object.freeze({ diff --git a/html/admin/js/graphViewer/graph/eventDispatcher.js b/html/admin/js/graphViewer/graph/eventDispatcher.js index e011efc277..818320008d 100644 --- a/html/admin/js/graphViewer/graph/eventDispatcher.js +++ b/html/admin/js/graphViewer/graph/eventDispatcher.js @@ -1,5 +1,5 @@ /*jslint indent: 2, nomen: true, maxlen: 100, sloppy: true, vars: true, white: true, plusplus: true */ -/*global _, $*/ +/*global _, $, window*/ /*global EventLibrary*/ //////////////////////////////////////////////////////////////////////////////// /// @brief Graph functionality @@ -80,11 +80,14 @@ function EventDispatcher(nodeShaper, edgeShaper, config) { self.events.STARTCREATEEDGE = function(callback) { return function(node) { + var e = d3.event || window.event; edgeStart = node; didInsert = false; if (callback !== undefined) { - callback(); + callback(node, e); } + // Necessary to omit dragging of the graph + e.stopPropagation(); }; }; diff --git a/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperSpec.js b/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperSpec.js index e058f8e6be..2b01b81aec 100644 --- a/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperSpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specEdgeShaper/edgeShaperSpec.js @@ -42,6 +42,7 @@ beforeEach(function () { svg = document.createElement("svg"); + svg.id = "svg"; document.body.appendChild(svg); }); @@ -274,6 +275,55 @@ expect($("#1-5 line").attr("x2")).toEqual("28.284271247461902"); }); + it('should be able to draw an edge that follows the cursor', function() { + var line, + jqLine, + cursorX, + cursorY, + nodeX = 15, + nodeY = 20, + shaper = new EdgeShaper(d3.select("svg")), + moveCB = shaper.addAnEdgeFollowingTheCursor(nodeX, nodeY); + + cursorX = 20; + cursorY = 30; + moveCB(cursorX, cursorY); + + expect($("#connectionLine").length).toEqual(1); + + jqLine = $("#connectionLine"); + line = document.getElementById("connectionLine"); + expect(line.tagName.toLowerCase()).toEqual("line"); + expect(jqLine.attr("x1")).toEqual(String(nodeX)); + expect(jqLine.attr("y1")).toEqual(String(nodeY)); + + expect(jqLine.attr("x2")).toEqual(String(cursorX)); + expect(jqLine.attr("y2")).toEqual(String(cursorY)); + + cursorX = 45; + cursorY = 12; + moveCB(cursorX, cursorY); + expect(jqLine.attr("x2")).toEqual(String(cursorX)); + expect(jqLine.attr("y2")).toEqual(String(cursorY)); + }); + + it('should be able to remove the cursor-following edge on demand', function() { + var line, + cursorX, + cursorY, + nodeX = 15, + nodeY = 20, + shaper = new EdgeShaper(d3.select("svg")), + moveCB; + + moveCB = shaper.addAnEdgeFollowingTheCursor(nodeX, nodeY); + cursorX = 20; + cursorY = 30; + moveCB(cursorX, cursorY); + shaper.removeCursorFollowingEdge(); + expect($("#connectionLine").length).toEqual(0); + }); + describe('testing for colours', function() { it('should have a default colouring of no colour flag is given', function() { diff --git a/html/admin/js/graphViewer/jasmine_test/specEvents/eventDispatcherUISpec.js b/html/admin/js/graphViewer/jasmine_test/specEvents/eventDispatcherUISpec.js index 20a9e31a6a..f10ad66e0d 100644 --- a/html/admin/js/graphViewer/jasmine_test/specEvents/eventDispatcherUISpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specEvents/eventDispatcherUISpec.js @@ -61,17 +61,21 @@ beforeEach(function () { nodes = [{ _id: 1, - _rev: 1, - _key: 1, + x: 3, + y: 4, _data: { _id: 1, + _rev: 1, + _key: 1, name: "Alice" } },{ _id: 2, - _rev: 2, - _key: 2, + x: 1, + y: 2, _data: { + _rev: 2, + _key: 2, _id: 2 } }]; @@ -122,6 +126,7 @@ }; svg = document.createElement("svg"); + svg.id = "svg"; document.body.appendChild(svg); nodeShaper = new NodeShaper(d3.select("svg")); edgeShaper = new EdgeShaper(d3.select("svg")); @@ -265,7 +270,7 @@ helper.simulateMouseEvent("click", "control_event_node_edit_submit"); expect(adapter.patchNode).toHaveBeenCalledWith( nodes[0], - { _id: "1", + { name: "Bob" }, jasmine.any(Function)); @@ -286,9 +291,6 @@ expect(adapter.patchEdge).toHaveBeenCalledWith( edges[0], { - _id: "12", - _rev: "12", - _key: "12", _from: "1", _to: "2", label: "newLabel" @@ -373,41 +375,95 @@ }); }); - it('should be able to add a connect control to the list', function() { - runs(function() { + describe('the connect control', function() { + + it('should be added to the list', function() { + runs(function() { + dispatcherUI.addControlConnect(); + + expect($("#control_event_list #control_event_connect").length).toEqual(1); + + helper.simulateMouseEvent("click", "control_event_connect"); + + expect(nodeShaper.changeTo).toHaveBeenCalledWith({ + actions: { + reset: true, + mousedown: jasmine.any(Function), + mouseup: jasmine.any(Function) + } + }); + + expect(edgeShaper.changeTo).toHaveBeenCalledWith({ + actions: { + reset: true + } + }); + + expect(mousePointerbox.className).toEqual("mousepointer icon-resize-horizontal"); + + helper.simulateMouseEvent("mousedown", "2"); + + helper.simulateMouseEvent("mouseup", "1"); + + expect(adapter.createEdge).toHaveBeenCalledWith( + {source: nodes[1], target: nodes[0]}, + jasmine.any(Function) + ); + + }); + }); + + it('should draw a line from startNode following the cursor', function() { + var line, + cursorX, + cursorY; + + spyOn(edgeShaper, "addAnEdgeFollowingTheCursor"); + dispatcherUI.addControlConnect(); - - expect($("#control_event_list #control_event_connect").length).toEqual(1); - helper.simulateMouseEvent("click", "control_event_connect"); - - expect(nodeShaper.changeTo).toHaveBeenCalledWith({ - actions: { - reset: true, - mousedown: jasmine.any(Function), - mouseup: jasmine.any(Function) - } - }); - - expect(edgeShaper.changeTo).toHaveBeenCalledWith({ - actions: { - reset: true - } - }); - - expect(mousePointerbox.className).toEqual("mousepointer icon-resize-horizontal"); - helper.simulateMouseEvent("mousedown", "2"); - helper.simulateMouseEvent("mouseup", "1"); - - expect(adapter.createEdge).toHaveBeenCalledWith( - {source: nodes[1], target: nodes[0]}, - jasmine.any(Function) + expect(edgeShaper.addAnEdgeFollowingTheCursor).toHaveBeenCalledWith( + 0, 0 ); + }); + + it('the cursor-line should follow the cursor on mousemove over svg', function() { + dispatcherUI.addControlConnect(); + helper.simulateMouseEvent("click", "control_event_connect"); + helper.simulateMouseEvent("mousedown", "2"); - }); + helper.simulateMouseMoveEvent("svg", 40, 50); + + var line = $("#connectionLine"); + expect(line.attr("x1")).toEqual(String(nodes[1].x)); + expect(line.attr("y1")).toEqual(String(nodes[1].y)); + expect(line.attr("x2")).toEqual("40"); + expect(line.attr("y2")).toEqual("50"); + }); + + it('the cursor-line should disappear on mouseup on svg', function() { + spyOn(edgeShaper, "removeCursorFollowingEdge"); + dispatcherUI.addControlConnect(); + helper.simulateMouseEvent("click", "control_event_connect"); + helper.simulateMouseEvent("mousedown", "2"); + helper.simulateMouseEvent("mouseup", "1-2"); + expect(edgeShaper.removeCursorFollowingEdge).toHaveBeenCalled(); + }); + + it('the cursor-line should disappear on mouseup on svg', function() { + spyOn(edgeShaper, "removeCursorFollowingEdge"); + dispatcherUI.addControlConnect(); + helper.simulateMouseEvent("click", "control_event_connect"); + helper.simulateMouseEvent("mousedown", "2"); + helper.simulateMouseEvent("mouseup", "1"); + expect(edgeShaper.removeCursorFollowingEdge).toHaveBeenCalled(); + }); + }); + + it('should be able to add all controls to the list', function () { dispatcherUI.addAll(); diff --git a/html/admin/js/graphViewer/ui/eventDispatcherControls.js b/html/admin/js/graphViewer/ui/eventDispatcherControls.js index 45e43ee77c..a272432ed8 100644 --- a/html/admin/js/graphViewer/ui/eventDispatcherControls.js +++ b/html/admin/js/graphViewer/ui/eventDispatcherControls.js @@ -111,6 +111,13 @@ function EventDispatcherControls(list, cursorIconBox, nodeShaper, edgeShaper, di return res; }, + getCursorPositionInSVG = function (ev) { + var pos = getCursorPosition(ev); + pos.x -= $('svg').offset().left; + pos.y -= $('svg').offset().top; + return pos; + }, + moveCursorBox = function(ev) { var pos = getCursorPosition(ev); pos.x += 7; @@ -217,14 +224,25 @@ function EventDispatcherControls(list, cursorIconBox, nodeShaper, edgeShaper, di callback = function() { setCursorIcon(icon); rebindNodes({ - mousedown: dispatcher.events.STARTCREATEEDGE(), + mousedown: dispatcher.events.STARTCREATEEDGE(function(startNode, ev) { + var pos = getCursorPositionInSVG(ev); + var moveCB = edgeShaper.addAnEdgeFollowingTheCursor(pos.x, pos.y); + dispatcher.bind("svg", "mousemove", function(ev) { + var pos = getCursorPositionInSVG(ev); + moveCB(pos.x, pos.y); + }); + }), mouseup: dispatcher.events.FINISHCREATEEDGE(function(edge){ + edgeShaper.removeCursorFollowingEdge(); + dispatcher.bind("svg", "mousemove", function(){}); }) }); rebindEdges(); rebindSVG({ - mouseup: dispatcher.events.CANCELCREATEEDGE(), - mouseout: dispatcher.events.CANCELCREATEEDGE() + mouseup: function() { + dispatcher.events.CANCELCREATEEDGE(); + edgeShaper.removeCursorFollowingEdge(); + } }); }; createIcon(icon, "connect", callback);