refactor(edge-wrapper): cleanup

This commit is contained in:
moklick
2023-12-18 11:00:27 +01:00
parent 1401bcae64
commit 5b455a0b2d
13 changed files with 180 additions and 175 deletions
@@ -1,12 +1,11 @@
// Updatable edges have a anchors around their handles to update the edge. // 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 { EdgeAnchor } from '../Edges/EdgeAnchor';
import type { EdgeWrapperProps, Edge } from '../../types/edges'; import type { EdgeWrapperProps, Edge } from '../../types/edges';
import { useStoreApi } from '../../hooks/useStore'; import { useStoreApi } from '../../hooks/useStore';
type EdgeUpdateAnchorsProps = { type EdgeUpdateAnchorsProps = {
edgePosition: NonNullable<ReturnType<typeof getEdgePosition>>;
edge: Edge; edge: Edge;
isUpdatable: boolean | 'source' | 'target'; isUpdatable: boolean | 'source' | 'target';
edgeUpdaterRadius: EdgeWrapperProps['edgeUpdaterRadius']; edgeUpdaterRadius: EdgeWrapperProps['edgeUpdaterRadius'];
@@ -17,20 +16,25 @@ type EdgeUpdateAnchorsProps = {
onEdgeUpdateEnd: EdgeWrapperProps['onEdgeUpdateEnd']; onEdgeUpdateEnd: EdgeWrapperProps['onEdgeUpdateEnd'];
setUpdateHover: (hover: boolean) => void; setUpdateHover: (hover: boolean) => void;
setUpdating: (updating: boolean) => void; setUpdating: (updating: boolean) => void;
}; } & EdgePosition;
function EdgeUpdateAnchors({ function EdgeUpdateAnchors({
isUpdatable, isUpdatable,
edgeUpdaterRadius, edgeUpdaterRadius,
edge,
targetHandleId,
sourceHandleId,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
onEdgeUpdate, onEdgeUpdate,
onEdgeUpdateStart, onEdgeUpdateStart,
onEdgeUpdateEnd, onEdgeUpdateEnd,
setUpdating, setUpdating,
setUpdateHover, setUpdateHover,
edgePosition,
edge,
targetHandleId,
sourceHandleId,
}: EdgeUpdateAnchorsProps) { }: EdgeUpdateAnchorsProps) {
const store = useStoreApi(); const store = useStoreApi();
@@ -104,9 +108,9 @@ function EdgeUpdateAnchors({
<> <>
{(isUpdatable === 'source' || isUpdatable === true) && ( {(isUpdatable === 'source' || isUpdatable === true) && (
<EdgeAnchor <EdgeAnchor
position={edgePosition.sourcePosition} position={sourcePosition}
centerX={edgePosition.sourceX} centerX={sourceX}
centerY={edgePosition.sourceY} centerY={sourceY}
radius={edgeUpdaterRadius} radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterSourceMouseDown} onMouseDown={onEdgeUpdaterSourceMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter} onMouseEnter={onEdgeUpdaterMouseEnter}
@@ -116,9 +120,9 @@ function EdgeUpdateAnchors({
)} )}
{(isUpdatable === 'target' || isUpdatable === true) && ( {(isUpdatable === 'target' || isUpdatable === true) && (
<EdgeAnchor <EdgeAnchor
position={edgePosition.targetPosition} position={targetPosition}
centerX={edgePosition.targetX} centerX={targetX}
centerY={edgePosition.targetY} centerY={targetY}
radius={edgeUpdaterRadius} radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterTargetMouseDown} onMouseDown={onEdgeUpdaterTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter} onMouseEnter={onEdgeUpdaterMouseEnter}
@@ -1,12 +1,18 @@
import { memo, useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react'; import { memo, useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { getMarkerId, elementSelectionKeys, getEdgePosition, errorMessages, getEdgeZIndex } from '@xyflow/system'; import {
getMarkerId,
elementSelectionKeys,
getEdgePosition,
errorMessages,
getElevatedEdgeZIndex,
} from '@xyflow/system';
import { useStoreApi, useStore } from '../../hooks/useStore'; import { useStoreApi, useStore } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions'; import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import type { EdgeWrapperProps, Node } from '../../types'; import type { EdgeWrapperProps } from '../../types';
import { builtinEdgeTypes } from './utils'; import { builtinEdgeTypes, nullPosition } from './utils';
import EdgeUpdateAnchors from './EdgeUpdateAnchors'; import EdgeUpdateAnchors from './EdgeUpdateAnchors';
function EdgeWrapper({ function EdgeWrapper({
@@ -53,45 +59,44 @@ function EdgeWrapper({
const [updateHover, setUpdateHover] = useState<boolean>(false); const [updateHover, setUpdateHover] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(false); const [updating, setUpdating] = useState<boolean>(false);
const store = useStoreApi(); const store = useStoreApi();
const prevSourceNode = useRef<Node | undefined>();
const prevTargetNode = useRef<Node | undefined>();
const prevZIndex = useRef<number | undefined>(edge.zIndex);
const prevEdgePosition = useRef<ReturnType<typeof getEdgePosition> | null>(null);
const { edgePosition, zIndex } = useStore( const { zIndex, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = useStore(
useCallback( useCallback(
(state) => { (store) => {
const sourceNode = state.nodeLookup.get(edge.source); const sourceNode = store.nodeLookup.get(edge.source);
const targetNode = state.nodeLookup.get(edge.target); const targetNode = store.nodeLookup.get(edge.target);
if (!sourceNode || !targetNode) { 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; const zIndex = getElevatedEdgeZIndex({
prevTargetNode.current = targetNode; selected: edge.selected,
zIndex: edge.zIndex,
prevEdgePosition.current = nodesChanged sourceNode,
? getEdgePosition({ targetNode,
id, elevateOnSelect: elevateEdgesOnSelect,
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);
return { return {
edgePosition: prevEdgePosition.current, zIndex,
zIndex: prevZIndex.current, ...(edgePosition || nullPosition),
}; };
}, },
[edge.source, edge.target, edge.selected, edge.zIndex] [edge.source, edge.target, edge.selected, edge.zIndex, elevateEdgesOnSelect]
), ),
shallow shallow
); );
@@ -100,12 +105,13 @@ function EdgeWrapper({
() => (edge.markerStart ? `url(#${getMarkerId(edge.markerStart, rfId)})` : undefined), () => (edge.markerStart ? `url(#${getMarkerId(edge.markerStart, rfId)})` : undefined),
[edge.markerStart, rfId] [edge.markerStart, rfId]
); );
const markerEndUrl = useMemo( const markerEndUrl = useMemo(
() => (edge.markerEnd ? `url(#${getMarkerId(edge.markerEnd, rfId)})` : undefined), () => (edge.markerEnd ? `url(#${getMarkerId(edge.markerEnd, rfId)})` : undefined),
[edge.markerEnd, rfId] [edge.markerEnd, rfId]
); );
if (edge.hidden || !edgePosition) { if (edge.hidden || !sourceX || !sourceY || !targetX || !targetY) {
return null; return null;
} }
@@ -213,14 +219,14 @@ function EdgeWrapper({
labelBgStyle={edge.labelBgStyle} labelBgStyle={edge.labelBgStyle}
labelBgPadding={edge.labelBgPadding} labelBgPadding={edge.labelBgPadding}
labelBgBorderRadius={edge.labelBgBorderRadius} labelBgBorderRadius={edge.labelBgBorderRadius}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
data={edge.data} data={edge.data}
style={edge.style} style={edge.style}
sourceX={edgePosition.sourceX}
sourceY={edgePosition.sourceY}
targetX={edgePosition.targetX}
targetY={edgePosition.targetY}
sourcePosition={edgePosition.sourcePosition}
targetPosition={edgePosition.targetPosition}
sourceHandleId={sourceHandleId} sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId} targetHandleId={targetHandleId}
markerStart={markerStartUrl} markerStart={markerStartUrl}
@@ -237,7 +243,12 @@ function EdgeWrapper({
onEdgeUpdate={onEdgeUpdate} onEdgeUpdate={onEdgeUpdate}
onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd} onEdgeUpdateEnd={onEdgeUpdateEnd}
edgePosition={edgePosition} sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
setUpdateHover={setUpdateHover} setUpdateHover={setUpdateHover}
setUpdating={setUpdating} setUpdating={setUpdating}
sourceHandleId={sourceHandleId} sourceHandleId={sourceHandleId}
@@ -15,3 +15,12 @@ export const builtinEdgeTypes: EdgeTypes = {
smoothstep: SmoothStepEdgeInternal as ComponentType<EdgeProps>, smoothstep: SmoothStepEdgeInternal as ComponentType<EdgeProps>,
simplebezier: SimpleBezierEdgeInternal as ComponentType<EdgeProps>, simplebezier: SimpleBezierEdgeInternal as ComponentType<EdgeProps>,
}; };
export const nullPosition = {
sourceX: null,
sourceY: null,
targetX: null,
targetY: null,
sourcePosition: null,
targetPosition: null,
};
@@ -125,16 +125,11 @@ const NodeWrapper = ({
const isParent = !!node[internalsSymbol]?.isParent; const isParent = !!node[internalsSymbol]?.isParent;
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const onMouseEnterHandler = const onMouseEnterHandler = onMouseEnter ? (event: MouseEvent) => onMouseEnter(event, { ...node }) : undefined;
onMouseEnter === undefined ? undefined : (event: MouseEvent) => onMouseEnter(event, { ...node }); const onMouseMoveHandler = onMouseMove ? (event: MouseEvent) => onMouseMove(event, { ...node }) : undefined;
const onMouseMoveHandler = const onMouseLeaveHandler = onMouseLeave ? (event: MouseEvent) => onMouseLeave(event, { ...node }) : undefined;
onMouseMove === undefined ? undefined : (event: MouseEvent) => onMouseMove(event, { ...node }); const onContextMenuHandler = onContextMenu ? (event: MouseEvent) => onContextMenu(event, { ...node }) : undefined;
const onMouseLeaveHandler = const onDoubleClickHandler = onDoubleClick ? (event: MouseEvent) => onDoubleClick(event, { ...node }) : undefined;
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 onSelectNodeHandler = (event: MouseEvent) => { const onSelectNodeHandler = (event: MouseEvent) => {
const { selectNodesOnDrag, nodeDragThreshold } = store.getState(); const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
+4 -3
View File
@@ -24,8 +24,9 @@ import {
type OnMoveEnd, type OnMoveEnd,
type IsValidConnection, type IsValidConnection,
type UpdateConnection, type UpdateConnection,
EdgeLookup, type EdgeLookup,
ConnectionLookup, type ConnectionLookup,
type NodeLookup,
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
@@ -49,7 +50,7 @@ export type ReactFlowStore = {
height: number; height: number;
transform: Transform; transform: Transform;
nodes: Node[]; nodes: Node[];
nodeLookup: Map<string, Node>; nodeLookup: NodeLookup<Node>;
edges: Edge[]; edges: Edge[];
edgeLookup: EdgeLookup<Edge>; edgeLookup: EdgeLookup<Edge>;
connectionLookup: ConnectionLookup; connectionLookup: ConnectionLookup;
+4 -4
View File
@@ -57,12 +57,11 @@ function applyChanges(changes: any[], elements: any[]): any[] {
let remainingChanges = changes; let remainingChanges = changes;
const updatedElements: any[] = []; const updatedElements: any[] = [];
for (let i = 0; i < elements.length; i++) { for (const item of elements) {
const nextChanges: any[] = []; const nextChanges: any[] = [];
const _remainingChanges: any[] = []; const _remainingChanges: any[] = [];
const item = elements[i];
remainingChanges.forEach((c) => { for (const c of remainingChanges) {
if (c.type === 'add') { if (c.type === 'add') {
updatedElements.push(c.item); updatedElements.push(c.item);
} else if (c.id === item.id) { } else if (c.id === item.id) {
@@ -70,7 +69,8 @@ function applyChanges(changes: any[], elements: any[]): any[] {
} else { } else {
_remainingChanges.push(c); _remainingChanges.push(c);
} }
}); }
remainingChanges = _remainingChanges; remainingChanges = _remainingChanges;
if (nextChanges.length === 0) { if (nextChanges.length === 0) {
@@ -4,6 +4,11 @@ import {
SelectionMode, SelectionMode,
ConnectionMode, ConnectionMode,
ConnectionLineType, ConnectionLineType,
devWarn,
adoptUserProvidedNodes,
getNodesBounds,
getViewportForBounds,
updateConnectionLookup,
type SelectionRect, type SelectionRect,
type SnapGrid, type SnapGrid,
type MarkerProps, type MarkerProps,
@@ -12,16 +17,12 @@ import {
type IsValidConnection, type IsValidConnection,
type NodeOrigin, type NodeOrigin,
type OnError, type OnError,
devWarn,
type Viewport, type Viewport,
adoptUserProvidedNodes,
getNodesBounds,
getViewportForBounds,
updateConnectionLookup,
type ConnectionLookup, type ConnectionLookup,
type OnConnect, type OnConnect,
type OnConnectStart, type OnConnectStart,
type OnConnectEnd type OnConnectEnd,
type NodeLookup
} from '@xyflow/system'; } from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -96,7 +97,7 @@ export const getInitialStore = ({
return { return {
flowId: writable<string | null>(null), flowId: writable<string | null>(null),
nodes: createNodesStore(nextNodes, nodeLookup), nodes: createNodesStore(nextNodes, nodeLookup),
nodeLookup: readable<Map<string, Node>>(nodeLookup), nodeLookup: readable<NodeLookup>(nodeLookup),
visibleNodes: readable<Node[]>([]), visibleNodes: readable<Node[]>([]),
edges: createEdgesStore(edges, connectionLookup, edgeLookup), edges: createEdgesStore(edges, connectionLookup, edgeLookup),
visibleEdges: readable<EdgeLayouted[]>([]), visibleEdges: readable<EdgeLayouted[]>([]),
+6 -5
View File
@@ -1,10 +1,10 @@
import { import {
writable, writable,
get,
type Unsubscriber, type Unsubscriber,
type Subscriber, type Subscriber,
type Updater, type Updater,
type Writable, type Writable
get
} from 'svelte/store'; } from 'svelte/store';
import { import {
adoptUserProvidedNodes, adoptUserProvidedNodes,
@@ -12,7 +12,8 @@ import {
type Viewport, type Viewport,
type PanZoomInstance, type PanZoomInstance,
type ConnectionLookup, type ConnectionLookup,
type EdgeLookup type EdgeLookup,
type NodeLookup
} from '@xyflow/system'; } from '@xyflow/system';
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types'; 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. // The user only passes in relative positions, so we need to calculate the absolute positions based on the parent nodes.
export const createNodesStore = ( export const createNodesStore = (
nodes: Node[], nodes: Node[],
nodeLookup: Map<string, Node> nodeLookup: NodeLookup<Node>
): { ): {
subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber; subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber;
update: (this: void, updater: Updater<Node[]>) => void; update: (this: void, updater: Updater<Node[]>) => void;
@@ -176,7 +177,7 @@ export const createNodesStore = (
export const createEdgesStore = ( export const createEdgesStore = (
edges: Edge[], edges: Edge[],
connectionLookup: ConnectionLookup, connectionLookup: ConnectionLookup,
edgeLookup: EdgeLookup, edgeLookup: EdgeLookup<Edge>,
defaultOptions?: DefaultEdgeOptions defaultOptions?: DefaultEdgeOptions
): Writable<Edge[]> & { setDefaultOptions: (opts: DefaultEdgeOptions) => void } => { ): Writable<Edge[]> & { setDefaultOptions: (opts: DefaultEdgeOptions) => void } => {
const { subscribe, set, update } = writable<Edge[]>([]); const { subscribe, set, update } = writable<Edge[]>([]);
@@ -1,5 +1,5 @@
import { derived } from 'svelte/store'; 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 { EdgeLayouted } from '$lib/types';
import type { SvelteFlowStoreState } from './types'; import type { SvelteFlowStoreState } from './types';
@@ -64,7 +64,13 @@ export function getVisibleEdges(store: SvelteFlowStoreState) {
if (edgePosition) { if (edgePosition) {
res.push({ res.push({
...edge, ...edge,
zIndex: getEdgeZIndex(edge.selected, edge.zIndex, sourceNode, targetNode, false), zIndex: getElevatedEdgeZIndex({
selected: edge.selected,
zIndex: edge.zIndex,
sourceNode,
targetNode,
elevateOnSelect: false
}),
...edgePosition ...edgePosition
}); });
} }
+1 -1
View File
@@ -78,4 +78,4 @@ export type EdgePosition = {
targetPosition: Position; targetPosition: Position;
}; };
export type EdgeLookup = Map<string, EdgeBase>; export type EdgeLookup<EdgeType extends EdgeBase = EdgeBase> = Map<string, EdgeType>;
+2
View File
@@ -107,3 +107,5 @@ export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void;
export type NodeHandle = Optional<HandleElement, 'width' | 'height'>; export type NodeHandle = Optional<HandleElement, 'width' | 'height'>;
export type Align = 'center' | 'start' | 'end'; export type Align = 'center' | 'start' | 'end';
export type NodeLookup<NodeType extends NodeBase = NodeBase> = Map<string, NodeType>;
+21 -15
View File
@@ -1,6 +1,6 @@
import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..'; import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..';
import { EdgeBase, NodeBase } from '../../types'; 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) // this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
export function getEdgeCenter({ export function getEdgeCenter({
@@ -23,23 +23,29 @@ export function getEdgeCenter({
return [centerX, centerY, xOffset, yOffset]; return [centerX, centerY, xOffset, yOffset];
} }
export function getEdgeZIndex( export type GetEdgeZIndexParams = {
selected: boolean | undefined, sourceNode: NodeBase;
zIndex: number | undefined, targetNode: NodeBase;
sourceNode: NodeBase, selected?: boolean;
targetNode: NodeBase, zIndex?: number;
elevateEdgesOnSelect?: boolean elevateOnSelect?: boolean;
) { };
const hasZIndex = isNumeric(zIndex);
let nextZIndex = hasZIndex ? zIndex! : 0;
if (elevateEdgesOnSelect) { export function getElevatedEdgeZIndex({
const edgeOrConnectedNodeSelected = selected || targetNode?.selected || sourceNode?.selected; sourceNode,
const selectedZIndex = Math.max(sourceNode?.[internalsSymbol]?.z || 0, targetNode?.[internalsSymbol]?.z || 0, 1000); targetNode,
nextZIndex = (hasZIndex ? zIndex! : 0) + (edgeOrConnectedNodeSelected ? selectedZIndex : 0); 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 = { type IsEdgeVisibleParams = {
+45 -76
View File
@@ -1,7 +1,7 @@
import { EdgePosition } from '../../types/edges'; import { EdgePosition } from '../../types/edges';
import { ConnectionMode, OnError } from '../../types/general'; import { ConnectionMode, OnError } from '../../types/general';
import { NodeBase, NodeHandle, NodeHandleBounds } from '../../types/nodes'; import { NodeBase, NodeHandle } from '../../types/nodes';
import { Position, Rect, XYPosition } from '../../types/utils'; import { Position } from '../../types/utils';
import { errorMessages, internalsSymbol } from '../../constants'; import { errorMessages, internalsSymbol } from '../../constants';
import { HandleElement } from '../../types'; import { HandleElement } from '../../types';
@@ -15,21 +15,28 @@ export type GetEdgePositionParams = {
onError?: OnError; onError?: OnError;
}; };
export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null { function isNodeInitialized(node: NodeBase): boolean {
const [sourceNodeRect, sourceHandleBounds, isSourceValid] = getHandleDataByNode(params.sourceNode); return !!node?.[internalsSymbol]?.handleBounds && !!node?.computed?.width;
const [targetNodeRect, targetHandleBounds, isTargetValid] = getHandleDataByNode(params.targetNode); }
if (!isSourceValid || !isTargetValid) { export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null {
const { sourceNode, targetNode } = params;
if (!isNodeInitialized(sourceNode) || !isNodeInitialized(targetNode)) {
return null; return null;
} }
// when connection type is loose we can define all handles as sources and connect source -> source const sourceHandleBounds = sourceNode[internalsSymbol]?.handleBounds || toHandleBounds(sourceNode.handles);
const targetNodeHandles = 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 params.connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target ? targetHandleBounds?.target ?? []
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []); : (targetHandleBounds?.target ?? []).concat(targetHandleBounds?.source ?? []),
const sourceHandle = getHandle(sourceHandleBounds!.source!, params.sourceHandle); params.targetHandle
const targetHandle = getHandle(targetNodeHandles!, params.targetHandle); );
const sourcePosition = sourceHandle?.position || Position.Bottom; const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top; const targetPosition = targetHandle?.position || Position.Top;
@@ -46,8 +53,8 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
return null; return null;
} }
const { x: sourceX, y: sourceY } = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle); const [sourceX, sourceY] = getHandlePosition(sourcePosition, sourceNode, sourceHandle);
const { x: targetX, y: targetY } = getHandlePosition(targetPosition, targetNodeRect, targetHandle); const [targetX, targetY] = getHandlePosition(targetPosition, targetNode, targetHandle);
return { return {
sourceX, sourceX,
@@ -64,79 +71,41 @@ function toHandleBounds(handles?: NodeHandle[]) {
return null; return null;
} }
return handles.reduce<NodeHandleBounds>( const source = [];
(res, item) => { const target = [];
item.width = item.width || 1;
item.height = item.height || 1;
if (item.type === 'source') { for (const handle of handles) {
res.source?.push(item as HandleElement); handle.width = handle.width || 1;
} handle.height = handle.height || 1;
if (item.type === 'target') { if (handle.type === 'source') {
res.target?.push(item as HandleElement); source.push(handle as HandleElement);
} } else if (handle.type === 'target') {
target.push(handle as HandleElement);
return res;
},
{
source: [],
target: [],
} }
); }
return {
source,
target,
};
} }
function getHandleDataByNode(node?: NodeBase): [Rect, NodeHandleBounds | null, boolean] { function getHandlePosition(position: Position, node: NodeBase, handle: HandleElement | null = null): number[] {
const handleBounds = node?.[internalsSymbol]?.handleBounds || toHandleBounds(node?.handles) || null; const x = (handle?.x ?? 0) + (node.computed?.positionAbsolute?.x ?? 0);
const nodeWidth = node?.computed?.width || node?.width; const y = (handle?.y ?? 0) + (node.computed?.positionAbsolute?.y ?? 0);
const nodeHeight = node?.computed?.height || node?.height; const width = handle?.width || (node?.computed?.width ?? node?.width ?? 0);
const height = handle?.height || (node?.computed?.height ?? node?.height ?? 0);
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;
switch (position) { switch (position) {
case Position.Top: case Position.Top:
return { return [x + width / 2, y];
x: x + width / 2,
y,
};
case Position.Right: case Position.Right:
return { return [x + width, y + height / 2];
x: x + width,
y: y + height / 2,
};
case Position.Bottom: case Position.Bottom:
return { return [x + width / 2, y + height];
x: x + width / 2,
y: y + height,
};
case Position.Left: case Position.Left:
return { return [x, y + height / 2];
x,
y: y + height / 2,
};
} }
} }