feat(rg): add onConnect callback

This commit is contained in:
moklick
2019-07-29 18:18:27 +02:00
parent cc14c724d0
commit 8fb45f4894
26 changed files with 434 additions and 120 deletions
+6 -2
View File
@@ -4,10 +4,14 @@ export default (props) => {
const { targetNode, sourceNode } = props;
const style = props.style || {};
const sourceX = sourceNode.__rg.position.x + (sourceNode.__rg.width / 2);
const sourceHandle = sourceNode.__rg.handleBounds.source;
const sourceHandleX = sourceHandle ? sourceHandle.x + (sourceHandle.width / 2) : sourceNode.__rg.width / 2;
const sourceX = sourceNode.__rg.position.x + sourceHandleX;
const sourceY = sourceNode.__rg.position.y + sourceNode.__rg.height;
const targetX = targetNode.__rg.position.x + (targetNode.__rg.width / 2);
const targetHandle = targetNode.__rg.handleBounds.target;
const targetHandleX = targetHandle ? targetHandle.x + (targetHandle.width / 2) : targetNode.__rg.width / 2;
const targetX = targetNode.__rg.position.x + targetHandleX;
const targetY = targetNode.__rg.position.y;
const yOffset = Math.abs(targetY - sourceY) / 2;
+6 -2
View File
@@ -4,10 +4,14 @@ export default (props) => {
const { targetNode, sourceNode } = props;
const style = props.style || {};
const sourceX = sourceNode.__rg.position.x + (sourceNode.__rg.width / 2);
const sourceHandle = sourceNode.__rg.handleBounds.source;
const sourceHandleX = sourceHandle ? sourceHandle.x + (sourceHandle.width / 2) : sourceNode.__rg.width / 2;
const sourceX = sourceNode.__rg.position.x + sourceHandleX;
const sourceY = sourceNode.__rg.position.y + sourceNode.__rg.height;
const targetX = targetNode.__rg.position.x + (targetNode.__rg.width / 2);
const targetHandle = targetNode.__rg.handleBounds.target;
const targetHandleX = targetHandle ? targetHandle.x + (targetHandle.width / 2) : targetNode.__rg.width / 2;
const targetX = targetNode.__rg.position.x + targetHandleX;
const targetY = targetNode.__rg.position.y;
return (
+2 -2
View File
@@ -5,7 +5,7 @@ import { GraphContext } from '../../GraphContext';
import { setSelectedElements } from '../../state/actions';
import { isEdge } from '../../graph-utils';
const isInputTarget = (e) => ['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.nodeName);
const isInput = e => ['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.nodeName);
export default EdgeComponent => memo((props) => {
const { state, dispatch } = useContext(GraphContext);
@@ -19,7 +19,7 @@ export default EdgeComponent => memo((props) => {
<g
className={edgeClasses}
onClick={(e) => {
if (isInputTarget(e)) {
if (isInput(e)) {
return false;
}
+1 -1
View File
@@ -13,7 +13,7 @@ class EdgeRenderer extends PureComponent {
}
if (!targetNode) {
throw new Error(`couldn't create edge for source id: ${e.target}`);
throw new Error(`couldn't create edge for target id: ${e.target}`);
}
const EdgeComponent = this.props.edgeTypes[edgeType] || this.props.edgeTypes.default;
-2
View File
@@ -9,7 +9,6 @@ export const GraphContext = createContext({});
export const Provider = (props) => {
const {
onElementClick,
children
} = props;
@@ -42,7 +41,6 @@ export const Provider = (props) => {
});
const graphContext = {
onElementClick,
state,
dispatch
};
+5 -1
View File
@@ -78,7 +78,11 @@ const GraphView = memo((props) => {
return (
<div className="react-graph__renderer">
<NodeRenderer nodeTypes={props.nodeTypes} />
<NodeRenderer
nodeTypes={props.nodeTypes}
onElementClick={props.onElementClick}
onConnect={props.onConnect}
/>
<EdgeRenderer
width={state.width}
height={state.height}
-15
View File
@@ -1,15 +0,0 @@
import React, { memo } from 'react';
import cx from 'classnames';
export default memo(({ input, output, ...rest }) => {
const handleClasses = cx(
'react-graph__handle', { input, output }
);
return (
<div
className={handleClasses}
{...rest}
/>
);
});
@@ -0,0 +1,37 @@
import React, { memo } from 'react';
import cx from 'classnames';
import { Consumer } from '../NodeIdContext'
function onDragStart(evt, nodeId) {
evt.dataTransfer.setData("text/plain", nodeId);
}
export default memo(({ source, target, ...rest }) => {
const handleClasses = cx(
'react-graph__handle',
{ source, target }
);
if (target) {
return (
<div
className={handleClasses}
{...rest}
/>
)
}
return (
<Consumer>
{nodeId => (
<div
className={handleClasses}
{...rest}
draggable
onDragStart={evt => onDragStart(evt, nodeId)}
/>
)}
</Consumer>
);
});
@@ -0,0 +1,5 @@
import React from 'react';
import BaseHandle from './BaseHandle';
export default props => <BaseHandle source {...props} />;
@@ -0,0 +1,6 @@
import React from 'react';
import BaseHandle from './BaseHandle';
export default props => <BaseHandle target {...props} />;
+8
View File
@@ -0,0 +1,8 @@
import { createContext } from 'react';
const NodeIdContext = createContext(null);
export const Provider = NodeIdContext.Provider;
export const Consumer = NodeIdContext.Consumer;
export default NodeIdContext;
+4 -3
View File
@@ -1,6 +1,7 @@
import React from 'react';
import Handle from '../Handle';
import TargetHandle from '../HandleTypes/TargetHandle';
import SourceHandle from '../HandleTypes/SourceHandle';
const nodeStyles = {
background: '#ff6060',
@@ -11,8 +12,8 @@ const nodeStyles = {
export default ({ data, style }) => (
<div style={{ ...nodeStyles, ...style }}>
<Handle input />
<TargetHandle />
{data.label}
<Handle output />
<SourceHandle />
</div>
);
+2 -2
View File
@@ -1,6 +1,6 @@
import React from 'react';
import Handle from '../Handle';
import SourceHandle from '../HandleTypes/SourceHandle';
const nodeStyles = {
background: '#9999ff',
@@ -15,6 +15,6 @@ export default ({ data, style }) => (
className="react-graph__node-inner"
>
{data.label}
<Handle output />
<SourceHandle />
</div>
);
+2 -2
View File
@@ -1,6 +1,6 @@
import React from 'react';
import Handle from '../Handle';
import TargetHandle from '../HandleTypes/TargetHandle';
const nodeStyles = {
background: '#55dd99',
@@ -11,7 +11,7 @@ const nodeStyles = {
export default ({ data, style }) => (
<div style={{ ...nodeStyles, ...style }}>
<Handle input />
<TargetHandle />
{data.label}
</div>
);
+50 -6
View File
@@ -5,14 +5,38 @@ import cx from 'classnames';
import { GraphContext } from '../../GraphContext';
import { updateNodeData, updateNodePos, setSelectedElements } from '../../state/actions';
import { isNode } from '../../graph-utils';
import { Provider } from '../NodeIdContext';
const isInputTarget = (e) => ['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.nodeName);
const isInput = e => ['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.nodeName);
const isHandle = e => e.target.className.includes('source');
const getHandleBounds = (sel, nodeElement, parentBounds) => {
const handle = nodeElement.querySelector(sel);
if (!handle) {
return null;
}
const bounds = handle.getBoundingClientRect();
return {
x: bounds.x - parentBounds.x,
y: bounds.y - parentBounds.y,
width: bounds.width,
height: bounds.height
};
};
const onDragOver = evt => {
evt.preventDefault();
evt.dataTransfer.dropEffect = "move";
}
export default NodeComponent => memo((props) => {
const nodeElement = useRef(null);
const { state, dispatch } = useContext(GraphContext);
const [offset, setOffset] = useState({ x: 0, y: 0 });
const { data, onClick, type, id, __rg } = props;
const { data, onClick, type, id, __rg, onConnect } = props;
const { position } = __rg;
const [ x, y, k ] = state.transform;
const selected = state.selectedElements.filter(isNode).map(e => e.id).includes(id);
@@ -22,12 +46,16 @@ export default NodeComponent => memo((props) => {
const bounds = nodeElement.current.getBoundingClientRect();
const unscaledWith = Math.round(bounds.width * (1 / k));
const unscaledHeight = Math.round(bounds.height * (1 / k));
const handleBounds = {
source: getHandleBounds('.source', nodeElement.current, bounds),
target: getHandleBounds('.target', nodeElement.current, bounds)
};
dispatch(updateNodeData(id, { width: unscaledWith, height: unscaledHeight }));
dispatch(updateNodeData(id, { width: unscaledWith, height: unscaledHeight, handleBounds }));
}, []);
const onStart = (evt) => {
if (isInputTarget(evt)) {
if (isInput(evt) || isHandle(evt)) {
return false;
}
@@ -54,7 +82,7 @@ export default NodeComponent => memo((props) => {
};
const onNodeClick = (evt) => {
if (isInputTarget(evt)) {
if (isInput(evt)) {
return false;
}
@@ -62,6 +90,18 @@ export default NodeComponent => memo((props) => {
onClick({ id, type, data, position });
};
const onDrop = (evt) => {
evt.preventDefault();
const sourceId = evt.dataTransfer.getData('text/plain');
if (sourceId === id) {
return false;
}
onConnect({ sourceId, targetId: id });
};
return (
<ReactDraggable.DraggableCore
grid={[1, 1]}
@@ -70,12 +110,16 @@ export default NodeComponent => memo((props) => {
scale={k}
>
<div
onDrop={onDrop}
onDragOver={onDragOver}
className={nodeClasses}
ref={nodeElement}
style={{ zIndex: selected ? 10 : 3, transform: `translate(${position.x}px,${position.y}px)` }}
onClick={onNodeClick}
>
<NodeComponent {...props} selected={selected} />
<Provider value={id}>
<NodeComponent {...props} selected={selected} />
</Provider>
</div>
</ReactDraggable.DraggableCore>
);
+5 -4
View File
@@ -4,7 +4,7 @@ import { Consumer } from '../GraphContext';
class NodeRenderer extends PureComponent {
renderNode(d, onElementClick) {
renderNode(d) {
const nodeType = d.type || 'default';
if (!this.props.nodeTypes[nodeType]) {
console.warn(`No node type found for type "${nodeType}". Using fallback type "default".`);
@@ -15,7 +15,8 @@ class NodeRenderer extends PureComponent {
return (
<NodeComponent
key={d.id}
onClick={onElementClick}
onClick={this.props.onElementClick}
onConnect={this.props.onConnect}
{...d}
/>
);
@@ -24,14 +25,14 @@ class NodeRenderer extends PureComponent {
render() {
return (
<Consumer>
{({ onElementClick, state }) => (
{({ state }) => (
<div
className="react-graph__nodes"
style={{
transform: `translate(${state.transform[0]}px,${state.transform[1]}px) scale(${state.transform[2]})`
}}
>
{state.nodes.map(d => this.renderNode(d, onElementClick))}
{state.nodes.map(d => this.renderNode(d))}
</div>
)}
</Consumer>
+5 -1
View File
@@ -27,7 +27,8 @@ class ReactGraph extends PureComponent {
render() {
const {
style, onElementClick, children, onLoad,
onMove, onChange, elements, onElementsRemove
onMove, onChange, elements, onElementsRemove,
onConnect
} = this.props;
const { nodes, edges } = elements
@@ -41,6 +42,8 @@ class ReactGraph extends PureComponent {
onLoad={onLoad}
onMove={onMove}
onChange={onChange}
onElementClick={onElementClick}
onConnect={onConnect}
nodeTypes={this.nodeTypes}
edgeTypes={this.edgeTypes}
/>
@@ -57,6 +60,7 @@ class ReactGraph extends PureComponent {
ReactGraph.defaultProps = {
onElementClick: () => {},
onElementsRemove: () => {},
onConnect: () => {},
onLoad: () => {},
onMove: () => {},
onChange: () => {},
+6
View File
@@ -14,8 +14,13 @@ export const removeElements = (elements, elementsToRemove) => {
});
}
let internalNodeId = 0;
const getId = () => internalNodeId++;
export const parseElements = e => {
e.type = e.type || 'default';
e.id = e.id ? e.id : getId();
if (isEdge(e)) {
return e;
@@ -23,6 +28,7 @@ export const parseElements = e => {
return {
...e,
id: e.id.toString(),
__rg: {
position: e.position,
width: null,
+6
View File
@@ -1,4 +1,7 @@
import ReactGraph from './ReactGraph';
import _SourceHandle from './NodeRenderer/HandleTypes/SourceHandle';
import _TargetHandle from './NodeRenderer/HandleTypes/TargetHandle';
import {
isNode as _isNode ,
isEdge as _isEdge,
@@ -9,4 +12,7 @@ export const isNode = _isNode;
export const isEdge = _isEdge;
export const removeElements = _removeElements;
export const SourceHandle = _SourceHandle;
export const TargetHandle = _TargetHandle;
export default ReactGraph;
+11 -5
View File
@@ -97,15 +97,21 @@
.react-graph__handle {
position: absolute;
width: 10px;
height: 5px;
transform: translate(-50%, 0);
height: 8px;
background: rgba(255, 255, 255, 0.4);
left: 50%;
top: 0;
&.output {
&.source {
top: auto;
left: 50%;
bottom: 0;
transform: translate(-50%, 0);
cursor: crosshair;
}
&.target {
left: 50%;
top: 0;
transform: translate(-50%, 0);
}
}