From 32ee96404456f63249dd3ca39512d36892ba4f77 Mon Sep 17 00:00:00 2001 From: moklick Date: Sat, 9 Oct 2021 11:33:10 +0200 Subject: [PATCH] refactor(elements): split into nodes and edges --- .../MiniMap/MiniMapNode.tsx | 2 +- src/additional-components/MiniMap/index.tsx | 18 +- src/components/ConnectionLine/index.tsx | 16 +- src/components/ElementUpdater/index.tsx | 18 +- src/components/Handle/handler.ts | 8 +- src/components/Handle/index.tsx | 14 +- src/components/Nodes/wrapNode.tsx | 64 ++-- src/components/NodesSelection/index.tsx | 2 +- src/container/EdgeRenderer/index.tsx | 303 +++++++++--------- src/container/EdgeRenderer/utils.ts | 4 +- src/container/FlowRenderer/index.tsx | 3 +- src/container/GraphView/index.tsx | 21 +- src/container/NodeRenderer/index.tsx | 19 +- src/container/ReactFlow/index.tsx | 29 +- src/hooks/useGlobalKeyHandler.ts | 27 +- src/index.ts | 2 + src/store/actions.ts | 17 +- src/store/configure-store.ts | 5 +- src/store/contants.ts | 5 +- src/store/index.ts | 3 + src/store/reducer.ts | 187 ++++++----- src/types/index.ts | 18 +- src/utils/graph.ts | 96 +++--- 23 files changed, 495 insertions(+), 386 deletions(-) diff --git a/src/additional-components/MiniMap/MiniMapNode.tsx b/src/additional-components/MiniMap/MiniMapNode.tsx index d8a48e9ed..e879211be 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 dbb86f6ca..6060b4632 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -41,9 +41,9 @@ const MiniMap = ({ 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 bb = getRectOfNodes(nodes); @@ -64,7 +64,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) + .filter((node) => !node.isHidden && node.width && node.height) .map((node) => ( { + const nodes = useStoreState((state) => state.nodes); const [sourceNode, setSourceNode] = useState(null); const nodeId = connectionNodeId; const handleId = connectionHandleId; @@ -54,12 +54,12 @@ export default ({ } 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; + ? sourceNode.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId) + : sourceNode.handleBounds[connectionHandleType][0]; + const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.width! / 2; + const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.height; + const sourceX = sourceNode.position.x + sourceHandleX; + const sourceY = sourceNode.position.y + sourceHandleY; const targetX = (connectionPositionX - transform[0]) / transform[2]; const targetY = (connectionPositionY - transform[1]) / transform[2]; diff --git a/src/components/ElementUpdater/index.tsx b/src/components/ElementUpdater/index.tsx index 5bbe70a69..f40636da9 100644 --- a/src/components/ElementUpdater/index.tsx +++ b/src/components/ElementUpdater/index.tsx @@ -1,18 +1,24 @@ import { useEffect } from 'react'; import { useStoreActions } from '../../store/hooks'; -import { Elements } from '../../types'; +import { Node, Edge } from '../../types'; interface ElementUpdaterProps { - elements: Elements; + nodes: Node[]; + edges: Edge[]; } -const ElementUpdater = ({ elements }: ElementUpdaterProps) => { - const setElements = useStoreActions((actions) => actions.setElements); +const ElementUpdater = ({ nodes, edges }: ElementUpdaterProps) => { + const setNodes = useStoreActions((actions) => actions.setNodes); + const setEdges = useStoreActions((actions) => actions.setEdges); useEffect(() => { - setElements(elements); - }, [elements]); + setNodes(nodes); + }, [nodes]); + + useEffect(() => { + setEdges(edges); + }, [edges]); return null; }; diff --git a/src/components/Handle/handler.ts b/src/components/Handle/handler.ts index 03fd37dfa..f2733a770 100644 --- a/src/components/Handle/handler.ts +++ b/src/components/Handle/handler.ts @@ -1,6 +1,9 @@ import { MouseEvent as ReactMouseEvent } from 'react'; +import { Store } from 'redux'; import { getHostForElement } from '../../utils'; +import { ReactFlowState } from '../../types'; +import { ReactFlowAction } from '../../store/actions'; import { ElementId, @@ -103,7 +106,8 @@ export function onMouseDown( onEdgeUpdateEnd?: (evt: MouseEvent) => void, onConnectStart?: OnConnectStartFunc, onConnectStop?: OnConnectStopFunc, - onConnectEnd?: OnConnectEndFunc + onConnectEnd?: OnConnectEndFunc, + store?: Store ): void { const reactFlowNode = (event.target as Element).closest('.react-flow'); // when react-flow is used inside a shadow root we can't use document @@ -177,7 +181,7 @@ export function onMouseDown( onConnectStop?.(event); if (isValid) { - onConnect?.(connection); + onConnect?.(connection, store?.getState().nodes || []); } onConnectEnd?.(event); diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx index 9a38c5504..317efe65f 100644 --- a/src/components/Handle/index.tsx +++ b/src/components/Handle/index.tsx @@ -1,9 +1,9 @@ import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react'; import cc from 'classcat'; -import { useStoreActions, useStoreState } from '../../store/hooks'; +import { useStoreActions, useStoreState, useStore } from '../../store/hooks'; import NodeIdContext from '../../contexts/NodeIdContext'; -import { HandleProps, Connection, ElementId, Position } from '../../types'; +import { HandleProps, Connection, ElementId, Position, Node } from '../../types'; import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler'; @@ -26,6 +26,7 @@ const Handle = forwardRef( }, ref ) => { + const store = useStore(); const nodeId = useContext(NodeIdContext) as ElementId; const setPosition = useStoreActions((actions) => actions.setConnectionPosition); const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId); @@ -38,9 +39,9 @@ const Handle = forwardRef( const isTarget = type === 'target'; const onConnectExtended = useCallback( - (params: Connection) => { - onConnectAction?.(params); - onConnect?.(params); + (params: Connection, nodes: Node[]) => { + onConnectAction?.(params, nodes); + onConnect?.(params, nodes); }, [onConnectAction, onConnect] ); @@ -61,7 +62,8 @@ const Handle = forwardRef( undefined, onConnectStart, onConnectStop, - onConnectEnd + onConnectEnd, + store ); }, [ diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index a69024167..04f9acefd 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -1,18 +1,8 @@ -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 { useStoreActions } from '../../store/hooks'; +import { useStoreActions, useStoreState } from '../../store/hooks'; import { Provider } from '../../contexts/NodeIdContext'; import { NodeComponentProps, WrapNodeProps } from '../../types'; @@ -50,9 +40,9 @@ export default (NodeComponent: ComponentType) => { resizeObserver, dragHandle, }: WrapNodeProps) => { - const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); + // const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); - const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff); + const onNodesChange = useStoreState((state) => state.onNodesChange); const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); const nodeElement = useRef(null); @@ -84,6 +74,7 @@ export default (NodeComponent: ComponentType) => { onMouseLeave, ] ); + const onMouseEnterHandler = useMemo(() => { if (!onMouseEnter || isDragging) { return; @@ -153,20 +144,25 @@ 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, + onNodesChange?.([ + { + id, + change: { + position: { + x: node.position.x, + y: node.position.y, + }, + isDragging: true, + }, }, - isDragging: true, - }); + ]); }, [id, node, onNodeDrag] ); @@ -185,10 +181,14 @@ export default (NodeComponent: ComponentType) => { return; } - updateNodePosDiff({ - id: node.id, - isDragging: false, - }); + onNodesChange?.([ + { + id: node.id, + change: { + isDragging: true, + }, + }, + ]); onNodeDragStop?.(event as MouseEvent, node); }, @@ -202,11 +202,11 @@ 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 && !isHidden) { + // updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); + // } + // }, [id, isHidden, sourcePosition, targetPosition]); useEffect(() => { if (nodeElement.current) { diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index efb780e7c..1b3c32576 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -45,7 +45,7 @@ export default ({ return { ...matchingNode, - position: matchingNode?.__rf.position, + position: matchingNode?.position, } as Node; }) : [], diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 1fd7f7662..3554d2f8d 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -4,7 +4,7 @@ import { useStoreState } from '../../store/hooks'; 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, @@ -14,7 +14,6 @@ import { ConnectionLineType, ConnectionLineComponent, ConnectionMode, - Transform, OnEdgeUpdateFunc, } from '../../types'; @@ -41,151 +40,167 @@ interface EdgeRendererProps { interface EdgeWrapperProps { edge: Edge; - props: EdgeRendererProps; - nodes: Node[]; + edgeTypes: any; + markerEndId?: string; + onElementClick?: (event: React.MouseEvent, element: 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; + targetNode?: Node; + sourceNode?: Node; selectedElements: Elements | null; elementsSelectable: boolean; - transform: Transform; - width: number; - height: number; - onlyRenderVisibleElements: boolean; connectionMode?: ConnectionMode; } -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 onConnectEdge = useCallback( - (connection: Connection) => { - props.onEdgeUpdate?.(edge, connection); - }, - [edge, props.onEdgeUpdate] - ); +const Edge = memo( + ({ + edge, + edgeTypes, + markerEndId, + onElementClick, + onEdgeContextMenu, + onEdgeMouseEnter, + onEdgeMouseMove, + onEdgeMouseLeave, + edgeUpdaterRadius, + onEdgeDoubleClick, + onEdgeUpdateStart, + onEdgeUpdateEnd, + onEdgeUpdate, + targetNode, + sourceNode, + selectedElements, + elementsSelectable, + connectionMode, + }: EdgeWrapperProps) => { + const sourceHandleId = edge.sourceHandle || null; + const targetHandleId = edge.targetHandle || null; - if (!sourceNode) { - console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`); - return null; - } + const onConnectEdge = useCallback( + (connection: Connection) => { + onEdgeUpdate?.(edge, connection); + }, + [edge, onEdgeUpdate] + ); - if (!targetNode) { - console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`); - return null; - } + 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 (!sourceNode.__rf.width || !targetNode.__rf.width) { - return null; - } + if (!targetNode) { + console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`); + return null; + } - const edgeType = edge.type || 'default'; - const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default; - const targetNodeBounds = targetNode.__rf.handleBounds; - // when connection type is loose we can define all handles as sources - const targetNodeHandles = - connectionMode === ConnectionMode.Strict - ? targetNodeBounds.target - : targetNodeBounds.target || targetNodeBounds.source; - const sourceHandle = getHandle(sourceNode.__rf.handleBounds.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; - } + // source and target node need to be initialized + if (!sourceNode.width || !targetNode.width) { + return null; + } - if (!targetHandle) { - console.warn(`couldn't create edge for target handle id: ${targetHandleId}; edge id: ${edge.id}`); - return null; - } + const edgeType = edge.type || 'default'; + const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default; + const targetNodeBounds = targetNode.handleBounds; + // when connection type is loose we can define all handles as sources + const targetNodeHandles = + connectionMode === ConnectionMode.Strict + ? targetNodeBounds.target + : targetNodeBounds.target || targetNodeBounds.source; + const sourceHandle = getHandle(sourceNode.handleBounds.source, sourceHandleId); + const targetHandle = getHandle(targetNodeHandles, targetHandleId); + const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom; + const targetPosition = targetHandle ? targetHandle.position : Position.Top; - const { sourceX, sourceY, targetX, targetY } = getEdgePositions( - sourceNode, - sourceHandle, - sourcePosition, - targetNode, - targetHandle, - targetPosition - ); + if (!sourceHandle) { + console.warn(`couldn't create edge for source handle id: ${sourceHandleId}; edge id: ${edge.id}`); + return null; + } - const isVisible = onlyRenderVisibleElements - ? isEdgeVisible({ - sourcePos: { x: sourceX, y: sourceY }, - targetPos: { x: targetX, y: targetY }, - width, - height, - transform, - }) - : true; - - if (!isVisible) { - return null; - } + if (!targetHandle) { + console.warn(`couldn't create edge for target handle id: ${targetHandleId}; edge id: ${edge.id}`); + return null; + } - const isSelected = selectedElements?.some((elm) => isEdge(elm) && elm.id === edge.id) || false; + const { sourceX, sourceY, targetX, targetY } = getEdgePositions( + sourceNode, + sourceHandle, + sourcePosition, + targetNode, + targetHandle, + targetPosition + ); - return ( - - ); -}; + // const isVisible = onlyRenderVisibleElements + // ? isEdgeVisible({ + // sourcePos: { x: sourceX, y: sourceY }, + // targetPos: { x: targetX, y: targetY }, + // width, + // height, + // transform, + // }) + // : true; + + // if (!isVisible) { + // return null; + // } + + const isSelected = selectedElements?.some((elm) => isEdge(elm) && elm.id === edge.id) || false; + + return ( + + ); + } +); const EdgeRenderer = (props: EdgeRendererProps) => { const transform = useStoreState((state) => state.transform); - const nodes = useStoreState((state) => state.nodes); const edges = useStoreState((state) => state.edges); const connectionNodeId = useStoreState((state) => state.connectionNodeId); const connectionHandleId = useStoreState((state) => state.connectionHandleId); @@ -201,13 +216,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { return null; } - const { - connectionLineType, - arrowHeadColor, - connectionLineStyle, - connectionLineComponent, - onlyRenderVisibleElements, - } = props; + const { connectionLineType, arrowHeadColor, connectionLineStyle, connectionLineComponent } = props; const transformStyle = `translate(${transform[0]},${transform[1]}) scale(${transform[2]})`; const renderConnectionLine = connectionNodeId && connectionHandleType; @@ -219,19 +228,25 @@ const EdgeRenderer = (props: EdgeRendererProps) => { ))} {renderConnectionLine && ( { diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index e002f366b..efcfe4e55 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -11,7 +11,7 @@ import { ReactFlowProps } from '../ReactFlow'; import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types'; -export interface GraphViewProps extends Omit { +export interface GraphViewProps extends Omit { nodeTypes: NodeTypesType; edgeTypes: EdgeTypesType; selectionKeyCode: KeyCode; @@ -55,7 +55,6 @@ const GraphView = ({ selectionKeyCode, multiSelectionKeyCode, zoomActivationKeyCode, - onElementsRemove, deleteKeyCode, onConnect, onConnectStart, @@ -95,6 +94,8 @@ const GraphView = ({ edgeUpdaterRadius, onEdgeUpdateStart, onEdgeUpdateEnd, + onNodesChange, + onEdgesChange, }: GraphViewProps) => { const isInitialized = useRef(false); const setOnConnect = useStoreActions((actions) => actions.setOnConnect); @@ -111,6 +112,9 @@ const GraphView = ({ const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent); const setNodeExtent = useStoreActions((actions) => actions.setNodeExtent); const setConnectionMode = useStoreActions((actions) => actions.setConnectionMode); + const setOnNodesChange = useStoreActions((actions) => actions.setOnNodesChange); + const setOnEdgesChange = useStoreActions((actions) => actions.setOnEdgesChange); + const currentStore = useStore(); const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper(); @@ -217,12 +221,23 @@ const GraphView = ({ } }, [connectionMode]); + useEffect(() => { + if (typeof onNodesChange !== 'undefined') { + setOnNodesChange(onNodesChange); + } + }, [onNodesChange]); + + useEffect(() => { + if (typeof onEdgesChange !== 'undefined') { + setOnEdgesChange(onEdgesChange); + } + }, [onEdgesChange]); + return ( { const nodesDraggable = useStoreState((state) => state.nodesDraggable); const nodesConnectable = useStoreState((state) => state.nodesConnectable); const elementsSelectable = useStoreState((state) => state.elementsSelectable); - const width = useStoreState((state) => state.width); - const height = useStoreState((state) => state.height); const nodes = useStoreState((state) => state.nodes); const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); - const visibleNodes = props.onlyRenderVisibleElements - ? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true) - : nodes; + // const visibleNodes = props.onlyRenderVisibleElements + // ? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true) + // : nodes; const transformStyle = useMemo( () => ({ @@ -59,7 +56,7 @@ const NodeRenderer = (props: NodeRendererProps) => { return (
- {visibleNodes.map((node) => { + {nodes.map((node) => { const nodeType = node.type || 'default'; const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; @@ -82,10 +79,10 @@ const NodeRenderer = (props: NodeRendererProps) => { sourcePosition={node.sourcePosition} targetPosition={node.targetPosition} isHidden={node.isHidden} - xPos={node.__rf.position.x} - yPos={node.__rf.position.y} - isDragging={node.__rf.isDragging} - isInitialized={node.__rf.width !== null && node.__rf.height !== null} + xPos={node.position.x} + yPos={node.position.y} + isDragging={node.isDragging} + isInitialized={node.width !== null && node.height !== null} snapGrid={props.snapGrid} snapToGrid={props.snapToGrid} selectNodesOnDrag={props.selectNodesOnDrag} diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index 66dbe234f..b6bcc6a3b 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -25,7 +25,6 @@ import { OnLoadFunc, Node, Edge, - Connection, ConnectionMode, ConnectionLineType, ConnectionLineComponent, @@ -33,11 +32,13 @@ import { OnConnectStartFunc, OnConnectStopFunc, OnConnectEndFunc, + OnConnectFunc, TranslateExtent, KeyCode, PanOnScrollMode, OnEdgeUpdateFunc, NodeExtent, + ElementChange, } from '../../types'; import '../../style.css'; @@ -57,9 +58,11 @@ const defaultEdgeTypes = { }; export interface ReactFlowProps extends Omit, 'onLoad'> { - elements: Elements; + nodes: Node[]; + edges: Edge[]; + onNodesChange?: (nodeChanges: ElementChange[]) => void; + onEdgesChange?: (edgeChanges: ElementChange[]) => void; onElementClick?: (event: ReactMouseEvent, element: Node | Edge) => void; - onElementsRemove?: (elements: Elements) => void; onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void; onNodeMouseEnter?: (event: ReactMouseEvent, node: Node) => void; onNodeMouseMove?: (event: ReactMouseEvent, node: Node) => void; @@ -68,7 +71,7 @@ export interface ReactFlowProps extends Omit, 'on onNodeDragStart?: (event: ReactMouseEvent, node: Node) => void; onNodeDrag?: (event: ReactMouseEvent, node: Node) => void; onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void; - onConnect?: (connection: Edge | Connection) => void; + onConnect?: OnConnectFunc; onConnectStart?: OnConnectStartFunc; onConnectStop?: OnConnectStopFunc; onConnectEnd?: OnConnectEndFunc; @@ -132,10 +135,14 @@ export interface ReactFlowProps extends Omit, 'on export type ReactFlowRefType = HTMLDivElement; +const initSnapGrid: [number, number] = [15, 15]; +const initDefaultPosition: [number, number] = [0, 0]; + const ReactFlow = forwardRef( ( { - elements = [], + nodes = [], + edges = [], className, nodeTypes = defaultNodeTypes, edgeTypes = defaultEdgeTypes, @@ -144,7 +151,6 @@ const ReactFlow = forwardRef( onMove, onMoveStart, onMoveEnd, - onElementsRemove, onConnect, onConnectStart, onConnectStop, @@ -171,7 +177,7 @@ const ReactFlow = forwardRef( multiSelectionKeyCode = 'Meta', zoomActivationKeyCode = 'Meta', snapToGrid = false, - snapGrid = [15, 15], + snapGrid = initSnapGrid, onlyRenderVisibleElements = false, selectNodesOnDrag = true, nodesDraggable, @@ -180,7 +186,7 @@ const ReactFlow = forwardRef( minZoom, maxZoom, defaultZoom = 1, - defaultPosition = [0, 0], + defaultPosition = initDefaultPosition, translateExtent, preventScrolling = true, nodeExtent, @@ -208,6 +214,8 @@ const ReactFlow = forwardRef( edgeUpdaterRadius = 10, nodeTypesId = '1', edgeTypesId = '1', + onNodesChange, + onEdgesChange, ...rest }, ref @@ -240,7 +248,6 @@ const ReactFlow = forwardRef( connectionLineStyle={connectionLineStyle} connectionLineComponent={connectionLineComponent} selectionKeyCode={selectionKeyCode} - onElementsRemove={onElementsRemove} deleteKeyCode={deleteKeyCode} multiSelectionKeyCode={multiSelectionKeyCode} zoomActivationKeyCode={zoomActivationKeyCode} @@ -287,8 +294,10 @@ const ReactFlow = forwardRef( onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateEnd={onEdgeUpdateEnd} edgeUpdaterRadius={edgeUpdaterRadius} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} /> - + {onSelectionChange && } {children} diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index 340620f50..4a09041b6 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -1,22 +1,23 @@ import { useEffect } from 'react'; -import { useStore, useStoreActions } from '../store/hooks'; +import { useStore, useStoreActions, useStoreState } from '../store/hooks'; import useKeyPress from './useKeyPress'; -import { isNode, getConnectedEdges } from '../utils/graph'; -import { Elements, KeyCode, ElementId, FlowElement } from '../types'; +import { isNode, isEdge, getConnectedEdges } from '../utils/graph'; +import { KeyCode } from '../types'; interface HookParams { deleteKeyCode: KeyCode; multiSelectionKeyCode: KeyCode; - onElementsRemove?: (elements: Elements) => void; } -export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => { +export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { const store = useStore(); const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); const setMultiSelectionActive = useStoreActions((actions) => actions.setMultiSelectionActive); const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements); + const onNodesChange = useStoreState((state) => state.onNodesChange); + const onEdgesChange = useStoreState((state) => state.onEdgesChange); const deleteKeyPressed = useKeyPress(deleteKeyCode); const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); @@ -24,19 +25,21 @@ export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: Hook useEffect(() => { const { edges, selectedElements } = store.getState(); - if (onElementsRemove && deleteKeyPressed && selectedElements) { + if (deleteKeyPressed && selectedElements) { const selectedNodes = selectedElements.filter(isNode); + const selectedEdges = selectedElements.filter(isEdge); const connectedEdges = getConnectedEdges(selectedNodes, edges); - const elementsToRemove = [...selectedElements, ...connectedEdges].reduce( - (res, item) => res.set(item.id, item), - new Map() - ); - onElementsRemove(Array.from(elementsToRemove.values())); + const nodeChanges = selectedNodes.map((n) => ({ id: n.id, delete: true })); + const edgeChanges = [...selectedEdges, ...connectedEdges].map((e) => ({ id: e.id, delete: true })); + + onNodesChange?.(nodeChanges); + onEdgesChange?.(edgeChanges); + unsetNodesSelection(); resetSelectedElements(); } - }, [deleteKeyPressed, onElementsRemove]); + }, [deleteKeyPressed, onNodesChange, onEdgesChange]); useEffect(() => { setMultiSelectionActive(multiSelectionKeyPressed); diff --git a/src/index.ts b/src/index.ts index 589e49c82..b46051bf9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,8 @@ export { updateEdge, getTransformForBounds, getRectOfNodes, + applyNodeChanges, + applyEdgeChanges, } from './utils/graph'; export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'; export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'; diff --git a/src/store/actions.ts b/src/store/actions.ts index 0f6bbc474..17ab0cee7 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -1,6 +1,8 @@ import { createAction } from './utils'; import { + Node, + Edge, Elements, OnConnectEndFunc, OnConnectFunc, @@ -18,6 +20,7 @@ import { SnapGrid, ConnectionMode, NodeExtent, + OnElementsChange, } from '../types'; import * as constants from './contants'; @@ -42,7 +45,8 @@ export const setOnConnectEnd = (onConnectEnd: OnConnectEndFunc) => onConnectEnd, }); -export const setElements = (elements: Elements) => createAction(constants.SET_ELEMENTS, elements); +export const setNodes = (nodes: Node[]) => createAction(constants.SET_NODES, nodes); +export const setEdges = (edges: Edge[]) => createAction(constants.SET_EDGES, edges); export const updateNodeDimensions = (updates: NodeDimensionUpdate[]) => createAction(constants.UPDATE_NODE_DIMENSIONS, updates); @@ -126,12 +130,19 @@ export const setConnectionMode = (connectionMode: ConnectionMode) => export const setNodeExtent = (nodeExtent: NodeExtent) => createAction(constants.SET_NODE_EXTENT, nodeExtent); +export const setOnNodesChange = (onNodesChange: OnElementsChange) => + createAction(constants.SET_ON_NODES_CHANGE, { onNodesChange }); + +export const setOnEdgesChange = (onEdgesChange: OnElementsChange) => + createAction(constants.SET_ON_EDGES_CHANGE, { onEdgesChange }); + export type ReactFlowAction = ReturnType< | typeof setOnConnect | typeof setOnConnectStart | typeof setOnConnectStop | typeof setOnConnectEnd - | typeof setElements + | typeof setNodes + | typeof setEdges | typeof updateNodeDimensions | typeof updateNodePos | typeof updateNodePosDiff @@ -160,4 +171,6 @@ export type ReactFlowAction = ReturnType< | typeof setMultiSelectionActive | typeof setConnectionMode | typeof setNodeExtent + | typeof setOnNodesChange + | typeof setOnEdgesChange >; diff --git a/src/store/configure-store.ts b/src/store/configure-store.ts index 6a33bc103..6005bdf1c 100644 --- a/src/store/configure-store.ts +++ b/src/store/configure-store.ts @@ -1,10 +1,11 @@ -import { createStore, Store } from 'redux'; +import { createStore, applyMiddleware, Store } from 'redux'; +import thunk from 'redux-thunk'; import { ReactFlowState } from '../types'; import { ReactFlowAction } from './actions'; import reactFlowReducer from './reducer'; export default function configureStore(preloadedState: ReactFlowState): Store { - const store = createStore(reactFlowReducer, preloadedState); + const store = createStore(reactFlowReducer, preloadedState, applyMiddleware(thunk)); return store; } diff --git a/src/store/contants.ts b/src/store/contants.ts index 389432df7..890ada356 100644 --- a/src/store/contants.ts +++ b/src/store/contants.ts @@ -2,7 +2,8 @@ export const SET_ON_CONNECT = 'SET_ON_CONNECT'; export const SET_ON_CONNECT_START = 'SET_ON_CONNECT_START'; export const SET_ON_CONNECT_STOP = 'SET_ON_CONNECT_STOP'; export const SET_ON_CONNECT_END = 'SET_ON_CONNECT_END'; -export const SET_ELEMENTS = 'SET_ELEMENTS'; +export const SET_NODES = 'SET_NODES'; +export const SET_EDGES = 'SET_EDGES'; export const UPDATE_NODE_DIMENSIONS = 'UPDATE_NODE_DIMENSIONS'; export const UPDATE_NODE_POS = 'UPDATE_NODE_POS'; export const UPDATE_NODE_POS_DIFF = 'UPDATE_NODE_POS_DIFF'; @@ -31,3 +32,5 @@ export const SET_ELEMENTS_SELECTABLE = 'SET_ELEMENTS_SELECTABLE'; export const SET_MULTI_SELECTION_ACTIVE = 'SET_MULTI_SELECTION_ACTIVE'; export const SET_CONNECTION_MODE = 'SET_CONNECTION_MODE'; export const SET_NODE_EXTENT = 'SET_NODE_EXTENT'; +export const SET_ON_NODES_CHANGE = 'SET_ON_NODES_CHANGE'; +export const SET_ON_EDGES_CHANGE = 'SET_ON_EDGES_CHANGE'; diff --git a/src/store/index.ts b/src/store/index.ts index 6640d9e06..fa2eda770 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -8,6 +8,9 @@ export const initialState: ReactFlowState = { transform: [0, 0, 1], nodes: [], edges: [], + onNodesChange: null, + onEdgesChange: null, + selectedElements: null, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, diff --git a/src/store/reducer.ts b/src/store/reducer.ts index d9b9fbb40..f5b7b18f9 100644 --- a/src/store/reducer.ts +++ b/src/store/reducer.ts @@ -1,109 +1,111 @@ import isEqual from 'fast-deep-equal'; import { clampPosition, getDimensions } from '../utils'; -import { - getNodesInside, - getConnectedEdges, - getRectOfNodes, - isNode, - isEdge, - parseNode, - parseEdge, -} from '../utils/graph'; +import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, parseNode, parseEdge } from '../utils/graph'; import { getHandleBounds } from '../components/Nodes/utils'; +import { getSourceTargetNodes } from '../container/EdgeRenderer/utils'; -import { ReactFlowState, Node, XYPosition, Edge } from '../types'; +import { ReactFlowState, Node, XYPosition, Edge, ElementChange } from '../types'; import * as constants from './contants'; import { ReactFlowAction } from './actions'; import { initialState } from './index'; -type NextElements = { - nextNodes: Node[]; - nextEdges: Edge[]; -}; - export default function reactFlowReducer(state = initialState, action: ReactFlowAction): ReactFlowState { switch (action.type) { - case constants.SET_ELEMENTS: { - const propElements = action.payload; - const nextElements: NextElements = { - nextNodes: [], - nextEdges: [], - }; - const { nextNodes, nextEdges } = propElements.reduce((res, propElement): NextElements => { - if (isNode(propElement)) { - const storeNode = state.nodes.find((node) => node.id === propElement.id); + case constants.SET_NODES: { + const propNodes = action.payload; + const nextNodes = propNodes.map((propNode: Node) => { + const storeNode = state.nodes.find((node) => node.id === propNode.id); - if (storeNode) { + if (storeNode) { + if (typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type) { const updatedNode: Node = { ...storeNode, - ...propElement, + ...propNode, }; + // we reset the elements dimensions here in order to force a re-calculation of the bounds. + // When the type of a node changes it is possible that the number or positions of handles changes too. + updatedNode.width = null; + return updatedNode; + } + } + + return parseNode(propNode, state.nodeExtent); + }); + + const updatedEdges = state.edges.map((edge) => { + const { sourceNode, targetNode } = getSourceTargetNodes(edge, nextNodes); + + if (sourceNode) { + edge.sourceNode = sourceNode; + } + if (targetNode) { + edge.targetNode = targetNode; + } + + return edge; + }); - if (storeNode.position.x !== propElement.position.x || storeNode.position.y !== propElement.position.y) { - updatedNode.__rf.position = propElement.position; - } + return { ...state, nodes: nextNodes, edges: updatedEdges }; + } + case constants.SET_EDGES: { + const propElements = action.payload; + const nextEdges = propElements.map((propEdge: Edge) => { + const storeEdge = state.edges.find((se) => se.id === propEdge.id); - if (typeof propElement.type !== 'undefined' && propElement.type !== storeNode.type) { - // we reset the elements dimensions here in order to force a re-calculation of the bounds. - // When the type of a node changes it is possible that the number or positions of handles changes too. - updatedNode.__rf.width = null; - } + if (storeEdge) { + return parseEdge(propEdge); + } else { + const parsedEdge = parseEdge(propEdge); + const { sourceNode, targetNode } = getSourceTargetNodes(parsedEdge, state.nodes); - res.nextNodes.push(updatedNode); - } else { - res.nextNodes.push(parseNode(propElement, state.nodeExtent)); + if (sourceNode) { + parsedEdge.sourceNode = sourceNode; } - } else if (isEdge(propElement)) { - const storeEdge = state.edges.find((se) => se.id === propElement.id); - - if (storeEdge) { - res.nextEdges.push({ - ...storeEdge, - ...propElement, - }); - } else { - res.nextEdges.push(parseEdge(propElement)); + if (targetNode) { + parsedEdge.targetNode = targetNode; } - } - return res; - }, nextElements); + return parsedEdge; + } + }); - return { ...state, nodes: nextNodes, edges: nextEdges }; + return { ...state, edges: nextEdges }; } case constants.UPDATE_NODE_DIMENSIONS: { - const updatedNodes = state.nodes.map((node) => { + const initialChanges: ElementChange[] = []; + const nodesToChange: ElementChange[] = state.nodes.reduce((res, node) => { const update = action.payload.find((u) => u.id === node.id); if (update) { const dimensions = getDimensions(update.nodeElement); const doUpdate = dimensions.width && dimensions.height && - (node.__rf.width !== dimensions.width || node.__rf.height !== dimensions.height || update.forceUpdate); + (node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate); if (doUpdate) { const handleBounds = getHandleBounds(update.nodeElement, state.transform[2]); - - return { - ...node, - __rf: { - ...node.__rf, + const change = { + id: node.id, + change: { ...dimensions, handleBounds, }, - }; + } as ElementChange; + + res.push(change); } } - return node; - }); + return res; + }, initialChanges); - return { - ...state, - nodes: updatedNodes, - }; + if (state.onNodesChange) { + requestAnimationFrame(() => state.onNodesChange?.(nodesToChange)); + } + + return state; } case constants.UPDATE_NODE_POS: { const { id, pos } = action.payload; @@ -117,13 +119,20 @@ export default function reactFlowReducer(state = initialState, action: ReactFlow }; } + if (state.onNodesChange) { + state.onNodesChange([{ id, change: { position } }]); + + return state; + } + const nextNodes = state.nodes.map((node) => { if (node.id === id) { return { ...node, + position, + __rf: { ...node.__rf, - position, }, }; } @@ -136,30 +145,28 @@ export default function reactFlowReducer(state = initialState, action: ReactFlow case constants.UPDATE_NODE_POS_DIFF: { const { id, diff, isDragging } = action.payload; - const nextNodes = state.nodes.map((node) => { - if (id === node.id || state.selectedElements?.find((sNode) => sNode.id === node.id)) { - const updatedNode = { - ...node, - __rf: { - ...node.__rf, - isDragging, - }, - }; - - if (diff) { - updatedNode.__rf.position = { - x: node.__rf.position.x + diff.x, - y: node.__rf.position.y + diff.y, - }; - } - - return updatedNode; + if (state.onNodesChange && id && diff) { + const matchingNode = state.nodes.find((n) => n.id === id); + + if (matchingNode) { + requestAnimationFrame(() => + state.onNodesChange?.([ + { + id, + change: { + position: { + x: matchingNode.position.x + diff.x, + y: matchingNode.position.y + diff.y, + isDragging, + }, + }, + }, + ]) + ); } + } - return node; - }); - - return { ...state, nodes: nextNodes }; + return state; } case constants.SET_USER_SELECTION: { const mousePos = action.payload; @@ -308,9 +315,9 @@ export default function reactFlowReducer(state = initialState, action: ReactFlow nodes: state.nodes.map((node) => { return { ...node, + position: clampPosition(node.position, nodeExtent), __rf: { ...node.__rf, - position: clampPosition(node.__rf.position, nodeExtent), }, }; }), @@ -334,6 +341,8 @@ export default function reactFlowReducer(state = initialState, action: ReactFlow case constants.SET_ELEMENTS_SELECTABLE: case constants.SET_MULTI_SELECTION_ACTIVE: case constants.SET_CONNECTION_MODE: + case constants.SET_ON_NODES_CHANGE: + case constants.SET_ON_EDGES_CHANGE: return { ...state, ...action.payload }; default: return state; diff --git a/src/types/index.ts b/src/types/index.ts index 754e10e24..6de72b6d4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -9,6 +9,12 @@ export type Elements = Array>; export type Transform = [number, number, number]; +export type ElementChange = { + id: string; + change?: any; + delete?: boolean; +}; + export enum Position { Left = 'left', Top = 'top', @@ -50,6 +56,10 @@ export interface Node { selectable?: boolean; connectable?: boolean; dragHandle?: string; + isDragging?: boolean; + width?: number | null; + height?: number | null; + handleBounds?: any; } export enum ArrowHeadType { @@ -76,6 +86,8 @@ export interface Edge { isHidden?: boolean; data?: T; className?: string; + sourceNode?: Node; + targetNode?: Node; } export enum BackgroundVariant { @@ -313,7 +325,7 @@ export type ConnectionLineComponentProps = { export type ConnectionLineComponent = React.ComponentType; -export type OnConnectFunc = (connection: Connection) => void; +export type OnConnectFunc = (connection: Connection, nodes: Node[]) => void; export type OnConnectStartParams = { nodeId: ElementId | null; handleId: ElementId | null; @@ -398,6 +410,8 @@ export type InitD3ZoomPayload = { transform: Transform; }; +export type OnElementsChange = (nodes: ElementChange[]) => void; + export interface ReactFlowState { width: number; height: number; @@ -406,6 +420,8 @@ export interface ReactFlowState { edges: Edge[]; selectedElements: Elements | null; selectedNodesBbox: Rect; + onNodesChange: OnElementsChange | null; + onEdgesChange: OnElementsChange | null; d3Zoom: ZoomBehavior | null; d3Selection: D3Selection | null; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 73b6c68b5..86d13a6e4 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -15,6 +15,7 @@ import { FlowExportObject, ReactFlowState, NodeExtent, + ElementChange, } from '../types'; export const isEdge = (element: Node | Connection | Edge): element is Edge => @@ -68,10 +69,10 @@ const connectionExists = (edge: Edge, elements: Elements) => { ); }; -export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elements => { +export const addEdge = (edgeParams: Edge | Connection, nodes: Node[], edges: Edge[]): Edge[] => { if (!edgeParams.source || !edgeParams.target) { console.warn("Can't create edge. An edge needs a source and a target."); - return elements; + return edges; } let edge: Edge; @@ -84,11 +85,11 @@ export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elem } as Edge; } - if (connectionExists(edge, elements)) { - return elements; + if (connectionExists(edge, nodes)) { + return edges; } - return elements.concat(edge); + return edges.concat(edge); }; export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: Elements): Elements => { @@ -147,30 +148,23 @@ export const onLoadProject = (currentStore: Store) => { }; export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => { - return { - ...node, - id: node.id.toString(), - type: node.type || 'default', - __rf: { - position: clampPosition(node.position, nodeExtent), - width: null, - height: null, - handleBounds: {}, - isDragging: false, - }, - }; + if (!node.type) { + node.type = 'default'; + } + + if (nodeExtent) { + node.position = clampPosition(node.position, nodeExtent); + } + + return node; }; export const parseEdge = (edge: Edge): Edge => { - return { - ...edge, - source: edge.source.toString(), - target: edge.target.toString(), - sourceHandle: edge.sourceHandle ? edge.sourceHandle.toString() : null, - targetHandle: edge.targetHandle ? edge.targetHandle.toString() : null, - id: edge.id.toString(), - type: edge.type || 'default', - }; + if (!edge.type) { + edge.type = 'default'; + } + + return edge; }; const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({ @@ -199,8 +193,8 @@ export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect => export const getRectOfNodes = (nodes: Node[]): Rect => { const box = nodes.reduce( - (currBox, { __rf: { position, width, height } = {} }) => - getBoundsOfBoxes(currBox, rectToBox({ ...position, width, height })), + (currBox, { position, width, height }) => + getBoundsOfBoxes(currBox, rectToBox({ ...position, width: width || 0, height: height || 0 })), { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity } ); @@ -227,12 +221,12 @@ export const getNodesInside = ( height: rect.height / tScale, }); - return nodes.filter(({ selectable = true, __rf: { position, width, height, isDragging } }) => { + return nodes.filter(({ selectable = true, position, width, height, isDragging }) => { if (excludeNonSelectableNodes && !selectable) { return false; } - const nBox = rectToBox({ ...position, width, height }); + const nBox = rectToBox({ ...position, width: width || 0, height: height || 0 }); const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x)); const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y)); const overlappingArea = Math.ceil(xOverlap * yOverlap); @@ -246,7 +240,7 @@ export const getNodesInside = ( return overlappingArea > 0; } - const area = width * height; + const area = (width || 0) * (height || 0); return overlappingArea >= area; }); @@ -259,17 +253,7 @@ export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => { }; const parseElements = (nodes: Node[], edges: Edge[]): Elements => { - return [ - ...nodes.map((node) => { - const n = { ...node }; - - n.position = n.__rf.position; - - delete n.__rf; - return n; - }), - ...edges.map((e) => ({ ...e })), - ]; + return [...nodes.map((n) => ({ ...n })), ...edges.map((e) => ({ ...e }))]; }; export const onLoadGetElements = (currentStore: Store) => { @@ -311,3 +295,31 @@ export const getTransformForBounds = ( return [x, y, clampedZoom]; }; + +function applyChanges(changes: ElementChange[], elements: any[]): any[] { + const initElements: any[] = []; + + return elements.reduce((res: any[], node: any) => { + const hasChange = changes.find((c) => c.id === node.id); + + if (hasChange?.delete) { + return res; + } + + if (hasChange?.change) { + res.push({ ...node, ...hasChange.change }); + } else { + res.push(node); + } + + return res; + }, initElements); +} + +export function applyNodeChanges(changes: ElementChange[], nodes: Node[]): Node[] { + return applyChanges(changes, nodes) as Node[]; +} + +export function applyEdgeChanges(changes: ElementChange[], edges: Edge[]): Edge[] { + return applyChanges(changes, edges) as Edge[]; +}