diff --git a/app/components/Caption.jsx b/app/components/Caption.jsx index a8b2ad5..4042a61 100644 --- a/app/components/Caption.jsx +++ b/app/components/Caption.jsx @@ -8,7 +8,7 @@ import merge from 'lodash/object/merge'; export default class Caption extends BaseComponent { constructor(props) { super(props); - this.bindAll('_handleDragStart', '_handleDrag', '_handleDragStop', '_handleClick'); + this.bindAll('_handleDragStart', '_handleDrag', '_handleDragStop', '_doDragStart', '_doDrag', '_doDragStop', '_handleClick'); this.state = props.caption.display; } @@ -78,8 +78,19 @@ export default class Caption extends BaseComponent { } } + + // keep initial position for comparison with drag position _handleDragStart(e, ui) { e.preventDefault(); + var isOnlyOne = (this.graph.props.selection["nodeIds"].length + this.graph.props.selection["captionIds"].length + this.graph.props.selection["edgeIds"].length) < 2; + if (!this.graph.props.showEditTools || !this.props.selected || isOnlyOne) { + this._doDragStart(e, ui); + } else { + this.props.onStart(e, ui, this); + } + } + + _doDragStart(e, ui) { this._startDrag = ui.position; this._startPosition = { x: this.state.x, @@ -87,9 +98,19 @@ export default class Caption extends BaseComponent { }; } + // while dragging node and its edges are updated only in state, not store _handleDrag(e, ui) { + if (this.props.isLocked) return; + var isOnlyOne = (this.graph.props.selection["nodeIds"].length + this.graph.props.selection["captionIds"].length + this.graph.props.selection["edgeIds"].length) < 2; + if (!this.graph.props.showEditTools || !this.props.selected || isOnlyOne) { + this._doDrag(e, ui, false); + } else { + this.props.onDrag(e, ui, this); + } + } + _doDrag(e, ui, isMultiple) { this._dragging = true; let deltaX = (ui.position.clientX - this._startDrag.clientX) / this.graph.state.actualZoom; @@ -98,10 +119,29 @@ export default class Caption extends BaseComponent { let y = this._startPosition.y + deltaY; this.setState({ x, y }); + + //update throughout drag so nodes know their siblings' positions when + //multiple nodes are dragged simultaneously + if (isMultiple){ + if (this._dragging) { + this.props.moveCaption(this.props.caption.id, this.state.x, this.state.y); + } + } } + // store updated once dragging is done _handleDragStop(e, ui) { // event fires every mouseup so we check for actual drag before updating store + var isOnlyOne = (this.graph.props.selection["nodeIds"].length + this.graph.props.selection["captionIds"].length + this.graph.props.selection["edgeIds"].length) < 2; + if (!this.graph.props.showEditTools || !this.props.selected || isOnlyOne) { + this._doDragStop(e, ui); + } else { + this.props.onStop(e, ui, this); + } + } + + _doDragStop(e, ui){ + // event fires every mouseup so we check for actual drag before updating store if (this._dragging) { this.props.moveCaption(this.props.caption.id, this.state.x, this.state.y); } diff --git a/app/components/Edge.jsx b/app/components/Edge.jsx index 4b7aecd..8f88e83 100644 --- a/app/components/Edge.jsx +++ b/app/components/Edge.jsx @@ -9,7 +9,7 @@ import classNames from 'classnames'; export default class Edge extends BaseComponent { constructor(props) { super(props); - this.bindAll('_handleDragStart', '_handleDrag', '_handleDragStop', '_handleClick', '_handleTextClick'); + this.bindAll('_handleDragStart', '_handleDrag', '_handleDragStop', '_doDragStart', '_doDrag', '_doDragStop', '_handleClick', '_handleTextClick'); // need control point immediately for dragging let { cx, cy } = this._calculateGeometry(props.edge.display); this.state = merge({}, props.edge.display, { cx, cy }); @@ -85,7 +85,7 @@ export default class Edge extends BaseComponent { JSON.stringify(nextState) !== JSON.stringify(this.state); } - _handleDragStart(event, ui) { + _doDragStart(event, ui) { event.preventDefault(); this._startDrag = ui.position; this._startPosition = { @@ -94,7 +94,7 @@ export default class Edge extends BaseComponent { } } - _handleDrag(event, ui) { + _doDrag(event, ui, isMultiple) { if (this.props.isLocked) return; this._dragging = true; // so that _handleClick knows it's not just a click @@ -106,15 +106,45 @@ export default class Edge extends BaseComponent { let cy = this._startPosition.y + deltaY; this.setState({ cx, cy }); + + if (isMultiple){ + if (this._dragging) { + this.props.moveEdge(this.props.edge.id, this.state.cx, this.state.cy); + } + } } - _handleDragStop(e, ui) { - // event fires every mouseup so we check for actual drag before updating store + _doDragStop(event, ui) { if (this._dragging) { this.props.moveEdge(this.props.edge.id, this.state.cx, this.state.cy); } } + _handleDragStart(event, ui) { + if (!this.graph.props.showEditTools || !this.props.selected) { + this._doDragStart(event, ui, false); + } else { + this.props.onStart(event, ui, this); + } + } + + _handleDrag(event, ui) { + if (!this.graph.props.showEditTools || !this.props.selected) { + this._doDrag(event, ui); + } else { + this.props.onDrag(event, ui, this); + } + } + + _handleDragStop(e, ui) { + // event fires every mouseup so we check for actual drag before updating store + if (!this.graph.props.showEditTools || !this.props.selected) { + this._doDragStop(event, ui); + } else { + this.props.onStop(event, ui, this); + } + } + _handleClick() { if (this._dragging) { this._dragging = false; diff --git a/app/components/Graph.jsx b/app/components/Graph.jsx index b61a4f3..df1af92 100644 --- a/app/components/Graph.jsx +++ b/app/components/Graph.jsx @@ -10,21 +10,24 @@ import values from 'lodash/object/values'; import min from 'lodash/collection/min'; import max from 'lodash/collection/max'; import includes from 'lodash/collection/includes'; +import filter from 'lodash/collection/filter'; + export default class Graph extends BaseComponent { constructor(props) { super(props); - this.bindAll('_handleDragStart', '_handleDrag', '_handleDragStop'); + this.bindAll('_handleDragStart', '_handleDrag', '_handleDragStop', '_handleDragGroupStart', '_handleDragGroup', '_handleDragGroupStop', '_manageDragStage'); this.nodes = {}; this.edges = {}; + this.captions = {}; this.mounted = false; let viewBox = this._computeViewbox(props.graph, props.zoom, props.viewOnlyHighlighted); this.state = { x: 0, y: 0, viewBox, height: props.height }; + } render() { let { x, y, prevGraph, viewBox, height } = this.state; - return ( ); + isLocked={this.props.isLocked} + onStart={this._handleDragGroupStart} + onDrag={this._handleDragGroup} + onStop={this._handleDragGroupStop} />); } _renderNodes() { @@ -83,20 +89,26 @@ export default class Graph extends BaseComponent { selected={this.props.selection && includes(this.props.selection.nodeIds, n.id)} clickNode={this.props.clickNode} moveNode={this.props.moveNode} - isLocked={this.props.isLocked} />); + isLocked={this.props.isLocked} + onStart={this._handleDragGroupStart} + onDrag={this._handleDragGroup} + onStop={this._handleDragGroupStop} />); } _renderCaptions() { return values(this.props.graph.captions).map((c, i) => { if (c) { c.graph = this; } }} + ref={(a) => { this.captions[c.id] = a; if (a) { a.graph = this; }} } key={c.id} caption={c} graphId={this.props.graph.id} selected={this.props.selection && includes(this.props.selection.captionIds, c.id)} moveCaption={this.props.moveCaption} clickCaption={this.props.clickCaption} - isLocked={this.props.isLocked} />); + isLocked={this.props.isLocked} + onStart={this._handleDragGroupStart} + onDrag={this._handleDragGroup} + onStop={this._handleDragGroupStop} />); } _renderMarkers() { @@ -214,6 +226,47 @@ export default class Graph extends BaseComponent { return { x, y }; } + //GROUP DRAGGING + // keep initial position for comparison with drag position + _handleDragGroupStart(e, ui, that) { + this._manageDragStage(e, ui, that, "start"); + } + + _handleDragGroup(e, ui, that) { + this._manageDragStage(e, ui, that, "drag"); + } + + _handleDragGroupStop(e, ui, that) { + this._manageDragStage(e, ui, that, "stop"); + } + + _manageDragStage(e, ui, that, stage){ + var theSelection = this.props.selection; + var elements = ["node", "caption", "edge"]; + //cycle through elements and move selected elements + for (var i = 0; i < elements.length; i++){ + var thisElement = theSelection[elements[i] + "Ids"]; + var selectedElements = _.filter(this[elements[i] + "s"], function(d){ + return _.indexOf(thisElement, d.props[elements[i]]["id"]) != -1; + }) + + if (stage == "start"){ + _.forEach(selectedElements, function(d){ + d._doDragStart(e, ui); + }) + } else if (stage == "drag"){ + _.forEach(selectedElements, function(d){ + d._doDrag(e, ui, true); + }) + } else { + _.forEach(selectedElements, function(d){ + d._doDragStop(e, ui); + }) + } + } + } + + // TRANSITION ANIMATION _animateTransition(oldViewBox, viewBox, duration) { diff --git a/app/components/Node.jsx b/app/components/Node.jsx index 711f067..2eb2aba 100644 --- a/app/components/Node.jsx +++ b/app/components/Node.jsx @@ -12,7 +12,7 @@ import Helpers from '../models/Helpers'; export default class Node extends BaseComponent { constructor(props) { super(props); - this.bindAll('_handleDragStart', '_handleDrag', '_handleDragStop', '_handleClick'); + this.bindAll('_handleDragStart', '_handleDrag', '_handleDragStop', '_doDragStart', '_doDrag', '_doDragStop', '_handleClick'); this.state = props.node.display; } @@ -47,6 +47,7 @@ export default class Node extends BaseComponent { } shouldComponentUpdate(nextProps, nextState) { + // return true; return nextProps.selected !== this.props.selected || JSON.stringify(nextState) !== JSON.stringify(this.state); } @@ -54,6 +55,15 @@ export default class Node extends BaseComponent { // keep initial position for comparison with drag position _handleDragStart(e, ui) { e.preventDefault(); + var isOnlyOne = (this.graph.props.selection["nodeIds"].length + this.graph.props.selection["captionIds"].length + this.graph.props.selection["edgeIds"].length) < 2; + if (!this.graph.props.showEditTools || !this.props.selected || isOnlyOne) { + this._doDragStart(e, ui); + } else { + this.props.onStart(e, ui, this); + } + } + + _doDragStart(e, ui) { this._startDrag = ui.position; this._startPosition = { x: this.state.x, @@ -63,36 +73,63 @@ export default class Node extends BaseComponent { // while dragging node and its edges are updated only in state, not store _handleDrag(e, ui) { + if (this.props.isLocked) return; - this._dragging = true; // so that _handleClick knows it's not just a click + var isOnlyOne = (this.graph.props.selection["nodeIds"].length + this.graph.props.selection["captionIds"].length + this.graph.props.selection["edgeIds"].length) < 2; + if (!this.graph.props.showEditTools || !this.props.selected || isOnlyOne) { + this._doDrag(e, ui, false); + } else { + this.props.onDrag(e, ui, this); + } + } - let n = this.props.node; - let deltaX = (ui.position.clientX - this._startDrag.clientX) / this.graph.state.actualZoom; - let deltaY = (ui.position.clientY - this._startDrag.clientY) / this.graph.state.actualZoom; - let x = this._startPosition.x + deltaX; - let y = this._startPosition.y + deltaY; + _doDrag(e, ui, isMultiple) { + this._dragging = true; // so that _handleClick knows it's not just a click - this.setState({ x, y }); + let n = this.props.node; + let deltaX = (ui.position.clientX - this._startDrag.clientX) / this.graph.state.actualZoom; + let deltaY = (ui.position.clientY - this._startDrag.clientY) / this.graph.state.actualZoom; + let x = this._startPosition.x + deltaX; + let y = this._startPosition.y + deltaY; - // update state of connecting edges - let edges = Graph.edgesConnectedToNode(this.props.graph, n.id); + this.setState({ x, y }); - edges.forEach(edge => { - let thisNodeNum = edge.node1_id == n.id ? 1 : 2; - let newEdge = Graph.moveEdgeNode(edge, thisNodeNum, x, y); - this.graph.edges[edge.id].setState(newEdge.display); - }); + // update state of connecting edges + let edges = Graph.edgesConnectedToNode(this.props.graph, n.id); + + edges.forEach(edge => { + let thisNodeNum = edge.node1_id == n.id ? 1 : 2; + let newEdge = Graph.moveEdgeNode(edge, thisNodeNum, x, y); + this.graph.edges[edge.id].setState(newEdge.display); + }); + + //update throughout drag so nodes know their siblings' positions when + //multiple nodes are dragged simultaneously + if (isMultiple){ + if (this._dragging) { + this.props.moveNode(this.props.node.id, this.state.x, this.state.y); + } + } } // store updated once dragging is done _handleDragStop(e, ui) { // event fires every mouseup so we check for actual drag before updating store - if (this._dragging) { - this.props.moveNode(this.props.node.id, this.state.x, this.state.y); + var isOnlyOne = (this.graph.props.selection["nodeIds"].length + this.graph.props.selection["captionIds"].length + this.graph.props.selection["edgeIds"].length) < 2; + if (!this.graph.props.showEditTools || !this.props.selected || isOnlyOne) { + this._doDragStop(e, ui); + } else { + this.props.onStop(e, ui, this); } } + _doDragStop(e, ui){ + if (this._dragging) { + this.props.moveNode(this.props.node.id, this.state.x, this.state.y); + } + } + _handleClick() { if (this._dragging) { this._dragging = false;