Merge pull request #1732 from wbkd/refactor/selection

Refactor/selection
This commit is contained in:
Moritz Klack
2021-11-30 18:23:37 +01:00
committed by GitHub
31 changed files with 532 additions and 520 deletions
+6 -16
View File
@@ -3,17 +3,15 @@ import { useState, MouseEvent, useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
addEdge, addEdge,
Background, Background,
applyNodeChanges,
applyEdgeChanges,
MiniMap, MiniMap,
Controls, Controls,
Node, Node,
Edge, Edge,
NodeChange,
EdgeChange,
OnLoadParams, OnLoadParams,
Connection, Connection,
MarkerType, MarkerType,
useNodesState,
useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import DebugNode from './DebugNode'; import DebugNode from './DebugNode';
@@ -108,12 +106,12 @@ const nodeTypes = {
const BasicFlow = () => { const BasicFlow = () => {
const [rfInstance, setRfInstance] = useState<OnLoadParams | null>(null); const [rfInstance, setRfInstance] = useState<OnLoadParams | null>(null);
const [nodes, setNodes] = useState<Node[]>(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Edge | Connection) => { const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => { setEdges((eds) => {
return addEdge(params, eds); return addEdge(connection, eds);
}); });
}, []); }, []);
const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []); const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []);
@@ -152,14 +150,6 @@ const BasicFlow = () => {
}); });
}; };
const onNodesChange = useCallback((changes: NodeChange[]) => {
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
setEdges((es) => applyEdgeChanges(changes, es));
}, []);
return ( return (
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
+8 -18
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, MouseEvent, useCallback } from 'react'; import { useState, useEffect, MouseEvent } from 'react';
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import ReactFlow, { import ReactFlow, {
@@ -10,11 +10,8 @@ import ReactFlow, {
Position, Position,
SnapGrid, SnapGrid,
Connection, Connection,
Edge, useNodesState,
NodeChange, useEdgesState,
applyNodeChanges,
applyEdgeChanges,
EdgeChange,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import ColorSelectorNode from './ColorSelectorNode'; import ColorSelectorNode from './ColorSelectorNode';
@@ -35,8 +32,9 @@ const nodeTypes = {
}; };
const CustomNodeFlow = () => { const CustomNodeFlow = () => {
const [nodes, setNodes] = useState<Node[]>([]); const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges] = useState<Edge[]>([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [bgColor, setBgColor] = useState<string>(initBgColor); const [bgColor, setBgColor] = useState<string>(initBgColor);
useEffect(() => { useEffect(() => {
@@ -100,16 +98,8 @@ const CustomNodeFlow = () => {
]); ]);
}, []); }, []);
const onConnect = (params: Connection | Edge) => const onConnect = (connection: Connection) =>
setEdges((eds) => addEdge({ ...params, animated: true, style: { stroke: '#fff' } }, eds)); setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds));
const onNodesChange = useCallback((changes: NodeChange[]) => {
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
setEdges((es) => applyEdgeChanges(changes, es));
}, []);
return ( return (
<ReactFlow <ReactFlow
+7 -19
View File
@@ -1,17 +1,13 @@
import { useState, useCallback } from 'react'; import { useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
addEdge, addEdge,
Background, Background,
OnLoadParams, OnLoadParams,
EdgeTypesType, EdgeTypesType,
Node,
Connection, Connection,
Edge, useNodesState,
applyNodeChanges, useEdgesState,
applyEdgeChanges,
NodeChange,
EdgeChange,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import './style.css'; import './style.css';
@@ -29,19 +25,11 @@ const edgeTypes: EdgeTypesType = {
}; };
const FloatingEdges = () => { const FloatingEdges = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Edge | Connection) => { const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(params, eds)); setEdges((eds) => addEdge(connection, eds));
}, []);
const onNodesChange = useCallback((changes: NodeChange[]) => {
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
setEdges((es) => applyEdgeChanges(changes, es));
}, []); }, []);
return ( return (
+15 -5
View File
@@ -1,7 +1,16 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import ReactFlow, { addEdge, MiniMap, Controls, Connection, Edge, Node } from 'react-flow-renderer'; import ReactFlow, {
addEdge,
MiniMap,
Controls,
Connection,
Edge,
Node,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
const initialNodes: Node[] = [ const initialNodes: Node[] = [
{ id: '1', type: 'input', hidden: true, data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, { id: '1', type: 'input', hidden: true, data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
@@ -23,12 +32,13 @@ const setHidden = (hidden: boolean) => (els: any[]) =>
}); });
const HiddenFlow = () => { const HiddenFlow = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes); const [nodes, setNodes] = useNodesState(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges); const [edges, setEdges] = useEdgesState(initialEdges);
const [isHidden, setIsHidden] = useState<boolean>(true); const [isHidden, setIsHidden] = useState<boolean>(true);
const onConnect = useCallback((params: Edge | Connection) => { const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(params, eds)); setEdges((eds) => addEdge(connection, eds));
}, []); }, []);
useEffect(() => { useEffect(() => {
+8 -16
View File
@@ -1,4 +1,4 @@
import { useState, useCallback } from 'react'; import { useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
ReactFlowProvider, ReactFlowProvider,
addEdge, addEdge,
@@ -7,15 +7,15 @@ import ReactFlow, {
Controls, Controls,
NodeChange, NodeChange,
EdgeChange, EdgeChange,
Node,
Connection, Connection,
Edge,
CoordinateExtent, CoordinateExtent,
Position, Position,
useNodesState,
useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import dagre from 'dagre'; import dagre from 'dagre';
import initialNodesAndEdges from './initial-elements'; import initialItems from './initial-elements';
import './layouting.css'; import './layouting.css';
@@ -28,13 +28,11 @@ const nodeExtent: CoordinateExtent = [
]; ];
const LayoutFlow = () => { const LayoutFlow = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodesAndEdges.nodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialItems.nodes);
const [edges, setEdges] = useState<Edge[]>(initialNodesAndEdges.edges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialItems.edges);
const onConnect = useCallback((params: Edge | Connection) => { const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => { setEdges((eds) => addEdge(connection, eds));
return addEdge(params, eds);
});
}, []); }, []);
const onLayout = (direction: string) => { const onLayout = (direction: string) => {
@@ -65,12 +63,6 @@ const LayoutFlow = () => {
setNodes(layoutedNodes); setNodes(layoutedNodes);
}; };
const onNodesChange = useCallback((changes: NodeChange[]) => setNodes((ns) => applyNodeChanges(changes, ns)), []);
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
setEdges((es) => applyEdgeChanges(changes, es));
}, []);
return ( return (
<div className="layoutflow"> <div className="layoutflow">
<ReactFlowProvider> <ReactFlowProvider>
+6 -22
View File
@@ -3,14 +3,12 @@ import { useState, MouseEvent, useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
addEdge, addEdge,
Background, Background,
applyNodeChanges, useNodesState,
applyEdgeChanges, useEdgesState,
MiniMap, MiniMap,
Controls, Controls,
Node, Node,
Edge, Edge,
NodeChange,
EdgeChange,
OnLoadParams, OnLoadParams,
Connection, Connection,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
@@ -85,19 +83,13 @@ const initialEdges: Edge[] = [
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' }, { id: 'e4b1-4b2', source: '4b1', target: '4b2' },
]; ];
function shuffle() {
return Math.random() - Math.random();
}
const NestedFlow = () => { const NestedFlow = () => {
const [rfInstance, setRfInstance] = useState<OnLoadParams | null>(null); const [rfInstance, setRfInstance] = useState<OnLoadParams | null>(null);
const [nodes, setNodes] = useState<Node[]>(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Edge | Connection) => { const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => { setEdges((eds) => addEdge(connection, eds));
return addEdge(params, eds);
});
}, []); }, []);
const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []); const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []);
@@ -135,14 +127,6 @@ const NestedFlow = () => {
}); });
}; };
const onNodesChange = useCallback((changes: NodeChange[]) => {
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
setEdges((es) => applyEdgeChanges(changes, es));
}, []);
return ( return (
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
+2 -4
View File
@@ -26,8 +26,8 @@ const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(30, 30);
const StressFlow = () => { const StressFlow = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes); const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges); const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onConnect = useCallback((params: Edge | Connection) => { const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(params, eds)); setEdges((eds) => addEdge(connection, eds));
}, []); }, []);
const updatePos = () => { const updatePos = () => {
setNodes((nds) => { setNodes((nds) => {
@@ -51,8 +51,6 @@ const StressFlow = () => {
}; };
const onNodesChange = useCallback((changes: NodeChange[]) => { const onNodesChange = useCallback((changes: NodeChange[]) => {
console.log('node change', changes);
setNodes((ns) => applyNodeChanges(changes, ns)); setNodes((ns) => applyNodeChanges(changes, ns));
}, []); }, []);
+2 -1
View File
@@ -62,9 +62,10 @@ const UpdatableEdge = () => {
const [edges, setEdges] = useState<Edge[]>(initialEdges); const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
setEdges((els) => updateEdge(oldEdge, newConnection, els)); setEdges((els) => updateEdge(oldEdge, newConnection, els));
const onConnect = (params: Connection | Edge) => setEdges((els) => addEdge(params, els)); const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
const onNodesChange = useCallback((changes: NodeChange[]) => { const onNodesChange = useCallback((changes: NodeChange[]) => {
console.log(changes);
setNodes((ns) => applyNodeChanges(changes, ns)); setNodes((ns) => applyNodeChanges(changes, ns));
}, []); }, []);
+13 -9
View File
@@ -1,5 +1,5 @@
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState } from 'react';
import ReactFlow, { Node, Edge, applyNodeChanges, NodeChange } from 'react-flow-renderer'; import ReactFlow, { Node, Edge, useNodesState, useEdgesState } from 'react-flow-renderer';
import './updatenode.css'; import './updatenode.css';
@@ -11,8 +11,8 @@ const initialNodes: Node[] = [
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }]; const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
const UpdateNode = () => { const UpdateNode = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const [nodeName, setNodeName] = useState<string>('Node 1'); const [nodeName, setNodeName] = useState<string>('Node 1');
const [nodeBg, setNodeBg] = useState<string>('#eee'); const [nodeBg, setNodeBg] = useState<string>('#eee');
@@ -60,12 +60,16 @@ const UpdateNode = () => {
); );
}, [nodeHidden]); }, [nodeHidden]);
const onNodesChange = useCallback((changes: NodeChange[]) => {
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
return ( return (
<ReactFlow nodes={nodes} edges={edges} defaultZoom={1.5} minZoom={0.2} maxZoom={4} onNodesChange={onNodesChange}> <ReactFlow
nodes={nodes}
edges={edges}
defaultZoom={1.5}
minZoom={0.2}
maxZoom={4}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
>
<div className="updatenode__controls"> <div className="updatenode__controls">
<label>label:</label> <label>label:</label>
<input value={nodeName} onChange={(evt) => setNodeName(evt.target.value)} /> <input value={nodeName} onChange={(evt) => setNodeName(evt.target.value)} />
+2 -2
View File
@@ -3,7 +3,7 @@ import cc from 'classcat';
import shallow from 'zustand/shallow'; import shallow from 'zustand/shallow';
import { useStore } from '../../store'; import { useStore } from '../../store';
import { getRectOfNodes } from '../../utils/graph'; import { getRectOfNodeInternals } from '../../utils/graph';
import { getBoundsofRects } from '../../utils'; import { getBoundsofRects } from '../../utils';
import { Node, ReactFlowState, Rect } from '../../types'; import { Node, ReactFlowState, Rect } from '../../types';
import MiniMapNode from './MiniMapNode'; import MiniMapNode from './MiniMapNode';
@@ -55,7 +55,7 @@ const MiniMap = ({
const hasNodes = nodeInternals && nodeInternals.size > 0; const hasNodes = nodeInternals && nodeInternals.size > 0;
// @TODO: work with nodeInternals instead of converting it to an array // @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(nodeInternals).map(([_, node]) => node); const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const bb = getRectOfNodes(nodes); const bb = getRectOfNodeInternals(nodes);
const viewBB: Rect = { const viewBB: Rect = {
x: -tX / tScale, x: -tX / tScale,
y: -tY / tScale, y: -tY / tScale,
+1 -1
View File
@@ -1,5 +1,5 @@
// These components are not used by React Flow directly // These components are not used by React Flow directly
// but the user can add them as children of a React Flow component // They can be added as children of a React Flow component
export { default as MiniMap } from './MiniMap'; export { default as MiniMap } from './MiniMap';
export { default as Controls, ControlButton } from './Controls'; export { default as Controls, ControlButton } from './Controls';
+5 -5
View File
@@ -9,9 +9,9 @@ import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph'; import { getMarkerId } from '../../utils/graph';
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
addSelectedElements: s.addSelectedElements, addSelectedEdges: s.addSelectedEdges,
setConnectionNodeId: s.setConnectionNodeId, setConnectionNodeId: s.setConnectionNodeId,
unsetNodesSelection: s.unsetNodesSelection, setNodesSelectionActive: s.setNodesSelectionActive,
setPosition: s.setConnectionPosition, setPosition: s.setConnectionPosition,
connectionMode: s.connectionMode, connectionMode: s.connectionMode,
}); });
@@ -57,7 +57,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
markerStart, markerStart,
}: WrapEdgeProps): JSX.Element | null => { }: WrapEdgeProps): JSX.Element | null => {
const store = useStoreApi(); const store = useStoreApi();
const { addSelectedElements, setConnectionNodeId, unsetNodesSelection, setPosition, connectionMode } = useStore( const { addSelectedEdges, setConnectionNodeId, setNodesSelectionActive, setPosition, connectionMode } = useStore(
selector, selector,
shallow shallow
); );
@@ -99,8 +99,8 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const onEdgeClick = useCallback( const onEdgeClick = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => { (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
if (elementsSelectable) { if (elementsSelectable) {
unsetNodesSelection(); setNodesSelectionActive(false);
addSelectedElements([edgeElement]); addSelectedEdges([edgeElement.id]);
} }
onClick?.(event, edgeElement); onClick?.(event, edgeElement);
+46 -66
View File
@@ -8,10 +8,9 @@ import { Provider } from '../../contexts/NodeIdContext';
import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types'; import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
addSelectedElements: s.addSelectedElements, addSelectedNodes: s.addSelectedNodes,
unsetNodesSelection: s.unsetNodesSelection, setNodesSelectionActive: s.setNodesSelectionActive,
updateNodePosition: s.updateNodePosition, updateNodePosition: s.updateNodePosition,
updateNodeDimensions: s.updateNodeDimensions,
unselectNodesAndEdges: s.unselectNodesAndEdges, unselectNodesAndEdges: s.unselectNodesAndEdges,
}); });
@@ -53,84 +52,67 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
noPanClassName, noPanClassName,
noDragClassName, noDragClassName,
}: WrapNodeProps) => { }: WrapNodeProps) => {
const { const { addSelectedNodes, unselectNodesAndEdges, setNodesSelectionActive, updateNodePosition } = useStore(
addSelectedElements, selector,
unselectNodesAndEdges, shallow
unsetNodesSelection, );
updateNodePosition,
// updateNodeDimensions,
} = useStore(selector, shallow);
const nodeElement = useRef<HTMLDivElement>(null); const nodeElement = useRef<HTMLDivElement>(null);
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]);
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
const nodeStyle: CSSProperties = useMemo( const nodeStyle: CSSProperties = useMemo(
() => ({ () => ({
zIndex, zIndex,
transform: `translate(${xPos}px,${yPos}px)`, transform: `translate(${xPos}px,${yPos}px)`,
pointerEvents: pointerEvents: hasPointerEvents ? 'all' : 'none',
isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none',
// prevents jumping of nodes on start // prevents jumping of nodes on start
opacity: isInitialized ? 1 : 0, opacity: isInitialized ? 1 : 0,
...style, ...style,
}), }),
[ [zIndex, xPos, yPos, hasPointerEvents, isInitialized, style]
xPos,
yPos,
isSelectable,
isDraggable,
onClick,
isInitialized,
style,
onMouseEnter,
onMouseMove,
onMouseLeave,
isParent,
zIndex,
]
); );
const onMouseEnterHandler = useMemo(() => { const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]);
if (!onMouseEnter || dragging) { const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
return;
}
return (event: MouseEvent) => onMouseEnter(event, node); const onMouseEnterHandler = useCallback(
}, [onMouseEnter, dragging, node]); (event: MouseEvent) => {
if (onMouseEnter && !dragging) {
onMouseEnter(event, node);
}
},
[onMouseEnter, dragging, node]
);
const onMouseMoveHandler = useMemo(() => { const onMouseMoveHandler = useCallback(
if (!onMouseMove || dragging) { (event: MouseEvent) => {
return; if (onMouseMove && !dragging) {
} onMouseMove(event, node);
}
},
[onMouseMove, dragging, node]
);
return (event: MouseEvent) => onMouseMove(event, node); const onMouseLeaveHandler = useCallback(
}, [onMouseMove, dragging, node]); (event: MouseEvent) => {
if (onMouseLeave && !dragging) {
onMouseLeave?.(event, node);
}
},
[onMouseLeave, dragging, node]
);
const onMouseLeaveHandler = useMemo(() => { const onContextMenuHandler = useCallback(
if (!onMouseLeave || dragging) { (event: MouseEvent) => onContextMenu?.(event, node),
return; [onContextMenu, node]
} );
return (event: MouseEvent) => onMouseLeave(event, node);
}, [onMouseLeave, dragging, node]);
const onContextMenuHandler = useMemo(() => {
if (!onContextMenu) {
return;
}
return (event: MouseEvent) => onContextMenu(event, node);
}, [onContextMenu, node]);
const onSelectNodeHandler = useCallback( const onSelectNodeHandler = useCallback(
(event: MouseEvent) => { (event: MouseEvent) => {
if (!isDraggable) { if (!isDraggable) {
if (isSelectable) { if (isSelectable) {
unsetNodesSelection(); setNodesSelectionActive(false);
if (!selected) { if (!selected) {
addSelectedElements([node]); addSelectedNodes([node.id]);
} }
} }
@@ -145,14 +127,14 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
onNodeDragStart?.(event as MouseEvent, node); onNodeDragStart?.(event as MouseEvent, node);
if (selectNodesOnDrag && isSelectable) { if (selectNodesOnDrag && isSelectable) {
unsetNodesSelection(); setNodesSelectionActive(false);
if (!selected) { if (!selected) {
addSelectedElements([node]); addSelectedNodes([node.id]);
} }
} else if (!selectNodesOnDrag && !selected && isSelectable) { } else if (!selectNodesOnDrag && !selected && isSelectable) {
unselectNodesAndEdges(); unselectNodesAndEdges();
unsetNodesSelection(); setNodesSelectionActive(false);
} }
}, },
[node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart] [node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
@@ -163,9 +145,7 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
node.position.x += draggableData.deltaX; node.position.x += draggableData.deltaX;
node.position.y += draggableData.deltaY; node.position.y += draggableData.deltaY;
if (onNodeDrag) { onNodeDrag?.(event as MouseEvent, node);
onNodeDrag(event as MouseEvent, node);
}
updateNodePosition({ id, dragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } }); updateNodePosition({ id, dragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } });
}, },
@@ -178,7 +158,7 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
// Because of that we set dragging to true inside the onDrag handler and handle the click here // Because of that we set dragging to true inside the onDrag handler and handle the click here
if (!dragging) { if (!dragging) {
if (isSelectable && !selectNodesOnDrag && !selected) { if (isSelectable && !selectNodesOnDrag && !selected) {
addSelectedElements([node]); addSelectedNodes([node.id]);
} }
onClick?.(event as MouseEvent, node); onClick?.(event as MouseEvent, node);
+17 -14
View File
@@ -3,12 +3,13 @@
* made a selectio with on or several nodes * made a selectio with on or several nodes
*/ */
import React, { useMemo, useCallback, useRef, MouseEvent } from 'react'; import React, { memo, useMemo, useCallback, useRef, MouseEvent } from 'react';
import ReactDraggable, { DraggableData } from 'react-draggable'; import { DraggableCore, DraggableData } from 'react-draggable';
import cc from 'classcat'; import cc from 'classcat';
import { useStore } from '../../store'; import { useStore } from '../../store';
import { Node, ReactFlowState } from '../../types'; import { Node, ReactFlowState } from '../../types';
import { getRectOfNodeInternals } from '../../utils/graph';
export interface NodesSelectionProps { export interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
@@ -21,7 +22,7 @@ export interface NodesSelectionProps {
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
transform: s.transform, transform: s.transform,
selectedNodesBbox: s.selectedNodesBbox, selectedNodesBbox: s.selectedNodesBbox,
selectionActive: s.selectionActive, userSelectionActive: s.userSelectionActive,
selectedNodes: Array.from(s.nodeInternals) selectedNodes: Array.from(s.nodeInternals)
.filter(([_, n]) => n.selected) .filter(([_, n]) => n.selected)
.map(([_, n]) => n), .map(([_, n]) => n),
@@ -30,14 +31,14 @@ const selector = (s: ReactFlowState) => ({
updateNodePosition: s.updateNodePosition, updateNodePosition: s.updateNodePosition,
}); });
export default ({ function NodesSelection({
onSelectionDragStart, onSelectionDragStart,
onSelectionDrag, onSelectionDrag,
onSelectionDragStop, onSelectionDragStop,
onSelectionContextMenu, onSelectionContextMenu,
noPanClassName, noPanClassName,
}: NodesSelectionProps) => { }: NodesSelectionProps) {
const { transform, selectedNodesBbox, selectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } = const { transform, userSelectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } =
useStore(selector); useStore(selector);
const [tX, tY, tScale] = transform; const [tX, tY, tScale] = transform;
const nodeRef = useRef(null); const nodeRef = useRef(null);
@@ -51,6 +52,8 @@ export default ({
[tX, tY, tScale] [tX, tY, tScale]
); );
const selectedNodesBbox = useMemo(() => getRectOfNodeInternals(selectedNodes), [selectedNodes]);
const innerStyle = useMemo( const innerStyle = useMemo(
() => ({ () => ({
width: selectedNodesBbox.width, width: selectedNodesBbox.width,
@@ -70,10 +73,6 @@ export default ({
const onDrag = useCallback( const onDrag = useCallback(
(event: MouseEvent, data: DraggableData) => { (event: MouseEvent, data: DraggableData) => {
if (onSelectionDrag) {
onSelectionDrag(event, selectedNodes);
}
updateNodePosition({ updateNodePosition({
diff: { diff: {
x: data.deltaX, x: data.deltaX,
@@ -81,6 +80,8 @@ export default ({
}, },
dragging: true, dragging: true,
}); });
onSelectionDrag?.(event, selectedNodes);
}, },
[onSelectionDrag, selectedNodes, updateNodePosition] [onSelectionDrag, selectedNodes, updateNodePosition]
); );
@@ -103,13 +104,13 @@ export default ({
[onSelectionContextMenu, selectedNodes] [onSelectionContextMenu, selectedNodes]
); );
if (!selectedNodes || selectionActive) { if (!selectedNodes?.length || userSelectionActive) {
return null; return null;
} }
return ( return (
<div className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} style={style}> <div className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} style={style}>
<ReactDraggable <DraggableCore
scale={tScale} scale={tScale}
grid={grid} grid={grid}
onStart={(event) => onStart(event as MouseEvent)} onStart={(event) => onStart(event as MouseEvent)}
@@ -124,7 +125,9 @@ export default ({
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
style={innerStyle} style={innerStyle}
/> />
</ReactDraggable> </DraggableCore>
</div> </div>
); );
}; }
export default memo(NodesSelection);
+127 -77
View File
@@ -2,104 +2,145 @@
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift * The user selection rectangle gets displayed when a user drags the mouse while pressing shift
*/ */
import React, { memo } from 'react'; import React, { memo, useState, useRef, useCallback } from 'react';
import shallow from 'zustand/shallow';
import { useStore } from '../../store'; import { useStore, useStoreApi } from '../../store';
import { XYPosition, ReactFlowState } from '../../types'; import { getSelectionChanges } from '../../utils/changes';
import { XYPosition, ReactFlowState, NodeChange, EdgeChange, Rect } from '../../types';
import { getConnectedEdges, getNodesInside } from '../../utils/graph';
type SelectionRect = Rect & {
startX: number;
startY: number;
draw: boolean;
};
type UserSelectionProps = { type UserSelectionProps = {
selectionKeyPressed: boolean; selectionKeyPressed: boolean;
}; };
function getMousePosition(event: React.MouseEvent): XYPosition | void { function getMousePosition(event: React.MouseEvent, containerBounds: DOMRect): XYPosition {
const reactFlowNode = (event.target as Element).closest('.react-flow');
if (!reactFlowNode) {
return;
}
const containerBounds = reactFlowNode.getBoundingClientRect();
return { return {
x: event.clientX - containerBounds.left, x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top, y: event.clientY - containerBounds.top,
}; };
} }
const userSelectionRectSelector = (state: ReactFlowState) => state.userSelectionRect;
const SelectionRect = () => {
const userSelectionRect = useStore(userSelectionRectSelector);
if (!userSelectionRect.draw) {
return null;
}
return (
<div
className="react-flow__selection react-flow__container"
style={{
width: userSelectionRect.width,
height: userSelectionRect.height,
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
}}
/>
);
};
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
selectionActive: s.selectionActive, userSelectionActive: s.userSelectionActive,
elementsSelectable: s.elementsSelectable, elementsSelectable: s.elementsSelectable,
setUserSelection: s.setUserSelection, setUserSelectionActive: s.setUserSelectionActive,
updateUserSelection: s.updateUserSelection, setNodesSelectionActive: s.setNodesSelectionActive,
unsetUserSelection: s.unsetUserSelection,
unsetNodesSelection: s.unsetNodesSelection,
}); });
export default memo(({ selectionKeyPressed }: UserSelectionProps) => { const initialRect: SelectionRect = {
const { startX: 0,
selectionActive, startY: 0,
elementsSelectable, x: 0,
setUserSelection, y: 0,
updateUserSelection, width: 0,
unsetUserSelection, height: 0,
unsetNodesSelection, draw: false,
} = useStore(selector); };
const renderUserSelectionPane = selectionActive || selectionKeyPressed; export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
const store = useStoreApi();
const prevSelectedNodesCount = useRef<number>(0);
const prevSelectedEdgesCount = useRef<number>(0);
const containerBounds = useRef<DOMRect>();
const [userSelectionRect, setUserSelectionRect] = useState<SelectionRect>(initialRect);
const { userSelectionActive, elementsSelectable, setUserSelectionActive, setNodesSelectionActive } = useStore(
selector,
shallow
);
const renderUserSelectionPane = userSelectionActive || selectionKeyPressed;
const resetUserSelection = useCallback(() => {
setUserSelectionRect(initialRect);
setUserSelectionActive(false);
prevSelectedNodesCount.current = 0;
prevSelectedEdgesCount.current = 0;
}, []);
const onMouseDown = useCallback((event: React.MouseEvent): void => {
const reactFlowNode = (event.target as Element).closest('.react-flow')!;
containerBounds.current = reactFlowNode.getBoundingClientRect();
const mousePos = getMousePosition(event, containerBounds.current!);
setUserSelectionRect({
width: 0,
height: 0,
startX: mousePos.x,
startY: mousePos.y,
x: mousePos.x,
y: mousePos.y,
draw: true,
});
setUserSelectionActive(true);
setNodesSelectionActive(false);
}, []);
const onMouseMove = (event: React.MouseEvent): void => {
if (!selectionKeyPressed || !userSelectionRect.draw || !containerBounds.current) {
return;
}
const mousePos = getMousePosition(event, containerBounds.current!);
const startX = userSelectionRect.startX ?? 0;
const startY = userSelectionRect.startY ?? 0;
const nextUserSelectRect = {
...userSelectionRect,
x: mousePos.x < startX ? mousePos.x : userSelectionRect.x,
y: mousePos.y < startY ? mousePos.y : userSelectionRect.y,
width: Math.abs(mousePos.x - startX),
height: Math.abs(mousePos.y - startY),
};
const { nodeInternals, edges, transform, onNodesChange, onEdgesChange } = store.getState();
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const selectedNodes = getNodesInside(nodeInternals, nextUserSelectRect, transform, false, true);
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id);
if (prevSelectedNodesCount.current !== selectedNodeIds.length) {
prevSelectedNodesCount.current = selectedNodeIds.length;
const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
if (changes.length) {
onNodesChange?.(changes);
}
}
if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) {
prevSelectedEdgesCount.current = selectedEdgeIds.length;
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
if (changes.length) {
onEdgesChange?.(changes);
}
}
setUserSelectionRect(nextUserSelectRect);
};
const onMouseUp = useCallback(() => {
setNodesSelectionActive(prevSelectedNodesCount.current > 0);
resetUserSelection();
}, []);
const onMouseLeave = useCallback(() => {
setNodesSelectionActive(false);
resetUserSelection();
}, []);
if (!elementsSelectable || !renderUserSelectionPane) { if (!elementsSelectable || !renderUserSelectionPane) {
return null; return null;
} }
const onMouseDown = (event: React.MouseEvent): void => {
const mousePos = getMousePosition(event);
if (!mousePos) {
return;
}
setUserSelection(mousePos);
};
const onMouseMove = (event: React.MouseEvent): void => {
if (!selectionKeyPressed || !selectionActive) {
return;
}
const mousePos = getMousePosition(event);
if (!mousePos) {
return;
}
updateUserSelection(mousePos);
};
const onMouseUp = () => unsetUserSelection();
const onMouseLeave = () => {
unsetUserSelection();
unsetNodesSelection();
};
return ( return (
<div <div
className="react-flow__selectionpane react-flow__container" className="react-flow__selectionpane react-flow__container"
@@ -108,7 +149,16 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
onMouseUp={onMouseUp} onMouseUp={onMouseUp}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
> >
<SelectionRect /> {userSelectionRect.draw && (
<div
className="react-flow__selection react-flow__container"
style={{
width: userSelectionRect.width,
height: userSelectionRect.height,
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
}}
/>
)}
</div> </div>
); );
}); });
+1 -7
View File
@@ -88,13 +88,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals, edge.source); const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals, edge.source);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals, edge.target); const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals, edge.target);
if (!sourceIsValid) { if (!sourceIsValid || !targetIsValid) {
console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`);
return null;
}
if (!targetIsValid) {
console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`);
return null; return null;
} }
+3 -3
View File
@@ -27,7 +27,7 @@ interface FlowRendererProps
} }
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
unsetNodesSelection: s.unsetNodesSelection, setNodesSelectionActive: s.setNodesSelectionActive,
resetSelectedElements: s.resetSelectedElements, resetSelectedElements: s.resetSelectedElements,
nodesSelectionActive: s.nodesSelectionActive, nodesSelectionActive: s.nodesSelectionActive,
}); });
@@ -62,7 +62,7 @@ const FlowRenderer = ({
noWheelClassName, noWheelClassName,
noPanClassName, noPanClassName,
}: FlowRendererProps) => { }: FlowRendererProps) => {
const { unsetNodesSelection, resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow); const { setNodesSelectionActive, resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow);
const selectionKeyPressed = useKeyPress(selectionKeyCode); const selectionKeyPressed = useKeyPress(selectionKeyCode);
@@ -71,7 +71,7 @@ const FlowRenderer = ({
const onClick = useCallback( const onClick = useCallback(
(event: MouseEvent) => { (event: MouseEvent) => {
onPaneClick?.(event); onPaneClick?.(event);
unsetNodesSelection(); setNodesSelectionActive(false);
resetSelectedElements(); resetSelectedElements();
}, },
[onPaneClick] [onPaneClick]
+3 -1
View File
@@ -1,5 +1,6 @@
import React, { memo, useMemo, ComponentType, MouseEvent, useEffect, useRef } from 'react'; import React, { memo, useMemo, ComponentType, MouseEvent, useEffect, useRef } from 'react';
import shallow from 'zustand/shallow'; import shallow from 'zustand/shallow';
import useVisibleNodes from '../../hooks/useVisibleNodes';
import { useStore } from '../../store'; import { useStore } from '../../store';
import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types'; import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types';
@@ -43,6 +44,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
snapToGrid, snapToGrid,
nodeInternals, nodeInternals,
} = useStore(selector, shallow); } = useStore(selector, shallow);
const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
const reseizeObserverRef = useRef<ResizeObserver>(); const reseizeObserverRef = useRef<ResizeObserver>();
const resizeObserver = useMemo(() => { const resizeObserver = useMemo(() => {
@@ -73,7 +75,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
return ( return (
<div className="react-flow__nodes react-flow__container"> <div className="react-flow__nodes react-flow__container">
{Array.from(nodeInternals).map(([_, node]) => { {nodes.map((node) => {
const nodeType = node.type || 'default'; const nodeType = node.type || 'default';
const internals = nodeInternals.get(node.id); const internals = nodeInternals.get(node.id);
+3 -3
View File
@@ -12,7 +12,7 @@ interface HookParams {
} }
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
unsetNodesSelection: s.unsetNodesSelection, setNodesSelectionActive: s.setNodesSelectionActive,
setMultiSelectionActive: s.setMultiSelectionActive, setMultiSelectionActive: s.setMultiSelectionActive,
resetSelectedElements: s.resetSelectedElements, resetSelectedElements: s.resetSelectedElements,
onNodesChange: s.onNodesChange, onNodesChange: s.onNodesChange,
@@ -21,7 +21,7 @@ const selector = (s: ReactFlowState) => ({
export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
const store = useStoreApi(); const store = useStoreApi();
const { unsetNodesSelection, setMultiSelectionActive, resetSelectedElements, onNodesChange, onEdgesChange } = const { setNodesSelectionActive, setMultiSelectionActive, resetSelectedElements, onNodesChange, onEdgesChange } =
useStore(selector, shallow); useStore(selector, shallow);
const deleteKeyPressed = useKeyPress(deleteKeyCode); const deleteKeyPressed = useKeyPress(deleteKeyCode);
@@ -46,7 +46,7 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
onNodesChange?.(nodeChanges); onNodesChange?.(nodeChanges);
onEdgesChange?.(edgeChanges); onEdgesChange?.(edgeChanges);
unsetNodesSelection(); setNodesSelectionActive(false);
resetSelectedElements(); resetSelectedElements();
} }
}, [deleteKeyPressed, onNodesChange, onEdgesChange]); }, [deleteKeyPressed, onNodesChange, onEdgesChange]);
+27
View File
@@ -0,0 +1,27 @@
import { useState, useCallback, SetStateAction, Dispatch } from 'react';
import { applyNodeChanges, applyEdgeChanges } from '../utils/changes';
import { Node, NodeChange, Edge, EdgeChange } from '../types';
type ApplyChanges<ItemType, ChangesType> = (changes: ChangesType[], items: ItemType[]) => ItemType[];
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
// returns a hook that can be used liked this:
// const [nodes, setNodes, onNodesChange] = useNodesState(intialNodes);
function createUseItemsState<ItemType, ChangesType>(
applyChangesFunction: ApplyChanges<ItemType, ChangesType>
): (initialItems: ItemType[]) => [ItemType[], Dispatch<SetStateAction<ItemType[]>>, OnChange<ChangesType>] {
return (initialItems: ItemType[]) => {
const [items, setItems] = useState<ItemType[]>(initialItems);
const onItemsChange = useCallback(
(changes: ChangesType[]) => setItems((items) => applyChangesFunction(changes, items)),
[]
);
return [items, setItems, onItemsChange];
};
}
export const useNodesState = createUseItemsState<Node, NodeChange>(applyNodeChanges as ApplyChanges<Node, NodeChange>);
export const useEdgesState = createUseItemsState<Edge, EdgeChange>(applyEdgeChanges as ApplyChanges<Edge, EdgeChange>);
+1 -1
View File
@@ -6,7 +6,7 @@ import { ReactFlowState } from '../types';
const updateSizeSelector = (state: ReactFlowState) => state.updateSize; const updateSizeSelector = (state: ReactFlowState) => state.updateSize;
function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>) { function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>): void {
const updateSize = useStore(updateSizeSelector); const updateSize = useStore(updateSizeSelector);
useEffect(() => { useEffect(() => {
+2 -4
View File
@@ -8,11 +8,9 @@ function useVisibleNodes(onlyRenderVisible: boolean) {
const nodes = useStore( const nodes = useStore(
useCallback( useCallback(
(s: ReactFlowState) => { (s: ReactFlowState) => {
// @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(s.nodeInternals).map(([_, node]) => node);
return onlyRenderVisible return onlyRenderVisible
? getNodesInside(nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true) ? getNodesInside(s.nodeInternals, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true)
: nodes; : Array.from(s.nodeInternals).map(([_, node]) => node);
}, },
[onlyRenderVisible] [onlyRenderVisible]
) )
+4 -2
View File
@@ -3,7 +3,7 @@ import { zoomIdentity } from 'd3-zoom';
import shallow from 'zustand/shallow'; import shallow from 'zustand/shallow';
import { useStoreApi, useStore } from '../store'; import { useStoreApi, useStore } from '../store';
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '../utils/graph'; import { getRectOfNodeInternals, pointToRendererPoint, getTransformForBounds } from '../utils/graph';
import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, ReactFlowState, Rect, XYPosition } from '../types'; import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, ReactFlowState, Rect, XYPosition } from '../types';
const DEFAULT_PADDING = 0.1; const DEFAULT_PADDING = 0.1;
@@ -48,7 +48,9 @@ const useZoomPanHelper = (): ZoomPanHelperFunctions => {
return; return;
} }
const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.hidden)); const bounds = getRectOfNodeInternals(
options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.hidden)
);
const [x, y, zoom] = getTransformForBounds( const [x, y, zoom] = getTransformForBounds(
bounds, bounds,
width, width,
+1
View File
@@ -27,6 +27,7 @@ export { applyNodeChanges, applyEdgeChanges } from './utils/changes';
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'; export { default as useZoomPanHelper } from './hooks/useZoomPanHelper';
export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'; export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals';
export * from './hooks/useNodesEdgesState';
export * from './additional-components'; export * from './additional-components';
export { useStore, useStoreApi } from './store'; export { useStore, useStoreApi } from './store';
+43 -180
View File
@@ -25,64 +25,14 @@ import {
Transform, Transform,
Dimensions, Dimensions,
XYPosition, XYPosition,
ReactFlowStore,
} from '../types'; } from '../types';
import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph';
import { getHandleBounds } from '../components/Nodes/utils'; import { getHandleBounds } from '../components/Nodes/utils';
import { createNodeInternals, createNodeOrEdgeSelectionChange } from './utils'; import { createSelectionChange, getSelectionChanges } from '../utils/changes';
import { createNodeInternals, createPositionChange, isParentSelected } from './utils';
import initialState from './initialState';
const { Provider, useStore, useStoreApi } = createContext<ReactFlowState>(); const { Provider, useStore, useStoreApi } = createContext<ReactFlowState>();
const infiniteExtent: CoordinateExtent = [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
];
const initialState: ReactFlowStore = {
width: 0,
height: 0,
transform: [0, 0, 1],
nodeInternals: new Map(),
edges: [],
onNodesChange: null,
onEdgesChange: null,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
d3Zoom: null,
d3Selection: null,
d3ZoomHandler: undefined,
minZoom: 0.5,
maxZoom: 2,
translateExtent: infiniteExtent,
nodeExtent: infiniteExtent,
nodesSelectionActive: false,
selectionActive: false,
userSelectionRect: {
startX: 0,
startY: 0,
x: 0,
y: 0,
width: 0,
height: 0,
draw: false,
},
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: 'source',
connectionPosition: { x: 0, y: 0 },
connectionMode: ConnectionMode.Strict,
snapGrid: [15, 15],
snapToGrid: false,
nodesDraggable: true,
nodesConnectable: true,
elementsSelectable: true,
multiSelectionActive: false,
reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-',
};
const createStore = () => const createStore = () =>
create<ReactFlowState>((set, get) => ({ create<ReactFlowState>((set, get) => ({
...initialState, ...initialState,
@@ -98,7 +48,7 @@ const createStore = () =>
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { updateNodeDimensions: (updates: NodeDimensionUpdate[]) => {
const { onNodesChange, transform, nodeInternals } = get(); const { onNodesChange, transform, nodeInternals } = get();
const nodesToChange: NodeChange[] = updates.reduce<NodeChange[]>((res, update) => { const changes: NodeChange[] = updates.reduce<NodeChange[]>((res, update) => {
const node = nodeInternals.get(update.id); const node = nodeInternals.get(update.id);
if (node) { if (node) {
@@ -117,12 +67,11 @@ const createStore = () =>
...dimensions, ...dimensions,
}); });
const change = { res.push({
id: node.id, id: node.id,
type: 'dimensions', type: 'dimensions',
dimensions, dimensions,
} as NodeChange; } as NodeChange);
res.push(change);
} }
} }
@@ -131,146 +80,59 @@ const createStore = () =>
set({ nodeInternals: new Map(nodeInternals) }); set({ nodeInternals: new Map(nodeInternals) });
if (nodesToChange?.length > 0) { if (changes?.length > 0) {
onNodesChange?.(nodesToChange); onNodesChange?.(changes);
} }
}, },
updateNodePosition: ({ id, diff, dragging }: NodeDiffUpdate) => { updateNodePosition: ({ id, diff, dragging }: NodeDiffUpdate) => {
const { onNodesChange, nodeExtent, nodeInternals } = get(); const { onNodesChange, nodeExtent, nodeInternals } = get();
if (onNodesChange) { if (onNodesChange) {
const nodes = Array.from(nodeInternals); const changes: NodeDimensionChange[] = [];
const matchingNodes = nodes.filter(([_, n]) => !!(n.selected || n.id === id));
if (matchingNodes?.length) {
onNodesChange(
matchingNodes?.map(([_, node]) => {
const change: NodeDimensionChange = {
id: node.id,
type: 'dimensions',
dragging: !!dragging,
};
if (diff) { nodeInternals.forEach((node) => {
let currentExtent = nodeExtent || node.extent; if (node.selected) {
if (!node.parentNode) {
changes.push(createPositionChange({ node, diff, dragging, nodeExtent, nodeInternals }));
} else if (!isParentSelected(node, nodeInternals)) {
changes.push(createPositionChange({ node, diff, dragging, nodeExtent, nodeInternals }));
}
} else if (node.id === id) {
changes.push(createPositionChange({ node, diff, dragging, nodeExtent, nodeInternals }));
}
});
if (node.extent === 'parent' && node.parentNode && node.width && node.height) { if (changes?.length) {
const parent = nodeInternals.get(node.parentNode); onNodesChange(changes);
currentExtent =
parent?.width && parent?.height
? [
[0, 0],
[parent.width - node.width, parent.height - node.height],
]
: currentExtent;
}
change.position = currentExtent
? clampPosition(
{
x: node.position.x + diff.x,
y: node.position.y + diff.y,
},
currentExtent
)
: { x: node.position.x + diff.x, y: node.position.y + diff.y };
}
return change;
})
);
} }
} }
}, },
setUserSelection: (mousePos: XYPosition) => { addSelectedNodes: (selectedNodeIds: string[]) => {
set({ const { multiSelectionActive, onNodesChange, nodeInternals } = get();
selectionActive: true,
userSelectionRect: {
width: 0,
height: 0,
startX: mousePos.x,
startY: mousePos.y,
x: mousePos.x,
y: mousePos.y,
draw: true,
},
});
},
updateUserSelection: (mousePos: XYPosition) => {
const { userSelectionRect, nodeInternals, edges, transform, onNodesChange, onEdgesChange } = get();
const startX = userSelectionRect.startX ?? 0;
const startY = userSelectionRect.startY ?? 0;
const nextUserSelectRect = {
...userSelectionRect,
x: mousePos.x < startX ? mousePos.x : userSelectionRect.x,
y: mousePos.y < startY ? mousePos.y : userSelectionRect.y,
width: Math.abs(mousePos.x - startX),
height: Math.abs(mousePos.y - startY),
};
// @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const selectedNodes = getNodesInside(nodes, nextUserSelectRect, transform, false, true);
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id);
onNodesChange?.(
nodes.map((n) => createNodeOrEdgeSelectionChange(selectedNodeIds.includes(n.id))(n)) as NodeChange[]
);
onEdgesChange?.(
edges.map((e) => createNodeOrEdgeSelectionChange(selectedEdgeIds.includes(e.id))(e)) as EdgeChange[]
);
set({
userSelectionRect: nextUserSelectRect,
});
},
unsetUserSelection: () => {
const { userSelectionRect, nodeInternals } = get();
// @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const selectedNodes = nodes.filter((node) => node.selected);
const stateUpdate = {
selectionActive: false,
userSelectionRect: {
...userSelectionRect,
draw: false,
},
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
nodesSelectionActive: false,
};
if (selectedNodes && selectedNodes.length > 0) {
const selectedNodesBbox = getRectOfNodes(selectedNodes);
stateUpdate.selectedNodesBbox = selectedNodesBbox;
stateUpdate.nodesSelectionActive = true;
}
set(stateUpdate);
},
addSelectedElements: (selectedElementsArr: Array<Node | Edge>) => {
const { multiSelectionActive, onNodesChange, onEdgesChange, nodeInternals, edges } = get();
// @TODO: work with nodeInternals instead of converting it to an array // @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(nodeInternals).map(([_, node]) => node); const nodes = Array.from(nodeInternals).map(([_, node]) => node);
let changedNodes; let changedNodes;
let changedEdges;
if (multiSelectionActive) { if (multiSelectionActive) {
changedNodes = selectedElementsArr.filter(isNode).map(createNodeOrEdgeSelectionChange(true)); changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true));
changedEdges = selectedElementsArr.filter(isEdge).map(createNodeOrEdgeSelectionChange(true));
} else { } else {
changedNodes = nodes.map((node) => changedNodes = getSelectionChanges(nodes, selectedNodeIds);
createNodeOrEdgeSelectionChange(selectedElementsArr.some((e) => e.id === node.id))(node)
);
changedEdges = edges.map((edge) =>
createNodeOrEdgeSelectionChange(selectedElementsArr.some((e) => e.id === edge.id))(edge)
);
} }
if (changedNodes.length) { if (changedNodes.length) {
onNodesChange?.(changedNodes as NodeChange[]); onNodesChange?.(changedNodes as NodeChange[]);
} }
},
addSelectedEdges: (selectedEdgeIds: string[]) => {
const { multiSelectionActive, onEdgesChange, edges } = get();
let changedEdges;
if (multiSelectionActive) {
changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true));
} else {
changedEdges = getSelectionChanges(edges, selectedEdgeIds);
}
if (changedEdges.length) { if (changedEdges.length) {
onEdgesChange?.(changedEdges as EdgeChange[]); onEdgesChange?.(changedEdges as EdgeChange[]);
@@ -283,9 +145,9 @@ const createStore = () =>
const nodesToUnselect = nodes.map((n) => { const nodesToUnselect = nodes.map((n) => {
n.selected = false; n.selected = false;
return createNodeOrEdgeSelectionChange(false)(n); return createSelectionChange(n.id, false);
}) as NodeChange[]; }) as NodeChange[];
const edgesToUnselect = edges.map(createNodeOrEdgeSelectionChange(false)) as EdgeChange[]; const edgesToUnselect = edges.map((edge) => createSelectionChange(edge.id, false)) as EdgeChange[];
if (nodesToUnselect.length) { if (nodesToUnselect.length) {
onNodesChange?.(nodesToUnselect); onNodesChange?.(nodesToUnselect);
@@ -324,8 +186,8 @@ const createStore = () =>
const { nodeInternals, edges, onNodesChange, onEdgesChange } = get(); const { nodeInternals, edges, onNodesChange, onEdgesChange } = get();
// @TODO: work with nodeInternals instead of converting it to an array // @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(nodeInternals).map(([_, node]) => node); const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const nodesToUnselect = nodes.filter((e) => e.selected).map(createNodeOrEdgeSelectionChange(false)); const nodesToUnselect = nodes.filter((e) => e.selected).map((n) => createSelectionChange(n.id, false));
const edgesToUnselect = edges.filter((e) => e.selected).map(createNodeOrEdgeSelectionChange(false)); const edgesToUnselect = edges.filter((e) => e.selected).map((e) => createSelectionChange(e.id, false));
if (nodesToUnselect.length) { if (nodesToUnselect.length) {
onNodesChange?.(nodesToUnselect as NodeChange[]); onNodesChange?.(nodesToUnselect as NodeChange[]);
@@ -346,7 +208,8 @@ const createStore = () =>
nodeInternals: new Map(nodeInternals), nodeInternals: new Map(nodeInternals),
}); });
}, },
unsetNodesSelection: () => set({ nodesSelectionActive: false }), setUserSelectionActive: (userSelectionActive: boolean) => set({ userSelectionActive }),
setNodesSelectionActive: (nodesSelectionActive: boolean) => set({ nodesSelectionActive }),
updateTransform: (transform: Transform) => set({ transform }), updateTransform: (transform: Transform) => set({ transform }),
updateSize: (size: Dimensions) => set({ width: size.width || 500, height: size.height || 500 }), updateSize: (size: Dimensions) => set({ width: size.width || 500, height: size.height || 500 }),
setOnConnect: (onConnect: OnConnect) => set({ onConnect }), setOnConnect: (onConnect: OnConnect) => set({ onConnect }),
+44
View File
@@ -0,0 +1,44 @@
import { CoordinateExtent, ReactFlowStore, ConnectionMode } from '../types';
const infiniteExtent: CoordinateExtent = [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
];
const initialState: ReactFlowStore = {
width: 0,
height: 0,
transform: [0, 0, 1],
nodeInternals: new Map(),
edges: [],
onNodesChange: null,
onEdgesChange: null,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
d3Zoom: null,
d3Selection: null,
d3ZoomHandler: undefined,
minZoom: 0.5,
maxZoom: 2,
translateExtent: infiniteExtent,
nodeExtent: infiniteExtent,
nodesSelectionActive: false,
userSelectionActive: false,
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: 'source',
connectionPosition: { x: 0, y: 0 },
connectionMode: ConnectionMode.Strict,
snapGrid: [15, 15],
snapToGrid: false,
nodesDraggable: true,
nodesConnectable: true,
elementsSelectable: true,
multiSelectionActive: false,
reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-',
};
export default initialState;
+69 -7
View File
@@ -1,5 +1,13 @@
import { Node, Edge, NodeInternals, NodeInternalsItem, XYZPosition } from '../types'; import {
import { isNumeric } from '../utils'; CoordinateExtent,
Node,
NodeDimensionChange,
NodeInternals,
NodeInternalsItem,
XYPosition,
XYZPosition,
} from '../types';
import { clampPosition, isNumeric } from '../utils';
type ParentNodes = Record<string, boolean>; type ParentNodes = Record<string, boolean>;
@@ -105,8 +113,62 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals)
return nextNodeInternals; return nextNodeInternals;
} }
export const createNodeOrEdgeSelectionChange = (selected: boolean) => (item: Node | Edge) => ({ export function isParentSelected(node: NodeInternalsItem, nodeInternals: NodeInternals): boolean {
id: item.id, if (!node.parentNode) {
type: 'select', return false;
selected, }
});
const parentNode = nodeInternals.get(node.parentNode);
if (!parentNode) {
return false;
}
if (parentNode.selected) {
return true;
}
return isParentSelected(parentNode, nodeInternals);
}
type CreatePostiionChangeParams = {
node: NodeInternalsItem;
nodeExtent: CoordinateExtent;
nodeInternals: NodeInternals;
diff?: XYPosition;
dragging?: boolean;
};
export function createPositionChange({
node,
diff,
dragging,
nodeExtent,
nodeInternals,
}: CreatePostiionChangeParams): NodeDimensionChange {
const change: NodeDimensionChange = {
id: node.id,
type: 'dimensions',
dragging: !!dragging,
};
if (diff) {
const nextPosition = { x: node.position.x + diff.x, y: node.position.y + diff.y };
let currentExtent = nodeExtent || node.extent;
if (node.extent === 'parent' && node.parentNode && node.width && node.height) {
const parent = nodeInternals.get(node.parentNode);
currentExtent =
parent?.width && parent?.height
? [
[0, 0],
[parent.width - node.width, parent.height - node.height],
]
: currentExtent;
}
change.position = currentExtent ? clampPosition(nextPosition, currentExtent) : nextPosition;
}
return change;
}
+5 -14
View File
@@ -24,12 +24,6 @@ export type OnNodesChange = (nodes: NodeChange[]) => void;
export type OnEdgesChange = (nodes: EdgeChange[]) => void; export type OnEdgesChange = (nodes: EdgeChange[]) => void;
export interface SelectionRect extends Rect {
startX: number;
startY: number;
draw: boolean;
}
export type OnLoadParams<T = any> = { export type OnLoadParams<T = any> = {
zoomIn: () => void; zoomIn: () => void;
zoomOut: () => void; zoomOut: () => void;
@@ -149,9 +143,7 @@ export type ReactFlowStore = {
nodeExtent: CoordinateExtent; nodeExtent: CoordinateExtent;
nodesSelectionActive: boolean; nodesSelectionActive: boolean;
selectionActive: boolean; userSelectionActive: boolean;
userSelectionRect: SelectionRect;
connectionNodeId: string | null; connectionNodeId: string | null;
connectionHandleId: string | null; connectionHandleId: string | null;
@@ -176,13 +168,12 @@ export type ReactFlowActions = {
setEdges: (edges: Edge[]) => void; setEdges: (edges: Edge[]) => void;
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
updateNodePosition: (update: NodeDiffUpdate) => void; updateNodePosition: (update: NodeDiffUpdate) => void;
setUserSelection: (mousePos: XYPosition) => void; setNodesSelectionActive: (isActive: boolean) => void;
updateUserSelection: (mousePos: XYPosition) => void; setUserSelectionActive: (isActive: boolean) => void;
unsetUserSelection: () => void;
unsetNodesSelection: () => void;
resetSelectedElements: () => void; resetSelectedElements: () => void;
unselectNodesAndEdges: () => void; unselectNodesAndEdges: () => void;
addSelectedElements: (elements: Elements) => void; addSelectedNodes: (nodeIds: string[]) => void;
addSelectedEdges: (edgeIds: string[]) => void;
updateTransform: (transform: Transform) => void; updateTransform: (transform: Transform) => void;
updateSize: (size: Dimensions) => void; updateSize: (size: Dimensions) => void;
initD3Zoom: (payload: InitD3ZoomPayload) => void; initD3Zoom: (payload: InitD3ZoomPayload) => void;
+2 -2
View File
@@ -104,9 +104,9 @@ export type NodeDimensionUpdate = {
}; };
export type NodeInternalsItem = Node & { export type NodeInternalsItem = Node & {
positionAbsolute?: XYPosition; positionAbsolute: XYPosition;
handleBounds?: NodeHandleBounds;
z: number; z: number;
handleBounds?: NodeHandleBounds;
isParent?: boolean; isParent?: boolean;
}; };
+20
View File
@@ -49,3 +49,23 @@ export function applyNodeChanges(changes: NodeChange[], nodes: Node[]): Node[] {
export function applyEdgeChanges(changes: EdgeChange[], edges: Edge[]): Edge[] { export function applyEdgeChanges(changes: EdgeChange[], edges: Edge[]): Edge[] {
return applyChanges(changes, edges) as Edge[]; return applyChanges(changes, edges) as Edge[];
} }
export const createSelectionChange = (id: string, selected: boolean) => ({
id,
type: 'select',
selected,
});
export function getSelectionChanges(items: any[], selectedIds: string[]) {
return items.reduce((res, item) => {
const willBeSelected = selectedIds.includes(item.id);
if (!item.selected && willBeSelected) {
res.push(createSelectionChange(item.id, true));
} else if (item.selected && !willBeSelected) {
res.push(createSelectionChange(item.id, false));
}
return res;
}, []);
}
+39 -21
View File
@@ -1,6 +1,16 @@
import { boxToRect, clamp, getBoundsOfBoxes, rectToBox } from '../utils'; import { boxToRect, clamp, getBoundsOfBoxes, rectToBox } from '../utils';
import { Node, Edge, Connection, EdgeMarkerType, Transform, XYPosition, Rect } from '../types'; import {
Node,
Edge,
Connection,
EdgeMarkerType,
Transform,
XYPosition,
Rect,
NodeInternals,
NodeInternalsItem,
} from '../types';
export const isEdge = (element: Node | Connection | Edge): element is Edge => export const isEdge = (element: Node | Connection | Edge): element is Edge =>
'id' in element && 'source' in element && 'target' in element; 'id' in element && 'source' in element && 'target' in element;
@@ -124,6 +134,7 @@ export const pointToRendererPoint = (
return position; return position;
}; };
// @TODO: use one function for getRectOfNodes and getRectOfNodeInternals
export const getRectOfNodes = (nodes: Node[]): Rect => { export const getRectOfNodes = (nodes: Node[]): Rect => {
const box = nodes.reduce( const box = nodes.reduce(
(currBox, { position, width, height }) => (currBox, { position, width, height }) =>
@@ -134,14 +145,24 @@ export const getRectOfNodes = (nodes: Node[]): Rect => {
return boxToRect(box); return boxToRect(box);
}; };
export const getRectOfNodeInternals = (nodes: NodeInternalsItem[]): Rect => {
const box = nodes.reduce(
(currBox, { positionAbsolute, width, height }) =>
getBoundsOfBoxes(currBox, rectToBox({ ...positionAbsolute, width: width || 0, height: height || 0 })),
{ x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }
);
return boxToRect(box);
};
export const getNodesInside = ( export const getNodesInside = (
nodes: Node[], nodeInternals: NodeInternals,
rect: Rect, rect: Rect,
[tx, ty, tScale]: Transform = [0, 0, 1], [tx, ty, tScale]: Transform = [0, 0, 1],
partially: boolean = false, partially: boolean = false,
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute // set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
excludeNonSelectableNodes: boolean = false excludeNonSelectableNodes: boolean = false
): Node[] => { ): NodeInternalsItem[] => {
const rBox = rectToBox({ const rBox = rectToBox({
x: (rect.x - tx) / tScale, x: (rect.x - tx) / tScale,
y: (rect.y - ty) / tScale, y: (rect.y - ty) / tScale,
@@ -149,35 +170,32 @@ export const getNodesInside = (
height: rect.height / tScale, height: rect.height / tScale,
}); });
return nodes.filter(({ selectable = true, position, width, height, dragging }) => { const visibleNodes: NodeInternalsItem[] = [];
nodeInternals.forEach((node) => {
const { positionAbsolute, width, height, dragging, selectable = true } = node;
if (excludeNonSelectableNodes && !selectable) { if (excludeNonSelectableNodes && !selectable) {
return false; return false;
} }
const nBox = rectToBox({ ...position, width: width || 0, height: height || 0 }); const nBox = rectToBox({ ...positionAbsolute, width: width || 0, height: height || 0 });
const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x)); const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x));
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y)); const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y));
const overlappingArea = Math.ceil(xOverlap * yOverlap); const overlappingArea = Math.ceil(xOverlap * yOverlap);
const notInitialized =
typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null || dragging;
if ( const partiallyVisible = partially && overlappingArea > 0;
typeof width === 'undefined' ||
typeof height === 'undefined' ||
width === null ||
height === null ||
dragging
) {
// nodes are initialized with width and height = null
return true;
}
if (partially) {
return overlappingArea > 0;
}
const area = (width || 0) * (height || 0); const area = (width || 0) * (height || 0);
const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
return overlappingArea >= area; if (isVisible) {
visibleNodes.push(node);
}
}); });
return visibleNodes;
}; };
export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => { export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {