From cf38388f149dadf3e9981d9b233f7aa1464d2ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20Mo=CC=88ller?= Date: Mon, 9 Nov 2020 17:23:59 +0100 Subject: [PATCH 01/43] refactor(zoom): move zoom methods into own hook, detatch from store --- example/src/Interaction/index.js | 1 + src/additional-components/Controls/index.tsx | 35 +++--- src/container/GraphView/index.tsx | 11 +- src/container/ReactFlow/Wrapper.tsx | 7 +- src/container/ZoomPane/index.tsx | 32 +++++- src/hooks/useZoomPanHelper.ts | 67 ++++++++++++ src/store/index.ts | 109 ++----------------- src/utils/graph.ts | 29 +---- 8 files changed, 139 insertions(+), 152 deletions(-) create mode 100644 src/hooks/useZoomPanHelper.ts diff --git a/example/src/Interaction/index.js b/example/src/Interaction/index.js index 90819509..39e83887 100644 --- a/example/src/Interaction/index.js +++ b/example/src/Interaction/index.js @@ -50,6 +50,7 @@ const InteractionFlow = () => { onPaneClick={captureZoomClick ? onPaneClick : undefined} onPaneScroll={captureZoomScroll ? onPaneScroll : undefined} onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined} + onMoveEnd={(evt) => console.log('on move end', evt)} > diff --git a/src/additional-components/Controls/index.tsx b/src/additional-components/Controls/index.tsx index ae5a53f3..983701e9 100644 --- a/src/additional-components/Controls/index.tsx +++ b/src/additional-components/Controls/index.tsx @@ -10,6 +10,7 @@ import LockIcon from '../../../assets/icons/lock.svg'; import UnlockIcon from '../../../assets/icons/unlock.svg'; import './style.css'; +import useZoomPanHelper from '../../hooks/useZoomPanHelper'; export interface ControlProps extends React.HTMLAttributes { showZoom?: boolean; @@ -33,9 +34,7 @@ const Controls = ({ className, }: ControlProps) => { const setInteractive = useStoreActions((actions) => actions.setInteractive); - const fitView = useStoreActions((actions) => actions.fitView); - const zoomIn = useStoreActions((actions) => actions.zoomIn); - const zoomOut = useStoreActions((actions) => actions.zoomOut); + const { zoomIn, zoomOut, fitView } = useZoomPanHelper(); const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable); const mapClasses = cc(['react-flow__controls', className]); @@ -44,20 +43,26 @@ const Controls = ({
{showZoom && ( <> -
{ - zoomIn(); - if (onZoomIn) { - onZoomIn(); - } - }}> +
{ + zoomIn(); + if (onZoomIn) { + onZoomIn(); + } + }} + >
-
{ - zoomOut(); - if (onZoomOut) { - onZoomOut(); - } - }}> +
{ + zoomOut(); + if (onZoomOut) { + onZoomOut(); + } + }} + >
diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index eb68438d..3f9ce248 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -22,6 +22,7 @@ import { OnConnectEndFunc, TranslateExtent, } from '../../types'; +import useZoomPanHelper from '../../hooks/useZoomPanHelper'; export interface GraphViewProps { elements: Elements; @@ -145,10 +146,8 @@ const GraphView = ({ const setMinZoom = useStoreActions((actions) => actions.setMinZoom); const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom); const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent); - const fitView = useStoreActions((actions) => actions.fitView); - const zoom = useStoreActions((actions) => actions.zoom); - const zoomTo = useStoreActions((actions) => actions.zoomTo); const currentStore = useStore(); + const { zoomIn, zoomOut, zoomTo, fitView } = useZoomPanHelper(); useElementUpdater(elements); @@ -157,9 +156,9 @@ const GraphView = ({ if (onLoad) { onLoad({ fitView: (params = { padding: 0.1 }) => fitView(params), - zoomIn: () => zoom(0.2), - zoomOut: () => zoom(-0.2), - zoomTo: (zoomLevel) => zoomTo(zoomLevel), + zoomIn, + zoomOut, + zoomTo, project: onLoadProject(currentStore), getElements: onLoadGetElements(currentStore), setTransform: (transform: FlowTransform) => diff --git a/src/container/ReactFlow/Wrapper.tsx b/src/container/ReactFlow/Wrapper.tsx index c5cc6de0..2081e092 100644 --- a/src/container/ReactFlow/Wrapper.tsx +++ b/src/container/ReactFlow/Wrapper.tsx @@ -1,10 +1,11 @@ import React, { FC } from 'react'; -import { StoreProvider, useStore } from 'easy-peasy'; +import { StoreProvider } from 'easy-peasy'; -import store, { StoreModel } from '../../store'; +import store from '../../store'; +import { useStore } from '../../store/hooks'; const Wrapper: FC = ({ children }) => { - const easyPeasyStore = useStore(); + const easyPeasyStore = useStore(); const isWrapepdWithReactFlowProvider = easyPeasyStore?.getState()?.reactFlowVersion; if (isWrapepdWithReactFlowProvider) { diff --git a/src/container/ZoomPane/index.tsx b/src/container/ZoomPane/index.tsx index 9fe19301..9ff0bc14 100644 --- a/src/container/ZoomPane/index.tsx +++ b/src/container/ZoomPane/index.tsx @@ -1,7 +1,11 @@ import React, { useEffect, useRef, ReactNode } from 'react'; +import { zoom, zoomIdentity } from 'd3-zoom'; +import { select } from 'd3-selection'; +import { clamp } from '../../utils'; + import useResizeHandler from '../../hooks/useResizeHandler'; -import { useStoreState, useStoreActions } from '../../store/hooks'; +import { useStoreState, useStoreActions, useStore } from '../../store/hooks'; import { FlowTransform, TranslateExtent } from '../../types'; interface ZoomPaneProps { @@ -55,14 +59,36 @@ const ZoomPane = ({ const d3Selection = useStoreState((s) => s.d3Selection); const d3ZoomHandler = useStoreState((s) => s.d3ZoomHandler); - const initD3 = useStoreActions((actions) => actions.initD3); + const initD3Zoom = useStoreActions((actions) => actions.initD3Zoom); const updateTransform = useStoreActions((actions) => actions.updateTransform); + const store = useStore(); useResizeHandler(zoomPane); useEffect(() => { if (zoomPane.current) { - initD3({ zoomPane: zoomPane.current, defaultPosition, defaultZoom, translateExtent }); + // initD3: action((state, { zoomPane, defaultPosition, defaultZoom, translateExtent }) => { + const state = store.getState(); + const currentTranslateExtent = typeof translateExtent !== 'undefined' ? translateExtent : state.translateExtent; + const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(currentTranslateExtent); + 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 updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom); + // selection.property('__zoom', updatedTransform); + + const zoomHandler = selection.on('wheel.zoom'); + + d3ZoomInstance.transform(selection, updatedTransform); + + initD3Zoom({ + d3Zoom: d3ZoomInstance, + d3Selection: selection, + d3ZoomHandler: zoomHandler, + }); } }, []); diff --git a/src/hooks/useZoomPanHelper.ts b/src/hooks/useZoomPanHelper.ts new file mode 100644 index 00000000..239e3514 --- /dev/null +++ b/src/hooks/useZoomPanHelper.ts @@ -0,0 +1,67 @@ +import { useCallback } from 'react'; +import { zoomIdentity } from 'd3-zoom'; +import { useStoreState, useStore } from '../store/hooks'; + +import { clamp } from '../utils'; +import { getRectOfNodes } from '../utils/graph'; +import { FitViewParams } from '../types'; + +export default () => { + const store = useStore(); + const d3Zoom = useStoreState((s) => s.d3Zoom); + const d3Selection = useStoreState((s) => s.d3Selection); + + const zoomIn = useCallback(() => { + if (d3Selection) { + d3Zoom?.scaleBy(d3Selection, 1.2); + } + }, [d3Zoom, d3Selection]); + + const zoomOut = useCallback(() => { + if (d3Selection) { + d3Zoom?.scaleBy(d3Selection, 1 / 1.2); + } + }, [d3Zoom, d3Selection]); + + const zoomTo = useCallback( + (zoomLevel: number) => { + if (d3Selection) { + d3Zoom?.scaleTo(d3Selection, zoomLevel); + } + }, + [d3Zoom, d3Selection] + ); + + const fitView = useCallback( + (options: FitViewParams) => { + const { padding = 0.1 } = options; + const { nodes, width, height, minZoom, maxZoom } = store.getState(); + + if (!d3Selection || !nodes.length) { + return; + } + + const bounds = getRectOfNodes(nodes); + const xZoom = width / (bounds.width * (1 + padding)); + const yZoom = height / (bounds.height * (1 + padding)); + const zoom = Math.min(xZoom, yZoom); + const clampedZoom = clamp(zoom, minZoom, maxZoom); + const boundsCenterX = bounds.x + bounds.width / 2; + const boundsCenterY = bounds.y + bounds.height / 2; + const x = width / 2 - boundsCenterX * clampedZoom; + const y = height / 2 - boundsCenterY * clampedZoom; + const k = clampedZoom; + const transform = zoomIdentity.translate(x, y).scale(k); + + d3Zoom?.transform(d3Selection, transform); + }, + [store, d3Zoom, d3Selection] + ); + + return { + zoomIn, + zoomOut, + zoomTo, + fitView, + }; +}; diff --git a/src/store/index.ts b/src/store/index.ts index 263e8b2f..24685222 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,10 +1,9 @@ import { createStore, Action, action, Thunk, thunk, computed, Computed } from 'easy-peasy'; import isEqual from 'fast-deep-equal'; import { Selection as D3Selection, ZoomBehavior } from 'd3'; -import { zoom, zoomIdentity } from 'd3-zoom'; -import { select } from 'd3-selection'; +import { zoomIdentity } from 'd3-zoom'; -import { getDimensions, clamp } from '../utils'; +import { getDimensions } from '../utils'; import { getHandleBounds } from '../components/Nodes/utils'; import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge } from '../utils/graph'; @@ -26,7 +25,6 @@ import { SetConnectionId, NodePosUpdate, NodeDiffUpdate, - FitViewParams, TranslateExtent, SnapGrid, } from '../types'; @@ -42,11 +40,10 @@ type NodeDimensionUpdate = { nodeElement: HTMLDivElement; }; -type InitD3 = { - zoomPane: Element; - defaultPosition: [number, number]; - defaultZoom: number; - translateExtent?: TranslateExtent; +type InitD3Zoom = { + d3Zoom: ZoomBehavior; + d3Selection: D3Selection; + d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined; }; export interface StoreModel { width: number; @@ -119,7 +116,7 @@ export interface StoreModel { updateSize: Action; - initD3: Action; + initD3Zoom: Action; setMinZoom: Action; setMaxZoom: Action; @@ -142,12 +139,6 @@ export interface StoreModel { updateUserSelection: Action; unsetUserSelection: Action; - fitView: Action; - zoomTo: Action; - zoom: Thunk; - zoomIn: Thunk; - zoomOut: Thunk; - setMultiSelectionActive: Action; } @@ -397,26 +388,10 @@ export const storeModel: StoreModel = { state.height = size.height || 500; }), - initD3: action((state, { zoomPane, defaultPosition, defaultZoom, translateExtent }) => { - const currentTranslateExtent = typeof translateExtent !== 'undefined' ? translateExtent : state.translateExtent; - const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(currentTranslateExtent); - const selection = select(zoomPane).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 updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom); - selection.property('__zoom', updatedTransform); - - const defaultHandler = selection.on('wheel.zoom'); - - state.transform[0] = clampedX; - state.transform[1] = clampedY; - state.transform[2] = clampedZoom; - state.d3Zoom = d3ZoomInstance; - state.d3Selection = selection; - state.d3ZoomHandler = defaultHandler; + initD3Zoom: action((state, { d3Zoom, d3Selection, d3ZoomHandler }) => { + state.d3Zoom = d3Zoom; + state.d3Selection = d3Selection; + state.d3ZoomHandler = d3ZoomHandler; state.d3Initialised = true; }), @@ -481,68 +456,6 @@ export const storeModel: StoreModel = { state.elementsSelectable = elementsSelectable; }), - fitView: action((state, payload = { padding: 0.1 }) => { - const { padding } = payload; - const { nodes, width, height, d3Selection, minZoom, maxZoom } = state; - - if (!d3Selection || !nodes.length) { - return; - } - - const bounds = getRectOfNodes(nodes); - const xZoom = width / (bounds.width * (1 + padding)); - const yZoom = height / (bounds.height * (1 + padding)); - const zoom = Math.min(xZoom, yZoom); - const clampedZoom = clamp(zoom, minZoom, maxZoom); - const boundsCenterX = bounds.x + bounds.width / 2; - const boundsCenterY = bounds.y + bounds.height / 2; - const transform = [width / 2 - boundsCenterX * clampedZoom, height / 2 - boundsCenterY * clampedZoom]; - const fittedTransform = zoomIdentity.translate(transform[0], transform[1]).scale(clampedZoom); - - // we need to sync the d3 zoom transform with the fitted transform - d3Selection.property('__zoom', fittedTransform); - - state.transform[0] = fittedTransform.x; - state.transform[1] = fittedTransform.y; - state.transform[2] = fittedTransform.k; - }), - - zoomTo: action((state, zoomLevel) => { - const { d3Selection, transform, minZoom, maxZoom } = state; - const nextZoom = clamp(zoomLevel, minZoom, maxZoom); - - if (d3Selection) { - // we want to zoom in and out to the center of the zoom pane - const center = [state.width / 2, state.height / 2]; - const centerInverted = [(center[0] - transform[0]) / transform[2], (center[1] - transform[1]) / transform[2]]; - const x = center[0] - centerInverted[0] * nextZoom; - const y = center[1] - centerInverted[1] * nextZoom; - const zoomedTransform = zoomIdentity.translate(x, y).scale(nextZoom); - - // we need to sync the d3 zoom transform with the zoomed transform - d3Selection.property('__zoom', zoomedTransform); - - state.transform[0] = zoomedTransform.x; - state.transform[1] = zoomedTransform.y; - state.transform[2] = zoomedTransform.k; - } - }), - - zoom: thunk((actions, amount, helpers) => { - const { transform } = helpers.getState(); - const nextZoom = transform[2] + amount; - - actions.zoomTo(nextZoom); - }), - - zoomIn: thunk((actions) => { - actions.zoom(0.2); - }), - - zoomOut: thunk((actions) => { - actions.zoom(-0.2); - }), - setMultiSelectionActive: action((state, isActive) => { state.multiSelectionActive = isActive; }), diff --git a/src/utils/graph.ts b/src/utils/graph.ts index d39c8f7e..1870a0fb 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -1,7 +1,6 @@ import { Store } from 'easy-peasy'; - -import store, { StoreModel } from '../store'; -import { ElementId, Node, Edge, Elements, Transform, XYPosition, Rect, FitViewParams, Box, Connection } from '../types'; +import { StoreModel } from '../store'; +import { ElementId, Node, Edge, Elements, Transform, XYPosition, Rect, Box, Connection } from '../types'; export const isEdge = (element: Node | Connection | Edge): element is Edge => 'id' in element && 'source' in element && 'target' in element; @@ -106,12 +105,6 @@ export const onLoadProject = (currentStore: Store) => { }; }; -export const project = (position: XYPosition): XYPosition => { - const { transform, snapToGrid, snapGrid } = store.getState(); - - return pointToRendererPoint(position, transform, snapToGrid, snapGrid); -}; - export const parseElement = (element: Node | Edge): Node | Edge => { if (!element.id) { throw new Error('All nodes and edges need to have an id.'); @@ -227,18 +220,6 @@ export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => { }); }; -export const fitView = (params: FitViewParams = { padding: 0.1 }): void => { - store.getActions().fitView(params); -}; - -const zoom = (amount: number): void => { - store.getActions().zoom(amount); -}; - -export const zoomIn = (): void => zoom(0.2); - -export const zoomOut = (): void => zoom(-0.2); - const parseElements = (nodes: Node[], edges: Edge[]): Elements => { return [ ...nodes.map((node) => { @@ -258,9 +239,3 @@ export const onLoadGetElements = (currentStore: Store) => { return parseElements(nodes, edges); }; }; - -export const getElements = (): Elements => { - const { nodes = [], edges = [] } = store.getState(); - - return parseElements(nodes, edges); -}; From cf87acc89b47776b8cbbd0043433b19074b593d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20Mo=CC=88ller?= Date: Mon, 9 Nov 2020 17:29:27 +0100 Subject: [PATCH 02/43] chore: release v8.0.0-next.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85da28c1..4ca33336 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "7.1.2", + "version": "8.0.0-next.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9b013526..e49deab9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "7.1.2", + "version": "8.0.0-next.0", "main": "dist/ReactFlow.js", "module": "dist/ReactFlow.esm.js", "types": "dist/index.d.ts", From 2db5a7bce24f830fab91bdd68e0549f2efe8077e Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 9 Nov 2020 17:48:51 +0100 Subject: [PATCH 03/43] refactor(getElements): use internal node position #603 --- src/utils/graph.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 1870a0fb..4871ceb5 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -210,14 +210,9 @@ export const getNodesInside = ( }; export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => { - const nodeIds = nodes.map((n) => n.id); + const nodeIds = nodes.map((node) => node.id); - return edges.filter((e) => { - const sourceId = e.source.split('__')[0]; - const targetId = e.target.split('__')[0]; - - return nodeIds.includes(sourceId) || nodeIds.includes(targetId); - }); + return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target)); }; const parseElements = (nodes: Node[], edges: Edge[]): Elements => { @@ -225,6 +220,8 @@ const parseElements = (nodes: Node[], edges: Edge[]): Elements => { ...nodes.map((node) => { const n = { ...node }; + n.position = n.__rf.position; + delete n.__rf; return n; }), From 7cc2306975cb0ba2363c8a6626ff223928238470 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 9 Nov 2020 18:17:36 +0100 Subject: [PATCH 04/43] feat(reactflowInstance): add toObject function closes #603 --- example/src/Basic/index.js | 14 ++++++++++---- src/container/GraphView/index.tsx | 3 ++- src/types/index.ts | 9 +++++++++ src/utils/graph.ts | 25 ++++++++++++++++++++++++- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/example/src/Basic/index.js b/example/src/Basic/index.js index 841e731e..2b38a838 100644 --- a/example/src/Basic/index.js +++ b/example/src/Basic/index.js @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import ReactFlow, { removeElements, addEdge, isNode, Background } from 'react-flow-renderer'; -const onLoad = (reactFlowInstance) => console.log('flow loaded:', reactFlowInstance); const onNodeDragStop = (event, node) => console.log('drag stop', node); const onElementClick = (event, element) => console.log('click', element); @@ -16,9 +15,11 @@ const initialElements = [ ]; const BasicFlow = () => { + const [rfInstance, setRfInstance] = useState(null); const [elements, setElements] = useState(initialElements); const onElementsRemove = (elementsToRemove) => setElements((els) => removeElements(elementsToRemove, els)); const onConnect = (params) => setElements((els) => addEdge(params, els)); + const onLoad = (reactFlowInstance) => setRfInstance(reactFlowInstance); const updatePos = () => { setElements((elms) => { @@ -38,6 +39,8 @@ const BasicFlow = () => { }); }; + const logToObject = () => console.log(rfInstance.toObject()); + return ( { > - +
+ + +
); }; diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 3f9ce248..7b82778a 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -5,7 +5,7 @@ import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; import useElementUpdater from '../../hooks/useElementUpdater'; -import { onLoadProject, onLoadGetElements } from '../../utils/graph'; +import { onLoadProject, onLoadGetElements, onLoadToObject } from '../../utils/graph'; import { Elements, NodeTypesType, @@ -163,6 +163,7 @@ const GraphView = ({ getElements: onLoadGetElements(currentStore), setTransform: (transform: FlowTransform) => setInitTransform({ x: transform.x, y: transform.y, k: transform.zoom }), + toObject: onLoadToObject(currentStore), }); } diff --git a/src/types/index.ts b/src/types/index.ts index 583d9391..d912f2bc 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -229,8 +229,16 @@ export interface WrapNodeProps { export type FitViewParams = { padding: number; }; + +export type FlowExportObject = { + elements: Elements; + 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; @@ -240,6 +248,7 @@ export type OnLoadParams = { project: ProjectFunc; getElements: () => Elements; setTransform: (transform: FlowTransform) => void; + toObject: ToObjectFunc; }; export type OnLoadFunc = (params: OnLoadParams) => void; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 4871ceb5..9285e59a 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -1,6 +1,17 @@ import { Store } from 'easy-peasy'; import { StoreModel } from '../store'; -import { ElementId, Node, Edge, Elements, Transform, XYPosition, Rect, Box, Connection } from '../types'; +import { + ElementId, + Node, + Edge, + Elements, + Transform, + XYPosition, + Rect, + Box, + Connection, + FlowExportObject, +} from '../types'; export const isEdge = (element: Node | Connection | Edge): element is Edge => 'id' in element && 'source' in element && 'target' in element; @@ -236,3 +247,15 @@ export const onLoadGetElements = (currentStore: Store) => { return parseElements(nodes, edges); }; }; + +export const onLoadToObject = (currentStore: Store) => { + return (): FlowExportObject => { + const { nodes = [], edges = [], transform } = currentStore.getState(); + + return { + elements: parseElements(nodes, edges), + position: [transform[0], transform[1]], + zoom: transform[2], + }; + }; +}; From c7bf8ffbcc6d09f286a101e5f74868663f893893 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 9 Nov 2020 18:27:41 +0100 Subject: [PATCH 05/43] refactor(initD3): pass initial transform --- src/container/GraphView/index.tsx | 6 +++--- src/container/ZoomPane/index.tsx | 9 +++------ src/store/index.ts | 10 ++++++---- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 7b82778a..72d8f2b8 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -132,7 +132,7 @@ const GraphView = ({ onPaneContextMenu, }: GraphViewProps) => { const isInitialised = useRef(false); - const d3Initialised = useStoreState((state) => state.d3Initialised); + const d3Zoom = useStoreState((state) => state.d3Zoom); const setOnConnect = useStoreActions((actions) => actions.setOnConnect); const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart); const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop); @@ -152,7 +152,7 @@ const GraphView = ({ useElementUpdater(elements); useEffect(() => { - if (!isInitialised.current && d3Initialised) { + if (!isInitialised.current && d3Zoom) { if (onLoad) { onLoad({ fitView: (params = { padding: 0.1 }) => fitView(params), @@ -169,7 +169,7 @@ const GraphView = ({ isInitialised.current = true; } - }, [d3Initialised, onLoad]); + }, [d3Zoom, onLoad]); useEffect(() => { if (onConnect) { diff --git a/src/container/ZoomPane/index.tsx b/src/container/ZoomPane/index.tsx index 9ff0bc14..4dcaba8b 100644 --- a/src/container/ZoomPane/index.tsx +++ b/src/container/ZoomPane/index.tsx @@ -67,7 +67,6 @@ const ZoomPane = ({ useEffect(() => { if (zoomPane.current) { - // initD3: action((state, { zoomPane, defaultPosition, defaultZoom, translateExtent }) => { const state = store.getState(); const currentTranslateExtent = typeof translateExtent !== 'undefined' ? translateExtent : state.translateExtent; const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(currentTranslateExtent); @@ -76,18 +75,16 @@ const ZoomPane = ({ 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 updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom); - // selection.property('__zoom', updatedTransform); - - const zoomHandler = selection.on('wheel.zoom'); d3ZoomInstance.transform(selection, updatedTransform); initD3Zoom({ d3Zoom: d3ZoomInstance, d3Selection: selection, - d3ZoomHandler: zoomHandler, + d3ZoomHandler: selection.on('wheel.zoom'), + // we need to pass transform because zoom handler is not registered when we set the initial transform + transform: [clampedX, clampedY, clampedZoom], }); } }, []); diff --git a/src/store/index.ts b/src/store/index.ts index 24685222..05e550cf 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -44,6 +44,7 @@ type InitD3Zoom = { d3Zoom: ZoomBehavior; d3Selection: D3Selection; d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined; + transform: Transform; }; export interface StoreModel { width: number; @@ -59,7 +60,6 @@ export interface StoreModel { d3Zoom: ZoomBehavior | null; d3Selection: D3Selection | null; d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined; - d3Initialised: boolean; minZoom: number; maxZoom: number; translateExtent: TranslateExtent; @@ -155,7 +155,6 @@ export const storeModel: StoreModel = { d3Zoom: null, d3Selection: null, - d3Initialised: false, d3ZoomHandler: undefined, minZoom: 0.5, maxZoom: 2, @@ -388,11 +387,14 @@ export const storeModel: StoreModel = { state.height = size.height || 500; }), - initD3Zoom: action((state, { d3Zoom, d3Selection, d3ZoomHandler }) => { + initD3Zoom: action((state, { d3Zoom, d3Selection, d3ZoomHandler, transform }) => { state.d3Zoom = d3Zoom; state.d3Selection = d3Selection; state.d3ZoomHandler = d3ZoomHandler; - state.d3Initialised = true; + + state.transform[0] = transform[0]; + state.transform[1] = transform[1]; + state.transform[2] = transform[2]; }), setMinZoom: action((state, minZoom) => { From e1ae70e7790596c1684f6f5de3ccd884f602e039 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 9 Nov 2020 19:30:21 +0100 Subject: [PATCH 06/43] refactor(zoomPanHelper): export and simplify --- example/src/Basic/index.js | 5 + src/additional-components/Controls/index.tsx | 59 +++++------- src/container/GraphView/index.tsx | 13 +-- src/container/ZoomPane/index.tsx | 2 +- src/hooks/useZoomPanHelper.ts | 97 +++++++++----------- src/index.ts | 1 + src/store/index.ts | 35 ++----- 7 files changed, 89 insertions(+), 123 deletions(-) diff --git a/example/src/Basic/index.js b/example/src/Basic/index.js index 2b38a838..3395d29b 100644 --- a/example/src/Basic/index.js +++ b/example/src/Basic/index.js @@ -41,6 +41,8 @@ const BasicFlow = () => { const logToObject = () => console.log(rfInstance.toObject()); + const resetTransform = () => rfInstance.setTransform({ x: 0, y: 0, zoom: 1 }); + return ( {
+ diff --git a/src/additional-components/Controls/index.tsx b/src/additional-components/Controls/index.tsx index 983701e9..ca11bfa3 100644 --- a/src/additional-components/Controls/index.tsx +++ b/src/additional-components/Controls/index.tsx @@ -1,4 +1,4 @@ -import React, { memo } from 'react'; +import React, { memo, useCallback } from 'react'; import cc from 'classcat'; import { useStoreState, useStoreActions } from '../../store/hooks'; @@ -39,56 +39,47 @@ const Controls = ({ const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable); const mapClasses = cc(['react-flow__controls', className]); + const onZoomInHandler = useCallback(() => { + zoomIn?.(); + onZoomIn?.(); + }, [zoomIn, onZoomIn]); + + const onZoomOutHandler = useCallback(() => { + zoomOut?.(); + onZoomOut?.(); + }, [zoomOut, onZoomOut]); + + const onFitViewHandler = useCallback(() => { + fitView?.(); + onFitView?.(); + }, [fitView, onFitView]); + + const onInteractiveChangeHandler = useCallback(() => { + setInteractive?.(!isInteractive); + onInteractiveChange?.(!isInteractive); + }, [isInteractive, setInteractive, onInteractiveChange]); + return (
{showZoom && ( <> -
{ - zoomIn(); - if (onZoomIn) { - onZoomIn(); - } - }} - > +
-
{ - zoomOut(); - if (onZoomOut) { - onZoomOut(); - } - }} - > +
)} {showFitView && ( -
{ - fitView({ padding: 0.1 }); - if (onFitView) { - onFitView(); - } - }} - > +
)} {showInteractive && (
{ - setInteractive(!isInteractive); - if (onInteractiveChange) { - onInteractiveChange(!isInteractive); - } - }} + onClick={onInteractiveChangeHandler} > {isInteractive ? : }
diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 72d8f2b8..44c209bd 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, memo, CSSProperties, MouseEvent, WheelEvent } from 'react'; -import { useStoreState, useStoreActions, useStore } from '../../store/hooks'; +import { useStoreActions, useStore } from '../../store/hooks'; import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; @@ -132,7 +132,6 @@ const GraphView = ({ onPaneContextMenu, }: GraphViewProps) => { const isInitialised = useRef(false); - const d3Zoom = useStoreState((state) => state.d3Zoom); const setOnConnect = useStoreActions((actions) => actions.setOnConnect); const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart); const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop); @@ -142,34 +141,32 @@ const GraphView = ({ const setNodesDraggable = useStoreActions((actions) => actions.setNodesDraggable); const setNodesConnectable = useStoreActions((actions) => actions.setNodesConnectable); const setElementsSelectable = useStoreActions((actions) => actions.setElementsSelectable); - const setInitTransform = useStoreActions((actions) => actions.setInitTransform); const setMinZoom = useStoreActions((actions) => actions.setMinZoom); const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom); const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent); const currentStore = useStore(); - const { zoomIn, zoomOut, zoomTo, fitView } = useZoomPanHelper(); + const { zoomIn, zoomOut, zoomTo, transform, fitView } = useZoomPanHelper(); useElementUpdater(elements); useEffect(() => { - if (!isInitialised.current && d3Zoom) { + if (!isInitialised.current && zoomIn && zoomOut && zoomTo && transform && fitView) { if (onLoad) { onLoad({ fitView: (params = { padding: 0.1 }) => fitView(params), zoomIn, zoomOut, zoomTo, + setTransform: transform, project: onLoadProject(currentStore), getElements: onLoadGetElements(currentStore), - setTransform: (transform: FlowTransform) => - setInitTransform({ x: transform.x, y: transform.y, k: transform.zoom }), toObject: onLoadToObject(currentStore), }); } isInitialised.current = true; } - }, [d3Zoom, onLoad]); + }, [onLoad, zoomIn, zoomOut, zoomTo, transform, fitView]); useEffect(() => { if (onConnect) { diff --git a/src/container/ZoomPane/index.tsx b/src/container/ZoomPane/index.tsx index 4dcaba8b..3d2d94e3 100644 --- a/src/container/ZoomPane/index.tsx +++ b/src/container/ZoomPane/index.tsx @@ -123,7 +123,7 @@ const ZoomPane = ({ d3Zoom.on('zoom', null); } else { d3Zoom.on('zoom', (event: any) => { - updateTransform(event.transform); + updateTransform([event.transform.x, event.transform.y, event.transform.k]); if (onMove) { const flowTransform = eventToFlowTransform(event.transform); diff --git a/src/hooks/useZoomPanHelper.ts b/src/hooks/useZoomPanHelper.ts index 239e3514..b2b5a0e7 100644 --- a/src/hooks/useZoomPanHelper.ts +++ b/src/hooks/useZoomPanHelper.ts @@ -1,67 +1,60 @@ -import { useCallback } from 'react'; +import { useMemo } from 'react'; import { zoomIdentity } from 'd3-zoom'; import { useStoreState, useStore } from '../store/hooks'; import { clamp } from '../utils'; import { getRectOfNodes } from '../utils/graph'; -import { FitViewParams } from '../types'; +import { FitViewParams, FlowTransform } from '../types'; + +const initialHelpers = { + zoomIn: null, + zoomOut: null, + zoomTo: null, + transform: null, + fitView: null, +}; export default () => { const store = useStore(); const d3Zoom = useStoreState((s) => s.d3Zoom); const d3Selection = useStoreState((s) => s.d3Selection); - const zoomIn = useCallback(() => { - if (d3Selection) { - d3Zoom?.scaleBy(d3Selection, 1.2); + const zoomPanHelperFunctions = useMemo(() => { + if (d3Selection && d3Zoom) { + return { + zoomIn: () => d3Zoom.scaleBy(d3Selection, 1.2), + zoomOut: () => d3Zoom.scaleBy(d3Selection, 1 / 1.2), + zoomTo: (zoomLevel: number) => d3Zoom.scaleTo(d3Selection, zoomLevel), + transform: (transform: FlowTransform) => { + const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom); + + d3Zoom.transform(d3Selection, nextTransform); + }, + fitView: (options: FitViewParams = { padding: 0.1 }) => { + const { nodes, width, height, minZoom, maxZoom } = store.getState(); + + if (!nodes.length) { + return; + } + + const bounds = getRectOfNodes(nodes); + const xZoom = width / (bounds.width * (1 + options.padding)); + const yZoom = height / (bounds.height * (1 + options.padding)); + const zoom = Math.min(xZoom, yZoom); + const clampedZoom = clamp(zoom, minZoom, maxZoom); + const boundsCenterX = bounds.x + bounds.width / 2; + const boundsCenterY = bounds.y + bounds.height / 2; + const x = width / 2 - boundsCenterX * clampedZoom; + const y = height / 2 - boundsCenterY * clampedZoom; + const transform = zoomIdentity.translate(x, y).scale(clampedZoom); + + d3Zoom.transform(d3Selection, transform); + }, + }; } + + return initialHelpers; }, [d3Zoom, d3Selection]); - const zoomOut = useCallback(() => { - if (d3Selection) { - d3Zoom?.scaleBy(d3Selection, 1 / 1.2); - } - }, [d3Zoom, d3Selection]); - - const zoomTo = useCallback( - (zoomLevel: number) => { - if (d3Selection) { - d3Zoom?.scaleTo(d3Selection, zoomLevel); - } - }, - [d3Zoom, d3Selection] - ); - - const fitView = useCallback( - (options: FitViewParams) => { - const { padding = 0.1 } = options; - const { nodes, width, height, minZoom, maxZoom } = store.getState(); - - if (!d3Selection || !nodes.length) { - return; - } - - const bounds = getRectOfNodes(nodes); - const xZoom = width / (bounds.width * (1 + padding)); - const yZoom = height / (bounds.height * (1 + padding)); - const zoom = Math.min(xZoom, yZoom); - const clampedZoom = clamp(zoom, minZoom, maxZoom); - const boundsCenterX = bounds.x + bounds.width / 2; - const boundsCenterY = bounds.y + bounds.height / 2; - const x = width / 2 - boundsCenterX * clampedZoom; - const y = height / 2 - boundsCenterY * clampedZoom; - const k = clampedZoom; - const transform = zoomIdentity.translate(x, y).scale(k); - - d3Zoom?.transform(d3Selection, transform); - }, - [store, d3Zoom, d3Selection] - ); - - return { - zoomIn, - zoomOut, - zoomTo, - fitView, - }; + return zoomPanHelperFunctions; }; diff --git a/src/index.ts b/src/index.ts index 3fe2e30e..6ce7ecc2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ export { getSmoothStepPath } from './components/Edges/SmoothStepEdge'; export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils'; export { isNode, isEdge, removeElements, addEdge, getOutgoers, getIncomers, getConnectedEdges } from './utils/graph'; +export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'; export * from './additional-components'; export * from './store/hooks'; diff --git a/src/store/index.ts b/src/store/index.ts index 05e550cf..406ac647 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,12 +1,11 @@ import { createStore, Action, action, Thunk, thunk, computed, Computed } from 'easy-peasy'; import isEqual from 'fast-deep-equal'; import { Selection as D3Selection, ZoomBehavior } from 'd3'; -import { zoomIdentity } from 'd3-zoom'; import { getDimensions } from '../utils'; +import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge } from '../utils/graph'; import { getHandleBounds } from '../components/Nodes/utils'; -import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge } from '../utils/graph'; import { ElementId, Elements, @@ -29,12 +28,6 @@ import { SnapGrid, } from '../types'; -type TransformXYK = { - x: number; - y: number; - k: number; -}; - type NodeDimensionUpdate = { id: ElementId; nodeElement: HTMLDivElement; @@ -110,9 +103,7 @@ export interface StoreModel { setSelectedElements: Action; addSelectedElements: Thunk; - updateTransform: Action; - - setInitTransform: Action; + updateTransform: Action; updateSize: Action; @@ -148,8 +139,8 @@ export const storeModel: StoreModel = { viewportBox: computed((state) => ({ x: 0, y: 0, width: state.width, height: state.height })), transform: [0, 0, 1], elements: [], - nodes: computed((state) => state.elements.filter((el) => isNode(el)) as Node[]), - edges: computed((state) => state.elements.filter((el) => isEdge(el)) as Edge[]), + nodes: computed((state) => state.elements.filter(isNode)), + edges: computed((state) => state.elements.filter(isEdge)), selectedElements: null, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, @@ -363,21 +354,9 @@ export const storeModel: StoreModel = { }), updateTransform: action((state, transform) => { - state.transform[0] = transform.x; - state.transform[1] = transform.y; - state.transform[2] = transform.k; - }), - - setInitTransform: action((state, transform) => { - state.transform[0] = transform.x; - state.transform[1] = transform.y; - state.transform[2] = transform.k; - - if (state.d3Selection) { - const updatedTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.k); - // we need to sync the d3 zoom transform with the updated transform - state.d3Selection.property('__zoom', updatedTransform); - } + state.transform[0] = transform[0]; + state.transform[1] = transform[1]; + state.transform[2] = transform[2]; }), updateSize: action((state, size) => { From d06868ecdeb16e80ff55d453035873fad38549a1 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 9 Nov 2020 22:00:26 +0100 Subject: [PATCH 07/43] refactor(graphview): extend react flow props --- src/container/FlowRenderer/index.tsx | 1 + src/container/GraphView/index.tsx | 70 ++++------------------------ src/hooks/useZoomPanHelper.ts | 6 +-- 3 files changed, 14 insertions(+), 63 deletions(-) diff --git a/src/container/FlowRenderer/index.tsx b/src/container/FlowRenderer/index.tsx index 12bec2f5..4127f481 100644 --- a/src/container/FlowRenderer/index.tsx +++ b/src/container/FlowRenderer/index.tsx @@ -20,6 +20,7 @@ interface FlowRendererProps | 'connectionLineType' | 'arrowHeadColor' | 'onlyRenderVisibleNodes' + | 'selectNodesOnDrag' > { children: ReactNode; } diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 44c209bd..c831b367 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, memo, CSSProperties, MouseEvent, WheelEvent } from 'react'; +import React, { useEffect, useRef, memo } from 'react'; import { useStoreActions, useStore } from '../../store/hooks'; import FlowRenderer from '../FlowRenderer'; @@ -6,76 +6,26 @@ import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; import useElementUpdater from '../../hooks/useElementUpdater'; import { onLoadProject, onLoadGetElements, onLoadToObject } from '../../utils/graph'; -import { - Elements, - NodeTypesType, - EdgeTypesType, - OnLoadFunc, - Node, - Edge, - Connection, - ConnectionLineType, - ConnectionLineComponent, - FlowTransform, - OnConnectStartFunc, - OnConnectStopFunc, - OnConnectEndFunc, - TranslateExtent, -} from '../../types'; import useZoomPanHelper from '../../hooks/useZoomPanHelper'; -export interface GraphViewProps { - elements: Elements; - onElementClick?: (event: MouseEvent, element: Node | Edge) => void; - onElementsRemove?: (elements: Elements) => void; - onNodeMouseEnter?: (event: MouseEvent, node: Node) => void; - onNodeMouseMove?: (event: MouseEvent, node: Node) => void; - onNodeMouseLeave?: (event: MouseEvent, node: Node) => void; - onNodeContextMenu?: (event: MouseEvent, node: Node) => void; - onNodeDragStart?: (event: MouseEvent, node: Node) => void; - onNodeDragStop?: (event: MouseEvent, node: Node) => void; - onConnect?: (connection: Connection | Edge) => void; - onConnectStart?: OnConnectStartFunc; - onConnectStop?: OnConnectStopFunc; - onConnectEnd?: OnConnectEndFunc; - onLoad?: OnLoadFunc; - onMove?: (flowTransform?: FlowTransform) => void; - onMoveStart?: (flowTransform?: FlowTransform) => void; - onMoveEnd?: (flowTransform?: FlowTransform) => void; - onPaneScroll?: (event?: WheelEvent) => void; - onPaneClick?: (event: MouseEvent) => void; - onPaneContextMenu?: (event: MouseEvent) => void; - onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void; - onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void; - onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void; - onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; - selectionKeyCode: number; +import { ReactFlowProps } from '../ReactFlow'; + +import { NodeTypesType, EdgeTypesType, ConnectionLineType } from '../../types'; + +export interface GraphViewProps extends Omit { nodeTypes: NodeTypesType; edgeTypes: EdgeTypesType; - connectionLineType: ConnectionLineType; - connectionLineStyle?: CSSProperties; - connectionLineComponent?: ConnectionLineComponent; + selectionKeyCode: number; deleteKeyCode: number; multiSelectionKeyCode: number; + connectionLineType: ConnectionLineType; snapToGrid: boolean; snapGrid: [number, number]; onlyRenderVisibleNodes: boolean; - nodesDraggable?: boolean; - nodesConnectable?: boolean; - elementsSelectable?: boolean; - selectNodesOnDrag?: boolean; - minZoom?: number; - maxZoom?: number; defaultZoom: number; defaultPosition: [number, number]; - translateExtent?: TranslateExtent; arrowHeadColor: string; - markerEndId?: string; - zoomOnScroll?: boolean; - panOnScroll?: boolean; - panOnScrollSpeed?: number; - zoomOnDoubleClick?: boolean; - paneMoveable?: boolean; + selectNodesOnDrag: boolean; } const GraphView = ({ @@ -114,7 +64,7 @@ const GraphView = ({ nodesDraggable, nodesConnectable, elementsSelectable, - selectNodesOnDrag = true, + selectNodesOnDrag, minZoom, maxZoom, defaultZoom, diff --git a/src/hooks/useZoomPanHelper.ts b/src/hooks/useZoomPanHelper.ts index b2b5a0e7..defb16be 100644 --- a/src/hooks/useZoomPanHelper.ts +++ b/src/hooks/useZoomPanHelper.ts @@ -1,12 +1,12 @@ import { useMemo } from 'react'; import { zoomIdentity } from 'd3-zoom'; -import { useStoreState, useStore } from '../store/hooks'; +import { useStoreState, useStore } from '../store/hooks'; import { clamp } from '../utils'; import { getRectOfNodes } from '../utils/graph'; import { FitViewParams, FlowTransform } from '../types'; -const initialHelpers = { +const initialZoomPanHelper = { zoomIn: null, zoomOut: null, zoomTo: null, @@ -53,7 +53,7 @@ export default () => { }; } - return initialHelpers; + return initialZoomPanHelper; }, [d3Zoom, d3Selection]); return zoomPanHelperFunctions; From 0fbefe5a58f193cdba037aad07a0f5b80c452884 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 9 Nov 2020 22:24:12 +0100 Subject: [PATCH 08/43] refactor(GraphView): reduce amounts of re-renderings --- .../ElementUpdater/index.tsx} | 20 ++++++++++++------- src/container/GraphView/index.tsx | 6 +----- src/container/ReactFlow/index.tsx | 3 ++- src/container/ZoomPane/index.tsx | 2 +- src/hooks/useGlobalKeyHandler.ts | 6 +++--- 5 files changed, 20 insertions(+), 17 deletions(-) rename src/{hooks/useElementUpdater.ts => components/ElementUpdater/index.tsx} (90%) diff --git a/src/hooks/useElementUpdater.ts b/src/components/ElementUpdater/index.tsx similarity index 90% rename from src/hooks/useElementUpdater.ts rename to src/components/ElementUpdater/index.tsx index 0f2bac7c..a84c5c01 100644 --- a/src/hooks/useElementUpdater.ts +++ b/src/components/ElementUpdater/index.tsx @@ -1,16 +1,20 @@ import { useEffect } from 'react'; import isEqual from 'fast-deep-equal'; -import { useStoreState, useStoreActions } from '../store/hooks'; -import { parseElement, isNode, isEdge } from '../utils/graph'; -import { Elements, Node, Edge, FlowElement } from '../types'; +import { useStoreState, useStoreActions } from '../../store/hooks'; +import { parseElement, isNode, isEdge } from '../../utils/graph'; +import { Elements, Node, Edge, FlowElement } from '../../types'; -const useElementUpdater = (propElements: Elements): void => { +interface ElementUpdaterProps { + elements: Elements; +} + +const ElementUpdater = ({ elements }: ElementUpdaterProps) => { const stateElements = useStoreState((state) => state.elements); const setElements = useStoreActions((actions) => actions.setElements); useEffect(() => { - const nextElements: Elements = propElements.map((propElement) => { + const nextElements: Elements = elements.map((propElement) => { const existingElement = stateElements.find((el) => el.id === propElement.id?.toString()); if (existingElement) { @@ -137,7 +141,9 @@ const useElementUpdater = (propElements: Elements): void => { if (elementsChanged) { setElements(nextElements); } - }, [propElements, stateElements]); + }, [elements, stateElements]); + + return null; }; -export default useElementUpdater; +export default ElementUpdater; diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index c831b367..2c870ab3 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -4,7 +4,6 @@ import { useStoreActions, useStore } from '../../store/hooks'; import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; -import useElementUpdater from '../../hooks/useElementUpdater'; import { onLoadProject, onLoadGetElements, onLoadToObject } from '../../utils/graph'; import useZoomPanHelper from '../../hooks/useZoomPanHelper'; @@ -12,7 +11,7 @@ import { ReactFlowProps } from '../ReactFlow'; import { NodeTypesType, EdgeTypesType, ConnectionLineType } from '../../types'; -export interface GraphViewProps extends Omit { +export interface GraphViewProps extends Omit { nodeTypes: NodeTypesType; edgeTypes: EdgeTypesType; selectionKeyCode: number; @@ -53,7 +52,6 @@ const GraphView = ({ multiSelectionKeyCode, onElementsRemove, deleteKeyCode, - elements, onConnect, onConnectStart, onConnectStop, @@ -97,8 +95,6 @@ const GraphView = ({ const currentStore = useStore(); const { zoomIn, zoomOut, zoomTo, transform, fitView } = useZoomPanHelper(); - useElementUpdater(elements); - useEffect(() => { if (!isInitialised.current && zoomIn && zoomOut && zoomTo && transform && fitView) { if (onLoad) { diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index 533d2100..8a4860b5 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -9,6 +9,7 @@ if (nodeEnv !== 'production') { } import GraphView from '../GraphView'; +import ElementUpdater from '../../components/ElementUpdater'; import DefaultNode from '../../components/Nodes/DefaultNode'; import InputNode from '../../components/Nodes/InputNode'; import OutputNode from '../../components/Nodes/OutputNode'; @@ -189,7 +190,6 @@ const ReactFlow = ({ onElementsRemove={onElementsRemove} deleteKeyCode={deleteKeyCode} multiSelectionKeyCode={multiSelectionKeyCode} - elements={elements} onConnect={onConnect} onConnectStart={onConnectStart} onConnectStop={onConnectStop} @@ -221,6 +221,7 @@ const ReactFlow = ({ onSelectionDragStop={onSelectionDragStop} onSelectionContextMenu={onSelectionContextMenu} /> + {onSelectionChange && } {children} diff --git a/src/container/ZoomPane/index.tsx b/src/container/ZoomPane/index.tsx index 3d2d94e3..e67c253d 100644 --- a/src/container/ZoomPane/index.tsx +++ b/src/container/ZoomPane/index.tsx @@ -55,13 +55,13 @@ const ZoomPane = ({ 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 store = useStore(); useResizeHandler(zoomPane); diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index a6319b3d..1a719f4c 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -import { useStoreState, useStoreActions } from '../store/hooks'; +import { useStore, useStoreActions } from '../store/hooks'; import useKeyPress from './useKeyPress'; import { isNode, getConnectedEdges } from '../utils/graph'; import { Elements } from '../types'; @@ -12,8 +12,7 @@ interface HookParams { } export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => { - const selectedElements = useStoreState((state) => state.selectedElements); - const edges = useStoreState((state) => state.edges); + const store = useStore(); const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); const setMultiSelectionActive = useStoreActions((actions) => actions.setMultiSelectionActive); @@ -23,6 +22,7 @@ export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: Hook const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); useEffect(() => { + const { edges, selectedElements } = store.getState(); if (onElementsRemove && deleteKeyPressed && selectedElements) { let elementsToRemove = selectedElements; From e088287370f50ab227ecd16c8c26dd3392f18407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20Mo=CC=88ller?= Date: Tue, 10 Nov 2020 12:58:15 +0100 Subject: [PATCH 09/43] fix(useKeyPress): reset key when window loses focus #641 --- src/hooks/useKeyPress.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/hooks/useKeyPress.ts b/src/hooks/useKeyPress.ts index 4fec31f2..6fb340fa 100644 --- a/src/hooks/useKeyPress.ts +++ b/src/hooks/useKeyPress.ts @@ -17,13 +17,19 @@ export default (keyCode: number): boolean => { } }; + const resetHandler = () => { + setKeyPressed(false); + }; + useEffect(() => { window.addEventListener('keydown', downHandler); window.addEventListener('keyup', upHandler); + window.addEventListener('blur', resetHandler); return () => { window.removeEventListener('keydown', downHandler); window.removeEventListener('keyup', upHandler); + window.removeEventListener('blur', resetHandler); }; }, []); From 7cd0d4e5634bc4e282fdd64c0783b90132ecd81b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20Mo=CC=88ller?= Date: Tue, 10 Nov 2020 13:01:39 +0100 Subject: [PATCH 10/43] change release:next npm task --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e49deab9..f597600c 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "test:all": "npm run test:chrome && npm run test:firefox", "test": "npm run dev:wait test:chrome", "release": "npm run test && release-it", - "release:next": "release-it major --preRelease=next" + "release:next": "release-it --preRelease=next" }, "dependencies": { "@babel/runtime": "^7.12.5", From 965fb2a212fdf3f90cebeb6ad7eecad46c9c8def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20Mo=CC=88ller?= Date: Tue, 10 Nov 2020 13:03:10 +0100 Subject: [PATCH 11/43] chore: release v8.0.0-next.1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4ca33336..190ccc0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "8.0.0-next.0", + "version": "8.0.0-next.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f597600c..f2cb51e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "8.0.0-next.0", + "version": "8.0.0-next.1", "main": "dist/ReactFlow.js", "module": "dist/ReactFlow.esm.js", "types": "dist/index.d.ts", From 959216f6fcff68166f865280b19ac225853510b5 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 10 Nov 2020 14:27:15 +0100 Subject: [PATCH 12/43] refactor(keycodes): allow strings and numbers as key codes --- src/container/GraphView/index.tsx | 8 +++---- src/container/ReactFlow/index.tsx | 13 ++++++----- src/hooks/useGlobalKeyHandler.ts | 6 ++--- src/hooks/useKeyPress.ts | 37 ++++++++++++++++--------------- src/types/index.ts | 2 ++ 5 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 2c870ab3..1efff10c 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -9,14 +9,14 @@ import useZoomPanHelper from '../../hooks/useZoomPanHelper'; import { ReactFlowProps } from '../ReactFlow'; -import { NodeTypesType, EdgeTypesType, ConnectionLineType } from '../../types'; +import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types'; export interface GraphViewProps extends Omit { nodeTypes: NodeTypesType; edgeTypes: EdgeTypesType; - selectionKeyCode: number; - deleteKeyCode: number; - multiSelectionKeyCode: number; + selectionKeyCode: KeyCode; + deleteKeyCode: KeyCode; + multiSelectionKeyCode: KeyCode; connectionLineType: ConnectionLineType; snapToGrid: boolean; snapGrid: [number, number]; diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index 8a4860b5..a4f47765 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -33,6 +33,7 @@ import { OnConnectStopFunc, OnConnectEndFunc, TranslateExtent, + KeyCode, } from '../../types'; import '../../style.css'; @@ -81,9 +82,9 @@ export interface ReactFlowProps extends Omit, 'on connectionLineType?: ConnectionLineType; connectionLineStyle?: CSSProperties; connectionLineComponent?: ConnectionLineComponent; - deleteKeyCode?: number; - selectionKeyCode?: number; - multiSelectionKeyCode?: number; + deleteKeyCode?: KeyCode; + selectionKeyCode?: KeyCode; + multiSelectionKeyCode?: KeyCode; snapToGrid?: boolean; snapGrid?: [number, number]; onlyRenderVisibleNodes?: boolean; @@ -134,9 +135,9 @@ const ReactFlow = ({ connectionLineType = ConnectionLineType.Bezier, connectionLineStyle, connectionLineComponent, - deleteKeyCode = 8, - selectionKeyCode = 16, - multiSelectionKeyCode = 91, + deleteKeyCode = 'Backspace', + selectionKeyCode = 'Shift', + multiSelectionKeyCode = 'Meta', snapToGrid = false, snapGrid = [15, 15], onlyRenderVisibleNodes = true, diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index 1a719f4c..d7e392a6 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -3,11 +3,11 @@ import { useEffect } from 'react'; import { useStore, useStoreActions } from '../store/hooks'; import useKeyPress from './useKeyPress'; import { isNode, getConnectedEdges } from '../utils/graph'; -import { Elements } from '../types'; +import { Elements, KeyCode } from '../types'; interface HookParams { - deleteKeyCode: number; - multiSelectionKeyCode: number; + deleteKeyCode: KeyCode; + multiSelectionKeyCode: KeyCode; onElementsRemove?: (elements: Elements) => void; } diff --git a/src/hooks/useKeyPress.ts b/src/hooks/useKeyPress.ts index 6fb340fa..80685137 100644 --- a/src/hooks/useKeyPress.ts +++ b/src/hooks/useKeyPress.ts @@ -1,27 +1,28 @@ import { useState, useEffect } from 'react'; import { isInputDOMNode } from '../utils'; +import { KeyCode } from '../types'; -export default (keyCode: number): boolean => { +export default (keyCode: KeyCode): boolean => { const [keyPressed, setKeyPressed] = useState(false); - function downHandler(event: KeyboardEvent) { - if (event.keyCode === keyCode && !isInputDOMNode(event)) { - setKeyPressed(true); - } - } - - const upHandler = (event: KeyboardEvent) => { - if (event.keyCode === keyCode && !isInputDOMNode(event)) { - setKeyPressed(false); - } - }; - - const resetHandler = () => { - setKeyPressed(false); - }; - useEffect(() => { + const downHandler = (event: KeyboardEvent) => { + if (!isInputDOMNode(event) && (event.key === keyCode || event.keyCode === keyCode)) { + event.preventDefault(); + + setKeyPressed(true); + } + }; + + const upHandler = (event: KeyboardEvent) => { + if (!isInputDOMNode(event) && (event.key === keyCode || event.keyCode === keyCode)) { + setKeyPressed(false); + } + }; + + const resetHandler = () => setKeyPressed(false); + window.addEventListener('keydown', downHandler); window.addEventListener('keyup', upHandler); window.addEventListener('blur', resetHandler); @@ -31,7 +32,7 @@ export default (keyCode: number): boolean => { window.removeEventListener('keyup', upHandler); window.removeEventListener('blur', resetHandler); }; - }, []); + }, [keyCode, setKeyPressed]); return keyPressed; }; diff --git a/src/types/index.ts b/src/types/index.ts index d912f2bc..d0aba74a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -329,3 +329,5 @@ export type FlowTransform = { }; export type TranslateExtent = [[number, number], [number, number]]; + +export type KeyCode = number | string; From 5e28762f53eba2f4f28271c014856b7357e414ea Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 10 Nov 2020 17:06:18 +0100 Subject: [PATCH 13/43] refactor(nodes): don't re-render on pan #677 --- src/components/Nodes/wrapNode.tsx | 4 ++-- src/container/NodeRenderer/index.tsx | 8 ++++---- src/types/index.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 5e69704b..4e8bfba6 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -12,7 +12,7 @@ export default (NodeComponent: ComponentType) => { id, type, data, - transform, + scale, xPos, yPos, selected, @@ -199,7 +199,7 @@ export default (NodeComponent: ComponentType) => { onStart={onDragStart} onDrag={onDrag} onStop={onDragStop} - scale={transform[2]} + scale={scale} disabled={!isDraggable} cancel=".nodrag" nodeRef={nodeElement} diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 66125c1d..8faf9cb6 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -2,7 +2,7 @@ import React, { memo, ComponentType, MouseEvent } from 'react'; import { useStoreState } from '../../store/hooks'; import { getNodesInside } from '../../utils/graph'; -import { Node, Transform, NodeTypesType, WrapNodeProps, Elements, Edge } from '../../types'; +import { Node, NodeTypesType, WrapNodeProps, Elements, Edge } from '../../types'; interface NodeRendererProps { nodeTypes: NodeTypesType; @@ -22,7 +22,7 @@ interface NodeRendererProps { function renderNode( node: Node, props: NodeRendererProps, - transform: Transform, + scale: number, selectedElements: Elements | null, nodesDraggable: boolean, nodesConnectable: boolean, @@ -57,7 +57,7 @@ function renderNode( onContextMenu={props.onNodeContextMenu} onNodeDragStart={props.onNodeDragStart} onNodeDragStop={props.onNodeDragStop} - transform={transform} + scale={scale} selected={isSelected} style={node.style} className={node.className} @@ -93,7 +93,7 @@ const NodeRenderer = (props: NodeRendererProps) => { return (
{renderNodes.map((node) => - renderNode(node, props, transform, selectedElements, nodesDraggable, nodesConnectable, elementsSelectable) + renderNode(node, props, transform[2], selectedElements, nodesDraggable, nodesConnectable, elementsSelectable) )}
); diff --git a/src/types/index.ts b/src/types/index.ts index d0aba74a..9f428d2f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -201,7 +201,7 @@ export interface WrapNodeProps { type: string; data: any; selected: boolean; - transform: Transform; + scale: number; xPos: number; yPos: number; isSelectable: boolean; From 8bba855a1721c13fe13e8ecd052f91cb1061a5d3 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 10 Nov 2020 17:06:58 +0100 Subject: [PATCH 14/43] 8.0.0-next.2 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 190ccc0a..6262176a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "8.0.0-next.1", + "version": "8.0.0-next.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f2cb51e1..102687af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "8.0.0-next.1", + "version": "8.0.0-next.2", "main": "dist/ReactFlow.js", "module": "dist/ReactFlow.esm.js", "types": "dist/index.d.ts", From ae6ce3cd0583bd18d59bd677020d7e82d0e1a59e Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 10 Nov 2020 17:56:19 +0100 Subject: [PATCH 15/43] feat(panOnScroll): add mode options free, vertical and horizontal --- example/src/Interaction/index.js | 20 +++++++++++++++++++- src/container/FlowRenderer/index.tsx | 2 ++ src/container/GraphView/index.tsx | 2 ++ src/container/ReactFlow/index.tsx | 4 ++++ src/container/ZoomPane/index.tsx | 10 ++++++---- src/types/index.ts | 6 ++++++ 6 files changed, 39 insertions(+), 5 deletions(-) diff --git a/example/src/Interaction/index.js b/example/src/Interaction/index.js index 39e83887..c60589ff 100644 --- a/example/src/Interaction/index.js +++ b/example/src/Interaction/index.js @@ -17,6 +17,7 @@ const onElementClick = (event, element) => console.log('click', element); const onPaneClick = (event) => console.log('onPaneClick', event); const onPaneScroll = (event) => console.log('onPaneScroll', event); const onPaneContextMenu = (event) => console.log('onPaneContextMenu', event); +const onMoveEnd = (event) => console.log('onMoveEnd', event); const InteractionFlow = () => { const [elements, setElements] = useState(initialElements); @@ -27,6 +28,7 @@ const InteractionFlow = () => { const [isConnectable, setIsConnectable] = useState(false); const [zoomOnScroll, setZoomOnScroll] = useState(false); const [panOnScroll, setPanOnScroll] = useState(false); + const [panOnScrollMode, setPanOnScrollMode] = useState('free'); const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState(false); const [paneMoveable, setPaneMoveable] = useState(true); const [captureZoomClick, setCaptureZoomClick] = useState(false); @@ -41,6 +43,7 @@ const InteractionFlow = () => { nodesDraggable={isDraggable} zoomOnScroll={zoomOnScroll} panOnScroll={panOnScroll} + panOnScrollMode={panOnScrollMode} zoomOnDoubleClick={zoomOnDoubleClick} onConnect={onConnect} onElementClick={captureElementClick ? onElementClick : undefined} @@ -50,7 +53,7 @@ const InteractionFlow = () => { onPaneClick={captureZoomClick ? onPaneClick : undefined} onPaneScroll={captureZoomScroll ? onPaneScroll : undefined} onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined} - onMoveEnd={(evt) => console.log('on move end', evt)} + onMoveEnd={onMoveEnd} > @@ -116,6 +119,21 @@ const InteractionFlow = () => { />
+
+ +