Merge pull request #4105 from xyflow/refactor/internal-nodes
React Flow 12: separate user nodes and internal nodes
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { memo, useEffect, useRef, type MouseEvent, useCallback, CSSProperties } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system';
|
||||
import { getInternalNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system';
|
||||
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import { Panel } from '../../components/Panel';
|
||||
@@ -26,7 +26,9 @@ const selector = (s: ReactFlowState) => {
|
||||
return {
|
||||
viewBB,
|
||||
boundingRect:
|
||||
s.nodes.length > 0 ? getBoundsOfRects(getNodesBounds(s.nodes, { nodeOrigin: s.nodeOrigin }), viewBB) : viewBB,
|
||||
s.nodeLookup.size > 0
|
||||
? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup, { nodeOrigin: s.nodeOrigin }), viewBB)
|
||||
: viewBB,
|
||||
rfId: s.rfId,
|
||||
nodeOrigin: s.nodeOrigin,
|
||||
panZoom: s.panZoom,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { MiniMapNode } from './MiniMapNode';
|
||||
import type { ReactFlowState, Node } from '../../types';
|
||||
import type { ReactFlowState, Node, InternalNode } from '../../types';
|
||||
import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types';
|
||||
|
||||
declare const window: any;
|
||||
@@ -85,7 +85,7 @@ function NodeComponentWrapperInner<NodeType extends Node>({
|
||||
shapeRendering: string;
|
||||
}) {
|
||||
const { node, x, y } = useStore((s) => {
|
||||
const node = s.nodeLookup.get(id) as NodeType;
|
||||
const node = s.nodeLookup.get(id) as InternalNode<NodeType>;
|
||||
const { x, y } = getNodePositionWithOrigin(node, node?.origin || nodeOrigin).positionAbsolute;
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,12 +5,14 @@ import {
|
||||
ResizeControlVariant,
|
||||
type XYResizerInstance,
|
||||
type XYResizerChange,
|
||||
XYResizerChildChange,
|
||||
type XYResizerChildChange,
|
||||
type NodeChange,
|
||||
type NodeDimensionChange,
|
||||
type NodePositionChange,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStoreApi } from '../../hooks/useStore';
|
||||
import { useNodeId } from '../../contexts/NodeIdContext';
|
||||
import type { NodeChange, NodeDimensionChange, NodePositionChange } from '../../types';
|
||||
import type { ResizeControlProps, ResizeControlLineProps } from './types';
|
||||
|
||||
function ResizeControl({
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { useCallback, CSSProperties } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getNodesBounds, Rect, Position, internalsSymbol, getNodeToolbarTransform } from '@xyflow/system';
|
||||
import { Rect, Position, getNodeToolbarTransform, getNodesBounds } from '@xyflow/system';
|
||||
|
||||
import { Node, ReactFlowState } from '../../types';
|
||||
import { InternalNode, ReactFlowState } from '../../types';
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { useNodeId } from '../../contexts/NodeIdContext';
|
||||
import { NodeToolbarPortal } from './NodeToolbarPortal';
|
||||
import type { NodeToolbarProps } from './types';
|
||||
|
||||
const nodeEqualityFn = (a?: Node, b?: Node) =>
|
||||
a?.computed?.positionAbsolute?.x !== b?.computed?.positionAbsolute?.x ||
|
||||
a?.computed?.positionAbsolute?.y !== b?.computed?.positionAbsolute?.y ||
|
||||
a?.computed?.width !== b?.computed?.width ||
|
||||
a?.computed?.height !== b?.computed?.height ||
|
||||
const nodeEqualityFn = (a?: InternalNode, b?: InternalNode) =>
|
||||
a?.internals.positionAbsolute.x !== b?.internals.positionAbsolute.x ||
|
||||
a?.internals.positionAbsolute.y !== b?.internals.positionAbsolute.y ||
|
||||
a?.measured.width !== b?.measured.width ||
|
||||
a?.measured.height !== b?.measured.height ||
|
||||
a?.selected !== b?.selected ||
|
||||
a?.[internalsSymbol]?.z !== b?.[internalsSymbol]?.z;
|
||||
a?.internals.z !== b?.internals.z;
|
||||
|
||||
const nodesEqualityFn = (a: Node[], b: Node[]) => {
|
||||
const nodesEqualityFn = (a: InternalNode[], b: InternalNode[]) => {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
@@ -49,10 +49,10 @@ export function NodeToolbar({
|
||||
const contextNodeId = useNodeId();
|
||||
|
||||
const nodesSelector = useCallback(
|
||||
(state: ReactFlowState): Node[] => {
|
||||
(state: ReactFlowState): InternalNode[] => {
|
||||
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || ''];
|
||||
|
||||
return nodeIds.reduce<Node[]>((acc, id) => {
|
||||
return nodeIds.reduce<InternalNode[]>((acc, id) => {
|
||||
const node = state.nodeLookup.get(id);
|
||||
if (node) {
|
||||
acc.push(node);
|
||||
@@ -74,7 +74,7 @@ export function NodeToolbar({
|
||||
}
|
||||
|
||||
const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin });
|
||||
const zIndex: number = Math.max(...nodes.map((node) => (node[internalsSymbol]?.z || 1) + 1));
|
||||
const zIndex: number = Math.max(...nodes.map((node) => (node.internals?.z || 1) + 1));
|
||||
|
||||
const wrapperStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
|
||||
@@ -2,7 +2,6 @@ import { CSSProperties, useCallback } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import cc from 'classcat';
|
||||
import {
|
||||
internalsSymbol,
|
||||
Position,
|
||||
ConnectionLineType,
|
||||
ConnectionMode,
|
||||
@@ -53,7 +52,7 @@ const ConnectionLine = ({
|
||||
),
|
||||
shallow
|
||||
);
|
||||
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
|
||||
const fromHandleBounds = fromNode?.internals?.handleBounds;
|
||||
let handleBounds = fromHandleBounds?.[handleType];
|
||||
|
||||
if (connectionMode === ConnectionMode.Loose) {
|
||||
@@ -65,10 +64,10 @@ const ConnectionLine = ({
|
||||
}
|
||||
|
||||
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
|
||||
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.computed?.width ?? 0) / 2;
|
||||
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.computed?.height ?? 0;
|
||||
const fromX = (fromNode.computed?.positionAbsolute?.x ?? 0) + fromHandleX;
|
||||
const fromY = (fromNode.computed?.positionAbsolute?.y ?? 0) + fromHandleY;
|
||||
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.measured.width ?? 0) / 2;
|
||||
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.measured.height ?? 0;
|
||||
const fromX = (fromNode.internals.positionAbsolute.x ?? 0) + fromHandleX;
|
||||
const fromY = (fromNode.internals.positionAbsolute.y ?? 0) + fromHandleY;
|
||||
const fromPosition = fromHandle?.position;
|
||||
const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
||||
onConnectStart,
|
||||
onConnectEnd,
|
||||
cancelConnection,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
rfId: flowId,
|
||||
panBy,
|
||||
updateConnection,
|
||||
@@ -82,7 +82,7 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
||||
domNode,
|
||||
handleId,
|
||||
nodeId,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
isTarget,
|
||||
edgeUpdaterType: handleType,
|
||||
lib,
|
||||
|
||||
@@ -126,7 +126,7 @@ function HandleComponent(
|
||||
connectionMode: currentStore.connectionMode,
|
||||
connectionRadius: currentStore.connectionRadius,
|
||||
domNode: currentStore.domNode,
|
||||
nodes: currentStore.nodes,
|
||||
nodeLookup: currentStore.nodeLookup,
|
||||
lib: currentStore.lib,
|
||||
isTarget,
|
||||
handleId,
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
errorMessages,
|
||||
getNodeDimensions,
|
||||
getPositionWithOrigin,
|
||||
internalsSymbol,
|
||||
isInputDOMNode,
|
||||
nodeHasDimensions,
|
||||
} from '@xyflow/system';
|
||||
@@ -19,7 +18,7 @@ import { useDrag } from '../../hooks/useDrag';
|
||||
import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
|
||||
import { handleNodeClick } from '../Nodes/utils';
|
||||
import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils';
|
||||
import type { Node, NodeWrapperProps } from '../../types';
|
||||
import type { InternalNode, Node, NodeWrapperProps } from '../../types';
|
||||
|
||||
export function NodeWrapper<NodeType extends Node>({
|
||||
id,
|
||||
@@ -44,11 +43,11 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
onError,
|
||||
}: NodeWrapperProps<NodeType>) {
|
||||
const { node, positionAbsoluteX, positionAbsoluteY, zIndex, isParent } = useStore((s) => {
|
||||
const node = s.nodeLookup.get(id)! as NodeType;
|
||||
const node = s.nodeLookup.get(id)! as InternalNode<NodeType>;
|
||||
|
||||
const positionAbsolute = nodeExtent
|
||||
? clampPosition(node.computed?.positionAbsolute, nodeExtent)
|
||||
: node.computed?.positionAbsolute || { x: 0, y: 0 };
|
||||
? clampPosition(node.internals.positionAbsolute, nodeExtent)
|
||||
: node.internals.positionAbsolute || { x: 0, y: 0 };
|
||||
|
||||
return {
|
||||
node,
|
||||
@@ -56,8 +55,8 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
// so we we need to force a re-render when some change
|
||||
positionAbsoluteX: positionAbsolute.x,
|
||||
positionAbsoluteY: positionAbsolute.y,
|
||||
zIndex: node[internalsSymbol]?.z ?? 0,
|
||||
isParent: !!node[internalsSymbol]?.isParent,
|
||||
zIndex: node.internals.z,
|
||||
isParent: node.internals.isParent,
|
||||
};
|
||||
}, shallow);
|
||||
|
||||
@@ -84,14 +83,16 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
const nodeDimensions = getNodeDimensions(node);
|
||||
const inlineDimensions = getNodeInlineStyleDimensions(node);
|
||||
const initialized = nodeHasDimensions(node);
|
||||
const hasHandleBounds = !!node[internalsSymbol]?.handleBounds;
|
||||
const hasHandleBounds = !!node.internals.handleBounds;
|
||||
|
||||
const moveSelectedNodes = useMoveSelectedNodes();
|
||||
|
||||
useEffect(() => {
|
||||
const currNode = nodeRef.current;
|
||||
|
||||
return () => {
|
||||
if (nodeRef.current) {
|
||||
resizeObserver?.unobserve(nodeRef.current);
|
||||
if (currNode) {
|
||||
resizeObserver?.unobserve(currNode);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
@@ -123,7 +124,7 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
if (targetPosChanged) {
|
||||
prevTargetPosition.current = node.targetPosition;
|
||||
}
|
||||
store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]]));
|
||||
store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, force: true }]]));
|
||||
}
|
||||
}, [id, nodeType, node.sourcePosition, node.targetPosition]);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { InputNode } from '../Nodes/InputNode';
|
||||
import { DefaultNode } from '../Nodes/DefaultNode';
|
||||
import { GroupNode } from '../Nodes/GroupNode';
|
||||
import { OutputNode } from '../Nodes/OutputNode';
|
||||
import type { Node, NodeTypes } from '../../types';
|
||||
import type { InternalNode, Node, NodeTypes } from '../../types';
|
||||
|
||||
export const arrowKeyDiffs: Record<string, XYPosition> = {
|
||||
ArrowUp: { x: 0, y: -1 },
|
||||
@@ -21,12 +21,12 @@ export const builtinNodeTypes: NodeTypes = {
|
||||
};
|
||||
|
||||
export function getNodeInlineStyleDimensions<NodeType extends Node = Node>(
|
||||
node: NodeType
|
||||
node: InternalNode<NodeType>
|
||||
): {
|
||||
width: number | string | undefined;
|
||||
height: number | string | undefined;
|
||||
} {
|
||||
if (!node.computed) {
|
||||
if (node.internals.handleBounds === undefined) {
|
||||
return {
|
||||
width: node.width ?? node.initialWidth ?? node.style?.width,
|
||||
height: node.height ?? node.initialHeight ?? node.style?.height,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import { useDrag } from '../../hooks/useDrag';
|
||||
import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
|
||||
import { arrowKeyDiffs } from '../NodeWrapper/utils';
|
||||
import type { Node, ReactFlowState } from '../../types';
|
||||
import type { InternalNode, Node, ReactFlowState } from '../../types';
|
||||
|
||||
export type NodesSelectionProps<NodeType> = {
|
||||
onSelectionContextMenu?: (event: MouseEvent, nodes: NodeType[]) => void;
|
||||
@@ -20,7 +20,13 @@ export type NodesSelectionProps<NodeType> = {
|
||||
};
|
||||
|
||||
const selector = (s: ReactFlowState) => {
|
||||
const selectedNodes = s.nodes.filter((n) => n.selected);
|
||||
const selectedNodes: InternalNode[] = [];
|
||||
for (const [, node] of s.nodeLookup) {
|
||||
if (node.selected) {
|
||||
selectedNodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
const { width, height, x, y } = getNodesBounds(selectedNodes, { nodeOrigin: s.nodeOrigin });
|
||||
|
||||
return {
|
||||
|
||||
@@ -15,7 +15,7 @@ type SelectionListenerProps = {
|
||||
};
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
selectedNodes: s.nodes.filter((n) => n.selected),
|
||||
selectedNodes: Array.from(s.nodeLookup.values()).filter((n) => n.selected),
|
||||
selectedEdges: s.edges.filter((e) => e.selected),
|
||||
});
|
||||
|
||||
|
||||
@@ -138,7 +138,6 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
|
||||
|
||||
if (fieldValue === previousFieldValue) continue;
|
||||
if (typeof props[fieldName] === 'undefined') continue;
|
||||
|
||||
// Custom handling with dedicated setters for some fields
|
||||
if (fieldName === 'nodes') setNodes(fieldValue as Node[]);
|
||||
else if (fieldName === 'edges') setEdges(fieldValue as Edge[]);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { ReactFlowState } from '../../types';
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { NodeDimensionUpdate } from '@xyflow/system';
|
||||
|
||||
const selector = (s: ReactFlowState) => s.updateNodeDimensions;
|
||||
|
||||
@@ -15,14 +16,13 @@ export function useResizeObserver() {
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
const updates = new Map();
|
||||
const updates = new Map<string, NodeDimensionUpdate>();
|
||||
|
||||
entries.forEach((entry: ResizeObserverEntry) => {
|
||||
const id = entry.target.getAttribute('data-id') as string;
|
||||
updates.set(id, {
|
||||
id,
|
||||
nodeElement: entry.target as HTMLDivElement,
|
||||
forceUpdate: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
import { useRef, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import cc from 'classcat';
|
||||
import { getNodesInside, getEventPosition, SelectionMode } from '@xyflow/system';
|
||||
import { getNodesInside, getEventPosition, SelectionMode, type NodeChange, type EdgeChange } from '@xyflow/system';
|
||||
|
||||
import { UserSelection } from '../../components/UserSelection';
|
||||
import { containerStyle } from '../../styles/utils';
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import { getSelectionChanges } from '../../utils';
|
||||
import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types';
|
||||
import type { ReactFlowProps, ReactFlowState } from '../../types';
|
||||
|
||||
type PaneProps = {
|
||||
isSelecting: boolean;
|
||||
@@ -128,7 +128,7 @@ export function Pane({
|
||||
};
|
||||
|
||||
const onMouseMove = (event: ReactMouseEvent): void => {
|
||||
const { userSelectionRect, edges, transform, nodeOrigin, nodes, triggerNodeChanges, triggerEdgeChanges } =
|
||||
const { userSelectionRect, edgeLookup, transform, nodeOrigin, nodeLookup, triggerNodeChanges, triggerEdgeChanges } =
|
||||
store.getState();
|
||||
if (!isSelecting || !containerBounds.current || !userSelectionRect) {
|
||||
return;
|
||||
@@ -149,7 +149,7 @@ export function Pane({
|
||||
};
|
||||
|
||||
const selectedNodes = getNodesInside(
|
||||
nodes,
|
||||
nodeLookup,
|
||||
nextUserSelectRect,
|
||||
transform,
|
||||
selectionMode === SelectionMode.Partial,
|
||||
@@ -163,22 +163,22 @@ export function Pane({
|
||||
for (const selectedNode of selectedNodes) {
|
||||
selectedNodeIds.add(selectedNode.id);
|
||||
|
||||
for (const edge of edges) {
|
||||
for (const [edgeId, edge] of edgeLookup) {
|
||||
if (edge.source === selectedNode.id || edge.target === selectedNode.id) {
|
||||
selectedEdgeIds.add(edge.id);
|
||||
selectedEdgeIds.add(edgeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSelectedNodesCount.current !== selectedNodeIds.size) {
|
||||
prevSelectedNodesCount.current = selectedNodeIds.size;
|
||||
const changes = getSelectionChanges(nodes, selectedNodeIds, true) as NodeChange[];
|
||||
const changes = getSelectionChanges(nodeLookup, selectedNodeIds, true) as NodeChange[];
|
||||
triggerNodeChanges(changes);
|
||||
}
|
||||
|
||||
if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) {
|
||||
prevSelectedEdgesCount.current = selectedEdgeIds.size;
|
||||
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
|
||||
const changes = getSelectionChanges(edgeLookup, selectedEdgeIds) as EdgeChange[];
|
||||
triggerEdgeChanges(changes);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useCallback } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useStore } from './useStore';
|
||||
import type { InternalNode, Node } from '../types';
|
||||
|
||||
/**
|
||||
* Hook for getting an internal node by id
|
||||
*
|
||||
* @public
|
||||
* @param id - id of the node
|
||||
* @returns array with visible node ids
|
||||
*/
|
||||
export function useInternalNode<NodeType extends Node = Node>(id: string): InternalNode<NodeType> | undefined {
|
||||
const node = useStore(
|
||||
useCallback((s) => s.nodeLookup.get(id) as InternalNode<NodeType> | undefined, [id]),
|
||||
shallow
|
||||
);
|
||||
|
||||
return node;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { calculateNodePosition, snapPosition, type XYPosition } from '@xyflow/system';
|
||||
|
||||
import { Node } from '../types';
|
||||
import { type Node } from '../types';
|
||||
import { useStoreApi } from './useStore';
|
||||
|
||||
const selectedAndDraggable = (nodesDraggable: boolean) => (n: Node) =>
|
||||
@@ -17,18 +17,11 @@ export function useMoveSelectedNodes() {
|
||||
const store = useStoreApi();
|
||||
|
||||
const moveSelectedNodes = useCallback((params: { direction: XYPosition; factor: number }) => {
|
||||
const {
|
||||
nodeExtent,
|
||||
nodes,
|
||||
snapToGrid,
|
||||
snapGrid,
|
||||
nodesDraggable,
|
||||
onError,
|
||||
updateNodePositions,
|
||||
nodeLookup,
|
||||
nodeOrigin,
|
||||
} = store.getState();
|
||||
const selectedNodes = nodes.filter(selectedAndDraggable(nodesDraggable));
|
||||
const { nodeExtent, snapToGrid, snapGrid, nodesDraggable, onError, updateNodePositions, nodeLookup, nodeOrigin } =
|
||||
store.getState();
|
||||
const nodeUpdates = [];
|
||||
const isSelected = selectedAndDraggable(nodesDraggable);
|
||||
|
||||
// by default a node moves 5px on each key press
|
||||
// if snap grid is enabled, we use that for the velocity
|
||||
const xVelo = snapToGrid ? snapGrid[0] : 5;
|
||||
@@ -37,32 +30,34 @@ export function useMoveSelectedNodes() {
|
||||
const xDiff = params.direction.x * xVelo * params.factor;
|
||||
const yDiff = params.direction.y * yVelo * params.factor;
|
||||
|
||||
const nodeUpdates = selectedNodes.map((node) => {
|
||||
if (node.computed?.positionAbsolute) {
|
||||
let nextPosition = {
|
||||
x: node.computed.positionAbsolute.x + xDiff,
|
||||
y: node.computed.positionAbsolute.y + yDiff,
|
||||
};
|
||||
|
||||
if (snapToGrid) {
|
||||
nextPosition = snapPosition(nextPosition, snapGrid);
|
||||
}
|
||||
|
||||
const { position, positionAbsolute } = calculateNodePosition({
|
||||
nodeId: node.id,
|
||||
nextPosition,
|
||||
nodeLookup,
|
||||
nodeExtent,
|
||||
nodeOrigin,
|
||||
onError,
|
||||
});
|
||||
|
||||
node.position = position;
|
||||
node.computed.positionAbsolute = positionAbsolute;
|
||||
for (const [, node] of nodeLookup) {
|
||||
if (!isSelected(node)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
let nextPosition = {
|
||||
x: node.internals.positionAbsolute.x + xDiff,
|
||||
y: node.internals.positionAbsolute.y + yDiff,
|
||||
};
|
||||
|
||||
if (snapToGrid) {
|
||||
nextPosition = snapPosition(nextPosition, snapGrid);
|
||||
}
|
||||
|
||||
const { position, positionAbsolute } = calculateNodePosition({
|
||||
nodeId: node.id,
|
||||
nextPosition,
|
||||
nodeLookup,
|
||||
nodeExtent,
|
||||
nodeOrigin,
|
||||
onError,
|
||||
});
|
||||
|
||||
node.position = position;
|
||||
node.internals.positionAbsolute = positionAbsolute;
|
||||
|
||||
nodeUpdates.push(node);
|
||||
}
|
||||
|
||||
updateNodePositions(nodeUpdates);
|
||||
}, []);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { internalsSymbol } from '@xyflow/system';
|
||||
|
||||
import { useStore } from './useStore';
|
||||
import type { ReactFlowState } from '../types';
|
||||
|
||||
@@ -8,13 +6,13 @@ export type UseNodesInitializedOptions = {
|
||||
};
|
||||
|
||||
const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) => {
|
||||
if (s.nodes.length === 0) {
|
||||
if (s.nodeLookup.size === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const node of s.nodes) {
|
||||
for (const [, node] of s.nodeLookup) {
|
||||
if (options.includeHiddenNodes || !node.hidden) {
|
||||
if (node[internalsSymbol]?.handleBounds === undefined) {
|
||||
if (node.internals.handleBounds === undefined) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
getElementsToRemove,
|
||||
getOverlappingArea,
|
||||
isRectObject,
|
||||
nodeHasDimensions,
|
||||
nodeToRect,
|
||||
type Rect,
|
||||
} from '@xyflow/system';
|
||||
import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system';
|
||||
|
||||
import useViewportHelper from './useViewportHelper';
|
||||
import { useStoreApi } from './useStore';
|
||||
import type { ReactFlowInstance, Instance, Node, Edge } from '../types';
|
||||
import type { ReactFlowInstance, Instance, Node, Edge, InternalNode } from '../types';
|
||||
import { getElementsDiffChanges, isNode } from '../utils';
|
||||
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
|
||||
|
||||
@@ -27,13 +20,20 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
const viewportHelper = useViewportHelper();
|
||||
const store = useStoreApi();
|
||||
|
||||
const getNodes = useCallback<Instance.GetNodes<NodeType>>(() => {
|
||||
return store.getState().nodes.map((n) => ({ ...n })) as NodeType[];
|
||||
}, []);
|
||||
const getNodes = useCallback<Instance.GetNodes<NodeType>>(
|
||||
() => store.getState().nodes.map((n) => ({ ...n })) as NodeType[],
|
||||
[]
|
||||
);
|
||||
|
||||
const getNode = useCallback<Instance.GetNode<NodeType>>((id) => {
|
||||
return store.getState().nodeLookup.get(id) as NodeType;
|
||||
}, []);
|
||||
const getInternalNode = useCallback<Instance.GetInternalNode<NodeType>>(
|
||||
(id) => store.getState().nodeLookup.get(id) as InternalNode<NodeType>,
|
||||
[]
|
||||
);
|
||||
|
||||
const getNode = useCallback<Instance.GetNode<NodeType>>(
|
||||
(id) => getInternalNode(id)?.internals.userNode as NodeType,
|
||||
[getInternalNode]
|
||||
);
|
||||
|
||||
const getEdges = useCallback<Instance.GetEdges<EdgeType>>(() => {
|
||||
const { edges = [] } = store.getState();
|
||||
@@ -223,13 +223,9 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
[]
|
||||
);
|
||||
|
||||
const getNodeRect = useCallback((nodeOrRect: NodeType | { id: NodeType['id'] }): Rect | null => {
|
||||
const node =
|
||||
isNode(nodeOrRect) && nodeHasDimensions(nodeOrRect)
|
||||
? nodeOrRect
|
||||
: (store.getState().nodeLookup.get(nodeOrRect.id) as NodeType);
|
||||
|
||||
return node ? nodeToRect(node) : null;
|
||||
const getNodeRect = useCallback(({ id }: { id: string }): Rect | null => {
|
||||
const internalNode = store.getState().nodeLookup.get(id);
|
||||
return internalNode ? nodeToRect(internalNode) : null;
|
||||
}, []);
|
||||
|
||||
const getIntersectingNodes = useCallback<Instance.GetIntersectingNodes<NodeType>>(
|
||||
@@ -242,7 +238,9 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
}
|
||||
|
||||
return (nodes || store.getState().nodes).filter((n) => {
|
||||
if (!isRect && (n.id === nodeOrRect!.id || !n.computed?.positionAbsolute)) {
|
||||
const internalNode = store.getState().nodeLookup.get(n.id);
|
||||
|
||||
if (internalNode && !isRect && (n.id === nodeOrRect!.id || !internalNode.internals.positionAbsolute)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -308,6 +306,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
...viewportHelper,
|
||||
getNodes,
|
||||
getNode,
|
||||
getInternalNode,
|
||||
getEdges,
|
||||
getEdge,
|
||||
setNodes,
|
||||
@@ -325,6 +324,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
viewportHelper,
|
||||
getNodes,
|
||||
getNode,
|
||||
getInternalNode,
|
||||
getEdges,
|
||||
getEdge,
|
||||
setNodes,
|
||||
|
||||
@@ -21,7 +21,7 @@ export function useUpdateNodeInternals(): UpdateNodeInternals {
|
||||
const nodeElement = domNode?.querySelector(`.react-flow__node[data-id="${updateId}"]`) as HTMLDivElement;
|
||||
|
||||
if (nodeElement) {
|
||||
updates.set(updateId, { id: updateId, nodeElement, forceUpdate: true });
|
||||
updates.set(updateId, { id: updateId, nodeElement, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -48,12 +48,12 @@ const useViewportHelper = (): ViewportHelperFunctions => {
|
||||
return { x, y, zoom };
|
||||
},
|
||||
fitView: (options) => {
|
||||
const { nodes, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState();
|
||||
const { nodeLookup, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState();
|
||||
|
||||
return panZoom
|
||||
? fitView(
|
||||
{
|
||||
nodes,
|
||||
nodeLookup,
|
||||
width,
|
||||
height,
|
||||
nodeOrigin,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { getNodesInside } from '@xyflow/system';
|
||||
import { useCallback } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getNodesInside } from '@xyflow/system';
|
||||
|
||||
import { useStore } from './useStore';
|
||||
import type { Node, ReactFlowState } from '../types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
|
||||
return onlyRenderVisible
|
||||
? getNodesInside<Node>(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
|
||||
? getNodesInside<Node>(s.nodeLookup, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
|
||||
(node) => node.id
|
||||
)
|
||||
: Array.from(s.nodeLookup.keys());
|
||||
|
||||
@@ -26,9 +26,10 @@ export { useNodesInitialized, type UseNodesInitializedOptions } from './hooks/us
|
||||
export { useHandleConnections } from './hooks/useHandleConnections';
|
||||
export { useNodesData } from './hooks/useNodesData';
|
||||
export { useConnection } from './hooks/useConnection';
|
||||
export { useInternalNode } from './hooks/useInternalNode';
|
||||
export { useNodeId } from './contexts/NodeIdContext';
|
||||
|
||||
export { applyNodeChanges, applyEdgeChanges, handleParentExpand } from './utils/changes';
|
||||
export { applyNodeChanges, applyEdgeChanges } from './utils/changes';
|
||||
export { isNode, isEdge } from './utils/general';
|
||||
|
||||
export * from './additional-components';
|
||||
@@ -103,5 +104,4 @@ export {
|
||||
addEdge,
|
||||
updateEdge,
|
||||
getConnectedEdges,
|
||||
internalsSymbol,
|
||||
} from '@xyflow/system';
|
||||
|
||||
@@ -2,27 +2,20 @@ import { createWithEqualityFn } from 'zustand/traditional';
|
||||
import {
|
||||
clampPosition,
|
||||
fitView as fitViewSystem,
|
||||
adoptUserProvidedNodes,
|
||||
adoptUserNodes,
|
||||
updateAbsolutePositions,
|
||||
panBy as panBySystem,
|
||||
Dimensions,
|
||||
updateNodeDimensions as updateNodeDimensionsSystem,
|
||||
updateConnectionLookup,
|
||||
handleParentExpand,
|
||||
NodeChange,
|
||||
EdgeSelectionChange,
|
||||
NodeSelectionChange,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
|
||||
import getInitialState from './initialState';
|
||||
import type {
|
||||
ReactFlowState,
|
||||
Node,
|
||||
Edge,
|
||||
NodeDimensionChange,
|
||||
EdgeSelectionChange,
|
||||
NodeSelectionChange,
|
||||
NodePositionChange,
|
||||
UnselectNodesAndEdgesParams,
|
||||
FitViewOptions,
|
||||
} from '../types';
|
||||
import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams, FitViewOptions, InternalNode } from '../types';
|
||||
|
||||
const createRFStore = ({
|
||||
nodes,
|
||||
@@ -52,9 +45,9 @@ const createRFStore = ({
|
||||
//
|
||||
// When this happens, we take the note objects passed by the user and extend them with fields
|
||||
// relevant for internal React Flow operations.
|
||||
const nodesWithInternalData = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
|
||||
adoptUserNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
|
||||
|
||||
set({ nodes: nodesWithInternalData });
|
||||
set({ nodes });
|
||||
},
|
||||
setEdges: (edges: Edge[]) => {
|
||||
const { connectionLookup, edgeLookup } = get();
|
||||
@@ -82,7 +75,6 @@ const createRFStore = ({
|
||||
const {
|
||||
onNodesChange,
|
||||
fitView,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
fitViewOnInit,
|
||||
fitViewDone,
|
||||
@@ -91,35 +83,21 @@ const createRFStore = ({
|
||||
nodeOrigin,
|
||||
debug,
|
||||
} = get();
|
||||
const changes: NodeDimensionChange[] = [];
|
||||
|
||||
const updatedNodes = updateNodeDimensionsSystem(
|
||||
updates,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
domNode,
|
||||
nodeOrigin,
|
||||
(id: string, dimensions: Dimensions) => {
|
||||
changes.push({
|
||||
id: id,
|
||||
type: 'dimensions',
|
||||
dimensions,
|
||||
});
|
||||
}
|
||||
);
|
||||
const changes = updateNodeDimensionsSystem(updates, nodeLookup, domNode, nodeOrigin);
|
||||
|
||||
if (!updatedNodes) {
|
||||
if (changes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextNodes = updateAbsolutePositions(updatedNodes, nodeLookup, nodeOrigin);
|
||||
updateAbsolutePositions(nodeLookup, { nodeOrigin });
|
||||
|
||||
// we call fitView once initially after all dimensions are set
|
||||
let nextFitViewDone = fitViewDone;
|
||||
if (!fitViewDone && fitViewOnInit) {
|
||||
nextFitViewDone = fitView(nextNodes, {
|
||||
nextFitViewDone = fitView({
|
||||
...fitViewOnInitOptions,
|
||||
nodes: fitViewOnInitOptions?.nodes || nextNodes,
|
||||
nodes: fitViewOnInitOptions?.nodes,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -128,7 +106,7 @@ const createRFStore = ({
|
||||
// has not provided an onNodesChange handler.
|
||||
// Nodes are only rendered if they have a width and height
|
||||
// attribute which they get from this handler.
|
||||
set({ nodes: nextNodes, fitViewDone: nextFitViewDone });
|
||||
set({ fitViewDone: nextFitViewDone });
|
||||
|
||||
if (changes?.length > 0) {
|
||||
if (debug) {
|
||||
@@ -138,18 +116,41 @@ const createRFStore = ({
|
||||
}
|
||||
},
|
||||
updateNodePositions: (nodeDragItems, dragging = false) => {
|
||||
const changes = nodeDragItems.map((node) => {
|
||||
const change: NodePositionChange = {
|
||||
const { nodeLookup } = get();
|
||||
const triggerChangeNodes: InternalNode[] = [];
|
||||
|
||||
const changes: NodeChange[] = nodeDragItems.map((node) => {
|
||||
// @todo add expandParent to drag item so that we can get rid of the look up here
|
||||
const internalNode = nodeLookup.get(node.id);
|
||||
const change: NodeChange = {
|
||||
id: node.id,
|
||||
type: 'position',
|
||||
position: node.position,
|
||||
positionAbsolute: node.computed?.positionAbsolute,
|
||||
dragging,
|
||||
};
|
||||
|
||||
if (internalNode?.expandParent && change.position) {
|
||||
triggerChangeNodes.push({
|
||||
...internalNode,
|
||||
position: change.position,
|
||||
internals: {
|
||||
...internalNode.internals,
|
||||
positionAbsolute: node.internals.positionAbsolute,
|
||||
},
|
||||
});
|
||||
|
||||
change.position.x = Math.max(0, change.position.x);
|
||||
change.position.y = Math.max(0, change.position.y);
|
||||
}
|
||||
|
||||
return change;
|
||||
});
|
||||
|
||||
if (triggerChangeNodes.length > 0) {
|
||||
const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup);
|
||||
changes.push(...parentExpandChanges);
|
||||
}
|
||||
|
||||
get().triggerNodeChanges(changes);
|
||||
},
|
||||
triggerNodeChanges: (changes) => {
|
||||
@@ -185,7 +186,7 @@ const createRFStore = ({
|
||||
}
|
||||
},
|
||||
addSelectedNodes: (selectedNodeIds) => {
|
||||
const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get();
|
||||
const { multiSelectionActive, edgeLookup, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get();
|
||||
|
||||
if (multiSelectionActive) {
|
||||
const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true));
|
||||
@@ -193,11 +194,11 @@ const createRFStore = ({
|
||||
return;
|
||||
}
|
||||
|
||||
triggerNodeChanges(getSelectionChanges(nodes, new Set([...selectedNodeIds]), true));
|
||||
triggerEdgeChanges(getSelectionChanges(edges));
|
||||
triggerNodeChanges(getSelectionChanges(nodeLookup, new Set([...selectedNodeIds]), true));
|
||||
triggerEdgeChanges(getSelectionChanges(edgeLookup));
|
||||
},
|
||||
addSelectedEdges: (selectedEdgeIds) => {
|
||||
const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get();
|
||||
const { multiSelectionActive, edgeLookup, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get();
|
||||
|
||||
if (multiSelectionActive) {
|
||||
const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true));
|
||||
@@ -205,8 +206,8 @@ const createRFStore = ({
|
||||
return;
|
||||
}
|
||||
|
||||
triggerEdgeChanges(getSelectionChanges(edges, new Set([...selectedEdgeIds])));
|
||||
triggerNodeChanges(getSelectionChanges(nodes, new Set(), true));
|
||||
triggerEdgeChanges(getSelectionChanges(edgeLookup, new Set([...selectedEdgeIds])));
|
||||
triggerNodeChanges(getSelectionChanges(nodeLookup, new Set(), true));
|
||||
},
|
||||
unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => {
|
||||
const { edges: storeEdges, nodes: storeNodes, triggerNodeChanges, triggerEdgeChanges } = get();
|
||||
@@ -255,29 +256,30 @@ const createRFStore = ({
|
||||
triggerEdgeChanges(edgeChanges);
|
||||
},
|
||||
setNodeExtent: (nodeExtent) => {
|
||||
const { nodes } = get();
|
||||
const { nodeLookup } = get();
|
||||
|
||||
for (const [, node] of nodeLookup) {
|
||||
const positionAbsolute = clampPosition(node.position, nodeExtent);
|
||||
|
||||
nodeLookup.set(node.id, {
|
||||
...node,
|
||||
internals: {
|
||||
...node.internals,
|
||||
positionAbsolute,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
set({
|
||||
nodeExtent,
|
||||
nodes: nodes.map((node) => {
|
||||
const positionAbsolute = clampPosition(node.position, nodeExtent);
|
||||
|
||||
return {
|
||||
...node,
|
||||
computed: {
|
||||
...node.computed,
|
||||
positionAbsolute,
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
},
|
||||
panBy: (delta): boolean => {
|
||||
const { transform, width, height, panZoom, translateExtent } = get();
|
||||
return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
|
||||
},
|
||||
fitView: (nodes: Node[], options?: FitViewOptions): boolean => {
|
||||
const { panZoom, width, height, minZoom, maxZoom, nodeOrigin } = get();
|
||||
fitView: (options?: FitViewOptions): boolean => {
|
||||
const { panZoom, width, height, minZoom, maxZoom, nodeOrigin, nodeLookup } = get();
|
||||
|
||||
if (!panZoom) {
|
||||
return false;
|
||||
@@ -285,7 +287,7 @@ const createRFStore = ({
|
||||
|
||||
return fitViewSystem(
|
||||
{
|
||||
nodes,
|
||||
nodeLookup,
|
||||
width,
|
||||
height,
|
||||
panZoom,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
infiniteExtent,
|
||||
ConnectionMode,
|
||||
adoptUserProvidedNodes,
|
||||
adoptUserNodes,
|
||||
getNodesBounds,
|
||||
getViewportForBounds,
|
||||
Transform,
|
||||
@@ -35,7 +35,7 @@ const getInitialState = ({
|
||||
const storeNodes = defaultNodes ?? nodes ?? [];
|
||||
|
||||
updateConnectionLookup(connectionLookup, edgeLookup, storeEdges);
|
||||
const nextNodes = adoptUserProvidedNodes(storeNodes, nodeLookup, {
|
||||
adoptUserNodes(storeNodes, nodeLookup, {
|
||||
nodeOrigin: [0, 0],
|
||||
elevateNodesOnSelect: false,
|
||||
});
|
||||
@@ -43,7 +43,7 @@ const getInitialState = ({
|
||||
let transform: Transform = [0, 0, 1];
|
||||
|
||||
if (fitView && width && height) {
|
||||
const nodesWithDimensions = nextNodes.filter(
|
||||
const nodesWithDimensions = storeNodes.filter(
|
||||
(node) => (node.width || node.initialWidth) && (node.height || node.initialHeight)
|
||||
);
|
||||
// @todo users nodeOrigin should be used here
|
||||
@@ -57,7 +57,7 @@ const getInitialState = ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
transform,
|
||||
nodes: nextNodes,
|
||||
nodes: storeNodes,
|
||||
nodeLookup,
|
||||
edges: storeEdges,
|
||||
edgeLookup,
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import type { XYPosition, Dimensions } from '@xyflow/system';
|
||||
|
||||
import type { Node, Edge } 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 Node = Node> = {
|
||||
item: NodeType;
|
||||
type: 'add';
|
||||
};
|
||||
|
||||
export type NodeReplaceChange<NodeType extends Node = Node> = {
|
||||
id: string;
|
||||
item: NodeType;
|
||||
type: 'replace';
|
||||
};
|
||||
|
||||
/**
|
||||
* Union type of all possible node changes.
|
||||
* @public
|
||||
*/
|
||||
export type NodeChange<NodeType extends Node = Node> =
|
||||
| NodeDimensionChange
|
||||
| NodePositionChange
|
||||
| NodeSelectionChange
|
||||
| NodeRemoveChange
|
||||
| NodeAddChange<NodeType>
|
||||
| NodeReplaceChange<NodeType>;
|
||||
|
||||
export type EdgeSelectionChange = NodeSelectionChange;
|
||||
export type EdgeRemoveChange = NodeRemoveChange;
|
||||
export type EdgeAddChange<EdgeType extends Edge = Edge> = {
|
||||
item: EdgeType;
|
||||
type: 'add';
|
||||
};
|
||||
|
||||
export type EdgeReplaceChange<EdgeType extends Edge = Edge> = {
|
||||
id: string;
|
||||
item: EdgeType;
|
||||
type: 'replace';
|
||||
};
|
||||
|
||||
export type EdgeChange<EdgeType extends Edge = Edge> =
|
||||
| EdgeSelectionChange
|
||||
| EdgeRemoveChange
|
||||
| EdgeAddChange<EdgeType>
|
||||
| EdgeReplaceChange<EdgeType>;
|
||||
@@ -12,9 +12,11 @@ import {
|
||||
XYPosition,
|
||||
OnBeforeDeleteBase,
|
||||
Connection,
|
||||
NodeChange,
|
||||
EdgeChange,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.';
|
||||
import type { Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.';
|
||||
|
||||
export type OnNodesChange<NodeType extends Node = Node> = (changes: NodeChange<NodeType>[]) => void;
|
||||
export type OnEdgesChange<EdgeType extends Edge = Edge> = (changes: EdgeChange<EdgeType>[]) => void;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './nodes';
|
||||
export * from './edges';
|
||||
export * from './changes';
|
||||
export * from './component-props';
|
||||
export * from './general';
|
||||
export * from './store';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
import type { Rect, Viewport } from '@xyflow/system';
|
||||
import type { Node, Edge, ViewportHelperFunctions } from '.';
|
||||
import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.';
|
||||
|
||||
export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
nodes: NodeType[];
|
||||
@@ -20,6 +20,7 @@ export namespace Instance {
|
||||
) => void;
|
||||
export type AddNodes<NodeType extends Node = Node> = (payload: NodeType[] | NodeType) => void;
|
||||
export type GetNode<NodeType extends Node = Node> = (id: string) => NodeType | undefined;
|
||||
export type GetInternalNode<NodeType extends Node = Node> = (id: string) => InternalNode<NodeType> | undefined;
|
||||
export type GetEdges<EdgeType extends Edge = Edge> = () => EdgeType[];
|
||||
export type SetEdges<EdgeType extends Edge = Edge> = (
|
||||
payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[])
|
||||
@@ -83,6 +84,13 @@ export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edg
|
||||
* @returns the node or undefined if no node was found
|
||||
*/
|
||||
getNode: Instance.GetNode<NodeType>;
|
||||
/**
|
||||
* Returns an internal node by id.
|
||||
*
|
||||
* @param id - the node id
|
||||
* @returns the internal node or undefined if no node was found
|
||||
*/
|
||||
getInternalNode: Instance.GetInternalNode<NodeType>;
|
||||
/**
|
||||
* Returns edges.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import type { CoordinateExtent, NodeBase, NodeOrigin, OnError, NodeProps as NodePropsBase } from '@xyflow/system';
|
||||
import type {
|
||||
CoordinateExtent,
|
||||
NodeBase,
|
||||
NodeOrigin,
|
||||
OnError,
|
||||
NodeProps as NodePropsBase,
|
||||
InternalNodeBase,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { NodeTypes } from './general';
|
||||
|
||||
@@ -17,6 +24,8 @@ export type Node<
|
||||
focusable?: boolean;
|
||||
};
|
||||
|
||||
export type InternalNode<NodeType extends Node = Node> = InternalNodeBase<NodeType>;
|
||||
|
||||
export type NodeMouseHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, node: NodeType) => void;
|
||||
export type SelectionDragHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, nodes: NodeType[]) => void;
|
||||
export type OnNodeDrag<NodeType extends Node = Node> = (
|
||||
|
||||
@@ -25,12 +25,13 @@ import {
|
||||
type EdgeLookup,
|
||||
type ConnectionLookup,
|
||||
type NodeLookup,
|
||||
NodeChange,
|
||||
EdgeChange,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
Edge,
|
||||
Node,
|
||||
NodeChange,
|
||||
OnNodesChange,
|
||||
OnEdgesChange,
|
||||
DefaultEdgeOptions,
|
||||
@@ -43,7 +44,7 @@ import type {
|
||||
OnNodeDrag,
|
||||
OnBeforeDelete,
|
||||
IsValidConnection,
|
||||
EdgeChange,
|
||||
InternalNode,
|
||||
} from '.';
|
||||
|
||||
export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
@@ -52,7 +53,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
|
||||
height: number;
|
||||
transform: Transform;
|
||||
nodes: NodeType[];
|
||||
nodeLookup: NodeLookup<NodeType>;
|
||||
nodeLookup: NodeLookup<InternalNode<NodeType>>;
|
||||
edges: Edge[];
|
||||
edgeLookup: EdgeLookup<EdgeType>;
|
||||
connectionLookup: ConnectionLookup;
|
||||
@@ -169,7 +170,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
|
||||
triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void;
|
||||
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
|
||||
panBy: PanBy;
|
||||
fitView: (nodes: NodeType[], options?: FitViewOptions) => boolean;
|
||||
fitView: (options?: FitViewOptions) => boolean;
|
||||
};
|
||||
|
||||
export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore<
|
||||
|
||||
@@ -1,51 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { EdgeLookup, NodeLookup } from '@xyflow/system';
|
||||
import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types';
|
||||
|
||||
export function handleParentExpand(updatedElements: any[], updateItem: any) {
|
||||
for (const [index, item] of updatedElements.entries()) {
|
||||
if (item.id === updateItem.parentNode) {
|
||||
const parent = { ...item };
|
||||
parent.computed ??= {};
|
||||
|
||||
const extendWidth = updateItem.position.x + updateItem.computed.width - parent.computed.width;
|
||||
const extendHeight = updateItem.position.y + updateItem.computed.height - parent.computed.height;
|
||||
|
||||
if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) {
|
||||
parent.width = parent.width ?? parent.computed.width;
|
||||
parent.height = parent.height ?? parent.computed.height;
|
||||
|
||||
if (extendWidth > 0) {
|
||||
parent.width += extendWidth;
|
||||
}
|
||||
|
||||
if (extendHeight > 0) {
|
||||
parent.height += extendHeight;
|
||||
}
|
||||
|
||||
if (updateItem.position.x < 0) {
|
||||
const xDiff = Math.abs(updateItem.position.x);
|
||||
parent.position.x = parent.position.x - xDiff;
|
||||
parent.width += xDiff;
|
||||
updateItem.position.x = 0;
|
||||
}
|
||||
|
||||
if (updateItem.position.y < 0) {
|
||||
const yDiff = Math.abs(updateItem.position.y);
|
||||
parent.position.y = parent.position.y - yDiff;
|
||||
parent.height += yDiff;
|
||||
updateItem.position.y = 0;
|
||||
}
|
||||
|
||||
parent.computed.width = parent.width;
|
||||
parent.computed.height = parent.height;
|
||||
|
||||
updatedElements[index] = parent;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
import {
|
||||
EdgeLookup,
|
||||
NodeLookup,
|
||||
EdgeChange,
|
||||
NodeChange,
|
||||
NodeSelectionChange,
|
||||
EdgeSelectionChange,
|
||||
} from '@xyflow/system';
|
||||
import type { Node, Edge, InternalNode } from '../types';
|
||||
|
||||
// This function applies changes to nodes or edges that are triggered by React Flow internally.
|
||||
// When you drag a node for example, React Flow will send a position change update.
|
||||
@@ -103,7 +65,7 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
const updatedElement = { ...element };
|
||||
|
||||
for (const change of changes) {
|
||||
applyChange(change, updatedElement, updatedElements);
|
||||
applyChange(change, updatedElement);
|
||||
}
|
||||
|
||||
updatedElements.push(updatedElement);
|
||||
@@ -113,7 +75,7 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
}
|
||||
|
||||
// Applies a single change to an element. This is a *mutable* update.
|
||||
function applyChange(change: any, element: any, elements: any[] = []): any {
|
||||
function applyChange(change: any, element: any): any {
|
||||
switch (change.type) {
|
||||
case 'select': {
|
||||
element.selected = change.selected;
|
||||
@@ -125,26 +87,18 @@ function applyChange(change: any, element: any, elements: any[] = []): any {
|
||||
element.position = change.position;
|
||||
}
|
||||
|
||||
if (typeof change.positionAbsolute !== 'undefined') {
|
||||
element.computed ??= {};
|
||||
element.computed.positionAbsolute = change.positionAbsolute;
|
||||
}
|
||||
|
||||
if (typeof change.dragging !== 'undefined') {
|
||||
element.dragging = change.dragging;
|
||||
}
|
||||
|
||||
if (element.expandParent) {
|
||||
handleParentExpand(elements, element);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'dimensions': {
|
||||
if (typeof change.dimensions !== 'undefined') {
|
||||
element.computed ??= {};
|
||||
element.computed.width = change.dimensions.width;
|
||||
element.computed.height = change.dimensions.height;
|
||||
element.measured ??= {};
|
||||
element.measured.width = change.dimensions.width;
|
||||
element.measured.height = change.dimensions.height;
|
||||
|
||||
if (change.resizing) {
|
||||
element.width = change.dimensions.width;
|
||||
@@ -156,10 +110,6 @@ function applyChange(change: any, element: any, elements: any[] = []): any {
|
||||
element.resizing = change.resizing;
|
||||
}
|
||||
|
||||
if (element.expandParent) {
|
||||
handleParentExpand(elements, element);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -228,13 +178,13 @@ export function createSelectionChange(id: string, selected: boolean): NodeSelect
|
||||
}
|
||||
|
||||
export function getSelectionChanges(
|
||||
items: any[],
|
||||
items: Map<string, any>,
|
||||
selectedIds: Set<string> = new Set(),
|
||||
mutateItem = false
|
||||
): NodeSelectionChange[] | EdgeSelectionChange[] {
|
||||
const changes: NodeSelectionChange[] | EdgeSelectionChange[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
for (const [, item] of items) {
|
||||
const willBeSelected = selectedIds.has(item.id);
|
||||
|
||||
// we don't want to set all items to selected=false on the first selection
|
||||
@@ -266,7 +216,7 @@ export function getElementsDiffChanges({
|
||||
lookup,
|
||||
}: {
|
||||
items: Node[] | undefined;
|
||||
lookup: NodeLookup<Node>;
|
||||
lookup: NodeLookup<InternalNode<Node>>;
|
||||
}): NodeChange[];
|
||||
export function getElementsDiffChanges({
|
||||
items,
|
||||
|
||||
Reference in New Issue
Block a user