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, {
addEdge,
Background,
applyNodeChanges,
applyEdgeChanges,
MiniMap,
Controls,
Node,
Edge,
NodeChange,
EdgeChange,
OnLoadParams,
Connection,
MarkerType,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import DebugNode from './DebugNode';
@@ -108,12 +106,12 @@ const nodeTypes = {
const BasicFlow = () => {
const [rfInstance, setRfInstance] = useState<OnLoadParams | null>(null);
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Edge | Connection) => {
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => {
return addEdge(params, eds);
return addEdge(connection, eds);
});
}, []);
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 (
<ReactFlow
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 ReactFlow, {
@@ -10,11 +10,8 @@ import ReactFlow, {
Position,
SnapGrid,
Connection,
Edge,
NodeChange,
applyNodeChanges,
applyEdgeChanges,
EdgeChange,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import ColorSelectorNode from './ColorSelectorNode';
@@ -35,8 +32,9 @@ const nodeTypes = {
};
const CustomNodeFlow = () => {
const [nodes, setNodes] = useState<Node[]>([]);
const [edges, setEdges] = useState<Edge[]>([]);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [bgColor, setBgColor] = useState<string>(initBgColor);
useEffect(() => {
@@ -100,16 +98,8 @@ const CustomNodeFlow = () => {
]);
}, []);
const onConnect = (params: Connection | Edge) =>
setEdges((eds) => addEdge({ ...params, 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));
}, []);
const onConnect = (connection: Connection) =>
setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds));
return (
<ReactFlow
+7 -19
View File
@@ -1,17 +1,13 @@
import { useState, useCallback } from 'react';
import { useCallback } from 'react';
import ReactFlow, {
addEdge,
Background,
OnLoadParams,
EdgeTypesType,
Node,
Connection,
Edge,
applyNodeChanges,
applyEdgeChanges,
NodeChange,
EdgeChange,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import './style.css';
@@ -29,19 +25,11 @@ const edgeTypes: EdgeTypesType = {
};
const FloatingEdges = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Edge | Connection) => {
setEdges((eds) => addEdge(params, eds));
}, []);
const onNodesChange = useCallback((changes: NodeChange[]) => {
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
setEdges((es) => applyEdgeChanges(changes, es));
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
return (
+15 -5
View File
@@ -1,7 +1,16 @@
import { useState, useCallback } 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[] = [
{ 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 [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const [nodes, setNodes] = useNodesState(initialNodes);
const [edges, setEdges] = useEdgesState(initialEdges);
const [isHidden, setIsHidden] = useState<boolean>(true);
const onConnect = useCallback((params: Edge | Connection) => {
setEdges((eds) => addEdge(params, eds));
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
useEffect(() => {
+8 -16
View File
@@ -1,4 +1,4 @@
import { useState, useCallback } from 'react';
import { useCallback } from 'react';
import ReactFlow, {
ReactFlowProvider,
addEdge,
@@ -7,15 +7,15 @@ import ReactFlow, {
Controls,
NodeChange,
EdgeChange,
Node,
Connection,
Edge,
CoordinateExtent,
Position,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import dagre from 'dagre';
import initialNodesAndEdges from './initial-elements';
import initialItems from './initial-elements';
import './layouting.css';
@@ -28,13 +28,11 @@ const nodeExtent: CoordinateExtent = [
];
const LayoutFlow = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodesAndEdges.nodes);
const [edges, setEdges] = useState<Edge[]>(initialNodesAndEdges.edges);
const [nodes, setNodes, onNodesChange] = useNodesState(initialItems.nodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialItems.edges);
const onConnect = useCallback((params: Edge | Connection) => {
setEdges((eds) => {
return addEdge(params, eds);
});
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
const onLayout = (direction: string) => {
@@ -65,12 +63,6 @@ const LayoutFlow = () => {
setNodes(layoutedNodes);
};
const onNodesChange = useCallback((changes: NodeChange[]) => setNodes((ns) => applyNodeChanges(changes, ns)), []);
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
setEdges((es) => applyEdgeChanges(changes, es));
}, []);
return (
<div className="layoutflow">
<ReactFlowProvider>
+6 -22
View File
@@ -3,14 +3,12 @@ import { useState, MouseEvent, useCallback } from 'react';
import ReactFlow, {
addEdge,
Background,
applyNodeChanges,
applyEdgeChanges,
useNodesState,
useEdgesState,
MiniMap,
Controls,
Node,
Edge,
NodeChange,
EdgeChange,
OnLoadParams,
Connection,
} from 'react-flow-renderer';
@@ -85,19 +83,13 @@ const initialEdges: Edge[] = [
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
];
function shuffle() {
return Math.random() - Math.random();
}
const NestedFlow = () => {
const [rfInstance, setRfInstance] = useState<OnLoadParams | null>(null);
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Edge | Connection) => {
setEdges((eds) => {
return addEdge(params, eds);
});
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
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 (
<ReactFlow
nodes={nodes}
+2 -4
View File
@@ -26,8 +26,8 @@ const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(30, 30);
const StressFlow = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onConnect = useCallback((params: Edge | Connection) => {
setEdges((eds) => addEdge(params, eds));
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
const updatePos = () => {
setNodes((nds) => {
@@ -51,8 +51,6 @@ const StressFlow = () => {
};
const onNodesChange = useCallback((changes: NodeChange[]) => {
console.log('node change', changes);
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
+2 -1
View File
@@ -62,9 +62,10 @@ const UpdatableEdge = () => {
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
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[]) => {
console.log(changes);
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
+13 -9
View File
@@ -1,5 +1,5 @@
import { useEffect, useState, useCallback } from 'react';
import ReactFlow, { Node, Edge, applyNodeChanges, NodeChange } from 'react-flow-renderer';
import { useEffect, useState } from 'react';
import ReactFlow, { Node, Edge, useNodesState, useEdgesState } from 'react-flow-renderer';
import './updatenode.css';
@@ -11,8 +11,8 @@ const initialNodes: Node[] = [
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
const UpdateNode = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const [nodeName, setNodeName] = useState<string>('Node 1');
const [nodeBg, setNodeBg] = useState<string>('#eee');
@@ -60,12 +60,16 @@ const UpdateNode = () => {
);
}, [nodeHidden]);
const onNodesChange = useCallback((changes: NodeChange[]) => {
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
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">
<label>label:</label>
<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 { useStore } from '../../store';
import { getRectOfNodes } from '../../utils/graph';
import { getRectOfNodeInternals } from '../../utils/graph';
import { getBoundsofRects } from '../../utils';
import { Node, ReactFlowState, Rect } from '../../types';
import MiniMapNode from './MiniMapNode';
@@ -55,7 +55,7 @@ const MiniMap = ({
const hasNodes = nodeInternals && nodeInternals.size > 0;
// @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const bb = getRectOfNodes(nodes);
const bb = getRectOfNodeInternals(nodes);
const viewBB: Rect = {
x: -tX / tScale,
y: -tY / tScale,
+1 -1
View File
@@ -1,5 +1,5 @@
// 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 Controls, ControlButton } from './Controls';
+5 -5
View File
@@ -9,9 +9,9 @@ import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph';
const selector = (s: ReactFlowState) => ({
addSelectedElements: s.addSelectedElements,
addSelectedEdges: s.addSelectedEdges,
setConnectionNodeId: s.setConnectionNodeId,
unsetNodesSelection: s.unsetNodesSelection,
setNodesSelectionActive: s.setNodesSelectionActive,
setPosition: s.setConnectionPosition,
connectionMode: s.connectionMode,
});
@@ -57,7 +57,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
markerStart,
}: WrapEdgeProps): JSX.Element | null => {
const store = useStoreApi();
const { addSelectedElements, setConnectionNodeId, unsetNodesSelection, setPosition, connectionMode } = useStore(
const { addSelectedEdges, setConnectionNodeId, setNodesSelectionActive, setPosition, connectionMode } = useStore(
selector,
shallow
);
@@ -99,8 +99,8 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const onEdgeClick = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
if (elementsSelectable) {
unsetNodesSelection();
addSelectedElements([edgeElement]);
setNodesSelectionActive(false);
addSelectedEdges([edgeElement.id]);
}
onClick?.(event, edgeElement);
+46 -66
View File
@@ -8,10 +8,9 @@ import { Provider } from '../../contexts/NodeIdContext';
import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => ({
addSelectedElements: s.addSelectedElements,
unsetNodesSelection: s.unsetNodesSelection,
addSelectedNodes: s.addSelectedNodes,
setNodesSelectionActive: s.setNodesSelectionActive,
updateNodePosition: s.updateNodePosition,
updateNodeDimensions: s.updateNodeDimensions,
unselectNodesAndEdges: s.unselectNodesAndEdges,
});
@@ -53,84 +52,67 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
noPanClassName,
noDragClassName,
}: WrapNodeProps) => {
const {
addSelectedElements,
unselectNodesAndEdges,
unsetNodesSelection,
updateNodePosition,
// updateNodeDimensions,
} = useStore(selector, shallow);
const { addSelectedNodes, unselectNodesAndEdges, setNodesSelectionActive, updateNodePosition } = useStore(
selector,
shallow
);
const nodeElement = useRef<HTMLDivElement>(null);
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 hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const nodeStyle: CSSProperties = useMemo(
() => ({
zIndex,
transform: `translate(${xPos}px,${yPos}px)`,
pointerEvents:
isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none',
pointerEvents: hasPointerEvents ? 'all' : 'none',
// prevents jumping of nodes on start
opacity: isInitialized ? 1 : 0,
...style,
}),
[
xPos,
yPos,
isSelectable,
isDraggable,
onClick,
isInitialized,
style,
onMouseEnter,
onMouseMove,
onMouseLeave,
isParent,
zIndex,
]
[zIndex, xPos, yPos, hasPointerEvents, isInitialized, style]
);
const onMouseEnterHandler = useMemo(() => {
if (!onMouseEnter || dragging) {
return;
}
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]);
return (event: MouseEvent) => onMouseEnter(event, node);
}, [onMouseEnter, dragging, node]);
const onMouseEnterHandler = useCallback(
(event: MouseEvent) => {
if (onMouseEnter && !dragging) {
onMouseEnter(event, node);
}
},
[onMouseEnter, dragging, node]
);
const onMouseMoveHandler = useMemo(() => {
if (!onMouseMove || dragging) {
return;
}
const onMouseMoveHandler = useCallback(
(event: MouseEvent) => {
if (onMouseMove && !dragging) {
onMouseMove(event, node);
}
},
[onMouseMove, dragging, node]
);
return (event: MouseEvent) => onMouseMove(event, node);
}, [onMouseMove, dragging, node]);
const onMouseLeaveHandler = useCallback(
(event: MouseEvent) => {
if (onMouseLeave && !dragging) {
onMouseLeave?.(event, node);
}
},
[onMouseLeave, dragging, node]
);
const onMouseLeaveHandler = useMemo(() => {
if (!onMouseLeave || dragging) {
return;
}
return (event: MouseEvent) => onMouseLeave(event, node);
}, [onMouseLeave, dragging, node]);
const onContextMenuHandler = useMemo(() => {
if (!onContextMenu) {
return;
}
return (event: MouseEvent) => onContextMenu(event, node);
}, [onContextMenu, node]);
const onContextMenuHandler = useCallback(
(event: MouseEvent) => onContextMenu?.(event, node),
[onContextMenu, node]
);
const onSelectNodeHandler = useCallback(
(event: MouseEvent) => {
if (!isDraggable) {
if (isSelectable) {
unsetNodesSelection();
setNodesSelectionActive(false);
if (!selected) {
addSelectedElements([node]);
addSelectedNodes([node.id]);
}
}
@@ -145,14 +127,14 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
onNodeDragStart?.(event as MouseEvent, node);
if (selectNodesOnDrag && isSelectable) {
unsetNodesSelection();
setNodesSelectionActive(false);
if (!selected) {
addSelectedElements([node]);
addSelectedNodes([node.id]);
}
} else if (!selectNodesOnDrag && !selected && isSelectable) {
unselectNodesAndEdges();
unsetNodesSelection();
setNodesSelectionActive(false);
}
},
[node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
@@ -163,9 +145,7 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
node.position.x += draggableData.deltaX;
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 } });
},
@@ -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
if (!dragging) {
if (isSelectable && !selectNodesOnDrag && !selected) {
addSelectedElements([node]);
addSelectedNodes([node.id]);
}
onClick?.(event as MouseEvent, node);
+17 -14
View File
@@ -3,12 +3,13 @@
* made a selectio with on or several nodes
*/
import React, { useMemo, useCallback, useRef, MouseEvent } from 'react';
import ReactDraggable, { DraggableData } from 'react-draggable';
import React, { memo, useMemo, useCallback, useRef, MouseEvent } from 'react';
import { DraggableCore, DraggableData } from 'react-draggable';
import cc from 'classcat';
import { useStore } from '../../store';
import { Node, ReactFlowState } from '../../types';
import { getRectOfNodeInternals } from '../../utils/graph';
export interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
@@ -21,7 +22,7 @@ export interface NodesSelectionProps {
const selector = (s: ReactFlowState) => ({
transform: s.transform,
selectedNodesBbox: s.selectedNodesBbox,
selectionActive: s.selectionActive,
userSelectionActive: s.userSelectionActive,
selectedNodes: Array.from(s.nodeInternals)
.filter(([_, n]) => n.selected)
.map(([_, n]) => n),
@@ -30,14 +31,14 @@ const selector = (s: ReactFlowState) => ({
updateNodePosition: s.updateNodePosition,
});
export default ({
function NodesSelection({
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
noPanClassName,
}: NodesSelectionProps) => {
const { transform, selectedNodesBbox, selectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } =
}: NodesSelectionProps) {
const { transform, userSelectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } =
useStore(selector);
const [tX, tY, tScale] = transform;
const nodeRef = useRef(null);
@@ -51,6 +52,8 @@ export default ({
[tX, tY, tScale]
);
const selectedNodesBbox = useMemo(() => getRectOfNodeInternals(selectedNodes), [selectedNodes]);
const innerStyle = useMemo(
() => ({
width: selectedNodesBbox.width,
@@ -70,10 +73,6 @@ export default ({
const onDrag = useCallback(
(event: MouseEvent, data: DraggableData) => {
if (onSelectionDrag) {
onSelectionDrag(event, selectedNodes);
}
updateNodePosition({
diff: {
x: data.deltaX,
@@ -81,6 +80,8 @@ export default ({
},
dragging: true,
});
onSelectionDrag?.(event, selectedNodes);
},
[onSelectionDrag, selectedNodes, updateNodePosition]
);
@@ -103,13 +104,13 @@ export default ({
[onSelectionContextMenu, selectedNodes]
);
if (!selectedNodes || selectionActive) {
if (!selectedNodes?.length || userSelectionActive) {
return null;
}
return (
<div className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} style={style}>
<ReactDraggable
<DraggableCore
scale={tScale}
grid={grid}
onStart={(event) => onStart(event as MouseEvent)}
@@ -124,7 +125,9 @@ export default ({
onContextMenu={onContextMenu}
style={innerStyle}
/>
</ReactDraggable>
</DraggableCore>
</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
*/
import React, { memo } from 'react';
import React, { memo, useState, useRef, useCallback } from 'react';
import shallow from 'zustand/shallow';
import { useStore } from '../../store';
import { XYPosition, ReactFlowState } from '../../types';
import { useStore, useStoreApi } from '../../store';
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 = {
selectionKeyPressed: boolean;
};
function getMousePosition(event: React.MouseEvent): XYPosition | void {
const reactFlowNode = (event.target as Element).closest('.react-flow');
if (!reactFlowNode) {
return;
}
const containerBounds = reactFlowNode.getBoundingClientRect();
function getMousePosition(event: React.MouseEvent, containerBounds: DOMRect): XYPosition {
return {
x: event.clientX - containerBounds.left,
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) => ({
selectionActive: s.selectionActive,
userSelectionActive: s.userSelectionActive,
elementsSelectable: s.elementsSelectable,
setUserSelection: s.setUserSelection,
updateUserSelection: s.updateUserSelection,
unsetUserSelection: s.unsetUserSelection,
unsetNodesSelection: s.unsetNodesSelection,
setUserSelectionActive: s.setUserSelectionActive,
setNodesSelectionActive: s.setNodesSelectionActive,
});
export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
const {
selectionActive,
elementsSelectable,
setUserSelection,
updateUserSelection,
unsetUserSelection,
unsetNodesSelection,
} = useStore(selector);
const initialRect: SelectionRect = {
startX: 0,
startY: 0,
x: 0,
y: 0,
width: 0,
height: 0,
draw: false,
};
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) {
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 (
<div
className="react-flow__selectionpane react-flow__container"
@@ -108,7 +149,16 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
onMouseUp={onMouseUp}
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>
);
});
+1 -7
View File
@@ -88,13 +88,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals, edge.source);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals, edge.target);
if (!sourceIsValid) {
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}`);
if (!sourceIsValid || !targetIsValid) {
return null;
}
+3 -3
View File
@@ -27,7 +27,7 @@ interface FlowRendererProps
}
const selector = (s: ReactFlowState) => ({
unsetNodesSelection: s.unsetNodesSelection,
setNodesSelectionActive: s.setNodesSelectionActive,
resetSelectedElements: s.resetSelectedElements,
nodesSelectionActive: s.nodesSelectionActive,
});
@@ -62,7 +62,7 @@ const FlowRenderer = ({
noWheelClassName,
noPanClassName,
}: FlowRendererProps) => {
const { unsetNodesSelection, resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow);
const { setNodesSelectionActive, resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow);
const selectionKeyPressed = useKeyPress(selectionKeyCode);
@@ -71,7 +71,7 @@ const FlowRenderer = ({
const onClick = useCallback(
(event: MouseEvent) => {
onPaneClick?.(event);
unsetNodesSelection();
setNodesSelectionActive(false);
resetSelectedElements();
},
[onPaneClick]
+3 -1
View File
@@ -1,5 +1,6 @@
import React, { memo, useMemo, ComponentType, MouseEvent, useEffect, useRef } from 'react';
import shallow from 'zustand/shallow';
import useVisibleNodes from '../../hooks/useVisibleNodes';
import { useStore } from '../../store';
import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types';
@@ -43,6 +44,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
snapToGrid,
nodeInternals,
} = useStore(selector, shallow);
const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
const reseizeObserverRef = useRef<ResizeObserver>();
const resizeObserver = useMemo(() => {
@@ -73,7 +75,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
return (
<div className="react-flow__nodes react-flow__container">
{Array.from(nodeInternals).map(([_, node]) => {
{nodes.map((node) => {
const nodeType = node.type || 'default';
const internals = nodeInternals.get(node.id);
+3 -3
View File
@@ -12,7 +12,7 @@ interface HookParams {
}
const selector = (s: ReactFlowState) => ({
unsetNodesSelection: s.unsetNodesSelection,
setNodesSelectionActive: s.setNodesSelectionActive,
setMultiSelectionActive: s.setMultiSelectionActive,
resetSelectedElements: s.resetSelectedElements,
onNodesChange: s.onNodesChange,
@@ -21,7 +21,7 @@ const selector = (s: ReactFlowState) => ({
export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
const store = useStoreApi();
const { unsetNodesSelection, setMultiSelectionActive, resetSelectedElements, onNodesChange, onEdgesChange } =
const { setNodesSelectionActive, setMultiSelectionActive, resetSelectedElements, onNodesChange, onEdgesChange } =
useStore(selector, shallow);
const deleteKeyPressed = useKeyPress(deleteKeyCode);
@@ -46,7 +46,7 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
onNodesChange?.(nodeChanges);
onEdgesChange?.(edgeChanges);
unsetNodesSelection();
setNodesSelectionActive(false);
resetSelectedElements();
}
}, [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;
function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>) {
function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>): void {
const updateSize = useStore(updateSizeSelector);
useEffect(() => {
+2 -4
View File
@@ -8,11 +8,9 @@ function useVisibleNodes(onlyRenderVisible: boolean) {
const nodes = useStore(
useCallback(
(s: ReactFlowState) => {
// @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(s.nodeInternals).map(([_, node]) => node);
return onlyRenderVisible
? getNodesInside(nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true)
: nodes;
? getNodesInside(s.nodeInternals, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true)
: Array.from(s.nodeInternals).map(([_, node]) => node);
},
[onlyRenderVisible]
)
+4 -2
View File
@@ -3,7 +3,7 @@ import { zoomIdentity } from 'd3-zoom';
import shallow from 'zustand/shallow';
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';
const DEFAULT_PADDING = 0.1;
@@ -48,7 +48,9 @@ const useZoomPanHelper = (): ZoomPanHelperFunctions => {
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(
bounds,
width,
+1
View File
@@ -27,6 +27,7 @@ export { applyNodeChanges, applyEdgeChanges } from './utils/changes';
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper';
export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals';
export * from './hooks/useNodesEdgesState';
export * from './additional-components';
export { useStore, useStoreApi } from './store';
+43 -180
View File
@@ -25,64 +25,14 @@ import {
Transform,
Dimensions,
XYPosition,
ReactFlowStore,
} from '../types';
import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph';
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 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 = () =>
create<ReactFlowState>((set, get) => ({
...initialState,
@@ -98,7 +48,7 @@ const createStore = () =>
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => {
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);
if (node) {
@@ -117,12 +67,11 @@ const createStore = () =>
...dimensions,
});
const change = {
res.push({
id: node.id,
type: 'dimensions',
dimensions,
} as NodeChange;
res.push(change);
} as NodeChange);
}
}
@@ -131,146 +80,59 @@ const createStore = () =>
set({ nodeInternals: new Map(nodeInternals) });
if (nodesToChange?.length > 0) {
onNodesChange?.(nodesToChange);
if (changes?.length > 0) {
onNodesChange?.(changes);
}
},
updateNodePosition: ({ id, diff, dragging }: NodeDiffUpdate) => {
const { onNodesChange, nodeExtent, nodeInternals } = get();
if (onNodesChange) {
const nodes = Array.from(nodeInternals);
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,
};
const changes: NodeDimensionChange[] = [];
if (diff) {
let currentExtent = nodeExtent || node.extent;
nodeInternals.forEach((node) => {
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) {
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(
{
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;
})
);
if (changes?.length) {
onNodesChange(changes);
}
}
},
setUserSelection: (mousePos: XYPosition) => {
set({
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();
addSelectedNodes: (selectedNodeIds: string[]) => {
const { multiSelectionActive, onNodesChange, nodeInternals } = get();
// @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
let changedNodes;
let changedEdges;
if (multiSelectionActive) {
changedNodes = selectedElementsArr.filter(isNode).map(createNodeOrEdgeSelectionChange(true));
changedEdges = selectedElementsArr.filter(isEdge).map(createNodeOrEdgeSelectionChange(true));
changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true));
} else {
changedNodes = nodes.map((node) =>
createNodeOrEdgeSelectionChange(selectedElementsArr.some((e) => e.id === node.id))(node)
);
changedEdges = edges.map((edge) =>
createNodeOrEdgeSelectionChange(selectedElementsArr.some((e) => e.id === edge.id))(edge)
);
changedNodes = getSelectionChanges(nodes, selectedNodeIds);
}
if (changedNodes.length) {
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) {
onEdgesChange?.(changedEdges as EdgeChange[]);
@@ -283,9 +145,9 @@ const createStore = () =>
const nodesToUnselect = nodes.map((n) => {
n.selected = false;
return createNodeOrEdgeSelectionChange(false)(n);
return createSelectionChange(n.id, false);
}) as NodeChange[];
const edgesToUnselect = edges.map(createNodeOrEdgeSelectionChange(false)) as EdgeChange[];
const edgesToUnselect = edges.map((edge) => createSelectionChange(edge.id, false)) as EdgeChange[];
if (nodesToUnselect.length) {
onNodesChange?.(nodesToUnselect);
@@ -324,8 +186,8 @@ const createStore = () =>
const { nodeInternals, edges, onNodesChange, onEdgesChange } = get();
// @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const nodesToUnselect = nodes.filter((e) => e.selected).map(createNodeOrEdgeSelectionChange(false));
const edgesToUnselect = edges.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((e) => createSelectionChange(e.id, false));
if (nodesToUnselect.length) {
onNodesChange?.(nodesToUnselect as NodeChange[]);
@@ -346,7 +208,8 @@ const createStore = () =>
nodeInternals: new Map(nodeInternals),
});
},
unsetNodesSelection: () => set({ nodesSelectionActive: false }),
setUserSelectionActive: (userSelectionActive: boolean) => set({ userSelectionActive }),
setNodesSelectionActive: (nodesSelectionActive: boolean) => set({ nodesSelectionActive }),
updateTransform: (transform: Transform) => set({ transform }),
updateSize: (size: Dimensions) => set({ width: size.width || 500, height: size.height || 500 }),
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 { isNumeric } from '../utils';
import {
CoordinateExtent,
Node,
NodeDimensionChange,
NodeInternals,
NodeInternalsItem,
XYPosition,
XYZPosition,
} from '../types';
import { clampPosition, isNumeric } from '../utils';
type ParentNodes = Record<string, boolean>;
@@ -105,8 +113,62 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals)
return nextNodeInternals;
}
export const createNodeOrEdgeSelectionChange = (selected: boolean) => (item: Node | Edge) => ({
id: item.id,
type: 'select',
selected,
});
export function isParentSelected(node: NodeInternalsItem, nodeInternals: NodeInternals): boolean {
if (!node.parentNode) {
return false;
}
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 interface SelectionRect extends Rect {
startX: number;
startY: number;
draw: boolean;
}
export type OnLoadParams<T = any> = {
zoomIn: () => void;
zoomOut: () => void;
@@ -149,9 +143,7 @@ export type ReactFlowStore = {
nodeExtent: CoordinateExtent;
nodesSelectionActive: boolean;
selectionActive: boolean;
userSelectionRect: SelectionRect;
userSelectionActive: boolean;
connectionNodeId: string | null;
connectionHandleId: string | null;
@@ -176,13 +168,12 @@ export type ReactFlowActions = {
setEdges: (edges: Edge[]) => void;
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
updateNodePosition: (update: NodeDiffUpdate) => void;
setUserSelection: (mousePos: XYPosition) => void;
updateUserSelection: (mousePos: XYPosition) => void;
unsetUserSelection: () => void;
unsetNodesSelection: () => void;
setNodesSelectionActive: (isActive: boolean) => void;
setUserSelectionActive: (isActive: boolean) => void;
resetSelectedElements: () => void;
unselectNodesAndEdges: () => void;
addSelectedElements: (elements: Elements) => void;
addSelectedNodes: (nodeIds: string[]) => void;
addSelectedEdges: (edgeIds: string[]) => void;
updateTransform: (transform: Transform) => void;
updateSize: (size: Dimensions) => void;
initD3Zoom: (payload: InitD3ZoomPayload) => void;
+2 -2
View File
@@ -104,9 +104,9 @@ export type NodeDimensionUpdate = {
};
export type NodeInternalsItem = Node & {
positionAbsolute?: XYPosition;
handleBounds?: NodeHandleBounds;
positionAbsolute: XYPosition;
z: number;
handleBounds?: NodeHandleBounds;
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[] {
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 { 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 =>
'id' in element && 'source' in element && 'target' in element;
@@ -124,6 +134,7 @@ export const pointToRendererPoint = (
return position;
};
// @TODO: use one function for getRectOfNodes and getRectOfNodeInternals
export const getRectOfNodes = (nodes: Node[]): Rect => {
const box = nodes.reduce(
(currBox, { position, width, height }) =>
@@ -134,14 +145,24 @@ export const getRectOfNodes = (nodes: Node[]): Rect => {
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 = (
nodes: Node[],
nodeInternals: NodeInternals,
rect: Rect,
[tx, ty, tScale]: Transform = [0, 0, 1],
partially: boolean = false,
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
excludeNonSelectableNodes: boolean = false
): Node[] => {
): NodeInternalsItem[] => {
const rBox = rectToBox({
x: (rect.x - tx) / tScale,
y: (rect.y - ty) / tScale,
@@ -149,35 +170,32 @@ export const getNodesInside = (
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) {
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 yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y));
const overlappingArea = Math.ceil(xOverlap * yOverlap);
const notInitialized =
typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null || dragging;
if (
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 partiallyVisible = partially && overlappingArea > 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[] => {