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;