From 1f41cc0d9a0ae3d27b84189aa0092afed9d879c8 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 13 Oct 2021 16:19:46 +0200 Subject: [PATCH] refactor(zustand): use shallow when needed --- package-lock.json | 7 +- package.json | 1 - src/additional-components/MiniMap/index.tsx | 3 +- src/components/Edges/wrapEdge.tsx | 7 +- src/components/ElementUpdater/index.tsx | 3 +- src/components/Handle/index.tsx | 3 +- src/components/Nodes/wrapNode.tsx | 17 +- src/components/NodesSelection/index.tsx | 40 +--- src/components/SelectionListener/index.tsx | 8 +- src/container/EdgeRenderer/index.tsx | 14 +- src/container/EdgeRenderer/utils.ts | 13 ++ src/container/FlowRenderer/index.tsx | 3 +- src/container/GraphView/index.tsx | 3 +- src/container/NodeRenderer/index.tsx | 17 +- src/container/ZoomPane/index.tsx | 3 +- src/hooks/useGlobalKeyHandler.ts | 13 +- src/hooks/useZoomPanHelper.ts | 3 +- src/store/index.ts | 223 ++++++++------------ src/types/index.ts | 4 +- 19 files changed, 159 insertions(+), 226 deletions(-) diff --git a/package-lock.json b/package-lock.json index a91d8284..76eefe19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "classcat": "^5.0.3", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0", - "fast-deep-equal": "^3.1.3", "react-draggable": "^4.4.4", "react-redux": "^7.2.5", "redux": "^4.1.1", @@ -5285,7 +5284,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-glob": { "version": "3.2.7", @@ -15220,7 +15220,8 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "fast-glob": { "version": "3.2.7", diff --git a/package.json b/package.json index 2f5d6d2c..1aaa2461 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "classcat": "^5.0.3", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0", - "fast-deep-equal": "^3.1.3", "react-draggable": "^4.4.4", "react-redux": "^7.2.5", "redux": "^4.1.1", diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index 1675f6b1..97443452 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -1,5 +1,6 @@ import React, { memo, HTMLAttributes } from 'react'; import cc from 'classcat'; +import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import { getRectOfNodes, getBoundsofRects } from '../../utils/graph'; @@ -34,7 +35,7 @@ const MiniMap = ({ nodeStrokeWidth = 2, maskColor = 'rgb(240, 242, 243, 0.7)', }: MiniMapProps) => { - const { width: containerWidth, height: containerHeight, transform, nodes } = useStore(selector); + const { width: containerWidth, height: containerHeight, transform, nodes } = useStore(selector, shallow); const [tX, tY, tScale] = transform; const mapClasses = cc(['react-flow__minimap', className]); diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx index 4e85d345..ef934e01 100644 --- a/src/components/Edges/wrapEdge.tsx +++ b/src/components/Edges/wrapEdge.tsx @@ -1,5 +1,6 @@ import React, { memo, ComponentType, useCallback, useState, useMemo } from 'react'; import cc from 'classcat'; +import shallow from 'zustand/shallow'; import { useStore, useStoreApi } from '../../store'; import { Edge, EdgeProps, WrapEdgeProps, ReactFlowState } from '../../types'; @@ -56,8 +57,10 @@ export default (EdgeComponent: ComponentType) => { onEdgeUpdateEnd, }: WrapEdgeProps): JSX.Element | null => { const store = useStoreApi(); - const { addSelectedElements, setConnectionNodeId, unsetNodesSelection, setPosition, connectionMode } = - useStore(selector); + const { addSelectedElements, setConnectionNodeId, unsetNodesSelection, setPosition, connectionMode } = useStore( + selector, + shallow + ); const [updating, setUpdating] = useState(false); diff --git a/src/components/ElementUpdater/index.tsx b/src/components/ElementUpdater/index.tsx index 555abc7a..df6f3745 100644 --- a/src/components/ElementUpdater/index.tsx +++ b/src/components/ElementUpdater/index.tsx @@ -1,4 +1,5 @@ import { useEffect } from 'react'; +import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import { Node, Edge, ReactFlowState } from '../../types'; @@ -14,7 +15,7 @@ const selector = (s: ReactFlowState) => ({ }); const ElementUpdater = ({ nodes, edges }: ElementUpdaterProps) => { - const { setNodes, setEdges } = useStore(selector); + const { setNodes, setEdges } = useStore(selector, shallow); useEffect(() => { setNodes(nodes); diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx index e8a152b6..490b1b98 100644 --- a/src/components/Handle/index.tsx +++ b/src/components/Handle/index.tsx @@ -1,5 +1,6 @@ import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react'; import cc from 'classcat'; +import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import NodeIdContext from '../../contexts/NodeIdContext'; @@ -45,7 +46,7 @@ const Handle = forwardRef( onConnectStop, onConnectEnd, connectionMode, - } = useStore(selector); + } = useStore(selector, shallow); const handleId = id || null; const isTarget = type === 'target'; diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 285a011e..d1b9be3c 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react'; import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'; import cc from 'classcat'; +import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import { Provider } from '../../contexts/NodeIdContext'; @@ -10,6 +11,7 @@ const selector = (s: ReactFlowState) => ({ addSelectedElements: s.addSelectedElements, onNodesChange: s.onNodesChange, unsetNodesSelection: s.unsetNodesSelection, + updateNodePosDiff: s.updateNodePosDiff, }); export default (NodeComponent: ComponentType) => { @@ -47,7 +49,7 @@ export default (NodeComponent: ComponentType) => { dragHandle, }: WrapNodeProps) => { // const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); - const { addSelectedElements, onNodesChange, unsetNodesSelection } = useStore(selector); + const { addSelectedElements, onNodesChange, unsetNodesSelection, updateNodePosDiff } = useStore(selector, shallow); const nodeElement = useRef(null); const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]); @@ -154,18 +156,7 @@ export default (NodeComponent: ComponentType) => { onNodeDrag(event as MouseEvent, node); } - onNodesChange?.([ - { - id, - change: { - position: { - x: node.position.x, - y: node.position.y, - }, - isDragging: true, - }, - }, - ]); + updateNodePosDiff({ id, isDragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } }); }, [id, node, onNodeDrag] ); diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index 26a792f0..4518be9e 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -7,7 +7,6 @@ import React, { useMemo, useCallback, useRef, MouseEvent } from 'react'; import ReactDraggable, { DraggableData } from 'react-draggable'; import { useStore } from '../../store'; -import { isNode } from '../../utils/graph'; import { Node, ReactFlowState } from '../../types'; export interface NodesSelectionProps { @@ -21,10 +20,9 @@ const selector = (s: ReactFlowState) => ({ transform: s.transform, selectedNodesBbox: s.selectedNodesBbox, selectionActive: s.selectionActive, - selectedElements: s.selectedElements, + selectedNodes: s.nodes.filter((n) => n.selected), snapToGrid: s.snapToGrid, snapGrid: s.snapGrid, - nodes: s.nodes, updateNodePosDiff: s.updateNodePosDiff, }); @@ -34,37 +32,13 @@ export default ({ onSelectionDragStop, onSelectionContextMenu, }: NodesSelectionProps) => { - const { - transform, - selectedNodesBbox, - selectionActive, - selectedElements, - snapToGrid, - snapGrid, - nodes, - updateNodePosDiff, - } = useStore(selector); + const { transform, selectedNodesBbox, selectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosDiff } = + useStore(selector); const [tX, tY, tScale] = transform; - const nodeRef = useRef(null); const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]); - const selectedNodes = useMemo( - () => - selectedElements - ? selectedElements.filter(isNode).map((selectedNode) => { - const matchingNode = nodes.find((node) => node.id === selectedNode.id); - - return { - ...matchingNode, - position: matchingNode?.position, - } as Node; - }) - : [], - [selectedElements, nodes] - ); - const style = useMemo( () => ({ transform: `translate(${tX}px,${tY}px) scale(${tScale})`, @@ -119,16 +93,12 @@ export default ({ const onContextMenu = useCallback( (event: MouseEvent) => { - const selectedNodes = selectedElements - ? selectedElements.filter(isNode).map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)!) - : []; - onSelectionContextMenu?.(event, selectedNodes); }, - [onSelectionContextMenu] + [onSelectionContextMenu, selectedNodes] ); - if (!selectedElements || selectionActive) { + if (!selectedNodes || selectionActive) { return null; } diff --git a/src/components/SelectionListener/index.tsx b/src/components/SelectionListener/index.tsx index ecda15db..250f9ecb 100644 --- a/src/components/SelectionListener/index.tsx +++ b/src/components/SelectionListener/index.tsx @@ -1,4 +1,5 @@ import { useEffect } from 'react'; +import shallow from 'zustand/shallow'; import { Elements, ReactFlowState } from '../../types'; import { useStore } from '../../store'; @@ -7,12 +8,15 @@ interface SelectionListenerProps { onSelectionChange: (elements: Elements | null) => void; } -const selectedElementsSelector = (s: ReactFlowState) => s.selectedElements; +const selectedElementsSelector = (s: ReactFlowState) => [ + ...s.nodes.filter((n) => n.selected), + ...s.edges.filter((e) => e.selected), +]; // This is just a helper component for calling the onSelectionChange listener. export default ({ onSelectionChange }: SelectionListenerProps) => { - const selectedElements = useStore(selectedElementsSelector); + const selectedElements = useStore(selectedElementsSelector, shallow); useEffect(() => { onSelectionChange(selectedElements); diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 6c922d6d..1157df2b 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -1,15 +1,14 @@ import React, { memo, CSSProperties, useCallback } from 'react'; +import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import ConnectionLine from '../../components/ConnectionLine/index'; -import { isEdge } from '../../utils/graph'; import MarkerDefinitions from './MarkerDefinitions'; import { getEdgePositions, getHandle } from './utils'; import { Position, Edge, Node, - Elements, Connection, ConnectionLineType, ConnectionLineComponent, @@ -55,7 +54,6 @@ interface EdgeWrapperProps { onEdgeUpdate?: OnEdgeUpdateFunc; targetNode?: Node; sourceNode?: Node; - selectedElements: Elements | null; elementsSelectable: boolean; connectionMode?: ConnectionMode; } @@ -77,7 +75,6 @@ const Edge = memo( onEdgeUpdate, targetNode, sourceNode, - selectedElements, elementsSelectable, connectionMode, }: EdgeWrapperProps) => { @@ -152,8 +149,6 @@ const Edge = memo( // return null; // } - const isSelected = selectedElements?.some((elm) => isEdge(elm) && elm.id === edge.id) || false; - return ( ({ connectionHandleId: s.connectionHandleId, connectionHandleType: s.connectionHandleType, connectionPosition: s.connectionPosition, - selectedElements: s.selectedElements, nodesConnectable: s.nodesConnectable, elementsSelectable: s.elementsSelectable, width: s.width, @@ -222,12 +216,11 @@ const EdgeRenderer = (props: EdgeRendererProps) => { connectionHandleId, connectionHandleType, connectionPosition, - selectedElements, nodesConnectable, elementsSelectable, width, height, - } = useStore(selector); + } = useStore(selector, shallow); if (!width) { return null; @@ -247,7 +240,6 @@ const EdgeRenderer = (props: EdgeRendererProps) => { edge={edge} sourceNode={edge.sourceNode} targetNode={edge.targetNode} - selectedElements={selectedElements} elementsSelectable={elementsSelectable} markerEndId={props.markerEndId} onEdgeContextMenu={props.onEdgeContextMenu} diff --git a/src/container/EdgeRenderer/utils.ts b/src/container/EdgeRenderer/utils.ts index 40ea3fe7..b6421f4e 100644 --- a/src/container/EdgeRenderer/utils.ts +++ b/src/container/EdgeRenderer/utils.ts @@ -169,3 +169,16 @@ export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNod { sourceNode: null, targetNode: null } as SourceTargetNode ); }; + +export const extendEdgeWithSourceAndTarget = (edge: Edge, nodes: Node[]): Edge => { + const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes); + + if (sourceNode) { + edge.sourceNode = sourceNode; + } + if (targetNode) { + edge.targetNode = targetNode; + } + + return edge; +}; diff --git a/src/container/FlowRenderer/index.tsx b/src/container/FlowRenderer/index.tsx index 39d951e5..fc105de2 100644 --- a/src/container/FlowRenderer/index.tsx +++ b/src/container/FlowRenderer/index.tsx @@ -1,4 +1,5 @@ import React, { useCallback, memo, ReactNode, WheelEvent, MouseEvent } from 'react'; +import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler'; @@ -60,7 +61,7 @@ const FlowRenderer = ({ onSelectionDragStop, onSelectionContextMenu, }: FlowRendererProps) => { - const { unsetNodesSelection, resetSelectedElements, nodesSelectionActive } = useStore(selector); + const { unsetNodesSelection, resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow); const selectionKeyPressed = useKeyPress(selectionKeyCode); diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 358a9dd3..20e28dc3 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef, memo } from 'react'; +import shallow from 'zustand/shallow'; import { useStore, useStoreApi } from '../../store'; import FlowRenderer from '../FlowRenderer'; @@ -136,7 +137,7 @@ const GraphView = ({ setConnectionMode, setOnNodesChange, setOnEdgesChange, - } = useStore(selector); + } = useStore(selector, shallow); const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper(); diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index cc47f8f3..6dbde07d 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -1,4 +1,5 @@ import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; +import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import { Node, NodeTypesType, ReactFlowState, Edge, WrapNodeProps } from '../../types'; @@ -21,7 +22,6 @@ interface NodeRendererProps { const selector = (s: ReactFlowState) => ({ transform: s.transform, - selectedElements: s.selectedElements, nodesDraggable: s.nodesDraggable, nodesConnectable: s.nodesConnectable, elementsSelectable: s.elementsSelectable, @@ -30,15 +30,10 @@ const selector = (s: ReactFlowState) => ({ }); const NodeRenderer = (props: NodeRendererProps) => { - const { - transform, - selectedElements, - nodesDraggable, - nodesConnectable, - elementsSelectable, - nodes, - updateNodeDimensions, - } = useStore(selector); + const { transform, nodesDraggable, nodesConnectable, elementsSelectable, nodes, updateNodeDimensions } = useStore( + selector, + shallow + ); // const visibleNodes = props.onlyRenderVisibleElements // ? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true) @@ -108,7 +103,7 @@ const NodeRenderer = (props: NodeRendererProps) => { onNodeDrag={props.onNodeDrag} onNodeDragStop={props.onNodeDragStop} scale={transform[2]} - selected={selectedElements?.some(({ id }) => id === node.id) || false} + selected={!!node.selected} isDraggable={isDraggable} isSelectable={isSelectable} isConnectable={isConnectable} diff --git a/src/container/ZoomPane/index.tsx b/src/container/ZoomPane/index.tsx index 6f62473a..3993c017 100644 --- a/src/container/ZoomPane/index.tsx +++ b/src/container/ZoomPane/index.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useRef, ReactNode } from 'react'; import { zoom, zoomIdentity } from 'd3-zoom'; import { select, pointer } from 'd3-selection'; +import shallow from 'zustand/shallow'; import { clamp } from '../../utils'; import useKeyPress from '../../hooks/useKeyPress'; @@ -74,7 +75,7 @@ const ZoomPane = ({ const store = useStoreApi(); const zoomPane = useRef(null); const prevTransform = useRef({ x: 0, y: 0, zoom: 0 }); - const { d3Zoom, d3Selection, d3ZoomHandler, initD3Zoom, updateTransform } = useStore(selector); + const { d3Zoom, d3Selection, d3ZoomHandler, initD3Zoom, updateTransform } = useStore(selector, shallow); const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode); useResizeHandler(zoomPane); diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index dcb9eade..30367672 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -1,8 +1,9 @@ import { useEffect } from 'react'; +import shallow from 'zustand/shallow'; import { useStore, useStoreApi } from '../store'; import useKeyPress from './useKeyPress'; -import { isNode, isEdge, getConnectedEdges } from '../utils/graph'; +import { getConnectedEdges } from '../utils/graph'; import { KeyCode, ReactFlowState } from '../types'; interface HookParams { @@ -21,17 +22,17 @@ const selector = (s: ReactFlowState) => ({ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { const store = useStoreApi(); const { unsetNodesSelection, setMultiSelectionActive, resetSelectedElements, onNodesChange, onEdgesChange } = - useStore(selector); + useStore(selector, shallow); const deleteKeyPressed = useKeyPress(deleteKeyCode); const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); useEffect(() => { - const { edges, selectedElements } = store.getState(); + const { nodes, edges } = store.getState(); + const selectedNodes = nodes.filter((n) => n.selected); + const selectedEdges = edges.filter((e) => e.selected); - if (deleteKeyPressed && selectedElements) { - const selectedNodes = selectedElements.filter(isNode); - const selectedEdges = selectedElements.filter(isEdge); + if (deleteKeyPressed && (selectedNodes || selectedEdges)) { const connectedEdges = getConnectedEdges(selectedNodes, edges); const nodeChanges = selectedNodes.map((n) => ({ id: n.id, delete: true })); diff --git a/src/hooks/useZoomPanHelper.ts b/src/hooks/useZoomPanHelper.ts index 525e1064..37938632 100644 --- a/src/hooks/useZoomPanHelper.ts +++ b/src/hooks/useZoomPanHelper.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react'; import { zoomIdentity } from 'd3-zoom'; +import shallow from 'zustand/shallow'; import { useStoreApi, useStore } from '../store'; import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '../utils/graph'; @@ -26,7 +27,7 @@ const selector = (s: ReactFlowState) => ({ const useZoomPanHelper = (): ZoomPanHelperFunctions => { const store = useStoreApi(); - const { d3Zoom, d3Selection } = useStore(selector); + const { d3Zoom, d3Selection } = useStore(selector, shallow); const zoomPanHelperFunctions = useMemo(() => { if (d3Selection && d3Zoom) { diff --git a/src/store/index.ts b/src/store/index.ts index 9bcfcbad..a9a1d1c7 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,6 +1,5 @@ import create from 'zustand'; import createContext from 'zustand/context'; -import isEqual from 'fast-deep-equal'; import { clampPosition, getDimensions } from '../utils'; import { @@ -26,8 +25,16 @@ import { SnapGrid, OnElementsChange, } from '../types'; -import { parseNode, parseEdge, isNode, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph'; -import { getSourceTargetNodes } from '../container/EdgeRenderer/utils'; +import { + parseNode, + parseEdge, + isNode, + isEdge, + getRectOfNodes, + getNodesInside, + getConnectedEdges, +} from '../utils/graph'; +import { extendEdgeWithSourceAndTarget } from '../container/EdgeRenderer/utils'; import { getHandleBounds } from '../components/Nodes/utils'; const { Provider, useStore, useStoreApi } = createContext(); @@ -42,7 +49,6 @@ const createStore = () => onNodesChange: null, onEdgesChange: null, - selectedElements: null, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, d3Zoom: null, @@ -110,18 +116,7 @@ const createStore = () => return parseNode(propNode, nodeExtent); }); - const updatedEdges = edges.map((edge) => { - const { sourceNode, targetNode } = getSourceTargetNodes(edge, nextNodes); - - if (sourceNode) { - edge.sourceNode = sourceNode; - } - if (targetNode) { - edge.targetNode = targetNode; - } - - return edge; - }); + const updatedEdges = edges.map((edge) => extendEdgeWithSourceAndTarget(edge, nextNodes)); set({ nodes: nextNodes, @@ -137,17 +132,7 @@ const createStore = () => if (storeEdge) { return parseEdge(propEdge); } else { - const parsedEdge = parseEdge(propEdge); - const { sourceNode, targetNode } = getSourceTargetNodes(parsedEdge, nodes); - - if (sourceNode) { - parsedEdge.sourceNode = sourceNode; - } - if (targetNode) { - parsedEdge.targetNode = targetNode; - } - - return parsedEdge; + return extendEdgeWithSourceAndTarget(parseEdge(propEdge), nodes); } }); @@ -190,23 +175,21 @@ const createStore = () => updateNodePosDiff: ({ id, diff, isDragging }: NodeDiffUpdate) => { const { onNodesChange, nodes } = get(); - if (onNodesChange && id && diff) { - const matchingNode = nodes.find((n) => n.id === id); + if (onNodesChange && diff) { + const matchingNodes = nodes.filter((n) => n.id === id || n.selected); - if (matchingNode) { - requestAnimationFrame(() => - onNodesChange([ - { - id, - change: { - position: { - x: matchingNode.position.x + diff.x, - y: matchingNode.position.y + diff.y, - isDragging, - }, + if (matchingNodes?.length) { + onNodesChange( + matchingNodes.map((n) => ({ + id: n.id, + change: { + position: { + x: n.position.x + diff.x, + y: n.position.y + diff.y, + isDragging, }, }, - ]) + })) ); } } @@ -226,7 +209,7 @@ const createStore = () => }); }, updateUserSelection: (mousePos: XYPosition) => { - const { userSelectionRect, nodes, edges, transform, selectedElements } = get(); + const { userSelectionRect, nodes, edges, transform, onNodesChange, onEdgesChange } = get(); const startX = userSelectionRect.startX ?? 0; const startY = userSelectionRect.startY ?? 0; @@ -241,23 +224,21 @@ const createStore = () => const selectedNodes = getNodesInside(nodes, nextUserSelectRect, transform, false, true); const selectedEdges = getConnectedEdges(selectedNodes, edges); - const nextSelectedElements = [...selectedNodes, ...selectedEdges]; - const selectedElementsChanged = !isEqual(nextSelectedElements, selectedElements); - - if (selectedElementsChanged) { - set({ - selectedElements: nextSelectedElements.length > 0 ? nextSelectedElements : null, - userSelectionRect: nextUserSelectRect, - }); - } else { - set({ - userSelectionRect: nextUserSelectRect, - }); + if (selectedNodes?.length) { + onNodesChange?.(selectedNodes.map((n) => ({ id: n.id, change: { selected: true } }))); } + + if (selectedEdges?.length) { + onEdgesChange?.(selectedEdges.map((n) => ({ id: n.id, change: { selected: true } }))); + } + + set({ + userSelectionRect: nextUserSelectRect, + }); }, unsetUserSelection: () => { - const { selectedElements, userSelectionRect } = get(); - const selectedNodes = selectedElements?.filter((node) => isNode(node) && node.position) as Node[]; + const { userSelectionRect, nodes } = get(); + const selectedNodes = nodes.filter((node) => node.selected); const stateUpdate = { selectionActive: false, @@ -265,7 +246,6 @@ const createStore = () => ...userSelectionRect, draw: false, }, - selectedElements: null, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, nodesSelectionActive: false, }; @@ -278,28 +258,29 @@ const createStore = () => set(stateUpdate); }, - setSelectedElements: (elements: Elements) => { - const { selectedElements } = get(); - const selectedElementsArr = Array.isArray(elements) ? elements : [elements]; - const selectedElementsUpdated = !isEqual(selectedElementsArr, selectedElements); - - set({ - selectedElements: selectedElementsUpdated ? selectedElementsArr : selectedElements, - }); - }, addSelectedElements: (elements: Elements) => { - const { multiSelectionActive, selectedElements } = get(); + const { multiSelectionActive, onNodesChange, onEdgesChange, nodes, edges } = get(); const selectedElementsArr = Array.isArray(elements) ? elements : [elements]; - let nextElements = selectedElementsArr; + let selectedNodes; + let selectedEdges; if (multiSelectionActive) { - nextElements = selectedElements ? [...selectedElements, ...selectedElementsArr] : selectedElementsArr; + selectedNodes = selectedElementsArr.filter(isNode).map((node) => ({ id: node.id, change: { selected: true } })); + selectedEdges = selectedElementsArr.filter(isEdge).map((edge) => ({ id: edge.id, change: { selected: true } })); + } else { + selectedNodes = nodes.map((node) => ({ + id: node.id, + change: { selected: selectedElementsArr.some((e) => e.id === node.id) }, + })); + selectedEdges = edges.map((edge) => ({ + id: edge.id, + change: { selected: selectedElementsArr.some((e) => e.id === edge.id) }, + })); } - const selectedElementsUpdated = !isEqual(nextElements, selectedElements); - - set({ selectedElements: selectedElementsUpdated ? nextElements : selectedElements }); + onNodesChange?.(selectedNodes); + onEdgesChange?.(selectedEdges); }, initD3Zoom: ({ d3Zoom, d3Selection, d3ZoomHandler, transform }: InitD3ZoomPayload) => { set({ @@ -334,77 +315,53 @@ const createStore = () => return { ...node, position: clampPosition(node.position, nodeExtent), - __rf: { - ...node.__rf, - }, }; }), }); }, - unsetNodesSelection: () => { - set({ nodesSelectionActive: false }); - }, resetSelectedElements: () => { - set({ selectedElements: null }); + const { nodes, edges, onNodesChange, onEdgesChange } = get(); + const selectedNodes = nodes + .filter((n) => n.selected) + .map((n) => ({ + id: n.id, + change: { selected: false }, + })); + + const selectedEdges = edges + .filter((e) => e.selected) + .map((e) => ({ + id: e.id, + change: { selected: false }, + })); + + onNodesChange?.(selectedNodes); + onEdgesChange?.(selectedEdges); }, - updateTransform: (transform: Transform) => { - set({ transform }); - }, - updateSize: (size: Dimensions) => { - set({ width: size.width || 500, height: size.height || 500 }); - }, - setOnConnect: (onConnect: OnConnectFunc) => { - set({ onConnect }); - }, - setOnConnectStart: (onConnectStart: OnConnectStartFunc) => { - set({ onConnectStart }); - }, - setOnConnectStop: (onConnectStop: OnConnectStopFunc) => { - set({ onConnectStop }); - }, - setOnConnectEnd: (onConnectEnd: OnConnectEndFunc) => { - set({ onConnectEnd }); - }, - setConnectionPosition: (connectionPosition: XYPosition) => { - set({ connectionPosition }); - }, - setConnectionNodeId: (params: SetConnectionId) => { - set({ ...params }); - }, - setSnapToGrid: (snapToGrid: boolean) => { - set({ snapToGrid }); - }, - setSnapGrid: (snapGrid: SnapGrid) => { - set({ snapGrid }); - }, - setInteractive: (isInteractive: boolean) => { + unsetNodesSelection: () => set({ nodesSelectionActive: false }), + updateTransform: (transform: Transform) => set({ transform }), + updateSize: (size: Dimensions) => set({ width: size.width || 500, height: size.height || 500 }), + setOnConnect: (onConnect: OnConnectFunc) => set({ onConnect }), + setOnConnectStart: (onConnectStart: OnConnectStartFunc) => set({ onConnectStart }), + setOnConnectStop: (onConnectStop: OnConnectStopFunc) => set({ onConnectStop }), + setOnConnectEnd: (onConnectEnd: OnConnectEndFunc) => set({ onConnectEnd }), + setConnectionPosition: (connectionPosition: XYPosition) => set({ connectionPosition }), + setConnectionNodeId: (params: SetConnectionId) => set({ ...params }), + setSnapToGrid: (snapToGrid: boolean) => set({ snapToGrid }), + setSnapGrid: (snapGrid: SnapGrid) => set({ snapGrid }), + setInteractive: (isInteractive: boolean) => set({ nodesDraggable: isInteractive, nodesConnectable: isInteractive, elementsSelectable: isInteractive, - }); - }, - setNodesDraggable: (nodesDraggable: boolean) => { - set({ nodesDraggable }); - }, - setNodesConnectable: (nodesConnectable: boolean) => { - set({ nodesConnectable }); - }, - setElementsSelectable: (elementsSelectable: boolean) => { - set({ elementsSelectable }); - }, - setMultiSelectionActive: (multiSelectionActive: boolean) => { - set({ multiSelectionActive }); - }, - setConnectionMode: (connectionMode: ConnectionMode) => { - set({ connectionMode }); - }, - setOnNodesChange: (onNodesChange: OnElementsChange) => { - set({ onNodesChange }); - }, - setOnEdgesChange: (onEdgesChange: OnElementsChange) => { - set({ onEdgesChange }); - }, + }), + setNodesDraggable: (nodesDraggable: boolean) => set({ nodesDraggable }), + setNodesConnectable: (nodesConnectable: boolean) => set({ nodesConnectable }), + setElementsSelectable: (elementsSelectable: boolean) => set({ elementsSelectable }), + setMultiSelectionActive: (multiSelectionActive: boolean) => set({ multiSelectionActive }), + setConnectionMode: (connectionMode: ConnectionMode) => set({ connectionMode }), + setOnNodesChange: (onNodesChange: OnElementsChange) => set({ onNodesChange }), + setOnEdgesChange: (onEdgesChange: OnElementsChange) => set({ onEdgesChange }), })); export { Provider, useStore, createStore, useStoreApi }; diff --git a/src/types/index.ts b/src/types/index.ts index 9974c501..b011b1b5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -60,6 +60,7 @@ export interface Node { width?: number | null; height?: number | null; handleBounds?: any; + selected?: boolean; } export enum ArrowHeadType { @@ -88,6 +89,7 @@ export interface Edge { className?: string; sourceNode?: Node; targetNode?: Node; + selected?: boolean; } export enum BackgroundVariant { @@ -418,7 +420,6 @@ export interface ReactFlowState { transform: Transform; nodes: Node[]; edges: Edge[]; - selectedElements: Elements | null; selectedNodesBbox: Rect; onNodesChange: OnElementsChange | null; onEdgesChange: OnElementsChange | null; @@ -462,7 +463,6 @@ export interface ReactFlowState { unsetUserSelection: () => void; unsetNodesSelection: () => void; resetSelectedElements: () => void; - setSelectedElements: (elements: Elements) => void; addSelectedElements: (elements: Elements) => void; updateTransform: (transform: Transform) => void; updateSize: (size: Dimensions) => void;