From 54af33afc762fc2540dbb30329117b193ef6bd00 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 11 Sep 2019 20:27:53 +0200 Subject: [PATCH] feat(handle): add type and position --- example/SimpleGraph.js | 18 +-- src/EdgeRenderer/EdgeTypes/BezierEdge.js | 21 +++- src/EdgeRenderer/index.js | 110 ++++++++++++------- src/NodeRenderer/HandleTypes/BaseHandle.js | 13 ++- src/NodeRenderer/HandleTypes/Handle.js | 39 +++++++ src/NodeRenderer/HandleTypes/SourceHandle.js | 3 +- src/NodeRenderer/HandleTypes/TargetHandle.js | 3 +- src/NodeRenderer/NodeTypes/DefaultNode.js | 7 +- src/NodeRenderer/NodeTypes/InputNode.js | 4 +- src/NodeRenderer/NodeTypes/OutputNode.js | 4 +- src/NodeRenderer/NodeTypes/wrapNode.js | 2 + src/index.js | 3 +- src/style.css | 20 +++- 13 files changed, 173 insertions(+), 74 deletions(-) create mode 100644 src/NodeRenderer/HandleTypes/Handle.js diff --git a/example/SimpleGraph.js b/example/SimpleGraph.js index 219aff88..f0091cb7 100644 --- a/example/SimpleGraph.js +++ b/example/SimpleGraph.js @@ -1,21 +1,21 @@ import React, { PureComponent } from 'react'; -import Graph, { isEdge, removeElements, getOutgoers, SourceHandle, TargetHandle, MiniMap } from '../src'; -// import Graph, { isEdge, removeElements, getOutgoers, SourceHandle, TargetHandle } from '../dist/ReactGraph'; +import Graph, { isEdge, removeElements, getOutgoers, Handle, MiniMap } from '../src'; +// import Graph, { isEdge, removeElements, getOutgoers, Handle } from '../dist/ReactGraph'; const SpecialNode = ({ data, styles }) => (
- - + +
I am special!
{data.label}
- +
); @@ -23,10 +23,10 @@ const InputNode = ({ data, styles }) => (
- +
{data.input}
data.onChange(e.target.value, data)} /> - +
); @@ -72,9 +72,11 @@ class App extends PureComponent { { id: '5', type: 'default', data: { label: '5 Another node'}, position: { x: 400, y: 300 } }, { id: '6', type: 'special', data: { onChange, label: '6 no option selected' }, position: { x: 425, y: 375 } }, { id: '7', type: 'output', data: { label: '7 output' }, position: { x: 250, y: 500 } }, - { id: '8', type: 'text', data: { onChange: onChangeInput, input: 'write something' }, position: { x: 300, y: 100 } }, + { id: '8', type: 'text', data: { onChange: onChangeInput, input: 'write something' }, position: { x: 350, y: 100 } }, + { id: '9', type: 'text', data: { label: 'right' }, position: { x: 600, y: 100 } }, { source: '1', target: '2', animated: true }, { source: '1', target: '8', animated: true }, + { source: '8', target: '9', animated: true }, { source: '2', target: '3' }, { source: '3', target: '4', type: 'step' }, { source: '3', target: '5' }, diff --git a/src/EdgeRenderer/EdgeTypes/BezierEdge.js b/src/EdgeRenderer/EdgeTypes/BezierEdge.js index f5a3d36b..713dc89d 100644 --- a/src/EdgeRenderer/EdgeTypes/BezierEdge.js +++ b/src/EdgeRenderer/EdgeTypes/BezierEdge.js @@ -1,13 +1,22 @@ import React, { memo } from 'react'; -export default memo((props) => { - const { - sourceX, sourceY, targetX, targetY, style = {} - } = props; - +export default memo(({ + sourceX, sourceY, targetX, targetY, + sourcePosition, targetPosition, style = {} +}) => { const yOffset = Math.abs(targetY - sourceY) / 2; const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset; - const dAttr = `M${sourceX},${sourceY} C${sourceX},${centerY} ${targetX},${centerY} ${targetX},${targetY}`; + + let dAttr = `M${sourceX},${sourceY} C${sourceX},${centerY} ${targetX},${centerY} ${targetX},${targetY}`; + + if (['left', 'right'].includes(sourcePosition) && ['left', 'right'].includes(targetPosition)) { + const xOffset = Math.abs(targetX - sourceX) / 2; + const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset; + + dAttr = `M${sourceX},${sourceY} C${centerX},${sourceY} ${centerX},${targetY} ${targetX},${targetY}`; + } else if (['left', 'right'].includes(sourcePosition) || ['left', 'right'].includes(targetPosition)) { + dAttr = `M${sourceX},${sourceY} C${sourceX},${targetY} ${sourceX},${targetY} ${targetX},${targetY}`; + } return ( d.id === sourceHandleId); +function getHandlePosition(position, node, handle = null) { + if (!handle) { + switch (position) { + case 'top': return { + x: node.__rg.width / 2, + y: 0 + }; + case 'right': return { + x: node.__rg.width, + y: node.__rg.height / 2 + }; + case 'bottom': return { + x: node.__rg.width / 2, + y: node.__rg.height + }; + case 'left': return { + x: 0, + y: node.__rg.height / 2 + }; } } - if (hasTargetHandle) { - if (targetNode.__rg.handleBounds.target.length === 1 || !targetHandleId) { - targetHandle = targetNode.__rg.handleBounds.target[0]; - } else if (targetHandleId) { - targetHandle = targetNode.__rg.handleBounds.target.find(d => d.id === targetHandleId); - } + switch (position) { + case 'top': return { + x: handle.x + (handle.width / 2), + y: handle.y + }; + case 'right': return { + x: handle.x + handle.width, + y: handle.y + (handle.height / 2) + }; + case 'bottom': return { + x: handle.x + (handle.width / 2), + y: handle.y + handle.height + }; + case 'left': return { + x: handle.x, + y: handle.y + (handle.height / 2) + }; + } +} + +function getHandle(bounds, handleId) { + let handle = null; + + if (!bounds) { + return null; } - const sourceHandleX = hasSourceHandle ? - sourceHandle.x + (sourceHandle.width / 2) : - sourceNode.__rg.width / 2; + // there is no handleId when there are no multiple handles/ handles with ids + // so we just pick the first one + if (bounds.length === 1 || !handleId) { + handle = bounds[0]; + } else if (handleId) { + handle = bounds.find(d => d.id === handleId); + } - const sourceHandleY = hasSourceHandle ? - sourceHandle.y + (sourceHandle.height / 2) : - sourceNode.__rg.height; + return handle; +} - const sourceX = sourceNode.__rg.position.x + sourceHandleX; - const sourceY = sourceNode.__rg.position.y + sourceHandleY; +function getEdgePositions({ sourceNode, sourceHandle, sourcePosition, targetNode, targetHandle, targetPosition }) { + const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle) + const sourceX = sourceNode.__rg.position.x + sourceHandlePos.x; + const sourceY = sourceNode.__rg.position.y + sourceHandlePos.y; - const targetHandleX = hasTargetHandle ? - targetHandle.x + (targetHandle.width / 2) : - targetNode.__rg.width / 2; - - const targetHandleY = hasTargetHandle ? - targetHandle.y + (targetHandle.height / 2) : - 0; - - const targetX = targetNode.__rg.position.x + targetHandleX; - const targetY = targetNode.__rg.position.y + targetHandleY; + const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle); + const targetX = targetNode.__rg.position.x + targetHandlePos.x; + const targetY = targetNode.__rg.position.y + targetHandlePos.y; return { sourceX, sourceY, targetX, targetY @@ -80,7 +102,15 @@ function renderEdge(e, props, state) { } const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default; - const { sourceX, sourceY, targetX, targetY } = getEdgePositions({ sourceNode, targetNode, sourceHandleId, targetHandleId }); + const sourceHandle = getHandle(sourceNode.__rg.handleBounds.source, sourceHandleId); + const targetHandle = getHandle(targetNode.__rg.handleBounds.target, targetHandleId); + const sourcePosition = sourceHandle ? sourceHandle.position : 'bottom'; + const targetPosition = targetHandle ? targetHandle.position : 'top'; + + const { sourceX, sourceY, targetX, targetY } = getEdgePositions({ + sourceNode, sourceHandle, sourcePosition, + targetNode, targetHandle, targetPosition + }); const selected = state.selectedElements .filter(isEdge) .find(elm => elm.source === sourceId && elm.target === targetId); @@ -102,6 +132,8 @@ function renderEdge(e, props, state) { sourceY={sourceY} targetX={targetX} targetY={targetY} + sourcePosition={sourcePosition} + targetPosition={targetPosition} /> ); } diff --git a/src/NodeRenderer/HandleTypes/BaseHandle.js b/src/NodeRenderer/HandleTypes/BaseHandle.js index b90e1b85..ab375179 100644 --- a/src/NodeRenderer/HandleTypes/BaseHandle.js +++ b/src/NodeRenderer/HandleTypes/BaseHandle.js @@ -40,23 +40,26 @@ function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarg } const BaseHandle = memo(({ - source, target, nodeId, onConnect, + type, nodeId, onConnect, position, setSourceId, setPosition, className, id = false, ...rest }) => { + const isTarget = type === 'target'; const handleClasses = cx( 'react-graph__handle', className, - { source, target } + position, + { source: !isTarget, target: isTarget } ); - const handleId = id ? `${nodeId}__${id}` : nodeId; + const nodeIdWithHandleId = id ? `${nodeId}__${id}` : nodeId; return (
onMouseDown(evt, { nodeId: handleId, setSourceId, setPosition, onConnect, isTarget: target })} + onMouseDown={evt => onMouseDown(evt, { nodeId: nodeIdWithHandleId, setSourceId, setPosition, onConnect, isTarget })} {...rest} /> ); diff --git a/src/NodeRenderer/HandleTypes/Handle.js b/src/NodeRenderer/HandleTypes/Handle.js new file mode 100644 index 00000000..1be0647b --- /dev/null +++ b/src/NodeRenderer/HandleTypes/Handle.js @@ -0,0 +1,39 @@ +import React, { memo, useContext } from 'react'; +import PropTypes from 'prop-types'; +import { useStoreActions, useStoreState } from 'easy-peasy'; + +import BaseHandle from './BaseHandle'; +import NodeIdContext from '../NodeIdContext' + +const Handle = memo((props) => { + const nodeId = useContext(NodeIdContext); + const { setPosition, setSourceId } = useStoreActions(a => ({ + setPosition: a.setConnectionPosition, + setSourceId: a.setConnectionSourceId + })); + const onConnect = useStoreState(s => s.onConnect); + + return ( + + ); +}); + +Handle.displayName = 'Handle'; + +Handle.propTypes = { + type: PropTypes.oneOf(['source', 'target']), + position: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), +}; + +Handle.defaultProps = { + type: 'source', + position: 'top' +}; + +export default Handle; diff --git a/src/NodeRenderer/HandleTypes/SourceHandle.js b/src/NodeRenderer/HandleTypes/SourceHandle.js index 10b7d50d..d913dfac 100644 --- a/src/NodeRenderer/HandleTypes/SourceHandle.js +++ b/src/NodeRenderer/HandleTypes/SourceHandle.js @@ -14,7 +14,8 @@ const SourceHandle = memo((props) => { return ( { return ( (
- + {data.label} - +
); diff --git a/src/NodeRenderer/NodeTypes/InputNode.js b/src/NodeRenderer/NodeTypes/InputNode.js index fbe33935..e45bd739 100644 --- a/src/NodeRenderer/NodeTypes/InputNode.js +++ b/src/NodeRenderer/NodeTypes/InputNode.js @@ -1,6 +1,6 @@ import React from 'react'; -import SourceHandle from '../HandleTypes/SourceHandle'; +import Handle from '../HandleTypes/Handle'; const nodeStyles = { background: '#9999ff', @@ -15,6 +15,6 @@ export default ({ data, style }) => ( className="react-graph__node-inner" > {data.label} - +
); diff --git a/src/NodeRenderer/NodeTypes/OutputNode.js b/src/NodeRenderer/NodeTypes/OutputNode.js index 785e98be..7f89509c 100644 --- a/src/NodeRenderer/NodeTypes/OutputNode.js +++ b/src/NodeRenderer/NodeTypes/OutputNode.js @@ -1,6 +1,6 @@ import React from 'react'; -import TargetHandle from '../HandleTypes/TargetHandle'; +import Handle from '../HandleTypes/TargetHandle'; const nodeStyles = { background: '#55dd99', @@ -11,7 +11,7 @@ const nodeStyles = { export default ({ data, style }) => (
- + {data.label}
); diff --git a/src/NodeRenderer/NodeTypes/wrapNode.js b/src/NodeRenderer/NodeTypes/wrapNode.js index 36a8d6fc..1d7ecd94 100644 --- a/src/NodeRenderer/NodeTypes/wrapNode.js +++ b/src/NodeRenderer/NodeTypes/wrapNode.js @@ -23,6 +23,7 @@ const getHandleBounds = (sel, nodeElement, parentBounds, k) => { const bounds = handle.getBoundingClientRect(); const dimensions = getDimensions(handle); const nodeIdAttr = handle.getAttribute('data-nodeid'); + const handlePosition = handle.getAttribute('data-handlepos'); const nodeIdSplitted = nodeIdAttr.split('__'); let handleId = null; @@ -33,6 +34,7 @@ const getHandleBounds = (sel, nodeElement, parentBounds, k) => { return { id: handleId, + position: handlePosition, x: (bounds.x - parentBounds.x) * (1 / k), y: (bounds.y - parentBounds.y) * (1 / k), ...dimensions diff --git a/src/index.js b/src/index.js index 26877591..f229bdb7 100644 --- a/src/index.js +++ b/src/index.js @@ -2,8 +2,7 @@ import ReactGraph from './ReactGraph'; export default ReactGraph; -export { default as SourceHandle } from './NodeRenderer/HandleTypes/SourceHandle'; -export { default as TargetHandle } from './NodeRenderer/HandleTypes/TargetHandle'; +export { default as Handle } from './NodeRenderer/HandleTypes/Handle'; export { default as MiniMap } from './Plugins/MiniMap'; export { diff --git a/src/style.css b/src/style.css index 45ab549f..3ef1ca16 100644 --- a/src/style.css +++ b/src/style.css @@ -104,21 +104,33 @@ width: 10px; height: 8px; background: rgba(255, 255, 255, 0.4); + cursor: crosshair; - &.source { + &.bottom { top: auto; left: 50%; bottom: 0; transform: translate(-50%, 0); - cursor: crosshair; } - &.target { + &.top { left: 50%; top: 0; - cursor: crosshair; transform: translate(-50%, 0); } + + &.left { + top: 50%; + left: 0; + transform: translate(0, -50%); + + } + + &.right { + right: 0; + top: 50%; + transform: translate(0, -50%); + } } .react-graph__nodesselection {