diff --git a/cypress/integration/flow/graph-utils.spec.js b/cypress/integration/flow/graph-utils.spec.js index e582231b..e6df1455 100644 --- a/cypress/integration/flow/graph-utils.spec.js +++ b/cypress/integration/flow/graph-utils.spec.js @@ -1,4 +1,4 @@ -import { isNode, isEdge, getOutgoers, getIncomers, removeElements, addEdge } from '../../../dist/ReactFlow.js'; +import { isNode, isEdge, getOutgoers, getIncomers, addEdge } from '../../../dist/ReactFlow.js'; const nodes = [ { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, @@ -69,66 +69,4 @@ describe('Graph Utils Testing', () => { } }); }); - - describe('tests removeElements function', () => { - it('removes a node', () => { - const nextElements = removeElements([nodes[0]], elements); - - const nextNodes = nextElements.filter((e) => isNode(e)); - const nextEdges = nextElements.filter((e) => isEdge(e)); - - expect(nextNodes.length).to.be.equal(nodes.length - 1); - expect(nextEdges.length).to.be.equal(edges.length - 2); - }); - - it('removes multiple nodes', () => { - const elementsToRemove = [nodes[0], nodes[1]]; - const nextElements = removeElements(elementsToRemove, elements); - const nextNodes = nextElements.filter((e) => isNode(e)); - const nextEdges = nextElements.filter((e) => isEdge(e)); - - expect(nextNodes.length).to.be.equal(nodes.length - 2); - expect(nextEdges.length).to.be.equal(0); - }); - - it('removes no node', () => { - const nextElementsNoRemove = removeElements([], elements); - expect(nextElementsNoRemove.length).to.be.equal(elements.length); - }); - - it('tries to removes node that does not exist', () => { - const nextElementsNoRemove = removeElements([{ id: 'id-that-does-not-exist' }], elements); - expect(nextElementsNoRemove.length).to.be.equal(elements.length); - }); - - it('removes an edge', () => { - const nextElements = removeElements([edges[0]], elements); - - const nextNodes = nextElements.filter((e) => isNode(e)); - const nextEdges = nextElements.filter((e) => isEdge(e)); - - expect(nextNodes.length).to.be.equal(nodes.length); - expect(nextEdges.length).to.be.equal(edges.length - 1); - }); - - it('removes multiple edges', () => { - const nextElements = removeElements([edges[0], edges[1]], elements); - - const nextNodes = nextElements.filter((e) => isNode(e)); - const nextEdges = nextElements.filter((e) => isEdge(e)); - - expect(nextNodes.length).to.be.equal(nodes.length); - expect(nextEdges.length).to.be.equal(edges.length - 2); - }); - - it('removes node and edge', () => { - const nextElements = removeElements([nodes[0], edges[0]], elements); - - const nextNodes = nextElements.filter((e) => isNode(e)); - const nextEdges = nextElements.filter((e) => isEdge(e)); - - expect(nextNodes.length).to.be.equal(nodes.length - 1); - expect(nextEdges.length).to.be.equal(edges.length - 2); - }); - }); }); diff --git a/example/src/Basic/DebugNode.tsx b/example/src/Basic/DebugNode.tsx new file mode 100644 index 00000000..f7cc4b36 --- /dev/null +++ b/example/src/Basic/DebugNode.tsx @@ -0,0 +1,27 @@ +import { memo, FC, CSSProperties } from 'react'; + +import { Handle, NodeProps, Position } from 'react-flow-renderer'; + +const infoStyle: CSSProperties = { fontSize: 11 }; +const idStyle: CSSProperties = { + fontSize: 10, + color: '#888899', + position: 'absolute', + top: 2, + left: 2, +}; + +const ColorSelectorNode: FC = ({ zIndex, xPos, yPos, id }) => { + return ( + <> + +
{id}
+
+ x:{Math.round(xPos || 0)} y:{Math.round(yPos || 0)} z:{zIndex} +
+ + + ); +}; + +export default memo(ColorSelectorNode); diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index ae003545..2335443f 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -1,49 +1,126 @@ -import React, { useState, MouseEvent } from 'react'; +import { useState, MouseEvent, useCallback } from 'react'; import ReactFlow, { - removeElements, addEdge, - isNode, Background, - Elements, - BackgroundVariant, - FlowElement, + applyNodeChanges, + applyEdgeChanges, + MiniMap, + Controls, Node, Edge, - Connection, + NodeChange, + EdgeChange, OnLoadParams, + Connection, } from 'react-flow-renderer'; +import DebugNode from './DebugNode'; const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); -const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); +const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); +const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge); -const initialElements: Elements = [ +const initialNodes: Node[] = [ { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, + { + id: '4', + data: { label: 'Node 4' }, + position: { x: 100, y: 200 }, + className: 'light', + style: { backgroundColor: 'rgba(255,50, 50, 0.5)', width: 500, height: 300 }, + }, + { + id: '4a', + data: { label: 'Node 4a' }, + position: { x: 15, y: 15 }, + className: 'light', + parentNode: '4', + extent: 'parent', + }, + { + id: '4b', + data: { label: 'Node 4b' }, + position: { x: 150, y: 50 }, + className: 'light', + style: { backgroundColor: 'rgba(50, 50, 255, 0.5)', height: 200, width: 300 }, + parentNode: '4', + }, + { + id: '4b1', + data: { label: 'Node 4b1' }, + position: { x: 20, y: 20 }, + className: 'light', + parentNode: '4b', + }, + { + id: '4b2', + data: { label: 'Node 4b2' }, + position: { x: 100, y: 100 }, + className: 'light', + parentNode: '4b', + }, + { + id: '5', + data: { label: 'Node 5' }, + position: { x: 650, y: 250 }, + className: 'light', + style: { backgroundColor: 'rgba(20 ,200, 255, 1.5)', width: 400, height: 150 }, + zIndex: 1000, + }, + { + id: '5a', + data: { label: 'Node 5a' }, + position: { x: 25, y: 50 }, + className: 'light', + parentNode: '5', + }, + { + id: '5b', + data: { label: 'Node 5b' }, + position: { x: 225, y: 50 }, + className: 'light', + parentNode: '5', + }, { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, - { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' }, +]; + +const initialEdges: Edge[] = [ { id: 'e1-2', source: '1', target: '2', animated: true }, { id: 'e1-3', source: '1', target: '3' }, + { id: 'e3-4', source: '3', target: '4', zIndex: 100 }, + { id: 'e3-4b', source: '3', target: '4b' }, + { id: 'e4a-4b1', source: '4a', target: '4b1' }, + { id: 'e4a-4b2', source: '4a', target: '4b2', zIndex: 100 }, + { id: 'e4b1-4b2', source: '4b1', target: '4b2' }, + { id: '3-5', source: '3', target: '5' }, ]; +const nodeTypes = { + default: DebugNode, +}; + const BasicFlow = () => { const [rfInstance, setRfInstance] = useState(null); - const [elements, setElements] = useState(initialElements); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - const onConnect = (params: Edge | Connection) => setElements((els) => addEdge(params, els)); - const onLoad = (reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance); + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState(initialEdges); + + const onConnect = useCallback((params: Edge | Connection) => { + setEdges((eds) => { + return addEdge(params, eds); + }); + }, []); + const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []); const updatePos = () => { - setElements((elms) => { - return elms.map((el) => { - if (isNode(el)) { - el.position = { - x: Math.random() * 400, - y: Math.random() * 400, - }; - } + setNodes((nds) => { + return nds.map((n) => { + n.position = { + x: Math.random() * 400, + y: Math.random() * 400, + }; - return el; + return n; }); }); }; @@ -52,31 +129,53 @@ const BasicFlow = () => { const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 }); const toggleClassnames = () => { - setElements((elms) => { - return elms.map((el) => { - if (isNode(el)) { - el.className = el.className === 'light' ? 'dark' : 'light'; - } - - return el; + setNodes((nds) => { + return nds.map((n) => { + n.className = n.className === 'light' ? 'dark' : 'light'; + return n; }); }); }; + const toggleChildNodes = () => { + setNodes((nds) => { + return nds.map((n) => { + n.hidden = !!n.parentNode && !n.hidden; + return n; + }); + }); + }; + + const onNodesChange = useCallback((changes: NodeChange[]) => { + console.log('node change', changes); + setNodes((ns) => applyNodeChanges(changes, ns)); + }, []); + + const onEdgesChange = useCallback((changes: EdgeChange[]) => { + setEdges((es) => applyEdgeChanges(changes, es)); + }, []); + return ( - + + +
+
diff --git a/example/src/CustomNode/index.tsx b/example/src/CustomNode/index.tsx index 804aabea..9f444c27 100644 --- a/example/src/CustomNode/index.tsx +++ b/example/src/CustomNode/index.tsx @@ -1,27 +1,30 @@ -import React, { useState, useEffect, MouseEvent } from 'react'; +import { useState, useEffect, MouseEvent, useCallback } from 'react'; import { ChangeEvent } from 'react'; import ReactFlow, { - isEdge, - removeElements, addEdge, MiniMap, Controls, Node, - FlowElement, OnLoadParams, - Elements, Position, SnapGrid, Connection, Edge, + NodeChange, + applyNodeChanges, + applyEdgeChanges, + EdgeChange, } from 'react-flow-renderer'; import ColorSelectorNode from './ColorSelectorNode'; -const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance); +const onLoad = (reactFlowInstance: OnLoadParams) => { + console.log('flow loaded:', reactFlowInstance); + reactFlowInstance.fitView(); +}; const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); -const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); +const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const initBgColor = '#1A192B'; @@ -32,15 +35,16 @@ const nodeTypes = { }; const CustomNodeFlow = () => { - const [elements, setElements] = useState([]); + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); const [bgColor, setBgColor] = useState(initBgColor); useEffect(() => { const onChange = (event: ChangeEvent) => { - setElements((els) => - els.map((e) => { - if (isEdge(e) || e.id !== '2') { - return e; + setNodes((nds) => + nds.map((node) => { + if (node.id !== '2') { + return node; } const color = event.target.value; @@ -48,9 +52,9 @@ const CustomNodeFlow = () => { setBgColor(color); return { - ...e, + ...node, data: { - ...e.data, + ...node.data, color, }, }; @@ -58,7 +62,7 @@ const CustomNodeFlow = () => { ); }; - setElements([ + setNodes([ { id: '1', type: 'input', @@ -87,22 +91,33 @@ const CustomNodeFlow = () => { position: { x: 550, y: 100 }, targetPosition: Position.Left, }, + ]); + setEdges([ { id: 'e1-2', source: '1', target: '2', 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' } }, ]); }, []); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const onConnect = (params: Connection | Edge) => - setElements((els) => addEdge({ ...params, animated: true, style: { stroke: '#fff' } }, els)); + setEdges((eds) => addEdge({ ...params, animated: true, style: { stroke: '#fff' } }, eds)); + + const onNodesChange = useCallback((changes: NodeChange[]) => { + setNodes((ns) => applyNodeChanges(changes, ns)); + }, []); + + const onEdgesChange = useCallback((changes: EdgeChange[]) => { + setEdges((es) => applyEdgeChanges(changes, es)); + }, []); return ( = ({ const targetNode = { id: 'connection-target', - __rf: { width: 1, height: 1, position: { x: targetX, y: targetY } }, + width: 1, + height: 1, + position: { x: targetX, y: targetY }, } as Node; const { sx, sy } = getEdgeParams(sourceNode, targetNode); diff --git a/example/src/FloatingEdges/FloatingEdge.tsx b/example/src/FloatingEdges/FloatingEdge.tsx index b03241d1..1812ee2b 100644 --- a/example/src/FloatingEdges/FloatingEdge.tsx +++ b/example/src/FloatingEdges/FloatingEdge.tsx @@ -1,13 +1,15 @@ import { FC, useMemo, CSSProperties } from 'react'; -import { EdgeProps, useStoreState, getBezierPath } from 'react-flow-renderer'; +import { EdgeProps, useStore, getBezierPath, ReactFlowState } from 'react-flow-renderer'; import { getEdgeParams } from './utils'; -const FloatingEdge: FC = ({ id, source, target, style }) => { - const nodes = useStoreState((state) => state.nodes); +const nodeSelector = (s: ReactFlowState) => s.nodeInternals; - const sourceNode = useMemo(() => nodes.find((n) => n.id === source), [source, nodes]); - const targetNode = useMemo(() => nodes.find((n) => n.id === target), [target, nodes]); +const FloatingEdge: FC = ({ id, source, target, style }) => { + const nodeInternals = useStore(nodeSelector); + + const sourceNode = useMemo(() => nodeInternals.get(source), [source, nodeInternals]); + const targetNode = useMemo(() => nodeInternals.get(target), [target, nodeInternals]); if (!sourceNode || !targetNode) { return null; diff --git a/example/src/FloatingEdges/index.tsx b/example/src/FloatingEdges/index.tsx index f1902515..8ec3ebc2 100644 --- a/example/src/FloatingEdges/index.tsx +++ b/example/src/FloatingEdges/index.tsx @@ -1,14 +1,17 @@ -import React, { useState } from 'react'; +import { useState, useCallback } from 'react'; import ReactFlow, { - removeElements, addEdge, Background, OnLoadParams, EdgeTypesType, - Elements, + Node, Connection, Edge, + applyNodeChanges, + applyEdgeChanges, + NodeChange, + EdgeChange, } from 'react-flow-renderer'; import './style.css'; @@ -19,24 +22,35 @@ import { createElements } from './utils'; const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView(); -const initialElements: Elements = createElements(); +const { nodes: initialNodes, edges: initialEdges } = createElements(); const edgeTypes: EdgeTypesType = { floating: FloatingEdge, }; -const NodeAsHandleFlow = () => { - const [elements, setElements] = useState(initialElements); +const FloatingEdges = () => { + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState(initialEdges); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = useCallback((params: Edge | Connection) => { + setEdges((eds) => addEdge(params, eds)); + }, []); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge({ ...params, type: 'floating' }, els)); + const onNodesChange = useCallback((changes: NodeChange[]) => { + setNodes((ns) => applyNodeChanges(changes, ns)); + }, []); + + const onEdgesChange = useCallback((changes: EdgeChange[]) => { + setEdges((es) => applyEdgeChanges(changes, es)); + }, []); return (
{ ); }; -export default NodeAsHandleFlow; +export default FloatingEdges; diff --git a/example/src/FloatingEdges/utils.ts b/example/src/FloatingEdges/utils.ts index c8962b72..eb8797d2 100644 --- a/example/src/FloatingEdges/utils.ts +++ b/example/src/FloatingEdges/utils.ts @@ -1,18 +1,19 @@ -import { Position, ArrowHeadType, Node, XYPosition } from 'react-flow-renderer'; +import { Position, ArrowHeadType, XYPosition, Node, Edge } from 'react-flow-renderer'; // this helper function returns the intersection point // of the line between the center of the intersectionNode and the target node function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition { // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a + const { width: intersectionNodeWidth, height: intersectionNodeHeight, position: intersectionNodePosition, - } = intersectionNode.__rf; - const targetPosition = targetNode.__rf.position; + } = intersectionNode; + const targetPosition = targetNode.position; - const w = intersectionNodeWidth / 2; - const h = intersectionNodeHeight / 2; + const w = (intersectionNodeWidth ?? 0) / 2; + const h = (intersectionNodeHeight ?? 0) / 2; const x2 = intersectionNodePosition.x + w; const y2 = intersectionNodePosition.y + h; @@ -32,7 +33,7 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPositi // returns the position (top,right,bottom or right) passed node compared to the intersection point function getEdgePosition(node: Node, intersectionPoint: XYPosition) { - const n = { ...node.__rf.position, ...node.__rf }; + const n = { ...node.position, ...node }; const nx = Math.round(n.x); const ny = Math.round(n.y); const px = Math.round(intersectionPoint.x); @@ -41,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) { if (px <= nx + 1) { return Position.Left; } - if (px >= nx + n.width - 1) { + if (px >= nx + (n.width ?? 0) - 1) { return Position.Right; } if (py <= ny + 1) { return Position.Top; } - if (py >= n.y + n.height - 1) { + if (py >= n.y + (n.height ?? 0) - 1) { return Position.Bottom; } @@ -72,11 +73,18 @@ export function getEdgeParams(source: Node, target: Node) { }; } -export function createElements() { - const elements = []; +type NodesAndEdges = { + nodes: Node[]; + edges: Edge[]; +}; + +export function createElements(): NodesAndEdges { + const nodes: Node[] = []; + const edges: Edge[] = []; + const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 }; - elements.push({ id: 'target', data: { label: 'Target' }, position: center }); + nodes.push({ id: 'target', data: { label: 'Target' }, position: center }); for (let i = 0; i < 8; i++) { const degrees = i * (360 / 8); @@ -84,16 +92,15 @@ export function createElements() { const x = 250 * Math.cos(radians) + center.x; const y = 250 * Math.sin(radians) + center.y; - elements.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } }); + nodes.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } }); - elements.push({ + edges.push({ id: `edge-${i}`, target: 'target', source: `${i}`, type: 'floating', - arrowHeadType: ArrowHeadType.Arrow, }); } - return elements; + return { nodes, edges }; } diff --git a/example/src/Hidden/index.tsx b/example/src/Hidden/index.tsx index 9869b2bc..4ac8eaa4 100644 --- a/example/src/Hidden/index.tsx +++ b/example/src/Hidden/index.tsx @@ -1,34 +1,43 @@ -import React, { useState } from 'react'; +import { useState, useCallback } from 'react'; import { useEffect } from 'react'; -import ReactFlow, { addEdge, MiniMap, Controls, Connection, Edge, Elements } from 'react-flow-renderer'; +import ReactFlow, { addEdge, MiniMap, Controls, Connection, Edge, Node } from 'react-flow-renderer'; -const initialElements: Elements = [ - { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, - { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, - { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, - { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, +const initialNodes: Node[] = [ + { id: '1', type: 'input', hidden: true, data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, + { id: '2', hidden: true, data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, + { id: '3', hidden: true, data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, + { id: '4', hidden: true, data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, +]; + +const initialEdges: Edge[] = [ { id: 'e1-2', source: '1', target: '2' }, { id: 'e1-3', source: '1', target: '3' }, { id: 'e3-4', source: '3', target: '4' }, ]; +const setHidden = (hidden: boolean) => (els: any[]) => + els.map((e: any) => { + e.hidden = hidden; + return e; + }); + const HiddenFlow = () => { - const [elements, setElements] = useState(initialElements); - const [isHidden, setIsHidden] = useState(false); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState(initialEdges); + const [isHidden, setIsHidden] = useState(true); + + const onConnect = useCallback((params: Edge | Connection) => { + setEdges((eds) => addEdge(params, eds)); + }, []); useEffect(() => { - setElements((els) => - els.map((e) => { - e.isHidden = isHidden; - return e; - }) - ); + setNodes(setHidden(isHidden)); + setNodes(setHidden(isHidden)); }, [isHidden]); return ( - + diff --git a/example/src/Layouting/index.tsx b/example/src/Layouting/index.tsx index 34ee23e6..56484640 100644 --- a/example/src/Layouting/index.tsx +++ b/example/src/Layouting/index.tsx @@ -1,74 +1,87 @@ -import React, { useState } from 'react'; +import { useState, useCallback } from 'react'; import ReactFlow, { ReactFlowProvider, addEdge, - removeElements, + applyNodeChanges, + applyEdgeChanges, Controls, - isNode, - Elements, + NodeChange, + EdgeChange, + Node, Connection, Edge, - NodeExtent, + CoordinateExtent, Position, } from 'react-flow-renderer'; import dagre from 'dagre'; -import initialElements from './initial-elements'; +import initialNodesAndEdges from './initial-elements'; import './layouting.css'; const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); -const nodeExtent: NodeExtent = [ +const nodeExtent: CoordinateExtent = [ [0, 0], [1000, 1000], ]; const LayoutFlow = () => { - const [elements, setElements] = useState(initialElements); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const [nodes, setNodes] = useState(initialNodesAndEdges.nodes); + const [edges, setEdges] = useState(initialNodesAndEdges.edges); + + const onConnect = useCallback((params: Edge | Connection) => { + setEdges((eds) => { + return addEdge(params, eds); + }); + }, []); const onLayout = (direction: string) => { const isHorizontal = direction === 'LR'; dagreGraph.setGraph({ rankdir: direction }); - elements.forEach((el) => { - if (isNode(el)) { - dagreGraph.setNode(el.id, { width: 150, height: 50 }); - } else { - dagreGraph.setEdge(el.source, el.target); - } + nodes.forEach((node) => { + dagreGraph.setNode(node.id, { width: 150, height: 50 }); + }); + + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); }); dagre.layout(dagreGraph); - const layoutedElements = elements.map((el) => { - if (isNode(el)) { - const nodeWithPosition = dagreGraph.node(el.id); - el.targetPosition = isHorizontal ? Position.Left : Position.Top; - el.sourcePosition = isHorizontal ? Position.Right : Position.Bottom; - // we need to pass a slightly different position in order to notify react flow about the change - // @TODO how can we change the position handling so that we dont need this hack? - el.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y }; - } + const layoutedNodes = nodes.map((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + node.targetPosition = isHorizontal ? Position.Left : Position.Top; + node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom; + // we need to pass a slightly different position in order to notify react flow about the change + // @TODO how can we change the position handling so that we dont need this hack? + node.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y }; - return el; + return node; }); - setElements(layoutedElements); + setNodes(layoutedNodes); }; + const onNodesChange = useCallback((changes: NodeChange[]) => setNodes((ns) => applyNodeChanges(changes, ns)), []); + + const onEdgesChange = useCallback((changes: EdgeChange[]) => { + setEdges((es) => applyEdgeChanges(changes, es)); + }, []); + return (
onLayout('TB')} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} > diff --git a/example/src/Layouting/initial-elements.ts b/example/src/Layouting/initial-elements.ts index 404b1b8b..ce763320 100644 --- a/example/src/Layouting/initial-elements.ts +++ b/example/src/Layouting/initial-elements.ts @@ -1,8 +1,8 @@ -import { Elements, XYPosition } from 'react-flow-renderer'; +import { Node, Edge, XYPosition } from 'react-flow-renderer'; const position: XYPosition = { x: 0, y: 0 }; -const elements: Elements = [ +const nodes: Node[] = [ { id: '1', type: 'input', @@ -56,6 +56,9 @@ const elements: Elements = [ position, }, { id: '7', type: 'output', data: { label: 'output' }, position: { x: 400, y: 450 } }, +]; + +const edges: Edge[] = [ { id: 'e12', source: '1', target: '2', type: 'smoothstep' }, { id: 'e13', source: '1', target: '3', type: 'smoothstep' }, { id: 'e22a', source: '2', target: '2a', type: 'smoothstep' }, @@ -68,4 +71,6 @@ const elements: Elements = [ { id: 'e57', source: '5', target: '7', type: 'smoothstep' }, ]; -export default elements; +const nodesAndEdges = { nodes, edges }; + +export default nodesAndEdges; diff --git a/example/src/NestedNodes/index.tsx b/example/src/NestedNodes/index.tsx new file mode 100644 index 00000000..ed445a64 --- /dev/null +++ b/example/src/NestedNodes/index.tsx @@ -0,0 +1,186 @@ +import { useState, MouseEvent, useCallback } from 'react'; + +import ReactFlow, { + addEdge, + Background, + applyNodeChanges, + applyEdgeChanges, + MiniMap, + Controls, + Node, + Edge, + NodeChange, + EdgeChange, + OnLoadParams, + Connection, +} from 'react-flow-renderer'; + +const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); +const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); +const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge); + +const initialNodes: Node[] = [ + { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, + { + id: '2', + data: { label: 'Node 2' }, + position: { x: 100, y: 100 }, + className: 'light', + style: { backgroundColor: 'rgba(255, 0, 0, 0.8)', width: 200, height: 200 }, + }, + { + id: '2a', + data: { label: 'Node 2a' }, + position: { x: 10, y: 50 }, + parentNode: '2', + }, + { id: '3', data: { label: 'Node 3' }, position: { x: 320, y: 100 }, className: 'light' }, + { + id: '4', + data: { label: 'Node 4' }, + position: { x: 320, y: 200 }, + className: 'light', + style: { backgroundColor: 'rgba(255, 0, 0, 0.7)', width: 300, height: 300 }, + }, + { + id: '4a', + data: { label: 'Node 4a' }, + position: { x: 15, y: 65 }, + className: 'light', + parentNode: '4', + extent: 'parent', + }, + { + id: '4b', + data: { label: 'Node 4b' }, + position: { x: 15, y: 120 }, + className: 'light', + style: { backgroundColor: 'rgba(255, 0, 255, 0.7)', height: 150, width: 270 }, + parentNode: '4', + }, + { + id: '4b1', + data: { label: 'Node 4b1' }, + position: { x: 20, y: 40 }, + className: 'light', + parentNode: '4b', + }, + { + id: '4b2', + data: { label: 'Node 4b2' }, + position: { x: 100, y: 100 }, + className: 'light', + parentNode: '4b', + }, +]; + +const initialEdges: Edge[] = [ + { id: 'e1-2', source: '1', target: '2', animated: true }, + { id: 'e1-3', source: '1', target: '3' }, + { id: 'e2a-4a', source: '2a', target: '4a' }, + { id: 'e3-4', source: '3', target: '4' }, + { id: 'e3-4b', source: '3', target: '4b' }, + { id: 'e4a-4b1', source: '4a', target: '4b1' }, + { id: 'e4a-4b2', source: '4a', target: '4b2' }, + { id: 'e4b1-4b2', source: '4b1', target: '4b2' }, +]; + +function shuffle() { + return Math.random() - Math.random(); +} + +const NestedFlow = () => { + const [rfInstance, setRfInstance] = useState(null); + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState(initialEdges); + + const onConnect = useCallback((params: Edge | Connection) => { + setEdges((eds) => { + return addEdge(params, eds); + }); + }, []); + const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []); + + const updatePos = () => { + setNodes((nds) => { + return nds.map((n) => { + n.position = { + x: Math.random() * 400, + y: Math.random() * 400, + }; + + return n; + }); + }); + }; + + const logToObject = () => console.log(rfInstance?.toObject()); + const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 }); + + const toggleClassnames = () => { + setNodes((nds) => { + return nds.map((n) => { + n.className = n.className === 'light' ? 'dark' : 'light'; + return n; + }); + }); + }; + + const toggleChildNodes = () => { + setNodes((nds) => { + return nds.map((n) => { + n.hidden = !!n.parentNode && !n.hidden; + return n; + }); + }); + }; + + const onNodesChange = useCallback((changes: NodeChange[]) => { + setNodes((ns) => applyNodeChanges(changes, ns)); + }, []); + + const onEdgesChange = useCallback((changes: EdgeChange[]) => { + setEdges((es) => applyEdgeChanges(changes, es)); + }, []); + + return ( + + + + + +
+ + + + + +
+
+ ); +}; + +export default NestedFlow; diff --git a/example/src/Stress/index.tsx b/example/src/Stress/index.tsx index b734c6b6..5b97298c 100644 --- a/example/src/Stress/index.tsx +++ b/example/src/Stress/index.tsx @@ -1,58 +1,63 @@ -import React, { useState, CSSProperties } from 'react'; +import { useState, CSSProperties, useCallback } from 'react'; import ReactFlow, { - removeElements, - addEdge, MiniMap, - isNode, Controls, Background, OnLoadParams, - Elements, - Connection, Edge, + Node, + NodeChange, + applyNodeChanges, + Connection, + addEdge, } from 'react-flow-renderer'; -import { getElements } from './utils'; +import { getNodesAndEdges } from './utils'; const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 4 }; const onLoad = (reactFlowInstance: OnLoadParams) => { reactFlowInstance.fitView(); - console.log(reactFlowInstance.getElements()); + console.log(reactFlowInstance.getNodes()); }; -const initialElements: Elements = getElements(30, 30); +const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(30, 30); const StressFlow = () => { - const [elements, setElements] = useState(initialElements); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState(initialEdges); + const onConnect = useCallback((params: Edge | Connection) => { + setEdges((eds) => addEdge(params, eds)); + }, []); const updatePos = () => { - setElements((elms) => { - return elms.map((el) => { - if (isNode(el)) { - return { - ...el, - position: { - x: Math.random() * window.innerWidth, - y: Math.random() * window.innerHeight, - }, - }; - } - - return el; + setNodes((nds) => { + return nds.map((n) => { + return { + ...n, + position: { + x: Math.random() * window.innerWidth, + y: Math.random() * window.innerHeight, + }, + }; }); }); }; const updateElements = () => { const grid = Math.ceil(Math.random() * 10); - setElements(getElements(grid, grid)); + const initialElements = getNodesAndEdges(grid, grid); + setNodes(initialElements.nodes); + setEdges(initialElements.edges); }; + const onNodesChange = useCallback((changes: NodeChange[]) => { + console.log('node change', changes); + + setNodes((ns) => applyNodeChanges(changes, ns)); + }, []); + return ( - + diff --git a/example/src/Stress/utils.ts b/example/src/Stress/utils.ts index a37eb8de..c5c6e307 100644 --- a/example/src/Stress/utils.ts +++ b/example/src/Stress/utils.ts @@ -1,7 +1,13 @@ -import { Elements } from 'react-flow-renderer'; +import { Node, Edge } from 'react-flow-renderer'; -export function getElements(xElements: number = 10, yElements: number = 10): Elements { - const initialElements = []; +type ElementsCollection = { + nodes: Node[]; + edges: Edge[]; +}; + +export function getNodesAndEdges(xElements: number = 10, yElements: number = 10): ElementsCollection { + const initialNodes = []; + const initialEdges: Edge[] = []; let nodeId = 1; let recentNodeId = null; @@ -15,10 +21,10 @@ export function getElements(xElements: number = 10, yElements: number = 10): Ele data, position, }; - initialElements.push(node); + initialNodes.push(node); if (recentNodeId && nodeId <= xElements * yElements) { - initialElements.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() }); + initialEdges.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() }); } recentNodeId = nodeId; @@ -26,5 +32,8 @@ export function getElements(xElements: number = 10, yElements: number = 10): Ele } } - return initialElements; + return { + nodes: initialNodes, + edges: initialEdges, + }; } diff --git a/example/src/UpdateNode/index.tsx b/example/src/UpdateNode/index.tsx index a7cce8f2..e38babad 100644 --- a/example/src/UpdateNode/index.tsx +++ b/example/src/UpdateNode/index.tsx @@ -1,64 +1,71 @@ -import React, { useEffect, useState } from 'react'; -import ReactFlow, { Elements } from 'react-flow-renderer'; +import { useEffect, useState, useCallback } from 'react'; +import ReactFlow, { Node, Edge, applyNodeChanges, NodeChange } from 'react-flow-renderer'; import './updatenode.css'; -const initialElements: Elements = [ +const initialNodes: Node[] = [ { id: '1', data: { label: '-' }, position: { x: 100, y: 100 } }, { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } }, - { id: 'e1-2', source: '1', target: '2' }, ]; +const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }]; + const UpdateNode = () => { - const [elements, setElements] = useState(initialElements); + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState(initialEdges); + const [nodeName, setNodeName] = useState('Node 1'); const [nodeBg, setNodeBg] = useState('#eee'); const [nodeHidden, setNodeHidden] = useState(false); useEffect(() => { - setElements((els) => - els.map((el) => { - if (el.id === '1') { + setNodes((nds) => + nds.map((n) => { + if (n.id === '1') { // it's important that you create a new object here in order to notify react flow about the change - el.data = { - ...el.data, + n.data = { + ...n.data, label: nodeName, }; } - return el; + return n; }) ); - }, [nodeName, setElements]); + }, [nodeName]); useEffect(() => { - setElements((els) => - els.map((el) => { - if (el.id === '1') { + setNodes((nds) => + nds.map((n) => { + if (n.id === '1') { // it's important that you create a new object here in order to notify react flow about the change - el.style = { ...el.style, backgroundColor: nodeBg }; + n.style = { ...n.style, backgroundColor: nodeBg }; } - return el; + return n; }) ); - }, [nodeBg, setElements]); + }, [nodeBg]); useEffect(() => { - setElements((els) => - els.map((el) => { - if (el.id === '1' || el.id === 'e1-2') { + setNodes((nds) => + nds.map((n) => { + if (n.id === '1' || n.id === 'e1-2') { // when you update a simple type you can just update the value - el.isHidden = nodeHidden; + n.hidden = nodeHidden; } - return el; + return n; }) ); - }, [nodeHidden, setElements]); + }, [nodeHidden]); + + const onNodesChange = useCallback((changes: NodeChange[]) => { + setNodes((ns) => applyNodeChanges(changes, ns)); + }, []); return ( - +
setNodeName(evt.target.value)} /> diff --git a/example/src/index.tsx b/example/src/index.tsx index 8371c0af..45aa0a16 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -1,140 +1,50 @@ -import React, { ChangeEvent } from 'react'; +import { ChangeEvent } from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter as Router, Route, Switch, withRouter } from 'react-router-dom'; -import Overview from './Overview'; import Basic from './Basic'; -import CustomNode from './CustomNode'; -import Stress from './Stress'; -import Interaction from './Interaction'; -import Empty from './Empty'; -import Edges from './Edges'; -import Validation from './Validation'; -import Provider from './Provider'; -import Hidden from './Hidden'; -import EdgeTypes from './EdgeTypes'; -import CustomConnectionLine from './CustomConnectionLine'; -import FloatingEdges from './FloatingEdges'; -import NodeTypeChange from './NodeTypeChange'; -import NodeTypesObjectChange from './NodeTypesObjectChange'; -import UpdatableEdge from './UpdatableEdge'; import UpdateNode from './UpdateNode'; -import SaveRestore from './SaveRestore'; -import DragNDrop from './DragNDrop'; -import Layout from './Layouting'; -import SwitchFlows from './Switch'; -import UseZoomPanHelper from './UseZoomPanHelper'; -import UseUpdateNodeInternals from './UseUpdateNodeInternals'; -import Undirectional from './Undirectional'; -import MultiFlows from './MultiFlows'; -import DragHandle from './DragHandle'; +import Stress from './Stress'; +import CustomNode from './CustomNode'; +import FloatingEdges from './FloatingEdges'; +import Layouting from './Layouting'; +import NestedNodes from './NestedNodes'; +import Hidden from './Hidden'; import './index.css'; const routes = [ { path: '/', - component: Overview, - }, - { - path: '/edges', - component: Edges, - }, - { - path: '/custom-node', - component: CustomNode, - }, - { - path: '/validation', - component: Validation, - }, - { - path: '/provider', - component: Provider, - }, - { - path: '/stress', - component: Stress, - }, - { - path: '/interaction', - component: Interaction, - }, - { - path: '/basic', component: Basic, }, - { - path: '/empty', - component: Empty, - }, - { - path: '/hidden', - component: Hidden, - }, - { - path: '/edge-types', - component: EdgeTypes, - }, - { - path: '/custom-connectionline', - component: CustomConnectionLine, - }, - { - path: '/floating-edges', - component: FloatingEdges, - }, - { - path: '/nodetype-change', - component: NodeTypeChange, - }, - { - path: '/nodetypesobject-change', - component: NodeTypesObjectChange, - }, - { - path: '/updatable-edge', - component: UpdatableEdge, - }, { path: '/update-node', component: UpdateNode, }, { - path: '/save-restore', - component: SaveRestore, + path: '/stress', + component: Stress, }, { - path: '/drag-and-drop', - component: DragNDrop, + path: '/custom-node', + component: CustomNode, + }, + { + path: '/floating-edges', + component: FloatingEdges, }, { path: '/layouting', - component: Layout, + component: Layouting, }, { - path: '/switch', - component: SwitchFlows, + path: '/nested-nodes', + component: NestedNodes, }, { - path: '/usezoompanhelper', - component: UseZoomPanHelper, - }, - { - path: '/useupdatenodeinternals', - component: UseUpdateNodeInternals, - }, - { - path: '/undirectional', - component: Undirectional, - }, - { - path: '/multiflows', - component: MultiFlows, - }, - { - path: '/draghandle', - component: DragHandle, + path: '/hidden', + component: Hidden, }, ]; diff --git a/example/src_oldapi/Basic/index.tsx b/example/src_oldapi/Basic/index.tsx new file mode 100644 index 00000000..19523f9b --- /dev/null +++ b/example/src_oldapi/Basic/index.tsx @@ -0,0 +1,98 @@ +import React, { useState, MouseEvent } from 'react'; + +import ReactFlow, { + removeElements, + addEdge, + isNode, + Background, + Elements, + BackgroundVariant, + FlowElement, + Node, + Edge, + Connection, + OnLoadParams, +} from 'react-flow-renderer'; + +const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); +const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); + +const initialNodes: Node[] = [ + { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, + { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, + { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, + { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' }, +]; + +const initialEdges: Edge[] = [ + { id: 'e1-2', source: '1', target: '2', animated: true }, + { id: 'e1-3', source: '1', target: '3' }, +]; + +const BasicFlow = () => { + const [rfInstance, setRfInstance] = useState(null); + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState(initialEdges); + // const onElementsRemove = (elementsToRemove: Elements) => setNodes((els) => removeElements(elementsToRemove, els)); + // const onConnect = (params: Edge | Connection) => setNodes((els) => addEdge(params, els)); + const onLoad = (reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance); + + const updatePos = () => { + setNodes((nds) => { + return nds.map((n) => { + n.position = { + x: Math.random() * 400, + y: Math.random() * 400, + }; + + return n; + }); + }); + }; + + const logToObject = () => console.log(rfInstance?.toObject()); + const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 }); + + const toggleClassnames = () => { + setNodes((nds) => { + return nds.map((n) => { + n.className = n.className === 'light' ? 'dark' : 'light'; + + return n; + }); + }); + }; + + return ( + + + +
+ + + + +
+
+ ); +}; + +export default BasicFlow; diff --git a/example/src/CustomConnectionLine/ConnectionLine.tsx b/example/src_oldapi/CustomConnectionLine/ConnectionLine.tsx similarity index 100% rename from example/src/CustomConnectionLine/ConnectionLine.tsx rename to example/src_oldapi/CustomConnectionLine/ConnectionLine.tsx diff --git a/example/src/CustomConnectionLine/index.tsx b/example/src_oldapi/CustomConnectionLine/index.tsx similarity index 100% rename from example/src/CustomConnectionLine/index.tsx rename to example/src_oldapi/CustomConnectionLine/index.tsx diff --git a/example/src_oldapi/CustomNode/ColorSelectorNode.tsx b/example/src_oldapi/CustomNode/ColorSelectorNode.tsx new file mode 100644 index 00000000..7ad20340 --- /dev/null +++ b/example/src_oldapi/CustomNode/ColorSelectorNode.tsx @@ -0,0 +1,25 @@ +import React, { memo, FC, CSSProperties } from 'react'; + +import { Handle, Position, NodeProps, Connection, Edge } from 'react-flow-renderer'; + +const targetHandleStyle: CSSProperties = { background: '#555' }; +const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 }; +const sourceHandleStyleB: CSSProperties = { ...targetHandleStyle, bottom: 10, top: 'auto' }; + +const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params); + +const ColorSelectorNode: FC = ({ data, isConnectable }) => { + return ( + <> + +
+ Custom Color Picker Node: {data.color} +
+ + + + + ); +}; + +export default memo(ColorSelectorNode); diff --git a/example/src_oldapi/CustomNode/index.tsx b/example/src_oldapi/CustomNode/index.tsx new file mode 100644 index 00000000..804aabea --- /dev/null +++ b/example/src_oldapi/CustomNode/index.tsx @@ -0,0 +1,135 @@ +import React, { useState, useEffect, MouseEvent } from 'react'; +import { ChangeEvent } from 'react'; + +import ReactFlow, { + isEdge, + removeElements, + addEdge, + MiniMap, + Controls, + Node, + FlowElement, + OnLoadParams, + Elements, + Position, + SnapGrid, + Connection, + Edge, +} from 'react-flow-renderer'; + +import ColorSelectorNode from './ColorSelectorNode'; + +const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance); +const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); +const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); + +const initBgColor = '#1A192B'; + +const connectionLineStyle = { stroke: '#fff' }; +const snapGrid: SnapGrid = [16, 16]; +const nodeTypes = { + selectorNode: ColorSelectorNode, +}; + +const CustomNodeFlow = () => { + const [elements, setElements] = useState([]); + const [bgColor, setBgColor] = useState(initBgColor); + + useEffect(() => { + const onChange = (event: ChangeEvent) => { + setElements((els) => + els.map((e) => { + if (isEdge(e) || e.id !== '2') { + return e; + } + + const color = event.target.value; + + setBgColor(color); + + return { + ...e, + data: { + ...e.data, + color, + }, + }; + }) + ); + }; + + setElements([ + { + id: '1', + type: 'input', + data: { label: 'An input node' }, + position: { x: 0, y: 50 }, + sourcePosition: Position.Right, + }, + { + id: '2', + type: 'selectorNode', + data: { onChange: onChange, color: initBgColor }, + style: { border: '1px solid #777', padding: 10 }, + position: { x: 250, y: 50 }, + }, + { + id: '3', + type: 'output', + data: { label: 'Output A' }, + position: { x: 550, y: 25 }, + targetPosition: Position.Left, + }, + { + id: '4', + type: 'output', + data: { label: 'Output B' }, + position: { x: 550, y: 100 }, + targetPosition: Position.Left, + }, + + { id: 'e1-2', source: '1', target: '2', 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' } }, + ]); + }, []); + + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = (params: Connection | Edge) => + setElements((els) => addEdge({ ...params, animated: true, style: { stroke: '#fff' } }, els)); + + return ( + + { + if (n.type === 'input') return '#0041d0'; + if (n.type === 'selectorNode') return bgColor; + if (n.type === 'output') return '#ff0072'; + + return '#eee'; + }} + nodeColor={(n: Node): string => { + if (n.type === 'selectorNode') return bgColor; + + return '#fff'; + }} + /> + + + ); +}; + +export default CustomNodeFlow; diff --git a/example/src/DragHandle/DragHandleNode.tsx b/example/src_oldapi/DragHandle/DragHandleNode.tsx similarity index 100% rename from example/src/DragHandle/DragHandleNode.tsx rename to example/src_oldapi/DragHandle/DragHandleNode.tsx diff --git a/example/src/DragHandle/index.tsx b/example/src_oldapi/DragHandle/index.tsx similarity index 100% rename from example/src/DragHandle/index.tsx rename to example/src_oldapi/DragHandle/index.tsx diff --git a/example/src/DragNDrop/Sidebar.tsx b/example/src_oldapi/DragNDrop/Sidebar.tsx similarity index 100% rename from example/src/DragNDrop/Sidebar.tsx rename to example/src_oldapi/DragNDrop/Sidebar.tsx diff --git a/example/src/DragNDrop/dnd.css b/example/src_oldapi/DragNDrop/dnd.css similarity index 100% rename from example/src/DragNDrop/dnd.css rename to example/src_oldapi/DragNDrop/dnd.css diff --git a/example/src/DragNDrop/index.tsx b/example/src_oldapi/DragNDrop/index.tsx similarity index 100% rename from example/src/DragNDrop/index.tsx rename to example/src_oldapi/DragNDrop/index.tsx diff --git a/example/src/EdgeTypes/index.tsx b/example/src_oldapi/EdgeTypes/index.tsx similarity index 100% rename from example/src/EdgeTypes/index.tsx rename to example/src_oldapi/EdgeTypes/index.tsx diff --git a/example/src/EdgeTypes/utils.ts b/example/src_oldapi/EdgeTypes/utils.ts similarity index 100% rename from example/src/EdgeTypes/utils.ts rename to example/src_oldapi/EdgeTypes/utils.ts diff --git a/example/src/Edges/CustomEdge.tsx b/example/src_oldapi/Edges/CustomEdge.tsx similarity index 100% rename from example/src/Edges/CustomEdge.tsx rename to example/src_oldapi/Edges/CustomEdge.tsx diff --git a/example/src/Edges/CustomEdge2.tsx b/example/src_oldapi/Edges/CustomEdge2.tsx similarity index 100% rename from example/src/Edges/CustomEdge2.tsx rename to example/src_oldapi/Edges/CustomEdge2.tsx diff --git a/example/src/Edges/index.tsx b/example/src_oldapi/Edges/index.tsx similarity index 100% rename from example/src/Edges/index.tsx rename to example/src_oldapi/Edges/index.tsx diff --git a/example/src/Empty/index.tsx b/example/src_oldapi/Empty/index.tsx similarity index 100% rename from example/src/Empty/index.tsx rename to example/src_oldapi/Empty/index.tsx diff --git a/example/src/Interaction/index.tsx b/example/src_oldapi/Interaction/index.tsx similarity index 100% rename from example/src/Interaction/index.tsx rename to example/src_oldapi/Interaction/index.tsx diff --git a/example/src/MultiFlows/index.tsx b/example/src_oldapi/MultiFlows/index.tsx similarity index 100% rename from example/src/MultiFlows/index.tsx rename to example/src_oldapi/MultiFlows/index.tsx diff --git a/example/src/MultiFlows/multiflows.css b/example/src_oldapi/MultiFlows/multiflows.css similarity index 100% rename from example/src/MultiFlows/multiflows.css rename to example/src_oldapi/MultiFlows/multiflows.css diff --git a/example/src/NodeTypeChange/index.tsx b/example/src_oldapi/NodeTypeChange/index.tsx similarity index 100% rename from example/src/NodeTypeChange/index.tsx rename to example/src_oldapi/NodeTypeChange/index.tsx diff --git a/example/src/NodeTypesObjectChange/index.tsx b/example/src_oldapi/NodeTypesObjectChange/index.tsx similarity index 100% rename from example/src/NodeTypesObjectChange/index.tsx rename to example/src_oldapi/NodeTypesObjectChange/index.tsx diff --git a/example/src/Overview/index.tsx b/example/src_oldapi/Overview/index.tsx similarity index 100% rename from example/src/Overview/index.tsx rename to example/src_oldapi/Overview/index.tsx diff --git a/example/src/Provider/Sidebar.tsx b/example/src_oldapi/Provider/Sidebar.tsx similarity index 100% rename from example/src/Provider/Sidebar.tsx rename to example/src_oldapi/Provider/Sidebar.tsx diff --git a/example/src/Provider/index.tsx b/example/src_oldapi/Provider/index.tsx similarity index 100% rename from example/src/Provider/index.tsx rename to example/src_oldapi/Provider/index.tsx diff --git a/example/src/Provider/provider.css b/example/src_oldapi/Provider/provider.css similarity index 100% rename from example/src/Provider/provider.css rename to example/src_oldapi/Provider/provider.css diff --git a/example/src/SaveRestore/Controls.tsx b/example/src_oldapi/SaveRestore/Controls.tsx similarity index 100% rename from example/src/SaveRestore/Controls.tsx rename to example/src_oldapi/SaveRestore/Controls.tsx diff --git a/example/src/SaveRestore/index.tsx b/example/src_oldapi/SaveRestore/index.tsx similarity index 100% rename from example/src/SaveRestore/index.tsx rename to example/src_oldapi/SaveRestore/index.tsx diff --git a/example/src/SaveRestore/save.css b/example/src_oldapi/SaveRestore/save.css similarity index 100% rename from example/src/SaveRestore/save.css rename to example/src_oldapi/SaveRestore/save.css diff --git a/example/src_oldapi/Stress/index.tsx b/example/src_oldapi/Stress/index.tsx new file mode 100644 index 00000000..b734c6b6 --- /dev/null +++ b/example/src_oldapi/Stress/index.tsx @@ -0,0 +1,70 @@ +import React, { useState, CSSProperties } from 'react'; +import ReactFlow, { + removeElements, + addEdge, + MiniMap, + isNode, + Controls, + Background, + OnLoadParams, + Elements, + Connection, + Edge, +} from 'react-flow-renderer'; + +import { getElements } from './utils'; + +const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 4 }; + +const onLoad = (reactFlowInstance: OnLoadParams) => { + reactFlowInstance.fitView(); + console.log(reactFlowInstance.getElements()); +}; + +const initialElements: Elements = getElements(30, 30); + +const StressFlow = () => { + const [elements, setElements] = useState(initialElements); + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + + const updatePos = () => { + setElements((elms) => { + return elms.map((el) => { + if (isNode(el)) { + return { + ...el, + position: { + x: Math.random() * window.innerWidth, + y: Math.random() * window.innerHeight, + }, + }; + } + + return el; + }); + }); + }; + + const updateElements = () => { + const grid = Math.ceil(Math.random() * 10); + setElements(getElements(grid, grid)); + }; + + return ( + + + + + +
+ + +
+
+ ); +}; + +export default StressFlow; diff --git a/example/src_oldapi/Stress/utils.ts b/example/src_oldapi/Stress/utils.ts new file mode 100644 index 00000000..a37eb8de --- /dev/null +++ b/example/src_oldapi/Stress/utils.ts @@ -0,0 +1,30 @@ +import { Elements } from 'react-flow-renderer'; + +export function getElements(xElements: number = 10, yElements: number = 10): Elements { + const initialElements = []; + let nodeId = 1; + let recentNodeId = null; + + for (let y = 0; y < yElements; y++) { + for (let x = 0; x < xElements; x++) { + const position = { x: x * 100, y: y * 50 }; + const data = { label: `Node ${nodeId}` }; + const node = { + id: nodeId.toString(), + style: { width: 50, fontSize: 11 }, + data, + position, + }; + initialElements.push(node); + + if (recentNodeId && nodeId <= xElements * yElements) { + initialElements.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() }); + } + + recentNodeId = nodeId; + nodeId++; + } + } + + return initialElements; +} diff --git a/example/src/Switch/index.tsx b/example/src_oldapi/Switch/index.tsx similarity index 100% rename from example/src/Switch/index.tsx rename to example/src_oldapi/Switch/index.tsx diff --git a/example/src/Undirectional/CustomNode.tsx b/example/src_oldapi/Undirectional/CustomNode.tsx similarity index 100% rename from example/src/Undirectional/CustomNode.tsx rename to example/src_oldapi/Undirectional/CustomNode.tsx diff --git a/example/src/Undirectional/index.tsx b/example/src_oldapi/Undirectional/index.tsx similarity index 100% rename from example/src/Undirectional/index.tsx rename to example/src_oldapi/Undirectional/index.tsx diff --git a/example/src/UpdatableEdge/index.tsx b/example/src_oldapi/UpdatableEdge/index.tsx similarity index 100% rename from example/src/UpdatableEdge/index.tsx rename to example/src_oldapi/UpdatableEdge/index.tsx diff --git a/example/src_oldapi/UpdateNode/index.tsx b/example/src_oldapi/UpdateNode/index.tsx new file mode 100644 index 00000000..a7cce8f2 --- /dev/null +++ b/example/src_oldapi/UpdateNode/index.tsx @@ -0,0 +1,78 @@ +import React, { useEffect, useState } from 'react'; +import ReactFlow, { Elements } from 'react-flow-renderer'; + +import './updatenode.css'; + +const initialElements: Elements = [ + { id: '1', data: { label: '-' }, position: { x: 100, y: 100 } }, + { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } }, + { id: 'e1-2', source: '1', target: '2' }, +]; + +const UpdateNode = () => { + const [elements, setElements] = useState(initialElements); + const [nodeName, setNodeName] = useState('Node 1'); + const [nodeBg, setNodeBg] = useState('#eee'); + const [nodeHidden, setNodeHidden] = useState(false); + + useEffect(() => { + setElements((els) => + els.map((el) => { + if (el.id === '1') { + // it's important that you create a new object here in order to notify react flow about the change + el.data = { + ...el.data, + label: nodeName, + }; + } + + return el; + }) + ); + }, [nodeName, setElements]); + + useEffect(() => { + setElements((els) => + els.map((el) => { + if (el.id === '1') { + // it's important that you create a new object here in order to notify react flow about the change + el.style = { ...el.style, backgroundColor: nodeBg }; + } + + return el; + }) + ); + }, [nodeBg, setElements]); + + useEffect(() => { + setElements((els) => + els.map((el) => { + if (el.id === '1' || el.id === 'e1-2') { + // when you update a simple type you can just update the value + el.isHidden = nodeHidden; + } + + return el; + }) + ); + }, [nodeHidden, setElements]); + + return ( + +
+ + setNodeName(evt.target.value)} /> + + + setNodeBg(evt.target.value)} /> + +
+ + setNodeHidden(evt.target.checked)} /> +
+
+
+ ); +}; + +export default UpdateNode; diff --git a/example/src_oldapi/UpdateNode/updatenode.css b/example/src_oldapi/UpdateNode/updatenode.css new file mode 100644 index 00000000..95438054 --- /dev/null +++ b/example/src_oldapi/UpdateNode/updatenode.css @@ -0,0 +1,21 @@ +.updatenode__controls { + position: absolute; + right: 10px; + top: 10px; + z-index: 4; + font-size: 12px; +} + +.updatenode__controls label { + display: block; +} + +.updatenode__bglabel { + margin-top: 10px; +} + +.updatenode__checkboxwrapper { + margin-top: 10px; + display: flex; + align-items: center; +} diff --git a/example/src/UseUpdateNodeInternals/CustomNode.tsx b/example/src_oldapi/UseUpdateNodeInternals/CustomNode.tsx similarity index 100% rename from example/src/UseUpdateNodeInternals/CustomNode.tsx rename to example/src_oldapi/UseUpdateNodeInternals/CustomNode.tsx diff --git a/example/src/UseUpdateNodeInternals/index.tsx b/example/src_oldapi/UseUpdateNodeInternals/index.tsx similarity index 100% rename from example/src/UseUpdateNodeInternals/index.tsx rename to example/src_oldapi/UseUpdateNodeInternals/index.tsx diff --git a/example/src/UseZoomPanHelper/index.tsx b/example/src_oldapi/UseZoomPanHelper/index.tsx similarity index 100% rename from example/src/UseZoomPanHelper/index.tsx rename to example/src_oldapi/UseZoomPanHelper/index.tsx diff --git a/example/src/Validation/index.tsx b/example/src_oldapi/Validation/index.tsx similarity index 100% rename from example/src/Validation/index.tsx rename to example/src_oldapi/Validation/index.tsx diff --git a/example/src/Validation/validation.css b/example/src_oldapi/Validation/validation.css similarity index 100% rename from example/src/Validation/validation.css rename to example/src_oldapi/Validation/validation.css diff --git a/example/src_oldapi/index.css b/example/src_oldapi/index.css new file mode 100644 index 00000000..3f6f9960 --- /dev/null +++ b/example/src_oldapi/index.css @@ -0,0 +1,82 @@ +body { + font-family: sans-serif; + color: #111; +} + +html, +body, +#root { + margin: 0; + height: 100%; +} + +#root { + display: flex; + flex-direction: column; +} + +header { + padding: 10px; + border-bottom: 1px solid #eee; + display: flex; + font-weight: 700; + align-items: center; +} + +.logo { + text-decoration: none; + display: block; + line-height: 1; +} + +header a, +header a:focus, +header a:active, +header a:visited { + color: #111; +} + +header a:hover { + color: #333; +} + +header select { + margin-left: 1em; +} + +.overview-example__add { + display: none; +} + +.react-flow__node a { + font-weight: 700; + color: #111; +} + +.react-flow__node.dark-node { + background: #0041d0; + color: #f8f8f8; +} + +.react-flow__node.dark { + background: #557; + color: #f8f8f8; +} + +.react-flow__node-selectorNode { + font-size: 12px; + background: #f0f2f3; + border: 1px solid 555; + border-radius: 5px; + text-align: center; +} + +.react-flow__node-selectorNode .react-flow__handle { + border-color: #f0f2f3; +} + +@media screen and (min-width: 768px) { + .overview-example__add { + display: block; + } +} diff --git a/example/src_oldapi/index.tsx b/example/src_oldapi/index.tsx new file mode 100644 index 00000000..f81278ba --- /dev/null +++ b/example/src_oldapi/index.tsx @@ -0,0 +1,165 @@ +import React, { ChangeEvent } from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter as Router, Route, Switch, withRouter } from 'react-router-dom'; + +import Overview from './Overview'; +import Basic from './Basic'; +import CustomNode from './CustomNode'; +import Stress from './Stress'; +import Interaction from './Interaction'; +import Empty from './Empty'; +import Edges from './Edges'; +import Validation from './Validation'; +import Provider from './Provider'; +import Hidden from './Hidden'; +import EdgeTypes from './EdgeTypes'; +import CustomConnectionLine from './CustomConnectionLine'; +import NodeTypeChange from './NodeTypeChange'; +import NodeTypesObjectChange from './NodeTypesObjectChange'; +import UpdatableEdge from './UpdatableEdge'; +import UpdateNode from './UpdateNode'; +import SaveRestore from './SaveRestore'; +import DragNDrop from './DragNDrop'; +import Layout from './Layouting'; +import SwitchFlows from './Switch'; +import UseZoomPanHelper from './UseZoomPanHelper'; +import UseUpdateNodeInternals from './UseUpdateNodeInternals'; +import Undirectional from './Undirectional'; +import MultiFlows from './MultiFlows'; +import DragHandle from './DragHandle'; + +import './index.css'; + +const routes = [ + { + path: '/', + component: Overview, + }, + { + path: '/edges', + component: Edges, + }, + { + path: '/custom-node', + component: CustomNode, + }, + { + path: '/validation', + component: Validation, + }, + { + path: '/provider', + component: Provider, + }, + { + path: '/stress', + component: Stress, + }, + { + path: '/interaction', + component: Interaction, + }, + { + path: '/basic', + component: Basic, + }, + { + path: '/empty', + component: Empty, + }, + { + path: '/hidden', + component: Hidden, + }, + { + path: '/edge-types', + component: EdgeTypes, + }, + { + path: '/custom-connectionline', + component: CustomConnectionLine, + }, + { + path: '/nodetype-change', + component: NodeTypeChange, + }, + { + path: '/nodetypesobject-change', + component: NodeTypesObjectChange, + }, + { + path: '/updatable-edge', + component: UpdatableEdge, + }, + { + path: '/update-node', + component: UpdateNode, + }, + { + path: '/save-restore', + component: SaveRestore, + }, + { + path: '/drag-and-drop', + component: DragNDrop, + }, + { + path: '/layouting', + component: Layout, + }, + { + path: '/switch', + component: SwitchFlows, + }, + { + path: '/usezoompanhelper', + component: UseZoomPanHelper, + }, + { + path: '/useupdatenodeinternals', + component: UseUpdateNodeInternals, + }, + { + path: '/undirectional', + component: Undirectional, + }, + { + path: '/multiflows', + component: MultiFlows, + }, + { + path: '/draghandle', + component: DragHandle, + }, +]; + +const Header = withRouter(({ history, location }) => { + const onChange = (event: ChangeEvent) => history.push(event.target.value); + + return ( +
+ + React Flow Dev + + +
+ ); +}); + +ReactDOM.render( + +
+ + {routes.map((route) => ( + } key={route.path} /> + ))} + + , + document.getElementById('root') +); diff --git a/example/src_oldapi/react-app-env.d.ts b/example/src_oldapi/react-app-env.d.ts new file mode 100644 index 00000000..6431bc5f --- /dev/null +++ b/example/src_oldapi/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/example/tsconfig.json b/example/tsconfig.json index a273b0cf..edc77511 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -20,7 +16,6 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": [ - "src" - ] + "include": ["src"], + "exclude": ["src_oldapi"] } diff --git a/package-lock.json b/package-lock.json index 0b755877..17711e21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.2", + "version": "10.0.0-next.16", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "10.0.0-next.2", + "version": "10.0.0-next.16", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", @@ -15,10 +15,11 @@ "classcat": "^5.0.3", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0", - "fast-deep-equal": "^3.1.3", "react-draggable": "^4.4.4", "react-redux": "^7.2.5", - "redux": "^4.1.1" + "redux": "^4.1.1", + "redux-thunk": "^2.3.0", + "zustand": "^3.5.13" }, "devDependencies": { "@babel/core": "^7.15.8", @@ -32,7 +33,8 @@ "@svgr/rollup": "^5.5.0", "@types/redux": "^3.6.31", "@types/resize-observer-browser": "^0.1.6", - "autoprefixer": "^10.3.7", + "@welldone-software/why-did-you-render": "^6.2.1", + "autoprefixer": "^10.3.6", "babel-preset-react-app": "^10.0.0", "cypress": "^8.5.0", "postcss": "^8.3.9", @@ -2836,6 +2838,18 @@ "@types/node": "*" } }, + "node_modules/@welldone-software/why-did-you-render": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-6.2.1.tgz", + "integrity": "sha512-eIVKeK6ueS3tuzCqMVTaaNrPYvb9cA8NHiNgLA7Op8SD4TiT31zqNjxmhzLEK+y3sBxcwr6YhsiQGX9EThrvaw==", + "dev": true, + "dependencies": { + "lodash": "^4" + }, + "peerDependencies": { + "react": "^16 || ^17" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -5255,7 +5269,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-glob": { "version": "3.2.7", @@ -9255,6 +9270,11 @@ "@babel/runtime": "^7.9.2" } }, + "node_modules/redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -11229,6 +11249,22 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "node_modules/zustand": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.5.13.tgz", + "integrity": "sha512-orO/XcYwSWffsrPVTdCtuKM/zkUaOIyKDasOk/lecsD3R0euELsj+cB65uKZ1KyinrK2STHIuUhRoLpH8QprQg==", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } } }, "dependencies": { @@ -13290,6 +13326,15 @@ "@types/node": "*" } }, + "@welldone-software/why-did-you-render": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-6.2.1.tgz", + "integrity": "sha512-eIVKeK6ueS3tuzCqMVTaaNrPYvb9cA8NHiNgLA7Op8SD4TiT31zqNjxmhzLEK+y3sBxcwr6YhsiQGX9EThrvaw==", + "dev": true, + "requires": { + "lodash": "^4" + } + }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -15147,7 +15192,8 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "fast-glob": { "version": "3.2.7", @@ -18028,6 +18074,11 @@ "@babel/runtime": "^7.9.2" } }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -19538,6 +19589,11 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "zustand": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.5.13.tgz", + "integrity": "sha512-orO/XcYwSWffsrPVTdCtuKM/zkUaOIyKDasOk/lecsD3R0euELsj+cB65uKZ1KyinrK2STHIuUhRoLpH8QprQg==" } } } diff --git a/package.json b/package.json index 0a50f64e..5f33733e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.2", + "version": "10.0.0-next.16", "engines": { "node": ">=12" }, @@ -37,10 +37,11 @@ "classcat": "^5.0.3", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0", - "fast-deep-equal": "^3.1.3", "react-draggable": "^4.4.4", "react-redux": "^7.2.5", - "redux": "^4.1.1" + "redux": "^4.1.1", + "redux-thunk": "^2.3.0", + "zustand": "^3.5.13" }, "devDependencies": { "@babel/core": "^7.15.8", @@ -54,7 +55,8 @@ "@svgr/rollup": "^5.5.0", "@types/redux": "^3.6.31", "@types/resize-observer-browser": "^0.1.6", - "autoprefixer": "^10.3.7", + "@welldone-software/why-did-you-render": "^6.2.1", + "autoprefixer": "^10.3.6", "babel-preset-react-app": "^10.0.0", "cypress": "^8.5.0", "postcss": "^8.3.9", diff --git a/src/additional-components/Background/index.tsx b/src/additional-components/Background/index.tsx index 0e99a835..8f73da12 100644 --- a/src/additional-components/Background/index.tsx +++ b/src/additional-components/Background/index.tsx @@ -1,8 +1,8 @@ import React, { memo, useMemo, FC, HTMLAttributes } from 'react'; import cc from 'classcat'; -import { useStoreState } from '../../store/hooks'; -import { BackgroundVariant } from '../../types'; +import { useStore } from '../../store'; +import { BackgroundVariant, ReactFlowState } from '../../types'; import { createGridLinesPath, createGridDotsPath } from './utils'; export interface BackgroundProps extends HTMLAttributes { @@ -17,6 +17,8 @@ const defaultColors = { [BackgroundVariant.Lines]: '#eee', }; +const transformSelector = (s: ReactFlowState) => s.transform; + const Background: FC = ({ variant = BackgroundVariant.Dots, gap = 15, @@ -25,11 +27,11 @@ const Background: FC = ({ style, className, }) => { - const [x, y, scale] = useStoreState((s) => s.transform); + const [x, y, scale] = useStore(transformSelector); // when there are multiple flows on a page we need to make sure that every background gets its own pattern. const patternId = useMemo(() => `pattern-${Math.floor(Math.random() * 100000)}`, []); - const bgClasses = cc(['react-flow__background', className]); + const bgClasses = cc(['react-flow__background', 'react-flow__container', className]); const scaledGap = gap * scale; const xOffset = x % scaledGap; const yOffset = y % scaledGap; diff --git a/src/additional-components/Controls/index.tsx b/src/additional-components/Controls/index.tsx index 5e4ece7f..850ee781 100644 --- a/src/additional-components/Controls/index.tsx +++ b/src/additional-components/Controls/index.tsx @@ -1,7 +1,7 @@ import React, { memo, useCallback, HTMLAttributes, FC, useEffect, useState } from 'react'; import cc from 'classcat'; -import { useStoreState, useStoreActions } from '../../store/hooks'; +import { useStore } from '../../store'; import PlusIcon from '../../../assets/icons/plus.svg'; import MinusIcon from '../../../assets/icons/minus.svg'; @@ -10,7 +10,7 @@ import LockIcon from '../../../assets/icons/lock.svg'; import UnlockIcon from '../../../assets/icons/unlock.svg'; import useZoomPanHelper from '../../hooks/useZoomPanHelper'; -import { FitViewParams } from '../../types'; +import { FitViewParams, ReactFlowState } from '../../types'; export interface ControlProps extends HTMLAttributes { showZoom?: boolean; @@ -31,6 +31,9 @@ export const ControlButton: FC = ({ children, className, ... ); +const setInteractiveSelector = (s: ReactFlowState) => s.setInteractive; +const isInteractiveSelector = (s: ReactFlowState) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable; + const Controls: FC = ({ style, showZoom = true, @@ -45,10 +48,10 @@ const Controls: FC = ({ children, }) => { const [isVisible, setIsVisible] = useState(false); - const setInteractive = useStoreActions((actions) => actions.setInteractive); + const setInteractive = useStore(setInteractiveSelector); + const isInteractive = useStore(isInteractiveSelector); const { zoomIn, zoomOut, fitView } = useZoomPanHelper(); - const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable); const mapClasses = cc(['react-flow__controls', className]); const onZoomInHandler = useCallback(() => { diff --git a/src/additional-components/MiniMap/MiniMapNode.tsx b/src/additional-components/MiniMap/MiniMapNode.tsx index d8a48e9e..e879211b 100644 --- a/src/additional-components/MiniMap/MiniMapNode.tsx +++ b/src/additional-components/MiniMap/MiniMapNode.tsx @@ -26,7 +26,7 @@ const MiniMapNode = ({ strokeWidth, className, borderRadius, - shapeRendering + shapeRendering, }: MiniMapNodeProps) => { const { background, backgroundColor } = style || {}; const fill = (color || background || backgroundColor) as string; diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index dbb86f6c..174caf4f 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -1,9 +1,11 @@ import React, { memo, HTMLAttributes } from 'react'; import cc from 'classcat'; +import shallow from 'zustand/shallow'; -import { useStoreState } from '../../store/hooks'; -import { getRectOfNodes, getBoundsofRects } from '../../utils/graph'; -import { Node, Rect } from '../../types'; +import { useStore } from '../../store'; +import { getRectOfNodes } from '../../utils/graph'; +import { getBoundsofRects } from '../../utils'; +import { Node, ReactFlowState, Rect } from '../../types'; import MiniMapNode from './MiniMapNode'; type StringFunc = (node: Node) => string; @@ -22,6 +24,13 @@ declare const window: any; const defaultWidth = 200; const defaultHeight = 150; +const selector = (s: ReactFlowState) => ({ + width: s.width, + height: s.height, + transform: s.transform, + nodeInternals: s.nodeInternals, +}); + const MiniMap = ({ style, className, @@ -32,20 +41,20 @@ const MiniMap = ({ nodeStrokeWidth = 2, maskColor = 'rgb(240, 242, 243, 0.7)', }: MiniMapProps) => { - const containerWidth = useStoreState((s) => s.width); - const containerHeight = useStoreState((s) => s.height); - const [tX, tY, tScale] = useStoreState((s) => s.transform); - const nodes = useStoreState((s) => s.nodes); + const { width: containerWidth, height: containerHeight, transform, nodeInternals } = useStore(selector, shallow); + const [tX, tY, tScale] = transform; const mapClasses = cc(['react-flow__minimap', className]); const elementWidth = (style?.width || defaultWidth)! as number; const elementHeight = (style?.height || defaultHeight)! as number; const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc; - const nodeStrokeColorFunc = (nodeStrokeColor instanceof Function - ? nodeStrokeColor - : () => nodeStrokeColor) as StringFunc; + const nodeStrokeColorFunc = ( + nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor + ) as StringFunc; const nodeClassNameFunc = (nodeClassName instanceof Function ? nodeClassName : () => nodeClassName) as StringFunc; - const hasNodes = nodes && nodes.length; + const hasNodes = nodeInternals && nodeInternals.size > 0; + // @TODO: work with nodeInternals instead of converting it to an array + const nodes = Array.from(nodeInternals).map(([_, node]) => node); const bb = getRectOfNodes(nodes); const viewBB: Rect = { x: -tX / tScale, @@ -64,7 +73,7 @@ const MiniMap = ({ const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset; const width = viewWidth + offset * 2; const height = viewHeight + offset * 2; - const shapeRendering = (typeof window === "undefined" || !!window.chrome) ? "crispEdges" : "geometricPrecision"; + const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; return ( - {nodes - .filter((node) => !node.isHidden) - .map((node) => ( - - ))} + {Array.from(nodeInternals) + .filter(([_, node]) => !node.hidden && node.width && node.height) + .map(([_, node]) => { + const positionAbsolute = nodeInternals.get(node.id)?.positionAbsolute; + + return ( + + ); + })} { - const store = useMemo(() => { - return configureStore(initialState); - }, []); - - return {children}; -}; +const ReactFlowProvider: FC = ({ children }) => {children}; ReactFlowProvider.displayName = 'ReactFlowProvider'; diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index e3c71f9e..390132d1 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -1,32 +1,34 @@ -import React, { useEffect, useState, CSSProperties } from 'react'; +import React, { useRef, CSSProperties } from 'react'; +import shallow from 'zustand/shallow'; +import { useStore } from '../../store'; import { getBezierPath } from '../Edges/BezierEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { - ElementId, - Node, - Transform, + NodeInternalsItem, HandleElement, - Position, ConnectionLineType, ConnectionLineComponent, HandleType, + Node, + ReactFlowState, + Position, } from '../../types'; interface ConnectionLineProps { - connectionNodeId: ElementId; - connectionHandleId: ElementId | null; + connectionNodeId: string; + connectionHandleId: string | null; connectionHandleType: HandleType; connectionPositionX: number; connectionPositionY: number; connectionLineType: ConnectionLineType; - nodes: Node[]; - transform: Transform; isConnectable: boolean; connectionLineStyle?: CSSProperties; CustomConnectionLineComponent?: ConnectionLineComponent; } +const selector = (s: ReactFlowState) => ({ nodeInternals: s.nodeInternals, transform: s.transform }); + export default ({ connectionNodeId, connectionHandleId, @@ -35,31 +37,32 @@ export default ({ connectionPositionX, connectionPositionY, connectionLineType = ConnectionLineType.Bezier, - nodes = [], - transform, isConnectable, CustomConnectionLineComponent, }: ConnectionLineProps) => { - const [sourceNode, setSourceNode] = useState(null); const nodeId = connectionNodeId; const handleId = connectionHandleId; - useEffect(() => { - const nextSourceNode = nodes.find((n) => n.id === nodeId) || null; - setSourceNode(nextSourceNode); - }, []); + const { nodeInternals, transform } = useStore(selector, shallow); + const sourceNodeInternals = useRef(nodeInternals.get(nodeId)); + const sourceNode = useRef(nodeInternals.get(nodeId)); - if (!sourceNode || !isConnectable) { + if ( + !sourceNode.current || + !sourceNodeInternals.current || + !isConnectable || + !sourceNodeInternals.current.handleBounds?.[connectionHandleType] + ) { return null; } const sourceHandle = handleId - ? sourceNode.__rf.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId) - : sourceNode.__rf.handleBounds[connectionHandleType][0]; - const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rf.width / 2; - const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rf.height; - const sourceX = sourceNode.__rf.position.x + sourceHandleX; - const sourceY = sourceNode.__rf.position.y + sourceHandleY; + ? sourceNodeInternals.current.handleBounds[connectionHandleType]!.find((d: HandleElement) => d.id === handleId) + : sourceNodeInternals.current.handleBounds[connectionHandleType]![0]; + const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNodeInternals.current.width! / 2; + const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNodeInternals.current.height!; + const sourceX = sourceNodeInternals.current.positionAbsolute!.x + sourceHandleX; + const sourceY = sourceNodeInternals.current.positionAbsolute!.y + sourceHandleY; const targetX = (connectionPositionX - transform[0]) / transform[2]; const targetY = (connectionPositionY - transform[1]) / transform[2]; @@ -79,7 +82,7 @@ export default ({ targetPosition={targetPosition} connectionLineType={connectionLineType} connectionLineStyle={connectionLineStyle} - sourceNode={sourceNode} + sourceNode={sourceNode.current as Node} sourceHandle={sourceHandle} /> diff --git a/src/components/Edges/BezierEdge.tsx b/src/components/Edges/BezierEdge.tsx index 2313767f..7edb5a42 100644 --- a/src/components/Edges/BezierEdge.tsx +++ b/src/components/Edges/BezierEdge.tsx @@ -1,7 +1,6 @@ import React, { memo } from 'react'; import EdgeText from './EdgeText'; - import { getCenter } from './utils'; import { EdgeProps, Position } from '../../types'; diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx index 4e191cf7..a89eb28e 100644 --- a/src/components/Edges/wrapEdge.tsx +++ b/src/components/Edges/wrapEdge.tsx @@ -1,12 +1,21 @@ import React, { memo, ComponentType, useCallback, useState, useMemo } from 'react'; import cc from 'classcat'; +import shallow from 'zustand/shallow'; -import { useStoreActions, useStoreState } from '../../store/hooks'; -import { Edge, EdgeProps, WrapEdgeProps } from '../../types'; +import { useStore, useStoreApi } from '../../store'; +import { Edge, EdgeProps, WrapEdgeProps, ReactFlowState } from '../../types'; import { onMouseDown } from '../../components/Handle/handler'; import { EdgeAnchor } from './EdgeAnchor'; import { getMarkerId } from '../../utils/graph'; +const selector = (s: ReactFlowState) => ({ + addSelectedElements: s.addSelectedElements, + setConnectionNodeId: s.setConnectionNodeId, + unsetNodesSelection: s.unsetNodesSelection, + setPosition: s.setConnectionPosition, + connectionMode: s.connectionMode, +}); + export default (EdgeComponent: ComponentType) => { const EdgeWrapper = ({ id, @@ -33,7 +42,7 @@ export default (EdgeComponent: ComponentType) => { sourcePosition, targetPosition, elementsSelectable, - isHidden, + hidden, sourceHandleId, targetHandleId, handleEdgeUpdate, @@ -48,11 +57,11 @@ export default (EdgeComponent: ComponentType) => { markerEnd, markerStart, }: WrapEdgeProps): JSX.Element | null => { - const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); - const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId); - const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); - const setPosition = useStoreActions((actions) => actions.setConnectionPosition); - const connectionMode = useStoreState((state) => state.connectionMode); + const store = useStoreApi(); + const { addSelectedElements, setConnectionNodeId, unsetNodesSelection, setPosition, connectionMode } = useStore( + selector, + shallow + ); const [updating, setUpdating] = useState(false); @@ -91,7 +100,7 @@ export default (EdgeComponent: ComponentType) => { (event: React.MouseEvent): void => { if (elementsSelectable) { unsetNodesSelection(); - addSelectedElements(edgeElement); + addSelectedElements([edgeElement]); } onClick?.(event, edgeElement); @@ -158,7 +167,8 @@ export default (EdgeComponent: ComponentType) => { isValidConnection, connectionMode, isSourceHandle ? 'target' : 'source', - _onEdgeUpdate + _onEdgeUpdate, + store.getState ); }, [ @@ -194,7 +204,7 @@ export default (EdgeComponent: ComponentType) => { const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart)})`, [markerStart]); const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd)})`, [markerEnd]); - if (isHidden) { + if (hidden) { return null; } diff --git a/src/components/ElementUpdater/index.tsx b/src/components/ElementUpdater/index.tsx deleted file mode 100644 index 5bbe70a6..00000000 --- a/src/components/ElementUpdater/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useEffect } from 'react'; - -import { useStoreActions } from '../../store/hooks'; -import { Elements } from '../../types'; - -interface ElementUpdaterProps { - elements: Elements; -} - -const ElementUpdater = ({ elements }: ElementUpdaterProps) => { - const setElements = useStoreActions((actions) => actions.setElements); - - useEffect(() => { - setElements(elements); - }, [elements]); - - return null; -}; - -export default ElementUpdater; diff --git a/src/components/Handle/handler.ts b/src/components/Handle/handler.ts index 03fd37df..2ef44055 100644 --- a/src/components/Handle/handler.ts +++ b/src/components/Handle/handler.ts @@ -3,16 +3,15 @@ import { MouseEvent as ReactMouseEvent } from 'react'; import { getHostForElement } from '../../utils'; import { - ElementId, - XYPosition, - OnConnectFunc, - OnConnectStartFunc, - OnConnectStopFunc, - OnConnectEndFunc, + OnConnect, + OnConnectStart, + OnConnectStop, + OnConnectEnd, ConnectionMode, SetConnectionId, Connection, HandleType, + XYPosition, } from '../../types'; type ValidConnectionFunc = (connection: Connection) => boolean; @@ -32,8 +31,8 @@ function checkElementBelowIsValid( event: MouseEvent, connectionMode: ConnectionMode, isTarget: boolean, - nodeId: ElementId, - handleId: ElementId | null, + nodeId: string, + handleId: string | null, isValidConnection: ValidConnectionFunc, doc: Document | ShadowRoot ) { @@ -91,19 +90,19 @@ function resetRecentHandle(hoveredHandle: Element): void { export function onMouseDown( event: ReactMouseEvent, - handleId: ElementId | null, - nodeId: ElementId, + handleId: string | null, + nodeId: string, setConnectionNodeId: SetSourceIdFunc, setPosition: SetPosition, - onConnect: OnConnectFunc, + onConnect: OnConnect, isTarget: boolean, isValidConnection: ValidConnectionFunc, connectionMode: ConnectionMode, elementEdgeUpdaterType?: HandleType, onEdgeUpdateEnd?: (evt: MouseEvent) => void, - onConnectStart?: OnConnectStartFunc, - onConnectStop?: OnConnectStopFunc, - onConnectEnd?: OnConnectEndFunc + onConnectStart?: OnConnectStart, + onConnectStop?: OnConnectStop, + onConnectEnd?: OnConnectEnd ): void { const reactFlowNode = (event.target as Element).closest('.react-flow'); // when react-flow is used inside a shadow root we can't use document diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx index 9a38c550..e3b0111c 100644 --- a/src/components/Handle/index.tsx +++ b/src/components/Handle/index.tsx @@ -1,9 +1,10 @@ import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react'; import cc from 'classcat'; +import shallow from 'zustand/shallow'; -import { useStoreActions, useStoreState } from '../../store/hooks'; +import { useStore } from '../../store'; import NodeIdContext from '../../contexts/NodeIdContext'; -import { HandleProps, Connection, ElementId, Position } from '../../types'; +import { HandleProps, Connection, ReactFlowState, Position } from '../../types'; import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler'; @@ -11,6 +12,16 @@ const alwaysValid = () => true; export type HandleComponentProps = HandleProps & Omit, 'id'>; +const selector = (s: ReactFlowState) => ({ + setPosition: s.setConnectionPosition, + setConnectionNodeId: s.setConnectionNodeId, + onConnectAction: s.onConnect, + onConnectStart: s.onConnectStart, + onConnectStop: s.onConnectStop, + onConnectEnd: s.onConnectEnd, + connectionMode: s.connectionMode, +}); + const Handle = forwardRef( ( { @@ -26,14 +37,17 @@ const Handle = forwardRef( }, ref ) => { - const nodeId = useContext(NodeIdContext) as ElementId; - const setPosition = useStoreActions((actions) => actions.setConnectionPosition); - const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId); - const onConnectAction = useStoreState((state) => state.onConnect); - const onConnectStart = useStoreState((state) => state.onConnectStart); - const onConnectStop = useStoreState((state) => state.onConnectStop); - const onConnectEnd = useStoreState((state) => state.onConnectEnd); - const connectionMode = useStoreState((state) => state.connectionMode); + const nodeId = useContext(NodeIdContext) as string; + const { + setPosition, + setConnectionNodeId, + onConnectAction, + onConnectStart, + onConnectStop, + onConnectEnd, + connectionMode, + } = useStore(selector, shallow); + const handleId = id || null; const isTarget = type === 'target'; diff --git a/src/components/Nodes/DefaultNode.tsx b/src/components/Nodes/DefaultNode.tsx index 11d9af2f..7d9dc5d5 100644 --- a/src/components/Nodes/DefaultNode.tsx +++ b/src/components/Nodes/DefaultNode.tsx @@ -8,13 +8,15 @@ const DefaultNode = ({ isConnectable, targetPosition = Position.Top, sourcePosition = Position.Bottom, -}: NodeProps) => ( - <> - - {data.label} - - -); +}: NodeProps) => { + return ( + <> + + {data.label} + + + ); +}; DefaultNode.displayName = 'DefaultNode'; diff --git a/src/components/Nodes/utils.ts b/src/components/Nodes/utils.ts index e917e853..1ed82013 100644 --- a/src/components/Nodes/utils.ts +++ b/src/components/Nodes/utils.ts @@ -13,7 +13,7 @@ export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => { export const getHandleBoundsByHandleType = ( selector: string, nodeElement: HTMLDivElement, - parentBounds: ClientRect | DOMRect, + parentBounds: DOMRect, k: number ): HandleElement[] | null => { const handles = nodeElement.querySelectorAll(selector); @@ -24,20 +24,18 @@ export const getHandleBoundsByHandleType = ( const handlesArray = Array.from(handles) as HTMLDivElement[]; - return handlesArray.map( - (handle): HandleElement => { - const bounds = handle.getBoundingClientRect(); - const dimensions = getDimensions(handle); - const handleId = handle.getAttribute('data-handleid'); - const handlePosition = (handle.getAttribute('data-handlepos') as unknown) as Position; + return handlesArray.map((handle): HandleElement => { + const bounds = handle.getBoundingClientRect(); + const dimensions = getDimensions(handle); + const handleId = handle.getAttribute('data-handleid'); + const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position; - return { - id: handleId, - position: handlePosition, - x: (bounds.left - parentBounds.left) / k, - y: (bounds.top - parentBounds.top) / k, - ...dimensions, - }; - } - ); + return { + id: handleId, + position: handlePosition, + x: (bounds.left - parentBounds.left) / k, + y: (bounds.top - parentBounds.top) / k, + ...dimensions, + }; + }); }; diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index a6902416..20348106 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -1,22 +1,21 @@ -import React, { - useEffect, - useLayoutEffect, - useRef, - memo, - ComponentType, - CSSProperties, - useMemo, - MouseEvent, - useCallback, -} from 'react'; +import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react'; import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'; import cc from 'classcat'; +import shallow from 'zustand/shallow'; -import { useStoreActions } from '../../store/hooks'; +import { useStore } from '../../store'; import { Provider } from '../../contexts/NodeIdContext'; -import { NodeComponentProps, WrapNodeProps } from '../../types'; +import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types'; -export default (NodeComponent: ComponentType) => { +const selector = (s: ReactFlowState) => ({ + addSelectedElements: s.addSelectedElements, + unsetNodesSelection: s.unsetNodesSelection, + updateNodePosition: s.updateNodePosition, + updateNodeDimensions: s.updateNodeDimensions, + unselectNodesAndEdges: s.unselectNodesAndEdges, +}); + +export default (NodeComponent: ComponentType) => { const NodeWrapper = ({ id, type, @@ -42,19 +41,25 @@ export default (NodeComponent: ComponentType) => { selectNodesOnDrag, sourcePosition, targetPosition, - isHidden, + hidden, isInitialized, snapToGrid, snapGrid, - isDragging, + dragging, resizeObserver, dragHandle, + zIndex, + isParent, + noPanClassName, + noDragClassName, }: WrapNodeProps) => { - const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); - const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); - const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff); - const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); - + const { + addSelectedElements, + unselectNodesAndEdges, + unsetNodesSelection, + updateNodePosition, + // updateNodeDimensions, + } = useStore(selector, shallow); const nodeElement = useRef(null); const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]); @@ -62,7 +67,7 @@ export default (NodeComponent: ComponentType) => { const nodeStyle: CSSProperties = useMemo( () => ({ - zIndex: selected ? 10 : 3, + zIndex, transform: `translate(${xPos}px,${yPos}px)`, pointerEvents: isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none', @@ -71,7 +76,6 @@ export default (NodeComponent: ComponentType) => { ...style, }), [ - selected, xPos, yPos, isSelectable, @@ -82,31 +86,34 @@ export default (NodeComponent: ComponentType) => { onMouseEnter, onMouseMove, onMouseLeave, + isParent, + zIndex, ] ); + const onMouseEnterHandler = useMemo(() => { - if (!onMouseEnter || isDragging) { + if (!onMouseEnter || dragging) { return; } return (event: MouseEvent) => onMouseEnter(event, node); - }, [onMouseEnter, isDragging, node]); + }, [onMouseEnter, dragging, node]); const onMouseMoveHandler = useMemo(() => { - if (!onMouseMove || isDragging) { + if (!onMouseMove || dragging) { return; } return (event: MouseEvent) => onMouseMove(event, node); - }, [onMouseMove, isDragging, node]); + }, [onMouseMove, dragging, node]); const onMouseLeaveHandler = useMemo(() => { - if (!onMouseLeave || isDragging) { + if (!onMouseLeave || dragging) { return; } return (event: MouseEvent) => onMouseLeave(event, node); - }, [onMouseLeave, isDragging, node]); + }, [onMouseLeave, dragging, node]); const onContextMenuHandler = useMemo(() => { if (!onContextMenu) { @@ -123,7 +130,7 @@ export default (NodeComponent: ComponentType) => { unsetNodesSelection(); if (!selected) { - addSelectedElements(node); + addSelectedElements([node]); } } @@ -141,11 +148,11 @@ export default (NodeComponent: ComponentType) => { unsetNodesSelection(); if (!selected) { - addSelectedElements(node); + addSelectedElements([node]); } } else if (!selectNodesOnDrag && !selected && isSelectable) { + unselectNodesAndEdges(); unsetNodesSelection(); - addSelectedElements([]); } }, [node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart] @@ -153,20 +160,14 @@ export default (NodeComponent: ComponentType) => { const onDrag = useCallback( (event: DraggableEvent, draggableData: DraggableData) => { + node.position.x += draggableData.deltaX; + node.position.y += draggableData.deltaY; + if (onNodeDrag) { - node.position.x += draggableData.deltaX; - node.position.y += draggableData.deltaY; onNodeDrag(event as MouseEvent, node); } - updateNodePosDiff({ - id, - diff: { - x: draggableData.deltaX, - y: draggableData.deltaY, - }, - isDragging: true, - }); + updateNodePosition({ id, dragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } }); }, [id, node, onNodeDrag] ); @@ -175,9 +176,9 @@ export default (NodeComponent: ComponentType) => { (event: DraggableEvent) => { // onDragStop also gets called when user just clicks on a node. // Because of that we set dragging to true inside the onDrag handler and handle the click here - if (!isDragging) { + if (!dragging) { if (isSelectable && !selectNodesOnDrag && !selected) { - addSelectedElements(node); + addSelectedElements([node]); } onClick?.(event as MouseEvent, node); @@ -185,14 +186,14 @@ export default (NodeComponent: ComponentType) => { return; } - updateNodePosDiff({ + updateNodePosition({ id: node.id, - isDragging: false, + dragging: false, }); onNodeDragStop?.(event as MouseEvent, node); }, - [node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, selected] + [node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, dragging, selected] ); const onNodeDoubleClickHandler = useCallback( @@ -202,12 +203,6 @@ export default (NodeComponent: ComponentType) => { [node, onNodeDoubleClick] ); - useLayoutEffect(() => { - if (nodeElement.current && !isHidden) { - updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); - } - }, [id, isHidden, sourcePosition, targetPosition]); - useEffect(() => { if (nodeElement.current) { const currNode = nodeElement.current; @@ -217,17 +212,19 @@ export default (NodeComponent: ComponentType) => { } }, []); - if (isHidden) { + if (hidden) { return null; } const nodeClasses = cc([ 'react-flow__node', `react-flow__node-${type}`, + noPanClassName, className, { selected, selectable: isSelectable, + parent: isParent, }, ]); @@ -238,7 +235,7 @@ export default (NodeComponent: ComponentType) => { onStop={onDragStop} scale={scale} disabled={!isDraggable} - cancel=".nodrag" + cancel={`.${noDragClassName}`} nodeRef={nodeElement} grid={grid} enableUserSelectHack={false} @@ -267,8 +264,9 @@ export default (NodeComponent: ComponentType) => { isConnectable={isConnectable} sourcePosition={sourcePosition} targetPosition={targetPosition} - isDragging={isDragging} + dragging={dragging} dragHandle={dragHandle} + zIndex={zIndex} />
diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index efb780e7..b75c5ea6 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -5,53 +5,45 @@ import React, { useMemo, useCallback, useRef, MouseEvent } from 'react'; import ReactDraggable, { DraggableData } from 'react-draggable'; +import cc from 'classcat'; -import { useStoreState, useStoreActions } from '../../store/hooks'; -import { isNode } from '../../utils/graph'; -import { Node } from '../../types'; +import { useStore } from '../../store'; +import { Node, ReactFlowState } from '../../types'; export interface NodesSelectionProps { onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void; onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; + noPanClassName?: string; } +// @TODO: work with nodeInternals instead of converting it to an array +const selector = (s: ReactFlowState) => ({ + transform: s.transform, + selectedNodesBbox: s.selectedNodesBbox, + selectionActive: s.selectionActive, + selectedNodes: Array.from(s.nodeInternals) + .filter(([_, n]) => n.selected) + .map(([_, n]) => n), + snapToGrid: s.snapToGrid, + snapGrid: s.snapGrid, + updateNodePosition: s.updateNodePosition, +}); export default ({ onSelectionDragStart, onSelectionDrag, onSelectionDragStop, onSelectionContextMenu, + noPanClassName, }: NodesSelectionProps) => { - const [tX, tY, tScale] = useStoreState((state) => state.transform); - const selectedNodesBbox = useStoreState((state) => state.selectedNodesBbox); - const selectionActive = useStoreState((state) => state.selectionActive); - const selectedElements = useStoreState((state) => state.selectedElements); - const snapToGrid = useStoreState((state) => state.snapToGrid); - const snapGrid = useStoreState((state) => state.snapGrid); - const nodes = useStoreState((state) => state.nodes); - - const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff); - + const { transform, selectedNodesBbox, selectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } = + useStore(selector); + const [tX, tY, tScale] = transform; const nodeRef = useRef(null); const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]); - const selectedNodes = useMemo( - () => - selectedElements - ? selectedElements.filter(isNode).map((selectedNode) => { - const matchingNode = nodes.find((node) => node.id === selectedNode.id); - - return { - ...matchingNode, - position: matchingNode?.__rf.position, - } as Node; - }) - : [], - [selectedElements, nodes] - ); - const style = useMemo( () => ({ transform: `translate(${tX}px,${tY}px) scale(${tScale})`, @@ -82,21 +74,21 @@ export default ({ onSelectionDrag(event, selectedNodes); } - updateNodePosDiff({ + updateNodePosition({ diff: { x: data.deltaX, y: data.deltaY, }, - isDragging: true, + dragging: true, }); }, - [onSelectionDrag, selectedNodes, updateNodePosDiff] + [onSelectionDrag, selectedNodes, updateNodePosition] ); const onStop = useCallback( (event: MouseEvent) => { - updateNodePosDiff({ - isDragging: false, + updateNodePosition({ + dragging: false, }); onSelectionDragStop?.(event, selectedNodes); @@ -106,21 +98,17 @@ export default ({ const onContextMenu = useCallback( (event: MouseEvent) => { - const selectedNodes = selectedElements - ? selectedElements.filter(isNode).map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)!) - : []; - onSelectionContextMenu?.(event, selectedNodes); }, - [onSelectionContextMenu] + [onSelectionContextMenu, selectedNodes] ); - if (!selectedElements || selectionActive) { + if (!selectedNodes || selectionActive) { return null; } return ( -
+
void; + onSelectionChange: OnSelectionChangeFunc; } -// This is a helper component for calling the onSelectionChange listener +// @TODO: work with nodeInternals instead of converting it to an array +const selectedElementsSelector = (s: ReactFlowState) => ({ + selectedNodes: Array.from(s.nodeInternals) + .filter(([_, n]) => n.selected) + .map(([_, node]) => node), + selectedEdges: s.edges.filter((e) => e.selected), +}); + +// This is just a helper component for calling the onSelectionChange listener. export default ({ onSelectionChange }: SelectionListenerProps) => { - const selectedElements = useStoreState((s) => s.selectedElements); + const { selectedNodes, selectedEdges } = useStore(selectedElementsSelector, shallow); useEffect(() => { - onSelectionChange(selectedElements); - }, [selectedElements]); + onSelectionChange({ nodes: selectedNodes, edges: selectedEdges }); + }, [selectedNodes, selectedEdges]); return null; }; diff --git a/src/components/StoreUpdater/index.tsx b/src/components/StoreUpdater/index.tsx new file mode 100644 index 00000000..894e3335 --- /dev/null +++ b/src/components/StoreUpdater/index.tsx @@ -0,0 +1,210 @@ +import { useEffect } from 'react'; +import shallow from 'zustand/shallow'; + +import { useStore } from '../../store'; +import { + Node, + Edge, + ReactFlowState, + OnConnect, + OnConnectStart, + OnConnectStop, + OnConnectEnd, + CoordinateExtent, + OnNodesChange, + OnEdgesChange, + ConnectionMode, + SnapGrid, +} from '../../types'; + +interface StoreUpdaterProps { + nodes: Node[]; + edges: Edge[]; + onConnect?: OnConnect; + onConnectStart?: OnConnectStart; + onConnectStop?: OnConnectStop; + onConnectEnd?: OnConnectEnd; + nodesDraggable?: boolean; + nodesConnectable?: boolean; + minZoom?: number; + maxZoom?: number; + nodeExtent?: CoordinateExtent; + onNodesChange?: OnNodesChange; + onEdgesChange?: OnEdgesChange; + elementsSelectable?: boolean; + connectionMode?: ConnectionMode; + snapToGrid?: boolean; + snapGrid?: SnapGrid; + translateExtent?: CoordinateExtent; +} + +const selector = (s: ReactFlowState) => ({ + setNodes: s.setNodes, + setEdges: s.setEdges, + setOnConnect: s.setOnConnect, + setOnConnectStart: s.setOnConnectStart, + setOnConnectStop: s.setOnConnectStop, + setOnConnectEnd: s.setOnConnectEnd, + setSnapGrid: s.setSnapGrid, + setSnapToGrid: s.setSnapToGrid, + setNodesDraggable: s.setNodesDraggable, + setNodesConnectable: s.setNodesConnectable, + setElementsSelectable: s.setElementsSelectable, + setMinZoom: s.setMinZoom, + setMaxZoom: s.setMaxZoom, + setTranslateExtent: s.setTranslateExtent, + setNodeExtent: s.setNodeExtent, + setConnectionMode: s.setConnectionMode, + setOnNodesChange: s.setOnNodesChange, + setOnEdgesChange: s.setOnEdgesChange, +}); + +const StoreUpdater = ({ + nodes, + edges, + onConnect, + onConnectStart, + onConnectStop, + onConnectEnd, + nodesDraggable, + nodesConnectable, + minZoom, + maxZoom, + nodeExtent, + onNodesChange, + onEdgesChange, + elementsSelectable, + connectionMode, + snapGrid, + snapToGrid, + translateExtent, +}: StoreUpdaterProps) => { + const { + setNodes, + setEdges, + setOnConnect, + setOnConnectStart, + setOnConnectStop, + setOnConnectEnd, + setNodesDraggable, + setSnapGrid, + setSnapToGrid, + setNodesConnectable, + setElementsSelectable, + setMinZoom, + setMaxZoom, + setTranslateExtent, + setNodeExtent, + setOnNodesChange, + setOnEdgesChange, + setConnectionMode, + } = useStore(selector, shallow); + + useEffect(() => { + setNodes(nodes); + }, [nodes]); + + useEffect(() => { + setEdges(edges, nodes); + }, [edges, nodes]); + + useEffect(() => { + if (onConnect) { + setOnConnect(onConnect); + } + }, [onConnect]); + + useEffect(() => { + if (onConnectStart) { + setOnConnectStart(onConnectStart); + } + }, [onConnectStart]); + + useEffect(() => { + if (onConnectStop) { + setOnConnectStop(onConnectStop); + } + }, [onConnectStop]); + + useEffect(() => { + if (onConnectEnd) { + setOnConnectEnd(onConnectEnd); + } + }, [onConnectEnd]); + + useEffect(() => { + if (typeof snapToGrid !== 'undefined') { + setSnapToGrid(snapToGrid); + } + }, [snapToGrid]); + + useEffect(() => { + if (typeof snapGrid !== 'undefined') { + setSnapGrid(snapGrid); + } + }, [snapGrid]); + + useEffect(() => { + if (typeof nodesDraggable !== 'undefined') { + setNodesDraggable(nodesDraggable); + } + }, [nodesDraggable]); + + useEffect(() => { + if (typeof nodesConnectable !== 'undefined') { + setNodesConnectable(nodesConnectable); + } + }, [nodesConnectable]); + + useEffect(() => { + if (typeof elementsSelectable !== 'undefined') { + setElementsSelectable(elementsSelectable); + } + }, [elementsSelectable]); + + useEffect(() => { + if (typeof minZoom !== 'undefined') { + setMinZoom(minZoom); + } + }, [minZoom]); + + useEffect(() => { + if (typeof maxZoom !== 'undefined') { + setMaxZoom(maxZoom); + } + }, [maxZoom]); + + useEffect(() => { + if (typeof translateExtent !== 'undefined') { + setTranslateExtent(translateExtent); + } + }, [translateExtent]); + + useEffect(() => { + if (typeof nodeExtent !== 'undefined') { + setNodeExtent(nodeExtent); + } + }, [nodeExtent]); + + useEffect(() => { + if (typeof connectionMode !== 'undefined') { + setConnectionMode(connectionMode); + } + }, [connectionMode]); + + useEffect(() => { + if (typeof onNodesChange !== 'undefined') { + setOnNodesChange(onNodesChange); + } + }, [onNodesChange]); + + useEffect(() => { + if (typeof onEdgesChange !== 'undefined') { + setOnEdgesChange(onEdgesChange); + } + }, [onEdgesChange]); + + return null; +}; + +export default StoreUpdater; diff --git a/src/components/UserSelection/index.tsx b/src/components/UserSelection/index.tsx index 14a8a13a..5ffbef7e 100644 --- a/src/components/UserSelection/index.tsx +++ b/src/components/UserSelection/index.tsx @@ -4,8 +4,8 @@ import React, { memo } from 'react'; -import { useStoreActions, useStoreState } from '../../store/hooks'; -import { XYPosition } from '../../types'; +import { useStore } from '../../store'; +import { XYPosition, ReactFlowState } from '../../types'; type UserSelectionProps = { selectionKeyPressed: boolean; @@ -25,8 +25,10 @@ function getMousePosition(event: React.MouseEvent): XYPosition | void { }; } +const userSelectionRectSelector = (state: ReactFlowState) => state.userSelectionRect; + const SelectionRect = () => { - const userSelectionRect = useStoreState((state) => state.userSelectionRect); + const userSelectionRect = useStore(userSelectionRectSelector); if (!userSelectionRect.draw) { return null; @@ -34,7 +36,7 @@ const SelectionRect = () => { return (
{ ); }; +const selector = (s: ReactFlowState) => ({ + selectionActive: s.selectionActive, + elementsSelectable: s.elementsSelectable, + setUserSelection: s.setUserSelection, + updateUserSelection: s.updateUserSelection, + unsetUserSelection: s.unsetUserSelection, + unsetNodesSelection: s.unsetNodesSelection, +}); + export default memo(({ selectionKeyPressed }: UserSelectionProps) => { - const selectionActive = useStoreState((state) => state.selectionActive); - const elementsSelectable = useStoreState((state) => state.elementsSelectable); + const { + selectionActive, + elementsSelectable, + setUserSelection, + updateUserSelection, + unsetUserSelection, + unsetNodesSelection, + } = useStore(selector); - const setUserSelection = useStoreActions((actions) => actions.setUserSelection); - const updateUserSelection = useStoreActions((actions) => actions.updateUserSelection); - const unsetUserSelection = useStoreActions((actions) => actions.unsetUserSelection); - const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); const renderUserSelectionPane = selectionActive || selectionKeyPressed; if (!elementsSelectable || !renderUserSelectionPane) { @@ -89,7 +102,7 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => { return (
s.edges; + const MarkerDefinitions = ({ defaultColor }: MarkerDefinitionsProps) => { - const edges = useStoreState((state) => state.edges); + const edges = useStore(edgesSelector); const markers = useMemo(() => { const ids: string[] = []; diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index eaea06ad..ff6794da 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -1,30 +1,30 @@ import React, { memo, CSSProperties, useCallback } from 'react'; +import shallow from 'zustand/shallow'; +import cc from 'classcat'; -import { useStoreState } from '../../store/hooks'; +import { useStore } from '../../store'; import ConnectionLine from '../../components/ConnectionLine/index'; -import { isEdge } from '../../utils/graph'; import MarkerDefinitions from './MarkerDefinitions'; -import { getEdgePositions, getHandle, isEdgeVisible, getSourceTargetNodes } from './utils'; +import { getEdgePositions, getHandle } from './utils'; import { Position, Edge, - Node, - Elements, Connection, ConnectionLineType, ConnectionLineComponent, ConnectionMode, - Transform, OnEdgeUpdateFunc, + ReactFlowState, + NodeHandleBounds, } from '../../types'; +import useVisibleEdges from '../../hooks/useVisibleEdges'; interface EdgeRendererProps { edgeTypes: any; connectionLineType: ConnectionLineType; connectionLineStyle?: CSSProperties; connectionLineComponent?: ConnectionLineComponent; - connectionMode?: ConnectionMode; - onElementClick?: (event: React.MouseEvent, element: Node | Edge) => void; + onEdgeClick?: (event: React.MouseEvent, node: Edge) => void; onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void; defaultMarkerColor: string; onlyRenderVisibleElements: boolean; @@ -36,215 +36,278 @@ interface EdgeRendererProps { onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge) => void; onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; edgeUpdaterRadius?: number; + noPanClassName?: string; } interface EdgeWrapperProps { edge: Edge; - props: EdgeRendererProps; - nodes: Node[]; - selectedElements: Elements | null; + edgeTypes: any; + markerEndId?: string; + onEdgeClick?: (event: React.MouseEvent, node: Edge) => void; + onEdgeContextMenu?: (event: React.MouseEvent, edge: Edge) => void; + onEdgeMouseEnter?: (event: React.MouseEvent, edge: Edge) => void; + onEdgeMouseMove?: (event: React.MouseEvent, edge: Edge) => void; + onEdgeMouseLeave?: (event: React.MouseEvent, edge: Edge) => void; + edgeUpdaterRadius?: number; + onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void; + onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge) => void; + onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; + onEdgeUpdate?: OnEdgeUpdateFunc; elementsSelectable: boolean; - transform: Transform; - width: number; - height: number; - onlyRenderVisibleElements: boolean; connectionMode?: ConnectionMode; + sourceNodeWidth?: number | null; + sourceNodeHeight?: number | null; + sourceNodeX?: number; + sourceNodeY?: number; + sourceNodeHandleBounds?: NodeHandleBounds; + targetNodeWidth?: number | null; + targetNodeHeight?: number | null; + targetNodeX?: number; + targetNodeY?: number; + targetNodeHandleBounds?: NodeHandleBounds; + noPanClassName?: string; } -const Edge = ({ - edge, - props, - nodes, - selectedElements, - elementsSelectable, - transform, - width, - height, - onlyRenderVisibleElements, - connectionMode, -}: EdgeWrapperProps) => { - const sourceHandleId = edge.sourceHandle || null; - const targetHandleId = edge.targetHandle || null; - const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes); +const Edge = memo( + ({ + edge, + edgeTypes, + markerEndId, + onEdgeClick, + onEdgeContextMenu, + onEdgeMouseEnter, + onEdgeMouseMove, + onEdgeMouseLeave, + edgeUpdaterRadius, + onEdgeDoubleClick, + onEdgeUpdateStart, + onEdgeUpdateEnd, + onEdgeUpdate, + connectionMode, + elementsSelectable, + sourceNodeWidth, + sourceNodeHeight, + sourceNodeX, + sourceNodeY, + sourceNodeHandleBounds, + targetNodeWidth, + targetNodeHeight, + targetNodeX, + targetNodeY, + targetNodeHandleBounds, + noPanClassName, + }: EdgeWrapperProps) => { + const sourceHandleId = edge.sourceHandle || null; + const targetHandleId = edge.targetHandle || null; - const onConnectEdge = useCallback( - (connection: Connection) => { - props.onEdgeUpdate?.(edge, connection); - }, - [edge, props.onEdgeUpdate] - ); + const onConnectEdge = useCallback( + (connection: Connection) => { + onEdgeUpdate?.(edge, connection); + }, + [edge, onEdgeUpdate] + ); - if (!sourceNode) { - console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`); - return null; + // source and target node need to be initialized + if (!sourceNodeHandleBounds || !targetNodeHandleBounds) { + return null; + } + + if ( + !sourceNodeWidth || + !sourceNodeHeight || + typeof sourceNodeX === 'undefined' || + typeof sourceNodeY === 'undefined' + ) { + console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`); + return null; + } + + if ( + !targetNodeWidth || + !targetNodeHeight || + typeof targetNodeX === 'undefined' || + typeof targetNodeY === 'undefined' + ) { + console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`); + return null; + } + + const edgeType = edge.type || 'default'; + const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default; + // when connection type is loose we can define all handles as sources + const targetNodeHandles = + connectionMode === ConnectionMode.Strict + ? targetNodeHandleBounds.target + : targetNodeHandleBounds.target || targetNodeHandleBounds.source; + const sourceHandle = getHandle(sourceNodeHandleBounds.source!, sourceHandleId); + const targetHandle = getHandle(targetNodeHandles!, targetHandleId); + 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}; edge id: ${edge.id}`); + return null; + } + + if (!targetHandle) { + console.warn(`couldn't create edge for target handle id: ${targetHandleId}; edge id: ${edge.id}`); + return null; + } + + const { sourceX, sourceY, targetX, targetY } = getEdgePositions( + { x: sourceNodeX, y: sourceNodeY, width: sourceNodeWidth, height: sourceNodeHeight }, + sourceHandle, + sourcePosition, + { x: targetNodeX, y: targetNodeY, width: targetNodeWidth, height: targetNodeHeight }, + targetHandle, + targetPosition + ); + + return ( +