pagraphcontrol/components/graph/base.js
2018-12-02 23:51:26 +03:00

409 lines
10 KiB
JavaScript

/* global document */
const {
merge,
} = require('ramda');
const r = require('r-dom');
const {
GraphView: GraphViewBase,
Node: NodeBase,
Edge: EdgeBase,
GraphUtils,
} = require('react-digraph');
const math = require('mathjs');
const d3 = require('d3');
const { size } = require('../../constants/view');
class GraphView extends GraphViewBase {
constructor(props) {
super(props);
if (props.layoutEngine) {
this.layoutEngine = props.layoutEngine;
}
Object.assign(this, {
_super_renderBackground: this.renderBackground,
renderBackground: this.constructor.prototype.renderBackground.bind(this),
_super_handleZoomStart: this.handleZoomStart,
handleZoomStart: this.constructor.prototype.handleZoomStart.bind(this),
_super_handleZoomEnd: this.handleZoomEnd,
handleZoomEnd: this.constructor.prototype.handleZoomEnd.bind(this),
_super_handleNodeMove: this.handleNodeMove,
handleNodeMove: this.constructor.prototype.handleNodeMove.bind(this),
_super_getEdgeComponent: this.getEdgeComponent,
getEdgeComponent: this.constructor.prototype.getEdgeComponent.bind(this),
_super_getNodeComponent: this.getNodeComponent,
getNodeComponent: this.constructor.prototype.getNodeComponent.bind(this),
_super_handleNodeMouseEnter: this.handleNodeMouseEnter,
handleNodeMouseEnter: this.constructor.prototype.handleNodeMouseEnter.bind(this),
});
}
static getDerivedStateFromProps(props, state) {
const derivedState = super.getDerivedStateFromProps(props, state);
if (props.layoutEngine) {
derivedState.nodes = props.layoutEngine.adjustNodes(derivedState.nodes, derivedState.nodesMap);
}
if (props.moved && props.selected) {
const edgeKey = `${props.moved.source}_${props.moved.target}`;
const nodeKey = `key-${props.selected.id}`;
if (derivedState.edgesMap[edgeKey] && derivedState.nodesMap[nodeKey]) {
derivedState.previousMoved = props.moved;
derivedState.draggingEdge = true;
derivedState.draggedEdge = props.moved;
derivedState.edgeEndNode = props.selected;
derivedState.hoveredNode = true;
derivedState.hoveredNodeData = props.selected;
derivedState.selectedNodeObj = {
nodeId: null,
node: null,
};
}
} else if (!props.moved && state.previousMoved) {
derivedState.previousMoved = null;
derivedState.draggingEdge = false;
derivedState.draggedEdge = null;
derivedState.edgeEndNode = null;
derivedState.hoveredNode = false;
derivedState.hoveredNodeData = null;
}
return derivedState;
}
shouldComponentUpdate(nextProps, nextState) {
return super.shouldComponentUpdate(nextProps, nextState) ||
this.state.edgeEndNode !== nextState.edgeEndNode;
}
componentDidUpdate(prevProps, prevState) {
const { nodeKey } = this.props;
if (this.state.edgeEndNode !== prevState.edgeEndNode) {
if (prevState.edgeEndNode) {
const prevNode = document.getElementById('node-' + prevState.edgeEndNode[nodeKey]);
prevNode.classList.remove('targeted');
}
if (this.state.edgeEndNode) {
const node = document.getElementById('node-' + this.state.edgeEndNode[nodeKey]);
node.classList.add('targeted');
}
}
if (!prevProps.moved && this.props.moved) {
this.removeEdgeElement(this.props.moved.source, this.props.moved.target);
} else if (prevProps.moved && !this.props.moved) {
const container = document.querySelector('#edge-custom-container');
if (container) {
container.remove();
}
}
if (this.props.selected &&
this.props.moved &&
(
prevProps.selected !== this.props.selected ||
prevProps.moved !== this.props.moved
) &&
this.state.draggedEdge
) {
this.dragEdge();
}
super.componentDidUpdate(prevProps, prevState);
}
getMouseCoordinates() {
if (this.props.selected && this.props.moved) {
return [
this.props.selected.x,
this.props.selected.y,
];
}
return super.getMouseCoordinates();
}
renderBackground() {
const { gridSize, backgroundFillId, renderBackground, onBackgroundMouseDown } = this.props;
if (renderBackground) {
return renderBackground({
gridSize,
backgroundFillId,
onMouseDown: onBackgroundMouseDown,
});
}
return this._super_renderBackground();
}
getNodeComponent(id, node) {
const { nodeTypes, nodeSubtypes, nodeSize, renderNode, renderNodeText, nodeKey } = this.props;
return r(Node, {
key: id,
id,
data: node,
nodeTypes,
nodeSize,
nodeKey,
nodeSubtypes,
onNodeMouseDown: this.props.onNodeMouseDown,
onNodeMouseEnter: this.handleNodeMouseEnter,
onNodeMouseLeave: this.handleNodeMouseLeave,
onNodeDragStart: this.props.onNodeDragStart,
onNodeDragEnd: this.props.onNodeDragEnd,
onNodeMove: this.handleNodeMove,
onNodeUpdate: this.handleNodeUpdate,
onNodeSelected: this.handleNodeSelected,
renderNode,
renderNodeText,
isSelected: this.state.selectedNodeObj.node === node,
layoutEngine: this.layoutEngine,
viewWrapperElem: this.viewWrapper.current,
});
}
handleZoomStart(...args) {
if (this.props.onZoomStart) {
this.props.onZoomStart();
}
return this._super_handleZoomStart(...args);
}
handleZoomEnd(...args) {
if (this.props.onZoomEnd) {
this.props.onZoomEnd();
}
return this._super_handleZoomEnd(...args);
}
handleNodeMove(position, nodeId, shiftKey) {
this._super_handleNodeMove(position, nodeId, shiftKey);
if (this.props.onNodeMove) {
this.props.onNodeMove(position, nodeId, shiftKey);
}
}
handleNodeMouseEnter(event, data, hovered) {
if (hovered && !this.state.hoveredNode) {
this.setState({
hoveredNode: true,
hoveredNodeData: data,
});
} else if (!hovered && this.state.draggingEdge) {
this.setState({
edgeEndNode: data,
});
} else {
this.setState({
hoveredNode: true,
hoveredNodeData: data,
});
}
}
getEdgeComponent(edge, nodeMoving) {
if (!this.props.renderEdge) {
return this._super_getEdgeComponent(edge);
}
const sourceNodeMapNode = this.getNodeById(edge.source);
const sourceNode = sourceNodeMapNode ? sourceNodeMapNode.node : null;
const targetNodeMapNode = this.getNodeById(edge.target);
const targetNode = targetNodeMapNode ? targetNodeMapNode.node : null;
const { targetPosition } = edge;
const { edgeTypes, edgeHandleSize, nodeSize, nodeKey, renderEdgeText } = this.props;
const selected = this.isEdgeSelected(edge);
return r(this.props.renderEdge || Edge, {
data: edge,
edgeTypes,
edgeHandleSize,
nodeSize,
sourceNode,
targetNode: targetNode || targetPosition,
nodeKey,
isSelected: selected,
nodeMoving,
renderEdgeText,
onEdgeMouseDown: this.props.onEdgeMouseDown,
});
}
syncRenderEdge(edge, nodeMoving = false) {
if (!edge.source) {
return;
}
const idVar = edge.target ? `${edge.source}-${edge.target}` : 'custom';
const id = `edge-${idVar}`;
const element = this.getEdgeComponent(edge, nodeMoving);
this.renderEdge(id, element, edge, nodeMoving);
if (this.isEdgeSelected(edge)) {
const container = document.getElementById(`${id}-container`);
container.parentNode.appendChild(container);
}
}
}
GraphView.defaultProps = merge(GraphViewBase.defaultProps, {
layoutEngineType: null,
});
class Node extends NodeBase {
constructor(props) {
super(props);
Object.assign(this, {
_super_handleDragStart: this.handleDragStart,
handleDragStart: this.constructor.prototype.handleDragStart.bind(this),
_super_handleDragEnd: this.handleDragEnd,
handleDragEnd: this.constructor.prototype.handleDragEnd.bind(this),
handleMouseDown: this.constructor.prototype.handleMouseDown.bind(this),
});
}
componentDidMount() {
d3
.select(this.nodeRef.current)
.on('mousedown', this.handleMouseDown);
super.componentDidMount();
}
componentWillUnmount() {
d3
.select(this.nodeRef.current)
.on('mousedown', null);
super.componentWillUnmount();
}
handleMouseDown() {
if (this.props.onNodeMouseDown) {
this.props.onNodeMouseDown(d3.event, this.props.data);
}
}
handleDragStart(...args) {
if (this.props.onNodeDragStart) {
this.props.onNodeDragStart(...args);
}
return this._super_handleDragStart(...args);
}
handleDragEnd(...args) {
if (this.props.onNodeDragEnd) {
this.props.onNodeDragEnd(...args);
}
this.oldSibling = null;
return this._super_handleDragEnd(...args);
}
}
EdgeBase.calculateOffset = function (nodeSize, source, target) {
const arrowVector = math.matrix([ target.x - source.x, target.y - source.y ]);
const offsetLength = Math.max(0, Math.min((0.75 * size), (math.norm(arrowVector) / 2) - 40));
const offsetVector = math.dotMultiply(arrowVector, (offsetLength / math.norm(arrowVector)) || 0);
return {
xOff: offsetVector.get([ 0 ]),
yOff: offsetVector.get([ 1 ]),
};
};
class Edge extends EdgeBase {
constructor(props) {
super(props);
Object.assign(this, {
handleMouseDown: this.constructor.prototype.handleMouseDown.bind(this),
});
}
componentDidMount() {
d3
.select(this.edgeOverlayRef.current)
.on('mousedown', this.handleMouseDown);
}
componentWillUnmount() {
d3
.select(this.edgeOverlayRef.current)
.on('mousedown', null);
}
handleMouseDown() {
if (this.props.onEdgeMouseDown) {
this.props.onEdgeMouseDown(d3.event, this.props.data);
}
}
render() {
const { data } = this.props;
const id = `${data.source || ''}_${data.target}`;
const className = GraphUtils.classNames('edge', {
selected: this.props.isSelected,
});
return r.g({
className: 'edge-container ' + (this.props.className || ''),
'data-source': data.source,
'data-target': data.target,
}, [
r.g({
className,
}, [
r.path({
className: 'edge-path',
d: this.getPathDescription(data) || undefined,
}),
]),
r.g({
className: 'edge-mouse-handler',
}, [
r.path({
className: 'edge-overlay-path',
ref: this.edgeOverlayRef,
id,
'data-source': data.source,
'data-target': data.target,
d: this.getPathDescription(data) || undefined,
}),
this.props.renderEdgeText && !this.props.nodeMoving && r(this.props.renderEdgeText, {
data,
transform: this.getEdgeHandleTranslation(),
selected: this.props.isSelected,
}),
]),
]);
}
}
module.exports = {
GraphView,
Edge,
};