diff --git a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx index 02576559..8f6ddfe3 100644 --- a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx +++ b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx @@ -1,12 +1,11 @@ // Updatable edges have a anchors around their handles to update the edge. -import { XYHandle, type Connection, getEdgePosition } from '@xyflow/system'; +import { XYHandle, type Connection, EdgePosition } from '@xyflow/system'; import { EdgeAnchor } from '../Edges/EdgeAnchor'; import type { EdgeWrapperProps, Edge } from '../../types/edges'; import { useStoreApi } from '../../hooks/useStore'; type EdgeUpdateAnchorsProps = { - edgePosition: NonNullable>; edge: Edge; isUpdatable: boolean | 'source' | 'target'; edgeUpdaterRadius: EdgeWrapperProps['edgeUpdaterRadius']; @@ -17,20 +16,25 @@ type EdgeUpdateAnchorsProps = { onEdgeUpdateEnd: EdgeWrapperProps['onEdgeUpdateEnd']; setUpdateHover: (hover: boolean) => void; setUpdating: (updating: boolean) => void; -}; +} & EdgePosition; function EdgeUpdateAnchors({ isUpdatable, edgeUpdaterRadius, + edge, + targetHandleId, + sourceHandleId, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, onEdgeUpdate, onEdgeUpdateStart, onEdgeUpdateEnd, setUpdating, setUpdateHover, - edgePosition, - edge, - targetHandleId, - sourceHandleId, }: EdgeUpdateAnchorsProps) { const store = useStoreApi(); @@ -104,9 +108,9 @@ function EdgeUpdateAnchors({ <> {(isUpdatable === 'source' || isUpdatable === true) && ( (false); const [updating, setUpdating] = useState(false); const store = useStoreApi(); - const prevSourceNode = useRef(); - const prevTargetNode = useRef(); - const prevZIndex = useRef(edge.zIndex); - const prevEdgePosition = useRef | null>(null); - const { edgePosition, zIndex } = useStore( + const { zIndex, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = useStore( useCallback( - (state) => { - const sourceNode = state.nodeLookup.get(edge.source); - const targetNode = state.nodeLookup.get(edge.target); + (store) => { + const sourceNode = store.nodeLookup.get(edge.source); + const targetNode = store.nodeLookup.get(edge.target); if (!sourceNode || !targetNode) { - return { edgePosition: null, zIndex: edge.zIndex }; + return { + zIndex: edge.zIndex, + ...nullPosition, + }; } - const nodesChanged = prevSourceNode.current !== sourceNode || prevTargetNode.current !== targetNode; + const edgePosition = getEdgePosition({ + id, + sourceNode, + targetNode, + sourceHandle: sourceHandleId || null, + targetHandle: targetHandleId || null, + connectionMode: store.connectionMode, + onError, + }); - prevSourceNode.current = sourceNode; - prevTargetNode.current = targetNode; - - prevEdgePosition.current = nodesChanged - ? getEdgePosition({ - id, - sourceNode, - targetNode, - sourceHandle: sourceHandleId || null, - targetHandle: targetHandleId || null, - connectionMode: state.connectionMode, - onError: state.onError, - }) - : prevEdgePosition.current; - prevZIndex.current = getEdgeZIndex(edge.selected, edge.zIndex, sourceNode, targetNode, elevateEdgesOnSelect); + const zIndex = getElevatedEdgeZIndex({ + selected: edge.selected, + zIndex: edge.zIndex, + sourceNode, + targetNode, + elevateOnSelect: elevateEdgesOnSelect, + }); return { - edgePosition: prevEdgePosition.current, - zIndex: prevZIndex.current, + zIndex, + ...(edgePosition || nullPosition), }; }, - [edge.source, edge.target, edge.selected, edge.zIndex] + [edge.source, edge.target, edge.selected, edge.zIndex, elevateEdgesOnSelect] ), shallow ); @@ -100,12 +105,13 @@ function EdgeWrapper({ () => (edge.markerStart ? `url(#${getMarkerId(edge.markerStart, rfId)})` : undefined), [edge.markerStart, rfId] ); + const markerEndUrl = useMemo( () => (edge.markerEnd ? `url(#${getMarkerId(edge.markerEnd, rfId)})` : undefined), [edge.markerEnd, rfId] ); - if (edge.hidden || !edgePosition) { + if (edge.hidden || !sourceX || !sourceY || !targetX || !targetY) { return null; } @@ -213,14 +219,14 @@ function EdgeWrapper({ labelBgStyle={edge.labelBgStyle} labelBgPadding={edge.labelBgPadding} labelBgBorderRadius={edge.labelBgBorderRadius} + sourceX={sourceX} + sourceY={sourceY} + targetX={targetX} + targetY={targetY} + sourcePosition={sourcePosition} + targetPosition={targetPosition} data={edge.data} style={edge.style} - sourceX={edgePosition.sourceX} - sourceY={edgePosition.sourceY} - targetX={edgePosition.targetX} - targetY={edgePosition.targetY} - sourcePosition={edgePosition.sourcePosition} - targetPosition={edgePosition.targetPosition} sourceHandleId={sourceHandleId} targetHandleId={targetHandleId} markerStart={markerStartUrl} @@ -237,7 +243,12 @@ function EdgeWrapper({ onEdgeUpdate={onEdgeUpdate} onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateEnd={onEdgeUpdateEnd} - edgePosition={edgePosition} + sourceX={sourceX} + sourceY={sourceY} + targetX={targetX} + targetY={targetY} + sourcePosition={sourcePosition} + targetPosition={targetPosition} setUpdateHover={setUpdateHover} setUpdating={setUpdating} sourceHandleId={sourceHandleId} diff --git a/packages/react/src/components/EdgeWrapper/utils.ts b/packages/react/src/components/EdgeWrapper/utils.ts index 294592c9..0ac2a2e8 100644 --- a/packages/react/src/components/EdgeWrapper/utils.ts +++ b/packages/react/src/components/EdgeWrapper/utils.ts @@ -15,3 +15,12 @@ export const builtinEdgeTypes: EdgeTypes = { smoothstep: SmoothStepEdgeInternal as ComponentType, simplebezier: SimpleBezierEdgeInternal as ComponentType, }; + +export const nullPosition = { + sourceX: null, + sourceY: null, + targetX: null, + targetY: null, + sourcePosition: null, + targetPosition: null, +}; diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index de7ce1d0..4aa655ec 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -125,16 +125,11 @@ const NodeWrapper = ({ const isParent = !!node[internalsSymbol]?.isParent; const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; - const onMouseEnterHandler = - onMouseEnter === undefined ? undefined : (event: MouseEvent) => onMouseEnter(event, { ...node }); - const onMouseMoveHandler = - onMouseMove === undefined ? undefined : (event: MouseEvent) => onMouseMove(event, { ...node }); - const onMouseLeaveHandler = - onMouseLeave === undefined ? undefined : (event: MouseEvent) => onMouseLeave(event, { ...node }); - const onContextMenuHandler = - onContextMenu === undefined ? undefined : (event: MouseEvent) => onContextMenu(event, { ...node }); - const onDoubleClickHandler = - onDoubleClick === undefined ? undefined : (event: MouseEvent) => onDoubleClick(event, { ...node }); + const onMouseEnterHandler = onMouseEnter ? (event: MouseEvent) => onMouseEnter(event, { ...node }) : undefined; + const onMouseMoveHandler = onMouseMove ? (event: MouseEvent) => onMouseMove(event, { ...node }) : undefined; + const onMouseLeaveHandler = onMouseLeave ? (event: MouseEvent) => onMouseLeave(event, { ...node }) : undefined; + const onContextMenuHandler = onContextMenu ? (event: MouseEvent) => onContextMenu(event, { ...node }) : undefined; + const onDoubleClickHandler = onDoubleClick ? (event: MouseEvent) => onDoubleClick(event, { ...node }) : undefined; const onSelectNodeHandler = (event: MouseEvent) => { const { selectNodesOnDrag, nodeDragThreshold } = store.getState(); diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 50bf9618..ab21c8c6 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -24,8 +24,9 @@ import { type OnMoveEnd, type IsValidConnection, type UpdateConnection, - EdgeLookup, - ConnectionLookup, + type EdgeLookup, + type ConnectionLookup, + type NodeLookup, } from '@xyflow/system'; import type { @@ -49,7 +50,7 @@ export type ReactFlowStore = { height: number; transform: Transform; nodes: Node[]; - nodeLookup: Map; + nodeLookup: NodeLookup; edges: Edge[]; edgeLookup: EdgeLookup; connectionLookup: ConnectionLookup; diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index bef54947..872a0806 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -57,12 +57,11 @@ function applyChanges(changes: any[], elements: any[]): any[] { let remainingChanges = changes; const updatedElements: any[] = []; - for (let i = 0; i < elements.length; i++) { + for (const item of elements) { const nextChanges: any[] = []; const _remainingChanges: any[] = []; - const item = elements[i]; - remainingChanges.forEach((c) => { + for (const c of remainingChanges) { if (c.type === 'add') { updatedElements.push(c.item); } else if (c.id === item.id) { @@ -70,7 +69,8 @@ function applyChanges(changes: any[], elements: any[]): any[] { } else { _remainingChanges.push(c); } - }); + } + remainingChanges = _remainingChanges; if (nextChanges.length === 0) { diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index addaa0ab..ead5652d 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -4,6 +4,11 @@ import { SelectionMode, ConnectionMode, ConnectionLineType, + devWarn, + adoptUserProvidedNodes, + getNodesBounds, + getViewportForBounds, + updateConnectionLookup, type SelectionRect, type SnapGrid, type MarkerProps, @@ -12,16 +17,12 @@ import { type IsValidConnection, type NodeOrigin, type OnError, - devWarn, type Viewport, - adoptUserProvidedNodes, - getNodesBounds, - getViewportForBounds, - updateConnectionLookup, type ConnectionLookup, type OnConnect, type OnConnectStart, - type OnConnectEnd + type OnConnectEnd, + type NodeLookup } from '@xyflow/system'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; @@ -96,7 +97,7 @@ export const getInitialStore = ({ return { flowId: writable(null), nodes: createNodesStore(nextNodes, nodeLookup), - nodeLookup: readable>(nodeLookup), + nodeLookup: readable(nodeLookup), visibleNodes: readable([]), edges: createEdgesStore(edges, connectionLookup, edgeLookup), visibleEdges: readable([]), diff --git a/packages/svelte/src/lib/store/utils.ts b/packages/svelte/src/lib/store/utils.ts index 1fb96e7c..0f72d746 100644 --- a/packages/svelte/src/lib/store/utils.ts +++ b/packages/svelte/src/lib/store/utils.ts @@ -1,10 +1,10 @@ import { writable, + get, type Unsubscriber, type Subscriber, type Updater, - type Writable, - get + type Writable } from 'svelte/store'; import { adoptUserProvidedNodes, @@ -12,7 +12,8 @@ import { type Viewport, type PanZoomInstance, type ConnectionLookup, - type EdgeLookup + type EdgeLookup, + type NodeLookup } from '@xyflow/system'; import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types'; @@ -126,7 +127,7 @@ export type NodeStoreOptions = { // The user only passes in relative positions, so we need to calculate the absolute positions based on the parent nodes. export const createNodesStore = ( nodes: Node[], - nodeLookup: Map + nodeLookup: NodeLookup ): { subscribe: (this: void, run: Subscriber) => Unsubscriber; update: (this: void, updater: Updater) => void; @@ -176,7 +177,7 @@ export const createNodesStore = ( export const createEdgesStore = ( edges: Edge[], connectionLookup: ConnectionLookup, - edgeLookup: EdgeLookup, + edgeLookup: EdgeLookup, defaultOptions?: DefaultEdgeOptions ): Writable & { setDefaultOptions: (opts: DefaultEdgeOptions) => void } => { const { subscribe, set, update } = writable([]); diff --git a/packages/svelte/src/lib/store/visible-edges.ts b/packages/svelte/src/lib/store/visible-edges.ts index 1dd4a956..d1b4a467 100644 --- a/packages/svelte/src/lib/store/visible-edges.ts +++ b/packages/svelte/src/lib/store/visible-edges.ts @@ -1,5 +1,5 @@ import { derived } from 'svelte/store'; -import { isEdgeVisible, getEdgePosition, getEdgeZIndex } from '@xyflow/system'; +import { isEdgeVisible, getEdgePosition, getElevatedEdgeZIndex } from '@xyflow/system'; import type { EdgeLayouted } from '$lib/types'; import type { SvelteFlowStoreState } from './types'; @@ -64,7 +64,13 @@ export function getVisibleEdges(store: SvelteFlowStoreState) { if (edgePosition) { res.push({ ...edge, - zIndex: getEdgeZIndex(edge.selected, edge.zIndex, sourceNode, targetNode, false), + zIndex: getElevatedEdgeZIndex({ + selected: edge.selected, + zIndex: edge.zIndex, + sourceNode, + targetNode, + elevateOnSelect: false + }), ...edgePosition }); } diff --git a/packages/system/src/types/edges.ts b/packages/system/src/types/edges.ts index 131cab62..6ddeb6d9 100644 --- a/packages/system/src/types/edges.ts +++ b/packages/system/src/types/edges.ts @@ -78,4 +78,4 @@ export type EdgePosition = { targetPosition: Position; }; -export type EdgeLookup = Map; +export type EdgeLookup = Map; diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 74b294fe..35410026 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -107,3 +107,5 @@ export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void; export type NodeHandle = Optional; export type Align = 'center' | 'start' | 'end'; + +export type NodeLookup = Map; diff --git a/packages/system/src/utils/edges/general.ts b/packages/system/src/utils/edges/general.ts index 73cbd7f9..57d93b5a 100644 --- a/packages/system/src/utils/edges/general.ts +++ b/packages/system/src/utils/edges/general.ts @@ -1,6 +1,6 @@ import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..'; import { EdgeBase, NodeBase } from '../../types'; -import { isNumeric, getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general'; +import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general'; // this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB) export function getEdgeCenter({ @@ -23,23 +23,29 @@ export function getEdgeCenter({ return [centerX, centerY, xOffset, yOffset]; } -export function getEdgeZIndex( - selected: boolean | undefined, - zIndex: number | undefined, - sourceNode: NodeBase, - targetNode: NodeBase, - elevateEdgesOnSelect?: boolean -) { - const hasZIndex = isNumeric(zIndex); - let nextZIndex = hasZIndex ? zIndex! : 0; +export type GetEdgeZIndexParams = { + sourceNode: NodeBase; + targetNode: NodeBase; + selected?: boolean; + zIndex?: number; + elevateOnSelect?: boolean; +}; - if (elevateEdgesOnSelect) { - const edgeOrConnectedNodeSelected = selected || targetNode?.selected || sourceNode?.selected; - const selectedZIndex = Math.max(sourceNode?.[internalsSymbol]?.z || 0, targetNode?.[internalsSymbol]?.z || 0, 1000); - nextZIndex = (hasZIndex ? zIndex! : 0) + (edgeOrConnectedNodeSelected ? selectedZIndex : 0); +export function getElevatedEdgeZIndex({ + sourceNode, + targetNode, + selected = false, + zIndex = 0, + elevateOnSelect = false, +}: GetEdgeZIndexParams): number { + if (!elevateOnSelect) { + return zIndex; } - return nextZIndex; + const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected; + const selectedZIndex = Math.max(sourceNode[internalsSymbol]?.z || 0, targetNode[internalsSymbol]?.z || 0, 1000); + + return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0); } type IsEdgeVisibleParams = { diff --git a/packages/system/src/utils/edges/positions.ts b/packages/system/src/utils/edges/positions.ts index 35e07834..f05b547e 100644 --- a/packages/system/src/utils/edges/positions.ts +++ b/packages/system/src/utils/edges/positions.ts @@ -1,7 +1,7 @@ import { EdgePosition } from '../../types/edges'; import { ConnectionMode, OnError } from '../../types/general'; -import { NodeBase, NodeHandle, NodeHandleBounds } from '../../types/nodes'; -import { Position, Rect, XYPosition } from '../../types/utils'; +import { NodeBase, NodeHandle } from '../../types/nodes'; +import { Position } from '../../types/utils'; import { errorMessages, internalsSymbol } from '../../constants'; import { HandleElement } from '../../types'; @@ -15,21 +15,28 @@ export type GetEdgePositionParams = { onError?: OnError; }; -export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null { - const [sourceNodeRect, sourceHandleBounds, isSourceValid] = getHandleDataByNode(params.sourceNode); - const [targetNodeRect, targetHandleBounds, isTargetValid] = getHandleDataByNode(params.targetNode); +function isNodeInitialized(node: NodeBase): boolean { + return !!node?.[internalsSymbol]?.handleBounds && !!node?.computed?.width; +} - if (!isSourceValid || !isTargetValid) { +export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null { + const { sourceNode, targetNode } = params; + + if (!isNodeInitialized(sourceNode) || !isNodeInitialized(targetNode)) { return null; } - // when connection type is loose we can define all handles as sources and connect source -> source - const targetNodeHandles = + const sourceHandleBounds = sourceNode[internalsSymbol]?.handleBounds || toHandleBounds(sourceNode.handles); + const targetHandleBounds = targetNode[internalsSymbol]?.handleBounds || toHandleBounds(targetNode.handles); + + const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle); + const targetHandle = getHandle( + // when connection type is loose we can define all handles as sources and connect source -> source params.connectionMode === ConnectionMode.Strict - ? targetHandleBounds!.target - : (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []); - const sourceHandle = getHandle(sourceHandleBounds!.source!, params.sourceHandle); - const targetHandle = getHandle(targetNodeHandles!, params.targetHandle); + ? targetHandleBounds?.target ?? [] + : (targetHandleBounds?.target ?? []).concat(targetHandleBounds?.source ?? []), + params.targetHandle + ); const sourcePosition = sourceHandle?.position || Position.Bottom; const targetPosition = targetHandle?.position || Position.Top; @@ -46,8 +53,8 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n return null; } - const { x: sourceX, y: sourceY } = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle); - const { x: targetX, y: targetY } = getHandlePosition(targetPosition, targetNodeRect, targetHandle); + const [sourceX, sourceY] = getHandlePosition(sourcePosition, sourceNode, sourceHandle); + const [targetX, targetY] = getHandlePosition(targetPosition, targetNode, targetHandle); return { sourceX, @@ -64,79 +71,41 @@ function toHandleBounds(handles?: NodeHandle[]) { return null; } - return handles.reduce( - (res, item) => { - item.width = item.width || 1; - item.height = item.height || 1; + const source = []; + const target = []; - if (item.type === 'source') { - res.source?.push(item as HandleElement); - } + for (const handle of handles) { + handle.width = handle.width || 1; + handle.height = handle.height || 1; - if (item.type === 'target') { - res.target?.push(item as HandleElement); - } - - return res; - }, - { - source: [], - target: [], + if (handle.type === 'source') { + source.push(handle as HandleElement); + } else if (handle.type === 'target') { + target.push(handle as HandleElement); } - ); + } + + return { + source, + target, + }; } -function getHandleDataByNode(node?: NodeBase): [Rect, NodeHandleBounds | null, boolean] { - const handleBounds = node?.[internalsSymbol]?.handleBounds || toHandleBounds(node?.handles) || null; - const nodeWidth = node?.computed?.width || node?.width; - const nodeHeight = node?.computed?.height || node?.height; - - const isValid = - handleBounds && - nodeWidth && - nodeHeight && - typeof node?.computed?.positionAbsolute?.x !== 'undefined' && - typeof node?.computed?.positionAbsolute?.y !== 'undefined'; - - return [ - { - x: node?.computed?.positionAbsolute?.x || 0, - y: node?.computed?.positionAbsolute?.y || 0, - width: nodeWidth || 0, - height: nodeHeight || 0, - }, - handleBounds, - !!isValid, - ]; -} - -function getHandlePosition(position: Position, nodeRect: Rect, handle: HandleElement | null = null): XYPosition { - const x = (handle?.x || 0) + nodeRect.x; - const y = (handle?.y || 0) + nodeRect.y; - const width = handle?.width || nodeRect.width; - const height = handle?.height || nodeRect.height; +function getHandlePosition(position: Position, node: NodeBase, handle: HandleElement | null = null): number[] { + const x = (handle?.x ?? 0) + (node.computed?.positionAbsolute?.x ?? 0); + const y = (handle?.y ?? 0) + (node.computed?.positionAbsolute?.y ?? 0); + const width = handle?.width || (node?.computed?.width ?? node?.width ?? 0); + const height = handle?.height || (node?.computed?.height ?? node?.height ?? 0); switch (position) { case Position.Top: - return { - x: x + width / 2, - y, - }; + return [x + width / 2, y]; case Position.Right: - return { - x: x + width, - y: y + height / 2, - }; + return [x + width, y + height / 2]; case Position.Bottom: - return { - x: x + width / 2, - y: y + height, - }; + return [x + width / 2, y + height]; case Position.Left: - return { - x, - y: y + height / 2, - }; + return [x, y + height / 2]; } }