diff --git a/example/src/CustomNode/ColorSelectorNode.js b/example/src/CustomNode/ColorSelectorNode.js index cd76d3b0..1b6381f3 100644 --- a/example/src/CustomNode/ColorSelectorNode.js +++ b/example/src/CustomNode/ColorSelectorNode.js @@ -15,8 +15,8 @@ export default memo(({ data }) => { Custom Color Picker Node: {data.color} - - + + ); }); diff --git a/example/src/CustomNode/index.js b/example/src/CustomNode/index.js index 7f184011..3a2e77c1 100644 --- a/example/src/CustomNode/index.js +++ b/example/src/CustomNode/index.js @@ -56,8 +56,8 @@ const CustomNodeFlow = () => { { id: '4', type: 'output', data: { label: 'Output B' }, position: { x: 550, y: 100 }, targetPosition: 'left' }, { id: 'e1-2', source: '1', target: '2', animated: true, style: { stroke: '#fff' } }, - { id: 'e2a-3', source: '2', sourceHandle: '0', target: '3', animated: true, style: { stroke: '#fff' } }, - { id: 'e2b-4', source: '2', sourceHandle: '1', target: '4', animated: true, style: { stroke: '#fff' } }, + { id: 'e2a-3', source: '2', sourceHandle: 'a', target: '3', animated: true, style: { stroke: '#fff' } }, + { id: 'e2b-4', source: '2', sourceHandle: 'b', target: '4', animated: true, style: { stroke: '#fff' } }, ]); }, []); diff --git a/example/src/Validation/index.js b/example/src/Validation/index.js index 713f3dbb..02d4bc57 100644 --- a/example/src/Validation/index.js +++ b/example/src/Validation/index.js @@ -20,7 +20,7 @@ const onConnectEnd = (event) => console.log('on connect end', event); const CustomInput = () => ( <>
Only connectable with B
- + ); diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index 05d942e3..bb9161bb 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -15,6 +15,7 @@ import { interface ConnectionLineProps { connectionNodeId: ElementId; + connectionHandleId: ElementId | null; connectionHandleType: HandleType; connectionPositionX: number; connectionPositionY: number; @@ -28,6 +29,7 @@ interface ConnectionLineProps { export default ({ connectionNodeId, + connectionHandleId, connectionHandleType, connectionLineStyle, connectionPositionX, @@ -39,10 +41,8 @@ export default ({ CustomConnectionLineComponent, }: ConnectionLineProps) => { const [sourceNode, setSourceNode] = useState(null); - const hasHandleId = connectionNodeId.includes('__'); - const sourceIdSplitted = connectionNodeId.split('__'); - const nodeId = sourceIdSplitted[0]; - const handleId = hasHandleId ? sourceIdSplitted[1] : null; + const nodeId = connectionNodeId; + const handleId = connectionHandleId; useEffect(() => { const nextSourceNode = nodes.find((n) => n.id === nodeId) || null; diff --git a/src/components/Handle/BaseHandle.tsx b/src/components/Handle/BaseHandle.tsx index 97d3ebf4..f6964042 100644 --- a/src/components/Handle/BaseHandle.tsx +++ b/src/components/Handle/BaseHandle.tsx @@ -28,7 +28,7 @@ interface BaseHandleProps { setConnectionNodeId: SetSourceIdFunc; setPosition: (pos: XYPosition) => void; isValidConnection: ValidConnectionFunc; - id?: ElementId | boolean; + id?: ElementId | null; className?: string; style?: CSSProperties; } @@ -42,7 +42,7 @@ type Result = { function onMouseDown( event: ReactMouseEvent, - handleId: ElementId, + handleId: ElementId | null, nodeId: ElementId, setConnectionNodeId: SetSourceIdFunc, setPosition: (pos: XYPosition) => void, @@ -67,7 +67,7 @@ function onMouseDown( x: event.clientX - containerBounds.left, y: event.clientY - containerBounds.top, }); - setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleType: handleType }); + setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType }); if (onConnectStart) { onConnectStart(event, { nodeId, handleType }); @@ -95,7 +95,10 @@ function onMouseDown( if (elementBelow && (elementBelow.classList.contains('target') || elementBelow.classList.contains('source'))) { result.isHoveringHandle = true; - if ((isTarget && elementBelow.classList.contains('source')) || (!isTarget && elementBelow.classList.contains('target'))) { + if ( + (isTarget && elementBelow.classList.contains('source')) || + (!isTarget && elementBelow.classList.contains('target')) + ) { let connection: Connection = { source: null, target: null, sourceHandle: null, targetHandle: null }; if (isTarget) { @@ -105,7 +108,7 @@ function onMouseDown( } else { const targetId = elementBelow.getAttribute('data-nodeid'); const targetHandleId = elementBelow.getAttribute('data-handleid'); - connection = { source: nodeId, sourceHandle: handleId, target: targetId , targetHandle: targetHandleId}; + connection = { source: nodeId, sourceHandle: handleId, target: targetId, targetHandle: targetHandleId }; } const isValid = isValidConnection(connection); @@ -155,7 +158,7 @@ function onMouseDown( } resetRecentHandle(); - setConnectionNodeId({ connectionNodeId: null, connectionHandleType: null }); + setConnectionNodeId({ connectionNodeId: null, connectionHandleId: null, connectionHandleType: null }); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); @@ -176,7 +179,7 @@ const BaseHandle = ({ setConnectionNodeId, setPosition, className, - id = false, + id = null, isValidConnection, ...rest }: BaseHandleProps) => { @@ -192,7 +195,8 @@ const BaseHandle = ({ }, ]); - const handleId = id ? `${id}`: '' + const handleId = id || null; + return (
d.id === handleId); } + if (typeof handle === 'undefined') { + return null; + } + return handle; } @@ -132,20 +136,22 @@ function renderEdge( selectedElements: Elements | null, elementsSelectable: boolean ) { - const sourceId = edge.source - const sourceHandleId = edge.sourceHandle! - const targetId = edge.target - const targetHandleId = edge.targetHandle! + const sourceId = edge.source; + const sourceHandleId = edge.sourceHandle || null; + const targetId = edge.target; + const targetHandleId = edge.targetHandle || null; const sourceNode = nodes.find((n) => n.id === sourceId); const targetNode = nodes.find((n) => n.id === targetId); if (!sourceNode) { - throw new Error(`couldn't create edge for source id: ${sourceId}`); + console.warn(`couldn't create edge for source id: ${sourceId}`); + return null; } if (!targetNode) { - throw new Error(`couldn't create edge for target id: ${targetId}`); + console.warn(`couldn't create edge for target id: ${targetId}`); + return null; } if (!sourceNode.__rf.width || !sourceNode.__rf.height) { @@ -159,6 +165,16 @@ function renderEdge( const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom; const targetPosition = targetHandle ? targetHandle.position : Position.Top; + if (!sourceHandle) { + console.warn(`couldn't create edge for source handle id: ${sourceHandleId}`); + return null; + } + + if (!targetHandle) { + console.warn(`couldn't create edge for source handle id: ${targetHandleId}`); + return null; + } + const { sourceX, sourceY, targetX, targetY } = getEdgePositions( sourceNode, sourceHandle, @@ -210,6 +226,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { const edges = useStoreState((state) => state.edges); const nodes = useStoreState((state) => state.nodes); const connectionNodeId = useStoreState((state) => state.connectionNodeId); + const connectionHandleId = useStoreState((state) => state.connectionHandleId); const connectionHandleType = useStoreState((state) => state.connectionHandleType); const connectionPosition = useStoreState((state) => state.connectionPosition); const selectedElements = useStoreState((state) => state.selectedElements); @@ -236,6 +253,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { { + setConnectionNodeId: action((state, { connectionNodeId, connectionHandleId, connectionHandleType }) => { state.connectionNodeId = connectionNodeId; + state.connectionHandleId = connectionHandleId; state.connectionHandleType = connectionHandleType; }), diff --git a/src/types/index.ts b/src/types/index.ts index 65d90984..adfad3e1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -60,8 +60,8 @@ export interface Edge { type?: string; source: ElementId; target: ElementId; - sourceHandle?: ElementId; - targetHandle?: ElementId; + sourceHandle?: ElementId | null; + targetHandle?: ElementId | null; label?: string; labelStyle?: CSSProperties; labelShowBg?: boolean; @@ -280,6 +280,7 @@ export type OnConnectEndFunc = (event: MouseEvent) => void; export type SetConnectionId = { connectionNodeId: ElementId | null; + connectionHandleId: ElementId | null; connectionHandleType: HandleType | null; }; @@ -294,7 +295,7 @@ export interface HandleProps { isConnectable?: boolean; onConnect?: OnConnectFunc; isValidConnection?: (connection: Connection) => boolean; - id?: string; + id?: ElementId; style?: CSSProperties; className?: string; } diff --git a/src/utils/graph.ts b/src/utils/graph.ts index bd170c6d..d39c8f7e 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -40,43 +40,29 @@ export const removeElements = (elementsToRemove: Elements, elements: Elements): }); }; -const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection): ElementId => `reactflow__edge-${source}${sourceHandle}-${target}${targetHandle}`; - -const existingConnection = (edge: Edge, elements: Elements) => { - for (const element of elements) { - if (isEdge(element)) { - if (element.source === edge.source && element.sourceHandle === edge.sourceHandle && element.target === edge.target && element.targetHandle === edge.targetHandle) { - return true - } - } - } - return false -} +const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection): ElementId => + `reactflow__edge-${source}${sourceHandle}-${target}${targetHandle}`; +const connectionExists = (edge: Edge, elements: Elements) => { + return elements.some( + (el) => + isEdge(el) && + el.source === edge.source && + el.target === edge.target && + (el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) && + (el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle)) + ); +}; export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elements => { if (!edgeParams.source || !edgeParams.target) { - throw new Error("Can't create edge. An edge needs a source and a target."); + console.warn("Can't create edge. An edge needs a source and a target."); + return elements; } - // make sure that there is node with the target and one with the source id - [edgeParams.source, edgeParams.target].forEach((id) => { - if (!elements.find((e) => isNode(e) && e.id === id)) { - throw new Error(`Can't create edge. Node with id=${id} does not exist.`); - } - }); - - // make sure that the handles exists in each node - const handleElements = Array.from(document.getElementsByClassName("react-flow__handle")) as HTMLDivElement[] - [[edgeParams.source, edgeParams.sourceHandle], [edgeParams.target, edgeParams.targetHandle]].forEach(([nodeId, handleId]) => { - if (!handleElements.find((he) => he.getAttribute('data-nodeid') === nodeId && he.getAttribute('data-handleid') === handleId)) { - throw new Error(`Can't create edge. Handle with id=${handleId} does not exist within Node with id=${nodeId}.`); - } - }) - - let edge: Edge + let edge: Edge; if (isEdge(edgeParams)) { - edge = {...edgeParams} + edge = { ...edgeParams }; } else { edge = { ...edgeParams, @@ -84,8 +70,11 @@ export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elem } as Edge; } - if (existingConnection(edge, elements)) {return elements} - return elements.concat(edge) + if (connectionExists(edge, elements)) { + return elements; + } + + return elements.concat(edge); }; export const pointToRendererPoint = ( @@ -133,8 +122,8 @@ export const parseElement = (element: Node | Edge): Node | Edge => { ...element, source: element.source.toString(), target: element.target.toString(), - sourceHandle: element.sourceHandle ? element.sourceHandle.toString() : '', - targetHandle: element.targetHandle ? element.targetHandle.toString() : '', + sourceHandle: element.sourceHandle ? element.sourceHandle.toString() : null, + targetHandle: element.targetHandle ? element.targetHandle.toString() : null, id: element.id.toString(), type: element.type || 'default', };