refactor(react): separate user nodes and indernal nodes
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { internalsSymbol } from '../constants';
|
||||
import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.';
|
||||
import { Optional } from '../utils/types';
|
||||
|
||||
@@ -63,18 +62,25 @@ export type NodeBase<
|
||||
computed?: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
positionAbsolute?: XYPosition;
|
||||
};
|
||||
};
|
||||
|
||||
export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = NodeType & {
|
||||
computed: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
// Only used internally
|
||||
[internalsSymbol]?: {
|
||||
z?: number;
|
||||
handleBounds?: NodeHandleBounds;
|
||||
isParent?: boolean;
|
||||
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
|
||||
* (which may lack some fields, like `computed` or `[internalSymbol]`. Used
|
||||
* as an optimization to avoid certain operations. */
|
||||
userProvidedNode: NodeBase<NodeData, NodeType>;
|
||||
userProvidedNode: NodeType;
|
||||
handleBounds?: NodeHandleBounds;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -120,6 +126,8 @@ export type NodeDragItem = {
|
||||
computed: {
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
};
|
||||
internals: {
|
||||
positionAbsolute: XYPosition;
|
||||
};
|
||||
extent?: 'parent' | CoordinateExtent;
|
||||
@@ -137,4 +145,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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.computed.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) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
NodeOrigin,
|
||||
SnapGrid,
|
||||
Transform,
|
||||
InternalNodeBase,
|
||||
} from '../types';
|
||||
import { type Viewport } from '../types';
|
||||
import { getNodePositionWithOrigin } from './graph';
|
||||
@@ -65,7 +66,7 @@ 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, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
|
||||
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
|
||||
|
||||
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);
|
||||
|
||||
return {
|
||||
|
||||
@@ -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 && 'computed' 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 | undefined,
|
||||
nodeOrigin: NodeOrigin = [0, 0]
|
||||
): { position: XYPosition; positionAbsolute: XYPosition } => {
|
||||
if (!node) {
|
||||
@@ -128,12 +133,10 @@ export const getNodePositionWithOrigin = (
|
||||
|
||||
return {
|
||||
position,
|
||||
positionAbsolute: node.computed?.positionAbsolute
|
||||
? {
|
||||
x: node.computed.positionAbsolute.x - offsetX,
|
||||
y: node.computed.positionAbsolute.y - offsetY,
|
||||
}
|
||||
: position,
|
||||
positionAbsolute: {
|
||||
x: node.internals.positionAbsolute.x - offsetX,
|
||||
y: node.internals.positionAbsolute.y - offsetY,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -151,6 +154,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 }
|
||||
@@ -161,6 +165,7 @@ export const getNodesBounds = (
|
||||
|
||||
const box = nodes.reduce(
|
||||
(currBox, node) => {
|
||||
// @ts-expect-error
|
||||
const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin);
|
||||
return getBoundsOfBoxes(
|
||||
currBox,
|
||||
@@ -176,8 +181,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 +235,15 @@ export const getNodesInside = <NodeType extends NodeBase>(
|
||||
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 width = computed?.width ?? node.width ?? node.initialWidth ?? null;
|
||||
const height = computed?.height ?? node.height ?? node.initialHeight ?? null;
|
||||
|
||||
if ((excludeNonSelectableNodes && !selectable) || hidden) {
|
||||
return res;
|
||||
continue;
|
||||
}
|
||||
|
||||
const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin));
|
||||
@@ -208,11 +254,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 +280,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 filteredNodes: InternalNodeBase[] = [];
|
||||
|
||||
nodeLookup.forEach((n) => {
|
||||
const isVisible = n.computed?.width && n.computed?.height && (options?.includeHiddenNodes || !n.hidden);
|
||||
|
||||
if (options?.nodes?.length) {
|
||||
return isVisible && options?.nodes.some((optionNode) => optionNode.id === n.id);
|
||||
if (
|
||||
isVisible &&
|
||||
(!options?.nodes || (options?.nodes.length && options?.nodes.some((optionNode) => optionNode.id === n.id)))
|
||||
) {
|
||||
filteredNodes.push(n);
|
||||
}
|
||||
|
||||
return isVisible;
|
||||
});
|
||||
|
||||
if (filteredNodes.length > 0) {
|
||||
@@ -303,7 +350,7 @@ export function calculateNodePosition<NodeType extends NodeBase>({
|
||||
}: {
|
||||
nodeId: string;
|
||||
nextPosition: XYPosition;
|
||||
nodeLookup: NodeLookup<NodeType>;
|
||||
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>;
|
||||
nodeOrigin?: NodeOrigin;
|
||||
nodeExtent?: CoordinateExtent;
|
||||
onError?: OnError;
|
||||
@@ -313,16 +360,17 @@ export function calculateNodePosition<NodeType extends NodeBase>({
|
||||
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.computed.width;
|
||||
const nodeHeight = node.computed.height;
|
||||
const parentWidth = parentNode.computed.width;
|
||||
const parentHeight = parentNode.computed.height;
|
||||
|
||||
if (nodeWidth && nodeHeight && parentWidth && parentHeight) {
|
||||
const currNodeOrigin = node.origin || nodeOrigin;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { internalsSymbol } from '../constants';
|
||||
import {
|
||||
NodeBase,
|
||||
CoordinateExtent,
|
||||
@@ -12,54 +11,53 @@ import {
|
||||
ConnectionLookup,
|
||||
EdgeBase,
|
||||
EdgeLookup,
|
||||
InternalNodeBase,
|
||||
} from '../types';
|
||||
import { getDimensions, getHandleBounds } from './dom';
|
||||
import { isNumeric } from './general';
|
||||
import { getNodePositionWithOrigin } from './graph';
|
||||
|
||||
type ParentNodes = Record<string, boolean>;
|
||||
type ParentNodes = Set<string>;
|
||||
|
||||
export function updateAbsolutePositions<NodeType extends NodeBase>(
|
||||
nodes: NodeType[],
|
||||
nodeLookup: Map<string, NodeType>,
|
||||
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
|
||||
nodeOrigin: NodeOrigin = [0, 0],
|
||||
parentNodes?: ParentNodes
|
||||
) {
|
||||
return nodes.map((node) => {
|
||||
for (const [, node] of nodeLookup) {
|
||||
if (node.parentNode && !nodeLookup.has(node.parentNode)) {
|
||||
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 { x, y, z } = calculateXYZPosition(
|
||||
node,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
{
|
||||
...node.position,
|
||||
z: node[internalsSymbol]?.z ?? 0,
|
||||
z: node.internals.z ?? 0,
|
||||
},
|
||||
parentNode?.origin || nodeOrigin
|
||||
);
|
||||
|
||||
const positionChanged = x !== node.computed?.positionAbsolute?.x || y !== node.computed?.positionAbsolute?.y;
|
||||
node.computed!.positionAbsolute = positionChanged
|
||||
const positionChanged = x !== node.internals.positionAbsolute?.x || y !== node.internals.positionAbsolute?.y;
|
||||
node.internals.positionAbsolute = positionChanged
|
||||
? {
|
||||
x,
|
||||
y,
|
||||
}
|
||||
: node.computed?.positionAbsolute;
|
||||
: node.internals.positionAbsolute;
|
||||
|
||||
node[internalsSymbol]!.z = z;
|
||||
node.internals.z = z;
|
||||
|
||||
if (parentNodes?.[node.id]) {
|
||||
node[internalsSymbol]!.isParent = true;
|
||||
if (parentNodes?.has(node.id)) {
|
||||
node.internals.isParent = true;
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
nodeLookup.set(node.id, node);
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateNodesOptions<NodeType extends NodeBase> = {
|
||||
@@ -70,64 +68,55 @@ type UpdateNodesOptions<NodeType extends NodeBase> = {
|
||||
|
||||
export function adoptUserProvidedNodes<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 parentNodes: ParentNodes = new Set();
|
||||
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
|
||||
|
||||
const nextNodes = nodes.map((n) => {
|
||||
nodes.forEach((n) => {
|
||||
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);
|
||||
return currentStoreNode;
|
||||
}
|
||||
|
||||
const node: NodeType = {
|
||||
const node: InternalNodeBase<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;
|
||||
}
|
||||
|
||||
Object.defineProperty(node, internalsSymbol, {
|
||||
enumerable: false,
|
||||
value: {
|
||||
handleBounds: currInternals?.handleBounds,
|
||||
z,
|
||||
internals: {
|
||||
positionAbsolute: n.position,
|
||||
handleBounds: currentStoreNode?.internals?.handleBounds,
|
||||
z: (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0),
|
||||
userProvidedNode: n,
|
||||
isParent: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
nodeLookup.set(node.id, node);
|
||||
|
||||
return node;
|
||||
});
|
||||
|
||||
const nodesWithPositions = updateAbsolutePositions(nextNodes, nodeLookup, options.nodeOrigin, parentNodes);
|
||||
|
||||
return nodesWithPositions;
|
||||
if (parentNodes.size > 0) {
|
||||
updateAbsolutePositions(nodeLookup, options.nodeOrigin, parentNodes);
|
||||
}
|
||||
}
|
||||
|
||||
function calculateXYZPosition<NodeType extends NodeBase>(
|
||||
node: NodeType,
|
||||
nodes: NodeType[],
|
||||
nodeLookup: Map<string, NodeType>,
|
||||
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
|
||||
result: XYZPosition,
|
||||
nodeOrigin: NodeOrigin
|
||||
): XYZPosition {
|
||||
@@ -140,12 +129,11 @@ function calculateXYZPosition<NodeType extends NodeBase>(
|
||||
|
||||
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
|
||||
);
|
||||
@@ -153,25 +141,25 @@ function calculateXYZPosition<NodeType extends NodeBase>(
|
||||
|
||||
export function updateNodeDimensions<NodeType extends NodeBase>(
|
||||
updates: Map<string, NodeDimensionUpdate>,
|
||||
nodes: NodeType[],
|
||||
nodeLookup: Map<string, NodeType>,
|
||||
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
|
||||
domNode: HTMLElement | null,
|
||||
nodeOrigin?: NodeOrigin,
|
||||
onUpdate?: (id: string, dimensions: Dimensions) => void
|
||||
): NodeType[] | null {
|
||||
): { hasUpdate: boolean } {
|
||||
const viewportNode = domNode?.querySelector('.xyflow__viewport');
|
||||
let hasUpdate = false;
|
||||
|
||||
if (!viewportNode) {
|
||||
return null;
|
||||
return { hasUpdate };
|
||||
}
|
||||
|
||||
const style = window.getComputedStyle(viewportNode);
|
||||
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
|
||||
|
||||
const nextNodes = nodes.map((node) => {
|
||||
const update = updates.get(node.id);
|
||||
updates.forEach((update) => {
|
||||
const node = nodeLookup.get(update.id);
|
||||
|
||||
if (update) {
|
||||
if (node) {
|
||||
const dimensions = getDimensions(update.nodeElement);
|
||||
const doUpdate = !!(
|
||||
dimensions.width &&
|
||||
@@ -180,6 +168,7 @@ export function updateNodeDimensions<NodeType extends NodeBase>(
|
||||
);
|
||||
|
||||
if (doUpdate) {
|
||||
hasUpdate = true;
|
||||
onUpdate?.(node.id, dimensions);
|
||||
|
||||
const newNode = {
|
||||
@@ -188,25 +177,20 @@ export function updateNodeDimensions<NodeType extends NodeBase>(
|
||||
...node.computed,
|
||||
...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),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
nodeLookup.set(node.id, newNode);
|
||||
|
||||
return newNode;
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
|
||||
return nextNodes;
|
||||
return { hasUpdate };
|
||||
}
|
||||
|
||||
export function panBy({
|
||||
|
||||
@@ -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.computed?.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.computed?.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({
|
||||
|
||||
@@ -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 {
|
||||
export function isParentSelected<NodeType extends NodeBase>(node: NodeType, nodeLookup: NodeLookup): boolean {
|
||||
if (!node.parentNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentNode = nodes.find((node) => node.id === node.parentNode);
|
||||
const parentNode = nodeLookup.get(node.parentNode);
|
||||
|
||||
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.parentNode || !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,
|
||||
parentNode: internalNode.parentNode,
|
||||
origin: internalNode.origin,
|
||||
expandParent: internalNode.expandParent,
|
||||
internals: {
|
||||
positionAbsolute: internalNode.internals.positionAbsolute || { x: 0, y: 0 },
|
||||
},
|
||||
computed: {
|
||||
width: internalNode.computed.width || 0,
|
||||
height: internalNode.computed.height || 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return dragItems;
|
||||
}
|
||||
|
||||
// returns two params:
|
||||
@@ -91,7 +95,6 @@ export function getEventHandlerParams<NodeType extends NodeBase>({
|
||||
position: n.position,
|
||||
computed: {
|
||||
...n.computed,
|
||||
positionAbsolute: n.computed.positionAbsolute,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user