feat(hooks): add useReactFlow hook, refactor types and properties

This commit is contained in:
Christopher Möller
2022-01-19 18:31:49 +01:00
parent 1755da3089
commit c2bae79916
27 changed files with 402 additions and 446 deletions
+24 -28
View File
@@ -1,15 +1,12 @@
import { useState, MouseEvent } from 'react';
import { MouseEvent } from 'react';
import ReactFlow, {
addEdge,
ReactFlowProvider,
Background,
BackgroundVariant,
Node,
Edge,
Connection,
useNodesState,
useEdgesState,
ReactFlowInstance,
useReactFlow,
} from 'react-flow-renderer';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
@@ -28,52 +25,45 @@ const initialEdges: Edge[] = [
];
const BasicFlow = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
const onPaneReady = (reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance);
const instance = useReactFlow();
const updatePos = () => {
setNodes((nds) => {
return nds.map((node) => {
instance.setNodes((nodes) =>
nodes.map((node) => {
node.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return node;
});
});
})
);
};
const logToObject = () => console.log(rfInstance?.toObject());
const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 });
const logToObject = () => console.log(instance.toObject());
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
setNodes((nds) => {
return nds.map((node) => {
instance.setNodes((nodes) =>
nodes.map((node) => {
node.className = node.className === 'light' ? 'dark' : 'light';
return node;
});
});
})
);
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onPaneReady={onPaneReady}
defaultNodes={initialNodes}
defaultEdges={initialEdges}
onNodeClick={onNodeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
defaultZoom={1.5}
minZoom={0.2}
maxZoom={4}
fitViewOnInit
>
<Background variant={BackgroundVariant.Lines} />
@@ -93,4 +83,10 @@ const BasicFlow = () => {
);
};
export default BasicFlow;
export default function App() {
return (
<ReactFlowProvider>
<BasicFlow />
</ReactFlowProvider>
);
}
+21 -31
View File
@@ -1,13 +1,10 @@
import { useState } from 'react';
import ReactFlow, {
useReactFlow,
Background,
BackgroundVariant,
Node,
Edge,
ReactFlowInstance,
ReactFlowProvider,
useUpdateNodes,
useUpdateEdges,
} from 'react-flow-renderer';
const defaultNodes: Node[] = [
@@ -27,40 +24,34 @@ const defaultEdgeOptions = {
};
const DefaultNodes = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const updateNodes = useUpdateNodes();
const updateEdges = useUpdateEdges();
const instance = useReactFlow();
const logToObject = () => console.log(rfInstance?.toObject());
const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 });
const logToObject = () => console.log(instance.toObject());
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
const updateNodePositions = () => {
updateNodes((nodes, setNodes) => {
setNodes(
nodes.map((node) => {
node.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
instance.setNodes((nodes) =>
nodes.map((node) => {
node.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return node;
})
);
});
return node;
})
);
};
const updateEdgeColors = () => {
updateEdges((edges, setEdges) => {
setEdges(
edges.map((edge) => {
edge.style = {
stroke: '#ff5050',
};
instance.setEdges((edges) =>
edges.map((edge) => {
edge.style = {
stroke: '#ff5050',
};
return edge;
})
);
});
return edge;
})
);
};
return (
@@ -68,7 +59,6 @@ const DefaultNodes = () => {
defaultNodes={defaultNodes}
defaultEdges={defaultEdges}
defaultEdgeOptions={defaultEdgeOptions}
onPaneReady={setRfInstance}
fitViewOnInit
>
<Background variant={BackgroundVariant.Lines} />
+2 -2
View File
@@ -7,7 +7,7 @@ import ReactFlow, {
Connection,
Edge,
PanOnScrollMode,
FlowTransform,
Viewport,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
@@ -31,7 +31,7 @@ const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
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 onMoveEnd = (viewport?: Viewport) => console.log('onMoveEnd', viewport);
const InteractionFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
+1 -1
View File
@@ -107,7 +107,7 @@ const NestedFlow = () => {
};
const logToObject = () => console.log(rfInstance?.toObject());
const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 });
const resetTransform = () => rfInstance?.setViewport({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
setNodes((nds) => {
+3 -3
View File
@@ -5,7 +5,7 @@ import ReactFlow, {
Controls,
Background,
Node,
FlowTransform,
Viewport,
SnapGrid,
Connection,
Edge,
@@ -36,8 +36,8 @@ const onPaneReady = (reactFlowInstance: ReactFlowInstance) => {
console.log('pane ready:', reactFlowInstance);
};
const onMoveStart = (transform?: FlowTransform) => console.log('zoom/move start', transform);
const onMoveEnd = (transform?: FlowTransform) => console.log('zoom/move end', transform);
const onMoveStart = (transform?: Viewport) => console.log('zoom/move start', transform);
const onMoveEnd = (transform?: Viewport) => 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);
+6 -6
View File
@@ -1,5 +1,5 @@
import React, { memo, useCallback, Dispatch, FC } from 'react';
import { useZoomPanHelper, ReactFlowInstance, Edge, Node, FlowExportObject } from 'react-flow-renderer';
import { useReactFlow, ReactFlowInstance, Edge, Node, ReactFlowJsonObject } from 'react-flow-renderer';
import localforage from 'localforage';
localforage.config({
@@ -18,7 +18,7 @@ type ControlsProps = {
};
const Controls: FC<ControlsProps> = ({ rfInstance, setNodes, setEdges }) => {
const { setTransform } = useZoomPanHelper();
const { setViewport } = useReactFlow();
const onSave = useCallback(() => {
if (rfInstance) {
@@ -29,18 +29,18 @@ const Controls: FC<ControlsProps> = ({ rfInstance, setNodes, setEdges }) => {
const onRestore = useCallback(() => {
const restoreFlow = async () => {
const flow: FlowExportObject | null = await localforage.getItem(flowKey);
const flow: ReactFlowJsonObject | null = await localforage.getItem(flowKey);
if (flow) {
const [x = 0, y = 0] = flow.position;
const { x, y, zoom } = flow.viewport;
setNodes(flow.nodes || []);
setEdges(flow.edges || []);
setTransform({ x, y, zoom: flow.zoom || 0 });
setViewport({ x, y, zoom: zoom || 0 });
}
};
restoreFlow();
}, [setNodes, setEdges, setTransform]);
}, [setNodes, setEdges, setViewport]);
const onAdd = useCallback(() => {
const newNode = {
+1 -1
View File
@@ -129,7 +129,7 @@ const BasicFlow = () => {
};
const logToObject = () => console.log(rfInstance?.toObject());
const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 });
const resetTransform = () => rfInstance?.setViewport({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
setNodes((nds) => {
+2 -2
View File
@@ -1,9 +1,9 @@
import { useCallback } from 'react';
import ReactFlow, {
useReactFlow,
NodeTypesType,
addEdge,
useZoomPanHelper,
ReactFlowProvider,
Node,
Connection,
@@ -184,7 +184,7 @@ const UpdateNodeInternalsFlow = () => {
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
const { project } = useZoomPanHelper();
const { project } = useReactFlow();
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
setEdges((els) => updateEdge(oldEdge, newConnection, els));
+2 -2
View File
@@ -3,7 +3,7 @@ import { useCallback, CSSProperties } from 'react';
import ReactFlow, {
NodeTypesType,
addEdge,
useZoomPanHelper,
useReactFlow,
ReactFlowProvider,
Node,
Connection,
@@ -41,7 +41,7 @@ const UpdateNodeInternalsFlow = () => {
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
const updateNodeInternals = useUpdateNodeInternals();
const { project } = useZoomPanHelper();
const { project } = useReactFlow();
const onPaneClick = useCallback(
(evt) =>
+2 -2
View File
@@ -5,7 +5,7 @@ import ReactFlow, {
addEdge,
Background,
MiniMap,
useZoomPanHelper,
useReactFlow,
ReactFlowProvider,
Connection,
Edge,
@@ -33,7 +33,7 @@ const UseZoomPanHelperFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const { project, setCenter, zoomIn, zoomOut } = useZoomPanHelper();
const { project, setCenter, zoomIn, zoomOut } = useReactFlow();
const onPaneClick = useCallback(
(evt) => {
@@ -1,16 +1,9 @@
import React, { memo, useMemo, FC, HTMLAttributes } from 'react';
import React, { memo, useMemo, FC } from 'react';
import cc from 'classcat';
import { useStore } from '../../store';
import { BackgroundVariant, ReactFlowState } from '../../types';
import { createGridLinesPath, createGridDotsPath } from './utils';
export interface BackgroundProps extends HTMLAttributes<SVGElement> {
variant?: BackgroundVariant;
gap?: number;
color?: string;
size?: number;
}
import { BackgroundVariant, ReactFlowState, BackgroundProps } from '../../types';
const defaultColors = {
[BackgroundVariant.Dots]: '#81818a',
+4 -17
View File
@@ -1,7 +1,8 @@
import React, { memo, useCallback, HTMLAttributes, FC, useEffect, useState } from 'react';
import React, { memo, useCallback, FC, useEffect, useState } from 'react';
import cc from 'classcat';
import { useStore, useStoreApi } from '../../store';
import useViewportHelper from '../../hooks/useViewportHelper';
import PlusIcon from '../../../assets/icons/plus.svg';
import MinusIcon from '../../../assets/icons/minus.svg';
@@ -9,21 +10,7 @@ import FitviewIcon from '../../../assets/icons/fitview.svg';
import LockIcon from '../../../assets/icons/lock.svg';
import UnlockIcon from '../../../assets/icons/unlock.svg';
import useZoomPanHelper from '../../hooks/useZoomPanHelper';
import { FitViewParams, ReactFlowState } from '../../types';
export interface ControlProps extends HTMLAttributes<HTMLDivElement> {
showZoom?: boolean;
showFitView?: boolean;
showInteractive?: boolean;
fitViewParams?: FitViewParams;
onZoomIn?: () => void;
onZoomOut?: () => void;
onFitView?: () => void;
onInteractiveChange?: (interactiveStatus: boolean) => void;
}
export interface ControlButtonProps extends HTMLAttributes<HTMLButtonElement> {}
import { ControlProps, ControlButtonProps, ReactFlowState } from '../../types';
export const ControlButton: FC<ControlButtonProps> = ({ children, className, ...rest }) => (
<button type="button" className={cc(['react-flow__controls-button', className])} {...rest}>
@@ -49,7 +36,7 @@ const Controls: FC<ControlProps> = ({
const store = useStoreApi();
const [isVisible, setIsVisible] = useState<boolean>(false);
const isInteractive = useStore(isInteractiveSelector);
const { zoomIn, zoomOut, fitView } = useZoomPanHelper();
const { zoomIn, zoomOut, fitView } = useViewportHelper();
const mapClasses = cc(['react-flow__controls', className]);
+9 -16
View File
@@ -1,23 +1,14 @@
import React, { memo, HTMLAttributes } from 'react';
import React, { memo } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import MiniMapNode from './MiniMapNode';
import { useStore } from '../../store';
import { getRectOfNodeInternals } from '../../utils/graph';
import { getBoundsofRects } from '../../utils';
import { Node, ReactFlowState, Rect } from '../../types';
import MiniMapNode from './MiniMapNode';
type StringFunc = (node: Node) => string;
export interface MiniMapProps extends HTMLAttributes<SVGSVGElement> {
nodeColor?: string | StringFunc;
nodeStrokeColor?: string | StringFunc;
nodeClassName?: string | StringFunc;
nodeBorderRadius?: number;
nodeStrokeWidth?: number;
maskColor?: string;
}
import { MiniMapProps, GetMiniMapNodeAttribute, ReactFlowState, Rect } from '../../types';
declare const window: any;
@@ -47,11 +38,13 @@ const MiniMap = ({
const mapClasses = cc(['react-flow__minimap', className]);
const elementWidth = (style?.width || defaultWidth)! as number;
const elementHeight = (style?.height || defaultHeight)! as number;
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc;
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as GetMiniMapNodeAttribute;
const nodeStrokeColorFunc = (
nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor
) as StringFunc;
const nodeClassNameFunc = (nodeClassName instanceof Function ? nodeClassName : () => nodeClassName) as StringFunc;
) as GetMiniMapNodeAttribute;
const nodeClassNameFunc = (
nodeClassName instanceof Function ? nodeClassName : () => nodeClassName
) as GetMiniMapNodeAttribute;
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);
+1 -2
View File
@@ -4,9 +4,8 @@ import FlowRenderer from '../FlowRenderer';
import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer';
import Viewport from '../Viewport';
import { ReactFlowProps } from '../ReactFlow';
import useOnPaneReadyHandler from '../../hooks/useOnPaneReadyHandler';
import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types';
import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode, ReactFlowProps } from '../../types';
export interface GraphViewProps extends Omit<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges'> {
nodeTypes: NodeTypesType;
+3 -120
View File
@@ -1,12 +1,4 @@
import React, {
useMemo,
CSSProperties,
HTMLAttributes,
MouseEvent as ReactMouseEvent,
WheelEvent,
forwardRef,
FunctionComponent,
} from 'react';
import React, { useMemo, forwardRef, FunctionComponent } from 'react';
import cc from 'classcat';
import GraphView from '../GraphView';
@@ -20,31 +12,8 @@ import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../compon
import Attribution from '../../components/Attribution';
import { createEdgeTypes } from '../EdgeRenderer/utils';
import Wrapper from './Wrapper';
import {
OnSelectionChangeFunc,
NodeTypesType,
EdgeTypesType,
Node,
Edge,
ConnectionMode,
ConnectionLineType,
ConnectionLineComponent,
FlowTransform,
OnConnectStart,
OnConnectStop,
OnConnectEnd,
OnConnect,
CoordinateExtent,
KeyCode,
PanOnScrollMode,
OnEdgeUpdateFunc,
NodeChange,
EdgeChange,
OnPaneReady,
ProOptions,
AttributionPosition,
DefaultEdgeOptions,
} from '../../types';
import { ConnectionMode, ConnectionLineType, PanOnScrollMode, ReactFlowProps, ReactFlowRefType } from '../../types';
import '../../style.css';
import '../../theme-default.css';
@@ -62,92 +31,6 @@ const defaultEdgeTypes = {
smoothstep: SmoothStepEdge,
};
export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onPaneReady'> {
nodes?: Node[];
edges?: Edge[];
defaultNodes?: Node[];
defaultEdges?: Edge[];
defaultEdgeOptions?: DefaultEdgeOptions;
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;
onNodeMouseLeave?: (event: ReactMouseEvent, node: Node) => void;
onNodeContextMenu?: (event: ReactMouseEvent, node: Node) => void;
onNodeDragStart?: (event: ReactMouseEvent, node: Node) => void;
onNodeDrag?: (event: ReactMouseEvent, node: Node) => void;
onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void;
onConnect?: OnConnect;
onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
onPaneReady?: OnPaneReady;
onMove?: (flowTransform?: FlowTransform) => void;
onMoveStart?: (flowTransform?: FlowTransform) => void;
onMoveEnd?: (flowTransform?: FlowTransform) => void;
onSelectionChange?: OnSelectionChangeFunc;
onSelectionDragStart?: (event: ReactMouseEvent, nodes: Node[]) => void;
onSelectionDrag?: (event: ReactMouseEvent, nodes: Node[]) => void;
onSelectionDragStop?: (event: ReactMouseEvent, nodes: Node[]) => void;
onSelectionContextMenu?: (event: ReactMouseEvent, nodes: Node[]) => void;
onPaneScroll?: (event?: WheelEvent) => void;
onPaneClick?: (event: ReactMouseEvent) => void;
onPaneContextMenu?: (event: ReactMouseEvent) => void;
nodeTypes?: NodeTypesType;
edgeTypes?: EdgeTypesType;
connectionMode?: ConnectionMode;
connectionLineType?: ConnectionLineType;
connectionLineStyle?: CSSProperties;
connectionLineComponent?: ConnectionLineComponent;
deleteKeyCode?: KeyCode;
selectionKeyCode?: KeyCode;
multiSelectionKeyCode?: KeyCode;
zoomActivationKeyCode?: KeyCode;
snapToGrid?: boolean;
snapGrid?: [number, number];
onlyRenderVisibleElements?: boolean;
nodesDraggable?: boolean;
nodesConnectable?: boolean;
elementsSelectable?: boolean;
selectNodesOnDrag?: boolean;
paneMoveable?: boolean;
minZoom?: number;
maxZoom?: number;
defaultZoom?: number;
defaultPosition?: [number, number];
translateExtent?: CoordinateExtent;
preventScrolling?: boolean;
nodeExtent?: CoordinateExtent;
defaultMarkerColor?: string;
zoomOnScroll?: boolean;
zoomOnPinch?: boolean;
panOnScroll?: boolean;
panOnScrollSpeed?: number;
panOnScrollMode?: PanOnScrollMode;
zoomOnDoubleClick?: boolean;
onEdgeUpdate?: OnEdgeUpdateFunc;
onEdgeContextMenu?: (event: ReactMouseEvent, edge: Edge) => void;
onEdgeMouseEnter?: (event: ReactMouseEvent, edge: Edge) => void;
onEdgeMouseMove?: (event: ReactMouseEvent, edge: Edge) => void;
onEdgeMouseLeave?: (event: ReactMouseEvent, edge: Edge) => void;
onEdgeDoubleClick?: (event: ReactMouseEvent, edge: Edge) => void;
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void;
edgeUpdaterRadius?: number;
noDragClassName?: string;
noWheelClassName?: string;
noPanClassName?: string;
fitViewOnInit?: boolean;
connectOnClick?: boolean;
attributionPosition?: AttributionPosition;
pro?: ProOptions;
}
export type ReactFlowRefType = HTMLDivElement;
const initSnapGrid: [number, number] = [15, 15];
const initDefaultPosition: [number, number] = [0, 0];
+11 -13
View File
@@ -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, PanOnScrollMode, KeyCode, ReactFlowState } from '../../types';
import { Viewport, PanOnScrollMode, KeyCode, ReactFlowState } from '../../types';
interface ZoomPaneProps {
selectionKeyPressed: boolean;
@@ -21,9 +21,9 @@ interface ZoomPaneProps {
paneMoveable?: boolean;
defaultPosition?: [number, number];
defaultZoom?: number;
onMove?: (flowTransform?: FlowTransform) => void;
onMoveStart?: (flowTransform?: FlowTransform) => void;
onMoveEnd?: (flowTransform?: FlowTransform) => void;
onMove?: (viewport?: Viewport) => void;
onMoveStart?: (viewport?: Viewport) => void;
onMoveEnd?: (viewport?: Viewport) => void;
zoomActivationKeyCode?: KeyCode;
preventScrolling?: boolean;
children: ReactNode;
@@ -31,15 +31,13 @@ interface ZoomPaneProps {
noPanClassName: string;
}
const viewChanged = (prevTransform: FlowTransform, eventTransform: any): boolean =>
prevTransform.x !== eventTransform.x ||
prevTransform.y !== eventTransform.y ||
prevTransform.zoom !== eventTransform.k;
const viewChanged = (prevViewport: Viewport, eventViewport: any): boolean =>
prevViewport.x !== eventViewport.x || prevViewport.y !== eventViewport.y || prevViewport.zoom !== eventViewport.k;
const eventToFlowTransform = (eventTransform: any): FlowTransform => ({
x: eventTransform.x,
y: eventTransform.y,
zoom: eventTransform.k,
const eventToFlowTransform = (eventViewport: any): Viewport => ({
x: eventViewport.x,
y: eventViewport.y,
zoom: eventViewport.k,
});
const isWrappedWithClass = (event: any, className: string | undefined) => event.target.closest(`.${className}`);
@@ -73,7 +71,7 @@ const ZoomPane = ({
}: ZoomPaneProps) => {
const store = useStoreApi();
const zoomPane = useRef<HTMLDivElement>(null);
const prevTransform = useRef<FlowTransform>({ x: 0, y: 0, zoom: 0 });
const prevTransform = useRef<Viewport>({ x: 0, y: 0, zoom: 0 });
const { d3Zoom, d3Selection, d3ZoomHandler } = useStore(selector, shallow);
const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode);
-31
View File
@@ -1,31 +0,0 @@
import { useCallback } from 'react';
import shallow from 'zustand/shallow';
import { NodeBounds } from '../types';
import { useStore } from '../store';
function useNodeBounds(id: string): NodeBounds | null {
const nodeBounds = useStore(
useCallback(
(s) => {
const nodeItem = s.nodeInternals.get(id);
if (!nodeItem) {
return null;
}
return {
...nodeItem.positionAbsolute,
width: nodeItem.width ?? null,
height: nodeItem.height ?? null,
};
},
[id]
),
shallow
);
return nodeBounds;
}
export default useNodeBounds;
+6 -54
View File
@@ -1,67 +1,19 @@
import { useEffect, useRef } from 'react';
import { pointToRendererPoint } from '../utils/graph';
import { useStoreApi } from '../store';
import useZoomPanHelper from '../hooks/useZoomPanHelper';
import { OnPaneReady, XYPosition, Node, Edge, FlowExportObject } from '../types';
import useReactFlow from './useReactFlow';
import { OnPaneReady } from '../types';
function useOnPaneReadyHandler(onPaneReady: OnPaneReady<any> | undefined) {
const ReactFlowInstance = useReactFlow();
const isInitialized = useRef<boolean>(false);
const store = useStoreApi();
const { zoomIn, zoomOut, zoomTo, getZoom, setTransform, getTransform, setCenter, fitView, initialized } =
useZoomPanHelper();
useEffect(() => {
if (!isInitialized.current && initialized) {
if (onPaneReady) {
const project = (position: XYPosition): XYPosition => {
const { transform, snapToGrid, snapGrid } = store.getState();
return pointToRendererPoint(position, transform, snapToGrid, snapGrid);
};
const getNodes = (): Node[] => {
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 }));
};
const getEdges = (): Edge[] => {
const { edges = [] } = store.getState();
return edges.map((e) => ({ ...e }));
};
const toObject = (): FlowExportObject => {
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 })),
position: [transform[0], transform[1]],
zoom: transform[2],
};
};
onPaneReady({
fitView: (params = { padding: 0.1 }) => fitView(params),
zoomIn,
zoomOut,
zoomTo,
getZoom,
setTransform,
getTransform,
setCenter,
project,
getNodes,
getEdges,
toObject,
});
}
if (!isInitialized.current && ReactFlowInstance.viewportInitialized && onPaneReady) {
onPaneReady(ReactFlowInstance);
isInitialized.current = true;
}
}, [onPaneReady, zoomIn, zoomOut, zoomTo, setTransform, fitView, initialized]);
}, [onPaneReady, ReactFlowInstance]);
}
export default useOnPaneReadyHandler;
+86
View File
@@ -0,0 +1,86 @@
import { useCallback } from 'react';
import useViewportHelper from './useViewportHelper';
import { useStoreApi } from '../store';
import { ReactFlowInstance, Instance } from '../types';
export default function useReactFlow<NodeData, EdgeData>(): ReactFlowInstance<NodeData, EdgeData> {
const { initialized: viewportInitialized, ...viewportHelperFunctions } = useViewportHelper();
const store = useStoreApi();
const getNodes = useCallback<Instance.GetNodes<NodeData>>(() => {
const { nodeInternals } = store.getState();
const nodes = Array.from(nodeInternals.values());
return nodes.map((n) => ({ ...n }));
}, []);
const getNode = useCallback<Instance.GetNode<NodeData>>((id) => {
const { nodeInternals } = store.getState();
return nodeInternals.get(id);
}, []);
const getEdges = useCallback<Instance.GetEdges<EdgeData>>(() => {
const { edges = [] } = store.getState();
return edges.map((e) => ({ ...e }));
}, []);
const getEdge = useCallback<Instance.GetEdge<EdgeData>>((id) => {
const { edges = [] } = store.getState();
return edges.find((e) => e.id === id);
}, []);
const setNodes = useCallback<Instance.SetNodes<NodeData>>((payload) => {
const { nodeInternals, setNodes } = store.getState();
const nodes = Array.from(nodeInternals.values());
const nextNodes = typeof payload === 'function' ? payload(nodes) : payload;
setNodes(nextNodes);
}, []);
const setEdges = useCallback<Instance.SetEdges<EdgeData>>((payload) => {
const { edges = [], setEdges } = store.getState();
const nextEdges = typeof payload === 'function' ? payload(edges) : payload;
setEdges(nextEdges);
}, []);
const addNodes = useCallback<Instance.AddNodes<NodeData>>((payload) => {
const nodes = Array.isArray(payload) ? payload : [payload];
const { nodeInternals, setNodes } = store.getState();
const currentNodes = Array.from(nodeInternals.values());
const nextNodes = [...currentNodes, ...nodes];
setNodes(nextNodes);
}, []);
const addEdges = useCallback<Instance.AddEdges<EdgeData>>((payload) => {
const nextEdges = Array.isArray(payload) ? payload : [payload];
const { edges = [], setEdges } = store.getState();
setEdges([...edges, ...nextEdges]);
}, []);
const toObject = useCallback<Instance.ToObject<NodeData, EdgeData>>(() => {
const { nodeInternals, edges = [], transform } = store.getState();
const nodes = Array.from(nodeInternals.values());
const [x, y, zoom] = transform;
return {
nodes: nodes.map((n) => ({ ...n })),
edges: edges.map((e) => ({ ...e })),
viewport: {
x,
y,
zoom,
},
};
}, []);
return {
...viewportHelperFunctions,
viewportInitialized,
getNodes,
getNode,
getEdges,
getEdge,
setNodes,
setEdges,
addNodes,
addEdges,
toObject,
};
}
-23
View File
@@ -1,23 +0,0 @@
import { useMemo } from 'react';
import { useStoreApi } from '../store';
import { Edge } from '../types';
type FunctionParams<T> = (nodes: Edge<T>[], setEdges: (nodes: Edge<T>[]) => void) => void;
function useUpdateEdges<T>() {
const store = useStoreApi();
const update = useMemo(() => {
return (action: FunctionParams<T>): void => {
const { edges, setEdges } = store.getState();
if (typeof action === 'function') {
action(edges, setEdges);
}
};
}, []);
return update;
}
export default useUpdateEdges;
-24
View File
@@ -1,24 +0,0 @@
import { useMemo } from 'react';
import { useStoreApi } from '../store';
import { Node } from '../types';
type FunctionParams<T> = (nodes: Node<T>[], setNodes: (nodes: Node<T>[]) => void) => void;
function useUpdateNodes<T>() {
const store = useStoreApi();
const update = useMemo(() => {
return (action: FunctionParams<T>): void => {
const { nodeInternals, setNodes } = store.getState();
const nodes = Array.from(nodeInternals.values()) as Node<T>[];
if (typeof action === 'function') {
action(nodes, setNodes);
}
};
}, []);
return update;
}
export default useUpdateNodes;
@@ -5,17 +5,17 @@ import { Selection as D3Selection } from 'd3';
import { useStoreApi, useStore } from '../store';
import { getRectOfNodeInternals, pointToRendererPoint, getTransformForBounds } from '../utils/graph';
import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, ReactFlowState, Rect, XYPosition } from '../types';
import { FitViewParams, Viewport, ViewportHelperFunctions, ReactFlowState, Rect, XYPosition } from '../types';
const DEFAULT_PADDING = 0.1;
const initialZoomPanHelper: ZoomPanHelperFunctions = {
const initialViewportHelper: ViewportHelperFunctions = {
zoomIn: () => {},
zoomOut: () => {},
zoomTo: (_: number) => {},
getZoom: () => 1,
setTransform: (_: FlowTransform) => {},
getTransform: () => ({ x: 0, y: 0, zoom: 1 }),
setViewport: (_: Viewport) => {},
getViewport: () => ({ x: 0, y: 0, zoom: 1 }),
fitView: (_: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {},
setCenter: (_: number, __: number) => {},
fitBounds: (_: Rect) => {},
@@ -32,11 +32,11 @@ const getTransition = (selection: D3Selection<Element, unknown, null, undefined>
return selection.transition().duration(duration);
};
const useZoomPanHelper = (): ZoomPanHelperFunctions => {
const useViewportHelper = (): ViewportHelperFunctions => {
const store = useStoreApi();
const { d3Zoom, d3Selection } = useStore(selector, shallow);
const zoomPanHelperFunctions = useMemo<ZoomPanHelperFunctions>(() => {
const viewportHelperFunctions = useMemo<ViewportHelperFunctions>(() => {
if (d3Selection && d3Zoom) {
return {
zoomIn: (options) => d3Zoom.scaleBy(getTransition(d3Selection, options?.duration), 1.2),
@@ -46,11 +46,11 @@ const useZoomPanHelper = (): ZoomPanHelperFunctions => {
const [, , zoom] = store.getState().transform;
return zoom;
},
setTransform: (transform, options) => {
setViewport: (transform, options) => {
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom);
d3Zoom.transform(getTransition(d3Selection, options?.duration), nextTransform);
},
getTransform: () => {
getViewport: () => {
const [x, y, zoom] = store.getState().transform;
return { x, y, zoom };
},
@@ -109,10 +109,10 @@ const useZoomPanHelper = (): ZoomPanHelperFunctions => {
};
}
return initialZoomPanHelper;
return initialViewportHelper;
}, [d3Zoom, d3Selection]);
return zoomPanHelperFunctions;
return viewportHelperFunctions;
};
export default useZoomPanHelper;
export default useViewportHelper;
+4 -12
View File
@@ -8,7 +8,7 @@ export { default as StraightEdge } from './components/Edges/StraightEdge';
export { default as StepEdge } from './components/Edges/StepEdge';
export { default as BezierEdge, getBezierPath } from './components/Edges/BezierEdge';
export { default as SmoothStepEdge, getSmoothStepPath } from './components/Edges/SmoothStepEdge';
export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils';
export * from './additional-components';
export {
isNode,
@@ -22,19 +22,11 @@ export {
getRectOfNodes,
} from './utils/graph';
export { applyNodeChanges, applyEdgeChanges } from './utils/changes';
export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils';
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper';
export { default as useReactFlow } from './hooks/useReactFlow';
export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals';
export { default as useNodeBounds } from './hooks/useNodeBounds';
export { default as useUpdateNodes } from './hooks/useUpdateNodes';
export { default as useUpdateEdges } from './hooks/useUpdateEdges';
export * from './hooks/useNodesEdgesState';
export * from './additional-components';
export { useStore, useStoreApi } from './store';
export * from './types';
export { ReactFlowProps } from './container/ReactFlow';
export { MiniMapProps } from './additional-components/MiniMap';
export { ControlProps } from './additional-components/Controls';
export { BackgroundProps } from './additional-components/Background';
export * from './types';
+146
View File
@@ -0,0 +1,146 @@
import React, { CSSProperties, HTMLAttributes, MouseEvent as ReactMouseEvent, WheelEvent } from 'react';
import {
OnSelectionChangeFunc,
NodeTypesType,
EdgeTypesType,
Node,
Edge,
ConnectionMode,
ConnectionLineType,
ConnectionLineComponent,
Viewport,
OnConnectStart,
OnConnectStop,
OnConnectEnd,
OnConnect,
CoordinateExtent,
KeyCode,
PanOnScrollMode,
OnEdgeUpdateFunc,
NodeChange,
EdgeChange,
OnPaneReady,
ProOptions,
AttributionPosition,
DefaultEdgeOptions,
FitViewParams,
BackgroundVariant,
} from '.';
export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onPaneReady'> {
nodes?: Node[];
edges?: Edge[];
defaultNodes?: Node[];
defaultEdges?: Edge[];
defaultEdgeOptions?: DefaultEdgeOptions;
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;
onNodeMouseLeave?: (event: ReactMouseEvent, node: Node) => void;
onNodeContextMenu?: (event: ReactMouseEvent, node: Node) => void;
onNodeDragStart?: (event: ReactMouseEvent, node: Node) => void;
onNodeDrag?: (event: ReactMouseEvent, node: Node) => void;
onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void;
onConnect?: OnConnect;
onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
onPaneReady?: OnPaneReady;
onMove?: (viewport?: Viewport) => void;
onMoveStart?: (viewport?: Viewport) => void;
onMoveEnd?: (viewport?: Viewport) => void;
onSelectionChange?: OnSelectionChangeFunc;
onSelectionDragStart?: (event: ReactMouseEvent, nodes: Node[]) => void;
onSelectionDrag?: (event: ReactMouseEvent, nodes: Node[]) => void;
onSelectionDragStop?: (event: ReactMouseEvent, nodes: Node[]) => void;
onSelectionContextMenu?: (event: ReactMouseEvent, nodes: Node[]) => void;
onPaneScroll?: (event?: WheelEvent) => void;
onPaneClick?: (event: ReactMouseEvent) => void;
onPaneContextMenu?: (event: ReactMouseEvent) => void;
nodeTypes?: NodeTypesType;
edgeTypes?: EdgeTypesType;
connectionMode?: ConnectionMode;
connectionLineType?: ConnectionLineType;
connectionLineStyle?: CSSProperties;
connectionLineComponent?: ConnectionLineComponent;
deleteKeyCode?: KeyCode;
selectionKeyCode?: KeyCode;
multiSelectionKeyCode?: KeyCode;
zoomActivationKeyCode?: KeyCode;
snapToGrid?: boolean;
snapGrid?: [number, number];
onlyRenderVisibleElements?: boolean;
nodesDraggable?: boolean;
nodesConnectable?: boolean;
elementsSelectable?: boolean;
selectNodesOnDrag?: boolean;
paneMoveable?: boolean;
minZoom?: number;
maxZoom?: number;
defaultZoom?: number;
defaultPosition?: [number, number];
translateExtent?: CoordinateExtent;
preventScrolling?: boolean;
nodeExtent?: CoordinateExtent;
defaultMarkerColor?: string;
zoomOnScroll?: boolean;
zoomOnPinch?: boolean;
panOnScroll?: boolean;
panOnScrollSpeed?: number;
panOnScrollMode?: PanOnScrollMode;
zoomOnDoubleClick?: boolean;
onEdgeUpdate?: OnEdgeUpdateFunc;
onEdgeContextMenu?: (event: ReactMouseEvent, edge: Edge) => void;
onEdgeMouseEnter?: (event: ReactMouseEvent, edge: Edge) => void;
onEdgeMouseMove?: (event: ReactMouseEvent, edge: Edge) => void;
onEdgeMouseLeave?: (event: ReactMouseEvent, edge: Edge) => void;
onEdgeDoubleClick?: (event: ReactMouseEvent, edge: Edge) => void;
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void;
edgeUpdaterRadius?: number;
noDragClassName?: string;
noWheelClassName?: string;
noPanClassName?: string;
fitViewOnInit?: boolean;
connectOnClick?: boolean;
attributionPosition?: AttributionPosition;
pro?: ProOptions;
}
export type ReactFlowRefType = HTMLDivElement;
export type GetMiniMapNodeAttribute<NodeData = any> = (node: Node<NodeData>) => string;
export interface MiniMapProps<NodeData = any> extends HTMLAttributes<SVGSVGElement> {
nodeColor?: string | GetMiniMapNodeAttribute<NodeData>;
nodeStrokeColor?: string | GetMiniMapNodeAttribute<NodeData>;
nodeClassName?: string | GetMiniMapNodeAttribute<NodeData>;
nodeBorderRadius?: number;
nodeStrokeWidth?: number;
maskColor?: string;
}
export interface ControlProps extends HTMLAttributes<HTMLDivElement> {
showZoom?: boolean;
showFitView?: boolean;
showInteractive?: boolean;
fitViewParams?: FitViewParams;
onZoomIn?: () => void;
onZoomOut?: () => void;
onFitView?: () => void;
onInteractiveChange?: (interactiveStatus: boolean) => void;
}
export interface ControlButtonProps extends HTMLAttributes<HTMLButtonElement> {}
export interface BackgroundProps extends HTMLAttributes<SVGElement> {
variant?: BackgroundVariant;
gap?: number;
color?: string;
size?: number;
}
+15 -36
View File
@@ -7,6 +7,7 @@ import { Node, NodeInternals, NodeDimensionUpdate, NodeDiffUpdate } from './node
import { Edge } from './edges';
import { HandleType, StartHandle } from './handles';
import { DefaultEdgeOptions } from '.';
import { ReactFlowInstance } from './instance';
export type NodeTypesType = { [key: string]: ReactNode };
export type EdgeTypesType = NodeTypesType;
@@ -15,36 +16,21 @@ export type FitView = (fitViewOptions?: FitViewParams) => void;
export type Project = (position: XYPosition) => XYPosition;
export type ToObject<T = any> = () => FlowExportObject<T>;
export type OnNodesChange = (nodes: NodeChange[]) => void;
export type OnEdgesChange = (nodes: EdgeChange[]) => void;
export type ZoomInOut = (options?: ZoomPanHelperFunctionOptions) => void;
export type ZoomTo = (zoomLevel: number, options?: ZoomPanHelperFunctionOptions) => void;
export type ZoomInOut = (options?: ViewportHelperFunctionOptions) => void;
export type ZoomTo = (zoomLevel: number, options?: ViewportHelperFunctionOptions) => void;
export type GetZoom = () => number;
export type GetTransform = () => FlowTransform;
export type SetTransform = (transform: FlowTransform, options?: ZoomPanHelperFunctionOptions) => void;
export type GetViewport = () => Viewport;
export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionOptions) => void;
export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => void;
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void;
export type ReactFlowInstance<T = any> = {
zoomIn: ZoomInOut;
zoomOut: ZoomInOut;
zoomTo: ZoomTo;
getZoom: () => number;
setCenter: SetCenter;
fitView: FitView;
project: Project;
getNodes: () => Node<T>[];
getEdges: () => Edge<T>[];
setTransform: SetTransform;
getTransform: () => FlowTransform;
toObject: ToObject<T>;
};
export type OnPaneReady<T = any> = (reactFlowInstance: ReactFlowInstance<T>) => void;
export type OnPaneReady<NodeData = any, EdgeData = any> = (
reactFlowInstance: ReactFlowInstance<NodeData, EdgeData>
) => void;
export interface Connection {
source: string | null;
@@ -58,13 +44,6 @@ export enum ConnectionMode {
Loose = 'loose',
}
export type FlowExportObject<T = any> = {
nodes: Node<T>[];
edges: Edge<T>[];
position: [number, number];
zoom: number;
};
export type OnConnect = (connection: Connection) => void;
export type FitViewParams = {
@@ -92,7 +71,7 @@ export enum BackgroundVariant {
Dots = 'dots',
}
export type FlowTransform = {
export type Viewport = {
x: number;
y: number;
zoom: number;
@@ -108,25 +87,25 @@ export enum PanOnScrollMode {
Horizontal = 'horizontal',
}
export type ZoomPanHelperFunctionOptions = {
export type ViewportHelperFunctionOptions = {
duration?: number;
};
export type SetCenterOptions = ZoomPanHelperFunctionOptions & {
export type SetCenterOptions = ViewportHelperFunctionOptions & {
zoom?: number;
};
export type FitBoundsOptions = ZoomPanHelperFunctionOptions & {
export type FitBoundsOptions = ViewportHelperFunctionOptions & {
padding?: number;
};
export interface ZoomPanHelperFunctions {
export interface ViewportHelperFunctions {
zoomIn: ZoomInOut;
zoomOut: ZoomInOut;
zoomTo: ZoomTo;
getZoom: GetZoom;
setTransform: SetTransform;
getTransform: GetTransform;
setViewport: SetViewport;
getViewport: GetViewport;
fitView: FitView;
setCenter: SetCenter;
fitBounds: FitBounds;
+2
View File
@@ -4,3 +4,5 @@ export * from './edges';
export * from './handles';
export * from './changes';
export * from './utils';
export * from './instance';
export * from './component-props';
+38
View File
@@ -0,0 +1,38 @@
import { ViewportHelperFunctions, Viewport } from './general';
import { Node } from './nodes';
import { Edge } from './edges';
export type ReactFlowJsonObject<NodeData = any, EdgeData = any> = {
nodes: Node<NodeData>[];
edges: Edge<EdgeData>[];
viewport: Viewport;
};
export namespace Instance {
export type GetNodes<NodeData> = () => Node<NodeData>[];
export type SetNodes<NodeData> = (
payload: Node<NodeData>[] | ((nodes: Node<NodeData>[]) => Node<NodeData>[])
) => void;
export type AddNodes<NodeData> = (payload: Node<NodeData>[] | Node<NodeData>) => void;
export type GetNode<NodeData> = (id: string) => Node<NodeData> | undefined;
export type GetEdges<EdgeData> = () => Edge<EdgeData>[];
export type SetEdges<EdgeData> = (
payload: Edge<EdgeData>[] | ((edges: Edge<EdgeData>[]) => Edge<EdgeData>[])
) => void;
export type GetEdge<EdgeData> = (id: string) => Edge<EdgeData> | undefined;
export type AddEdges<EdgeData> = (payload: Edge<EdgeData>[] | Edge<EdgeData>) => void;
export type ToObject<NodeData = any, EdgeData = any> = () => ReactFlowJsonObject<NodeData, EdgeData>;
}
export type ReactFlowInstance<NodeData = any, EdgeData = any> = {
getNodes: Instance.GetNodes<NodeData>;
setNodes: Instance.SetNodes<NodeData>;
addNodes: Instance.AddNodes<NodeData>;
getNode: Instance.GetNode<NodeData>;
getEdges: Instance.GetEdges<EdgeData>;
setEdges: Instance.SetEdges<EdgeData>;
addEdges: Instance.AddEdges<EdgeData>;
getEdge: Instance.GetEdge<EdgeData>;
toObject: Instance.ToObject<NodeData, EdgeData>;
viewportInitialized: boolean;
} & Omit<ViewportHelperFunctions, 'initialized'>;