diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index e1914c6f..eee70449 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -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(null); - const [nodes, setNodes] = useState(initialNodes); - const [edges, setEdges] = useState(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 ( { - const [nodes, setNodes] = useState([]); - const [edges, setEdges] = useState([]); + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [bgColor, setBgColor] = useState(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 ( { - const [nodes, setNodes] = useState(initialNodes); - const [edges, setEdges] = useState(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 ( diff --git a/example/src/Hidden/index.tsx b/example/src/Hidden/index.tsx index 4957c046..384bbe4c 100644 --- a/example/src/Hidden/index.tsx +++ b/example/src/Hidden/index.tsx @@ -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(initialNodes); - const [edges, setEdges] = useState(initialEdges); + const [nodes, setNodes] = useNodesState(initialNodes); + const [edges, setEdges] = useEdgesState(initialEdges); + const [isHidden, setIsHidden] = useState(true); - const onConnect = useCallback((params: Edge | Connection) => { - setEdges((eds) => addEdge(params, eds)); + const onConnect = useCallback((connection: Connection) => { + setEdges((eds) => addEdge(connection, eds)); }, []); useEffect(() => { diff --git a/example/src/Layouting/index.tsx b/example/src/Layouting/index.tsx index 56484640..a4fc53c7 100644 --- a/example/src/Layouting/index.tsx +++ b/example/src/Layouting/index.tsx @@ -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(initialNodesAndEdges.nodes); - const [edges, setEdges] = useState(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 (
diff --git a/example/src/NestedNodes/index.tsx b/example/src/NestedNodes/index.tsx index ed445a64..bf46d158 100644 --- a/example/src/NestedNodes/index.tsx +++ b/example/src/NestedNodes/index.tsx @@ -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(null); - const [nodes, setNodes] = useState(initialNodes); - const [edges, setEdges] = useState(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 ( { const [nodes, setNodes] = useState(initialNodes); const [edges, setEdges] = useState(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)); }, []); diff --git a/example/src/UpdatableEdge/index.tsx b/example/src/UpdatableEdge/index.tsx index dc2cb641..2b814472 100644 --- a/example/src/UpdatableEdge/index.tsx +++ b/example/src/UpdatableEdge/index.tsx @@ -62,9 +62,10 @@ const UpdatableEdge = () => { const [edges, setEdges] = useState(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)); }, []); diff --git a/example/src/UpdateNode/index.tsx b/example/src/UpdateNode/index.tsx index e38babad..4a460dcb 100644 --- a/example/src/UpdateNode/index.tsx +++ b/example/src/UpdateNode/index.tsx @@ -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(initialNodes); - const [edges, setEdges] = useState(initialEdges); + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [nodeName, setNodeName] = useState('Node 1'); const [nodeBg, setNodeBg] = useState('#eee'); @@ -60,12 +60,16 @@ const UpdateNode = () => { ); }, [nodeHidden]); - const onNodesChange = useCallback((changes: NodeChange[]) => { - setNodes((ns) => applyNodeChanges(changes, ns)); - }, []); - return ( - +
setNodeName(evt.target.value)} /> diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index 174caf4f..144ba314 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -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, diff --git a/src/additional-components/index.ts b/src/additional-components/index.ts index 3c96dfb6..3f030a79 100644 --- a/src/additional-components/index.ts +++ b/src/additional-components/index.ts @@ -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'; diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx index f249863c..1697ef64 100644 --- a/src/components/Edges/wrapEdge.tsx +++ b/src/components/Edges/wrapEdge.tsx @@ -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) => { 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) => { const onEdgeClick = useCallback( (event: React.MouseEvent): void => { if (elementsSelectable) { - unsetNodesSelection(); - addSelectedElements([edgeElement]); + setNodesSelectionActive(false); + addSelectedEdges([edgeElement.id]); } onClick?.(event, edgeElement); diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index dc3a495a..be29a0c7 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -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) => { noPanClassName, noDragClassName, }: WrapNodeProps) => { - const { - addSelectedElements, - unselectNodesAndEdges, - unsetNodesSelection, - updateNodePosition, - // updateNodeDimensions, - } = useStore(selector, shallow); + const { addSelectedNodes, unselectNodesAndEdges, setNodesSelectionActive, updateNodePosition } = useStore( + selector, + shallow + ); const nodeElement = useRef(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) => { 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) => { 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) => { // 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); diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index b75c5ea6..115320ee 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -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 (
- onStart(event as MouseEvent)} @@ -124,7 +125,9 @@ export default ({ onContextMenu={onContextMenu} style={innerStyle} /> - +
); -}; +} + +export default memo(NodesSelection); diff --git a/src/components/UserSelection/index.tsx b/src/components/UserSelection/index.tsx index 5ffbef7e..b8692387 100644 --- a/src/components/UserSelection/index.tsx +++ b/src/components/UserSelection/index.tsx @@ -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 ( -
- ); -}; - 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(0); + const prevSelectedEdgesCount = useRef(0); + const containerBounds = useRef(); + const [userSelectionRect, setUserSelectionRect] = useState(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 (
{ onMouseUp={onMouseUp} onMouseLeave={onMouseLeave} > - + {userSelectionRect.draw && ( +
+ )}
); }); diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 216c53d5..536f5005 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -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; } diff --git a/src/container/FlowRenderer/index.tsx b/src/container/FlowRenderer/index.tsx index 458e9c59..02f10bd7 100644 --- a/src/container/FlowRenderer/index.tsx +++ b/src/container/FlowRenderer/index.tsx @@ -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] diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 6bb39015..156f45bb 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -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(); const resizeObserver = useMemo(() => { @@ -73,7 +75,7 @@ const NodeRenderer = (props: NodeRendererProps) => { return (
- {Array.from(nodeInternals).map(([_, node]) => { + {nodes.map((node) => { const nodeType = node.type || 'default'; const internals = nodeInternals.get(node.id); diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index ead31a62..27f283e7 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -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]); diff --git a/src/hooks/useNodesEdgesState.ts b/src/hooks/useNodesEdgesState.ts new file mode 100644 index 00000000..d64abdb7 --- /dev/null +++ b/src/hooks/useNodesEdgesState.ts @@ -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 = (changes: ChangesType[], items: ItemType[]) => ItemType[]; +type OnChange = (changes: ChangesType[]) => void; + +// returns a hook that can be used liked this: +// const [nodes, setNodes, onNodesChange] = useNodesState(intialNodes); +function createUseItemsState( + applyChangesFunction: ApplyChanges +): (initialItems: ItemType[]) => [ItemType[], Dispatch>, OnChange] { + return (initialItems: ItemType[]) => { + const [items, setItems] = useState(initialItems); + + const onItemsChange = useCallback( + (changes: ChangesType[]) => setItems((items) => applyChangesFunction(changes, items)), + [] + ); + + return [items, setItems, onItemsChange]; + }; +} + +export const useNodesState = createUseItemsState(applyNodeChanges as ApplyChanges); +export const useEdgesState = createUseItemsState(applyEdgeChanges as ApplyChanges); diff --git a/src/hooks/useResizeHandler.ts b/src/hooks/useResizeHandler.ts index fc2c830b..12450359 100644 --- a/src/hooks/useResizeHandler.ts +++ b/src/hooks/useResizeHandler.ts @@ -6,7 +6,7 @@ import { ReactFlowState } from '../types'; const updateSizeSelector = (state: ReactFlowState) => state.updateSize; -function useResizeHandler(rendererNode: MutableRefObject) { +function useResizeHandler(rendererNode: MutableRefObject): void { const updateSize = useStore(updateSizeSelector); useEffect(() => { diff --git a/src/hooks/useVisibleNodes.ts b/src/hooks/useVisibleNodes.ts index 4252db59..8bd2e37d 100644 --- a/src/hooks/useVisibleNodes.ts +++ b/src/hooks/useVisibleNodes.ts @@ -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] ) diff --git a/src/hooks/useZoomPanHelper.ts b/src/hooks/useZoomPanHelper.ts index 735ef4b6..ea547f49 100644 --- a/src/hooks/useZoomPanHelper.ts +++ b/src/hooks/useZoomPanHelper.ts @@ -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, diff --git a/src/index.ts b/src/index.ts index 6aa465d3..71856c24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/store/index.ts b/src/store/index.ts index 286b3a49..d346d330 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -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(); -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((set, get) => ({ ...initialState, @@ -98,7 +48,7 @@ const createStore = () => updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { const { onNodesChange, transform, nodeInternals } = get(); - const nodesToChange: NodeChange[] = updates.reduce((res, update) => { + const changes: NodeChange[] = updates.reduce((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) => { - 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 }), diff --git a/src/store/initialState.ts b/src/store/initialState.ts new file mode 100644 index 00000000..9da2acfe --- /dev/null +++ b/src/store/initialState.ts @@ -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; diff --git a/src/store/utils.ts b/src/store/utils.ts index 8247df6b..afb3e063 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -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; @@ -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; +} diff --git a/src/types/general.ts b/src/types/general.ts index b8dc5c15..0f0340f8 100644 --- a/src/types/general.ts +++ b/src/types/general.ts @@ -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 = { 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; diff --git a/src/types/nodes.ts b/src/types/nodes.ts index 76d0cd34..9b876467 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -104,9 +104,9 @@ export type NodeDimensionUpdate = { }; export type NodeInternalsItem = Node & { - positionAbsolute?: XYPosition; - handleBounds?: NodeHandleBounds; + positionAbsolute: XYPosition; z: number; + handleBounds?: NodeHandleBounds; isParent?: boolean; }; diff --git a/src/utils/changes.ts b/src/utils/changes.ts index 64743226..520f3642 100644 --- a/src/utils/changes.ts +++ b/src/utils/changes.ts @@ -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; + }, []); +} diff --git a/src/utils/graph.ts b/src/utils/graph.ts index fdadcd74..6452fff4 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -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[] => {