From be3b451d1d3d13c1150a4272238c0cf2e74ff5bd Mon Sep 17 00:00:00 2001 From: Moritz Klack Date: Tue, 5 Oct 2021 15:27:07 +0200 Subject: [PATCH 001/263] Refactor: add possibility to use different keys or combinations as a key code (#1554) * feat(keys): support for combinations and multiple possibilites for one action * refactor(globalKeyHandler): pass deps to useEffect * refactor(examples): define key codes outside of the component --- example/src/EdgeTypes/index.tsx | 8 +++ src/hooks/useGlobalKeyHandler.ts | 2 +- src/hooks/useKeyPress.ts | 103 ++++++++++++++++++++++++++----- src/types/index.ts | 2 +- src/utils/index.ts | 11 ---- 5 files changed, 97 insertions(+), 29 deletions(-) diff --git a/example/src/EdgeTypes/index.tsx b/example/src/EdgeTypes/index.tsx index 7d87e879..7c87fe7e 100644 --- a/example/src/EdgeTypes/index.tsx +++ b/example/src/EdgeTypes/index.tsx @@ -23,6 +23,9 @@ const onLoad = (reactFlowInstance: OnLoadParams) => { const initialElements = getElements(); +const multiSelectionKeyCode = ['ShiftLeft', 'ShiftRight']; +const deleteKeyCode = ['AltLeft+KeyD', 'Backspace']; + const EdgeTypesFlow = () => { const [elements, setElements] = useState(initialElements); const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); @@ -35,6 +38,11 @@ const EdgeTypesFlow = () => { onElementsRemove={onElementsRemove} onConnect={onConnect} minZoom={0.2} + zoomOnScroll={false} + selectionKeyCode="a+s" + multiSelectionKeyCode={multiSelectionKeyCode} + deleteKeyCode={deleteKeyCode} + zoomActivationKeyCode="z" > diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index c43997a9..340620f5 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -36,7 +36,7 @@ export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: Hook unsetNodesSelection(); resetSelectedElements(); } - }, [deleteKeyPressed]); + }, [deleteKeyPressed, onElementsRemove]); useEffect(() => { setMultiSelectionActive(multiSelectionKeyPressed); diff --git a/src/hooks/useKeyPress.ts b/src/hooks/useKeyPress.ts index 358a6d1a..3b1b94d0 100644 --- a/src/hooks/useKeyPress.ts +++ b/src/hooks/useKeyPress.ts @@ -1,40 +1,111 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef, useMemo } from 'react'; -import { isInputDOMNode } from '../utils'; import { KeyCode } from '../types'; -export default (keyCode?: KeyCode): boolean => { +type Keys = Array; +type PressedKeys = Set; +type KeyOrCode = 'key' | 'code'; +export interface UseKeyPressOptions { + target: Document | HTMLElement | ShadowRoot; +} + +// the keycode can be a string 'a' or an array of strings ['a', 'a+d'] +// a string means a single key 'a' or a combination when '+' is used 'a+d' +// an array means different possibilites. Explainer: ['a', 'd+s'] here the +// user can use the single key 'a' or the combination 'd' + 's' +export default (keyCode: KeyCode | null = null, options: UseKeyPressOptions = { target: document }): boolean => { const [keyPressed, setKeyPressed] = useState(false); - useEffect(() => { - if (typeof keyCode !== 'undefined') { - const downHandler = (event: KeyboardEvent) => { - if (!isInputDOMNode(event) && (event.key === keyCode || event.keyCode === keyCode)) { - event.preventDefault(); + // we need to remember the pressed keys in order to support combinations + const pressedKeys = useRef(new Set([])); + // keyCodes = array with single keys [['a']] or key combinations [['a', 's']] + // keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft'] + // used to check if we store event.code or event.key. When the code is in the list of keysToWatch + // we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft" + // and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when + // we can't find it in the list of keysToWatch. + const [keyCodes, keysToWatch] = useMemo<[Array, Keys]>(() => { + if (keyCode !== null) { + const keyCodeArr = Array.isArray(keyCode) ? keyCode : [keyCode]; + const keys = keyCodeArr.map((kc) => kc.split('+')); + const keysFlat = keys.reduce((res: Keys, item) => res.concat(...item), []); + + return [keys, keysFlat]; + } + + return [[], []]; + }, [keyCode]); + + useEffect(() => { + if (keyCode !== null) { + const downHandler = (event: KeyboardEvent) => { + const keyOrCode = useKeyOrCode(event.code, keysToWatch); + pressedKeys.current.add(event[keyOrCode]); + + if (isMatchingKey(event, keyCodes, pressedKeys.current)) { + event.preventDefault(); setKeyPressed(true); } }; const upHandler = (event: KeyboardEvent) => { - if (!isInputDOMNode(event) && (event.key === keyCode || event.keyCode === keyCode)) { + const keyOrCode = useKeyOrCode(event.code, keysToWatch); + + if (isMatchingKey(event, keyCodes, pressedKeys.current)) { setKeyPressed(false); } + + pressedKeys.current.delete(event[keyOrCode]); }; - const resetHandler = () => setKeyPressed(false); + const resetHandler = () => { + pressedKeys.current.clear(); + setKeyPressed(false); + }; - window.addEventListener('keydown', downHandler); - window.addEventListener('keyup', upHandler); - window.addEventListener('blur', resetHandler); + options.target.addEventListener('keydown', downHandler as EventListenerOrEventListenerObject); + options.target.addEventListener('keyup', upHandler as EventListenerOrEventListenerObject); + options.target.addEventListener('blur', resetHandler); return () => { - window.removeEventListener('keydown', downHandler); - window.removeEventListener('keyup', upHandler); - window.removeEventListener('blur', resetHandler); + pressedKeys.current.clear(); + + options.target.removeEventListener('keydown', downHandler as EventListenerOrEventListenerObject); + options.target.removeEventListener('keyup', upHandler as EventListenerOrEventListenerObject); + options.target.removeEventListener('blur', resetHandler); }; } }, [keyCode, setKeyPressed]); return keyPressed; }; + +// utils + +function isMatchingKey(event: KeyboardEvent, keyCodes: Array, pressedKeys: PressedKeys): boolean { + if (isInputDOMNode(event)) { + return false; + } + + return ( + keyCodes + // we only want to compare same sizes of keyCode definitions + // and pressed keys. When the user specified 'Meta' as a key somewhere + // this would also be truthy without this filter when user presses 'Meta' + 'r' + .filter((keys) => keys.length === pressedKeys.size) + // since we want to support multiple possibilities only one of the + // combinations need to be part of the pressed keys + .some((keys) => keys.every((k) => pressedKeys.has(k))) + ); +} + +function useKeyOrCode(eventCode: string, keysToWatch: KeyCode): KeyOrCode { + return keysToWatch.includes(eventCode) ? 'code' : 'key'; +} + +function isInputDOMNode(e: KeyboardEvent): boolean { + const target = e?.target as HTMLElement; + + return ['INPUT', 'SELECT', 'TEXTAREA'].includes(target?.nodeName) || target?.hasAttribute('contenteditable'); +} diff --git a/src/types/index.ts b/src/types/index.ts index 8bb678ea..754e10e2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -363,7 +363,7 @@ export type FlowTransform = { export type TranslateExtent = [[number, number], [number, number]]; export type NodeExtent = TranslateExtent; -export type KeyCode = number | string; +export type KeyCode = string | Array; export enum PanOnScrollMode { Free = 'free', diff --git a/src/utils/index.ts b/src/utils/index.ts index 77d1eef5..29c3149f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,16 +1,5 @@ -import { DraggableEvent } from 'react-draggable'; -import { MouseEvent as ReactMouseEvent } from 'react'; - import { Dimensions, XYPosition, NodeExtent } from '../types'; -export const isInputDOMNode = (e: ReactMouseEvent | DraggableEvent | KeyboardEvent) => { - const target = e?.target as HTMLElement; - - return ( - ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target?.nodeName) || target?.hasAttribute('contenteditable') - ); -}; - export const getDimensions = (node: HTMLDivElement): Dimensions => ({ width: node.offsetWidth, height: node.offsetHeight, From 32ee96404456f63249dd3ca39512d36892ba4f77 Mon Sep 17 00:00:00 2001 From: moklick Date: Sat, 9 Oct 2021 11:33:10 +0200 Subject: [PATCH 002/263] refactor(elements): split into nodes and edges --- .../MiniMap/MiniMapNode.tsx | 2 +- src/additional-components/MiniMap/index.tsx | 18 +- src/components/ConnectionLine/index.tsx | 16 +- src/components/ElementUpdater/index.tsx | 18 +- src/components/Handle/handler.ts | 8 +- src/components/Handle/index.tsx | 14 +- src/components/Nodes/wrapNode.tsx | 64 ++-- src/components/NodesSelection/index.tsx | 2 +- src/container/EdgeRenderer/index.tsx | 313 +++++++++--------- src/container/EdgeRenderer/utils.ts | 4 +- src/container/FlowRenderer/index.tsx | 3 +- src/container/GraphView/index.tsx | 21 +- src/container/NodeRenderer/index.tsx | 19 +- src/container/ReactFlow/index.tsx | 29 +- src/hooks/useGlobalKeyHandler.ts | 27 +- src/index.ts | 2 + src/store/actions.ts | 17 +- src/store/configure-store.ts | 5 +- src/store/contants.ts | 5 +- src/store/index.ts | 3 + src/store/reducer.ts | 191 ++++++----- src/types/index.ts | 18 +- src/utils/graph.ts | 96 +++--- 23 files changed, 502 insertions(+), 393 deletions(-) diff --git a/src/additional-components/MiniMap/MiniMapNode.tsx b/src/additional-components/MiniMap/MiniMapNode.tsx index d8a48e9e..e879211b 100644 --- a/src/additional-components/MiniMap/MiniMapNode.tsx +++ b/src/additional-components/MiniMap/MiniMapNode.tsx @@ -26,7 +26,7 @@ const MiniMapNode = ({ strokeWidth, className, borderRadius, - shapeRendering + shapeRendering, }: MiniMapNodeProps) => { const { background, backgroundColor } = style || {}; const fill = (color || background || backgroundColor) as string; diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index dbb86f6c..6060b463 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -41,9 +41,9 @@ const MiniMap = ({ const elementWidth = (style?.width || defaultWidth)! as number; const elementHeight = (style?.height || defaultHeight)! as number; const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc; - const nodeStrokeColorFunc = (nodeStrokeColor instanceof Function - ? nodeStrokeColor - : () => nodeStrokeColor) as StringFunc; + const nodeStrokeColorFunc = ( + nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor + ) as StringFunc; const nodeClassNameFunc = (nodeClassName instanceof Function ? nodeClassName : () => nodeClassName) as StringFunc; const hasNodes = nodes && nodes.length; const bb = getRectOfNodes(nodes); @@ -64,7 +64,7 @@ const MiniMap = ({ const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset; const width = viewWidth + offset * 2; const height = viewHeight + offset * 2; - const shapeRendering = (typeof window === "undefined" || !!window.chrome) ? "crispEdges" : "geometricPrecision"; + const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; return ( {nodes - .filter((node) => !node.isHidden) + .filter((node) => !node.isHidden && node.width && node.height) .map((node) => ( { + const nodes = useStoreState((state) => state.nodes); const [sourceNode, setSourceNode] = useState(null); const nodeId = connectionNodeId; const handleId = connectionHandleId; @@ -54,12 +54,12 @@ export default ({ } const sourceHandle = handleId - ? sourceNode.__rf.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId) - : sourceNode.__rf.handleBounds[connectionHandleType][0]; - const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rf.width / 2; - const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rf.height; - const sourceX = sourceNode.__rf.position.x + sourceHandleX; - const sourceY = sourceNode.__rf.position.y + sourceHandleY; + ? sourceNode.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId) + : sourceNode.handleBounds[connectionHandleType][0]; + const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.width! / 2; + const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.height; + const sourceX = sourceNode.position.x + sourceHandleX; + const sourceY = sourceNode.position.y + sourceHandleY; const targetX = (connectionPositionX - transform[0]) / transform[2]; const targetY = (connectionPositionY - transform[1]) / transform[2]; diff --git a/src/components/ElementUpdater/index.tsx b/src/components/ElementUpdater/index.tsx index 5bbe70a6..f40636da 100644 --- a/src/components/ElementUpdater/index.tsx +++ b/src/components/ElementUpdater/index.tsx @@ -1,18 +1,24 @@ import { useEffect } from 'react'; import { useStoreActions } from '../../store/hooks'; -import { Elements } from '../../types'; +import { Node, Edge } from '../../types'; interface ElementUpdaterProps { - elements: Elements; + nodes: Node[]; + edges: Edge[]; } -const ElementUpdater = ({ elements }: ElementUpdaterProps) => { - const setElements = useStoreActions((actions) => actions.setElements); +const ElementUpdater = ({ nodes, edges }: ElementUpdaterProps) => { + const setNodes = useStoreActions((actions) => actions.setNodes); + const setEdges = useStoreActions((actions) => actions.setEdges); useEffect(() => { - setElements(elements); - }, [elements]); + setNodes(nodes); + }, [nodes]); + + useEffect(() => { + setEdges(edges); + }, [edges]); return null; }; diff --git a/src/components/Handle/handler.ts b/src/components/Handle/handler.ts index 03fd37df..f2733a77 100644 --- a/src/components/Handle/handler.ts +++ b/src/components/Handle/handler.ts @@ -1,6 +1,9 @@ import { MouseEvent as ReactMouseEvent } from 'react'; +import { Store } from 'redux'; import { getHostForElement } from '../../utils'; +import { ReactFlowState } from '../../types'; +import { ReactFlowAction } from '../../store/actions'; import { ElementId, @@ -103,7 +106,8 @@ export function onMouseDown( onEdgeUpdateEnd?: (evt: MouseEvent) => void, onConnectStart?: OnConnectStartFunc, onConnectStop?: OnConnectStopFunc, - onConnectEnd?: OnConnectEndFunc + onConnectEnd?: OnConnectEndFunc, + store?: Store ): void { const reactFlowNode = (event.target as Element).closest('.react-flow'); // when react-flow is used inside a shadow root we can't use document @@ -177,7 +181,7 @@ export function onMouseDown( onConnectStop?.(event); if (isValid) { - onConnect?.(connection); + onConnect?.(connection, store?.getState().nodes || []); } onConnectEnd?.(event); diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx index 9a38c550..317efe65 100644 --- a/src/components/Handle/index.tsx +++ b/src/components/Handle/index.tsx @@ -1,9 +1,9 @@ import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react'; import cc from 'classcat'; -import { useStoreActions, useStoreState } from '../../store/hooks'; +import { useStoreActions, useStoreState, useStore } from '../../store/hooks'; import NodeIdContext from '../../contexts/NodeIdContext'; -import { HandleProps, Connection, ElementId, Position } from '../../types'; +import { HandleProps, Connection, ElementId, Position, Node } from '../../types'; import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler'; @@ -26,6 +26,7 @@ const Handle = forwardRef( }, ref ) => { + const store = useStore(); const nodeId = useContext(NodeIdContext) as ElementId; const setPosition = useStoreActions((actions) => actions.setConnectionPosition); const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId); @@ -38,9 +39,9 @@ const Handle = forwardRef( const isTarget = type === 'target'; const onConnectExtended = useCallback( - (params: Connection) => { - onConnectAction?.(params); - onConnect?.(params); + (params: Connection, nodes: Node[]) => { + onConnectAction?.(params, nodes); + onConnect?.(params, nodes); }, [onConnectAction, onConnect] ); @@ -61,7 +62,8 @@ const Handle = forwardRef( undefined, onConnectStart, onConnectStop, - onConnectEnd + onConnectEnd, + store ); }, [ diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index a6902416..04f9acef 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -1,18 +1,8 @@ -import React, { - useEffect, - useLayoutEffect, - useRef, - memo, - ComponentType, - CSSProperties, - useMemo, - MouseEvent, - useCallback, -} from 'react'; +import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react'; import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'; import cc from 'classcat'; -import { useStoreActions } from '../../store/hooks'; +import { useStoreActions, useStoreState } from '../../store/hooks'; import { Provider } from '../../contexts/NodeIdContext'; import { NodeComponentProps, WrapNodeProps } from '../../types'; @@ -50,9 +40,9 @@ export default (NodeComponent: ComponentType) => { resizeObserver, dragHandle, }: WrapNodeProps) => { - const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); + // const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); - const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff); + const onNodesChange = useStoreState((state) => state.onNodesChange); const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); const nodeElement = useRef(null); @@ -84,6 +74,7 @@ export default (NodeComponent: ComponentType) => { onMouseLeave, ] ); + const onMouseEnterHandler = useMemo(() => { if (!onMouseEnter || isDragging) { return; @@ -153,20 +144,25 @@ export default (NodeComponent: ComponentType) => { const onDrag = useCallback( (event: DraggableEvent, draggableData: DraggableData) => { + node.position.x += draggableData.deltaX; + node.position.y += draggableData.deltaY; + if (onNodeDrag) { - node.position.x += draggableData.deltaX; - node.position.y += draggableData.deltaY; onNodeDrag(event as MouseEvent, node); } - updateNodePosDiff({ - id, - diff: { - x: draggableData.deltaX, - y: draggableData.deltaY, + onNodesChange?.([ + { + id, + change: { + position: { + x: node.position.x, + y: node.position.y, + }, + isDragging: true, + }, }, - isDragging: true, - }); + ]); }, [id, node, onNodeDrag] ); @@ -185,10 +181,14 @@ export default (NodeComponent: ComponentType) => { return; } - updateNodePosDiff({ - id: node.id, - isDragging: false, - }); + onNodesChange?.([ + { + id: node.id, + change: { + isDragging: true, + }, + }, + ]); onNodeDragStop?.(event as MouseEvent, node); }, @@ -202,11 +202,11 @@ export default (NodeComponent: ComponentType) => { [node, onNodeDoubleClick] ); - useLayoutEffect(() => { - if (nodeElement.current && !isHidden) { - updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); - } - }, [id, isHidden, sourcePosition, targetPosition]); + // useEffect(() => { + // if (nodeElement.current && !isHidden) { + // updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); + // } + // }, [id, isHidden, sourcePosition, targetPosition]); useEffect(() => { if (nodeElement.current) { diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index efb780e7..1b3c3257 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -45,7 +45,7 @@ export default ({ return { ...matchingNode, - position: matchingNode?.__rf.position, + position: matchingNode?.position, } as Node; }) : [], diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 1fd7f766..3554d2f8 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -4,7 +4,7 @@ import { useStoreState } from '../../store/hooks'; import ConnectionLine from '../../components/ConnectionLine/index'; import { isEdge } from '../../utils/graph'; import MarkerDefinitions from './MarkerDefinitions'; -import { getEdgePositions, getHandle, isEdgeVisible, getSourceTargetNodes } from './utils'; +import { getEdgePositions, getHandle } from './utils'; import { Position, Edge, @@ -14,7 +14,6 @@ import { ConnectionLineType, ConnectionLineComponent, ConnectionMode, - Transform, OnEdgeUpdateFunc, } from '../../types'; @@ -41,151 +40,167 @@ interface EdgeRendererProps { interface EdgeWrapperProps { edge: Edge; - props: EdgeRendererProps; - nodes: Node[]; + edgeTypes: any; + markerEndId?: string; + onElementClick?: (event: React.MouseEvent, element: Node | Edge) => void; + onEdgeContextMenu?: (event: React.MouseEvent, edge: Edge) => void; + onEdgeMouseEnter?: (event: React.MouseEvent, edge: Edge) => void; + onEdgeMouseMove?: (event: React.MouseEvent, edge: Edge) => void; + onEdgeMouseLeave?: (event: React.MouseEvent, edge: Edge) => void; + edgeUpdaterRadius?: number; + onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void; + onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge) => void; + onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; + onEdgeUpdate?: OnEdgeUpdateFunc; + targetNode?: Node; + sourceNode?: Node; selectedElements: Elements | null; elementsSelectable: boolean; - transform: Transform; - width: number; - height: number; - onlyRenderVisibleElements: boolean; connectionMode?: ConnectionMode; } -const Edge = ({ - edge, - props, - nodes, - selectedElements, - elementsSelectable, - transform, - width, - height, - onlyRenderVisibleElements, - connectionMode, -}: EdgeWrapperProps) => { - const sourceHandleId = edge.sourceHandle || null; - const targetHandleId = edge.targetHandle || null; - const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes); - - const onConnectEdge = useCallback( - (connection: Connection) => { - props.onEdgeUpdate?.(edge, connection); - }, - [edge, props.onEdgeUpdate] - ); - - if (!sourceNode) { - console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`); - return null; - } - - if (!targetNode) { - console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`); - return null; - } - - // source and target node need to be initialized - if (!sourceNode.__rf.width || !targetNode.__rf.width) { - return null; - } - - const edgeType = edge.type || 'default'; - const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default; - const targetNodeBounds = targetNode.__rf.handleBounds; - // when connection type is loose we can define all handles as sources - const targetNodeHandles = - connectionMode === ConnectionMode.Strict - ? targetNodeBounds.target - : targetNodeBounds.target || targetNodeBounds.source; - const sourceHandle = getHandle(sourceNode.__rf.handleBounds.source, sourceHandleId); - const targetHandle = getHandle(targetNodeHandles, targetHandleId); - const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom; - const targetPosition = targetHandle ? targetHandle.position : Position.Top; - - if (!sourceHandle) { - console.warn(`couldn't create edge for source handle id: ${sourceHandleId}; edge id: ${edge.id}`); - return null; - } - - if (!targetHandle) { - console.warn(`couldn't create edge for target handle id: ${targetHandleId}; edge id: ${edge.id}`); - return null; - } - - const { sourceX, sourceY, targetX, targetY } = getEdgePositions( - sourceNode, - sourceHandle, - sourcePosition, +const Edge = memo( + ({ + edge, + edgeTypes, + markerEndId, + onElementClick, + onEdgeContextMenu, + onEdgeMouseEnter, + onEdgeMouseMove, + onEdgeMouseLeave, + edgeUpdaterRadius, + onEdgeDoubleClick, + onEdgeUpdateStart, + onEdgeUpdateEnd, + onEdgeUpdate, targetNode, - targetHandle, - targetPosition - ); + sourceNode, + selectedElements, + elementsSelectable, + connectionMode, + }: EdgeWrapperProps) => { + const sourceHandleId = edge.sourceHandle || null; + const targetHandleId = edge.targetHandle || null; - const isVisible = onlyRenderVisibleElements - ? isEdgeVisible({ - sourcePos: { x: sourceX, y: sourceY }, - targetPos: { x: targetX, y: targetY }, - width, - height, - transform, - }) - : true; + const onConnectEdge = useCallback( + (connection: Connection) => { + onEdgeUpdate?.(edge, connection); + }, + [edge, onEdgeUpdate] + ); - if (!isVisible) { - return null; + if (!sourceNode) { + console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`); + return null; + } + + if (!targetNode) { + console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`); + return null; + } + + // source and target node need to be initialized + if (!sourceNode.width || !targetNode.width) { + return null; + } + + const edgeType = edge.type || 'default'; + const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default; + const targetNodeBounds = targetNode.handleBounds; + // when connection type is loose we can define all handles as sources + const targetNodeHandles = + connectionMode === ConnectionMode.Strict + ? targetNodeBounds.target + : targetNodeBounds.target || targetNodeBounds.source; + const sourceHandle = getHandle(sourceNode.handleBounds.source, sourceHandleId); + const targetHandle = getHandle(targetNodeHandles, targetHandleId); + const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom; + const targetPosition = targetHandle ? targetHandle.position : Position.Top; + + if (!sourceHandle) { + console.warn(`couldn't create edge for source handle id: ${sourceHandleId}; edge id: ${edge.id}`); + return null; + } + + if (!targetHandle) { + console.warn(`couldn't create edge for target handle id: ${targetHandleId}; edge id: ${edge.id}`); + return null; + } + + const { sourceX, sourceY, targetX, targetY } = getEdgePositions( + sourceNode, + sourceHandle, + sourcePosition, + targetNode, + targetHandle, + targetPosition + ); + + // const isVisible = onlyRenderVisibleElements + // ? isEdgeVisible({ + // sourcePos: { x: sourceX, y: sourceY }, + // targetPos: { x: targetX, y: targetY }, + // width, + // height, + // transform, + // }) + // : true; + + // if (!isVisible) { + // return null; + // } + + const isSelected = selectedElements?.some((elm) => isEdge(elm) && elm.id === edge.id) || false; + + return ( + + ); } - - const isSelected = selectedElements?.some((elm) => isEdge(elm) && elm.id === edge.id) || false; - - return ( - - ); -}; +); const EdgeRenderer = (props: EdgeRendererProps) => { const transform = useStoreState((state) => state.transform); - const nodes = useStoreState((state) => state.nodes); const edges = useStoreState((state) => state.edges); const connectionNodeId = useStoreState((state) => state.connectionNodeId); const connectionHandleId = useStoreState((state) => state.connectionHandleId); @@ -201,13 +216,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { return null; } - const { - connectionLineType, - arrowHeadColor, - connectionLineStyle, - connectionLineComponent, - onlyRenderVisibleElements, - } = props; + const { connectionLineType, arrowHeadColor, connectionLineStyle, connectionLineComponent } = props; const transformStyle = `translate(${transform[0]},${transform[1]}) scale(${transform[2]})`; const renderConnectionLine = connectionNodeId && connectionHandleType; @@ -219,19 +228,25 @@ const EdgeRenderer = (props: EdgeRendererProps) => { ))} {renderConnectionLine && ( { diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index e002f366..efcfe4e5 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -11,7 +11,7 @@ import { ReactFlowProps } from '../ReactFlow'; import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types'; -export interface GraphViewProps extends Omit { +export interface GraphViewProps extends Omit { nodeTypes: NodeTypesType; edgeTypes: EdgeTypesType; selectionKeyCode: KeyCode; @@ -55,7 +55,6 @@ const GraphView = ({ selectionKeyCode, multiSelectionKeyCode, zoomActivationKeyCode, - onElementsRemove, deleteKeyCode, onConnect, onConnectStart, @@ -95,6 +94,8 @@ const GraphView = ({ edgeUpdaterRadius, onEdgeUpdateStart, onEdgeUpdateEnd, + onNodesChange, + onEdgesChange, }: GraphViewProps) => { const isInitialized = useRef(false); const setOnConnect = useStoreActions((actions) => actions.setOnConnect); @@ -111,6 +112,9 @@ const GraphView = ({ const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent); const setNodeExtent = useStoreActions((actions) => actions.setNodeExtent); const setConnectionMode = useStoreActions((actions) => actions.setConnectionMode); + const setOnNodesChange = useStoreActions((actions) => actions.setOnNodesChange); + const setOnEdgesChange = useStoreActions((actions) => actions.setOnEdgesChange); + const currentStore = useStore(); const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper(); @@ -217,12 +221,23 @@ const GraphView = ({ } }, [connectionMode]); + useEffect(() => { + if (typeof onNodesChange !== 'undefined') { + setOnNodesChange(onNodesChange); + } + }, [onNodesChange]); + + useEffect(() => { + if (typeof onEdgesChange !== 'undefined') { + setOnEdgesChange(onEdgesChange); + } + }, [onEdgesChange]); + return ( { const nodesDraggable = useStoreState((state) => state.nodesDraggable); const nodesConnectable = useStoreState((state) => state.nodesConnectable); const elementsSelectable = useStoreState((state) => state.elementsSelectable); - const width = useStoreState((state) => state.width); - const height = useStoreState((state) => state.height); const nodes = useStoreState((state) => state.nodes); const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); - const visibleNodes = props.onlyRenderVisibleElements - ? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true) - : nodes; + // const visibleNodes = props.onlyRenderVisibleElements + // ? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true) + // : nodes; const transformStyle = useMemo( () => ({ @@ -59,7 +56,7 @@ const NodeRenderer = (props: NodeRendererProps) => { return (
- {visibleNodes.map((node) => { + {nodes.map((node) => { const nodeType = node.type || 'default'; const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; @@ -82,10 +79,10 @@ const NodeRenderer = (props: NodeRendererProps) => { sourcePosition={node.sourcePosition} targetPosition={node.targetPosition} isHidden={node.isHidden} - xPos={node.__rf.position.x} - yPos={node.__rf.position.y} - isDragging={node.__rf.isDragging} - isInitialized={node.__rf.width !== null && node.__rf.height !== null} + xPos={node.position.x} + yPos={node.position.y} + isDragging={node.isDragging} + isInitialized={node.width !== null && node.height !== null} snapGrid={props.snapGrid} snapToGrid={props.snapToGrid} selectNodesOnDrag={props.selectNodesOnDrag} diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index 66dbe234..b6bcc6a3 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -25,7 +25,6 @@ import { OnLoadFunc, Node, Edge, - Connection, ConnectionMode, ConnectionLineType, ConnectionLineComponent, @@ -33,11 +32,13 @@ import { OnConnectStartFunc, OnConnectStopFunc, OnConnectEndFunc, + OnConnectFunc, TranslateExtent, KeyCode, PanOnScrollMode, OnEdgeUpdateFunc, NodeExtent, + ElementChange, } from '../../types'; import '../../style.css'; @@ -57,9 +58,11 @@ const defaultEdgeTypes = { }; export interface ReactFlowProps extends Omit, 'onLoad'> { - elements: Elements; + nodes: Node[]; + edges: Edge[]; + onNodesChange?: (nodeChanges: ElementChange[]) => void; + onEdgesChange?: (edgeChanges: ElementChange[]) => void; onElementClick?: (event: ReactMouseEvent, element: Node | Edge) => void; - onElementsRemove?: (elements: Elements) => void; onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void; onNodeMouseEnter?: (event: ReactMouseEvent, node: Node) => void; onNodeMouseMove?: (event: ReactMouseEvent, node: Node) => void; @@ -68,7 +71,7 @@ export interface ReactFlowProps extends Omit, 'on onNodeDragStart?: (event: ReactMouseEvent, node: Node) => void; onNodeDrag?: (event: ReactMouseEvent, node: Node) => void; onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void; - onConnect?: (connection: Edge | Connection) => void; + onConnect?: OnConnectFunc; onConnectStart?: OnConnectStartFunc; onConnectStop?: OnConnectStopFunc; onConnectEnd?: OnConnectEndFunc; @@ -132,10 +135,14 @@ export interface ReactFlowProps extends Omit, 'on export type ReactFlowRefType = HTMLDivElement; +const initSnapGrid: [number, number] = [15, 15]; +const initDefaultPosition: [number, number] = [0, 0]; + const ReactFlow = forwardRef( ( { - elements = [], + nodes = [], + edges = [], className, nodeTypes = defaultNodeTypes, edgeTypes = defaultEdgeTypes, @@ -144,7 +151,6 @@ const ReactFlow = forwardRef( onMove, onMoveStart, onMoveEnd, - onElementsRemove, onConnect, onConnectStart, onConnectStop, @@ -171,7 +177,7 @@ const ReactFlow = forwardRef( multiSelectionKeyCode = 'Meta', zoomActivationKeyCode = 'Meta', snapToGrid = false, - snapGrid = [15, 15], + snapGrid = initSnapGrid, onlyRenderVisibleElements = false, selectNodesOnDrag = true, nodesDraggable, @@ -180,7 +186,7 @@ const ReactFlow = forwardRef( minZoom, maxZoom, defaultZoom = 1, - defaultPosition = [0, 0], + defaultPosition = initDefaultPosition, translateExtent, preventScrolling = true, nodeExtent, @@ -208,6 +214,8 @@ const ReactFlow = forwardRef( edgeUpdaterRadius = 10, nodeTypesId = '1', edgeTypesId = '1', + onNodesChange, + onEdgesChange, ...rest }, ref @@ -240,7 +248,6 @@ const ReactFlow = forwardRef( connectionLineStyle={connectionLineStyle} connectionLineComponent={connectionLineComponent} selectionKeyCode={selectionKeyCode} - onElementsRemove={onElementsRemove} deleteKeyCode={deleteKeyCode} multiSelectionKeyCode={multiSelectionKeyCode} zoomActivationKeyCode={zoomActivationKeyCode} @@ -287,8 +294,10 @@ const ReactFlow = forwardRef( onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateEnd={onEdgeUpdateEnd} edgeUpdaterRadius={edgeUpdaterRadius} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} /> - + {onSelectionChange && } {children} diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index 340620f5..4a09041b 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -1,22 +1,23 @@ import { useEffect } from 'react'; -import { useStore, useStoreActions } from '../store/hooks'; +import { useStore, useStoreActions, useStoreState } from '../store/hooks'; import useKeyPress from './useKeyPress'; -import { isNode, getConnectedEdges } from '../utils/graph'; -import { Elements, KeyCode, ElementId, FlowElement } from '../types'; +import { isNode, isEdge, getConnectedEdges } from '../utils/graph'; +import { KeyCode } from '../types'; interface HookParams { deleteKeyCode: KeyCode; multiSelectionKeyCode: KeyCode; - onElementsRemove?: (elements: Elements) => void; } -export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => { +export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { const store = useStore(); const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); const setMultiSelectionActive = useStoreActions((actions) => actions.setMultiSelectionActive); const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements); + const onNodesChange = useStoreState((state) => state.onNodesChange); + const onEdgesChange = useStoreState((state) => state.onEdgesChange); const deleteKeyPressed = useKeyPress(deleteKeyCode); const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); @@ -24,19 +25,21 @@ export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: Hook useEffect(() => { const { edges, selectedElements } = store.getState(); - if (onElementsRemove && deleteKeyPressed && selectedElements) { + if (deleteKeyPressed && selectedElements) { const selectedNodes = selectedElements.filter(isNode); + const selectedEdges = selectedElements.filter(isEdge); const connectedEdges = getConnectedEdges(selectedNodes, edges); - const elementsToRemove = [...selectedElements, ...connectedEdges].reduce( - (res, item) => res.set(item.id, item), - new Map() - ); - onElementsRemove(Array.from(elementsToRemove.values())); + const nodeChanges = selectedNodes.map((n) => ({ id: n.id, delete: true })); + const edgeChanges = [...selectedEdges, ...connectedEdges].map((e) => ({ id: e.id, delete: true })); + + onNodesChange?.(nodeChanges); + onEdgesChange?.(edgeChanges); + unsetNodesSelection(); resetSelectedElements(); } - }, [deleteKeyPressed, onElementsRemove]); + }, [deleteKeyPressed, onNodesChange, onEdgesChange]); useEffect(() => { setMultiSelectionActive(multiSelectionKeyPressed); diff --git a/src/index.ts b/src/index.ts index 589e49c8..b46051bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,8 @@ export { updateEdge, getTransformForBounds, getRectOfNodes, + applyNodeChanges, + applyEdgeChanges, } from './utils/graph'; export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'; export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'; diff --git a/src/store/actions.ts b/src/store/actions.ts index 0f6bbc47..17ab0cee 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -1,6 +1,8 @@ import { createAction } from './utils'; import { + Node, + Edge, Elements, OnConnectEndFunc, OnConnectFunc, @@ -18,6 +20,7 @@ import { SnapGrid, ConnectionMode, NodeExtent, + OnElementsChange, } from '../types'; import * as constants from './contants'; @@ -42,7 +45,8 @@ export const setOnConnectEnd = (onConnectEnd: OnConnectEndFunc) => onConnectEnd, }); -export const setElements = (elements: Elements) => createAction(constants.SET_ELEMENTS, elements); +export const setNodes = (nodes: Node[]) => createAction(constants.SET_NODES, nodes); +export const setEdges = (edges: Edge[]) => createAction(constants.SET_EDGES, edges); export const updateNodeDimensions = (updates: NodeDimensionUpdate[]) => createAction(constants.UPDATE_NODE_DIMENSIONS, updates); @@ -126,12 +130,19 @@ export const setConnectionMode = (connectionMode: ConnectionMode) => export const setNodeExtent = (nodeExtent: NodeExtent) => createAction(constants.SET_NODE_EXTENT, nodeExtent); +export const setOnNodesChange = (onNodesChange: OnElementsChange) => + createAction(constants.SET_ON_NODES_CHANGE, { onNodesChange }); + +export const setOnEdgesChange = (onEdgesChange: OnElementsChange) => + createAction(constants.SET_ON_EDGES_CHANGE, { onEdgesChange }); + export type ReactFlowAction = ReturnType< | typeof setOnConnect | typeof setOnConnectStart | typeof setOnConnectStop | typeof setOnConnectEnd - | typeof setElements + | typeof setNodes + | typeof setEdges | typeof updateNodeDimensions | typeof updateNodePos | typeof updateNodePosDiff @@ -160,4 +171,6 @@ export type ReactFlowAction = ReturnType< | typeof setMultiSelectionActive | typeof setConnectionMode | typeof setNodeExtent + | typeof setOnNodesChange + | typeof setOnEdgesChange >; diff --git a/src/store/configure-store.ts b/src/store/configure-store.ts index 6a33bc10..6005bdf1 100644 --- a/src/store/configure-store.ts +++ b/src/store/configure-store.ts @@ -1,10 +1,11 @@ -import { createStore, Store } from 'redux'; +import { createStore, applyMiddleware, Store } from 'redux'; +import thunk from 'redux-thunk'; import { ReactFlowState } from '../types'; import { ReactFlowAction } from './actions'; import reactFlowReducer from './reducer'; export default function configureStore(preloadedState: ReactFlowState): Store { - const store = createStore(reactFlowReducer, preloadedState); + const store = createStore(reactFlowReducer, preloadedState, applyMiddleware(thunk)); return store; } diff --git a/src/store/contants.ts b/src/store/contants.ts index 389432df..890ada35 100644 --- a/src/store/contants.ts +++ b/src/store/contants.ts @@ -2,7 +2,8 @@ export const SET_ON_CONNECT = 'SET_ON_CONNECT'; export const SET_ON_CONNECT_START = 'SET_ON_CONNECT_START'; export const SET_ON_CONNECT_STOP = 'SET_ON_CONNECT_STOP'; export const SET_ON_CONNECT_END = 'SET_ON_CONNECT_END'; -export const SET_ELEMENTS = 'SET_ELEMENTS'; +export const SET_NODES = 'SET_NODES'; +export const SET_EDGES = 'SET_EDGES'; export const UPDATE_NODE_DIMENSIONS = 'UPDATE_NODE_DIMENSIONS'; export const UPDATE_NODE_POS = 'UPDATE_NODE_POS'; export const UPDATE_NODE_POS_DIFF = 'UPDATE_NODE_POS_DIFF'; @@ -31,3 +32,5 @@ export const SET_ELEMENTS_SELECTABLE = 'SET_ELEMENTS_SELECTABLE'; export const SET_MULTI_SELECTION_ACTIVE = 'SET_MULTI_SELECTION_ACTIVE'; export const SET_CONNECTION_MODE = 'SET_CONNECTION_MODE'; export const SET_NODE_EXTENT = 'SET_NODE_EXTENT'; +export const SET_ON_NODES_CHANGE = 'SET_ON_NODES_CHANGE'; +export const SET_ON_EDGES_CHANGE = 'SET_ON_EDGES_CHANGE'; diff --git a/src/store/index.ts b/src/store/index.ts index 6640d9e0..fa2eda77 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -8,6 +8,9 @@ export const initialState: ReactFlowState = { transform: [0, 0, 1], nodes: [], edges: [], + onNodesChange: null, + onEdgesChange: null, + selectedElements: null, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, diff --git a/src/store/reducer.ts b/src/store/reducer.ts index d9b9fbb4..f5b7b18f 100644 --- a/src/store/reducer.ts +++ b/src/store/reducer.ts @@ -1,109 +1,111 @@ import isEqual from 'fast-deep-equal'; import { clampPosition, getDimensions } from '../utils'; -import { - getNodesInside, - getConnectedEdges, - getRectOfNodes, - isNode, - isEdge, - parseNode, - parseEdge, -} from '../utils/graph'; +import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, parseNode, parseEdge } from '../utils/graph'; import { getHandleBounds } from '../components/Nodes/utils'; +import { getSourceTargetNodes } from '../container/EdgeRenderer/utils'; -import { ReactFlowState, Node, XYPosition, Edge } from '../types'; +import { ReactFlowState, Node, XYPosition, Edge, ElementChange } from '../types'; import * as constants from './contants'; import { ReactFlowAction } from './actions'; import { initialState } from './index'; -type NextElements = { - nextNodes: Node[]; - nextEdges: Edge[]; -}; - export default function reactFlowReducer(state = initialState, action: ReactFlowAction): ReactFlowState { switch (action.type) { - case constants.SET_ELEMENTS: { - const propElements = action.payload; - const nextElements: NextElements = { - nextNodes: [], - nextEdges: [], - }; - const { nextNodes, nextEdges } = propElements.reduce((res, propElement): NextElements => { - if (isNode(propElement)) { - const storeNode = state.nodes.find((node) => node.id === propElement.id); + case constants.SET_NODES: { + const propNodes = action.payload; + const nextNodes = propNodes.map((propNode: Node) => { + const storeNode = state.nodes.find((node) => node.id === propNode.id); - if (storeNode) { + if (storeNode) { + if (typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type) { const updatedNode: Node = { ...storeNode, - ...propElement, + ...propNode, }; - - if (storeNode.position.x !== propElement.position.x || storeNode.position.y !== propElement.position.y) { - updatedNode.__rf.position = propElement.position; - } - - if (typeof propElement.type !== 'undefined' && propElement.type !== storeNode.type) { - // we reset the elements dimensions here in order to force a re-calculation of the bounds. - // When the type of a node changes it is possible that the number or positions of handles changes too. - updatedNode.__rf.width = null; - } - - res.nextNodes.push(updatedNode); - } else { - res.nextNodes.push(parseNode(propElement, state.nodeExtent)); - } - } else if (isEdge(propElement)) { - const storeEdge = state.edges.find((se) => se.id === propElement.id); - - if (storeEdge) { - res.nextEdges.push({ - ...storeEdge, - ...propElement, - }); - } else { - res.nextEdges.push(parseEdge(propElement)); + // we reset the elements dimensions here in order to force a re-calculation of the bounds. + // When the type of a node changes it is possible that the number or positions of handles changes too. + updatedNode.width = null; + return updatedNode; } } - return res; - }, nextElements); + return parseNode(propNode, state.nodeExtent); + }); - return { ...state, nodes: nextNodes, edges: nextEdges }; + const updatedEdges = state.edges.map((edge) => { + const { sourceNode, targetNode } = getSourceTargetNodes(edge, nextNodes); + + if (sourceNode) { + edge.sourceNode = sourceNode; + } + if (targetNode) { + edge.targetNode = targetNode; + } + + return edge; + }); + + return { ...state, nodes: nextNodes, edges: updatedEdges }; + } + case constants.SET_EDGES: { + const propElements = action.payload; + const nextEdges = propElements.map((propEdge: Edge) => { + const storeEdge = state.edges.find((se) => se.id === propEdge.id); + + if (storeEdge) { + return parseEdge(propEdge); + } else { + const parsedEdge = parseEdge(propEdge); + const { sourceNode, targetNode } = getSourceTargetNodes(parsedEdge, state.nodes); + + if (sourceNode) { + parsedEdge.sourceNode = sourceNode; + } + if (targetNode) { + parsedEdge.targetNode = targetNode; + } + + return parsedEdge; + } + }); + + return { ...state, edges: nextEdges }; } case constants.UPDATE_NODE_DIMENSIONS: { - const updatedNodes = state.nodes.map((node) => { + const initialChanges: ElementChange[] = []; + const nodesToChange: ElementChange[] = state.nodes.reduce((res, node) => { const update = action.payload.find((u) => u.id === node.id); if (update) { const dimensions = getDimensions(update.nodeElement); const doUpdate = dimensions.width && dimensions.height && - (node.__rf.width !== dimensions.width || node.__rf.height !== dimensions.height || update.forceUpdate); + (node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate); if (doUpdate) { const handleBounds = getHandleBounds(update.nodeElement, state.transform[2]); - - return { - ...node, - __rf: { - ...node.__rf, + const change = { + id: node.id, + change: { ...dimensions, handleBounds, }, - }; + } as ElementChange; + + res.push(change); } } - return node; - }); + return res; + }, initialChanges); - return { - ...state, - nodes: updatedNodes, - }; + if (state.onNodesChange) { + requestAnimationFrame(() => state.onNodesChange?.(nodesToChange)); + } + + return state; } case constants.UPDATE_NODE_POS: { const { id, pos } = action.payload; @@ -117,13 +119,20 @@ export default function reactFlowReducer(state = initialState, action: ReactFlow }; } + if (state.onNodesChange) { + state.onNodesChange([{ id, change: { position } }]); + + return state; + } + const nextNodes = state.nodes.map((node) => { if (node.id === id) { return { ...node, + position, + __rf: { ...node.__rf, - position, }, }; } @@ -136,30 +145,28 @@ export default function reactFlowReducer(state = initialState, action: ReactFlow case constants.UPDATE_NODE_POS_DIFF: { const { id, diff, isDragging } = action.payload; - const nextNodes = state.nodes.map((node) => { - if (id === node.id || state.selectedElements?.find((sNode) => sNode.id === node.id)) { - const updatedNode = { - ...node, - __rf: { - ...node.__rf, - isDragging, - }, - }; + if (state.onNodesChange && id && diff) { + const matchingNode = state.nodes.find((n) => n.id === id); - if (diff) { - updatedNode.__rf.position = { - x: node.__rf.position.x + diff.x, - y: node.__rf.position.y + diff.y, - }; - } - - return updatedNode; + if (matchingNode) { + requestAnimationFrame(() => + state.onNodesChange?.([ + { + id, + change: { + position: { + x: matchingNode.position.x + diff.x, + y: matchingNode.position.y + diff.y, + isDragging, + }, + }, + }, + ]) + ); } + } - return node; - }); - - return { ...state, nodes: nextNodes }; + return state; } case constants.SET_USER_SELECTION: { const mousePos = action.payload; @@ -308,9 +315,9 @@ export default function reactFlowReducer(state = initialState, action: ReactFlow nodes: state.nodes.map((node) => { return { ...node, + position: clampPosition(node.position, nodeExtent), __rf: { ...node.__rf, - position: clampPosition(node.__rf.position, nodeExtent), }, }; }), @@ -334,6 +341,8 @@ export default function reactFlowReducer(state = initialState, action: ReactFlow case constants.SET_ELEMENTS_SELECTABLE: case constants.SET_MULTI_SELECTION_ACTIVE: case constants.SET_CONNECTION_MODE: + case constants.SET_ON_NODES_CHANGE: + case constants.SET_ON_EDGES_CHANGE: return { ...state, ...action.payload }; default: return state; diff --git a/src/types/index.ts b/src/types/index.ts index 754e10e2..6de72b6d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -9,6 +9,12 @@ export type Elements = Array>; export type Transform = [number, number, number]; +export type ElementChange = { + id: string; + change?: any; + delete?: boolean; +}; + export enum Position { Left = 'left', Top = 'top', @@ -50,6 +56,10 @@ export interface Node { selectable?: boolean; connectable?: boolean; dragHandle?: string; + isDragging?: boolean; + width?: number | null; + height?: number | null; + handleBounds?: any; } export enum ArrowHeadType { @@ -76,6 +86,8 @@ export interface Edge { isHidden?: boolean; data?: T; className?: string; + sourceNode?: Node; + targetNode?: Node; } export enum BackgroundVariant { @@ -313,7 +325,7 @@ export type ConnectionLineComponentProps = { export type ConnectionLineComponent = React.ComponentType; -export type OnConnectFunc = (connection: Connection) => void; +export type OnConnectFunc = (connection: Connection, nodes: Node[]) => void; export type OnConnectStartParams = { nodeId: ElementId | null; handleId: ElementId | null; @@ -398,6 +410,8 @@ export type InitD3ZoomPayload = { transform: Transform; }; +export type OnElementsChange = (nodes: ElementChange[]) => void; + export interface ReactFlowState { width: number; height: number; @@ -406,6 +420,8 @@ export interface ReactFlowState { edges: Edge[]; selectedElements: Elements | null; selectedNodesBbox: Rect; + onNodesChange: OnElementsChange | null; + onEdgesChange: OnElementsChange | null; d3Zoom: ZoomBehavior | null; d3Selection: D3Selection | null; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 73b6c68b..86d13a6e 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -15,6 +15,7 @@ import { FlowExportObject, ReactFlowState, NodeExtent, + ElementChange, } from '../types'; export const isEdge = (element: Node | Connection | Edge): element is Edge => @@ -68,10 +69,10 @@ const connectionExists = (edge: Edge, elements: Elements) => { ); }; -export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elements => { +export const addEdge = (edgeParams: Edge | Connection, nodes: Node[], edges: Edge[]): Edge[] => { if (!edgeParams.source || !edgeParams.target) { console.warn("Can't create edge. An edge needs a source and a target."); - return elements; + return edges; } let edge: Edge; @@ -84,11 +85,11 @@ export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elem } as Edge; } - if (connectionExists(edge, elements)) { - return elements; + if (connectionExists(edge, nodes)) { + return edges; } - return elements.concat(edge); + return edges.concat(edge); }; export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: Elements): Elements => { @@ -147,30 +148,23 @@ export const onLoadProject = (currentStore: Store) => { }; export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => { - return { - ...node, - id: node.id.toString(), - type: node.type || 'default', - __rf: { - position: clampPosition(node.position, nodeExtent), - width: null, - height: null, - handleBounds: {}, - isDragging: false, - }, - }; + if (!node.type) { + node.type = 'default'; + } + + if (nodeExtent) { + node.position = clampPosition(node.position, nodeExtent); + } + + return node; }; export const parseEdge = (edge: Edge): Edge => { - return { - ...edge, - source: edge.source.toString(), - target: edge.target.toString(), - sourceHandle: edge.sourceHandle ? edge.sourceHandle.toString() : null, - targetHandle: edge.targetHandle ? edge.targetHandle.toString() : null, - id: edge.id.toString(), - type: edge.type || 'default', - }; + if (!edge.type) { + edge.type = 'default'; + } + + return edge; }; const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({ @@ -199,8 +193,8 @@ export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect => export const getRectOfNodes = (nodes: Node[]): Rect => { const box = nodes.reduce( - (currBox, { __rf: { position, width, height } = {} }) => - getBoundsOfBoxes(currBox, rectToBox({ ...position, width, height })), + (currBox, { position, width, height }) => + getBoundsOfBoxes(currBox, rectToBox({ ...position, width: width || 0, height: height || 0 })), { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity } ); @@ -227,12 +221,12 @@ export const getNodesInside = ( height: rect.height / tScale, }); - return nodes.filter(({ selectable = true, __rf: { position, width, height, isDragging } }) => { + return nodes.filter(({ selectable = true, position, width, height, isDragging }) => { if (excludeNonSelectableNodes && !selectable) { return false; } - const nBox = rectToBox({ ...position, width, height }); + const nBox = rectToBox({ ...position, 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); @@ -246,7 +240,7 @@ export const getNodesInside = ( return overlappingArea > 0; } - const area = width * height; + const area = (width || 0) * (height || 0); return overlappingArea >= area; }); @@ -259,17 +253,7 @@ export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => { }; const parseElements = (nodes: Node[], edges: Edge[]): Elements => { - return [ - ...nodes.map((node) => { - const n = { ...node }; - - n.position = n.__rf.position; - - delete n.__rf; - return n; - }), - ...edges.map((e) => ({ ...e })), - ]; + return [...nodes.map((n) => ({ ...n })), ...edges.map((e) => ({ ...e }))]; }; export const onLoadGetElements = (currentStore: Store) => { @@ -311,3 +295,31 @@ export const getTransformForBounds = ( return [x, y, clampedZoom]; }; + +function applyChanges(changes: ElementChange[], elements: any[]): any[] { + const initElements: any[] = []; + + return elements.reduce((res: any[], node: any) => { + const hasChange = changes.find((c) => c.id === node.id); + + if (hasChange?.delete) { + return res; + } + + if (hasChange?.change) { + res.push({ ...node, ...hasChange.change }); + } else { + res.push(node); + } + + return res; + }, initElements); +} + +export function applyNodeChanges(changes: ElementChange[], nodes: Node[]): Node[] { + return applyChanges(changes, nodes) as Node[]; +} + +export function applyEdgeChanges(changes: ElementChange[], edges: Edge[]): Edge[] { + return applyChanges(changes, edges) as Edge[]; +} From e01249a87c1e8f23864c2ccac05f16820a49f952 Mon Sep 17 00:00:00 2001 From: moklick Date: Sat, 9 Oct 2021 16:49:41 +0200 Subject: [PATCH 003/263] refactor(examples): hide old api examples --- example/.gitignore | 2 + example/src/Basic/index.tsx | 71 +++--- .../CustomConnectionLine/ConnectionLine.tsx | 19 -- example/src/CustomConnectionLine/index.tsx | 33 --- example/src/CustomNode/ColorSelectorNode.tsx | 25 -- example/src/CustomNode/index.tsx | 135 ----------- example/src/DragHandle/DragHandleNode.tsx | 33 --- example/src/DragHandle/index.tsx | 21 -- example/src/DragNDrop/Sidebar.tsx | 25 -- example/src/DragNDrop/dnd.css | 37 --- example/src/DragNDrop/index.tsx | 75 ------ example/src/EdgeTypes/index.tsx | 54 ----- example/src/EdgeTypes/utils.ts | 106 -------- example/src/Edges/CustomEdge.tsx | 31 --- example/src/Edges/CustomEdge2.tsx | 44 ---- example/src/Edges/index.tsx | 115 --------- example/src/Empty/index.tsx | 60 ----- example/src/Hidden/index.tsx | 53 ---- example/src/Interaction/index.tsx | 226 ------------------ example/src/Layouting/index.tsx | 86 ------- example/src/Layouting/initial-elements.ts | 71 ------ example/src/Layouting/layouting.css | 11 - example/src/MultiFlows/index.tsx | 45 ---- example/src/MultiFlows/multiflows.css | 13 - example/src/NodeTypeChange/index.tsx | 55 ----- example/src/NodeTypesObjectChange/index.tsx | 76 ------ example/src/Overview/index.tsx | 202 ---------------- example/src/Provider/Sidebar.tsx | 36 --- example/src/Provider/index.tsx | 57 ----- example/src/Provider/provider.css | 45 ---- example/src/SaveRestore/Controls.tsx | 61 ----- example/src/SaveRestore/index.tsx | 37 --- example/src/SaveRestore/save.css | 11 - example/src/Stress/index.tsx | 51 ++-- example/src/Stress/utils.ts | 21 +- example/src/Switch/index.tsx | 53 ---- example/src/Undirectional/CustomNode.tsx | 19 -- example/src/Undirectional/index.tsx | 221 ----------------- example/src/UpdatableEdge/index.tsx | 79 ------ example/src/UpdateNode/index.tsx | 57 +++-- .../src/UseUpdateNodeInternals/CustomNode.tsx | 34 --- example/src/UseUpdateNodeInternals/index.tsx | 101 -------- example/src/UseZoomPanHelper/index.tsx | 65 ----- example/src/Validation/index.tsx | 73 ------ example/src/Validation/validation.css | 31 --- example/src/index.tsx | 116 +-------- package-lock.json | 38 ++- package.json | 4 +- 48 files changed, 161 insertions(+), 2773 deletions(-) delete mode 100644 example/src/CustomConnectionLine/ConnectionLine.tsx delete mode 100644 example/src/CustomConnectionLine/index.tsx delete mode 100644 example/src/CustomNode/ColorSelectorNode.tsx delete mode 100644 example/src/CustomNode/index.tsx delete mode 100644 example/src/DragHandle/DragHandleNode.tsx delete mode 100644 example/src/DragHandle/index.tsx delete mode 100644 example/src/DragNDrop/Sidebar.tsx delete mode 100644 example/src/DragNDrop/dnd.css delete mode 100644 example/src/DragNDrop/index.tsx delete mode 100644 example/src/EdgeTypes/index.tsx delete mode 100644 example/src/EdgeTypes/utils.ts delete mode 100644 example/src/Edges/CustomEdge.tsx delete mode 100644 example/src/Edges/CustomEdge2.tsx delete mode 100644 example/src/Edges/index.tsx delete mode 100644 example/src/Empty/index.tsx delete mode 100644 example/src/Hidden/index.tsx delete mode 100644 example/src/Interaction/index.tsx delete mode 100644 example/src/Layouting/index.tsx delete mode 100644 example/src/Layouting/initial-elements.ts delete mode 100644 example/src/Layouting/layouting.css delete mode 100644 example/src/MultiFlows/index.tsx delete mode 100644 example/src/MultiFlows/multiflows.css delete mode 100644 example/src/NodeTypeChange/index.tsx delete mode 100644 example/src/NodeTypesObjectChange/index.tsx delete mode 100644 example/src/Overview/index.tsx delete mode 100644 example/src/Provider/Sidebar.tsx delete mode 100644 example/src/Provider/index.tsx delete mode 100644 example/src/Provider/provider.css delete mode 100644 example/src/SaveRestore/Controls.tsx delete mode 100644 example/src/SaveRestore/index.tsx delete mode 100644 example/src/SaveRestore/save.css delete mode 100644 example/src/Switch/index.tsx delete mode 100644 example/src/Undirectional/CustomNode.tsx delete mode 100644 example/src/Undirectional/index.tsx delete mode 100644 example/src/UpdatableEdge/index.tsx delete mode 100644 example/src/UseUpdateNodeInternals/CustomNode.tsx delete mode 100644 example/src/UseUpdateNodeInternals/index.tsx delete mode 100644 example/src/UseZoomPanHelper/index.tsx delete mode 100644 example/src/Validation/index.tsx delete mode 100644 example/src/Validation/validation.css diff --git a/example/.gitignore b/example/.gitignore index 4d29575d..703c7975 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +src_oldapi \ No newline at end of file diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index ae003545..4872d56b 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -1,49 +1,54 @@ -import React, { useState, MouseEvent } from 'react'; +import { useState, MouseEvent, useCallback } from 'react'; import ReactFlow, { - removeElements, addEdge, - isNode, Background, - Elements, - BackgroundVariant, + applyNodeChanges, + applyEdgeChanges, + MiniMap, + Controls, FlowElement, Node, Edge, - Connection, + ElementChange, OnLoadParams, + Connection, } from 'react-flow-renderer'; const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); -const initialElements: Elements = [ +const initialNodes: Node[] = [ { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' }, +]; + +const initialEdges: Edge[] = [ { id: 'e1-2', source: '1', target: '2', animated: true }, { id: 'e1-3', source: '1', target: '3' }, ]; const BasicFlow = () => { const [rfInstance, setRfInstance] = useState(null); - const [elements, setElements] = useState(initialElements); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - const onConnect = (params: Edge | Connection) => setElements((els) => addEdge(params, els)); + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState(initialEdges); + + const onConnect = useCallback((params: Edge | Connection, nds: Node[]) => { + setEdges((eds) => addEdge(params, nds, eds)); + }, []); const onLoad = (reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance); const updatePos = () => { - setElements((elms) => { - return elms.map((el) => { - if (isNode(el)) { - el.position = { - x: Math.random() * 400, - y: Math.random() * 400, - }; - } + setNodes((nds) => { + return nds.map((n) => { + n.position = { + x: Math.random() * 400, + y: Math.random() * 400, + }; - return el; + return n; }); }); }; @@ -52,23 +57,31 @@ const BasicFlow = () => { const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 }); const toggleClassnames = () => { - setElements((elms) => { - return elms.map((el) => { - if (isNode(el)) { - el.className = el.className === 'light' ? 'dark' : 'light'; - } + setNodes((nds) => { + return nds.map((n) => { + n.className = n.className === 'light' ? 'dark' : 'light'; - return el; + return n; }); }); }; + const onNodesChange = useCallback((changes: ElementChange[]) => { + setNodes((ns) => applyNodeChanges(changes, ns)); + }, []); + + const onEdgesChange = useCallback((changes: ElementChange[]) => { + setEdges((es) => applyEdgeChanges(changes, es)); + }, []); + return ( { minZoom={0.2} maxZoom={4} > - + + +
- - ); -}; - -export default EmptyFlow; diff --git a/example/src/Hidden/index.tsx b/example/src/Hidden/index.tsx deleted file mode 100644 index 9869b2bc..00000000 --- a/example/src/Hidden/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { useState } from 'react'; - -import { useEffect } from 'react'; -import ReactFlow, { addEdge, MiniMap, Controls, Connection, Edge, Elements } from 'react-flow-renderer'; - -const initialElements: Elements = [ - { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, - { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, - { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, - { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, - { id: 'e1-2', source: '1', target: '2' }, - { id: 'e1-3', source: '1', target: '3' }, - { id: 'e3-4', source: '3', target: '4' }, -]; - -const HiddenFlow = () => { - const [elements, setElements] = useState(initialElements); - const [isHidden, setIsHidden] = useState(false); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - - useEffect(() => { - setElements((els) => - els.map((e) => { - e.isHidden = isHidden; - return e; - }) - ); - }, [isHidden]); - - return ( - - - - -
-
- -
-
-
- ); -}; - -export default HiddenFlow; diff --git a/example/src/Interaction/index.tsx b/example/src/Interaction/index.tsx deleted file mode 100644 index 2ca7900c..00000000 --- a/example/src/Interaction/index.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import React, { useState, MouseEvent, WheelEvent } from 'react'; -import ReactFlow, { - addEdge, - MiniMap, - Controls, - Elements, - Node, - FlowElement, - Connection, - Edge, - PanOnScrollMode, - FlowTransform, -} from 'react-flow-renderer'; - -const initialElements: Elements = [ - { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, - { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, - { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, - { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, - { id: 'e1-2', source: '1', target: '2', animated: true }, - { id: 'e1-3', source: '1', target: '3' }, -]; - -const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node); -const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); -const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); -const onPaneClick = (event: MouseEvent) => console.log('onPaneClick', event); -const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event); -const onPaneContextMenu = (event: MouseEvent) => console.log('onPaneContextMenu', event); -const onMoveEnd = (flowTranasform?: FlowTransform) => console.log('onMoveEnd', flowTranasform); - -const InteractionFlow = () => { - const [elements, setElements] = useState(initialElements); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - - const [isSelectable, setIsSelectable] = useState(false); - const [isDraggable, setIsDraggable] = useState(false); - const [isConnectable, setIsConnectable] = useState(false); - const [zoomOnScroll, setZoomOnScroll] = useState(false); - const [zoomOnPinch, setZoomOnPinch] = useState(false); - const [panOnScroll, setPanOnScroll] = useState(false); - const [panOnScrollMode, setPanOnScrollMode] = useState(PanOnScrollMode.Free); - const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState(false); - const [paneMoveable, setPaneMoveable] = useState(true); - const [captureZoomClick, setCaptureZoomClick] = useState(false); - const [captureZoomScroll, setCaptureZoomScroll] = useState(false); - const [captureElementClick, setCaptureElementClick] = useState(false); - - return ( - - - - -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- ); -}; - -export default InteractionFlow; diff --git a/example/src/Layouting/index.tsx b/example/src/Layouting/index.tsx deleted file mode 100644 index 34ee23e6..00000000 --- a/example/src/Layouting/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, { useState } from 'react'; -import ReactFlow, { - ReactFlowProvider, - addEdge, - removeElements, - Controls, - isNode, - Elements, - Connection, - Edge, - NodeExtent, - Position, -} from 'react-flow-renderer'; -import dagre from 'dagre'; - -import initialElements from './initial-elements'; - -import './layouting.css'; - -const dagreGraph = new dagre.graphlib.Graph(); -dagreGraph.setDefaultEdgeLabel(() => ({})); - -const nodeExtent: NodeExtent = [ - [0, 0], - [1000, 1000], -]; - -const LayoutFlow = () => { - const [elements, setElements] = useState(initialElements); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - - const onLayout = (direction: string) => { - const isHorizontal = direction === 'LR'; - dagreGraph.setGraph({ rankdir: direction }); - - elements.forEach((el) => { - if (isNode(el)) { - dagreGraph.setNode(el.id, { width: 150, height: 50 }); - } else { - dagreGraph.setEdge(el.source, el.target); - } - }); - - dagre.layout(dagreGraph); - - const layoutedElements = elements.map((el) => { - if (isNode(el)) { - const nodeWithPosition = dagreGraph.node(el.id); - el.targetPosition = isHorizontal ? Position.Left : Position.Top; - el.sourcePosition = isHorizontal ? Position.Right : Position.Bottom; - // we need to pass a slightly different position in order to notify react flow about the change - // @TODO how can we change the position handling so that we dont need this hack? - el.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y }; - } - - return el; - }); - - setElements(layoutedElements); - }; - - return ( -
- - onLayout('TB')} - > - - -
- - -
-
-
- ); -}; - -export default LayoutFlow; diff --git a/example/src/Layouting/initial-elements.ts b/example/src/Layouting/initial-elements.ts deleted file mode 100644 index 404b1b8b..00000000 --- a/example/src/Layouting/initial-elements.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Elements, XYPosition } from 'react-flow-renderer'; - -const position: XYPosition = { x: 0, y: 0 }; - -const elements: Elements = [ - { - id: '1', - type: 'input', - data: { label: 'input' }, - position, - }, - { - id: '2', - data: { label: 'node 2' }, - position, - }, - { - id: '2a', - data: { label: 'node 2a' }, - position, - }, - { - id: '2b', - data: { label: 'node 2b' }, - position, - }, - { - id: '2c', - data: { label: 'node 2c' }, - position, - }, - { - id: '2d', - data: { label: 'node 2d' }, - position, - }, - { - id: '3', - data: { label: 'node 3' }, - position, - }, - { - id: '4', - data: { label: 'node 4' }, - position, - }, - { - id: '5', - data: { label: 'node 5' }, - position, - }, - { - id: '6', - type: 'output', - data: { label: 'output' }, - position, - }, - { id: '7', type: 'output', data: { label: 'output' }, position: { x: 400, y: 450 } }, - { id: 'e12', source: '1', target: '2', type: 'smoothstep' }, - { id: 'e13', source: '1', target: '3', type: 'smoothstep' }, - { id: 'e22a', source: '2', target: '2a', type: 'smoothstep' }, - { id: 'e22b', source: '2', target: '2b', type: 'smoothstep' }, - { id: 'e22c', source: '2', target: '2c', type: 'smoothstep' }, - { id: 'e2c2d', source: '2c', target: '2d', type: 'smoothstep' }, - - { id: 'e45', source: '4', target: '5', type: 'smoothstep' }, - { id: 'e56', source: '5', target: '6', type: 'smoothstep' }, - { id: 'e57', source: '5', target: '7', type: 'smoothstep' }, -]; - -export default elements; diff --git a/example/src/Layouting/layouting.css b/example/src/Layouting/layouting.css deleted file mode 100644 index 6d248b22..00000000 --- a/example/src/Layouting/layouting.css +++ /dev/null @@ -1,11 +0,0 @@ -.layoutflow { - flex-grow: 1; - position: relative; -} - -.layoutflow .controls { - position: absolute; - right: 10px; - top: 10px; - z-index: 10; -} diff --git a/example/src/MultiFlows/index.tsx b/example/src/MultiFlows/index.tsx deleted file mode 100644 index 8e185992..00000000 --- a/example/src/MultiFlows/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { useState, FC } from 'react'; - -import ReactFlow, { - removeElements, - addEdge, - Background, - Elements, - Edge, - Connection, - ReactFlowProvider, -} from 'react-flow-renderer'; - -import './multiflows.css'; - -const initialElements: Elements = [ - { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, - { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, - { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, - { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' }, - { id: 'e1-2', source: '1', target: '2', animated: true }, - { id: 'e1-3', source: '1', target: '3' }, -]; - -const Flow: FC = () => { - const [elements, setElements] = useState(initialElements); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - const onConnect = (params: Edge | Connection) => setElements((els) => addEdge(params, els)); - - return ( - - - - - - ); -}; - -const MultiFlows: FC = () => ( -
- - -
-); - -export default MultiFlows; diff --git a/example/src/MultiFlows/multiflows.css b/example/src/MultiFlows/multiflows.css deleted file mode 100644 index b7017390..00000000 --- a/example/src/MultiFlows/multiflows.css +++ /dev/null @@ -1,13 +0,0 @@ -.react-flow__example-multiflows { - display: flex; - height: 100%; -} - -.react-flow__example-multiflows .react-flow { - width: 100%; - height: 100%; -} - -.react-flow__example-multiflows .react-flow:first-child { - border-right: 2px solid #333; -} diff --git a/example/src/NodeTypeChange/index.tsx b/example/src/NodeTypeChange/index.tsx deleted file mode 100644 index c5e4ff90..00000000 --- a/example/src/NodeTypeChange/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { useState, CSSProperties } from 'react'; - -import ReactFlow, { addEdge, isEdge, OnLoadParams, Elements, Position, Connection, Edge } from 'react-flow-renderer'; - -const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView(); - -const initialElements: Elements = [ - { - id: '1', - sourcePosition: Position.Right, - type: 'input', - data: { label: 'Input' }, - position: { x: 0, y: 80 }, - }, - { - id: '2', - type: 'output', - sourcePosition: Position.Right, - targetPosition: Position.Left, - data: { label: 'A Node' }, - position: { x: 250, y: 0 }, - }, - { id: 'e1-2', source: '1', type: 'smoothstep', target: '2', animated: true }, -]; - -const buttonStyle: CSSProperties = { position: 'absolute', right: 10, top: 30, zIndex: 4 }; - -const NodeTypeChangeFlow = () => { - const [elements, setElements] = useState(initialElements); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - const changeType = () => { - setElements((elms) => - elms.map((el) => { - if (isEdge(el) || el.type === 'input') { - return el; - } - - return { - ...el, - type: el.type === 'default' ? 'output' : 'default', - }; - }) - ); - }; - - return ( - - - - ); -}; - -export default NodeTypeChangeFlow; diff --git a/example/src/NodeTypesObjectChange/index.tsx b/example/src/NodeTypesObjectChange/index.tsx deleted file mode 100644 index 54b34fcc..00000000 --- a/example/src/NodeTypesObjectChange/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { useState, CSSProperties, FC } from 'react'; - -import ReactFlow, { - addEdge, - Elements, - Position, - Connection, - Edge, - NodeProps, - NodeTypesType, -} from 'react-flow-renderer'; - -const initialElements: Elements = [ - { - id: '1', - sourcePosition: Position.Right, - type: 'input', - data: { label: 'Input' }, - position: { x: 0, y: 80 }, - }, - { - id: '2', - type: 'a', - sourcePosition: Position.Right, - targetPosition: Position.Left, - data: { label: 'A Node' }, - position: { x: 250, y: 0 }, - }, -]; - -const buttonStyle: CSSProperties = { position: 'absolute', right: 10, top: 30, zIndex: 4 }; - -const nodeStyles: CSSProperties = { padding: '10px 15px', border: '1px solid #ddd' }; - -const NodeA: FC = () => { - return
A
; -}; - -const NodeB: FC = () => { - return
B
; -}; - -type NodeTypesObject = { - [key: string]: NodeTypesType; -}; - -const nodeTypesObjects: NodeTypesObject = { - a: { - a: NodeA, - }, - b: { - b: NodeB, - }, -}; - -const NodeTypeChangeFlow = () => { - const [nodeTypesId, setNodeTypesId] = useState('a'); - const [elements, setElements] = useState(initialElements); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - const changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a')); - - return ( - - - - ); -}; - -export default NodeTypeChangeFlow; diff --git a/example/src/Overview/index.tsx b/example/src/Overview/index.tsx deleted file mode 100644 index ee13f916..00000000 --- a/example/src/Overview/index.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import React, { useState, MouseEvent, CSSProperties } from 'react'; - -import ReactFlow, { - removeElements, - addEdge, - MiniMap, - Controls, - Background, - isNode, - Node, - Elements, - FlowElement, - OnLoadParams, - FlowTransform, - SnapGrid, - ArrowHeadType, - Connection, - Edge, -} from 'react-flow-renderer'; - -const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node); -const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node); -const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); -const onNodeDoubleClick = (_: MouseEvent, node: Node) => console.log('node double click', node); -const onPaneClick = (event: MouseEvent) => console.log('pane click', event); -const onPaneScroll = (event?: MouseEvent) => console.log('pane scroll', event); -const onPaneContextMenu = (event: MouseEvent) => console.log('pane context menu', event); -const onSelectionDrag = (_: MouseEvent, nodes: Node[]) => console.log('selection drag', nodes); -const onSelectionDragStart = (_: MouseEvent, nodes: Node[]) => console.log('selection drag start', nodes); -const onSelectionDragStop = (_: MouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes); -const onSelectionContextMenu = (event: MouseEvent, nodes: Node[]) => { - event.preventDefault(); - console.log('selection context menu', nodes); -}; -const onElementClick = (_: MouseEvent, element: FlowElement) => - console.log(`${isNode(element) ? 'node' : 'edge'} click:`, element); -const onSelectionChange = (elements: Elements | null) => console.log('selection change', elements); -const onLoad = (reactFlowInstance: OnLoadParams) => { - console.log('flow loaded:', reactFlowInstance); - reactFlowInstance.fitView(); -}; - -const onMoveStart = (transform?: FlowTransform) => console.log('zoom/move start', transform); -const onMoveEnd = (transform?: FlowTransform) => console.log('zoom/move end', transform); -const onEdgeContextMenu = (_: MouseEvent, edge: Edge) => console.log('edge context menu', edge); -const onEdgeMouseEnter = (_: MouseEvent, edge: Edge) => console.log('edge mouse enter', edge); -const onEdgeMouseMove = (_: MouseEvent, edge: Edge) => console.log('edge mouse move', edge); -const onEdgeMouseLeave = (_: MouseEvent, edge: Edge) => console.log('edge mouse leave', edge); -const onEdgeDoubleClick = (_: MouseEvent, edge: Edge) => console.log('edge double click', edge); - -const initialElements: Elements = [ - { - id: '1', - type: 'input', - data: { - label: ( - <> - Welcome to React Flow! - - ), - }, - position: { x: 250, y: 0 }, - }, - { - id: '2', - data: { - label: ( - <> - This is a default node - - ), - }, - position: { x: 100, y: 100 }, - }, - { - id: '3', - data: { - label: ( - <> - This one has a custom style - - ), - }, - position: { x: 400, y: 100 }, - style: { background: '#D6D5E6', color: '#333', border: '1px solid #222138', width: 180 }, - }, - { - id: '4', - position: { x: 250, y: 200 }, - data: { - label: ( - <> - You can find the docs on{' '} - - Github - - - ), - }, - }, - { - id: '5', - data: { - label: ( - <> - Or check out the other examples - - ), - }, - position: { x: 250, y: 325 }, - }, - { - id: '6', - type: 'output', - data: { - label: ( - <> - An output node - - ), - }, - position: { x: 100, y: 480 }, - }, - { id: '7', type: 'output', data: { label: 'Another output node' }, position: { x: 400, y: 450 } }, - { id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' }, - { id: 'e1-3', source: '1', target: '3' }, - { id: 'e3-4', source: '3', target: '4', animated: true, label: 'animated edge' }, - { id: 'e4-5', source: '4', target: '5', arrowHeadType: ArrowHeadType.Arrow, label: 'edge with arrow head' }, - { id: 'e5-6', source: '5', target: '6', type: 'smoothstep', label: 'smooth step edge' }, - { - id: 'e5-7', - source: '5', - target: '7', - type: 'step', - style: { stroke: '#f6ab6c' }, - label: 'a step edge', - animated: true, - labelStyle: { fill: '#f6ab6c', fontWeight: 700 }, - }, -]; - -const connectionLineStyle: CSSProperties = { stroke: '#ddd' }; -const snapGrid: SnapGrid = [16, 16]; - -const nodeStrokeColor = (n: Node): string => { - if (n.style?.background) return n.style.background as string; - if (n.type === 'input') return '#0041d0'; - if (n.type === 'output') return '#ff0072'; - if (n.type === 'default') return '#1a192b'; - - return '#eee'; -}; - -const nodeColor = (n: Node): string => { - if (n.style?.background) return n.style.background as string; - - return '#fff'; -}; - -const OverviewFlow = () => { - const [elements, setElements] = useState(initialElements); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - - return ( - - - - - - ); -}; - -export default OverviewFlow; diff --git a/example/src/Provider/Sidebar.tsx b/example/src/Provider/Sidebar.tsx deleted file mode 100644 index 0b2c5f24..00000000 --- a/example/src/Provider/Sidebar.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { useStoreState, useStoreActions } from 'react-flow-renderer'; - -const Sidebar = () => { - const nodes = useStoreState((store) => store.nodes); - const transform = useStoreState((store) => store.transform); - const setSelectedElements = useStoreActions((actions) => actions.setSelectedElements); - - const selectAll = () => { - setSelectedElements(nodes.map((node) => ({ id: node.id, type: node.type }))); - }; - - return ( - - ); -}; - -export default Sidebar; diff --git a/example/src/Provider/index.tsx b/example/src/Provider/index.tsx deleted file mode 100644 index a7a78b7c..00000000 --- a/example/src/Provider/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React, { useState, MouseEvent } from 'react'; -import ReactFlow, { - ReactFlowProvider, - addEdge, - removeElements, - Controls, - OnLoadParams, - FlowElement, - Connection, - Edge, - Elements, - ConnectionMode, -} from 'react-flow-renderer'; - -import Sidebar from './Sidebar'; - -import './provider.css'; - -const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); -const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance); - -const initialElements: Elements = [ - { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, - { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, - { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, - { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, - { id: 'e1-2', source: '1', target: '2', animated: true }, - { id: 'e1-3', source: '1', target: '3' }, -]; - -const ProviderFlow = () => { - const [elements, setElements] = useState(initialElements); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - - return ( -
- - -
- - - -
-
-
- ); -}; - -export default ProviderFlow; diff --git a/example/src/Provider/provider.css b/example/src/Provider/provider.css deleted file mode 100644 index aa719acc..00000000 --- a/example/src/Provider/provider.css +++ /dev/null @@ -1,45 +0,0 @@ -.providerflow { - flex-direction: column; - display: flex; - height: 100%; -} - -.providerflow aside { - border-right: 1px solid #eee; - padding: 15px 10px; - font-size: 12px; - background: #fcfcfc; -} - -.providerflow aside .description { - margin-bottom: 10px; -} - -.providerflow aside .title { - font-weight: 700; - margin-bottom: 5px; -} - -.providerflow aside .transform { - margin-bottom: 20px; -} - -.providerflow .reactflow-wrapper { - flex-grow: 1; - height: 100%; -} - -.providerflow .selectall { - margin-top: 10px; -} - -@media screen and (min-width: 768px) { - .providerflow { - flex-direction: row; - } - - .providerflow aside { - width: 20%; - max-width: 250px; - } -} \ No newline at end of file diff --git a/example/src/SaveRestore/Controls.tsx b/example/src/SaveRestore/Controls.tsx deleted file mode 100644 index f79f5e47..00000000 --- a/example/src/SaveRestore/Controls.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React, { memo, useCallback, Dispatch, FC } from 'react'; -import { useZoomPanHelper, OnLoadParams, Elements, FlowExportObject } from 'react-flow-renderer'; -import localforage from 'localforage'; - -localforage.config({ - name: 'react-flow', - storeName: 'flows', -}); - -const flowKey = 'example-flow'; - -const getNodeId = () => `randomnode_${+new Date()}`; - -type ControlsProps = { - rfInstance?: OnLoadParams; - setElements: Dispatch>>; -}; - -const Controls: FC = ({ rfInstance, setElements }) => { - const { transform } = useZoomPanHelper(); - - const onSave = useCallback(() => { - if (rfInstance) { - const flow = rfInstance.toObject(); - localforage.setItem(flowKey, flow); - } - }, [rfInstance]); - - const onRestore = useCallback(() => { - const restoreFlow = async () => { - const flow: FlowExportObject | null = await localforage.getItem(flowKey); - - if (flow) { - const [x = 0, y = 0] = flow.position; - setElements(flow.elements || []); - transform({ x, y, zoom: flow.zoom || 0 }); - } - }; - - restoreFlow(); - }, [setElements, transform]); - - const onAdd = useCallback(() => { - const newNode = { - id: `random_node-${getNodeId()}`, - data: { label: 'Added node' }, - position: { x: Math.random() * window.innerWidth - 100, y: Math.random() * window.innerHeight }, - }; - setElements((els) => els.concat(newNode)); - }, [setElements]); - - return ( -
- - - -
- ); -}; - -export default memo(Controls); diff --git a/example/src/SaveRestore/index.tsx b/example/src/SaveRestore/index.tsx deleted file mode 100644 index 9457248d..00000000 --- a/example/src/SaveRestore/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { useState } from 'react'; -import ReactFlow, { - ReactFlowProvider, - removeElements, - addEdge, - Elements, - Connection, - Edge, - OnLoadParams, -} from 'react-flow-renderer'; - -import Controls from './Controls'; - -import './save.css'; - -const initialElements: Elements = [ - { id: '1', data: { label: 'Node 1' }, position: { x: 100, y: 100 } }, - { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } }, - { id: 'e1-2', source: '1', target: '2' }, -]; - -const SaveRestore = () => { - const [rfInstance, setRfInstance] = useState(); - const [elements, setElements] = useState(initialElements); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - - return ( - - - - - - ); -}; - -export default SaveRestore; diff --git a/example/src/SaveRestore/save.css b/example/src/SaveRestore/save.css deleted file mode 100644 index ffd028dd..00000000 --- a/example/src/SaveRestore/save.css +++ /dev/null @@ -1,11 +0,0 @@ -.save__controls { - position: absolute; - right: 10px; - top: 10px; - z-index: 4; - font-size: 12px; -} - -.save__controls button { - margin-left: 5px; -} diff --git a/example/src/Stress/index.tsx b/example/src/Stress/index.tsx index b734c6b6..2b27158b 100644 --- a/example/src/Stress/index.tsx +++ b/example/src/Stress/index.tsx @@ -1,15 +1,13 @@ -import React, { useState, CSSProperties } from 'react'; +import { useState, CSSProperties, useCallback } from 'react'; import ReactFlow, { - removeElements, - addEdge, MiniMap, - isNode, Controls, Background, OnLoadParams, - Elements, - Connection, Edge, + Node, + ElementChange, + applyNodeChanges, } from 'react-flow-renderer'; import { getElements } from './utils'; @@ -21,38 +19,41 @@ const onLoad = (reactFlowInstance: OnLoadParams) => { console.log(reactFlowInstance.getElements()); }; -const initialElements: Elements = getElements(30, 30); +const initialElements = getElements(30, 30); const StressFlow = () => { - const [elements, setElements] = useState(initialElements); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + const [nodes, setNodes] = useState(initialElements.nodes); + const [edges, setEdges] = useState(initialElements.edges); + // const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + // const onConnect = (params: Connection | Edge, nds: Node[]) => setElements((els) => addEdge(params, els)); const updatePos = () => { - setElements((elms) => { - return elms.map((el) => { - if (isNode(el)) { - return { - ...el, - position: { - x: Math.random() * window.innerWidth, - y: Math.random() * window.innerHeight, - }, - }; - } - - return el; + setNodes((nds) => { + return nds.map((n) => { + return { + ...n, + position: { + x: Math.random() * window.innerWidth, + y: Math.random() * window.innerHeight, + }, + }; }); }); }; const updateElements = () => { const grid = Math.ceil(Math.random() * 10); - setElements(getElements(grid, grid)); + const initialElements = getElements(grid, grid); + setNodes(initialElements.nodes); + setEdges(initialElements.edges); }; + const onNodesChange = useCallback((changes: ElementChange[]) => { + setNodes((ns) => applyNodeChanges(changes, ns)); + }, []); + return ( - + diff --git a/example/src/Stress/utils.ts b/example/src/Stress/utils.ts index a37eb8de..0d2abc90 100644 --- a/example/src/Stress/utils.ts +++ b/example/src/Stress/utils.ts @@ -1,7 +1,13 @@ -import { Elements } from 'react-flow-renderer'; +import { Node, Edge } from 'react-flow-renderer'; -export function getElements(xElements: number = 10, yElements: number = 10): Elements { - const initialElements = []; +type ElementsCollection = { + nodes: Node[]; + edges: Edge[]; +}; + +export function getElements(xElements: number = 10, yElements: number = 10): ElementsCollection { + const initialNodes = []; + const initialEdges = []; let nodeId = 1; let recentNodeId = null; @@ -15,10 +21,10 @@ export function getElements(xElements: number = 10, yElements: number = 10): Ele data, position, }; - initialElements.push(node); + initialNodes.push(node); if (recentNodeId && nodeId <= xElements * yElements) { - initialElements.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() }); + initialEdges.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() }); } recentNodeId = nodeId; @@ -26,5 +32,8 @@ export function getElements(xElements: number = 10, yElements: number = 10): Ele } } - return initialElements; + return { + nodes: initialNodes, + edges: initialEdges, + }; } diff --git a/example/src/Switch/index.tsx b/example/src/Switch/index.tsx deleted file mode 100644 index 03dacf99..00000000 --- a/example/src/Switch/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { useState, MouseEvent } from 'react'; - -import ReactFlow, { removeElements, addEdge, Node, FlowElement, Elements, Connection, Edge } from 'react-flow-renderer'; - -const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); -const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); - -const elementsA: Elements = [ - { id: '1a', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, - { id: '2a', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, - { id: '3a', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, - { id: '4a', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' }, - { id: 'e1-2', source: '1a', target: '2a' }, - { id: 'e1-3', source: '1a', target: '3a' }, -]; - -const elementsB: Elements = [ - { id: 'inputb', type: 'input', data: { label: 'Input' }, position: { x: 300, y: 5 }, className: 'light' }, - { id: '1b', data: { label: 'Node 1' }, position: { x: 0, y: 100 }, className: 'light' }, - { id: '2b', data: { label: 'Node 2' }, position: { x: 200, y: 100 }, className: 'light' }, - { id: '3b', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, - { id: '4b', data: { label: 'Node 4' }, position: { x: 600, y: 100 }, className: 'light' }, - - { id: 'e1b', source: 'inputb', target: '1b' }, - { id: 'e2b', source: 'inputb', target: '2b' }, - { id: 'e3b', source: 'inputb', target: '3b' }, - { id: 'e4b', source: 'inputb', target: '4b' }, -]; - -const BasicFlow = () => { - const [elements, setElements] = useState(elementsA); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - - return ( - -
- - -
-
- ); -}; - -export default BasicFlow; diff --git a/example/src/Undirectional/CustomNode.tsx b/example/src/Undirectional/CustomNode.tsx deleted file mode 100644 index ab5853e8..00000000 --- a/example/src/Undirectional/CustomNode.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React, { memo, FC, CSSProperties } from 'react'; - -import { Handle, Position, NodeProps } from 'react-flow-renderer'; - -const nodeStyles: CSSProperties = { padding: '10px 15px', border: '1px solid #ddd' }; - -const CustomNode: FC = ({ id }) => { - return ( -
-
node {id}
- - - - -
- ); -}; - -export default memo(CustomNode); diff --git a/example/src/Undirectional/index.tsx b/example/src/Undirectional/index.tsx deleted file mode 100644 index f05ad21e..00000000 --- a/example/src/Undirectional/index.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import React, { useState, useCallback } from 'react'; - -import ReactFlow, { - NodeTypesType, - addEdge, - useZoomPanHelper, - ReactFlowProvider, - Elements, - Connection, - Edge, - ElementId, - ConnectionLineType, - ConnectionMode, - updateEdge, - ArrowHeadType, -} from 'react-flow-renderer'; -import CustomNode from './CustomNode'; - -const initialElements: Elements = [ - { - id: '00', - type: 'custom', - position: { x: 300, y: 250 }, - }, - { - id: '01', - type: 'custom', - position: { x: 100, y: 50 }, - }, - { - id: '02', - type: 'custom', - position: { x: 500, y: 50 }, - }, - { - id: '03', - type: 'custom', - position: { x: 500, y: 500 }, - }, - { - id: '04', - type: 'custom', - position: { x: 100, y: 500 }, - }, - { - id: '10', - type: 'custom', - position: { x: 300, y: 5 }, - }, - { - id: '20', - type: 'custom', - position: { x: 600, y: 250 }, - }, - { - id: '30', - type: 'custom', - position: { x: 300, y: 600 }, - }, - { - id: '40', - type: 'custom', - position: { x: 5, y: 250 }, - }, - { - id: 'e0-1a', - source: '00', - target: '01', - sourceHandle: 'left', - targetHandle: 'bottom', - type: 'smoothstep', - arrowHeadType: ArrowHeadType.Arrow, - }, - { - id: 'e0-1b', - source: '00', - target: '01', - sourceHandle: 'top', - targetHandle: 'right', - type: 'smoothstep', - arrowHeadType: ArrowHeadType.Arrow, - }, - { - id: 'e0-2a', - source: '00', - target: '02', - sourceHandle: 'top', - targetHandle: 'left', - type: 'smoothstep', - arrowHeadType: ArrowHeadType.Arrow, - }, - { - id: 'e0-2b', - source: '00', - target: '02', - sourceHandle: 'right', - targetHandle: 'bottom', - type: 'smoothstep', - arrowHeadType: ArrowHeadType.Arrow, - }, - { - id: 'e0-3a', - source: '00', - target: '03', - sourceHandle: 'right', - targetHandle: 'top', - type: 'smoothstep', - arrowHeadType: ArrowHeadType.Arrow, - }, - { - id: 'e0-3b', - source: '00', - target: '03', - sourceHandle: 'bottom', - targetHandle: 'left', - type: 'smoothstep', - arrowHeadType: ArrowHeadType.Arrow, - }, - { - id: 'e0-4a', - source: '00', - target: '04', - sourceHandle: 'bottom', - targetHandle: 'right', - type: 'smoothstep', - arrowHeadType: ArrowHeadType.Arrow, - }, - { - id: 'e0-4b', - source: '00', - target: '04', - sourceHandle: 'left', - targetHandle: 'top', - type: 'smoothstep', - arrowHeadType: ArrowHeadType.Arrow, - }, - { - id: 'e0-10', - source: '00', - target: '10', - sourceHandle: 'top', - targetHandle: 'bottom', - type: 'smoothstep', - arrowHeadType: ArrowHeadType.Arrow, - }, - { - id: 'e0-20', - source: '00', - target: '20', - sourceHandle: 'right', - targetHandle: 'left', - type: 'smoothstep', - arrowHeadType: ArrowHeadType.Arrow, - }, - { - id: 'e0-30', - source: '00', - target: '30', - sourceHandle: 'bottom', - targetHandle: 'top', - type: 'smoothstep', - arrowHeadType: ArrowHeadType.Arrow, - }, - { - id: 'e0-40', - source: '00', - target: '40', - sourceHandle: 'left', - targetHandle: 'right', - type: 'smoothstep', - arrowHeadType: ArrowHeadType.Arrow, - }, -]; - -const nodeTypes: NodeTypesType = { - custom: CustomNode, -}; - -let id = 4; -const getId = (): ElementId => `${id++}`; - -const UpdateNodeInternalsFlow = () => { - const [elements, setElements] = useState(initialElements); - const onConnect = (params: Connection | Edge) => - setElements((els) => addEdge({ ...params, type: 'smoothstep' }, els)); - const { project } = useZoomPanHelper(); - const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => - setElements((els) => updateEdge(oldEdge, newConnection, els)); - - const onPaneClick = useCallback( - (evt) => - setElements((els) => - els.concat({ - id: getId(), - position: project({ x: evt.clientX, y: evt.clientY - 40 }), - type: 'custom', - }) - ), - [project] - ); - - return ( - - ); -}; - -const WrappedFlow = () => ( - - - -); - -export default WrappedFlow; diff --git a/example/src/UpdatableEdge/index.tsx b/example/src/UpdatableEdge/index.tsx deleted file mode 100644 index 4ffdab04..00000000 --- a/example/src/UpdatableEdge/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React, { useState } from 'react'; -import ReactFlow, { - Controls, - updateEdge, - addEdge, - Elements, - OnLoadParams, - Connection, - Edge, - removeElements, -} from 'react-flow-renderer'; - -const initialElements: Elements = [ - { - id: '1', - type: 'input', - data: { - label: ( - <> - Node A - - ), - }, - position: { x: 250, y: 0 }, - }, - { - id: '2', - data: { - label: ( - <> - Node B - - ), - }, - position: { x: 100, y: 100 }, - }, - { - id: '3', - data: { - label: ( - <> - Node C - - ), - }, - position: { x: 400, y: 100 }, - style: { background: '#D6D5E6', color: '#333', border: '1px solid #222138', width: 180 }, - }, - { id: 'e1-2', source: '1', target: '2', label: 'This is a draggable edge' }, -]; - -const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView(); -const onEdgeUpdateStart = (_: React.MouseEvent, edge: Edge) => console.log('start update', edge); -const onEdgeUpdateEnd = (_: MouseEvent, edge: Edge) => console.log('end update', edge); - -const UpdatableEdge = () => { - const [elements, setElements] = useState(initialElements); - const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => - setElements((els) => updateEdge(oldEdge, newConnection, els)); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - - return ( - - - - ); -}; - -export default UpdatableEdge; diff --git a/example/src/UpdateNode/index.tsx b/example/src/UpdateNode/index.tsx index a7cce8f2..a4c10abe 100644 --- a/example/src/UpdateNode/index.tsx +++ b/example/src/UpdateNode/index.tsx @@ -1,64 +1,71 @@ -import React, { useEffect, useState } from 'react'; -import ReactFlow, { Elements } from 'react-flow-renderer'; +import { useEffect, useState, useCallback } from 'react'; +import ReactFlow, { Node, Edge, applyNodeChanges, ElementChange } from 'react-flow-renderer'; import './updatenode.css'; -const initialElements: Elements = [ +const initialNodes: Node[] = [ { id: '1', data: { label: '-' }, position: { x: 100, y: 100 } }, { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } }, - { id: 'e1-2', source: '1', target: '2' }, ]; +const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }]; + const UpdateNode = () => { - const [elements, setElements] = useState(initialElements); + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState(initialEdges); + const [nodeName, setNodeName] = useState('Node 1'); const [nodeBg, setNodeBg] = useState('#eee'); const [nodeHidden, setNodeHidden] = useState(false); useEffect(() => { - setElements((els) => - els.map((el) => { - if (el.id === '1') { + setNodes((nds) => + nds.map((n) => { + if (n.id === '1') { // it's important that you create a new object here in order to notify react flow about the change - el.data = { - ...el.data, + n.data = { + ...n.data, label: nodeName, }; } - return el; + return n; }) ); - }, [nodeName, setElements]); + }, [nodeName]); useEffect(() => { - setElements((els) => - els.map((el) => { - if (el.id === '1') { + setNodes((nds) => + nds.map((n) => { + if (n.id === '1') { // it's important that you create a new object here in order to notify react flow about the change - el.style = { ...el.style, backgroundColor: nodeBg }; + n.style = { ...n.style, backgroundColor: nodeBg }; } - return el; + return n; }) ); - }, [nodeBg, setElements]); + }, [nodeBg]); useEffect(() => { - setElements((els) => - els.map((el) => { - if (el.id === '1' || el.id === 'e1-2') { + setNodes((nds) => + nds.map((n) => { + if (n.id === '1' || n.id === 'e1-2') { // when you update a simple type you can just update the value - el.isHidden = nodeHidden; + n.isHidden = nodeHidden; } - return el; + return n; }) ); - }, [nodeHidden, setElements]); + }, [nodeHidden]); + + const onNodesChange = useCallback((changes: ElementChange[]) => { + setNodes((ns) => applyNodeChanges(changes, ns)); + }, []); return ( - +
setNodeName(evt.target.value)} /> diff --git a/example/src/UseUpdateNodeInternals/CustomNode.tsx b/example/src/UseUpdateNodeInternals/CustomNode.tsx deleted file mode 100644 index 969a4c3c..00000000 --- a/example/src/UseUpdateNodeInternals/CustomNode.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { memo, FC, useMemo, CSSProperties } from 'react'; - -import { Handle, Position, NodeProps } from 'react-flow-renderer'; - -const nodeStyles: CSSProperties = { padding: 10, border: '1px solid #ddd' }; - -const CustomNode: FC = ({ data }) => { - const handles = useMemo( - () => - Array.from({ length: data.handleCount }, (x, i) => { - const handleId = `handle-${i}`; - return ( - - ); - }), - [data.handleCount, data.handlePosition] - ); - - return ( -
- -
output handle count: {data.handleCount}
- {handles} -
- ); -}; - -export default memo(CustomNode); diff --git a/example/src/UseUpdateNodeInternals/index.tsx b/example/src/UseUpdateNodeInternals/index.tsx deleted file mode 100644 index 2b254c5a..00000000 --- a/example/src/UseUpdateNodeInternals/index.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { useState, useCallback, CSSProperties } from 'react'; - -import ReactFlow, { - NodeTypesType, - addEdge, - useZoomPanHelper, - ReactFlowProvider, - Elements, - Connection, - Edge, - ElementId, - useUpdateNodeInternals, - Position, - isEdge, -} from 'react-flow-renderer'; -import CustomNode from './CustomNode'; - -const initialHandleCount = 1; - -const initialElements: Elements = [ - { - id: '1', - type: 'custom', - data: { label: 'Node 1', handleCount: initialHandleCount, handlePosition: 0 }, - position: { x: 250, y: 5 }, - }, -]; - -const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 10 }; - -const nodeTypes: NodeTypesType = { - custom: CustomNode, -}; - -let id = 5; -const getId = (): ElementId => `${id++}`; - -const UpdateNodeInternalsFlow = () => { - const [elements, setElements] = useState(initialElements); - const updateNodeInternals = useUpdateNodeInternals(); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - const { project } = useZoomPanHelper(); - - const onPaneClick = useCallback( - (evt) => - setElements((els) => - els.concat({ - id: getId(), - position: project({ x: evt.clientX, y: evt.clientY - 40 }), - data: { label: 'new node' }, - targetPosition: Position.Left, - sourcePosition: Position.Right, - }) - ), - [project] - ); - - const toggleHandleCount = useCallback(() => { - setElements((els) => - els.map((el) => { - if (isEdge(el)) { - return el; - } - - return { ...el, data: { ...el.data, handleCount: el.data?.handleCount === 1 ? 2 : 1 } }; - }) - ); - }, []); - - const toggleHandlePosition = useCallback(() => { - setElements((els) => - els.map((el) => { - if (isEdge(el)) { - return el; - } - - return { ...el, data: { ...el.data, handlePosition: el.data?.handlePosition === 0 ? 1 : 0 } }; - }) - ); - }, []); - - const updateNode = useCallback(() => updateNodeInternals('1'), [updateNodeInternals]); - - return ( - -
- - - -
-
- ); -}; - -const WrappedFlow = () => ( - - - -); - -export default WrappedFlow; diff --git a/example/src/UseZoomPanHelper/index.tsx b/example/src/UseZoomPanHelper/index.tsx deleted file mode 100644 index 94ff30f8..00000000 --- a/example/src/UseZoomPanHelper/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { useState, useCallback } from 'react'; - -import ReactFlow, { - removeElements, - addEdge, - Background, - MiniMap, - useZoomPanHelper, - ReactFlowProvider, - Elements, - ElementId, - Connection, - Edge, -} from 'react-flow-renderer'; - -const initialElements: Elements = [ - { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, - { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, - { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, - { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' }, - { id: 'e1-2', source: '1', target: '2', animated: true }, - { id: 'e1-3', source: '1', target: '3' }, -]; - -let id = 5; -const getId = (): ElementId => `${id++}`; - -const UseZoomPanHelperFlow = () => { - const [elements, setElements] = useState(initialElements); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - const { project } = useZoomPanHelper(); - - const onPaneClick = useCallback( - (evt) => { - const projectedPosition = project({ x: evt.clientX, y: evt.clientY - 40 }); - - setElements((els) => - els.concat({ - id: getId(), - position: projectedPosition, - data: { - label: `${projectedPosition.x}-${projectedPosition.y}`, - }, - }) - ); - }, - [project] - ); - - return ( - - - - - ); -}; - -const WrappedFlow = () => ( - - - -); - -export default WrappedFlow; diff --git a/example/src/Validation/index.tsx b/example/src/Validation/index.tsx deleted file mode 100644 index d5209ed4..00000000 --- a/example/src/Validation/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useState, MouseEvent as ReactMouseEvent, FC } from 'react'; -import ReactFlow, { - addEdge, - Handle, - OnLoadParams, - Connection, - Position, - Elements, - Edge, - OnConnectStartParams, - NodeProps, - NodeTypesType, -} from 'react-flow-renderer'; - -import './validation.css'; - -const initialElements: Elements = [ - { id: '0', type: 'custominput', position: { x: 0, y: 150 } }, - { id: 'A', type: 'customnode', position: { x: 250, y: 0 } }, - { id: 'B', type: 'customnode', position: { x: 250, y: 150 } }, - { id: 'C', type: 'customnode', position: { x: 250, y: 300 } }, -]; - -const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView(); -const isValidConnection = (connection: Connection) => connection.target === 'B'; -const onConnectStart = (_: ReactMouseEvent, { nodeId, handleType }: OnConnectStartParams) => - console.log('on connect start', { nodeId, handleType }); -const onConnectStop = (event: MouseEvent) => console.log('on connect stop', event); -const onConnectEnd = (event: MouseEvent) => console.log('on connect end', event); - -const CustomInput: FC = () => ( - <> -
Only connectable with B
- - -); - -const CustomNode: FC = ({ id }) => ( - <> - -
{id}
- - -); - -const nodeTypes: NodeTypesType = { - custominput: CustomInput, - customnode: CustomNode, -}; - -const HorizontalFlow = () => { - const [elements, setElements] = useState(initialElements); - const onConnect = (params: Connection | Edge) => { - console.log('on connect', params); - setElements((els) => addEdge(params, els)); - }; - - return ( - - ); -}; - -export default HorizontalFlow; diff --git a/example/src/Validation/validation.css b/example/src/Validation/validation.css deleted file mode 100644 index a777e6c5..00000000 --- a/example/src/Validation/validation.css +++ /dev/null @@ -1,31 +0,0 @@ -.validationflow .react-flow__node { - width: 150px; - border-radius: 5px; - padding: 10px; - color: #555; - border: 1px solid #ddd; - text-align: center; - font-size: 12px; -} - -.validationflow .react-flow__node-customnode { - background: #e6e6e9; - border: 1px solid #ddd; -} - -.react-flow__node-custominput .react-flow__handle { - background: #e6e6e9; -} - -.validationflow .react-flow__node-custominput { - background: #fff; - -} - -.validationflow .react-flow__handle-connecting { - background: #ff6060; -} - -.validationflow .react-flow__handle-valid { - background: #55dd99; -} \ No newline at end of file diff --git a/example/src/index.tsx b/example/src/index.tsx index f81278ba..d6acbeba 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -2,134 +2,24 @@ import React, { ChangeEvent } from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter as Router, Route, Switch, withRouter } from 'react-router-dom'; -import Overview from './Overview'; import Basic from './Basic'; -import CustomNode from './CustomNode'; -import Stress from './Stress'; -import Interaction from './Interaction'; -import Empty from './Empty'; -import Edges from './Edges'; -import Validation from './Validation'; -import Provider from './Provider'; -import Hidden from './Hidden'; -import EdgeTypes from './EdgeTypes'; -import CustomConnectionLine from './CustomConnectionLine'; -import NodeTypeChange from './NodeTypeChange'; -import NodeTypesObjectChange from './NodeTypesObjectChange'; -import UpdatableEdge from './UpdatableEdge'; import UpdateNode from './UpdateNode'; -import SaveRestore from './SaveRestore'; -import DragNDrop from './DragNDrop'; -import Layout from './Layouting'; -import SwitchFlows from './Switch'; -import UseZoomPanHelper from './UseZoomPanHelper'; -import UseUpdateNodeInternals from './UseUpdateNodeInternals'; -import Undirectional from './Undirectional'; -import MultiFlows from './MultiFlows'; -import DragHandle from './DragHandle'; +import Stress from './Stress'; import './index.css'; const routes = [ - { - path: '/', - component: Overview, - }, - { - path: '/edges', - component: Edges, - }, - { - path: '/custom-node', - component: CustomNode, - }, - { - path: '/validation', - component: Validation, - }, - { - path: '/provider', - component: Provider, - }, - { - path: '/stress', - component: Stress, - }, - { - path: '/interaction', - component: Interaction, - }, { path: '/basic', component: Basic, }, - { - path: '/empty', - component: Empty, - }, - { - path: '/hidden', - component: Hidden, - }, - { - path: '/edge-types', - component: EdgeTypes, - }, - { - path: '/custom-connectionline', - component: CustomConnectionLine, - }, - { - path: '/nodetype-change', - component: NodeTypeChange, - }, - { - path: '/nodetypesobject-change', - component: NodeTypesObjectChange, - }, - { - path: '/updatable-edge', - component: UpdatableEdge, - }, { path: '/update-node', component: UpdateNode, }, { - path: '/save-restore', - component: SaveRestore, - }, - { - path: '/drag-and-drop', - component: DragNDrop, - }, - { - path: '/layouting', - component: Layout, - }, - { - path: '/switch', - component: SwitchFlows, - }, - { - path: '/usezoompanhelper', - component: UseZoomPanHelper, - }, - { - path: '/useupdatenodeinternals', - component: UseUpdateNodeInternals, - }, - { - path: '/undirectional', - component: Undirectional, - }, - { - path: '/multiflows', - component: MultiFlows, - }, - { - path: '/draghandle', - component: DragHandle, + path: '/stress', + component: Stress, }, ]; diff --git a/package-lock.json b/package-lock.json index ace876fc..9121c531 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "fast-deep-equal": "^3.1.3", "react-draggable": "^4.4.4", "react-redux": "^7.2.5", - "redux": "^4.1.1" + "redux": "^4.1.1", + "redux-thunk": "^2.3.0" }, "devDependencies": { "@babel/core": "^7.15.5", @@ -32,6 +33,7 @@ "@svgr/rollup": "^5.5.0", "@types/redux": "^3.6.31", "@types/resize-observer-browser": "^0.1.6", + "@welldone-software/why-did-you-render": "^6.2.1", "autoprefixer": "^10.3.6", "babel-preset-react-app": "^10.0.0", "cypress": "^8.5.0", @@ -52,6 +54,9 @@ "start-server-and-test": "^1.14.0", "typescript": "^4.4.3" }, + "engines": { + "node": ">=12" + }, "peerDependencies": { "react": "16 || 17", "react-dom": "16 || 17" @@ -2833,6 +2838,18 @@ "@types/node": "*" } }, + "node_modules/@welldone-software/why-did-you-render": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-6.2.1.tgz", + "integrity": "sha512-eIVKeK6ueS3tuzCqMVTaaNrPYvb9cA8NHiNgLA7Op8SD4TiT31zqNjxmhzLEK+y3sBxcwr6YhsiQGX9EThrvaw==", + "dev": true, + "dependencies": { + "lodash": "^4" + }, + "peerDependencies": { + "react": "^16 || ^17" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -9261,6 +9278,11 @@ "@babel/runtime": "^7.9.2" } }, + "node_modules/redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -13296,6 +13318,15 @@ "@types/node": "*" } }, + "@welldone-software/why-did-you-render": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-6.2.1.tgz", + "integrity": "sha512-eIVKeK6ueS3tuzCqMVTaaNrPYvb9cA8NHiNgLA7Op8SD4TiT31zqNjxmhzLEK+y3sBxcwr6YhsiQGX9EThrvaw==", + "dev": true, + "requires": { + "lodash": "^4" + } + }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -18047,6 +18078,11 @@ "@babel/runtime": "^7.9.2" } }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", diff --git a/package.json b/package.json index 3e5250e4..6f81fe7b 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "fast-deep-equal": "^3.1.3", "react-draggable": "^4.4.4", "react-redux": "^7.2.5", - "redux": "^4.1.1" + "redux": "^4.1.1", + "redux-thunk": "^2.3.0" }, "devDependencies": { "@babel/core": "^7.15.5", @@ -54,6 +55,7 @@ "@svgr/rollup": "^5.5.0", "@types/redux": "^3.6.31", "@types/resize-observer-browser": "^0.1.6", + "@welldone-software/why-did-you-render": "^6.2.1", "autoprefixer": "^10.3.6", "babel-preset-react-app": "^10.0.0", "cypress": "^8.5.0", From ee0d29029ae81505e047b9dcd7f2f8030421651c Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 13 Oct 2021 12:54:01 +0200 Subject: [PATCH 004/263] refactor(state): replace redux with zustand --- package-lock.json | 24 +- package.json | 3 +- .../Background/index.tsx | 8 +- src/additional-components/Controls/index.tsx | 11 +- src/additional-components/MiniMap/index.tsx | 12 +- .../ReactFlowProvider/index.tsx | 14 +- src/components/ConnectionLine/index.tsx | 7 +- src/components/Edges/BezierEdge.tsx | 1 - src/components/Edges/wrapEdge.tsx | 38 +- src/components/ElementUpdater/index.tsx | 12 +- src/components/Handle/handler.ts | 8 +- src/components/Handle/index.tsx | 35 +- src/components/Nodes/DefaultNode.tsx | 16 +- src/components/Nodes/utils.ts | 30 +- src/components/Nodes/wrapNode.tsx | 21 +- src/components/NodesSelection/index.tsx | 35 +- src/components/SelectionListener/index.tsx | 10 +- src/components/UserSelection/index.tsx | 33 +- src/container/EdgeRenderer/index.tsx | 41 +- src/container/FlowRenderer/index.tsx | 14 +- src/container/GraphView/index.tsx | 42 +- src/container/NodeRenderer/index.tsx | 30 +- src/container/ReactFlow/Wrapper.tsx | 31 +- src/container/ZoomPane/index.tsx | 24 +- src/hooks/useGlobalKeyHandler.ts | 22 +- src/hooks/useResizeHandler.ts | 7 +- src/hooks/useUpdateNodeInternals.ts | 10 +- src/hooks/useZoomPanHelper.ts | 14 +- src/index.ts | 2 +- src/store/actions.ts | 176 ------- src/store/configure-store.ts | 11 - src/store/contants.ts | 36 -- src/store/hooks.ts | 48 -- src/store/index.ts | 452 +++++++++++++++--- src/store/reducer.ts | 350 -------------- src/store/utils.ts | 5 - src/types/index.ts | 35 ++ src/utils/graph.ts | 16 +- 38 files changed, 787 insertions(+), 897 deletions(-) delete mode 100644 src/store/actions.ts delete mode 100644 src/store/configure-store.ts delete mode 100644 src/store/contants.ts delete mode 100644 src/store/hooks.ts delete mode 100644 src/store/reducer.ts delete mode 100644 src/store/utils.ts diff --git a/package-lock.json b/package-lock.json index 9121c531..a91d8284 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,8 @@ "react-draggable": "^4.4.4", "react-redux": "^7.2.5", "redux": "^4.1.1", - "redux-thunk": "^2.3.0" + "redux-thunk": "^2.3.0", + "zustand": "^3.5.13" }, "devDependencies": { "@babel/core": "^7.15.5", @@ -11257,6 +11258,22 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "node_modules/zustand": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.5.13.tgz", + "integrity": "sha512-orO/XcYwSWffsrPVTdCtuKM/zkUaOIyKDasOk/lecsD3R0euELsj+cB65uKZ1KyinrK2STHIuUhRoLpH8QprQg==", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } } }, "dependencies": { @@ -19593,6 +19610,11 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "zustand": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.5.13.tgz", + "integrity": "sha512-orO/XcYwSWffsrPVTdCtuKM/zkUaOIyKDasOk/lecsD3R0euELsj+cB65uKZ1KyinrK2STHIuUhRoLpH8QprQg==" } } } diff --git a/package.json b/package.json index 6f81fe7b..2f5d6d2c 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "react-draggable": "^4.4.4", "react-redux": "^7.2.5", "redux": "^4.1.1", - "redux-thunk": "^2.3.0" + "redux-thunk": "^2.3.0", + "zustand": "^3.5.13" }, "devDependencies": { "@babel/core": "^7.15.5", diff --git a/src/additional-components/Background/index.tsx b/src/additional-components/Background/index.tsx index 0e99a835..10ecf3e1 100644 --- a/src/additional-components/Background/index.tsx +++ b/src/additional-components/Background/index.tsx @@ -1,8 +1,8 @@ import React, { memo, useMemo, FC, HTMLAttributes } from 'react'; import cc from 'classcat'; -import { useStoreState } from '../../store/hooks'; -import { BackgroundVariant } from '../../types'; +import { useStore } from '../../store'; +import { BackgroundVariant, ReactFlowState } from '../../types'; import { createGridLinesPath, createGridDotsPath } from './utils'; export interface BackgroundProps extends HTMLAttributes { @@ -17,6 +17,8 @@ const defaultColors = { [BackgroundVariant.Lines]: '#eee', }; +const transformSelector = (s: ReactFlowState) => s.transform; + const Background: FC = ({ variant = BackgroundVariant.Dots, gap = 15, @@ -25,7 +27,7 @@ const Background: FC = ({ style, className, }) => { - const [x, y, scale] = useStoreState((s) => s.transform); + const [x, y, scale] = useStore(transformSelector); // when there are multiple flows on a page we need to make sure that every background gets its own pattern. const patternId = useMemo(() => `pattern-${Math.floor(Math.random() * 100000)}`, []); diff --git a/src/additional-components/Controls/index.tsx b/src/additional-components/Controls/index.tsx index 5e4ece7f..850ee781 100644 --- a/src/additional-components/Controls/index.tsx +++ b/src/additional-components/Controls/index.tsx @@ -1,7 +1,7 @@ import React, { memo, useCallback, HTMLAttributes, FC, useEffect, useState } from 'react'; import cc from 'classcat'; -import { useStoreState, useStoreActions } from '../../store/hooks'; +import { useStore } from '../../store'; import PlusIcon from '../../../assets/icons/plus.svg'; import MinusIcon from '../../../assets/icons/minus.svg'; @@ -10,7 +10,7 @@ import LockIcon from '../../../assets/icons/lock.svg'; import UnlockIcon from '../../../assets/icons/unlock.svg'; import useZoomPanHelper from '../../hooks/useZoomPanHelper'; -import { FitViewParams } from '../../types'; +import { FitViewParams, ReactFlowState } from '../../types'; export interface ControlProps extends HTMLAttributes { showZoom?: boolean; @@ -31,6 +31,9 @@ export const ControlButton: FC = ({ children, className, ... ); +const setInteractiveSelector = (s: ReactFlowState) => s.setInteractive; +const isInteractiveSelector = (s: ReactFlowState) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable; + const Controls: FC = ({ style, showZoom = true, @@ -45,10 +48,10 @@ const Controls: FC = ({ children, }) => { const [isVisible, setIsVisible] = useState(false); - const setInteractive = useStoreActions((actions) => actions.setInteractive); + const setInteractive = useStore(setInteractiveSelector); + const isInteractive = useStore(isInteractiveSelector); const { zoomIn, zoomOut, fitView } = useZoomPanHelper(); - const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable); const mapClasses = cc(['react-flow__controls', className]); const onZoomInHandler = useCallback(() => { diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index 6060b463..1675f6b1 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -1,9 +1,9 @@ import React, { memo, HTMLAttributes } from 'react'; import cc from 'classcat'; -import { useStoreState } from '../../store/hooks'; +import { useStore } from '../../store'; import { getRectOfNodes, getBoundsofRects } from '../../utils/graph'; -import { Node, Rect } from '../../types'; +import { Node, Rect, ReactFlowState } from '../../types'; import MiniMapNode from './MiniMapNode'; type StringFunc = (node: Node) => string; @@ -22,6 +22,8 @@ declare const window: any; const defaultWidth = 200; const defaultHeight = 150; +const selector = (s: ReactFlowState) => ({ width: s.width, height: s.height, transform: s.transform, nodes: s.nodes }); + const MiniMap = ({ style, className, @@ -32,10 +34,8 @@ const MiniMap = ({ nodeStrokeWidth = 2, maskColor = 'rgb(240, 242, 243, 0.7)', }: MiniMapProps) => { - const containerWidth = useStoreState((s) => s.width); - const containerHeight = useStoreState((s) => s.height); - const [tX, tY, tScale] = useStoreState((s) => s.transform); - const nodes = useStoreState((s) => s.nodes); + const { width: containerWidth, height: containerHeight, transform, nodes } = useStore(selector); + const [tX, tY, tScale] = transform; const mapClasses = cc(['react-flow__minimap', className]); const elementWidth = (style?.width || defaultWidth)! as number; diff --git a/src/additional-components/ReactFlowProvider/index.tsx b/src/additional-components/ReactFlowProvider/index.tsx index d1abc923..a7044a3f 100644 --- a/src/additional-components/ReactFlowProvider/index.tsx +++ b/src/additional-components/ReactFlowProvider/index.tsx @@ -1,16 +1,8 @@ -import React, { FC, useMemo } from 'react'; -import { Provider } from 'react-redux'; +import React, { FC } from 'react'; -import { initialState } from '../../store'; -import configureStore from '../../store/configure-store'; +import { Provider, createStore } from '../../store'; -const ReactFlowProvider: FC = ({ children }) => { - const store = useMemo(() => { - return configureStore(initialState); - }, []); - - return {children}; -}; +const ReactFlowProvider: FC = ({ children }) => {children}; ReactFlowProvider.displayName = 'ReactFlowProvider'; diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index 8c8a5ffc..da1ed174 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, CSSProperties } from 'react'; -import { useStoreState } from '../../store/hooks'; +import { useStore } from '../../store'; import { getBezierPath } from '../Edges/BezierEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { @@ -12,6 +12,7 @@ import { ConnectionLineType, ConnectionLineComponent, HandleType, + ReactFlowState, } from '../../types'; interface ConnectionLineProps { @@ -27,6 +28,8 @@ interface ConnectionLineProps { CustomConnectionLineComponent?: ConnectionLineComponent; } +const nodesSelector = (s: ReactFlowState) => s.nodes; + export default ({ connectionNodeId, connectionHandleId, @@ -39,7 +42,7 @@ export default ({ isConnectable, CustomConnectionLineComponent, }: ConnectionLineProps) => { - const nodes = useStoreState((state) => state.nodes); + const nodes = useStore(nodesSelector); const [sourceNode, setSourceNode] = useState(null); const nodeId = connectionNodeId; const handleId = connectionHandleId; diff --git a/src/components/Edges/BezierEdge.tsx b/src/components/Edges/BezierEdge.tsx index 368fdcc6..b75eb949 100644 --- a/src/components/Edges/BezierEdge.tsx +++ b/src/components/Edges/BezierEdge.tsx @@ -1,7 +1,6 @@ import React, { memo } from 'react'; import EdgeText from './EdgeText'; - import { getMarkerEnd, getCenter } from './utils'; import { EdgeProps, Position } from '../../types'; diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx index 60384e74..4e85d345 100644 --- a/src/components/Edges/wrapEdge.tsx +++ b/src/components/Edges/wrapEdge.tsx @@ -1,11 +1,19 @@ import React, { memo, ComponentType, useCallback, useState, useMemo } from 'react'; import cc from 'classcat'; -import { useStoreActions, useStoreState } from '../../store/hooks'; -import { Edge, EdgeProps, WrapEdgeProps } from '../../types'; +import { useStore, useStoreApi } from '../../store'; +import { Edge, EdgeProps, WrapEdgeProps, ReactFlowState } from '../../types'; import { onMouseDown } from '../../components/Handle/handler'; import { EdgeAnchor } from './EdgeAnchor'; +const selector = (s: ReactFlowState) => ({ + addSelectedElements: s.addSelectedElements, + setConnectionNodeId: s.setConnectionNodeId, + unsetNodesSelection: s.unsetNodesSelection, + setPosition: s.setConnectionPosition, + connectionMode: s.connectionMode, +}); + export default (EdgeComponent: ComponentType) => { const EdgeWrapper = ({ id, @@ -47,11 +55,9 @@ export default (EdgeComponent: ComponentType) => { onEdgeUpdateStart, onEdgeUpdateEnd, }: WrapEdgeProps): JSX.Element | null => { - const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); - const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId); - const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); - const setPosition = useStoreActions((actions) => actions.setConnectionPosition); - const connectionMode = useStoreState((state) => state.connectionMode); + const store = useStoreApi(); + const { addSelectedElements, setConnectionNodeId, unsetNodesSelection, setPosition, connectionMode } = + useStore(selector); const [updating, setUpdating] = useState(false); @@ -90,7 +96,7 @@ export default (EdgeComponent: ComponentType) => { (event: React.MouseEvent): void => { if (elementsSelectable) { unsetNodesSelection(); - addSelectedElements(edgeElement); + addSelectedElements([edgeElement]); } onClick?.(event, edgeElement); @@ -157,10 +163,22 @@ export default (EdgeComponent: ComponentType) => { isValidConnection, connectionMode, isSourceHandle ? 'target' : 'source', - _onEdgeUpdate + _onEdgeUpdate, + store.getState ); }, - [id, source, target, type, sourceHandleId, targetHandleId, setConnectionNodeId, setPosition, edgeElement, onConnectEdge] + [ + id, + source, + target, + type, + sourceHandleId, + targetHandleId, + setConnectionNodeId, + setPosition, + edgeElement, + onConnectEdge, + ] ); const onEdgeUpdaterSourceMouseDown = useCallback( diff --git a/src/components/ElementUpdater/index.tsx b/src/components/ElementUpdater/index.tsx index f40636da..555abc7a 100644 --- a/src/components/ElementUpdater/index.tsx +++ b/src/components/ElementUpdater/index.tsx @@ -1,16 +1,20 @@ import { useEffect } from 'react'; -import { useStoreActions } from '../../store/hooks'; -import { Node, Edge } from '../../types'; +import { useStore } from '../../store'; +import { Node, Edge, ReactFlowState } from '../../types'; interface ElementUpdaterProps { nodes: Node[]; edges: Edge[]; } +const selector = (s: ReactFlowState) => ({ + setNodes: s.setNodes, + setEdges: s.setEdges, +}); + const ElementUpdater = ({ nodes, edges }: ElementUpdaterProps) => { - const setNodes = useStoreActions((actions) => actions.setNodes); - const setEdges = useStoreActions((actions) => actions.setEdges); + const { setNodes, setEdges } = useStore(selector); useEffect(() => { setNodes(nodes); diff --git a/src/components/Handle/handler.ts b/src/components/Handle/handler.ts index f2733a77..53f7d213 100644 --- a/src/components/Handle/handler.ts +++ b/src/components/Handle/handler.ts @@ -1,9 +1,8 @@ import { MouseEvent as ReactMouseEvent } from 'react'; -import { Store } from 'redux'; +import { GetState } from 'zustand'; import { getHostForElement } from '../../utils'; import { ReactFlowState } from '../../types'; -import { ReactFlowAction } from '../../store/actions'; import { ElementId, @@ -107,7 +106,7 @@ export function onMouseDown( onConnectStart?: OnConnectStartFunc, onConnectStop?: OnConnectStopFunc, onConnectEnd?: OnConnectEndFunc, - store?: Store + getState?: GetState ): void { const reactFlowNode = (event.target as Element).closest('.react-flow'); // when react-flow is used inside a shadow root we can't use document @@ -181,7 +180,8 @@ export function onMouseDown( onConnectStop?.(event); if (isValid) { - onConnect?.(connection, store?.getState().nodes || []); + const nodes = getState?.().nodes; + onConnect?.(connection, nodes || []); } onConnectEnd?.(event); diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx index 317efe65..e8a152b6 100644 --- a/src/components/Handle/index.tsx +++ b/src/components/Handle/index.tsx @@ -1,9 +1,9 @@ import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react'; import cc from 'classcat'; -import { useStoreActions, useStoreState, useStore } from '../../store/hooks'; +import { useStore } from '../../store'; import NodeIdContext from '../../contexts/NodeIdContext'; -import { HandleProps, Connection, ElementId, Position, Node } from '../../types'; +import { HandleProps, Connection, ElementId, Position, Node, ReactFlowState } from '../../types'; import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler'; @@ -11,6 +11,16 @@ const alwaysValid = () => true; export type HandleComponentProps = HandleProps & Omit, 'id'>; +const selector = (s: ReactFlowState) => ({ + setPosition: s.setConnectionPosition, + setConnectionNodeId: s.setConnectionNodeId, + onConnectAction: s.onConnect, + onConnectStart: s.onConnectStart, + onConnectStop: s.onConnectStop, + onConnectEnd: s.onConnectEnd, + connectionMode: s.connectionMode, +}); + const Handle = forwardRef( ( { @@ -26,15 +36,17 @@ const Handle = forwardRef( }, ref ) => { - const store = useStore(); const nodeId = useContext(NodeIdContext) as ElementId; - const setPosition = useStoreActions((actions) => actions.setConnectionPosition); - const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId); - const onConnectAction = useStoreState((state) => state.onConnect); - const onConnectStart = useStoreState((state) => state.onConnectStart); - const onConnectStop = useStoreState((state) => state.onConnectStop); - const onConnectEnd = useStoreState((state) => state.onConnectEnd); - const connectionMode = useStoreState((state) => state.connectionMode); + const { + setPosition, + setConnectionNodeId, + onConnectAction, + onConnectStart, + onConnectStop, + onConnectEnd, + connectionMode, + } = useStore(selector); + const handleId = id || null; const isTarget = type === 'target'; @@ -62,8 +74,7 @@ const Handle = forwardRef( undefined, onConnectStart, onConnectStop, - onConnectEnd, - store + onConnectEnd ); }, [ diff --git a/src/components/Nodes/DefaultNode.tsx b/src/components/Nodes/DefaultNode.tsx index 11d9af2f..7d9dc5d5 100644 --- a/src/components/Nodes/DefaultNode.tsx +++ b/src/components/Nodes/DefaultNode.tsx @@ -8,13 +8,15 @@ const DefaultNode = ({ isConnectable, targetPosition = Position.Top, sourcePosition = Position.Bottom, -}: NodeProps) => ( - <> - - {data.label} - - -); +}: NodeProps) => { + return ( + <> + + {data.label} + + + ); +}; DefaultNode.displayName = 'DefaultNode'; diff --git a/src/components/Nodes/utils.ts b/src/components/Nodes/utils.ts index e917e853..1ed82013 100644 --- a/src/components/Nodes/utils.ts +++ b/src/components/Nodes/utils.ts @@ -13,7 +13,7 @@ export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => { export const getHandleBoundsByHandleType = ( selector: string, nodeElement: HTMLDivElement, - parentBounds: ClientRect | DOMRect, + parentBounds: DOMRect, k: number ): HandleElement[] | null => { const handles = nodeElement.querySelectorAll(selector); @@ -24,20 +24,18 @@ export const getHandleBoundsByHandleType = ( const handlesArray = Array.from(handles) as HTMLDivElement[]; - return handlesArray.map( - (handle): HandleElement => { - const bounds = handle.getBoundingClientRect(); - const dimensions = getDimensions(handle); - const handleId = handle.getAttribute('data-handleid'); - const handlePosition = (handle.getAttribute('data-handlepos') as unknown) as Position; + return handlesArray.map((handle): HandleElement => { + const bounds = handle.getBoundingClientRect(); + const dimensions = getDimensions(handle); + const handleId = handle.getAttribute('data-handleid'); + const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position; - return { - id: handleId, - position: handlePosition, - x: (bounds.left - parentBounds.left) / k, - y: (bounds.top - parentBounds.top) / k, - ...dimensions, - }; - } - ); + return { + id: handleId, + position: handlePosition, + x: (bounds.left - parentBounds.left) / k, + y: (bounds.top - parentBounds.top) / k, + ...dimensions, + }; + }); }; diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 04f9acef..285a011e 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -2,9 +2,15 @@ import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'; import cc from 'classcat'; -import { useStoreActions, useStoreState } from '../../store/hooks'; +import { useStore } from '../../store'; import { Provider } from '../../contexts/NodeIdContext'; -import { NodeComponentProps, WrapNodeProps } from '../../types'; +import { NodeComponentProps, WrapNodeProps, ReactFlowState } from '../../types'; + +const selector = (s: ReactFlowState) => ({ + addSelectedElements: s.addSelectedElements, + onNodesChange: s.onNodesChange, + unsetNodesSelection: s.unsetNodesSelection, +}); export default (NodeComponent: ComponentType) => { const NodeWrapper = ({ @@ -41,10 +47,7 @@ export default (NodeComponent: ComponentType) => { dragHandle, }: WrapNodeProps) => { // const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); - const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); - const onNodesChange = useStoreState((state) => state.onNodesChange); - const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); - + const { addSelectedElements, onNodesChange, unsetNodesSelection } = useStore(selector); const nodeElement = useRef(null); const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]); @@ -114,7 +117,7 @@ export default (NodeComponent: ComponentType) => { unsetNodesSelection(); if (!selected) { - addSelectedElements(node); + addSelectedElements([node]); } } @@ -132,7 +135,7 @@ export default (NodeComponent: ComponentType) => { unsetNodesSelection(); if (!selected) { - addSelectedElements(node); + addSelectedElements([node]); } } else if (!selectNodesOnDrag && !selected && isSelectable) { unsetNodesSelection(); @@ -173,7 +176,7 @@ export default (NodeComponent: ComponentType) => { // Because of that we set dragging to true inside the onDrag handler and handle the click here if (!isDragging) { if (isSelectable && !selectNodesOnDrag && !selected) { - addSelectedElements(node); + addSelectedElements([node]); } onClick?.(event as MouseEvent, node); diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index 1b3c3257..26a792f0 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -6,9 +6,9 @@ import React, { useMemo, useCallback, useRef, MouseEvent } from 'react'; import ReactDraggable, { DraggableData } from 'react-draggable'; -import { useStoreState, useStoreActions } from '../../store/hooks'; +import { useStore } from '../../store'; import { isNode } from '../../utils/graph'; -import { Node } from '../../types'; +import { Node, ReactFlowState } from '../../types'; export interface NodesSelectionProps { onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void; @@ -17,21 +17,34 @@ export interface NodesSelectionProps { onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; } +const selector = (s: ReactFlowState) => ({ + transform: s.transform, + selectedNodesBbox: s.selectedNodesBbox, + selectionActive: s.selectionActive, + selectedElements: s.selectedElements, + snapToGrid: s.snapToGrid, + snapGrid: s.snapGrid, + nodes: s.nodes, + updateNodePosDiff: s.updateNodePosDiff, +}); + export default ({ onSelectionDragStart, onSelectionDrag, onSelectionDragStop, onSelectionContextMenu, }: NodesSelectionProps) => { - const [tX, tY, tScale] = useStoreState((state) => state.transform); - const selectedNodesBbox = useStoreState((state) => state.selectedNodesBbox); - const selectionActive = useStoreState((state) => state.selectionActive); - const selectedElements = useStoreState((state) => state.selectedElements); - const snapToGrid = useStoreState((state) => state.snapToGrid); - const snapGrid = useStoreState((state) => state.snapGrid); - const nodes = useStoreState((state) => state.nodes); - - const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff); + const { + transform, + selectedNodesBbox, + selectionActive, + selectedElements, + snapToGrid, + snapGrid, + nodes, + updateNodePosDiff, + } = useStore(selector); + const [tX, tY, tScale] = transform; const nodeRef = useRef(null); diff --git a/src/components/SelectionListener/index.tsx b/src/components/SelectionListener/index.tsx index 98e1e0d4..ecda15db 100644 --- a/src/components/SelectionListener/index.tsx +++ b/src/components/SelectionListener/index.tsx @@ -1,18 +1,18 @@ import { useEffect } from 'react'; -import { Elements } from '../../types'; -import { useStoreState } from '../../store/hooks'; +import { Elements, ReactFlowState } from '../../types'; +import { useStore } from '../../store'; interface SelectionListenerProps { onSelectionChange: (elements: Elements | null) => void; } +const selectedElementsSelector = (s: ReactFlowState) => s.selectedElements; + // This is just a helper component for calling the onSelectionChange listener. -// As soon as easy-peasy has implemented the effectOn hook, we can remove this component -// and use the hook instead. https://github.com/ctrlplusb/easy-peasy/pull/459 export default ({ onSelectionChange }: SelectionListenerProps) => { - const selectedElements = useStoreState((s) => s.selectedElements); + const selectedElements = useStore(selectedElementsSelector); useEffect(() => { onSelectionChange(selectedElements); diff --git a/src/components/UserSelection/index.tsx b/src/components/UserSelection/index.tsx index 14a8a13a..ca0fe9d2 100644 --- a/src/components/UserSelection/index.tsx +++ b/src/components/UserSelection/index.tsx @@ -4,8 +4,8 @@ import React, { memo } from 'react'; -import { useStoreActions, useStoreState } from '../../store/hooks'; -import { XYPosition } from '../../types'; +import { useStore } from '../../store'; +import { XYPosition, ReactFlowState } from '../../types'; type UserSelectionProps = { selectionKeyPressed: boolean; @@ -25,8 +25,10 @@ function getMousePosition(event: React.MouseEvent): XYPosition | void { }; } +const userSelectionRectSelector = (state: ReactFlowState) => state.userSelectionRect; + const SelectionRect = () => { - const userSelectionRect = useStoreState((state) => state.userSelectionRect); + const userSelectionRect = useStore(userSelectionRectSelector); if (!userSelectionRect.draw) { return null; @@ -44,14 +46,25 @@ const SelectionRect = () => { ); }; -export default memo(({ selectionKeyPressed }: UserSelectionProps) => { - const selectionActive = useStoreState((state) => state.selectionActive); - const elementsSelectable = useStoreState((state) => state.elementsSelectable); +const selector = (s: ReactFlowState) => ({ + selectionActive: s.selectionActive, + elementsSelectable: s.elementsSelectable, + setUserSelection: s.setUserSelection, + updateUserSelection: s.updateUserSelection, + unsetUserSelection: s.unsetUserSelection, + unsetNodesSelection: s.unsetNodesSelection, +}); + +export default memo(({ selectionKeyPressed }: UserSelectionProps) => { + const { + selectionActive, + elementsSelectable, + setUserSelection, + updateUserSelection, + unsetUserSelection, + unsetNodesSelection, + } = useStore(selector); - const setUserSelection = useStoreActions((actions) => actions.setUserSelection); - const updateUserSelection = useStoreActions((actions) => actions.updateUserSelection); - const unsetUserSelection = useStoreActions((actions) => actions.unsetUserSelection); - const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); const renderUserSelectionPane = selectionActive || selectionKeyPressed; if (!elementsSelectable || !renderUserSelectionPane) { diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 3554d2f8..6c922d6d 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -1,6 +1,6 @@ import React, { memo, CSSProperties, useCallback } from 'react'; -import { useStoreState } from '../../store/hooks'; +import { useStore } from '../../store'; import ConnectionLine from '../../components/ConnectionLine/index'; import { isEdge } from '../../utils/graph'; import MarkerDefinitions from './MarkerDefinitions'; @@ -15,6 +15,7 @@ import { ConnectionLineComponent, ConnectionMode, OnEdgeUpdateFunc, + ReactFlowState, } from '../../types'; interface EdgeRendererProps { @@ -199,18 +200,34 @@ const Edge = memo( } ); +const selector = (s: ReactFlowState) => ({ + transform: s.transform, + edges: s.edges, + connectionNodeId: s.connectionNodeId, + connectionHandleId: s.connectionHandleId, + connectionHandleType: s.connectionHandleType, + connectionPosition: s.connectionPosition, + selectedElements: s.selectedElements, + nodesConnectable: s.nodesConnectable, + elementsSelectable: s.elementsSelectable, + width: s.width, + height: s.height, +}); + const EdgeRenderer = (props: EdgeRendererProps) => { - const transform = useStoreState((state) => state.transform); - const edges = useStoreState((state) => state.edges); - const connectionNodeId = useStoreState((state) => state.connectionNodeId); - const connectionHandleId = useStoreState((state) => state.connectionHandleId); - const connectionHandleType = useStoreState((state) => state.connectionHandleType); - const connectionPosition = useStoreState((state) => state.connectionPosition); - const selectedElements = useStoreState((state) => state.selectedElements); - const nodesConnectable = useStoreState((state) => state.nodesConnectable); - const elementsSelectable = useStoreState((state) => state.elementsSelectable); - const width = useStoreState((state) => state.width); - const height = useStoreState((state) => state.height); + const { + transform, + edges, + connectionNodeId, + connectionHandleId, + connectionHandleType, + connectionPosition, + selectedElements, + nodesConnectable, + elementsSelectable, + width, + height, + } = useStore(selector); if (!width) { return null; diff --git a/src/container/FlowRenderer/index.tsx b/src/container/FlowRenderer/index.tsx index 58ee5884..39d951e5 100644 --- a/src/container/FlowRenderer/index.tsx +++ b/src/container/FlowRenderer/index.tsx @@ -1,13 +1,13 @@ import React, { useCallback, memo, ReactNode, WheelEvent, MouseEvent } from 'react'; -import { useStoreActions, useStoreState } from '../../store/hooks'; +import { useStore } from '../../store'; import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler'; import useKeyPress from '../../hooks/useKeyPress'; - import { GraphViewProps } from '../GraphView'; import ZoomPane from '../ZoomPane'; import UserSelection from '../../components/UserSelection'; import NodesSelection from '../../components/NodesSelection'; +import { ReactFlowState } from '../../types'; interface FlowRendererProps extends Omit< @@ -25,6 +25,12 @@ interface FlowRendererProps children: ReactNode; } +const selector = (s: ReactFlowState) => ({ + unsetNodesSelection: s.unsetNodesSelection, + resetSelectedElements: s.resetSelectedElements, + nodesSelectionActive: s.nodesSelectionActive, +}); + const FlowRenderer = ({ children, onPaneClick, @@ -54,9 +60,7 @@ const FlowRenderer = ({ onSelectionDragStop, onSelectionContextMenu, }: FlowRendererProps) => { - const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); - const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements); - const nodesSelectionActive = useStoreState((state) => state.nodesSelectionActive); + const { unsetNodesSelection, resetSelectedElements, nodesSelectionActive } = useStore(selector); const selectionKeyPressed = useKeyPress(selectionKeyCode); diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index efcfe4e5..bfcbf045 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, memo } from 'react'; -import { useStoreActions, useStore } from '../../store/hooks'; +import { useStore, useStoreApi } from '../../store'; import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; @@ -98,24 +98,24 @@ const GraphView = ({ onEdgesChange, }: GraphViewProps) => { const isInitialized = useRef(false); - const setOnConnect = useStoreActions((actions) => actions.setOnConnect); - const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart); - const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop); - const setOnConnectEnd = useStoreActions((actions) => actions.setOnConnectEnd); - const setSnapGrid = useStoreActions((actions) => actions.setSnapGrid); - const setSnapToGrid = useStoreActions((actions) => actions.setSnapToGrid); - const setNodesDraggable = useStoreActions((actions) => actions.setNodesDraggable); - const setNodesConnectable = useStoreActions((actions) => actions.setNodesConnectable); - const setElementsSelectable = useStoreActions((actions) => actions.setElementsSelectable); - const setMinZoom = useStoreActions((actions) => actions.setMinZoom); - const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom); - const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent); - const setNodeExtent = useStoreActions((actions) => actions.setNodeExtent); - const setConnectionMode = useStoreActions((actions) => actions.setConnectionMode); - const setOnNodesChange = useStoreActions((actions) => actions.setOnNodesChange); - const setOnEdgesChange = useStoreActions((actions) => actions.setOnEdgesChange); + const store = useStoreApi(); + const setOnConnect = useStore((s) => s.setOnConnect); + const setOnConnectStart = useStore((s) => s.setOnConnectStart); + const setOnConnectStop = useStore((s) => s.setOnConnectStop); + const setOnConnectEnd = useStore((s) => s.setOnConnectEnd); + const setSnapGrid = useStore((s) => s.setSnapGrid); + const setSnapToGrid = useStore((s) => s.setSnapToGrid); + const setNodesDraggable = useStore((s) => s.setNodesDraggable); + const setNodesConnectable = useStore((s) => s.setNodesConnectable); + const setElementsSelectable = useStore((s) => s.setElementsSelectable); + const setMinZoom = useStore((s) => s.setMinZoom); + const setMaxZoom = useStore((s) => s.setMaxZoom); + const setTranslateExtent = useStore((s) => s.setTranslateExtent); + const setNodeExtent = useStore((s) => s.setNodeExtent); + const setConnectionMode = useStore((s) => s.setConnectionMode); + const setOnNodesChange = useStore((s) => s.setOnNodesChange); + const setOnEdgesChange = useStore((s) => s.setOnEdgesChange); - const currentStore = useStore(); const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper(); useEffect(() => { @@ -127,9 +127,9 @@ const GraphView = ({ zoomOut, zoomTo, setTransform: transform, - project: onLoadProject(currentStore), - getElements: onLoadGetElements(currentStore), - toObject: onLoadToObject(currentStore), + project: onLoadProject(store.getState), + getElements: onLoadGetElements(store.getState), + toObject: onLoadToObject(store.getState), }); } diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 97669df3..cc47f8f3 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -1,7 +1,7 @@ import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; -import { useStoreState, useStoreActions } from '../../store/hooks'; -import { Node, NodeTypesType, WrapNodeProps, Edge } from '../../types'; +import { useStore } from '../../store'; +import { Node, NodeTypesType, ReactFlowState, Edge, WrapNodeProps } from '../../types'; interface NodeRendererProps { nodeTypes: NodeTypesType; selectNodesOnDrag: boolean; @@ -19,14 +19,26 @@ interface NodeRendererProps { onlyRenderVisibleElements: boolean; } +const selector = (s: ReactFlowState) => ({ + transform: s.transform, + selectedElements: s.selectedElements, + nodesDraggable: s.nodesDraggable, + nodesConnectable: s.nodesConnectable, + elementsSelectable: s.elementsSelectable, + nodes: s.nodes, + updateNodeDimensions: s.updateNodeDimensions, +}); + const NodeRenderer = (props: NodeRendererProps) => { - const transform = useStoreState((state) => state.transform); - const selectedElements = useStoreState((state) => state.selectedElements); - const nodesDraggable = useStoreState((state) => state.nodesDraggable); - const nodesConnectable = useStoreState((state) => state.nodesConnectable); - const elementsSelectable = useStoreState((state) => state.elementsSelectable); - const nodes = useStoreState((state) => state.nodes); - const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); + const { + transform, + selectedElements, + nodesDraggable, + nodesConnectable, + elementsSelectable, + nodes, + updateNodeDimensions, + } = useStore(selector); // const visibleNodes = props.onlyRenderVisibleElements // ? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true) diff --git a/src/container/ReactFlow/Wrapper.tsx b/src/container/ReactFlow/Wrapper.tsx index 4a41a47c..67f22cf6 100644 --- a/src/container/ReactFlow/Wrapper.tsx +++ b/src/container/ReactFlow/Wrapper.tsx @@ -1,21 +1,26 @@ -import React, { FC, useContext, useMemo } from 'react'; -import { Provider, ReactReduxContext } from 'react-redux'; +import React, { FC } from 'react'; -import store from '../../store'; +import { Provider, createStore } from '../../store'; +// import { ReactFlowState } from '../../types'; + +// const reactFlowVersionSelector = (s: ReactFlowState) => s.reactFlowVersion; const Wrapper: FC = ({ children }) => { - const contextValue = useContext(ReactReduxContext); - const isWrappedWithReactFlowProvider = useMemo(() => contextValue?.store?.getState()?.reactFlowVersion, [ - contextValue, - ]); + // let isWrapped = useRef(true); - if (isWrappedWithReactFlowProvider) { - // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode - // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 - return <>{children}; - } + // try { + // useStoreApi(); + // } catch { + // isWrapped.current = false; + // } - return {children}; + // if (isWrapped) { + // // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode + // // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 + // return <>{children}; + // } + + return {children}; }; Wrapper.displayName = 'ReactFlowWrapper'; diff --git a/src/container/ZoomPane/index.tsx b/src/container/ZoomPane/index.tsx index 5af2659b..6f62473a 100644 --- a/src/container/ZoomPane/index.tsx +++ b/src/container/ZoomPane/index.tsx @@ -5,8 +5,8 @@ import { select, pointer } from 'd3-selection'; import { clamp } from '../../utils'; import useKeyPress from '../../hooks/useKeyPress'; import useResizeHandler from '../../hooks/useResizeHandler'; -import { useStoreState, useStoreActions, useStore } from '../../store/hooks'; -import { FlowTransform, TranslateExtent, PanOnScrollMode, KeyCode } from '../../types'; +import { useStore, useStoreApi } from '../../store'; +import { FlowTransform, TranslateExtent, PanOnScrollMode, KeyCode, ReactFlowState } from '../../types'; interface ZoomPaneProps { selectionKeyPressed: boolean; @@ -42,6 +42,15 @@ const eventToFlowTransform = (eventTransform: any): FlowTransform => ({ const hasNoWheelClass = (event: any) => event.target.closest('.nowheel'); +const selector = (s: ReactFlowState) => ({ + d3Zoom: s.d3Zoom, + d3Selection: s.d3Selection, + d3ZoomHandler: s.d3ZoomHandler, + + initD3Zoom: s.initD3Zoom, + updateTransform: s.updateTransform, +}); + const ZoomPane = ({ onMove, onMoveStart, @@ -62,17 +71,10 @@ const ZoomPane = ({ preventScrolling = true, children, }: ZoomPaneProps) => { + const store = useStoreApi(); const zoomPane = useRef(null); const prevTransform = useRef({ x: 0, y: 0, zoom: 0 }); - - const store = useStore(); - const d3Zoom = useStoreState((s) => s.d3Zoom); - const d3Selection = useStoreState((s) => s.d3Selection); - const d3ZoomHandler = useStoreState((s) => s.d3ZoomHandler); - - const initD3Zoom = useStoreActions((actions) => actions.initD3Zoom); - const updateTransform = useStoreActions((actions) => actions.updateTransform); - + const { d3Zoom, d3Selection, d3ZoomHandler, initD3Zoom, updateTransform } = useStore(selector); const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode); useResizeHandler(zoomPane); diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index 4a09041b..dcb9eade 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -1,23 +1,27 @@ import { useEffect } from 'react'; -import { useStore, useStoreActions, useStoreState } from '../store/hooks'; +import { useStore, useStoreApi } from '../store'; import useKeyPress from './useKeyPress'; import { isNode, isEdge, getConnectedEdges } from '../utils/graph'; -import { KeyCode } from '../types'; +import { KeyCode, ReactFlowState } from '../types'; interface HookParams { deleteKeyCode: KeyCode; multiSelectionKeyCode: KeyCode; } -export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { - const store = useStore(); +const selector = (s: ReactFlowState) => ({ + unsetNodesSelection: s.unsetNodesSelection, + setMultiSelectionActive: s.setMultiSelectionActive, + resetSelectedElements: s.resetSelectedElements, + onNodesChange: s.onNodesChange, + onEdgesChange: s.onEdgesChange, +}); - const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); - const setMultiSelectionActive = useStoreActions((actions) => actions.setMultiSelectionActive); - const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements); - const onNodesChange = useStoreState((state) => state.onNodesChange); - const onEdgesChange = useStoreState((state) => state.onEdgesChange); +export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { + const store = useStoreApi(); + const { unsetNodesSelection, setMultiSelectionActive, resetSelectedElements, onNodesChange, onEdgesChange } = + useStore(selector); const deleteKeyPressed = useKeyPress(deleteKeyCode); const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); diff --git a/src/hooks/useResizeHandler.ts b/src/hooks/useResizeHandler.ts index 164c7686..921aaef3 100644 --- a/src/hooks/useResizeHandler.ts +++ b/src/hooks/useResizeHandler.ts @@ -1,10 +1,13 @@ import { useEffect, MutableRefObject } from 'react'; -import { useStoreActions } from '../store/hooks'; +import { useStore } from '../store'; import { getDimensions } from '../utils'; +import { ReactFlowState } from '../types'; + +const updateSizeSelector = (state: ReactFlowState) => state.updateSize; export default (rendererNode: MutableRefObject) => { - const updateSize = useStoreActions((actions) => actions.updateSize); + const updateSize = useStore(updateSizeSelector); useEffect(() => { let resizeObserver: ResizeObserver; diff --git a/src/hooks/useUpdateNodeInternals.ts b/src/hooks/useUpdateNodeInternals.ts index 7d64af8a..69e978a2 100644 --- a/src/hooks/useUpdateNodeInternals.ts +++ b/src/hooks/useUpdateNodeInternals.ts @@ -1,13 +1,15 @@ import { useCallback } from 'react'; -import { useStoreActions } from '../store/hooks'; -import { ElementId, UpdateNodeInternals } from '../types'; +import { useStore } from '../store'; +import { ElementId, UpdateNodeInternals, ReactFlowState } from '../types'; + +const updateNodeDimsSelector = (state: ReactFlowState) => state.updateNodeDimensions; function useUpdateNodeInternals(): UpdateNodeInternals { - const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); + const updateNodeDimensions = useStore(updateNodeDimsSelector); return useCallback((id: ElementId) => { - const nodeElement = document.querySelector(`.react-flow__node[data-id="${id}"]`); + const nodeElement = document.querySelector(`.react-flow__node[data-id="${id}"]`) as HTMLDivElement; if (nodeElement) { updateNodeDimensions([{ id, nodeElement, forceUpdate: true }]); diff --git a/src/hooks/useZoomPanHelper.ts b/src/hooks/useZoomPanHelper.ts index f5b91403..525e1064 100644 --- a/src/hooks/useZoomPanHelper.ts +++ b/src/hooks/useZoomPanHelper.ts @@ -1,9 +1,9 @@ import { useMemo } from 'react'; import { zoomIdentity } from 'd3-zoom'; -import { useStoreState, useStore } from '../store/hooks'; +import { useStoreApi, useStore } from '../store'; import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '../utils/graph'; -import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, XYPosition } from '../types'; +import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, XYPosition, ReactFlowState } from '../types'; const DEFAULT_PADDING = 0.1; @@ -19,10 +19,14 @@ const initialZoomPanHelper: ZoomPanHelperFunctions = { initialized: false, }; +const selector = (s: ReactFlowState) => ({ + d3Zoom: s.d3Zoom, + d3Selection: s.d3Selection, +}); + const useZoomPanHelper = (): ZoomPanHelperFunctions => { - const store = useStore(); - const d3Zoom = useStoreState((s) => s.d3Zoom); - const d3Selection = useStoreState((s) => s.d3Selection); + const store = useStoreApi(); + const { d3Zoom, d3Selection } = useStore(selector); const zoomPanHelperFunctions = useMemo(() => { if (d3Selection && d3Zoom) { diff --git a/src/index.ts b/src/index.ts index b46051bf..38713781 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,7 +28,7 @@ export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'; export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'; export * from './additional-components'; -export * from './store/hooks'; +export { useStore, useStoreApi } from './store'; export * from './types'; export { ReactFlowProps } from './container/ReactFlow'; diff --git a/src/store/actions.ts b/src/store/actions.ts deleted file mode 100644 index 17ab0cee..00000000 --- a/src/store/actions.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { createAction } from './utils'; - -import { - Node, - Edge, - Elements, - OnConnectEndFunc, - OnConnectFunc, - OnConnectStartFunc, - OnConnectStopFunc, - NodeDimensionUpdate, - NodePosUpdate, - NodeDiffUpdate, - XYPosition, - Transform, - Dimensions, - InitD3ZoomPayload, - TranslateExtent, - SetConnectionId, - SnapGrid, - ConnectionMode, - NodeExtent, - OnElementsChange, -} from '../types'; - -import * as constants from './contants'; - -export const setOnConnect = (onConnect: OnConnectFunc) => - createAction(constants.SET_ON_CONNECT, { - onConnect, - }); - -export const setOnConnectStart = (onConnectStart: OnConnectStartFunc) => - createAction(constants.SET_ON_CONNECT_START, { - onConnectStart, - }); - -export const setOnConnectStop = (onConnectStop: OnConnectStopFunc) => - createAction(constants.SET_ON_CONNECT_STOP, { - onConnectStop, - }); - -export const setOnConnectEnd = (onConnectEnd: OnConnectEndFunc) => - createAction(constants.SET_ON_CONNECT_END, { - onConnectEnd, - }); - -export const setNodes = (nodes: Node[]) => createAction(constants.SET_NODES, nodes); -export const setEdges = (edges: Edge[]) => createAction(constants.SET_EDGES, edges); - -export const updateNodeDimensions = (updates: NodeDimensionUpdate[]) => - createAction(constants.UPDATE_NODE_DIMENSIONS, updates); - -export const updateNodePos = (payload: NodePosUpdate) => createAction(constants.UPDATE_NODE_POS, payload); - -export const updateNodePosDiff = (payload: NodeDiffUpdate) => createAction(constants.UPDATE_NODE_POS_DIFF, payload); - -export const setUserSelection = (mousePos: XYPosition) => createAction(constants.SET_USER_SELECTION, mousePos); - -export const updateUserSelection = (mousePos: XYPosition) => createAction(constants.UPDATE_USER_SELECTION, mousePos); - -export const unsetUserSelection = () => createAction(constants.UNSET_USER_SELECTION); - -export const setSelection = (selectionActive: boolean) => - createAction(constants.SET_SELECTION, { - selectionActive, - }); - -export const unsetNodesSelection = () => - createAction(constants.UNSET_NODES_SELECTION, { - nodesSelectionActive: false, - }); - -export const resetSelectedElements = () => - createAction(constants.RESET_SELECTED_ELEMENTS, { - selectedElements: null, - }); - -export const setSelectedElements = (elements: Elements) => createAction(constants.SET_SELECTED_ELEMENTS, elements); - -export const addSelectedElements = (elements: Elements) => createAction(constants.ADD_SELECTED_ELEMENTS, elements); - -export const updateTransform = (transform: Transform) => createAction(constants.UPDATE_TRANSFORM, { transform }); - -export const updateSize = (size: Dimensions) => - createAction(constants.UPDATE_SIZE, { - width: size.width || 500, - height: size.height || 500, - }); - -export const initD3Zoom = (payload: InitD3ZoomPayload) => createAction(constants.INIT_D3ZOOM, payload); - -export const setMinZoom = (minZoom: number) => createAction(constants.SET_MINZOOM, minZoom); - -export const setMaxZoom = (maxZoom: number) => createAction(constants.SET_MAXZOOM, maxZoom); - -export const setTranslateExtent = (translateExtent: TranslateExtent) => - createAction(constants.SET_TRANSLATEEXTENT, translateExtent); - -export const setConnectionPosition = (connectionPosition: XYPosition) => - createAction(constants.SET_CONNECTION_POSITION, { connectionPosition }); - -export const setConnectionNodeId = (payload: SetConnectionId) => createAction(constants.SET_CONNECTION_NODEID, payload); - -export const setSnapToGrid = (snapToGrid: boolean) => createAction(constants.SET_SNAPTOGRID, { snapToGrid }); - -export const setSnapGrid = (snapGrid: SnapGrid) => createAction(constants.SET_SNAPGRID, { snapGrid }); - -export const setInteractive = (isInteractive: boolean) => - createAction(constants.SET_INTERACTIVE, { - nodesDraggable: isInteractive, - nodesConnectable: isInteractive, - elementsSelectable: isInteractive, - }); - -export const setNodesDraggable = (nodesDraggable: boolean) => - createAction(constants.SET_NODES_DRAGGABLE, { nodesDraggable }); - -export const setNodesConnectable = (nodesConnectable: boolean) => - createAction(constants.SET_NODES_CONNECTABLE, { nodesConnectable }); - -export const setElementsSelectable = (elementsSelectable: boolean) => - createAction(constants.SET_ELEMENTS_SELECTABLE, { elementsSelectable }); - -export const setMultiSelectionActive = (multiSelectionActive: boolean) => - createAction(constants.SET_MULTI_SELECTION_ACTIVE, { multiSelectionActive }); - -export const setConnectionMode = (connectionMode: ConnectionMode) => - createAction(constants.SET_CONNECTION_MODE, { connectionMode }); - -export const setNodeExtent = (nodeExtent: NodeExtent) => createAction(constants.SET_NODE_EXTENT, nodeExtent); - -export const setOnNodesChange = (onNodesChange: OnElementsChange) => - createAction(constants.SET_ON_NODES_CHANGE, { onNodesChange }); - -export const setOnEdgesChange = (onEdgesChange: OnElementsChange) => - createAction(constants.SET_ON_EDGES_CHANGE, { onEdgesChange }); - -export type ReactFlowAction = ReturnType< - | typeof setOnConnect - | typeof setOnConnectStart - | typeof setOnConnectStop - | typeof setOnConnectEnd - | typeof setNodes - | typeof setEdges - | typeof updateNodeDimensions - | typeof updateNodePos - | typeof updateNodePosDiff - | typeof setUserSelection - | typeof updateUserSelection - | typeof unsetUserSelection - | typeof setSelection - | typeof unsetNodesSelection - | typeof resetSelectedElements - | typeof setSelectedElements - | typeof addSelectedElements - | typeof updateTransform - | typeof updateSize - | typeof initD3Zoom - | typeof setMinZoom - | typeof setMaxZoom - | typeof setTranslateExtent - | typeof setConnectionPosition - | typeof setConnectionNodeId - | typeof setSnapToGrid - | typeof setSnapGrid - | typeof setInteractive - | typeof setNodesDraggable - | typeof setNodesConnectable - | typeof setElementsSelectable - | typeof setMultiSelectionActive - | typeof setConnectionMode - | typeof setNodeExtent - | typeof setOnNodesChange - | typeof setOnEdgesChange ->; diff --git a/src/store/configure-store.ts b/src/store/configure-store.ts deleted file mode 100644 index 6005bdf1..00000000 --- a/src/store/configure-store.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createStore, applyMiddleware, Store } from 'redux'; -import thunk from 'redux-thunk'; - -import { ReactFlowState } from '../types'; -import { ReactFlowAction } from './actions'; -import reactFlowReducer from './reducer'; - -export default function configureStore(preloadedState: ReactFlowState): Store { - const store = createStore(reactFlowReducer, preloadedState, applyMiddleware(thunk)); - return store; -} diff --git a/src/store/contants.ts b/src/store/contants.ts deleted file mode 100644 index 890ada35..00000000 --- a/src/store/contants.ts +++ /dev/null @@ -1,36 +0,0 @@ -export const SET_ON_CONNECT = 'SET_ON_CONNECT'; -export const SET_ON_CONNECT_START = 'SET_ON_CONNECT_START'; -export const SET_ON_CONNECT_STOP = 'SET_ON_CONNECT_STOP'; -export const SET_ON_CONNECT_END = 'SET_ON_CONNECT_END'; -export const SET_NODES = 'SET_NODES'; -export const SET_EDGES = 'SET_EDGES'; -export const UPDATE_NODE_DIMENSIONS = 'UPDATE_NODE_DIMENSIONS'; -export const UPDATE_NODE_POS = 'UPDATE_NODE_POS'; -export const UPDATE_NODE_POS_DIFF = 'UPDATE_NODE_POS_DIFF'; -export const SET_USER_SELECTION = 'SET_USER_SELECTION'; -export const UPDATE_USER_SELECTION = 'UPDATE_USER_SELECTION'; -export const UNSET_USER_SELECTION = 'UNSET_USER_SELECTION'; -export const SET_SELECTION = 'SET_SELECTION'; -export const UNSET_NODES_SELECTION = 'UNSET_NODES_SELECTION'; -export const SET_SELECTED_ELEMENTS = 'SET_SELECTED_ELEMENTS'; -export const RESET_SELECTED_ELEMENTS = 'RESET_SELECTED_ELEMENTS'; -export const ADD_SELECTED_ELEMENTS = 'ADD_SELECTED_ELEMENTS'; -export const UPDATE_TRANSFORM = 'UPDATE_TRANSFORM'; -export const UPDATE_SIZE = 'UPDATE_SIZE'; -export const INIT_D3ZOOM = 'INIT_D3ZOOM'; -export const SET_MINZOOM = 'SET_MINZOOM'; -export const SET_MAXZOOM = 'SET_MAXZOOM'; -export const SET_TRANSLATEEXTENT = 'SET_TRANSLATEEXTENT'; -export const SET_CONNECTION_POSITION = 'SET_CONNECTION_POSITION'; -export const SET_CONNECTION_NODEID = 'SET_CONNECTION_NODEID'; -export const SET_SNAPTOGRID = 'SET_SNAPTOGRID'; -export const SET_SNAPGRID = 'SET_SNAPGRID'; -export const SET_INTERACTIVE = 'SET_INTERACTIVE'; -export const SET_NODES_DRAGGABLE = 'SET_NODES_DRAGGABLE'; -export const SET_NODES_CONNECTABLE = 'SET_NODES_CONNECTABLE'; -export const SET_ELEMENTS_SELECTABLE = 'SET_ELEMENTS_SELECTABLE'; -export const SET_MULTI_SELECTION_ACTIVE = 'SET_MULTI_SELECTION_ACTIVE'; -export const SET_CONNECTION_MODE = 'SET_CONNECTION_MODE'; -export const SET_NODE_EXTENT = 'SET_NODE_EXTENT'; -export const SET_ON_NODES_CHANGE = 'SET_ON_NODES_CHANGE'; -export const SET_ON_EDGES_CHANGE = 'SET_ON_EDGES_CHANGE'; diff --git a/src/store/hooks.ts b/src/store/hooks.ts deleted file mode 100644 index 4ee7affc..00000000 --- a/src/store/hooks.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { bindActionCreators, Store, ActionCreator, ActionCreatorsMapObject } from 'redux'; -import { - useStore as useStoreRedux, - useSelector, - useDispatch as reduxUseDispatch, - TypedUseSelectorHook, -} from 'react-redux'; -import { useMemo } from 'react'; - -import { ReactFlowDispatch } from './index'; -import * as actions from './actions'; -import { ReactFlowAction } from './actions'; -import { ReactFlowState } from '../types'; - -export const useTypedSelector: TypedUseSelectorHook = useSelector; - -export type ActionCreatorSelector = (acts: typeof actions) => ActionCreator; -export type ActionMapObjectSelector = (acts: typeof actions) => ActionCreatorsMapObject; -export type ActionSelector = (acts: typeof actions) => ActionCreatorsMapObject | ActionCreator; - -export function useStoreActions( - actionSelector: ActionCreatorSelector -): ActionCreator; - -export function useStoreActions( - actionSelector: ActionMapObjectSelector -): ActionCreatorsMapObject; - -export function useStoreActions(actionSelector: ActionSelector) { - const dispatch: ReactFlowDispatch = reduxUseDispatch(); - const currAction = actionSelector(actions); - - const action = useMemo(() => { - // this looks weird but required if both ActionSelector and ActionMapObjectSelector are supported - return typeof currAction === 'function' - ? bindActionCreators(currAction, dispatch) - : bindActionCreators(currAction, dispatch); - }, [dispatch, currAction]); - - return action; -} - -export const useStoreState = useTypedSelector; -export const useStore = (): Store => { - const store = useStoreRedux(); - return store; -}; -export const useDispatch: ReactFlowDispatch = reduxUseDispatch; diff --git a/src/store/index.ts b/src/store/index.ts index fa2eda77..9bcfcbad 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,66 +1,410 @@ -import configureStore from './configure-store'; +import create from 'zustand'; +import createContext from 'zustand/context'; +import isEqual from 'fast-deep-equal'; -import { ReactFlowState, ConnectionMode } from '../types'; +import { clampPosition, getDimensions } from '../utils'; +import { + ReactFlowState, + ConnectionMode, + Node, + Edge, + ElementChange, + NodeDimensionUpdate, + NodeDiffUpdate, + XYPosition, + Elements, + InitD3ZoomPayload, + TranslateExtent, + NodeExtent, + Transform, + Dimensions, + OnConnectFunc, + OnConnectStartFunc, + OnConnectStopFunc, + OnConnectEndFunc, + SetConnectionId, + SnapGrid, + OnElementsChange, +} from '../types'; +import { parseNode, parseEdge, isNode, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph'; +import { getSourceTargetNodes } from '../container/EdgeRenderer/utils'; +import { getHandleBounds } from '../components/Nodes/utils'; -export const initialState: ReactFlowState = { - width: 0, - height: 0, - transform: [0, 0, 1], - nodes: [], - edges: [], - onNodesChange: null, - onEdgesChange: null, +const { Provider, useStore, useStoreApi } = createContext(); - selectedElements: null, - selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, - - d3Zoom: null, - d3Selection: null, - d3ZoomHandler: undefined, - minZoom: 0.5, - maxZoom: 2, - translateExtent: [ - [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], - [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], - ], - - nodeExtent: [ - [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], - [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], - ], - - nodesSelectionActive: false, - selectionActive: false, - - userSelectionRect: { - startX: 0, - startY: 0, - x: 0, - y: 0, +const createStore = () => + create((set, get) => ({ width: 0, height: 0, - draw: false, - }, - connectionNodeId: null, - connectionHandleId: null, - connectionHandleType: 'source', - connectionPosition: { x: 0, y: 0 }, - connectionMode: ConnectionMode.Strict, + transform: [0, 0, 1], + nodes: [], + edges: [], + onNodesChange: null, + onEdgesChange: null, - snapGrid: [15, 15], - snapToGrid: false, + selectedElements: null, + selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, - nodesDraggable: true, - nodesConnectable: true, - elementsSelectable: true, + d3Zoom: null, + d3Selection: null, + d3ZoomHandler: undefined, + minZoom: 0.5, + maxZoom: 2, + translateExtent: [ + [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], + [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], + ], - multiSelectionActive: false, + nodeExtent: [ + [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], + [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], + ], - reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-', -}; + nodesSelectionActive: false, + selectionActive: false, -const store = configureStore(initialState); + 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, -export type ReactFlowDispatch = typeof store.dispatch; + snapGrid: [15, 15], + snapToGrid: false, -export default store; + nodesDraggable: true, + nodesConnectable: true, + elementsSelectable: true, + + multiSelectionActive: false, + + reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-', + + setNodes: (propNodes: Node[]) => { + const { nodes, edges, nodeExtent } = get(); + const nextNodes = propNodes.map((propNode: Node) => { + const storeNode = nodes.find((node) => node.id === propNode.id); + + if (storeNode) { + if (typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type) { + const updatedNode: Node = { + ...storeNode, + ...propNode, + }; + // we reset the elements dimensions here in order to force a re-calculation of the bounds. + // When the type of a node changes it is possible that the number or positions of handles changes too. + updatedNode.width = null; + return updatedNode; + } + } + + 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; + }); + + set({ + nodes: nextNodes, + edges: updatedEdges, + }); + }, + setEdges: (propEdges: Edge[]) => { + const { edges, nodes } = get(); + + const nextEdges = propEdges.map((propEdge: Edge) => { + const storeEdge = edges.find((se) => se.id === propEdge.id); + + 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; + } + }); + + set({ edges: nextEdges }); + }, + updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { + const { onNodesChange, nodes, transform } = get(); + + const initialChanges: ElementChange[] = []; + const nodesToChange: ElementChange[] = nodes.reduce((res, node) => { + const update = updates.find((u) => u.id === node.id); + if (update) { + const dimensions = getDimensions(update.nodeElement); + const doUpdate = + dimensions.width && + dimensions.height && + (node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate); + + if (doUpdate) { + const handleBounds = getHandleBounds(update.nodeElement, transform[2]); + const change = { + id: node.id, + change: { + ...dimensions, + handleBounds, + }, + } as ElementChange; + + res.push(change); + } + } + + return res; + }, initialChanges); + + if (onNodesChange) { + onNodesChange(nodesToChange); + } + }, + updateNodePosDiff: ({ id, diff, isDragging }: NodeDiffUpdate) => { + const { onNodesChange, nodes } = get(); + + if (onNodesChange && id && diff) { + const matchingNode = nodes.find((n) => n.id === id); + + if (matchingNode) { + requestAnimationFrame(() => + onNodesChange([ + { + id, + change: { + position: { + x: matchingNode.position.x + diff.x, + y: matchingNode.position.y + diff.y, + isDragging, + }, + }, + }, + ]) + ); + } + } + }, + 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, nodes, edges, transform, selectedElements } = 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), + }; + + 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, + }); + } + }, + unsetUserSelection: () => { + const { selectedElements, userSelectionRect } = get(); + const selectedNodes = selectedElements?.filter((node) => isNode(node) && node.position) as Node[]; + + const stateUpdate = { + selectionActive: false, + userSelectionRect: { + ...userSelectionRect, + draw: false, + }, + selectedElements: null, + 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); + }, + 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 selectedElementsArr = Array.isArray(elements) ? elements : [elements]; + + let nextElements = selectedElementsArr; + + if (multiSelectionActive) { + nextElements = selectedElements ? [...selectedElements, ...selectedElementsArr] : selectedElementsArr; + } + + const selectedElementsUpdated = !isEqual(nextElements, selectedElements); + + set({ selectedElements: selectedElementsUpdated ? nextElements : selectedElements }); + }, + initD3Zoom: ({ d3Zoom, d3Selection, d3ZoomHandler, transform }: InitD3ZoomPayload) => { + set({ + d3Zoom, + d3Selection, + d3ZoomHandler, + transform, + }); + }, + setMinZoom: (minZoom: number) => { + const { d3Zoom, maxZoom } = get(); + d3Zoom?.scaleExtent([minZoom, maxZoom]); + + set({ minZoom }); + }, + setMaxZoom: (maxZoom: number) => { + const { d3Zoom, minZoom } = get(); + d3Zoom?.scaleExtent([minZoom, maxZoom]); + + set({ maxZoom }); + }, + setTranslateExtent: (translateExtent: TranslateExtent) => { + const { d3Zoom } = get(); + d3Zoom?.translateExtent(translateExtent); + + set({ translateExtent }); + }, + setNodeExtent: (nodeExtent: NodeExtent) => { + set({ + nodeExtent, + nodes: get().nodes.map((node) => { + return { + ...node, + position: clampPosition(node.position, nodeExtent), + __rf: { + ...node.__rf, + }, + }; + }), + }); + }, + unsetNodesSelection: () => { + set({ nodesSelectionActive: false }); + }, + resetSelectedElements: () => { + set({ selectedElements: null }); + }, + 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 }); + }, + })); + +export { Provider, useStore, createStore, useStoreApi }; diff --git a/src/store/reducer.ts b/src/store/reducer.ts deleted file mode 100644 index f5b7b18f..00000000 --- a/src/store/reducer.ts +++ /dev/null @@ -1,350 +0,0 @@ -import isEqual from 'fast-deep-equal'; - -import { clampPosition, getDimensions } from '../utils'; -import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, parseNode, parseEdge } from '../utils/graph'; -import { getHandleBounds } from '../components/Nodes/utils'; -import { getSourceTargetNodes } from '../container/EdgeRenderer/utils'; - -import { ReactFlowState, Node, XYPosition, Edge, ElementChange } from '../types'; -import * as constants from './contants'; -import { ReactFlowAction } from './actions'; - -import { initialState } from './index'; - -export default function reactFlowReducer(state = initialState, action: ReactFlowAction): ReactFlowState { - switch (action.type) { - case constants.SET_NODES: { - const propNodes = action.payload; - const nextNodes = propNodes.map((propNode: Node) => { - const storeNode = state.nodes.find((node) => node.id === propNode.id); - - if (storeNode) { - if (typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type) { - const updatedNode: Node = { - ...storeNode, - ...propNode, - }; - // we reset the elements dimensions here in order to force a re-calculation of the bounds. - // When the type of a node changes it is possible that the number or positions of handles changes too. - updatedNode.width = null; - return updatedNode; - } - } - - return parseNode(propNode, state.nodeExtent); - }); - - const updatedEdges = state.edges.map((edge) => { - const { sourceNode, targetNode } = getSourceTargetNodes(edge, nextNodes); - - if (sourceNode) { - edge.sourceNode = sourceNode; - } - if (targetNode) { - edge.targetNode = targetNode; - } - - return edge; - }); - - return { ...state, nodes: nextNodes, edges: updatedEdges }; - } - case constants.SET_EDGES: { - const propElements = action.payload; - const nextEdges = propElements.map((propEdge: Edge) => { - const storeEdge = state.edges.find((se) => se.id === propEdge.id); - - if (storeEdge) { - return parseEdge(propEdge); - } else { - const parsedEdge = parseEdge(propEdge); - const { sourceNode, targetNode } = getSourceTargetNodes(parsedEdge, state.nodes); - - if (sourceNode) { - parsedEdge.sourceNode = sourceNode; - } - if (targetNode) { - parsedEdge.targetNode = targetNode; - } - - return parsedEdge; - } - }); - - return { ...state, edges: nextEdges }; - } - case constants.UPDATE_NODE_DIMENSIONS: { - const initialChanges: ElementChange[] = []; - const nodesToChange: ElementChange[] = state.nodes.reduce((res, node) => { - const update = action.payload.find((u) => u.id === node.id); - if (update) { - const dimensions = getDimensions(update.nodeElement); - const doUpdate = - dimensions.width && - dimensions.height && - (node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate); - - if (doUpdate) { - const handleBounds = getHandleBounds(update.nodeElement, state.transform[2]); - const change = { - id: node.id, - change: { - ...dimensions, - handleBounds, - }, - } as ElementChange; - - res.push(change); - } - } - - return res; - }, initialChanges); - - if (state.onNodesChange) { - requestAnimationFrame(() => state.onNodesChange?.(nodesToChange)); - } - - return state; - } - case constants.UPDATE_NODE_POS: { - const { id, pos } = action.payload; - let position: XYPosition = pos; - - if (state.snapToGrid) { - const [gridSizeX, gridSizeY] = state.snapGrid; - position = { - x: gridSizeX * Math.round(pos.x / gridSizeX), - y: gridSizeY * Math.round(pos.y / gridSizeY), - }; - } - - if (state.onNodesChange) { - state.onNodesChange([{ id, change: { position } }]); - - return state; - } - - const nextNodes = state.nodes.map((node) => { - if (node.id === id) { - return { - ...node, - position, - - __rf: { - ...node.__rf, - }, - }; - } - - return node; - }); - - return { ...state, nodes: nextNodes }; - } - case constants.UPDATE_NODE_POS_DIFF: { - const { id, diff, isDragging } = action.payload; - - if (state.onNodesChange && id && diff) { - const matchingNode = state.nodes.find((n) => n.id === id); - - if (matchingNode) { - requestAnimationFrame(() => - state.onNodesChange?.([ - { - id, - change: { - position: { - x: matchingNode.position.x + diff.x, - y: matchingNode.position.y + diff.y, - isDragging, - }, - }, - }, - ]) - ); - } - } - - return state; - } - case constants.SET_USER_SELECTION: { - const mousePos = action.payload; - - return { - ...state, - selectionActive: true, - userSelectionRect: { - width: 0, - height: 0, - startX: mousePos.x, - startY: mousePos.y, - x: mousePos.x, - y: mousePos.y, - draw: true, - }, - }; - } - case constants.UPDATE_USER_SELECTION: { - const mousePos = action.payload; - const startX = state.userSelectionRect.startX ?? 0; - const startY = state.userSelectionRect.startY ?? 0; - - const nextUserSelectRect = { - ...state.userSelectionRect, - x: mousePos.x < startX ? mousePos.x : state.userSelectionRect.x, - y: mousePos.y < startY ? mousePos.y : state.userSelectionRect.y, - width: Math.abs(mousePos.x - startX), - height: Math.abs(mousePos.y - startY), - }; - - const selectedNodes = getNodesInside(state.nodes, nextUserSelectRect, state.transform, false, true); - const selectedEdges = getConnectedEdges(selectedNodes, state.edges); - - const nextSelectedElements = [...selectedNodes, ...selectedEdges]; - const selectedElementsChanged = !isEqual(nextSelectedElements, state.selectedElements); - const selectedElementsUpdate = selectedElementsChanged - ? { - selectedElements: nextSelectedElements.length > 0 ? nextSelectedElements : null, - } - : {}; - - return { - ...state, - ...selectedElementsUpdate, - userSelectionRect: nextUserSelectRect, - }; - } - case constants.UNSET_USER_SELECTION: { - const selectedNodes = state.selectedElements?.filter((node) => isNode(node) && node.__rf) as Node[]; - - const stateUpdate = { - ...state, - selectionActive: false, - userSelectionRect: { - ...state.userSelectionRect, - draw: false, - }, - }; - - if (!selectedNodes || selectedNodes.length === 0) { - stateUpdate.selectedElements = null; - stateUpdate.nodesSelectionActive = false; - } else { - const selectedNodesBbox = getRectOfNodes(selectedNodes); - stateUpdate.selectedNodesBbox = selectedNodesBbox; - stateUpdate.nodesSelectionActive = true; - } - - return stateUpdate; - } - case constants.SET_SELECTED_ELEMENTS: { - const elements = action.payload; - const selectedElementsArr = Array.isArray(elements) ? elements : [elements]; - const selectedElementsUpdated = !isEqual(selectedElementsArr, state.selectedElements); - const selectedElements = selectedElementsUpdated ? selectedElementsArr : state.selectedElements; - - return { - ...state, - selectedElements, - }; - } - case constants.ADD_SELECTED_ELEMENTS: { - const { multiSelectionActive, selectedElements } = state; - const elements = action.payload; - const selectedElementsArr = Array.isArray(elements) ? elements : [elements]; - - let nextElements = selectedElementsArr; - - if (multiSelectionActive) { - nextElements = selectedElements ? [...selectedElements, ...selectedElementsArr] : selectedElementsArr; - } - - const selectedElementsUpdated = !isEqual(nextElements, state.selectedElements); - const nextSelectedElements = selectedElementsUpdated ? nextElements : state.selectedElements; - - return { ...state, selectedElements: nextSelectedElements }; - } - case constants.INIT_D3ZOOM: { - const { d3Zoom, d3Selection, d3ZoomHandler, transform } = action.payload; - - return { - ...state, - d3Zoom, - d3Selection, - d3ZoomHandler, - transform, - }; - } - case constants.SET_MINZOOM: { - const minZoom = action.payload; - - state.d3Zoom?.scaleExtent([minZoom, state.maxZoom]); - - return { - ...state, - minZoom, - }; - } - - case constants.SET_MAXZOOM: { - const maxZoom = action.payload; - - state.d3Zoom?.scaleExtent([state.minZoom, maxZoom]); - - return { - ...state, - maxZoom, - }; - } - case constants.SET_TRANSLATEEXTENT: { - const translateExtent = action.payload; - - state.d3Zoom?.translateExtent(translateExtent); - - return { - ...state, - translateExtent, - }; - } - case constants.SET_NODE_EXTENT: { - const nodeExtent = action.payload; - return { - ...state, - nodeExtent, - nodes: state.nodes.map((node) => { - return { - ...node, - position: clampPosition(node.position, nodeExtent), - __rf: { - ...node.__rf, - }, - }; - }), - }; - } - case constants.SET_ON_CONNECT: - case constants.SET_ON_CONNECT_START: - case constants.SET_ON_CONNECT_STOP: - case constants.SET_ON_CONNECT_END: - case constants.RESET_SELECTED_ELEMENTS: - case constants.UNSET_NODES_SELECTION: - case constants.UPDATE_TRANSFORM: - case constants.UPDATE_SIZE: - case constants.SET_CONNECTION_POSITION: - case constants.SET_CONNECTION_NODEID: - case constants.SET_SNAPTOGRID: - case constants.SET_SNAPGRID: - case constants.SET_INTERACTIVE: - case constants.SET_NODES_DRAGGABLE: - case constants.SET_NODES_CONNECTABLE: - case constants.SET_ELEMENTS_SELECTABLE: - case constants.SET_MULTI_SELECTION_ACTIVE: - case constants.SET_CONNECTION_MODE: - case constants.SET_ON_NODES_CHANGE: - case constants.SET_ON_EDGES_CHANGE: - return { ...state, ...action.payload }; - default: - return state; - } -} diff --git a/src/store/utils.ts b/src/store/utils.ts deleted file mode 100644 index c4bfbaf3..00000000 --- a/src/store/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function createAction(type: T): { type: T }; -export function createAction(type: T, payload: P): { type: T; payload: P }; -export function createAction(type: string, payload?: any) { - return { type, payload }; -} diff --git a/src/types/index.ts b/src/types/index.ts index 6de72b6d..9974c501 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -453,6 +453,41 @@ export interface ReactFlowState { reactFlowVersion: string; + setNodes: (nodes: Node[]) => void; + setEdges: (edges: Edge[]) => void; + updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; + updateNodePosDiff: (update: NodeDiffUpdate) => void; + setUserSelection: (mousePos: XYPosition) => void; + updateUserSelection: (mousePos: XYPosition) => void; + unsetUserSelection: () => void; + unsetNodesSelection: () => void; + resetSelectedElements: () => void; + setSelectedElements: (elements: Elements) => void; + addSelectedElements: (elements: Elements) => void; + updateTransform: (transform: Transform) => void; + updateSize: (size: Dimensions) => void; + initD3Zoom: (payload: InitD3ZoomPayload) => void; + setMinZoom: (minZoom: number) => void; + setMaxZoom: (maxZoom: number) => void; + setTranslateExtent: (translateExtent: TranslateExtent) => void; + setNodeExtent: (nodeExtent: NodeExtent) => void; + setOnConnect: (onConnectFunction: OnConnectFunc) => void; + setOnConnectStart: (onConnectFunction: OnConnectStartFunc) => void; + setOnConnectStop: (onConnectFunction: OnConnectStopFunc) => void; + setOnConnectEnd: (onConnectFunction: OnConnectEndFunc) => void; + setConnectionPosition: (connectionPosition: XYPosition) => void; + setConnectionNodeId: (payload: SetConnectionId) => void; + setSnapToGrid: (snapToGrid: boolean) => void; + setSnapGrid: (snapGrid: SnapGrid) => void; + setInteractive: (isInteractive: boolean) => void; + setNodesDraggable: (nodesDraggable: boolean) => void; + setNodesConnectable: (nodesConnectable: boolean) => void; + setElementsSelectable: (elementsSelectable: boolean) => void; + setMultiSelectionActive: (multiSelectionActive: boolean) => void; + setConnectionMode: (connectionMode: ConnectionMode) => void; + setOnNodesChange: (onNodesChange: OnElementsChange) => void; + setOnEdgesChange: (onEdgesChange: OnElementsChange) => void; + onConnect?: OnConnectFunc; onConnectStart?: OnConnectStartFunc; onConnectStop?: OnConnectStopFunc; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 86d13a6e..8e84b139 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -1,6 +1,7 @@ -import { Store } from 'redux'; +import { GetState } from 'zustand'; import { clampPosition, clamp } from '../utils'; +import { ReactFlowState } from '../types'; import { ElementId, @@ -13,7 +14,6 @@ import { Box, Connection, FlowExportObject, - ReactFlowState, NodeExtent, ElementChange, } from '../types'; @@ -139,9 +139,9 @@ export const pointToRendererPoint = ( return position; }; -export const onLoadProject = (currentStore: Store) => { +export const onLoadProject = (getState: GetState) => { return (position: XYPosition): XYPosition => { - const { transform, snapToGrid, snapGrid } = currentStore.getState(); + const { transform, snapToGrid, snapGrid } = getState(); return pointToRendererPoint(position, transform, snapToGrid, snapGrid); }; @@ -256,17 +256,17 @@ const parseElements = (nodes: Node[], edges: Edge[]): Elements => { return [...nodes.map((n) => ({ ...n })), ...edges.map((e) => ({ ...e }))]; }; -export const onLoadGetElements = (currentStore: Store) => { +export const onLoadGetElements = (getState: GetState) => { return (): Elements => { - const { nodes = [], edges = [] } = currentStore.getState(); + const { nodes = [], edges = [] } = getState(); return parseElements(nodes, edges); }; }; -export const onLoadToObject = (currentStore: Store) => { +export const onLoadToObject = (getState: GetState) => { return (): FlowExportObject => { - const { nodes = [], edges = [], transform } = currentStore.getState(); + const { nodes = [], edges = [], transform } = getState(); return { elements: parseElements(nodes, edges), From 3f8c25209672e06a6e2dc9c57e953b34c43e388c Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 13 Oct 2021 13:52:45 +0200 Subject: [PATCH 005/263] refactor(graphview): use one hook --- example/src/Basic/index.tsx | 2 +- example/src/Stress/utils.ts | 2 +- src/container/GraphView/index.tsx | 56 +++++++++++++++++++++---------- src/index.ts | 2 ++ src/wdyr.ts | 10 ++++++ 5 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 src/wdyr.ts diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index 4872d56b..fad5f17d 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -38,7 +38,7 @@ const BasicFlow = () => { const onConnect = useCallback((params: Edge | Connection, nds: Node[]) => { setEdges((eds) => addEdge(params, nds, eds)); }, []); - const onLoad = (reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance); + const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []); const updatePos = () => { setNodes((nds) => { diff --git a/example/src/Stress/utils.ts b/example/src/Stress/utils.ts index 0d2abc90..36cf2949 100644 --- a/example/src/Stress/utils.ts +++ b/example/src/Stress/utils.ts @@ -7,7 +7,7 @@ type ElementsCollection = { export function getElements(xElements: number = 10, yElements: number = 10): ElementsCollection { const initialNodes = []; - const initialEdges = []; + const initialEdges: Edge[] = []; let nodeId = 1; let recentNodeId = null; diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index bfcbf045..358a9dd3 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -9,7 +9,7 @@ import useZoomPanHelper from '../../hooks/useZoomPanHelper'; import { ReactFlowProps } from '../ReactFlow'; -import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types'; +import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode, ReactFlowState } from '../../types'; export interface GraphViewProps extends Omit { nodeTypes: NodeTypesType; @@ -27,6 +27,25 @@ export interface GraphViewProps extends Omit ({ + setOnConnect: s.setOnConnect, + setOnConnectStart: s.setOnConnectStart, + setOnConnectStop: s.setOnConnectStop, + setOnConnectEnd: s.setOnConnectEnd, + setSnapGrid: s.setSnapGrid, + setSnapToGrid: s.setSnapToGrid, + setNodesDraggable: s.setNodesDraggable, + setNodesConnectable: s.setNodesConnectable, + setElementsSelectable: s.setElementsSelectable, + setMinZoom: s.setMinZoom, + setMaxZoom: s.setMaxZoom, + setTranslateExtent: s.setTranslateExtent, + setNodeExtent: s.setNodeExtent, + setConnectionMode: s.setConnectionMode, + setOnNodesChange: s.setOnNodesChange, + setOnEdgesChange: s.setOnEdgesChange, +}); + const GraphView = ({ nodeTypes, edgeTypes, @@ -99,22 +118,25 @@ const GraphView = ({ }: GraphViewProps) => { const isInitialized = useRef(false); const store = useStoreApi(); - const setOnConnect = useStore((s) => s.setOnConnect); - const setOnConnectStart = useStore((s) => s.setOnConnectStart); - const setOnConnectStop = useStore((s) => s.setOnConnectStop); - const setOnConnectEnd = useStore((s) => s.setOnConnectEnd); - const setSnapGrid = useStore((s) => s.setSnapGrid); - const setSnapToGrid = useStore((s) => s.setSnapToGrid); - const setNodesDraggable = useStore((s) => s.setNodesDraggable); - const setNodesConnectable = useStore((s) => s.setNodesConnectable); - const setElementsSelectable = useStore((s) => s.setElementsSelectable); - const setMinZoom = useStore((s) => s.setMinZoom); - const setMaxZoom = useStore((s) => s.setMaxZoom); - const setTranslateExtent = useStore((s) => s.setTranslateExtent); - const setNodeExtent = useStore((s) => s.setNodeExtent); - const setConnectionMode = useStore((s) => s.setConnectionMode); - const setOnNodesChange = useStore((s) => s.setOnNodesChange); - const setOnEdgesChange = useStore((s) => s.setOnEdgesChange); + + const { + setOnConnect, + setOnConnectStart, + setOnConnectStop, + setOnConnectEnd, + setSnapGrid, + setSnapToGrid, + setNodesDraggable, + setNodesConnectable, + setElementsSelectable, + setMinZoom, + setMaxZoom, + setTranslateExtent, + setNodeExtent, + setConnectionMode, + setOnNodesChange, + setOnEdgesChange, + } = useStore(selector); const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper(); diff --git a/src/index.ts b/src/index.ts index 38713781..ce6e08f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ +// import './wdyr'; + import ReactFlow from './container/ReactFlow'; export default ReactFlow; diff --git a/src/wdyr.ts b/src/wdyr.ts new file mode 100644 index 00000000..79be3097 --- /dev/null +++ b/src/wdyr.ts @@ -0,0 +1,10 @@ +/// + +import React from 'react'; + +if (process.env.NODE_ENV === 'development') { + const whyDidYouRender = require('@welldone-software/why-did-you-render'); + whyDidYouRender(React, { + trackAllPureComponents: true, + }); +} From 1f41cc0d9a0ae3d27b84189aa0092afed9d879c8 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 13 Oct 2021 16:19:46 +0200 Subject: [PATCH 006/263] 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; From 3dc79276ad082820f00003a40740d3f6f294b2e5 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 13 Oct 2021 16:30:34 +0200 Subject: [PATCH 007/263] refactor(store): cleanup --- src/store/index.ts | 60 +++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/src/store/index.ts b/src/store/index.ts index a9a1d1c7..f0b61b65 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -39,6 +39,14 @@ import { getHandleBounds } from '../components/Nodes/utils'; const { Provider, useStore, useStoreApi } = createContext(); +const unselectElements = (elements: Elements) => + elements + .filter((e) => e.selected) + .map((e) => ({ + id: e.id, + change: { selected: false }, + })); + const createStore = () => create((set, get) => ({ width: 0, @@ -262,34 +270,33 @@ const createStore = () => const { multiSelectionActive, onNodesChange, onEdgesChange, nodes, edges } = get(); const selectedElementsArr = Array.isArray(elements) ? elements : [elements]; - let selectedNodes; - let selectedEdges; + let changedNodes; + let changedEdges; if (multiSelectionActive) { - selectedNodes = selectedElementsArr.filter(isNode).map((node) => ({ id: node.id, change: { selected: true } })); - selectedEdges = selectedElementsArr.filter(isEdge).map((edge) => ({ id: edge.id, change: { selected: true } })); + changedNodes = selectedElementsArr.filter(isNode).map((node) => ({ id: node.id, change: { selected: true } })); + changedEdges = selectedElementsArr.filter(isEdge).map((edge) => ({ id: edge.id, change: { selected: true } })); } else { - selectedNodes = nodes.map((node) => ({ + changedNodes = nodes.map((node) => ({ id: node.id, change: { selected: selectedElementsArr.some((e) => e.id === node.id) }, })); - selectedEdges = edges.map((edge) => ({ + changedEdges = edges.map((edge) => ({ id: edge.id, change: { selected: selectedElementsArr.some((e) => e.id === edge.id) }, })); } - onNodesChange?.(selectedNodes); - onEdgesChange?.(selectedEdges); + onNodesChange?.(changedNodes); + onEdgesChange?.(changedEdges); }, - initD3Zoom: ({ d3Zoom, d3Selection, d3ZoomHandler, transform }: InitD3ZoomPayload) => { + initD3Zoom: ({ d3Zoom, d3Selection, d3ZoomHandler, transform }: InitD3ZoomPayload) => set({ d3Zoom, d3Selection, d3ZoomHandler, transform, - }); - }, + }), setMinZoom: (minZoom: number) => { const { d3Zoom, maxZoom } = get(); d3Zoom?.scaleExtent([minZoom, maxZoom]); @@ -308,7 +315,14 @@ const createStore = () => set({ translateExtent }); }, - setNodeExtent: (nodeExtent: NodeExtent) => { + + resetSelectedElements: () => { + const { nodes, edges, onNodesChange, onEdgesChange } = get(); + + onNodesChange?.(unselectElements(nodes)); + onEdgesChange?.(unselectElements(edges)); + }, + setNodeExtent: (nodeExtent: NodeExtent) => set({ nodeExtent, nodes: get().nodes.map((node) => { @@ -317,27 +331,7 @@ const createStore = () => position: clampPosition(node.position, nodeExtent), }; }), - }); - }, - resetSelectedElements: () => { - 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); - }, + }), unsetNodesSelection: () => set({ nodesSelectionActive: false }), updateTransform: (transform: Transform) => set({ transform }), updateSize: (size: Dimensions) => set({ width: size.width || 500, height: size.height || 500 }), From d3b1148bcab61d7bf925fa9b82b5a1b73d43b264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 14 Oct 2021 17:53:50 +0200 Subject: [PATCH 008/263] feat(markers): implement more generic api for markerEnd and markerStart handling --- example/src/Edges/CustomEdge.tsx | 7 +- example/src/Edges/CustomEdge2.tsx | 7 +- example/src/Edges/index.tsx | 15 ++- example/src/FloatingEdges/FloatingEdge.tsx | 7 +- example/src/FloatingEdges/index.tsx | 4 +- example/src/Overview/index.tsx | 3 +- example/src/Undirectional/index.tsx | 13 -- src/components/Edges/BezierEdge.tsx | 16 ++- src/components/Edges/SmoothStepEdge.tsx | 20 +-- src/components/Edges/StraightEdge.tsx | 7 +- src/components/Edges/wrapEdge.tsx | 24 +++- .../EdgeRenderer/MarkerDefinitions.tsx | 126 ++++++++++++------ src/container/EdgeRenderer/index.tsx | 11 +- src/container/FlowRenderer/index.tsx | 1 + src/container/GraphView/index.tsx | 8 +- src/container/ReactFlow/index.tsx | 9 +- src/types/index.ts | 18 ++- src/utils/graph.ts | 16 +++ 18 files changed, 192 insertions(+), 120 deletions(-) diff --git a/example/src/Edges/CustomEdge.tsx b/example/src/Edges/CustomEdge.tsx index f72ff831..9a3c4230 100644 --- a/example/src/Edges/CustomEdge.tsx +++ b/example/src/Edges/CustomEdge.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { EdgeProps, getBezierPath, getMarkerEnd } from 'react-flow-renderer'; +import { EdgeProps, getBezierPath } from 'react-flow-renderer'; const CustomEdge: FC = ({ id, @@ -10,15 +10,12 @@ const CustomEdge: FC = ({ sourcePosition, targetPosition, data, - arrowHeadType, - markerEndId, }) => { const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition }); - const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); return ( <> - + {data.text} diff --git a/example/src/Edges/CustomEdge2.tsx b/example/src/Edges/CustomEdge2.tsx index 361b7f92..63b010a9 100644 --- a/example/src/Edges/CustomEdge2.tsx +++ b/example/src/Edges/CustomEdge2.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { EdgeProps, getBezierPath, getMarkerEnd, EdgeText, getEdgeCenter } from 'react-flow-renderer'; +import { EdgeProps, getBezierPath, EdgeText, getEdgeCenter } from 'react-flow-renderer'; const CustomEdge: FC = ({ id, @@ -10,11 +10,8 @@ const CustomEdge: FC = ({ sourcePosition, targetPosition, data, - arrowHeadType, - markerEndId, }) => { const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition }); - const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); const [centerX, centerY] = getEdgeCenter({ sourceX, sourceY, @@ -24,7 +21,7 @@ const CustomEdge: FC = ({ return ( <> - + ), labelStyle: { fill: 'red', fontWeight: 700 }, - arrowHeadType: ArrowHeadType.Arrow, + style: { stroke: '#ffcc00' }, + markerEnd: { + type: ArrowHeadType.ArrowClosed, + }, + markerStart: { + type: ArrowHeadType.Arrow, + color: '#FFCC00', + }, }, { id: 'e5-7', @@ -64,7 +71,10 @@ const initialElements: Elements = [ labelBgPadding: [8, 4], labelBgBorderRadius: 4, labelBgStyle: { fill: '#FFCC00', color: '#fff', fillOpacity: 0.7 }, - arrowHeadType: ArrowHeadType.ArrowClosed, + markerEnd: { + type: ArrowHeadType.ArrowClosed, + color: '#FFCC00', + }, }, { id: 'e5-8', @@ -72,7 +82,6 @@ const initialElements: Elements = [ target: '8', type: 'custom', data: { text: 'custom edge' }, - arrowHeadType: ArrowHeadType.ArrowClosed, }, { id: 'e5-9', diff --git a/example/src/FloatingEdges/FloatingEdge.tsx b/example/src/FloatingEdges/FloatingEdge.tsx index 60ee4b93..b03241d1 100644 --- a/example/src/FloatingEdges/FloatingEdge.tsx +++ b/example/src/FloatingEdges/FloatingEdge.tsx @@ -1,11 +1,10 @@ import { FC, useMemo, CSSProperties } from 'react'; -import { EdgeProps, getMarkerEnd, useStoreState, getBezierPath } from 'react-flow-renderer'; +import { EdgeProps, useStoreState, getBezierPath } from 'react-flow-renderer'; import { getEdgeParams } from './utils'; -const FloatingEdge: FC = ({ id, source, target, arrowHeadType, markerEndId, style }) => { +const FloatingEdge: FC = ({ id, source, target, style }) => { const nodes = useStoreState((state) => state.nodes); - const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); const sourceNode = useMemo(() => nodes.find((n) => n.id === source), [source, nodes]); const targetNode = useMemo(() => nodes.find((n) => n.id === target), [target, nodes]); @@ -27,7 +26,7 @@ const FloatingEdge: FC = ({ id, source, target, arrowHeadType, marker return ( - + ); }; diff --git a/example/src/FloatingEdges/index.tsx b/example/src/FloatingEdges/index.tsx index 7bdfc7a2..f1902515 100644 --- a/example/src/FloatingEdges/index.tsx +++ b/example/src/FloatingEdges/index.tsx @@ -9,7 +9,6 @@ import ReactFlow, { Elements, Connection, Edge, - ArrowHeadType, } from 'react-flow-renderer'; import './style.css'; @@ -31,8 +30,7 @@ const NodeAsHandleFlow = () => { const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - const onConnect = (params: Connection | Edge) => - setElements((els) => addEdge({ ...params, type: 'floating', arrowHeadType: ArrowHeadType.Arrow }, els)); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge({ ...params, type: 'floating' }, els)); return (
diff --git a/example/src/Overview/index.tsx b/example/src/Overview/index.tsx index ee13f916..17e0c9b5 100644 --- a/example/src/Overview/index.tsx +++ b/example/src/Overview/index.tsx @@ -13,7 +13,6 @@ import ReactFlow, { OnLoadParams, FlowTransform, SnapGrid, - ArrowHeadType, Connection, Edge, } from 'react-flow-renderer'; @@ -125,7 +124,7 @@ const initialElements: Elements = [ { id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' }, { id: 'e1-3', source: '1', target: '3' }, { id: 'e3-4', source: '3', target: '4', animated: true, label: 'animated edge' }, - { id: 'e4-5', source: '4', target: '5', arrowHeadType: ArrowHeadType.Arrow, label: 'edge with arrow head' }, + { id: 'e4-5', source: '4', target: '5', label: 'edge with arrow head' }, { id: 'e5-6', source: '5', target: '6', type: 'smoothstep', label: 'smooth step edge' }, { id: 'e5-7', diff --git a/example/src/Undirectional/index.tsx b/example/src/Undirectional/index.tsx index 9f51296a..7687f4b0 100644 --- a/example/src/Undirectional/index.tsx +++ b/example/src/Undirectional/index.tsx @@ -12,7 +12,6 @@ import ReactFlow, { ConnectionLineType, ConnectionMode, updateEdge, - ArrowHeadType, } from 'react-flow-renderer'; import CustomNode from './CustomNode'; @@ -69,7 +68,6 @@ const initialElements: Elements = [ sourceHandle: 'left', targetHandle: 'bottom', type: 'default', - arrowHeadType: ArrowHeadType.Arrow, }, { id: 'e0-1b', @@ -78,7 +76,6 @@ const initialElements: Elements = [ sourceHandle: 'top', targetHandle: 'right', type: 'default', - arrowHeadType: ArrowHeadType.Arrow, }, { id: 'e0-2a', @@ -87,7 +84,6 @@ const initialElements: Elements = [ sourceHandle: 'top', targetHandle: 'left', type: 'default', - arrowHeadType: ArrowHeadType.Arrow, }, { id: 'e0-2b', @@ -96,7 +92,6 @@ const initialElements: Elements = [ sourceHandle: 'right', targetHandle: 'bottom', type: 'default', - arrowHeadType: ArrowHeadType.Arrow, }, { id: 'e0-3a', @@ -105,7 +100,6 @@ const initialElements: Elements = [ sourceHandle: 'right', targetHandle: 'top', type: 'default', - arrowHeadType: ArrowHeadType.Arrow, }, { id: 'e0-3b', @@ -114,7 +108,6 @@ const initialElements: Elements = [ sourceHandle: 'bottom', targetHandle: 'left', type: 'default', - arrowHeadType: ArrowHeadType.Arrow, }, { id: 'e0-4a', @@ -123,7 +116,6 @@ const initialElements: Elements = [ sourceHandle: 'bottom', targetHandle: 'right', type: 'default', - arrowHeadType: ArrowHeadType.Arrow, }, { id: 'e0-4b', @@ -132,7 +124,6 @@ const initialElements: Elements = [ sourceHandle: 'left', targetHandle: 'top', type: 'default', - arrowHeadType: ArrowHeadType.Arrow, }, { id: 'e0-10', @@ -141,7 +132,6 @@ const initialElements: Elements = [ sourceHandle: 'top', targetHandle: 'bottom', type: 'default', - arrowHeadType: ArrowHeadType.Arrow, }, { id: 'e0-20', @@ -150,7 +140,6 @@ const initialElements: Elements = [ sourceHandle: 'right', targetHandle: 'left', type: 'default', - arrowHeadType: ArrowHeadType.Arrow, }, { id: 'e0-30', @@ -159,7 +148,6 @@ const initialElements: Elements = [ sourceHandle: 'bottom', targetHandle: 'top', type: 'default', - arrowHeadType: ArrowHeadType.Arrow, }, { id: 'e0-40', @@ -168,7 +156,6 @@ const initialElements: Elements = [ sourceHandle: 'left', targetHandle: 'right', type: 'default', - arrowHeadType: ArrowHeadType.Arrow, }, ]; diff --git a/src/components/Edges/BezierEdge.tsx b/src/components/Edges/BezierEdge.tsx index b4c94197..2313767f 100644 --- a/src/components/Edges/BezierEdge.tsx +++ b/src/components/Edges/BezierEdge.tsx @@ -2,7 +2,7 @@ import React, { memo } from 'react'; import EdgeText from './EdgeText'; -import { getMarkerEnd, getCenter } from './utils'; +import { getCenter } from './utils'; import { EdgeProps, Position } from '../../types'; interface GetBezierPathParams { @@ -60,8 +60,8 @@ export default memo( labelBgPadding, labelBgBorderRadius, style, - arrowHeadType, - markerEndId, + markerEnd, + markerStart, }: EdgeProps) => { const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }); const path = getBezierPath({ @@ -86,11 +86,15 @@ export default memo( /> ) : null; - const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); - return ( <> - + {text} ); diff --git a/src/components/Edges/SmoothStepEdge.tsx b/src/components/Edges/SmoothStepEdge.tsx index 0c42410a..3bb087dd 100644 --- a/src/components/Edges/SmoothStepEdge.tsx +++ b/src/components/Edges/SmoothStepEdge.tsx @@ -1,7 +1,7 @@ import React, { memo } from 'react'; import EdgeText from './EdgeText'; -import { getMarkerEnd, getCenter } from './utils'; +import { getCenter } from './utils'; import { EdgeSmoothStepProps, Position } from '../../types'; // These are some helper methods for drawing the round corners @@ -73,8 +73,8 @@ export function getSmoothStepPath({ sourceY <= targetY ? rightTopCorner(cX, sourceY, cornerSize) : rightBottomCorner(cX, sourceY, cornerSize); secondCornerPath = sourceY <= targetY ? bottomLeftCorner(cX, targetY, cornerSize) : topLeftCorner(cX, targetY, cornerSize); - } else if (sourcePosition === Position.Right && targetPosition === Position.Left){ - // and sourceX > targetX + } else if (sourcePosition === Position.Right && targetPosition === Position.Left) { + // and sourceX > targetX firstCornerPath = sourceY <= targetY ? leftTopCorner(cX, sourceY, cornerSize) : leftBottomCorner(cX, sourceY, cornerSize); secondCornerPath = @@ -126,8 +126,8 @@ export default memo( style, sourcePosition = Position.Bottom, targetPosition = Position.Top, - arrowHeadType, - markerEndId, + markerEnd, + markerStart, borderRadius = 5, }: EdgeSmoothStepProps) => { const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }); @@ -142,8 +142,6 @@ export default memo( borderRadius, }); - const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); - const text = label ? ( - + {text} ); diff --git a/src/components/Edges/StraightEdge.tsx b/src/components/Edges/StraightEdge.tsx index 1bd3dedf..b7f0fac1 100644 --- a/src/components/Edges/StraightEdge.tsx +++ b/src/components/Edges/StraightEdge.tsx @@ -1,7 +1,6 @@ import React, { memo } from 'react'; import EdgeText from './EdgeText'; -import { getMarkerEnd } from './utils'; import { EdgeProps } from '../../types'; export default memo( @@ -17,15 +16,14 @@ export default memo( labelBgPadding, labelBgBorderRadius, style, - arrowHeadType, - markerEndId, + markerEnd, + markerStart, }: EdgeProps) => { const yOffset = Math.abs(targetY - sourceY) / 2; const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset; const xOffset = Math.abs(targetX - sourceX) / 2; const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset; - const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); const text = label ? ( {text} diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx index 60384e74..4e191cf7 100644 --- a/src/components/Edges/wrapEdge.tsx +++ b/src/components/Edges/wrapEdge.tsx @@ -5,6 +5,7 @@ import { useStoreActions, useStoreState } from '../../store/hooks'; import { Edge, EdgeProps, WrapEdgeProps } from '../../types'; import { onMouseDown } from '../../components/Handle/handler'; import { EdgeAnchor } from './EdgeAnchor'; +import { getMarkerId } from '../../utils/graph'; export default (EdgeComponent: ComponentType) => { const EdgeWrapper = ({ @@ -23,7 +24,6 @@ export default (EdgeComponent: ComponentType) => { labelBgPadding, labelBgBorderRadius, style, - arrowHeadType, source, target, sourceX, @@ -33,7 +33,6 @@ export default (EdgeComponent: ComponentType) => { sourcePosition, targetPosition, elementsSelectable, - markerEndId, isHidden, sourceHandleId, targetHandleId, @@ -46,6 +45,8 @@ export default (EdgeComponent: ComponentType) => { edgeUpdaterRadius, onEdgeUpdateStart, onEdgeUpdateEnd, + markerEnd, + markerStart, }: WrapEdgeProps): JSX.Element | null => { const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId); @@ -160,7 +161,18 @@ export default (EdgeComponent: ComponentType) => { _onEdgeUpdate ); }, - [id, source, target, type, sourceHandleId, targetHandleId, setConnectionNodeId, setPosition, edgeElement, onConnectEdge] + [ + id, + source, + target, + type, + sourceHandleId, + targetHandleId, + setConnectionNodeId, + setPosition, + edgeElement, + onConnectEdge, + ] ); const onEdgeUpdaterSourceMouseDown = useCallback( @@ -179,6 +191,8 @@ export default (EdgeComponent: ComponentType) => { const onEdgeUpdaterMouseEnter = useCallback(() => setUpdating(true), [setUpdating]); const onEdgeUpdaterMouseOut = useCallback(() => setUpdating(false), [setUpdating]); + const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart)})`, [markerStart]); + const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd)})`, [markerEnd]); if (isHidden) { return null; @@ -208,16 +222,16 @@ export default (EdgeComponent: ComponentType) => { labelBgBorderRadius={labelBgBorderRadius} data={data} style={style} - arrowHeadType={arrowHeadType} sourceX={sourceX} sourceY={sourceY} targetX={targetX} targetY={targetY} sourcePosition={sourcePosition} targetPosition={targetPosition} - markerEndId={markerEndId} sourceHandleId={sourceHandleId} targetHandleId={targetHandleId} + markerStart={markerStartUrl} + markerEnd={markerEndUrl} /> {handleEdgeUpdate && ( ( - - {children} - -); - -interface MarkerDefinitionsProps { +interface SymbolProps { color: string; } -const MarkerDefinitions = ({ color }: MarkerDefinitionsProps) => { +interface EdgeMarkerExtended extends EdgeMarker { + id: string; +} + +const ArrowSymbol = ({ color }: SymbolProps) => { + return ( + + ); +}; + +const ArrowClosedSymbol = ({ color }: SymbolProps) => { + return ( + + ); +}; + +const markerSymbols = { + [ArrowHeadType.Arrow]: ArrowSymbol, + [ArrowHeadType.ArrowClosed]: ArrowClosedSymbol, +}; + +const Marker = ({ id, type, color }: MarkerProps) => { + const Symbol = markerSymbols[type]; + + return ( + + + + ); +}; + +const MarkerDefinitions = ({ defaultColor }: MarkerDefinitionsProps) => { + const edges = useStoreState((state) => state.edges); + const markers = useMemo(() => { + const ids: string[] = []; + + return edges.reduce((markers, edge) => { + [edge.markerStart, edge.markerEnd].forEach((marker) => { + if (marker && typeof marker === 'object') { + const markerId = getMarkerId(marker); + if (!ids.includes(markerId)) { + markers.push({ id: markerId, ...marker }); + ids.push(markerId); + } + } + }); + return markers.sort((a, b) => a.id.localeCompare(b.id)); + }, []); + }, [edges]); + return ( - - - - - - + {markers.map((marker: EdgeMarkerExtended) => ( + + ))} ); }; diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 1fd7f766..eaea06ad 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -26,8 +26,7 @@ interface EdgeRendererProps { connectionMode?: ConnectionMode; onElementClick?: (event: React.MouseEvent, element: Node | Edge) => void; onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void; - arrowHeadColor: string; - markerEndId?: string; + defaultMarkerColor: string; onlyRenderVisibleElements: boolean; onEdgeUpdate?: OnEdgeUpdateFunc; onEdgeContextMenu?: (event: React.MouseEvent, edge: Edge) => void; @@ -155,7 +154,8 @@ const Edge = ({ labelBgPadding={edge.labelBgPadding} labelBgBorderRadius={edge.labelBgBorderRadius} style={edge.style} - arrowHeadType={edge.arrowHeadType} + markerEnd={edge.markerEnd} + markerStart={edge.markerStart} source={edge.source} target={edge.target} sourceHandleId={sourceHandleId} @@ -167,7 +167,6 @@ const Edge = ({ sourcePosition={sourcePosition} targetPosition={targetPosition} elementsSelectable={elementsSelectable} - markerEndId={props.markerEndId} isHidden={edge.isHidden} onConnectEdge={onConnectEdge} handleEdgeUpdate={typeof props.onEdgeUpdate !== 'undefined'} @@ -203,7 +202,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { const { connectionLineType, - arrowHeadColor, + defaultMarkerColor, connectionLineStyle, connectionLineComponent, onlyRenderVisibleElements, @@ -213,7 +212,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { return ( - + {edges.map((edge: Edge) => ( { children: ReactNode; } diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index e002f366..51663eb5 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -23,7 +23,7 @@ export interface GraphViewProps extends Omit, 'on translateExtent?: TranslateExtent; preventScrolling?: boolean; nodeExtent?: NodeExtent; - arrowHeadColor?: string; - markerEndId?: string; + defaultMarkerColor?: string; zoomOnScroll?: boolean; zoomOnPinch?: boolean; panOnScroll?: boolean; @@ -184,8 +183,7 @@ const ReactFlow = forwardRef( translateExtent, preventScrolling = true, nodeExtent, - arrowHeadColor = '#b1b1b7', - markerEndId, + defaultMarkerColor = '#b1b1b7', zoomOnScroll = true, zoomOnPinch = true, panOnScroll = false, @@ -262,8 +260,7 @@ const ReactFlow = forwardRef( translateExtent={translateExtent} preventScrolling={preventScrolling} nodeExtent={nodeExtent} - arrowHeadColor={arrowHeadColor} - markerEndId={markerEndId} + defaultMarkerColor={defaultMarkerColor} zoomOnScroll={zoomOnScroll} zoomOnPinch={zoomOnPinch} zoomOnDoubleClick={zoomOnDoubleClick} diff --git a/src/types/index.ts b/src/types/index.ts index 754e10e2..97938453 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -57,6 +57,13 @@ export enum ArrowHeadType { ArrowClosed = 'arrowclosed', } +export interface EdgeMarker { + type: ArrowHeadType; + color?: string; +} + +export type EdgeMarkerType = string | EdgeMarker; + export interface Edge { id: ElementId; type?: string; @@ -72,10 +79,11 @@ export interface Edge { labelBgBorderRadius?: number; style?: CSSProperties; animated?: boolean; - arrowHeadType?: ArrowHeadType; isHidden?: boolean; data?: T; className?: string; + markerStart?: EdgeMarkerType; + markerEnd?: EdgeMarkerType; } export enum BackgroundVariant { @@ -111,7 +119,6 @@ export interface WrapEdgeProps { labelBgPadding?: [number, number]; labelBgBorderRadius?: number; style?: CSSProperties; - arrowHeadType?: ArrowHeadType; source: ElementId; target: ElementId; sourceHandleId: ElementId | null; @@ -123,7 +130,6 @@ export interface WrapEdgeProps { sourcePosition: Position; targetPosition: Position; elementsSelectable?: boolean; - markerEndId?: string; isHidden?: boolean; handleEdgeUpdate: boolean; onConnectEdge: OnConnectFunc; @@ -134,6 +140,8 @@ export interface WrapEdgeProps { edgeUpdaterRadius?: number; onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge) => void; onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; + markerStart?: EdgeMarkerType; + markerEnd?: EdgeMarkerType; } export interface EdgeProps { @@ -155,11 +163,11 @@ export interface EdgeProps { labelBgPadding?: [number, number]; labelBgBorderRadius?: number; style?: CSSProperties; - arrowHeadType?: ArrowHeadType; - markerEndId?: string; data?: T; sourceHandleId?: ElementId | null; targetHandleId?: ElementId | null; + markerStart?: string; + markerEnd?: string; } export interface EdgeSmoothStepProps extends EdgeProps { borderRadius?: number; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 73b6c68b..2c6f4309 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -15,6 +15,7 @@ import { FlowExportObject, ReactFlowState, NodeExtent, + EdgeMarkerType, } from '../types'; export const isEdge = (element: Node | Connection | Edge): element is Edge => @@ -57,6 +58,21 @@ export const removeElements = (elementsToRemove: Elements, elements: Elements): const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection): ElementId => `reactflow__edge-${source}${sourceHandle}-${target}${targetHandle}`; +export const getMarkerId = (marker: EdgeMarkerType | undefined): string => { + if (typeof marker === 'undefined') { + return ''; + } + + if (typeof marker === 'string') { + return marker; + } + + return Object.keys(marker) + .sort() + .map((key: string) => `${key}=${(marker as any)[key]}`) + .join('&'); +}; + const connectionExists = (edge: Edge, elements: Elements) => { return elements.some( (el) => From 1b73c8fff5857523d8940a3c36c87172762116d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Fri, 15 Oct 2021 11:51:54 +0200 Subject: [PATCH 009/263] feat(markers): add more properties for custom markers --- example/src/Edges/index.tsx | 8 ++- .../EdgeRenderer/MarkerDefinitions.tsx | 52 ++++++++++--------- src/types/index.ts | 4 ++ 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/example/src/Edges/index.tsx b/example/src/Edges/index.tsx index 9676da7e..c46cca68 100644 --- a/example/src/Edges/index.tsx +++ b/example/src/Edges/index.tsx @@ -56,7 +56,12 @@ const initialElements: Elements = [ labelStyle: { fill: 'red', fontWeight: 700 }, style: { stroke: '#ffcc00' }, markerEnd: { - type: ArrowHeadType.ArrowClosed, + type: ArrowHeadType.Arrow, + color: '#FFCC00', + units: 'userSpaceOnUse', + width: 20, + height: 20, + strokeWidth: 2, }, markerStart: { type: ArrowHeadType.Arrow, @@ -73,7 +78,6 @@ const initialElements: Elements = [ labelBgStyle: { fill: '#FFCC00', color: '#fff', fillOpacity: 0.7 }, markerEnd: { type: ArrowHeadType.ArrowClosed, - color: '#FFCC00', }, }, { diff --git a/src/container/EdgeRenderer/MarkerDefinitions.tsx b/src/container/EdgeRenderer/MarkerDefinitions.tsx index f4dc34a6..9645ea0e 100644 --- a/src/container/EdgeRenderer/MarkerDefinitions.tsx +++ b/src/container/EdgeRenderer/MarkerDefinitions.tsx @@ -2,43 +2,35 @@ import React, { useMemo } from 'react'; import { useStoreState } from '../../store/hooks'; import { EdgeMarker, ArrowHeadType } from '../../types'; import { getMarkerId } from '../../utils/graph'; -interface MarkerProps { +interface MarkerProps extends EdgeMarker { id: string; - type: ArrowHeadType; - color: string; } interface MarkerDefinitionsProps { defaultColor: string; } -interface SymbolProps { - color: string; -} +type SymbolProps = Omit; -interface EdgeMarkerExtended extends EdgeMarker { - id: string; -} - -const ArrowSymbol = ({ color }: SymbolProps) => { +const ArrowSymbol = ({ color = 'none', strokeWidth = 1 }: SymbolProps) => { return ( ); }; -const ArrowClosedSymbol = ({ color }: SymbolProps) => { +const ArrowClosedSymbol = ({ color = 'none', strokeWidth = 1 }: SymbolProps) => { return ( @@ -50,21 +42,22 @@ const markerSymbols = { [ArrowHeadType.ArrowClosed]: ArrowClosedSymbol, }; -const Marker = ({ id, type, color }: MarkerProps) => { +const Marker = ({ id, type, color, width = 12.5, height = 12.5, units = 'strokeWidth', strokeWidth }: MarkerProps) => { const Symbol = markerSymbols[type]; return ( - + ); }; @@ -74,24 +67,33 @@ const MarkerDefinitions = ({ defaultColor }: MarkerDefinitionsProps) => { const markers = useMemo(() => { const ids: string[] = []; - return edges.reduce((markers, edge) => { + return edges.reduce((markers, edge) => { [edge.markerStart, edge.markerEnd].forEach((marker) => { if (marker && typeof marker === 'object') { const markerId = getMarkerId(marker); if (!ids.includes(markerId)) { - markers.push({ id: markerId, ...marker }); + markers.push({ id: markerId, color: marker.color || defaultColor, ...marker }); ids.push(markerId); } } }); return markers.sort((a, b) => a.id.localeCompare(b.id)); }, []); - }, [edges]); + }, [edges, defaultColor]); return ( - {markers.map((marker: EdgeMarkerExtended) => ( - + {markers.map((marker: MarkerProps) => ( + ))} ); diff --git a/src/types/index.ts b/src/types/index.ts index 97938453..2a660572 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -60,6 +60,10 @@ export enum ArrowHeadType { export interface EdgeMarker { type: ArrowHeadType; color?: string; + width?: number; + height?: number; + units?: 'userSpaceOnUse' | 'strokeWidth'; + strokeWidth?: number; } export type EdgeMarkerType = string | EdgeMarker; From b08809d8108d34ed214d788c61dfce7550ab7c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Fri, 15 Oct 2021 13:07:02 +0200 Subject: [PATCH 010/263] feat(markers): add option to control orientation of markers --- .release-it.json | 2 +- example/src/Edges/index.tsx | 58 ++++++++++--------- package-lock.json | 4 +- package.json | 4 +- .../EdgeRenderer/MarkerDefinitions.tsx | 14 ++++- src/types/index.ts | 3 +- 6 files changed, 50 insertions(+), 35 deletions(-) diff --git a/.release-it.json b/.release-it.json index 93f9ec7b..2521416c 100644 --- a/.release-it.json +++ b/.release-it.json @@ -7,6 +7,6 @@ }, "hooks": { "after:bump": "npm run build", - "after:release": "echo Successfully released ${name} v${version} to ${repo.repository}." + "after:release": "echo Successfully released ${name} v${version}." } } diff --git a/example/src/Edges/index.tsx b/example/src/Edges/index.tsx index c46cca68..1f04b388 100644 --- a/example/src/Edges/index.tsx +++ b/example/src/Edges/index.tsx @@ -41,33 +41,6 @@ const initialElements: Elements = [ { id: 'e3-4', source: '3', target: '4', type: 'straight', label: 'straight edge' }, { id: 'e3-3a', source: '3', target: '3a', type: 'straight', label: 'label only edge', style: { stroke: 'none' } }, { id: 'e3-5', source: '4', target: '5', animated: true, label: 'animated styled edge', style: { stroke: 'red' } }, - { - id: 'e5-6', - source: '5', - target: '6', - label: ( - <> - i am using - - {''} - - - ), - labelStyle: { fill: 'red', fontWeight: 700 }, - style: { stroke: '#ffcc00' }, - markerEnd: { - type: ArrowHeadType.Arrow, - color: '#FFCC00', - units: 'userSpaceOnUse', - width: 20, - height: 20, - strokeWidth: 2, - }, - markerStart: { - type: ArrowHeadType.Arrow, - color: '#FFCC00', - }, - }, { id: 'e5-7', source: '5', @@ -94,6 +67,37 @@ const initialElements: Elements = [ type: 'custom2', data: { text: 'custom edge 2' }, }, + { + id: 'e5-6', + source: '5', + target: '6', + label: ( + <> + i am using + + {''} + + + ), + labelStyle: { fill: 'red', fontWeight: 700 }, + style: { stroke: '#ffcc00' }, + markerEnd: { + type: ArrowHeadType.Arrow, + color: '#FFCC00', + units: 'userSpaceOnUse', + width: 20, + height: 20, + strokeWidth: 2, + }, + markerStart: { + type: ArrowHeadType.ArrowClosed, + color: '#FFCC00', + orient: 'auto-start-reverse', + units: 'userSpaceOnUse', + width: 20, + height: 20, + }, + }, ]; const edgeTypes: EdgeTypesType = { diff --git a/package-lock.json b/package-lock.json index dee85300..97f528ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "9.6.9", + "version": "10.0.0-next.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "9.6.9", + "version": "10.0.0-next.1", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/package.json b/package.json index d4f573b4..f50814a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "9.6.9", + "version": "10.0.0-next.1", "engines": { "node": ">=12" }, @@ -28,7 +28,7 @@ "cy:open": "cypress open", "release": "npm run test && release-it", "release:notest": "release-it", - "release:next": "release-it --preRelease=next" + "release:next": "release-it --preRelease=next --no-git" }, "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/src/container/EdgeRenderer/MarkerDefinitions.tsx b/src/container/EdgeRenderer/MarkerDefinitions.tsx index 9645ea0e..222435f4 100644 --- a/src/container/EdgeRenderer/MarkerDefinitions.tsx +++ b/src/container/EdgeRenderer/MarkerDefinitions.tsx @@ -42,7 +42,16 @@ const markerSymbols = { [ArrowHeadType.ArrowClosed]: ArrowClosedSymbol, }; -const Marker = ({ id, type, color, width = 12.5, height = 12.5, units = 'strokeWidth', strokeWidth }: MarkerProps) => { +const Marker = ({ + id, + type, + color, + width = 12.5, + height = 12.5, + units = 'strokeWidth', + strokeWidth, + orient = 'auto', +}: MarkerProps) => { const Symbol = markerSymbols[type]; return ( @@ -53,7 +62,7 @@ const Marker = ({ id, type, color, width = 12.5, height = 12.5, units = 'strokeW markerWidth={`${width}`} markerHeight={`${height}`} viewBox="-10 -10 20 20" - orient="auto" + orient={orient} refX="0" refY="0" > @@ -93,6 +102,7 @@ const MarkerDefinitions = ({ defaultColor }: MarkerDefinitionsProps) => { height={marker.height} units={marker.units} strokeWidth={marker.strokeWidth} + orient={marker.orient} /> ))} diff --git a/src/types/index.ts b/src/types/index.ts index 2a660572..a03f847e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -62,7 +62,8 @@ export interface EdgeMarker { color?: string; width?: number; height?: number; - units?: 'userSpaceOnUse' | 'strokeWidth'; + units?: string; + orient?: string; strokeWidth?: number; } From dc5172e19e9e7e06a479ee46a7e0bf7cd5f152fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Fri, 15 Oct 2021 13:43:19 +0200 Subject: [PATCH 011/263] feat(markers): bump next version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 97f528ec..0b755877 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.1", + "version": "10.0.0-next.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "10.0.0-next.1", + "version": "10.0.0-next.2", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/package.json b/package.json index f50814a8..0a50f64e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.1", + "version": "10.0.0-next.2", "engines": { "node": ">=12" }, From 410b12946d10a157189bb0b7c242487831b6b6e8 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 19 Oct 2021 11:09:49 +0200 Subject: [PATCH 012/263] refactor(state): separate nodes and edges --- example/src/Basic/index.tsx | 14 +- example/src/Stress/index.tsx | 4 +- example/src/UpdateNode/index.tsx | 4 +- src/components/ConnectionLine/index.tsx | 8 +- src/components/Edges/wrapEdge.tsx | 6 +- src/components/ElementUpdater/index.tsx | 31 --- src/components/Nodes/wrapNode.tsx | 58 +++--- src/components/NodesSelection/index.tsx | 12 +- src/components/SelectionListener/index.tsx | 4 +- src/components/StoreUpdater/index.tsx | 211 +++++++++++++++++++++ src/container/EdgeRenderer/index.tsx | 138 +++++++++----- src/container/EdgeRenderer/utils.ts | 32 +--- src/container/FlowRenderer/index.tsx | 4 +- src/container/GraphView/index.tsx | 175 ++--------------- src/container/NodeRenderer/index.tsx | 40 ++-- src/container/ReactFlow/Wrapper.tsx | 27 ++- src/container/ReactFlow/index.tsx | 62 +++--- src/container/ZoomPane/index.tsx | 15 +- src/hooks/useGlobalKeyHandler.ts | 13 +- src/store/index.ts | 168 ++++++++-------- src/types/index.ts | 69 +++++-- src/utils/graph.ts | 78 ++++---- 22 files changed, 635 insertions(+), 538 deletions(-) delete mode 100644 src/components/ElementUpdater/index.tsx create mode 100644 src/components/StoreUpdater/index.tsx diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index fad5f17d..f47d372a 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -7,16 +7,17 @@ import ReactFlow, { applyEdgeChanges, MiniMap, Controls, - FlowElement, Node, Edge, - ElementChange, + NodeChange, + EdgeChange, OnLoadParams, Connection, } from 'react-flow-renderer'; const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); -const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); +const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); +const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge); const initialNodes: Node[] = [ { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, @@ -66,11 +67,11 @@ const BasicFlow = () => { }); }; - const onNodesChange = useCallback((changes: ElementChange[]) => { + const onNodesChange = useCallback((changes: NodeChange[]) => { setNodes((ns) => applyNodeChanges(changes, ns)); }, []); - const onEdgesChange = useCallback((changes: ElementChange[]) => { + const onEdgesChange = useCallback((changes: EdgeChange[]) => { setEdges((es) => applyEdgeChanges(changes, es)); }, []); @@ -81,7 +82,8 @@ const BasicFlow = () => { onLoad={onLoad} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} - onElementClick={onElementClick} + onNodeClick={onNodeClick} + onEdgeClick={onEdgeClick} onConnect={onConnect} onNodeDragStop={onNodeDragStop} className="react-flow-basic-example" diff --git a/example/src/Stress/index.tsx b/example/src/Stress/index.tsx index 2b27158b..f3cf1df2 100644 --- a/example/src/Stress/index.tsx +++ b/example/src/Stress/index.tsx @@ -6,7 +6,7 @@ import ReactFlow, { OnLoadParams, Edge, Node, - ElementChange, + NodeChange, applyNodeChanges, } from 'react-flow-renderer'; @@ -48,7 +48,7 @@ const StressFlow = () => { setEdges(initialElements.edges); }; - const onNodesChange = useCallback((changes: ElementChange[]) => { + const onNodesChange = useCallback((changes: NodeChange[]) => { setNodes((ns) => applyNodeChanges(changes, ns)); }, []); diff --git a/example/src/UpdateNode/index.tsx b/example/src/UpdateNode/index.tsx index a4c10abe..8a4269aa 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, ElementChange } from 'react-flow-renderer'; +import ReactFlow, { Node, Edge, applyNodeChanges, NodeChange } from 'react-flow-renderer'; import './updatenode.css'; @@ -60,7 +60,7 @@ const UpdateNode = () => { ); }, [nodeHidden]); - const onNodesChange = useCallback((changes: ElementChange[]) => { + const onNodesChange = useCallback((changes: NodeChange[]) => { setNodes((ns) => applyNodeChanges(changes, ns)); }, []); diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index da1ed174..926ed692 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -52,15 +52,15 @@ export default ({ setSourceNode(nextSourceNode); }, []); - if (!sourceNode || !isConnectable) { + if (!sourceNode || !isConnectable || !sourceNode.handleBounds?.[connectionHandleType]) { return null; } const sourceHandle = handleId - ? sourceNode.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId) - : sourceNode.handleBounds[connectionHandleType][0]; + ? sourceNode.handleBounds[connectionHandleType]!.find((d: HandleElement) => d.id === handleId) + : sourceNode.handleBounds[connectionHandleType]![0]; const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.width! / 2; - const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.height; + const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.height!; const sourceX = sourceNode.position.x + sourceHandleX; const sourceY = sourceNode.position.y + sourceHandleY; diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx index ef934e01..8bb9caae 100644 --- a/src/components/Edges/wrapEdge.tsx +++ b/src/components/Edges/wrapEdge.tsx @@ -23,7 +23,7 @@ export default (EdgeComponent: ComponentType) => { data, onClick, onEdgeDoubleClick, - selected, + isSelected, animated, label, labelStyle, @@ -69,7 +69,7 @@ export default (EdgeComponent: ComponentType) => { 'react-flow__edge', `react-flow__edge-${type}`, className, - { selected, animated, inactive, updating }, + { selected: isSelected, animated, inactive, updating }, ]); const edgeElement = useMemo(() => { @@ -219,7 +219,7 @@ export default (EdgeComponent: ComponentType) => { id={id} source={source} target={target} - selected={selected} + isSelected={isSelected} animated={animated} label={label} labelStyle={labelStyle} diff --git a/src/components/ElementUpdater/index.tsx b/src/components/ElementUpdater/index.tsx deleted file mode 100644 index df6f3745..00000000 --- a/src/components/ElementUpdater/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect } from 'react'; -import shallow from 'zustand/shallow'; - -import { useStore } from '../../store'; -import { Node, Edge, ReactFlowState } from '../../types'; - -interface ElementUpdaterProps { - nodes: Node[]; - edges: Edge[]; -} - -const selector = (s: ReactFlowState) => ({ - setNodes: s.setNodes, - setEdges: s.setEdges, -}); - -const ElementUpdater = ({ nodes, edges }: ElementUpdaterProps) => { - const { setNodes, setEdges } = useStore(selector, shallow); - - useEffect(() => { - setNodes(nodes); - }, [nodes]); - - useEffect(() => { - setEdges(edges); - }, [edges]); - - return null; -}; - -export default ElementUpdater; diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index d1b9be3c..ebf1106b 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -9,9 +9,9 @@ import { NodeComponentProps, WrapNodeProps, ReactFlowState } from '../../types'; const selector = (s: ReactFlowState) => ({ addSelectedElements: s.addSelectedElements, - onNodesChange: s.onNodesChange, unsetNodesSelection: s.unsetNodesSelection, - updateNodePosDiff: s.updateNodePosDiff, + updateNodePosition: s.updateNodePosition, + updateNodeDimensions: s.updateNodeDimensions, }); export default (NodeComponent: ComponentType) => { @@ -22,7 +22,7 @@ export default (NodeComponent: ComponentType) => { scale, xPos, yPos, - selected, + isSelected, onClick, onMouseEnter, onMouseMove, @@ -48,8 +48,10 @@ export default (NodeComponent: ComponentType) => { resizeObserver, dragHandle, }: WrapNodeProps) => { - // const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); - const { addSelectedElements, onNodesChange, unsetNodesSelection, updateNodePosDiff } = useStore(selector, shallow); + const { addSelectedElements, unsetNodesSelection, updateNodePosition, updateNodeDimensions } = useStore( + selector, + shallow + ); const nodeElement = useRef(null); const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]); @@ -57,7 +59,7 @@ export default (NodeComponent: ComponentType) => { const nodeStyle: CSSProperties = useMemo( () => ({ - zIndex: selected ? 10 : 3, + zIndex: isSelected ? 10 : 3, transform: `translate(${xPos}px,${yPos}px)`, pointerEvents: isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none', @@ -66,7 +68,7 @@ export default (NodeComponent: ComponentType) => { ...style, }), [ - selected, + isSelected, xPos, yPos, isSelectable, @@ -118,7 +120,7 @@ export default (NodeComponent: ComponentType) => { if (isSelectable) { unsetNodesSelection(); - if (!selected) { + if (!isSelected) { addSelectedElements([node]); } } @@ -126,7 +128,7 @@ export default (NodeComponent: ComponentType) => { onClick?.(event, node); } }, - [isSelectable, selected, isDraggable, onClick, node] + [isSelectable, isSelected, isDraggable, onClick, node] ); const onDragStart = useCallback( @@ -136,15 +138,15 @@ export default (NodeComponent: ComponentType) => { if (selectNodesOnDrag && isSelectable) { unsetNodesSelection(); - if (!selected) { + if (!isSelected) { addSelectedElements([node]); } - } else if (!selectNodesOnDrag && !selected && isSelectable) { + } else if (!selectNodesOnDrag && !isSelected && isSelectable) { unsetNodesSelection(); addSelectedElements([]); } }, - [node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart] + [node, isSelected, selectNodesOnDrag, isSelectable, onNodeDragStart] ); const onDrag = useCallback( @@ -156,7 +158,7 @@ export default (NodeComponent: ComponentType) => { onNodeDrag(event as MouseEvent, node); } - updateNodePosDiff({ id, isDragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } }); + updateNodePosition({ id, isDragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } }); }, [id, node, onNodeDrag] ); @@ -166,7 +168,7 @@ export default (NodeComponent: ComponentType) => { // onDragStop also gets called when user just clicks on a node. // Because of that we set dragging to true inside the onDrag handler and handle the click here if (!isDragging) { - if (isSelectable && !selectNodesOnDrag && !selected) { + if (isSelectable && !selectNodesOnDrag && !isSelected) { addSelectedElements([node]); } @@ -175,18 +177,14 @@ export default (NodeComponent: ComponentType) => { return; } - onNodesChange?.([ - { - id: node.id, - change: { - isDragging: true, - }, - }, - ]); + updateNodePosition({ + id: node.id, + isDragging: false, + }); onNodeDragStop?.(event as MouseEvent, node); }, - [node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, selected] + [node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, isSelected] ); const onNodeDoubleClickHandler = useCallback( @@ -196,11 +194,11 @@ export default (NodeComponent: ComponentType) => { [node, onNodeDoubleClick] ); - // useEffect(() => { - // if (nodeElement.current && !isHidden) { - // updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); - // } - // }, [id, isHidden, sourcePosition, targetPosition]); + useEffect(() => { + if (nodeElement.current && !isHidden && !isInitialized) { + updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); + } + }, [id, isHidden, sourcePosition, targetPosition, isInitialized]); useEffect(() => { if (nodeElement.current) { @@ -220,7 +218,7 @@ export default (NodeComponent: ComponentType) => { `react-flow__node-${type}`, className, { - selected, + selected: isSelected, selectable: isSelectable, }, ]); @@ -257,7 +255,7 @@ export default (NodeComponent: ComponentType) => { type={type} xPos={xPos} yPos={yPos} - selected={selected} + isSelected={isSelected} isConnectable={isConnectable} sourcePosition={sourcePosition} targetPosition={targetPosition} diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index 4518be9e..5cc8e7ac 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -20,10 +20,10 @@ const selector = (s: ReactFlowState) => ({ transform: s.transform, selectedNodesBbox: s.selectedNodesBbox, selectionActive: s.selectionActive, - selectedNodes: s.nodes.filter((n) => n.selected), + selectedNodes: s.nodes.filter((n) => n.isSelected), snapToGrid: s.snapToGrid, snapGrid: s.snapGrid, - updateNodePosDiff: s.updateNodePosDiff, + updateNodePosition: s.updateNodePosition, }); export default ({ @@ -32,7 +32,7 @@ export default ({ onSelectionDragStop, onSelectionContextMenu, }: NodesSelectionProps) => { - const { transform, selectedNodesBbox, selectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosDiff } = + const { transform, selectedNodesBbox, selectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } = useStore(selector); const [tX, tY, tScale] = transform; const nodeRef = useRef(null); @@ -69,7 +69,7 @@ export default ({ onSelectionDrag(event, selectedNodes); } - updateNodePosDiff({ + updateNodePosition({ diff: { x: data.deltaX, y: data.deltaY, @@ -77,12 +77,12 @@ export default ({ isDragging: true, }); }, - [onSelectionDrag, selectedNodes, updateNodePosDiff] + [onSelectionDrag, selectedNodes, updateNodePosition] ); const onStop = useCallback( (event: MouseEvent) => { - updateNodePosDiff({ + updateNodePosition({ isDragging: false, }); diff --git a/src/components/SelectionListener/index.tsx b/src/components/SelectionListener/index.tsx index 250f9ecb..a99a95a0 100644 --- a/src/components/SelectionListener/index.tsx +++ b/src/components/SelectionListener/index.tsx @@ -9,8 +9,8 @@ interface SelectionListenerProps { } const selectedElementsSelector = (s: ReactFlowState) => [ - ...s.nodes.filter((n) => n.selected), - ...s.edges.filter((e) => e.selected), + ...s.nodes.filter((n) => n.isSelected), + ...s.edges.filter((e) => e.isSelected), ]; // This is just a helper component for calling the onSelectionChange listener. diff --git a/src/components/StoreUpdater/index.tsx b/src/components/StoreUpdater/index.tsx new file mode 100644 index 00000000..235b7b08 --- /dev/null +++ b/src/components/StoreUpdater/index.tsx @@ -0,0 +1,211 @@ +import { useEffect } from 'react'; +import shallow from 'zustand/shallow'; + +import { useStore } from '../../store'; +import { + Node, + Edge, + ReactFlowState, + OnConnectFunc, + OnConnectStartFunc, + OnConnectStopFunc, + OnConnectEndFunc, + NodeExtent, + OnNodesChange, + OnEdgesChange, + ConnectionMode, + SnapGrid, + TranslateExtent, +} from '../../types'; + +interface StoreUpdaterProps { + nodes: Node[]; + edges: Edge[]; + onConnect?: OnConnectFunc; + onConnectStart?: OnConnectStartFunc; + onConnectStop?: OnConnectStopFunc; + onConnectEnd?: OnConnectEndFunc; + nodesDraggable?: boolean; + nodesConnectable?: boolean; + minZoom?: number; + maxZoom?: number; + nodeExtent?: NodeExtent; + onNodesChange?: OnNodesChange; + onEdgesChange?: OnEdgesChange; + elementsSelectable?: boolean; + connectionMode?: ConnectionMode; + snapToGrid?: boolean; + snapGrid?: SnapGrid; + translateExtent?: TranslateExtent; +} + +const selector = (s: ReactFlowState) => ({ + setNodes: s.setNodes, + setEdges: s.setEdges, + setOnConnect: s.setOnConnect, + setOnConnectStart: s.setOnConnectStart, + setOnConnectStop: s.setOnConnectStop, + setOnConnectEnd: s.setOnConnectEnd, + setSnapGrid: s.setSnapGrid, + setSnapToGrid: s.setSnapToGrid, + setNodesDraggable: s.setNodesDraggable, + setNodesConnectable: s.setNodesConnectable, + setElementsSelectable: s.setElementsSelectable, + setMinZoom: s.setMinZoom, + setMaxZoom: s.setMaxZoom, + setTranslateExtent: s.setTranslateExtent, + setNodeExtent: s.setNodeExtent, + setConnectionMode: s.setConnectionMode, + setOnNodesChange: s.setOnNodesChange, + setOnEdgesChange: s.setOnEdgesChange, +}); + +const StoreUpdater = ({ + nodes, + edges, + onConnect, + onConnectStart, + onConnectStop, + onConnectEnd, + nodesDraggable, + nodesConnectable, + minZoom, + maxZoom, + nodeExtent, + onNodesChange, + onEdgesChange, + elementsSelectable, + connectionMode, + snapGrid, + snapToGrid, + translateExtent, +}: StoreUpdaterProps) => { + const { + setNodes, + setEdges, + setOnConnect, + setOnConnectStart, + setOnConnectStop, + setOnConnectEnd, + setNodesDraggable, + setSnapGrid, + setSnapToGrid, + setNodesConnectable, + setElementsSelectable, + setMinZoom, + setMaxZoom, + setTranslateExtent, + setNodeExtent, + setOnNodesChange, + setOnEdgesChange, + setConnectionMode, + } = useStore(selector, shallow); + + useEffect(() => { + setNodes(nodes); + }, [nodes]); + + useEffect(() => { + setEdges(edges, nodes); + }, [edges, nodes]); + + useEffect(() => { + if (onConnect) { + setOnConnect(onConnect); + } + }, [onConnect]); + + useEffect(() => { + if (onConnectStart) { + setOnConnectStart(onConnectStart); + } + }, [onConnectStart]); + + useEffect(() => { + if (onConnectStop) { + setOnConnectStop(onConnectStop); + } + }, [onConnectStop]); + + useEffect(() => { + if (onConnectEnd) { + setOnConnectEnd(onConnectEnd); + } + }, [onConnectEnd]); + + useEffect(() => { + if (typeof snapToGrid !== 'undefined') { + setSnapToGrid(snapToGrid); + } + }, [snapToGrid]); + + useEffect(() => { + if (typeof snapGrid !== 'undefined') { + setSnapGrid(snapGrid); + } + }, [snapGrid]); + + useEffect(() => { + if (typeof nodesDraggable !== 'undefined') { + setNodesDraggable(nodesDraggable); + } + }, [nodesDraggable]); + + useEffect(() => { + if (typeof nodesConnectable !== 'undefined') { + setNodesConnectable(nodesConnectable); + } + }, [nodesConnectable]); + + useEffect(() => { + if (typeof elementsSelectable !== 'undefined') { + setElementsSelectable(elementsSelectable); + } + }, [elementsSelectable]); + + useEffect(() => { + if (typeof minZoom !== 'undefined') { + setMinZoom(minZoom); + } + }, [minZoom]); + + useEffect(() => { + if (typeof maxZoom !== 'undefined') { + setMaxZoom(maxZoom); + } + }, [maxZoom]); + + useEffect(() => { + if (typeof translateExtent !== 'undefined') { + setTranslateExtent(translateExtent); + } + }, [translateExtent]); + + useEffect(() => { + if (typeof nodeExtent !== 'undefined') { + setNodeExtent(nodeExtent); + } + }, [nodeExtent]); + + useEffect(() => { + if (typeof connectionMode !== 'undefined') { + setConnectionMode(connectionMode); + } + }, [connectionMode]); + + useEffect(() => { + if (typeof onNodesChange !== 'undefined') { + setOnNodesChange(onNodesChange); + } + }, [onNodesChange]); + + useEffect(() => { + if (typeof onEdgesChange !== 'undefined') { + setOnEdgesChange(onEdgesChange); + } + }, [onEdgesChange]); + + return null; +}; + +export default StoreUpdater; diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 1157df2b..739aa4a6 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -4,7 +4,7 @@ import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import ConnectionLine from '../../components/ConnectionLine/index'; import MarkerDefinitions from './MarkerDefinitions'; -import { getEdgePositions, getHandle } from './utils'; +import { getEdgePositions, getHandle, getSourceTargetNodes } from './utils'; import { Position, Edge, @@ -15,15 +15,17 @@ import { ConnectionMode, OnEdgeUpdateFunc, ReactFlowState, + NodeHandleBounds, } from '../../types'; interface EdgeRendererProps { + nodes: Node[]; + edges: Edge[]; edgeTypes: any; connectionLineType: ConnectionLineType; connectionLineStyle?: CSSProperties; connectionLineComponent?: ConnectionLineComponent; - connectionMode?: ConnectionMode; - onElementClick?: (event: React.MouseEvent, element: Node | Edge) => void; + onEdgeClick?: (event: React.MouseEvent, node: Edge) => void; onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void; arrowHeadColor: string; markerEndId?: string; @@ -42,7 +44,7 @@ interface EdgeWrapperProps { edge: Edge; edgeTypes: any; markerEndId?: string; - onElementClick?: (event: React.MouseEvent, element: Node | Edge) => void; + onEdgeClick?: (event: React.MouseEvent, node: Edge) => void; onEdgeContextMenu?: (event: React.MouseEvent, edge: Edge) => void; onEdgeMouseEnter?: (event: React.MouseEvent, edge: Edge) => void; onEdgeMouseMove?: (event: React.MouseEvent, edge: Edge) => void; @@ -52,10 +54,18 @@ interface EdgeWrapperProps { onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge) => void; onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; onEdgeUpdate?: OnEdgeUpdateFunc; - targetNode?: Node; - sourceNode?: Node; elementsSelectable: boolean; connectionMode?: ConnectionMode; + sourceNodeWidth?: number | null; + sourceNodeHeight?: number | null; + sourceNodeX?: number; + sourceNodeY?: number; + sourceNodeHandleBounds?: NodeHandleBounds; + targetNodeWidth?: number | null; + targetNodeHeight?: number | null; + targetNodeX?: number; + targetNodeY?: number; + targetNodeHandleBounds?: NodeHandleBounds; } const Edge = memo( @@ -63,7 +73,7 @@ const Edge = memo( edge, edgeTypes, markerEndId, - onElementClick, + onEdgeClick, onEdgeContextMenu, onEdgeMouseEnter, onEdgeMouseMove, @@ -73,10 +83,18 @@ const Edge = memo( onEdgeUpdateStart, onEdgeUpdateEnd, onEdgeUpdate, - targetNode, - sourceNode, - elementsSelectable, connectionMode, + elementsSelectable, + sourceNodeWidth, + sourceNodeHeight, + sourceNodeX, + sourceNodeY, + sourceNodeHandleBounds, + targetNodeWidth, + targetNodeHeight, + targetNodeX, + targetNodeY, + targetNodeHandleBounds, }: EdgeWrapperProps) => { const sourceHandleId = edge.sourceHandle || null; const targetHandleId = edge.targetHandle || null; @@ -88,31 +106,40 @@ const Edge = memo( [edge, onEdgeUpdate] ); - if (!sourceNode) { + // source and target node need to be initialized + if (!sourceNodeHandleBounds || !targetNodeHandleBounds) { + return null; + } + + if ( + !sourceNodeWidth || + !sourceNodeHeight || + typeof sourceNodeX === 'undefined' || + typeof sourceNodeY === 'undefined' + ) { console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`); return null; } - if (!targetNode) { + if ( + !targetNodeWidth || + !targetNodeHeight || + typeof targetNodeX === 'undefined' || + typeof targetNodeY === 'undefined' + ) { console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`); return null; } - // source and target node need to be initialized - if (!sourceNode.width || !targetNode.width) { - return null; - } - const edgeType = edge.type || 'default'; const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default; - const targetNodeBounds = targetNode.handleBounds; // when connection type is loose we can define all handles as sources const targetNodeHandles = connectionMode === ConnectionMode.Strict - ? targetNodeBounds.target - : targetNodeBounds.target || targetNodeBounds.source; - const sourceHandle = getHandle(sourceNode.handleBounds.source, sourceHandleId); - const targetHandle = getHandle(targetNodeHandles, targetHandleId); + ? targetNodeHandleBounds.target + : targetNodeHandleBounds.target || targetNodeHandleBounds.source; + const sourceHandle = getHandle(sourceNodeHandleBounds.source!, sourceHandleId); + const targetHandle = getHandle(targetNodeHandles!, targetHandleId); const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom; const targetPosition = targetHandle ? targetHandle.position : Position.Top; @@ -127,10 +154,10 @@ const Edge = memo( } const { sourceX, sourceY, targetX, targetY } = getEdgePositions( - sourceNode, + { x: sourceNodeX, y: sourceNodeY, width: sourceNodeWidth, height: sourceNodeHeight }, sourceHandle, sourcePosition, - targetNode, + { x: targetNodeX, y: targetNodeY, width: targetNodeWidth, height: targetNodeHeight }, targetHandle, targetPosition ); @@ -156,8 +183,8 @@ const Edge = memo( className={edge.className} type={edge.type} data={edge.data} - onClick={onElementClick} - selected={!!edge.selected} + onClick={onEdgeClick} + isSelected={!!edge.isSelected} animated={edge.animated} label={edge.label} labelStyle={edge.labelStyle} @@ -197,7 +224,6 @@ const Edge = memo( const selector = (s: ReactFlowState) => ({ transform: s.transform, - edges: s.edges, connectionNodeId: s.connectionNodeId, connectionHandleId: s.connectionHandleId, connectionHandleType: s.connectionHandleType, @@ -206,12 +232,12 @@ const selector = (s: ReactFlowState) => ({ elementsSelectable: s.elementsSelectable, width: s.width, height: s.height, + connectionMode: s.connectionMode, }); const EdgeRenderer = (props: EdgeRendererProps) => { const { transform, - edges, connectionNodeId, connectionHandleId, connectionHandleType, @@ -220,6 +246,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { elementsSelectable, width, height, + connectionMode, } = useStore(selector, shallow); if (!width) { @@ -227,33 +254,44 @@ const EdgeRenderer = (props: EdgeRendererProps) => { } const { connectionLineType, arrowHeadColor, connectionLineStyle, connectionLineComponent } = props; - const transformStyle = `translate(${transform[0]},${transform[1]}) scale(${transform[2]})`; const renderConnectionLine = connectionNodeId && connectionHandleType; return ( - - {edges.map((edge: Edge) => ( - - ))} + + {props.edges.map((edge: Edge) => { + const { sourceNode, targetNode } = getSourceTargetNodes(edge, props.nodes); + return ( + + ); + })} {renderConnectionLine && ( { - const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle); - const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle); + const sourceHandlePos = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle); + const targetHandlePos = getHandlePosition(targetPosition, targetNodeRect, targetHandle); return { sourceX: sourceHandlePos.x, @@ -169,16 +170,3 @@ 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 fc105de2..2db1ab80 100644 --- a/src/container/FlowRenderer/index.tsx +++ b/src/container/FlowRenderer/index.tsx @@ -22,6 +22,8 @@ interface FlowRendererProps | 'arrowHeadColor' | 'onlyRenderVisibleElements' | 'selectNodesOnDrag' + | 'nodes' + | 'edges' > { children: ReactNode; } @@ -54,7 +56,6 @@ const FlowRenderer = ({ paneMoveable, defaultPosition, defaultZoom, - translateExtent, preventScrolling, onSelectionDragStart, onSelectionDrag, @@ -106,7 +107,6 @@ const FlowRenderer = ({ paneMoveable={paneMoveable} defaultPosition={defaultPosition} defaultZoom={defaultZoom} - translateExtent={translateExtent} zoomActivationKeyCode={zoomActivationKeyCode} preventScrolling={preventScrolling} > diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 20e28dc3..8fddc4f3 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef, memo } from 'react'; -import shallow from 'zustand/shallow'; -import { useStore, useStoreApi } from '../../store'; +import { useStoreApi } from '../../store'; import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; @@ -10,17 +9,15 @@ import useZoomPanHelper from '../../hooks/useZoomPanHelper'; import { ReactFlowProps } from '../ReactFlow'; -import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode, ReactFlowState } from '../../types'; +import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types'; -export interface GraphViewProps extends Omit { +export interface GraphViewProps extends Omit { nodeTypes: NodeTypesType; edgeTypes: EdgeTypesType; selectionKeyCode: KeyCode; deleteKeyCode: KeyCode; multiSelectionKeyCode: KeyCode; connectionLineType: ConnectionLineType; - snapToGrid: boolean; - snapGrid: [number, number]; onlyRenderVisibleElements: boolean; defaultZoom: number; defaultPosition: [number, number]; @@ -28,33 +25,17 @@ export interface GraphViewProps extends Omit ({ - setOnConnect: s.setOnConnect, - setOnConnectStart: s.setOnConnectStart, - setOnConnectStop: s.setOnConnectStop, - setOnConnectEnd: s.setOnConnectEnd, - setSnapGrid: s.setSnapGrid, - setSnapToGrid: s.setSnapToGrid, - setNodesDraggable: s.setNodesDraggable, - setNodesConnectable: s.setNodesConnectable, - setElementsSelectable: s.setElementsSelectable, - setMinZoom: s.setMinZoom, - setMaxZoom: s.setMaxZoom, - setTranslateExtent: s.setTranslateExtent, - setNodeExtent: s.setNodeExtent, - setConnectionMode: s.setConnectionMode, - setOnNodesChange: s.setOnNodesChange, - setOnEdgesChange: s.setOnEdgesChange, -}); - const GraphView = ({ + nodes, + edges, nodeTypes, edgeTypes, onMove, onMoveStart, onMoveEnd, onLoad, - onElementClick, + onNodeClick, + onEdgeClick, onNodeDoubleClick, onEdgeDoubleClick, onNodeMouseEnter, @@ -68,7 +49,6 @@ const GraphView = ({ onSelectionDrag, onSelectionDragStop, onSelectionContextMenu, - connectionMode, connectionLineType, connectionLineStyle, connectionLineComponent, @@ -76,24 +56,12 @@ const GraphView = ({ multiSelectionKeyCode, zoomActivationKeyCode, deleteKeyCode, - onConnect, - onConnectStart, - onConnectStop, - onConnectEnd, - snapToGrid, - snapGrid, onlyRenderVisibleElements, - nodesDraggable, - nodesConnectable, elementsSelectable, selectNodesOnDrag, - minZoom, - maxZoom, defaultZoom, defaultPosition, - translateExtent, preventScrolling, - nodeExtent, arrowHeadColor, markerEndId, zoomOnScroll, @@ -114,31 +82,9 @@ const GraphView = ({ edgeUpdaterRadius, onEdgeUpdateStart, onEdgeUpdateEnd, - onNodesChange, - onEdgesChange, }: GraphViewProps) => { const isInitialized = useRef(false); const store = useStoreApi(); - - const { - setOnConnect, - setOnConnectStart, - setOnConnectStop, - setOnConnectEnd, - setSnapGrid, - setSnapToGrid, - setNodesDraggable, - setNodesConnectable, - setElementsSelectable, - setMinZoom, - setMaxZoom, - setTranslateExtent, - setNodeExtent, - setConnectionMode, - setOnNodesChange, - setOnEdgesChange, - } = useStore(selector, shallow); - const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper(); useEffect(() => { @@ -160,102 +106,6 @@ const GraphView = ({ } }, [onLoad, zoomIn, zoomOut, zoomTo, transform, fitView, initialized]); - useEffect(() => { - if (onConnect) { - setOnConnect(onConnect); - } - }, [onConnect]); - - useEffect(() => { - if (onConnectStart) { - setOnConnectStart(onConnectStart); - } - }, [onConnectStart]); - - useEffect(() => { - if (onConnectStop) { - setOnConnectStop(onConnectStop); - } - }, [onConnectStop]); - - useEffect(() => { - if (onConnectEnd) { - setOnConnectEnd(onConnectEnd); - } - }, [onConnectEnd]); - - useEffect(() => { - if (typeof snapToGrid !== 'undefined') { - setSnapToGrid(snapToGrid); - } - }, [snapToGrid]); - - useEffect(() => { - if (typeof snapGrid !== 'undefined') { - setSnapGrid(snapGrid); - } - }, [snapGrid]); - - useEffect(() => { - if (typeof nodesDraggable !== 'undefined') { - setNodesDraggable(nodesDraggable); - } - }, [nodesDraggable]); - - useEffect(() => { - if (typeof nodesConnectable !== 'undefined') { - setNodesConnectable(nodesConnectable); - } - }, [nodesConnectable]); - - useEffect(() => { - if (typeof elementsSelectable !== 'undefined') { - setElementsSelectable(elementsSelectable); - } - }, [elementsSelectable]); - - useEffect(() => { - if (typeof minZoom !== 'undefined') { - setMinZoom(minZoom); - } - }, [minZoom]); - - useEffect(() => { - if (typeof maxZoom !== 'undefined') { - setMaxZoom(maxZoom); - } - }, [maxZoom]); - - useEffect(() => { - if (typeof translateExtent !== 'undefined') { - setTranslateExtent(translateExtent); - } - }, [translateExtent]); - - useEffect(() => { - if (typeof nodeExtent !== 'undefined') { - setNodeExtent(nodeExtent); - } - }, [nodeExtent]); - - useEffect(() => { - if (typeof connectionMode !== 'undefined') { - setConnectionMode(connectionMode); - } - }, [connectionMode]); - - useEffect(() => { - if (typeof onNodesChange !== 'undefined') { - setOnNodesChange(onNodesChange); - } - }, [onNodesChange]); - - useEffect(() => { - if (typeof onEdgesChange !== 'undefined') { - setOnEdgesChange(onEdgesChange); - } - }, [onEdgesChange]); - return ( ); diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 6dbde07d..a808fb79 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -2,11 +2,11 @@ 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'; +import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types'; interface NodeRendererProps { nodeTypes: NodeTypesType; selectNodesOnDrag: boolean; - onElementClick?: (event: MouseEvent, element: Node | Edge) => void; + onNodeClick?: (event: MouseEvent, element: Node) => void; onNodeDoubleClick?: (event: MouseEvent, element: Node) => void; onNodeMouseEnter?: (event: MouseEvent, node: Node) => void; onNodeMouseMove?: (event: MouseEvent, node: Node) => void; @@ -15,9 +15,8 @@ interface NodeRendererProps { onNodeDragStart?: (event: MouseEvent, node: Node) => void; onNodeDrag?: (event: MouseEvent, node: Node) => void; onNodeDragStop?: (event: MouseEvent, node: Node) => void; - snapToGrid: boolean; - snapGrid: [number, number]; onlyRenderVisibleElements: boolean; + nodes: Node[]; } const selector = (s: ReactFlowState) => ({ @@ -25,15 +24,21 @@ const selector = (s: ReactFlowState) => ({ nodesDraggable: s.nodesDraggable, nodesConnectable: s.nodesConnectable, elementsSelectable: s.elementsSelectable, - nodes: s.nodes, updateNodeDimensions: s.updateNodeDimensions, + snapGrid: s.snapGrid, + snapToGrid: s.snapToGrid, }); const NodeRenderer = (props: NodeRendererProps) => { - const { transform, nodesDraggable, nodesConnectable, elementsSelectable, nodes, updateNodeDimensions } = useStore( - selector, - shallow - ); + const { + transform, + nodesDraggable, + nodesConnectable, + elementsSelectable, + updateNodeDimensions, + snapGrid, + snapToGrid, + } = useStore(selector, shallow); // const visibleNodes = props.onlyRenderVisibleElements // ? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true) @@ -63,7 +68,7 @@ const NodeRenderer = (props: NodeRendererProps) => { return (
- {nodes.map((node) => { + {props.nodes.map((node) => { const nodeType = node.type || 'default'; const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; @@ -74,6 +79,11 @@ const NodeRenderer = (props: NodeRendererProps) => { const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')); const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined')); const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined')); + const isInitialized = + node.width !== null && + node.height !== null && + typeof node.width !== 'undefined' && + typeof node.height !== 'undefined'; return ( { xPos={node.position.x} yPos={node.position.y} isDragging={node.isDragging} - isInitialized={node.width !== null && node.height !== null} - snapGrid={props.snapGrid} - snapToGrid={props.snapToGrid} + isInitialized={isInitialized} + snapGrid={snapGrid} + snapToGrid={snapToGrid} selectNodesOnDrag={props.selectNodesOnDrag} - onClick={props.onElementClick} + onClick={props.onNodeClick} onMouseEnter={props.onNodeMouseEnter} onMouseMove={props.onNodeMouseMove} onMouseLeave={props.onNodeMouseLeave} @@ -103,7 +113,7 @@ const NodeRenderer = (props: NodeRendererProps) => { onNodeDrag={props.onNodeDrag} onNodeDragStop={props.onNodeDragStop} scale={transform[2]} - selected={!!node.selected} + isSelected={!!node.isSelected} isDraggable={isDraggable} isSelectable={isSelectable} isConnectable={isConnectable} diff --git a/src/container/ReactFlow/Wrapper.tsx b/src/container/ReactFlow/Wrapper.tsx index 67f22cf6..43aac450 100644 --- a/src/container/ReactFlow/Wrapper.tsx +++ b/src/container/ReactFlow/Wrapper.tsx @@ -1,24 +1,21 @@ import React, { FC } from 'react'; -import { Provider, createStore } from '../../store'; -// import { ReactFlowState } from '../../types'; - -// const reactFlowVersionSelector = (s: ReactFlowState) => s.reactFlowVersion; +import { Provider, createStore, useStoreApi } from '../../store'; const Wrapper: FC = ({ children }) => { - // let isWrapped = useRef(true); + let isWrapped = true; - // try { - // useStoreApi(); - // } catch { - // isWrapped.current = false; - // } + try { + useStoreApi(); + } catch (e) { + isWrapped = false; + } - // if (isWrapped) { - // // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode - // // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 - // return <>{children}; - // } + if (isWrapped) { + // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 + return <>{children}; + } return {children}; }; diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index b6bcc6a3..3f7b4106 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -9,7 +9,7 @@ import React, { import cc from 'classcat'; import GraphView from '../GraphView'; -import ElementUpdater from '../../components/ElementUpdater'; +import StoreUpdater from '../../components/StoreUpdater'; import DefaultNode from '../../components/Nodes/DefaultNode'; import InputNode from '../../components/Nodes/InputNode'; import OutputNode from '../../components/Nodes/OutputNode'; @@ -38,7 +38,8 @@ import { PanOnScrollMode, OnEdgeUpdateFunc, NodeExtent, - ElementChange, + NodeChange, + EdgeChange, } from '../../types'; import '../../style.css'; @@ -60,9 +61,10 @@ const defaultEdgeTypes = { export interface ReactFlowProps extends Omit, 'onLoad'> { nodes: Node[]; edges: Edge[]; - onNodesChange?: (nodeChanges: ElementChange[]) => void; - onEdgesChange?: (edgeChanges: ElementChange[]) => void; - onElementClick?: (event: ReactMouseEvent, element: Node | Edge) => void; + onNodesChange?: (nodeChanges: NodeChange[]) => void; + onEdgesChange?: (edgeChanges: EdgeChange[]) => void; + onNodeClick?: (event: React.MouseEvent, node: Node) => void; + onEdgeClick?: (event: React.MouseEvent, node: Edge) => void; onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void; onNodeMouseEnter?: (event: ReactMouseEvent, node: Node) => void; onNodeMouseMove?: (event: ReactMouseEvent, node: Node) => void; @@ -129,8 +131,6 @@ export interface ReactFlowProps extends Omit, 'on onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge) => void; onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; edgeUpdaterRadius?: number; - nodeTypesId?: string; - edgeTypesId?: string; } export type ReactFlowRefType = HTMLDivElement; @@ -146,7 +146,8 @@ const ReactFlow = forwardRef( className, nodeTypes = defaultNodeTypes, edgeTypes = defaultEdgeTypes, - onElementClick, + onNodeClick, + onEdgeClick, onLoad, onMove, onMoveStart, @@ -212,27 +213,28 @@ const ReactFlow = forwardRef( onEdgeUpdateStart, onEdgeUpdateEnd, edgeUpdaterRadius = 10, - nodeTypesId = '1', - edgeTypesId = '1', onNodesChange, onEdgesChange, ...rest }, ref ) => { - const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), [nodeTypesId]); - const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), [edgeTypesId]); + const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), [nodeTypes]); + const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), [edgeTypes]); const reactFlowClasses = cc(['react-flow', className]); return (
( onNodeDragStop={onNodeDragStop} nodeTypes={nodeTypesParsed} edgeTypes={edgeTypesParsed} - connectionMode={connectionMode} connectionLineType={connectionLineType} connectionLineStyle={connectionLineStyle} connectionLineComponent={connectionLineComponent} @@ -251,24 +252,11 @@ const ReactFlow = forwardRef( deleteKeyCode={deleteKeyCode} multiSelectionKeyCode={multiSelectionKeyCode} zoomActivationKeyCode={zoomActivationKeyCode} - onConnect={onConnect} - onConnectStart={onConnectStart} - onConnectStop={onConnectStop} - onConnectEnd={onConnectEnd} - snapToGrid={snapToGrid} - snapGrid={snapGrid} onlyRenderVisibleElements={onlyRenderVisibleElements} - nodesDraggable={nodesDraggable} - nodesConnectable={nodesConnectable} - elementsSelectable={elementsSelectable} selectNodesOnDrag={selectNodesOnDrag} - minZoom={minZoom} - maxZoom={maxZoom} defaultZoom={defaultZoom} defaultPosition={defaultPosition} - translateExtent={translateExtent} preventScrolling={preventScrolling} - nodeExtent={nodeExtent} arrowHeadColor={arrowHeadColor} markerEndId={markerEndId} zoomOnScroll={zoomOnScroll} @@ -294,10 +282,28 @@ const ReactFlow = forwardRef( onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateEnd={onEdgeUpdateEnd} edgeUpdaterRadius={edgeUpdaterRadius} + /> + - + {onSelectionChange && } {children} diff --git a/src/container/ZoomPane/index.tsx b/src/container/ZoomPane/index.tsx index 3993c017..c5e25404 100644 --- a/src/container/ZoomPane/index.tsx +++ b/src/container/ZoomPane/index.tsx @@ -7,7 +7,7 @@ import { clamp } from '../../utils'; import useKeyPress from '../../hooks/useKeyPress'; import useResizeHandler from '../../hooks/useResizeHandler'; import { useStore, useStoreApi } from '../../store'; -import { FlowTransform, TranslateExtent, PanOnScrollMode, KeyCode, ReactFlowState } from '../../types'; +import { FlowTransform, PanOnScrollMode, KeyCode, ReactFlowState } from '../../types'; interface ZoomPaneProps { selectionKeyPressed: boolean; @@ -21,7 +21,6 @@ interface ZoomPaneProps { paneMoveable?: boolean; defaultPosition?: [number, number]; defaultZoom?: number; - translateExtent?: TranslateExtent; onMove?: (flowTransform?: FlowTransform) => void; onMoveStart?: (flowTransform?: FlowTransform) => void; onMoveEnd?: (flowTransform?: FlowTransform) => void; @@ -67,7 +66,6 @@ const ZoomPane = ({ paneMoveable = true, defaultPosition = [0, 0], defaultZoom = 1, - translateExtent, zoomActivationKeyCode, preventScrolling = true, children, @@ -82,14 +80,13 @@ const ZoomPane = ({ useEffect(() => { if (zoomPane.current) { - const state = store.getState(); - const currentTranslateExtent = typeof translateExtent !== 'undefined' ? translateExtent : state.translateExtent; - const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(currentTranslateExtent); + const { minZoom, maxZoom, translateExtent } = store.getState(); + const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent); const selection = select(zoomPane.current as Element).call(d3ZoomInstance); - const clampedX = clamp(defaultPosition[0], currentTranslateExtent[0][0], currentTranslateExtent[1][0]); - const clampedY = clamp(defaultPosition[1], currentTranslateExtent[0][1], currentTranslateExtent[1][1]); - const clampedZoom = clamp(defaultZoom, state.minZoom, state.maxZoom); + const clampedX = clamp(defaultPosition[0], translateExtent[0][0], translateExtent[1][0]); + const clampedY = clamp(defaultPosition[1], translateExtent[0][1], translateExtent[1][1]); + const clampedZoom = clamp(defaultZoom, minZoom, maxZoom); const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom); d3ZoomInstance.transform(selection, updatedTransform); diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index 30367672..87b8a360 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -4,7 +4,7 @@ import shallow from 'zustand/shallow'; import { useStore, useStoreApi } from '../store'; import useKeyPress from './useKeyPress'; import { getConnectedEdges } from '../utils/graph'; -import { KeyCode, ReactFlowState } from '../types'; +import { EdgeChange, KeyCode, NodeChange, ReactFlowState } from '../types'; interface HookParams { deleteKeyCode: KeyCode; @@ -29,14 +29,17 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { useEffect(() => { const { nodes, edges } = store.getState(); - const selectedNodes = nodes.filter((n) => n.selected); - const selectedEdges = edges.filter((e) => e.selected); + const selectedNodes = nodes.filter((n) => n.isSelected); + const selectedEdges = edges.filter((e) => e.isSelected); if (deleteKeyPressed && (selectedNodes || selectedEdges)) { const connectedEdges = getConnectedEdges(selectedNodes, edges); - const nodeChanges = selectedNodes.map((n) => ({ id: n.id, delete: true })); - const edgeChanges = [...selectedEdges, ...connectedEdges].map((e) => ({ id: e.id, delete: true })); + const nodeChanges: NodeChange[] = selectedNodes.map((n) => ({ id: n.id, type: 'remove' })); + const edgeChanges: EdgeChange[] = [...selectedEdges, ...connectedEdges].map((e) => ({ + id: e.id, + type: 'remove', + })); onNodesChange?.(nodeChanges); onEdgesChange?.(edgeChanges); diff --git a/src/store/index.ts b/src/store/index.ts index f0b61b65..5976bb3b 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -7,7 +7,6 @@ import { ConnectionMode, Node, Edge, - ElementChange, NodeDimensionUpdate, NodeDiffUpdate, XYPosition, @@ -23,28 +22,24 @@ import { OnConnectEndFunc, SetConnectionId, SnapGrid, - OnElementsChange, + NodeChange, + OnNodesChange, + OnEdgesChange, + EdgeChange, + NodePositionChange, } from '../types'; -import { - parseNode, - parseEdge, - isNode, - isEdge, - getRectOfNodes, - getNodesInside, - getConnectedEdges, -} from '../utils/graph'; -import { extendEdgeWithSourceAndTarget } from '../container/EdgeRenderer/utils'; +import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph'; import { getHandleBounds } from '../components/Nodes/utils'; const { Provider, useStore, useStoreApi } = createContext(); -const unselectElements = (elements: Elements) => +const unselectElements = (elements: Elements): NodeChange[] | EdgeChange[] => elements - .filter((e) => e.selected) + .filter((e) => e.isSelected) .map((e) => ({ id: e.id, - change: { selected: false }, + type: 'select', + isSelected: false, })); const createStore = () => @@ -104,53 +99,37 @@ const createStore = () => reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-', setNodes: (propNodes: Node[]) => { - const { nodes, edges, nodeExtent } = get(); + const { nodes } = get(); const nextNodes = propNodes.map((propNode: Node) => { const storeNode = nodes.find((node) => node.id === propNode.id); if (storeNode) { if (typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type) { - const updatedNode: Node = { - ...storeNode, - ...propNode, - }; // we reset the elements dimensions here in order to force a re-calculation of the bounds. // When the type of a node changes it is possible that the number or positions of handles changes too. - updatedNode.width = null; - return updatedNode; + return { + ...propNode, + width: null, + height: null, + }; } } - return parseNode(propNode, nodeExtent); + return propNode; }); - const updatedEdges = edges.map((edge) => extendEdgeWithSourceAndTarget(edge, nextNodes)); - set({ nodes: nextNodes, - edges: updatedEdges, }); }, - setEdges: (propEdges: Edge[]) => { - const { edges, nodes } = get(); - - const nextEdges = propEdges.map((propEdge: Edge) => { - const storeEdge = edges.find((se) => se.id === propEdge.id); - - if (storeEdge) { - return parseEdge(propEdge); - } else { - return extendEdgeWithSourceAndTarget(parseEdge(propEdge), nodes); - } - }); - - set({ edges: nextEdges }); + setEdges: (edges: Edge[]) => { + set({ edges }); }, updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { const { onNodesChange, nodes, transform } = get(); - const initialChanges: ElementChange[] = []; - const nodesToChange: ElementChange[] = nodes.reduce((res, node) => { + const initialChanges: NodeChange[] = []; + const nodesToChange: NodeChange[] = nodes.reduce((res, node) => { const update = updates.find((u) => u.id === node.id); if (update) { const dimensions = getDimensions(update.nodeElement); @@ -163,12 +142,10 @@ const createStore = () => const handleBounds = getHandleBounds(update.nodeElement, transform[2]); const change = { id: node.id, - change: { - ...dimensions, - handleBounds, - }, - } as ElementChange; - + type: 'dimensions', + dimensions, + handleBounds, + } as NodeChange; res.push(change); } } @@ -176,28 +153,37 @@ const createStore = () => return res; }, initialChanges); - if (onNodesChange) { - onNodesChange(nodesToChange); - } + onNodesChange?.(nodesToChange); }, - updateNodePosDiff: ({ id, diff, isDragging }: NodeDiffUpdate) => { - const { onNodesChange, nodes } = get(); + updateNodePosition: ({ id, diff, isDragging }: NodeDiffUpdate) => { + const { onNodesChange, nodes, nodeExtent } = get(); - if (onNodesChange && diff) { - const matchingNodes = nodes.filter((n) => n.id === id || n.selected); + if (onNodesChange) { + const matchingNodes = nodes.filter((n) => n.id === id || n.isSelected); 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, - }, - }, - })) + matchingNodes.map((n) => { + const change: NodePositionChange = { + id: n.id, + type: 'position', + isDragging: !!isDragging, + }; + + if (diff) { + change.position = nodeExtent + ? clampPosition( + { + x: n.position.x + diff.x, + y: n.position.y + diff.y, + }, + nodeExtent + ) + : { x: n.position.x + diff.x, y: n.position.y + diff.y }; + } + + return change; + }) ); } } @@ -230,15 +216,11 @@ const createStore = () => }; const selectedNodes = getNodesInside(nodes, nextUserSelectRect, transform, false, true); - const selectedEdges = getConnectedEdges(selectedNodes, edges); + const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id); + const selectedNodeIds = selectedNodes.map((n) => n.id); - 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 } }))); - } + onNodesChange?.(nodes.map((n) => ({ id: n.id, type: 'select', isSelected: selectedNodeIds.includes(n.id) }))); + onEdgesChange?.(edges.map((e) => ({ id: e.id, type: 'select', isSelected: selectedEdgeIds.includes(e.id) }))); set({ userSelectionRect: nextUserSelectRect, @@ -246,7 +228,7 @@ const createStore = () => }, unsetUserSelection: () => { const { userSelectionRect, nodes } = get(); - const selectedNodes = nodes.filter((node) => node.selected); + const selectedNodes = nodes.filter((node) => node.isSelected); const stateUpdate = { selectionActive: false, @@ -274,21 +256,32 @@ const createStore = () => let changedEdges; if (multiSelectionActive) { - changedNodes = selectedElementsArr.filter(isNode).map((node) => ({ id: node.id, change: { selected: true } })); - changedEdges = selectedElementsArr.filter(isEdge).map((edge) => ({ id: edge.id, change: { selected: true } })); + changedNodes = selectedElementsArr + .filter(isNode) + .map((node) => ({ id: node.id, type: 'select', isSelected: true })); + changedEdges = selectedElementsArr + .filter(isEdge) + .map((edge) => ({ id: edge.id, type: 'select', isSelected: true })); } else { changedNodes = nodes.map((node) => ({ id: node.id, - change: { selected: selectedElementsArr.some((e) => e.id === node.id) }, + type: 'select', + isSelected: selectedElementsArr.some((e) => e.id === node.id), })); changedEdges = edges.map((edge) => ({ id: edge.id, - change: { selected: selectedElementsArr.some((e) => e.id === edge.id) }, + type: 'select', + isSelected: selectedElementsArr.some((e) => e.id === edge.id), })); } - onNodesChange?.(changedNodes); - onEdgesChange?.(changedEdges); + if (changedNodes.length) { + onNodesChange?.(changedNodes as NodeChange[]); + } + + if (changedEdges.length) { + onEdgesChange?.(changedEdges as EdgeChange[]); + } }, initD3Zoom: ({ d3Zoom, d3Selection, d3ZoomHandler, transform }: InitD3ZoomPayload) => set({ @@ -319,8 +312,15 @@ const createStore = () => resetSelectedElements: () => { const { nodes, edges, onNodesChange, onEdgesChange } = get(); - onNodesChange?.(unselectElements(nodes)); - onEdgesChange?.(unselectElements(edges)); + const nodesToUnselect = unselectElements(nodes) as NodeChange[]; + const edgesToUnselect = unselectElements(edges) as EdgeChange[]; + + if (nodesToUnselect.length) { + onNodesChange?.(nodesToUnselect); + } + if (edgesToUnselect.length) { + onEdgesChange?.(edgesToUnselect); + } }, setNodeExtent: (nodeExtent: NodeExtent) => set({ @@ -354,8 +354,8 @@ const createStore = () => 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 }), + setOnNodesChange: (onNodesChange: OnNodesChange) => set({ onNodesChange }), + setOnEdgesChange: (onEdgesChange: OnEdgesChange) => set({ onEdgesChange }), })); export { Provider, useStore, createStore, useStoreApi }; diff --git a/src/types/index.ts b/src/types/index.ts index b011b1b5..32f98f6e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -9,11 +9,32 @@ export type Elements = Array>; export type Transform = [number, number, number]; -export type ElementChange = { +export type NodeDimensionChange = { id: string; - change?: any; - delete?: boolean; + type: 'dimensions'; + dimensions: Dimensions; + handleBounds?: NodeHandleBounds; }; +export type NodePositionChange = { + id: string; + type: 'position'; + position?: XYPosition; + isDragging?: boolean; +}; +export type NodeSelectionChange = { + id: string; + type: 'select'; + isSelected: boolean; +}; +export type NodeRemoveChange = { + id: string; + type: 'remove'; +}; +export type NodeChange = NodeDimensionChange | NodePositionChange | NodeSelectionChange | NodeRemoveChange; + +export type EdgeSelectionChange = NodeSelectionChange; +export type EdgeRemoveChange = NodeRemoveChange; +export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange; export enum Position { Left = 'left', @@ -41,26 +62,30 @@ export interface Box extends XYPosition { export type SnapGrid = [number, number]; +export type NodeHandleBounds = { + source: HandleElement[] | null; + target: HandleElement[] | null; +}; + export interface Node { id: ElementId; position: XYPosition; type?: string; - __rf?: any; data?: T; style?: CSSProperties; className?: string; targetPosition?: Position; sourcePosition?: Position; isHidden?: boolean; + isSelected?: boolean; + isDragging?: boolean; draggable?: boolean; selectable?: boolean; connectable?: boolean; dragHandle?: string; - isDragging?: boolean; width?: number | null; height?: number | null; - handleBounds?: any; - selected?: boolean; + handleBounds?: NodeHandleBounds; } export enum ArrowHeadType { @@ -89,7 +114,7 @@ export interface Edge { className?: string; sourceNode?: Node; targetNode?: Node; - selected?: boolean; + isSelected?: boolean; } export enum BackgroundVariant { @@ -116,7 +141,7 @@ export interface WrapEdgeProps { data?: T; onClick?: (event: React.MouseEvent, edge: Edge) => void; onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void; - selected: boolean; + isSelected: boolean; animated?: boolean; label?: string | ReactNode; labelStyle?: CSSProperties; @@ -158,7 +183,7 @@ export interface EdgeProps { sourceY: number; targetX: number; targetY: number; - selected?: boolean; + isSelected?: boolean; animated?: boolean; sourcePosition: Position; targetPosition: Position; @@ -194,7 +219,7 @@ export interface NodeProps { id: ElementId; type: string; data: T; - selected: boolean; + isSelected: boolean; isConnectable: boolean; xPos?: number; yPos?: number; @@ -208,7 +233,7 @@ export interface NodeComponentProps { id: ElementId; type: string; data: T; - selected?: boolean; + isSelected?: boolean; isConnectable: boolean; transform?: Transform; xPos?: number; @@ -233,7 +258,7 @@ export interface WrapNodeProps { id: ElementId; type: string; data: T; - selected: boolean; + isSelected: boolean; scale: number; xPos: number; yPos: number; @@ -271,7 +296,8 @@ export type FitViewParams = { }; export type FlowExportObject = { - elements: Elements; + nodes: Node[]; + edges: Edge[]; position: [number, number]; zoom: number; }; @@ -412,7 +438,8 @@ export type InitD3ZoomPayload = { transform: Transform; }; -export type OnElementsChange = (nodes: ElementChange[]) => void; +export type OnNodesChange = (nodes: NodeChange[]) => void; +export type OnEdgesChange = (nodes: EdgeChange[]) => void; export interface ReactFlowState { width: number; @@ -421,8 +448,8 @@ export interface ReactFlowState { nodes: Node[]; edges: Edge[]; selectedNodesBbox: Rect; - onNodesChange: OnElementsChange | null; - onEdgesChange: OnElementsChange | null; + onNodesChange: OnNodesChange | null; + onEdgesChange: OnEdgesChange | null; d3Zoom: ZoomBehavior | null; d3Selection: D3Selection | null; @@ -455,9 +482,9 @@ export interface ReactFlowState { reactFlowVersion: string; setNodes: (nodes: Node[]) => void; - setEdges: (edges: Edge[]) => void; + setEdges: (edges: Edge[], nodes: Node[]) => void; updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; - updateNodePosDiff: (update: NodeDiffUpdate) => void; + updateNodePosition: (update: NodeDiffUpdate) => void; setUserSelection: (mousePos: XYPosition) => void; updateUserSelection: (mousePos: XYPosition) => void; unsetUserSelection: () => void; @@ -485,8 +512,8 @@ export interface ReactFlowState { setElementsSelectable: (elementsSelectable: boolean) => void; setMultiSelectionActive: (multiSelectionActive: boolean) => void; setConnectionMode: (connectionMode: ConnectionMode) => void; - setOnNodesChange: (onNodesChange: OnElementsChange) => void; - setOnEdgesChange: (onEdgesChange: OnElementsChange) => void; + setOnNodesChange: (onNodesChange: OnNodesChange) => void; + setOnEdgesChange: (onEdgesChange: OnEdgesChange) => void; onConnect?: OnConnectFunc; onConnectStart?: OnConnectStartFunc; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 8e84b139..109406c5 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -1,7 +1,6 @@ import { GetState } from 'zustand'; -import { clampPosition, clamp } from '../utils'; -import { ReactFlowState } from '../types'; +import { clamp } from '../utils'; import { ElementId, @@ -14,8 +13,9 @@ import { Box, Connection, FlowExportObject, - NodeExtent, - ElementChange, + EdgeChange, + NodeChange, + ReactFlowState, } from '../types'; export const isEdge = (element: Node | Connection | Edge): element is Edge => @@ -147,26 +147,6 @@ export const onLoadProject = (getState: GetState) => { }; }; -export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => { - if (!node.type) { - node.type = 'default'; - } - - if (nodeExtent) { - node.position = clampPosition(node.position, nodeExtent); - } - - return node; -}; - -export const parseEdge = (edge: Edge): Edge => { - if (!edge.type) { - edge.type = 'default'; - } - - return edge; -}; - const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({ x: Math.min(box1.x, box2.x), y: Math.min(box1.y, box2.y), @@ -269,7 +249,8 @@ export const onLoadToObject = (getState: GetState) => { const { nodes = [], edges = [], transform } = getState(); return { - elements: parseElements(nodes, edges), + nodes: nodes.map((n) => ({ ...n })), + edges: edges.map((e) => ({ ...e })), position: [transform[0], transform[1]], zoom: transform[2], }; @@ -296,30 +277,51 @@ export const getTransformForBounds = ( return [x, y, clampedZoom]; }; -function applyChanges(changes: ElementChange[], elements: any[]): any[] { +function applyChanges(changes: NodeChange[] | EdgeChange[], elements: any[]): any[] { const initElements: any[] = []; - return elements.reduce((res: any[], node: any) => { - const hasChange = changes.find((c) => c.id === node.id); + return elements.reduce((res: any[], item: any) => { + const currentChange = changes.find((c) => c.id === item.id); - if (hasChange?.delete) { - return res; - } - - if (hasChange?.change) { - res.push({ ...node, ...hasChange.change }); - } else { - res.push(node); + if (currentChange) { + switch (currentChange.type) { + case 'dimensions': { + res.push({ ...item, ...currentChange.dimensions, handleBounds: currentChange.handleBounds }); + return res; + } + case 'select': { + res.push({ ...item, isSelected: currentChange.isSelected }); + return res; + } + case 'position': { + const updateItem = { ...item }; + + if (typeof currentChange.position !== 'undefined') { + updateItem.position = currentChange.position; + } + + if (typeof currentChange.isDragging !== 'undefined') { + updateItem.isDragging = currentChange.isDragging; + } + + res.push(updateItem); + return res; + } + case 'remove': { + return res; + } + } } + res.push(item); return res; }, initElements); } -export function applyNodeChanges(changes: ElementChange[], nodes: Node[]): Node[] { +export function applyNodeChanges(changes: NodeChange[], nodes: Node[]): Node[] { return applyChanges(changes, nodes) as Node[]; } -export function applyEdgeChanges(changes: ElementChange[], edges: Edge[]): Edge[] { +export function applyEdgeChanges(changes: EdgeChange[], edges: Edge[]): Edge[] { return applyChanges(changes, edges) as Edge[]; } From 1525af39cfbed907cbd5268846b3c0f919881631 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 19 Oct 2021 14:57:34 +0200 Subject: [PATCH 013/263] refactor(elements): render only visible elements --- cypress/integration/flow/graph-utils.spec.js | 64 +------- example/src/Basic/index.tsx | 4 +- example/src/CustomNode/ColorSelectorNode.tsx | 25 ++++ example/src/CustomNode/index.tsx | 147 +++++++++++++++++++ example/src/Stress/index.tsx | 29 ++-- example/src/Stress/utils.ts | 2 +- example/src/index.tsx | 5 + src/components/Handle/handler.ts | 8 +- src/components/Handle/index.tsx | 8 +- src/components/Nodes/wrapNode.tsx | 14 +- src/components/SelectionListener/index.tsx | 18 +-- src/container/EdgeRenderer/index.tsx | 60 +++++--- src/container/EdgeRenderer/utils.ts | 20 ++- src/container/FlowRenderer/index.tsx | 3 - src/container/GraphView/index.tsx | 12 +- src/container/NodeRenderer/index.tsx | 19 ++- src/container/ReactFlow/index.tsx | 6 +- src/index.ts | 1 - src/store/index.ts | 72 ++++----- src/types/index.ts | 8 +- src/utils/graph.ts | 68 ++++----- 21 files changed, 376 insertions(+), 217 deletions(-) create mode 100644 example/src/CustomNode/ColorSelectorNode.tsx create mode 100644 example/src/CustomNode/index.tsx diff --git a/cypress/integration/flow/graph-utils.spec.js b/cypress/integration/flow/graph-utils.spec.js index e582231b..e6df1455 100644 --- a/cypress/integration/flow/graph-utils.spec.js +++ b/cypress/integration/flow/graph-utils.spec.js @@ -1,4 +1,4 @@ -import { isNode, isEdge, getOutgoers, getIncomers, removeElements, addEdge } from '../../../dist/ReactFlow.js'; +import { isNode, isEdge, getOutgoers, getIncomers, addEdge } from '../../../dist/ReactFlow.js'; const nodes = [ { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, @@ -69,66 +69,4 @@ describe('Graph Utils Testing', () => { } }); }); - - describe('tests removeElements function', () => { - it('removes a node', () => { - const nextElements = removeElements([nodes[0]], elements); - - const nextNodes = nextElements.filter((e) => isNode(e)); - const nextEdges = nextElements.filter((e) => isEdge(e)); - - expect(nextNodes.length).to.be.equal(nodes.length - 1); - expect(nextEdges.length).to.be.equal(edges.length - 2); - }); - - it('removes multiple nodes', () => { - const elementsToRemove = [nodes[0], nodes[1]]; - const nextElements = removeElements(elementsToRemove, elements); - const nextNodes = nextElements.filter((e) => isNode(e)); - const nextEdges = nextElements.filter((e) => isEdge(e)); - - expect(nextNodes.length).to.be.equal(nodes.length - 2); - expect(nextEdges.length).to.be.equal(0); - }); - - it('removes no node', () => { - const nextElementsNoRemove = removeElements([], elements); - expect(nextElementsNoRemove.length).to.be.equal(elements.length); - }); - - it('tries to removes node that does not exist', () => { - const nextElementsNoRemove = removeElements([{ id: 'id-that-does-not-exist' }], elements); - expect(nextElementsNoRemove.length).to.be.equal(elements.length); - }); - - it('removes an edge', () => { - const nextElements = removeElements([edges[0]], elements); - - const nextNodes = nextElements.filter((e) => isNode(e)); - const nextEdges = nextElements.filter((e) => isEdge(e)); - - expect(nextNodes.length).to.be.equal(nodes.length); - expect(nextEdges.length).to.be.equal(edges.length - 1); - }); - - it('removes multiple edges', () => { - const nextElements = removeElements([edges[0], edges[1]], elements); - - const nextNodes = nextElements.filter((e) => isNode(e)); - const nextEdges = nextElements.filter((e) => isEdge(e)); - - expect(nextNodes.length).to.be.equal(nodes.length); - expect(nextEdges.length).to.be.equal(edges.length - 2); - }); - - it('removes node and edge', () => { - const nextElements = removeElements([nodes[0], edges[0]], elements); - - const nextNodes = nextElements.filter((e) => isNode(e)); - const nextEdges = nextElements.filter((e) => isEdge(e)); - - expect(nextNodes.length).to.be.equal(nodes.length - 1); - expect(nextEdges.length).to.be.equal(edges.length - 2); - }); - }); }); diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index f47d372a..e13d4a7a 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -36,8 +36,8 @@ const BasicFlow = () => { const [nodes, setNodes] = useState(initialNodes); const [edges, setEdges] = useState(initialEdges); - const onConnect = useCallback((params: Edge | Connection, nds: Node[]) => { - setEdges((eds) => addEdge(params, nds, eds)); + const onConnect = useCallback((params: Edge | Connection) => { + setEdges((eds) => addEdge(params, eds)); }, []); const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []); diff --git a/example/src/CustomNode/ColorSelectorNode.tsx b/example/src/CustomNode/ColorSelectorNode.tsx new file mode 100644 index 00000000..7ad20340 --- /dev/null +++ b/example/src/CustomNode/ColorSelectorNode.tsx @@ -0,0 +1,25 @@ +import React, { memo, FC, CSSProperties } from 'react'; + +import { Handle, Position, NodeProps, Connection, Edge } from 'react-flow-renderer'; + +const targetHandleStyle: CSSProperties = { background: '#555' }; +const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 }; +const sourceHandleStyleB: CSSProperties = { ...targetHandleStyle, bottom: 10, top: 'auto' }; + +const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params); + +const ColorSelectorNode: FC = ({ data, isConnectable }) => { + return ( + <> + +
+ Custom Color Picker Node: {data.color} +
+ + + + + ); +}; + +export default memo(ColorSelectorNode); diff --git a/example/src/CustomNode/index.tsx b/example/src/CustomNode/index.tsx new file mode 100644 index 00000000..5c786f58 --- /dev/null +++ b/example/src/CustomNode/index.tsx @@ -0,0 +1,147 @@ +import { useState, useEffect, MouseEvent, useCallback } from 'react'; +import { ChangeEvent } from 'react'; + +import ReactFlow, { + addEdge, + MiniMap, + Controls, + Node, + OnLoadParams, + Position, + SnapGrid, + Connection, + Edge, + NodeChange, + applyNodeChanges, + applyEdgeChanges, + EdgeChange, +} from 'react-flow-renderer'; + +import ColorSelectorNode from './ColorSelectorNode'; + +const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance); +const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); +const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); + +const initBgColor = '#1A192B'; + +const connectionLineStyle = { stroke: '#fff' }; +const snapGrid: SnapGrid = [16, 16]; +const nodeTypes = { + selectorNode: ColorSelectorNode, +}; + +const CustomNodeFlow = () => { + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + const [bgColor, setBgColor] = useState(initBgColor); + + useEffect(() => { + const onChange = (event: ChangeEvent) => { + setNodes((nds) => + nds.map((node) => { + if (node.id !== '2') { + return node; + } + + const color = event.target.value; + + setBgColor(color); + + return { + ...node, + data: { + ...node.data, + color, + }, + }; + }) + ); + }; + + setNodes([ + { + id: '1', + type: 'input', + data: { label: 'An input node' }, + position: { x: 0, y: 50 }, + sourcePosition: Position.Right, + }, + { + id: '2', + type: 'selectorNode', + data: { onChange: onChange, color: initBgColor }, + style: { border: '1px solid #777', padding: 10 }, + position: { x: 250, y: 50 }, + }, + { + id: '3', + type: 'output', + data: { label: 'Output A' }, + position: { x: 550, y: 25 }, + targetPosition: Position.Left, + }, + { + id: '4', + type: 'output', + data: { label: 'Output B' }, + position: { x: 550, y: 100 }, + targetPosition: Position.Left, + }, + ]); + + setEdges([ + { id: 'e1-2', source: '1', target: '2', animated: true, style: { stroke: '#fff' } }, + { id: 'e2a-3', source: '2', sourceHandle: 'a', target: '3', animated: true, style: { stroke: '#fff' } }, + { id: 'e2b-4', source: '2', sourceHandle: 'b', target: '4', animated: true, style: { stroke: '#fff' } }, + ]); + }, []); + + 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)); + }, []); + + return ( + + { + if (n.type === 'input') return '#0041d0'; + if (n.type === 'selectorNode') return bgColor; + if (n.type === 'output') return '#ff0072'; + + return '#eee'; + }} + nodeColor={(n: Node): string => { + if (n.type === 'selectorNode') return bgColor; + + return '#fff'; + }} + /> + + + ); +}; + +export default CustomNodeFlow; diff --git a/example/src/Stress/index.tsx b/example/src/Stress/index.tsx index f3cf1df2..e562ed50 100644 --- a/example/src/Stress/index.tsx +++ b/example/src/Stress/index.tsx @@ -8,25 +8,27 @@ import ReactFlow, { Node, NodeChange, applyNodeChanges, + Connection, + addEdge, } from 'react-flow-renderer'; -import { getElements } from './utils'; +import { getNodesAndEdges } from './utils'; const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 4 }; const onLoad = (reactFlowInstance: OnLoadParams) => { reactFlowInstance.fitView(); - console.log(reactFlowInstance.getElements()); + console.log(reactFlowInstance.getNodes()); }; -const initialElements = getElements(30, 30); +const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(30, 30); const StressFlow = () => { - const [nodes, setNodes] = useState(initialElements.nodes); - const [edges, setEdges] = useState(initialElements.edges); - // const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - // const onConnect = (params: Connection | Edge, nds: Node[]) => setElements((els) => addEdge(params, els)); - + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState(initialEdges); + const onConnect = useCallback((params: Edge | Connection) => { + setEdges((eds) => addEdge(params, eds)); + }, []); const updatePos = () => { setNodes((nds) => { return nds.map((n) => { @@ -43,7 +45,7 @@ const StressFlow = () => { const updateElements = () => { const grid = Math.ceil(Math.random() * 10); - const initialElements = getElements(grid, grid); + const initialElements = getNodesAndEdges(grid, grid); setNodes(initialElements.nodes); setEdges(initialElements.edges); }; @@ -53,7 +55,14 @@ const StressFlow = () => { }, []); return ( - + diff --git a/example/src/Stress/utils.ts b/example/src/Stress/utils.ts index 36cf2949..c5c6e307 100644 --- a/example/src/Stress/utils.ts +++ b/example/src/Stress/utils.ts @@ -5,7 +5,7 @@ type ElementsCollection = { edges: Edge[]; }; -export function getElements(xElements: number = 10, yElements: number = 10): ElementsCollection { +export function getNodesAndEdges(xElements: number = 10, yElements: number = 10): ElementsCollection { const initialNodes = []; const initialEdges: Edge[] = []; let nodeId = 1; diff --git a/example/src/index.tsx b/example/src/index.tsx index d6acbeba..6e08eabd 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -5,6 +5,7 @@ import { BrowserRouter as Router, Route, Switch, withRouter } from 'react-router import Basic from './Basic'; import UpdateNode from './UpdateNode'; import Stress from './Stress'; +import CustomNode from './CustomNode'; import './index.css'; @@ -21,6 +22,10 @@ const routes = [ path: '/stress', component: Stress, }, + { + path: '/custom-node', + component: CustomNode, + }, ]; const Header = withRouter(({ history, location }) => { diff --git a/src/components/Handle/handler.ts b/src/components/Handle/handler.ts index 53f7d213..03fd37df 100644 --- a/src/components/Handle/handler.ts +++ b/src/components/Handle/handler.ts @@ -1,8 +1,6 @@ import { MouseEvent as ReactMouseEvent } from 'react'; -import { GetState } from 'zustand'; import { getHostForElement } from '../../utils'; -import { ReactFlowState } from '../../types'; import { ElementId, @@ -105,8 +103,7 @@ export function onMouseDown( onEdgeUpdateEnd?: (evt: MouseEvent) => void, onConnectStart?: OnConnectStartFunc, onConnectStop?: OnConnectStopFunc, - onConnectEnd?: OnConnectEndFunc, - getState?: GetState + onConnectEnd?: OnConnectEndFunc ): void { const reactFlowNode = (event.target as Element).closest('.react-flow'); // when react-flow is used inside a shadow root we can't use document @@ -180,8 +177,7 @@ export function onMouseDown( onConnectStop?.(event); if (isValid) { - const nodes = getState?.().nodes; - onConnect?.(connection, nodes || []); + onConnect?.(connection); } onConnectEnd?.(event); diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx index 490b1b98..2f594b27 100644 --- a/src/components/Handle/index.tsx +++ b/src/components/Handle/index.tsx @@ -4,7 +4,7 @@ import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import NodeIdContext from '../../contexts/NodeIdContext'; -import { HandleProps, Connection, ElementId, Position, Node, ReactFlowState } from '../../types'; +import { HandleProps, Connection, ElementId, Position, ReactFlowState } from '../../types'; import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler'; @@ -52,9 +52,9 @@ const Handle = forwardRef( const isTarget = type === 'target'; const onConnectExtended = useCallback( - (params: Connection, nodes: Node[]) => { - onConnectAction?.(params, nodes); - onConnect?.(params, nodes); + (params: Connection) => { + onConnectAction?.(params); + onConnect?.(params); }, [onConnectAction, onConnect] ); diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index ebf1106b..b61d0550 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -12,6 +12,7 @@ const selector = (s: ReactFlowState) => ({ unsetNodesSelection: s.unsetNodesSelection, updateNodePosition: s.updateNodePosition, updateNodeDimensions: s.updateNodeDimensions, + unselectNodesAndEdges: s.unselectNodesAndEdges, }); export default (NodeComponent: ComponentType) => { @@ -48,10 +49,13 @@ export default (NodeComponent: ComponentType) => { resizeObserver, dragHandle, }: WrapNodeProps) => { - const { addSelectedElements, unsetNodesSelection, updateNodePosition, updateNodeDimensions } = useStore( - selector, - shallow - ); + const { + addSelectedElements, + unselectNodesAndEdges, + unsetNodesSelection, + updateNodePosition, + updateNodeDimensions, + } = useStore(selector, shallow); const nodeElement = useRef(null); const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]); @@ -142,8 +146,8 @@ export default (NodeComponent: ComponentType) => { addSelectedElements([node]); } } else if (!selectNodesOnDrag && !isSelected && isSelectable) { + unselectNodesAndEdges(); unsetNodesSelection(); - addSelectedElements([]); } }, [node, isSelected, selectNodesOnDrag, isSelectable, onNodeDragStart] diff --git a/src/components/SelectionListener/index.tsx b/src/components/SelectionListener/index.tsx index a99a95a0..a4a92441 100644 --- a/src/components/SelectionListener/index.tsx +++ b/src/components/SelectionListener/index.tsx @@ -1,26 +1,26 @@ import { useEffect } from 'react'; import shallow from 'zustand/shallow'; -import { Elements, ReactFlowState } from '../../types'; +import { ReactFlowState, OnSelectionChangeFunc } from '../../types'; import { useStore } from '../../store'; interface SelectionListenerProps { - onSelectionChange: (elements: Elements | null) => void; + onSelectionChange: OnSelectionChangeFunc; } -const selectedElementsSelector = (s: ReactFlowState) => [ - ...s.nodes.filter((n) => n.isSelected), - ...s.edges.filter((e) => e.isSelected), -]; +const selectedElementsSelector = (s: ReactFlowState) => ({ + selectedNodes: s.nodes.filter((n) => n.isSelected), + selectedEdges: s.edges.filter((e) => e.isSelected), +}); // This is just a helper component for calling the onSelectionChange listener. export default ({ onSelectionChange }: SelectionListenerProps) => { - const selectedElements = useStore(selectedElementsSelector, shallow); + const { selectedNodes, selectedEdges } = useStore(selectedElementsSelector, shallow); useEffect(() => { - onSelectionChange(selectedElements); - }, [selectedElements]); + onSelectionChange({ nodes: selectedNodes, edges: selectedEdges }); + }, [selectedNodes, selectedEdges]); return null; }; diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 739aa4a6..8abe99a5 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -4,11 +4,10 @@ import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import ConnectionLine from '../../components/ConnectionLine/index'; import MarkerDefinitions from './MarkerDefinitions'; -import { getEdgePositions, getHandle, getSourceTargetNodes } from './utils'; +import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from './utils'; import { Position, Edge, - Node, Connection, ConnectionLineType, ConnectionLineComponent, @@ -19,8 +18,6 @@ import { } from '../../types'; interface EdgeRendererProps { - nodes: Node[]; - edges: Edge[]; edgeTypes: any; connectionLineType: ConnectionLineType; connectionLineStyle?: CSSProperties; @@ -162,26 +159,12 @@ const Edge = memo( targetPosition ); - // const isVisible = onlyRenderVisibleElements - // ? isEdgeVisible({ - // sourcePos: { x: sourceX, y: sourceY }, - // targetPos: { x: targetX, y: targetY }, - // width, - // height, - // transform, - // }) - // : true; - - // if (!isVisible) { - // return null; - // } - return ( ({ width: s.width, height: s.height, connectionMode: s.connectionMode, + nodes: s.nodes, }); const EdgeRenderer = (props: EdgeRendererProps) => { @@ -247,8 +231,42 @@ const EdgeRenderer = (props: EdgeRendererProps) => { width, height, connectionMode, + nodes, } = useStore(selector, shallow); + const edges = useStore( + useCallback( + (s: ReactFlowState) => { + if (!props.onlyRenderVisibleElements) { + return s.edges; + } + + return s.edges.filter((e) => { + const { sourceNode, targetNode } = getSourceTargetNodes(e, s.nodes); + + return ( + sourceNode?.width && + sourceNode?.height && + targetNode?.width && + targetNode?.height && + isEdgeVisible({ + sourcePos: sourceNode.position, + targetPos: targetNode.position, + sourceWidth: sourceNode.width, + sourceHeight: sourceNode.height, + targetWidth: targetNode.width, + targetHeight: targetNode.height, + width: s.width, + height: s.height, + transform: s.transform, + }) + ); + }); + }, + [props.onlyRenderVisibleElements] + ) + ); + if (!width) { return null; } @@ -260,8 +278,8 @@ const EdgeRenderer = (props: EdgeRendererProps) => { - {props.edges.map((edge: Edge) => { - const { sourceNode, targetNode } = getSourceTargetNodes(edge, props.nodes); + {edges.map((edge: Edge) => { + const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes); return ( { children: ReactNode; } diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 8fddc4f3..24938a74 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -4,14 +4,14 @@ import { useStoreApi } from '../../store'; import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; -import { onLoadProject, onLoadGetElements, onLoadToObject } from '../../utils/graph'; +import { onLoadProject, onLoadGetNodes, onLoadGetEdges, onLoadToObject } from '../../utils/graph'; import useZoomPanHelper from '../../hooks/useZoomPanHelper'; import { ReactFlowProps } from '../ReactFlow'; import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types'; -export interface GraphViewProps extends Omit { +export interface GraphViewProps extends Omit { nodeTypes: NodeTypesType; edgeTypes: EdgeTypesType; selectionKeyCode: KeyCode; @@ -26,8 +26,6 @@ export interface GraphViewProps extends Omit ); diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index a808fb79..ad12d642 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -1,8 +1,9 @@ -import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; +import React, { memo, useMemo, ComponentType, MouseEvent, useCallback } from 'react'; import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types'; +import { getNodesInside } from '../../utils/graph'; interface NodeRendererProps { nodeTypes: NodeTypesType; selectNodesOnDrag: boolean; @@ -16,7 +17,6 @@ interface NodeRendererProps { onNodeDrag?: (event: MouseEvent, node: Node) => void; onNodeDragStop?: (event: MouseEvent, node: Node) => void; onlyRenderVisibleElements: boolean; - nodes: Node[]; } const selector = (s: ReactFlowState) => ({ @@ -40,9 +40,16 @@ const NodeRenderer = (props: NodeRendererProps) => { snapToGrid, } = useStore(selector, shallow); - // const visibleNodes = props.onlyRenderVisibleElements - // ? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true) - // : nodes; + const nodes = useStore( + useCallback( + (s: ReactFlowState) => { + return props.onlyRenderVisibleElements + ? getNodesInside(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true) + : s.nodes; + }, + [props.onlyRenderVisibleElements] + ) + ); const transformStyle = useMemo( () => ({ @@ -68,7 +75,7 @@ const NodeRenderer = (props: NodeRendererProps) => { return (
- {props.nodes.map((node) => { + {nodes.map((node) => { const nodeType = node.type || 'default'; const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index 3f7b4106..1e7ba986 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -19,7 +19,7 @@ import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../compon import { createEdgeTypes } from '../EdgeRenderer/utils'; import Wrapper from './Wrapper'; import { - Elements, + OnSelectionChangeFunc, NodeTypesType, EdgeTypesType, OnLoadFunc, @@ -81,7 +81,7 @@ export interface ReactFlowProps extends Omit, 'on onMove?: (flowTransform?: FlowTransform) => void; onMoveStart?: (flowTransform?: FlowTransform) => void; onMoveEnd?: (flowTransform?: FlowTransform) => void; - onSelectionChange?: (elements: Elements | null) => void; + onSelectionChange?: OnSelectionChangeFunc; onSelectionDragStart?: (event: ReactMouseEvent, nodes: Node[]) => void; onSelectionDrag?: (event: ReactMouseEvent, nodes: Node[]) => void; onSelectionDragStop?: (event: ReactMouseEvent, nodes: Node[]) => void; @@ -227,8 +227,6 @@ const ReactFlow = forwardRef(
(); -const unselectElements = (elements: Elements): NodeChange[] | EdgeChange[] => - elements - .filter((e) => e.isSelected) - .map((e) => ({ - id: e.id, - type: 'select', - isSelected: false, - })); +const createNodeOrEdgeSelectionChange = (isSelected: boolean) => (item: Node | Edge) => ({ + id: item.id, + type: 'select', + isSelected, +}); const createStore = () => create((set, get) => ({ @@ -219,8 +215,12 @@ const createStore = () => const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id); const selectedNodeIds = selectedNodes.map((n) => n.id); - onNodesChange?.(nodes.map((n) => ({ id: n.id, type: 'select', isSelected: selectedNodeIds.includes(n.id) }))); - onEdgesChange?.(edges.map((e) => ({ id: e.id, type: 'select', isSelected: selectedEdgeIds.includes(e.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, @@ -248,31 +248,22 @@ const createStore = () => set(stateUpdate); }, - addSelectedElements: (elements: Elements) => { + addSelectedElements: (selectedElementsArr: Array) => { const { multiSelectionActive, onNodesChange, onEdgesChange, nodes, edges } = get(); - const selectedElementsArr = Array.isArray(elements) ? elements : [elements]; let changedNodes; let changedEdges; if (multiSelectionActive) { - changedNodes = selectedElementsArr - .filter(isNode) - .map((node) => ({ id: node.id, type: 'select', isSelected: true })); - changedEdges = selectedElementsArr - .filter(isEdge) - .map((edge) => ({ id: edge.id, type: 'select', isSelected: true })); + changedNodes = selectedElementsArr.filter(isNode).map(createNodeOrEdgeSelectionChange(true)); + changedEdges = selectedElementsArr.filter(isEdge).map(createNodeOrEdgeSelectionChange(true)); } else { - changedNodes = nodes.map((node) => ({ - id: node.id, - type: 'select', - isSelected: selectedElementsArr.some((e) => e.id === node.id), - })); - changedEdges = edges.map((edge) => ({ - id: edge.id, - type: 'select', - isSelected: selectedElementsArr.some((e) => e.id === edge.id), - })); + 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) + ); } if (changedNodes.length) { @@ -283,6 +274,21 @@ const createStore = () => onEdgesChange?.(changedEdges as EdgeChange[]); } }, + unselectNodesAndEdges: () => { + const { nodes, edges, onNodesChange, onEdgesChange } = get(); + const nodesToUnselect = nodes.map((n) => { + n.isSelected = false; + return createNodeOrEdgeSelectionChange(false)(n); + }) as NodeChange[]; + const edgesToUnselect = edges.map(createNodeOrEdgeSelectionChange(false)) as EdgeChange[]; + + if (nodesToUnselect.length) { + onNodesChange?.(nodesToUnselect); + } + if (edgesToUnselect.length) { + onEdgesChange?.(edgesToUnselect); + } + }, initD3Zoom: ({ d3Zoom, d3Selection, d3ZoomHandler, transform }: InitD3ZoomPayload) => set({ d3Zoom, @@ -312,14 +318,14 @@ const createStore = () => resetSelectedElements: () => { const { nodes, edges, onNodesChange, onEdgesChange } = get(); - const nodesToUnselect = unselectElements(nodes) as NodeChange[]; - const edgesToUnselect = unselectElements(edges) as EdgeChange[]; + const nodesToUnselect = nodes.filter((e) => e.isSelected).map(createNodeOrEdgeSelectionChange(false)); + const edgesToUnselect = edges.filter((e) => e.isSelected).map(createNodeOrEdgeSelectionChange(false)); if (nodesToUnselect.length) { - onNodesChange?.(nodesToUnselect); + onNodesChange?.(nodesToUnselect as NodeChange[]); } if (edgesToUnselect.length) { - onEdgesChange?.(edgesToUnselect); + onEdgesChange?.(edgesToUnselect as EdgeChange[]); } }, setNodeExtent: (nodeExtent: NodeExtent) => diff --git a/src/types/index.ts b/src/types/index.ts index 32f98f6e..c9751832 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -312,7 +312,8 @@ export type OnLoadParams = { zoomTo: (zoomLevel: number) => void; fitView: FitViewFunc; project: ProjectFunc; - getElements: () => Elements; + getNodes: () => Node[]; + getEdges: () => Edge[]; setTransform: (transform: FlowTransform) => void; toObject: ToObjectFunc; }; @@ -353,7 +354,7 @@ export type ConnectionLineComponentProps = { export type ConnectionLineComponent = React.ComponentType; -export type OnConnectFunc = (connection: Connection, nodes: Node[]) => void; +export type OnConnectFunc = (connection: Connection) => void; export type OnConnectStartParams = { nodeId: ElementId | null; handleId: ElementId | null; @@ -490,6 +491,7 @@ export interface ReactFlowState { unsetUserSelection: () => void; unsetNodesSelection: () => void; resetSelectedElements: () => void; + unselectNodesAndEdges: () => void; addSelectedElements: (elements: Elements) => void; updateTransform: (transform: Transform) => void; updateSize: (size: Dimensions) => void; @@ -522,3 +524,5 @@ export interface ReactFlowState { } export type UpdateNodeInternals = (nodeId: ElementId) => void; + +export type OnSelectionChangeFunc = (params: { nodes: Node[]; edges: Edge[] }) => void; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 109406c5..07ff0662 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -24,52 +24,38 @@ export const isEdge = (element: Node | Connection | Edge): element is Edge => export const isNode = (element: Node | Connection | Edge): element is Node => 'id' in element && !('source' in element) && !('target' in element); -export const getOutgoers = (node: Node, elements: Elements): Node[] => { +export const getOutgoers = (node: Node, nodes: Node[], edges: Edge[]): Node[] => { if (!isNode(node)) { return []; } - const outgoerIds = elements.filter((e) => isEdge(e) && e.source === node.id).map((e) => (e as Edge).target); - return elements.filter((e) => outgoerIds.includes(e.id)) as Node[]; + const outgoerIds = edges.filter((e) => e.source === node.id).map((e) => e.target); + return nodes.filter((n) => outgoerIds.includes(n.id)); }; -export const getIncomers = (node: Node, elements: Elements): Node[] => { +export const getIncomers = (node: Node, nodes: Node[], edges: Edge[]): Node[] => { if (!isNode(node)) { return []; } - const incomersIds = elements.filter((e) => isEdge(e) && e.target === node.id).map((e) => (e as Edge).source); - return elements.filter((e) => incomersIds.includes(e.id)) as Node[]; -}; - -export const removeElements = (elementsToRemove: Elements, elements: Elements): Elements => { - const nodeIdsToRemove = elementsToRemove.map((n) => n.id); - - return elements.filter((element) => { - const edgeElement = element as Edge; - return !( - nodeIdsToRemove.includes(element.id) || - nodeIdsToRemove.includes(edgeElement.target) || - nodeIdsToRemove.includes(edgeElement.source) - ); - }); + const incomersIds = edges.filter((e) => e.target === node.id).map((e) => e.source); + return nodes.filter((n) => incomersIds.includes(n.id)); }; const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection): ElementId => `reactflow__edge-${source}${sourceHandle}-${target}${targetHandle}`; -const connectionExists = (edge: Edge, elements: Elements) => { - return elements.some( - (el) => - isEdge(el) && - el.source === edge.source && - el.target === edge.target && - (el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) && - (el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle)) +const connectionExists = (edge: Edge, edges: Edge[]) => { + return edges.some( + (e) => + edge.source === e.source && + edge.target === e.target && + (edge.sourceHandle === e.sourceHandle || (!edge.sourceHandle && !e.sourceHandle)) && + (edge.targetHandle === e.targetHandle || (!edge.targetHandle && !e.targetHandle)) ); }; -export const addEdge = (edgeParams: Edge | Connection, nodes: Node[], edges: Edge[]): Edge[] => { +export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] => { if (!edgeParams.source || !edgeParams.target) { console.warn("Can't create edge. An edge needs a source and a target."); return edges; @@ -85,7 +71,7 @@ export const addEdge = (edgeParams: Edge | Connection, nodes: Node[], edges: Edg } as Edge; } - if (connectionExists(edge, nodes)) { + if (connectionExists(edge, edges)) { return edges; } @@ -211,7 +197,13 @@ export const getNodesInside = ( const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y)); const overlappingArea = Math.ceil(xOverlap * yOverlap); - if (width === null || height === null || isDragging) { + if ( + typeof width === 'undefined' || + typeof height === 'undefined' || + width === null || + height === null || + isDragging + ) { // nodes are initialized with width and height = null return true; } @@ -232,15 +224,19 @@ export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => { return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target)); }; -const parseElements = (nodes: Node[], edges: Edge[]): Elements => { - return [...nodes.map((n) => ({ ...n })), ...edges.map((e) => ({ ...e }))]; +export const onLoadGetNodes = (getState: GetState) => { + return (): Node[] => { + const { nodes = [] } = getState(); + + return nodes.map((n) => ({ ...n })); + }; }; -export const onLoadGetElements = (getState: GetState) => { - return (): Elements => { - const { nodes = [], edges = [] } = getState(); +export const onLoadGetEdges = (getState: GetState) => { + return (): Edge[] => { + const { edges = [] } = getState(); - return parseElements(nodes, edges); + return edges.map((e) => ({ ...e })); }; }; From f957462eb69c29a4182c3e057404ead49985bb76 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 20 Oct 2021 10:23:09 +0200 Subject: [PATCH 014/263] chore(examples): add old api examples --- example/.gitignore | 2 - example/src_oldapi/Basic/index.tsx | 98 ++++++++ .../CustomConnectionLine/ConnectionLine.tsx | 19 ++ .../src_oldapi/CustomConnectionLine/index.tsx | 33 +++ .../CustomNode/ColorSelectorNode.tsx | 25 ++ example/src_oldapi/CustomNode/index.tsx | 135 +++++++++++ .../src_oldapi/DragHandle/DragHandleNode.tsx | 33 +++ example/src_oldapi/DragHandle/index.tsx | 21 ++ example/src_oldapi/DragNDrop/Sidebar.tsx | 25 ++ example/src_oldapi/DragNDrop/dnd.css | 37 +++ example/src_oldapi/DragNDrop/index.tsx | 75 ++++++ example/src_oldapi/EdgeTypes/index.tsx | 54 +++++ example/src_oldapi/EdgeTypes/utils.ts | 106 ++++++++ example/src_oldapi/Edges/CustomEdge.tsx | 31 +++ example/src_oldapi/Edges/CustomEdge2.tsx | 44 ++++ example/src_oldapi/Edges/index.tsx | 115 +++++++++ example/src_oldapi/Empty/index.tsx | 60 +++++ example/src_oldapi/Hidden/index.tsx | 53 ++++ example/src_oldapi/Interaction/index.tsx | 226 ++++++++++++++++++ example/src_oldapi/Layouting/index.tsx | 86 +++++++ .../src_oldapi/Layouting/initial-elements.ts | 71 ++++++ example/src_oldapi/Layouting/layouting.css | 11 + example/src_oldapi/MultiFlows/index.tsx | 45 ++++ example/src_oldapi/MultiFlows/multiflows.css | 13 + example/src_oldapi/NodeTypeChange/index.tsx | 55 +++++ .../NodeTypesObjectChange/index.tsx | 76 ++++++ example/src_oldapi/Overview/index.tsx | 202 ++++++++++++++++ example/src_oldapi/Provider/Sidebar.tsx | 36 +++ example/src_oldapi/Provider/index.tsx | 57 +++++ example/src_oldapi/Provider/provider.css | 45 ++++ example/src_oldapi/SaveRestore/Controls.tsx | 61 +++++ example/src_oldapi/SaveRestore/index.tsx | 37 +++ example/src_oldapi/SaveRestore/save.css | 11 + example/src_oldapi/Stress/index.tsx | 70 ++++++ example/src_oldapi/Stress/utils.ts | 30 +++ example/src_oldapi/Switch/index.tsx | 53 ++++ .../src_oldapi/Undirectional/CustomNode.tsx | 19 ++ example/src_oldapi/Undirectional/index.tsx | 221 +++++++++++++++++ example/src_oldapi/UpdatableEdge/index.tsx | 79 ++++++ example/src_oldapi/UpdateNode/index.tsx | 78 ++++++ example/src_oldapi/UpdateNode/updatenode.css | 21 ++ .../UseUpdateNodeInternals/CustomNode.tsx | 34 +++ .../UseUpdateNodeInternals/index.tsx | 101 ++++++++ example/src_oldapi/UseZoomPanHelper/index.tsx | 65 +++++ example/src_oldapi/Validation/index.tsx | 73 ++++++ example/src_oldapi/Validation/validation.css | 31 +++ example/src_oldapi/index.css | 82 +++++++ example/src_oldapi/index.tsx | 165 +++++++++++++ example/src_oldapi/react-app-env.d.ts | 1 + example/tsconfig.json | 11 +- 50 files changed, 3122 insertions(+), 10 deletions(-) create mode 100644 example/src_oldapi/Basic/index.tsx create mode 100644 example/src_oldapi/CustomConnectionLine/ConnectionLine.tsx create mode 100644 example/src_oldapi/CustomConnectionLine/index.tsx create mode 100644 example/src_oldapi/CustomNode/ColorSelectorNode.tsx create mode 100644 example/src_oldapi/CustomNode/index.tsx create mode 100644 example/src_oldapi/DragHandle/DragHandleNode.tsx create mode 100644 example/src_oldapi/DragHandle/index.tsx create mode 100644 example/src_oldapi/DragNDrop/Sidebar.tsx create mode 100644 example/src_oldapi/DragNDrop/dnd.css create mode 100644 example/src_oldapi/DragNDrop/index.tsx create mode 100644 example/src_oldapi/EdgeTypes/index.tsx create mode 100644 example/src_oldapi/EdgeTypes/utils.ts create mode 100644 example/src_oldapi/Edges/CustomEdge.tsx create mode 100644 example/src_oldapi/Edges/CustomEdge2.tsx create mode 100644 example/src_oldapi/Edges/index.tsx create mode 100644 example/src_oldapi/Empty/index.tsx create mode 100644 example/src_oldapi/Hidden/index.tsx create mode 100644 example/src_oldapi/Interaction/index.tsx create mode 100644 example/src_oldapi/Layouting/index.tsx create mode 100644 example/src_oldapi/Layouting/initial-elements.ts create mode 100644 example/src_oldapi/Layouting/layouting.css create mode 100644 example/src_oldapi/MultiFlows/index.tsx create mode 100644 example/src_oldapi/MultiFlows/multiflows.css create mode 100644 example/src_oldapi/NodeTypeChange/index.tsx create mode 100644 example/src_oldapi/NodeTypesObjectChange/index.tsx create mode 100644 example/src_oldapi/Overview/index.tsx create mode 100644 example/src_oldapi/Provider/Sidebar.tsx create mode 100644 example/src_oldapi/Provider/index.tsx create mode 100644 example/src_oldapi/Provider/provider.css create mode 100644 example/src_oldapi/SaveRestore/Controls.tsx create mode 100644 example/src_oldapi/SaveRestore/index.tsx create mode 100644 example/src_oldapi/SaveRestore/save.css create mode 100644 example/src_oldapi/Stress/index.tsx create mode 100644 example/src_oldapi/Stress/utils.ts create mode 100644 example/src_oldapi/Switch/index.tsx create mode 100644 example/src_oldapi/Undirectional/CustomNode.tsx create mode 100644 example/src_oldapi/Undirectional/index.tsx create mode 100644 example/src_oldapi/UpdatableEdge/index.tsx create mode 100644 example/src_oldapi/UpdateNode/index.tsx create mode 100644 example/src_oldapi/UpdateNode/updatenode.css create mode 100644 example/src_oldapi/UseUpdateNodeInternals/CustomNode.tsx create mode 100644 example/src_oldapi/UseUpdateNodeInternals/index.tsx create mode 100644 example/src_oldapi/UseZoomPanHelper/index.tsx create mode 100644 example/src_oldapi/Validation/index.tsx create mode 100644 example/src_oldapi/Validation/validation.css create mode 100644 example/src_oldapi/index.css create mode 100644 example/src_oldapi/index.tsx create mode 100644 example/src_oldapi/react-app-env.d.ts diff --git a/example/.gitignore b/example/.gitignore index 703c7975..4d29575d 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -21,5 +21,3 @@ npm-debug.log* yarn-debug.log* yarn-error.log* - -src_oldapi \ No newline at end of file diff --git a/example/src_oldapi/Basic/index.tsx b/example/src_oldapi/Basic/index.tsx new file mode 100644 index 00000000..19523f9b --- /dev/null +++ b/example/src_oldapi/Basic/index.tsx @@ -0,0 +1,98 @@ +import React, { useState, MouseEvent } from 'react'; + +import ReactFlow, { + removeElements, + addEdge, + isNode, + Background, + Elements, + BackgroundVariant, + FlowElement, + Node, + Edge, + Connection, + OnLoadParams, +} from 'react-flow-renderer'; + +const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); +const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); + +const initialNodes: Node[] = [ + { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, + { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, + { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, + { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' }, +]; + +const initialEdges: Edge[] = [ + { id: 'e1-2', source: '1', target: '2', animated: true }, + { id: 'e1-3', source: '1', target: '3' }, +]; + +const BasicFlow = () => { + const [rfInstance, setRfInstance] = useState(null); + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState(initialEdges); + // const onElementsRemove = (elementsToRemove: Elements) => setNodes((els) => removeElements(elementsToRemove, els)); + // const onConnect = (params: Edge | Connection) => setNodes((els) => addEdge(params, els)); + const onLoad = (reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance); + + const updatePos = () => { + setNodes((nds) => { + return nds.map((n) => { + n.position = { + x: Math.random() * 400, + y: Math.random() * 400, + }; + + return n; + }); + }); + }; + + const logToObject = () => console.log(rfInstance?.toObject()); + const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 }); + + const toggleClassnames = () => { + setNodes((nds) => { + return nds.map((n) => { + n.className = n.className === 'light' ? 'dark' : 'light'; + + return n; + }); + }); + }; + + return ( + + + +
+ + + + +
+
+ ); +}; + +export default BasicFlow; diff --git a/example/src_oldapi/CustomConnectionLine/ConnectionLine.tsx b/example/src_oldapi/CustomConnectionLine/ConnectionLine.tsx new file mode 100644 index 00000000..f24fcc28 --- /dev/null +++ b/example/src_oldapi/CustomConnectionLine/ConnectionLine.tsx @@ -0,0 +1,19 @@ +import React, { FC } from 'react'; +import { ConnectionLineComponentProps } from 'react-flow-renderer'; + +const ConnectionLine: FC = ({ sourceX, sourceY, targetX, targetY }) => { + return ( + + + + + ); +}; + +export default ConnectionLine; diff --git a/example/src_oldapi/CustomConnectionLine/index.tsx b/example/src_oldapi/CustomConnectionLine/index.tsx new file mode 100644 index 00000000..48cbf561 --- /dev/null +++ b/example/src_oldapi/CustomConnectionLine/index.tsx @@ -0,0 +1,33 @@ +import React, { useState } from 'react'; +import ReactFlow, { + removeElements, + addEdge, + Background, + BackgroundVariant, + Elements, + Connection, + Edge, +} from 'react-flow-renderer'; + +import ConnectionLine from './ConnectionLine'; + +const initialElements: Elements = [{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }]; + +const ConnectionLineFlow = () => { + const [elements, setElements] = useState(initialElements); + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + + return ( + + + + ); +}; + +export default ConnectionLineFlow; diff --git a/example/src_oldapi/CustomNode/ColorSelectorNode.tsx b/example/src_oldapi/CustomNode/ColorSelectorNode.tsx new file mode 100644 index 00000000..7ad20340 --- /dev/null +++ b/example/src_oldapi/CustomNode/ColorSelectorNode.tsx @@ -0,0 +1,25 @@ +import React, { memo, FC, CSSProperties } from 'react'; + +import { Handle, Position, NodeProps, Connection, Edge } from 'react-flow-renderer'; + +const targetHandleStyle: CSSProperties = { background: '#555' }; +const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 }; +const sourceHandleStyleB: CSSProperties = { ...targetHandleStyle, bottom: 10, top: 'auto' }; + +const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params); + +const ColorSelectorNode: FC = ({ data, isConnectable }) => { + return ( + <> + +
+ Custom Color Picker Node: {data.color} +
+ + + + + ); +}; + +export default memo(ColorSelectorNode); diff --git a/example/src_oldapi/CustomNode/index.tsx b/example/src_oldapi/CustomNode/index.tsx new file mode 100644 index 00000000..804aabea --- /dev/null +++ b/example/src_oldapi/CustomNode/index.tsx @@ -0,0 +1,135 @@ +import React, { useState, useEffect, MouseEvent } from 'react'; +import { ChangeEvent } from 'react'; + +import ReactFlow, { + isEdge, + removeElements, + addEdge, + MiniMap, + Controls, + Node, + FlowElement, + OnLoadParams, + Elements, + Position, + SnapGrid, + Connection, + Edge, +} from 'react-flow-renderer'; + +import ColorSelectorNode from './ColorSelectorNode'; + +const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance); +const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); +const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); + +const initBgColor = '#1A192B'; + +const connectionLineStyle = { stroke: '#fff' }; +const snapGrid: SnapGrid = [16, 16]; +const nodeTypes = { + selectorNode: ColorSelectorNode, +}; + +const CustomNodeFlow = () => { + const [elements, setElements] = useState([]); + const [bgColor, setBgColor] = useState(initBgColor); + + useEffect(() => { + const onChange = (event: ChangeEvent) => { + setElements((els) => + els.map((e) => { + if (isEdge(e) || e.id !== '2') { + return e; + } + + const color = event.target.value; + + setBgColor(color); + + return { + ...e, + data: { + ...e.data, + color, + }, + }; + }) + ); + }; + + setElements([ + { + id: '1', + type: 'input', + data: { label: 'An input node' }, + position: { x: 0, y: 50 }, + sourcePosition: Position.Right, + }, + { + id: '2', + type: 'selectorNode', + data: { onChange: onChange, color: initBgColor }, + style: { border: '1px solid #777', padding: 10 }, + position: { x: 250, y: 50 }, + }, + { + id: '3', + type: 'output', + data: { label: 'Output A' }, + position: { x: 550, y: 25 }, + targetPosition: Position.Left, + }, + { + id: '4', + type: 'output', + data: { label: 'Output B' }, + position: { x: 550, y: 100 }, + targetPosition: Position.Left, + }, + + { id: 'e1-2', source: '1', target: '2', animated: true, style: { stroke: '#fff' } }, + { id: 'e2a-3', source: '2', sourceHandle: 'a', target: '3', animated: true, style: { stroke: '#fff' } }, + { id: 'e2b-4', source: '2', sourceHandle: 'b', target: '4', animated: true, style: { stroke: '#fff' } }, + ]); + }, []); + + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = (params: Connection | Edge) => + setElements((els) => addEdge({ ...params, animated: true, style: { stroke: '#fff' } }, els)); + + return ( + + { + if (n.type === 'input') return '#0041d0'; + if (n.type === 'selectorNode') return bgColor; + if (n.type === 'output') return '#ff0072'; + + return '#eee'; + }} + nodeColor={(n: Node): string => { + if (n.type === 'selectorNode') return bgColor; + + return '#fff'; + }} + /> + + + ); +}; + +export default CustomNodeFlow; diff --git a/example/src_oldapi/DragHandle/DragHandleNode.tsx b/example/src_oldapi/DragHandle/DragHandleNode.tsx new file mode 100644 index 00000000..377e45de --- /dev/null +++ b/example/src_oldapi/DragHandle/DragHandleNode.tsx @@ -0,0 +1,33 @@ +import { memo, FC } from 'react'; + +import { Handle, Position, NodeProps, Connection, Edge } from 'react-flow-renderer'; + +const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params); + +const labelStyle = { + display: 'flex', + alignItems: 'center', +}; + +const dragHandleStyle = { + display: 'inline-block', + width: 25, + height: 25, + backgroundColor: 'teal', + marginLeft: 5, + borderRadius: '50%', +}; + +const ColorSelectorNode: FC = () => { + return ( + <> + +
+ Only draggable here → +
+ + + ); +}; + +export default memo(ColorSelectorNode); diff --git a/example/src_oldapi/DragHandle/index.tsx b/example/src_oldapi/DragHandle/index.tsx new file mode 100644 index 00000000..b5620243 --- /dev/null +++ b/example/src_oldapi/DragHandle/index.tsx @@ -0,0 +1,21 @@ +import ReactFlow from 'react-flow-renderer'; + +import DragHandleNode from './DragHandleNode'; + +const nodeTypes = { + dragHandleNode: DragHandleNode, +}; + +const elements = [ + { + id: '2', + type: 'dragHandleNode', + dragHandle: '.custom-drag-handle', + style: { border: '1px solid #ddd', padding: '20px 40px' }, + position: { x: 200, y: 200 }, + }, +]; + +const DragHandleFlow = () => ; + +export default DragHandleFlow; diff --git a/example/src_oldapi/DragNDrop/Sidebar.tsx b/example/src_oldapi/DragNDrop/Sidebar.tsx new file mode 100644 index 00000000..7c1fba58 --- /dev/null +++ b/example/src_oldapi/DragNDrop/Sidebar.tsx @@ -0,0 +1,25 @@ +import React, { DragEvent } from 'react'; + +const onDragStart = (event: DragEvent, nodeType: string) => { + event.dataTransfer.setData('application/reactflow', nodeType); + event.dataTransfer.effectAllowed = 'move'; +}; + +const Sidebar = () => { + return ( + + ); +}; + +export default Sidebar; diff --git a/example/src_oldapi/DragNDrop/dnd.css b/example/src_oldapi/DragNDrop/dnd.css new file mode 100644 index 00000000..26d490c8 --- /dev/null +++ b/example/src_oldapi/DragNDrop/dnd.css @@ -0,0 +1,37 @@ +.dndflow { + flex-direction: column; + display: flex; + height: 100%; +} + +.dndflow aside { + border-right: 1px solid #eee; + padding: 15px 10px; + font-size: 12px; + background: #fcfcfc; +} + +.dndflow aside > * { + margin-bottom: 10px; + cursor: grab; +} + +.dndflow aside .description { + margin-bottom: 10px; +} + +.dndflow .reactflow-wrapper { + flex-grow: 1; + height: 100%; +} + +@media screen and (min-width: 768px) { + .dndflow { + flex-direction: row; + } + + .dndflow aside { + width: 20%; + max-width: 180px; + } +} diff --git a/example/src_oldapi/DragNDrop/index.tsx b/example/src_oldapi/DragNDrop/index.tsx new file mode 100644 index 00000000..0c238fb6 --- /dev/null +++ b/example/src_oldapi/DragNDrop/index.tsx @@ -0,0 +1,75 @@ +import React, { useState, DragEvent } from 'react'; +import ReactFlow, { + ReactFlowProvider, + addEdge, + removeElements, + Controls, + OnLoadParams, + Elements, + Connection, + Edge, + ElementId, + Node, +} from 'react-flow-renderer'; + +import Sidebar from './Sidebar'; + +import './dnd.css'; + +const initialElements = [{ id: '1', type: 'input', data: { label: 'input node' }, position: { x: 250, y: 5 } }]; + +const onDragOver = (event: DragEvent) => { + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; +}; + +let id = 0; +const getId = (): ElementId => `dndnode_${id++}`; + +const DnDFlow = () => { + const [reactFlowInstance, setReactFlowInstance] = useState(); + const [elements, setElements] = useState(initialElements); + + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onLoad = (_reactFlowInstance: OnLoadParams) => setReactFlowInstance(_reactFlowInstance); + + const onDrop = (event: DragEvent) => { + event.preventDefault(); + + if (reactFlowInstance) { + const type = event.dataTransfer.getData('application/reactflow'); + const position = reactFlowInstance.project({ x: event.clientX, y: event.clientY - 40 }); + const newNode: Node = { + id: getId(), + type, + position, + data: { label: `${type} node` }, + }; + + setElements((es) => es.concat(newNode)); + } + }; + + return ( +
+ +
+ + + +
+ +
+
+ ); +}; + +export default DnDFlow; diff --git a/example/src_oldapi/EdgeTypes/index.tsx b/example/src_oldapi/EdgeTypes/index.tsx new file mode 100644 index 00000000..7c87fe7e --- /dev/null +++ b/example/src_oldapi/EdgeTypes/index.tsx @@ -0,0 +1,54 @@ +/** + * Example for checking the different edge types and source and target positions + */ +import React, { useState } from 'react'; + +import ReactFlow, { + removeElements, + addEdge, + MiniMap, + Controls, + Background, + OnLoadParams, + Connection, + Edge, + Elements, +} from 'react-flow-renderer'; +import { getElements } from './utils'; + +const onLoad = (reactFlowInstance: OnLoadParams) => { + reactFlowInstance.fitView(); + console.log(reactFlowInstance.getElements()); +}; + +const initialElements = getElements(); + +const multiSelectionKeyCode = ['ShiftLeft', 'ShiftRight']; +const deleteKeyCode = ['AltLeft+KeyD', 'Backspace']; + +const EdgeTypesFlow = () => { + const [elements, setElements] = useState(initialElements); + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + + return ( + + + + + + ); +}; + +export default EdgeTypesFlow; diff --git a/example/src_oldapi/EdgeTypes/utils.ts b/example/src_oldapi/EdgeTypes/utils.ts new file mode 100644 index 00000000..727501a7 --- /dev/null +++ b/example/src_oldapi/EdgeTypes/utils.ts @@ -0,0 +1,106 @@ +import { ElementId, Elements, Position } from 'react-flow-renderer'; + +const nodeWidth = 80; +const nodeGapWidth = nodeWidth * 2; +const nodeStyle = { width: nodeWidth, fontSize: 11, color: 'white' }; + +const sourceTargetPositions = [ + { source: Position.Bottom, target: Position.Top }, + { source: Position.Right, target: Position.Left }, +]; +const nodeColors = [ + ['#1e9e99', '#4cb3ac', '#6ec9c0', '#8ddfd4'], + ['#0f4c75', '#1b5d8b', '#276fa1', '#3282b8'], +]; +const edgeTypes = ['default', 'step', 'smoothstep', 'straight']; +const offsets = [ + { + x: 0, + y: -nodeGapWidth, + }, + { + x: nodeGapWidth, + y: -nodeGapWidth, + }, + { + x: nodeGapWidth, + y: 0, + }, + { + x: nodeGapWidth, + y: nodeGapWidth, + }, + { + x: 0, + y: nodeGapWidth, + }, + { + x: -nodeGapWidth, + y: nodeGapWidth, + }, + { + x: -nodeGapWidth, + y: 0, + }, + { + x: -nodeGapWidth, + y: -nodeGapWidth, + }, +]; + +let id = 0; +const getNodeId = (): ElementId => (id++).toString(); + +export function getElements(): Elements { + const initialElements = []; + + for (let sourceTargetIndex = 0; sourceTargetIndex < sourceTargetPositions.length; sourceTargetIndex++) { + const currSourceTargetPos = sourceTargetPositions[sourceTargetIndex]; + + for (let edgeTypeIndex = 0; edgeTypeIndex < edgeTypes.length; edgeTypeIndex++) { + const currEdgeType = edgeTypes[edgeTypeIndex]; + + for (let offsetIndex = 0; offsetIndex < offsets.length; offsetIndex++) { + const currOffset = offsets[offsetIndex]; + + const style = { ...nodeStyle, background: nodeColors[sourceTargetIndex][edgeTypeIndex] }; + const sourcePosition = { + x: offsetIndex * nodeWidth * 4, + y: edgeTypeIndex * 300 + sourceTargetIndex * edgeTypes.length * 300, + }; + const sourceId = getNodeId(); + const sourceData = { label: `Source ${sourceId}` }; + const sourceNode = { + id: sourceId, + style, + data: sourceData, + position: sourcePosition, + sourcePosition: currSourceTargetPos.source, + targetPosition: currSourceTargetPos.target, + }; + + const targetId = getNodeId(); + const targetData = { label: `Target ${targetId}` }; + const targetPosition = { + x: sourcePosition.x + currOffset.x, + y: sourcePosition.y + currOffset.y, + }; + const targetNode = { + id: targetId, + style, + data: targetData, + position: targetPosition, + sourcePosition: currSourceTargetPos.source, + targetPosition: currSourceTargetPos.target, + }; + + initialElements.push(sourceNode); + initialElements.push(targetNode); + + initialElements.push({ id: `${sourceId}-${targetId}`, source: sourceId, target: targetId, type: currEdgeType }); + } + } + } + + return initialElements; +} diff --git a/example/src_oldapi/Edges/CustomEdge.tsx b/example/src_oldapi/Edges/CustomEdge.tsx new file mode 100644 index 00000000..f72ff831 --- /dev/null +++ b/example/src_oldapi/Edges/CustomEdge.tsx @@ -0,0 +1,31 @@ +import { FC } from 'react'; +import { EdgeProps, getBezierPath, getMarkerEnd } from 'react-flow-renderer'; + +const CustomEdge: FC = ({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + data, + arrowHeadType, + markerEndId, +}) => { + const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition }); + const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); + + return ( + <> + + + + {data.text} + + + + ); +}; + +export default CustomEdge; diff --git a/example/src_oldapi/Edges/CustomEdge2.tsx b/example/src_oldapi/Edges/CustomEdge2.tsx new file mode 100644 index 00000000..361b7f92 --- /dev/null +++ b/example/src_oldapi/Edges/CustomEdge2.tsx @@ -0,0 +1,44 @@ +import { FC } from 'react'; +import { EdgeProps, getBezierPath, getMarkerEnd, EdgeText, getEdgeCenter } from 'react-flow-renderer'; + +const CustomEdge: FC = ({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + data, + arrowHeadType, + markerEndId, +}) => { + const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition }); + const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); + const [centerX, centerY] = getEdgeCenter({ + sourceX, + sourceY, + targetX, + targetY, + }); + + return ( + <> + + console.log(data)} + /> + ; + + ); +}; + +export default CustomEdge; diff --git a/example/src_oldapi/Edges/index.tsx b/example/src_oldapi/Edges/index.tsx new file mode 100644 index 00000000..39320c6d --- /dev/null +++ b/example/src_oldapi/Edges/index.tsx @@ -0,0 +1,115 @@ +import React, { useState, MouseEvent } from 'react'; + +import ReactFlow, { + removeElements, + addEdge, + MiniMap, + Controls, + Background, + OnLoadParams, + FlowElement, + EdgeTypesType, + Elements, + Connection, + Edge, + ArrowHeadType, + Node, +} from 'react-flow-renderer'; + +import CustomEdge from './CustomEdge'; +import CustomEdge2 from './CustomEdge2'; + +const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView(); +const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); +const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); + +const initialElements: Elements = [ + { id: '1', type: 'input', data: { label: 'Input 1' }, position: { x: 250, y: 0 } }, + { id: '2', data: { label: 'Node 2' }, position: { x: 150, y: 100 } }, + { id: '2a', data: { label: 'Node 2a' }, position: { x: 0, y: 180 } }, + { id: '3', data: { label: 'Node 3' }, position: { x: 250, y: 200 } }, + { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 300 } }, + { id: '3a', data: { label: 'Node 3a' }, position: { x: 150, y: 300 } }, + { id: '5', data: { label: 'Node 5' }, position: { x: 250, y: 400 } }, + { id: '6', type: 'output', data: { label: 'Output 6' }, position: { x: 50, y: 550 } }, + { id: '7', type: 'output', data: { label: 'Output 7' }, position: { x: 250, y: 550 } }, + { id: '8', type: 'output', data: { label: 'Output 8' }, position: { x: 525, y: 600 } }, + { id: '9', type: 'output', data: { label: 'Output 9' }, position: { x: 675, y: 500 } }, + { id: 'e1-2', source: '1', target: '2', label: 'bezier edge (default)', className: 'normal-edge' }, + { id: 'e2-2a', source: '2', target: '2a', type: 'smoothstep', label: 'smoothstep edge' }, + { id: 'e2-3', source: '2', target: '3', type: 'step', label: 'step edge' }, + { id: 'e3-4', source: '3', target: '4', type: 'straight', label: 'straight edge' }, + { id: 'e3-3a', source: '3', target: '3a', type: 'straight', label: 'label only edge', style: { stroke: 'none' } }, + { id: 'e3-5', source: '4', target: '5', animated: true, label: 'animated styled edge', style: { stroke: 'red' } }, + { + id: 'e5-6', + source: '5', + target: '6', + label: ( + <> + i am using + + {''} + + + ), + labelStyle: { fill: 'red', fontWeight: 700 }, + arrowHeadType: ArrowHeadType.Arrow, + }, + { + id: 'e5-7', + source: '5', + target: '7', + label: 'label with styled bg', + labelBgPadding: [8, 4], + labelBgBorderRadius: 4, + labelBgStyle: { fill: '#FFCC00', color: '#fff', fillOpacity: 0.7 }, + arrowHeadType: ArrowHeadType.ArrowClosed, + }, + { + id: 'e5-8', + source: '5', + target: '8', + type: 'custom', + data: { text: 'custom edge' }, + arrowHeadType: ArrowHeadType.ArrowClosed, + }, + { + id: 'e5-9', + source: '5', + target: '9', + type: 'custom2', + data: { text: 'custom edge 2' }, + }, +]; + +const edgeTypes: EdgeTypesType = { + custom: CustomEdge, + custom2: CustomEdge2, +}; + +const EdgesFlow = () => { + const [elements, setElements] = useState(initialElements); + + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + + return ( + + + + + + ); +}; + +export default EdgesFlow; diff --git a/example/src_oldapi/Empty/index.tsx b/example/src_oldapi/Empty/index.tsx new file mode 100644 index 00000000..9bfc7c25 --- /dev/null +++ b/example/src_oldapi/Empty/index.tsx @@ -0,0 +1,60 @@ +import React, { useState, MouseEvent, CSSProperties } from 'react'; + +import ReactFlow, { + removeElements, + addEdge, + MiniMap, + Controls, + Background, + OnLoadParams, + Elements, + ElementId, + Node, + FlowElement, + BackgroundVariant, + Connection, + Edge, +} from 'react-flow-renderer'; + +const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance); +const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); +const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); + +const buttonStyle: CSSProperties = { position: 'absolute', left: 10, top: 10, zIndex: 4 }; + +const EmptyFlow = () => { + const [elements, setElements] = useState([]); + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + const addRandomNode = () => { + const nodeId: ElementId = (elements.length + 1).toString(); + const newNode: Node = { + id: nodeId, + data: { label: `Node: ${nodeId}` }, + position: { x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight }, + }; + setElements((els) => els.concat(newNode)); + }; + + return ( + onConnect(p)} + onNodeDragStop={onNodeDragStop} + onlyRenderVisibleElements={false} + > + + + + + + + ); +}; + +export default EmptyFlow; diff --git a/example/src_oldapi/Hidden/index.tsx b/example/src_oldapi/Hidden/index.tsx new file mode 100644 index 00000000..9869b2bc --- /dev/null +++ b/example/src_oldapi/Hidden/index.tsx @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; + +import { useEffect } from 'react'; +import ReactFlow, { addEdge, MiniMap, Controls, Connection, Edge, Elements } from 'react-flow-renderer'; + +const initialElements: Elements = [ + { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, + { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, + { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, + { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, + { id: 'e1-2', source: '1', target: '2' }, + { id: 'e1-3', source: '1', target: '3' }, + { id: 'e3-4', source: '3', target: '4' }, +]; + +const HiddenFlow = () => { + const [elements, setElements] = useState(initialElements); + const [isHidden, setIsHidden] = useState(false); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + + useEffect(() => { + setElements((els) => + els.map((e) => { + e.isHidden = isHidden; + return e; + }) + ); + }, [isHidden]); + + return ( + + + + +
+
+ +
+
+
+ ); +}; + +export default HiddenFlow; diff --git a/example/src_oldapi/Interaction/index.tsx b/example/src_oldapi/Interaction/index.tsx new file mode 100644 index 00000000..2ca7900c --- /dev/null +++ b/example/src_oldapi/Interaction/index.tsx @@ -0,0 +1,226 @@ +import React, { useState, MouseEvent, WheelEvent } from 'react'; +import ReactFlow, { + addEdge, + MiniMap, + Controls, + Elements, + Node, + FlowElement, + Connection, + Edge, + PanOnScrollMode, + FlowTransform, +} from 'react-flow-renderer'; + +const initialElements: Elements = [ + { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, + { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, + { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, + { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, + { id: 'e1-2', source: '1', target: '2', animated: true }, + { id: 'e1-3', source: '1', target: '3' }, +]; + +const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node); +const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); +const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); +const onPaneClick = (event: MouseEvent) => console.log('onPaneClick', event); +const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event); +const onPaneContextMenu = (event: MouseEvent) => console.log('onPaneContextMenu', event); +const onMoveEnd = (flowTranasform?: FlowTransform) => console.log('onMoveEnd', flowTranasform); + +const InteractionFlow = () => { + const [elements, setElements] = useState(initialElements); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + + const [isSelectable, setIsSelectable] = useState(false); + const [isDraggable, setIsDraggable] = useState(false); + const [isConnectable, setIsConnectable] = useState(false); + const [zoomOnScroll, setZoomOnScroll] = useState(false); + const [zoomOnPinch, setZoomOnPinch] = useState(false); + const [panOnScroll, setPanOnScroll] = useState(false); + const [panOnScrollMode, setPanOnScrollMode] = useState(PanOnScrollMode.Free); + const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState(false); + const [paneMoveable, setPaneMoveable] = useState(true); + const [captureZoomClick, setCaptureZoomClick] = useState(false); + const [captureZoomScroll, setCaptureZoomScroll] = useState(false); + const [captureElementClick, setCaptureElementClick] = useState(false); + + return ( + + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ ); +}; + +export default InteractionFlow; diff --git a/example/src_oldapi/Layouting/index.tsx b/example/src_oldapi/Layouting/index.tsx new file mode 100644 index 00000000..34ee23e6 --- /dev/null +++ b/example/src_oldapi/Layouting/index.tsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react'; +import ReactFlow, { + ReactFlowProvider, + addEdge, + removeElements, + Controls, + isNode, + Elements, + Connection, + Edge, + NodeExtent, + Position, +} from 'react-flow-renderer'; +import dagre from 'dagre'; + +import initialElements from './initial-elements'; + +import './layouting.css'; + +const dagreGraph = new dagre.graphlib.Graph(); +dagreGraph.setDefaultEdgeLabel(() => ({})); + +const nodeExtent: NodeExtent = [ + [0, 0], + [1000, 1000], +]; + +const LayoutFlow = () => { + const [elements, setElements] = useState(initialElements); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + + const onLayout = (direction: string) => { + const isHorizontal = direction === 'LR'; + dagreGraph.setGraph({ rankdir: direction }); + + elements.forEach((el) => { + if (isNode(el)) { + dagreGraph.setNode(el.id, { width: 150, height: 50 }); + } else { + dagreGraph.setEdge(el.source, el.target); + } + }); + + dagre.layout(dagreGraph); + + const layoutedElements = elements.map((el) => { + if (isNode(el)) { + const nodeWithPosition = dagreGraph.node(el.id); + el.targetPosition = isHorizontal ? Position.Left : Position.Top; + el.sourcePosition = isHorizontal ? Position.Right : Position.Bottom; + // we need to pass a slightly different position in order to notify react flow about the change + // @TODO how can we change the position handling so that we dont need this hack? + el.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y }; + } + + return el; + }); + + setElements(layoutedElements); + }; + + return ( +
+ + onLayout('TB')} + > + + +
+ + +
+
+
+ ); +}; + +export default LayoutFlow; diff --git a/example/src_oldapi/Layouting/initial-elements.ts b/example/src_oldapi/Layouting/initial-elements.ts new file mode 100644 index 00000000..404b1b8b --- /dev/null +++ b/example/src_oldapi/Layouting/initial-elements.ts @@ -0,0 +1,71 @@ +import { Elements, XYPosition } from 'react-flow-renderer'; + +const position: XYPosition = { x: 0, y: 0 }; + +const elements: Elements = [ + { + id: '1', + type: 'input', + data: { label: 'input' }, + position, + }, + { + id: '2', + data: { label: 'node 2' }, + position, + }, + { + id: '2a', + data: { label: 'node 2a' }, + position, + }, + { + id: '2b', + data: { label: 'node 2b' }, + position, + }, + { + id: '2c', + data: { label: 'node 2c' }, + position, + }, + { + id: '2d', + data: { label: 'node 2d' }, + position, + }, + { + id: '3', + data: { label: 'node 3' }, + position, + }, + { + id: '4', + data: { label: 'node 4' }, + position, + }, + { + id: '5', + data: { label: 'node 5' }, + position, + }, + { + id: '6', + type: 'output', + data: { label: 'output' }, + position, + }, + { id: '7', type: 'output', data: { label: 'output' }, position: { x: 400, y: 450 } }, + { id: 'e12', source: '1', target: '2', type: 'smoothstep' }, + { id: 'e13', source: '1', target: '3', type: 'smoothstep' }, + { id: 'e22a', source: '2', target: '2a', type: 'smoothstep' }, + { id: 'e22b', source: '2', target: '2b', type: 'smoothstep' }, + { id: 'e22c', source: '2', target: '2c', type: 'smoothstep' }, + { id: 'e2c2d', source: '2c', target: '2d', type: 'smoothstep' }, + + { id: 'e45', source: '4', target: '5', type: 'smoothstep' }, + { id: 'e56', source: '5', target: '6', type: 'smoothstep' }, + { id: 'e57', source: '5', target: '7', type: 'smoothstep' }, +]; + +export default elements; diff --git a/example/src_oldapi/Layouting/layouting.css b/example/src_oldapi/Layouting/layouting.css new file mode 100644 index 00000000..6d248b22 --- /dev/null +++ b/example/src_oldapi/Layouting/layouting.css @@ -0,0 +1,11 @@ +.layoutflow { + flex-grow: 1; + position: relative; +} + +.layoutflow .controls { + position: absolute; + right: 10px; + top: 10px; + z-index: 10; +} diff --git a/example/src_oldapi/MultiFlows/index.tsx b/example/src_oldapi/MultiFlows/index.tsx new file mode 100644 index 00000000..8e185992 --- /dev/null +++ b/example/src_oldapi/MultiFlows/index.tsx @@ -0,0 +1,45 @@ +import React, { useState, FC } from 'react'; + +import ReactFlow, { + removeElements, + addEdge, + Background, + Elements, + Edge, + Connection, + ReactFlowProvider, +} from 'react-flow-renderer'; + +import './multiflows.css'; + +const initialElements: Elements = [ + { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, + { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, + { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, + { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' }, + { id: 'e1-2', source: '1', target: '2', animated: true }, + { id: 'e1-3', source: '1', target: '3' }, +]; + +const Flow: FC = () => { + const [elements, setElements] = useState(initialElements); + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = (params: Edge | Connection) => setElements((els) => addEdge(params, els)); + + return ( + + + + + + ); +}; + +const MultiFlows: FC = () => ( +
+ + +
+); + +export default MultiFlows; diff --git a/example/src_oldapi/MultiFlows/multiflows.css b/example/src_oldapi/MultiFlows/multiflows.css new file mode 100644 index 00000000..b7017390 --- /dev/null +++ b/example/src_oldapi/MultiFlows/multiflows.css @@ -0,0 +1,13 @@ +.react-flow__example-multiflows { + display: flex; + height: 100%; +} + +.react-flow__example-multiflows .react-flow { + width: 100%; + height: 100%; +} + +.react-flow__example-multiflows .react-flow:first-child { + border-right: 2px solid #333; +} diff --git a/example/src_oldapi/NodeTypeChange/index.tsx b/example/src_oldapi/NodeTypeChange/index.tsx new file mode 100644 index 00000000..c5e4ff90 --- /dev/null +++ b/example/src_oldapi/NodeTypeChange/index.tsx @@ -0,0 +1,55 @@ +import React, { useState, CSSProperties } from 'react'; + +import ReactFlow, { addEdge, isEdge, OnLoadParams, Elements, Position, Connection, Edge } from 'react-flow-renderer'; + +const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView(); + +const initialElements: Elements = [ + { + id: '1', + sourcePosition: Position.Right, + type: 'input', + data: { label: 'Input' }, + position: { x: 0, y: 80 }, + }, + { + id: '2', + type: 'output', + sourcePosition: Position.Right, + targetPosition: Position.Left, + data: { label: 'A Node' }, + position: { x: 250, y: 0 }, + }, + { id: 'e1-2', source: '1', type: 'smoothstep', target: '2', animated: true }, +]; + +const buttonStyle: CSSProperties = { position: 'absolute', right: 10, top: 30, zIndex: 4 }; + +const NodeTypeChangeFlow = () => { + const [elements, setElements] = useState(initialElements); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + const changeType = () => { + setElements((elms) => + elms.map((el) => { + if (isEdge(el) || el.type === 'input') { + return el; + } + + return { + ...el, + type: el.type === 'default' ? 'output' : 'default', + }; + }) + ); + }; + + return ( + + + + ); +}; + +export default NodeTypeChangeFlow; diff --git a/example/src_oldapi/NodeTypesObjectChange/index.tsx b/example/src_oldapi/NodeTypesObjectChange/index.tsx new file mode 100644 index 00000000..54b34fcc --- /dev/null +++ b/example/src_oldapi/NodeTypesObjectChange/index.tsx @@ -0,0 +1,76 @@ +import React, { useState, CSSProperties, FC } from 'react'; + +import ReactFlow, { + addEdge, + Elements, + Position, + Connection, + Edge, + NodeProps, + NodeTypesType, +} from 'react-flow-renderer'; + +const initialElements: Elements = [ + { + id: '1', + sourcePosition: Position.Right, + type: 'input', + data: { label: 'Input' }, + position: { x: 0, y: 80 }, + }, + { + id: '2', + type: 'a', + sourcePosition: Position.Right, + targetPosition: Position.Left, + data: { label: 'A Node' }, + position: { x: 250, y: 0 }, + }, +]; + +const buttonStyle: CSSProperties = { position: 'absolute', right: 10, top: 30, zIndex: 4 }; + +const nodeStyles: CSSProperties = { padding: '10px 15px', border: '1px solid #ddd' }; + +const NodeA: FC = () => { + return
A
; +}; + +const NodeB: FC = () => { + return
B
; +}; + +type NodeTypesObject = { + [key: string]: NodeTypesType; +}; + +const nodeTypesObjects: NodeTypesObject = { + a: { + a: NodeA, + }, + b: { + b: NodeB, + }, +}; + +const NodeTypeChangeFlow = () => { + const [nodeTypesId, setNodeTypesId] = useState('a'); + const [elements, setElements] = useState(initialElements); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + const changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a')); + + return ( + + + + ); +}; + +export default NodeTypeChangeFlow; diff --git a/example/src_oldapi/Overview/index.tsx b/example/src_oldapi/Overview/index.tsx new file mode 100644 index 00000000..ee13f916 --- /dev/null +++ b/example/src_oldapi/Overview/index.tsx @@ -0,0 +1,202 @@ +import React, { useState, MouseEvent, CSSProperties } from 'react'; + +import ReactFlow, { + removeElements, + addEdge, + MiniMap, + Controls, + Background, + isNode, + Node, + Elements, + FlowElement, + OnLoadParams, + FlowTransform, + SnapGrid, + ArrowHeadType, + Connection, + Edge, +} from 'react-flow-renderer'; + +const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node); +const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node); +const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); +const onNodeDoubleClick = (_: MouseEvent, node: Node) => console.log('node double click', node); +const onPaneClick = (event: MouseEvent) => console.log('pane click', event); +const onPaneScroll = (event?: MouseEvent) => console.log('pane scroll', event); +const onPaneContextMenu = (event: MouseEvent) => console.log('pane context menu', event); +const onSelectionDrag = (_: MouseEvent, nodes: Node[]) => console.log('selection drag', nodes); +const onSelectionDragStart = (_: MouseEvent, nodes: Node[]) => console.log('selection drag start', nodes); +const onSelectionDragStop = (_: MouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes); +const onSelectionContextMenu = (event: MouseEvent, nodes: Node[]) => { + event.preventDefault(); + console.log('selection context menu', nodes); +}; +const onElementClick = (_: MouseEvent, element: FlowElement) => + console.log(`${isNode(element) ? 'node' : 'edge'} click:`, element); +const onSelectionChange = (elements: Elements | null) => console.log('selection change', elements); +const onLoad = (reactFlowInstance: OnLoadParams) => { + console.log('flow loaded:', reactFlowInstance); + reactFlowInstance.fitView(); +}; + +const onMoveStart = (transform?: FlowTransform) => console.log('zoom/move start', transform); +const onMoveEnd = (transform?: FlowTransform) => console.log('zoom/move end', transform); +const onEdgeContextMenu = (_: MouseEvent, edge: Edge) => console.log('edge context menu', edge); +const onEdgeMouseEnter = (_: MouseEvent, edge: Edge) => console.log('edge mouse enter', edge); +const onEdgeMouseMove = (_: MouseEvent, edge: Edge) => console.log('edge mouse move', edge); +const onEdgeMouseLeave = (_: MouseEvent, edge: Edge) => console.log('edge mouse leave', edge); +const onEdgeDoubleClick = (_: MouseEvent, edge: Edge) => console.log('edge double click', edge); + +const initialElements: Elements = [ + { + id: '1', + type: 'input', + data: { + label: ( + <> + Welcome to React Flow! + + ), + }, + position: { x: 250, y: 0 }, + }, + { + id: '2', + data: { + label: ( + <> + This is a default node + + ), + }, + position: { x: 100, y: 100 }, + }, + { + id: '3', + data: { + label: ( + <> + This one has a custom style + + ), + }, + position: { x: 400, y: 100 }, + style: { background: '#D6D5E6', color: '#333', border: '1px solid #222138', width: 180 }, + }, + { + id: '4', + position: { x: 250, y: 200 }, + data: { + label: ( + <> + You can find the docs on{' '} + + Github + + + ), + }, + }, + { + id: '5', + data: { + label: ( + <> + Or check out the other examples + + ), + }, + position: { x: 250, y: 325 }, + }, + { + id: '6', + type: 'output', + data: { + label: ( + <> + An output node + + ), + }, + position: { x: 100, y: 480 }, + }, + { id: '7', type: 'output', data: { label: 'Another output node' }, position: { x: 400, y: 450 } }, + { id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' }, + { id: 'e1-3', source: '1', target: '3' }, + { id: 'e3-4', source: '3', target: '4', animated: true, label: 'animated edge' }, + { id: 'e4-5', source: '4', target: '5', arrowHeadType: ArrowHeadType.Arrow, label: 'edge with arrow head' }, + { id: 'e5-6', source: '5', target: '6', type: 'smoothstep', label: 'smooth step edge' }, + { + id: 'e5-7', + source: '5', + target: '7', + type: 'step', + style: { stroke: '#f6ab6c' }, + label: 'a step edge', + animated: true, + labelStyle: { fill: '#f6ab6c', fontWeight: 700 }, + }, +]; + +const connectionLineStyle: CSSProperties = { stroke: '#ddd' }; +const snapGrid: SnapGrid = [16, 16]; + +const nodeStrokeColor = (n: Node): string => { + if (n.style?.background) return n.style.background as string; + if (n.type === 'input') return '#0041d0'; + if (n.type === 'output') return '#ff0072'; + if (n.type === 'default') return '#1a192b'; + + return '#eee'; +}; + +const nodeColor = (n: Node): string => { + if (n.style?.background) return n.style.background as string; + + return '#fff'; +}; + +const OverviewFlow = () => { + const [elements, setElements] = useState(initialElements); + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + + return ( + + + + + + ); +}; + +export default OverviewFlow; diff --git a/example/src_oldapi/Provider/Sidebar.tsx b/example/src_oldapi/Provider/Sidebar.tsx new file mode 100644 index 00000000..0b2c5f24 --- /dev/null +++ b/example/src_oldapi/Provider/Sidebar.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { useStoreState, useStoreActions } from 'react-flow-renderer'; + +const Sidebar = () => { + const nodes = useStoreState((store) => store.nodes); + const transform = useStoreState((store) => store.transform); + const setSelectedElements = useStoreActions((actions) => actions.setSelectedElements); + + const selectAll = () => { + setSelectedElements(nodes.map((node) => ({ id: node.id, type: node.type }))); + }; + + return ( + + ); +}; + +export default Sidebar; diff --git a/example/src_oldapi/Provider/index.tsx b/example/src_oldapi/Provider/index.tsx new file mode 100644 index 00000000..a7a78b7c --- /dev/null +++ b/example/src_oldapi/Provider/index.tsx @@ -0,0 +1,57 @@ +import React, { useState, MouseEvent } from 'react'; +import ReactFlow, { + ReactFlowProvider, + addEdge, + removeElements, + Controls, + OnLoadParams, + FlowElement, + Connection, + Edge, + Elements, + ConnectionMode, +} from 'react-flow-renderer'; + +import Sidebar from './Sidebar'; + +import './provider.css'; + +const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); +const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance); + +const initialElements: Elements = [ + { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, + { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, + { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, + { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, + { id: 'e1-2', source: '1', target: '2', animated: true }, + { id: 'e1-3', source: '1', target: '3' }, +]; + +const ProviderFlow = () => { + const [elements, setElements] = useState(initialElements); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + + return ( +
+ + +
+ + + +
+
+
+ ); +}; + +export default ProviderFlow; diff --git a/example/src_oldapi/Provider/provider.css b/example/src_oldapi/Provider/provider.css new file mode 100644 index 00000000..aa719acc --- /dev/null +++ b/example/src_oldapi/Provider/provider.css @@ -0,0 +1,45 @@ +.providerflow { + flex-direction: column; + display: flex; + height: 100%; +} + +.providerflow aside { + border-right: 1px solid #eee; + padding: 15px 10px; + font-size: 12px; + background: #fcfcfc; +} + +.providerflow aside .description { + margin-bottom: 10px; +} + +.providerflow aside .title { + font-weight: 700; + margin-bottom: 5px; +} + +.providerflow aside .transform { + margin-bottom: 20px; +} + +.providerflow .reactflow-wrapper { + flex-grow: 1; + height: 100%; +} + +.providerflow .selectall { + margin-top: 10px; +} + +@media screen and (min-width: 768px) { + .providerflow { + flex-direction: row; + } + + .providerflow aside { + width: 20%; + max-width: 250px; + } +} \ No newline at end of file diff --git a/example/src_oldapi/SaveRestore/Controls.tsx b/example/src_oldapi/SaveRestore/Controls.tsx new file mode 100644 index 00000000..f79f5e47 --- /dev/null +++ b/example/src_oldapi/SaveRestore/Controls.tsx @@ -0,0 +1,61 @@ +import React, { memo, useCallback, Dispatch, FC } from 'react'; +import { useZoomPanHelper, OnLoadParams, Elements, FlowExportObject } from 'react-flow-renderer'; +import localforage from 'localforage'; + +localforage.config({ + name: 'react-flow', + storeName: 'flows', +}); + +const flowKey = 'example-flow'; + +const getNodeId = () => `randomnode_${+new Date()}`; + +type ControlsProps = { + rfInstance?: OnLoadParams; + setElements: Dispatch>>; +}; + +const Controls: FC = ({ rfInstance, setElements }) => { + const { transform } = useZoomPanHelper(); + + const onSave = useCallback(() => { + if (rfInstance) { + const flow = rfInstance.toObject(); + localforage.setItem(flowKey, flow); + } + }, [rfInstance]); + + const onRestore = useCallback(() => { + const restoreFlow = async () => { + const flow: FlowExportObject | null = await localforage.getItem(flowKey); + + if (flow) { + const [x = 0, y = 0] = flow.position; + setElements(flow.elements || []); + transform({ x, y, zoom: flow.zoom || 0 }); + } + }; + + restoreFlow(); + }, [setElements, transform]); + + const onAdd = useCallback(() => { + const newNode = { + id: `random_node-${getNodeId()}`, + data: { label: 'Added node' }, + position: { x: Math.random() * window.innerWidth - 100, y: Math.random() * window.innerHeight }, + }; + setElements((els) => els.concat(newNode)); + }, [setElements]); + + return ( +
+ + + +
+ ); +}; + +export default memo(Controls); diff --git a/example/src_oldapi/SaveRestore/index.tsx b/example/src_oldapi/SaveRestore/index.tsx new file mode 100644 index 00000000..9457248d --- /dev/null +++ b/example/src_oldapi/SaveRestore/index.tsx @@ -0,0 +1,37 @@ +import React, { useState } from 'react'; +import ReactFlow, { + ReactFlowProvider, + removeElements, + addEdge, + Elements, + Connection, + Edge, + OnLoadParams, +} from 'react-flow-renderer'; + +import Controls from './Controls'; + +import './save.css'; + +const initialElements: Elements = [ + { id: '1', data: { label: 'Node 1' }, position: { x: 100, y: 100 } }, + { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } }, + { id: 'e1-2', source: '1', target: '2' }, +]; + +const SaveRestore = () => { + const [rfInstance, setRfInstance] = useState(); + const [elements, setElements] = useState(initialElements); + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + + return ( + + + + + + ); +}; + +export default SaveRestore; diff --git a/example/src_oldapi/SaveRestore/save.css b/example/src_oldapi/SaveRestore/save.css new file mode 100644 index 00000000..ffd028dd --- /dev/null +++ b/example/src_oldapi/SaveRestore/save.css @@ -0,0 +1,11 @@ +.save__controls { + position: absolute; + right: 10px; + top: 10px; + z-index: 4; + font-size: 12px; +} + +.save__controls button { + margin-left: 5px; +} diff --git a/example/src_oldapi/Stress/index.tsx b/example/src_oldapi/Stress/index.tsx new file mode 100644 index 00000000..b734c6b6 --- /dev/null +++ b/example/src_oldapi/Stress/index.tsx @@ -0,0 +1,70 @@ +import React, { useState, CSSProperties } from 'react'; +import ReactFlow, { + removeElements, + addEdge, + MiniMap, + isNode, + Controls, + Background, + OnLoadParams, + Elements, + Connection, + Edge, +} from 'react-flow-renderer'; + +import { getElements } from './utils'; + +const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 4 }; + +const onLoad = (reactFlowInstance: OnLoadParams) => { + reactFlowInstance.fitView(); + console.log(reactFlowInstance.getElements()); +}; + +const initialElements: Elements = getElements(30, 30); + +const StressFlow = () => { + const [elements, setElements] = useState(initialElements); + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + + const updatePos = () => { + setElements((elms) => { + return elms.map((el) => { + if (isNode(el)) { + return { + ...el, + position: { + x: Math.random() * window.innerWidth, + y: Math.random() * window.innerHeight, + }, + }; + } + + return el; + }); + }); + }; + + const updateElements = () => { + const grid = Math.ceil(Math.random() * 10); + setElements(getElements(grid, grid)); + }; + + return ( + + + + + +
+ + +
+
+ ); +}; + +export default StressFlow; diff --git a/example/src_oldapi/Stress/utils.ts b/example/src_oldapi/Stress/utils.ts new file mode 100644 index 00000000..a37eb8de --- /dev/null +++ b/example/src_oldapi/Stress/utils.ts @@ -0,0 +1,30 @@ +import { Elements } from 'react-flow-renderer'; + +export function getElements(xElements: number = 10, yElements: number = 10): Elements { + const initialElements = []; + let nodeId = 1; + let recentNodeId = null; + + for (let y = 0; y < yElements; y++) { + for (let x = 0; x < xElements; x++) { + const position = { x: x * 100, y: y * 50 }; + const data = { label: `Node ${nodeId}` }; + const node = { + id: nodeId.toString(), + style: { width: 50, fontSize: 11 }, + data, + position, + }; + initialElements.push(node); + + if (recentNodeId && nodeId <= xElements * yElements) { + initialElements.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() }); + } + + recentNodeId = nodeId; + nodeId++; + } + } + + return initialElements; +} diff --git a/example/src_oldapi/Switch/index.tsx b/example/src_oldapi/Switch/index.tsx new file mode 100644 index 00000000..03dacf99 --- /dev/null +++ b/example/src_oldapi/Switch/index.tsx @@ -0,0 +1,53 @@ +import React, { useState, MouseEvent } from 'react'; + +import ReactFlow, { removeElements, addEdge, Node, FlowElement, Elements, Connection, Edge } from 'react-flow-renderer'; + +const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); +const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); + +const elementsA: Elements = [ + { id: '1a', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, + { id: '2a', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, + { id: '3a', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, + { id: '4a', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' }, + { id: 'e1-2', source: '1a', target: '2a' }, + { id: 'e1-3', source: '1a', target: '3a' }, +]; + +const elementsB: Elements = [ + { id: 'inputb', type: 'input', data: { label: 'Input' }, position: { x: 300, y: 5 }, className: 'light' }, + { id: '1b', data: { label: 'Node 1' }, position: { x: 0, y: 100 }, className: 'light' }, + { id: '2b', data: { label: 'Node 2' }, position: { x: 200, y: 100 }, className: 'light' }, + { id: '3b', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, + { id: '4b', data: { label: 'Node 4' }, position: { x: 600, y: 100 }, className: 'light' }, + + { id: 'e1b', source: 'inputb', target: '1b' }, + { id: 'e2b', source: 'inputb', target: '2b' }, + { id: 'e3b', source: 'inputb', target: '3b' }, + { id: 'e4b', source: 'inputb', target: '4b' }, +]; + +const BasicFlow = () => { + const [elements, setElements] = useState(elementsA); + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + + return ( + +
+ + +
+
+ ); +}; + +export default BasicFlow; diff --git a/example/src_oldapi/Undirectional/CustomNode.tsx b/example/src_oldapi/Undirectional/CustomNode.tsx new file mode 100644 index 00000000..ab5853e8 --- /dev/null +++ b/example/src_oldapi/Undirectional/CustomNode.tsx @@ -0,0 +1,19 @@ +import React, { memo, FC, CSSProperties } from 'react'; + +import { Handle, Position, NodeProps } from 'react-flow-renderer'; + +const nodeStyles: CSSProperties = { padding: '10px 15px', border: '1px solid #ddd' }; + +const CustomNode: FC = ({ id }) => { + return ( +
+
node {id}
+ + + + +
+ ); +}; + +export default memo(CustomNode); diff --git a/example/src_oldapi/Undirectional/index.tsx b/example/src_oldapi/Undirectional/index.tsx new file mode 100644 index 00000000..f05ad21e --- /dev/null +++ b/example/src_oldapi/Undirectional/index.tsx @@ -0,0 +1,221 @@ +import React, { useState, useCallback } from 'react'; + +import ReactFlow, { + NodeTypesType, + addEdge, + useZoomPanHelper, + ReactFlowProvider, + Elements, + Connection, + Edge, + ElementId, + ConnectionLineType, + ConnectionMode, + updateEdge, + ArrowHeadType, +} from 'react-flow-renderer'; +import CustomNode from './CustomNode'; + +const initialElements: Elements = [ + { + id: '00', + type: 'custom', + position: { x: 300, y: 250 }, + }, + { + id: '01', + type: 'custom', + position: { x: 100, y: 50 }, + }, + { + id: '02', + type: 'custom', + position: { x: 500, y: 50 }, + }, + { + id: '03', + type: 'custom', + position: { x: 500, y: 500 }, + }, + { + id: '04', + type: 'custom', + position: { x: 100, y: 500 }, + }, + { + id: '10', + type: 'custom', + position: { x: 300, y: 5 }, + }, + { + id: '20', + type: 'custom', + position: { x: 600, y: 250 }, + }, + { + id: '30', + type: 'custom', + position: { x: 300, y: 600 }, + }, + { + id: '40', + type: 'custom', + position: { x: 5, y: 250 }, + }, + { + id: 'e0-1a', + source: '00', + target: '01', + sourceHandle: 'left', + targetHandle: 'bottom', + type: 'smoothstep', + arrowHeadType: ArrowHeadType.Arrow, + }, + { + id: 'e0-1b', + source: '00', + target: '01', + sourceHandle: 'top', + targetHandle: 'right', + type: 'smoothstep', + arrowHeadType: ArrowHeadType.Arrow, + }, + { + id: 'e0-2a', + source: '00', + target: '02', + sourceHandle: 'top', + targetHandle: 'left', + type: 'smoothstep', + arrowHeadType: ArrowHeadType.Arrow, + }, + { + id: 'e0-2b', + source: '00', + target: '02', + sourceHandle: 'right', + targetHandle: 'bottom', + type: 'smoothstep', + arrowHeadType: ArrowHeadType.Arrow, + }, + { + id: 'e0-3a', + source: '00', + target: '03', + sourceHandle: 'right', + targetHandle: 'top', + type: 'smoothstep', + arrowHeadType: ArrowHeadType.Arrow, + }, + { + id: 'e0-3b', + source: '00', + target: '03', + sourceHandle: 'bottom', + targetHandle: 'left', + type: 'smoothstep', + arrowHeadType: ArrowHeadType.Arrow, + }, + { + id: 'e0-4a', + source: '00', + target: '04', + sourceHandle: 'bottom', + targetHandle: 'right', + type: 'smoothstep', + arrowHeadType: ArrowHeadType.Arrow, + }, + { + id: 'e0-4b', + source: '00', + target: '04', + sourceHandle: 'left', + targetHandle: 'top', + type: 'smoothstep', + arrowHeadType: ArrowHeadType.Arrow, + }, + { + id: 'e0-10', + source: '00', + target: '10', + sourceHandle: 'top', + targetHandle: 'bottom', + type: 'smoothstep', + arrowHeadType: ArrowHeadType.Arrow, + }, + { + id: 'e0-20', + source: '00', + target: '20', + sourceHandle: 'right', + targetHandle: 'left', + type: 'smoothstep', + arrowHeadType: ArrowHeadType.Arrow, + }, + { + id: 'e0-30', + source: '00', + target: '30', + sourceHandle: 'bottom', + targetHandle: 'top', + type: 'smoothstep', + arrowHeadType: ArrowHeadType.Arrow, + }, + { + id: 'e0-40', + source: '00', + target: '40', + sourceHandle: 'left', + targetHandle: 'right', + type: 'smoothstep', + arrowHeadType: ArrowHeadType.Arrow, + }, +]; + +const nodeTypes: NodeTypesType = { + custom: CustomNode, +}; + +let id = 4; +const getId = (): ElementId => `${id++}`; + +const UpdateNodeInternalsFlow = () => { + const [elements, setElements] = useState(initialElements); + const onConnect = (params: Connection | Edge) => + setElements((els) => addEdge({ ...params, type: 'smoothstep' }, els)); + const { project } = useZoomPanHelper(); + const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => + setElements((els) => updateEdge(oldEdge, newConnection, els)); + + const onPaneClick = useCallback( + (evt) => + setElements((els) => + els.concat({ + id: getId(), + position: project({ x: evt.clientX, y: evt.clientY - 40 }), + type: 'custom', + }) + ), + [project] + ); + + return ( + + ); +}; + +const WrappedFlow = () => ( + + + +); + +export default WrappedFlow; diff --git a/example/src_oldapi/UpdatableEdge/index.tsx b/example/src_oldapi/UpdatableEdge/index.tsx new file mode 100644 index 00000000..4ffdab04 --- /dev/null +++ b/example/src_oldapi/UpdatableEdge/index.tsx @@ -0,0 +1,79 @@ +import React, { useState } from 'react'; +import ReactFlow, { + Controls, + updateEdge, + addEdge, + Elements, + OnLoadParams, + Connection, + Edge, + removeElements, +} from 'react-flow-renderer'; + +const initialElements: Elements = [ + { + id: '1', + type: 'input', + data: { + label: ( + <> + Node A + + ), + }, + position: { x: 250, y: 0 }, + }, + { + id: '2', + data: { + label: ( + <> + Node B + + ), + }, + position: { x: 100, y: 100 }, + }, + { + id: '3', + data: { + label: ( + <> + Node C + + ), + }, + position: { x: 400, y: 100 }, + style: { background: '#D6D5E6', color: '#333', border: '1px solid #222138', width: 180 }, + }, + { id: 'e1-2', source: '1', target: '2', label: 'This is a draggable edge' }, +]; + +const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView(); +const onEdgeUpdateStart = (_: React.MouseEvent, edge: Edge) => console.log('start update', edge); +const onEdgeUpdateEnd = (_: MouseEvent, edge: Edge) => console.log('end update', edge); + +const UpdatableEdge = () => { + const [elements, setElements] = useState(initialElements); + const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => + setElements((els) => updateEdge(oldEdge, newConnection, els)); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + + return ( + + + + ); +}; + +export default UpdatableEdge; diff --git a/example/src_oldapi/UpdateNode/index.tsx b/example/src_oldapi/UpdateNode/index.tsx new file mode 100644 index 00000000..a7cce8f2 --- /dev/null +++ b/example/src_oldapi/UpdateNode/index.tsx @@ -0,0 +1,78 @@ +import React, { useEffect, useState } from 'react'; +import ReactFlow, { Elements } from 'react-flow-renderer'; + +import './updatenode.css'; + +const initialElements: Elements = [ + { id: '1', data: { label: '-' }, position: { x: 100, y: 100 } }, + { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } }, + { id: 'e1-2', source: '1', target: '2' }, +]; + +const UpdateNode = () => { + const [elements, setElements] = useState(initialElements); + const [nodeName, setNodeName] = useState('Node 1'); + const [nodeBg, setNodeBg] = useState('#eee'); + const [nodeHidden, setNodeHidden] = useState(false); + + useEffect(() => { + setElements((els) => + els.map((el) => { + if (el.id === '1') { + // it's important that you create a new object here in order to notify react flow about the change + el.data = { + ...el.data, + label: nodeName, + }; + } + + return el; + }) + ); + }, [nodeName, setElements]); + + useEffect(() => { + setElements((els) => + els.map((el) => { + if (el.id === '1') { + // it's important that you create a new object here in order to notify react flow about the change + el.style = { ...el.style, backgroundColor: nodeBg }; + } + + return el; + }) + ); + }, [nodeBg, setElements]); + + useEffect(() => { + setElements((els) => + els.map((el) => { + if (el.id === '1' || el.id === 'e1-2') { + // when you update a simple type you can just update the value + el.isHidden = nodeHidden; + } + + return el; + }) + ); + }, [nodeHidden, setElements]); + + return ( + +
+ + setNodeName(evt.target.value)} /> + + + setNodeBg(evt.target.value)} /> + +
+ + setNodeHidden(evt.target.checked)} /> +
+
+
+ ); +}; + +export default UpdateNode; diff --git a/example/src_oldapi/UpdateNode/updatenode.css b/example/src_oldapi/UpdateNode/updatenode.css new file mode 100644 index 00000000..95438054 --- /dev/null +++ b/example/src_oldapi/UpdateNode/updatenode.css @@ -0,0 +1,21 @@ +.updatenode__controls { + position: absolute; + right: 10px; + top: 10px; + z-index: 4; + font-size: 12px; +} + +.updatenode__controls label { + display: block; +} + +.updatenode__bglabel { + margin-top: 10px; +} + +.updatenode__checkboxwrapper { + margin-top: 10px; + display: flex; + align-items: center; +} diff --git a/example/src_oldapi/UseUpdateNodeInternals/CustomNode.tsx b/example/src_oldapi/UseUpdateNodeInternals/CustomNode.tsx new file mode 100644 index 00000000..969a4c3c --- /dev/null +++ b/example/src_oldapi/UseUpdateNodeInternals/CustomNode.tsx @@ -0,0 +1,34 @@ +import React, { memo, FC, useMemo, CSSProperties } from 'react'; + +import { Handle, Position, NodeProps } from 'react-flow-renderer'; + +const nodeStyles: CSSProperties = { padding: 10, border: '1px solid #ddd' }; + +const CustomNode: FC = ({ data }) => { + const handles = useMemo( + () => + Array.from({ length: data.handleCount }, (x, i) => { + const handleId = `handle-${i}`; + return ( + + ); + }), + [data.handleCount, data.handlePosition] + ); + + return ( +
+ +
output handle count: {data.handleCount}
+ {handles} +
+ ); +}; + +export default memo(CustomNode); diff --git a/example/src_oldapi/UseUpdateNodeInternals/index.tsx b/example/src_oldapi/UseUpdateNodeInternals/index.tsx new file mode 100644 index 00000000..2b254c5a --- /dev/null +++ b/example/src_oldapi/UseUpdateNodeInternals/index.tsx @@ -0,0 +1,101 @@ +import React, { useState, useCallback, CSSProperties } from 'react'; + +import ReactFlow, { + NodeTypesType, + addEdge, + useZoomPanHelper, + ReactFlowProvider, + Elements, + Connection, + Edge, + ElementId, + useUpdateNodeInternals, + Position, + isEdge, +} from 'react-flow-renderer'; +import CustomNode from './CustomNode'; + +const initialHandleCount = 1; + +const initialElements: Elements = [ + { + id: '1', + type: 'custom', + data: { label: 'Node 1', handleCount: initialHandleCount, handlePosition: 0 }, + position: { x: 250, y: 5 }, + }, +]; + +const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 10 }; + +const nodeTypes: NodeTypesType = { + custom: CustomNode, +}; + +let id = 5; +const getId = (): ElementId => `${id++}`; + +const UpdateNodeInternalsFlow = () => { + const [elements, setElements] = useState(initialElements); + const updateNodeInternals = useUpdateNodeInternals(); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + const { project } = useZoomPanHelper(); + + const onPaneClick = useCallback( + (evt) => + setElements((els) => + els.concat({ + id: getId(), + position: project({ x: evt.clientX, y: evt.clientY - 40 }), + data: { label: 'new node' }, + targetPosition: Position.Left, + sourcePosition: Position.Right, + }) + ), + [project] + ); + + const toggleHandleCount = useCallback(() => { + setElements((els) => + els.map((el) => { + if (isEdge(el)) { + return el; + } + + return { ...el, data: { ...el.data, handleCount: el.data?.handleCount === 1 ? 2 : 1 } }; + }) + ); + }, []); + + const toggleHandlePosition = useCallback(() => { + setElements((els) => + els.map((el) => { + if (isEdge(el)) { + return el; + } + + return { ...el, data: { ...el.data, handlePosition: el.data?.handlePosition === 0 ? 1 : 0 } }; + }) + ); + }, []); + + const updateNode = useCallback(() => updateNodeInternals('1'), [updateNodeInternals]); + + return ( + +
+ + + +
+
+ ); +}; + +const WrappedFlow = () => ( + + + +); + +export default WrappedFlow; diff --git a/example/src_oldapi/UseZoomPanHelper/index.tsx b/example/src_oldapi/UseZoomPanHelper/index.tsx new file mode 100644 index 00000000..94ff30f8 --- /dev/null +++ b/example/src_oldapi/UseZoomPanHelper/index.tsx @@ -0,0 +1,65 @@ +import React, { useState, useCallback } from 'react'; + +import ReactFlow, { + removeElements, + addEdge, + Background, + MiniMap, + useZoomPanHelper, + ReactFlowProvider, + Elements, + ElementId, + Connection, + Edge, +} from 'react-flow-renderer'; + +const initialElements: Elements = [ + { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, + { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, + { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, + { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' }, + { id: 'e1-2', source: '1', target: '2', animated: true }, + { id: 'e1-3', source: '1', target: '3' }, +]; + +let id = 5; +const getId = (): ElementId => `${id++}`; + +const UseZoomPanHelperFlow = () => { + const [elements, setElements] = useState(initialElements); + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); + const { project } = useZoomPanHelper(); + + const onPaneClick = useCallback( + (evt) => { + const projectedPosition = project({ x: evt.clientX, y: evt.clientY - 40 }); + + setElements((els) => + els.concat({ + id: getId(), + position: projectedPosition, + data: { + label: `${projectedPosition.x}-${projectedPosition.y}`, + }, + }) + ); + }, + [project] + ); + + return ( + + + + + ); +}; + +const WrappedFlow = () => ( + + + +); + +export default WrappedFlow; diff --git a/example/src_oldapi/Validation/index.tsx b/example/src_oldapi/Validation/index.tsx new file mode 100644 index 00000000..d5209ed4 --- /dev/null +++ b/example/src_oldapi/Validation/index.tsx @@ -0,0 +1,73 @@ +import React, { useState, MouseEvent as ReactMouseEvent, FC } from 'react'; +import ReactFlow, { + addEdge, + Handle, + OnLoadParams, + Connection, + Position, + Elements, + Edge, + OnConnectStartParams, + NodeProps, + NodeTypesType, +} from 'react-flow-renderer'; + +import './validation.css'; + +const initialElements: Elements = [ + { id: '0', type: 'custominput', position: { x: 0, y: 150 } }, + { id: 'A', type: 'customnode', position: { x: 250, y: 0 } }, + { id: 'B', type: 'customnode', position: { x: 250, y: 150 } }, + { id: 'C', type: 'customnode', position: { x: 250, y: 300 } }, +]; + +const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView(); +const isValidConnection = (connection: Connection) => connection.target === 'B'; +const onConnectStart = (_: ReactMouseEvent, { nodeId, handleType }: OnConnectStartParams) => + console.log('on connect start', { nodeId, handleType }); +const onConnectStop = (event: MouseEvent) => console.log('on connect stop', event); +const onConnectEnd = (event: MouseEvent) => console.log('on connect end', event); + +const CustomInput: FC = () => ( + <> +
Only connectable with B
+ + +); + +const CustomNode: FC = ({ id }) => ( + <> + +
{id}
+ + +); + +const nodeTypes: NodeTypesType = { + custominput: CustomInput, + customnode: CustomNode, +}; + +const HorizontalFlow = () => { + const [elements, setElements] = useState(initialElements); + const onConnect = (params: Connection | Edge) => { + console.log('on connect', params); + setElements((els) => addEdge(params, els)); + }; + + return ( + + ); +}; + +export default HorizontalFlow; diff --git a/example/src_oldapi/Validation/validation.css b/example/src_oldapi/Validation/validation.css new file mode 100644 index 00000000..a777e6c5 --- /dev/null +++ b/example/src_oldapi/Validation/validation.css @@ -0,0 +1,31 @@ +.validationflow .react-flow__node { + width: 150px; + border-radius: 5px; + padding: 10px; + color: #555; + border: 1px solid #ddd; + text-align: center; + font-size: 12px; +} + +.validationflow .react-flow__node-customnode { + background: #e6e6e9; + border: 1px solid #ddd; +} + +.react-flow__node-custominput .react-flow__handle { + background: #e6e6e9; +} + +.validationflow .react-flow__node-custominput { + background: #fff; + +} + +.validationflow .react-flow__handle-connecting { + background: #ff6060; +} + +.validationflow .react-flow__handle-valid { + background: #55dd99; +} \ No newline at end of file diff --git a/example/src_oldapi/index.css b/example/src_oldapi/index.css new file mode 100644 index 00000000..3f6f9960 --- /dev/null +++ b/example/src_oldapi/index.css @@ -0,0 +1,82 @@ +body { + font-family: sans-serif; + color: #111; +} + +html, +body, +#root { + margin: 0; + height: 100%; +} + +#root { + display: flex; + flex-direction: column; +} + +header { + padding: 10px; + border-bottom: 1px solid #eee; + display: flex; + font-weight: 700; + align-items: center; +} + +.logo { + text-decoration: none; + display: block; + line-height: 1; +} + +header a, +header a:focus, +header a:active, +header a:visited { + color: #111; +} + +header a:hover { + color: #333; +} + +header select { + margin-left: 1em; +} + +.overview-example__add { + display: none; +} + +.react-flow__node a { + font-weight: 700; + color: #111; +} + +.react-flow__node.dark-node { + background: #0041d0; + color: #f8f8f8; +} + +.react-flow__node.dark { + background: #557; + color: #f8f8f8; +} + +.react-flow__node-selectorNode { + font-size: 12px; + background: #f0f2f3; + border: 1px solid 555; + border-radius: 5px; + text-align: center; +} + +.react-flow__node-selectorNode .react-flow__handle { + border-color: #f0f2f3; +} + +@media screen and (min-width: 768px) { + .overview-example__add { + display: block; + } +} diff --git a/example/src_oldapi/index.tsx b/example/src_oldapi/index.tsx new file mode 100644 index 00000000..f81278ba --- /dev/null +++ b/example/src_oldapi/index.tsx @@ -0,0 +1,165 @@ +import React, { ChangeEvent } from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter as Router, Route, Switch, withRouter } from 'react-router-dom'; + +import Overview from './Overview'; +import Basic from './Basic'; +import CustomNode from './CustomNode'; +import Stress from './Stress'; +import Interaction from './Interaction'; +import Empty from './Empty'; +import Edges from './Edges'; +import Validation from './Validation'; +import Provider from './Provider'; +import Hidden from './Hidden'; +import EdgeTypes from './EdgeTypes'; +import CustomConnectionLine from './CustomConnectionLine'; +import NodeTypeChange from './NodeTypeChange'; +import NodeTypesObjectChange from './NodeTypesObjectChange'; +import UpdatableEdge from './UpdatableEdge'; +import UpdateNode from './UpdateNode'; +import SaveRestore from './SaveRestore'; +import DragNDrop from './DragNDrop'; +import Layout from './Layouting'; +import SwitchFlows from './Switch'; +import UseZoomPanHelper from './UseZoomPanHelper'; +import UseUpdateNodeInternals from './UseUpdateNodeInternals'; +import Undirectional from './Undirectional'; +import MultiFlows from './MultiFlows'; +import DragHandle from './DragHandle'; + +import './index.css'; + +const routes = [ + { + path: '/', + component: Overview, + }, + { + path: '/edges', + component: Edges, + }, + { + path: '/custom-node', + component: CustomNode, + }, + { + path: '/validation', + component: Validation, + }, + { + path: '/provider', + component: Provider, + }, + { + path: '/stress', + component: Stress, + }, + { + path: '/interaction', + component: Interaction, + }, + { + path: '/basic', + component: Basic, + }, + { + path: '/empty', + component: Empty, + }, + { + path: '/hidden', + component: Hidden, + }, + { + path: '/edge-types', + component: EdgeTypes, + }, + { + path: '/custom-connectionline', + component: CustomConnectionLine, + }, + { + path: '/nodetype-change', + component: NodeTypeChange, + }, + { + path: '/nodetypesobject-change', + component: NodeTypesObjectChange, + }, + { + path: '/updatable-edge', + component: UpdatableEdge, + }, + { + path: '/update-node', + component: UpdateNode, + }, + { + path: '/save-restore', + component: SaveRestore, + }, + { + path: '/drag-and-drop', + component: DragNDrop, + }, + { + path: '/layouting', + component: Layout, + }, + { + path: '/switch', + component: SwitchFlows, + }, + { + path: '/usezoompanhelper', + component: UseZoomPanHelper, + }, + { + path: '/useupdatenodeinternals', + component: UseUpdateNodeInternals, + }, + { + path: '/undirectional', + component: Undirectional, + }, + { + path: '/multiflows', + component: MultiFlows, + }, + { + path: '/draghandle', + component: DragHandle, + }, +]; + +const Header = withRouter(({ history, location }) => { + const onChange = (event: ChangeEvent) => history.push(event.target.value); + + return ( +
+ + React Flow Dev + + +
+ ); +}); + +ReactDOM.render( + +
+ + {routes.map((route) => ( + } key={route.path} /> + ))} + + , + document.getElementById('root') +); diff --git a/example/src_oldapi/react-app-env.d.ts b/example/src_oldapi/react-app-env.d.ts new file mode 100644 index 00000000..6431bc5f --- /dev/null +++ b/example/src_oldapi/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/example/tsconfig.json b/example/tsconfig.json index a273b0cf..edc77511 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -20,7 +16,6 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": [ - "src" - ] + "include": ["src"], + "exclude": ["src_oldapi"] } From 767fbd4377d2ed8441b1f125afeaf465b305b844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Wed, 20 Oct 2021 16:57:44 +0200 Subject: [PATCH 015/263] feat(noderenderer): render child nodes --- example/src/Basic/index.tsx | 22 ++- src/components/ConnectionLine/index.tsx | 3 +- src/components/Nodes/wrapNode.tsx | 2 +- src/container/EdgeRenderer/utils.ts | 4 +- src/container/NodeRenderer/index.tsx | 187 ++++++++++++++++-------- src/store/index.ts | 14 +- src/types/index.ts | 1 + src/utils/graph.ts | 16 +- 8 files changed, 180 insertions(+), 69 deletions(-) diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index e13d4a7a..e59f8f6e 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -23,7 +23,27 @@ const initialNodes: Node[] = [ { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, - { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' }, + { + id: '4', + data: { label: 'Node 4' }, + position: { x: 400, y: 200 }, + className: 'light', + style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, + childNodes: [ + { id: '4a', data: { label: 'Node 4a' }, position: { x: 400, y: 400 }, className: 'light' }, + { + id: '4b', + data: { label: 'Node 4b' }, + position: { x: 500, y: 500 }, + className: 'light', + style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, + childNodes: [ + { id: '4b1', data: { label: 'Node 4b1' }, position: { x: 450, y: 450 }, className: 'light' }, + { id: '4b2', data: { label: 'Node 4b2' }, position: { x: 550, y: 550 }, className: 'light' }, + ], + }, + ], + }, ]; const initialEdges: Edge[] = [ diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index 926ed692..aed5a5c2 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -14,6 +14,7 @@ import { HandleType, ReactFlowState, } from '../../types'; +import { flattenNodes } from '../../utils/graph'; interface ConnectionLineProps { connectionNodeId: ElementId; @@ -28,7 +29,7 @@ interface ConnectionLineProps { CustomConnectionLineComponent?: ConnectionLineComponent; } -const nodesSelector = (s: ReactFlowState) => s.nodes; +const nodesSelector = (s: ReactFlowState) => flattenNodes(s.nodes); export default ({ connectionNodeId, diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index b61d0550..6da0f95c 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -68,7 +68,7 @@ export default (NodeComponent: ComponentType) => { pointerEvents: isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none', // prevents jumping of nodes on start - opacity: isInitialized ? 1 : 0, + // opacity: isInitialized ? 1 : 0, ...style, }), [ diff --git a/src/container/EdgeRenderer/utils.ts b/src/container/EdgeRenderer/utils.ts index d3e4ac20..19e3b9ac 100644 --- a/src/container/EdgeRenderer/utils.ts +++ b/src/container/EdgeRenderer/utils.ts @@ -2,7 +2,7 @@ import { ComponentType } from 'react'; import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges'; import wrapEdge from '../../components/Edges/wrapEdge'; -import { rectToBox } from '../../utils/graph'; +import { rectToBox, flattenNodes } from '../../utils/graph'; import { EdgeTypesType, @@ -171,7 +171,7 @@ type SourceTargetNode = { }; export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNode => { - return nodes.reduce( + return flattenNodes(nodes).reduce( (res, node) => { if (node.id === edge.source) { res.sourceNode = node; diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index ad12d642..f362f63a 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -1,9 +1,9 @@ -import React, { memo, useMemo, ComponentType, MouseEvent, useCallback } from 'react'; +import React, { memo, useMemo, ComponentType, MouseEvent, useCallback, Fragment } from 'react'; import shallow from 'zustand/shallow'; import { useStore } from '../../store'; -import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types'; -import { getNodesInside } from '../../utils/graph'; +import { Node, NodeTypesType, ReactFlowState, WrapNodeProps, SnapGrid } from '../../types'; +import { getNodesInside, getRectOfNodes } from '../../utils/graph'; interface NodeRendererProps { nodeTypes: NodeTypesType; selectNodesOnDrag: boolean; @@ -29,6 +29,122 @@ const selector = (s: ReactFlowState) => ({ snapToGrid: s.snapToGrid, }); +interface NodesProps extends NodeRendererProps { + nodes?: Node[]; + isDraggable?: boolean; + resizeObserver: ResizeObserver | null; + scale: number; + snapToGrid: boolean; + snapGrid: SnapGrid; + nodesDraggable: boolean; + nodesConnectable: boolean; + elementsSelectable: boolean; +} + +const Nodes = memo( + ({ + nodes = [], + isDraggable, + resizeObserver, + scale, + snapToGrid, + snapGrid, + nodesDraggable, + nodesConnectable, + elementsSelectable, + ...props + }: NodesProps) => { + return ( + <> + {nodes.map((node) => { + const nodeType = node.type || 'default'; + + if (!props.nodeTypes[nodeType]) { + console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); + } + + const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; + const isNodeDraggable = + typeof isDraggable !== 'undefined' + ? isDraggable + : !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')); + const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined')); + const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined')); + const isInitialized = + node.width !== null && + node.height !== null && + typeof node.width !== 'undefined' && + typeof node.height !== 'undefined'; + + if (node.childNodes) { + const childRect = getRectOfNodes(node.childNodes); + node.position = node.isDragging + ? node.position + : { x: Math.round(childRect.x) - 10, y: Math.round(childRect.y) - 10 }; + node.style = { + ...node.style, + width: Math.round(childRect.width) + 20, + height: Math.round(childRect.height) + 20, + boxSizing: 'border-box', + }; + } + + return ( + + + {node.childNodes && ( + + )} + + ); + })} + + ); + } +); + const NodeRenderer = (props: NodeRendererProps) => { const { transform, @@ -75,60 +191,17 @@ const NodeRenderer = (props: NodeRendererProps) => { return (
- {nodes.map((node) => { - const nodeType = node.type || 'default'; - const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; - - if (!props.nodeTypes[nodeType]) { - console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); - } - - const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')); - const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined')); - const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined')); - const isInitialized = - node.width !== null && - node.height !== null && - typeof node.width !== 'undefined' && - typeof node.height !== 'undefined'; - - return ( - - ); - })} +
); }; diff --git a/src/store/index.ts b/src/store/index.ts index 1d9a85e9..eb5555ee 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -27,7 +27,7 @@ import { EdgeChange, NodePositionChange, } from '../types'; -import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph'; +import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges, flattenNodes } from '../utils/graph'; import { getHandleBounds } from '../components/Nodes/utils'; const { Provider, useStore, useStoreApi } = createContext(); @@ -125,7 +125,7 @@ const createStore = () => const { onNodesChange, nodes, transform } = get(); const initialChanges: NodeChange[] = []; - const nodesToChange: NodeChange[] = nodes.reduce((res, node) => { + const nodesToChange: NodeChange[] = flattenNodes(nodes).reduce((res, node) => { const update = updates.find((u) => u.id === node.id); if (update) { const dimensions = getDimensions(update.nodeElement); @@ -155,11 +155,12 @@ const createStore = () => const { onNodesChange, nodes, nodeExtent } = get(); if (onNodesChange) { - const matchingNodes = nodes.filter((n) => n.id === id || n.isSelected); + const matchingNodes = flattenNodes(nodes).filter((n) => n.id === id || n.isSelected); + const matchingChildNodes = flattenNodes(matchingNodes); //.filter(n => !!n.childNodes).reduce((result, node) => result.concat(node.childNodes!), [])); - if (matchingNodes?.length) { + if (matchingChildNodes?.length) { onNodesChange( - matchingNodes.map((n) => { + matchingChildNodes.map((n) => { const change: NodePositionChange = { id: n.id, type: 'position', @@ -276,7 +277,8 @@ const createStore = () => }, unselectNodesAndEdges: () => { const { nodes, edges, onNodesChange, onEdgesChange } = get(); - const nodesToUnselect = nodes.map((n) => { + + const nodesToUnselect = flattenNodes(nodes).map((n) => { n.isSelected = false; return createNodeOrEdgeSelectionChange(false)(n); }) as NodeChange[]; diff --git a/src/types/index.ts b/src/types/index.ts index 064c4cc4..817657dd 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -86,6 +86,7 @@ export interface Node { width?: number | null; height?: number | null; handleBounds?: NodeHandleBounds; + childNodes?: Node[]; } export enum ArrowHeadType { diff --git a/src/utils/graph.ts b/src/utils/graph.ts index cda69ca1..4895966b 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -61,7 +61,7 @@ export const getMarkerId = (marker: EdgeMarkerType | undefined): string => { .join('&'); }; -const connectionExists = (edge: Edge, elements: Elements) => { +const connectionExists = (edge: Edge, elements: Edge[]) => { return elements.some( (el) => isEdge(el) && @@ -294,6 +294,10 @@ function applyChanges(changes: NodeChange[] | EdgeChange[], elements: any[]): an const initElements: any[] = []; return elements.reduce((res: any[], item: any) => { + if (item.childNodes) { + item.childNodes = applyChanges(changes, item.childNodes); + } + const currentChange = changes.find((c) => c.id === item.id); if (currentChange) { @@ -338,3 +342,13 @@ export function applyNodeChanges(changes: NodeChange[], nodes: Node[]): Node[] { export function applyEdgeChanges(changes: EdgeChange[], edges: Edge[]): Edge[] { return applyChanges(changes, edges) as Edge[]; } + +export function flattenNodes(nodes: Node[] | undefined): Node[] { + if (!nodes) { + return []; + } + + return nodes.reduce((result, node) => { + return result.concat([node, ...flattenNodes(node.childNodes)]); + }, []); +} From 9350884836a47b6ab22e66f0564ff2f46a04cc3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Wed, 20 Oct 2021 18:06:41 +0200 Subject: [PATCH 016/263] fix(perf): improve flattenNodes --- src/store/index.ts | 86 +++++++++++++++++++++++++++++++++++++++++----- src/utils/graph.ts | 32 ++++++++++++----- 2 files changed, 101 insertions(+), 17 deletions(-) diff --git a/src/store/index.ts b/src/store/index.ts index eb5555ee..10693808 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -38,6 +38,50 @@ const createNodeOrEdgeSelectionChange = (isSelected: boolean) => (item: Node | E isSelected, }); +const findNodeById = (id: string, nodes: Node[]): Node | null => { + let res = null; + + for (let i = 0; i < nodes.length; i++) { + const n = nodes[i]; + + if (n.id === id) { + return n; + } + + if (n.childNodes) { + res = findNodeById(id, n.childNodes); + + if (res) { + return res; + } + } + } + + return res; +}; + +const findNodes = (condition: (node: Node) => boolean, nodes: Node[]): Node[] => { + let res = []; + + for (let i = 0; i < nodes.length; i++) { + const n = nodes[i]; + + if (condition(n)) { + res.push(n); + } + + if (n.childNodes) { + const matches = findNodes(condition, n.childNodes); + + for (let j = 0; j < matches.length; j++) { + res.push(matches[j]); + } + } + } + + return res; +}; + const createStore = () => create((set, get) => ({ width: 0, @@ -96,6 +140,7 @@ const createStore = () => setNodes: (propNodes: Node[]) => { const { nodes } = get(); + const nextNodes = propNodes.map((propNode: Node) => { const storeNode = nodes.find((node) => node.id === propNode.id); @@ -124,10 +169,10 @@ const createStore = () => updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { const { onNodesChange, nodes, transform } = get(); - const initialChanges: NodeChange[] = []; - const nodesToChange: NodeChange[] = flattenNodes(nodes).reduce((res, node) => { - const update = updates.find((u) => u.id === node.id); - if (update) { + const nodesToChange: NodeChange[] = updates.reduce((res, update) => { + const node = findNodeById(update.id, nodes); + + if (node) { const dimensions = getDimensions(update.nodeElement); const doUpdate = dimensions.width && @@ -147,7 +192,31 @@ const createStore = () => } return res; - }, initialChanges); + }, []); + + // const nodesToChange: NodeChange[] = flattenNodes(nodes).reduce((res, node) => { + // const update = updates.find((u) => u.id === node.id); + // if (update) { + // const dimensions = getDimensions(update.nodeElement); + // const doUpdate = + // dimensions.width && + // dimensions.height && + // (node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate); + + // if (doUpdate) { + // const handleBounds = getHandleBounds(update.nodeElement, transform[2]); + // const change = { + // id: node.id, + // type: 'dimensions', + // dimensions, + // handleBounds, + // } as NodeChange; + // res.push(change); + // } + // } + + // return res; + // }, initialChanges); onNodesChange?.(nodesToChange); }, @@ -155,12 +224,11 @@ const createStore = () => const { onNodesChange, nodes, nodeExtent } = get(); if (onNodesChange) { - const matchingNodes = flattenNodes(nodes).filter((n) => n.id === id || n.isSelected); - const matchingChildNodes = flattenNodes(matchingNodes); //.filter(n => !!n.childNodes).reduce((result, node) => result.concat(node.childNodes!), [])); + const matchingNodes = flattenNodes(findNodes((n) => n.id === id || !!n.isSelected, nodes)); - if (matchingChildNodes?.length) { + if (matchingNodes?.length) { onNodesChange( - matchingChildNodes.map((n) => { + matchingNodes.map((n) => { const change: NodePositionChange = { id: n.id, type: 'position', diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 4895966b..7e88a608 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -343,12 +343,28 @@ export function applyEdgeChanges(changes: EdgeChange[], edges: Edge[]): Edge[] { return applyChanges(changes, edges) as Edge[]; } -export function flattenNodes(nodes: Node[] | undefined): Node[] { - if (!nodes) { - return []; - } - - return nodes.reduce((result, node) => { - return result.concat([node, ...flattenNodes(node.childNodes)]); - }, []); +function flat(arr: Node[], target: Node[]) { + arr.forEach(function (el) { + if (el.childNodes) { + flat(el.childNodes, target); + } else { + target.push(el); + } + }); +} + +export function flattenNodes(nodes: Node[]): Node[] { + const flattened: Node[] = []; + flat(nodes, flattened); + return flattened; + + // return nodes.reduce((result, node) => { + // result.push(node); + + // if (node.childNodes) { + // result.push(...flattenNodes(node.childNodes)); + // } + + // return result; + // }, []); } From 450cb90e67b752ec2d9c94dea3e087b130437a5d Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 20 Oct 2021 19:26:22 +0200 Subject: [PATCH 017/263] refactor(nested-nodes): zIndex --- src/components/Nodes/wrapNode.tsx | 3 +- src/container/NodeRenderer/index.tsx | 206 ++++++++++++++------------- src/types/index.ts | 1 + 3 files changed, 107 insertions(+), 103 deletions(-) diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 6da0f95c..a0b30cd9 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -48,6 +48,7 @@ export default (NodeComponent: ComponentType) => { isDragging, resizeObserver, dragHandle, + zIndex, }: WrapNodeProps) => { const { addSelectedElements, @@ -63,7 +64,7 @@ export default (NodeComponent: ComponentType) => { const nodeStyle: CSSProperties = useMemo( () => ({ - zIndex: isSelected ? 10 : 3, + zIndex: isSelected ? zIndex + 1 : zIndex, transform: `translate(${xPos}px,${yPos}px)`, pointerEvents: isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none', diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index f362f63a..16e3e037 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -39,111 +39,112 @@ interface NodesProps extends NodeRendererProps { nodesDraggable: boolean; nodesConnectable: boolean; elementsSelectable: boolean; + recursionDepth: number; } -const Nodes = memo( - ({ - nodes = [], - isDraggable, - resizeObserver, - scale, - snapToGrid, - snapGrid, - nodesDraggable, - nodesConnectable, - elementsSelectable, - ...props - }: NodesProps) => { +function Nodes({ + nodes = [], + isDraggable, + resizeObserver, + scale, + snapToGrid, + snapGrid, + nodesDraggable, + nodesConnectable, + elementsSelectable, + recursionDepth, + ...props +}: NodesProps): any { + return nodes.map((node) => { + const nodeType = node.type || 'default'; + + if (!props.nodeTypes[nodeType]) { + console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); + } + + const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; + const isNodeDraggable = + typeof isDraggable !== 'undefined' + ? isDraggable + : !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')); + const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined')); + const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined')); + const isInitialized = + node.width !== null && + node.height !== null && + typeof node.width !== 'undefined' && + typeof node.height !== 'undefined'; + let childRect; + + if (node.childNodes) { + childRect = getRectOfNodes(node.childNodes); + node.position = node.isDragging + ? node.position + : { x: Math.round(childRect.x) - 10, y: Math.round(childRect.y) - 10 }; + node.style = { + ...node.style, + width: Math.round(childRect.width) + 20, + height: Math.round(childRect.height) + 20, + boxSizing: 'border-box', + }; + } + return ( - <> - {nodes.map((node) => { - const nodeType = node.type || 'default'; - - if (!props.nodeTypes[nodeType]) { - console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); - } - - const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; - const isNodeDraggable = - typeof isDraggable !== 'undefined' - ? isDraggable - : !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')); - const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined')); - const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined')); - const isInitialized = - node.width !== null && - node.height !== null && - typeof node.width !== 'undefined' && - typeof node.height !== 'undefined'; - - if (node.childNodes) { - const childRect = getRectOfNodes(node.childNodes); - node.position = node.isDragging - ? node.position - : { x: Math.round(childRect.x) - 10, y: Math.round(childRect.y) - 10 }; - node.style = { - ...node.style, - width: Math.round(childRect.width) + 20, - height: Math.round(childRect.height) + 20, - boxSizing: 'border-box', - }; - } - - return ( - - - {node.childNodes && ( - - )} - - ); - })} - + + + {node.childNodes && ( + + )} + ); - } -); + }); +} + +const MemoizedNodes = memo(Nodes); const NodeRenderer = (props: NodeRendererProps) => { const { @@ -191,7 +192,7 @@ const NodeRenderer = (props: NodeRendererProps) => { return (
- { resizeObserver={resizeObserver} elementsSelectable={elementsSelectable} scale={transform[2]} + recursionDepth={0} {...props} />
diff --git a/src/types/index.ts b/src/types/index.ts index 817657dd..b3133c0a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -300,6 +300,7 @@ export interface WrapNodeProps { isDragging?: boolean; resizeObserver: ResizeObserver | null; dragHandle?: string; + zIndex: number; } export type FitViewParams = { From 8f23321bd02418db80200d42ec4e464d172633ec Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 21 Oct 2021 09:05:56 +0200 Subject: [PATCH 018/263] refactor(hooks): add useVisibleNodes and useVisibleEdges --- src/container/EdgeRenderer/index.tsx | 36 ++--------------------- src/container/NodeRenderer/index.tsx | 17 +++-------- src/hooks/useVisibleEdges.ts | 44 ++++++++++++++++++++++++++++ src/hooks/useVisibleNodes.ts | 22 ++++++++++++++ 4 files changed, 73 insertions(+), 46 deletions(-) create mode 100644 src/hooks/useVisibleEdges.ts create mode 100644 src/hooks/useVisibleNodes.ts diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 4b707b98..0b83ce99 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -4,7 +4,7 @@ import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import ConnectionLine from '../../components/ConnectionLine/index'; import MarkerDefinitions from './MarkerDefinitions'; -import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from './utils'; +import { getEdgePositions, getHandle, getSourceTargetNodes } from './utils'; import { Position, Edge, @@ -16,6 +16,7 @@ import { ReactFlowState, NodeHandleBounds, } from '../../types'; +import useVisibleEdges from '../../hooks/useVisibleEdges'; interface EdgeRendererProps { edgeTypes: any; @@ -234,38 +235,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { nodes, } = useStore(selector, shallow); - const edges = useStore( - useCallback( - (s: ReactFlowState) => { - if (!props.onlyRenderVisibleElements) { - return s.edges; - } - - return s.edges.filter((e) => { - const { sourceNode, targetNode } = getSourceTargetNodes(e, s.nodes); - - return ( - sourceNode?.width && - sourceNode?.height && - targetNode?.width && - targetNode?.height && - isEdgeVisible({ - sourcePos: sourceNode.position, - targetPos: targetNode.position, - sourceWidth: sourceNode.width, - sourceHeight: sourceNode.height, - targetWidth: targetNode.width, - targetHeight: targetNode.height, - width: s.width, - height: s.height, - transform: s.transform, - }) - ); - }); - }, - [props.onlyRenderVisibleElements] - ) - ); + const edges = useVisibleEdges(props.onlyRenderVisibleElements); if (!width) { return null; diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 16e3e037..2f8be03f 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -1,9 +1,10 @@ -import React, { memo, useMemo, ComponentType, MouseEvent, useCallback, Fragment } from 'react'; +import React, { memo, useMemo, ComponentType, MouseEvent, Fragment } from 'react'; import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import { Node, NodeTypesType, ReactFlowState, WrapNodeProps, SnapGrid } from '../../types'; -import { getNodesInside, getRectOfNodes } from '../../utils/graph'; +import { getRectOfNodes } from '../../utils/graph'; +import useVisibleNodes from '../../hooks/useVisibleNodes'; interface NodeRendererProps { nodeTypes: NodeTypesType; selectNodesOnDrag: boolean; @@ -156,17 +157,7 @@ const NodeRenderer = (props: NodeRendererProps) => { snapGrid, snapToGrid, } = useStore(selector, shallow); - - const nodes = useStore( - useCallback( - (s: ReactFlowState) => { - return props.onlyRenderVisibleElements - ? getNodesInside(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true) - : s.nodes; - }, - [props.onlyRenderVisibleElements] - ) - ); + const nodes = useVisibleNodes(props.onlyRenderVisibleElements); const transformStyle = useMemo( () => ({ diff --git a/src/hooks/useVisibleEdges.ts b/src/hooks/useVisibleEdges.ts new file mode 100644 index 00000000..74406400 --- /dev/null +++ b/src/hooks/useVisibleEdges.ts @@ -0,0 +1,44 @@ +import { useCallback } from 'react'; + +import { useStore } from '../store'; +import { isEdgeVisible, getSourceTargetNodes } from '../container/EdgeRenderer/utils'; +import { ReactFlowState } from '../types'; + +function useVisibleEdges(onlyRenderVisible: boolean) { + const edges = useStore( + useCallback( + (s: ReactFlowState) => { + if (!onlyRenderVisible) { + return s.edges; + } + + return s.edges.filter((e) => { + const { sourceNode, targetNode } = getSourceTargetNodes(e, s.nodes); + + return ( + sourceNode?.width && + sourceNode?.height && + targetNode?.width && + targetNode?.height && + isEdgeVisible({ + sourcePos: sourceNode.position, + targetPos: targetNode.position, + sourceWidth: sourceNode.width, + sourceHeight: sourceNode.height, + targetWidth: targetNode.width, + targetHeight: targetNode.height, + width: s.width, + height: s.height, + transform: s.transform, + }) + ); + }); + }, + [onlyRenderVisible] + ) + ); + + return edges; +} + +export default useVisibleEdges; diff --git a/src/hooks/useVisibleNodes.ts b/src/hooks/useVisibleNodes.ts new file mode 100644 index 00000000..d6c9bd96 --- /dev/null +++ b/src/hooks/useVisibleNodes.ts @@ -0,0 +1,22 @@ +import { useCallback } from 'react'; + +import { useStore } from '../store'; +import { getNodesInside } from '../utils/graph'; +import { ReactFlowState } from '../types'; + +function useVisibleNodes(onlyRenderVisible: boolean) { + const nodes = useStore( + useCallback( + (s: ReactFlowState) => { + return onlyRenderVisible + ? getNodesInside(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true) + : s.nodes; + }, + [onlyRenderVisible] + ) + ); + + return nodes; +} + +export default useVisibleNodes; From 9ad894d2dfdaca7f6cd73f86857ecfc1dfc7c131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 21 Oct 2021 11:53:44 +0200 Subject: [PATCH 019/263] feat(nodes): add helper functions to iterate over child nodes --- example/src/Basic/index.tsx | 4 +- src/components/ConnectionLine/index.tsx | 4 +- src/container/EdgeRenderer/utils.ts | 29 +++--- src/index.ts | 3 + src/store/index.ts | 66 +++---------- src/utils/graph.ts | 26 ----- src/utils/nodes.ts | 121 ++++++++++++++++++++++++ 7 files changed, 157 insertions(+), 96 deletions(-) create mode 100644 src/utils/nodes.ts diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index e59f8f6e..a9802e6f 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -13,6 +13,7 @@ import ReactFlow, { EdgeChange, OnLoadParams, Connection, + nodeHelper, } from 'react-flow-renderer'; const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); @@ -79,9 +80,8 @@ const BasicFlow = () => { const toggleClassnames = () => { setNodes((nds) => { - return nds.map((n) => { + return nodeHelper(nds).map((n) => { n.className = n.className === 'light' ? 'dark' : 'light'; - return n; }); }); diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index aed5a5c2..f69da656 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -14,7 +14,7 @@ import { HandleType, ReactFlowState, } from '../../types'; -import { flattenNodes } from '../../utils/graph'; +import { nodeHelper } from '../../utils/nodes'; interface ConnectionLineProps { connectionNodeId: ElementId; @@ -29,7 +29,7 @@ interface ConnectionLineProps { CustomConnectionLineComponent?: ConnectionLineComponent; } -const nodesSelector = (s: ReactFlowState) => flattenNodes(s.nodes); +const nodesSelector = (s: ReactFlowState) => nodeHelper(s.nodes).flatten(); export default ({ connectionNodeId, diff --git a/src/container/EdgeRenderer/utils.ts b/src/container/EdgeRenderer/utils.ts index 19e3b9ac..f6ad4360 100644 --- a/src/container/EdgeRenderer/utils.ts +++ b/src/container/EdgeRenderer/utils.ts @@ -2,7 +2,8 @@ import { ComponentType } from 'react'; import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges'; import wrapEdge from '../../components/Edges/wrapEdge'; -import { rectToBox, flattenNodes } from '../../utils/graph'; +import { rectToBox } from '../../utils/graph'; +import { nodeHelper } from '../../utils/nodes'; import { EdgeTypesType, @@ -171,16 +172,18 @@ type SourceTargetNode = { }; export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNode => { - return flattenNodes(nodes).reduce( - (res, node) => { - if (node.id === edge.source) { - res.sourceNode = node; - } - if (node.id === edge.target) { - res.targetNode = node; - } - return res; - }, - { sourceNode: null, targetNode: null } as SourceTargetNode - ); + return nodeHelper(nodes) + .flatten() + .reduce( + (res, node) => { + if (node.id === edge.source) { + res.sourceNode = node; + } + if (node.id === edge.target) { + res.targetNode = node; + } + return res; + }, + { sourceNode: null, targetNode: null } as SourceTargetNode + ); }; diff --git a/src/index.ts b/src/index.ts index 41314b5f..139872f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,9 @@ export { applyNodeChanges, applyEdgeChanges, } from './utils/graph'; + +export { nodeHelper } from './utils/nodes'; + export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'; export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'; diff --git a/src/store/index.ts b/src/store/index.ts index 10693808..210d9d33 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -27,7 +27,8 @@ import { EdgeChange, NodePositionChange, } from '../types'; -import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges, flattenNodes } from '../utils/graph'; +import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph'; +import { nodeHelper } from '../utils/nodes'; import { getHandleBounds } from '../components/Nodes/utils'; const { Provider, useStore, useStoreApi } = createContext(); @@ -38,50 +39,6 @@ const createNodeOrEdgeSelectionChange = (isSelected: boolean) => (item: Node | E isSelected, }); -const findNodeById = (id: string, nodes: Node[]): Node | null => { - let res = null; - - for (let i = 0; i < nodes.length; i++) { - const n = nodes[i]; - - if (n.id === id) { - return n; - } - - if (n.childNodes) { - res = findNodeById(id, n.childNodes); - - if (res) { - return res; - } - } - } - - return res; -}; - -const findNodes = (condition: (node: Node) => boolean, nodes: Node[]): Node[] => { - let res = []; - - for (let i = 0; i < nodes.length; i++) { - const n = nodes[i]; - - if (condition(n)) { - res.push(n); - } - - if (n.childNodes) { - const matches = findNodes(condition, n.childNodes); - - for (let j = 0; j < matches.length; j++) { - res.push(matches[j]); - } - } - } - - return res; -}; - const createStore = () => create((set, get) => ({ width: 0, @@ -170,7 +127,7 @@ const createStore = () => const { onNodesChange, nodes, transform } = get(); const nodesToChange: NodeChange[] = updates.reduce((res, update) => { - const node = findNodeById(update.id, nodes); + const node = nodeHelper(nodes).find((n) => n.id === update.id); if (node) { const dimensions = getDimensions(update.nodeElement); @@ -224,11 +181,12 @@ const createStore = () => const { onNodesChange, nodes, nodeExtent } = get(); if (onNodesChange) { - const matchingNodes = flattenNodes(findNodes((n) => n.id === id || !!n.isSelected, nodes)); + const matchingNodes = nodeHelper(nodes).filter((n) => n.id === id || !!n.isSelected); + const changingNodes = nodeHelper(matchingNodes).flatten(); - if (matchingNodes?.length) { + if (changingNodes?.length) { onNodesChange( - matchingNodes.map((n) => { + changingNodes.map((n) => { const change: NodePositionChange = { id: n.id, type: 'position', @@ -346,10 +304,12 @@ const createStore = () => unselectNodesAndEdges: () => { const { nodes, edges, onNodesChange, onEdgesChange } = get(); - const nodesToUnselect = flattenNodes(nodes).map((n) => { - n.isSelected = false; - return createNodeOrEdgeSelectionChange(false)(n); - }) as NodeChange[]; + const nodesToUnselect = nodeHelper(nodes) + .flatten() + .map((n) => { + n.isSelected = false; + return createNodeOrEdgeSelectionChange(false)(n); + }) as NodeChange[]; const edgesToUnselect = edges.map(createNodeOrEdgeSelectionChange(false)) as EdgeChange[]; if (nodesToUnselect.length) { diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 7e88a608..f1441d29 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -342,29 +342,3 @@ export function applyNodeChanges(changes: NodeChange[], nodes: Node[]): Node[] { export function applyEdgeChanges(changes: EdgeChange[], edges: Edge[]): Edge[] { return applyChanges(changes, edges) as Edge[]; } - -function flat(arr: Node[], target: Node[]) { - arr.forEach(function (el) { - if (el.childNodes) { - flat(el.childNodes, target); - } else { - target.push(el); - } - }); -} - -export function flattenNodes(nodes: Node[]): Node[] { - const flattened: Node[] = []; - flat(nodes, flattened); - return flattened; - - // return nodes.reduce((result, node) => { - // result.push(node); - - // if (node.childNodes) { - // result.push(...flattenNodes(node.childNodes)); - // } - - // return result; - // }, []); -} diff --git a/src/utils/nodes.ts b/src/utils/nodes.ts new file mode 100644 index 00000000..47a33898 --- /dev/null +++ b/src/utils/nodes.ts @@ -0,0 +1,121 @@ +import { Node } from '../types'; + +function flat(arr: Node[], target: Node[]) { + arr.forEach(function (el) { + if (el.childNodes) { + flat(el.childNodes, target); + } else { + target.push(el); + } + }); +} + +const filterNodes = (condition: (node: Node) => boolean, nodes: Node[]): Node[] => { + let res = []; + + for (let i = 0; i < nodes.length; i++) { + const n = nodes[i]; + + if (condition(n)) { + res.push(n); + } + + if (n.childNodes) { + const matches = filterNodes(condition, n.childNodes); + + for (let j = 0; j < matches.length; j++) { + res.push(matches[j]); + } + } + } + + return res; +}; + +const mapNodes = (accessor: (node: Node) => any, nodes: Node[]): Node[] => { + let res = []; + + for (let i = 0; i < nodes.length; i++) { + const n = nodes[i]; + res.push(accessor(n)); + + if (n.childNodes) { + n.childNodes = mapNodes(accessor, n.childNodes); + } + } + + return res; +}; + +const forEachNode = (accessor: (node: Node) => any, nodes: Node[]): void => { + for (let i = 0; i < nodes.length; i++) { + const n = nodes[i]; + accessor(n); + + if (n.childNodes) { + forEachNode(accessor, n.childNodes); + } + } +}; + +function findNode(accessor: (node: Node) => boolean, nodes: Node[]): Node | undefined { + let res = undefined; + + for (let i = 0; i < nodes.length; i++) { + const n = nodes[i]; + + if (accessor(n)) { + return n; + } + + if (n.childNodes) { + res = findNode(accessor, n.childNodes); + + if (res) { + return res; + } + } + } + + return res; +} + +export interface NodeHelper { + filter: (accessor: (node: Node) => boolean) => Node[]; + map: (accessor: (node: Node) => any) => Node[]; + find: (accessor: (node: Node) => boolean) => Node | undefined; + forEach: (accessor: (node: Node) => void) => void; + flatten: () => Node[]; +} + +export function nodeHelper(nodes: Node[]): NodeHelper { + const flatten: NodeHelper['flatten'] = () => { + const flattened: Node[] = []; + flat(nodes, flattened); + return flattened; + }; + + const filter: NodeHelper['filter'] = (accessor) => { + return filterNodes(accessor, nodes); + }; + + const forEach: NodeHelper['forEach'] = (accessor) => { + return forEachNode(accessor, nodes); + }; + + const find: NodeHelper['find'] = (accessor) => { + return findNode(accessor, nodes); + }; + + const map: NodeHelper['map'] = (accessor) => { + return mapNodes(accessor, nodes); + }; + + return { + filter, + forEach, + flatten, + find, + map, + }; +} From b008c7b5485b697c58f17c4525a96e480f6763be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 21 Oct 2021 12:04:58 +0200 Subject: [PATCH 020/263] chore(graph-utils): rename elements to edges --- src/utils/graph.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils/graph.ts b/src/utils/graph.ts index f1441d29..b4cb5edc 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -61,10 +61,9 @@ export const getMarkerId = (marker: EdgeMarkerType | undefined): string => { .join('&'); }; -const connectionExists = (edge: Edge, elements: Edge[]) => { - return elements.some( +const connectionExists = (edge: Edge, edges: Edge[]) => { + return edges.some( (el) => - isEdge(el) && el.source === edge.source && el.target === edge.target && (el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) && From b15d3012e0de60bdb9046f8db3c59931a5f2b153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 21 Oct 2021 14:08:39 +0200 Subject: [PATCH 021/263] fix(nested-nodes): fix getSourceTargetNode for childNodes --- example/src/Basic/index.tsx | 40 ++++++++++++++++++++++++++--- src/container/EdgeRenderer/utils.ts | 36 ++++++++++++++++---------- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index a9802e6f..513c7621 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -31,7 +31,13 @@ const initialNodes: Node[] = [ className: 'light', style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, childNodes: [ - { id: '4a', data: { label: 'Node 4a' }, position: { x: 400, y: 400 }, className: 'light' }, + { + id: '4a', + draggable: false, + data: { label: 'Node 4a', isNested: true }, + position: { x: 400, y: 400 }, + className: 'light', + }, { id: '4b', data: { label: 'Node 4b' }, @@ -39,8 +45,20 @@ const initialNodes: Node[] = [ className: 'light', style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, childNodes: [ - { id: '4b1', data: { label: 'Node 4b1' }, position: { x: 450, y: 450 }, className: 'light' }, - { id: '4b2', data: { label: 'Node 4b2' }, position: { x: 550, y: 550 }, className: 'light' }, + { + id: '4b1', + draggable: false, + data: { label: 'Node 4b1', isNested: true }, + position: { x: 450, y: 450 }, + className: 'light', + }, + { + id: '4b2', + draggable: false, + data: { label: 'Node 4b2', isNested: true }, + position: { x: 550, y: 550 }, + className: 'light', + }, ], }, ], @@ -58,7 +76,9 @@ const BasicFlow = () => { const [edges, setEdges] = useState(initialEdges); const onConnect = useCallback((params: Edge | Connection) => { - setEdges((eds) => addEdge(params, eds)); + setEdges((eds) => { + return addEdge(params, eds); + }); }, []); const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []); @@ -87,6 +107,15 @@ const BasicFlow = () => { }); }; + const toggleChildNodes = () => { + setNodes((nds) => { + return nodeHelper(nds).map((n) => { + n.isHidden = n.data.isNested && !n.isHidden; + return n; + }); + }); + }; + const onNodesChange = useCallback((changes: NodeChange[]) => { setNodes((ns) => applyNodeChanges(changes, ns)); }, []); @@ -125,6 +154,9 @@ const BasicFlow = () => { +
diff --git a/src/container/EdgeRenderer/utils.ts b/src/container/EdgeRenderer/utils.ts index f6ad4360..c9c3ddc8 100644 --- a/src/container/EdgeRenderer/utils.ts +++ b/src/container/EdgeRenderer/utils.ts @@ -172,18 +172,26 @@ type SourceTargetNode = { }; export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNode => { - return nodeHelper(nodes) - .flatten() - .reduce( - (res, node) => { - if (node.id === edge.source) { - res.sourceNode = node; - } - if (node.id === edge.target) { - res.targetNode = node; - } - return res; - }, - { sourceNode: null, targetNode: null } as SourceTargetNode - ); + const sourceNode = nodeHelper(nodes).find((n) => n.id === edge.source) || null; + const targetNode = nodeHelper(nodes).find((n) => n.id === edge.target) || null; + + return { + sourceNode, + targetNode, + }; + + // return nodeHelper(nodes) + // .flatten() + // .reduce( + // (res, node) => { + // if (node.id === edge.source) { + // res.sourceNode = node; + // } + // if (node.id === edge.target) { + // res.targetNode = node; + // } + // return res; + // }, + // { sourceNode: null, targetNode: null } as SourceTargetNode + // ); }; From 64239fbb5176142b26b46efffdc74de53f2c6395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 21 Oct 2021 14:33:34 +0200 Subject: [PATCH 022/263] refactor(edgerenderer): cleanup edge utils --- src/container/EdgeRenderer/index.tsx | 1 + src/container/EdgeRenderer/utils.ts | 36 +++++++++++----------------- src/utils/nodes.ts | 3 +-- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 0b83ce99..dc8e960b 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -249,6 +249,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { {edges.map((edge: Edge) => { + // @todo: getSourceTargetNodes is called many times during dragging/creating edges const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes); return ( { - const sourceNode = nodeHelper(nodes).find((n) => n.id === edge.source) || null; - const targetNode = nodeHelper(nodes).find((n) => n.id === edge.target) || null; - - return { - sourceNode, - targetNode, - }; - - // return nodeHelper(nodes) - // .flatten() - // .reduce( - // (res, node) => { - // if (node.id === edge.source) { - // res.sourceNode = node; - // } - // if (node.id === edge.target) { - // res.targetNode = node; - // } - // return res; - // }, - // { sourceNode: null, targetNode: null } as SourceTargetNode - // ); + return nodeHelper(nodes) + .flatten() + .reduce( + (res, node) => { + if (node.id === edge.source) { + res.sourceNode = node; + } + if (node.id === edge.target) { + res.targetNode = node; + } + return res; + }, + { sourceNode: null, targetNode: null } as SourceTargetNode + ); }; diff --git a/src/utils/nodes.ts b/src/utils/nodes.ts index 47a33898..c5520b1b 100644 --- a/src/utils/nodes.ts +++ b/src/utils/nodes.ts @@ -2,10 +2,9 @@ import { Node } from '../types'; function flat(arr: Node[], target: Node[]) { arr.forEach(function (el) { + target.push(el); if (el.childNodes) { flat(el.childNodes, target); - } else { - target.push(el); } }); } From 2252ea11ae2760a00db808a7849b2d90cb78cd1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 21 Oct 2021 15:39:27 +0200 Subject: [PATCH 023/263] feat(nested-nodes): change data structure to flat array of nodes --- example/src/Basic/index.tsx | 68 +++++++------- src/components/ConnectionLine/index.tsx | 4 +- src/container/EdgeRenderer/utils.ts | 27 +++--- src/container/NodeRenderer/index.tsx | 26 ++--- src/index.ts | 2 - src/store/index.ts | 20 ++-- src/types/index.ts | 2 +- src/utils/nodes.ts | 120 ------------------------ 8 files changed, 64 insertions(+), 205 deletions(-) delete mode 100644 src/utils/nodes.ts diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index 513c7621..aa854864 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -13,7 +13,6 @@ import ReactFlow, { EdgeChange, OnLoadParams, Connection, - nodeHelper, } from 'react-flow-renderer'; const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); @@ -30,38 +29,35 @@ const initialNodes: Node[] = [ position: { x: 400, y: 200 }, className: 'light', style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, - childNodes: [ - { - id: '4a', - draggable: false, - data: { label: 'Node 4a', isNested: true }, - position: { x: 400, y: 400 }, - className: 'light', - }, - { - id: '4b', - data: { label: 'Node 4b' }, - position: { x: 500, y: 500 }, - className: 'light', - style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, - childNodes: [ - { - id: '4b1', - draggable: false, - data: { label: 'Node 4b1', isNested: true }, - position: { x: 450, y: 450 }, - className: 'light', - }, - { - id: '4b2', - draggable: false, - data: { label: 'Node 4b2', isNested: true }, - position: { x: 550, y: 550 }, - className: 'light', - }, - ], - }, - ], + }, + { + id: '4a', + data: { label: 'Node 4a', isNested: true }, + position: { x: 400, y: 400 }, + className: 'light', + parentNode: '4', + }, + { + id: '4b', + data: { label: 'Node 4b' }, + position: { x: 500, y: 500 }, + className: 'light', + style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, + parentNode: '4', + }, + { + id: '4b1', + data: { label: 'Node 4b1', isNested: true }, + position: { x: 450, y: 450 }, + className: 'light', + parentNode: '4b', + }, + { + id: '4b2', + data: { label: 'Node 4b2', isNested: true }, + position: { x: 550, y: 550 }, + className: 'light', + parentNode: '4b', }, ]; @@ -100,7 +96,7 @@ const BasicFlow = () => { const toggleClassnames = () => { setNodes((nds) => { - return nodeHelper(nds).map((n) => { + return nds.map((n) => { n.className = n.className === 'light' ? 'dark' : 'light'; return n; }); @@ -109,8 +105,8 @@ const BasicFlow = () => { const toggleChildNodes = () => { setNodes((nds) => { - return nodeHelper(nds).map((n) => { - n.isHidden = n.data.isNested && !n.isHidden; + return nds.map((n) => { + n.isHidden = !!n.parentNode && !n.isHidden; return n; }); }); diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index f69da656..8095b821 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -14,8 +14,6 @@ import { HandleType, ReactFlowState, } from '../../types'; -import { nodeHelper } from '../../utils/nodes'; - interface ConnectionLineProps { connectionNodeId: ElementId; connectionHandleId: ElementId | null; @@ -29,7 +27,7 @@ interface ConnectionLineProps { CustomConnectionLineComponent?: ConnectionLineComponent; } -const nodesSelector = (s: ReactFlowState) => nodeHelper(s.nodes).flatten(); +const nodesSelector = (s: ReactFlowState) => s.nodes; export default ({ connectionNodeId, diff --git a/src/container/EdgeRenderer/utils.ts b/src/container/EdgeRenderer/utils.ts index f6ad4360..d3e4ac20 100644 --- a/src/container/EdgeRenderer/utils.ts +++ b/src/container/EdgeRenderer/utils.ts @@ -3,7 +3,6 @@ import { ComponentType } from 'react'; import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges'; import wrapEdge from '../../components/Edges/wrapEdge'; import { rectToBox } from '../../utils/graph'; -import { nodeHelper } from '../../utils/nodes'; import { EdgeTypesType, @@ -172,18 +171,16 @@ type SourceTargetNode = { }; export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNode => { - return nodeHelper(nodes) - .flatten() - .reduce( - (res, node) => { - if (node.id === edge.source) { - res.sourceNode = node; - } - if (node.id === edge.target) { - res.targetNode = node; - } - return res; - }, - { sourceNode: null, targetNode: null } as SourceTargetNode - ); + return nodes.reduce( + (res, node) => { + if (node.id === edge.source) { + res.sourceNode = node; + } + if (node.id === edge.target) { + res.targetNode = node; + } + return res; + }, + { sourceNode: null, targetNode: null } as SourceTargetNode + ); }; diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 2f8be03f..58026bff 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -77,8 +77,16 @@ function Nodes({ typeof node.height !== 'undefined'; let childRect; - if (node.childNodes) { - childRect = getRectOfNodes(node.childNodes); + const childNodes = nodes.filter((n) => n.parentNode === node.id); + + // if (childNodes.length) { + // console.log(node.id, childNodes, getRectOfNodes(childNodes)); + // } + + // console.log(childNodes); + + if (childNodes.length) { + childRect = getRectOfNodes(childNodes); node.position = node.isDragging ? node.position : { x: Math.round(childRect.x) - 10, y: Math.round(childRect.y) - 10 }; @@ -126,20 +134,6 @@ function Nodes({ dragHandle={node.dragHandle} zIndex={3 + recursionDepth} /> - {node.childNodes && ( - - )} ); }); diff --git a/src/index.ts b/src/index.ts index 139872f6..47989de5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,8 +26,6 @@ export { applyEdgeChanges, } from './utils/graph'; -export { nodeHelper } from './utils/nodes'; - export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'; export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'; diff --git a/src/store/index.ts b/src/store/index.ts index 210d9d33..9da0b50c 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -28,7 +28,6 @@ import { NodePositionChange, } from '../types'; import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph'; -import { nodeHelper } from '../utils/nodes'; import { getHandleBounds } from '../components/Nodes/utils'; const { Provider, useStore, useStoreApi } = createContext(); @@ -127,7 +126,7 @@ const createStore = () => const { onNodesChange, nodes, transform } = get(); const nodesToChange: NodeChange[] = updates.reduce((res, update) => { - const node = nodeHelper(nodes).find((n) => n.id === update.id); + const node = nodes.find((n) => n.id === update.id); if (node) { const dimensions = getDimensions(update.nodeElement); @@ -181,12 +180,11 @@ const createStore = () => const { onNodesChange, nodes, nodeExtent } = get(); if (onNodesChange) { - const matchingNodes = nodeHelper(nodes).filter((n) => n.id === id || !!n.isSelected); - const changingNodes = nodeHelper(matchingNodes).flatten(); + const matchingNodes = nodes.filter((n) => n.id === id || n.parentNode === id || !!n.isSelected); - if (changingNodes?.length) { + if (matchingNodes?.length) { onNodesChange( - changingNodes.map((n) => { + matchingNodes.map((n) => { const change: NodePositionChange = { id: n.id, type: 'position', @@ -304,12 +302,10 @@ const createStore = () => unselectNodesAndEdges: () => { const { nodes, edges, onNodesChange, onEdgesChange } = get(); - const nodesToUnselect = nodeHelper(nodes) - .flatten() - .map((n) => { - n.isSelected = false; - return createNodeOrEdgeSelectionChange(false)(n); - }) as NodeChange[]; + const nodesToUnselect = nodes.map((n) => { + n.isSelected = false; + return createNodeOrEdgeSelectionChange(false)(n); + }) as NodeChange[]; const edgesToUnselect = edges.map(createNodeOrEdgeSelectionChange(false)) as EdgeChange[]; if (nodesToUnselect.length) { diff --git a/src/types/index.ts b/src/types/index.ts index b3133c0a..23fbb7e1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -86,7 +86,7 @@ export interface Node { width?: number | null; height?: number | null; handleBounds?: NodeHandleBounds; - childNodes?: Node[]; + parentNode?: ElementId; } export enum ArrowHeadType { diff --git a/src/utils/nodes.ts b/src/utils/nodes.ts deleted file mode 100644 index c5520b1b..00000000 --- a/src/utils/nodes.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Node } from '../types'; - -function flat(arr: Node[], target: Node[]) { - arr.forEach(function (el) { - target.push(el); - if (el.childNodes) { - flat(el.childNodes, target); - } - }); -} - -const filterNodes = (condition: (node: Node) => boolean, nodes: Node[]): Node[] => { - let res = []; - - for (let i = 0; i < nodes.length; i++) { - const n = nodes[i]; - - if (condition(n)) { - res.push(n); - } - - if (n.childNodes) { - const matches = filterNodes(condition, n.childNodes); - - for (let j = 0; j < matches.length; j++) { - res.push(matches[j]); - } - } - } - - return res; -}; - -const mapNodes = (accessor: (node: Node) => any, nodes: Node[]): Node[] => { - let res = []; - - for (let i = 0; i < nodes.length; i++) { - const n = nodes[i]; - res.push(accessor(n)); - - if (n.childNodes) { - n.childNodes = mapNodes(accessor, n.childNodes); - } - } - - return res; -}; - -const forEachNode = (accessor: (node: Node) => any, nodes: Node[]): void => { - for (let i = 0; i < nodes.length; i++) { - const n = nodes[i]; - accessor(n); - - if (n.childNodes) { - forEachNode(accessor, n.childNodes); - } - } -}; - -function findNode(accessor: (node: Node) => boolean, nodes: Node[]): Node | undefined { - let res = undefined; - - for (let i = 0; i < nodes.length; i++) { - const n = nodes[i]; - - if (accessor(n)) { - return n; - } - - if (n.childNodes) { - res = findNode(accessor, n.childNodes); - - if (res) { - return res; - } - } - } - - return res; -} - -export interface NodeHelper { - filter: (accessor: (node: Node) => boolean) => Node[]; - map: (accessor: (node: Node) => any) => Node[]; - find: (accessor: (node: Node) => boolean) => Node | undefined; - forEach: (accessor: (node: Node) => void) => void; - flatten: () => Node[]; -} - -export function nodeHelper(nodes: Node[]): NodeHelper { - const flatten: NodeHelper['flatten'] = () => { - const flattened: Node[] = []; - flat(nodes, flattened); - return flattened; - }; - - const filter: NodeHelper['filter'] = (accessor) => { - return filterNodes(accessor, nodes); - }; - - const forEach: NodeHelper['forEach'] = (accessor) => { - return forEachNode(accessor, nodes); - }; - - const find: NodeHelper['find'] = (accessor) => { - return findNode(accessor, nodes); - }; - - const map: NodeHelper['map'] = (accessor) => { - return mapNodes(accessor, nodes); - }; - - return { - filter, - forEach, - flatten, - find, - map, - }; -} From c1d5c35a0434f9bb6bfe4318d0566ee962e5093e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 21 Oct 2021 16:59:52 +0200 Subject: [PATCH 024/263] feat(nested-nodes): recursively get children of moved node --- src/container/NodeRenderer/index.tsx | 2 +- src/store/index.ts | 59 ++++++++++++++++------------ 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 58026bff..d78552ae 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -77,7 +77,7 @@ function Nodes({ typeof node.height !== 'undefined'; let childRect; - const childNodes = nodes.filter((n) => n.parentNode === node.id); + const childNodes = nodes.filter((n) => n.parentNode === node.id && !n.isHidden); // if (childNodes.length) { // console.log(node.id, childNodes, getRectOfNodes(childNodes)); diff --git a/src/store/index.ts b/src/store/index.ts index 9da0b50c..977ac643 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -38,6 +38,39 @@ const createNodeOrEdgeSelectionChange = (isSelected: boolean) => (item: Node | E isSelected, }); +// @todo needs refactoring / improvements +function findMatchingNodes(id: string | undefined, nodes: Node[]): Node[] { + if (!id) { + return nodes.filter((n) => !!n.isSelected); + } + + const result = []; + const children = []; + + for (let i = 0; i < nodes.length; i++) { + const n = nodes[i]; + + if (n.id === id) { + result.push(n); + } + + if (n.parentNode === id) { + children.push(n); + } + } + + for (let i = 0; i < children.length; i++) { + const n = children[i]; + const matches = findMatchingNodes(n.id, nodes); + + for (let j = 0; j < matches.length; j++) { + result.push(matches[j]); + } + } + + return result; +} + const createStore = () => create((set, get) => ({ width: 0, @@ -150,37 +183,13 @@ const createStore = () => return res; }, []); - // const nodesToChange: NodeChange[] = flattenNodes(nodes).reduce((res, node) => { - // const update = updates.find((u) => u.id === node.id); - // if (update) { - // const dimensions = getDimensions(update.nodeElement); - // const doUpdate = - // dimensions.width && - // dimensions.height && - // (node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate); - - // if (doUpdate) { - // const handleBounds = getHandleBounds(update.nodeElement, transform[2]); - // const change = { - // id: node.id, - // type: 'dimensions', - // dimensions, - // handleBounds, - // } as NodeChange; - // res.push(change); - // } - // } - - // return res; - // }, initialChanges); - onNodesChange?.(nodesToChange); }, updateNodePosition: ({ id, diff, isDragging }: NodeDiffUpdate) => { const { onNodesChange, nodes, nodeExtent } = get(); if (onNodesChange) { - const matchingNodes = nodes.filter((n) => n.id === id || n.parentNode === id || !!n.isSelected); + const matchingNodes = findMatchingNodes(id, nodes); if (matchingNodes?.length) { onNodesChange( From 8b95cd712f53c710823e4d1936feb3a5bd3eb41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Fri, 22 Oct 2021 12:09:01 +0200 Subject: [PATCH 025/263] feat(noderenderer): render nodes recursively --- example/src/Basic/index.tsx | 7 +++--- src/container/NodeRenderer/index.tsx | 37 +++++++++++++++++++--------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index aa854864..3b0cf32e 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -32,7 +32,7 @@ const initialNodes: Node[] = [ }, { id: '4a', - data: { label: 'Node 4a', isNested: true }, + data: { label: 'Node 4a' }, position: { x: 400, y: 400 }, className: 'light', parentNode: '4', @@ -47,14 +47,14 @@ const initialNodes: Node[] = [ }, { id: '4b1', - data: { label: 'Node 4b1', isNested: true }, + data: { label: 'Node 4b1' }, position: { x: 450, y: 450 }, className: 'light', parentNode: '4b', }, { id: '4b2', - data: { label: 'Node 4b2', isNested: true }, + data: { label: 'Node 4b2' }, position: { x: 550, y: 550 }, className: 'light', parentNode: '4b', @@ -135,6 +135,7 @@ const BasicFlow = () => { defaultZoom={1.5} minZoom={0.2} maxZoom={4} + onlyRenderVisibleElements={false} > diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index d78552ae..72bae44a 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -31,7 +31,7 @@ const selector = (s: ReactFlowState) => ({ }); interface NodesProps extends NodeRendererProps { - nodes?: Node[]; + nodes: Node[]; isDraggable?: boolean; resizeObserver: ResizeObserver | null; scale: number; @@ -41,10 +41,12 @@ interface NodesProps extends NodeRendererProps { nodesConnectable: boolean; elementsSelectable: boolean; recursionDepth: number; + parentId?: string; } function Nodes({ - nodes = [], + nodes, + parentId, isDraggable, resizeObserver, scale, @@ -56,7 +58,12 @@ function Nodes({ recursionDepth, ...props }: NodesProps): any { - return nodes.map((node) => { + const rootNodes = useMemo( + () => (parentId ? nodes.filter((n) => n.parentNode === parentId) : nodes.filter((n) => !n.parentNode)), + [nodes, parentId] + ); + + return rootNodes.map((node) => { const nodeType = node.type || 'default'; if (!props.nodeTypes[nodeType]) { @@ -75,18 +82,11 @@ function Nodes({ node.height !== null && typeof node.width !== 'undefined' && typeof node.height !== 'undefined'; - let childRect; const childNodes = nodes.filter((n) => n.parentNode === node.id && !n.isHidden); - // if (childNodes.length) { - // console.log(node.id, childNodes, getRectOfNodes(childNodes)); - // } - - // console.log(childNodes); - if (childNodes.length) { - childRect = getRectOfNodes(childNodes); + const childRect = getRectOfNodes(childNodes); node.position = node.isDragging ? node.position : { x: Math.round(childRect.x) - 10, y: Math.round(childRect.y) - 10 }; @@ -130,10 +130,23 @@ function Nodes({ isDraggable={isNodeDraggable} isSelectable={isSelectable} isConnectable={isConnectable} - resizeObserver={resizeObserver} + resizeObserver={childNodes.length ? resizeObserver : null} dragHandle={node.dragHandle} zIndex={3 + recursionDepth} /> + ); }); From dc36ebf31ff35c4beafe9c959045de7fd62ae88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Fri, 22 Oct 2021 15:13:14 +0200 Subject: [PATCH 026/263] feat(nested-nodes): callback for parent node size change --- example/src/Basic/index.tsx | 32 +++-- src/container/NodeRenderer/index.tsx | 174 ++++++++++++++++++--------- 2 files changed, 139 insertions(+), 67 deletions(-) diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index 3b0cf32e..e8c650e1 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -1,4 +1,4 @@ -import { useState, MouseEvent, useCallback } from 'react'; +import { useState, useMemo, MouseEvent, useCallback } from 'react'; import ReactFlow, { addEdge, @@ -26,21 +26,23 @@ const initialNodes: Node[] = [ { id: '4', data: { label: 'Node 4' }, - position: { x: 400, y: 200 }, + position: { x: 0, y: 0 }, className: 'light', style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, + width: 0, + height: 0, }, { id: '4a', data: { label: 'Node 4a' }, - position: { x: 400, y: 400 }, + position: { x: 70, y: 200 }, className: 'light', parentNode: '4', }, { id: '4b', data: { label: 'Node 4b' }, - position: { x: 500, y: 500 }, + position: { x: 0, y: 0 }, className: 'light', style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, parentNode: '4', @@ -48,14 +50,14 @@ const initialNodes: Node[] = [ { id: '4b1', data: { label: 'Node 4b1' }, - position: { x: 450, y: 450 }, + position: { x: 150, y: 270 }, className: 'light', parentNode: '4b', }, { id: '4b2', data: { label: 'Node 4b2' }, - position: { x: 550, y: 550 }, + position: { x: 420, y: 370 }, className: 'light', parentNode: '4b', }, @@ -64,6 +66,13 @@ const initialNodes: Node[] = [ const initialEdges: Edge[] = [ { id: 'e1-2', source: '1', target: '2', animated: true }, { id: 'e1-3', source: '1', target: '3' }, + { id: 'e2-4a', source: '2', target: '4a', animated: true }, + { id: 'e3-4', source: '3', target: '4' }, + { id: 'e3-4b', source: '3', target: '4b' }, + { id: 'e3-4b2', source: '3', target: '4b2' }, + { id: 'e4a-4b1', source: '4a', target: '4b1' }, + { id: 'e4a-4b2', source: '4a', target: '4b2' }, + { id: 'e4b1-4b2', source: '4b1', target: '4b2' }, ]; const BasicFlow = () => { @@ -120,9 +129,18 @@ const BasicFlow = () => { setEdges((es) => applyEdgeChanges(changes, es)); }, []); + const nodesWithLabel = useMemo( + () => + nodes.map((n) => { + n.data = { ...n.data, label: `${n.width}x${n.height}` }; + return n; + }), + [nodes] + ); + return ( s.onNodesChange); + const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; + const isNodeDraggable = + typeof isDraggable !== 'undefined' + ? isDraggable + : !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')); + const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined')); + const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined')); + const isInitialized = + node.width !== null && + node.height !== null && + typeof node.width !== 'undefined' && + typeof node.height !== 'undefined'; + + const childNodes = useMemo(() => nodes.filter((n) => n.parentNode === node.id && !n.isHidden), [nodes, node.id]); + const childRect = useMemo(() => getRectOfNodes(childNodes), [childNodes]); + const isParentNode = !!childNodes.length; + + node.style = useMemo(() => { + if (isParentNode) { + return { + ...node.style, + width: Math.floor(childRect.width) + 20, + height: Math.floor(childRect.height) + 20, + boxSizing: 'border-box', + }; + } + return node.style; + }, [childRect.width, childRect.height, isParentNode, node.style]); + + useEffect(() => { + if (onNodesChange && isParentNode) { + onNodesChange([ + { + type: 'position', + id: node.id, + position: { x: Math.floor(childRect.x) - 10, y: Math.floor(childRect.y) - 10 }, + }, + ]); + } + }, [childRect.width, childRect.height, childRect.x, childRect.y, isParentNode]); + + return ( + + ); +} + function Nodes({ nodes, parentId, @@ -70,69 +171,22 @@ function Nodes({ console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); } - const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; - const isNodeDraggable = - typeof isDraggable !== 'undefined' - ? isDraggable - : !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')); - const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined')); - const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined')); - const isInitialized = - node.width !== null && - node.height !== null && - typeof node.width !== 'undefined' && - typeof node.height !== 'undefined'; - - const childNodes = nodes.filter((n) => n.parentNode === node.id && !n.isHidden); - - if (childNodes.length) { - const childRect = getRectOfNodes(childNodes); - node.position = node.isDragging - ? node.position - : { x: Math.round(childRect.x) - 10, y: Math.round(childRect.y) - 10 }; - node.style = { - ...node.style, - width: Math.round(childRect.width) + 20, - height: Math.round(childRect.height) + 20, - boxSizing: 'border-box', - }; - } - return ( - Date: Mon, 25 Oct 2021 13:04:15 +0200 Subject: [PATCH 027/263] fix(hooks/keypress): check if document is defined --- src/hooks/useKeyPress.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/hooks/useKeyPress.ts b/src/hooks/useKeyPress.ts index 3b1b94d0..4095af3f 100644 --- a/src/hooks/useKeyPress.ts +++ b/src/hooks/useKeyPress.ts @@ -6,14 +6,16 @@ type Keys = Array; type PressedKeys = Set; type KeyOrCode = 'key' | 'code'; export interface UseKeyPressOptions { - target: Document | HTMLElement | ShadowRoot; + target: Document | HTMLElement | ShadowRoot | null; } +const doc = typeof document !== 'undefined' ? document : null; + // the keycode can be a string 'a' or an array of strings ['a', 'a+d'] // a string means a single key 'a' or a combination when '+' is used 'a+d' // an array means different possibilites. Explainer: ['a', 'd+s'] here the // user can use the single key 'a' or the combination 'd' + 's' -export default (keyCode: KeyCode | null = null, options: UseKeyPressOptions = { target: document }): boolean => { +export default (keyCode: KeyCode | null = null, options: UseKeyPressOptions = { target: doc }): boolean => { const [keyPressed, setKeyPressed] = useState(false); // we need to remember the pressed keys in order to support combinations @@ -64,16 +66,16 @@ export default (keyCode: KeyCode | null = null, options: UseKeyPressOptions = { setKeyPressed(false); }; - options.target.addEventListener('keydown', downHandler as EventListenerOrEventListenerObject); - options.target.addEventListener('keyup', upHandler as EventListenerOrEventListenerObject); - options.target.addEventListener('blur', resetHandler); + options?.target?.addEventListener('keydown', downHandler as EventListenerOrEventListenerObject); + options?.target?.addEventListener('keyup', upHandler as EventListenerOrEventListenerObject); + options?.target?.addEventListener('blur', resetHandler); return () => { pressedKeys.current.clear(); - options.target.removeEventListener('keydown', downHandler as EventListenerOrEventListenerObject); - options.target.removeEventListener('keyup', upHandler as EventListenerOrEventListenerObject); - options.target.removeEventListener('blur', resetHandler); + options?.target?.removeEventListener('keydown', downHandler as EventListenerOrEventListenerObject); + options?.target?.removeEventListener('keyup', upHandler as EventListenerOrEventListenerObject); + options?.target?.removeEventListener('blur', resetHandler); }; } }, [keyCode, setKeyPressed]); From e8d26002a11103fad7f2be28743ff0178d75d548 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 25 Oct 2021 16:56:54 +0200 Subject: [PATCH 028/263] refactor(node-renderer): do not mutate node styles --- src/container/NodeRenderer/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 002b015a..40ebb906 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -83,14 +83,14 @@ function Node({ const childRect = useMemo(() => getRectOfNodes(childNodes), [childNodes]); const isParentNode = !!childNodes.length; - node.style = useMemo(() => { + const style = useMemo(() => { if (isParentNode) { return { ...node.style, width: Math.floor(childRect.width) + 20, height: Math.floor(childRect.height) + 20, boxSizing: 'border-box', - }; + } as React.CSSProperties; } return node.style; }, [childRect.width, childRect.height, isParentNode, node.style]); @@ -111,7 +111,7 @@ function Node({ Date: Wed, 27 Oct 2021 12:53:28 +0200 Subject: [PATCH 029/263] feat(noderenderer): convert from flat to nested format internally --- src/container/NodeRenderer/index.tsx | 43 ++++++++++++---------------- src/hooks/useVisibleNodes.ts | 23 +++++++++++++-- src/types/index.ts | 1 + 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 002b015a..3c2025cc 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -44,14 +44,12 @@ interface NodesProps extends NodeRendererProps { parentId?: string; } -interface NodeProps extends NodesProps { - nodes: Node[]; +interface NodeProps extends Omit { node: Node; nodeType: string; } function Node({ - nodes, node, nodeType, isDraggable, @@ -79,7 +77,7 @@ function Node({ typeof node.width !== 'undefined' && typeof node.height !== 'undefined'; - const childNodes = useMemo(() => nodes.filter((n) => n.parentNode === node.id && !n.isHidden), [nodes, node.id]); + const { childNodes = [] } = node; const childRect = useMemo(() => getRectOfNodes(childNodes), [childNodes]); const isParentNode = !!childNodes.length; @@ -147,7 +145,6 @@ function Node({ function Nodes({ nodes, - parentId, isDraggable, resizeObserver, scale, @@ -159,12 +156,7 @@ function Nodes({ recursionDepth, ...props }: NodesProps): any { - const rootNodes = useMemo( - () => (parentId ? nodes.filter((n) => n.parentNode === parentId) : nodes.filter((n) => !n.parentNode)), - [nodes, parentId] - ); - - return rootNodes.map((node) => { + return nodes.map((node) => { const nodeType = node.type || 'default'; if (!props.nodeTypes[nodeType]) { @@ -175,7 +167,6 @@ function Nodes({ - + {node.childNodes && node.childNodes.length > 0 && ( + + )} ); }); diff --git a/src/hooks/useVisibleNodes.ts b/src/hooks/useVisibleNodes.ts index d6c9bd96..4a56f2f4 100644 --- a/src/hooks/useVisibleNodes.ts +++ b/src/hooks/useVisibleNodes.ts @@ -2,7 +2,26 @@ import { useCallback } from 'react'; import { useStore } from '../store'; import { getNodesInside } from '../utils/graph'; -import { ReactFlowState } from '../types'; +import { ReactFlowState, Node } from '../types'; + +function getChildNodes(nodes: Node[], parent?: Node): Node[] { + const children: Node[] = []; + const remaining: Node[] = []; + + for (let i = 0; i < nodes.length; i++) { + const n = nodes[i]; + if ((!parent && !n.parentNode) || n.parentNode === parent?.id) { + children.push(n); + } else { + remaining.push(n); + } + } + + return children.map((child) => { + child.childNodes = getChildNodes(remaining, child); + return child; + }); +} function useVisibleNodes(onlyRenderVisible: boolean) { const nodes = useStore( @@ -16,7 +35,7 @@ function useVisibleNodes(onlyRenderVisible: boolean) { ) ); - return nodes; + return getChildNodes(nodes); } export default useVisibleNodes; diff --git a/src/types/index.ts b/src/types/index.ts index 23fbb7e1..3ecfaa10 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -87,6 +87,7 @@ export interface Node { height?: number | null; handleBounds?: NodeHandleBounds; parentNode?: ElementId; + childNodes?: Node[]; } export enum ArrowHeadType { From 241f6041256b0940cc0902e6fc4d7da867073d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Wed, 27 Oct 2021 15:38:29 +0200 Subject: [PATCH 030/263] feat(nested-nodes): simplify nested node rendering --- example/src/Basic/index.tsx | 27 ++++++++------------ src/components/Nodes/wrapNode.tsx | 11 ++++++-- src/container/NodeRenderer/index.tsx | 38 +++++----------------------- src/store/index.ts | 6 ++--- src/style.css | 1 + src/types/index.ts | 14 +++++----- src/utils/graph.ts | 15 +++++++---- 7 files changed, 47 insertions(+), 65 deletions(-) diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index e8c650e1..0b5c574d 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -26,38 +26,40 @@ const initialNodes: Node[] = [ { id: '4', data: { label: 'Node 4' }, - position: { x: 0, y: 0 }, + position: { x: 100, y: 200 }, className: 'light', style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, - width: 0, - height: 0, + width: 600, + height: 300, }, { id: '4a', data: { label: 'Node 4a' }, - position: { x: 70, y: 200 }, + position: { x: 115, y: 215 }, className: 'light', parentNode: '4', }, { id: '4b', data: { label: 'Node 4b' }, - position: { x: 0, y: 0 }, + position: { x: 250, y: 250 }, className: 'light', style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, parentNode: '4', + height: 200, + width: 350, }, { id: '4b1', data: { label: 'Node 4b1' }, - position: { x: 150, y: 270 }, + position: { x: 270, y: 270 }, className: 'light', parentNode: '4b', }, { id: '4b2', data: { label: 'Node 4b2' }, - position: { x: 420, y: 370 }, + position: { x: 500, y: 400 }, className: 'light', parentNode: '4b', }, @@ -129,18 +131,9 @@ const BasicFlow = () => { setEdges((es) => applyEdgeChanges(changes, es)); }, []); - const nodesWithLabel = useMemo( - () => - nodes.map((n) => { - n.data = { ...n.data, label: `${n.width}x${n.height}` }; - return n; - }), - [nodes] - ); - return ( ) => { scale, xPos, yPos, + width, + height, isSelected, onClick, onMouseEnter, @@ -49,6 +51,7 @@ export default (NodeComponent: ComponentType) => { resizeObserver, dragHandle, zIndex, + isParentNode, }: WrapNodeProps) => { const { addSelectedElements, @@ -70,6 +73,8 @@ export default (NodeComponent: ComponentType) => { isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none', // prevents jumping of nodes on start // opacity: isInitialized ? 1 : 0, + width: isParentNode && width !== null ? width : 'auto', + height: isParentNode && height !== null ? height : 'auto', ...style, }), [ @@ -84,6 +89,7 @@ export default (NodeComponent: ComponentType) => { onMouseEnter, onMouseMove, onMouseLeave, + isParentNode, ] ); @@ -200,10 +206,10 @@ export default (NodeComponent: ComponentType) => { ); useEffect(() => { - if (nodeElement.current && !isHidden && !isInitialized) { + if (nodeElement.current && !isHidden && (!isInitialized || isParentNode)) { updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); } - }, [id, isHidden, sourcePosition, targetPosition, isInitialized]); + }, [id, isHidden, sourcePosition, targetPosition, isInitialized, isParentNode]); useEffect(() => { if (nodeElement.current) { @@ -225,6 +231,7 @@ export default (NodeComponent: ComponentType) => { { selected: isSelected, selectable: isSelectable, + parent: isParentNode, }, ]); diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index e3deef5a..477e1658 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -1,9 +1,8 @@ -import React, { memo, useMemo, ComponentType, MouseEvent, Fragment, useEffect } from 'react'; +import React, { memo, useMemo, ComponentType, MouseEvent, Fragment } from 'react'; import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import { Node, NodeTypesType, ReactFlowState, WrapNodeProps, SnapGrid } from '../../types'; -import { getRectOfNodes } from '../../utils/graph'; import useVisibleNodes from '../../hooks/useVisibleNodes'; interface NodeRendererProps { nodeTypes: NodeTypesType; @@ -63,7 +62,7 @@ function Node({ recursionDepth, ...props }: NodeProps) { - const onNodesChange = useStore((s) => s.onNodesChange); + // const onNodesChange = useStore((s) => s.onNodesChange); const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; const isNodeDraggable = typeof isDraggable !== 'undefined' @@ -77,39 +76,13 @@ function Node({ typeof node.width !== 'undefined' && typeof node.height !== 'undefined'; - const { childNodes = [] } = node; - const childRect = useMemo(() => getRectOfNodes(childNodes), [childNodes]); - const isParentNode = !!childNodes.length; - - const style = useMemo(() => { - if (isParentNode) { - return { - ...node.style, - width: Math.floor(childRect.width) + 20, - height: Math.floor(childRect.height) + 20, - boxSizing: 'border-box', - } as React.CSSProperties; - } - return node.style; - }, [childRect.width, childRect.height, isParentNode, node.style]); - - useEffect(() => { - if (onNodesChange && isParentNode) { - onNodesChange([ - { - type: 'position', - id: node.id, - position: { x: Math.floor(childRect.x) - 10, y: Math.floor(childRect.y) - 10 }, - }, - ]); - } - }, [childRect.width, childRect.height, childRect.x, childRect.y, isParentNode]); + const isParentNode = !!node.childNodes?.length; return ( ); } diff --git a/src/store/index.ts b/src/store/index.ts index 977ac643..2c5ab2db 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -25,7 +25,7 @@ import { OnNodesChange, OnEdgesChange, EdgeChange, - NodePositionChange, + NodeDimensionChange, } from '../types'; import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph'; import { getHandleBounds } from '../components/Nodes/utils'; @@ -194,9 +194,9 @@ const createStore = () => if (matchingNodes?.length) { onNodesChange( matchingNodes.map((n) => { - const change: NodePositionChange = { + const change: NodeDimensionChange = { id: n.id, - type: 'position', + type: 'dimensions', isDragging: !!isDragging, }; diff --git a/src/style.css b/src/style.css index 327cfbef..8fac54d9 100644 --- a/src/style.css +++ b/src/style.css @@ -95,6 +95,7 @@ user-select: none; pointer-events: all; transform-origin: 0 0; + box-sizing: border-box; } .react-flow__nodesselection { diff --git a/src/types/index.ts b/src/types/index.ts index 3ecfaa10..9a84ae20 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,15 +12,12 @@ export type Transform = [number, number, number]; export type NodeDimensionChange = { id: string; type: 'dimensions'; - dimensions: Dimensions; - handleBounds?: NodeHandleBounds; -}; -export type NodePositionChange = { - id: string; - type: 'position'; + dimensions?: Dimensions; position?: XYPosition; + handleBounds?: NodeHandleBounds; isDragging?: boolean; }; + export type NodeSelectionChange = { id: string; type: 'select'; @@ -30,7 +27,7 @@ export type NodeRemoveChange = { id: string; type: 'remove'; }; -export type NodeChange = NodeDimensionChange | NodePositionChange | NodeSelectionChange | NodeRemoveChange; +export type NodeChange = NodeDimensionChange | NodeSelectionChange | NodeRemoveChange; export type EdgeSelectionChange = NodeSelectionChange; export type EdgeRemoveChange = NodeRemoveChange; @@ -277,6 +274,8 @@ export interface WrapNodeProps { scale: number; xPos: number; yPos: number; + width?: number | null; + height?: number | null; isSelectable: boolean; isDraggable: boolean; isConnectable: boolean; @@ -302,6 +301,7 @@ export interface WrapNodeProps { resizeObserver: ResizeObserver | null; dragHandle?: string; zIndex: number; + isParentNode: boolean; } export type FitViewParams = { diff --git a/src/utils/graph.ts b/src/utils/graph.ts index b4cb5edc..179db8f7 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -301,17 +301,22 @@ function applyChanges(changes: NodeChange[] | EdgeChange[], elements: any[]): an if (currentChange) { switch (currentChange.type) { - case 'dimensions': { - res.push({ ...item, ...currentChange.dimensions, handleBounds: currentChange.handleBounds }); - return res; - } case 'select': { res.push({ ...item, isSelected: currentChange.isSelected }); return res; } - case 'position': { + case 'dimensions': { const updateItem = { ...item }; + if (typeof currentChange.dimensions !== 'undefined') { + updateItem.width = currentChange.dimensions.width; + updateItem.height = currentChange.dimensions.height; + } + + if (typeof currentChange.handleBounds !== 'undefined') { + updateItem.handleBounds = currentChange.handleBounds; + } + if (typeof currentChange.position !== 'undefined') { updateItem.position = currentChange.position; } From 3f992db54a880e51c7ab102d77d23392ea41d8f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Wed, 27 Oct 2021 15:47:14 +0200 Subject: [PATCH 031/263] chore(package.json): release 10.0.0-next.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f4030c88..491e545e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.2", + "version": "10.0.0-next.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "10.0.0-next.2", + "version": "10.0.0-next.6", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/package.json b/package.json index 33eb1308..14344f3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.2", + "version": "10.0.0-next.6", "engines": { "node": ">=12" }, From 74e860587cbc574c03c1f038a9ff3da65a9bd337 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 1 Nov 2021 18:12:53 +0100 Subject: [PATCH 032/263] refactor(node): pass width and height dep to style memo --- src/components/Nodes/wrapNode.tsx | 2 ++ src/utils/graph.ts | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 45fe388c..23912700 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -90,6 +90,8 @@ export default (NodeComponent: ComponentType) => { onMouseMove, onMouseLeave, isParentNode, + width, + height, ] ); diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 179db8f7..cdcc2d1f 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -293,10 +293,6 @@ function applyChanges(changes: NodeChange[] | EdgeChange[], elements: any[]): an const initElements: any[] = []; return elements.reduce((res: any[], item: any) => { - if (item.childNodes) { - item.childNodes = applyChanges(changes, item.childNodes); - } - const currentChange = changes.find((c) => c.id === item.id); if (currentChange) { From 6e3ff4e849456861e7e1e9f6f39ceb3386ec65f7 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 2 Nov 2021 12:52:06 +0100 Subject: [PATCH 033/263] refactor(wrapNode): call updateNodeDim when target or source pos changes --- src/components/Nodes/wrapNode.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 23912700..c071d805 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -208,10 +208,10 @@ export default (NodeComponent: ComponentType) => { ); useEffect(() => { - if (nodeElement.current && !isHidden && (!isInitialized || isParentNode)) { + if (nodeElement.current && !isHidden) { updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); } - }, [id, isHidden, sourcePosition, targetPosition, isInitialized, isParentNode]); + }, [id, isHidden, sourcePosition, targetPosition]); useEffect(() => { if (nodeElement.current) { From 65991de037611783d1387895e2981fab668ab386 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 2 Nov 2021 12:52:24 +0100 Subject: [PATCH 034/263] chore(examples): add layouting with new api --- example/src/CustomNode/index.tsx | 5 +- example/src/Layouting/index.tsx | 99 +++++++++++++++++++ .../Layouting/initial-elements.ts | 11 ++- .../Layouting/layouting.css | 0 example/src/index.tsx | 7 +- example/src_oldapi/Layouting/index.tsx | 86 ---------------- package-lock.json | 4 +- package.json | 2 +- 8 files changed, 120 insertions(+), 94 deletions(-) create mode 100644 example/src/Layouting/index.tsx rename example/{src_oldapi => src}/Layouting/initial-elements.ts (88%) rename example/{src_oldapi => src}/Layouting/layouting.css (100%) delete mode 100644 example/src_oldapi/Layouting/index.tsx diff --git a/example/src/CustomNode/index.tsx b/example/src/CustomNode/index.tsx index 5c786f58..9f444c27 100644 --- a/example/src/CustomNode/index.tsx +++ b/example/src/CustomNode/index.tsx @@ -19,7 +19,10 @@ import ReactFlow, { import ColorSelectorNode from './ColorSelectorNode'; -const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance); +const onLoad = (reactFlowInstance: OnLoadParams) => { + console.log('flow loaded:', reactFlowInstance); + reactFlowInstance.fitView(); +}; const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); diff --git a/example/src/Layouting/index.tsx b/example/src/Layouting/index.tsx new file mode 100644 index 00000000..cbae1310 --- /dev/null +++ b/example/src/Layouting/index.tsx @@ -0,0 +1,99 @@ +import { useState, useCallback } from 'react'; +import ReactFlow, { + ReactFlowProvider, + addEdge, + applyNodeChanges, + applyEdgeChanges, + Controls, + NodeChange, + EdgeChange, + Node, + Connection, + Edge, + NodeExtent, + Position, +} from 'react-flow-renderer'; +import dagre from 'dagre'; + +import initialNodesAndEdges from './initial-elements'; + +import './layouting.css'; + +const dagreGraph = new dagre.graphlib.Graph(); +dagreGraph.setDefaultEdgeLabel(() => ({})); + +const nodeExtent: NodeExtent = [ + [0, 0], + [1000, 1000], +]; + +const LayoutFlow = () => { + const [nodes, setNodes] = useState(initialNodesAndEdges.nodes); + const [edges, setEdges] = useState(initialNodesAndEdges.edges); + + const onConnect = useCallback((params: Edge | Connection) => { + setEdges((eds) => { + return addEdge(params, eds); + }); + }, []); + + const onLayout = (direction: string) => { + const isHorizontal = direction === 'LR'; + dagreGraph.setGraph({ rankdir: direction }); + + nodes.forEach((node) => { + dagreGraph.setNode(node.id, { width: 150, height: 50 }); + }); + + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + dagre.layout(dagreGraph); + + const layoutedNodes = nodes.map((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + node.targetPosition = isHorizontal ? Position.Left : Position.Top; + node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom; + // we need to pass a slightly different position in order to notify react flow about the change + // @TODO how can we change the position handling so that we dont need this hack? + node.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y }; + + return node; + }); + + setNodes(layoutedNodes); + }; + + const onNodesChange = useCallback((changes: NodeChange[]) => setNodes((ns) => applyNodeChanges(changes, ns)), []); + + const onEdgesChange = useCallback((changes: EdgeChange[]) => { + setEdges((es) => applyEdgeChanges(changes, es)); + }, []); + + return ( +
+ + onLayout('TB')} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} + > + + +
+ + +
+
+
+ ); +}; + +export default LayoutFlow; diff --git a/example/src_oldapi/Layouting/initial-elements.ts b/example/src/Layouting/initial-elements.ts similarity index 88% rename from example/src_oldapi/Layouting/initial-elements.ts rename to example/src/Layouting/initial-elements.ts index 404b1b8b..ce763320 100644 --- a/example/src_oldapi/Layouting/initial-elements.ts +++ b/example/src/Layouting/initial-elements.ts @@ -1,8 +1,8 @@ -import { Elements, XYPosition } from 'react-flow-renderer'; +import { Node, Edge, XYPosition } from 'react-flow-renderer'; const position: XYPosition = { x: 0, y: 0 }; -const elements: Elements = [ +const nodes: Node[] = [ { id: '1', type: 'input', @@ -56,6 +56,9 @@ const elements: Elements = [ position, }, { id: '7', type: 'output', data: { label: 'output' }, position: { x: 400, y: 450 } }, +]; + +const edges: Edge[] = [ { id: 'e12', source: '1', target: '2', type: 'smoothstep' }, { id: 'e13', source: '1', target: '3', type: 'smoothstep' }, { id: 'e22a', source: '2', target: '2a', type: 'smoothstep' }, @@ -68,4 +71,6 @@ const elements: Elements = [ { id: 'e57', source: '5', target: '7', type: 'smoothstep' }, ]; -export default elements; +const nodesAndEdges = { nodes, edges }; + +export default nodesAndEdges; diff --git a/example/src_oldapi/Layouting/layouting.css b/example/src/Layouting/layouting.css similarity index 100% rename from example/src_oldapi/Layouting/layouting.css rename to example/src/Layouting/layouting.css diff --git a/example/src/index.tsx b/example/src/index.tsx index 60faf4c0..1f44867f 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent } from 'react'; +import { ChangeEvent } from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter as Router, Route, Switch, withRouter } from 'react-router-dom'; @@ -7,6 +7,7 @@ import UpdateNode from './UpdateNode'; import Stress from './Stress'; import CustomNode from './CustomNode'; import FloatingEdges from './FloatingEdges'; +import Layouting from './Layouting'; import './index.css'; @@ -31,6 +32,10 @@ const routes = [ path: '/floating-edges', component: FloatingEdges, }, + { + path: '/layouting', + component: Layouting, + }, ]; const Header = withRouter(({ history, location }) => { diff --git a/example/src_oldapi/Layouting/index.tsx b/example/src_oldapi/Layouting/index.tsx deleted file mode 100644 index 34ee23e6..00000000 --- a/example/src_oldapi/Layouting/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, { useState } from 'react'; -import ReactFlow, { - ReactFlowProvider, - addEdge, - removeElements, - Controls, - isNode, - Elements, - Connection, - Edge, - NodeExtent, - Position, -} from 'react-flow-renderer'; -import dagre from 'dagre'; - -import initialElements from './initial-elements'; - -import './layouting.css'; - -const dagreGraph = new dagre.graphlib.Graph(); -dagreGraph.setDefaultEdgeLabel(() => ({})); - -const nodeExtent: NodeExtent = [ - [0, 0], - [1000, 1000], -]; - -const LayoutFlow = () => { - const [elements, setElements] = useState(initialElements); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - - const onLayout = (direction: string) => { - const isHorizontal = direction === 'LR'; - dagreGraph.setGraph({ rankdir: direction }); - - elements.forEach((el) => { - if (isNode(el)) { - dagreGraph.setNode(el.id, { width: 150, height: 50 }); - } else { - dagreGraph.setEdge(el.source, el.target); - } - }); - - dagre.layout(dagreGraph); - - const layoutedElements = elements.map((el) => { - if (isNode(el)) { - const nodeWithPosition = dagreGraph.node(el.id); - el.targetPosition = isHorizontal ? Position.Left : Position.Top; - el.sourcePosition = isHorizontal ? Position.Right : Position.Bottom; - // we need to pass a slightly different position in order to notify react flow about the change - // @TODO how can we change the position handling so that we dont need this hack? - el.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y }; - } - - return el; - }); - - setElements(layoutedElements); - }; - - return ( -
- - onLayout('TB')} - > - - -
- - -
-
-
- ); -}; - -export default LayoutFlow; diff --git a/package-lock.json b/package-lock.json index 491e545e..121210a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.6", + "version": "10.0.0-next.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "10.0.0-next.6", + "version": "10.0.0-next.7", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/package.json b/package.json index 14344f3c..1cd02bc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.6", + "version": "10.0.0-next.7", "engines": { "node": ">=12" }, From 8cd969dd84b50f74472bf15a512093f6247f1ae9 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 2 Nov 2021 14:33:09 +0100 Subject: [PATCH 035/263] refactor(wrapNode): call update dim when node is not initialized --- package-lock.json | 4 ++-- package.json | 2 +- src/components/Nodes/wrapNode.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 121210a3..423bc22b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.7", + "version": "10.0.0-next.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "10.0.0-next.7", + "version": "10.0.0-next.8", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/package.json b/package.json index 1cd02bc5..79b42e5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.7", + "version": "10.0.0-next.8", "engines": { "node": ">=12" }, diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index c071d805..aa195e18 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -208,10 +208,10 @@ export default (NodeComponent: ComponentType) => { ); useEffect(() => { - if (nodeElement.current && !isHidden) { + if (nodeElement.current && (!isHidden || !isInitialized)) { updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); } - }, [id, isHidden, sourcePosition, targetPosition]); + }, [id, isHidden, sourcePosition, targetPosition, isInitialized]); useEffect(() => { if (nodeElement.current) { From 248b563231b08c8f3c88be0f548a4ecdd8ca5ba9 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 2 Nov 2021 18:58:47 +0100 Subject: [PATCH 036/263] refactor(nodes): use relative positions for children --- example/src/Basic/index.tsx | 8 +- src/components/Nodes/wrapNode.tsx | 2 +- src/container/EdgeRenderer/index.tsx | 17 ++-- src/container/NodeRenderer/index.tsx | 44 +++++++-- src/hooks/useNodeLookup.ts | 12 +++ src/hooks/useVisibleNodes.ts | 10 +- src/store/index.ts | 138 ++++++++++++++++----------- src/types/index.ts | 17 ++++ 8 files changed, 168 insertions(+), 80 deletions(-) create mode 100644 src/hooks/useNodeLookup.ts diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index 0b5c574d..82683ecc 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -35,14 +35,14 @@ const initialNodes: Node[] = [ { id: '4a', data: { label: 'Node 4a' }, - position: { x: 115, y: 215 }, + position: { x: 15, y: 15 }, className: 'light', parentNode: '4', }, { id: '4b', data: { label: 'Node 4b' }, - position: { x: 250, y: 250 }, + position: { x: 80, y: 80 }, className: 'light', style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, parentNode: '4', @@ -52,14 +52,14 @@ const initialNodes: Node[] = [ { id: '4b1', data: { label: 'Node 4b1' }, - position: { x: 270, y: 270 }, + position: { x: 20, y: 20 }, className: 'light', parentNode: '4b', }, { id: '4b2', data: { label: 'Node 4b2' }, - position: { x: 500, y: 400 }, + position: { x: 100, y: 100 }, className: 'light', parentNode: '4b', }, diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index aa195e18..f59d1eb6 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -211,7 +211,7 @@ export default (NodeComponent: ComponentType) => { if (nodeElement.current && (!isHidden || !isInitialized)) { updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); } - }, [id, isHidden, sourcePosition, targetPosition, isInitialized]); + }, [id, isHidden, sourcePosition, targetPosition, type, isInitialized]); useEffect(() => { if (nodeElement.current) { diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index dc8e960b..141b5342 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -4,7 +4,7 @@ import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import ConnectionLine from '../../components/ConnectionLine/index'; import MarkerDefinitions from './MarkerDefinitions'; -import { getEdgePositions, getHandle, getSourceTargetNodes } from './utils'; +import { getEdgePositions, getHandle } from './utils'; import { Position, Edge, @@ -17,6 +17,7 @@ import { NodeHandleBounds, } from '../../types'; import useVisibleEdges from '../../hooks/useVisibleEdges'; +import useNodeLookup from '../../hooks/useNodeLookup'; interface EdgeRendererProps { edgeTypes: any; @@ -232,8 +233,8 @@ const EdgeRenderer = (props: EdgeRendererProps) => { width, height, connectionMode, - nodes, } = useStore(selector, shallow); + const nodeLookup = useNodeLookup(); const edges = useVisibleEdges(props.onlyRenderVisibleElements); @@ -250,20 +251,22 @@ const EdgeRenderer = (props: EdgeRendererProps) => { {edges.map((edge: Edge) => { // @todo: getSourceTargetNodes is called many times during dragging/creating edges - const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes); + const sourceNode = nodeLookup.current.get(edge.source); + const targetNode = nodeLookup.current.get(edge.target); + return ( ({ updateNodeDimensions: s.updateNodeDimensions, snapGrid: s.snapGrid, snapToGrid: s.snapToGrid, + nodeLookup: s.nodeLookup, }); interface NodesProps extends NodeRendererProps { - nodes: Node[]; + nodes: NodeRendererNode[]; + nodeLookup: NodeLookup; isDraggable?: boolean; resizeObserver: ResizeObserver | null; scale: number; @@ -43,13 +54,17 @@ interface NodesProps extends NodeRendererProps { parentId?: string; } -interface NodeProps extends Omit { +interface NodeProps extends Omit { node: Node; nodeType: string; + childNodes?: NodeRendererNode[]; + positionAbsoluteX?: number; + positionAbsoluteY?: number; } function Node({ node, + childNodes, nodeType, isDraggable, resizeObserver, @@ -60,6 +75,8 @@ function Node({ nodesConnectable, elementsSelectable, recursionDepth, + positionAbsoluteX, + positionAbsoluteY, ...props }: NodeProps) { // const onNodesChange = useStore((s) => s.onNodesChange); @@ -76,7 +93,7 @@ function Node({ typeof node.width !== 'undefined' && typeof node.height !== 'undefined'; - const isParentNode = !!node.childNodes?.length; + const isParentNode = !!childNodes?.length; return ( { + return nodes.map(({ node, childNodes }) => { const nodeType = node.type || 'default'; + const lookupNode = nodeLookup.get(node.id); if (!props.nodeTypes[nodeType]) { console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); @@ -143,6 +162,7 @@ function Nodes({ - {node.childNodes && node.childNodes.length > 0 && ( + {childNodes && childNodes.length > 0 && ( { snapGrid, snapToGrid, } = useStore(selector, shallow); + const nodeLookup = useNodeLookup(); + const nodes = useVisibleNodes(props.onlyRenderVisibleElements); const transformStyle = useMemo( @@ -215,6 +240,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
{ - child.childNodes = getChildNodes(remaining, child); + child.childNodes = getChildNodes(remaining, child.node); return child; }); } diff --git a/src/store/index.ts b/src/store/index.ts index 2c5ab2db..9f6e7b2b 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -26,6 +26,8 @@ import { OnEdgesChange, EdgeChange, NodeDimensionChange, + NodeLookup, + NodeLookupItem, } from '../types'; import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph'; import { getHandleBounds } from '../components/Nodes/utils'; @@ -39,36 +41,53 @@ const createNodeOrEdgeSelectionChange = (isSelected: boolean) => (item: Node | E }); // @todo needs refactoring / improvements -function findMatchingNodes(id: string | undefined, nodes: Node[]): Node[] { - if (!id) { - return nodes.filter((n) => !!n.isSelected); +// function findMatchingNodes(id: string | undefined, nodes: Node[]): Node[] { +// if (!id) { +// return nodes.filter((n) => !!n.isSelected); +// } + +// const result = []; +// const children = []; + +// for (let i = 0; i < nodes.length; i++) { +// const n = nodes[i]; + +// if (n.id === id) { +// result.push(n); +// } + +// if (n.parentNode === id) { +// children.push(n); +// } +// } + +// for (let i = 0; i < children.length; i++) { +// const n = children[i]; +// const matches = findMatchingNodes(n.id, nodes); + +// for (let j = 0; j < matches.length; j++) { +// result.push(matches[j]); +// } +// } + +// return result; +// } + +function addPositions(posA: XYPosition, posB: XYPosition): XYPosition { + return { + x: (posA.x ?? 0) + (posB.x ?? 0), + y: (posA.y ?? 0) + (posB.y ?? 0), + }; +} + +function getAbsolutePosition(node: NodeLookupItem, nodeLookup: NodeLookup, result: XYPosition): XYPosition { + const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : false; + + if (!parentNode) { + return result; } - const result = []; - const children = []; - - for (let i = 0; i < nodes.length; i++) { - const n = nodes[i]; - - if (n.id === id) { - result.push(n); - } - - if (n.parentNode === id) { - children.push(n); - } - } - - for (let i = 0; i < children.length; i++) { - const n = children[i]; - const matches = findMatchingNodes(n.id, nodes); - - for (let j = 0; j < matches.length; j++) { - result.push(matches[j]); - } - } - - return result; + return getAbsolutePosition(parentNode, nodeLookup, addPositions(result, parentNode.position || { x: 0, y: 0 })); } const createStore = () => @@ -127,36 +146,45 @@ const createStore = () => reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-', - setNodes: (propNodes: Node[]) => { - const { nodes } = get(); + nodeLookup: new Map(), - const nextNodes = propNodes.map((propNode: Node) => { - const storeNode = nodes.find((node) => node.id === propNode.id); + setNodes: (nodes: Node[]) => { + const { nodeLookup } = get(); - if (storeNode) { - if (typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type) { - // we reset the elements dimensions here in order to force a re-calculation of the bounds. - // When the type of a node changes it is possible that the number or positions of handles changes too. - return { - ...propNode, - width: null, - height: null, - }; - } + nodes.forEach((node) => { + const lookupNode = { + ...nodeLookup.get(node.id), + width: node.width || null, + height: node.height || null, + position: node.position, + positionAbsolute: node.position, + }; + if (node.parentNode) { + lookupNode.parentNode = node.parentNode; } - - return propNode; + nodeLookup.set(node.id, lookupNode); }); - set({ - nodes: nextNodes, - }); + nodes + .filter((node) => node.parentNode) + .forEach((node) => { + const positionAbsolute = getAbsolutePosition(node, nodeLookup, node.position); + + if (positionAbsolute) { + nodeLookup.set(node.id, { + ...nodeLookup.get(node.id), + positionAbsolute, + }); + } + }); + + set({ nodes }); }, setEdges: (edges: Edge[]) => { set({ edges }); }, updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { - const { onNodesChange, nodes, transform } = get(); + const { onNodesChange, nodes, transform, nodeLookup } = get(); const nodesToChange: NodeChange[] = updates.reduce((res, update) => { const node = nodes.find((n) => n.id === update.id); @@ -170,6 +198,8 @@ const createStore = () => if (doUpdate) { const handleBounds = getHandleBounds(update.nodeElement, transform[2]); + nodeLookup.set(node.id, { ...nodeLookup.get(node.id), handleBounds }); + const change = { id: node.id, type: 'dimensions', @@ -189,13 +219,13 @@ const createStore = () => const { onNodesChange, nodes, nodeExtent } = get(); if (onNodesChange) { - const matchingNodes = findMatchingNodes(id, nodes); + const matchingNodes = nodes.filter((n) => !!n.isSelected || n.id === id); if (matchingNodes?.length) { onNodesChange( - matchingNodes.map((n) => { + matchingNodes.map((node) => { const change: NodeDimensionChange = { - id: n.id, + id: node.id, type: 'dimensions', isDragging: !!isDragging, }; @@ -204,12 +234,12 @@ const createStore = () => change.position = nodeExtent ? clampPosition( { - x: n.position.x + diff.x, - y: n.position.y + diff.y, + x: node.position.x + diff.x, + y: node.position.y + diff.y, }, nodeExtent ) - : { x: n.position.x + diff.x, y: n.position.y + diff.y }; + : { x: node.position.x + diff.x, y: node.position.y + diff.y }; } return change; diff --git a/src/types/index.ts b/src/types/index.ts index 9a84ae20..52d4e109 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -458,11 +458,23 @@ export type InitD3ZoomPayload = { export type OnNodesChange = (nodes: NodeChange[]) => void; export type OnEdgesChange = (nodes: EdgeChange[]) => void; +export type NodeLookupItem = { + width?: number | null; + height?: number | null; + parentNode?: ElementId; + position?: XYPosition; + positionAbsolute?: XYPosition; + handleBounds?: NodeHandleBounds; +}; + +export type NodeLookup = Map; + export interface ReactFlowState { width: number; height: number; transform: Transform; nodes: Node[]; + nodeLookup: NodeLookup; edges: Edge[]; selectedNodesBbox: Rect; onNodesChange: OnNodesChange | null; @@ -542,3 +554,8 @@ export interface ReactFlowState { export type UpdateNodeInternals = (nodeId: ElementId) => void; export type OnSelectionChangeFunc = (params: { nodes: Node[]; edges: Edge[] }) => void; + +export type NodeRendererNode = { + childNodes?: NodeRendererNode[]; + node: Node; +}; From 73338738229efdb9121e119230fc652797156d0b Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 3 Nov 2021 10:03:23 +0100 Subject: [PATCH 037/263] refactor(nodelookup): add treelevel --- src/container/EdgeRenderer/index.tsx | 4 ++-- src/container/NodeRenderer/index.tsx | 4 +++- src/hooks/useNodeLookup.ts | 2 +- src/store/index.ts | 35 +++++++++++++++++++++------- src/types/index.ts | 1 + 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 141b5342..a462c73a 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -251,8 +251,8 @@ const EdgeRenderer = (props: EdgeRendererProps) => { {edges.map((edge: Edge) => { // @todo: getSourceTargetNodes is called many times during dragging/creating edges - const sourceNode = nodeLookup.current.get(edge.source); - const targetNode = nodeLookup.current.get(edge.target); + const sourceNode = nodeLookup.get(edge.source); + const targetNode = nodeLookup.get(edge.target); return ( { }); }, []); + console.log(nodeLookup); + return (
(item: Node | E // return result; // } -function addPositions(posA: XYPosition, posB: XYPosition): XYPosition { +type XYPosAndTreeLevel = XYPosition & { treeLevel: number }; + +function addPositions(a: XYPosAndTreeLevel, b: XYPosition): XYPosAndTreeLevel { return { - x: (posA.x ?? 0) + (posB.x ?? 0), - y: (posA.y ?? 0) + (posB.y ?? 0), + x: (a.x ?? 0) + (b.x ?? 0), + y: (a.y ?? 0) + (b.y ?? 0), + treeLevel: a.treeLevel + 1, }; } -function getAbsolutePosition(node: NodeLookupItem, nodeLookup: NodeLookup, result: XYPosition): XYPosition { +function getAbsolutePositionAndTreeLevel( + node: NodeLookupItem, + nodeLookup: NodeLookup, + result: XYPosAndTreeLevel +): XYPosAndTreeLevel { const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : false; if (!parentNode) { return result; } - return getAbsolutePosition(parentNode, nodeLookup, addPositions(result, parentNode.position || { x: 0, y: 0 })); + return getAbsolutePositionAndTreeLevel( + parentNode, + nodeLookup, + addPositions(result, parentNode.position || { x: 0, y: 0 }) + ); } const createStore = () => @@ -158,6 +169,7 @@ const createStore = () => height: node.height || null, position: node.position, positionAbsolute: node.position, + treeLevel: 0, }; if (node.parentNode) { lookupNode.parentNode = node.parentNode; @@ -168,12 +180,19 @@ const createStore = () => nodes .filter((node) => node.parentNode) .forEach((node) => { - const positionAbsolute = getAbsolutePosition(node, nodeLookup, node.position); + const positionAbsoluteAndTreeLevel = getAbsolutePositionAndTreeLevel(node, nodeLookup, { + ...node.position, + treeLevel: 0, + }); - if (positionAbsolute) { + if (positionAbsoluteAndTreeLevel) { nodeLookup.set(node.id, { ...nodeLookup.get(node.id), - positionAbsolute, + positionAbsolute: { + x: positionAbsoluteAndTreeLevel.x, + y: positionAbsoluteAndTreeLevel.y, + }, + treeLevel: positionAbsoluteAndTreeLevel.treeLevel, }); } }); diff --git a/src/types/index.ts b/src/types/index.ts index 52d4e109..e9a23cac 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -465,6 +465,7 @@ export type NodeLookupItem = { position?: XYPosition; positionAbsolute?: XYPosition; handleBounds?: NodeHandleBounds; + treeLevel?: number; }; export type NodeLookup = Map; From 79e46d90ad72f7dc94782398488a72c035f6754b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Wed, 3 Nov 2021 17:51:39 +0100 Subject: [PATCH 038/263] wip: multiple edge renderers --- example/src/Basic/index.tsx | 4 +- src/components/Nodes/wrapNode.tsx | 3 +- src/container/EdgeRenderer/index.tsx | 110 +++++++++--------- src/container/GraphView/index.tsx | 78 +++++++------ src/container/NodeRenderer/index.tsx | 166 ++++++++------------------- src/hooks/useVisibleEdges.ts | 47 ++++++-- src/hooks/useVisibleNodes.ts | 23 +--- src/store/index.ts | 35 +----- src/style.css | 9 +- src/types/index.ts | 1 + 10 files changed, 201 insertions(+), 275 deletions(-) diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index 82683ecc..f86bffb5 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -28,7 +28,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 4' }, position: { x: 100, y: 200 }, className: 'light', - style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, + style: { backgroundColor: 'rgba(255, 0, 0, 1)' }, width: 600, height: 300, }, @@ -44,7 +44,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 4b' }, position: { x: 80, y: 80 }, className: 'light', - style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, + style: { backgroundColor: 'rgba(255, 255, 0, 0.2)' }, parentNode: '4', height: 200, width: 350, diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index f59d1eb6..22fecc7a 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -67,7 +67,8 @@ export default (NodeComponent: ComponentType) => { const nodeStyle: CSSProperties = useMemo( () => ({ - zIndex: isSelected ? zIndex + 1 : zIndex, + zIndex, + // zIndex: isSelected ? zIndex + 1 : zIndex, transform: `translate(${xPos}px,${yPos}px)`, pointerEvents: isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none', diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index a462c73a..90a4caed 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -235,8 +235,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { connectionMode, } = useStore(selector, shallow); const nodeLookup = useNodeLookup(); - - const edges = useVisibleEdges(props.onlyRenderVisibleElements); + const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeLookup); if (!width) { return null; @@ -246,59 +245,62 @@ const EdgeRenderer = (props: EdgeRendererProps) => { const renderConnectionLine = connectionNodeId && connectionHandleType; return ( - - - - {edges.map((edge: Edge) => { - // @todo: getSourceTargetNodes is called many times during dragging/creating edges - const sourceNode = nodeLookup.get(edge.source); - const targetNode = nodeLookup.get(edge.target); + <> + {edgeTree.map(({ level, edges, isMaxLevel }) => ( + + {isMaxLevel && } + + {edges.map((edge: Edge) => { + const sourceNode = nodeLookup.get(edge.source); + const targetNode = nodeLookup.get(edge.target); - return ( - - ); - })} - {renderConnectionLine && ( - - )} - - + return ( + + ); + })} + {renderConnectionLine && isMaxLevel && ( + + )} + + + ))} + ); }; diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index df89027a..cbaa3820 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, memo } from 'react'; -import { useStoreApi } from '../../store'; +import { useStoreApi, useStore } from '../../store'; import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; @@ -82,7 +82,8 @@ const GraphView = ({ }: GraphViewProps) => { const isInitialized = useRef(false); const store = useStoreApi(); - const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper(); + const { zoomIn, zoomOut, zoomTo, transform: setTransform, fitView, initialized } = useZoomPanHelper(); + const transform = useStore((s) => s.transform); useEffect(() => { if (!isInitialized.current && initialized) { @@ -92,7 +93,7 @@ const GraphView = ({ zoomIn, zoomOut, zoomTo, - setTransform: transform, + setTransform: setTransform, project: onLoadProject(store.getState), getNodes: onLoadGetNodes(store.getState), getEdges: onLoadGetEdges(store.getState), @@ -104,6 +105,11 @@ const GraphView = ({ } }, [onLoad, zoomIn, zoomOut, zoomTo, transform, fitView, initialized]); + const transformStyle = { + transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`, + transformOrigin: '0 0', + }; + return ( - - +
+ + +
); }; diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index a75c3271..a3e70cbf 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -1,16 +1,8 @@ -import React, { memo, useMemo, ComponentType, MouseEvent, Fragment } from 'react'; +import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; import shallow from 'zustand/shallow'; import { useStore } from '../../store'; -import { - Node, - NodeTypesType, - ReactFlowState, - WrapNodeProps, - SnapGrid, - NodeRendererNode, - NodeLookup, -} from '../../types'; +import { Node, NodeTypesType, ReactFlowState, WrapNodeProps, SnapGrid, NodeRendererNode } from '../../types'; import useVisibleNodes from '../../hooks/useVisibleNodes'; import useNodeLookup from '../../hooks/useNodeLookup'; interface NodeRendererProps { @@ -39,9 +31,12 @@ const selector = (s: ReactFlowState) => ({ nodeLookup: s.nodeLookup, }); -interface NodesProps extends NodeRendererProps { - nodes: NodeRendererNode[]; - nodeLookup: NodeLookup; +interface NodeProps extends NodeRendererProps { + node: Node; + nodeType: string; + childNodes?: NodeRendererNode[]; + xPos?: number; + yPos?: number; isDraggable?: boolean; resizeObserver: ResizeObserver | null; scale: number; @@ -50,16 +45,8 @@ interface NodesProps extends NodeRendererProps { nodesDraggable: boolean; nodesConnectable: boolean; elementsSelectable: boolean; - recursionDepth: number; - parentId?: string; -} - -interface NodeProps extends Omit { - node: Node; - nodeType: string; - childNodes?: NodeRendererNode[]; - positionAbsoluteX?: number; - positionAbsoluteY?: number; + treeLevel?: number; + isParentNode?: boolean; } function Node({ @@ -74,9 +61,10 @@ function Node({ nodesDraggable, nodesConnectable, elementsSelectable, - recursionDepth, - positionAbsoluteX, - positionAbsoluteY, + treeLevel = 0, + xPos, + yPos, + isParentNode = false, ...props }: NodeProps) { // const onNodesChange = useStore((s) => s.onNodesChange); @@ -93,8 +81,6 @@ function Node({ typeof node.width !== 'undefined' && typeof node.height !== 'undefined'; - const isParentNode = !!childNodes?.length; - return ( ); } -function Nodes({ - nodes, - nodeLookup, - isDraggable, - resizeObserver, - scale, - snapToGrid, - snapGrid, - nodesDraggable, - nodesConnectable, - elementsSelectable, - recursionDepth, - ...props -}: NodesProps): any { - return nodes.map(({ node, childNodes }) => { - const nodeType = node.type || 'default'; - const lookupNode = nodeLookup.get(node.id); - - if (!props.nodeTypes[nodeType]) { - console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); - } - - return ( - - - {childNodes && childNodes.length > 0 && ( - - )} - - ); - }); -} - -const MemoizedNodes = memo(Nodes); - const NodeRenderer = (props: NodeRendererProps) => { const { transform, @@ -210,17 +132,10 @@ const NodeRenderer = (props: NodeRendererProps) => { snapGrid, snapToGrid, } = useStore(selector, shallow); + const nodeLookup = useNodeLookup(); - const nodes = useVisibleNodes(props.onlyRenderVisibleElements); - const transformStyle = useMemo( - () => ({ - transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`, - }), - [transform[0], transform[1], transform[2]] - ); - const resizeObserver = useMemo(() => { if (typeof ResizeObserver === 'undefined') { return null; @@ -236,23 +151,36 @@ const NodeRenderer = (props: NodeRendererProps) => { }); }, []); - console.log(nodeLookup); - return ( -
- +
+ {nodes.map((node) => { + const nodeType = node.type || 'default'; + const lookupNode = nodeLookup.get(node.id); + + if (!props.nodeTypes[nodeType]) { + console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); + } + + return ( + + ); + })}
); }; diff --git a/src/hooks/useVisibleEdges.ts b/src/hooks/useVisibleEdges.ts index 74406400..14cd22e9 100644 --- a/src/hooks/useVisibleEdges.ts +++ b/src/hooks/useVisibleEdges.ts @@ -1,10 +1,40 @@ import { useCallback } from 'react'; import { useStore } from '../store'; -import { isEdgeVisible, getSourceTargetNodes } from '../container/EdgeRenderer/utils'; -import { ReactFlowState } from '../types'; +import { isEdgeVisible } from '../container/EdgeRenderer/utils'; +import { ReactFlowState, NodeLookup, Edge } from '../types'; -function useVisibleEdges(onlyRenderVisible: boolean) { +function groupEdgesByTreeLevel(edges: Edge[], nodeLookup: NodeLookup) { + let maxLevel = -1; + + const levelLookup = edges.reduce>((tree, edge) => { + const treeLevel = Math.max( + nodeLookup.get(edge.source)?.treeLevel || 0, + nodeLookup.get(edge.target)?.treeLevel || 0 + ); + if (tree[treeLevel]) { + tree[treeLevel].push(edge); + } else { + tree[treeLevel] = [edge]; + } + + maxLevel = treeLevel > maxLevel ? treeLevel : maxLevel; + + return tree; + }, {}); + + return Object.entries(levelLookup).map(([key, edges]) => { + const level = +key; + + return { + edges, + level, + isMaxLevel: level === maxLevel, + }; + }); +} + +function useVisibleEdges(onlyRenderVisible: boolean, nodeLookup: NodeLookup) { const edges = useStore( useCallback( (s: ReactFlowState) => { @@ -13,7 +43,8 @@ function useVisibleEdges(onlyRenderVisible: boolean) { } return s.edges.filter((e) => { - const { sourceNode, targetNode } = getSourceTargetNodes(e, s.nodes); + const sourceNode = nodeLookup.get(e.source); + const targetNode = nodeLookup.get(e.target); return ( sourceNode?.width && @@ -21,8 +52,8 @@ function useVisibleEdges(onlyRenderVisible: boolean) { targetNode?.width && targetNode?.height && isEdgeVisible({ - sourcePos: sourceNode.position, - targetPos: targetNode.position, + sourcePos: sourceNode.position || { x: 0, y: 0 }, + targetPos: targetNode.position || { x: 0, y: 0 }, sourceWidth: sourceNode.width, sourceHeight: sourceNode.height, targetWidth: targetNode.width, @@ -34,11 +65,11 @@ function useVisibleEdges(onlyRenderVisible: boolean) { ); }); }, - [onlyRenderVisible] + [onlyRenderVisible, nodeLookup] ) ); - return edges; + return groupEdgesByTreeLevel(edges, nodeLookup); } export default useVisibleEdges; diff --git a/src/hooks/useVisibleNodes.ts b/src/hooks/useVisibleNodes.ts index 6f2b613d..d6c9bd96 100644 --- a/src/hooks/useVisibleNodes.ts +++ b/src/hooks/useVisibleNodes.ts @@ -2,26 +2,7 @@ import { useCallback } from 'react'; import { useStore } from '../store'; import { getNodesInside } from '../utils/graph'; -import { ReactFlowState, Node, NodeRendererNode } from '../types'; - -function getChildNodes(nodes: Node[], parent?: Node): NodeRendererNode[] { - const children: NodeRendererNode[] = []; - const remaining: Node[] = []; - - for (let i = 0; i < nodes.length; i++) { - const n = nodes[i]; - if ((!parent && !n.parentNode) || n.parentNode === parent?.id) { - children.push({ node: n }); - } else { - remaining.push(n); - } - } - - return children.map((child) => { - child.childNodes = getChildNodes(remaining, child.node); - return child; - }); -} +import { ReactFlowState } from '../types'; function useVisibleNodes(onlyRenderVisible: boolean) { const nodes = useStore( @@ -35,7 +16,7 @@ function useVisibleNodes(onlyRenderVisible: boolean) { ) ); - return getChildNodes(nodes); + return nodes; } export default useVisibleNodes; diff --git a/src/store/index.ts b/src/store/index.ts index b4f50a30..ccfaa8b5 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -40,39 +40,6 @@ const createNodeOrEdgeSelectionChange = (isSelected: boolean) => (item: Node | E isSelected, }); -// @todo needs refactoring / improvements -// function findMatchingNodes(id: string | undefined, nodes: Node[]): Node[] { -// if (!id) { -// return nodes.filter((n) => !!n.isSelected); -// } - -// const result = []; -// const children = []; - -// for (let i = 0; i < nodes.length; i++) { -// const n = nodes[i]; - -// if (n.id === id) { -// result.push(n); -// } - -// if (n.parentNode === id) { -// children.push(n); -// } -// } - -// for (let i = 0; i < children.length; i++) { -// const n = children[i]; -// const matches = findMatchingNodes(n.id, nodes); - -// for (let j = 0; j < matches.length; j++) { -// result.push(matches[j]); -// } -// } - -// return result; -// } - type XYPosAndTreeLevel = XYPosition & { treeLevel: number }; function addPositions(a: XYPosAndTreeLevel, b: XYPosition): XYPosAndTreeLevel { @@ -185,6 +152,8 @@ const createStore = () => treeLevel: 0, }); + nodeLookup.set(node.parentNode!, { ...nodeLookup.get(node.parentNode!), isParentNode: true }); + if (positionAbsoluteAndTreeLevel) { nodeLookup.set(node.id, { ...nodeLookup.get(node.id), diff --git a/src/style.css b/src/style.css index 8fac54d9..6064fde8 100644 --- a/src/style.css +++ b/src/style.css @@ -38,7 +38,6 @@ top: 0; left: 0; pointer-events: none; - z-index: 2; } .react-flow__edge { @@ -87,7 +86,6 @@ height: 100%; pointer-events: none; transform-origin: 0 0; - z-index: 3; } .react-flow__node { @@ -186,3 +184,10 @@ bottom: 10px; right: 10px; } + +.react-flow__container { + position: absolute; + width: 100%; + height: 100%; + pointer-events: none; +} diff --git a/src/types/index.ts b/src/types/index.ts index e9a23cac..b5801a52 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -466,6 +466,7 @@ export type NodeLookupItem = { positionAbsolute?: XYPosition; handleBounds?: NodeHandleBounds; treeLevel?: number; + isParentNode?: boolean; }; export type NodeLookup = Map; From 1b390e103d7b622f7ea44bde73bf5e015c7fa7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 4 Nov 2021 12:42:14 +0100 Subject: [PATCH 039/263] feat(stacking): simplify zIndex and treeLevel behaviour --- example/src/Basic/index.tsx | 12 +++++------- src/components/ConnectionLine/index.tsx | 20 +++++++++----------- src/container/EdgeRenderer/index.tsx | 2 +- src/container/NodeRenderer/index.tsx | 2 +- src/store/index.ts | 12 +++++++----- src/types/index.ts | 3 +-- 6 files changed, 24 insertions(+), 27 deletions(-) diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index f86bffb5..bf5e2143 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -28,7 +28,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 4' }, position: { x: 100, y: 200 }, className: 'light', - style: { backgroundColor: 'rgba(255, 0, 0, 1)' }, + style: { backgroundColor: 'rgba(255, 0, 0, 0.8)' }, width: 600, height: 300, }, @@ -42,12 +42,12 @@ const initialNodes: Node[] = [ { id: '4b', data: { label: 'Node 4b' }, - position: { x: 80, y: 80 }, + position: { x: 150, y: 50 }, className: 'light', - style: { backgroundColor: 'rgba(255, 255, 0, 0.2)' }, + style: { backgroundColor: 'rgba(255, 0, 255, 0.8)' }, + height: 300, + width: 300, parentNode: '4', - height: 200, - width: 350, }, { id: '4b1', @@ -68,10 +68,8 @@ const initialNodes: Node[] = [ const initialEdges: Edge[] = [ { id: 'e1-2', source: '1', target: '2', animated: true }, { id: 'e1-3', source: '1', target: '3' }, - { id: 'e2-4a', source: '2', target: '4a', animated: true }, { id: 'e3-4', source: '3', target: '4' }, { id: 'e3-4b', source: '3', target: '4b' }, - { id: 'e3-4b2', source: '3', target: '4b2' }, { id: 'e4a-4b1', source: '4a', target: '4b1' }, { id: 'e4a-4b2', source: '4a', target: '4b2' }, { id: 'e4b1-4b2', source: '4b1', target: '4b2' }, diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index 8095b821..59aac550 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -1,19 +1,19 @@ import React, { useEffect, useState, CSSProperties } from 'react'; -import { useStore } from '../../store'; import { getBezierPath } from '../Edges/BezierEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { ElementId, - Node, + NodeLookupItem, Transform, HandleElement, Position, ConnectionLineType, ConnectionLineComponent, HandleType, - ReactFlowState, + Node, } from '../../types'; +import useNodeLookup from '../../hooks/useNodeLookup'; interface ConnectionLineProps { connectionNodeId: ElementId; connectionHandleId: ElementId | null; @@ -27,8 +27,6 @@ interface ConnectionLineProps { CustomConnectionLineComponent?: ConnectionLineComponent; } -const nodesSelector = (s: ReactFlowState) => s.nodes; - export default ({ connectionNodeId, connectionHandleId, @@ -41,13 +39,13 @@ export default ({ isConnectable, CustomConnectionLineComponent, }: ConnectionLineProps) => { - const nodes = useStore(nodesSelector); - const [sourceNode, setSourceNode] = useState(null); + const nodeLookup = useNodeLookup(); + const [sourceNode, setSourceNode] = useState(null); const nodeId = connectionNodeId; const handleId = connectionHandleId; useEffect(() => { - const nextSourceNode = nodes.find((n) => n.id === nodeId) || null; + const nextSourceNode = nodeLookup.get(nodeId) || null; setSourceNode(nextSourceNode); }, []); @@ -60,8 +58,8 @@ export default ({ : sourceNode.handleBounds[connectionHandleType]![0]; const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.width! / 2; const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.height!; - const sourceX = sourceNode.position.x + sourceHandleX; - const sourceY = sourceNode.position.y + sourceHandleY; + const sourceX = sourceNode.position!.x + sourceHandleX; + const sourceY = sourceNode.position!.y + sourceHandleY; const targetX = (connectionPositionX - transform[0]) / transform[2]; const targetY = (connectionPositionY - transform[1]) / transform[2]; @@ -81,7 +79,7 @@ export default ({ targetPosition={targetPosition} connectionLineType={connectionLineType} connectionLineStyle={connectionLineStyle} - sourceNode={sourceNode} + sourceNode={sourceNode as Node} sourceHandle={sourceHandle} /> diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 90a4caed..6e8ba9c9 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -247,7 +247,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { return ( <> {edgeTree.map(({ level, edges, isMaxLevel }) => ( - + {isMaxLevel && } {edges.map((edge: Edge) => { diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index a3e70cbf..f4b5fb92 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -116,7 +116,7 @@ function Node({ isConnectable={isConnectable} resizeObserver={resizeObserver} dragHandle={node.dragHandle} - zIndex={6 + treeLevel} + zIndex={treeLevel} isParentNode={isParentNode} /> ); diff --git a/src/store/index.ts b/src/store/index.ts index ccfaa8b5..63038415 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -136,7 +136,7 @@ const createStore = () => height: node.height || null, position: node.position, positionAbsolute: node.position, - treeLevel: 0, + treeLevel: node.zIndex || 0, }; if (node.parentNode) { lookupNode.parentNode = node.parentNode; @@ -149,19 +149,21 @@ const createStore = () => .forEach((node) => { const positionAbsoluteAndTreeLevel = getAbsolutePositionAndTreeLevel(node, nodeLookup, { ...node.position, - treeLevel: 0, + treeLevel: node.zIndex || 0, }); nodeLookup.set(node.parentNode!, { ...nodeLookup.get(node.parentNode!), isParentNode: true }); if (positionAbsoluteAndTreeLevel) { + const { treeLevel, x, y } = positionAbsoluteAndTreeLevel; + nodeLookup.set(node.id, { ...nodeLookup.get(node.id), positionAbsolute: { - x: positionAbsoluteAndTreeLevel.x, - y: positionAbsoluteAndTreeLevel.y, + x, + y, }, - treeLevel: positionAbsoluteAndTreeLevel.treeLevel, + treeLevel, }); } }); diff --git a/src/types/index.ts b/src/types/index.ts index b5801a52..378a4666 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -82,9 +82,8 @@ export interface Node { dragHandle?: string; width?: number | null; height?: number | null; - handleBounds?: NodeHandleBounds; parentNode?: ElementId; - childNodes?: Node[]; + zIndex?: number; } export enum ArrowHeadType { From 2bcb0990506c6116e8e033750f4310be251a5de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 4 Nov 2021 13:05:53 +0100 Subject: [PATCH 040/263] bump next version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 423bc22b..dad5df37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.8", + "version": "10.0.0-next.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "10.0.0-next.8", + "version": "10.0.0-next.9", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/package.json b/package.json index 79b42e5f..1e28bb3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.8", + "version": "10.0.0-next.9", "engines": { "node": ">=12" }, From 5547b0d6ccc11f99b1ee2d777f26e0aeec97fd49 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 4 Nov 2021 14:03:22 +0100 Subject: [PATCH 041/263] refactor(nodelookup): use as normale store item --- src/components/ConnectionLine/index.tsx | 8 ++++++-- src/container/EdgeRenderer/index.tsx | 5 ++--- src/container/NodeRenderer/index.tsx | 4 ++-- src/hooks/useNodeLookup.ts | 12 ------------ src/store/index.ts | 25 ++++++++++++++++--------- 5 files changed, 26 insertions(+), 28 deletions(-) delete mode 100644 src/hooks/useNodeLookup.ts diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index 59aac550..24a0f57b 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState, CSSProperties } from 'react'; +import { useStore } from '../../store'; import { getBezierPath } from '../Edges/BezierEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { @@ -12,8 +13,9 @@ import { ConnectionLineComponent, HandleType, Node, + ReactFlowState, } from '../../types'; -import useNodeLookup from '../../hooks/useNodeLookup'; + interface ConnectionLineProps { connectionNodeId: ElementId; connectionHandleId: ElementId | null; @@ -27,6 +29,8 @@ interface ConnectionLineProps { CustomConnectionLineComponent?: ConnectionLineComponent; } +const selector = (s: ReactFlowState) => s.nodeLookup; + export default ({ connectionNodeId, connectionHandleId, @@ -39,7 +43,7 @@ export default ({ isConnectable, CustomConnectionLineComponent, }: ConnectionLineProps) => { - const nodeLookup = useNodeLookup(); + const nodeLookup = useStore(selector); const [sourceNode, setSourceNode] = useState(null); const nodeId = connectionNodeId; const handleId = connectionHandleId; diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 6e8ba9c9..66b24aa4 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -17,7 +17,6 @@ import { NodeHandleBounds, } from '../../types'; import useVisibleEdges from '../../hooks/useVisibleEdges'; -import useNodeLookup from '../../hooks/useNodeLookup'; interface EdgeRendererProps { edgeTypes: any; @@ -218,7 +217,7 @@ const selector = (s: ReactFlowState) => ({ width: s.width, height: s.height, connectionMode: s.connectionMode, - nodes: s.nodes, + nodeLookup: s.nodeLookup, }); const EdgeRenderer = (props: EdgeRendererProps) => { @@ -233,8 +232,8 @@ const EdgeRenderer = (props: EdgeRendererProps) => { width, height, connectionMode, + nodeLookup, } = useStore(selector, shallow); - const nodeLookup = useNodeLookup(); const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeLookup); if (!width) { diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index f4b5fb92..9f8c2880 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -4,7 +4,7 @@ import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import { Node, NodeTypesType, ReactFlowState, WrapNodeProps, SnapGrid, NodeRendererNode } from '../../types'; import useVisibleNodes from '../../hooks/useVisibleNodes'; -import useNodeLookup from '../../hooks/useNodeLookup'; + interface NodeRendererProps { nodeTypes: NodeTypesType; selectNodesOnDrag: boolean; @@ -131,9 +131,9 @@ const NodeRenderer = (props: NodeRendererProps) => { updateNodeDimensions, snapGrid, snapToGrid, + nodeLookup, } = useStore(selector, shallow); - const nodeLookup = useNodeLookup(); const nodes = useVisibleNodes(props.onlyRenderVisibleElements); const resizeObserver = useMemo(() => { diff --git a/src/hooks/useNodeLookup.ts b/src/hooks/useNodeLookup.ts deleted file mode 100644 index 2d9d695c..00000000 --- a/src/hooks/useNodeLookup.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useRef } from 'react'; - -import { useStoreApi } from '../store'; - -function useNodeLookup() { - const store = useStoreApi(); - const nodeLookup = useRef(store.getState().nodeLookup); - - return nodeLookup.current; -} - -export default useNodeLookup; diff --git a/src/store/index.ts b/src/store/index.ts index 63038415..d139d124 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -28,6 +28,7 @@ import { NodeDimensionChange, NodeLookup, NodeLookupItem, + ElementId, } from '../types'; import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph'; import { getHandleBounds } from '../components/Nodes/utils'; @@ -128,9 +129,10 @@ const createStore = () => setNodes: (nodes: Node[]) => { const { nodeLookup } = get(); + const nextNodeLookup = new Map(); nodes.forEach((node) => { - const lookupNode = { + const lookupNode: NodeLookupItem = { ...nodeLookup.get(node.id), width: node.width || null, height: node.height || null, @@ -141,24 +143,24 @@ const createStore = () => if (node.parentNode) { lookupNode.parentNode = node.parentNode; } - nodeLookup.set(node.id, lookupNode); + nextNodeLookup.set(node.id, lookupNode); }); nodes .filter((node) => node.parentNode) .forEach((node) => { - const positionAbsoluteAndTreeLevel = getAbsolutePositionAndTreeLevel(node, nodeLookup, { + const positionAbsoluteAndTreeLevel = getAbsolutePositionAndTreeLevel(node, nextNodeLookup, { ...node.position, treeLevel: node.zIndex || 0, }); - nodeLookup.set(node.parentNode!, { ...nodeLookup.get(node.parentNode!), isParentNode: true }); + nextNodeLookup.set(node.parentNode!, { ...nextNodeLookup.get(node.parentNode!), isParentNode: true }); if (positionAbsoluteAndTreeLevel) { const { treeLevel, x, y } = positionAbsoluteAndTreeLevel; - nodeLookup.set(node.id, { - ...nodeLookup.get(node.id), + nextNodeLookup.set(node.id, { + ...nextNodeLookup.get(node.id), positionAbsolute: { x, y, @@ -168,7 +170,7 @@ const createStore = () => } }); - set({ nodes }); + set({ nodes, nodeLookup: nextNodeLookup }); }, setEdges: (edges: Edge[]) => { set({ edges }); @@ -188,13 +190,16 @@ const createStore = () => if (doUpdate) { const handleBounds = getHandleBounds(update.nodeElement, transform[2]); - nodeLookup.set(node.id, { ...nodeLookup.get(node.id), handleBounds }); + nodeLookup.set(node.id, { + ...nodeLookup.get(node.id), + handleBounds, + ...dimensions, + }); const change = { id: node.id, type: 'dimensions', dimensions, - handleBounds, } as NodeChange; res.push(change); } @@ -203,6 +208,8 @@ const createStore = () => return res; }, []); + set({ nodeLookup: new Map(nodeLookup) }); + onNodesChange?.(nodesToChange); }, updateNodePosition: ({ id, diff, isDragging }: NodeDiffUpdate) => { From 1db53e8fe20739fbfc8fb10ee6148a3577dc20da Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 4 Nov 2021 14:05:30 +0100 Subject: [PATCH 042/263] chore(package): bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index dad5df37..9371ace8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.9", + "version": "10.0.0-next.10", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "10.0.0-next.9", + "version": "10.0.0-next.10", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/package.json b/package.json index 1e28bb3d..45912eb1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.9", + "version": "10.0.0-next.10", "engines": { "node": ">=12" }, From a3a9249d170f59f733b3e75479eec6a44afe65a9 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 4 Nov 2021 14:14:11 +0100 Subject: [PATCH 043/263] fix(minimap): use abs pos --- src/additional-components/MiniMap/index.tsx | 46 +++++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index 97443452..1f13149f 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -23,7 +23,13 @@ declare const window: any; const defaultWidth = 200; const defaultHeight = 150; -const selector = (s: ReactFlowState) => ({ width: s.width, height: s.height, transform: s.transform, nodes: s.nodes }); +const selector = (s: ReactFlowState) => ({ + width: s.width, + height: s.height, + transform: s.transform, + nodes: s.nodes, + nodeLookup: s.nodeLookup, +}); const MiniMap = ({ style, @@ -35,7 +41,7 @@ const MiniMap = ({ nodeStrokeWidth = 2, maskColor = 'rgb(240, 242, 243, 0.7)', }: MiniMapProps) => { - const { width: containerWidth, height: containerHeight, transform, nodes } = useStore(selector, shallow); + const { width: containerWidth, height: containerHeight, transform, nodes, nodeLookup } = useStore(selector, shallow); const [tX, tY, tScale] = transform; const mapClasses = cc(['react-flow__minimap', className]); @@ -77,22 +83,26 @@ const MiniMap = ({ > {nodes .filter((node) => !node.isHidden && node.width && node.height) - .map((node) => ( - - ))} + .map((node) => { + const positionAbsolute = nodeLookup.get(node.id)?.positionAbsolute; + + return ( + + ); + })} Date: Thu, 4 Nov 2021 14:33:21 +0100 Subject: [PATCH 044/263] refactor(nodelookup): use ref for node renderer --- src/container/NodeRenderer/index.tsx | 7 +++---- src/hooks/useNodeLookupRef.ts | 14 ++++++++++++++ src/utils/graph.ts | 4 ---- 3 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 src/hooks/useNodeLookupRef.ts diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 9f8c2880..6661fb6e 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -4,6 +4,7 @@ import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import { Node, NodeTypesType, ReactFlowState, WrapNodeProps, SnapGrid, NodeRendererNode } from '../../types'; import useVisibleNodes from '../../hooks/useVisibleNodes'; +import useNodeLookupRef from '../../hooks/useNodeLookupRef'; interface NodeRendererProps { nodeTypes: NodeTypesType; @@ -28,7 +29,6 @@ const selector = (s: ReactFlowState) => ({ updateNodeDimensions: s.updateNodeDimensions, snapGrid: s.snapGrid, snapToGrid: s.snapToGrid, - nodeLookup: s.nodeLookup, }); interface NodeProps extends NodeRendererProps { @@ -131,9 +131,8 @@ const NodeRenderer = (props: NodeRendererProps) => { updateNodeDimensions, snapGrid, snapToGrid, - nodeLookup, } = useStore(selector, shallow); - + const nodeLookup = useNodeLookupRef(); const nodes = useVisibleNodes(props.onlyRenderVisibleElements); const resizeObserver = useMemo(() => { @@ -155,7 +154,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
{nodes.map((node) => { const nodeType = node.type || 'default'; - const lookupNode = nodeLookup.get(node.id); + const lookupNode = nodeLookup.current.get(node.id); if (!props.nodeTypes[nodeType]) { console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); diff --git a/src/hooks/useNodeLookupRef.ts b/src/hooks/useNodeLookupRef.ts new file mode 100644 index 00000000..707751f3 --- /dev/null +++ b/src/hooks/useNodeLookupRef.ts @@ -0,0 +1,14 @@ +import { useRef, useEffect } from 'react'; + +import { useStoreApi } from '../store'; + +function useNodeLookupRef() { + const store = useStoreApi(); + const nodeLookup = useRef(store.getState().nodeLookup); + + useEffect(() => store.subscribe((state) => (nodeLookup.current = state.nodeLookup)), []); + + return nodeLookup; +} + +export default useNodeLookupRef; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index cdcc2d1f..bc648a7c 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -309,10 +309,6 @@ function applyChanges(changes: NodeChange[] | EdgeChange[], elements: any[]): an updateItem.height = currentChange.dimensions.height; } - if (typeof currentChange.handleBounds !== 'undefined') { - updateItem.handleBounds = currentChange.handleBounds; - } - if (typeof currentChange.position !== 'undefined') { updateItem.position = currentChange.position; } From 72f1d8b4e311f7d17e5533580d65c3a7c23dd968 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 4 Nov 2021 15:13:14 +0100 Subject: [PATCH 045/263] refactor(stlyes): use react-flow__container helper --- example/src/Basic/index.tsx | 8 +- .../Background/index.tsx | 2 +- src/components/ConnectionLine/index.tsx | 8 +- src/components/Nodes/wrapNode.tsx | 6 - src/components/NodesSelection/index.tsx | 2 +- src/components/UserSelection/index.tsx | 4 +- src/container/EdgeRenderer/index.tsx | 11 +- src/container/FlowRenderer/index.tsx | 7 +- src/container/GraphView/index.tsx | 17 +- src/container/NodeRenderer/index.tsx | 164 +++++------------- src/container/Viewport/index.tsx | 25 +++ src/style.css | 44 +---- 12 files changed, 108 insertions(+), 190 deletions(-) create mode 100644 src/container/Viewport/index.tsx diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index bf5e2143..40ed203c 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -28,9 +28,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 4' }, position: { x: 100, y: 200 }, className: 'light', - style: { backgroundColor: 'rgba(255, 0, 0, 0.8)' }, - width: 600, - height: 300, + style: { backgroundColor: 'rgba(255, 0, 0, 0.8)', width: 600, height: 300 }, }, { id: '4a', @@ -44,9 +42,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 4b' }, position: { x: 150, y: 50 }, className: 'light', - style: { backgroundColor: 'rgba(255, 0, 255, 0.8)' }, - height: 300, - width: 300, + style: { backgroundColor: 'rgba(255, 0, 255, 0.8)', height: 300, width: 300 }, parentNode: '4', }, { diff --git a/src/additional-components/Background/index.tsx b/src/additional-components/Background/index.tsx index 10ecf3e1..8f73da12 100644 --- a/src/additional-components/Background/index.tsx +++ b/src/additional-components/Background/index.tsx @@ -31,7 +31,7 @@ const Background: FC = ({ // when there are multiple flows on a page we need to make sure that every background gets its own pattern. const patternId = useMemo(() => `pattern-${Math.floor(Math.random() * 100000)}`, []); - const bgClasses = cc(['react-flow__background', className]); + const bgClasses = cc(['react-flow__background', 'react-flow__container', className]); const scaledGap = gap * scale; const xOffset = x % scaledGap; const yOffset = y % scaledGap; diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index 24a0f57b..f89f62d9 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState, CSSProperties } from 'react'; +import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import { getBezierPath } from '../Edges/BezierEdge'; @@ -6,7 +7,6 @@ import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { ElementId, NodeLookupItem, - Transform, HandleElement, Position, ConnectionLineType, @@ -23,13 +23,12 @@ interface ConnectionLineProps { connectionPositionX: number; connectionPositionY: number; connectionLineType: ConnectionLineType; - transform: Transform; isConnectable: boolean; connectionLineStyle?: CSSProperties; CustomConnectionLineComponent?: ConnectionLineComponent; } -const selector = (s: ReactFlowState) => s.nodeLookup; +const selector = (s: ReactFlowState) => ({ nodeLookup: s.nodeLookup, transform: s.transform }); export default ({ connectionNodeId, @@ -39,11 +38,10 @@ export default ({ connectionPositionX, connectionPositionY, connectionLineType = ConnectionLineType.Bezier, - transform, isConnectable, CustomConnectionLineComponent, }: ConnectionLineProps) => { - const nodeLookup = useStore(selector); + const { nodeLookup, transform } = useStore(selector, shallow); const [sourceNode, setSourceNode] = useState(null); const nodeId = connectionNodeId; const handleId = connectionHandleId; diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 22fecc7a..dfc9cc71 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -23,8 +23,6 @@ export default (NodeComponent: ComponentType) => { scale, xPos, yPos, - width, - height, isSelected, onClick, onMouseEnter, @@ -74,8 +72,6 @@ export default (NodeComponent: ComponentType) => { isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none', // prevents jumping of nodes on start // opacity: isInitialized ? 1 : 0, - width: isParentNode && width !== null ? width : 'auto', - height: isParentNode && height !== null ? height : 'auto', ...style, }), [ @@ -91,8 +87,6 @@ export default (NodeComponent: ComponentType) => { onMouseMove, onMouseLeave, isParentNode, - width, - height, ] ); diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index 5cc8e7ac..e42bbc1f 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -103,7 +103,7 @@ export default ({ } return ( -
+
{ return (
{ return (
({ - transform: s.transform, connectionNodeId: s.connectionNodeId, connectionHandleId: s.connectionHandleId, connectionHandleType: s.connectionHandleType, @@ -222,7 +221,6 @@ const selector = (s: ReactFlowState) => ({ const EdgeRenderer = (props: EdgeRendererProps) => { const { - transform, connectionNodeId, connectionHandleId, connectionHandleType, @@ -246,7 +244,13 @@ const EdgeRenderer = (props: EdgeRendererProps) => { return ( <> {edgeTree.map(({ level, edges, isMaxLevel }) => ( - + {isMaxLevel && } {edges.map((edge: Edge) => { @@ -289,7 +293,6 @@ const EdgeRenderer = (props: EdgeRendererProps) => { connectionHandleType={connectionHandleType!} connectionPositionX={connectionPosition.x} connectionPositionY={connectionPosition.y} - transform={transform} connectionLineStyle={connectionLineStyle} connectionLineType={connectionLineType} isConnectable={nodesConnectable} diff --git a/src/container/FlowRenderer/index.tsx b/src/container/FlowRenderer/index.tsx index 91f6681d..9cfc3216 100644 --- a/src/container/FlowRenderer/index.tsx +++ b/src/container/FlowRenderer/index.tsx @@ -118,7 +118,12 @@ const FlowRenderer = ({ onSelectionContextMenu={onSelectionContextMenu} /> )} -
+
); }; diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index cbaa3820..6af31a8e 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useRef, memo } from 'react'; -import { useStoreApi, useStore } from '../../store'; +import { useStoreApi } from '../../store'; import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; +import Viewport from '../Viewport'; import { onLoadProject, onLoadGetNodes, onLoadGetEdges, onLoadToObject } from '../../utils/graph'; import useZoomPanHelper from '../../hooks/useZoomPanHelper'; @@ -83,7 +84,6 @@ const GraphView = ({ const isInitialized = useRef(false); const store = useStoreApi(); const { zoomIn, zoomOut, zoomTo, transform: setTransform, fitView, initialized } = useZoomPanHelper(); - const transform = useStore((s) => s.transform); useEffect(() => { if (!isInitialized.current && initialized) { @@ -93,7 +93,7 @@ const GraphView = ({ zoomIn, zoomOut, zoomTo, - setTransform: setTransform, + setTransform, project: onLoadProject(store.getState), getNodes: onLoadGetNodes(store.getState), getEdges: onLoadGetEdges(store.getState), @@ -103,12 +103,7 @@ const GraphView = ({ isInitialized.current = true; } - }, [onLoad, zoomIn, zoomOut, zoomTo, transform, fitView, initialized]); - - const transformStyle = { - transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`, - transformOrigin: '0 0', - }; + }, [onLoad, zoomIn, zoomOut, zoomTo, setTransform, fitView, initialized]); return ( -
+ -
+
); }; diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 6661fb6e..4a774271 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -2,7 +2,7 @@ import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; import shallow from 'zustand/shallow'; import { useStore } from '../../store'; -import { Node, NodeTypesType, ReactFlowState, WrapNodeProps, SnapGrid, NodeRendererNode } from '../../types'; +import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types'; import useVisibleNodes from '../../hooks/useVisibleNodes'; import useNodeLookupRef from '../../hooks/useNodeLookupRef'; @@ -22,7 +22,7 @@ interface NodeRendererProps { } const selector = (s: ReactFlowState) => ({ - transform: s.transform, + scale: s.transform[2], nodesDraggable: s.nodesDraggable, nodesConnectable: s.nodesConnectable, elementsSelectable: s.elementsSelectable, @@ -31,107 +31,9 @@ const selector = (s: ReactFlowState) => ({ snapToGrid: s.snapToGrid, }); -interface NodeProps extends NodeRendererProps { - node: Node; - nodeType: string; - childNodes?: NodeRendererNode[]; - xPos?: number; - yPos?: number; - isDraggable?: boolean; - resizeObserver: ResizeObserver | null; - scale: number; - snapToGrid: boolean; - snapGrid: SnapGrid; - nodesDraggable: boolean; - nodesConnectable: boolean; - elementsSelectable: boolean; - treeLevel?: number; - isParentNode?: boolean; -} - -function Node({ - node, - childNodes, - nodeType, - isDraggable, - resizeObserver, - scale, - snapToGrid, - snapGrid, - nodesDraggable, - nodesConnectable, - elementsSelectable, - treeLevel = 0, - xPos, - yPos, - isParentNode = false, - ...props -}: NodeProps) { - // const onNodesChange = useStore((s) => s.onNodesChange); - const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; - const isNodeDraggable = - typeof isDraggable !== 'undefined' - ? isDraggable - : !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')); - const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined')); - const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined')); - const isInitialized = - node.width !== null && - node.height !== null && - typeof node.width !== 'undefined' && - typeof node.height !== 'undefined'; - - return ( - - ); -} - const NodeRenderer = (props: NodeRendererProps) => { - const { - transform, - nodesDraggable, - nodesConnectable, - elementsSelectable, - updateNodeDimensions, - snapGrid, - snapToGrid, - } = useStore(selector, shallow); + const { scale, nodesDraggable, nodesConnectable, elementsSelectable, updateNodeDimensions, snapGrid, snapToGrid } = + useStore(selector, shallow); const nodeLookup = useNodeLookupRef(); const nodes = useVisibleNodes(props.onlyRenderVisibleElements); @@ -151,7 +53,7 @@ const NodeRenderer = (props: NodeRendererProps) => { }, []); return ( -
+
{nodes.map((node) => { const nodeType = node.type || 'default'; const lookupNode = nodeLookup.current.get(node.id); @@ -160,23 +62,51 @@ const NodeRenderer = (props: NodeRendererProps) => { console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); } + const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; + const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')); + const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined')); + const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined')); + const isInitialized = + node.width !== null && + node.height !== null && + typeof node.width !== 'undefined' && + typeof node.height !== 'undefined'; + return ( - ); })} diff --git a/src/container/Viewport/index.tsx b/src/container/Viewport/index.tsx new file mode 100644 index 00000000..590b7516 --- /dev/null +++ b/src/container/Viewport/index.tsx @@ -0,0 +1,25 @@ +import React, { ReactNode } from 'react'; + +import { useStore } from '../../store'; +import { ReactFlowState } from '../../types'; + +const selector = (s: ReactFlowState) => s.transform; + +type ViewportProps = { + children: ReactNode; +}; + +function Viewport({ children }: ViewportProps) { + const transform = useStore(selector); + + return ( +
+ {children} +
+ ); +} + +export default Viewport; diff --git a/src/style.css b/src/style.css index 6064fde8..fe74d2c0 100644 --- a/src/style.css +++ b/src/style.css @@ -5,12 +5,10 @@ overflow: hidden; } -.react-flow__renderer, -.react-flow__pane, -.react-flow__selectionpane { +.react-flow__container { + position: absolute; width: 100%; height: 100%; - position: absolute; top: 0; left: 0; } @@ -19,6 +17,11 @@ z-index: 1; } +.react-flow__viewport { + transform-origin: 0 0; + z-index: 2; +} + .react-flow__renderer { z-index: 4; } @@ -27,17 +30,9 @@ z-index: 5; } -.react-flow__selection { - position: absolute; - top: 0; - left: 0; -} - .react-flow__edges { - position: absolute; - top: 0; - left: 0; pointer-events: none; + overflow: visible; } .react-flow__edge { @@ -81,9 +76,6 @@ } .react-flow__nodes { - position: absolute; - width: 100%; - height: 100%; pointer-events: none; transform-origin: 0 0; } @@ -98,11 +90,6 @@ .react-flow__nodesselection { z-index: 3; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; transform-origin: left top; pointer-events: none; @@ -153,14 +140,6 @@ /* additional components */ -.react-flow__background { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - .react-flow__controls { position: absolute; z-index: 5; @@ -184,10 +163,3 @@ bottom: 10px; right: 10px; } - -.react-flow__container { - position: absolute; - width: 100%; - height: 100%; - pointer-events: none; -} From 50250e56706bd17111d027308d5e033ef6036145 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 4 Nov 2021 15:32:04 +0100 Subject: [PATCH 046/263] refactor(connectionline): use abs pos --- src/components/ConnectionLine/index.tsx | 34 +++++++++++++------------ src/container/NodeRenderer/index.tsx | 1 + src/container/ZoomPane/index.tsx | 2 +- src/hooks/useResizeHandler.ts | 6 +++-- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index f89f62d9..55143f72 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, CSSProperties } from 'react'; +import React, { useRef, CSSProperties } from 'react'; import shallow from 'zustand/shallow'; import { useStore } from '../../store'; @@ -28,7 +28,7 @@ interface ConnectionLineProps { CustomConnectionLineComponent?: ConnectionLineComponent; } -const selector = (s: ReactFlowState) => ({ nodeLookup: s.nodeLookup, transform: s.transform }); +const selector = (s: ReactFlowState) => ({ nodeLookup: s.nodeLookup, nodes: s.nodes, transform: s.transform }); export default ({ connectionNodeId, @@ -41,27 +41,29 @@ export default ({ isConnectable, CustomConnectionLineComponent, }: ConnectionLineProps) => { - const { nodeLookup, transform } = useStore(selector, shallow); - const [sourceNode, setSourceNode] = useState(null); const nodeId = connectionNodeId; const handleId = connectionHandleId; - useEffect(() => { - const nextSourceNode = nodeLookup.get(nodeId) || null; - setSourceNode(nextSourceNode); - }, []); + const { nodeLookup, nodes, transform } = useStore(selector, shallow); + const sourceNodeInternals = useRef(nodeLookup.get(nodeId)); + const sourceNode = useRef(nodes.find((n) => n.id === nodeId)); - if (!sourceNode || !isConnectable || !sourceNode.handleBounds?.[connectionHandleType]) { + if ( + !sourceNode.current || + !sourceNodeInternals.current || + !isConnectable || + !sourceNodeInternals.current.handleBounds?.[connectionHandleType] + ) { return null; } const sourceHandle = handleId - ? sourceNode.handleBounds[connectionHandleType]!.find((d: HandleElement) => d.id === handleId) - : sourceNode.handleBounds[connectionHandleType]![0]; - const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.width! / 2; - const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.height!; - const sourceX = sourceNode.position!.x + sourceHandleX; - const sourceY = sourceNode.position!.y + sourceHandleY; + ? sourceNodeInternals.current.handleBounds[connectionHandleType]!.find((d: HandleElement) => d.id === handleId) + : sourceNodeInternals.current.handleBounds[connectionHandleType]![0]; + const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNodeInternals.current.width! / 2; + const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNodeInternals.current.height!; + const sourceX = sourceNodeInternals.current.positionAbsolute!.x + sourceHandleX; + const sourceY = sourceNodeInternals.current.positionAbsolute!.y + sourceHandleY; const targetX = (connectionPositionX - transform[0]) / transform[2]; const targetY = (connectionPositionY - transform[1]) / transform[2]; @@ -81,7 +83,7 @@ export default ({ targetPosition={targetPosition} connectionLineType={connectionLineType} connectionLineStyle={connectionLineStyle} - sourceNode={sourceNode as Node} + sourceNode={sourceNode.current as Node} sourceHandle={sourceHandle} /> diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 4a774271..30d045c4 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -74,6 +74,7 @@ const NodeRenderer = (props: NodeRendererProps) => { return ( +
{children}
); diff --git a/src/hooks/useResizeHandler.ts b/src/hooks/useResizeHandler.ts index 921aaef3..fc2c830b 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; -export default (rendererNode: MutableRefObject) => { +function useResizeHandler(rendererNode: MutableRefObject) { const updateSize = useStore(updateSizeSelector); useEffect(() => { @@ -42,4 +42,6 @@ export default (rendererNode: MutableRefObject) => { } }; }, []); -}; +} + +export default useResizeHandler; From b4b15612a324da979c01e55ec206399775a128e7 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 4 Nov 2021 15:51:20 +0100 Subject: [PATCH 047/263] refactor(graphView): add useOnLoadHandler --- src/container/EdgeRenderer/utils.ts | 33 +------------ src/container/GraphView/index.tsx | 32 ++----------- src/hooks/useOnLoadHandler.ts | 72 +++++++++++++++++++++++++++++ src/utils/graph.ts | 41 ---------------- 4 files changed, 76 insertions(+), 102 deletions(-) create mode 100644 src/hooks/useOnLoadHandler.ts diff --git a/src/container/EdgeRenderer/utils.ts b/src/container/EdgeRenderer/utils.ts index d3e4ac20..8a5826d1 100644 --- a/src/container/EdgeRenderer/utils.ts +++ b/src/container/EdgeRenderer/utils.ts @@ -4,18 +4,7 @@ import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../compon import wrapEdge from '../../components/Edges/wrapEdge'; import { rectToBox } from '../../utils/graph'; -import { - EdgeTypesType, - EdgeProps, - Position, - Node, - XYPosition, - ElementId, - HandleElement, - Transform, - Edge, - Rect, -} from '../../types'; +import { EdgeTypesType, EdgeProps, Position, XYPosition, ElementId, HandleElement, Transform, Rect } from '../../types'; export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType { const standardTypes: EdgeTypesType = { @@ -164,23 +153,3 @@ export function isEdgeVisible({ return overlappingArea > 0; } - -type SourceTargetNode = { - sourceNode: Node | null; - targetNode: Node | null; -}; - -export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNode => { - return nodes.reduce( - (res, node) => { - if (node.id === edge.source) { - res.sourceNode = node; - } - if (node.id === edge.target) { - res.targetNode = node; - } - return res; - }, - { sourceNode: null, targetNode: null } as SourceTargetNode - ); -}; diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 6af31a8e..34c7c668 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -1,15 +1,11 @@ -import React, { useEffect, useRef, memo } from 'react'; +import React, { memo } from 'react'; -import { useStoreApi } from '../../store'; import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; import Viewport from '../Viewport'; -import { onLoadProject, onLoadGetNodes, onLoadGetEdges, onLoadToObject } from '../../utils/graph'; -import useZoomPanHelper from '../../hooks/useZoomPanHelper'; - import { ReactFlowProps } from '../ReactFlow'; - +import useOnLoadHandler from '../../hooks/useOnLoadHandler'; import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types'; export interface GraphViewProps extends Omit { @@ -81,29 +77,7 @@ const GraphView = ({ onEdgeUpdateStart, onEdgeUpdateEnd, }: GraphViewProps) => { - const isInitialized = useRef(false); - const store = useStoreApi(); - const { zoomIn, zoomOut, zoomTo, transform: setTransform, fitView, initialized } = useZoomPanHelper(); - - useEffect(() => { - if (!isInitialized.current && initialized) { - if (onLoad) { - onLoad({ - fitView: (params = { padding: 0.1 }) => fitView(params), - zoomIn, - zoomOut, - zoomTo, - setTransform, - project: onLoadProject(store.getState), - getNodes: onLoadGetNodes(store.getState), - getEdges: onLoadGetEdges(store.getState), - toObject: onLoadToObject(store.getState), - }); - } - - isInitialized.current = true; - } - }, [onLoad, zoomIn, zoomOut, zoomTo, setTransform, fitView, initialized]); + useOnLoadHandler(onLoad); return ( ) => { + return (position: XYPosition): XYPosition => { + const { transform, snapToGrid, snapGrid } = getState(); + + return pointToRendererPoint(position, transform, snapToGrid, snapGrid); + }; +}; + +export const onLoadGetNodes = (getState: GetState) => { + return (): Node[] => { + const { nodes = [] } = getState(); + + return nodes.map((n) => ({ ...n })); + }; +}; + +export const onLoadGetEdges = (getState: GetState) => { + return (): Edge[] => { + const { edges = [] } = getState(); + + return edges.map((e) => ({ ...e })); + }; +}; + +export const onLoadToObject = (getState: GetState) => { + return (): FlowExportObject => { + const { nodes = [], edges = [], transform } = getState(); + + return { + nodes: nodes.map((n) => ({ ...n })), + edges: edges.map((e) => ({ ...e })), + position: [transform[0], transform[1]], + zoom: transform[2], + }; + }; +}; + +function useOnLoadHandler(onLoad: OnLoadFunc | undefined) { + const isInitialized = useRef(false); + const store = useStoreApi(); + const { zoomIn, zoomOut, zoomTo, transform: setTransform, fitView, initialized } = useZoomPanHelper(); + + useEffect(() => { + if (!isInitialized.current && initialized) { + if (onLoad) { + onLoad({ + fitView: (params = { padding: 0.1 }) => fitView(params), + zoomIn, + zoomOut, + zoomTo, + setTransform, + project: onLoadProject(store.getState), + getNodes: onLoadGetNodes(store.getState), + getEdges: onLoadGetEdges(store.getState), + toObject: onLoadToObject(store.getState), + }); + } + + isInitialized.current = true; + } + }, [onLoad, zoomIn, zoomOut, zoomTo, setTransform, fitView, initialized]); +} + +export default useOnLoadHandler; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index bc648a7c..ddbabe40 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -1,5 +1,3 @@ -import { GetState } from 'zustand'; - import { clamp } from '../utils'; import { @@ -12,10 +10,8 @@ import { Rect, Box, Connection, - FlowExportObject, EdgeChange, NodeChange, - ReactFlowState, EdgeMarkerType, } from '../types'; @@ -141,14 +137,6 @@ export const pointToRendererPoint = ( return position; }; -export const onLoadProject = (getState: GetState) => { - return (position: XYPosition): XYPosition => { - const { transform, snapToGrid, snapGrid } = getState(); - - return pointToRendererPoint(position, transform, snapToGrid, snapGrid); - }; -}; - const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({ x: Math.min(box1.x, box2.x), y: Math.min(box1.y, box2.y), @@ -240,35 +228,6 @@ export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => { return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target)); }; -export const onLoadGetNodes = (getState: GetState) => { - return (): Node[] => { - const { nodes = [] } = getState(); - - return nodes.map((n) => ({ ...n })); - }; -}; - -export const onLoadGetEdges = (getState: GetState) => { - return (): Edge[] => { - const { edges = [] } = getState(); - - return edges.map((e) => ({ ...e })); - }; -}; - -export const onLoadToObject = (getState: GetState) => { - return (): FlowExportObject => { - const { nodes = [], edges = [], transform } = getState(); - - return { - nodes: nodes.map((n) => ({ ...n })), - edges: edges.map((e) => ({ ...e })), - position: [transform[0], transform[1]], - zoom: transform[2], - }; - }; -}; - export const getTransformForBounds = ( bounds: Rect, width: number, From cc7debf8de41641e4932b15e7cafcd58c20158d0 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 4 Nov 2021 16:10:35 +0100 Subject: [PATCH 048/263] refactor(utils): cleanup --- package-lock.json | 4 +- package.json | 2 +- src/additional-components/MiniMap/index.tsx | 3 +- src/container/EdgeRenderer/utils.ts | 2 +- src/hooks/useOnLoadHandler.ts | 74 +++++++--------- src/index.ts | 3 +- src/utils/changes.ts | 51 +++++++++++ src/utils/graph.ts | 96 +-------------------- src/utils/index.ts | 26 +++++- 9 files changed, 116 insertions(+), 145 deletions(-) create mode 100644 src/utils/changes.ts diff --git a/package-lock.json b/package-lock.json index 9371ace8..01063812 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.10", + "version": "10.0.0-next.11", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "10.0.0-next.10", + "version": "10.0.0-next.11", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/package.json b/package.json index 45912eb1..dfb777aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.10", + "version": "10.0.0-next.11", "engines": { "node": ">=12" }, diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index 1f13149f..7cc82df9 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -3,7 +3,8 @@ import cc from 'classcat'; import shallow from 'zustand/shallow'; import { useStore } from '../../store'; -import { getRectOfNodes, getBoundsofRects } from '../../utils/graph'; +import { getRectOfNodes } from '../../utils/graph'; +import { getBoundsofRects } from '../../utils'; import { Node, Rect, ReactFlowState } from '../../types'; import MiniMapNode from './MiniMapNode'; diff --git a/src/container/EdgeRenderer/utils.ts b/src/container/EdgeRenderer/utils.ts index 8a5826d1..1f94f616 100644 --- a/src/container/EdgeRenderer/utils.ts +++ b/src/container/EdgeRenderer/utils.ts @@ -2,7 +2,7 @@ import { ComponentType } from 'react'; import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges'; import wrapEdge from '../../components/Edges/wrapEdge'; -import { rectToBox } from '../../utils/graph'; +import { rectToBox } from '../../utils'; import { EdgeTypesType, EdgeProps, Position, XYPosition, ElementId, HandleElement, Transform, Rect } from '../../types'; diff --git a/src/hooks/useOnLoadHandler.ts b/src/hooks/useOnLoadHandler.ts index 714840e7..39c123eb 100644 --- a/src/hooks/useOnLoadHandler.ts +++ b/src/hooks/useOnLoadHandler.ts @@ -1,47 +1,9 @@ -import { GetState } from 'zustand'; import { useEffect, useRef } from 'react'; import { pointToRendererPoint } from '../utils/graph'; import { useStoreApi } from '../store'; import useZoomPanHelper from '../hooks/useZoomPanHelper'; -import { OnLoadFunc, ReactFlowState, XYPosition, Node, Edge, FlowExportObject } from '../types'; - -export const onLoadProject = (getState: GetState) => { - return (position: XYPosition): XYPosition => { - const { transform, snapToGrid, snapGrid } = getState(); - - return pointToRendererPoint(position, transform, snapToGrid, snapGrid); - }; -}; - -export const onLoadGetNodes = (getState: GetState) => { - return (): Node[] => { - const { nodes = [] } = getState(); - - return nodes.map((n) => ({ ...n })); - }; -}; - -export const onLoadGetEdges = (getState: GetState) => { - return (): Edge[] => { - const { edges = [] } = getState(); - - return edges.map((e) => ({ ...e })); - }; -}; - -export const onLoadToObject = (getState: GetState) => { - return (): FlowExportObject => { - const { nodes = [], edges = [], transform } = getState(); - - return { - nodes: nodes.map((n) => ({ ...n })), - edges: edges.map((e) => ({ ...e })), - position: [transform[0], transform[1]], - zoom: transform[2], - }; - }; -}; +import { OnLoadFunc, XYPosition, Node, Edge, FlowExportObject } from '../types'; function useOnLoadHandler(onLoad: OnLoadFunc | undefined) { const isInitialized = useRef(false); @@ -51,16 +13,42 @@ function useOnLoadHandler(onLoad: OnLoadFunc | undefined) { useEffect(() => { if (!isInitialized.current && initialized) { if (onLoad) { + const project = (position: XYPosition): XYPosition => { + const { transform, snapToGrid, snapGrid } = store.getState(); + return pointToRendererPoint(position, transform, snapToGrid, snapGrid); + }; + + const getNodes = (): Node[] => { + const { nodes = [] } = store.getState(); + return nodes.map((n) => ({ ...n })); + }; + + const getEdges = (): Edge[] => { + const { edges = [] } = store.getState(); + return edges.map((e) => ({ ...e })); + }; + + const toObject = (): FlowExportObject => { + const { nodes = [], edges = [], transform } = store.getState(); + + return { + nodes: nodes.map((n) => ({ ...n })), + edges: edges.map((e) => ({ ...e })), + position: [transform[0], transform[1]], + zoom: transform[2], + }; + }; + onLoad({ fitView: (params = { padding: 0.1 }) => fitView(params), zoomIn, zoomOut, zoomTo, setTransform, - project: onLoadProject(store.getState), - getNodes: onLoadGetNodes(store.getState), - getEdges: onLoadGetEdges(store.getState), - toObject: onLoadToObject(store.getState), + project, + getNodes, + getEdges, + toObject, }); } diff --git a/src/index.ts b/src/index.ts index 47989de5..6aa465d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,9 +22,8 @@ export { updateEdge, getTransformForBounds, getRectOfNodes, - applyNodeChanges, - applyEdgeChanges, } from './utils/graph'; +export { applyNodeChanges, applyEdgeChanges } from './utils/changes'; export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'; export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'; diff --git a/src/utils/changes.ts b/src/utils/changes.ts new file mode 100644 index 00000000..89ac2037 --- /dev/null +++ b/src/utils/changes.ts @@ -0,0 +1,51 @@ +import { Node, Edge, EdgeChange, NodeChange } from '../types'; + +function applyChanges(changes: NodeChange[] | EdgeChange[], elements: any[]): any[] { + const initElements: any[] = []; + + return elements.reduce((res: any[], item: any) => { + const currentChange = changes.find((c) => c.id === item.id); + + if (currentChange) { + switch (currentChange.type) { + case 'select': { + res.push({ ...item, isSelected: currentChange.isSelected }); + return res; + } + case 'dimensions': { + const updateItem = { ...item }; + + if (typeof currentChange.dimensions !== 'undefined') { + updateItem.width = currentChange.dimensions.width; + updateItem.height = currentChange.dimensions.height; + } + + if (typeof currentChange.position !== 'undefined') { + updateItem.position = currentChange.position; + } + + if (typeof currentChange.isDragging !== 'undefined') { + updateItem.isDragging = currentChange.isDragging; + } + + res.push(updateItem); + return res; + } + case 'remove': { + return res; + } + } + } + + res.push(item); + return res; + }, initElements); +} + +export function applyNodeChanges(changes: NodeChange[], nodes: Node[]): Node[] { + return applyChanges(changes, nodes) as Node[]; +} + +export function applyEdgeChanges(changes: EdgeChange[], edges: Edge[]): Edge[] { + return applyChanges(changes, edges) as Edge[]; +} diff --git a/src/utils/graph.ts b/src/utils/graph.ts index ddbabe40..d9055596 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -1,19 +1,6 @@ -import { clamp } from '../utils'; +import { boxToRect, clamp, getBoundsOfBoxes, rectToBox } from '../utils'; -import { - ElementId, - Node, - Edge, - Elements, - Transform, - XYPosition, - Rect, - Box, - Connection, - EdgeChange, - NodeChange, - EdgeMarkerType, -} from '../types'; +import { ElementId, Node, Edge, Elements, Transform, XYPosition, Rect, Connection, EdgeMarkerType } from '../types'; export const isEdge = (element: Node | Connection | Edge): element is Edge => 'id' in element && 'source' in element && 'target' in element; @@ -137,30 +124,6 @@ export const pointToRendererPoint = ( return position; }; -const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({ - x: Math.min(box1.x, box2.x), - y: Math.min(box1.y, box2.y), - x2: Math.max(box1.x2, box2.x2), - y2: Math.max(box1.y2, box2.y2), -}); - -export const rectToBox = ({ x, y, width, height }: Rect): Box => ({ - x, - y, - x2: x + width, - y2: y + height, -}); - -export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({ - x, - y, - width: x2 - x, - height: y2 - y, -}); - -export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect => - boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2))); - export const getRectOfNodes = (nodes: Node[]): Rect => { const box = nodes.reduce( (currBox, { position, width, height }) => @@ -171,11 +134,6 @@ export const getRectOfNodes = (nodes: Node[]): Rect => { return boxToRect(box); }; -export const graphPosToZoomedPos = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => ({ - x: x * tScale + tx, - y: y * tScale + ty, -}); - export const getNodesInside = ( nodes: Node[], rect: Rect, @@ -247,53 +205,3 @@ export const getTransformForBounds = ( return [x, y, clampedZoom]; }; - -function applyChanges(changes: NodeChange[] | EdgeChange[], elements: any[]): any[] { - const initElements: any[] = []; - - return elements.reduce((res: any[], item: any) => { - const currentChange = changes.find((c) => c.id === item.id); - - if (currentChange) { - switch (currentChange.type) { - case 'select': { - res.push({ ...item, isSelected: currentChange.isSelected }); - return res; - } - case 'dimensions': { - const updateItem = { ...item }; - - if (typeof currentChange.dimensions !== 'undefined') { - updateItem.width = currentChange.dimensions.width; - updateItem.height = currentChange.dimensions.height; - } - - if (typeof currentChange.position !== 'undefined') { - updateItem.position = currentChange.position; - } - - if (typeof currentChange.isDragging !== 'undefined') { - updateItem.isDragging = currentChange.isDragging; - } - - res.push(updateItem); - return res; - } - case 'remove': { - return res; - } - } - } - - res.push(item); - return res; - }, initElements); -} - -export function applyNodeChanges(changes: NodeChange[], nodes: Node[]): Node[] { - return applyChanges(changes, nodes) as Node[]; -} - -export function applyEdgeChanges(changes: EdgeChange[], edges: Edge[]): Edge[] { - return applyChanges(changes, edges) as Edge[]; -} diff --git a/src/utils/index.ts b/src/utils/index.ts index 29c3149f..ec44298a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,4 @@ -import { Dimensions, XYPosition, NodeExtent } from '../types'; +import { Dimensions, XYPosition, NodeExtent, Box, Rect } from '../types'; export const getDimensions = (node: HTMLDivElement): Dimensions => ({ width: node.offsetWidth, @@ -14,3 +14,27 @@ export const clampPosition = (position: XYPosition, extent: NodeExtent) => ({ export const getHostForElement = (element: HTMLElement): Document | ShadowRoot => (element.getRootNode?.() as Document | ShadowRoot) || window?.document; + +export const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({ + x: Math.min(box1.x, box2.x), + y: Math.min(box1.y, box2.y), + x2: Math.max(box1.x2, box2.x2), + y2: Math.max(box1.y2, box2.y2), +}); + +export const rectToBox = ({ x, y, width, height }: Rect): Box => ({ + x, + y, + x2: x + width, + y2: y + height, +}); + +export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({ + x, + y, + width: x2 - x, + height: y2 - y, +}); + +export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect => + boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2))); From 996b1b51f5aab400b4b47765c8450bad6c51960c Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 4 Nov 2021 18:13:21 +0100 Subject: [PATCH 049/263] refactor(store): rename nodeLookup to nodeInternals --- example/src/Basic/index.tsx | 2 +- src/additional-components/MiniMap/index.tsx | 12 ++- src/components/ConnectionLine/index.tsx | 8 +- src/components/Nodes/wrapNode.tsx | 4 +- src/container/EdgeRenderer/index.tsx | 10 +-- src/container/NodeRenderer/index.tsx | 14 ++-- src/hooks/useNodeInternalsRef.ts | 14 ++++ src/hooks/useNodeLookupRef.ts | 14 ---- src/hooks/useVisibleEdges.ts | 18 ++--- src/store/index.ts | 90 +++------------------ src/store/utils.ts | 77 ++++++++++++++++++ src/style.css | 1 + src/types/index.ts | 6 +- 13 files changed, 142 insertions(+), 128 deletions(-) create mode 100644 src/hooks/useNodeInternalsRef.ts delete mode 100644 src/hooks/useNodeLookupRef.ts create mode 100644 src/store/utils.ts diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index 40ed203c..0e53b216 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, MouseEvent, useCallback } from 'react'; +import { useState, MouseEvent, useCallback } from 'react'; import ReactFlow, { addEdge, diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index 7cc82df9..9cc5a0c1 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -29,7 +29,7 @@ const selector = (s: ReactFlowState) => ({ height: s.height, transform: s.transform, nodes: s.nodes, - nodeLookup: s.nodeLookup, + nodeInternals: s.nodeInternals, }); const MiniMap = ({ @@ -42,7 +42,13 @@ const MiniMap = ({ nodeStrokeWidth = 2, maskColor = 'rgb(240, 242, 243, 0.7)', }: MiniMapProps) => { - const { width: containerWidth, height: containerHeight, transform, nodes, nodeLookup } = useStore(selector, shallow); + const { + width: containerWidth, + height: containerHeight, + transform, + nodes, + nodeInternals, + } = useStore(selector, shallow); const [tX, tY, tScale] = transform; const mapClasses = cc(['react-flow__minimap', className]); @@ -85,7 +91,7 @@ const MiniMap = ({ {nodes .filter((node) => !node.isHidden && node.width && node.height) .map((node) => { - const positionAbsolute = nodeLookup.get(node.id)?.positionAbsolute; + const positionAbsolute = nodeInternals.get(node.id)?.positionAbsolute; return ( ({ nodeLookup: s.nodeLookup, nodes: s.nodes, transform: s.transform }); +const selector = (s: ReactFlowState) => ({ nodeInternals: s.nodeInternals, nodes: s.nodes, transform: s.transform }); export default ({ connectionNodeId, @@ -44,8 +44,8 @@ export default ({ const nodeId = connectionNodeId; const handleId = connectionHandleId; - const { nodeLookup, nodes, transform } = useStore(selector, shallow); - const sourceNodeInternals = useRef(nodeLookup.get(nodeId)); + const { nodeInternals, nodes, transform } = useStore(selector, shallow); + const sourceNodeInternals = useRef(nodeInternals.get(nodeId)); const sourceNode = useRef(nodes.find((n) => n.id === nodeId)); if ( diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index dfc9cc71..b1cd90fe 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -66,12 +66,11 @@ export default (NodeComponent: ComponentType) => { const nodeStyle: CSSProperties = useMemo( () => ({ zIndex, - // zIndex: isSelected ? zIndex + 1 : zIndex, transform: `translate(${xPos}px,${yPos}px)`, pointerEvents: isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none', // prevents jumping of nodes on start - // opacity: isInitialized ? 1 : 0, + opacity: isInitialized ? 1 : 0, ...style, }), [ @@ -87,6 +86,7 @@ export default (NodeComponent: ComponentType) => { onMouseMove, onMouseLeave, isParentNode, + zIndex, ] ); diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index eea579d2..738aa8e9 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -216,7 +216,7 @@ const selector = (s: ReactFlowState) => ({ width: s.width, height: s.height, connectionMode: s.connectionMode, - nodeLookup: s.nodeLookup, + nodeInternals: s.nodeInternals, }); const EdgeRenderer = (props: EdgeRendererProps) => { @@ -230,9 +230,9 @@ const EdgeRenderer = (props: EdgeRendererProps) => { width, height, connectionMode, - nodeLookup, + nodeInternals, } = useStore(selector, shallow); - const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeLookup); + const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals); if (!width) { return null; @@ -254,8 +254,8 @@ const EdgeRenderer = (props: EdgeRendererProps) => { {isMaxLevel && } {edges.map((edge: Edge) => { - const sourceNode = nodeLookup.get(edge.source); - const targetNode = nodeLookup.get(edge.target); + const sourceNode = nodeInternals.get(edge.source); + const targetNode = nodeInternals.get(edge.target); return ( ({ const NodeRenderer = (props: NodeRendererProps) => { const { scale, nodesDraggable, nodesConnectable, elementsSelectable, updateNodeDimensions, snapGrid, snapToGrid } = useStore(selector, shallow); - const nodeLookup = useNodeLookupRef(); + const nodeInternals = useNodeInternalsRef(); const nodes = useVisibleNodes(props.onlyRenderVisibleElements); const resizeObserver = useMemo(() => { @@ -56,7 +56,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
{nodes.map((node) => { const nodeType = node.type || 'default'; - const lookupNode = nodeLookup.current.get(node.id); + const internals = nodeInternals.current.get(node.id); if (!props.nodeTypes[nodeType]) { console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); @@ -83,8 +83,8 @@ const NodeRenderer = (props: NodeRendererProps) => { sourcePosition={node.sourcePosition} targetPosition={node.targetPosition} isHidden={node.isHidden} - xPos={lookupNode?.positionAbsolute?.x || 0} - yPos={lookupNode?.positionAbsolute?.y || 0} + xPos={internals?.positionAbsolute?.x || 0} + yPos={internals?.positionAbsolute?.y || 0} isDragging={node.isDragging} isInitialized={isInitialized} snapGrid={snapGrid} @@ -106,8 +106,8 @@ const NodeRenderer = (props: NodeRendererProps) => { isConnectable={isConnectable} resizeObserver={resizeObserver} dragHandle={node.dragHandle} - zIndex={lookupNode?.treeLevel || 0} - isParentNode={!!lookupNode?.isParentNode} + zIndex={internals?.treeLevel || 0} + isParentNode={!!internals?.isParentNode} /> ); })} diff --git a/src/hooks/useNodeInternalsRef.ts b/src/hooks/useNodeInternalsRef.ts new file mode 100644 index 00000000..d6b9b2d9 --- /dev/null +++ b/src/hooks/useNodeInternalsRef.ts @@ -0,0 +1,14 @@ +import { useRef, useEffect } from 'react'; + +import { useStoreApi } from '../store'; + +function useNodeInternalsRef() { + const store = useStoreApi(); + const nodeInternals = useRef(store.getState().nodeInternals); + + useEffect(() => store.subscribe((state) => (nodeInternals.current = state.nodeInternals)), []); + + return nodeInternals; +} + +export default useNodeInternalsRef; diff --git a/src/hooks/useNodeLookupRef.ts b/src/hooks/useNodeLookupRef.ts deleted file mode 100644 index 707751f3..00000000 --- a/src/hooks/useNodeLookupRef.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useRef, useEffect } from 'react'; - -import { useStoreApi } from '../store'; - -function useNodeLookupRef() { - const store = useStoreApi(); - const nodeLookup = useRef(store.getState().nodeLookup); - - useEffect(() => store.subscribe((state) => (nodeLookup.current = state.nodeLookup)), []); - - return nodeLookup; -} - -export default useNodeLookupRef; diff --git a/src/hooks/useVisibleEdges.ts b/src/hooks/useVisibleEdges.ts index 14cd22e9..33befa1e 100644 --- a/src/hooks/useVisibleEdges.ts +++ b/src/hooks/useVisibleEdges.ts @@ -2,15 +2,15 @@ import { useCallback } from 'react'; import { useStore } from '../store'; import { isEdgeVisible } from '../container/EdgeRenderer/utils'; -import { ReactFlowState, NodeLookup, Edge } from '../types'; +import { ReactFlowState, NodeInternals, Edge } from '../types'; -function groupEdgesByTreeLevel(edges: Edge[], nodeLookup: NodeLookup) { +function groupEdgesByTreeLevel(edges: Edge[], nodeInternals: NodeInternals) { let maxLevel = -1; const levelLookup = edges.reduce>((tree, edge) => { const treeLevel = Math.max( - nodeLookup.get(edge.source)?.treeLevel || 0, - nodeLookup.get(edge.target)?.treeLevel || 0 + nodeInternals.get(edge.source)?.treeLevel || 0, + nodeInternals.get(edge.target)?.treeLevel || 0 ); if (tree[treeLevel]) { tree[treeLevel].push(edge); @@ -34,7 +34,7 @@ function groupEdgesByTreeLevel(edges: Edge[], nodeLookup: NodeLookup) { }); } -function useVisibleEdges(onlyRenderVisible: boolean, nodeLookup: NodeLookup) { +function useVisibleEdges(onlyRenderVisible: boolean, nodeInternals: NodeInternals) { const edges = useStore( useCallback( (s: ReactFlowState) => { @@ -43,8 +43,8 @@ function useVisibleEdges(onlyRenderVisible: boolean, nodeLookup: NodeLookup) { } return s.edges.filter((e) => { - const sourceNode = nodeLookup.get(e.source); - const targetNode = nodeLookup.get(e.target); + const sourceNode = nodeInternals.get(e.source); + const targetNode = nodeInternals.get(e.target); return ( sourceNode?.width && @@ -65,11 +65,11 @@ function useVisibleEdges(onlyRenderVisible: boolean, nodeLookup: NodeLookup) { ); }); }, - [onlyRenderVisible, nodeLookup] + [onlyRenderVisible, nodeInternals] ) ); - return groupEdgesByTreeLevel(edges, nodeLookup); + return groupEdgesByTreeLevel(edges, nodeInternals); } export default useVisibleEdges; diff --git a/src/store/index.ts b/src/store/index.ts index d139d124..b6c3cba0 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -26,12 +26,10 @@ import { OnEdgesChange, EdgeChange, NodeDimensionChange, - NodeLookup, - NodeLookupItem, - ElementId, } from '../types'; import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph'; import { getHandleBounds } from '../components/Nodes/utils'; +import { createNodeInternals } from './utils'; const { Provider, useStore, useStoreApi } = createContext(); @@ -41,34 +39,6 @@ const createNodeOrEdgeSelectionChange = (isSelected: boolean) => (item: Node | E isSelected, }); -type XYPosAndTreeLevel = XYPosition & { treeLevel: number }; - -function addPositions(a: XYPosAndTreeLevel, b: XYPosition): XYPosAndTreeLevel { - return { - x: (a.x ?? 0) + (b.x ?? 0), - y: (a.y ?? 0) + (b.y ?? 0), - treeLevel: a.treeLevel + 1, - }; -} - -function getAbsolutePositionAndTreeLevel( - node: NodeLookupItem, - nodeLookup: NodeLookup, - result: XYPosAndTreeLevel -): XYPosAndTreeLevel { - const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : false; - - if (!parentNode) { - return result; - } - - return getAbsolutePositionAndTreeLevel( - parentNode, - nodeLookup, - addPositions(result, parentNode.position || { x: 0, y: 0 }) - ); -} - const createStore = () => create((set, get) => ({ width: 0, @@ -125,58 +95,18 @@ const createStore = () => reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-', - nodeLookup: new Map(), + nodeInternals: new Map(), setNodes: (nodes: Node[]) => { - const { nodeLookup } = get(); - const nextNodeLookup = new Map(); + const nodeInternals = createNodeInternals(nodes, get().nodeInternals); - nodes.forEach((node) => { - const lookupNode: NodeLookupItem = { - ...nodeLookup.get(node.id), - width: node.width || null, - height: node.height || null, - position: node.position, - positionAbsolute: node.position, - treeLevel: node.zIndex || 0, - }; - if (node.parentNode) { - lookupNode.parentNode = node.parentNode; - } - nextNodeLookup.set(node.id, lookupNode); - }); - - nodes - .filter((node) => node.parentNode) - .forEach((node) => { - const positionAbsoluteAndTreeLevel = getAbsolutePositionAndTreeLevel(node, nextNodeLookup, { - ...node.position, - treeLevel: node.zIndex || 0, - }); - - nextNodeLookup.set(node.parentNode!, { ...nextNodeLookup.get(node.parentNode!), isParentNode: true }); - - if (positionAbsoluteAndTreeLevel) { - const { treeLevel, x, y } = positionAbsoluteAndTreeLevel; - - nextNodeLookup.set(node.id, { - ...nextNodeLookup.get(node.id), - positionAbsolute: { - x, - y, - }, - treeLevel, - }); - } - }); - - set({ nodes, nodeLookup: nextNodeLookup }); + set({ nodes, nodeInternals }); }, setEdges: (edges: Edge[]) => { set({ edges }); }, updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { - const { onNodesChange, nodes, transform, nodeLookup } = get(); + const { onNodesChange, nodes, transform, nodeInternals } = get(); const nodesToChange: NodeChange[] = updates.reduce((res, update) => { const node = nodes.find((n) => n.id === update.id); @@ -190,8 +120,8 @@ const createStore = () => if (doUpdate) { const handleBounds = getHandleBounds(update.nodeElement, transform[2]); - nodeLookup.set(node.id, { - ...nodeLookup.get(node.id), + nodeInternals.set(node.id, { + ...nodeInternals.get(node.id), handleBounds, ...dimensions, }); @@ -208,7 +138,7 @@ const createStore = () => return res; }, []); - set({ nodeLookup: new Map(nodeLookup) }); + set({ nodeInternals: new Map(nodeInternals) }); onNodesChange?.(nodesToChange); }, @@ -216,11 +146,11 @@ const createStore = () => const { onNodesChange, nodes, nodeExtent } = get(); if (onNodesChange) { - const matchingNodes = nodes.filter((n) => !!n.isSelected || n.id === id); + const matchingNodes = nodes.filter((n) => !!(n.isSelected || n.id === id)); if (matchingNodes?.length) { onNodesChange( - matchingNodes.map((node) => { + matchingNodes?.map((node) => { const change: NodeDimensionChange = { id: node.id, type: 'dimensions', diff --git a/src/store/utils.ts b/src/store/utils.ts new file mode 100644 index 00000000..928adf45 --- /dev/null +++ b/src/store/utils.ts @@ -0,0 +1,77 @@ +import { ElementId, Node, NodeInternals, NodeInternalsItem, XYPosition } from '../types'; + +type XYPosAndTreeLevel = XYPosition & { treeLevel: number }; + +function addPositions(a: XYPosAndTreeLevel, b: XYPosition): XYPosAndTreeLevel { + return { + x: (a.x ?? 0) + (b.x ?? 0), + y: (a.y ?? 0) + (b.y ?? 0), + treeLevel: a.treeLevel + 1, + }; +} + +function getAbsolutePosAndTreeLevel( + node: NodeInternalsItem, + nodeInternals: NodeInternals, + result: XYPosAndTreeLevel +): XYPosAndTreeLevel { + const parentNode = node.parentNode ? nodeInternals.get(node.parentNode) : false; + + if (!parentNode) { + return result; + } + + return getAbsolutePosAndTreeLevel( + parentNode, + nodeInternals, + addPositions(result, parentNode.position || { x: 0, y: 0 }) + ); +} +export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals): NodeInternals { + const nextNodeInternals = new Map(); + const parentNodes: Record = {}; + + nodes.forEach((node) => { + const internals: NodeInternalsItem = { + ...nodeInternals.get(node.id), + width: node.width || null, + height: node.height || null, + position: node.position, + positionAbsolute: node.position, + treeLevel: node.zIndex || 0, + }; + if (node.parentNode) { + internals.parentNode = node.parentNode; + parentNodes[node.parentNode] = true; + } + nextNodeInternals.set(node.id, internals); + }); + + nodes.forEach((node) => { + const updatedInternals: NodeInternalsItem = nextNodeInternals.get(node.id)!; + + if (node.parentNode) { + const positionAbsoluteAndTreeLevel = getAbsolutePosAndTreeLevel(node, nextNodeInternals, { + ...node.position, + treeLevel: node.zIndex || 0, + }); + + const { treeLevel, x, y } = positionAbsoluteAndTreeLevel; + + nextNodeInternals.set(node.parentNode!, { ...nextNodeInternals.get(node.parentNode!), isParentNode: true }); + updatedInternals.positionAbsolute = { + x, + y, + }; + updatedInternals.treeLevel = treeLevel; + } + + if ((node.isDragging || node.isSelected) && !parentNodes[node.id]) { + nextNodeInternals.set(node.id, { ...updatedInternals, treeLevel: 1000 }); + } else { + nextNodeInternals.set(node.id, { ...updatedInternals, treeLevel: updatedInternals?.treeLevel || 0 }); + } + }); + + return nextNodeInternals; +} diff --git a/src/style.css b/src/style.css index fe74d2c0..30290001 100644 --- a/src/style.css +++ b/src/style.css @@ -20,6 +20,7 @@ .react-flow__viewport { transform-origin: 0 0; z-index: 2; + pointer-events: none; } .react-flow__renderer { diff --git a/src/types/index.ts b/src/types/index.ts index 378a4666..02cf3d43 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -457,7 +457,7 @@ export type InitD3ZoomPayload = { export type OnNodesChange = (nodes: NodeChange[]) => void; export type OnEdgesChange = (nodes: EdgeChange[]) => void; -export type NodeLookupItem = { +export type NodeInternalsItem = { width?: number | null; height?: number | null; parentNode?: ElementId; @@ -468,14 +468,14 @@ export type NodeLookupItem = { isParentNode?: boolean; }; -export type NodeLookup = Map; +export type NodeInternals = Map; export interface ReactFlowState { width: number; height: number; transform: Transform; nodes: Node[]; - nodeLookup: NodeLookup; + nodeInternals: NodeInternals; edges: Edge[]; selectedNodesBbox: Rect; onNodesChange: OnNodesChange | null; From fefcc441aff7465526149a2217034b2a039e7da7 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 4 Nov 2021 18:14:49 +0100 Subject: [PATCH 050/263] chore(package): bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01063812..7dd454ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.11", + "version": "10.0.0-next.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "10.0.0-next.11", + "version": "10.0.0-next.12", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/package.json b/package.json index dfb777aa..f5608d80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.11", + "version": "10.0.0-next.12", "engines": { "node": ">=12" }, From fdf2571402179acd73d060c4e44b659f8c6d7f89 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 4 Nov 2021 21:43:47 +0100 Subject: [PATCH 051/263] feat(nodes): add extent option --- example/src/Basic/index.tsx | 1 + example/src/Layouting/index.tsx | 4 ++-- src/components/StoreUpdater/index.tsx | 7 +++---- src/container/ReactFlow/index.tsx | 7 +++---- src/store/index.ts | 26 +++++++++++++++++++------- src/types/index.ts | 12 ++++++------ src/utils/index.ts | 4 ++-- 7 files changed, 36 insertions(+), 25 deletions(-) diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index 0e53b216..75069deb 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -36,6 +36,7 @@ const initialNodes: Node[] = [ position: { x: 15, y: 15 }, className: 'light', parentNode: '4', + extent: 'parent', }, { id: '4b', diff --git a/example/src/Layouting/index.tsx b/example/src/Layouting/index.tsx index cbae1310..56484640 100644 --- a/example/src/Layouting/index.tsx +++ b/example/src/Layouting/index.tsx @@ -10,7 +10,7 @@ import ReactFlow, { Node, Connection, Edge, - NodeExtent, + CoordinateExtent, Position, } from 'react-flow-renderer'; import dagre from 'dagre'; @@ -22,7 +22,7 @@ import './layouting.css'; const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); -const nodeExtent: NodeExtent = [ +const nodeExtent: CoordinateExtent = [ [0, 0], [1000, 1000], ]; diff --git a/src/components/StoreUpdater/index.tsx b/src/components/StoreUpdater/index.tsx index 235b7b08..33593e4d 100644 --- a/src/components/StoreUpdater/index.tsx +++ b/src/components/StoreUpdater/index.tsx @@ -10,12 +10,11 @@ import { OnConnectStartFunc, OnConnectStopFunc, OnConnectEndFunc, - NodeExtent, + CoordinateExtent, OnNodesChange, OnEdgesChange, ConnectionMode, SnapGrid, - TranslateExtent, } from '../../types'; interface StoreUpdaterProps { @@ -29,14 +28,14 @@ interface StoreUpdaterProps { nodesConnectable?: boolean; minZoom?: number; maxZoom?: number; - nodeExtent?: NodeExtent; + nodeExtent?: CoordinateExtent; onNodesChange?: OnNodesChange; onEdgesChange?: OnEdgesChange; elementsSelectable?: boolean; connectionMode?: ConnectionMode; snapToGrid?: boolean; snapGrid?: SnapGrid; - translateExtent?: TranslateExtent; + translateExtent?: CoordinateExtent; } const selector = (s: ReactFlowState) => ({ diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index c9c184cb..9f266fb7 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -33,11 +33,10 @@ import { OnConnectStopFunc, OnConnectEndFunc, OnConnectFunc, - TranslateExtent, + CoordinateExtent, KeyCode, PanOnScrollMode, OnEdgeUpdateFunc, - NodeExtent, NodeChange, EdgeChange, } from '../../types'; @@ -111,9 +110,9 @@ export interface ReactFlowProps extends Omit, 'on maxZoom?: number; defaultZoom?: number; defaultPosition?: [number, number]; - translateExtent?: TranslateExtent; + translateExtent?: CoordinateExtent; preventScrolling?: boolean; - nodeExtent?: NodeExtent; + nodeExtent?: CoordinateExtent; defaultMarkerColor?: string; zoomOnScroll?: boolean; zoomOnPinch?: boolean; diff --git a/src/store/index.ts b/src/store/index.ts index b6c3cba0..570d3c3e 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -11,8 +11,7 @@ import { NodeDiffUpdate, XYPosition, InitD3ZoomPayload, - TranslateExtent, - NodeExtent, + CoordinateExtent, Transform, Dimensions, OnConnectFunc, @@ -143,7 +142,7 @@ const createStore = () => onNodesChange?.(nodesToChange); }, updateNodePosition: ({ id, diff, isDragging }: NodeDiffUpdate) => { - const { onNodesChange, nodes, nodeExtent } = get(); + const { onNodesChange, nodes, nodeExtent, nodeInternals } = get(); if (onNodesChange) { const matchingNodes = nodes.filter((n) => !!(n.isSelected || n.id === id)); @@ -158,13 +157,26 @@ const createStore = () => }; if (diff) { - change.position = nodeExtent + 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( { x: node.position.x + diff.x, y: node.position.y + diff.y, }, - nodeExtent + currentExtent ) : { x: node.position.x + diff.x, y: node.position.y + diff.y }; } @@ -300,7 +312,7 @@ const createStore = () => set({ maxZoom }); }, - setTranslateExtent: (translateExtent: TranslateExtent) => { + setTranslateExtent: (translateExtent: CoordinateExtent) => { const { d3Zoom } = get(); d3Zoom?.translateExtent(translateExtent); @@ -320,7 +332,7 @@ const createStore = () => onEdgesChange?.(edgesToUnselect as EdgeChange[]); } }, - setNodeExtent: (nodeExtent: NodeExtent) => + setNodeExtent: (nodeExtent: CoordinateExtent) => set({ nodeExtent, nodes: get().nodes.map((node) => { diff --git a/src/types/index.ts b/src/types/index.ts index 02cf3d43..f8403993 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -84,6 +84,7 @@ export interface Node { height?: number | null; parentNode?: ElementId; zIndex?: number; + extent?: 'parent' | CoordinateExtent; } export enum ArrowHeadType { @@ -416,8 +417,7 @@ export type FlowTransform = { zoom: number; }; -export type TranslateExtent = [[number, number], [number, number]]; -export type NodeExtent = TranslateExtent; +export type CoordinateExtent = [[number, number], [number, number]]; export type KeyCode = string | Array; @@ -486,8 +486,8 @@ export interface ReactFlowState { d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined; minZoom: number; maxZoom: number; - translateExtent: TranslateExtent; - nodeExtent: NodeExtent; + translateExtent: CoordinateExtent; + nodeExtent: CoordinateExtent; nodesSelectionActive: boolean; selectionActive: boolean; @@ -527,8 +527,8 @@ export interface ReactFlowState { initD3Zoom: (payload: InitD3ZoomPayload) => void; setMinZoom: (minZoom: number) => void; setMaxZoom: (maxZoom: number) => void; - setTranslateExtent: (translateExtent: TranslateExtent) => void; - setNodeExtent: (nodeExtent: NodeExtent) => void; + setTranslateExtent: (translateExtent: CoordinateExtent) => void; + setNodeExtent: (nodeExtent: CoordinateExtent) => void; setOnConnect: (onConnectFunction: OnConnectFunc) => void; setOnConnectStart: (onConnectFunction: OnConnectStartFunc) => void; setOnConnectStop: (onConnectFunction: OnConnectStopFunc) => void; diff --git a/src/utils/index.ts b/src/utils/index.ts index ec44298a..084f5beb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,4 @@ -import { Dimensions, XYPosition, NodeExtent, Box, Rect } from '../types'; +import { Dimensions, XYPosition, CoordinateExtent, Box, Rect } from '../types'; export const getDimensions = (node: HTMLDivElement): Dimensions => ({ width: node.offsetWidth, @@ -7,7 +7,7 @@ export const getDimensions = (node: HTMLDivElement): Dimensions => ({ export const clamp = (val: number, min: number = 0, max: number = 1): number => Math.min(Math.max(val, min), max); -export const clampPosition = (position: XYPosition, extent: NodeExtent) => ({ +export const clampPosition = (position: XYPosition, extent: CoordinateExtent) => ({ x: clamp(position.x, extent[0][0], extent[1][0]), y: clamp(position.y, extent[0][1], extent[1][1]), }); From 1425a4363b4cd9cfa2c6f4a1fe19986f8f6c9627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Fri, 5 Nov 2021 17:42:18 +0100 Subject: [PATCH 052/263] fix(nested): add nested nodes example, always show nodes on top while dragging or selected --- example/src/NestedNodes/index.tsx | 186 ++++++++++++++++++++++++++++++ example/src/index.tsx | 7 +- src/store/utils.ts | 16 +-- 3 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 example/src/NestedNodes/index.tsx diff --git a/example/src/NestedNodes/index.tsx b/example/src/NestedNodes/index.tsx new file mode 100644 index 00000000..232578e0 --- /dev/null +++ b/example/src/NestedNodes/index.tsx @@ -0,0 +1,186 @@ +import { useState, MouseEvent, useCallback } from 'react'; + +import ReactFlow, { + addEdge, + Background, + applyNodeChanges, + applyEdgeChanges, + MiniMap, + Controls, + Node, + Edge, + NodeChange, + EdgeChange, + OnLoadParams, + Connection, +} from 'react-flow-renderer'; + +const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); +const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); +const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge); + +const initialNodes: Node[] = [ + { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, + { + id: '2', + data: { label: 'Node 2' }, + position: { x: 100, y: 100 }, + className: 'light', + style: { backgroundColor: 'rgba(255, 0, 0, 0.8)', width: 200, height: 200 }, + }, + { + id: '2a', + data: { label: 'Node 2a' }, + position: { x: 10, y: 50 }, + parentNode: '2', + }, + { id: '3', data: { label: 'Node 3' }, position: { x: 320, y: 100 }, className: 'light' }, + { + id: '4', + data: { label: 'Node 4' }, + position: { x: 320, y: 200 }, + className: 'light', + style: { backgroundColor: 'rgba(255, 0, 0, 0.7)', width: 300, height: 300 }, + }, + { + id: '4a', + data: { label: 'Node 4a' }, + position: { x: 15, y: 65 }, + className: 'light', + parentNode: '4', + extent: 'parent', + }, + { + id: '4b', + data: { label: 'Node 4b' }, + position: { x: 15, y: 120 }, + className: 'light', + style: { backgroundColor: 'rgba(255, 0, 255, 0.7)', height: 150, width: 270 }, + parentNode: '4', + }, + { + id: '4b1', + data: { label: 'Node 4b1' }, + position: { x: 20, y: 40 }, + className: 'light', + parentNode: '4b', + }, + { + id: '4b2', + data: { label: 'Node 4b2' }, + position: { x: 100, y: 100 }, + className: 'light', + parentNode: '4b', + }, +]; + +const initialEdges: Edge[] = [ + { id: 'e1-2', source: '1', target: '2', animated: true }, + { id: 'e1-3', source: '1', target: '3' }, + { id: 'e2a-4a', source: '2a', target: '4a' }, + { id: 'e3-4', source: '3', target: '4' }, + { id: 'e3-4b', source: '3', target: '4b' }, + { id: 'e4a-4b1', source: '4a', target: '4b1' }, + { id: 'e4a-4b2', source: '4a', target: '4b2' }, + { 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 onConnect = useCallback((params: Edge | Connection) => { + setEdges((eds) => { + return addEdge(params, eds); + }); + }, []); + const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []); + + const updatePos = () => { + setNodes((nds) => { + return nds.map((n) => { + n.position = { + x: Math.random() * 400, + y: Math.random() * 400, + }; + + return n; + }); + }); + }; + + const logToObject = () => console.log(rfInstance?.toObject()); + const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 }); + + const toggleClassnames = () => { + setNodes((nds) => { + return nds.map((n) => { + n.className = n.className === 'light' ? 'dark' : 'light'; + return n; + }); + }); + }; + + const toggleChildNodes = () => { + setNodes((nds) => { + return nds.map((n) => { + n.isHidden = !!n.parentNode && !n.isHidden; + return n; + }); + }); + }; + + const onNodesChange = useCallback((changes: NodeChange[]) => { + setNodes((ns) => applyNodeChanges(changes, ns)); + }, []); + + const onEdgesChange = useCallback((changes: EdgeChange[]) => { + setEdges((es) => applyEdgeChanges(changes, es)); + }, []); + + return ( + + + + + +
+ + + + + +
+
+ ); +}; + +export default NestedFlow; diff --git a/example/src/index.tsx b/example/src/index.tsx index 1f44867f..b23fc09c 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -8,12 +8,13 @@ import Stress from './Stress'; import CustomNode from './CustomNode'; import FloatingEdges from './FloatingEdges'; import Layouting from './Layouting'; +import NestedNodes from './NestedNodes'; import './index.css'; const routes = [ { - path: '/basic', + path: '/', component: Basic, }, { @@ -36,6 +37,10 @@ const routes = [ path: '/layouting', component: Layouting, }, + { + path: '/nested-nodes', + component: NestedNodes, + }, ]; const Header = withRouter(({ history, location }) => { diff --git a/src/store/utils.ts b/src/store/utils.ts index 928adf45..7ab8c1a6 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -38,7 +38,7 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) height: node.height || null, position: node.position, positionAbsolute: node.position, - treeLevel: node.zIndex || 0, + treeLevel: node.isDragging || node.isSelected ? 1000 : node.zIndex || 0, }; if (node.parentNode) { internals.parentNode = node.parentNode; @@ -51,9 +51,11 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) const updatedInternals: NodeInternalsItem = nextNodeInternals.get(node.id)!; if (node.parentNode) { + const parentNodeInternal = nextNodeInternals.get(node.parentNode); + const positionAbsoluteAndTreeLevel = getAbsolutePosAndTreeLevel(node, nextNodeInternals, { ...node.position, - treeLevel: node.zIndex || 0, + treeLevel: node.zIndex || updatedInternals?.treeLevel || parentNodeInternal?.treeLevel || 0, }); const { treeLevel, x, y } = positionAbsoluteAndTreeLevel; @@ -66,11 +68,11 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) updatedInternals.treeLevel = treeLevel; } - if ((node.isDragging || node.isSelected) && !parentNodes[node.id]) { - nextNodeInternals.set(node.id, { ...updatedInternals, treeLevel: 1000 }); - } else { - nextNodeInternals.set(node.id, { ...updatedInternals, treeLevel: updatedInternals?.treeLevel || 0 }); - } + // if (node.isDragging || node.isSelected) { + // nextNodeInternals.set(node.id, { ...updatedInternals, treeLevel: 1000 }); + // } else { + // nextNodeInternals.set(node.id, { ...updatedInternals, treeLevel: updatedInternals?.treeLevel || 0 }); + // } }); return nextNodeInternals; From 9057d3ab75316334e02a208093e7eeda67bc1f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Fri, 5 Nov 2021 17:44:35 +0100 Subject: [PATCH 053/263] chore(release): bump next version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7dd454ba..eb33b332 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.12", + "version": "10.0.0-next.13", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "10.0.0-next.12", + "version": "10.0.0-next.13", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/package.json b/package.json index f5608d80..fbafd4e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.12", + "version": "10.0.0-next.13", "engines": { "node": ">=12" }, From b0bfefeef49b4989f1dd1fbd04383b06f98f058e Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 8 Nov 2021 17:55:02 +0100 Subject: [PATCH 054/263] chore(example): add debug node --- example/src/Basic/DebugNode.tsx | 27 +++++++++++++ example/src/Basic/index.tsx | 63 ++++++++++++++++++++++--------- src/components/Nodes/wrapNode.tsx | 1 + src/store/utils.ts | 8 +--- src/types/index.ts | 2 + 5 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 example/src/Basic/DebugNode.tsx diff --git a/example/src/Basic/DebugNode.tsx b/example/src/Basic/DebugNode.tsx new file mode 100644 index 00000000..f7cc4b36 --- /dev/null +++ b/example/src/Basic/DebugNode.tsx @@ -0,0 +1,27 @@ +import { memo, FC, CSSProperties } from 'react'; + +import { Handle, NodeProps, Position } from 'react-flow-renderer'; + +const infoStyle: CSSProperties = { fontSize: 11 }; +const idStyle: CSSProperties = { + fontSize: 10, + color: '#888899', + position: 'absolute', + top: 2, + left: 2, +}; + +const ColorSelectorNode: FC = ({ zIndex, xPos, yPos, id }) => { + return ( + <> + +
{id}
+
+ x:{Math.round(xPos || 0)} y:{Math.round(yPos || 0)} z:{zIndex} +
+ + + ); +}; + +export default memo(ColorSelectorNode); diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index 75069deb..1416b115 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -14,6 +14,7 @@ import ReactFlow, { OnLoadParams, Connection, } from 'react-flow-renderer'; +import DebugNode from './DebugNode'; const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); @@ -21,15 +22,6 @@ const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge); const initialNodes: Node[] = [ { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, - { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, - { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, - { - id: '4', - data: { label: 'Node 4' }, - position: { x: 100, y: 200 }, - className: 'light', - style: { backgroundColor: 'rgba(255, 0, 0, 0.8)', width: 600, height: 300 }, - }, { id: '4a', data: { label: 'Node 4a' }, @@ -38,14 +30,7 @@ const initialNodes: Node[] = [ parentNode: '4', extent: 'parent', }, - { - id: '4b', - data: { label: 'Node 4b' }, - position: { x: 150, y: 50 }, - className: 'light', - style: { backgroundColor: 'rgba(255, 0, 255, 0.8)', height: 300, width: 300 }, - parentNode: '4', - }, + { id: '4b1', data: { label: 'Node 4b1' }, @@ -60,6 +45,44 @@ const initialNodes: Node[] = [ className: 'light', parentNode: '4b', }, + { + id: '4b', + data: { label: 'Node 4b' }, + position: { x: 150, y: 50 }, + className: 'light', + style: { backgroundColor: 'rgba(50, 50, 255, 0.5)', height: 200, width: 300 }, + parentNode: '4', + }, + { + id: '4', + data: { label: 'Node 4' }, + position: { x: 100, y: 200 }, + className: 'light', + style: { backgroundColor: 'rgba(255,50, 50, 0.5)', width: 500, height: 300 }, + }, + { + id: '5a', + data: { label: 'Node 5a' }, + position: { x: 25, y: 50 }, + className: 'light', + parentNode: '5', + }, + { + id: '5b', + data: { label: 'Node 5b' }, + position: { x: 225, y: 50 }, + className: 'light', + parentNode: '5', + }, + { + id: '5', + data: { label: 'Node 5' }, + position: { x: 650, y: 250 }, + className: 'light', + style: { backgroundColor: 'rgba(20 ,200, 255, 1.5)', width: 400, height: 150 }, + }, + { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, + { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, ]; const initialEdges: Edge[] = [ @@ -70,8 +93,13 @@ const initialEdges: Edge[] = [ { id: 'e4a-4b1', source: '4a', target: '4b1' }, { id: 'e4a-4b2', source: '4a', target: '4b2' }, { id: 'e4b1-4b2', source: '4b1', target: '4b2' }, + { id: '3-5', source: '3', target: '5' }, ]; +const nodeTypes = { + default: DebugNode, +}; + const BasicFlow = () => { const [rfInstance, setRfInstance] = useState(null); const [nodes, setNodes] = useState(initialNodes); @@ -142,6 +170,7 @@ const BasicFlow = () => { minZoom={0.2} maxZoom={4} onlyRenderVisibleElements={false} + nodeTypes={nodeTypes} > diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index b1cd90fe..e87f388e 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -270,6 +270,7 @@ export default (NodeComponent: ComponentType) => { targetPosition={targetPosition} isDragging={isDragging} dragHandle={dragHandle} + zIndex={zIndex} />
diff --git a/src/store/utils.ts b/src/store/utils.ts index 7ab8c1a6..1a7d4d06 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -55,7 +55,7 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) const positionAbsoluteAndTreeLevel = getAbsolutePosAndTreeLevel(node, nextNodeInternals, { ...node.position, - treeLevel: node.zIndex || updatedInternals?.treeLevel || parentNodeInternal?.treeLevel || 0, + treeLevel: updatedInternals.treeLevel || parentNodeInternal?.treeLevel || 0, }); const { treeLevel, x, y } = positionAbsoluteAndTreeLevel; @@ -67,12 +67,6 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) }; updatedInternals.treeLevel = treeLevel; } - - // if (node.isDragging || node.isSelected) { - // nextNodeInternals.set(node.id, { ...updatedInternals, treeLevel: 1000 }); - // } else { - // nextNodeInternals.set(node.id, { ...updatedInternals, treeLevel: updatedInternals?.treeLevel || 0 }); - // } }); return nextNodeInternals; diff --git a/src/types/index.ts b/src/types/index.ts index f8403993..2e794286 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -239,6 +239,7 @@ export interface NodeProps { sourcePosition?: Position; isDragging?: boolean; dragHandle?: string; + zIndex?: number; } export interface NodeComponentProps { @@ -264,6 +265,7 @@ export interface NodeComponentProps { style?: CSSProperties; isDragging?: boolean; dragHandle?: string; + zIndex?: number; } export interface WrapNodeProps { From df25cb32f092cf6e49e90eed9f27fa1ce3e17ef3 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 8 Nov 2021 20:55:11 +0100 Subject: [PATCH 055/263] refactor(nodes): stacking order --- src/store/utils.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/store/utils.ts b/src/store/utils.ts index 1a7d4d06..923620af 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -2,11 +2,11 @@ import { ElementId, Node, NodeInternals, NodeInternalsItem, XYPosition } from '. type XYPosAndTreeLevel = XYPosition & { treeLevel: number }; -function addPositions(a: XYPosAndTreeLevel, b: XYPosition): XYPosAndTreeLevel { +function addPositions(a: XYPosAndTreeLevel, b: XYPosAndTreeLevel): XYPosAndTreeLevel { return { x: (a.x ?? 0) + (b.x ?? 0), y: (a.y ?? 0) + (b.y ?? 0), - treeLevel: a.treeLevel + 1, + treeLevel: a.treeLevel + (b.treeLevel || 1), }; } @@ -24,7 +24,11 @@ function getAbsolutePosAndTreeLevel( return getAbsolutePosAndTreeLevel( parentNode, nodeInternals, - addPositions(result, parentNode.position || { x: 0, y: 0 }) + addPositions(result, { + x: parentNode.position?.x || 0, + y: parentNode.position?.y || 0, + treeLevel: parentNode.treeLevel || 0, + }) ); } export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals): NodeInternals { @@ -50,17 +54,21 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) nodes.forEach((node) => { const updatedInternals: NodeInternalsItem = nextNodeInternals.get(node.id)!; - if (node.parentNode) { - const parentNodeInternal = nextNodeInternals.get(node.parentNode); + if (node.parentNode || parentNodes[node.id]) { + if (node.parentNode) { + const parentNodeInternal = nextNodeInternals.get(node.parentNode); + if (parentNodeInternal) { + parentNodeInternal.isParentNode = true; + } + } const positionAbsoluteAndTreeLevel = getAbsolutePosAndTreeLevel(node, nextNodeInternals, { ...node.position, - treeLevel: updatedInternals.treeLevel || parentNodeInternal?.treeLevel || 0, + treeLevel: updatedInternals.treeLevel || 1, }); const { treeLevel, x, y } = positionAbsoluteAndTreeLevel; - nextNodeInternals.set(node.parentNode!, { ...nextNodeInternals.get(node.parentNode!), isParentNode: true }); updatedInternals.positionAbsolute = { x, y, From 050bb3130f7f05e49173653b56615f670d842a0e Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 9 Nov 2021 12:11:27 +0100 Subject: [PATCH 056/263] refactor(z-level): implement new stacking logic --- src/components/Nodes/wrapNode.tsx | 6 +-- src/container/NodeRenderer/index.tsx | 4 +- src/hooks/useVisibleEdges.ts | 17 +++--- src/store/utils.ts | 77 +++++++++++++++------------- src/types/index.ts | 7 +-- 5 files changed, 58 insertions(+), 53 deletions(-) diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index e87f388e..da2f507e 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -49,7 +49,7 @@ export default (NodeComponent: ComponentType) => { resizeObserver, dragHandle, zIndex, - isParentNode, + isParent, }: WrapNodeProps) => { const { addSelectedElements, @@ -85,7 +85,7 @@ export default (NodeComponent: ComponentType) => { onMouseEnter, onMouseMove, onMouseLeave, - isParentNode, + isParent, zIndex, ] ); @@ -228,7 +228,7 @@ export default (NodeComponent: ComponentType) => { { selected: isSelected, selectable: isSelectable, - parent: isParentNode, + parent: isParent, }, ]); diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index a34fdc46..7876c1ec 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -106,8 +106,8 @@ const NodeRenderer = (props: NodeRendererProps) => { isConnectable={isConnectable} resizeObserver={resizeObserver} dragHandle={node.dragHandle} - zIndex={internals?.treeLevel || 0} - isParentNode={!!internals?.isParentNode} + zIndex={internals?.z || 0} + isParent={!!internals?.isParent} /> ); })} diff --git a/src/hooks/useVisibleEdges.ts b/src/hooks/useVisibleEdges.ts index 33befa1e..837150f1 100644 --- a/src/hooks/useVisibleEdges.ts +++ b/src/hooks/useVisibleEdges.ts @@ -4,21 +4,18 @@ import { useStore } from '../store'; import { isEdgeVisible } from '../container/EdgeRenderer/utils'; import { ReactFlowState, NodeInternals, Edge } from '../types'; -function groupEdgesByTreeLevel(edges: Edge[], nodeInternals: NodeInternals) { +function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals) { let maxLevel = -1; const levelLookup = edges.reduce>((tree, edge) => { - const treeLevel = Math.max( - nodeInternals.get(edge.source)?.treeLevel || 0, - nodeInternals.get(edge.target)?.treeLevel || 0 - ); - if (tree[treeLevel]) { - tree[treeLevel].push(edge); + const z = Math.max(nodeInternals.get(edge.source)?.z || 0, nodeInternals.get(edge.target)?.z || 0); + if (tree[z]) { + tree[z].push(edge); } else { - tree[treeLevel] = [edge]; + tree[z] = [edge]; } - maxLevel = treeLevel > maxLevel ? treeLevel : maxLevel; + maxLevel = z > maxLevel ? z : maxLevel; return tree; }, {}); @@ -69,7 +66,7 @@ function useVisibleEdges(onlyRenderVisible: boolean, nodeInternals: NodeInternal ) ); - return groupEdgesByTreeLevel(edges, nodeInternals); + return groupEdgesByZLevel(edges, nodeInternals); } export default useVisibleEdges; diff --git a/src/store/utils.ts b/src/store/utils.ts index 923620af..9333a004 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -1,48 +1,50 @@ import { ElementId, Node, NodeInternals, NodeInternalsItem, XYPosition } from '../types'; -type XYPosAndTreeLevel = XYPosition & { treeLevel: number }; +type XYZPosition = XYPosition & { z: number }; +type ParentNodes = Record; -function addPositions(a: XYPosAndTreeLevel, b: XYPosAndTreeLevel): XYPosAndTreeLevel { - return { - x: (a.x ?? 0) + (b.x ?? 0), - y: (a.y ?? 0) + (b.y ?? 0), - treeLevel: a.treeLevel + (b.treeLevel || 1), - }; -} - -function getAbsolutePosAndTreeLevel( +function calculateXYZPosition( node: NodeInternalsItem, nodeInternals: NodeInternals, - result: XYPosAndTreeLevel -): XYPosAndTreeLevel { - const parentNode = node.parentNode ? nodeInternals.get(node.parentNode) : false; - - if (!parentNode) { + parentNodes: ParentNodes, + result: XYZPosition +): XYZPosition { + if (!node.parentNode) { return result; } + const parentNode = nodeInternals.get(node.parentNode)!; - return getAbsolutePosAndTreeLevel( - parentNode, - nodeInternals, - addPositions(result, { - x: parentNode.position?.x || 0, - y: parentNode.position?.y || 0, - treeLevel: parentNode.treeLevel || 0, - }) - ); + // +1 for each recursion level + let zAddition = 1; + + // +2 if it's a parent node, so that groups/parents are always on top + if (parentNodes[node.parentNode!]) { + zAddition = 2; + } + + if (parentNode.z) { + zAddition += parentNode.z; + } + + return calculateXYZPosition(parentNode, nodeInternals, parentNodes, { + x: (result.x ?? 0) + (parentNode.position?.x ?? 0), + y: (result.y ?? 0) + (parentNode.position?.y ?? 0), + z: (result.z ?? 0) + zAddition, + }); } export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals): NodeInternals { const nextNodeInternals = new Map(); - const parentNodes: Record = {}; + const parentNodes: ParentNodes = {}; nodes.forEach((node) => { const internals: NodeInternalsItem = { ...nodeInternals.get(node.id), + id: node.id, width: node.width || null, height: node.height || null, position: node.position, positionAbsolute: node.position, - treeLevel: node.isDragging || node.isSelected ? 1000 : node.zIndex || 0, + z: node.isDragging || node.isSelected ? 1000 : node.zIndex || 0, }; if (node.parentNode) { internals.parentNode = node.parentNode; @@ -55,25 +57,30 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) const updatedInternals: NodeInternalsItem = nextNodeInternals.get(node.id)!; if (node.parentNode || parentNodes[node.id]) { - if (node.parentNode) { - const parentNodeInternal = nextNodeInternals.get(node.parentNode); - if (parentNodeInternal) { - parentNodeInternal.isParentNode = true; + let startingZ = updatedInternals.z; + + if (!startingZ) { + if (parentNodes[node.id] && node.parentNode) { + startingZ = 2; + } else if (node.parentNode) { + startingZ = 1; } } - const positionAbsoluteAndTreeLevel = getAbsolutePosAndTreeLevel(node, nextNodeInternals, { + const { x, y, z } = calculateXYZPosition(node, nextNodeInternals, parentNodes, { ...node.position, - treeLevel: updatedInternals.treeLevel || 1, + z: startingZ as number, }); - const { treeLevel, x, y } = positionAbsoluteAndTreeLevel; - updatedInternals.positionAbsolute = { x, y, }; - updatedInternals.treeLevel = treeLevel; + updatedInternals.z = z; + + if (parentNodes[node.id]) { + updatedInternals.isParent = true; + } } }); diff --git a/src/types/index.ts b/src/types/index.ts index 2e794286..5b99da4d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -303,7 +303,7 @@ export interface WrapNodeProps { resizeObserver: ResizeObserver | null; dragHandle?: string; zIndex: number; - isParentNode: boolean; + isParent: boolean; } export type FitViewParams = { @@ -460,14 +460,15 @@ export type OnNodesChange = (nodes: NodeChange[]) => void; export type OnEdgesChange = (nodes: EdgeChange[]) => void; export type NodeInternalsItem = { + id?: string; width?: number | null; height?: number | null; parentNode?: ElementId; position?: XYPosition; positionAbsolute?: XYPosition; handleBounds?: NodeHandleBounds; - treeLevel?: number; - isParentNode?: boolean; + z?: number; + isParent?: boolean; }; export type NodeInternals = Map; From a79e953ab3b4213d33c6b7898493fe7ed9823e5b Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 9 Nov 2021 12:35:21 +0100 Subject: [PATCH 057/263] refactor(boolean-props): omit is prefix --- example/src/Stress/index.tsx | 9 +---- src/components/Edges/wrapEdge.tsx | 6 +-- src/components/Nodes/wrapNode.tsx | 43 +++++++++++----------- src/components/NodesSelection/index.tsx | 6 +-- src/components/SelectionListener/index.tsx | 4 +- src/container/EdgeRenderer/index.tsx | 2 +- src/container/NodeRenderer/index.tsx | 4 +- src/hooks/useGlobalKeyHandler.ts | 4 +- src/store/index.ts | 18 ++++----- src/store/utils.ts | 6 +-- src/types/index.ts | 28 +++++++------- src/utils/changes.ts | 6 +-- src/utils/graph.ts | 4 +- 13 files changed, 66 insertions(+), 74 deletions(-) diff --git a/example/src/Stress/index.tsx b/example/src/Stress/index.tsx index e562ed50..e8bc9323 100644 --- a/example/src/Stress/index.tsx +++ b/example/src/Stress/index.tsx @@ -55,14 +55,7 @@ const StressFlow = () => { }, []); return ( - + diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx index 0e9555e1..eadf55f8 100644 --- a/src/components/Edges/wrapEdge.tsx +++ b/src/components/Edges/wrapEdge.tsx @@ -24,7 +24,7 @@ export default (EdgeComponent: ComponentType) => { data, onClick, onEdgeDoubleClick, - isSelected, + selected, animated, label, labelStyle, @@ -70,7 +70,7 @@ export default (EdgeComponent: ComponentType) => { 'react-flow__edge', `react-flow__edge-${type}`, className, - { selected: isSelected, animated, inactive, updating }, + { selected, animated, inactive, updating }, ]); const edgeElement = useMemo(() => { @@ -222,7 +222,7 @@ export default (EdgeComponent: ComponentType) => { id={id} source={source} target={target} - isSelected={isSelected} + selected={selected} animated={animated} label={label} labelStyle={labelStyle} diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index da2f507e..f700f106 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -23,7 +23,7 @@ export default (NodeComponent: ComponentType) => { scale, xPos, yPos, - isSelected, + selected, onClick, onMouseEnter, onMouseMove, @@ -45,7 +45,7 @@ export default (NodeComponent: ComponentType) => { isInitialized, snapToGrid, snapGrid, - isDragging, + dragging, resizeObserver, dragHandle, zIndex, @@ -74,7 +74,6 @@ export default (NodeComponent: ComponentType) => { ...style, }), [ - isSelected, xPos, yPos, isSelectable, @@ -91,28 +90,28 @@ export default (NodeComponent: ComponentType) => { ); const onMouseEnterHandler = useMemo(() => { - if (!onMouseEnter || isDragging) { + if (!onMouseEnter || dragging) { return; } return (event: MouseEvent) => onMouseEnter(event, node); - }, [onMouseEnter, isDragging, node]); + }, [onMouseEnter, dragging, node]); const onMouseMoveHandler = useMemo(() => { - if (!onMouseMove || isDragging) { + if (!onMouseMove || dragging) { return; } return (event: MouseEvent) => onMouseMove(event, node); - }, [onMouseMove, isDragging, node]); + }, [onMouseMove, dragging, node]); const onMouseLeaveHandler = useMemo(() => { - if (!onMouseLeave || isDragging) { + if (!onMouseLeave || dragging) { return; } return (event: MouseEvent) => onMouseLeave(event, node); - }, [onMouseLeave, isDragging, node]); + }, [onMouseLeave, dragging, node]); const onContextMenuHandler = useMemo(() => { if (!onContextMenu) { @@ -128,7 +127,7 @@ export default (NodeComponent: ComponentType) => { if (isSelectable) { unsetNodesSelection(); - if (!isSelected) { + if (!selected) { addSelectedElements([node]); } } @@ -136,7 +135,7 @@ export default (NodeComponent: ComponentType) => { onClick?.(event, node); } }, - [isSelectable, isSelected, isDraggable, onClick, node] + [isSelectable, selected, isDraggable, onClick, node] ); const onDragStart = useCallback( @@ -146,15 +145,15 @@ export default (NodeComponent: ComponentType) => { if (selectNodesOnDrag && isSelectable) { unsetNodesSelection(); - if (!isSelected) { + if (!selected) { addSelectedElements([node]); } - } else if (!selectNodesOnDrag && !isSelected && isSelectable) { + } else if (!selectNodesOnDrag && !selected && isSelectable) { unselectNodesAndEdges(); unsetNodesSelection(); } }, - [node, isSelected, selectNodesOnDrag, isSelectable, onNodeDragStart] + [node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart] ); const onDrag = useCallback( @@ -166,7 +165,7 @@ export default (NodeComponent: ComponentType) => { onNodeDrag(event as MouseEvent, node); } - updateNodePosition({ id, isDragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } }); + updateNodePosition({ id, dragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } }); }, [id, node, onNodeDrag] ); @@ -175,8 +174,8 @@ export default (NodeComponent: ComponentType) => { (event: DraggableEvent) => { // onDragStop also gets called when user just clicks on a node. // Because of that we set dragging to true inside the onDrag handler and handle the click here - if (!isDragging) { - if (isSelectable && !selectNodesOnDrag && !isSelected) { + if (!dragging) { + if (isSelectable && !selectNodesOnDrag && !selected) { addSelectedElements([node]); } @@ -187,12 +186,12 @@ export default (NodeComponent: ComponentType) => { updateNodePosition({ id: node.id, - isDragging: false, + dragging: false, }); onNodeDragStop?.(event as MouseEvent, node); }, - [node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, isSelected] + [node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, dragging, selected] ); const onNodeDoubleClickHandler = useCallback( @@ -226,7 +225,7 @@ export default (NodeComponent: ComponentType) => { `react-flow__node-${type}`, className, { - selected: isSelected, + selected, selectable: isSelectable, parent: isParent, }, @@ -264,11 +263,11 @@ export default (NodeComponent: ComponentType) => { type={type} xPos={xPos} yPos={yPos} - isSelected={isSelected} + selected={selected} isConnectable={isConnectable} sourcePosition={sourcePosition} targetPosition={targetPosition} - isDragging={isDragging} + dragging={dragging} dragHandle={dragHandle} zIndex={zIndex} /> diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index e42bbc1f..bf46a6a2 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -20,7 +20,7 @@ const selector = (s: ReactFlowState) => ({ transform: s.transform, selectedNodesBbox: s.selectedNodesBbox, selectionActive: s.selectionActive, - selectedNodes: s.nodes.filter((n) => n.isSelected), + selectedNodes: s.nodes.filter((n) => n.selected), snapToGrid: s.snapToGrid, snapGrid: s.snapGrid, updateNodePosition: s.updateNodePosition, @@ -74,7 +74,7 @@ export default ({ x: data.deltaX, y: data.deltaY, }, - isDragging: true, + dragging: true, }); }, [onSelectionDrag, selectedNodes, updateNodePosition] @@ -83,7 +83,7 @@ export default ({ const onStop = useCallback( (event: MouseEvent) => { updateNodePosition({ - isDragging: false, + dragging: false, }); onSelectionDragStop?.(event, selectedNodes); diff --git a/src/components/SelectionListener/index.tsx b/src/components/SelectionListener/index.tsx index a4a92441..d2cfd097 100644 --- a/src/components/SelectionListener/index.tsx +++ b/src/components/SelectionListener/index.tsx @@ -9,8 +9,8 @@ interface SelectionListenerProps { } const selectedElementsSelector = (s: ReactFlowState) => ({ - selectedNodes: s.nodes.filter((n) => n.isSelected), - selectedEdges: s.edges.filter((e) => e.isSelected), + selectedNodes: s.nodes.filter((n) => n.selected), + selectedEdges: s.edges.filter((e) => e.selected), }); // This is just a helper component for calling the onSelectionChange listener. diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 738aa8e9..7d6329fc 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -167,7 +167,7 @@ const Edge = memo( type={edgeType} data={edge.data} onClick={onEdgeClick} - isSelected={!!edge.isSelected} + selected={!!edge.selected} animated={edge.animated} label={edge.label} labelStyle={edge.labelStyle} diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 7876c1ec..d02dc5d6 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -85,7 +85,7 @@ const NodeRenderer = (props: NodeRendererProps) => { isHidden={node.isHidden} xPos={internals?.positionAbsolute?.x || 0} yPos={internals?.positionAbsolute?.y || 0} - isDragging={node.isDragging} + dragging={node.dragging} isInitialized={isInitialized} snapGrid={snapGrid} snapToGrid={snapToGrid} @@ -100,7 +100,7 @@ const NodeRenderer = (props: NodeRendererProps) => { onNodeDrag={props.onNodeDrag} onNodeDragStop={props.onNodeDragStop} scale={scale} - isSelected={!!node.isSelected} + selected={!!node.selected} isDraggable={isDraggable} isSelectable={isSelectable} isConnectable={isConnectable} diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index 87b8a360..19743457 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -29,8 +29,8 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { useEffect(() => { const { nodes, edges } = store.getState(); - const selectedNodes = nodes.filter((n) => n.isSelected); - const selectedEdges = edges.filter((e) => e.isSelected); + const selectedNodes = nodes.filter((n) => n.selected); + const selectedEdges = edges.filter((e) => e.selected); if (deleteKeyPressed && (selectedNodes || selectedEdges)) { const connectedEdges = getConnectedEdges(selectedNodes, edges); diff --git a/src/store/index.ts b/src/store/index.ts index 570d3c3e..27764555 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -32,10 +32,10 @@ import { createNodeInternals } from './utils'; const { Provider, useStore, useStoreApi } = createContext(); -const createNodeOrEdgeSelectionChange = (isSelected: boolean) => (item: Node | Edge) => ({ +const createNodeOrEdgeSelectionChange = (selected: boolean) => (item: Node | Edge) => ({ id: item.id, type: 'select', - isSelected, + selected, }); const createStore = () => @@ -141,11 +141,11 @@ const createStore = () => onNodesChange?.(nodesToChange); }, - updateNodePosition: ({ id, diff, isDragging }: NodeDiffUpdate) => { + updateNodePosition: ({ id, diff, dragging }: NodeDiffUpdate) => { const { onNodesChange, nodes, nodeExtent, nodeInternals } = get(); if (onNodesChange) { - const matchingNodes = nodes.filter((n) => !!(n.isSelected || n.id === id)); + const matchingNodes = nodes.filter((n) => !!(n.selected || n.id === id)); if (matchingNodes?.length) { onNodesChange( @@ -153,7 +153,7 @@ const createStore = () => const change: NodeDimensionChange = { id: node.id, type: 'dimensions', - isDragging: !!isDragging, + dragging: !!dragging, }; if (diff) { @@ -231,7 +231,7 @@ const createStore = () => }, unsetUserSelection: () => { const { userSelectionRect, nodes } = get(); - const selectedNodes = nodes.filter((node) => node.isSelected); + const selectedNodes = nodes.filter((node) => node.selected); const stateUpdate = { selectionActive: false, @@ -281,7 +281,7 @@ const createStore = () => const { nodes, edges, onNodesChange, onEdgesChange } = get(); const nodesToUnselect = nodes.map((n) => { - n.isSelected = false; + n.selected = false; return createNodeOrEdgeSelectionChange(false)(n); }) as NodeChange[]; const edgesToUnselect = edges.map(createNodeOrEdgeSelectionChange(false)) as EdgeChange[]; @@ -322,8 +322,8 @@ const createStore = () => resetSelectedElements: () => { const { nodes, edges, onNodesChange, onEdgesChange } = get(); - const nodesToUnselect = nodes.filter((e) => e.isSelected).map(createNodeOrEdgeSelectionChange(false)); - const edgesToUnselect = edges.filter((e) => e.isSelected).map(createNodeOrEdgeSelectionChange(false)); + const nodesToUnselect = nodes.filter((e) => e.selected).map(createNodeOrEdgeSelectionChange(false)); + const edgesToUnselect = edges.filter((e) => e.selected).map(createNodeOrEdgeSelectionChange(false)); if (nodesToUnselect.length) { onNodesChange?.(nodesToUnselect as NodeChange[]); diff --git a/src/store/utils.ts b/src/store/utils.ts index 9333a004..42704f21 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -44,7 +44,7 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) height: node.height || null, position: node.position, positionAbsolute: node.position, - z: node.isDragging || node.isSelected ? 1000 : node.zIndex || 0, + z: node.dragging || node.selected ? 1000 : node.zIndex || 0, }; if (node.parentNode) { internals.parentNode = node.parentNode; @@ -57,7 +57,7 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) const updatedInternals: NodeInternalsItem = nextNodeInternals.get(node.id)!; if (node.parentNode || parentNodes[node.id]) { - let startingZ = updatedInternals.z; + let startingZ = updatedInternals.z || 0; if (!startingZ) { if (parentNodes[node.id] && node.parentNode) { @@ -69,7 +69,7 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) const { x, y, z } = calculateXYZPosition(node, nextNodeInternals, parentNodes, { ...node.position, - z: startingZ as number, + z: startingZ, }); updatedInternals.positionAbsolute = { diff --git a/src/types/index.ts b/src/types/index.ts index 5b99da4d..d37949b9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -15,13 +15,13 @@ export type NodeDimensionChange = { dimensions?: Dimensions; position?: XYPosition; handleBounds?: NodeHandleBounds; - isDragging?: boolean; + dragging?: boolean; }; export type NodeSelectionChange = { id: string; type: 'select'; - isSelected: boolean; + selected: boolean; }; export type NodeRemoveChange = { id: string; @@ -74,8 +74,8 @@ export interface Node { targetPosition?: Position; sourcePosition?: Position; isHidden?: boolean; - isSelected?: boolean; - isDragging?: boolean; + selected?: boolean; + dragging?: boolean; draggable?: boolean; selectable?: boolean; connectable?: boolean; @@ -124,7 +124,7 @@ export interface Edge { className?: string; sourceNode?: Node; targetNode?: Node; - isSelected?: boolean; + selected?: boolean; markerStart?: EdgeMarkerType; markerEnd?: EdgeMarkerType; } @@ -153,7 +153,7 @@ export interface WrapEdgeProps { data?: T; onClick?: (event: React.MouseEvent, edge: Edge) => void; onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void; - isSelected: boolean; + selected: boolean; animated?: boolean; label?: string | ReactNode; labelStyle?: CSSProperties; @@ -195,7 +195,7 @@ export interface EdgeProps { sourceY: number; targetX: number; targetY: number; - isSelected?: boolean; + selected?: boolean; animated?: boolean; sourcePosition: Position; targetPosition: Position; @@ -231,13 +231,13 @@ export interface NodeProps { id: ElementId; type: string; data: T; - isSelected: boolean; + selected: boolean; isConnectable: boolean; xPos?: number; yPos?: number; targetPosition?: Position; sourcePosition?: Position; - isDragging?: boolean; + dragging?: boolean; dragHandle?: string; zIndex?: number; } @@ -246,7 +246,7 @@ export interface NodeComponentProps { id: ElementId; type: string; data: T; - isSelected?: boolean; + selected?: boolean; isConnectable: boolean; transform?: Transform; xPos?: number; @@ -263,7 +263,7 @@ export interface NodeComponentProps { onNodeDrag?: (node: Node) => void; onNodeDragStop?: (node: Node) => void; style?: CSSProperties; - isDragging?: boolean; + dragging?: boolean; dragHandle?: string; zIndex?: number; } @@ -272,7 +272,7 @@ export interface WrapNodeProps { id: ElementId; type: string; data: T; - isSelected: boolean; + selected: boolean; scale: number; xPos: number; yPos: number; @@ -299,7 +299,7 @@ export interface WrapNodeProps { isInitialized?: boolean; snapToGrid?: boolean; snapGrid?: SnapGrid; - isDragging?: boolean; + dragging?: boolean; resizeObserver: ResizeObserver | null; dragHandle?: string; zIndex: number; @@ -410,7 +410,7 @@ export type NodePosUpdate = { export type NodeDiffUpdate = { id?: ElementId; diff?: XYPosition; - isDragging?: boolean; + dragging?: boolean; }; export type FlowTransform = { diff --git a/src/utils/changes.ts b/src/utils/changes.ts index 89ac2037..64743226 100644 --- a/src/utils/changes.ts +++ b/src/utils/changes.ts @@ -9,7 +9,7 @@ function applyChanges(changes: NodeChange[] | EdgeChange[], elements: any[]): an if (currentChange) { switch (currentChange.type) { case 'select': { - res.push({ ...item, isSelected: currentChange.isSelected }); + res.push({ ...item, selected: currentChange.selected }); return res; } case 'dimensions': { @@ -24,8 +24,8 @@ function applyChanges(changes: NodeChange[] | EdgeChange[], elements: any[]): an updateItem.position = currentChange.position; } - if (typeof currentChange.isDragging !== 'undefined') { - updateItem.isDragging = currentChange.isDragging; + if (typeof currentChange.dragging !== 'undefined') { + updateItem.dragging = currentChange.dragging; } res.push(updateItem); diff --git a/src/utils/graph.ts b/src/utils/graph.ts index d9055596..9ceba91d 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -149,7 +149,7 @@ export const getNodesInside = ( height: rect.height / tScale, }); - return nodes.filter(({ selectable = true, position, width, height, isDragging }) => { + return nodes.filter(({ selectable = true, position, width, height, dragging }) => { if (excludeNonSelectableNodes && !selectable) { return false; } @@ -164,7 +164,7 @@ export const getNodesInside = ( typeof height === 'undefined' || width === null || height === null || - isDragging + dragging ) { // nodes are initialized with width and height = null return true; From 8a2ac1e31e2a6c4afbb3aa08deb1ab2d16cffae4 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 9 Nov 2021 14:52:39 +0100 Subject: [PATCH 058/263] refactor(types): cleanup --- src/additional-components/MiniMap/index.tsx | 2 +- src/components/ConnectionLine/index.tsx | 7 +- src/components/Handle/handler.ts | 27 +- src/components/Handle/index.tsx | 4 +- src/components/Nodes/wrapNode.tsx | 4 +- src/components/StoreUpdater/index.tsx | 16 +- src/container/EdgeRenderer/utils.ts | 4 +- src/container/NodeRenderer/index.tsx | 2 +- src/container/NodeRenderer/utils.ts | 20 +- src/container/ReactFlow/index.tsx | 20 +- src/contexts/NodeIdContext.ts | 4 +- src/hooks/useOnLoadHandler.ts | 4 +- src/hooks/useUpdateNodeInternals.ts | 4 +- src/hooks/useZoomPanHelper.ts | 2 +- src/store/index.ts | 48 +- src/store/utils.ts | 13 +- src/types/changes.ts | 28 + src/types/edges.ts | 158 ++++++ src/types/general.ts | 217 ++++++++ src/types/handles.ts | 18 + src/types/index.ts | 571 +------------------- src/types/nodes.ts | 116 ++++ src/types/utils.ts | 29 + src/utils/graph.ts | 4 +- 24 files changed, 655 insertions(+), 667 deletions(-) create mode 100644 src/types/changes.ts create mode 100644 src/types/edges.ts create mode 100644 src/types/general.ts create mode 100644 src/types/handles.ts create mode 100644 src/types/nodes.ts create mode 100644 src/types/utils.ts diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index 9cc5a0c1..98735e55 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -5,7 +5,7 @@ import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import { getRectOfNodes } from '../../utils/graph'; import { getBoundsofRects } from '../../utils'; -import { Node, Rect, ReactFlowState } from '../../types'; +import { Node, ReactFlowState, Rect } from '../../types'; import MiniMapNode from './MiniMapNode'; type StringFunc = (node: Node) => string; diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index 7a40dd71..8ce8e0f1 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -5,20 +5,19 @@ import { useStore } from '../../store'; import { getBezierPath } from '../Edges/BezierEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { - ElementId, NodeInternalsItem, HandleElement, - Position, ConnectionLineType, ConnectionLineComponent, HandleType, Node, ReactFlowState, + Position, } from '../../types'; interface ConnectionLineProps { - connectionNodeId: ElementId; - connectionHandleId: ElementId | null; + connectionNodeId: string; + connectionHandleId: string | null; connectionHandleType: HandleType; connectionPositionX: number; connectionPositionY: number; diff --git a/src/components/Handle/handler.ts b/src/components/Handle/handler.ts index 03fd37df..2ef44055 100644 --- a/src/components/Handle/handler.ts +++ b/src/components/Handle/handler.ts @@ -3,16 +3,15 @@ import { MouseEvent as ReactMouseEvent } from 'react'; import { getHostForElement } from '../../utils'; import { - ElementId, - XYPosition, - OnConnectFunc, - OnConnectStartFunc, - OnConnectStopFunc, - OnConnectEndFunc, + OnConnect, + OnConnectStart, + OnConnectStop, + OnConnectEnd, ConnectionMode, SetConnectionId, Connection, HandleType, + XYPosition, } from '../../types'; type ValidConnectionFunc = (connection: Connection) => boolean; @@ -32,8 +31,8 @@ function checkElementBelowIsValid( event: MouseEvent, connectionMode: ConnectionMode, isTarget: boolean, - nodeId: ElementId, - handleId: ElementId | null, + nodeId: string, + handleId: string | null, isValidConnection: ValidConnectionFunc, doc: Document | ShadowRoot ) { @@ -91,19 +90,19 @@ function resetRecentHandle(hoveredHandle: Element): void { export function onMouseDown( event: ReactMouseEvent, - handleId: ElementId | null, - nodeId: ElementId, + handleId: string | null, + nodeId: string, setConnectionNodeId: SetSourceIdFunc, setPosition: SetPosition, - onConnect: OnConnectFunc, + onConnect: OnConnect, isTarget: boolean, isValidConnection: ValidConnectionFunc, connectionMode: ConnectionMode, elementEdgeUpdaterType?: HandleType, onEdgeUpdateEnd?: (evt: MouseEvent) => void, - onConnectStart?: OnConnectStartFunc, - onConnectStop?: OnConnectStopFunc, - onConnectEnd?: OnConnectEndFunc + onConnectStart?: OnConnectStart, + onConnectStop?: OnConnectStop, + onConnectEnd?: OnConnectEnd ): void { const reactFlowNode = (event.target as Element).closest('.react-flow'); // when react-flow is used inside a shadow root we can't use document diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx index 2f594b27..e3b0111c 100644 --- a/src/components/Handle/index.tsx +++ b/src/components/Handle/index.tsx @@ -4,7 +4,7 @@ import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import NodeIdContext from '../../contexts/NodeIdContext'; -import { HandleProps, Connection, ElementId, Position, ReactFlowState } from '../../types'; +import { HandleProps, Connection, ReactFlowState, Position } from '../../types'; import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler'; @@ -37,7 +37,7 @@ const Handle = forwardRef( }, ref ) => { - const nodeId = useContext(NodeIdContext) as ElementId; + const nodeId = useContext(NodeIdContext) as string; const { setPosition, setConnectionNodeId, diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index f700f106..58265fc4 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -5,7 +5,7 @@ import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import { Provider } from '../../contexts/NodeIdContext'; -import { NodeComponentProps, WrapNodeProps, ReactFlowState } from '../../types'; +import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types'; const selector = (s: ReactFlowState) => ({ addSelectedElements: s.addSelectedElements, @@ -15,7 +15,7 @@ const selector = (s: ReactFlowState) => ({ unselectNodesAndEdges: s.unselectNodesAndEdges, }); -export default (NodeComponent: ComponentType) => { +export default (NodeComponent: ComponentType) => { const NodeWrapper = ({ id, type, diff --git a/src/components/StoreUpdater/index.tsx b/src/components/StoreUpdater/index.tsx index 33593e4d..894e3335 100644 --- a/src/components/StoreUpdater/index.tsx +++ b/src/components/StoreUpdater/index.tsx @@ -6,10 +6,10 @@ import { Node, Edge, ReactFlowState, - OnConnectFunc, - OnConnectStartFunc, - OnConnectStopFunc, - OnConnectEndFunc, + OnConnect, + OnConnectStart, + OnConnectStop, + OnConnectEnd, CoordinateExtent, OnNodesChange, OnEdgesChange, @@ -20,10 +20,10 @@ import { interface StoreUpdaterProps { nodes: Node[]; edges: Edge[]; - onConnect?: OnConnectFunc; - onConnectStart?: OnConnectStartFunc; - onConnectStop?: OnConnectStopFunc; - onConnectEnd?: OnConnectEndFunc; + onConnect?: OnConnect; + onConnectStart?: OnConnectStart; + onConnectStop?: OnConnectStop; + onConnectEnd?: OnConnectEnd; nodesDraggable?: boolean; nodesConnectable?: boolean; minZoom?: number; diff --git a/src/container/EdgeRenderer/utils.ts b/src/container/EdgeRenderer/utils.ts index 1f94f616..29a29425 100644 --- a/src/container/EdgeRenderer/utils.ts +++ b/src/container/EdgeRenderer/utils.ts @@ -4,7 +4,7 @@ import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../compon import wrapEdge from '../../components/Edges/wrapEdge'; import { rectToBox } from '../../utils'; -import { EdgeTypesType, EdgeProps, Position, XYPosition, ElementId, HandleElement, Transform, Rect } from '../../types'; +import { EdgeTypesType, EdgeProps, HandleElement, Position, XYPosition, Transform, Rect } from '../../types'; export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType { const standardTypes: EdgeTypesType = { @@ -59,7 +59,7 @@ export function getHandlePosition(position: Position, nodeRect: Rect, handle: an } } -export function getHandle(bounds: HandleElement[], handleId: ElementId | null): HandleElement | null { +export function getHandle(bounds: HandleElement[], handleId: string | null): HandleElement | null { if (!bounds) { return null; } diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index d02dc5d6..3482db92 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -85,7 +85,7 @@ const NodeRenderer = (props: NodeRendererProps) => { isHidden={node.isHidden} xPos={internals?.positionAbsolute?.x || 0} yPos={internals?.positionAbsolute?.y || 0} - dragging={node.dragging} + dragging={!!node.dragging} isInitialized={isInitialized} snapGrid={snapGrid} snapToGrid={snapToGrid} diff --git a/src/container/NodeRenderer/utils.ts b/src/container/NodeRenderer/utils.ts index 9af887a9..ede9dc72 100644 --- a/src/container/NodeRenderer/utils.ts +++ b/src/container/NodeRenderer/utils.ts @@ -4,28 +4,20 @@ import DefaultNode from '../../components/Nodes/DefaultNode'; import InputNode from '../../components/Nodes/InputNode'; import OutputNode from '../../components/Nodes/OutputNode'; import wrapNode from '../../components/Nodes/wrapNode'; -import { NodeTypesType, NodeComponentProps } from '../../types'; +import { NodeTypesType, NodeProps } from '../../types'; export function createNodeTypes(nodeTypes: NodeTypesType): NodeTypesType { const standardTypes: NodeTypesType = { - input: wrapNode((nodeTypes.input || InputNode) as ComponentType< - NodeComponentProps - >), - default: wrapNode((nodeTypes.default || DefaultNode) as ComponentType< - NodeComponentProps - >), - output: wrapNode((nodeTypes.output || OutputNode) as ComponentType< - NodeComponentProps - >), + input: wrapNode((nodeTypes.input || InputNode) as ComponentType), + default: wrapNode((nodeTypes.default || DefaultNode) as ComponentType), + output: wrapNode((nodeTypes.output || OutputNode) as ComponentType), }; const wrappedTypes = {} as NodeTypesType; const specialTypes: NodeTypesType = Object.keys(nodeTypes) - .filter(k => !['input', 'default', 'output'].includes(k)) + .filter((k) => !['input', 'default', 'output'].includes(k)) .reduce((res, key) => { - res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType< - NodeComponentProps - >); + res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType); return res; }, wrappedTypes); diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index 9f266fb7..fdfc5756 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -22,17 +22,17 @@ import { OnSelectionChangeFunc, NodeTypesType, EdgeTypesType, - OnLoadFunc, + OnLoad, Node, Edge, ConnectionMode, ConnectionLineType, ConnectionLineComponent, FlowTransform, - OnConnectStartFunc, - OnConnectStopFunc, - OnConnectEndFunc, - OnConnectFunc, + OnConnectStart, + OnConnectStop, + OnConnectEnd, + OnConnect, CoordinateExtent, KeyCode, PanOnScrollMode, @@ -72,11 +72,11 @@ export interface ReactFlowProps extends Omit, 'on onNodeDragStart?: (event: ReactMouseEvent, node: Node) => void; onNodeDrag?: (event: ReactMouseEvent, node: Node) => void; onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void; - onConnect?: OnConnectFunc; - onConnectStart?: OnConnectStartFunc; - onConnectStop?: OnConnectStopFunc; - onConnectEnd?: OnConnectEndFunc; - onLoad?: OnLoadFunc; + onConnect?: OnConnect; + onConnectStart?: OnConnectStart; + onConnectStop?: OnConnectStop; + onConnectEnd?: OnConnectEnd; + onLoad?: OnLoad; onMove?: (flowTransform?: FlowTransform) => void; onMoveStart?: (flowTransform?: FlowTransform) => void; onMoveEnd?: (flowTransform?: FlowTransform) => void; diff --git a/src/contexts/NodeIdContext.ts b/src/contexts/NodeIdContext.ts index cc19227f..ba112d70 100644 --- a/src/contexts/NodeIdContext.ts +++ b/src/contexts/NodeIdContext.ts @@ -1,8 +1,6 @@ import { createContext } from 'react'; -import { ElementId } from '../types'; - -type ContextProps = ElementId | null; +type ContextProps = string | null; export const NodeIdContext = createContext>(null); export const Provider = NodeIdContext.Provider; diff --git a/src/hooks/useOnLoadHandler.ts b/src/hooks/useOnLoadHandler.ts index 39c123eb..ed128817 100644 --- a/src/hooks/useOnLoadHandler.ts +++ b/src/hooks/useOnLoadHandler.ts @@ -3,9 +3,9 @@ import { useEffect, useRef } from 'react'; import { pointToRendererPoint } from '../utils/graph'; import { useStoreApi } from '../store'; import useZoomPanHelper from '../hooks/useZoomPanHelper'; -import { OnLoadFunc, XYPosition, Node, Edge, FlowExportObject } from '../types'; +import { OnLoad, XYPosition, Node, Edge, FlowExportObject } from '../types'; -function useOnLoadHandler(onLoad: OnLoadFunc | undefined) { +function useOnLoadHandler(onLoad: OnLoad | undefined) { const isInitialized = useRef(false); const store = useStoreApi(); const { zoomIn, zoomOut, zoomTo, transform: setTransform, fitView, initialized } = useZoomPanHelper(); diff --git a/src/hooks/useUpdateNodeInternals.ts b/src/hooks/useUpdateNodeInternals.ts index 69e978a2..27da895f 100644 --- a/src/hooks/useUpdateNodeInternals.ts +++ b/src/hooks/useUpdateNodeInternals.ts @@ -1,14 +1,14 @@ import { useCallback } from 'react'; import { useStore } from '../store'; -import { ElementId, UpdateNodeInternals, ReactFlowState } from '../types'; +import { UpdateNodeInternals, ReactFlowState } from '../types'; const updateNodeDimsSelector = (state: ReactFlowState) => state.updateNodeDimensions; function useUpdateNodeInternals(): UpdateNodeInternals { const updateNodeDimensions = useStore(updateNodeDimsSelector); - return useCallback((id: ElementId) => { + return useCallback((id: string) => { const nodeElement = document.querySelector(`.react-flow__node[data-id="${id}"]`) as HTMLDivElement; if (nodeElement) { diff --git a/src/hooks/useZoomPanHelper.ts b/src/hooks/useZoomPanHelper.ts index 37938632..aacc48da 100644 --- a/src/hooks/useZoomPanHelper.ts +++ b/src/hooks/useZoomPanHelper.ts @@ -4,7 +4,7 @@ import shallow from 'zustand/shallow'; import { useStoreApi, useStore } from '../store'; import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '../utils/graph'; -import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, XYPosition, ReactFlowState } from '../types'; +import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, ReactFlowState, Rect, XYPosition } from '../types'; const DEFAULT_PADDING = 0.1; diff --git a/src/store/index.ts b/src/store/index.ts index 27764555..c12e10c7 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -9,15 +9,12 @@ import { Edge, NodeDimensionUpdate, NodeDiffUpdate, - XYPosition, InitD3ZoomPayload, CoordinateExtent, - Transform, - Dimensions, - OnConnectFunc, - OnConnectStartFunc, - OnConnectStopFunc, - OnConnectEndFunc, + OnConnect, + OnConnectStart, + OnConnectStop, + OnConnectEnd, SetConnectionId, SnapGrid, NodeChange, @@ -25,18 +22,20 @@ import { OnEdgesChange, EdgeChange, NodeDimensionChange, + Transform, + Dimensions, + XYPosition, } from '../types'; import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph'; import { getHandleBounds } from '../components/Nodes/utils'; -import { createNodeInternals } from './utils'; +import { createNodeInternals, createNodeOrEdgeSelectionChange } from './utils'; const { Provider, useStore, useStoreApi } = createContext(); -const createNodeOrEdgeSelectionChange = (selected: boolean) => (item: Node | Edge) => ({ - id: item.id, - type: 'select', - selected, -}); +const infiniteExtent: CoordinateExtent = [ + [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], + [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], +]; const createStore = () => create((set, get) => ({ @@ -47,27 +46,16 @@ const createStore = () => 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: [ - [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], - [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], - ], - - nodeExtent: [ - [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], - [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], - ], - + translateExtent: infiniteExtent, + nodeExtent: infiniteExtent, nodesSelectionActive: false, selectionActive: false, - userSelectionRect: { startX: 0, startY: 0, @@ -345,10 +333,10 @@ const createStore = () => 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 }), + setOnConnect: (onConnect: OnConnect) => set({ onConnect }), + setOnConnectStart: (onConnectStart: OnConnectStart) => set({ onConnectStart }), + setOnConnectStop: (onConnectStop: OnConnectStop) => set({ onConnectStop }), + setOnConnectEnd: (onConnectEnd: OnConnectEnd) => set({ onConnectEnd }), setConnectionPosition: (connectionPosition: XYPosition) => set({ connectionPosition }), setConnectionNodeId: (params: SetConnectionId) => set({ ...params }), setSnapToGrid: (snapToGrid: boolean) => set({ snapToGrid }), diff --git a/src/store/utils.ts b/src/store/utils.ts index 42704f21..6f34e2cb 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -1,7 +1,6 @@ -import { ElementId, Node, NodeInternals, NodeInternalsItem, XYPosition } from '../types'; +import { Node, Edge, NodeInternals, NodeInternalsItem, XYZPosition } from '../types'; -type XYZPosition = XYPosition & { z: number }; -type ParentNodes = Record; +type ParentNodes = Record; function calculateXYZPosition( node: NodeInternalsItem, @@ -33,7 +32,7 @@ function calculateXYZPosition( }); } export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals): NodeInternals { - const nextNodeInternals = new Map(); + const nextNodeInternals = new Map(); const parentNodes: ParentNodes = {}; nodes.forEach((node) => { @@ -86,3 +85,9 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) return nextNodeInternals; } + +export const createNodeOrEdgeSelectionChange = (selected: boolean) => (item: Node | Edge) => ({ + id: item.id, + type: 'select', + selected, +}); diff --git a/src/types/changes.ts b/src/types/changes.ts new file mode 100644 index 00000000..fd29a477 --- /dev/null +++ b/src/types/changes.ts @@ -0,0 +1,28 @@ +import { XYPosition, Dimensions } from './utils'; +import { NodeHandleBounds } from './nodes'; + +export type NodeDimensionChange = { + id: string; + type: 'dimensions'; + dimensions?: Dimensions; + position?: XYPosition; + handleBounds?: NodeHandleBounds; + dragging?: boolean; +}; + +export type NodeSelectionChange = { + id: string; + type: 'select'; + selected: boolean; +}; + +export type NodeRemoveChange = { + id: string; + type: 'remove'; +}; + +export type NodeChange = NodeDimensionChange | NodeSelectionChange | NodeRemoveChange; + +export type EdgeSelectionChange = NodeSelectionChange; +export type EdgeRemoveChange = NodeRemoveChange; +export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange; diff --git a/src/types/edges.ts b/src/types/edges.ts new file mode 100644 index 00000000..94d242d7 --- /dev/null +++ b/src/types/edges.ts @@ -0,0 +1,158 @@ +import { CSSProperties, ReactNode, HTMLAttributes } from 'react'; + +import { Position } from './utils'; +import { OnConnect, Connection } from './general'; +import { HandleElement } from './handles'; +import { Node } from './nodes'; + +// interface for the user edge items +export interface Edge { + id: string; + type?: string; + source: string; + target: string; + sourceHandle?: string | null; + targetHandle?: string | null; + label?: string | ReactNode; + labelStyle?: CSSProperties; + labelShowBg?: boolean; + labelBgStyle?: CSSProperties; + labelBgPadding?: [number, number]; + labelBgBorderRadius?: number; + style?: CSSProperties; + animated?: boolean; + isHidden?: boolean; + data?: T; + className?: string; + sourceNode?: Node; + targetNode?: Node; + selected?: boolean; + markerStart?: EdgeMarkerType; + markerEnd?: EdgeMarkerType; +} + +// props that get passed to a custom edge +export interface EdgeProps { + id: string; + source: string; + target: string; + sourceX: number; + sourceY: number; + targetX: number; + targetY: number; + selected?: boolean; + animated?: boolean; + sourcePosition: Position; + targetPosition: Position; + label?: string | ReactNode; + labelStyle?: CSSProperties; + labelShowBg?: boolean; + labelBgStyle?: CSSProperties; + labelBgPadding?: [number, number]; + labelBgBorderRadius?: number; + style?: CSSProperties; + data?: T; + sourceHandleId?: string | null; + targetHandleId?: string | null; + markerStart?: string; + markerEnd?: string; +} + +export type EdgeMouseHandler = (event: React.MouseEvent, edge: Edge) => void; + +export interface WrapEdgeProps { + id: string; + className?: string; + type: string; + data?: T; + onClick?: EdgeMouseHandler; + onEdgeDoubleClick?: EdgeMouseHandler; + selected: boolean; + animated?: boolean; + label?: string | ReactNode; + labelStyle?: CSSProperties; + labelShowBg?: boolean; + labelBgStyle?: CSSProperties; + labelBgPadding?: [number, number]; + labelBgBorderRadius?: number; + style?: CSSProperties; + source: string; + target: string; + sourceHandleId: string | null; + targetHandleId: string | null; + sourceX: number; + sourceY: number; + targetX: number; + targetY: number; + sourcePosition: Position; + targetPosition: Position; + elementsSelectable?: boolean; + isHidden?: boolean; + handleEdgeUpdate: boolean; + onConnectEdge: OnConnect; + onContextMenu?: EdgeMouseHandler; + onMouseEnter?: EdgeMouseHandler; + onMouseMove?: EdgeMouseHandler; + onMouseLeave?: EdgeMouseHandler; + edgeUpdaterRadius?: number; + onEdgeUpdateStart?: EdgeMouseHandler; + onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; + markerStart?: EdgeMarkerType; + markerEnd?: EdgeMarkerType; +} + +export interface EdgeSmoothStepProps extends EdgeProps { + borderRadius?: number; +} + +export interface EdgeTextProps extends HTMLAttributes { + x: number; + y: number; + label?: string | ReactNode; + labelStyle?: CSSProperties; + labelShowBg?: boolean; + labelBgStyle?: CSSProperties; + labelBgPadding?: [number, number]; + labelBgBorderRadius?: number; +} + +export enum ConnectionLineType { + Bezier = 'default', + Straight = 'straight', + Step = 'step', + SmoothStep = 'smoothstep', +} + +export type ConnectionLineComponentProps = { + sourceX: number; + sourceY: number; + sourcePosition?: Position; + targetX: number; + targetY: number; + targetPosition?: Position; + connectionLineStyle?: CSSProperties; + connectionLineType: ConnectionLineType; + sourceNode?: Node; + sourceHandle?: HandleElement; +}; + +export type ConnectionLineComponent = React.ComponentType; + +export type OnEdgeUpdateFunc = (oldEdge: Edge, newConnection: Connection) => void; + +export interface EdgeMarker { + type: ArrowHeadType; + color?: string; + width?: number; + height?: number; + units?: string; + orient?: string; + strokeWidth?: number; +} + +export type EdgeMarkerType = string | EdgeMarker; + +export enum ArrowHeadType { + Arrow = 'arrow', + ArrowClosed = 'arrowclosed', +} diff --git a/src/types/general.ts b/src/types/general.ts new file mode 100644 index 00000000..68dd316e --- /dev/null +++ b/src/types/general.ts @@ -0,0 +1,217 @@ +import { MouseEvent as ReactMouseEvent, ReactNode } from 'react'; +import { Selection as D3Selection, ZoomBehavior } from 'd3'; + +import { XYPosition, Rect, Transform, CoordinateExtent, Dimensions } from './utils'; +import { NodeChange, EdgeChange } from './changes'; +import { Node, NodeInternals, NodeDimensionUpdate, NodeDiffUpdate } from './nodes'; +import { Edge } from './edges'; +import { HandleType } from './handles'; + +export type FlowElement = Node | Edge; + +export type Elements = Array>; + +export type NodeTypesType = { [key: string]: ReactNode }; +export type EdgeTypesType = NodeTypesType; + +export type FitView = (fitViewOptions?: FitViewParams) => void; + +export type Project = (position: XYPosition) => XYPosition; + +export type ToObject = () => FlowExportObject; + +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; + zoomTo: (zoomLevel: number) => void; + fitView: FitView; + project: Project; + getNodes: () => Node[]; + getEdges: () => Edge[]; + setTransform: (transform: FlowTransform) => void; + toObject: ToObject; +}; + +export type OnLoad = (params: OnLoadParams) => void; + +export interface Connection { + source: string | null; + target: string | null; + sourceHandle: string | null; + targetHandle: string | null; +} + +export enum ConnectionMode { + Strict = 'strict', + Loose = 'loose', +} + +export type FlowExportObject = { + nodes: Node[]; + edges: Edge[]; + position: [number, number]; + zoom: number; +}; + +export type OnConnect = (connection: Connection) => void; + +export type FitViewParams = { + padding?: number; + includeHiddenNodes?: boolean; + minZoom?: number; + maxZoom?: number; +}; + +export type OnConnectStartParams = { + nodeId: string | null; + handleId: string | null; + handleType: HandleType | null; +}; + +export type OnConnectStart = (event: ReactMouseEvent, params: OnConnectStartParams) => void; + +export type OnConnectStop = (event: MouseEvent) => void; + +export type OnConnectEnd = (event: MouseEvent) => void; + +export type SetConnectionId = { + connectionNodeId: string | null; + connectionHandleId: string | null; + connectionHandleType: HandleType | null; +}; + +export enum BackgroundVariant { + Lines = 'lines', + Dots = 'dots', +} + +export type FlowTransform = { + x: number; + y: number; + zoom: number; +}; + +export type KeyCode = string | Array; + +export type SnapGrid = [number, number]; + +export enum PanOnScrollMode { + Free = 'free', + Vertical = 'vertical', + Horizontal = 'horizontal', +} + +export interface ZoomPanHelperFunctions { + zoomIn: () => void; + zoomOut: () => void; + zoomTo: (zoomLevel: number) => void; + transform: (transform: FlowTransform) => void; + fitView: FitView; + setCenter: (x: number, y: number, zoom?: number) => void; + fitBounds: (bounds: Rect, padding?: number) => void; + project: (position: XYPosition) => XYPosition; + initialized: boolean; +} + +export type InitD3ZoomPayload = { + d3Zoom: ZoomBehavior; + d3Selection: D3Selection; + d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined; + transform: Transform; +}; + +export interface ReactFlowState { + width: number; + height: number; + transform: Transform; + nodes: Node[]; + nodeInternals: NodeInternals; + edges: Edge[]; + selectedNodesBbox: Rect; + onNodesChange: OnNodesChange | null; + onEdgesChange: OnEdgesChange | null; + + d3Zoom: ZoomBehavior | null; + d3Selection: D3Selection | null; + d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined; + minZoom: number; + maxZoom: number; + translateExtent: CoordinateExtent; + nodeExtent: CoordinateExtent; + + nodesSelectionActive: boolean; + selectionActive: boolean; + + userSelectionRect: SelectionRect; + + connectionNodeId: string | null; + connectionHandleId: string | null; + connectionHandleType: HandleType | null; + connectionPosition: XYPosition; + connectionMode: ConnectionMode; + + snapToGrid: boolean; + snapGrid: SnapGrid; + + nodesDraggable: boolean; + nodesConnectable: boolean; + elementsSelectable: boolean; + + multiSelectionActive: boolean; + + reactFlowVersion: string; + + setNodes: (nodes: Node[]) => void; + setEdges: (edges: Edge[], nodes: Node[]) => void; + updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; + updateNodePosition: (update: NodeDiffUpdate) => void; + setUserSelection: (mousePos: XYPosition) => void; + updateUserSelection: (mousePos: XYPosition) => void; + unsetUserSelection: () => void; + unsetNodesSelection: () => void; + resetSelectedElements: () => void; + unselectNodesAndEdges: () => void; + addSelectedElements: (elements: Elements) => void; + updateTransform: (transform: Transform) => void; + updateSize: (size: Dimensions) => void; + initD3Zoom: (payload: InitD3ZoomPayload) => void; + setMinZoom: (minZoom: number) => void; + setMaxZoom: (maxZoom: number) => void; + setTranslateExtent: (translateExtent: CoordinateExtent) => void; + setNodeExtent: (nodeExtent: CoordinateExtent) => void; + setOnConnect: (onConnectFunction: OnConnect) => void; + setOnConnectStart: (onConnectFunction: OnConnectStart) => void; + setOnConnectStop: (onConnectFunction: OnConnectStop) => void; + setOnConnectEnd: (onConnectFunction: OnConnectEnd) => void; + setConnectionPosition: (connectionPosition: XYPosition) => void; + setConnectionNodeId: (payload: SetConnectionId) => void; + setSnapToGrid: (snapToGrid: boolean) => void; + setSnapGrid: (snapGrid: SnapGrid) => void; + setInteractive: (isInteractive: boolean) => void; + setNodesDraggable: (nodesDraggable: boolean) => void; + setNodesConnectable: (nodesConnectable: boolean) => void; + setElementsSelectable: (elementsSelectable: boolean) => void; + setMultiSelectionActive: (multiSelectionActive: boolean) => void; + setConnectionMode: (connectionMode: ConnectionMode) => void; + setOnNodesChange: (onNodesChange: OnNodesChange) => void; + setOnEdgesChange: (onEdgesChange: OnEdgesChange) => void; + + onConnect?: OnConnect; + onConnectStart?: OnConnectStart; + onConnectStop?: OnConnectStop; + onConnectEnd?: OnConnectEnd; +} + +export type UpdateNodeInternals = (nodeId: string) => void; + +export type OnSelectionChangeFunc = (params: { nodes: Node[]; edges: Edge[] }) => void; diff --git a/src/types/handles.ts b/src/types/handles.ts new file mode 100644 index 00000000..9114b360 --- /dev/null +++ b/src/types/handles.ts @@ -0,0 +1,18 @@ +import { XYPosition, Position, Dimensions } from './utils'; +import { OnConnect, Connection } from './general'; + +export type HandleType = 'source' | 'target'; + +export interface HandleElement extends XYPosition, Dimensions { + id?: string | null; + position: Position; +} + +export interface HandleProps { + type: HandleType; + position: Position; + isConnectable?: boolean; + onConnect?: OnConnect; + isValidConnection?: (connection: Connection) => boolean; + id?: string; +} diff --git a/src/types/index.ts b/src/types/index.ts index d37949b9..bf9e688a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,565 +1,6 @@ -import React, { CSSProperties, MouseEvent as ReactMouseEvent, HTMLAttributes, ReactNode } from 'react'; -import { Selection as D3Selection, ZoomBehavior } from 'd3'; - -export type ElementId = string; - -export type FlowElement = Node | Edge; - -export type Elements = Array>; - -export type Transform = [number, number, number]; - -export type NodeDimensionChange = { - id: string; - type: 'dimensions'; - dimensions?: Dimensions; - position?: XYPosition; - handleBounds?: NodeHandleBounds; - dragging?: boolean; -}; - -export type NodeSelectionChange = { - id: string; - type: 'select'; - selected: boolean; -}; -export type NodeRemoveChange = { - id: string; - type: 'remove'; -}; -export type NodeChange = NodeDimensionChange | NodeSelectionChange | NodeRemoveChange; - -export type EdgeSelectionChange = NodeSelectionChange; -export type EdgeRemoveChange = NodeRemoveChange; -export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange; - -export enum Position { - Left = 'left', - Top = 'top', - Right = 'right', - Bottom = 'bottom', -} - -export interface XYPosition { - x: number; - y: number; -} - -export interface Dimensions { - width: number; - height: number; -} - -export interface Rect extends Dimensions, XYPosition {} - -export interface Box extends XYPosition { - x2: number; - y2: number; -} - -export type SnapGrid = [number, number]; - -export type NodeHandleBounds = { - source: HandleElement[] | null; - target: HandleElement[] | null; -}; - -export interface Node { - id: ElementId; - position: XYPosition; - type?: string; - data?: T; - style?: CSSProperties; - className?: string; - targetPosition?: Position; - sourcePosition?: Position; - isHidden?: boolean; - selected?: boolean; - dragging?: boolean; - draggable?: boolean; - selectable?: boolean; - connectable?: boolean; - dragHandle?: string; - width?: number | null; - height?: number | null; - parentNode?: ElementId; - zIndex?: number; - extent?: 'parent' | CoordinateExtent; -} - -export enum ArrowHeadType { - Arrow = 'arrow', - ArrowClosed = 'arrowclosed', -} - -export interface EdgeMarker { - type: ArrowHeadType; - color?: string; - width?: number; - height?: number; - units?: string; - orient?: string; - strokeWidth?: number; -} - -export type EdgeMarkerType = string | EdgeMarker; - -export interface Edge { - id: ElementId; - type?: string; - source: ElementId; - target: ElementId; - sourceHandle?: ElementId | null; - targetHandle?: ElementId | null; - label?: string | ReactNode; - labelStyle?: CSSProperties; - labelShowBg?: boolean; - labelBgStyle?: CSSProperties; - labelBgPadding?: [number, number]; - labelBgBorderRadius?: number; - style?: CSSProperties; - animated?: boolean; - isHidden?: boolean; - data?: T; - className?: string; - sourceNode?: Node; - targetNode?: Node; - selected?: boolean; - markerStart?: EdgeMarkerType; - markerEnd?: EdgeMarkerType; -} - -export enum BackgroundVariant { - Lines = 'lines', - Dots = 'dots', -} - -export type HandleType = 'source' | 'target'; - -export type NodeTypesType = { [key: string]: ReactNode }; - -export type EdgeTypesType = NodeTypesType; - -export interface SelectionRect extends Rect { - startX: number; - startY: number; - draw: boolean; -} - -export interface WrapEdgeProps { - id: ElementId; - className?: string; - type: string; - data?: T; - onClick?: (event: React.MouseEvent, edge: Edge) => void; - onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void; - selected: boolean; - animated?: boolean; - label?: string | ReactNode; - labelStyle?: CSSProperties; - labelShowBg?: boolean; - labelBgStyle?: CSSProperties; - labelBgPadding?: [number, number]; - labelBgBorderRadius?: number; - style?: CSSProperties; - source: ElementId; - target: ElementId; - sourceHandleId: ElementId | null; - targetHandleId: ElementId | null; - sourceX: number; - sourceY: number; - targetX: number; - targetY: number; - sourcePosition: Position; - targetPosition: Position; - elementsSelectable?: boolean; - isHidden?: boolean; - handleEdgeUpdate: boolean; - onConnectEdge: OnConnectFunc; - onContextMenu?: (event: React.MouseEvent, edge: Edge) => void; - onMouseEnter?: (event: React.MouseEvent, edge: Edge) => void; - onMouseMove?: (event: React.MouseEvent, edge: Edge) => void; - onMouseLeave?: (event: React.MouseEvent, edge: Edge) => void; - edgeUpdaterRadius?: number; - onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge) => void; - onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; - markerStart?: EdgeMarkerType; - markerEnd?: EdgeMarkerType; -} - -export interface EdgeProps { - id: ElementId; - source: ElementId; - target: ElementId; - sourceX: number; - sourceY: number; - targetX: number; - targetY: number; - selected?: boolean; - animated?: boolean; - sourcePosition: Position; - targetPosition: Position; - label?: string | ReactNode; - labelStyle?: CSSProperties; - labelShowBg?: boolean; - labelBgStyle?: CSSProperties; - labelBgPadding?: [number, number]; - labelBgBorderRadius?: number; - style?: CSSProperties; - data?: T; - sourceHandleId?: ElementId | null; - targetHandleId?: ElementId | null; - markerStart?: string; - markerEnd?: string; -} -export interface EdgeSmoothStepProps extends EdgeProps { - borderRadius?: number; -} - -export interface EdgeTextProps extends HTMLAttributes { - x: number; - y: number; - label?: string | ReactNode; - labelStyle?: CSSProperties; - labelShowBg?: boolean; - labelBgStyle?: CSSProperties; - labelBgPadding?: [number, number]; - labelBgBorderRadius?: number; -} - -export interface NodeProps { - id: ElementId; - type: string; - data: T; - selected: boolean; - isConnectable: boolean; - xPos?: number; - yPos?: number; - targetPosition?: Position; - sourcePosition?: Position; - dragging?: boolean; - dragHandle?: string; - zIndex?: number; -} - -export interface NodeComponentProps { - id: ElementId; - type: string; - data: T; - selected?: boolean; - isConnectable: boolean; - transform?: Transform; - xPos?: number; - yPos?: number; - targetPosition?: Position; - sourcePosition?: Position; - onClick?: (node: Node) => void; - onNodeDoubleClick?: (node: Node) => void; - onMouseEnter?: (node: Node) => void; - onMouseMove?: (node: Node) => void; - onMouseLeave?: (node: Node) => void; - onContextMenu?: (node: Node) => void; - onNodeDragStart?: (node: Node) => void; - onNodeDrag?: (node: Node) => void; - onNodeDragStop?: (node: Node) => void; - style?: CSSProperties; - dragging?: boolean; - dragHandle?: string; - zIndex?: number; -} - -export interface WrapNodeProps { - id: ElementId; - type: string; - data: T; - selected: boolean; - scale: number; - xPos: number; - yPos: number; - width?: number | null; - height?: number | null; - isSelectable: boolean; - isDraggable: boolean; - isConnectable: boolean; - selectNodesOnDrag: boolean; - onClick?: (event: ReactMouseEvent, node: Node) => void; - onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void; - onMouseEnter?: (event: ReactMouseEvent, node: Node) => void; - onMouseMove?: (event: ReactMouseEvent, node: Node) => void; - onMouseLeave?: (event: ReactMouseEvent, node: Node) => void; - onContextMenu?: (event: ReactMouseEvent, node: Node) => void; - onNodeDragStart?: (event: ReactMouseEvent, node: Node) => void; - onNodeDrag?: (event: ReactMouseEvent, node: Node) => void; - onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void; - style?: CSSProperties; - className?: string; - sourcePosition?: Position; - targetPosition?: Position; - isHidden?: boolean; - isInitialized?: boolean; - snapToGrid?: boolean; - snapGrid?: SnapGrid; - dragging?: boolean; - resizeObserver: ResizeObserver | null; - dragHandle?: string; - zIndex: number; - isParent: boolean; -} - -export type FitViewParams = { - padding?: number; - includeHiddenNodes?: boolean; - minZoom?: number; - maxZoom?: number; -}; - -export type FlowExportObject = { - nodes: Node[]; - edges: Edge[]; - position: [number, number]; - zoom: number; -}; - -export type FitViewFunc = (fitViewOptions?: FitViewParams) => void; -export type ProjectFunc = (position: XYPosition) => XYPosition; -export type ToObjectFunc = () => FlowExportObject; - -export type OnLoadParams = { - zoomIn: () => void; - zoomOut: () => void; - zoomTo: (zoomLevel: number) => void; - fitView: FitViewFunc; - project: ProjectFunc; - getNodes: () => Node[]; - getEdges: () => Edge[]; - setTransform: (transform: FlowTransform) => void; - toObject: ToObjectFunc; -}; - -export type OnLoadFunc = (params: OnLoadParams) => void; - -export interface Connection { - source: ElementId | null; - target: ElementId | null; - sourceHandle: ElementId | null; - targetHandle: ElementId | null; -} - -export enum ConnectionMode { - Strict = 'strict', - Loose = 'loose', -} - -export enum ConnectionLineType { - Bezier = 'default', - Straight = 'straight', - Step = 'step', - SmoothStep = 'smoothstep', -} - -export type ConnectionLineComponentProps = { - sourceX: number; - sourceY: number; - sourcePosition?: Position; - targetX: number; - targetY: number; - targetPosition?: Position; - connectionLineStyle?: CSSProperties; - connectionLineType: ConnectionLineType; - sourceNode?: Node; - sourceHandle?: HandleElement; -}; - -export type ConnectionLineComponent = React.ComponentType; - -export type OnConnectFunc = (connection: Connection) => void; -export type OnConnectStartParams = { - nodeId: ElementId | null; - handleId: ElementId | null; - handleType: HandleType | null; -}; -export type OnConnectStartFunc = (event: ReactMouseEvent, params: OnConnectStartParams) => void; -export type OnConnectStopFunc = (event: MouseEvent) => void; -export type OnConnectEndFunc = (event: MouseEvent) => void; - -export type SetConnectionId = { - connectionNodeId: ElementId | null; - connectionHandleId: ElementId | null; - connectionHandleType: HandleType | null; -}; - -export interface HandleElement extends XYPosition, Dimensions { - id?: ElementId | null; - position: Position; -} - -export interface HandleProps { - type: HandleType; - position: Position; - isConnectable?: boolean; - onConnect?: OnConnectFunc; - isValidConnection?: (connection: Connection) => boolean; - id?: ElementId; -} - -export type NodePosUpdate = { - id: ElementId; - pos: XYPosition; -}; - -export type NodeDiffUpdate = { - id?: ElementId; - diff?: XYPosition; - dragging?: boolean; -}; - -export type FlowTransform = { - x: number; - y: number; - zoom: number; -}; - -export type CoordinateExtent = [[number, number], [number, number]]; - -export type KeyCode = string | Array; - -export enum PanOnScrollMode { - Free = 'free', - Vertical = 'vertical', - Horizontal = 'horizontal', -} - -export interface ZoomPanHelperFunctions { - zoomIn: () => void; - zoomOut: () => void; - zoomTo: (zoomLevel: number) => void; - transform: (transform: FlowTransform) => void; - fitView: FitViewFunc; - setCenter: (x: number, y: number, zoom?: number) => void; - fitBounds: (bounds: Rect, padding?: number) => void; - project: (position: XYPosition) => XYPosition; - initialized: boolean; -} - -export type OnEdgeUpdateFunc = (oldEdge: Edge, newConnection: Connection) => void; - -export type NodeDimensionUpdate = { - id: ElementId; - nodeElement: HTMLDivElement; - forceUpdate?: boolean; -}; - -export type InitD3ZoomPayload = { - d3Zoom: ZoomBehavior; - d3Selection: D3Selection; - d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined; - transform: Transform; -}; - -export type OnNodesChange = (nodes: NodeChange[]) => void; -export type OnEdgesChange = (nodes: EdgeChange[]) => void; - -export type NodeInternalsItem = { - id?: string; - width?: number | null; - height?: number | null; - parentNode?: ElementId; - position?: XYPosition; - positionAbsolute?: XYPosition; - handleBounds?: NodeHandleBounds; - z?: number; - isParent?: boolean; -}; - -export type NodeInternals = Map; - -export interface ReactFlowState { - width: number; - height: number; - transform: Transform; - nodes: Node[]; - nodeInternals: NodeInternals; - edges: Edge[]; - selectedNodesBbox: Rect; - onNodesChange: OnNodesChange | null; - onEdgesChange: OnEdgesChange | null; - - d3Zoom: ZoomBehavior | null; - d3Selection: D3Selection | null; - d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined; - minZoom: number; - maxZoom: number; - translateExtent: CoordinateExtent; - nodeExtent: CoordinateExtent; - - nodesSelectionActive: boolean; - selectionActive: boolean; - - userSelectionRect: SelectionRect; - - connectionNodeId: ElementId | null; - connectionHandleId: ElementId | null; - connectionHandleType: HandleType | null; - connectionPosition: XYPosition; - connectionMode: ConnectionMode; - - snapToGrid: boolean; - snapGrid: SnapGrid; - - nodesDraggable: boolean; - nodesConnectable: boolean; - elementsSelectable: boolean; - - multiSelectionActive: boolean; - - reactFlowVersion: string; - - setNodes: (nodes: Node[]) => void; - setEdges: (edges: Edge[], nodes: Node[]) => void; - updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; - updateNodePosition: (update: NodeDiffUpdate) => void; - setUserSelection: (mousePos: XYPosition) => void; - updateUserSelection: (mousePos: XYPosition) => void; - unsetUserSelection: () => void; - unsetNodesSelection: () => void; - resetSelectedElements: () => void; - unselectNodesAndEdges: () => void; - addSelectedElements: (elements: Elements) => void; - updateTransform: (transform: Transform) => void; - updateSize: (size: Dimensions) => void; - initD3Zoom: (payload: InitD3ZoomPayload) => void; - setMinZoom: (minZoom: number) => void; - setMaxZoom: (maxZoom: number) => void; - setTranslateExtent: (translateExtent: CoordinateExtent) => void; - setNodeExtent: (nodeExtent: CoordinateExtent) => void; - setOnConnect: (onConnectFunction: OnConnectFunc) => void; - setOnConnectStart: (onConnectFunction: OnConnectStartFunc) => void; - setOnConnectStop: (onConnectFunction: OnConnectStopFunc) => void; - setOnConnectEnd: (onConnectFunction: OnConnectEndFunc) => void; - setConnectionPosition: (connectionPosition: XYPosition) => void; - setConnectionNodeId: (payload: SetConnectionId) => void; - setSnapToGrid: (snapToGrid: boolean) => void; - setSnapGrid: (snapGrid: SnapGrid) => void; - setInteractive: (isInteractive: boolean) => void; - setNodesDraggable: (nodesDraggable: boolean) => void; - setNodesConnectable: (nodesConnectable: boolean) => void; - setElementsSelectable: (elementsSelectable: boolean) => void; - setMultiSelectionActive: (multiSelectionActive: boolean) => void; - setConnectionMode: (connectionMode: ConnectionMode) => void; - setOnNodesChange: (onNodesChange: OnNodesChange) => void; - setOnEdgesChange: (onEdgesChange: OnEdgesChange) => void; - - onConnect?: OnConnectFunc; - onConnectStart?: OnConnectStartFunc; - onConnectStop?: OnConnectStopFunc; - onConnectEnd?: OnConnectEndFunc; -} - -export type UpdateNodeInternals = (nodeId: ElementId) => void; - -export type OnSelectionChangeFunc = (params: { nodes: Node[]; edges: Edge[] }) => void; - -export type NodeRendererNode = { - childNodes?: NodeRendererNode[]; - node: Node; -}; +export * from './general'; +export * from './nodes'; +export * from './edges'; +export * from './handles'; +export * from './changes'; +export * from './utils'; diff --git a/src/types/nodes.ts b/src/types/nodes.ts new file mode 100644 index 00000000..f2b7b02a --- /dev/null +++ b/src/types/nodes.ts @@ -0,0 +1,116 @@ +import { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; + +import { SnapGrid } from './general'; +import { XYPosition, Position, CoordinateExtent } from './utils'; +import { HandleElement } from './handles'; + +// interface for the user node items +export interface Node { + id: string; + position: XYPosition; + type?: string; + data?: T; + style?: CSSProperties; + className?: string; + targetPosition?: Position; + sourcePosition?: Position; + isHidden?: boolean; + selected?: boolean; + dragging?: boolean; + draggable?: boolean; + selectable?: boolean; + connectable?: boolean; + dragHandle?: string; + width?: number | null; + height?: number | null; + parentNode?: string; + zIndex?: number; + extent?: 'parent' | CoordinateExtent; +} + +// props that get passed to a custom node +export interface NodeProps { + id: string; + type: string; + data: T; + selected: boolean; + isConnectable: boolean; + xPos: number; + yPos: number; + dragging: boolean; + zIndex: number; + targetPosition?: Position; + sourcePosition?: Position; + dragHandle?: string; +} + +export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void; + +export interface WrapNodeProps { + id: string; + type: string; + data: T; + selected: boolean; + isConnectable: boolean; + scale: number; + xPos: number; + yPos: number; + width?: number | null; + height?: number | null; + isSelectable: boolean; + isDraggable: boolean; + selectNodesOnDrag: boolean; + onClick?: NodeMouseHandler; + onNodeDoubleClick?: NodeMouseHandler; + onMouseEnter?: NodeMouseHandler; + onMouseMove?: NodeMouseHandler; + onMouseLeave?: NodeMouseHandler; + onContextMenu?: NodeMouseHandler; + onNodeDragStart?: NodeMouseHandler; + onNodeDrag?: NodeMouseHandler; + onNodeDragStop?: NodeMouseHandler; + style?: CSSProperties; + className?: string; + sourcePosition?: Position; + targetPosition?: Position; + isHidden?: boolean; + isInitialized?: boolean; + snapToGrid?: boolean; + snapGrid?: SnapGrid; + dragging: boolean; + resizeObserver: ResizeObserver | null; + dragHandle?: string; + zIndex: number; + isParent: boolean; +} + +export type NodeHandleBounds = { + source: HandleElement[] | null; + target: HandleElement[] | null; +}; + +export type NodeDiffUpdate = { + id?: string; + diff?: XYPosition; + dragging?: boolean; +}; + +export type NodeDimensionUpdate = { + id: string; + nodeElement: HTMLDivElement; + forceUpdate?: boolean; +}; + +export type NodeInternalsItem = { + id?: string; + width?: number | null; + height?: number | null; + parentNode?: string; + position?: XYPosition; + positionAbsolute?: XYPosition; + handleBounds?: NodeHandleBounds; + z?: number; + isParent?: boolean; +}; + +export type NodeInternals = Map; diff --git a/src/types/utils.ts b/src/types/utils.ts new file mode 100644 index 00000000..eadce743 --- /dev/null +++ b/src/types/utils.ts @@ -0,0 +1,29 @@ +export enum Position { + Left = 'left', + Top = 'top', + Right = 'right', + Bottom = 'bottom', +} + +export interface XYPosition { + x: number; + y: number; +} + +export type XYZPosition = XYPosition & { z: number }; + +export interface Dimensions { + width: number; + height: number; +} + +export interface Rect extends Dimensions, XYPosition {} + +export interface Box extends XYPosition { + x2: number; + y2: number; +} + +export type Transform = [number, number, number]; + +export type CoordinateExtent = [[number, number], [number, number]]; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 9ceba91d..b575bbf7 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -1,6 +1,6 @@ import { boxToRect, clamp, getBoundsOfBoxes, rectToBox } from '../utils'; -import { ElementId, Node, Edge, Elements, Transform, XYPosition, Rect, Connection, EdgeMarkerType } from '../types'; +import { Node, Edge, Elements, Connection, EdgeMarkerType, Transform, XYPosition, Rect } from '../types'; export const isEdge = (element: Node | Connection | Edge): element is Edge => 'id' in element && 'source' in element && 'target' in element; @@ -26,7 +26,7 @@ export const getIncomers = (node: Node, nodes: Node[], edges: Edge[]): Node[] => return nodes.filter((n) => incomersIds.includes(n.id)); }; -const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection): ElementId => +const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection): string => `reactflow__edge-${source}${sourceHandle}-${target}${targetHandle}`; export const getMarkerId = (marker: EdgeMarkerType | undefined): string => { From f0e79c634afbd601328b3cc409a0f881e22a673c Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 9 Nov 2021 16:32:02 +0100 Subject: [PATCH 059/263] refactor(nodes-z): groups start with z=2 --- package-lock.json | 4 ++-- package.json | 2 +- src/store/utils.ts | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index eb33b332..1756d2e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.13", + "version": "10.0.0-next.14", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "10.0.0-next.13", + "version": "10.0.0-next.14", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/package.json b/package.json index fbafd4e1..b2fdbe7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.13", + "version": "10.0.0-next.14", "engines": { "node": ">=12" }, diff --git a/src/store/utils.ts b/src/store/utils.ts index 6f34e2cb..be01a1b0 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -36,6 +36,7 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) const parentNodes: ParentNodes = {}; nodes.forEach((node) => { + const z = node.zIndex ? node.zIndex : node.dragging || node.selected ? 1000 : 0; const internals: NodeInternalsItem = { ...nodeInternals.get(node.id), id: node.id, @@ -43,7 +44,7 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) height: node.height || null, position: node.position, positionAbsolute: node.position, - z: node.dragging || node.selected ? 1000 : node.zIndex || 0, + z, }; if (node.parentNode) { internals.parentNode = node.parentNode; @@ -59,7 +60,7 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) let startingZ = updatedInternals.z || 0; if (!startingZ) { - if (parentNodes[node.id] && node.parentNode) { + if (parentNodes[node.id]) { startingZ = 2; } else if (node.parentNode) { startingZ = 1; From c29decade5452446cd36b60d3554d69c4c5e2624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Wed, 10 Nov 2021 18:06:28 +0100 Subject: [PATCH 060/263] feat(stacking): disable auto layering, instead configure via zIndex prop --- example/src/Basic/index.tsx | 44 +++++++++++++++---------------- src/hooks/useVisibleEdges.ts | 2 +- src/store/utils.ts | 51 +++++++++++++++++++++++++++--------- src/types/edges.ts | 1 + 4 files changed, 62 insertions(+), 36 deletions(-) diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index 1416b115..aedd31cd 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -22,6 +22,13 @@ const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge); const initialNodes: Node[] = [ { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, + { + id: '4', + data: { label: 'Node 4' }, + position: { x: 100, y: 200 }, + className: 'light', + style: { backgroundColor: 'rgba(255,50, 50, 0.5)', width: 500, height: 300 }, + }, { id: '4a', data: { label: 'Node 4a' }, @@ -30,7 +37,14 @@ const initialNodes: Node[] = [ parentNode: '4', extent: 'parent', }, - + { + id: '4b', + data: { label: 'Node 4b' }, + position: { x: 150, y: 50 }, + className: 'light', + style: { backgroundColor: 'rgba(50, 50, 255, 0.5)', height: 200, width: 300 }, + parentNode: '4', + }, { id: '4b1', data: { label: 'Node 4b1' }, @@ -46,19 +60,12 @@ const initialNodes: Node[] = [ parentNode: '4b', }, { - id: '4b', - data: { label: 'Node 4b' }, - position: { x: 150, y: 50 }, + id: '5', + data: { label: 'Node 5' }, + position: { x: 650, y: 250 }, className: 'light', - style: { backgroundColor: 'rgba(50, 50, 255, 0.5)', height: 200, width: 300 }, - parentNode: '4', - }, - { - id: '4', - data: { label: 'Node 4' }, - position: { x: 100, y: 200 }, - className: 'light', - style: { backgroundColor: 'rgba(255,50, 50, 0.5)', width: 500, height: 300 }, + style: { backgroundColor: 'rgba(20 ,200, 255, 1.5)', width: 400, height: 150 }, + zIndex: 1000, }, { id: '5a', @@ -74,13 +81,6 @@ const initialNodes: Node[] = [ className: 'light', parentNode: '5', }, - { - id: '5', - data: { label: 'Node 5' }, - position: { x: 650, y: 250 }, - className: 'light', - style: { backgroundColor: 'rgba(20 ,200, 255, 1.5)', width: 400, height: 150 }, - }, { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, ]; @@ -88,10 +88,10 @@ const initialNodes: Node[] = [ const initialEdges: Edge[] = [ { id: 'e1-2', source: '1', target: '2', animated: true }, { id: 'e1-3', source: '1', target: '3' }, - { id: 'e3-4', source: '3', target: '4' }, + { id: 'e3-4', source: '3', target: '4', zIndex: 100 }, { id: 'e3-4b', source: '3', target: '4b' }, { id: 'e4a-4b1', source: '4a', target: '4b1' }, - { id: 'e4a-4b2', source: '4a', target: '4b2' }, + { id: 'e4a-4b2', source: '4a', target: '4b2', zIndex: 100 }, { id: 'e4b1-4b2', source: '4b1', target: '4b2' }, { id: '3-5', source: '3', target: '5' }, ]; diff --git a/src/hooks/useVisibleEdges.ts b/src/hooks/useVisibleEdges.ts index 837150f1..5ab023d5 100644 --- a/src/hooks/useVisibleEdges.ts +++ b/src/hooks/useVisibleEdges.ts @@ -8,7 +8,7 @@ function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals) { let maxLevel = -1; const levelLookup = edges.reduce>((tree, edge) => { - const z = Math.max(nodeInternals.get(edge.source)?.z || 0, nodeInternals.get(edge.target)?.z || 0); + const z = edge.zIndex || Math.max(nodeInternals.get(edge.source)?.z || 0, nodeInternals.get(edge.target)?.z || 0); if (tree[z]) { tree[z].push(edge); } else { diff --git a/src/store/utils.ts b/src/store/utils.ts index be01a1b0..2fa99b8d 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -31,6 +31,42 @@ function calculateXYZPosition( z: (result.z ?? 0) + zAddition, }); } + +// function createTree(items: Node[]): any { +// const rootItems = []; +// const lookup: Record = {}; + +// for (const item of items) { +// const parentId = item.parentNode; + +// if (!lookup[item.id]) { +// lookup[item.id] = { childNodes: [], z: 0 }; +// } + +// lookup[item.id] = { +// node: item, +// childNodes: lookup[item.id].childNodes, +// z: lookup[item.id].z, +// }; + +// const treeItem = lookup[item.id]; + +// if (!parentId) { +// rootItems.push(treeItem); +// } else { +// if (!lookup[parentId]) { +// lookup[parentId] = { childNodes: [], z: 0 }; +// } + +// lookup[parentId].childNodes.push({ ...treeItem, z: lookup[parentId].z + 1 }); +// } +// } + +// console.log(lookup); + +// return rootItems; +// } + export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals): NodeInternals { const nextNodeInternals = new Map(); const parentNodes: ParentNodes = {}; @@ -57,26 +93,15 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) const updatedInternals: NodeInternalsItem = nextNodeInternals.get(node.id)!; if (node.parentNode || parentNodes[node.id]) { - let startingZ = updatedInternals.z || 0; - - if (!startingZ) { - if (parentNodes[node.id]) { - startingZ = 2; - } else if (node.parentNode) { - startingZ = 1; - } - } - - const { x, y, z } = calculateXYZPosition(node, nextNodeInternals, parentNodes, { + const { x, y } = calculateXYZPosition(node, nextNodeInternals, parentNodes, { ...node.position, - z: startingZ, + z: 0, }); updatedInternals.positionAbsolute = { x, y, }; - updatedInternals.z = z; if (parentNodes[node.id]) { updatedInternals.isParent = true; diff --git a/src/types/edges.ts b/src/types/edges.ts index 94d242d7..7e7f1875 100644 --- a/src/types/edges.ts +++ b/src/types/edges.ts @@ -29,6 +29,7 @@ export interface Edge { selected?: boolean; markerStart?: EdgeMarkerType; markerEnd?: EdgeMarkerType; + zIndex?: number; } // props that get passed to a custom edge From 0974aa57327d6e606c9d1173bb23fff8bcd36b40 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 18 Nov 2021 13:01:11 +0100 Subject: [PATCH 061/263] refactor(nodes): only use node internals --- example/src/Basic/index.tsx | 1 + example/src/FloatingEdges/FloatingEdge.tsx | 8 +-- example/src/Hidden/index.tsx | 62 +++++++++++++++++++ example/src/Stress/index.tsx | 2 + example/src/index.tsx | 5 ++ example/src_oldapi/Hidden/index.tsx | 53 ----------------- src/additional-components/MiniMap/index.tsx | 19 +++--- src/components/ConnectionLine/index.tsx | 6 +- src/components/Nodes/wrapNode.tsx | 8 +-- src/components/NodesSelection/index.tsx | 6 +- src/components/SelectionListener/index.tsx | 5 +- src/container/NodeRenderer/index.tsx | 21 ++++--- src/hooks/useGlobalKeyHandler.ts | 4 +- src/hooks/useNodeInternalsRef.ts | 14 ----- src/hooks/useOnLoadHandler.ts | 9 ++- src/hooks/useVisibleNodes.ts | 6 +- src/hooks/useZoomPanHelper.ts | 5 +- src/store/index.ts | 66 ++++++++++++--------- src/store/utils.ts | 14 +++-- src/types/general.ts | 1 - src/types/nodes.ts | 7 +-- 21 files changed, 171 insertions(+), 151 deletions(-) create mode 100644 example/src/Hidden/index.tsx delete mode 100644 example/src_oldapi/Hidden/index.tsx delete mode 100644 src/hooks/useNodeInternalsRef.ts diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index 1416b115..825fd333 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -147,6 +147,7 @@ const BasicFlow = () => { }; const onNodesChange = useCallback((changes: NodeChange[]) => { + console.log('node change', changes); setNodes((ns) => applyNodeChanges(changes, ns)); }, []); diff --git a/example/src/FloatingEdges/FloatingEdge.tsx b/example/src/FloatingEdges/FloatingEdge.tsx index a163fcbe..1812ee2b 100644 --- a/example/src/FloatingEdges/FloatingEdge.tsx +++ b/example/src/FloatingEdges/FloatingEdge.tsx @@ -3,13 +3,13 @@ import { EdgeProps, useStore, getBezierPath, ReactFlowState } from 'react-flow-r import { getEdgeParams } from './utils'; -const nodeSelector = (s: ReactFlowState) => s.nodes; +const nodeSelector = (s: ReactFlowState) => s.nodeInternals; const FloatingEdge: FC = ({ id, source, target, style }) => { - const nodes = useStore(nodeSelector); + const nodeInternals = useStore(nodeSelector); - const sourceNode = useMemo(() => nodes.find((n) => n.id === source), [source, nodes]); - const targetNode = useMemo(() => nodes.find((n) => n.id === target), [target, nodes]); + const sourceNode = useMemo(() => nodeInternals.get(source), [source, nodeInternals]); + const targetNode = useMemo(() => nodeInternals.get(target), [target, nodeInternals]); if (!sourceNode || !targetNode) { return null; diff --git a/example/src/Hidden/index.tsx b/example/src/Hidden/index.tsx new file mode 100644 index 00000000..b0249310 --- /dev/null +++ b/example/src/Hidden/index.tsx @@ -0,0 +1,62 @@ +import { useState, useCallback } from 'react'; + +import { useEffect } from 'react'; +import ReactFlow, { addEdge, MiniMap, Controls, Connection, Edge, Node } from 'react-flow-renderer'; + +const initialNodes: Node[] = [ + { id: '1', type: 'input', isHidden: true, data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, + { id: '2', isHidden: true, data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, + { id: '3', isHidden: true, data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, + { id: '4', isHidden: true, data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, +]; + +const initialEdges: Edge[] = [ + { id: 'e1-2', source: '1', target: '2' }, + { id: 'e1-3', source: '1', target: '3' }, + { id: 'e3-4', source: '3', target: '4' }, +]; + +const setHidden = (isHidden: boolean) => (els: any[]) => + els.map((e: any) => { + e.isHidden = isHidden; + return e; + }); + +const HiddenFlow = () => { + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState(initialEdges); + const [isHidden, setIsHidden] = useState(true); + + const onConnect = useCallback((params: Edge | Connection) => { + setEdges((eds) => addEdge(params, eds)); + }, []); + + useEffect(() => { + setNodes(setHidden(isHidden)); + setNodes(setHidden(isHidden)); + }, [isHidden]); + + return ( + + + + +
+
+ +
+
+
+ ); +}; + +export default HiddenFlow; diff --git a/example/src/Stress/index.tsx b/example/src/Stress/index.tsx index e8bc9323..5b97298c 100644 --- a/example/src/Stress/index.tsx +++ b/example/src/Stress/index.tsx @@ -51,6 +51,8 @@ const StressFlow = () => { }; const onNodesChange = useCallback((changes: NodeChange[]) => { + console.log('node change', changes); + setNodes((ns) => applyNodeChanges(changes, ns)); }, []); diff --git a/example/src/index.tsx b/example/src/index.tsx index b23fc09c..45aa0a16 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -9,6 +9,7 @@ import CustomNode from './CustomNode'; import FloatingEdges from './FloatingEdges'; import Layouting from './Layouting'; import NestedNodes from './NestedNodes'; +import Hidden from './Hidden'; import './index.css'; @@ -41,6 +42,10 @@ const routes = [ path: '/nested-nodes', component: NestedNodes, }, + { + path: '/hidden', + component: Hidden, + }, ]; const Header = withRouter(({ history, location }) => { diff --git a/example/src_oldapi/Hidden/index.tsx b/example/src_oldapi/Hidden/index.tsx deleted file mode 100644 index 9869b2bc..00000000 --- a/example/src_oldapi/Hidden/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { useState } from 'react'; - -import { useEffect } from 'react'; -import ReactFlow, { addEdge, MiniMap, Controls, Connection, Edge, Elements } from 'react-flow-renderer'; - -const initialElements: Elements = [ - { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, - { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, - { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, - { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, - { id: 'e1-2', source: '1', target: '2' }, - { id: 'e1-3', source: '1', target: '3' }, - { id: 'e3-4', source: '3', target: '4' }, -]; - -const HiddenFlow = () => { - const [elements, setElements] = useState(initialElements); - const [isHidden, setIsHidden] = useState(false); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); - - useEffect(() => { - setElements((els) => - els.map((e) => { - e.isHidden = isHidden; - return e; - }) - ); - }, [isHidden]); - - return ( - - - - -
-
- -
-
-
- ); -}; - -export default HiddenFlow; diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index 98735e55..bf5d7e02 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -28,7 +28,6 @@ const selector = (s: ReactFlowState) => ({ width: s.width, height: s.height, transform: s.transform, - nodes: s.nodes, nodeInternals: s.nodeInternals, }); @@ -42,13 +41,7 @@ const MiniMap = ({ nodeStrokeWidth = 2, maskColor = 'rgb(240, 242, 243, 0.7)', }: MiniMapProps) => { - const { - width: containerWidth, - height: containerHeight, - transform, - nodes, - nodeInternals, - } = useStore(selector, shallow); + const { width: containerWidth, height: containerHeight, transform, nodeInternals } = useStore(selector, shallow); const [tX, tY, tScale] = transform; const mapClasses = cc(['react-flow__minimap', className]); @@ -59,7 +52,9 @@ const MiniMap = ({ nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor ) as StringFunc; const nodeClassNameFunc = (nodeClassName instanceof Function ? nodeClassName : () => nodeClassName) as StringFunc; - const hasNodes = nodes && nodes.length; + 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 viewBB: Rect = { x: -tX / tScale, @@ -88,9 +83,9 @@ const MiniMap = ({ style={style} className={mapClasses} > - {nodes - .filter((node) => !node.isHidden && node.width && node.height) - .map((node) => { + {Array.from(nodeInternals) + .filter(([_, node]) => !node.isHidden && node.width && node.height) + .map(([_, node]) => { const positionAbsolute = nodeInternals.get(node.id)?.positionAbsolute; return ( diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index 8ce8e0f1..390132d1 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -27,7 +27,7 @@ interface ConnectionLineProps { CustomConnectionLineComponent?: ConnectionLineComponent; } -const selector = (s: ReactFlowState) => ({ nodeInternals: s.nodeInternals, nodes: s.nodes, transform: s.transform }); +const selector = (s: ReactFlowState) => ({ nodeInternals: s.nodeInternals, transform: s.transform }); export default ({ connectionNodeId, @@ -43,9 +43,9 @@ export default ({ const nodeId = connectionNodeId; const handleId = connectionHandleId; - const { nodeInternals, nodes, transform } = useStore(selector, shallow); + const { nodeInternals, transform } = useStore(selector, shallow); const sourceNodeInternals = useRef(nodeInternals.get(nodeId)); - const sourceNode = useRef(nodes.find((n) => n.id === nodeId)); + const sourceNode = useRef(nodeInternals.get(nodeId)); if ( !sourceNode.current || diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 58265fc4..deccc17b 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -56,7 +56,7 @@ export default (NodeComponent: ComponentType) => { unselectNodesAndEdges, unsetNodesSelection, updateNodePosition, - updateNodeDimensions, + // updateNodeDimensions, } = useStore(selector, shallow); const nodeElement = useRef(null); @@ -201,12 +201,6 @@ export default (NodeComponent: ComponentType) => { [node, onNodeDoubleClick] ); - useEffect(() => { - if (nodeElement.current && (!isHidden || !isInitialized)) { - updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); - } - }, [id, isHidden, sourcePosition, targetPosition, type, isInitialized]); - useEffect(() => { if (nodeElement.current) { const currNode = nodeElement.current; diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index bf46a6a2..95fac2c0 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -15,12 +15,14 @@ export interface NodesSelectionProps { onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void; onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; } - +// @TODO: work with nodeInternals instead of converting it to an array const selector = (s: ReactFlowState) => ({ transform: s.transform, selectedNodesBbox: s.selectedNodesBbox, selectionActive: s.selectionActive, - selectedNodes: s.nodes.filter((n) => n.selected), + selectedNodes: Array.from(s.nodeInternals) + .filter(([_, n]) => n.selected) + .map(([_, n]) => n), snapToGrid: s.snapToGrid, snapGrid: s.snapGrid, updateNodePosition: s.updateNodePosition, diff --git a/src/components/SelectionListener/index.tsx b/src/components/SelectionListener/index.tsx index d2cfd097..c8b59795 100644 --- a/src/components/SelectionListener/index.tsx +++ b/src/components/SelectionListener/index.tsx @@ -8,8 +8,11 @@ interface SelectionListenerProps { onSelectionChange: OnSelectionChangeFunc; } +// @TODO: work with nodeInternals instead of converting it to an array const selectedElementsSelector = (s: ReactFlowState) => ({ - selectedNodes: s.nodes.filter((n) => n.selected), + selectedNodes: Array.from(s.nodeInternals) + .filter(([_, n]) => n.selected) + .map(([_, node]) => node), selectedEdges: s.edges.filter((e) => e.selected), }); diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 3482db92..8a093d6e 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -3,8 +3,6 @@ import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types'; -import useVisibleNodes from '../../hooks/useVisibleNodes'; -import useNodeInternalsRef from '../../hooks/useNodeInternalsRef'; interface NodeRendererProps { nodeTypes: NodeTypesType; @@ -29,13 +27,20 @@ const selector = (s: ReactFlowState) => ({ updateNodeDimensions: s.updateNodeDimensions, snapGrid: s.snapGrid, snapToGrid: s.snapToGrid, + nodeInternals: s.nodeInternals, }); const NodeRenderer = (props: NodeRendererProps) => { - const { scale, nodesDraggable, nodesConnectable, elementsSelectable, updateNodeDimensions, snapGrid, snapToGrid } = - useStore(selector, shallow); - const nodeInternals = useNodeInternalsRef(); - const nodes = useVisibleNodes(props.onlyRenderVisibleElements); + const { + scale, + nodesDraggable, + nodesConnectable, + elementsSelectable, + updateNodeDimensions, + snapGrid, + snapToGrid, + nodeInternals, + } = useStore(selector, shallow); const resizeObserver = useMemo(() => { if (typeof ResizeObserver === 'undefined') { @@ -54,9 +59,9 @@ const NodeRenderer = (props: NodeRendererProps) => { return (
- {nodes.map((node) => { + {Array.from(nodeInternals).map(([_, node]) => { const nodeType = node.type || 'default'; - const internals = nodeInternals.current.get(node.id); + const internals = nodeInternals.get(node.id); if (!props.nodeTypes[nodeType]) { console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index 19743457..ead31a62 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -28,7 +28,9 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); useEffect(() => { - const { nodes, edges } = store.getState(); + const { nodeInternals, edges } = store.getState(); + // @TODO: work with nodeInternals instead of converting it to an array + const nodes = Array.from(nodeInternals).map(([_, node]) => node); const selectedNodes = nodes.filter((n) => n.selected); const selectedEdges = edges.filter((e) => e.selected); diff --git a/src/hooks/useNodeInternalsRef.ts b/src/hooks/useNodeInternalsRef.ts deleted file mode 100644 index d6b9b2d9..00000000 --- a/src/hooks/useNodeInternalsRef.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useRef, useEffect } from 'react'; - -import { useStoreApi } from '../store'; - -function useNodeInternalsRef() { - const store = useStoreApi(); - const nodeInternals = useRef(store.getState().nodeInternals); - - useEffect(() => store.subscribe((state) => (nodeInternals.current = state.nodeInternals)), []); - - return nodeInternals; -} - -export default useNodeInternalsRef; diff --git a/src/hooks/useOnLoadHandler.ts b/src/hooks/useOnLoadHandler.ts index ed128817..d75ba9f4 100644 --- a/src/hooks/useOnLoadHandler.ts +++ b/src/hooks/useOnLoadHandler.ts @@ -19,7 +19,9 @@ function useOnLoadHandler(onLoad: OnLoad | undefined) { }; const getNodes = (): Node[] => { - const { nodes = [] } = store.getState(); + const { nodeInternals } = store.getState(); + // @TODO: work with nodeInternals instead of converting it to an array + const nodes = Array.from(nodeInternals).map(([_, node]) => node); return nodes.map((n) => ({ ...n })); }; @@ -29,8 +31,9 @@ function useOnLoadHandler(onLoad: OnLoad | undefined) { }; const toObject = (): FlowExportObject => { - const { nodes = [], edges = [], transform } = store.getState(); - + const { nodeInternals, edges = [], transform } = store.getState(); + // @TODO: work with nodeInternals instead of converting it to an array + const nodes = Array.from(nodeInternals).map(([_, node]) => node); return { nodes: nodes.map((n) => ({ ...n })), edges: edges.map((e) => ({ ...e })), diff --git a/src/hooks/useVisibleNodes.ts b/src/hooks/useVisibleNodes.ts index d6c9bd96..4252db59 100644 --- a/src/hooks/useVisibleNodes.ts +++ b/src/hooks/useVisibleNodes.ts @@ -8,9 +8,11 @@ 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(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true) - : s.nodes; + ? getNodesInside(nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true) + : nodes; }, [onlyRenderVisible] ) diff --git a/src/hooks/useZoomPanHelper.ts b/src/hooks/useZoomPanHelper.ts index aacc48da..7ba41095 100644 --- a/src/hooks/useZoomPanHelper.ts +++ b/src/hooks/useZoomPanHelper.ts @@ -41,8 +41,9 @@ const useZoomPanHelper = (): ZoomPanHelperFunctions => { d3Zoom.transform(d3Selection, nextTransform); }, fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => { - const { nodes, width, height, minZoom, maxZoom } = store.getState(); - + const { nodeInternals, width, height, minZoom, maxZoom } = store.getState(); + // @TODO: work with nodeInternals instead of converting it to an array + const nodes = Array.from(nodeInternals).map(([_, node]) => node); if (!nodes.length) { return; } diff --git a/src/store/index.ts b/src/store/index.ts index c12e10c7..54c718ad 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -42,7 +42,6 @@ const createStore = () => width: 0, height: 0, transform: [0, 0, 1], - nodes: [], edges: [], onNodesChange: null, onEdgesChange: null, @@ -87,28 +86,29 @@ const createStore = () => setNodes: (nodes: Node[]) => { const nodeInternals = createNodeInternals(nodes, get().nodeInternals); - set({ nodes, nodeInternals }); + set({ nodeInternals }); }, setEdges: (edges: Edge[]) => { set({ edges }); }, updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { - const { onNodesChange, nodes, transform, nodeInternals } = get(); + const { onNodesChange, transform, nodeInternals } = get(); const nodesToChange: NodeChange[] = updates.reduce((res, update) => { - const node = nodes.find((n) => n.id === update.id); + const node = nodeInternals.get(update.id); if (node) { const dimensions = getDimensions(update.nodeElement); - const doUpdate = + const doUpdate = !!( dimensions.width && dimensions.height && - (node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate); + (node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate) + ); if (doUpdate) { const handleBounds = getHandleBounds(update.nodeElement, transform[2]); nodeInternals.set(node.id, { - ...nodeInternals.get(node.id), + ...node, handleBounds, ...dimensions, }); @@ -127,17 +127,19 @@ const createStore = () => set({ nodeInternals: new Map(nodeInternals) }); - onNodesChange?.(nodesToChange); + if (nodesToChange?.length > 0) { + onNodesChange?.(nodesToChange); + } }, updateNodePosition: ({ id, diff, dragging }: NodeDiffUpdate) => { - const { onNodesChange, nodes, nodeExtent, nodeInternals } = get(); + const { onNodesChange, nodeExtent, nodeInternals } = get(); if (onNodesChange) { - const matchingNodes = nodes.filter((n) => !!(n.selected || n.id === id)); - + const nodes = Array.from(nodeInternals); + const matchingNodes = nodes.filter(([_, n]) => !!(n.selected || n.id === id)); if (matchingNodes?.length) { onNodesChange( - matchingNodes?.map((node) => { + matchingNodes?.map(([_, node]) => { const change: NodeDimensionChange = { id: node.id, type: 'dimensions', @@ -190,7 +192,7 @@ const createStore = () => }); }, updateUserSelection: (mousePos: XYPosition) => { - const { userSelectionRect, nodes, edges, transform, onNodesChange, onEdgesChange } = get(); + const { userSelectionRect, nodeInternals, edges, transform, onNodesChange, onEdgesChange } = get(); const startX = userSelectionRect.startX ?? 0; const startY = userSelectionRect.startY ?? 0; @@ -202,6 +204,8 @@ const createStore = () => 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); @@ -218,7 +222,9 @@ const createStore = () => }); }, unsetUserSelection: () => { - const { userSelectionRect, nodes } = get(); + 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 = { @@ -240,8 +246,9 @@ const createStore = () => set(stateUpdate); }, addSelectedElements: (selectedElementsArr: Array) => { - const { multiSelectionActive, onNodesChange, onEdgesChange, nodes, edges } = get(); - + const { multiSelectionActive, onNodesChange, onEdgesChange, nodeInternals, edges } = get(); + // @TODO: work with nodeInternals instead of converting it to an array + const nodes = Array.from(nodeInternals).map(([_, node]) => node); let changedNodes; let changedEdges; @@ -266,7 +273,9 @@ const createStore = () => } }, unselectNodesAndEdges: () => { - const { nodes, edges, onNodesChange, onEdgesChange } = get(); + 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.map((n) => { n.selected = false; @@ -308,8 +317,9 @@ const createStore = () => }, resetSelectedElements: () => { - const { nodes, edges, onNodesChange, onEdgesChange } = get(); - + 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)); @@ -320,16 +330,18 @@ const createStore = () => onEdgesChange?.(edgesToUnselect as EdgeChange[]); } }, - setNodeExtent: (nodeExtent: CoordinateExtent) => + setNodeExtent: (nodeExtent: CoordinateExtent) => { + const { nodeInternals } = get(); + + nodeInternals.forEach((node) => { + node.positionAbsolute = clampPosition(node.position, nodeExtent); + }); + set({ nodeExtent, - nodes: get().nodes.map((node) => { - return { - ...node, - position: clampPosition(node.position, nodeExtent), - }; - }), - }), + nodeInternals: new Map(nodeInternals), + }); + }, unsetNodesSelection: () => set({ nodesSelectionActive: false }), updateTransform: (transform: Transform) => set({ transform }), updateSize: (size: Dimensions) => set({ width: size.width || 500, height: size.height || 500 }), diff --git a/src/store/utils.ts b/src/store/utils.ts index be01a1b0..25505d50 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -39,11 +39,11 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) const z = node.zIndex ? node.zIndex : node.dragging || node.selected ? 1000 : 0; const internals: NodeInternalsItem = { ...nodeInternals.get(node.id), - id: node.id, - width: node.width || null, - height: node.height || null, - position: node.position, - positionAbsolute: node.position, + ...node, + positionAbsolute: { + x: node.position.x, + y: node.position.y, + }, z, }; if (node.parentNode) { @@ -56,6 +56,10 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) nodes.forEach((node) => { const updatedInternals: NodeInternalsItem = nextNodeInternals.get(node.id)!; + if (node.parentNode && !nextNodeInternals.has(node.parentNode)) { + throw new Error(`Parent node ${node.parentNode} not found`); + } + if (node.parentNode || parentNodes[node.id]) { let startingZ = updatedInternals.z || 0; diff --git a/src/types/general.ts b/src/types/general.ts index 68dd316e..5cafafef 100644 --- a/src/types/general.ts +++ b/src/types/general.ts @@ -134,7 +134,6 @@ export interface ReactFlowState { width: number; height: number; transform: Transform; - nodes: Node[]; nodeInternals: NodeInternals; edges: Edge[]; selectedNodesBbox: Rect; diff --git a/src/types/nodes.ts b/src/types/nodes.ts index f2b7b02a..60d052bc 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -101,12 +101,7 @@ export type NodeDimensionUpdate = { forceUpdate?: boolean; }; -export type NodeInternalsItem = { - id?: string; - width?: number | null; - height?: number | null; - parentNode?: string; - position?: XYPosition; +export type NodeInternalsItem = Node & { positionAbsolute?: XYPosition; handleBounds?: NodeHandleBounds; z?: number; From fe9c1f02dc6167c61aa5b14a5bc412927487115e Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 18 Nov 2021 13:06:14 +0100 Subject: [PATCH 062/263] refactor(nodes-edges): rename isHidden to hidden --- example/src/Basic/index.tsx | 2 +- example/src/Hidden/index.tsx | 12 ++++++------ example/src/NestedNodes/index.tsx | 2 +- example/src/UpdateNode/index.tsx | 2 +- src/additional-components/MiniMap/index.tsx | 2 +- src/components/Edges/wrapEdge.tsx | 4 ++-- src/components/Nodes/wrapNode.tsx | 4 ++-- src/container/EdgeRenderer/index.tsx | 2 +- src/container/NodeRenderer/index.tsx | 2 +- src/hooks/useZoomPanHelper.ts | 2 +- src/types/edges.ts | 4 ++-- src/types/nodes.ts | 4 ++-- 12 files changed, 21 insertions(+), 21 deletions(-) diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index 0184971e..2335443f 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -140,7 +140,7 @@ const BasicFlow = () => { const toggleChildNodes = () => { setNodes((nds) => { return nds.map((n) => { - n.isHidden = !!n.parentNode && !n.isHidden; + n.hidden = !!n.parentNode && !n.hidden; return n; }); }); diff --git a/example/src/Hidden/index.tsx b/example/src/Hidden/index.tsx index b0249310..4ac8eaa4 100644 --- a/example/src/Hidden/index.tsx +++ b/example/src/Hidden/index.tsx @@ -4,10 +4,10 @@ import { useEffect } from 'react'; import ReactFlow, { addEdge, MiniMap, Controls, Connection, Edge, Node } from 'react-flow-renderer'; const initialNodes: Node[] = [ - { id: '1', type: 'input', isHidden: true, data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, - { id: '2', isHidden: true, data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, - { id: '3', isHidden: true, data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, - { id: '4', isHidden: true, data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, + { id: '1', type: 'input', hidden: true, data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, + { id: '2', hidden: true, data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, + { id: '3', hidden: true, data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, + { id: '4', hidden: true, data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, ]; const initialEdges: Edge[] = [ @@ -16,9 +16,9 @@ const initialEdges: Edge[] = [ { id: 'e3-4', source: '3', target: '4' }, ]; -const setHidden = (isHidden: boolean) => (els: any[]) => +const setHidden = (hidden: boolean) => (els: any[]) => els.map((e: any) => { - e.isHidden = isHidden; + e.hidden = hidden; return e; }); diff --git a/example/src/NestedNodes/index.tsx b/example/src/NestedNodes/index.tsx index 232578e0..ed445a64 100644 --- a/example/src/NestedNodes/index.tsx +++ b/example/src/NestedNodes/index.tsx @@ -129,7 +129,7 @@ const NestedFlow = () => { const toggleChildNodes = () => { setNodes((nds) => { return nds.map((n) => { - n.isHidden = !!n.parentNode && !n.isHidden; + n.hidden = !!n.parentNode && !n.hidden; return n; }); }); diff --git a/example/src/UpdateNode/index.tsx b/example/src/UpdateNode/index.tsx index 8a4269aa..e38babad 100644 --- a/example/src/UpdateNode/index.tsx +++ b/example/src/UpdateNode/index.tsx @@ -52,7 +52,7 @@ const UpdateNode = () => { nds.map((n) => { if (n.id === '1' || n.id === 'e1-2') { // when you update a simple type you can just update the value - n.isHidden = nodeHidden; + n.hidden = nodeHidden; } return n; diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index bf5d7e02..174caf4f 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -84,7 +84,7 @@ const MiniMap = ({ className={mapClasses} > {Array.from(nodeInternals) - .filter(([_, node]) => !node.isHidden && node.width && node.height) + .filter(([_, node]) => !node.hidden && node.width && node.height) .map(([_, node]) => { const positionAbsolute = nodeInternals.get(node.id)?.positionAbsolute; diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx index eadf55f8..a89eb28e 100644 --- a/src/components/Edges/wrapEdge.tsx +++ b/src/components/Edges/wrapEdge.tsx @@ -42,7 +42,7 @@ export default (EdgeComponent: ComponentType) => { sourcePosition, targetPosition, elementsSelectable, - isHidden, + hidden, sourceHandleId, targetHandleId, handleEdgeUpdate, @@ -204,7 +204,7 @@ export default (EdgeComponent: ComponentType) => { const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart)})`, [markerStart]); const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd)})`, [markerEnd]); - if (isHidden) { + if (hidden) { return null; } diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index deccc17b..15c0e7c1 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -41,7 +41,7 @@ export default (NodeComponent: ComponentType) => { selectNodesOnDrag, sourcePosition, targetPosition, - isHidden, + hidden, isInitialized, snapToGrid, snapGrid, @@ -210,7 +210,7 @@ export default (NodeComponent: ComponentType) => { } }, []); - if (isHidden) { + if (hidden) { return null; } diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 7d6329fc..a970d5b6 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -190,7 +190,7 @@ const Edge = memo( targetPosition={targetPosition} elementsSelectable={elementsSelectable} markerEndId={markerEndId} - isHidden={edge.isHidden} + hidden={edge.hidden} onConnectEdge={onConnectEdge} handleEdgeUpdate={typeof onEdgeUpdate !== 'undefined'} onContextMenu={onEdgeContextMenu} diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 8a093d6e..2dee0dd5 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -87,7 +87,7 @@ const NodeRenderer = (props: NodeRendererProps) => { data={node.data} sourcePosition={node.sourcePosition} targetPosition={node.targetPosition} - isHidden={node.isHidden} + hidden={node.hidden} xPos={internals?.positionAbsolute?.x || 0} yPos={internals?.positionAbsolute?.y || 0} dragging={!!node.dragging} diff --git a/src/hooks/useZoomPanHelper.ts b/src/hooks/useZoomPanHelper.ts index 7ba41095..735ef4b6 100644 --- a/src/hooks/useZoomPanHelper.ts +++ b/src/hooks/useZoomPanHelper.ts @@ -48,7 +48,7 @@ const useZoomPanHelper = (): ZoomPanHelperFunctions => { return; } - const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.isHidden)); + const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.hidden)); const [x, y, zoom] = getTransformForBounds( bounds, width, diff --git a/src/types/edges.ts b/src/types/edges.ts index 7e7f1875..21ea21e2 100644 --- a/src/types/edges.ts +++ b/src/types/edges.ts @@ -21,7 +21,7 @@ export interface Edge { labelBgBorderRadius?: number; style?: CSSProperties; animated?: boolean; - isHidden?: boolean; + hidden?: boolean; data?: T; className?: string; sourceNode?: Node; @@ -88,7 +88,7 @@ export interface WrapEdgeProps { sourcePosition: Position; targetPosition: Position; elementsSelectable?: boolean; - isHidden?: boolean; + hidden?: boolean; handleEdgeUpdate: boolean; onConnectEdge: OnConnect; onContextMenu?: EdgeMouseHandler; diff --git a/src/types/nodes.ts b/src/types/nodes.ts index 60d052bc..fe11ef48 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -14,7 +14,7 @@ export interface Node { className?: string; targetPosition?: Position; sourcePosition?: Position; - isHidden?: boolean; + hidden?: boolean; selected?: boolean; dragging?: boolean; draggable?: boolean; @@ -73,7 +73,7 @@ export interface WrapNodeProps { className?: string; sourcePosition?: Position; targetPosition?: Position; - isHidden?: boolean; + hidden?: boolean; isInitialized?: boolean; snapToGrid?: boolean; snapGrid?: SnapGrid; From e1f864f270a81e2d289c203981b862c7b1658ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 18 Nov 2021 16:29:06 +0100 Subject: [PATCH 063/263] feat(flow): add properties for configuring no-interaction classes --- src/container/EdgeRenderer/index.tsx | 7 ++++- src/container/FlowRenderer/index.tsx | 6 ++++ src/container/GraphView/index.tsx | 8 ++++++ src/container/NodeRenderer/index.tsx | 4 ++- src/container/ReactFlow/index.tsx | 9 ++++++ src/container/ZoomPane/index.tsx | 41 ++++++++++++++++++---------- 6 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 7d6329fc..9d242090 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -1,5 +1,6 @@ import React, { memo, CSSProperties, useCallback } from 'react'; import shallow from 'zustand/shallow'; +import cc from 'classcat'; import { useStore } from '../../store'; import ConnectionLine from '../../components/ConnectionLine/index'; @@ -35,6 +36,7 @@ interface EdgeRendererProps { onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge) => void; onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; edgeUpdaterRadius?: number; + noPanClassName?: string; } interface EdgeWrapperProps { @@ -63,6 +65,7 @@ interface EdgeWrapperProps { targetNodeX?: number; targetNodeY?: number; targetNodeHandleBounds?: NodeHandleBounds; + noPanClassName?: string; } const Edge = memo( @@ -92,6 +95,7 @@ const Edge = memo( targetNodeX, targetNodeY, targetNodeHandleBounds, + noPanClassName, }: EdgeWrapperProps) => { const sourceHandleId = edge.sourceHandle || null; const targetHandleId = edge.targetHandle || null; @@ -163,7 +167,7 @@ const Edge = memo( { onEdgeUpdate={props.onEdgeUpdate} edgeTypes={props.edgeTypes} connectionMode={connectionMode} + noPanClassName={props.noPanClassName} /> ); })} diff --git a/src/container/FlowRenderer/index.tsx b/src/container/FlowRenderer/index.tsx index 9cfc3216..9a34403d 100644 --- a/src/container/FlowRenderer/index.tsx +++ b/src/container/FlowRenderer/index.tsx @@ -59,6 +59,9 @@ const FlowRenderer = ({ onSelectionDrag, onSelectionDragStop, onSelectionContextMenu, + noDragClassName, + noZoomClassName, + noPanClassName, }: FlowRendererProps) => { const { unsetNodesSelection, resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow); @@ -107,6 +110,9 @@ const FlowRenderer = ({ defaultZoom={defaultZoom} zoomActivationKeyCode={zoomActivationKeyCode} preventScrolling={preventScrolling} + noDragClassName={noDragClassName} + noZoomClassName={noZoomClassName} + noPanClassName={noPanClassName} > {children} diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 34c7c668..e08dd5ee 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -76,6 +76,9 @@ const GraphView = ({ edgeUpdaterRadius, onEdgeUpdateStart, onEdgeUpdateEnd, + noDragClassName, + noZoomClassName, + noPanClassName, }: GraphViewProps) => { useOnLoadHandler(onLoad); @@ -106,6 +109,9 @@ const GraphView = ({ onSelectionDragStop={onSelectionDragStop} onSelectionContextMenu={onSelectionContextMenu} preventScrolling={preventScrolling} + noDragClassName={noDragClassName} + noZoomClassName={noZoomClassName} + noPanClassName={noPanClassName} > diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 3482db92..94c74c0e 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -1,5 +1,6 @@ import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; import shallow from 'zustand/shallow'; +import cc from 'classcat'; import { useStore } from '../../store'; import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types'; @@ -19,6 +20,7 @@ interface NodeRendererProps { onNodeDrag?: (event: MouseEvent, node: Node) => void; onNodeDragStop?: (event: MouseEvent, node: Node) => void; onlyRenderVisibleElements: boolean; + noPanClassName?: string; } const selector = (s: ReactFlowState) => ({ @@ -76,7 +78,7 @@ const NodeRenderer = (props: NodeRendererProps) => { , 'on onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge) => void; onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; edgeUpdaterRadius?: number; + noDragClassName?: string; + noZoomClassName?: string; + noPanClassName?: string; } export type ReactFlowRefType = HTMLDivElement; @@ -212,6 +215,9 @@ const ReactFlow = forwardRef( edgeUpdaterRadius = 10, onNodesChange, onEdgesChange, + noDragClassName = 'nodrag', + noZoomClassName = 'nowheel', + noPanClassName = 'nopan', ...rest }, ref @@ -276,6 +282,9 @@ const ReactFlow = forwardRef( onEdgeUpdateEnd={onEdgeUpdateEnd} edgeUpdaterRadius={edgeUpdaterRadius} defaultMarkerColor={defaultMarkerColor} + noDragClassName={noDragClassName} + noZoomClassName={noZoomClassName} + noPanClassName={noPanClassName} /> @@ -40,7 +43,8 @@ const eventToFlowTransform = (eventTransform: any): FlowTransform => ({ zoom: eventTransform.k, }); -const hasNoWheelClass = (event: any) => event.target.closest('.nowheel'); +const isWrappedWithClass = (event: any, className: string | undefined) => event.target.closest(`.${className}`); +// const hasNoWheelClass = (event: any) => event.target.closest('.nowheel'); const selector = (s: ReactFlowState) => ({ d3Zoom: s.d3Zoom, @@ -69,6 +73,8 @@ const ZoomPane = ({ zoomActivationKeyCode, preventScrolling = true, children, + noZoomClassName, + noPanClassName, }: ZoomPaneProps) => { const store = useStoreApi(); const zoomPane = useRef(null); @@ -106,7 +112,7 @@ const ZoomPane = ({ if (panOnScroll && !zoomActivationKeyPressed) { d3Selection .on('wheel', (event: any) => { - if (hasNoWheelClass(event)) { + if (isWrappedWithClass(event, noZoomClassName)) { return false; } event.preventDefault(); @@ -140,7 +146,7 @@ const ZoomPane = ({ } else if (typeof d3ZoomHandler !== 'undefined') { d3Selection .on('wheel', (event: any) => { - if (!preventScrolling || hasNoWheelClass(event)) { + if (!preventScrolling || isWrappedWithClass(event, noZoomClassName)) { return null; } @@ -158,6 +164,7 @@ const ZoomPane = ({ zoomActivationKeyPressed, zoomOnPinch, preventScrolling, + noZoomClassName, ]); useEffect(() => { @@ -230,22 +237,28 @@ const ZoomPane = ({ return false; } - if (hasNoWheelClass(event) && event.type === 'wheel') { + // if the target element is inside the nowheel class, we prevent zooming + if (isWrappedWithClass(event, noZoomClassName) && event.type === 'wheel') { + return false; + } + + // if the target element is inside the nopan class, we prevent panning + if (isWrappedWithClass(event, noPanClassName) && event.type !== 'wheel') { return false; } // when the target element is a node, we still allow zooming - if ( - (event.target.closest('.react-flow__node') || event.target.closest('.react-flow__edge')) && - event.type !== 'wheel' - ) { - return false; - } + // if ( + // (event.target.closest('.react-flow__node') || event.target.closest('.react-flow__edge')) && + // event.type !== 'wheel' + // ) { + // return false; + // } - // when the target element is a node selection, we still allow zooming - if (event.target.closest('.react-flow__nodesselection') && event.type !== 'wheel') { - return false; - } + // // when the target element is a node selection, we still allow zooming + // if (event.target.closest('.react-flow__nodesselection') && event.type !== 'wheel') { + // return false; + // } if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') { return false; From 004dcc652895f1023430877519a5b8785b511073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 18 Nov 2021 16:53:41 +0100 Subject: [PATCH 064/263] feat(selection): disable pane movement when selection is dragged --- package-lock.json | 4 ++-- package.json | 2 +- src/components/NodesSelection/index.tsx | 5 ++++- src/container/FlowRenderer/index.tsx | 1 + 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1756d2e2..9299f831 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.14", + "version": "10.0.0-next.17", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "10.0.0-next.14", + "version": "10.0.0-next.17", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/package.json b/package.json index b2fdbe7a..1133fcf0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.14", + "version": "10.0.0-next.17", "engines": { "node": ">=12" }, diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index 95fac2c0..b75c5ea6 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -5,6 +5,7 @@ import React, { useMemo, useCallback, useRef, MouseEvent } from 'react'; import ReactDraggable, { DraggableData } from 'react-draggable'; +import cc from 'classcat'; import { useStore } from '../../store'; import { Node, ReactFlowState } from '../../types'; @@ -14,6 +15,7 @@ export interface NodesSelectionProps { onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void; onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; + noPanClassName?: string; } // @TODO: work with nodeInternals instead of converting it to an array const selector = (s: ReactFlowState) => ({ @@ -33,6 +35,7 @@ export default ({ onSelectionDrag, onSelectionDragStop, onSelectionContextMenu, + noPanClassName, }: NodesSelectionProps) => { const { transform, selectedNodesBbox, selectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } = useStore(selector); @@ -105,7 +108,7 @@ export default ({ } return ( -
+
)}
Date: Thu, 18 Nov 2021 17:57:45 +0100 Subject: [PATCH 065/263] refactor(subgraphs): calculate z for child nodes --- package-lock.json | 4 ++-- package.json | 2 +- src/store/utils.ts | 31 ++++++++++--------------------- src/types/nodes.ts | 2 +- src/utils/index.ts | 2 ++ 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1756d2e2..17711e21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.14", + "version": "10.0.0-next.16", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "10.0.0-next.14", + "version": "10.0.0-next.16", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/package.json b/package.json index b2fdbe7a..5f33733e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.14", + "version": "10.0.0-next.16", "engines": { "node": ">=12" }, diff --git a/src/store/utils.ts b/src/store/utils.ts index fbfc1a09..8247df6b 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -1,4 +1,5 @@ import { Node, Edge, NodeInternals, NodeInternalsItem, XYZPosition } from '../types'; +import { isNumeric } from '../utils'; type ParentNodes = Record; @@ -13,22 +14,10 @@ function calculateXYZPosition( } const parentNode = nodeInternals.get(node.parentNode)!; - // +1 for each recursion level - let zAddition = 1; - - // +2 if it's a parent node, so that groups/parents are always on top - if (parentNodes[node.parentNode!]) { - zAddition = 2; - } - - if (parentNode.z) { - zAddition += parentNode.z; - } - return calculateXYZPosition(parentNode, nodeInternals, parentNodes, { x: (result.x ?? 0) + (parentNode.position?.x ?? 0), y: (result.y ?? 0) + (parentNode.position?.y ?? 0), - z: (result.z ?? 0) + zAddition, + z: parentNode.z > node.z ? parentNode.z : node.z, }); } @@ -72,7 +61,7 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) const parentNodes: ParentNodes = {}; nodes.forEach((node) => { - const z = node.zIndex ? node.zIndex : node.dragging || node.selected ? 1000 : 0; + const z = isNumeric(node.zIndex) ? node.zIndex : node.dragging || node.selected ? 1000 : 0; const internals: NodeInternalsItem = { ...nodeInternals.get(node.id), ...node, @@ -89,26 +78,26 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) nextNodeInternals.set(node.id, internals); }); - nodes.forEach((node) => { - const updatedInternals: NodeInternalsItem = nextNodeInternals.get(node.id)!; - + nextNodeInternals.forEach((node) => { if (node.parentNode && !nextNodeInternals.has(node.parentNode)) { throw new Error(`Parent node ${node.parentNode} not found`); } if (node.parentNode || parentNodes[node.id]) { - const { x, y } = calculateXYZPosition(node, nextNodeInternals, parentNodes, { + const { x, y, z } = calculateXYZPosition(node, nextNodeInternals, parentNodes, { ...node.position, - z: 0, + z: node.z, }); - updatedInternals.positionAbsolute = { + node.positionAbsolute = { x, y, }; + node.z = z; + if (parentNodes[node.id]) { - updatedInternals.isParent = true; + node.isParent = true; } } }); diff --git a/src/types/nodes.ts b/src/types/nodes.ts index fe11ef48..ed9fcfdc 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -104,7 +104,7 @@ export type NodeDimensionUpdate = { export type NodeInternalsItem = Node & { positionAbsolute?: XYPosition; handleBounds?: NodeHandleBounds; - z?: number; + z: number; isParent?: boolean; }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 084f5beb..926977cb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -38,3 +38,5 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({ export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect => boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2))); + +export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n); From 9d159c1e87b0ebc2bb09d89b81926e33d2aa2d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 18 Nov 2021 18:01:20 +0100 Subject: [PATCH 066/263] chore(types): enforce defaultProps inside react flow component --- src/components/Nodes/wrapNode.tsx | 5 ++++- src/container/FlowRenderer/index.tsx | 6 ++---- src/container/GraphView/index.tsx | 8 ++++++-- src/container/NodeRenderer/index.tsx | 8 +++++--- src/container/ReactFlow/index.tsx | 9 +++++---- src/container/ZoomPane/index.tsx | 16 +++++++--------- src/types/nodes.ts | 2 ++ 7 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 15c0e7c1..20348106 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -50,6 +50,8 @@ export default (NodeComponent: ComponentType) => { dragHandle, zIndex, isParent, + noPanClassName, + noDragClassName, }: WrapNodeProps) => { const { addSelectedElements, @@ -217,6 +219,7 @@ export default (NodeComponent: ComponentType) => { const nodeClasses = cc([ 'react-flow__node', `react-flow__node-${type}`, + noPanClassName, className, { selected, @@ -232,7 +235,7 @@ export default (NodeComponent: ComponentType) => { onStop={onDragStop} scale={scale} disabled={!isDraggable} - cancel=".nodrag" + cancel={`.${noDragClassName}`} nodeRef={nodeElement} grid={grid} enableUserSelectHack={false} diff --git a/src/container/FlowRenderer/index.tsx b/src/container/FlowRenderer/index.tsx index 8b8d3088..458e9c59 100644 --- a/src/container/FlowRenderer/index.tsx +++ b/src/container/FlowRenderer/index.tsx @@ -59,8 +59,7 @@ const FlowRenderer = ({ onSelectionDrag, onSelectionDragStop, onSelectionContextMenu, - noDragClassName, - noZoomClassName, + noWheelClassName, noPanClassName, }: FlowRendererProps) => { const { unsetNodesSelection, resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow); @@ -110,8 +109,7 @@ const FlowRenderer = ({ defaultZoom={defaultZoom} zoomActivationKeyCode={zoomActivationKeyCode} preventScrolling={preventScrolling} - noDragClassName={noDragClassName} - noZoomClassName={noZoomClassName} + noWheelClassName={noWheelClassName} noPanClassName={noPanClassName} > {children} diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index e08dd5ee..5561833d 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -20,6 +20,9 @@ export interface GraphViewProps extends Omit { useOnLoadHandler(onLoad); @@ -110,7 +113,7 @@ const GraphView = ({ onSelectionContextMenu={onSelectionContextMenu} preventScrolling={preventScrolling} noDragClassName={noDragClassName} - noZoomClassName={noZoomClassName} + noWheelClassName={noWheelClassName} noPanClassName={noPanClassName} > @@ -147,6 +150,7 @@ const GraphView = ({ selectNodesOnDrag={selectNodesOnDrag} onlyRenderVisibleElements={onlyRenderVisibleElements} noPanClassName={noPanClassName} + noDragClassName={noDragClassName} /> diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index c43bb847..a60fdb88 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -1,6 +1,5 @@ import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; import shallow from 'zustand/shallow'; -import cc from 'classcat'; import { useStore } from '../../store'; import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types'; @@ -18,7 +17,8 @@ interface NodeRendererProps { onNodeDrag?: (event: MouseEvent, node: Node) => void; onNodeDragStop?: (event: MouseEvent, node: Node) => void; onlyRenderVisibleElements: boolean; - noPanClassName?: string; + noPanClassName: string; + noDragClassName: string; } const selector = (s: ReactFlowState) => ({ @@ -83,7 +83,7 @@ const NodeRenderer = (props: NodeRendererProps) => { { dragHandle={node.dragHandle} zIndex={internals?.z || 0} isParent={!!internals?.isParent} + noDragClassName={props.noDragClassName} + noPanClassName={props.noPanClassName} /> ); })} diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index 66a007f1..87c3bbfb 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -5,6 +5,7 @@ import React, { MouseEvent as ReactMouseEvent, WheelEvent, forwardRef, + FunctionComponent, } from 'react'; import cc from 'classcat'; @@ -130,7 +131,7 @@ export interface ReactFlowProps extends Omit, 'on onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; edgeUpdaterRadius?: number; noDragClassName?: string; - noZoomClassName?: string; + noWheelClassName?: string; noPanClassName?: string; } @@ -139,7 +140,7 @@ export type ReactFlowRefType = HTMLDivElement; const initSnapGrid: [number, number] = [15, 15]; const initDefaultPosition: [number, number] = [0, 0]; -const ReactFlow = forwardRef( +const ReactFlow: FunctionComponent = forwardRef( ( { nodes = [], @@ -216,7 +217,7 @@ const ReactFlow = forwardRef( onNodesChange, onEdgesChange, noDragClassName = 'nodrag', - noZoomClassName = 'nowheel', + noWheelClassName = 'nowheel', noPanClassName = 'nopan', ...rest }, @@ -283,7 +284,7 @@ const ReactFlow = forwardRef( edgeUpdaterRadius={edgeUpdaterRadius} defaultMarkerColor={defaultMarkerColor} noDragClassName={noDragClassName} - noZoomClassName={noZoomClassName} + noWheelClassName={noWheelClassName} noPanClassName={noPanClassName} /> @@ -44,7 +43,6 @@ const eventToFlowTransform = (eventTransform: any): FlowTransform => ({ }); const isWrappedWithClass = (event: any, className: string | undefined) => event.target.closest(`.${className}`); -// const hasNoWheelClass = (event: any) => event.target.closest('.nowheel'); const selector = (s: ReactFlowState) => ({ d3Zoom: s.d3Zoom, @@ -73,7 +71,7 @@ const ZoomPane = ({ zoomActivationKeyCode, preventScrolling = true, children, - noZoomClassName, + noWheelClassName, noPanClassName, }: ZoomPaneProps) => { const store = useStoreApi(); @@ -112,7 +110,7 @@ const ZoomPane = ({ if (panOnScroll && !zoomActivationKeyPressed) { d3Selection .on('wheel', (event: any) => { - if (isWrappedWithClass(event, noZoomClassName)) { + if (isWrappedWithClass(event, noWheelClassName)) { return false; } event.preventDefault(); @@ -146,7 +144,7 @@ const ZoomPane = ({ } else if (typeof d3ZoomHandler !== 'undefined') { d3Selection .on('wheel', (event: any) => { - if (!preventScrolling || isWrappedWithClass(event, noZoomClassName)) { + if (!preventScrolling || isWrappedWithClass(event, noWheelClassName)) { return null; } @@ -164,7 +162,7 @@ const ZoomPane = ({ zoomActivationKeyPressed, zoomOnPinch, preventScrolling, - noZoomClassName, + noWheelClassName, ]); useEffect(() => { @@ -238,7 +236,7 @@ const ZoomPane = ({ } // if the target element is inside the nowheel class, we prevent zooming - if (isWrappedWithClass(event, noZoomClassName) && event.type === 'wheel') { + if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') { return false; } diff --git a/src/types/nodes.ts b/src/types/nodes.ts index fe11ef48..ba79bf09 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -82,6 +82,8 @@ export interface WrapNodeProps { dragHandle?: string; zIndex: number; isParent: boolean; + noPanClassName: string; + noDragClassName: string; } export type NodeHandleBounds = { From 8a095df19207adc052f266f847a150951946ecc0 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 23 Nov 2021 17:58:53 +0100 Subject: [PATCH 067/263] refactor(nodes): only observe node when not hidden --- example/src/Hidden/index.tsx | 2 +- src/components/Nodes/wrapNode.tsx | 4 ++-- src/container/EdgeRenderer/index.tsx | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/example/src/Hidden/index.tsx b/example/src/Hidden/index.tsx index 4ac8eaa4..4957c046 100644 --- a/example/src/Hidden/index.tsx +++ b/example/src/Hidden/index.tsx @@ -33,7 +33,7 @@ const HiddenFlow = () => { useEffect(() => { setNodes(setHidden(isHidden)); - setNodes(setHidden(isHidden)); + setEdges(setHidden(isHidden)); }, [isHidden]); return ( diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 20348106..dc3a495a 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -204,13 +204,13 @@ export default (NodeComponent: ComponentType) => { ); useEffect(() => { - if (nodeElement.current) { + if (nodeElement.current && !hidden) { const currNode = nodeElement.current; resizeObserver?.observe(currNode); return () => resizeObserver?.unobserve(currNode); } - }, []); + }, [hidden]); if (hidden) { return null; diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index ff6794da..a5c0e426 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -41,6 +41,7 @@ interface EdgeRendererProps { interface EdgeWrapperProps { edge: Edge; + hidden: boolean; edgeTypes: any; markerEndId?: string; onEdgeClick?: (event: React.MouseEvent, node: Edge) => void; @@ -71,6 +72,7 @@ interface EdgeWrapperProps { const Edge = memo( ({ edge, + hidden, edgeTypes, markerEndId, onEdgeClick, @@ -194,7 +196,7 @@ const Edge = memo( targetPosition={targetPosition} elementsSelectable={elementsSelectable} markerEndId={markerEndId} - hidden={edge.hidden} + hidden={hidden} onConnectEdge={onConnectEdge} handleEdgeUpdate={typeof onEdgeUpdate !== 'undefined'} onContextMenu={onEdgeContextMenu} @@ -265,6 +267,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {