Merge pull request #4105 from xyflow/refactor/internal-nodes
React Flow 12: separate user nodes and internal nodes
This commit is contained in:
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user