refactor(react): separate user nodes and indernal nodes

This commit is contained in:
moklick
2024-03-27 11:22:43 +01:00
parent d775012e1b
commit 844d574c4f
33 changed files with 374 additions and 315 deletions
@@ -5,8 +5,8 @@ import { getEdgeParams } from './utils';
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => { const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
const { sourceNode, targetNode } = useStore((s) => { const { sourceNode, targetNode } = useStore((s) => {
const sourceNode = s.nodes.find((n) => n.id === source); const sourceNode = s.nodeLookup.get(source);
const targetNode = s.nodes.find((n) => n.id === target); const targetNode = s.nodeLookup.get(target);
return { sourceNode, targetNode }; return { sourceNode, targetNode };
}); });
@@ -1,4 +1,4 @@
import { Position, XYPosition, Node, Edge } from '@xyflow/react'; import { Position, XYPosition, Node, Edge, InternalNode } from '@xyflow/react';
// this helper function returns the intersection point // this helper function returns the intersection point
// of the line between the center of the intersectionNode and the target node // of the line between the center of the intersectionNode and the target node
@@ -56,7 +56,7 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
} }
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge // returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
export function getEdgeParams(source: Node, target: Node) { export function getEdgeParams(source: InternalNode, target: InternalNode) {
const sourceIntersectionPoint = getNodeIntersection(source, target); const sourceIntersectionPoint = getNodeIntersection(source, target);
const targetIntersectionPoint = getNodeIntersection(target, source); const targetIntersectionPoint = getNodeIntersection(target, source);
@@ -3,7 +3,7 @@
import { memo, useEffect, useRef, type MouseEvent, useCallback, CSSProperties } from 'react'; import { memo, useEffect, useRef, type MouseEvent, useCallback, CSSProperties } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { getNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system'; import { getInternalNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import { Panel } from '../../components/Panel'; import { Panel } from '../../components/Panel';
@@ -26,7 +26,9 @@ const selector = (s: ReactFlowState) => {
return { return {
viewBB, viewBB,
boundingRect: boundingRect:
s.nodes.length > 0 ? getBoundsOfRects(getNodesBounds(s.nodes, { nodeOrigin: s.nodeOrigin }), viewBB) : viewBB, s.nodeLookup.size > 0
? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup, { nodeOrigin: s.nodeOrigin }), viewBB)
: viewBB,
rfId: s.rfId, rfId: s.rfId,
nodeOrigin: s.nodeOrigin, nodeOrigin: s.nodeOrigin,
panZoom: s.panZoom, panZoom: s.panZoom,
@@ -1,23 +1,23 @@
import { useCallback, CSSProperties } from 'react'; import { useCallback, CSSProperties } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { getNodesBounds, Rect, Position, internalsSymbol, getNodeToolbarTransform } from '@xyflow/system'; import { Rect, Position, getNodeToolbarTransform, getNodesBounds } from '@xyflow/system';
import { Node, ReactFlowState } from '../../types'; import { InternalNode, ReactFlowState } from '../../types';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext'; import { useNodeId } from '../../contexts/NodeIdContext';
import { NodeToolbarPortal } from './NodeToolbarPortal'; import { NodeToolbarPortal } from './NodeToolbarPortal';
import type { NodeToolbarProps } from './types'; import type { NodeToolbarProps } from './types';
const nodeEqualityFn = (a?: Node, b?: Node) => const nodeEqualityFn = (a?: InternalNode, b?: InternalNode) =>
a?.computed?.positionAbsolute?.x !== b?.computed?.positionAbsolute?.x || a?.internals.positionAbsolute.x !== b?.internals.positionAbsolute.x ||
a?.computed?.positionAbsolute?.y !== b?.computed?.positionAbsolute?.y || a?.internals.positionAbsolute.y !== b?.internals.positionAbsolute.y ||
a?.computed?.width !== b?.computed?.width || a?.computed.width !== b?.computed.width ||
a?.computed?.height !== b?.computed?.height || a?.computed.height !== b?.computed.height ||
a?.selected !== b?.selected || a?.selected !== b?.selected ||
a?.[internalsSymbol]?.z !== b?.[internalsSymbol]?.z; a?.internals.z !== b?.internals.z;
const nodesEqualityFn = (a: Node[], b: Node[]) => { const nodesEqualityFn = (a: InternalNode[], b: InternalNode[]) => {
if (a.length !== b.length) { if (a.length !== b.length) {
return false; return false;
} }
@@ -49,10 +49,10 @@ export function NodeToolbar({
const contextNodeId = useNodeId(); const contextNodeId = useNodeId();
const nodesSelector = useCallback( const nodesSelector = useCallback(
(state: ReactFlowState): Node[] => { (state: ReactFlowState): InternalNode[] => {
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || '']; const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || ''];
return nodeIds.reduce<Node[]>((acc, id) => { return nodeIds.reduce<InternalNode[]>((acc, id) => {
const node = state.nodeLookup.get(id); const node = state.nodeLookup.get(id);
if (node) { if (node) {
acc.push(node); acc.push(node);
@@ -74,7 +74,7 @@ export function NodeToolbar({
} }
const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin }); const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin });
const zIndex: number = Math.max(...nodes.map((node) => (node[internalsSymbol]?.z || 1) + 1)); const zIndex: number = Math.max(...nodes.map((node) => (node.internals?.z || 1) + 1));
const wrapperStyle: CSSProperties = { const wrapperStyle: CSSProperties = {
position: 'absolute', position: 'absolute',
@@ -2,7 +2,6 @@ import { CSSProperties, useCallback } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import cc from 'classcat'; import cc from 'classcat';
import { import {
internalsSymbol,
Position, Position,
ConnectionLineType, ConnectionLineType,
ConnectionMode, ConnectionMode,
@@ -53,7 +52,7 @@ const ConnectionLine = ({
), ),
shallow shallow
); );
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds; const fromHandleBounds = fromNode?.internals?.handleBounds;
let handleBounds = fromHandleBounds?.[handleType]; let handleBounds = fromHandleBounds?.[handleType];
if (connectionMode === ConnectionMode.Loose) { if (connectionMode === ConnectionMode.Loose) {
@@ -65,10 +64,10 @@ const ConnectionLine = ({
} }
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0]; const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.computed?.width ?? 0) / 2; const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.computed.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.computed?.height ?? 0; const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.computed.height ?? 0;
const fromX = (fromNode.computed?.positionAbsolute?.x ?? 0) + fromHandleX; const fromX = (fromNode.internals.positionAbsolute.x ?? 0) + fromHandleX;
const fromY = (fromNode.computed?.positionAbsolute?.y ?? 0) + fromHandleY; const fromY = (fromNode.internals.positionAbsolute.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position; const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : null; const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
@@ -54,7 +54,7 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
onConnectStart, onConnectStart,
onConnectEnd, onConnectEnd,
cancelConnection, cancelConnection,
nodes, nodeLookup,
rfId: flowId, rfId: flowId,
panBy, panBy,
updateConnection, updateConnection,
@@ -82,7 +82,7 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
domNode, domNode,
handleId, handleId,
nodeId, nodeId,
nodes, nodeLookup,
isTarget, isTarget,
edgeUpdaterType: handleType, edgeUpdaterType: handleType,
lib, lib,
@@ -126,7 +126,7 @@ function HandleComponent(
connectionMode: currentStore.connectionMode, connectionMode: currentStore.connectionMode,
connectionRadius: currentStore.connectionRadius, connectionRadius: currentStore.connectionRadius,
domNode: currentStore.domNode, domNode: currentStore.domNode,
nodes: currentStore.nodes, nodeLookup: currentStore.nodeLookup,
lib: currentStore.lib, lib: currentStore.lib,
isTarget, isTarget,
handleId, handleId,
@@ -7,7 +7,6 @@ import {
errorMessages, errorMessages,
getNodeDimensions, getNodeDimensions,
getPositionWithOrigin, getPositionWithOrigin,
internalsSymbol,
isInputDOMNode, isInputDOMNode,
nodeHasDimensions, nodeHasDimensions,
} from '@xyflow/system'; } from '@xyflow/system';
@@ -19,7 +18,7 @@ import { useDrag } from '../../hooks/useDrag';
import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes'; import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
import { handleNodeClick } from '../Nodes/utils'; import { handleNodeClick } from '../Nodes/utils';
import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils'; import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils';
import type { Node, NodeWrapperProps } from '../../types'; import type { InternalNode, Node, NodeWrapperProps } from '../../types';
export function NodeWrapper<NodeType extends Node>({ export function NodeWrapper<NodeType extends Node>({
id, id,
@@ -44,11 +43,11 @@ export function NodeWrapper<NodeType extends Node>({
onError, onError,
}: NodeWrapperProps<NodeType>) { }: NodeWrapperProps<NodeType>) {
const { node, positionAbsoluteX, positionAbsoluteY, zIndex, isParent } = useStore((s) => { const { node, positionAbsoluteX, positionAbsoluteY, zIndex, isParent } = useStore((s) => {
const node = s.nodeLookup.get(id)! as NodeType; const node = s.nodeLookup.get(id)! as InternalNode<NodeType>;
const positionAbsolute = nodeExtent const positionAbsolute = nodeExtent
? clampPosition(node.computed?.positionAbsolute, nodeExtent) ? clampPosition(node.internals.positionAbsolute, nodeExtent)
: node.computed?.positionAbsolute || { x: 0, y: 0 }; : node.internals.positionAbsolute || { x: 0, y: 0 };
return { return {
node, node,
@@ -56,8 +55,8 @@ export function NodeWrapper<NodeType extends Node>({
// so we we need to force a re-render when some change // so we we need to force a re-render when some change
positionAbsoluteX: positionAbsolute.x, positionAbsoluteX: positionAbsolute.x,
positionAbsoluteY: positionAbsolute.y, positionAbsoluteY: positionAbsolute.y,
zIndex: node[internalsSymbol]?.z ?? 0, zIndex: node.internals.z,
isParent: !!node[internalsSymbol]?.isParent, isParent: node.internals.isParent,
}; };
}, shallow); }, shallow);
@@ -84,7 +83,7 @@ export function NodeWrapper<NodeType extends Node>({
const nodeDimensions = getNodeDimensions(node); const nodeDimensions = getNodeDimensions(node);
const inlineDimensions = getNodeInlineStyleDimensions(node); const inlineDimensions = getNodeInlineStyleDimensions(node);
const initialized = nodeHasDimensions(node); const initialized = nodeHasDimensions(node);
const hasHandleBounds = !!node[internalsSymbol]?.handleBounds; const hasHandleBounds = !!node.internals.handleBounds;
const moveSelectedNodes = useMoveSelectedNodes(); const moveSelectedNodes = useMoveSelectedNodes();
@@ -4,7 +4,7 @@ import { InputNode } from '../Nodes/InputNode';
import { DefaultNode } from '../Nodes/DefaultNode'; import { DefaultNode } from '../Nodes/DefaultNode';
import { GroupNode } from '../Nodes/GroupNode'; import { GroupNode } from '../Nodes/GroupNode';
import { OutputNode } from '../Nodes/OutputNode'; import { OutputNode } from '../Nodes/OutputNode';
import type { Node, NodeTypes } from '../../types'; import type { InternalNode, Node, NodeTypes } from '../../types';
export const arrowKeyDiffs: Record<string, XYPosition> = { export const arrowKeyDiffs: Record<string, XYPosition> = {
ArrowUp: { x: 0, y: -1 }, ArrowUp: { x: 0, y: -1 },
@@ -21,12 +21,12 @@ export const builtinNodeTypes: NodeTypes = {
}; };
export function getNodeInlineStyleDimensions<NodeType extends Node = Node>( export function getNodeInlineStyleDimensions<NodeType extends Node = Node>(
node: NodeType node: InternalNode<NodeType>
): { ): {
width: number | string | undefined; width: number | string | undefined;
height: number | string | undefined; height: number | string | undefined;
} { } {
if (!node.computed) { if (node.internals.handleBounds === undefined) {
return { return {
width: node.width ?? node.initialWidth ?? node.style?.width, width: node.width ?? node.initialWidth ?? node.style?.width,
height: node.height ?? node.initialHeight ?? node.style?.height, height: node.height ?? node.initialHeight ?? node.style?.height,
@@ -11,7 +11,7 @@ import { useStore, useStoreApi } from '../../hooks/useStore';
import { useDrag } from '../../hooks/useDrag'; import { useDrag } from '../../hooks/useDrag';
import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes'; import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
import { arrowKeyDiffs } from '../NodeWrapper/utils'; import { arrowKeyDiffs } from '../NodeWrapper/utils';
import type { Node, ReactFlowState } from '../../types'; import type { InternalNode, Node, ReactFlowState } from '../../types';
export type NodesSelectionProps<NodeType> = { export type NodesSelectionProps<NodeType> = {
onSelectionContextMenu?: (event: MouseEvent, nodes: NodeType[]) => void; onSelectionContextMenu?: (event: MouseEvent, nodes: NodeType[]) => void;
@@ -20,7 +20,13 @@ export type NodesSelectionProps<NodeType> = {
}; };
const selector = (s: ReactFlowState) => { const selector = (s: ReactFlowState) => {
const selectedNodes = s.nodes.filter((n) => n.selected); const selectedNodes: InternalNode[] = [];
for (const [, node] of s.nodeLookup) {
if (node.selected) {
selectedNodes.push(node);
}
}
const { width, height, x, y } = getNodesBounds(selectedNodes, { nodeOrigin: s.nodeOrigin }); const { width, height, x, y } = getNodesBounds(selectedNodes, { nodeOrigin: s.nodeOrigin });
return { return {
@@ -15,7 +15,7 @@ type SelectionListenerProps = {
}; };
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
selectedNodes: s.nodes.filter((n) => n.selected), selectedNodes: Array.from(s.nodeLookup.values()).filter((n) => n.selected),
selectedEdges: s.edges.filter((e) => e.selected), selectedEdges: s.edges.filter((e) => e.selected),
}); });
+6 -6
View File
@@ -128,7 +128,7 @@ export function Pane({
}; };
const onMouseMove = (event: ReactMouseEvent): void => { const onMouseMove = (event: ReactMouseEvent): void => {
const { userSelectionRect, edges, transform, nodeOrigin, nodes, triggerNodeChanges, triggerEdgeChanges } = const { userSelectionRect, edgeLookup, transform, nodeOrigin, nodeLookup, triggerNodeChanges, triggerEdgeChanges } =
store.getState(); store.getState();
if (!isSelecting || !containerBounds.current || !userSelectionRect) { if (!isSelecting || !containerBounds.current || !userSelectionRect) {
return; return;
@@ -149,7 +149,7 @@ export function Pane({
}; };
const selectedNodes = getNodesInside( const selectedNodes = getNodesInside(
nodes, nodeLookup,
nextUserSelectRect, nextUserSelectRect,
transform, transform,
selectionMode === SelectionMode.Partial, selectionMode === SelectionMode.Partial,
@@ -163,22 +163,22 @@ export function Pane({
for (const selectedNode of selectedNodes) { for (const selectedNode of selectedNodes) {
selectedNodeIds.add(selectedNode.id); selectedNodeIds.add(selectedNode.id);
for (const edge of edges) { for (const [edgeId, edge] of edgeLookup) {
if (edge.source === selectedNode.id || edge.target === selectedNode.id) { if (edge.source === selectedNode.id || edge.target === selectedNode.id) {
selectedEdgeIds.add(edge.id); selectedEdgeIds.add(edgeId);
} }
} }
} }
if (prevSelectedNodesCount.current !== selectedNodeIds.size) { if (prevSelectedNodesCount.current !== selectedNodeIds.size) {
prevSelectedNodesCount.current = selectedNodeIds.size; prevSelectedNodesCount.current = selectedNodeIds.size;
const changes = getSelectionChanges(nodes, selectedNodeIds, true) as NodeChange[]; const changes = getSelectionChanges(nodeLookup, selectedNodeIds, true) as NodeChange[];
triggerNodeChanges(changes); triggerNodeChanges(changes);
} }
if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) { if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) {
prevSelectedEdgesCount.current = selectedEdgeIds.size; prevSelectedEdgesCount.current = selectedEdgeIds.size;
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[]; const changes = getSelectionChanges(edgeLookup, selectedEdgeIds) as EdgeChange[];
triggerEdgeChanges(changes); triggerEdgeChanges(changes);
} }
@@ -1,7 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { calculateNodePosition, snapPosition, type XYPosition } from '@xyflow/system'; import { calculateNodePosition, snapPosition, type XYPosition } from '@xyflow/system';
import { Node } from '../types'; import { type Node } from '../types';
import { useStoreApi } from './useStore'; import { useStoreApi } from './useStore';
const selectedAndDraggable = (nodesDraggable: boolean) => (n: Node) => const selectedAndDraggable = (nodesDraggable: boolean) => (n: Node) =>
@@ -17,18 +17,11 @@ export function useMoveSelectedNodes() {
const store = useStoreApi(); const store = useStoreApi();
const moveSelectedNodes = useCallback((params: { direction: XYPosition; factor: number }) => { const moveSelectedNodes = useCallback((params: { direction: XYPosition; factor: number }) => {
const { const { nodeExtent, snapToGrid, snapGrid, nodesDraggable, onError, updateNodePositions, nodeLookup, nodeOrigin } =
nodeExtent, store.getState();
nodes, const nodeUpdates = [];
snapToGrid, const isSelected = selectedAndDraggable(nodesDraggable);
snapGrid,
nodesDraggable,
onError,
updateNodePositions,
nodeLookup,
nodeOrigin,
} = store.getState();
const selectedNodes = nodes.filter(selectedAndDraggable(nodesDraggable));
// by default a node moves 5px on each key press // by default a node moves 5px on each key press
// if snap grid is enabled, we use that for the velocity // if snap grid is enabled, we use that for the velocity
const xVelo = snapToGrid ? snapGrid[0] : 5; const xVelo = snapToGrid ? snapGrid[0] : 5;
@@ -37,32 +30,34 @@ export function useMoveSelectedNodes() {
const xDiff = params.direction.x * xVelo * params.factor; const xDiff = params.direction.x * xVelo * params.factor;
const yDiff = params.direction.y * yVelo * params.factor; const yDiff = params.direction.y * yVelo * params.factor;
const nodeUpdates = selectedNodes.map((node) => { for (const [, node] of nodeLookup) {
if (node.computed?.positionAbsolute) { if (!isSelected(node)) {
let nextPosition = { continue;
x: node.computed.positionAbsolute.x + xDiff,
y: node.computed.positionAbsolute.y + yDiff,
};
if (snapToGrid) {
nextPosition = snapPosition(nextPosition, snapGrid);
}
const { position, positionAbsolute } = calculateNodePosition({
nodeId: node.id,
nextPosition,
nodeLookup,
nodeExtent,
nodeOrigin,
onError,
});
node.position = position;
node.computed.positionAbsolute = positionAbsolute;
} }
return node; let nextPosition = {
}); x: node.internals.positionAbsolute.x + xDiff,
y: node.internals.positionAbsolute.y + yDiff,
};
if (snapToGrid) {
nextPosition = snapPosition(nextPosition, snapGrid);
}
const { position, positionAbsolute } = calculateNodePosition({
nodeId: node.id,
nextPosition,
nodeLookup,
nodeExtent,
nodeOrigin,
onError,
});
node.position = position;
node.internals.positionAbsolute = positionAbsolute;
nodeUpdates.push(node);
}
updateNodePositions(nodeUpdates); updateNodePositions(nodeUpdates);
}, []); }, []);
@@ -1,5 +1,3 @@
import { internalsSymbol } from '@xyflow/system';
import { useStore } from './useStore'; import { useStore } from './useStore';
import type { ReactFlowState } from '../types'; import type { ReactFlowState } from '../types';
@@ -8,13 +6,13 @@ export type UseNodesInitializedOptions = {
}; };
const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) => { const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) => {
if (s.nodes.length === 0) { if (s.nodeLookup.size === 0) {
return false; return false;
} }
for (const node of s.nodes) { for (const [, node] of s.nodeLookup) {
if (options.includeHiddenNodes || !node.hidden) { if (options.includeHiddenNodes || !node.hidden) {
if (node[internalsSymbol]?.handleBounds === undefined) { if (node.internals.handleBounds === undefined) {
return false; return false;
} }
} }
+5 -3
View File
@@ -32,7 +32,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
}, []); }, []);
const getNode = useCallback<Instance.GetNode<NodeType>>((id) => { const getNode = useCallback<Instance.GetNode<NodeType>>((id) => {
return store.getState().nodeLookup.get(id) as NodeType; return store.getState().nodeLookup.get(id)?.internals.userProvidedNode as NodeType;
}, []); }, []);
const getEdges = useCallback<Instance.GetEdges<EdgeType>>(() => { const getEdges = useCallback<Instance.GetEdges<EdgeType>>(() => {
@@ -227,7 +227,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
const node = const node =
isNode(nodeOrRect) && nodeHasDimensions(nodeOrRect) isNode(nodeOrRect) && nodeHasDimensions(nodeOrRect)
? nodeOrRect ? nodeOrRect
: (store.getState().nodeLookup.get(nodeOrRect.id) as NodeType); : (store.getState().nodeLookup.get(nodeOrRect.id)?.internals.userProvidedNode as NodeType);
return node ? nodeToRect(node) : null; return node ? nodeToRect(node) : null;
}, []); }, []);
@@ -242,7 +242,9 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
} }
return (nodes || store.getState().nodes).filter((n) => { return (nodes || store.getState().nodes).filter((n) => {
if (!isRect && (n.id === nodeOrRect!.id || !n.computed?.positionAbsolute)) { const internalNode = store.getState().nodeLookup.get(n.id);
if (internalNode && !isRect && (n.id === nodeOrRect!.id || !internalNode.internals.positionAbsolute)) {
return false; return false;
} }
-1
View File
@@ -103,5 +103,4 @@ export {
addEdge, addEdge,
updateEdge, updateEdge,
getConnectedEdges, getConnectedEdges,
internalsSymbol,
} from '@xyflow/system'; } from '@xyflow/system';
+16 -18
View File
@@ -52,9 +52,9 @@ const createRFStore = ({
// //
// When this happens, we take the note objects passed by the user and extend them with fields // When this happens, we take the note objects passed by the user and extend them with fields
// relevant for internal React Flow operations. // relevant for internal React Flow operations.
const nodesWithInternalData = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect }); adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
set({ nodes: nodesWithInternalData }); set({ nodes });
}, },
setEdges: (edges: Edge[]) => { setEdges: (edges: Edge[]) => {
const { connectionLookup, edgeLookup } = get(); const { connectionLookup, edgeLookup } = get();
@@ -80,9 +80,9 @@ const createRFStore = ({
// new dimensions and update the nodes. // new dimensions and update the nodes.
updateNodeDimensions: (updates) => { updateNodeDimensions: (updates) => {
const { const {
nodes,
onNodesChange, onNodesChange,
fitView, fitView,
nodes,
nodeLookup, nodeLookup,
fitViewOnInit, fitViewOnInit,
fitViewDone, fitViewDone,
@@ -95,7 +95,6 @@ const createRFStore = ({
const updatedNodes = updateNodeDimensionsSystem( const updatedNodes = updateNodeDimensionsSystem(
updates, updates,
nodes,
nodeLookup, nodeLookup,
domNode, domNode,
nodeOrigin, nodeOrigin,
@@ -112,14 +111,14 @@ const createRFStore = ({
return; return;
} }
const nextNodes = updateAbsolutePositions(updatedNodes, nodeLookup, nodeOrigin); updateAbsolutePositions(nodeLookup, nodeOrigin);
// we call fitView once initially after all dimensions are set // we call fitView once initially after all dimensions are set
let nextFitViewDone = fitViewDone; let nextFitViewDone = fitViewDone;
if (!fitViewDone && fitViewOnInit) { if (!fitViewDone && fitViewOnInit) {
nextFitViewDone = fitView(nextNodes, { nextFitViewDone = fitView({
...fitViewOnInitOptions, ...fitViewOnInitOptions,
nodes: fitViewOnInitOptions?.nodes || nextNodes, nodes: fitViewOnInitOptions?.nodes || nodes,
}); });
} }
@@ -128,7 +127,7 @@ const createRFStore = ({
// has not provided an onNodesChange handler. // has not provided an onNodesChange handler.
// Nodes are only rendered if they have a width and height // Nodes are only rendered if they have a width and height
// attribute which they get from this handler. // attribute which they get from this handler.
set({ nodes: nextNodes, fitViewDone: nextFitViewDone }); set({ nodes, fitViewDone: nextFitViewDone });
if (changes?.length > 0) { if (changes?.length > 0) {
if (debug) { if (debug) {
@@ -143,7 +142,6 @@ const createRFStore = ({
id: node.id, id: node.id,
type: 'position', type: 'position',
position: node.position, position: node.position,
positionAbsolute: node.computed?.positionAbsolute,
dragging, dragging,
}; };
@@ -185,7 +183,7 @@ const createRFStore = ({
} }
}, },
addSelectedNodes: (selectedNodeIds) => { addSelectedNodes: (selectedNodeIds) => {
const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get(); const { multiSelectionActive, edgeLookup, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get();
if (multiSelectionActive) { if (multiSelectionActive) {
const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)); const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true));
@@ -193,11 +191,11 @@ const createRFStore = ({
return; return;
} }
triggerNodeChanges(getSelectionChanges(nodes, new Set([...selectedNodeIds]), true)); triggerNodeChanges(getSelectionChanges(nodeLookup, new Set([...selectedNodeIds]), true));
triggerEdgeChanges(getSelectionChanges(edges)); triggerEdgeChanges(getSelectionChanges(edgeLookup));
}, },
addSelectedEdges: (selectedEdgeIds) => { addSelectedEdges: (selectedEdgeIds) => {
const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get(); const { multiSelectionActive, edgeLookup, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get();
if (multiSelectionActive) { if (multiSelectionActive) {
const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)); const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true));
@@ -205,8 +203,8 @@ const createRFStore = ({
return; return;
} }
triggerEdgeChanges(getSelectionChanges(edges, new Set([...selectedEdgeIds]))); triggerEdgeChanges(getSelectionChanges(edgeLookup, new Set([...selectedEdgeIds])));
triggerNodeChanges(getSelectionChanges(nodes, new Set(), true)); triggerNodeChanges(getSelectionChanges(nodeLookup, new Set(), true));
}, },
unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => { unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => {
const { edges: storeEdges, nodes: storeNodes, triggerNodeChanges, triggerEdgeChanges } = get(); const { edges: storeEdges, nodes: storeNodes, triggerNodeChanges, triggerEdgeChanges } = get();
@@ -276,8 +274,8 @@ const createRFStore = ({
const { transform, width, height, panZoom, translateExtent } = get(); const { transform, width, height, panZoom, translateExtent } = get();
return panBySystem({ delta, panZoom, transform, translateExtent, width, height }); return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
}, },
fitView: (nodes: Node[], options?: FitViewOptions): boolean => { fitView: (options?: FitViewOptions): boolean => {
const { panZoom, width, height, minZoom, maxZoom, nodeOrigin } = get(); const { panZoom, width, height, minZoom, maxZoom, nodeOrigin, nodeLookup } = get();
if (!panZoom) { if (!panZoom) {
return false; return false;
@@ -285,7 +283,7 @@ const createRFStore = ({
return fitViewSystem( return fitViewSystem(
{ {
nodes, nodeLookup,
width, width,
height, height,
panZoom, panZoom,
+3 -3
View File
@@ -35,7 +35,7 @@ const getInitialState = ({
const storeNodes = defaultNodes ?? nodes ?? []; const storeNodes = defaultNodes ?? nodes ?? [];
updateConnectionLookup(connectionLookup, edgeLookup, storeEdges); updateConnectionLookup(connectionLookup, edgeLookup, storeEdges);
const nextNodes = adoptUserProvidedNodes(storeNodes, nodeLookup, { adoptUserProvidedNodes(storeNodes, nodeLookup, {
nodeOrigin: [0, 0], nodeOrigin: [0, 0],
elevateNodesOnSelect: false, elevateNodesOnSelect: false,
}); });
@@ -43,7 +43,7 @@ const getInitialState = ({
let transform: Transform = [0, 0, 1]; let transform: Transform = [0, 0, 1];
if (fitView && width && height) { if (fitView && width && height) {
const nodesWithDimensions = nextNodes.filter( const nodesWithDimensions = storeNodes.filter(
(node) => (node.width || node.initialWidth) && (node.height || node.initialHeight) (node) => (node.width || node.initialWidth) && (node.height || node.initialHeight)
); );
// @todo users nodeOrigin should be used here // @todo users nodeOrigin should be used here
@@ -57,7 +57,7 @@ const getInitialState = ({
width: 0, width: 0,
height: 0, height: 0,
transform, transform,
nodes: nextNodes, nodes: storeNodes,
nodeLookup, nodeLookup,
edges: storeEdges, edges: storeEdges,
edgeLookup, edgeLookup,
+10 -1
View File
@@ -1,5 +1,12 @@
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import type { CoordinateExtent, NodeBase, NodeOrigin, OnError, NodeProps as NodePropsBase } from '@xyflow/system'; import type {
CoordinateExtent,
NodeBase,
NodeOrigin,
OnError,
NodeProps as NodePropsBase,
InternalNodeBase,
} from '@xyflow/system';
import { NodeTypes } from './general'; import { NodeTypes } from './general';
@@ -17,6 +24,8 @@ export type Node<
focusable?: boolean; focusable?: boolean;
}; };
export type InternalNode<NodeType extends Node = Node> = InternalNodeBase<NodeType>;
export type NodeMouseHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, node: NodeType) => void; export type NodeMouseHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, node: NodeType) => void;
export type SelectionDragHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, nodes: NodeType[]) => void; export type SelectionDragHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, nodes: NodeType[]) => void;
export type OnNodeDrag<NodeType extends Node = Node> = ( export type OnNodeDrag<NodeType extends Node = Node> = (
+3 -2
View File
@@ -44,6 +44,7 @@ import type {
OnBeforeDelete, OnBeforeDelete,
IsValidConnection, IsValidConnection,
EdgeChange, EdgeChange,
InternalNode,
} from '.'; } from '.';
export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge = Edge> = { export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
@@ -52,7 +53,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
height: number; height: number;
transform: Transform; transform: Transform;
nodes: NodeType[]; nodes: NodeType[];
nodeLookup: NodeLookup<NodeType>; nodeLookup: NodeLookup<InternalNode<NodeType>>;
edges: Edge[]; edges: Edge[];
edgeLookup: EdgeLookup<EdgeType>; edgeLookup: EdgeLookup<EdgeType>;
connectionLookup: ConnectionLookup; connectionLookup: ConnectionLookup;
@@ -169,7 +170,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void; triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void;
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void; triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
panBy: PanBy; panBy: PanBy;
fitView: (nodes: NodeType[], options?: FitViewOptions) => boolean; fitView: (options?: FitViewOptions) => boolean;
}; };
export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore< export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore<
+12 -9
View File
@@ -1,6 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { EdgeLookup, NodeLookup } from '@xyflow/system'; import { EdgeLookup, NodeLookup } from '@xyflow/system';
import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types'; import type {
Node,
Edge,
EdgeChange,
NodeChange,
NodeSelectionChange,
EdgeSelectionChange,
InternalNode,
} from '../types';
export function handleParentExpand(updatedElements: any[], updateItem: any) { export function handleParentExpand(updatedElements: any[], updateItem: any) {
for (const [index, item] of updatedElements.entries()) { for (const [index, item] of updatedElements.entries()) {
@@ -125,11 +133,6 @@ function applyChange(change: any, element: any, elements: any[] = []): any {
element.position = change.position; element.position = change.position;
} }
if (typeof change.positionAbsolute !== 'undefined') {
element.computed ??= {};
element.computed.positionAbsolute = change.positionAbsolute;
}
if (typeof change.dragging !== 'undefined') { if (typeof change.dragging !== 'undefined') {
element.dragging = change.dragging; element.dragging = change.dragging;
} }
@@ -228,13 +231,13 @@ export function createSelectionChange(id: string, selected: boolean): NodeSelect
} }
export function getSelectionChanges( export function getSelectionChanges(
items: any[], items: Map<string, any>,
selectedIds: Set<string> = new Set(), selectedIds: Set<string> = new Set(),
mutateItem = false mutateItem = false
): NodeSelectionChange[] | EdgeSelectionChange[] { ): NodeSelectionChange[] | EdgeSelectionChange[] {
const changes: NodeSelectionChange[] | EdgeSelectionChange[] = []; const changes: NodeSelectionChange[] | EdgeSelectionChange[] = [];
for (const item of items) { for (const [, item] of items) {
const willBeSelected = selectedIds.has(item.id); const willBeSelected = selectedIds.has(item.id);
// we don't want to set all items to selected=false on the first selection // we don't want to set all items to selected=false on the first selection
@@ -266,7 +269,7 @@ export function getElementsDiffChanges({
lookup, lookup,
}: { }: {
items: Node[] | undefined; items: Node[] | undefined;
lookup: NodeLookup<Node>; lookup: NodeLookup<InternalNode<Node>>;
}): NodeChange[]; }): NodeChange[];
export function getElementsDiffChanges({ export function getElementsDiffChanges({
items, items,
-2
View File
@@ -25,8 +25,6 @@ export const errorMessages = {
`Node with id "${id}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`, `Node with id "${id}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`,
}; };
export const internalsSymbol = Symbol.for('internals');
export const infiniteExtent: CoordinateExtent = [ export const infiniteExtent: CoordinateExtent = [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
+2 -2
View File
@@ -2,7 +2,7 @@
import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3'; import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3';
import type { XYPosition, Rect } from './utils'; import type { XYPosition, Rect } from './utils';
import type { NodeBase, NodeDragItem, NodeOrigin } from './nodes'; import type { InternalNodeBase, NodeBase, NodeDragItem, NodeOrigin } from './nodes';
import type { ConnectingHandle, HandleType } from './handles'; import type { ConnectingHandle, HandleType } from './handles';
import { PanZoomInstance } from './panzoom'; import { PanZoomInstance } from './panzoom';
import { EdgeBase } from '..'; import { EdgeBase } from '..';
@@ -52,7 +52,7 @@ export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
export type IsValidConnection = (edge: EdgeBase | Connection) => boolean; export type IsValidConnection = (edge: EdgeBase | Connection) => boolean;
export type FitViewParamsBase<NodeType extends NodeBase> = { export type FitViewParamsBase<NodeType extends NodeBase> = {
nodes: NodeType[]; nodeLookup: Map<string, InternalNodeBase<NodeType>>;
width: number; width: number;
height: number; height: number;
panZoom: PanZoomInstance; panZoom: PanZoomInstance;
+16 -8
View File
@@ -1,4 +1,3 @@
import { internalsSymbol } from '../constants';
import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.'; import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.';
import { Optional } from '../utils/types'; import { Optional } from '../utils/types';
@@ -63,18 +62,25 @@ export type NodeBase<
computed?: { computed?: {
width?: number; width?: number;
height?: number; height?: number;
positionAbsolute?: XYPosition;
}; };
};
export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = NodeType & {
computed: {
width?: number;
height?: number;
};
// Only used internally // Only used internally
[internalsSymbol]?: { internals: {
z?: number; positionAbsolute: XYPosition;
handleBounds?: NodeHandleBounds; z: number;
isParent?: boolean; // @todo should we rename this to "handles" and use same type as node.handles?
isParent: boolean;
/** Holds a reference to the original node object provided by the user /** Holds a reference to the original node object provided by the user
* (which may lack some fields, like `computed` or `[internalSymbol]`. Used * (which may lack some fields, like `computed` or `[internalSymbol]`. Used
* as an optimization to avoid certain operations. */ * as an optimization to avoid certain operations. */
userProvidedNode: NodeBase<NodeData, NodeType>; userProvidedNode: NodeType;
handleBounds?: NodeHandleBounds;
}; };
}; };
@@ -120,6 +126,8 @@ export type NodeDragItem = {
computed: { computed: {
width: number | null; width: number | null;
height: number | null; height: number | null;
};
internals: {
positionAbsolute: XYPosition; positionAbsolute: XYPosition;
}; };
extent?: 'parent' | CoordinateExtent; extent?: 'parent' | CoordinateExtent;
@@ -137,4 +145,4 @@ 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>; export type NodeLookup<NodeType extends InternalNodeBase = InternalNodeBase> = Map<string, NodeType>;
+6 -7
View File
@@ -1,5 +1,4 @@
import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..'; import { Connection, InternalNodeBase, Transform, errorMessages, isEdgeBase, EdgeBase } from '../..';
import { EdgeBase, NodeBase } from '../../types';
import { 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)
@@ -24,8 +23,8 @@ export function getEdgeCenter({
} }
export type GetEdgeZIndexParams = { export type GetEdgeZIndexParams = {
sourceNode: NodeBase; sourceNode: InternalNodeBase;
targetNode: NodeBase; targetNode: InternalNodeBase;
selected?: boolean; selected?: boolean;
zIndex?: number; zIndex?: number;
elevateOnSelect?: boolean; elevateOnSelect?: boolean;
@@ -43,14 +42,14 @@ export function getElevatedEdgeZIndex({
} }
const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected; const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected;
const selectedZIndex = Math.max(sourceNode[internalsSymbol]?.z || 0, targetNode[internalsSymbol]?.z || 0, 1000); const selectedZIndex = Math.max(sourceNode.internals.z || 0, targetNode.internals.z || 0, 1000);
return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0); return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
} }
type IsEdgeVisibleParams = { type IsEdgeVisibleParams = {
sourceNode: NodeBase; sourceNode: InternalNodeBase;
targetNode: NodeBase; targetNode: InternalNodeBase;
width: number; width: number;
height: number; height: number;
transform: Transform; transform: Transform;
+13 -12
View File
@@ -1,25 +1,26 @@
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 } from '../../types/nodes'; import { InternalNodeBase, NodeHandle } from '../../types/nodes';
import { Position } from '../../types/utils'; import { Position } from '../../types/utils';
import { errorMessages, internalsSymbol } from '../../constants'; import { errorMessages } from '../../constants';
import { HandleElement } from '../../types'; import { HandleElement } from '../../types';
import { getNodeDimensions } from '../general'; import { getNodeDimensions } from '../general';
export type GetEdgePositionParams = { export type GetEdgePositionParams = {
id: string; id: string;
sourceNode: NodeBase; sourceNode: InternalNodeBase;
sourceHandle: string | null; sourceHandle: string | null;
targetNode: NodeBase; targetNode: InternalNodeBase;
targetHandle: string | null; targetHandle: string | null;
connectionMode: ConnectionMode; connectionMode: ConnectionMode;
onError?: OnError; onError?: OnError;
}; };
function isNodeInitialized(node: NodeBase): boolean { function isNodeInitialized(node: InternalNodeBase): boolean {
return ( return (
!!(node?.[internalsSymbol]?.handleBounds || node?.handles?.length) && node &&
!!(node?.computed?.width || node?.width || node?.initialWidth) !!(node.internals.handleBounds || node.handles?.length) &&
!!(node.computed.width || node.width || node.initialWidth)
); );
} }
@@ -30,8 +31,8 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
return null; return null;
} }
const sourceHandleBounds = sourceNode[internalsSymbol]?.handleBounds || toHandleBounds(sourceNode.handles); const sourceHandleBounds = sourceNode.internals.handleBounds || toHandleBounds(sourceNode.handles);
const targetHandleBounds = targetNode[internalsSymbol]?.handleBounds || toHandleBounds(targetNode.handles); const targetHandleBounds = targetNode.internals.handleBounds || toHandleBounds(targetNode.handles);
const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle); const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle);
const targetHandle = getHandle( const targetHandle = getHandle(
@@ -95,9 +96,9 @@ function toHandleBounds(handles?: NodeHandle[]) {
}; };
} }
function getHandlePosition(position: Position, node: NodeBase, handle: HandleElement | null = null): number[] { function getHandlePosition(position: Position, node: InternalNodeBase, handle: HandleElement | null = null): number[] {
const x = (handle?.x ?? 0) + (node.computed?.positionAbsolute?.x ?? 0); const x = (handle?.x ?? 0) + (node.internals.positionAbsolute?.x ?? 0);
const y = (handle?.y ?? 0) + (node.computed?.positionAbsolute?.y ?? 0); const y = (handle?.y ?? 0) + (node.internals.positionAbsolute?.y ?? 0);
const { width, height } = handle ?? getNodeDimensions(node); const { width, height } = handle ?? getNodeDimensions(node);
switch (position) { switch (position) {
+3 -2
View File
@@ -8,6 +8,7 @@ import type {
NodeOrigin, NodeOrigin,
SnapGrid, SnapGrid,
Transform, Transform,
InternalNodeBase,
} from '../types'; } from '../types';
import { type Viewport } from '../types'; import { type Viewport } from '../types';
import { getNodePositionWithOrigin } from './graph'; import { getNodePositionWithOrigin } from './graph';
@@ -65,7 +66,7 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
height: y2 - y, height: y2 - y,
}); });
export const nodeToRect = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => { export const nodeToRect = (node: InternalNodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin); const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
return { return {
@@ -75,7 +76,7 @@ export const nodeToRect = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rec
}; };
}; };
export const nodeToBox = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => { export const nodeToBox = (node: InternalNodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => {
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin); const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
return { return {
+74 -26
View File
@@ -24,6 +24,7 @@ import {
OnError, OnError,
OnBeforeDeleteBase, OnBeforeDeleteBase,
NodeLookup, NodeLookup,
InternalNodeBase,
} from '../types'; } from '../types';
import { errorMessages } from '../constants'; import { errorMessages } from '../constants';
@@ -47,6 +48,10 @@ export const isEdgeBase = <EdgeType extends EdgeBase = EdgeBase>(element: any):
export const isNodeBase = <NodeType extends NodeBase = NodeBase>(element: any): element is NodeType => export const isNodeBase = <NodeType extends NodeBase = NodeBase>(element: any): element is NodeType =>
'id' in element && 'position' in element && !('source' in element) && !('target' in element); 'id' in element && 'position' in element && !('source' in element) && !('target' in element);
export const isInternalNodeBase = <NodeType extends InternalNodeBase = InternalNodeBase>(
element: any
): element is NodeType => 'id' in element && 'computed' in element && !('source' in element) && !('target' in element);
/** /**
* Pass in a node, and get connected nodes where edge.source === node.id * Pass in a node, and get connected nodes where edge.source === node.id
* @public * @public
@@ -101,7 +106,7 @@ export const getIncomers = <NodeType extends NodeBase = NodeBase, EdgeType exten
}; };
export const getNodePositionWithOrigin = ( export const getNodePositionWithOrigin = (
node: NodeBase | undefined, node: InternalNodeBase | undefined,
nodeOrigin: NodeOrigin = [0, 0] nodeOrigin: NodeOrigin = [0, 0]
): { position: XYPosition; positionAbsolute: XYPosition } => { ): { position: XYPosition; positionAbsolute: XYPosition } => {
if (!node) { if (!node) {
@@ -128,12 +133,10 @@ export const getNodePositionWithOrigin = (
return { return {
position, position,
positionAbsolute: node.computed?.positionAbsolute positionAbsolute: {
? { x: node.internals.positionAbsolute.x - offsetX,
x: node.computed.positionAbsolute.x - offsetX, y: node.internals.positionAbsolute.y - offsetY,
y: node.computed.positionAbsolute.y - offsetY, },
}
: position,
}; };
}; };
@@ -151,6 +154,7 @@ export type GetNodesBoundsParams = {
* @param params.useRelativePosition - Whether to use the relative or absolute node positions * @param params.useRelativePosition - Whether to use the relative or absolute node positions
* @returns Bounding box enclosing all nodes * @returns Bounding box enclosing all nodes
*/ */
// @todo how to handle this if users do not have absolute positions?
export const getNodesBounds = ( export const getNodesBounds = (
nodes: NodeBase[], nodes: NodeBase[],
params: GetNodesBoundsParams = { nodeOrigin: [0, 0], useRelativePosition: false } params: GetNodesBoundsParams = { nodeOrigin: [0, 0], useRelativePosition: false }
@@ -161,6 +165,7 @@ export const getNodesBounds = (
const box = nodes.reduce( const box = nodes.reduce(
(currBox, node) => { (currBox, node) => {
// @ts-expect-error
const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin); const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin);
return getBoundsOfBoxes( return getBoundsOfBoxes(
currBox, currBox,
@@ -176,8 +181,47 @@ export const getNodesBounds = (
return boxToRect(box); return boxToRect(box);
}; };
export const getNodesInside = <NodeType extends NodeBase>( export type GetInternalNodesBoundsParams = {
nodes: NodeType[], nodeOrigin?: NodeOrigin;
useRelativePosition?: boolean;
filter?: (node: NodeBase) => boolean;
};
/**
* Determines a bounding box that contains all given nodes in an array
* @internal
*/
export const getInternalNodesBounds = (
nodeLookup: NodeLookup,
params: GetInternalNodesBoundsParams = {
nodeOrigin: [0, 0],
useRelativePosition: false,
}
): Rect => {
if (nodeLookup.size === 0) {
return { x: 0, y: 0, width: 0, height: 0 };
}
let box = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity };
nodeLookup.forEach((node) => {
if (params.filter == undefined || params.filter(node)) {
const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin);
box = getBoundsOfBoxes(
box,
rectToBox({
...nodePos[params.useRelativePosition ? 'position' : 'positionAbsolute'],
...getNodeDimensions(node),
})
);
}
});
return boxToRect(box);
};
export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
rect: Rect, rect: Rect,
[tx, ty, tScale]: Transform = [0, 0, 1], [tx, ty, tScale]: Transform = [0, 0, 1],
partially = false, partially = false,
@@ -191,13 +235,15 @@ export const getNodesInside = <NodeType extends NodeBase>(
height: rect.height / tScale, height: rect.height / tScale,
}; };
const visibleNodes = nodes.reduce<NodeType[]>((res, node) => { const visibleNodes: NodeType[] = [];
for (const [, node] of nodeLookup) {
const { computed, selectable = true, hidden = false } = node; const { computed, selectable = true, hidden = false } = node;
const width = computed?.width ?? node.width ?? node.initialWidth ?? null; const width = computed?.width ?? node.width ?? node.initialWidth ?? null;
const height = computed?.height ?? node.height ?? node.initialHeight ?? null; const height = computed?.height ?? node.height ?? node.initialHeight ?? null;
if ((excludeNonSelectableNodes && !selectable) || hidden) { if ((excludeNonSelectableNodes && !selectable) || hidden) {
return res; continue;
} }
const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin)); const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin));
@@ -208,11 +254,9 @@ export const getNodesInside = <NodeType extends NodeBase>(
const isVisible = notInitialized || partiallyVisible || overlappingArea >= area; const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
if (isVisible || node.dragging) { if (isVisible || node.dragging) {
res.push(node); visibleNodes.push(node);
} }
}
return res;
}, []);
return visibleNodes; return visibleNodes;
}; };
@@ -236,17 +280,20 @@ export const getConnectedEdges = <NodeType extends NodeBase = NodeBase, EdgeType
}; };
export function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>( export function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>(
{ nodes, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params, { nodeLookup, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params,
options?: Options options?: Options
) { ) {
const filteredNodes = nodes.filter((n) => { const filteredNodes: InternalNodeBase[] = [];
nodeLookup.forEach((n) => {
const isVisible = n.computed?.width && n.computed?.height && (options?.includeHiddenNodes || !n.hidden); const isVisible = n.computed?.width && n.computed?.height && (options?.includeHiddenNodes || !n.hidden);
if (options?.nodes?.length) { if (
return isVisible && options?.nodes.some((optionNode) => optionNode.id === n.id); isVisible &&
(!options?.nodes || (options?.nodes.length && options?.nodes.some((optionNode) => optionNode.id === n.id)))
) {
filteredNodes.push(n);
} }
return isVisible;
}); });
if (filteredNodes.length > 0) { if (filteredNodes.length > 0) {
@@ -303,7 +350,7 @@ export function calculateNodePosition<NodeType extends NodeBase>({
}: { }: {
nodeId: string; nodeId: string;
nextPosition: XYPosition; nextPosition: XYPosition;
nodeLookup: NodeLookup<NodeType>; nodeLookup: NodeLookup<InternalNodeBase<NodeType>>;
nodeOrigin?: NodeOrigin; nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
onError?: OnError; onError?: OnError;
@@ -313,16 +360,17 @@ export function calculateNodePosition<NodeType extends NodeBase>({
const { x: parentX, y: parentY } = parentNode const { x: parentX, y: parentY } = parentNode
? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute ? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute
: { x: 0, y: 0 }; : { x: 0, y: 0 };
let currentExtent = clampNodeExtent(node, node.extent || nodeExtent); let currentExtent = clampNodeExtent(node, node.extent || nodeExtent);
if (node.extent === 'parent' && !node.expandParent) { if (node.extent === 'parent' && !node.expandParent) {
if (!parentNode) { if (!parentNode) {
onError?.('005', errorMessages['error005']()); onError?.('005', errorMessages['error005']());
} else { } else {
const nodeWidth = node.computed?.width; const nodeWidth = node.computed.width;
const nodeHeight = node.computed?.height; const nodeHeight = node.computed.height;
const parentWidth = parentNode?.computed?.width; const parentWidth = parentNode.computed.width;
const parentHeight = parentNode?.computed?.height; const parentHeight = parentNode.computed.height;
if (nodeWidth && nodeHeight && parentWidth && parentHeight) { if (nodeWidth && nodeHeight && parentWidth && parentHeight) {
const currNodeOrigin = node.origin || nodeOrigin; const currNodeOrigin = node.origin || nodeOrigin;
+45 -61
View File
@@ -1,4 +1,3 @@
import { internalsSymbol } from '../constants';
import { import {
NodeBase, NodeBase,
CoordinateExtent, CoordinateExtent,
@@ -12,54 +11,53 @@ import {
ConnectionLookup, ConnectionLookup,
EdgeBase, EdgeBase,
EdgeLookup, EdgeLookup,
InternalNodeBase,
} from '../types'; } from '../types';
import { getDimensions, getHandleBounds } from './dom'; import { getDimensions, getHandleBounds } from './dom';
import { isNumeric } from './general'; import { isNumeric } from './general';
import { getNodePositionWithOrigin } from './graph'; import { getNodePositionWithOrigin } from './graph';
type ParentNodes = Record<string, boolean>; type ParentNodes = Set<string>;
export function updateAbsolutePositions<NodeType extends NodeBase>( export function updateAbsolutePositions<NodeType extends NodeBase>(
nodes: NodeType[], nodeLookup: Map<string, InternalNodeBase<NodeType>>,
nodeLookup: Map<string, NodeType>,
nodeOrigin: NodeOrigin = [0, 0], nodeOrigin: NodeOrigin = [0, 0],
parentNodes?: ParentNodes parentNodes?: ParentNodes
) { ) {
return nodes.map((node) => { for (const [, node] of nodeLookup) {
if (node.parentNode && !nodeLookup.has(node.parentNode)) { if (node.parentNode && !nodeLookup.has(node.parentNode)) {
throw new Error(`Parent node ${node.parentNode} not found`); throw new Error(`Parent node ${node.parentNode} not found`);
} }
if (node.parentNode || parentNodes?.[node.id]) { if (node.parentNode || parentNodes?.has(node.id)) {
const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : null; const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : null;
const { x, y, z } = calculateXYZPosition( const { x, y, z } = calculateXYZPosition(
node, node,
nodes,
nodeLookup, nodeLookup,
{ {
...node.position, ...node.position,
z: node[internalsSymbol]?.z ?? 0, z: node.internals.z ?? 0,
}, },
parentNode?.origin || nodeOrigin parentNode?.origin || nodeOrigin
); );
const positionChanged = x !== node.computed?.positionAbsolute?.x || y !== node.computed?.positionAbsolute?.y; const positionChanged = x !== node.internals.positionAbsolute?.x || y !== node.internals.positionAbsolute?.y;
node.computed!.positionAbsolute = positionChanged node.internals.positionAbsolute = positionChanged
? { ? {
x, x,
y, y,
} }
: node.computed?.positionAbsolute; : node.internals.positionAbsolute;
node[internalsSymbol]!.z = z; node.internals.z = z;
if (parentNodes?.[node.id]) { if (parentNodes?.has(node.id)) {
node[internalsSymbol]!.isParent = true; node.internals.isParent = true;
} }
} }
return node; nodeLookup.set(node.id, node);
}); }
} }
type UpdateNodesOptions<NodeType extends NodeBase> = { type UpdateNodesOptions<NodeType extends NodeBase> = {
@@ -70,64 +68,55 @@ type UpdateNodesOptions<NodeType extends NodeBase> = {
export function adoptUserProvidedNodes<NodeType extends NodeBase>( export function adoptUserProvidedNodes<NodeType extends NodeBase>(
nodes: NodeType[], nodes: NodeType[],
nodeLookup: Map<string, NodeType>, nodeLookup: Map<string, InternalNodeBase<NodeType>>,
options: UpdateNodesOptions<NodeType> = { options: UpdateNodesOptions<NodeType> = {
nodeOrigin: [0, 0] as NodeOrigin, nodeOrigin: [0, 0] as NodeOrigin,
elevateNodesOnSelect: true, elevateNodesOnSelect: true,
defaults: {}, defaults: {},
} }
): NodeType[] { ) {
const tmpLookup = new Map(nodeLookup); const tmpLookup = new Map(nodeLookup);
nodeLookup.clear(); nodeLookup.clear();
const parentNodes: ParentNodes = {}; const parentNodes: ParentNodes = new Set();
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
const nextNodes = nodes.map((n) => { nodes.forEach((n) => {
const currentStoreNode = tmpLookup.get(n.id); const currentStoreNode = tmpLookup.get(n.id);
if (n === currentStoreNode?.[internalsSymbol]?.userProvidedNode) { if (n.parentNode) {
parentNodes.add(n.parentNode);
}
if (n === currentStoreNode?.internals?.userProvidedNode) {
nodeLookup.set(n.id, currentStoreNode); nodeLookup.set(n.id, currentStoreNode);
return currentStoreNode; return currentStoreNode;
} }
const node: NodeType = { const node: InternalNodeBase<NodeType> = {
...options.defaults, ...options.defaults,
...n, ...n,
computed: { computed: {
positionAbsolute: n.position,
width: n.computed?.width, width: n.computed?.width,
height: n.computed?.height, height: n.computed?.height,
}, },
}; internals: {
const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0); positionAbsolute: n.position,
const currInternals = n?.[internalsSymbol] || currentStoreNode?.[internalsSymbol]; handleBounds: currentStoreNode?.internals?.handleBounds,
z: (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0),
if (node.parentNode) {
parentNodes[node.parentNode] = true;
}
Object.defineProperty(node, internalsSymbol, {
enumerable: false,
value: {
handleBounds: currInternals?.handleBounds,
z,
userProvidedNode: n, userProvidedNode: n,
isParent: false,
}, },
}); };
nodeLookup.set(node.id, node); nodeLookup.set(node.id, node);
return node;
}); });
const nodesWithPositions = updateAbsolutePositions(nextNodes, nodeLookup, options.nodeOrigin, parentNodes); if (parentNodes.size > 0) {
updateAbsolutePositions(nodeLookup, options.nodeOrigin, parentNodes);
return nodesWithPositions; }
} }
function calculateXYZPosition<NodeType extends NodeBase>( function calculateXYZPosition<NodeType extends NodeBase>(
node: NodeType, node: NodeType,
nodes: NodeType[], nodeLookup: Map<string, InternalNodeBase<NodeType>>,
nodeLookup: Map<string, NodeType>,
result: XYZPosition, result: XYZPosition,
nodeOrigin: NodeOrigin nodeOrigin: NodeOrigin
): XYZPosition { ): XYZPosition {
@@ -140,12 +129,11 @@ function calculateXYZPosition<NodeType extends NodeBase>(
return calculateXYZPosition( return calculateXYZPosition(
parentNode, parentNode,
nodes,
nodeLookup, nodeLookup,
{ {
x: (result.x ?? 0) + parentNodePosition.x, x: (result.x ?? 0) + parentNodePosition.x,
y: (result.y ?? 0) + parentNodePosition.y, y: (result.y ?? 0) + parentNodePosition.y,
z: (parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0) ? parentNode[internalsSymbol]?.z ?? 0 : result.z ?? 0, z: (parentNode.internals.z ?? 0) > (result.z ?? 0) ? parentNode.internals.z ?? 0 : result.z ?? 0,
}, },
parentNode.origin || nodeOrigin parentNode.origin || nodeOrigin
); );
@@ -153,25 +141,25 @@ function calculateXYZPosition<NodeType extends NodeBase>(
export function updateNodeDimensions<NodeType extends NodeBase>( export function updateNodeDimensions<NodeType extends NodeBase>(
updates: Map<string, NodeDimensionUpdate>, updates: Map<string, NodeDimensionUpdate>,
nodes: NodeType[], nodeLookup: Map<string, InternalNodeBase<NodeType>>,
nodeLookup: Map<string, NodeType>,
domNode: HTMLElement | null, domNode: HTMLElement | null,
nodeOrigin?: NodeOrigin, nodeOrigin?: NodeOrigin,
onUpdate?: (id: string, dimensions: Dimensions) => void onUpdate?: (id: string, dimensions: Dimensions) => void
): NodeType[] | null { ): { hasUpdate: boolean } {
const viewportNode = domNode?.querySelector('.xyflow__viewport'); const viewportNode = domNode?.querySelector('.xyflow__viewport');
let hasUpdate = false;
if (!viewportNode) { if (!viewportNode) {
return null; return { hasUpdate };
} }
const style = window.getComputedStyle(viewportNode); const style = window.getComputedStyle(viewportNode);
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform); const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
const nextNodes = nodes.map((node) => { updates.forEach((update) => {
const update = updates.get(node.id); const node = nodeLookup.get(update.id);
if (update) { if (node) {
const dimensions = getDimensions(update.nodeElement); const dimensions = getDimensions(update.nodeElement);
const doUpdate = !!( const doUpdate = !!(
dimensions.width && dimensions.width &&
@@ -180,6 +168,7 @@ export function updateNodeDimensions<NodeType extends NodeBase>(
); );
if (doUpdate) { if (doUpdate) {
hasUpdate = true;
onUpdate?.(node.id, dimensions); onUpdate?.(node.id, dimensions);
const newNode = { const newNode = {
@@ -188,25 +177,20 @@ export function updateNodeDimensions<NodeType extends NodeBase>(
...node.computed, ...node.computed,
...dimensions, ...dimensions,
}, },
[internalsSymbol]: { internals: {
...node[internalsSymbol], ...node.internals,
handleBounds: { handleBounds: {
source: getHandleBounds('.source', update.nodeElement, zoom, node.origin || nodeOrigin), source: getHandleBounds('.source', update.nodeElement, zoom, node.origin || nodeOrigin),
target: getHandleBounds('.target', update.nodeElement, zoom, node.origin || nodeOrigin), target: getHandleBounds('.target', update.nodeElement, zoom, node.origin || nodeOrigin),
}, },
}, },
}; };
nodeLookup.set(node.id, newNode); nodeLookup.set(node.id, newNode);
return newNode;
} }
} }
return node;
}); });
return nextNodes; return { hasUpdate };
} }
export function panBy({ export function panBy({
+15 -11
View File
@@ -26,13 +26,14 @@ import type {
OnSelectionDrag, OnSelectionDrag,
UpdateNodePositions, UpdateNodePositions,
Box, Box,
InternalNodeBase,
} from '../types'; } from '../types';
export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void; export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void;
type StoreItems<OnNodeDrag> = { type StoreItems<OnNodeDrag> = {
nodes: NodeBase[]; nodes: NodeBase[];
nodeLookup: Map<string, NodeBase>; nodeLookup: Map<string, InternalNodeBase>;
edges: EdgeBase[]; edges: EdgeBase[];
nodeExtent: CoordinateExtent; nodeExtent: CoordinateExtent;
snapGrid: SnapGrid; snapGrid: SnapGrid;
@@ -130,19 +131,23 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
// if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node // if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node
// based on its position so that the node stays at it's position relative to the selection. // based on its position so that the node stays at it's position relative to the selection.
const adjustedNodeExtent: CoordinateExtent = [ let adjustedNodeExtent: CoordinateExtent = [
[nodeExtent[0][0], nodeExtent[0][1]], [nodeExtent[0][0], nodeExtent[0][1]],
[nodeExtent[1][0], nodeExtent[1][1]], [nodeExtent[1][0], nodeExtent[1][1]],
]; ];
if (dragItems.length > 1 && nodeExtent && !n.extent) { if (dragItems.length > 1 && nodeExtent && !n.extent) {
adjustedNodeExtent[0][0] = n.computed.positionAbsolute.x - nodesBox.x + nodeExtent[0][0]; const { positionAbsolute } = n.internals;
adjustedNodeExtent[1][0] = const x1 = positionAbsolute.x - nodesBox.x + nodeExtent[0][0];
n.computed.positionAbsolute.x + (n.computed?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0]; const x2 = positionAbsolute.x + (n.computed?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0];
adjustedNodeExtent[0][1] = n.computed.positionAbsolute.y - nodesBox.y + nodeExtent[0][1]; const y1 = positionAbsolute.y - nodesBox.y + nodeExtent[0][1];
adjustedNodeExtent[1][1] = const y2 = positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
n.computed.positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
adjustedNodeExtent = [
[x1, y1],
[x2, y2],
];
} }
const { position, positionAbsolute } = calculateNodePosition({ const { position, positionAbsolute } = calculateNodePosition({
@@ -158,7 +163,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
hasChange = hasChange || n.position.x !== position.x || n.position.y !== position.y; hasChange = hasChange || n.position.x !== position.x || n.position.y !== position.y;
n.position = position; n.position = position;
n.computed.positionAbsolute = positionAbsolute; n.internals.positionAbsolute = positionAbsolute;
return n; return n;
}); });
@@ -208,7 +213,6 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
function startDrag(event: UseDragEvent) { function startDrag(event: UseDragEvent) {
const { const {
nodes,
nodeLookup, nodeLookup,
multiSelectionActive, multiSelectionActive,
nodesDraggable, nodesDraggable,
@@ -236,7 +240,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
lastPos = pointerPos; lastPos = pointerPos;
dragItems = getDragItems(nodes, nodesDraggable, pointerPos, nodeId); dragItems = getDragItems(nodeLookup, nodesDraggable, pointerPos, nodeId);
if (dragItems.length > 0 && (onDragStart || onNodeDragStart || (!nodeId && onSelectionDragStart))) { if (dragItems.length > 0 && (onDragStart || onNodeDragStart || (!nodeId && onSelectionDragStart))) {
const [currentNode, currentNodes] = getEventHandlerParams({ const [currentNode, currentNodes] = getEventHandlerParams({
+37 -34
View File
@@ -1,15 +1,15 @@
import { type NodeDragItem, type XYPosition, NodeBase } from '../types'; import { type NodeDragItem, type XYPosition, InternalNodeBase, NodeBase, NodeLookup } from '../types';
export function wrapSelectionDragFunc(selectionFunc?: (event: MouseEvent, nodes: NodeBase[]) => void) { export function wrapSelectionDragFunc(selectionFunc?: (event: MouseEvent, nodes: NodeBase[]) => void) {
return (event: MouseEvent, _: NodeBase, nodes: NodeBase[]) => selectionFunc?.(event, nodes); return (event: MouseEvent, _: NodeBase, nodes: NodeBase[]) => selectionFunc?.(event, nodes);
} }
export function isParentSelected<NodeType extends NodeBase>(node: NodeType, nodes: NodeType[]): boolean { export function isParentSelected<NodeType extends NodeBase>(node: NodeType, nodeLookup: NodeLookup): boolean {
if (!node.parentNode) { if (!node.parentNode) {
return false; return false;
} }
const parentNode = nodes.find((node) => node.id === node.parentNode); const parentNode = nodeLookup.get(node.parentNode);
if (!parentNode) { if (!parentNode) {
return false; return false;
@@ -19,7 +19,7 @@ export function isParentSelected<NodeType extends NodeBase>(node: NodeType, node
return true; return true;
} }
return isParentSelected(parentNode, nodes); return isParentSelected(parentNode, nodeLookup);
} }
export function hasSelector(target: Element, selector: string, domNode: Element): boolean { export function hasSelector(target: Element, selector: string, domNode: Element): boolean {
@@ -36,39 +36,43 @@ export function hasSelector(target: Element, selector: string, domNode: Element)
// looks for all selected nodes and created a NodeDragItem for each of them // looks for all selected nodes and created a NodeDragItem for each of them
export function getDragItems<NodeType extends NodeBase>( export function getDragItems<NodeType extends NodeBase>(
nodes: NodeType[], nodeLookup: Map<string, InternalNodeBase<NodeType>>,
nodesDraggable: boolean, nodesDraggable: boolean,
mousePos: XYPosition, mousePos: XYPosition,
nodeId?: string nodeId?: string
): NodeDragItem[] { ): NodeDragItem[] {
return nodes const dragItems: NodeDragItem[] = [];
.filter(
(n) => for (const [id, node] of nodeLookup) {
(n.selected || n.id === nodeId) && if (
(!n.parentNode || !isParentSelected(n, nodes)) && (node.selected || node.id === nodeId) &&
(n.draggable || (nodesDraggable && typeof n.draggable === 'undefined')) (!node.parentNode || !isParentSelected(node, nodeLookup)) &&
) (node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'))
.map((n) => ({ ) {
id: n.id, const internalNode = nodeLookup.get(id)!;
position: n.position || { x: 0, y: 0 },
distance: { dragItems.push({
x: mousePos.x - (n.computed?.positionAbsolute?.x ?? 0), id: internalNode.id,
y: mousePos.y - (n.computed?.positionAbsolute?.y ?? 0), position: internalNode.position || { x: 0, y: 0 },
}, distance: {
delta: { x: mousePos.x - (internalNode.internals.positionAbsolute?.x ?? 0),
x: 0, y: mousePos.y - (internalNode.internals.positionAbsolute?.y ?? 0),
y: 0, },
}, extent: internalNode.extent,
extent: n.extent, parentNode: internalNode.parentNode,
parentNode: n.parentNode, origin: internalNode.origin,
origin: n.origin, expandParent: internalNode.expandParent,
expandParent: n.expandParent, internals: {
computed: { positionAbsolute: internalNode.internals.positionAbsolute || { x: 0, y: 0 },
positionAbsolute: n.computed?.positionAbsolute || { x: 0, y: 0 }, },
width: n.computed?.width || 0, computed: {
height: n.computed?.height || 0, width: internalNode.computed.width || 0,
}, height: internalNode.computed.height || 0,
})); },
});
}
}
return dragItems;
} }
// returns two params: // returns two params:
@@ -91,7 +95,6 @@ export function getEventHandlerParams<NodeType extends NodeBase>({
position: n.position, position: n.position,
computed: { computed: {
...n.computed, ...n.computed,
positionAbsolute: n.computed.positionAbsolute,
}, },
}; };
}); });
+4 -4
View File
@@ -6,13 +6,13 @@ import {
type HandleType, type HandleType,
type Connection, type Connection,
type PanBy, type PanBy,
type NodeBase,
type Transform, type Transform,
type ConnectingHandle, type ConnectingHandle,
type OnConnectEnd, type OnConnectEnd,
type UpdateConnection, type UpdateConnection,
type IsValidConnection, type IsValidConnection,
type ConnectionHandle, type ConnectionHandle,
NodeLookup,
} from '../types'; } from '../types';
import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils'; import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils';
@@ -25,7 +25,7 @@ export type OnPointerDownParams = {
handleId: string | null; handleId: string | null;
nodeId: string; nodeId: string;
isTarget: boolean; isTarget: boolean;
nodes: NodeBase[]; nodeLookup: NodeLookup;
lib: string; lib: string;
flowId: string | null; flowId: string | null;
edgeUpdaterType?: HandleType; edgeUpdaterType?: HandleType;
@@ -79,7 +79,7 @@ function onPointerDown(
edgeUpdaterType, edgeUpdaterType,
isTarget, isTarget,
domNode, domNode,
nodes, nodeLookup,
lib, lib,
autoPanOnConnect, autoPanOnConnect,
flowId, flowId,
@@ -116,7 +116,7 @@ function onPointerDown(
let handleDomNode: Element | null = null; let handleDomNode: Element | null = null;
const handleLookup = getHandleLookup({ const handleLookup = getHandleLookup({
nodes, nodeLookup,
nodeId, nodeId,
handleId, handleId,
handleType, handleType,
+22 -20
View File
@@ -3,15 +3,15 @@ import {
type HandleType, type HandleType,
type NodeHandleBounds, type NodeHandleBounds,
type XYPosition, type XYPosition,
type NodeBase,
type ConnectionHandle, type ConnectionHandle,
InternalNodeBase,
NodeLookup,
} from '../types'; } from '../types';
import { internalsSymbol } from '../constants';
// this functions collects all handles and adds an absolute position // this functions collects all handles and adds an absolute position
// so that we can later find the closest handle to the mouse position // so that we can later find the closest handle to the mouse position
export function getHandles( export function getHandles(
node: NodeBase, node: InternalNodeBase,
handleBounds: NodeHandleBounds, handleBounds: NodeHandleBounds,
type: HandleType, type: HandleType,
currentHandle: string currentHandle: string
@@ -22,8 +22,8 @@ export function getHandles(
id: h.id || null, id: h.id || null,
type, type,
nodeId: node.id, nodeId: node.id,
x: (node.computed?.positionAbsolute?.x ?? 0) + h.x + h.width / 2, x: (node.internals.positionAbsolute.x ?? 0) + h.x + h.width / 2,
y: (node.computed?.positionAbsolute?.y ?? 0) + h.y + h.height / 2, y: (node.internals.positionAbsolute.y ?? 0) + h.y + h.height / 2,
}); });
} }
return res; return res;
@@ -62,28 +62,30 @@ export function getClosestHandle(
} }
type GetHandleLookupParams = { type GetHandleLookupParams = {
nodes: NodeBase[]; nodeLookup: NodeLookup;
nodeId: string; nodeId: string;
handleId: string | null; handleId: string | null;
handleType: string; handleType: string;
}; };
export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) { export function getHandleLookup({
return nodes.reduce<ConnectionHandle[]>((res, node) => { nodeLookup,
if (node[internalsSymbol]) { nodeId,
const { handleBounds } = node[internalsSymbol]; handleId,
let sourceHandles: ConnectionHandle[] = []; handleType,
let targetHandles: ConnectionHandle[] = []; }: GetHandleLookupParams): ConnectionHandle[] {
const connectionHandles: ConnectionHandle[] = [];
if (handleBounds) { for (const [, node] of nodeLookup) {
sourceHandles = getHandles(node, handleBounds, 'source', `${nodeId}-${handleId}-${handleType}`); if (node.internals.handleBounds) {
targetHandles = getHandles(node, handleBounds, 'target', `${nodeId}-${handleId}-${handleType}`); const id = `${nodeId}-${handleId}-${handleType}`;
} const sourceHandles = getHandles(node, node.internals.handleBounds, 'source', id);
const targetHandles = getHandles(node, node.internals.handleBounds, 'target', id);
res.push(...sourceHandles, ...targetHandles); connectionHandles.push(...sourceHandles, ...targetHandles);
} }
return res; }
}, []);
return connectionHandles;
} }
export function getHandleType( export function getHandleType(