feat(nodes): allow multiple handles

This commit is contained in:
moklick
2019-09-11 17:58:18 +02:00
parent 46bf04f88d
commit 7958022ded
6 changed files with 85 additions and 32 deletions

View File

@@ -7,7 +7,8 @@ const SpecialNode = ({ data, styles }) => (
<div
style={{ background: '#FFCC00', padding: 10, borderRadius: 2, ...styles }}
>
<TargetHandle style={{ left: 10, background: '#999' }} />
<TargetHandle id="a" style={{ left: 10, background: '#999' }} />
<TargetHandle id="b" style={{ left: 30, background: '#999' }} />
<div>I am <strong>special</strong>!<br />{data.label}</div>
<select onChange={(e) => data.onChange(e.target.value, data)}>
<option value="1">1</option>
@@ -77,6 +78,7 @@ class App extends PureComponent {
{ source: '2', target: '3' },
{ source: '3', target: '4', type: 'step' },
{ source: '3', target: '5' },
{ source: '5', target: '6__b' },
{ source: '5', target: '6', type: 'step', animated: true, style: { stroke: '#FFCC00' } },
{ source: '6', target: '7', style: { stroke: '#FFCC00' }},
]

6
package-lock.json generated
View File

@@ -8378,9 +8378,9 @@
"dev": true
},
"ts-toolbelt": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-3.8.1.tgz",
"integrity": "sha512-XDAWEHVscqto6pG0gPSbYaEfEBcl9V0JK5pQIQJ3M+6sYnxK5ZOPyyRA/aq78WxKlLvBSXCPJ1fPX+7qCCg2Zg=="
"version": "3.8.91",
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-3.8.91.tgz",
"integrity": "sha512-HVY9kFtQCtAfuHPf8XK9BWzwUueGtviZ5i/x6hu9uMIy+PWqlwjD1eqRTBaCewOclR5w07j1bgaT7cWNWrJgZw=="
},
"tty-browserify": {
"version": "0.0.0",

View File

@@ -3,8 +3,13 @@ import cx from 'classnames';
export default (props) => {
const [sourceNode, setSourceNode] = useState(null);
const hasHandleId = props.connectionSourceId.includes('__');
const sourceIdSplitted = props.connectionSourceId.split('__');
const nodeId = sourceIdSplitted[0];
const handleId = hasHandleId ? sourceIdSplitted[1] : null;
useEffect(() => {
setSourceNode(props.nodes.find(n => n.id === props.connectionSourceId));
setSourceNode(props.nodes.find(n => n.id === nodeId));
}, []);
if (!sourceNode) {
@@ -14,7 +19,7 @@ export default (props) => {
const style = props.connectionLineStyle || {};
const className = cx('react-graph__edge', 'connection', props.className);
const sourceHandle = sourceNode.__rg.handleBounds.source;
const sourceHandle = handleId ? sourceNode.__rg.handleBounds.source.find(d => d.id === handleId) : sourceNode.__rg.handleBounds.source[0];
const sourceHandleX = sourceHandle ? sourceHandle.x + (sourceHandle.width / 2) : sourceNode.__rg.width / 2;
const sourceHandleY = sourceHandle ? sourceHandle.y + (sourceHandle.height / 2) : sourceNode.__rg.height;
const sourceX = sourceNode.__rg.position.x + sourceHandleX;

View File

@@ -4,27 +4,48 @@ import { useStoreState } from 'easy-peasy';
import ConnectionLine from '../ConnectionLine';
import { isEdge } from '../graph-utils';
function getEdgePositions(sourceNode, targetNode) {
function getEdgePositions({ sourceNode, targetNode, sourceHandleId, targetHandleId }) {
const hasSourceHandle = !!sourceNode.__rg.handleBounds.source;
const hasTargetHandle = !!targetNode.__rg.handleBounds.target;
let sourceHandle = null;
let targetHandle = null;
if (hasSourceHandle) {
// there is no sourceHandleId when there are no multiple handles/ handles with ids
// so we just pick the first one
if (sourceNode.__rg.handleBounds.source.length === 1 || !sourceHandleId) {
sourceHandle = sourceNode.__rg.handleBounds.source[0];
} else if (sourceHandleId) {
sourceHandle = sourceNode.__rg.handleBounds.source.find(d => d.id === sourceHandleId);
}
}
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);
}
}
const sourceHandleX = hasSourceHandle ?
sourceNode.__rg.handleBounds.source.x + (sourceNode.__rg.handleBounds.source.width / 2) :
sourceHandle.x + (sourceHandle.width / 2) :
sourceNode.__rg.width / 2;
const sourceHandleY = hasSourceHandle ?
sourceNode.__rg.handleBounds.source.y + (sourceNode.__rg.handleBounds.source.height / 2) :
sourceHandle.y + (sourceHandle.height / 2) :
sourceNode.__rg.height;
const sourceX = sourceNode.__rg.position.x + sourceHandleX;
const sourceY = sourceNode.__rg.position.y + sourceHandleY;
const targetHandleX = hasTargetHandle ?
targetNode.__rg.handleBounds.target.x + (targetNode.__rg.handleBounds.target.width / 2) :
targetHandle.x + (targetHandle.width / 2) :
targetNode.__rg.width / 2;
const targetHandleY = hasTargetHandle ?
targetNode.__rg.handleBounds.target.y + (targetNode.__rg.handleBounds.target.height / 2) :
targetHandle.y + (targetHandle.height / 2) :
0;
const targetX = targetNode.__rg.position.x + targetHandleX;
@@ -37,22 +58,32 @@ function getEdgePositions(sourceNode, targetNode) {
function renderEdge(e, props, state) {
const edgeType = e.type || 'default';
const sourceNode = state.nodes.find(n => n.id === e.source);
const targetNode = state.nodes.find(n => n.id === e.target);
const hasSourceHandleId = e.source.includes('__');
const hasTargetHandleId = e.target.includes('__');
const sourceId = hasSourceHandleId ? e.source.split('__')[0] : e.source;
const targetId = hasTargetHandleId ? e.target.split('__')[0] : e.target;
const sourceHandleId = hasSourceHandleId ? e.source.split('__')[1] : null;
const targetHandleId = hasTargetHandleId ? e.target.split('__')[1] : null;
const sourceNode = state.nodes.find(n => n.id === sourceId);
const targetNode = state.nodes.find(n => n.id === targetId);
if (!sourceNode) {
throw new Error(`couldn't create edge for source id: ${e.source}`);
throw new Error(`couldn't create edge for source id: ${sourceId}`);
}
if (!targetNode) {
throw new Error(`couldn't create edge for target id: ${e.target}`);
throw new Error(`couldn't create edge for target id: ${targetId}`);
}
const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default;
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(sourceNode, targetNode);
const { sourceX, sourceY, targetX, targetY } = getEdgePositions({ sourceNode, targetNode, sourceHandleId, targetHandleId });
const selected = state.selectedElements
.filter(isEdge)
.find(elm => elm.source === e.source && elm.target === e.target);
.find(elm => elm.source === sourceId && elm.target === targetId);
return (
<EdgeComponent
@@ -63,8 +94,10 @@ function renderEdge(e, props, state) {
selected={selected}
animated={e.animated}
style={e.style}
source={e.source}
target={e.target}
source={sourceId}
target={targetId}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}

View File

@@ -41,7 +41,8 @@ function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarg
const BaseHandle = memo(({
source, target, nodeId, onConnect,
setSourceId, setPosition, className, ...rest
setSourceId, setPosition, className,
id = false, ...rest
}) => {
const handleClasses = cx(
'react-graph__handle',
@@ -49,11 +50,13 @@ const BaseHandle = memo(({
{ source, target }
);
const handleId = id ? `${nodeId}__${id}` : nodeId;
return (
<div
data-nodeid={nodeId}
data-nodeid={handleId}
className={handleClasses}
onMouseDown={evt => onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarget: target })}
onMouseDown={evt => onMouseDown(evt, { nodeId: handleId, setSourceId, setPosition, onConnect, isTarget: target })}
{...rest}
/>
);

View File

@@ -13,20 +13,31 @@ const isHandle = e => (
);
const getHandleBounds = (sel, nodeElement, parentBounds, k) => {
const handle = nodeElement.querySelector(sel);
const handles = nodeElement.querySelectorAll(sel);
if (!handle) {
if (!handles || !handles.length) {
return null;
}
const bounds = handle.getBoundingClientRect();
const dimensions = getDimensions(handle);
return [].map.call(handles, (handle) => {
const bounds = handle.getBoundingClientRect();
const dimensions = getDimensions(handle);
const nodeIdAttr = handle.getAttribute('data-nodeid');
const nodeIdSplitted = nodeIdAttr.split('__');
return {
x: (bounds.x - parentBounds.x) * (1 / k),
y: (bounds.y - parentBounds.y) * (1 / k),
...dimensions
};
let handleId = null;
if (nodeIdSplitted) {
handleId = nodeIdSplitted.length ? nodeIdSplitted[1] : nodeIdSplitted;
}
return {
id: handleId,
x: (bounds.x - parentBounds.x) * (1 / k),
y: (bounds.y - parentBounds.y) * (1 / k),
...dimensions
};
});
};
const onStart = (evt, { setOffset, onClick, id, type, data, position, transform }) => {
@@ -92,7 +103,6 @@ export default NodeComponent => {
source: getHandleBounds('.source', nodeElement.current, bounds, transform[2]),
target: getHandleBounds('.target', nodeElement.current, bounds, transform[2])
};
store.dispatch.updateNodeData({ id, ...dimensions, handleBounds });
}, []);