diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index 82683ecc..f86bffb5 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -28,7 +28,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 4' }, position: { x: 100, y: 200 }, className: 'light', - style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, + style: { backgroundColor: 'rgba(255, 0, 0, 1)' }, width: 600, height: 300, }, @@ -44,7 +44,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 4b' }, position: { x: 80, y: 80 }, className: 'light', - style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, + style: { backgroundColor: 'rgba(255, 255, 0, 0.2)' }, parentNode: '4', height: 200, width: 350, diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index f59d1eb6..22fecc7a 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -67,7 +67,8 @@ export default (NodeComponent: ComponentType) => { const nodeStyle: CSSProperties = useMemo( () => ({ - zIndex: isSelected ? zIndex + 1 : zIndex, + zIndex, + // zIndex: isSelected ? zIndex + 1 : zIndex, transform: `translate(${xPos}px,${yPos}px)`, pointerEvents: isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none', diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index a462c73a..90a4caed 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -235,8 +235,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { connectionMode, } = useStore(selector, shallow); const nodeLookup = useNodeLookup(); - - const edges = useVisibleEdges(props.onlyRenderVisibleElements); + const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeLookup); if (!width) { return null; @@ -246,59 +245,62 @@ const EdgeRenderer = (props: EdgeRendererProps) => { const renderConnectionLine = connectionNodeId && connectionHandleType; return ( - - - - {edges.map((edge: Edge) => { - // @todo: getSourceTargetNodes is called many times during dragging/creating edges - const sourceNode = nodeLookup.get(edge.source); - const targetNode = nodeLookup.get(edge.target); + <> + {edgeTree.map(({ level, edges, isMaxLevel }) => ( + + {isMaxLevel && } + + {edges.map((edge: Edge) => { + const sourceNode = nodeLookup.get(edge.source); + const targetNode = nodeLookup.get(edge.target); - return ( - - ); - })} - {renderConnectionLine && ( - - )} - - + return ( + + ); + })} + {renderConnectionLine && isMaxLevel && ( + + )} + + + ))} + ); }; diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index df89027a..cbaa3820 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, memo } from 'react'; -import { useStoreApi } from '../../store'; +import { useStoreApi, useStore } from '../../store'; import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; @@ -82,7 +82,8 @@ const GraphView = ({ }: GraphViewProps) => { const isInitialized = useRef(false); const store = useStoreApi(); - const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper(); + const { zoomIn, zoomOut, zoomTo, transform: setTransform, fitView, initialized } = useZoomPanHelper(); + const transform = useStore((s) => s.transform); useEffect(() => { if (!isInitialized.current && initialized) { @@ -92,7 +93,7 @@ const GraphView = ({ zoomIn, zoomOut, zoomTo, - setTransform: transform, + setTransform: setTransform, project: onLoadProject(store.getState), getNodes: onLoadGetNodes(store.getState), getEdges: onLoadGetEdges(store.getState), @@ -104,6 +105,11 @@ const GraphView = ({ } }, [onLoad, zoomIn, zoomOut, zoomTo, transform, fitView, initialized]); + const transformStyle = { + transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`, + transformOrigin: '0 0', + }; + return ( - - +
+ + +
); }; diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index a75c3271..a3e70cbf 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -1,16 +1,8 @@ -import React, { memo, useMemo, ComponentType, MouseEvent, Fragment } from 'react'; +import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; import shallow from 'zustand/shallow'; import { useStore } from '../../store'; -import { - Node, - NodeTypesType, - ReactFlowState, - WrapNodeProps, - SnapGrid, - NodeRendererNode, - NodeLookup, -} from '../../types'; +import { Node, NodeTypesType, ReactFlowState, WrapNodeProps, SnapGrid, NodeRendererNode } from '../../types'; import useVisibleNodes from '../../hooks/useVisibleNodes'; import useNodeLookup from '../../hooks/useNodeLookup'; interface NodeRendererProps { @@ -39,9 +31,12 @@ const selector = (s: ReactFlowState) => ({ nodeLookup: s.nodeLookup, }); -interface NodesProps extends NodeRendererProps { - nodes: NodeRendererNode[]; - nodeLookup: NodeLookup; +interface NodeProps extends NodeRendererProps { + node: Node; + nodeType: string; + childNodes?: NodeRendererNode[]; + xPos?: number; + yPos?: number; isDraggable?: boolean; resizeObserver: ResizeObserver | null; scale: number; @@ -50,16 +45,8 @@ interface NodesProps extends NodeRendererProps { nodesDraggable: boolean; nodesConnectable: boolean; elementsSelectable: boolean; - recursionDepth: number; - parentId?: string; -} - -interface NodeProps extends Omit { - node: Node; - nodeType: string; - childNodes?: NodeRendererNode[]; - positionAbsoluteX?: number; - positionAbsoluteY?: number; + treeLevel?: number; + isParentNode?: boolean; } function Node({ @@ -74,9 +61,10 @@ function Node({ nodesDraggable, nodesConnectable, elementsSelectable, - recursionDepth, - positionAbsoluteX, - positionAbsoluteY, + treeLevel = 0, + xPos, + yPos, + isParentNode = false, ...props }: NodeProps) { // const onNodesChange = useStore((s) => s.onNodesChange); @@ -93,8 +81,6 @@ function Node({ typeof node.width !== 'undefined' && typeof node.height !== 'undefined'; - const isParentNode = !!childNodes?.length; - return ( ); } -function Nodes({ - nodes, - nodeLookup, - isDraggable, - resizeObserver, - scale, - snapToGrid, - snapGrid, - nodesDraggable, - nodesConnectable, - elementsSelectable, - recursionDepth, - ...props -}: NodesProps): any { - return nodes.map(({ node, childNodes }) => { - const nodeType = node.type || 'default'; - const lookupNode = nodeLookup.get(node.id); - - if (!props.nodeTypes[nodeType]) { - console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); - } - - return ( - - - {childNodes && childNodes.length > 0 && ( - - )} - - ); - }); -} - -const MemoizedNodes = memo(Nodes); - const NodeRenderer = (props: NodeRendererProps) => { const { transform, @@ -210,17 +132,10 @@ const NodeRenderer = (props: NodeRendererProps) => { snapGrid, snapToGrid, } = useStore(selector, shallow); + const nodeLookup = useNodeLookup(); - const nodes = useVisibleNodes(props.onlyRenderVisibleElements); - const transformStyle = useMemo( - () => ({ - transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`, - }), - [transform[0], transform[1], transform[2]] - ); - const resizeObserver = useMemo(() => { if (typeof ResizeObserver === 'undefined') { return null; @@ -236,23 +151,36 @@ const NodeRenderer = (props: NodeRendererProps) => { }); }, []); - console.log(nodeLookup); - return ( -
- +
+ {nodes.map((node) => { + const nodeType = node.type || 'default'; + const lookupNode = nodeLookup.get(node.id); + + if (!props.nodeTypes[nodeType]) { + console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); + } + + return ( + + ); + })}
); }; diff --git a/src/hooks/useVisibleEdges.ts b/src/hooks/useVisibleEdges.ts index 74406400..14cd22e9 100644 --- a/src/hooks/useVisibleEdges.ts +++ b/src/hooks/useVisibleEdges.ts @@ -1,10 +1,40 @@ import { useCallback } from 'react'; import { useStore } from '../store'; -import { isEdgeVisible, getSourceTargetNodes } from '../container/EdgeRenderer/utils'; -import { ReactFlowState } from '../types'; +import { isEdgeVisible } from '../container/EdgeRenderer/utils'; +import { ReactFlowState, NodeLookup, Edge } from '../types'; -function useVisibleEdges(onlyRenderVisible: boolean) { +function groupEdgesByTreeLevel(edges: Edge[], nodeLookup: NodeLookup) { + let maxLevel = -1; + + const levelLookup = edges.reduce>((tree, edge) => { + const treeLevel = Math.max( + nodeLookup.get(edge.source)?.treeLevel || 0, + nodeLookup.get(edge.target)?.treeLevel || 0 + ); + if (tree[treeLevel]) { + tree[treeLevel].push(edge); + } else { + tree[treeLevel] = [edge]; + } + + maxLevel = treeLevel > maxLevel ? treeLevel : maxLevel; + + return tree; + }, {}); + + return Object.entries(levelLookup).map(([key, edges]) => { + const level = +key; + + return { + edges, + level, + isMaxLevel: level === maxLevel, + }; + }); +} + +function useVisibleEdges(onlyRenderVisible: boolean, nodeLookup: NodeLookup) { const edges = useStore( useCallback( (s: ReactFlowState) => { @@ -13,7 +43,8 @@ function useVisibleEdges(onlyRenderVisible: boolean) { } return s.edges.filter((e) => { - const { sourceNode, targetNode } = getSourceTargetNodes(e, s.nodes); + const sourceNode = nodeLookup.get(e.source); + const targetNode = nodeLookup.get(e.target); return ( sourceNode?.width && @@ -21,8 +52,8 @@ function useVisibleEdges(onlyRenderVisible: boolean) { targetNode?.width && targetNode?.height && isEdgeVisible({ - sourcePos: sourceNode.position, - targetPos: targetNode.position, + sourcePos: sourceNode.position || { x: 0, y: 0 }, + targetPos: targetNode.position || { x: 0, y: 0 }, sourceWidth: sourceNode.width, sourceHeight: sourceNode.height, targetWidth: targetNode.width, @@ -34,11 +65,11 @@ function useVisibleEdges(onlyRenderVisible: boolean) { ); }); }, - [onlyRenderVisible] + [onlyRenderVisible, nodeLookup] ) ); - return edges; + return groupEdgesByTreeLevel(edges, nodeLookup); } export default useVisibleEdges; diff --git a/src/hooks/useVisibleNodes.ts b/src/hooks/useVisibleNodes.ts index 6f2b613d..d6c9bd96 100644 --- a/src/hooks/useVisibleNodes.ts +++ b/src/hooks/useVisibleNodes.ts @@ -2,26 +2,7 @@ import { useCallback } from 'react'; import { useStore } from '../store'; import { getNodesInside } from '../utils/graph'; -import { ReactFlowState, Node, NodeRendererNode } from '../types'; - -function getChildNodes(nodes: Node[], parent?: Node): NodeRendererNode[] { - const children: NodeRendererNode[] = []; - const remaining: Node[] = []; - - for (let i = 0; i < nodes.length; i++) { - const n = nodes[i]; - if ((!parent && !n.parentNode) || n.parentNode === parent?.id) { - children.push({ node: n }); - } else { - remaining.push(n); - } - } - - return children.map((child) => { - child.childNodes = getChildNodes(remaining, child.node); - return child; - }); -} +import { ReactFlowState } from '../types'; function useVisibleNodes(onlyRenderVisible: boolean) { const nodes = useStore( @@ -35,7 +16,7 @@ function useVisibleNodes(onlyRenderVisible: boolean) { ) ); - return getChildNodes(nodes); + return nodes; } export default useVisibleNodes; diff --git a/src/store/index.ts b/src/store/index.ts index b4f50a30..ccfaa8b5 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -40,39 +40,6 @@ const createNodeOrEdgeSelectionChange = (isSelected: boolean) => (item: Node | E isSelected, }); -// @todo needs refactoring / improvements -// function findMatchingNodes(id: string | undefined, nodes: Node[]): Node[] { -// if (!id) { -// return nodes.filter((n) => !!n.isSelected); -// } - -// const result = []; -// const children = []; - -// for (let i = 0; i < nodes.length; i++) { -// const n = nodes[i]; - -// if (n.id === id) { -// result.push(n); -// } - -// if (n.parentNode === id) { -// children.push(n); -// } -// } - -// for (let i = 0; i < children.length; i++) { -// const n = children[i]; -// const matches = findMatchingNodes(n.id, nodes); - -// for (let j = 0; j < matches.length; j++) { -// result.push(matches[j]); -// } -// } - -// return result; -// } - type XYPosAndTreeLevel = XYPosition & { treeLevel: number }; function addPositions(a: XYPosAndTreeLevel, b: XYPosition): XYPosAndTreeLevel { @@ -185,6 +152,8 @@ const createStore = () => treeLevel: 0, }); + nodeLookup.set(node.parentNode!, { ...nodeLookup.get(node.parentNode!), isParentNode: true }); + if (positionAbsoluteAndTreeLevel) { nodeLookup.set(node.id, { ...nodeLookup.get(node.id), diff --git a/src/style.css b/src/style.css index 8fac54d9..6064fde8 100644 --- a/src/style.css +++ b/src/style.css @@ -38,7 +38,6 @@ top: 0; left: 0; pointer-events: none; - z-index: 2; } .react-flow__edge { @@ -87,7 +86,6 @@ height: 100%; pointer-events: none; transform-origin: 0 0; - z-index: 3; } .react-flow__node { @@ -186,3 +184,10 @@ bottom: 10px; right: 10px; } + +.react-flow__container { + position: absolute; + width: 100%; + height: 100%; + pointer-events: none; +} diff --git a/src/types/index.ts b/src/types/index.ts index e9a23cac..b5801a52 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -466,6 +466,7 @@ export type NodeLookupItem = { positionAbsolute?: XYPosition; handleBounds?: NodeHandleBounds; treeLevel?: number; + isParentNode?: boolean; }; export type NodeLookup = Map;