From 2adf37849b7164b8219a0205dc1afa5c18dbd75c Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Wed, 23 Nov 2022 13:15:10 +0100 Subject: [PATCH 01/12] ref: stop using try..catch as control flow in Wrapper Previously, Wrapper used try..catch around a call to useStoreApi to check whether there was an available StoreContext, instead of simply using useContext and checking the value. This leads to poor developer experience when using the devtools feature "pause on caught exceptions" as it causes the devtools to break on every render of a Wrapper that is not within a StoreContext provider. The catch is also universal, meaning that it could catch a completely unrelated error and happily keep going instead of crashing which is undesirable. This commit changes this try..catch into a simple call to useContext to check if a StoreContext is available. --- packages/core/src/container/ReactFlow/Wrapper.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/core/src/container/ReactFlow/Wrapper.tsx b/packages/core/src/container/ReactFlow/Wrapper.tsx index 297e3b49..b6069948 100644 --- a/packages/core/src/container/ReactFlow/Wrapper.tsx +++ b/packages/core/src/container/ReactFlow/Wrapper.tsx @@ -1,18 +1,10 @@ -import type { FC, PropsWithChildren } from 'react'; +import { FC, PropsWithChildren, useContext } from 'react'; -import { useStoreApi } from '../../hooks/useStore'; +import StoreContext from '../../contexts/RFStoreContext'; import ReactFlowProvider from '../../components/ReactFlowProvider'; const Wrapper: FC = ({ children }) => { - let isWrapped = true; - - try { - useStoreApi(); - } catch (e) { - isWrapped = false; - } - - if (isWrapped) { + if (useContext(StoreContext)) { // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 return <>{children}; From d4b00452a0f4e4111868e2cf11a914aa3cd80f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 24 Nov 2022 12:51:17 +0100 Subject: [PATCH 02/12] feat(node-toolbar): allow multiple nodeIds as property to enable multi selection --- .../NodeToolbar/SelectedNodesToolbar.tsx | 13 ++++++++ .../src/examples/NodeToolbar/index.tsx | 2 ++ packages/node-toolbar/src/NodeToolbar.tsx | 33 ++++++++++++++----- packages/node-toolbar/src/types.ts | 2 +- 4 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 examples/vite-app/src/examples/NodeToolbar/SelectedNodesToolbar.tsx diff --git a/examples/vite-app/src/examples/NodeToolbar/SelectedNodesToolbar.tsx b/examples/vite-app/src/examples/NodeToolbar/SelectedNodesToolbar.tsx new file mode 100644 index 00000000..9c0db9cc --- /dev/null +++ b/examples/vite-app/src/examples/NodeToolbar/SelectedNodesToolbar.tsx @@ -0,0 +1,13 @@ +import { NodeToolbar, useNodes } from 'reactflow'; + +export default function SelectedNodesToolbar() { + const nodes = useNodes(); + const selectedNodeIds = nodes.filter((node) => node.selected).map((node) => node.id); + const isVisible = selectedNodeIds.length > 1; + + return ( + + + + ); +} diff --git a/examples/vite-app/src/examples/NodeToolbar/index.tsx b/examples/vite-app/src/examples/NodeToolbar/index.tsx index 9a5815df..601d8618 100644 --- a/examples/vite-app/src/examples/NodeToolbar/index.tsx +++ b/examples/vite-app/src/examples/NodeToolbar/index.tsx @@ -11,6 +11,7 @@ import ReactFlow, { } from 'reactflow'; import CustomNode from './CustomNode'; +import SelectedNodesToolbar from './SelectedNodesToolbar'; const nodeTypes: NodeTypes = { custom: CustomNode, @@ -79,6 +80,7 @@ export default function NodeToolbarExample() { + ); } diff --git a/packages/node-toolbar/src/NodeToolbar.tsx b/packages/node-toolbar/src/NodeToolbar.tsx index caadf257..66ebd618 100644 --- a/packages/node-toolbar/src/NodeToolbar.tsx +++ b/packages/node-toolbar/src/NodeToolbar.tsx @@ -15,9 +15,7 @@ import shallow from 'zustand/shallow'; import NodeToolbarPortal from './NodeToolbarPortal'; import { NodeToolbarProps } from './types'; -type SelectedNode = Node | undefined; - -const nodeEqualityFn = (a: SelectedNode, b: SelectedNode) => +const nodeEqualityFn = (a: Node | undefined, b: Node | undefined) => a?.positionAbsolute?.x === b?.positionAbsolute?.x && a?.positionAbsolute?.y === b?.positionAbsolute?.y && a?.width === b?.width && @@ -25,6 +23,10 @@ const nodeEqualityFn = (a: SelectedNode, b: SelectedNode) => a?.selected === b?.selected && a?.[internalsSymbol]?.z === b?.[internalsSymbol]?.z; +const nodesEqualityFn = (a: Node[], b: Node[]) => { + return a.length === b.length && a.every((node, i) => nodeEqualityFn(node, b[i])); +}; + const storeSelector = (state: ReactFlowState) => ({ transform: state.transform, nodeOrigin: state.nodeOrigin, @@ -70,21 +72,34 @@ function NodeToolbar({ offset = 10, ...rest }: NodeToolbarProps) { - const nodeSelector = useCallback((state: ReactFlowState): SelectedNode => state.nodeInternals.get(nodeId), [nodeId]); - const node = useStore(nodeSelector, nodeEqualityFn); + const nodeIds: string[] = typeof nodeId === 'string' ? [nodeId] : nodeId; + const nodesSelector = useCallback( + (state: ReactFlowState): Node[] => + nodeIds.reduce((acc, id) => { + const node = state.nodeInternals.get(id); + if (node) { + acc.push(node); + } + return acc; + }, [] as Node[]), + [nodeIds] + ); + const nodes = useStore(nodesSelector, nodesEqualityFn); const { transform, nodeOrigin, selectedNodesCount } = useStore(storeSelector, shallow); - const isActive = typeof isVisible === 'boolean' ? isVisible : node?.selected && selectedNodesCount === 1; + const isActive = + typeof isVisible === 'boolean' ? isVisible : nodes.length === 1 && nodes[0].selected && selectedNodesCount === 1; - if (!isActive || !node) { + if (!isActive || !nodes.length) { return null; } - const nodeRect: Rect = getRectOfNodes([node], nodeOrigin); + const nodeRect: Rect = getRectOfNodes(nodes, nodeOrigin); + const zIndex: number = Math.max(...nodes.map((node) => (node[internalsSymbol]?.z || 1) + 1)); const wrapperStyle: CSSProperties = { position: 'absolute', transform: getTransform(nodeRect, transform, position, offset), - zIndex: (node[internalsSymbol]?.z || 1) + 1, + zIndex, ...style, }; diff --git a/packages/node-toolbar/src/types.ts b/packages/node-toolbar/src/types.ts index 1e6576b6..a9f547ad 100644 --- a/packages/node-toolbar/src/types.ts +++ b/packages/node-toolbar/src/types.ts @@ -2,7 +2,7 @@ import { Position } from '@reactflow/core'; import type { HTMLAttributes } from 'react'; export type NodeToolbarProps = HTMLAttributes & { - nodeId: string; + nodeId: string | string[]; isVisible?: boolean; position?: Position; offset?: number; From ec94d9ecdc964d6d66c04e9242f195614bbfdbbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 24 Nov 2022 12:59:32 +0100 Subject: [PATCH 03/12] chore(node-toolbar): add changeset --- .changeset/six-dolls-search.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/six-dolls-search.md diff --git a/.changeset/six-dolls-search.md b/.changeset/six-dolls-search.md new file mode 100644 index 00000000..51992026 --- /dev/null +++ b/.changeset/six-dolls-search.md @@ -0,0 +1,5 @@ +--- +'@reactflow/node-toolbar': patch +--- + +Allow multiple node ids to be passed to the node toolbar for enabling multi selection toolbars From 3685420ebf0705a42ed8291d6bf44d6d29343be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 24 Nov 2022 16:55:51 +0100 Subject: [PATCH 04/12] fix(subflow-node-origin): use node origin in node position calculation --- .../vite-app/src/examples/Subflow/index.tsx | 5 +- packages/core/src/hooks/useDrag/index.ts | 3 +- packages/core/src/hooks/useDrag/utils.ts | 21 ++++---- packages/core/src/store/index.ts | 9 ++-- packages/core/src/store/utils.ts | 43 +++++++++++----- packages/core/src/utils/graph.ts | 51 +++++++++++++++---- packages/minimap/src/MiniMap.tsx | 1 + 7 files changed, 97 insertions(+), 36 deletions(-) diff --git a/examples/vite-app/src/examples/Subflow/index.tsx b/examples/vite-app/src/examples/Subflow/index.tsx index 84003c83..8d2945e0 100644 --- a/examples/vite-app/src/examples/Subflow/index.tsx +++ b/examples/vite-app/src/examples/Subflow/index.tsx @@ -11,6 +11,7 @@ import ReactFlow, { Controls, MiniMap, Background, + NodeOrigin, } from 'reactflow'; import DebugNode from './DebugNode'; @@ -21,6 +22,7 @@ const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge); const defaultViewport = { x: 0, y: 0, zoom: 1.5 }; +const nodeOrigin: NodeOrigin = [0.5, 0.5]; const initialNodes: Node[] = [ { @@ -90,7 +92,7 @@ const initialNodes: Node[] = [ { id: '5a', data: { label: 'Node 5a' }, - position: { x: 25, y: 50 }, + position: { x: 0, y: 0 }, className: 'light', parentNode: '5', extent: 'parent', @@ -205,6 +207,7 @@ const Subflow = () => { onlyRenderVisibleElements={false} nodeTypes={nodeTypes} fitView + nodeOrigin={nodeOrigin} > diff --git a/packages/core/src/hooks/useDrag/index.ts b/packages/core/src/hooks/useDrag/index.ts index 319c1993..47c2113a 100644 --- a/packages/core/src/hooks/useDrag/index.ts +++ b/packages/core/src/hooks/useDrag/index.ts @@ -114,6 +114,7 @@ function useDrag({ onSelectionDrag, snapGrid, snapToGrid, + nodeOrigin, } = store.getState(); const pointerPos = getPointerPosition(event); // skip events without movement @@ -133,7 +134,7 @@ function useDrag({ nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]); } - const updatedPos = calcNextPosition(n, nextPosition, nodeInternals, nodeExtent); + const updatedPos = calcNextPosition(n, nextPosition, nodeInternals, nodeExtent, nodeOrigin); n.position = updatedPos.position; n.positionAbsolute = updatedPos.positionAbsolute; diff --git a/packages/core/src/hooks/useDrag/utils.ts b/packages/core/src/hooks/useDrag/utils.ts index be25f3c1..f9eb6ade 100644 --- a/packages/core/src/hooks/useDrag/utils.ts +++ b/packages/core/src/hooks/useDrag/utils.ts @@ -1,7 +1,8 @@ import type { RefObject } from 'react'; import { clampPosition, devWarn } from '../../utils'; -import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, XYPosition } from '../../types'; +import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, NodeOrigin, XYPosition } from '../../types'; +import { getNodePosition } from '../../utils/graph'; export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean { if (!node.parentNode) { @@ -60,20 +61,22 @@ export function calcNextPosition( node: NodeDragItem | Node, nextPosition: XYPosition, nodeInternals: NodeInternals, - nodeExtent?: CoordinateExtent + nodeExtent?: CoordinateExtent, + nodeOrigin?: NodeOrigin ): { position: XYPosition; positionAbsolute: XYPosition } { let currentExtent = node.extent || nodeExtent; if (node.extent === 'parent') { if (node.parentNode && node.width && node.height) { const parent = nodeInternals.get(node.parentNode); + const parentPosition = getNodePosition(parent, nodeOrigin); currentExtent = - parent?.positionAbsolute && parent?.width && parent?.height + parentPosition.positionAbsolute && parent?.width && parent?.height ? [ - [parent.positionAbsolute.x, parent.positionAbsolute.y], + [parentPosition.positionAbsolute.x, parentPosition.positionAbsolute.y], [ - parent.positionAbsolute.x + parent.width - node.width, - parent.positionAbsolute.y + parent.height - node.height, + parentPosition.positionAbsolute.x + parent.width - node.width, + parentPosition.positionAbsolute.y + parent.height - node.height, ], ] : currentExtent; @@ -84,8 +87,8 @@ export function calcNextPosition( } } else if (node.extent && node.parentNode) { const parent = nodeInternals.get(node.parentNode); - const parentX = parent?.positionAbsolute?.x ?? 0; - const parentY = parent?.positionAbsolute?.y ?? 0; + const parentPosition = getNodePosition(parent, nodeOrigin); + const { x: parentX, y: parentY } = parentPosition.positionAbsolute; currentExtent = [ [node.extent[0][0] + parentX, node.extent[0][1] + parentY], [node.extent[1][0] + parentX, node.extent[1][1] + parentY], @@ -96,7 +99,7 @@ export function calcNextPosition( if (node.parentNode) { const parentNode = nodeInternals.get(node.parentNode); - parentPosition = { x: parentNode?.positionAbsolute?.x ?? 0, y: parentNode?.positionAbsolute?.y ?? 0 }; + parentPosition = getNodePosition(parentNode, nodeOrigin).positionAbsolute; } const positionAbsolute = currentExtent diff --git a/packages/core/src/store/index.ts b/packages/core/src/store/index.ts index e09abbd7..5ffd64c3 100644 --- a/packages/core/src/store/index.ts +++ b/packages/core/src/store/index.ts @@ -23,7 +23,8 @@ const createRFStore = () => createStore((set, get) => ({ ...initialState, setNodes: (nodes: Node[]) => { - set({ nodeInternals: createNodeInternals(nodes, get().nodeInternals) }); + const { nodeInternals, nodeOrigin } = get(); + set({ nodeInternals: createNodeInternals(nodes, nodeInternals, nodeOrigin) }); }, setEdges: (edges: Edge[]) => { const { defaultEdgeOptions = {} } = get(); @@ -33,7 +34,7 @@ const createRFStore = () => const hasDefaultNodes = typeof nodes !== 'undefined'; const hasDefaultEdges = typeof edges !== 'undefined'; - const nodeInternals = hasDefaultNodes ? createNodeInternals(nodes, new Map()) : new Map(); + const nodeInternals = hasDefaultNodes ? createNodeInternals(nodes, new Map(), get().nodeOrigin) : new Map(); const nextEdges = hasDefaultEdges ? edges : []; set({ nodeInternals, edges: nextEdges, hasDefaultNodes, hasDefaultEdges }); @@ -102,7 +103,7 @@ const createRFStore = () => } }, updateNodePositions: (nodeDragItems: NodeDragItem[] | Node[], positionChanged = true, dragging = false) => { - const { onNodesChange, nodeInternals, hasDefaultNodes } = get(); + const { onNodesChange, nodeInternals, hasDefaultNodes, nodeOrigin } = get(); if (hasDefaultNodes || onNodesChange) { const changes = nodeDragItems.map((node) => { @@ -123,7 +124,7 @@ const createRFStore = () => if (changes?.length) { if (hasDefaultNodes) { const nodes = applyNodeChanges(changes, Array.from(nodeInternals.values())); - const nextNodeInternals = createNodeInternals(nodes, nodeInternals); + const nextNodeInternals = createNodeInternals(nodes, nodeInternals, nodeOrigin); set({ nodeInternals: nextNodeInternals }); } diff --git a/packages/core/src/store/utils.ts b/packages/core/src/store/utils.ts index 2af09d46..c1b4fd7e 100644 --- a/packages/core/src/store/utils.ts +++ b/packages/core/src/store/utils.ts @@ -2,7 +2,7 @@ import { zoomIdentity } from 'd3-zoom'; import type { StoreApi } from 'zustand'; import { internalsSymbol, isNumeric } from '../utils'; -import { getD3Transition, getRectOfNodes, getTransformForBounds } from '../utils/graph'; +import { getD3Transition, getRectOfNodes, getTransformForBounds, getNodePosition } from '../utils/graph'; import type { Edge, EdgeSelectionChange, @@ -12,6 +12,7 @@ import type { ReactFlowState, XYZPosition, FitViewOptions, + NodeOrigin, } from '../types'; type ParentNodes = Record; @@ -20,21 +21,33 @@ function calculateXYZPosition( node: Node, nodeInternals: NodeInternals, parentNodes: ParentNodes, - result: XYZPosition + result: XYZPosition, + nodeOrigin: NodeOrigin ): XYZPosition { if (!node.parentNode) { return result; } const parentNode = nodeInternals.get(node.parentNode)!; + const parentNodePosition = getNodePosition(parentNode, nodeOrigin); - return calculateXYZPosition(parentNode, nodeInternals, parentNodes, { - x: (result.x ?? 0) + (parentNode.position?.x ?? 0), - y: (result.y ?? 0) + (parentNode.position?.y ?? 0), - z: (parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0) ? parentNode[internalsSymbol]?.z ?? 0 : result.z ?? 0, - }); + return calculateXYZPosition( + parentNode, + nodeInternals, + parentNodes, + { + 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, + }, + nodeOrigin + ); } -export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals): NodeInternals { +export function createNodeInternals( + nodes: Node[], + nodeInternals: NodeInternals, + nodeOrigin: NodeOrigin +): NodeInternals { const nextNodeInternals = new Map(); const parentNodes: ParentNodes = {}; @@ -74,10 +87,16 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) } if (node.parentNode || parentNodes[node.id]) { - const { x, y, z } = calculateXYZPosition(node, nextNodeInternals, parentNodes, { - ...node.position, - z: node[internalsSymbol]?.z ?? 0, - }); + const { x, y, z } = calculateXYZPosition( + node, + nextNodeInternals, + parentNodes, + { + ...node.position, + z: node[internalsSymbol]?.z ?? 0, + }, + nodeOrigin + ); node.positionAbsolute = { x, diff --git a/packages/core/src/utils/graph.ts b/packages/core/src/utils/graph.ts index 0408603f..d6a4d068 100644 --- a/packages/core/src/utils/graph.ts +++ b/packages/core/src/utils/graph.ts @@ -141,23 +141,55 @@ export const pointToRendererPoint = ( return position; }; +export const getNodePosition = ( + node: Node | undefined, + nodeOrigin: NodeOrigin = [0, 0] +): XYPosition & { positionAbsolute: XYPosition } => { + if (!node) { + return { + x: 0, + y: 0, + positionAbsolute: { + x: 0, + y: 0, + }, + }; + } + + const position: XYPosition = { + x: node.position.x - (node.width ?? 0) * nodeOrigin[0], + y: node.position.y - (node.height ?? 0) * nodeOrigin[1], + }; + + const positionAbsolute: XYPosition = { + x: (node.positionAbsolute?.x ?? 0) - (node.width ?? 0) * nodeOrigin[0], + y: (node.positionAbsolute?.y ?? 0) - (node.height ?? 0) * nodeOrigin[1], + }; + + return { + ...position, + positionAbsolute, + }; +}; + export const getRectOfNodes = (nodes: Node[], nodeOrigin: NodeOrigin = [0, 0]): Rect => { if (nodes.length === 0) { return { x: 0, y: 0, width: 0, height: 0 }; } const box = nodes.reduce( - (currBox, { positionAbsolute, position, width, height }) => { + (currBox, node) => { + const { positionAbsolute, ...position } = getNodePosition(node, nodeOrigin); const nodeX = positionAbsolute ? positionAbsolute.x : position.x; const nodeY = positionAbsolute ? positionAbsolute.y : position.y; return getBoundsOfBoxes( currBox, rectToBox({ - x: nodeX - nodeOrigin[0] * (width || 0), - y: nodeY - nodeOrigin[1] * (height || 0), - width: width || 0, - height: height || 0, + x: nodeX, + y: nodeY, + width: node.width || 0, + height: node.height || 0, }) ); }, @@ -186,15 +218,17 @@ export const getNodesInside = ( const visibleNodes: Node[] = []; nodeInternals.forEach((node) => { - const { width, height, selectable = true, positionAbsolute = { x: 0, y: 0 } } = node; + const { width, height, selectable = true } = node; if (excludeNonSelectableNodes && !selectable) { return false; } + const { positionAbsolute } = getNodePosition(node, nodeOrigin); + const nodeRect = { - x: positionAbsolute.x - nodeOrigin[0] * (width || 0), - y: positionAbsolute.y - nodeOrigin[1] * (height || 0), + x: positionAbsolute.x, + y: positionAbsolute.y, width: width || 0, height: height || 0, }; @@ -243,4 +277,3 @@ export const getTransformForBounds = ( export const getD3Transition = (selection: D3Selection, duration = 0) => { return selection.transition().duration(duration); }; - diff --git a/packages/minimap/src/MiniMap.tsx b/packages/minimap/src/MiniMap.tsx index 4764a987..8d804f4e 100644 --- a/packages/minimap/src/MiniMap.tsx +++ b/packages/minimap/src/MiniMap.tsx @@ -162,6 +162,7 @@ function MiniMap({ {nodes.map((node) => ( Date: Thu, 24 Nov 2022 22:36:00 +0100 Subject: [PATCH 05/12] fix(subflow-node-origin): use helper function in minimap, calculate extent corrrectly when using nodeOrigin and subflows --- .../vite-app/src/examples/Subflow/index.tsx | 2 - packages/core/src/hooks/useDrag/utils.ts | 19 ++++--- packages/core/src/index.ts | 1 + packages/core/src/store/utils.ts | 4 +- packages/core/src/utils/graph.ts | 6 +-- packages/minimap/src/MiniMap.tsx | 50 +++++++++++-------- 6 files changed, 47 insertions(+), 35 deletions(-) diff --git a/examples/vite-app/src/examples/Subflow/index.tsx b/examples/vite-app/src/examples/Subflow/index.tsx index 8d2945e0..55aea018 100644 --- a/examples/vite-app/src/examples/Subflow/index.tsx +++ b/examples/vite-app/src/examples/Subflow/index.tsx @@ -22,7 +22,6 @@ const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge); const defaultViewport = { x: 0, y: 0, zoom: 1.5 }; -const nodeOrigin: NodeOrigin = [0.5, 0.5]; const initialNodes: Node[] = [ { @@ -207,7 +206,6 @@ const Subflow = () => { onlyRenderVisibleElements={false} nodeTypes={nodeTypes} fitView - nodeOrigin={nodeOrigin} > diff --git a/packages/core/src/hooks/useDrag/utils.ts b/packages/core/src/hooks/useDrag/utils.ts index f9eb6ade..61b0d3f9 100644 --- a/packages/core/src/hooks/useDrag/utils.ts +++ b/packages/core/src/hooks/useDrag/utils.ts @@ -2,7 +2,7 @@ import type { RefObject } from 'react'; import { clampPosition, devWarn } from '../../utils'; import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, NodeOrigin, XYPosition } from '../../types'; -import { getNodePosition } from '../../utils/graph'; +import { getNodePositionWithOrigin } from '../../utils/graph'; export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean { if (!node.parentNode) { @@ -62,21 +62,24 @@ export function calcNextPosition( nextPosition: XYPosition, nodeInternals: NodeInternals, nodeExtent?: CoordinateExtent, - nodeOrigin?: NodeOrigin + nodeOrigin: NodeOrigin = [0, 0] ): { position: XYPosition; positionAbsolute: XYPosition } { let currentExtent = node.extent || nodeExtent; if (node.extent === 'parent') { if (node.parentNode && node.width && node.height) { const parent = nodeInternals.get(node.parentNode); - const parentPosition = getNodePosition(parent, nodeOrigin); + const parentPosition = getNodePositionWithOrigin(parent, nodeOrigin); currentExtent = parentPosition.positionAbsolute && parent?.width && parent?.height ? [ - [parentPosition.positionAbsolute.x, parentPosition.positionAbsolute.y], [ - parentPosition.positionAbsolute.x + parent.width - node.width, - parentPosition.positionAbsolute.y + parent.height - node.height, + parentPosition.positionAbsolute.x + node.width * nodeOrigin[0], + parentPosition.positionAbsolute.y + node.height * nodeOrigin[1], + ], + [ + parentPosition.positionAbsolute.x + parent.width - node.width + node.width * nodeOrigin[0], + parentPosition.positionAbsolute.y + parent.height - node.height + node.height * nodeOrigin[1], ], ] : currentExtent; @@ -87,7 +90,7 @@ export function calcNextPosition( } } else if (node.extent && node.parentNode) { const parent = nodeInternals.get(node.parentNode); - const parentPosition = getNodePosition(parent, nodeOrigin); + const parentPosition = getNodePositionWithOrigin(parent, nodeOrigin); const { x: parentX, y: parentY } = parentPosition.positionAbsolute; currentExtent = [ [node.extent[0][0] + parentX, node.extent[0][1] + parentY], @@ -99,7 +102,7 @@ export function calcNextPosition( if (node.parentNode) { const parentNode = nodeInternals.get(node.parentNode); - parentPosition = getNodePosition(parentNode, nodeOrigin).positionAbsolute; + parentPosition = getNodePositionWithOrigin(parentNode, nodeOrigin).positionAbsolute; } const positionAbsolute = currentExtent diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e2e077db..d51540e9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -19,6 +19,7 @@ export { updateEdge, getTransformForBounds, getRectOfNodes, + getNodePositionWithOrigin, } from './utils/graph'; export { applyNodeChanges, applyEdgeChanges } from './utils/changes'; export { getMarkerEnd } from './components/Edges/utils'; diff --git a/packages/core/src/store/utils.ts b/packages/core/src/store/utils.ts index c1b4fd7e..02a4516b 100644 --- a/packages/core/src/store/utils.ts +++ b/packages/core/src/store/utils.ts @@ -2,7 +2,7 @@ import { zoomIdentity } from 'd3-zoom'; import type { StoreApi } from 'zustand'; import { internalsSymbol, isNumeric } from '../utils'; -import { getD3Transition, getRectOfNodes, getTransformForBounds, getNodePosition } from '../utils/graph'; +import { getD3Transition, getRectOfNodes, getTransformForBounds, getNodePositionWithOrigin } from '../utils/graph'; import type { Edge, EdgeSelectionChange, @@ -28,7 +28,7 @@ function calculateXYZPosition( return result; } const parentNode = nodeInternals.get(node.parentNode)!; - const parentNodePosition = getNodePosition(parentNode, nodeOrigin); + const parentNodePosition = getNodePositionWithOrigin(parentNode, nodeOrigin); return calculateXYZPosition( parentNode, diff --git a/packages/core/src/utils/graph.ts b/packages/core/src/utils/graph.ts index d6a4d068..f07267a5 100644 --- a/packages/core/src/utils/graph.ts +++ b/packages/core/src/utils/graph.ts @@ -141,7 +141,7 @@ export const pointToRendererPoint = ( return position; }; -export const getNodePosition = ( +export const getNodePositionWithOrigin = ( node: Node | undefined, nodeOrigin: NodeOrigin = [0, 0] ): XYPosition & { positionAbsolute: XYPosition } => { @@ -179,7 +179,7 @@ export const getRectOfNodes = (nodes: Node[], nodeOrigin: NodeOrigin = [0, 0]): const box = nodes.reduce( (currBox, node) => { - const { positionAbsolute, ...position } = getNodePosition(node, nodeOrigin); + const { positionAbsolute, ...position } = getNodePositionWithOrigin(node, nodeOrigin); const nodeX = positionAbsolute ? positionAbsolute.x : position.x; const nodeY = positionAbsolute ? positionAbsolute.y : position.y; @@ -224,7 +224,7 @@ export const getNodesInside = ( return false; } - const { positionAbsolute } = getNodePosition(node, nodeOrigin); + const { positionAbsolute } = getNodePositionWithOrigin(node, nodeOrigin); const nodeRect = { x: positionAbsolute.x, diff --git a/packages/minimap/src/MiniMap.tsx b/packages/minimap/src/MiniMap.tsx index 8d804f4e..890df176 100644 --- a/packages/minimap/src/MiniMap.tsx +++ b/packages/minimap/src/MiniMap.tsx @@ -7,7 +7,14 @@ import shallow from 'zustand/shallow'; import { zoom, zoomIdentity } from 'd3-zoom'; import type { D3ZoomEvent } from 'd3-zoom'; import { select, pointer } from 'd3-selection'; -import { useStore, getRectOfNodes, Panel, getBoundsOfRects, useStoreApi } from '@reactflow/core'; +import { + useStore, + getRectOfNodes, + Panel, + getBoundsOfRects, + useStoreApi, + getNodePositionWithOrigin, +} from '@reactflow/core'; import type { ReactFlowState, Rect } from '@reactflow/core'; import MiniMapNode from './MiniMapNode'; @@ -159,25 +166,28 @@ function MiniMap({ onClick={onSvgClick} > {ariaLabel && {ariaLabel}} - {nodes.map((node) => ( - - ))} + {nodes.map((node) => { + const { positionAbsolute } = getNodePositionWithOrigin(node, nodeOrigin); + + return ( + + ); + })} Date: Thu, 24 Nov 2022 22:38:05 +0100 Subject: [PATCH 06/12] chore(subflow-node-origin): create changeset --- .changeset/neat-fishes-burn.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/neat-fishes-burn.md diff --git a/.changeset/neat-fishes-burn.md b/.changeset/neat-fishes-burn.md new file mode 100644 index 00000000..c771c907 --- /dev/null +++ b/.changeset/neat-fishes-burn.md @@ -0,0 +1,6 @@ +--- +'@reactflow/core': patch +'@reactflow/minimap': patch +--- + +Fix and improve the behaviour when using nodeOrigin in combination with subflows From a8d590a60be23437e1d0a87a5d7a6f6ba9d86be9 Mon Sep 17 00:00:00 2001 From: moklick Date: Fri, 25 Nov 2022 21:25:00 +0100 Subject: [PATCH 07/12] chore(nodeOriginHelper): cleanup --- packages/core/src/utils/graph.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/core/src/utils/graph.ts b/packages/core/src/utils/graph.ts index f07267a5..ea390967 100644 --- a/packages/core/src/utils/graph.ts +++ b/packages/core/src/utils/graph.ts @@ -156,19 +156,18 @@ export const getNodePositionWithOrigin = ( }; } - const position: XYPosition = { - x: node.position.x - (node.width ?? 0) * nodeOrigin[0], - y: node.position.y - (node.height ?? 0) * nodeOrigin[1], - }; - - const positionAbsolute: XYPosition = { - x: (node.positionAbsolute?.x ?? 0) - (node.width ?? 0) * nodeOrigin[0], - y: (node.positionAbsolute?.y ?? 0) - (node.height ?? 0) * nodeOrigin[1], + const offset: XYPosition = { + x: (node.width ?? 0) * nodeOrigin[0], + y: (node.height ?? 0) * nodeOrigin[1], }; return { - ...position, - positionAbsolute, + x: node.position.x - offset.x, + y: node.position.y - offset.y, + positionAbsolute: { + x: (node.positionAbsolute?.x ?? 0) - offset.x, + y: (node.positionAbsolute?.y ?? 0) - offset.y, + }, }; }; From 1363d1e398c37950ac097725715d14e98c884df3 Mon Sep 17 00:00:00 2001 From: moklick Date: Fri, 25 Nov 2022 21:35:39 +0100 Subject: [PATCH 08/12] chore(node-toolbar): cleanup --- .../examples/NodeToolbar/SelectedNodesToolbar.tsx | 12 ++++++++---- packages/node-toolbar/src/NodeToolbar.tsx | 12 +++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/examples/vite-app/src/examples/NodeToolbar/SelectedNodesToolbar.tsx b/examples/vite-app/src/examples/NodeToolbar/SelectedNodesToolbar.tsx index 9c0db9cc..349db385 100644 --- a/examples/vite-app/src/examples/NodeToolbar/SelectedNodesToolbar.tsx +++ b/examples/vite-app/src/examples/NodeToolbar/SelectedNodesToolbar.tsx @@ -1,13 +1,17 @@ -import { NodeToolbar, useNodes } from 'reactflow'; +import { NodeToolbar, ReactFlowState, useStore } from 'reactflow'; + +const selectedNodesSelector = (state: ReactFlowState) => + Array.from(state.nodeInternals.values()) + .filter((node) => node.selected) + .map((node) => node.id); export default function SelectedNodesToolbar() { - const nodes = useNodes(); - const selectedNodeIds = nodes.filter((node) => node.selected).map((node) => node.id); + const selectedNodeIds = useStore(selectedNodesSelector); const isVisible = selectedNodeIds.length > 1; return ( - + ); } diff --git a/packages/node-toolbar/src/NodeToolbar.tsx b/packages/node-toolbar/src/NodeToolbar.tsx index 66ebd618..1dfd7669 100644 --- a/packages/node-toolbar/src/NodeToolbar.tsx +++ b/packages/node-toolbar/src/NodeToolbar.tsx @@ -72,17 +72,19 @@ function NodeToolbar({ offset = 10, ...rest }: NodeToolbarProps) { - const nodeIds: string[] = typeof nodeId === 'string' ? [nodeId] : nodeId; const nodesSelector = useCallback( - (state: ReactFlowState): Node[] => - nodeIds.reduce((acc, id) => { + (state: ReactFlowState): Node[] => { + const nodeIds: string[] = typeof nodeId === 'string' ? [nodeId] : nodeId; + + return nodeIds.reduce((acc, id) => { const node = state.nodeInternals.get(id); if (node) { acc.push(node); } return acc; - }, [] as Node[]), - [nodeIds] + }, [] as Node[]); + }, + [nodeId] ); const nodes = useStore(nodesSelector, nodesEqualityFn); const { transform, nodeOrigin, selectedNodesCount } = useStore(storeSelector, shallow); From d142725ecc02da577283812fc6e557b4711ad283 Mon Sep 17 00:00:00 2001 From: moklick Date: Fri, 25 Nov 2022 21:48:33 +0100 Subject: [PATCH 09/12] fix(isRectObj): repair test for rect closes #2597 --- packages/core/src/utils/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 02cff5a0..d869a81a 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -53,7 +53,8 @@ export const getOverlappingArea = (rectA: Rect, rectB: Rect): number => { }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isRectObject = (obj: any): obj is Rect => !!obj.width && !!obj.height && !!obj.x && !!obj.y; +export const isRectObject = (obj: any): obj is Rect => + isNumeric(obj.width) && isNumeric(obj.height) && isNumeric(obj.x) && isNumeric(obj.y); /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n); From b2c728137d1b53e38883f044fa447585c377a6af Mon Sep 17 00:00:00 2001 From: moklick Date: Fri, 25 Nov 2022 21:50:15 +0100 Subject: [PATCH 10/12] chore(changest): add --- .changeset/silver-dots-try.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/silver-dots-try.md diff --git a/.changeset/silver-dots-try.md b/.changeset/silver-dots-try.md new file mode 100644 index 00000000..23994cc4 --- /dev/null +++ b/.changeset/silver-dots-try.md @@ -0,0 +1,5 @@ +--- +'@reactflow/core': patch +--- + +fix isRectObject function From 22db82c8b8ab7d9feeb20fdedc6b50bb7479af4c Mon Sep 17 00:00:00 2001 From: moklick Date: Fri, 25 Nov 2022 22:03:48 +0100 Subject: [PATCH 11/12] chore(wrapper): cleanup --- packages/core/src/container/ReactFlow/Wrapper.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/src/container/ReactFlow/Wrapper.tsx b/packages/core/src/container/ReactFlow/Wrapper.tsx index b6069948..f0f56d24 100644 --- a/packages/core/src/container/ReactFlow/Wrapper.tsx +++ b/packages/core/src/container/ReactFlow/Wrapper.tsx @@ -1,10 +1,13 @@ -import { FC, PropsWithChildren, useContext } from 'react'; +import { useContext } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import StoreContext from '../../contexts/RFStoreContext'; import ReactFlowProvider from '../../components/ReactFlowProvider'; const Wrapper: FC = ({ children }) => { - if (useContext(StoreContext)) { + const isWrapped = useContext(StoreContext); + + if (isWrapped) { // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 return <>{children}; From b0302ce4261a992bee841bae84af347d03be690f Mon Sep 17 00:00:00 2001 From: moklick Date: Fri, 25 Nov 2022 22:05:51 +0100 Subject: [PATCH 12/12] chore(changeset): add --- .changeset/perfect-spiders-own.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/perfect-spiders-own.md diff --git a/.changeset/perfect-spiders-own.md b/.changeset/perfect-spiders-own.md new file mode 100644 index 00000000..d565e109 --- /dev/null +++ b/.changeset/perfect-spiders-own.md @@ -0,0 +1,5 @@ +--- +'@reactflow/core': patch +--- + +Wrapper: dont use try catch for checking if provider is available