Merge pull request #4105 from xyflow/refactor/internal-nodes

React Flow 12: separate user nodes and internal nodes
This commit is contained in:
Moritz Klack
2024-04-09 13:33:25 +02:00
committed by GitHub
57 changed files with 722 additions and 582 deletions
-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.`,
};
export const internalsSymbol = Symbol.for('internals');
export const infiniteExtent: CoordinateExtent = [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
+69
View File
@@ -0,0 +1,69 @@
import type { XYPosition, Dimensions, NodeBase, EdgeBase } from '.';
export type NodeDimensionChange = {
id: string;
type: 'dimensions';
dimensions?: Dimensions;
resizing?: boolean;
};
export type NodePositionChange = {
id: string;
type: 'position';
position?: XYPosition;
positionAbsolute?: XYPosition;
dragging?: boolean;
};
export type NodeSelectionChange = {
id: string;
type: 'select';
selected: boolean;
};
export type NodeRemoveChange = {
id: string;
type: 'remove';
};
export type NodeAddChange<NodeType extends NodeBase = NodeBase> = {
item: NodeType;
type: 'add';
};
export type NodeReplaceChange<NodeType extends NodeBase = NodeBase> = {
id: string;
item: NodeType;
type: 'replace';
};
/**
* Union type of all possible node changes.
* @public
*/
export type NodeChange<NodeType extends NodeBase = NodeBase> =
| NodeDimensionChange
| NodePositionChange
| NodeSelectionChange
| NodeRemoveChange
| NodeAddChange<NodeType>
| NodeReplaceChange<NodeType>;
export type EdgeSelectionChange = NodeSelectionChange;
export type EdgeRemoveChange = NodeRemoveChange;
export type EdgeAddChange<EdgeType extends EdgeBase = EdgeBase> = {
item: EdgeType;
type: 'add';
};
export type EdgeReplaceChange<EdgeType extends EdgeBase = EdgeBase> = {
id: string;
item: EdgeType;
type: 'replace';
};
export type EdgeChange<EdgeType extends EdgeBase = EdgeBase> =
| EdgeSelectionChange
| EdgeRemoveChange
| EdgeAddChange<EdgeType>
| EdgeReplaceChange<EdgeType>;
+3 -3
View File
@@ -2,7 +2,7 @@
import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3';
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 { PanZoomInstance } from './panzoom';
import { EdgeBase } from '..';
@@ -52,7 +52,7 @@ export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
export type IsValidConnection = (edge: EdgeBase | Connection) => boolean;
export type FitViewParamsBase<NodeType extends NodeBase> = {
nodes: NodeType[];
nodeLookup: Map<string, InternalNodeBase<NodeType>>;
width: number;
height: number;
panZoom: PanZoomInstance;
@@ -127,7 +127,7 @@ export type SelectionRect = Rect & {
export type OnError = (id: string, message: string) => void;
export type UpdateNodePositions = (dragItems: NodeDragItem[] | NodeBase[], dragging?: boolean) => void;
export type UpdateNodePositions = (dragItems: NodeDragItem[] | InternalNodeBase[], dragging?: boolean) => void;
export type PanBy = (delta: XYPosition) => boolean;
export type UpdateConnection = (params: {
+1
View File
@@ -1,3 +1,4 @@
export * from './changes';
export * from './general';
export * from './nodes';
export * from './edges';
+22 -16
View File
@@ -1,4 +1,3 @@
import { internalsSymbol } from '../constants';
import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.';
import { Optional } from '../utils/types';
@@ -44,7 +43,7 @@ export type NodeBase<
initialWidth?: number;
initialHeight?: number;
/** Parent node id, used for creating sub-flows */
parentNode?: string;
parentId?: string;
zIndex?: number;
/** Boundary a node can be moved in
* @example 'parent' or [[0, 0], [100, 100]]
@@ -60,21 +59,26 @@ export type NodeBase<
*/
origin?: NodeOrigin;
handles?: NodeHandle[];
computed?: {
measured?: {
width?: number;
height?: number;
positionAbsolute?: XYPosition;
};
};
// Only used internally
[internalsSymbol]?: {
z?: number;
export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = NodeType & {
measured: {
width?: number;
height?: number;
};
internals: {
positionAbsolute: XYPosition;
z: number;
// @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.
* Used as an optimization to avoid certain operations. */
userNode: NodeType;
handleBounds?: NodeHandleBounds;
isParent?: boolean;
/** Holds a reference to the original node object provided by the user
* (which may lack some fields, like `computed` or `[internalSymbol]`. Used
* as an optimization to avoid certain operations. */
userProvidedNode: NodeBase<NodeData, NodeType>;
};
};
@@ -104,7 +108,7 @@ export type NodeHandleBounds = {
export type NodeDimensionUpdate = {
id: string;
nodeElement: HTMLDivElement;
forceUpdate?: boolean;
force?: boolean;
};
export type NodeBounds = XYPosition & {
@@ -117,13 +121,15 @@ export type NodeDragItem = {
position: XYPosition;
// distance from the mouse cursor to the node when start dragging
distance: XYPosition;
computed: {
measured: {
width: number | null;
height: number | null;
};
internals: {
positionAbsolute: XYPosition;
};
extent?: 'parent' | CoordinateExtent;
parentNode?: string;
parentId?: string;
dragging?: boolean;
origin?: NodeOrigin;
expandParent?: boolean;
@@ -137,4 +143,4 @@ export type NodeHandle = Optional<HandleElement, 'width' | 'height'>;
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 { EdgeBase, NodeBase } from '../../types';
import { Connection, InternalNodeBase, Transform, errorMessages, isEdgeBase, EdgeBase } from '../..';
import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general';
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
@@ -24,8 +23,8 @@ export function getEdgeCenter({
}
export type GetEdgeZIndexParams = {
sourceNode: NodeBase;
targetNode: NodeBase;
sourceNode: InternalNodeBase;
targetNode: InternalNodeBase;
selected?: boolean;
zIndex?: number;
elevateOnSelect?: boolean;
@@ -43,14 +42,14 @@ export function getElevatedEdgeZIndex({
}
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);
}
type IsEdgeVisibleParams = {
sourceNode: NodeBase;
targetNode: NodeBase;
sourceNode: InternalNodeBase;
targetNode: InternalNodeBase;
width: number;
height: number;
transform: Transform;
+13 -12
View File
@@ -1,25 +1,26 @@
import { EdgePosition } from '../../types/edges';
import { ConnectionMode, OnError } from '../../types/general';
import { NodeBase, NodeHandle } from '../../types/nodes';
import { InternalNodeBase, NodeHandle } from '../../types/nodes';
import { Position } from '../../types/utils';
import { errorMessages, internalsSymbol } from '../../constants';
import { errorMessages } from '../../constants';
import { HandleElement } from '../../types';
import { getNodeDimensions } from '../general';
export type GetEdgePositionParams = {
id: string;
sourceNode: NodeBase;
sourceNode: InternalNodeBase;
sourceHandle: string | null;
targetNode: NodeBase;
targetNode: InternalNodeBase;
targetHandle: string | null;
connectionMode: ConnectionMode;
onError?: OnError;
};
function isNodeInitialized(node: NodeBase): boolean {
function isNodeInitialized(node: InternalNodeBase): boolean {
return (
!!(node?.[internalsSymbol]?.handleBounds || node?.handles?.length) &&
!!(node?.computed?.width || node?.width || node?.initialWidth)
node &&
!!(node.internals.handleBounds || node.handles?.length) &&
!!(node.measured.width || node.width || node.initialWidth)
);
}
@@ -30,8 +31,8 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
return null;
}
const sourceHandleBounds = sourceNode[internalsSymbol]?.handleBounds || toHandleBounds(sourceNode.handles);
const targetHandleBounds = targetNode[internalsSymbol]?.handleBounds || toHandleBounds(targetNode.handles);
const sourceHandleBounds = sourceNode.internals.handleBounds || toHandleBounds(sourceNode.handles);
const targetHandleBounds = targetNode.internals.handleBounds || toHandleBounds(targetNode.handles);
const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle);
const targetHandle = getHandle(
@@ -95,9 +96,9 @@ function toHandleBounds(handles?: NodeHandle[]) {
};
}
function getHandlePosition(position: Position, node: NodeBase, handle: HandleElement | null = null): number[] {
const x = (handle?.x ?? 0) + (node.computed?.positionAbsolute?.x ?? 0);
const y = (handle?.y ?? 0) + (node.computed?.positionAbsolute?.y ?? 0);
function getHandlePosition(position: Position, node: InternalNodeBase, handle: HandleElement | null = null): number[] {
const x = (handle?.x ?? 0) + (node.internals.positionAbsolute?.x ?? 0);
const y = (handle?.y ?? 0) + (node.internals.positionAbsolute?.y ?? 0);
const { width, height } = handle ?? getNodeDimensions(node);
switch (position) {
+11 -10
View File
@@ -8,6 +8,7 @@ import type {
NodeOrigin,
SnapGrid,
Transform,
InternalNodeBase,
} from '../types';
import { type Viewport } from '../types';
import { getNodePositionWithOrigin } from './graph';
@@ -65,23 +66,23 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
height: y2 - y,
});
export const nodeToRect = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
export const nodeToRect = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
return {
...positionAbsolute,
width: node.computed?.width ?? node.width ?? 0,
height: node.computed?.height ?? node.height ?? 0,
width: node.measured?.width ?? node.width ?? 0,
height: node.measured?.height ?? node.height ?? 0,
};
};
export const nodeToBox = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => {
export const nodeToBox = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => {
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
return {
...positionAbsolute,
x2: positionAbsolute.x + (node.computed?.width ?? node.width ?? 0),
y2: positionAbsolute.y + (node.computed?.height ?? node.height ?? 0),
x2: positionAbsolute.x + (node.measured?.width ?? node.width ?? 0),
y2: positionAbsolute.y + (node.measured?.height ?? node.height ?? 0),
};
};
@@ -207,14 +208,14 @@ export function getNodeDimensions<NodeType extends NodeBase = NodeBase>(
node: NodeType
): { width: number; height: number } {
return {
width: node.computed?.width ?? node.width ?? node.initialWidth ?? 0,
height: node.computed?.height ?? node.height ?? node.initialHeight ?? 0,
width: node.measured?.width ?? node.width ?? node.initialWidth ?? 0,
height: node.measured?.height ?? node.height ?? node.initialHeight ?? 0,
};
}
export function nodeHasDimensions<NodeType extends NodeBase = NodeBase>(node: NodeType): boolean {
return (
(node.computed?.width ?? node.width ?? node.initialWidth) !== undefined &&
(node.computed?.height ?? node.height ?? node.initialHeight) !== undefined
(node.measured?.width ?? node.width ?? node.initialWidth) !== undefined &&
(node.measured?.height ?? node.height ?? node.initialHeight) !== undefined
);
}
+83 -33
View File
@@ -24,6 +24,7 @@ import {
OnError,
OnBeforeDeleteBase,
NodeLookup,
InternalNodeBase,
} from '../types';
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 =>
'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 && 'internals' in element && !('source' in element) && !('target' in element);
/**
* Pass in a node, and get connected nodes where edge.source === node.id
* @public
@@ -101,7 +106,7 @@ export const getIncomers = <NodeType extends NodeBase = NodeBase, EdgeType exten
};
export const getNodePositionWithOrigin = (
node: NodeBase | undefined,
node: InternalNodeBase | NodeBase | undefined,
nodeOrigin: NodeOrigin = [0, 0]
): { position: XYPosition; positionAbsolute: XYPosition } => {
if (!node) {
@@ -128,12 +133,13 @@ export const getNodePositionWithOrigin = (
return {
position,
positionAbsolute: node.computed?.positionAbsolute
? {
x: node.computed.positionAbsolute.x - offsetX,
y: node.computed.positionAbsolute.y - offsetY,
}
: position,
positionAbsolute:
'internals' in node
? {
x: node.internals.positionAbsolute.x - offsetX,
y: node.internals.positionAbsolute.y - offsetY,
}
: position,
};
};
@@ -151,6 +157,7 @@ export type GetNodesBoundsParams = {
* @param params.useRelativePosition - Whether to use the relative or absolute node positions
* @returns Bounding box enclosing all nodes
*/
// @todo how to handle this if users do not have absolute positions?
export const getNodesBounds = (
nodes: NodeBase[],
params: GetNodesBoundsParams = { nodeOrigin: [0, 0], useRelativePosition: false }
@@ -176,8 +183,47 @@ export const getNodesBounds = (
return boxToRect(box);
};
export const getNodesInside = <NodeType extends NodeBase>(
nodes: NodeType[],
export type GetInternalNodesBoundsParams = {
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,
[tx, ty, tScale]: Transform = [0, 0, 1],
partially = false,
@@ -191,13 +237,15 @@ export const getNodesInside = <NodeType extends NodeBase>(
height: rect.height / tScale,
};
const visibleNodes = nodes.reduce<NodeType[]>((res, node) => {
const { computed, selectable = true, hidden = false } = node;
const width = computed?.width ?? node.width ?? node.initialWidth ?? null;
const height = computed?.height ?? node.height ?? node.initialHeight ?? null;
const visibleNodes: NodeType[] = [];
for (const [, node] of nodeLookup) {
const { measured, selectable = true, hidden = false } = node;
const width = measured.width ?? node.width ?? node.initialWidth ?? null;
const height = measured.height ?? node.height ?? node.initialHeight ?? null;
if ((excludeNonSelectableNodes && !selectable) || hidden) {
return res;
continue;
}
const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin));
@@ -208,11 +256,9 @@ export const getNodesInside = <NodeType extends NodeBase>(
const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
if (isVisible || node.dragging) {
res.push(node);
visibleNodes.push(node);
}
return res;
}, []);
}
return visibleNodes;
};
@@ -236,17 +282,20 @@ export const getConnectedEdges = <NodeType extends NodeBase = NodeBase, EdgeType
};
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
) {
const filteredNodes = nodes.filter((n) => {
const isVisible = n.computed?.width && n.computed?.height && (options?.includeHiddenNodes || !n.hidden);
const filteredNodes: InternalNodeBase[] = [];
if (options?.nodes?.length) {
return isVisible && options?.nodes.some((optionNode) => optionNode.id === n.id);
nodeLookup.forEach((n) => {
const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden);
if (
isVisible &&
(!options?.nodes || (options?.nodes.length && options?.nodes.some((optionNode) => optionNode.id === n.id)))
) {
filteredNodes.push(n);
}
return isVisible;
});
if (filteredNodes.length > 0) {
@@ -284,7 +333,7 @@ function clampNodeExtent<NodeType extends NodeBase>(
if (!extent || extent === 'parent') {
return extent;
}
return [extent[0], [extent[1][0] - (node.computed?.width ?? 0), extent[1][1] - (node.computed?.height ?? 0)]];
return [extent[0], [extent[1][0] - (node.measured?.width ?? 0), extent[1][1] - (node.measured?.height ?? 0)]];
}
/**
@@ -303,26 +352,27 @@ export function calculateNodePosition<NodeType extends NodeBase>({
}: {
nodeId: string;
nextPosition: XYPosition;
nodeLookup: NodeLookup<NodeType>;
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>;
nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent;
onError?: OnError;
}): { position: XYPosition; positionAbsolute: XYPosition } {
const node = nodeLookup.get(nodeId)!;
const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : undefined;
const parentNode = node.parentId ? nodeLookup.get(node.parentId) : undefined;
const { x: parentX, y: parentY } = parentNode
? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute
: { x: 0, y: 0 };
let currentExtent = clampNodeExtent(node, node.extent || nodeExtent);
if (node.extent === 'parent' && !node.expandParent) {
if (!parentNode) {
onError?.('005', errorMessages['error005']());
} else {
const nodeWidth = node.computed?.width;
const nodeHeight = node.computed?.height;
const parentWidth = parentNode?.computed?.width;
const parentHeight = parentNode?.computed?.height;
const nodeWidth = node.measured.width;
const nodeHeight = node.measured.height;
const parentWidth = parentNode.measured.width;
const parentHeight = parentNode.measured.height;
if (nodeWidth && nodeHeight && parentWidth && parentHeight) {
const currNodeOrigin = node.origin || nodeOrigin;
@@ -390,7 +440,7 @@ export async function getElementsToRemove<NodeType extends NodeBase = NodeBase,
}
const isIncluded = nodeIds.includes(node.id);
const parentHit = !isIncluded && node.parentNode && matchingNodes.find((n) => n.id === node.parentNode);
const parentHit = !isIncluded && node.parentId && matchingNodes.find((n) => n.id === node.parentId);
if (isIncluded || parentHit) {
matchingNodes.push(node);
+169 -96
View File
@@ -1,8 +1,6 @@
import { internalsSymbol } from '../constants';
import {
NodeBase,
CoordinateExtent,
Dimensions,
NodeDimensionUpdate,
NodeOrigin,
PanZoomInstance,
@@ -12,54 +10,58 @@ import {
ConnectionLookup,
EdgeBase,
EdgeLookup,
InternalNodeBase,
NodeChange,
NodeLookup,
Rect,
} from '../types';
import { getDimensions, getHandleBounds } from './dom';
import { isNumeric } from './general';
import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './general';
import { getNodePositionWithOrigin } from './graph';
type ParentNodes = Record<string, boolean>;
export function updateAbsolutePositions<NodeType extends NodeBase>(
nodes: NodeType[],
nodeLookup: Map<string, NodeType>,
nodeOrigin: NodeOrigin = [0, 0],
parentNodes?: ParentNodes
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
options: UpdateNodesOptions<NodeType> = {
nodeOrigin: [0, 0] as NodeOrigin,
elevateNodesOnSelect: true,
defaults: {},
},
parentNodeIds?: Set<string>
) {
return nodes.map((node) => {
if (node.parentNode && !nodeLookup.has(node.parentNode)) {
throw new Error(`Parent node ${node.parentNode} not found`);
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
for (const [id, node] of nodeLookup) {
const parentId = node.parentId;
if (parentId && !nodeLookup.has(parentId)) {
throw new Error(`Parent node ${parentId} not found`);
}
if (node.parentNode || parentNodes?.[node.id]) {
const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : null;
if (parentId || node.internals.isParent || parentNodeIds?.has(id)) {
const parentNode = parentId ? nodeLookup.get(parentId) : null;
const { x, y, z } = calculateXYZPosition(
node,
nodes,
nodeLookup,
{
...node.position,
z: node[internalsSymbol]?.z ?? 0,
z: (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0),
},
parentNode?.origin || nodeOrigin
parentNode?.origin || options.nodeOrigin
);
const positionChanged = x !== node.computed?.positionAbsolute?.x || y !== node.computed?.positionAbsolute?.y;
node.computed!.positionAbsolute = positionChanged
? {
x,
y,
}
: node.computed?.positionAbsolute;
const currPosition = node.internals.positionAbsolute;
const positionChanged = x !== currPosition.x || y !== currPosition.y;
node[internalsSymbol]!.z = z;
node.internals.positionAbsolute = positionChanged ? { x, y } : currPosition;
node.internals.z = z;
if (parentNodes?.[node.id]) {
node[internalsSymbol]!.isParent = true;
if (parentNodeIds !== undefined) {
node.internals.isParent = !!parentNodeIds?.has(id);
}
}
return node;
});
nodeLookup.set(id, node);
}
}
}
type UpdateNodesOptions<NodeType extends NodeBase> = {
@@ -68,128 +70,188 @@ type UpdateNodesOptions<NodeType extends NodeBase> = {
defaults?: Partial<NodeType>;
};
export function adoptUserProvidedNodes<NodeType extends NodeBase>(
export function adoptUserNodes<NodeType extends NodeBase>(
nodes: NodeType[],
nodeLookup: Map<string, NodeType>,
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
options: UpdateNodesOptions<NodeType> = {
nodeOrigin: [0, 0] as NodeOrigin,
elevateNodesOnSelect: true,
defaults: {},
}
): NodeType[] {
) {
const tmpLookup = new Map(nodeLookup);
nodeLookup.clear();
const parentNodes: ParentNodes = {};
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
const parentNodeIds = new Set<string>();
const nextNodes = nodes.map((n) => {
const currentStoreNode = tmpLookup.get(n.id);
if (n === currentStoreNode?.[internalsSymbol]?.userProvidedNode) {
nodeLookup.set(n.id, currentStoreNode);
return currentStoreNode;
nodes.forEach((userNode) => {
const currentStoreNode = tmpLookup.get(userNode.id);
if (userNode.parentId) {
parentNodeIds.add(userNode.parentId);
}
const node: NodeType = {
...options.defaults,
...n,
computed: {
positionAbsolute: n.position,
width: n.computed?.width,
height: n.computed?.height,
},
};
const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0);
const currInternals = n?.[internalsSymbol] || currentStoreNode?.[internalsSymbol];
if (node.parentNode) {
parentNodes[node.parentNode] = true;
if (userNode === currentStoreNode?.internals.userNode) {
nodeLookup.set(userNode.id, currentStoreNode);
} else {
nodeLookup.set(userNode.id, {
...options.defaults,
...userNode,
measured: {
width: userNode.measured?.width,
height: userNode.measured?.height,
},
internals: {
positionAbsolute: userNode.position,
handleBounds: currentStoreNode?.internals?.handleBounds,
z: (isNumeric(userNode.zIndex) ? userNode.zIndex : 0) + (userNode.selected ? selectedNodeZ : 0),
userNode,
isParent: false,
},
});
}
Object.defineProperty(node, internalsSymbol, {
enumerable: false,
value: {
handleBounds: currInternals?.handleBounds,
z,
userProvidedNode: n,
},
});
nodeLookup.set(node.id, node);
return node;
});
const nodesWithPositions = updateAbsolutePositions(nextNodes, nodeLookup, options.nodeOrigin, parentNodes);
return nodesWithPositions;
if (parentNodeIds.size > 0) {
updateAbsolutePositions(nodeLookup, options, parentNodeIds);
}
}
function calculateXYZPosition<NodeType extends NodeBase>(
node: NodeType,
nodes: NodeType[],
nodeLookup: Map<string, NodeType>,
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
result: XYZPosition,
nodeOrigin: NodeOrigin
nodeOrigin: NodeOrigin = [0, 0]
): XYZPosition {
if (!node.parentNode) {
if (!node.parentId) {
return result;
}
const parentNode = nodeLookup.get(node.parentNode)!;
const parentNode = nodeLookup.get(node.parentId)!;
const { position: parentNodePosition } = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin);
return calculateXYZPosition(
parentNode,
nodes,
nodeLookup,
{
x: (result.x ?? 0) + parentNodePosition.x,
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
);
}
export function updateNodeDimensions<NodeType extends NodeBase>(
export function handleParentExpand(nodes: InternalNodeBase[], nodeLookup: NodeLookup): NodeChange[] {
const changes: NodeChange[] = [];
const chilNodeRects = new Map<string, Rect>();
nodes.forEach((node) => {
const parentId = node.parentId;
if (node.expandParent && parentId) {
const parentNode = nodeLookup.get(parentId);
if (parentNode) {
const parentRect = chilNodeRects.get(parentId) || nodeToRect(parentNode, node.origin);
const expandedRect = getBoundsOfRects(parentRect, nodeToRect(node, node.origin));
chilNodeRects.set(parentId, expandedRect);
}
}
});
if (chilNodeRects.size > 0) {
chilNodeRects.forEach((rect, id) => {
const origParent = nodeLookup.get(id)!;
const { position } = getNodePositionWithOrigin(origParent, origParent.origin);
const dimensions = getNodeDimensions(origParent);
if (rect.x < position.x || rect.y < position.y) {
const xChange = Math.round(Math.abs(position.x - rect.x));
const yChange = Math.round(Math.abs(position.y - rect.y));
changes.push({
id,
type: 'position',
position: {
x: position.x - xChange,
y: position.y - yChange,
},
});
changes.push({
id,
type: 'dimensions',
resizing: true,
dimensions: {
width: dimensions.width + xChange,
height: dimensions.height + yChange,
},
});
// @todo we need to reset child node positions if < 0
} else if (dimensions.width < rect.width || dimensions.height < rect.height) {
changes.push({
id,
type: 'dimensions',
resizing: true,
dimensions: {
width: Math.max(dimensions.width, rect.width),
height: Math.max(dimensions.height, rect.height),
},
});
}
});
}
return changes;
}
export function updateNodeDimensions<NodeType extends InternalNodeBase>(
updates: Map<string, NodeDimensionUpdate>,
nodes: NodeType[],
nodeLookup: Map<string, NodeType>,
domNode: HTMLElement | null,
nodeOrigin?: NodeOrigin,
onUpdate?: (id: string, dimensions: Dimensions) => void
): NodeType[] | null {
nodeOrigin?: NodeOrigin
): NodeChange[] {
const viewportNode = domNode?.querySelector('.xyflow__viewport');
if (!viewportNode) {
return null;
return [];
}
const changes: NodeChange[] = [];
const style = window.getComputedStyle(viewportNode);
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
// in this array we collect nodes, that might trigger changes (like expanding parent)
const triggerChangeNodes: NodeType[] = [];
const nextNodes = nodes.map((node) => {
const update = updates.get(node.id);
updates.forEach((update) => {
const node = nodeLookup.get(update.id);
if (update) {
if (node?.hidden) {
nodeLookup.set(node.id, {
...node,
internals: {
...node.internals,
handleBounds: undefined,
},
});
} else if (node) {
const dimensions = getDimensions(update.nodeElement);
const doUpdate = !!(
dimensions.width &&
dimensions.height &&
(node.computed?.width !== dimensions.width || node.computed?.height !== dimensions.height || update.forceUpdate)
(node.measured?.width !== dimensions.width || node.measured?.height !== dimensions.height || update.force)
);
if (doUpdate) {
onUpdate?.(node.id, dimensions);
const newNode = {
...node,
computed: {
...node.computed,
measured: {
...node.measured,
...dimensions,
},
[internalsSymbol]: {
...node[internalsSymbol],
internals: {
...node.internals,
handleBounds: {
source: getHandleBounds('.source', update.nodeElement, zoom, node.origin || nodeOrigin),
target: getHandleBounds('.target', update.nodeElement, zoom, node.origin || nodeOrigin),
@@ -199,14 +261,25 @@ export function updateNodeDimensions<NodeType extends NodeBase>(
nodeLookup.set(node.id, newNode);
return newNode;
changes.push({
id: newNode.id,
type: 'dimensions',
dimensions,
});
if (newNode.expandParent) {
triggerChangeNodes.push(newNode);
}
}
}
return node;
});
return nextNodes;
if (triggerChangeNodes.length > 0) {
const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup);
changes.push(...parentExpandChanges);
}
return changes;
}
export function panBy({
+15 -11
View File
@@ -26,13 +26,14 @@ import type {
OnSelectionDrag,
UpdateNodePositions,
Box,
InternalNodeBase,
} from '../types';
export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void;
type StoreItems<OnNodeDrag> = {
nodes: NodeBase[];
nodeLookup: Map<string, NodeBase>;
nodeLookup: Map<string, InternalNodeBase>;
edges: EdgeBase[];
nodeExtent: CoordinateExtent;
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
// 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[1][0], nodeExtent[1][1]],
];
if (dragItems.length > 1 && nodeExtent && !n.extent) {
adjustedNodeExtent[0][0] = n.computed.positionAbsolute.x - nodesBox.x + nodeExtent[0][0];
adjustedNodeExtent[1][0] =
n.computed.positionAbsolute.x + (n.computed?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0];
const { positionAbsolute } = n.internals;
const x1 = positionAbsolute.x - nodesBox.x + nodeExtent[0][0];
const x2 = positionAbsolute.x + (n.measured?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0];
adjustedNodeExtent[0][1] = n.computed.positionAbsolute.y - nodesBox.y + nodeExtent[0][1];
adjustedNodeExtent[1][1] =
n.computed.positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
const y1 = positionAbsolute.y - nodesBox.y + nodeExtent[0][1];
const y2 = positionAbsolute.y + (n.measured?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
adjustedNodeExtent = [
[x1, y1],
[x2, y2],
];
}
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;
n.position = position;
n.computed.positionAbsolute = positionAbsolute;
n.internals.positionAbsolute = positionAbsolute;
return n;
});
@@ -208,7 +213,6 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
function startDrag(event: UseDragEvent) {
const {
nodes,
nodeLookup,
multiSelectionActive,
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 });
lastPos = pointerPos;
dragItems = getDragItems(nodes, nodesDraggable, pointerPos, nodeId);
dragItems = getDragItems(nodeLookup, nodesDraggable, pointerPos, nodeId);
if (dragItems.length > 0 && (onDragStart || onNodeDragStart || (!nodeId && onSelectionDragStart))) {
const [currentNode, currentNodes] = getEventHandlerParams({
+40 -37
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) {
return (event: MouseEvent, _: NodeBase, nodes: NodeBase[]) => selectionFunc?.(event, nodes);
}
export function isParentSelected<NodeType extends NodeBase>(node: NodeType, nodes: NodeType[]): boolean {
if (!node.parentNode) {
export function isParentSelected<NodeType extends NodeBase>(node: NodeType, nodeLookup: NodeLookup): boolean {
if (!node.parentId) {
return false;
}
const parentNode = nodes.find((node) => node.id === node.parentNode);
const parentNode = nodeLookup.get(node.parentId);
if (!parentNode) {
return false;
@@ -19,7 +19,7 @@ export function isParentSelected<NodeType extends NodeBase>(node: NodeType, node
return true;
}
return isParentSelected(parentNode, nodes);
return isParentSelected(parentNode, nodeLookup);
}
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
export function getDragItems<NodeType extends NodeBase>(
nodes: NodeType[],
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
nodesDraggable: boolean,
mousePos: XYPosition,
nodeId?: string
): NodeDragItem[] {
return nodes
.filter(
(n) =>
(n.selected || n.id === nodeId) &&
(!n.parentNode || !isParentSelected(n, nodes)) &&
(n.draggable || (nodesDraggable && typeof n.draggable === 'undefined'))
)
.map((n) => ({
id: n.id,
position: n.position || { x: 0, y: 0 },
distance: {
x: mousePos.x - (n.computed?.positionAbsolute?.x ?? 0),
y: mousePos.y - (n.computed?.positionAbsolute?.y ?? 0),
},
delta: {
x: 0,
y: 0,
},
extent: n.extent,
parentNode: n.parentNode,
origin: n.origin,
expandParent: n.expandParent,
computed: {
positionAbsolute: n.computed?.positionAbsolute || { x: 0, y: 0 },
width: n.computed?.width || 0,
height: n.computed?.height || 0,
},
}));
const dragItems: NodeDragItem[] = [];
for (const [id, node] of nodeLookup) {
if (
(node.selected || node.id === nodeId) &&
(!node.parentId || !isParentSelected(node, nodeLookup)) &&
(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'))
) {
const internalNode = nodeLookup.get(id)!;
dragItems.push({
id: internalNode.id,
position: internalNode.position || { x: 0, y: 0 },
distance: {
x: mousePos.x - (internalNode.internals.positionAbsolute?.x ?? 0),
y: mousePos.y - (internalNode.internals.positionAbsolute?.y ?? 0),
},
extent: internalNode.extent,
parentId: internalNode.parentId,
origin: internalNode.origin,
expandParent: internalNode.expandParent,
internals: {
positionAbsolute: internalNode.internals.positionAbsolute || { x: 0, y: 0 },
},
measured: {
width: internalNode.measured.width || 0,
height: internalNode.measured.height || 0,
},
});
}
}
return dragItems;
}
// returns two params:
@@ -89,9 +93,8 @@ export function getEventHandlerParams<NodeType extends NodeBase>({
return {
...node,
position: n.position,
computed: {
...n.computed,
positionAbsolute: n.computed.positionAbsolute,
measured: {
...n.measured,
},
};
});
+4 -4
View File
@@ -6,13 +6,13 @@ import {
type HandleType,
type Connection,
type PanBy,
type NodeBase,
type Transform,
type ConnectingHandle,
type OnConnectEnd,
type UpdateConnection,
type IsValidConnection,
type ConnectionHandle,
NodeLookup,
} from '../types';
import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils';
@@ -25,7 +25,7 @@ export type OnPointerDownParams = {
handleId: string | null;
nodeId: string;
isTarget: boolean;
nodes: NodeBase[];
nodeLookup: NodeLookup;
lib: string;
flowId: string | null;
edgeUpdaterType?: HandleType;
@@ -79,7 +79,7 @@ function onPointerDown(
edgeUpdaterType,
isTarget,
domNode,
nodes,
nodeLookup,
lib,
autoPanOnConnect,
flowId,
@@ -116,7 +116,7 @@ function onPointerDown(
let handleDomNode: Element | null = null;
const handleLookup = getHandleLookup({
nodes,
nodeLookup,
nodeId,
handleId,
handleType,
+22 -20
View File
@@ -3,15 +3,15 @@ import {
type HandleType,
type NodeHandleBounds,
type XYPosition,
type NodeBase,
type ConnectionHandle,
InternalNodeBase,
NodeLookup,
} from '../types';
import { internalsSymbol } from '../constants';
// this functions collects all handles and adds an absolute position
// so that we can later find the closest handle to the mouse position
export function getHandles(
node: NodeBase,
node: InternalNodeBase,
handleBounds: NodeHandleBounds,
type: HandleType,
currentHandle: string
@@ -22,8 +22,8 @@ export function getHandles(
id: h.id || null,
type,
nodeId: node.id,
x: (node.computed?.positionAbsolute?.x ?? 0) + h.x + h.width / 2,
y: (node.computed?.positionAbsolute?.y ?? 0) + h.y + h.height / 2,
x: (node.internals.positionAbsolute.x ?? 0) + h.x + h.width / 2,
y: (node.internals.positionAbsolute.y ?? 0) + h.y + h.height / 2,
});
}
return res;
@@ -62,28 +62,30 @@ export function getClosestHandle(
}
type GetHandleLookupParams = {
nodes: NodeBase[];
nodeLookup: NodeLookup;
nodeId: string;
handleId: string | null;
handleType: string;
};
export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) {
return nodes.reduce<ConnectionHandle[]>((res, node) => {
if (node[internalsSymbol]) {
const { handleBounds } = node[internalsSymbol];
let sourceHandles: ConnectionHandle[] = [];
let targetHandles: ConnectionHandle[] = [];
export function getHandleLookup({
nodeLookup,
nodeId,
handleId,
handleType,
}: GetHandleLookupParams): ConnectionHandle[] {
const connectionHandles: ConnectionHandle[] = [];
if (handleBounds) {
sourceHandles = getHandles(node, handleBounds, 'source', `${nodeId}-${handleId}-${handleType}`);
targetHandles = getHandles(node, handleBounds, 'target', `${nodeId}-${handleId}-${handleType}`);
}
res.push(...sourceHandles, ...targetHandles);
for (const [, node] of nodeLookup) {
if (node.internals.handleBounds) {
const id = `${nodeId}-${handleId}-${handleType}`;
const sourceHandles = getHandles(node, node.internals.handleBounds, 'source', id);
const targetHandles = getHandles(node, node.internals.handleBounds, 'target', id);
connectionHandles.push(...sourceHandles, ...targetHandles);
}
return res;
}, []);
}
return connectionHandles;
}
export function getHandleType(
+7 -7
View File
@@ -71,15 +71,15 @@ export type XYResizerInstance = {
function nodeToParentExtent(node: NodeBase): CoordinateExtent {
return [
[0, 0],
[node.computed!.width!, node.computed!.height!],
[node.measured!.width!, node.measured!.height!],
];
}
function nodeToChildExtent(child: NodeBase, parent: NodeBase, nodeOrigin: NodeOrigin): CoordinateExtent {
const x = parent.position.x + child.position.x;
const y = parent.position.y + child.position.y;
const width = child.computed!.width! ?? 0;
const height = child.computed!.height! ?? 0;
const width = child.measured!.width! ?? 0;
const height = child.measured!.height! ?? 0;
const originOffsetX = nodeOrigin[0] * width;
const originOffsetY = nodeOrigin[1] * height;
@@ -121,8 +121,8 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
prevValues = {
width: node.computed?.width ?? 0,
height: node.computed?.height ?? 0,
width: node.measured?.width ?? 0,
height: node.measured?.height ?? 0,
x: node.position.x ?? 0,
y: node.position.y ?? 0,
};
@@ -136,7 +136,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize
parentNode = undefined;
if (node.extent === 'parent' || node.expandParent) {
parentNode = nodeLookup.get(node.parentNode!);
parentNode = nodeLookup.get(node.parentId!);
if (parentNode && node.extent === 'parent') {
parentExtent = nodeToParentExtent(parentNode);
}
@@ -148,7 +148,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize
childExtent = undefined;
for (const [childId, child] of nodeLookup) {
if (child.parentNode === nodeId) {
if (child.parentId === nodeId) {
childNodes.push({
id: childId,
position: { ...child.position },