Merge branch 'next' into refactor/tsdoc
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { Connection } from '../types';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function areConnectionMapsEqual(a?: Map<string, Connection>, b?: Map<string, Connection>) {
|
||||
if (!a && !b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!a || !b || a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!a.size && !b.size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const key of a.keys()) {
|
||||
if (!b.has(key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* We call the callback for all connections in a that are not in b
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function handleConnectionChange(
|
||||
a: Map<string, Connection>,
|
||||
b: Map<string, Connection>,
|
||||
cb?: (diff: Connection[]) => void
|
||||
) {
|
||||
if (!cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diff: Connection[] = [];
|
||||
|
||||
a.forEach((connection, key) => {
|
||||
if (!b?.has(key)) {
|
||||
diff.push(connection);
|
||||
}
|
||||
});
|
||||
|
||||
if (diff.length) {
|
||||
cb(diff);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..';
|
||||
import { EdgeBase, NodeBase } from '../../types';
|
||||
import { isNumeric, getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general';
|
||||
import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general';
|
||||
|
||||
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
|
||||
export function getEdgeCenter({
|
||||
@@ -23,63 +23,29 @@ export function getEdgeCenter({
|
||||
return [centerX, centerY, xOffset, yOffset];
|
||||
}
|
||||
|
||||
const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }];
|
||||
|
||||
export type GroupedEdges<EdgeType extends EdgeBase> = {
|
||||
edges: EdgeType[];
|
||||
level: number;
|
||||
isMaxLevel: boolean;
|
||||
export type GetEdgeZIndexParams = {
|
||||
sourceNode: NodeBase;
|
||||
targetNode: NodeBase;
|
||||
selected?: boolean;
|
||||
zIndex?: number;
|
||||
elevateOnSelect?: boolean;
|
||||
};
|
||||
|
||||
export function groupEdgesByZLevel<EdgeType extends EdgeBase>(
|
||||
edges: EdgeType[],
|
||||
nodeLookup: Map<string, NodeBase>,
|
||||
elevateEdgesOnSelect = false
|
||||
): GroupedEdges<EdgeType>[] {
|
||||
let maxLevel = -1;
|
||||
|
||||
const levelLookup = edges.reduce<Record<string, EdgeType[]>>((tree, edge) => {
|
||||
const hasZIndex = isNumeric(edge.zIndex);
|
||||
let z = hasZIndex ? edge.zIndex! : 0;
|
||||
|
||||
if (elevateEdgesOnSelect) {
|
||||
const targetNode = nodeLookup.get(edge.target);
|
||||
const sourceNode = nodeLookup.get(edge.source);
|
||||
const edgeOrConnectedNodeSelected = edge.selected || targetNode?.selected || sourceNode?.selected;
|
||||
const selectedZIndex = Math.max(
|
||||
sourceNode?.[internalsSymbol]?.z || 0,
|
||||
targetNode?.[internalsSymbol]?.z || 0,
|
||||
1000
|
||||
);
|
||||
z = (hasZIndex ? edge.zIndex! : 0) + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
|
||||
}
|
||||
|
||||
if (tree[z]) {
|
||||
tree[z].push(edge);
|
||||
} else {
|
||||
tree[z] = [edge];
|
||||
}
|
||||
|
||||
maxLevel = z > maxLevel ? z : maxLevel;
|
||||
|
||||
return tree;
|
||||
}, {});
|
||||
|
||||
const edgeTree = Object.entries(levelLookup).map(([key, edges]) => {
|
||||
const level = +key;
|
||||
|
||||
return {
|
||||
edges,
|
||||
level,
|
||||
isMaxLevel: level === maxLevel,
|
||||
};
|
||||
});
|
||||
|
||||
if (edgeTree.length === 0) {
|
||||
return defaultEdgeTree;
|
||||
export function getElevatedEdgeZIndex({
|
||||
sourceNode,
|
||||
targetNode,
|
||||
selected = false,
|
||||
zIndex = 0,
|
||||
elevateOnSelect = false,
|
||||
}: GetEdgeZIndexParams): number {
|
||||
if (!elevateOnSelect) {
|
||||
return zIndex;
|
||||
}
|
||||
|
||||
return edgeTree;
|
||||
const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected;
|
||||
const selectedZIndex = Math.max(sourceNode[internalsSymbol]?.z || 0, targetNode[internalsSymbol]?.z || 0, 1000);
|
||||
|
||||
return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
|
||||
}
|
||||
|
||||
type IsEdgeVisibleParams = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EdgePosition } from '../../types/edges';
|
||||
import { ConnectionMode, OnError } from '../../types/general';
|
||||
import { NodeBase, NodeHandle, NodeHandleBounds } from '../../types/nodes';
|
||||
import { Position, Rect, XYPosition } from '../../types/utils';
|
||||
import { NodeBase, NodeHandle } from '../../types/nodes';
|
||||
import { Position } from '../../types/utils';
|
||||
import { errorMessages, internalsSymbol } from '../../constants';
|
||||
import { HandleElement } from '../../types';
|
||||
|
||||
@@ -15,21 +15,28 @@ export type GetEdgePositionParams = {
|
||||
onError?: OnError;
|
||||
};
|
||||
|
||||
export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null {
|
||||
const [sourceNodeRect, sourceHandleBounds, isSourceValid] = getHandleDataByNode(params.sourceNode);
|
||||
const [targetNodeRect, targetHandleBounds, isTargetValid] = getHandleDataByNode(params.targetNode);
|
||||
function isNodeInitialized(node: NodeBase): boolean {
|
||||
return !!node?.[internalsSymbol]?.handleBounds && !!node?.computed?.width;
|
||||
}
|
||||
|
||||
if (!isSourceValid || !isTargetValid) {
|
||||
export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null {
|
||||
const { sourceNode, targetNode } = params;
|
||||
|
||||
if (!isNodeInitialized(sourceNode) || !isNodeInitialized(targetNode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// when connection type is loose we can define all handles as sources and connect source -> source
|
||||
const targetNodeHandles =
|
||||
const sourceHandleBounds = sourceNode[internalsSymbol]?.handleBounds || toHandleBounds(sourceNode.handles);
|
||||
const targetHandleBounds = targetNode[internalsSymbol]?.handleBounds || toHandleBounds(targetNode.handles);
|
||||
|
||||
const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle);
|
||||
const targetHandle = getHandle(
|
||||
// when connection type is loose we can define all handles as sources and connect source -> source
|
||||
params.connectionMode === ConnectionMode.Strict
|
||||
? targetHandleBounds!.target
|
||||
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []);
|
||||
const sourceHandle = getHandle(sourceHandleBounds!.source!, params.sourceHandle);
|
||||
const targetHandle = getHandle(targetNodeHandles!, params.targetHandle);
|
||||
? targetHandleBounds?.target ?? []
|
||||
: (targetHandleBounds?.target ?? []).concat(targetHandleBounds?.source ?? []),
|
||||
params.targetHandle
|
||||
);
|
||||
const sourcePosition = sourceHandle?.position || Position.Bottom;
|
||||
const targetPosition = targetHandle?.position || Position.Top;
|
||||
|
||||
@@ -46,8 +53,8 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
|
||||
return null;
|
||||
}
|
||||
|
||||
const { x: sourceX, y: sourceY } = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
|
||||
const { x: targetX, y: targetY } = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
|
||||
const [sourceX, sourceY] = getHandlePosition(sourcePosition, sourceNode, sourceHandle);
|
||||
const [targetX, targetY] = getHandlePosition(targetPosition, targetNode, targetHandle);
|
||||
|
||||
return {
|
||||
sourceX,
|
||||
@@ -64,79 +71,41 @@ function toHandleBounds(handles?: NodeHandle[]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return handles.reduce<NodeHandleBounds>(
|
||||
(res, item) => {
|
||||
item.width = item.width || 1;
|
||||
item.height = item.height || 1;
|
||||
const source = [];
|
||||
const target = [];
|
||||
|
||||
if (item.type === 'source') {
|
||||
res.source?.push(item as HandleElement);
|
||||
}
|
||||
for (const handle of handles) {
|
||||
handle.width = handle.width || 1;
|
||||
handle.height = handle.height || 1;
|
||||
|
||||
if (item.type === 'target') {
|
||||
res.target?.push(item as HandleElement);
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
{
|
||||
source: [],
|
||||
target: [],
|
||||
if (handle.type === 'source') {
|
||||
source.push(handle as HandleElement);
|
||||
} else if (handle.type === 'target') {
|
||||
target.push(handle as HandleElement);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
source,
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
function getHandleDataByNode(node?: NodeBase): [Rect, NodeHandleBounds | null, boolean] {
|
||||
const handleBounds = node?.[internalsSymbol]?.handleBounds || toHandleBounds(node?.handles) || null;
|
||||
const nodeWidth = node?.computed?.width || node?.width;
|
||||
const nodeHeight = node?.computed?.height || node?.height;
|
||||
|
||||
const isValid =
|
||||
handleBounds &&
|
||||
nodeWidth &&
|
||||
nodeHeight &&
|
||||
typeof node?.computed?.positionAbsolute?.x !== 'undefined' &&
|
||||
typeof node?.computed?.positionAbsolute?.y !== 'undefined';
|
||||
|
||||
return [
|
||||
{
|
||||
x: node?.computed?.positionAbsolute?.x || 0,
|
||||
y: node?.computed?.positionAbsolute?.y || 0,
|
||||
width: nodeWidth || 0,
|
||||
height: nodeHeight || 0,
|
||||
},
|
||||
handleBounds,
|
||||
!!isValid,
|
||||
];
|
||||
}
|
||||
|
||||
function getHandlePosition(position: Position, nodeRect: Rect, handle: HandleElement | null = null): XYPosition {
|
||||
const x = (handle?.x || 0) + nodeRect.x;
|
||||
const y = (handle?.y || 0) + nodeRect.y;
|
||||
const width = handle?.width || nodeRect.width;
|
||||
const height = handle?.height || nodeRect.height;
|
||||
function getHandlePosition(position: Position, node: NodeBase, handle: HandleElement | null = null): number[] {
|
||||
const x = (handle?.x ?? 0) + (node.computed?.positionAbsolute?.x ?? 0);
|
||||
const y = (handle?.y ?? 0) + (node.computed?.positionAbsolute?.y ?? 0);
|
||||
const width = handle?.width || (node?.computed?.width ?? node?.width ?? 0);
|
||||
const height = handle?.height || (node?.computed?.height ?? node?.height ?? 0);
|
||||
|
||||
switch (position) {
|
||||
case Position.Top:
|
||||
return {
|
||||
x: x + width / 2,
|
||||
y,
|
||||
};
|
||||
return [x + width / 2, y];
|
||||
case Position.Right:
|
||||
return {
|
||||
x: x + width,
|
||||
y: y + height / 2,
|
||||
};
|
||||
return [x + width, y + height / 2];
|
||||
case Position.Bottom:
|
||||
return {
|
||||
x: x + width / 2,
|
||||
y: y + height,
|
||||
};
|
||||
return [x + width / 2, y + height];
|
||||
case Position.Left:
|
||||
return {
|
||||
x,
|
||||
y: y + height / 2,
|
||||
};
|
||||
return [x, y + height / 2];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
getViewportForBounds,
|
||||
} from './general';
|
||||
import {
|
||||
type Connection,
|
||||
type Transform,
|
||||
type XYPosition,
|
||||
type Rect,
|
||||
@@ -33,9 +32,8 @@ import { errorMessages } from '../constants';
|
||||
* @param element - The element to test
|
||||
* @returns A boolean indicating whether the element is an Edge
|
||||
*/
|
||||
export const isEdgeBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
|
||||
element: NodeType | Connection | EdgeType
|
||||
): element is EdgeType => 'id' in element && 'source' in element && 'target' in element;
|
||||
export const isEdgeBase = <EdgeType extends EdgeBase = EdgeBase>(element: any): element is EdgeType =>
|
||||
'id' in element && 'source' in element && 'target' in element;
|
||||
|
||||
/**
|
||||
* Test whether an object is useable as a Node
|
||||
@@ -44,9 +42,8 @@ export const isEdgeBase = <NodeType extends NodeBase = NodeBase, EdgeType extend
|
||||
* @param element - The element to test
|
||||
* @returns A boolean indicating whether the element is an Node
|
||||
*/
|
||||
export const isNodeBase = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
|
||||
element: NodeType | Connection | EdgeType
|
||||
): element is NodeType => 'id' in element && !('source' in element) && !('target' in element);
|
||||
export const isNodeBase = <NodeType extends NodeBase = NodeBase>(element: any): element is NodeType =>
|
||||
'id' in element && !('source' in element) && !('target' in element);
|
||||
|
||||
/**
|
||||
* Pass in a node, and get connected nodes where edge.source === node.id
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './connections';
|
||||
export * from './dom';
|
||||
export * from './edges';
|
||||
export * from './graph';
|
||||
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
Transform,
|
||||
XYPosition,
|
||||
XYZPosition,
|
||||
ConnectionLookup,
|
||||
EdgeBase,
|
||||
EdgeLookup,
|
||||
} from '../types';
|
||||
import { getDimensions, getHandleBounds } from './dom';
|
||||
import { isNumeric } from './general';
|
||||
@@ -40,10 +43,13 @@ export function updateAbsolutePositions<NodeType extends NodeBase>(
|
||||
parentNode?.origin || nodeOrigin
|
||||
);
|
||||
|
||||
node.computed!.positionAbsolute = {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
const positionChanged = x !== node.computed?.positionAbsolute?.x || y !== node.computed?.positionAbsolute?.y;
|
||||
node.computed!.positionAbsolute = positionChanged
|
||||
? {
|
||||
x,
|
||||
y,
|
||||
}
|
||||
: node.computed?.positionAbsolute;
|
||||
|
||||
node[internalsSymbol]!.z = z;
|
||||
|
||||
@@ -62,7 +68,7 @@ type UpdateNodesOptions<NodeType extends NodeBase> = {
|
||||
defaults?: Partial<NodeType>;
|
||||
};
|
||||
|
||||
export function updateNodes<NodeType extends NodeBase>(
|
||||
export function adoptUserProvidedNodes<NodeType extends NodeBase>(
|
||||
nodes: NodeType[],
|
||||
nodeLookup: Map<string, NodeType>,
|
||||
options: UpdateNodesOptions<NodeType> = {
|
||||
@@ -71,11 +77,18 @@ export function updateNodes<NodeType extends NodeBase>(
|
||||
defaults: {},
|
||||
}
|
||||
): NodeType[] {
|
||||
const tmpLookup = new Map(nodeLookup);
|
||||
nodeLookup.clear();
|
||||
const parentNodes: ParentNodes = {};
|
||||
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
|
||||
|
||||
const nextNodes = nodes.map((n) => {
|
||||
const currentStoreNode = nodeLookup.get(n.id);
|
||||
const currentStoreNode = tmpLookup.get(n.id);
|
||||
if (n === currentStoreNode?.[internalsSymbol]?.userProvidedNode) {
|
||||
nodeLookup.set(n.id, currentStoreNode);
|
||||
return currentStoreNode;
|
||||
}
|
||||
|
||||
const node: NodeType = {
|
||||
...options.defaults,
|
||||
...n,
|
||||
@@ -97,6 +110,7 @@ export function updateNodes<NodeType extends NodeBase>(
|
||||
value: {
|
||||
handleBounds: currInternals?.handleBounds,
|
||||
z,
|
||||
userProvidedNode: n,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -137,14 +151,14 @@ function calculateXYZPosition<NodeType extends NodeBase>(
|
||||
);
|
||||
}
|
||||
|
||||
export function updateNodeDimensions(
|
||||
export function updateNodeDimensions<NodeType extends NodeBase>(
|
||||
updates: Map<string, NodeDimensionUpdate>,
|
||||
nodes: NodeBase[],
|
||||
nodeLookup: Map<string, NodeBase>,
|
||||
nodes: NodeType[],
|
||||
nodeLookup: Map<string, NodeType>,
|
||||
domNode: HTMLElement | null,
|
||||
nodeOrigin?: NodeOrigin,
|
||||
onUpdate?: (id: string, dimensions: Dimensions) => void
|
||||
): NodeBase[] | null {
|
||||
): NodeType[] | null {
|
||||
const viewportNode = domNode?.querySelector('.xyflow__viewport');
|
||||
|
||||
if (!viewportNode) {
|
||||
@@ -233,3 +247,23 @@ export function panBy({
|
||||
|
||||
return transformChanged;
|
||||
}
|
||||
|
||||
export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) {
|
||||
connectionLookup.clear();
|
||||
edgeLookup.clear();
|
||||
|
||||
for (const edge of edges) {
|
||||
const { source, target, sourceHandle = null, targetHandle = null } = edge;
|
||||
|
||||
const sourceKey = `${source}-source-${sourceHandle}`;
|
||||
const targetKey = `${target}-target-${targetHandle}`;
|
||||
|
||||
const prevSource = connectionLookup.get(sourceKey) || new Map();
|
||||
const prevTarget = connectionLookup.get(targetKey) || new Map();
|
||||
const connection = { source, target, sourceHandle, targetHandle };
|
||||
|
||||
edgeLookup.set(edge.id, edge);
|
||||
connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection));
|
||||
connectionLookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user