feat(nodes): allow multiple handles
This commit is contained in:
@@ -7,7 +7,8 @@ const SpecialNode = ({ data, styles }) => (
|
|||||||
<div
|
<div
|
||||||
style={{ background: '#FFCC00', padding: 10, borderRadius: 2, ...styles }}
|
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>
|
<div>I am <strong>special</strong>!<br />{data.label}</div>
|
||||||
<select onChange={(e) => data.onChange(e.target.value, data)}>
|
<select onChange={(e) => data.onChange(e.target.value, data)}>
|
||||||
<option value="1">1</option>
|
<option value="1">1</option>
|
||||||
@@ -77,6 +78,7 @@ class App extends PureComponent {
|
|||||||
{ source: '2', target: '3' },
|
{ source: '2', target: '3' },
|
||||||
{ source: '3', target: '4', type: 'step' },
|
{ source: '3', target: '4', type: 'step' },
|
||||||
{ source: '3', target: '5' },
|
{ source: '3', target: '5' },
|
||||||
|
{ source: '5', target: '6__b' },
|
||||||
{ source: '5', target: '6', type: 'step', animated: true, style: { stroke: '#FFCC00' } },
|
{ source: '5', target: '6', type: 'step', animated: true, style: { stroke: '#FFCC00' } },
|
||||||
{ source: '6', target: '7', style: { stroke: '#FFCC00' }},
|
{ source: '6', target: '7', style: { stroke: '#FFCC00' }},
|
||||||
]
|
]
|
||||||
|
|||||||
Generated
+3
-3
@@ -8378,9 +8378,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ts-toolbelt": {
|
"ts-toolbelt": {
|
||||||
"version": "3.8.1",
|
"version": "3.8.91",
|
||||||
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-3.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-3.8.91.tgz",
|
||||||
"integrity": "sha512-XDAWEHVscqto6pG0gPSbYaEfEBcl9V0JK5pQIQJ3M+6sYnxK5ZOPyyRA/aq78WxKlLvBSXCPJ1fPX+7qCCg2Zg=="
|
"integrity": "sha512-HVY9kFtQCtAfuHPf8XK9BWzwUueGtviZ5i/x6hu9uMIy+PWqlwjD1eqRTBaCewOclR5w07j1bgaT7cWNWrJgZw=="
|
||||||
},
|
},
|
||||||
"tty-browserify": {
|
"tty-browserify": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
|||||||
@@ -3,8 +3,13 @@ import cx from 'classnames';
|
|||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
const [sourceNode, setSourceNode] = useState(null);
|
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(() => {
|
useEffect(() => {
|
||||||
setSourceNode(props.nodes.find(n => n.id === props.connectionSourceId));
|
setSourceNode(props.nodes.find(n => n.id === nodeId));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!sourceNode) {
|
if (!sourceNode) {
|
||||||
@@ -14,7 +19,7 @@ export default (props) => {
|
|||||||
const style = props.connectionLineStyle || {};
|
const style = props.connectionLineStyle || {};
|
||||||
const className = cx('react-graph__edge', 'connection', props.className);
|
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 sourceHandleX = sourceHandle ? sourceHandle.x + (sourceHandle.width / 2) : sourceNode.__rg.width / 2;
|
||||||
const sourceHandleY = sourceHandle ? sourceHandle.y + (sourceHandle.height / 2) : sourceNode.__rg.height;
|
const sourceHandleY = sourceHandle ? sourceHandle.y + (sourceHandle.height / 2) : sourceNode.__rg.height;
|
||||||
const sourceX = sourceNode.__rg.position.x + sourceHandleX;
|
const sourceX = sourceNode.__rg.position.x + sourceHandleX;
|
||||||
|
|||||||
+46
-13
@@ -4,27 +4,48 @@ import { useStoreState } from 'easy-peasy';
|
|||||||
import ConnectionLine from '../ConnectionLine';
|
import ConnectionLine from '../ConnectionLine';
|
||||||
import { isEdge } from '../graph-utils';
|
import { isEdge } from '../graph-utils';
|
||||||
|
|
||||||
function getEdgePositions(sourceNode, targetNode) {
|
function getEdgePositions({ sourceNode, targetNode, sourceHandleId, targetHandleId }) {
|
||||||
const hasSourceHandle = !!sourceNode.__rg.handleBounds.source;
|
const hasSourceHandle = !!sourceNode.__rg.handleBounds.source;
|
||||||
const hasTargetHandle = !!targetNode.__rg.handleBounds.target;
|
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 ?
|
const sourceHandleX = hasSourceHandle ?
|
||||||
sourceNode.__rg.handleBounds.source.x + (sourceNode.__rg.handleBounds.source.width / 2) :
|
sourceHandle.x + (sourceHandle.width / 2) :
|
||||||
sourceNode.__rg.width / 2;
|
sourceNode.__rg.width / 2;
|
||||||
|
|
||||||
const sourceHandleY = hasSourceHandle ?
|
const sourceHandleY = hasSourceHandle ?
|
||||||
sourceNode.__rg.handleBounds.source.y + (sourceNode.__rg.handleBounds.source.height / 2) :
|
sourceHandle.y + (sourceHandle.height / 2) :
|
||||||
sourceNode.__rg.height;
|
sourceNode.__rg.height;
|
||||||
|
|
||||||
const sourceX = sourceNode.__rg.position.x + sourceHandleX;
|
const sourceX = sourceNode.__rg.position.x + sourceHandleX;
|
||||||
const sourceY = sourceNode.__rg.position.y + sourceHandleY;
|
const sourceY = sourceNode.__rg.position.y + sourceHandleY;
|
||||||
|
|
||||||
const targetHandleX = hasTargetHandle ?
|
const targetHandleX = hasTargetHandle ?
|
||||||
targetNode.__rg.handleBounds.target.x + (targetNode.__rg.handleBounds.target.width / 2) :
|
targetHandle.x + (targetHandle.width / 2) :
|
||||||
targetNode.__rg.width / 2;
|
targetNode.__rg.width / 2;
|
||||||
|
|
||||||
const targetHandleY = hasTargetHandle ?
|
const targetHandleY = hasTargetHandle ?
|
||||||
targetNode.__rg.handleBounds.target.y + (targetNode.__rg.handleBounds.target.height / 2) :
|
targetHandle.y + (targetHandle.height / 2) :
|
||||||
0;
|
0;
|
||||||
|
|
||||||
const targetX = targetNode.__rg.position.x + targetHandleX;
|
const targetX = targetNode.__rg.position.x + targetHandleX;
|
||||||
@@ -37,22 +58,32 @@ function getEdgePositions(sourceNode, targetNode) {
|
|||||||
|
|
||||||
function renderEdge(e, props, state) {
|
function renderEdge(e, props, state) {
|
||||||
const edgeType = e.type || 'default';
|
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) {
|
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) {
|
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 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
|
const selected = state.selectedElements
|
||||||
.filter(isEdge)
|
.filter(isEdge)
|
||||||
.find(elm => elm.source === e.source && elm.target === e.target);
|
.find(elm => elm.source === sourceId && elm.target === targetId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EdgeComponent
|
<EdgeComponent
|
||||||
@@ -63,8 +94,10 @@ function renderEdge(e, props, state) {
|
|||||||
selected={selected}
|
selected={selected}
|
||||||
animated={e.animated}
|
animated={e.animated}
|
||||||
style={e.style}
|
style={e.style}
|
||||||
source={e.source}
|
source={sourceId}
|
||||||
target={e.target}
|
target={targetId}
|
||||||
|
sourceHandleId={sourceHandleId}
|
||||||
|
targetHandleId={targetHandleId}
|
||||||
sourceX={sourceX}
|
sourceX={sourceX}
|
||||||
sourceY={sourceY}
|
sourceY={sourceY}
|
||||||
targetX={targetX}
|
targetX={targetX}
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarg
|
|||||||
|
|
||||||
const BaseHandle = memo(({
|
const BaseHandle = memo(({
|
||||||
source, target, nodeId, onConnect,
|
source, target, nodeId, onConnect,
|
||||||
setSourceId, setPosition, className, ...rest
|
setSourceId, setPosition, className,
|
||||||
|
id = false, ...rest
|
||||||
}) => {
|
}) => {
|
||||||
const handleClasses = cx(
|
const handleClasses = cx(
|
||||||
'react-graph__handle',
|
'react-graph__handle',
|
||||||
@@ -49,11 +50,13 @@ const BaseHandle = memo(({
|
|||||||
{ source, target }
|
{ source, target }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleId = id ? `${nodeId}__${id}` : nodeId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-nodeid={nodeId}
|
data-nodeid={handleId}
|
||||||
className={handleClasses}
|
className={handleClasses}
|
||||||
onMouseDown={evt => onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarget: target })}
|
onMouseDown={evt => onMouseDown(evt, { nodeId: handleId, setSourceId, setPosition, onConnect, isTarget: target })}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,20 +13,31 @@ const isHandle = e => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getHandleBounds = (sel, nodeElement, parentBounds, k) => {
|
const getHandleBounds = (sel, nodeElement, parentBounds, k) => {
|
||||||
const handle = nodeElement.querySelector(sel);
|
const handles = nodeElement.querySelectorAll(sel);
|
||||||
|
|
||||||
if (!handle) {
|
if (!handles || !handles.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bounds = handle.getBoundingClientRect();
|
return [].map.call(handles, (handle) => {
|
||||||
const dimensions = getDimensions(handle);
|
const bounds = handle.getBoundingClientRect();
|
||||||
|
const dimensions = getDimensions(handle);
|
||||||
|
const nodeIdAttr = handle.getAttribute('data-nodeid');
|
||||||
|
const nodeIdSplitted = nodeIdAttr.split('__');
|
||||||
|
|
||||||
return {
|
let handleId = null;
|
||||||
x: (bounds.x - parentBounds.x) * (1 / k),
|
|
||||||
y: (bounds.y - parentBounds.y) * (1 / k),
|
if (nodeIdSplitted) {
|
||||||
...dimensions
|
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 }) => {
|
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]),
|
source: getHandleBounds('.source', nodeElement.current, bounds, transform[2]),
|
||||||
target: getHandleBounds('.target', nodeElement.current, bounds, transform[2])
|
target: getHandleBounds('.target', nodeElement.current, bounds, transform[2])
|
||||||
};
|
};
|
||||||
|
|
||||||
store.dispatch.updateNodeData({ id, ...dimensions, handleBounds });
|
store.dispatch.updateNodeData({ id, ...dimensions, handleBounds });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user