diff --git a/packages/react/src/components/Edges/wrapEdge.tsx b/packages/react/src/components/Edges/wrapEdge.tsx index 8172dfb6..3562b0f6 100644 --- a/packages/react/src/components/Edges/wrapEdge.tsx +++ b/packages/react/src/components/Edges/wrapEdge.tsx @@ -1,14 +1,14 @@ -import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent } from 'react'; +import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent, useEffect } from 'react'; import cc from 'classcat'; import { getMarkerId, elementSelectionKeys, XYHandle, type Connection } from '@xyflow/system'; -import { useStoreApi } from '../../hooks/useStore'; +import { useStoreApi, useStore } from '../../hooks/useStore'; import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions'; import { EdgeAnchor } from './EdgeAnchor'; import { getMouseHandler } from './utils'; import type { EdgeProps, WrapEdgeProps } from '../../types'; - -const alwaysValidConnection = () => true; +import { getEdgePosition } from '../../hooks/useVisibleEdges'; +import { shallow } from 'zustand/shallow'; export default (EdgeComponent: ComponentType) => { const EdgeWrapper = ({ @@ -29,12 +29,6 @@ export default (EdgeComponent: ComponentType) => { style, source, target, - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, isSelectable, hidden, sourceHandleId, @@ -60,11 +54,34 @@ export default (EdgeComponent: ComponentType) => { const [updateHover, setUpdateHover] = useState(false); const [updating, setUpdating] = useState(false); const store = useStoreApi(); + const edgePosition = useStore((state) => { + const sourceNode = state.nodeInternals.get(source); + const targetNode = state.nodeInternals.get(target); + + if (!sourceNode || !targetNode) { + return null; + } + + const pos = getEdgePosition({ + sourceNode, + targetNode, + sourceHandle: sourceHandleId || null, + targetHandle: targetHandleId || null, + connectionMode: state.connectionMode, + onError: state.onError, + }); + + return pos; + }, shallow); + + useEffect(() => { + // console.log(edgePosition); + }, [edgePosition]); const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]); const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]); - if (hidden) { + if (hidden || !edgePosition) { return null; } @@ -211,12 +228,12 @@ export default (EdgeComponent: ComponentType) => { labelBgBorderRadius={labelBgBorderRadius} data={data} style={style} - sourceX={sourceX} - sourceY={sourceY} - targetX={targetX} - targetY={targetY} - sourcePosition={sourcePosition} - targetPosition={targetPosition} + sourceX={edgePosition.sourceX} + sourceY={edgePosition.sourceY} + targetX={edgePosition.targetX} + targetY={edgePosition.targetY} + sourcePosition={edgePosition.sourcePosition} + targetPosition={edgePosition.targetPosition} sourceHandleId={sourceHandleId} targetHandleId={targetHandleId} markerStart={markerStartUrl} @@ -229,9 +246,9 @@ export default (EdgeComponent: ComponentType) => { <> {(isUpdatable === 'source' || isUpdatable === true) && ( ) => { )} {(isUpdatable === 'target' || isUpdatable === true) && ( ({ - nodesConnectable: s.nodesConnectable, + width: s.width, + height: s.height, edgesFocusable: s.edgesFocusable, edgesUpdatable: s.edgesUpdatable, elementsSelectable: s.elementsSelectable, - width: s.width, - height: s.height, connectionMode: s.connectionMode, - nodeInternals: s.nodeInternals, onError: s.onError, }); @@ -65,9 +62,8 @@ const EdgeRenderer = ({ onEdgeUpdateEnd, children, }: EdgeRendererProps) => { - const { edgesFocusable, edgesUpdatable, elementsSelectable, width, height, connectionMode, nodeInternals, onError } = - useStore(selector, shallow); - const edgeTree = useVisibleEdges(onlyRenderVisibleElements, nodeInternals, elevateEdgesOnSelect); + const { width, height, edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow); + const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect); if (!width) { return null; @@ -85,14 +81,7 @@ const EdgeRenderer = ({ > {isMaxLevel && } - {edges.map((edge: Edge) => { - const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source)); - const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target)); - - if (!sourceIsValid || !targetIsValid) { - return null; - } - + {edges.map((edge) => { let edgeType = edge.type || 'default'; if (!edgeTypes[edgeType]) { @@ -100,16 +89,7 @@ const EdgeRenderer = ({ edgeType = 'default'; } - const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default; - // when connection type is loose we can define all handles as sources and connect source -> source - const targetNodeHandles = - connectionMode === ConnectionMode.Strict - ? targetHandleBounds!.target - : (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []); - const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle); - const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle); - const sourcePosition = sourceHandle?.position || Position.Bottom; - const targetPosition = targetHandle?.position || Position.Top; + const EdgeComponent = edgeTypes[edgeType]; const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined')); const isUpdatable = typeof onEdgeUpdate !== 'undefined' && @@ -119,27 +99,12 @@ const EdgeRenderer = ({ (elementsSelectable && typeof edge.selectable === 'undefined') ); - if (!sourceHandle || !targetHandle) { - onError?.('008', errorMessages['error008'](sourceHandle, edge)); - - return null; - } - - const { sourceX, sourceY, targetX, targetY } = getEdgePositions( - sourceNodeRect, - sourceHandle, - sourcePosition, - targetNodeRect, - targetHandle, - targetPosition - ); - return ( >((tree, edge) => { @@ -51,42 +67,104 @@ function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals, elevate return edgeTree; } -function useVisibleEdges(onlyRenderVisible: boolean, nodeInternals: NodeInternals, elevateEdgesOnSelect: boolean) { +type LayoutEdgeParams = { + sourceNode: BaseNode; + sourceHandle: string | null; + targetNode: BaseNode; + targetHandle: string | null; + connectionMode: ConnectionMode; + onError?: OnError; +}; + +export function getEdgePosition(params: LayoutEdgeParams): EdgePosition | null { + const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(params.sourceNode); + const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(params.targetNode); + + if (!sourceIsValid || !targetIsValid) { + return null; + } + + // when connection type is loose we can define all handles as sources and connect source -> source + const targetNodeHandles = + params.connectionMode === ConnectionMode.Strict + ? targetHandleBounds!.target + : (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []); + const sourceHandle = getHandle(sourceHandleBounds!.source!, params.sourceHandle); + const targetHandle = getHandle(targetNodeHandles!, params.targetHandle); + const sourcePosition = sourceHandle?.position || Position.Bottom; + const targetPosition = targetHandle?.position || Position.Top; + + if (!sourceHandle || !targetHandle) { + params.onError?.('008', errorMessages['error008'](sourceHandle, {} as Edge)); + + return null; + } + + const { sourceX, sourceY, targetX, targetY } = getEdgePositions( + sourceNodeRect, + sourceHandle, + sourcePosition, + targetNodeRect, + targetHandle, + targetPosition + ); + + return { + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + }; +} + +function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boolean): GroupedEdges[] { const edges = useStore( useCallback( (s: ReactFlowState) => { - if (!onlyRenderVisible) { - return s.edges; - } + const visibleEdges = onlyRenderVisible + ? s.edges.filter((e) => { + const sourceNode = s.nodeInternals.get(e.source); + const targetNode = s.nodeInternals.get(e.target); - return s.edges.filter((e) => { - const sourceNode = nodeInternals.get(e.source); - const targetNode = nodeInternals.get(e.target); - - return ( - sourceNode?.width && - sourceNode?.height && - targetNode?.width && - targetNode?.height && - isEdgeVisible({ - sourcePos: sourceNode.positionAbsolute || { x: 0, y: 0 }, - targetPos: targetNode.positionAbsolute || { x: 0, y: 0 }, - sourceWidth: sourceNode.width, - sourceHeight: sourceNode.height, - targetWidth: targetNode.width, - targetHeight: targetNode.height, - width: s.width, - height: s.height, - transform: s.transform, + return ( + sourceNode?.width && + sourceNode?.height && + targetNode?.width && + targetNode?.height && + isEdgeVisible({ + sourcePos: sourceNode.positionAbsolute || { x: 0, y: 0 }, + targetPos: targetNode.positionAbsolute || { x: 0, y: 0 }, + sourceWidth: sourceNode.width, + sourceHeight: sourceNode.height, + targetWidth: targetNode.width, + targetHeight: targetNode.height, + width: s.width, + height: s.height, + transform: s.transform, + }) + ); }) - ); - }); + : s.edges; + + return groupEdgesByZLevel(visibleEdges, s.nodeInternals, elevateEdgesOnSelect); }, - [onlyRenderVisible, nodeInternals] - ) + [onlyRenderVisible, elevateEdgesOnSelect] + ), + (groupA, groupB) => { + const unEqual = groupA.some( + (item, index) => + item.isMaxLevel !== groupB[index].isMaxLevel || + item.level !== groupB[index].level || + !shallow(item.edges, groupB[index].edges) + ); + + return !unEqual; + } ); - return groupEdgesByZLevel(edges, nodeInternals, elevateEdgesOnSelect); + return edges; } export default useVisibleEdges; diff --git a/packages/react/src/types/edges.ts b/packages/react/src/types/edges.ts index b071f42c..6f13d97c 100644 --- a/packages/react/src/types/edges.ts +++ b/packages/react/src/types/edges.ts @@ -54,12 +54,6 @@ export type WrapEdgeProps = Omit, 'sourceHandle' | 'targetHandl onEdgeDoubleClick?: EdgeMouseHandler; sourceHandleId?: string | null; targetHandleId?: string | null; - sourceX: number; - sourceY: number; - targetX: number; - targetY: number; - sourcePosition: Position; - targetPosition: Position; onEdgeUpdate?: OnEdgeUpdateFunc; onContextMenu?: EdgeMouseHandler; onMouseEnter?: EdgeMouseHandler; @@ -88,18 +82,8 @@ export type EdgeProps = Pick< Edge, 'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target' > & - Pick< - WrapEdgeProps, - | 'sourceX' - | 'sourceY' - | 'targetX' - | 'targetY' - | 'sourcePosition' - | 'targetPosition' - | 'sourceHandleId' - | 'targetHandleId' - | 'interactionWidth' - > & + Pick & + EdgePosition & EdgeLabelOptions & { markerStart?: string; markerEnd?: string; @@ -140,3 +124,12 @@ export type ConnectionLineComponentProps = { }; export type ConnectionLineComponent = ComponentType; + +export type EdgePosition = { + sourceX: number; + sourceY: number; + targetX: number; + targetY: number; + sourcePosition: Position; + targetPosition: Position; +}; diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts index f459f3f0..16d6e99f 100644 --- a/packages/system/src/xyhandle/XYHandle.ts +++ b/packages/system/src/xyhandle/XYHandle.ts @@ -90,7 +90,7 @@ function onPointerDown( getTransform, }: OnPointerDownParams ) { - // when react-flow is used inside a shadow root we can't use document + // when xyflow is used inside a shadow root we can't use document const doc = getHostForElement(event.target as HTMLElement); let autoPanId = 0; let closestHandle: ConnectionHandle | null;