Merge branch 'main' into feature/edgeUpdateWithMoreInfo-1961

This commit is contained in:
Moritz Klack
2022-04-11 13:28:45 +02:00
committed by GitHub
162 changed files with 34920 additions and 27001 deletions
+7 -12
View File
@@ -1,22 +1,17 @@
import React, { memo, useMemo, FC, HTMLAttributes } from 'react';
import React, { memo, useMemo, FC } from 'react';
import cc from 'classcat';
import { useStoreState } from '../../store/hooks';
import { BackgroundVariant } from '../../types';
import { useStore } from '../../store';
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',
[BackgroundVariant.Lines]: '#eee',
};
const transformSelector = (s: ReactFlowState) => s.transform;
const Background: FC<BackgroundProps> = ({
variant = BackgroundVariant.Dots,
gap = 15,
@@ -25,11 +20,11 @@ const Background: FC<BackgroundProps> = ({
style,
className,
}) => {
const [x, y, scale] = useStoreState((s) => s.transform);
const [x, y, scale] = useStore(transformSelector);
// when there are multiple flows on a page we need to make sure that every background gets its own pattern.
const patternId = useMemo(() => `pattern-${Math.floor(Math.random() * 100000)}`, []);
const bgClasses = cc(['react-flow__background', className]);
const bgClasses = cc(['react-flow__background', 'react-flow__container', className]);
const scaledGap = gap * scale;
const xOffset = x % scaledGap;
const yOffset = y % scaledGap;
@@ -0,0 +1,11 @@
import React from 'react';
function FitViewIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 30">
<path d="M3.692 4.63c0-.53.4-.938.939-.938h5.215V0H4.708C2.13 0 0 2.054 0 4.63v5.216h3.692V4.631zM27.354 0h-5.2v3.692h5.17c.53 0 .984.4.984.939v5.215H32V4.631A4.624 4.624 0 0027.354 0zm.954 24.83c0 .532-.4.94-.939.94h-5.215v3.768h5.215c2.577 0 4.631-2.13 4.631-4.707v-5.139h-3.692v5.139zm-23.677.94c-.531 0-.939-.4-.939-.94v-5.138H0v5.139c0 2.577 2.13 4.707 4.708 4.707h5.138V25.77H4.631z" />
</svg>
);
}
export default FitViewIcon;
@@ -0,0 +1,11 @@
import React from 'react';
function LockIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32">
<path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0 8 0 4.571 3.429 4.571 7.619v3.048H3.048A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047zm4.724-13.866H7.467V7.619c0-2.59 2.133-4.724 4.723-4.724 2.591 0 4.724 2.133 4.724 4.724v3.048z" />
</svg>
);
}
export default LockIcon;
@@ -0,0 +1,11 @@
import React from 'react';
function MinusIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 5">
<path d="M0 0h32v4.2H0z" />
</svg>
);
}
export default MinusIcon;
@@ -0,0 +1,11 @@
import React from 'react';
function PlusIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path d="M32 18.133H18.133V32h-4.266V18.133H0v-4.266h13.867V0h4.266v13.867H32z" />
</svg>
);
}
export default PlusIcon;
@@ -0,0 +1,11 @@
import React from 'react';
function UnlockIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32">
<path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0c-4.114 1.828-1.37 2.133.305 2.438 1.676.305 4.42 2.59 4.42 5.181v3.048H3.047A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047z" />
</svg>
);
}
export default UnlockIcon;
+48 -34
View File
@@ -1,29 +1,16 @@
import React, { memo, useCallback, HTMLAttributes, FC, useEffect, useState } from 'react';
import React, { memo, useCallback, FC, useEffect, useState } from 'react';
import cc from 'classcat';
import { useStoreState, useStoreActions } from '../../store/hooks';
import { useStore, useStoreApi } from '../../store';
import useReactFlow from '../../hooks/useReactFlow';
import PlusIcon from '../../../assets/icons/plus.svg';
import MinusIcon from '../../../assets/icons/minus.svg';
import FitviewIcon from '../../../assets/icons/fitview.svg';
import LockIcon from '../../../assets/icons/lock.svg';
import UnlockIcon from '../../../assets/icons/unlock.svg';
import PlusIcon from './Icons/Plus';
import MinusIcon from './Icons/Minus';
import FitviewIcon from './Icons/FitView';
import LockIcon from './Icons/Lock';
import UnlockIcon from './Icons/Unlock';
import useZoomPanHelper from '../../hooks/useZoomPanHelper';
import { FitViewParams } 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}>
@@ -31,12 +18,14 @@ export const ControlButton: FC<ControlButtonProps> = ({ children, className, ...
</button>
);
const isInteractiveSelector = (s: ReactFlowState) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable;
const Controls: FC<ControlProps> = ({
style,
showZoom = true,
showFitView = true,
showInteractive = true,
fitViewParams,
fitViewOptions,
onZoomIn,
onZoomOut,
onFitView,
@@ -44,11 +33,11 @@ const Controls: FC<ControlProps> = ({
className,
children,
}) => {
const store = useStoreApi();
const [isVisible, setIsVisible] = useState<boolean>(false);
const setInteractive = useStoreActions((actions) => actions.setInteractive);
const { zoomIn, zoomOut, fitView } = useZoomPanHelper();
const isInteractive = useStore(isInteractiveSelector);
const { zoomIn, zoomOut, fitView } = useReactFlow();
const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable);
const mapClasses = cc(['react-flow__controls', className]);
const onZoomInHandler = useCallback(() => {
@@ -62,14 +51,19 @@ const Controls: FC<ControlProps> = ({
}, [zoomOut, onZoomOut]);
const onFitViewHandler = useCallback(() => {
fitView?.(fitViewParams);
fitView?.(fitViewOptions);
onFitView?.();
}, [fitView, fitViewParams, onFitView]);
}, [fitView, fitViewOptions, onFitView]);
const onInteractiveChangeHandler = useCallback(() => {
setInteractive?.(!isInteractive);
store.setState({
nodesDraggable: !isInteractive,
nodesConnectable: !isInteractive,
elementsSelectable: !isInteractive,
});
onInteractiveChange?.(!isInteractive);
}, [isInteractive, setInteractive, onInteractiveChange]);
}, [isInteractive, onInteractiveChange]);
useEffect(() => {
setIsVisible(true);
@@ -83,21 +77,41 @@ const Controls: FC<ControlProps> = ({
<div className={mapClasses} style={style}>
{showZoom && (
<>
<ControlButton onClick={onZoomInHandler} className="react-flow__controls-zoomin">
<ControlButton
onClick={onZoomInHandler}
className="react-flow__controls-zoomin"
title="zoom in"
aria-label="zoom in"
>
<PlusIcon />
</ControlButton>
<ControlButton onClick={onZoomOutHandler} className="react-flow__controls-zoomout">
<ControlButton
onClick={onZoomOutHandler}
className="react-flow__controls-zoomout"
title="zoom out"
aria-label="zoom out"
>
<MinusIcon />
</ControlButton>
</>
)}
{showFitView && (
<ControlButton className="react-flow__controls-fitview" onClick={onFitViewHandler}>
<ControlButton
className="react-flow__controls-fitview"
onClick={onFitViewHandler}
title="fit view"
aria-label="fit view"
>
<FitviewIcon />
</ControlButton>
)}
{showInteractive && (
<ControlButton className="react-flow__controls-interactive" onClick={onInteractiveChangeHandler}>
<ControlButton
className="react-flow__controls-interactive"
onClick={onInteractiveChangeHandler}
title="toggle interactivity"
aria-label="toggle interactivity"
>
{isInteractive ? <UnlockIcon /> : <LockIcon />}
</ControlButton>
)}
@@ -26,7 +26,7 @@ const MiniMapNode = ({
strokeWidth,
className,
borderRadius,
shapeRendering
shapeRendering,
}: MiniMapNodeProps) => {
const { background, backgroundColor } = style || {};
const fill = (color || background || backgroundColor) as string;
+48 -42
View File
@@ -1,27 +1,27 @@
import React, { memo, HTMLAttributes } from 'react';
import React, { memo } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreState } from '../../store/hooks';
import { getRectOfNodes, getBoundsofRects } from '../../utils/graph';
import { Node, Rect } from '../../types';
import MiniMapNode from './MiniMapNode';
type StringFunc = (node: Node) => string;
import { useStore } from '../../store';
import { getRectOfNodes } from '../../utils/graph';
import { getBoundsofRects } from '../../utils';
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;
const defaultWidth = 200;
const defaultHeight = 150;
const selector = (s: ReactFlowState) => ({
width: s.width,
height: s.height,
transform: s.transform,
nodeInternals: s.nodeInternals,
});
const MiniMap = ({
style,
className,
@@ -32,20 +32,22 @@ const MiniMap = ({
nodeStrokeWidth = 2,
maskColor = 'rgb(240, 242, 243, 0.7)',
}: MiniMapProps) => {
const containerWidth = useStoreState((s) => s.width);
const containerHeight = useStoreState((s) => s.height);
const [tX, tY, tScale] = useStoreState((s) => s.transform);
const nodes = useStoreState((s) => s.nodes);
const { width: containerWidth, height: containerHeight, transform, nodeInternals } = useStore(selector, shallow);
const [tX, tY, tScale] = transform;
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 nodeStrokeColorFunc = (nodeStrokeColor instanceof Function
? nodeStrokeColor
: () => nodeStrokeColor) as StringFunc;
const nodeClassNameFunc = (nodeClassName instanceof Function ? nodeClassName : () => nodeClassName) as StringFunc;
const hasNodes = nodes && nodes.length;
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as GetMiniMapNodeAttribute;
const nodeStrokeColorFunc = (
nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor
) 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);
const bb = getRectOfNodes(nodes);
const viewBB: Rect = {
x: -tX / tScale,
@@ -64,7 +66,7 @@ const MiniMap = ({
const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset;
const width = viewWidth + offset * 2;
const height = viewHeight + offset * 2;
const shapeRendering = (typeof window === "undefined" || !!window.chrome) ? "crispEdges" : "geometricPrecision";
const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
return (
<svg
@@ -74,24 +76,28 @@ const MiniMap = ({
style={style}
className={mapClasses}
>
{nodes
.filter((node) => !node.isHidden)
.map((node) => (
<MiniMapNode
key={node.id}
x={node.__rf.position.x}
y={node.__rf.position.y}
width={node.__rf.width}
height={node.__rf.height}
style={node.style}
className={nodeClassNameFunc(node)}
color={nodeColorFunc(node)}
borderRadius={nodeBorderRadius}
strokeColor={nodeStrokeColorFunc(node)}
strokeWidth={nodeStrokeWidth}
shapeRendering={shapeRendering}
/>
))}
{Array.from(nodeInternals)
.filter(([_, node]) => !node.hidden && node.width && node.height)
.map(([_, node]) => {
const positionAbsolute = nodeInternals.get(node.id)?.positionAbsolute;
return (
<MiniMapNode
key={node.id}
x={positionAbsolute?.x || 0}
y={positionAbsolute?.y || 0}
width={node.width!}
height={node.height!}
style={node.style}
className={nodeClassNameFunc(node)}
color={nodeColorFunc(node)}
borderRadius={nodeBorderRadius}
strokeColor={nodeStrokeColorFunc(node)}
strokeWidth={nodeStrokeWidth}
shapeRendering={shapeRendering}
/>
);
})}
<path
className="react-flow__minimap-mask"
d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
@@ -1,16 +1,8 @@
import React, { FC, useMemo } from 'react';
import { Provider } from 'react-redux';
import React, { FC, PropsWithChildren } from 'react';
import { initialState } from '../../store';
import configureStore from '../../store/configure-store';
import { Provider, createStore } from '../../store';
const ReactFlowProvider: FC = ({ children }) => {
const store = useMemo(() => {
return configureStore(initialState);
}, []);
return <Provider store={store}>{children}</Provider>;
};
const ReactFlowProvider: FC<PropsWithChildren<{}>> = ({ children }) => <Provider createStore={createStore}>{children}</Provider>;
ReactFlowProvider.displayName = 'ReactFlowProvider';
+1 -1
View File
@@ -1,5 +1,5 @@
// These components are not used by React Flow directly
// but the user can add them as children of a React Flow component
// They can be added as children of a React Flow component
export { default as MiniMap } from './MiniMap';
export { default as Controls, ControlButton } from './Controls';
+35
View File
@@ -0,0 +1,35 @@
import React from 'react';
import cc from 'classcat';
import { AttributionPosition, ProOptions } from '../../types';
type AttributionProps = {
proOptions?: ProOptions;
position?: AttributionPosition;
};
function Attribution({ proOptions, position = 'bottom-right' }: AttributionProps) {
if (
(proOptions?.account === 'paid-sponsor' ||
proOptions?.account === 'paid-enterprise' ||
proOptions?.account === 'paid-custom') &&
proOptions?.hideAttribution
) {
return null;
}
const positionClasses = `${position}`.split('-');
return (
<div
className={cc(['react-flow__attribution', ...positionClasses])}
data-message="Please only hide this attribution when you are subscribed to React Flow Pro: https://pro.reactflow.dev/pricing"
>
<a href="https://reactflow.dev" target="_blank" rel="noopener noreferrer">
React Flow
</a>
</div>
);
}
export default Attribution;
+87 -64
View File
@@ -1,39 +1,25 @@
import React, { useEffect, useState, CSSProperties } from 'react';
import React, { useRef, CSSProperties } from 'react';
import shallow from 'zustand/shallow';
import { useStore } from '../../store';
import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import {
ElementId,
Node,
Transform,
HandleElement,
Position,
ConnectionLineType,
ConnectionLineComponent,
HandleType,
} from '../../types';
import { ConnectionLineType, ConnectionLineComponent, HandleType, Node, ReactFlowState, Position } from '../../types';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
interface ConnectionLineProps {
connectionNodeId: ElementId;
connectionHandleId: ElementId | null;
connectionNodeId: string;
connectionHandleId: string | null;
connectionHandleType: HandleType;
connectionPositionX: number;
connectionPositionY: number;
connectionLineType: ConnectionLineType;
nodes: Node[];
transform: Transform;
isConnectable: boolean;
connectionLineStyle?: CSSProperties;
CustomConnectionLineComponent?: ConnectionLineComponent;
}
const getSourceHandle = (handleId: ElementId | null, sourceNode: Node, connectionHandleType: HandleType) => {
const handleTypeInverted = connectionHandleType === 'source' ? 'target' : 'source';
const handleBound =
sourceNode.__rf.handleBounds[connectionHandleType] || sourceNode.__rf.handleBounds[handleTypeInverted];
return handleId ? handleBound.find((d: HandleElement) => d.id === handleId) : handleBound[0];
};
const selector = (s: ReactFlowState) => ({ nodeInternals: s.nodeInternals, transform: s.transform });
export default ({
connectionNodeId,
@@ -43,35 +29,76 @@ export default ({
connectionPositionX,
connectionPositionY,
connectionLineType = ConnectionLineType.Bezier,
nodes = [],
transform,
isConnectable,
CustomConnectionLineComponent,
}: ConnectionLineProps) => {
const [sourceNode, setSourceNode] = useState<Node | null>(null);
const nodeId = connectionNodeId;
const handleId = connectionHandleId;
useEffect(() => {
const nextSourceNode = nodes.find((n) => n.id === nodeId) || null;
setSourceNode(nextSourceNode);
}, []);
const { nodeInternals, transform } = useStore(selector, shallow);
const fromNode = useRef<Node | undefined>(nodeInternals.get(nodeId));
if (!sourceNode || !isConnectable) {
if (!fromNode.current || !isConnectable || !fromNode.current.handleBounds?.[connectionHandleType]) {
return null;
}
const sourceHandle = getSourceHandle(handleId, sourceNode, connectionHandleType);
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rf.width / 2;
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rf.height;
const sourceX = sourceNode.__rf.position.x + sourceHandleX;
const sourceY = sourceNode.__rf.position.y + sourceHandleY;
const handleBound = fromNode.current.handleBounds?.[connectionHandleType];
const fromHandle = handleId ? handleBound?.find((d) => d.id === handleId) : handleBound?.[0];
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.current?.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.current?.height ?? 0;
const fromX = (fromNode.current.positionAbsolute?.x || 0) + fromHandleX;
const fromY = (fromNode.current.positionAbsolute?.y || 0) + fromHandleY;
const targetX = (connectionPositionX - transform[0]) / transform[2];
const targetY = (connectionPositionY - transform[1]) / transform[2];
const toX = (connectionPositionX - transform[0]) / transform[2];
const toY = (connectionPositionY - transform[1]) / transform[2];
const isRightOrLeft = sourceHandle?.position === Position.Left || sourceHandle?.position === Position.Right;
const targetPosition = isRightOrLeft ? Position.Left : Position.Top;
const fromPosition = fromHandle?.position;
let toPosition: Position | undefined;
switch (fromPosition) {
case Position.Left:
toPosition = Position.Right;
break;
case Position.Right:
toPosition = Position.Left;
break;
case Position.Top:
toPosition = Position.Bottom;
break;
case Position.Bottom:
toPosition = Position.Top;
break;
}
let sourceX: number,
sourceY: number,
sourcePosition: Position | undefined,
targetX: number,
targetY: number,
targetPosition: Position | undefined;
switch (connectionHandleType) {
case 'source':
{
sourceX = fromX;
sourceY = fromY;
sourcePosition = fromPosition;
targetX = toX;
targetY = toY;
targetPosition = toPosition;
}
break;
case 'target':
{
sourceX = toX;
sourceY = toY;
sourcePosition = toPosition;
targetX = fromX;
targetY = fromY;
targetPosition = fromPosition;
}
break;
}
if (CustomConnectionLineComponent) {
return (
@@ -79,14 +106,17 @@ export default ({
<CustomConnectionLineComponent
sourceX={sourceX}
sourceY={sourceY}
sourcePosition={sourceHandle?.position}
sourcePosition={sourcePosition}
targetX={targetX}
targetY={targetY}
targetPosition={targetPosition}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
sourceNode={sourceNode}
sourceHandle={sourceHandle}
fromNode={fromNode.current}
fromHandle={fromHandle}
// backward compatibility, mark as deprecated?
sourceNode={fromNode.current}
sourceHandle={fromHandle}
/>
</g>
);
@@ -94,34 +124,27 @@ export default ({
let dAttr: string = '';
const pathParams = {
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
};
if (connectionLineType === ConnectionLineType.Bezier) {
dAttr = getBezierPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
});
// we assume the destination position is opposite to the source position
dAttr = getBezierPath(pathParams);
} else if (connectionLineType === ConnectionLineType.Step) {
dAttr = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
...pathParams,
borderRadius: 0,
});
} else if (connectionLineType === ConnectionLineType.SmoothStep) {
dAttr = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
});
dAttr = getSmoothStepPath(pathParams);
} else if (connectionLineType === ConnectionLineType.SimpleBezier) {
dAttr = getSimpleBezierPath(pathParams);
} else {
dAttr = `M${sourceX},${sourceY} ${targetX},${targetY}`;
}
+39
View File
@@ -0,0 +1,39 @@
import React from 'react';
import EdgeText from './EdgeText';
import { BaseEdgeProps } from '../../types';
export default ({
path,
centerX,
centerY,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
markerEnd,
markerStart,
}: BaseEdgeProps) => {
const text = label ? (
<EdgeText
x={centerX}
y={centerY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
/>
) : null;
return (
<>
<path style={style} d={path} className="react-flow__edge-path" markerEnd={markerEnd} markerStart={markerStart} />
{text}
</>
);
};
+177 -98
View File
@@ -1,98 +1,177 @@
import React, { memo } from 'react';
import EdgeText from './EdgeText';
import { getMarkerEnd, getCenter } from './utils';
import { EdgeProps, Position } from '../../types';
interface GetBezierPathParams {
sourceX: number;
sourceY: number;
sourcePosition?: Position;
targetX: number;
targetY: number;
targetPosition?: Position;
centerX?: number;
centerY?: number;
}
export function getBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
centerX,
centerY,
}: GetBezierPathParams): string {
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY });
const leftAndRight = [Position.Left, Position.Right];
const cX = typeof centerX !== 'undefined' ? centerX : _centerX;
const cY = typeof centerY !== 'undefined' ? centerY : _centerY;
let path = `M${sourceX},${sourceY} C${sourceX},${cY} ${targetX},${cY} ${targetX},${targetY}`;
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
path = `M${sourceX},${sourceY} C${cX},${sourceY} ${cX},${targetY} ${targetX},${targetY}`;
} else if (leftAndRight.includes(targetPosition)) {
path = `M${sourceX},${sourceY} Q${sourceX},${targetY} ${targetX},${targetY}`;
} else if (leftAndRight.includes(sourcePosition)) {
path = `M${sourceX},${sourceY} Q${targetX},${sourceY} ${targetX},${targetY}`;
}
return path;
}
export default memo(
({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
arrowHeadType,
markerEndId,
}: EdgeProps) => {
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
const path = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const text = label ? (
<EdgeText
x={centerX}
y={centerY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
/>
) : null;
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
return (
<>
<path style={style} d={path} className="react-flow__edge-path" markerEnd={markerEnd} />
{text}
</>
);
}
);
import React, { memo } from 'react';
import { EdgeProps, Position } from '../../types';
import BaseEdge from './BaseEdge';
export interface GetBezierPathParams {
sourceX: number;
sourceY: number;
sourcePosition?: Position;
targetX: number;
targetY: number;
targetPosition?: Position;
curvature?: number;
}
interface GetControlWithCurvatureParams {
pos: Position;
x1: number;
y1: number;
x2: number;
y2: number;
c: number;
}
function calculateControlOffset(distance: number, curvature: number): number {
if (distance >= 0) {
return 0.5 * distance;
} else {
return curvature * 25 * Math.sqrt(-distance);
}
}
function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurvatureParams): [number, number] {
let ctX: number, ctY: number;
switch (pos) {
case Position.Left:
{
ctX = x1 - calculateControlOffset(x1 - x2, c);
ctY = y1;
}
break;
case Position.Right:
{
ctX = x1 + calculateControlOffset(x2 - x1, c);
ctY = y1;
}
break;
case Position.Top:
{
ctX = x1;
ctY = y1 - calculateControlOffset(y1 - y2, c);
}
break;
case Position.Bottom:
{
ctX = x1;
ctY = y1 + calculateControlOffset(y2 - y1, c);
}
break;
}
return [ctX, ctY];
}
export function getBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
curvature = 0.25,
}: GetBezierPathParams): string {
const [sourceControlX, sourceControlY] = getControlWithCurvature({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature,
});
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature,
});
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`;
}
// @TODO: this function will recalculate the control points
// one option is to let getXXXPath() return center points
// but will introduce breaking changes
// the getCenter() of other types of edges might need to change, too
export function getBezierCenter({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
curvature = 0.25,
}: GetBezierPathParams): [number, number, number, number] {
const [sourceControlX, sourceControlY] = getControlWithCurvature({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature,
});
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature,
});
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125;
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125;
const xOffset = Math.abs(centerX - sourceX);
const yOffset = Math.abs(centerY - sourceY);
return [centerX, centerY, xOffset, yOffset];
}
export default memo(
({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
markerEnd,
markerStart,
curvature,
}: EdgeProps) => {
const params = {
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
curvature,
};
const path = getBezierPath(params);
const [centerX, centerY] = getBezierCenter(params);
return (
<BaseEdge
path={path}
centerX={centerX}
centerY={centerY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
style={style}
markerEnd={markerEnd}
markerStart={markerStart}
/>
);
}
);
+149
View File
@@ -0,0 +1,149 @@
import React, { memo } from 'react';
import { EdgeProps, Position } from '../../types';
import BaseEdge from './BaseEdge';
export interface GetSimpleBezierPathParams {
sourceX: number;
sourceY: number;
sourcePosition?: Position;
targetX: number;
targetY: number;
targetPosition?: Position;
}
interface GetControlParams {
pos: Position;
x1: number;
y1: number;
x2: number;
y2: number;
}
function getControl({ pos, x1, y1, x2, y2 }: GetControlParams): [number, number] {
let ctX: number, ctY: number;
switch (pos) {
case Position.Left:
case Position.Right:
{
ctX = 0.5 * (x1 + x2);
ctY = y1;
}
break;
case Position.Top:
case Position.Bottom:
{
ctX = x1;
ctY = 0.5 * (y1 + y2);
}
break;
}
return [ctX, ctY];
}
export function getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
}: GetSimpleBezierPathParams): string {
const [sourceControlX, sourceControlY] = getControl({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
});
const [targetControlX, targetControlY] = getControl({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
});
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`;
}
// @TODO: this function will recalculate the control points
// one option is to let getXXXPath() return center points
// but will introduce breaking changes
// the getCenter() of other types of edges might need to change, too
export function getSimpleBezierCenter({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
}: GetSimpleBezierPathParams): [number, number, number, number] {
const [sourceControlX, sourceControlY] = getControl({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
});
const [targetControlX, targetControlY] = getControl({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
});
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125;
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125;
const xOffset = Math.abs(centerX - sourceX);
const yOffset = Math.abs(centerY - sourceY);
return [centerX, centerY, xOffset, yOffset];
}
export default memo(
({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
markerEnd,
markerStart,
}: EdgeProps) => {
const params = {
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
};
const path = getSimpleBezierPath(params);
const [centerX, centerY] = getSimpleBezierCenter(params);
return (
<BaseEdge
path={path}
centerX={centerX}
centerY={centerY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
style={style}
markerEnd={markerEnd}
markerStart={markerStart}
/>
);
}
);
+13 -18
View File
@@ -1,8 +1,8 @@
import React, { memo } from 'react';
import EdgeText from './EdgeText';
import { getMarkerEnd, getCenter } from './utils';
import { getCenter } from './utils';
import { EdgeSmoothStepProps, Position } from '../../types';
import BaseEdge from './BaseEdge';
// These are some helper methods for drawing the round corners
// The name indicates the direction of the path. "bottomLeftCorner" goes
@@ -21,7 +21,7 @@ const topLeftCorner = (x: number, y: number, size: number): string => `L ${x},${
const topRightCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x - size},${y}`;
const rightTopCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y + size}`;
interface GetSmoothStepPathParams {
export interface GetSmoothStepPathParams {
sourceX: number;
sourceY: number;
sourcePosition?: Position;
@@ -130,8 +130,8 @@ export default memo(
style,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
arrowHeadType,
markerEndId,
markerEnd,
markerStart,
borderRadius = 5,
}: EdgeSmoothStepProps) => {
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
@@ -146,26 +146,21 @@ export default memo(
borderRadius,
});
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
const text = label ? (
<EdgeText
x={centerX}
y={centerY}
return (
<BaseEdge
path={path}
centerX={centerX}
centerY={centerY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
style={style}
markerEnd={markerEnd}
markerStart={markerStart}
/>
) : null;
return (
<>
<path style={style} className="react-flow__edge-path" d={path} markerEnd={markerEnd} />
{text}
</>
);
}
);
+12 -21
View File
@@ -1,7 +1,6 @@
import React, { memo } from 'react';
import EdgeText from './EdgeText';
import { getMarkerEnd } from './utils';
import BaseEdge from './BaseEdge';
import { EdgeProps } from '../../types';
export default memo(
@@ -17,39 +16,31 @@ export default memo(
labelBgPadding,
labelBgBorderRadius,
style,
arrowHeadType,
markerEndId,
markerEnd,
markerStart,
}: EdgeProps) => {
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
const xOffset = Math.abs(targetX - sourceX) / 2;
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
const path = `M ${sourceX},${sourceY}L ${targetX},${targetY}`;
const text = label ? (
<EdgeText
x={centerX}
y={centerY}
return (
<BaseEdge
path={path}
centerX={centerX}
centerY={centerY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
style={style}
markerEnd={markerEnd}
markerStart={markerStart}
/>
) : null;
return (
<>
<path
style={style}
className="react-flow__edge-path"
d={`M ${sourceX},${sourceY}L ${targetX},${targetY}`}
markerEnd={markerEnd}
/>
{text}
</>
);
}
);
+3 -2
View File
@@ -1,4 +1,5 @@
export { default as BezierEdge } from './BezierEdge';
export { default as StepEdge } from './StepEdge';
export { default as SimpleBezierEdge } from './SimpleBezierEdge';
export { default as SmoothStepEdge } from './SmoothStepEdge';
export { default as StepEdge } from './StepEdge';
export { default as StraightEdge } from './StraightEdge';
export { default as BezierEdge } from './BezierEdge';
+3 -3
View File
@@ -1,11 +1,11 @@
import { ArrowHeadType, Position } from '../../types';
import { MarkerType, Position } from '../../types';
export const getMarkerEnd = (arrowHeadType?: ArrowHeadType, markerEndId?: string): string => {
export const getMarkerEnd = (markerType?: MarkerType, markerEndId?: string): string => {
if (typeof markerEndId !== 'undefined' && markerEndId) {
return `url(#${markerEndId})`;
}
return typeof arrowHeadType !== 'undefined' ? `url(#react-flow__${arrowHeadType})` : 'none';
return typeof markerType !== 'undefined' ? `url(#react-flow__${markerType})` : 'none';
};
export interface GetCenterParams {
+35 -20
View File
@@ -1,10 +1,17 @@
import React, { memo, ComponentType, useCallback, useState, useMemo } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreActions, useStoreState } from '../../store/hooks';
import { Edge, EdgeProps, WrapEdgeProps } from '../../types';
import { useStore, useStoreApi } from '../../store';
import { Edge, EdgeProps, WrapEdgeProps, ReactFlowState, Connection } from '../../types';
import { onMouseDown } from '../../components/Handle/handler';
import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph';
const selector = (s: ReactFlowState) => ({
addSelectedEdges: s.addSelectedEdges,
connectionMode: s.connectionMode,
});
export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({
@@ -23,7 +30,6 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
labelBgPadding,
labelBgBorderRadius,
style,
arrowHeadType,
source,
target,
sourceX,
@@ -33,29 +39,27 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
sourcePosition,
targetPosition,
elementsSelectable,
markerEndId,
isHidden,
hidden,
sourceHandleId,
targetHandleId,
handleEdgeUpdate,
onConnectEdge,
onContextMenu,
onMouseEnter,
onMouseMove,
onMouseLeave,
edgeUpdaterRadius,
onEdgeUpdate,
onEdgeUpdateStart,
onEdgeUpdateEnd,
markerEnd,
markerStart,
}: WrapEdgeProps): JSX.Element | null => {
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
const connectionMode = useStoreState((state) => state.connectionMode);
const store = useStoreApi();
const { addSelectedEdges, connectionMode } = useStore(selector, shallow);
const [updating, setUpdating] = useState<boolean>(false);
const inactive = !elementsSelectable && !onClick;
const handleEdgeUpdate = typeof onEdgeUpdate !== 'undefined';
const edgeClasses = cc([
'react-flow__edge',
`react-flow__edge-${type}`,
@@ -89,8 +93,8 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const onEdgeClick = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
if (elementsSelectable) {
unsetNodesSelection();
addSelectedElements(edgeElement);
store.setState({ nodesSelectionActive: false });
addSelectedEdges([edgeElement.id]);
}
onClick?.(event, edgeElement);
@@ -147,21 +151,30 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edgeElement, handleType)
: undefined;
const onConnectEdge = (connection: Connection) => {
const { edges } = store.getState();
const edge = edges.find((e) => e.id === id);
if (edge && onEdgeUpdate) {
onEdgeUpdate(edge, connection);
}
};
onMouseDown(
event,
handleId,
nodeId,
setConnectionNodeId,
setPosition,
store.setState,
onConnectEdge,
isTarget,
isValidConnection,
connectionMode,
handleType,
_onEdgeUpdate
store.getState
);
},
[id, source, target, type, sourceHandleId, targetHandleId, setConnectionNodeId, setPosition, edgeElement, onConnectEdge]
[id, source, target, type, sourceHandleId, targetHandleId, edgeElement, onEdgeUpdate]
);
const onEdgeUpdaterSourceMouseDown = useCallback(
@@ -180,8 +193,10 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const onEdgeUpdaterMouseEnter = useCallback(() => setUpdating(true), [setUpdating]);
const onEdgeUpdaterMouseOut = useCallback(() => setUpdating(false), [setUpdating]);
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart)})`, [markerStart]);
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd)})`, [markerEnd]);
if (isHidden) {
if (hidden) {
return null;
}
@@ -209,16 +224,16 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
labelBgBorderRadius={labelBgBorderRadius}
data={data}
style={style}
arrowHeadType={arrowHeadType}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
markerEndId={markerEndId}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
markerStart={markerStartUrl}
markerEnd={markerEndUrl}
/>
{handleEdgeUpdate && (
<g
-20
View File
@@ -1,20 +0,0 @@
import { useEffect } from 'react';
import { useStoreActions } from '../../store/hooks';
import { Elements } from '../../types';
interface ElementUpdaterProps {
elements: Elements;
}
const ElementUpdater = ({ elements }: ElementUpdaterProps) => {
const setElements = useStoreActions((actions) => actions.setElements);
useEffect(() => {
setElements(elements);
}, [elements]);
return null;
};
export default ElementUpdater;
+34 -33
View File
@@ -1,24 +1,19 @@
import { MouseEvent as ReactMouseEvent } from 'react';
import { SetState } from 'zustand';
import { getHostForElement } from '../../utils';
import {
ElementId,
XYPosition,
OnConnectFunc,
OnConnectStartFunc,
OnConnectStopFunc,
OnConnectEndFunc,
OnConnect,
OnConnectStart,
OnConnectStop,
OnConnectEnd,
ConnectionMode,
SetConnectionId,
Connection,
HandleType,
ReactFlowState,
} from '../../types';
type ValidConnectionFunc = (connection: Connection) => boolean;
export type SetSourceIdFunc = (params: SetConnectionId) => void;
export type SetPosition = (pos: XYPosition) => void;
type Result = {
elementBelow: Element | null;
@@ -28,17 +23,15 @@ type Result = {
};
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
function checkElementBelowIsValid(
export function checkElementBelowIsValid(
event: MouseEvent,
connectionMode: ConnectionMode,
isTarget: boolean,
nodeId: ElementId,
handleId: ElementId | null,
nodeId: string,
handleId: string | null,
isValidConnection: ValidConnectionFunc,
doc: Document | ShadowRoot
) {
// TODO: why does this throw an error? elementFromPoint should be available for ShadowRoot too
// @ts-ignore
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
const elementBelowIsTarget = elementBelow?.classList.contains('target') || false;
const elementBelowIsSource = elementBelow?.classList.contains('source') || false;
@@ -91,19 +84,18 @@ function resetRecentHandle(hoveredHandle: Element): void {
export function onMouseDown(
event: ReactMouseEvent,
handleId: ElementId | null,
nodeId: ElementId,
setConnectionNodeId: SetSourceIdFunc,
setPosition: SetPosition,
onConnect: OnConnectFunc,
handleId: string | null,
nodeId: string,
setState: SetState<ReactFlowState>,
onConnect: OnConnect,
isTarget: boolean,
isValidConnection: ValidConnectionFunc,
connectionMode: ConnectionMode,
elementEdgeUpdaterType?: HandleType,
onEdgeUpdateEnd?: (evt: MouseEvent) => void,
onConnectStart?: OnConnectStartFunc,
onConnectStop?: OnConnectStopFunc,
onConnectEnd?: OnConnectEndFunc
onConnectStart?: OnConnectStart,
onConnectStop?: OnConnectStop,
onConnectEnd?: OnConnectEnd
): void {
const reactFlowNode = (event.target as Element).closest('.react-flow');
// when react-flow is used inside a shadow root we can't use document
@@ -113,7 +105,6 @@ export function onMouseDown(
return;
}
// @ts-ignore
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
const elementBelowIsTarget = elementBelow?.classList.contains('target');
const elementBelowIsSource = elementBelow?.classList.contains('source');
@@ -126,18 +117,24 @@ export function onMouseDown(
const containerBounds = reactFlowNode.getBoundingClientRect();
let recentHoveredHandle: Element;
setPosition({
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
setState({
connectionPosition: {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
},
connectionNodeId: nodeId,
connectionHandleId: handleId,
connectionHandleType: handleType,
});
setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType });
onConnectStart?.(event, { nodeId, handleId, handleType });
function onMouseMove(event: MouseEvent) {
setPosition({
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
setState({
connectionPosition: {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
},
});
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
@@ -187,7 +184,11 @@ export function onMouseDown(
}
resetRecentHandle(recentHoveredHandle);
setConnectionNodeId({ connectionNodeId: null, connectionHandleId: null, connectionHandleType: null });
setState({
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: null,
});
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject);
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject);
+107 -33
View File
@@ -1,16 +1,29 @@
import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreActions, useStoreState } from '../../store/hooks';
import { useStore, useStoreApi } from '../../store';
import NodeIdContext from '../../contexts/NodeIdContext';
import { HandleProps, Connection, ElementId, Position } from '../../types';
import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler';
import { HandleProps, Connection, ReactFlowState, Position } from '../../types';
import { checkElementBelowIsValid, onMouseDown } from './handler';
import { getHostForElement } from '../../utils';
import { addEdge } from '../../utils/graph';
const alwaysValid = () => true;
export type HandleComponentProps = HandleProps & Omit<HTMLAttributes<HTMLDivElement>, 'id'>;
const selector = (s: ReactFlowState) => ({
onConnectAction: s.onConnect,
onConnectStart: s.onConnectStart,
onConnectStop: s.onConnectStop,
onConnectEnd: s.onConnectEnd,
connectionMode: s.connectionMode,
connectionStartHandle: s.connectionStartHandle,
connectOnClick: s.connectOnClick,
hasDefaultEdges: s.hasDefaultEdges,
});
const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
(
{
@@ -26,49 +39,64 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
},
ref
) => {
const nodeId = useContext(NodeIdContext) as ElementId;
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
const onConnectAction = useStoreState((state) => state.onConnect);
const onConnectStart = useStoreState((state) => state.onConnectStart);
const onConnectStop = useStoreState((state) => state.onConnectStop);
const onConnectEnd = useStoreState((state) => state.onConnectEnd);
const connectionMode = useStoreState((state) => state.connectionMode);
const store = useStoreApi();
const nodeId = useContext(NodeIdContext) as string;
const {
onConnectAction,
onConnectStart,
onConnectStop,
onConnectEnd,
connectionMode,
connectionStartHandle,
connectOnClick,
hasDefaultEdges,
} = useStore(selector, shallow);
const handleId = id || null;
const isTarget = type === 'target';
const onConnectExtended = useCallback(
(params: Connection) => {
onConnectAction?.(params);
onConnect?.(params);
const { defaultEdgeOptions } = store.getState();
const edgeParams = {
...defaultEdgeOptions,
...params,
};
if (hasDefaultEdges) {
const { edges } = store.getState();
store.setState({ edges: addEdge(edgeParams, edges) });
}
onConnectAction?.(edgeParams);
onConnect?.(edgeParams);
},
[onConnectAction, onConnect]
[hasDefaultEdges, onConnectAction, onConnect]
);
const onMouseDownHandler = useCallback(
(event: React.MouseEvent) => {
onMouseDown(
event,
handleId,
nodeId,
setConnectionNodeId as unknown as SetSourceIdFunc,
setPosition as unknown as SetPosition,
onConnectExtended,
isTarget,
isValidConnection,
connectionMode,
undefined,
undefined,
onConnectStart,
onConnectStop,
onConnectEnd
);
if (event.button === 0) {
onMouseDown(
event,
handleId,
nodeId,
store.setState,
onConnectExtended,
isTarget,
isValidConnection,
connectionMode,
undefined,
undefined,
onConnectStart,
onConnectStop,
onConnectEnd
);
}
},
[
handleId,
nodeId,
setConnectionNodeId,
setPosition,
onConnectExtended,
isTarget,
isValidConnection,
@@ -79,6 +107,47 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
]
);
const onClick = useCallback(
(event: React.MouseEvent) => {
if (!connectionStartHandle) {
onConnectStart?.(event, { nodeId, handleId, handleType: type });
store.setState({ connectionStartHandle: { nodeId, type, handleId } });
} else {
const doc = getHostForElement(event.target as HTMLElement);
const { connection, isValid } = checkElementBelowIsValid(
event as unknown as MouseEvent,
connectionMode,
connectionStartHandle.type === 'target',
connectionStartHandle.nodeId,
connectionStartHandle.handleId || null,
isValidConnection,
doc
);
onConnectStop?.(event as unknown as MouseEvent);
if (isValid) {
onConnectExtended(connection);
}
onConnectEnd?.(event as unknown as MouseEvent);
store.setState({ connectionStartHandle: null });
}
},
[
connectionStartHandle,
onConnectStart,
onConnectExtended,
onConnectStop,
onConnectEnd,
isTarget,
nodeId,
handleId,
type,
]
);
const handleClasses = cc([
'react-flow__handle',
`react-flow__handle-${position}`,
@@ -88,6 +157,10 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
source: !isTarget,
target: isTarget,
connectable: isConnectable,
connecting:
connectionStartHandle?.nodeId === nodeId &&
connectionStartHandle?.handleId === handleId &&
connectionStartHandle?.type === type,
},
]);
@@ -98,6 +171,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
data-handlepos={position}
className={handleClasses}
onMouseDown={onMouseDownHandler}
onClick={connectOnClick ? onClick : undefined}
ref={ref}
{...rest}
>
+9 -7
View File
@@ -8,13 +8,15 @@ const DefaultNode = ({
isConnectable,
targetPosition = Position.Top,
sourcePosition = Position.Bottom,
}: NodeProps) => (
<>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
{data.label}
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
</>
);
}: NodeProps) => {
return (
<>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
{data?.label}
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
</>
);
};
DefaultNode.displayName = 'DefaultNode';
+5
View File
@@ -0,0 +1,5 @@
const GroupNode = () => null;
GroupNode.displayName = 'GroupNode';
export default GroupNode;
+1 -1
View File
@@ -5,7 +5,7 @@ import { NodeProps, Position } from '../../types';
const InputNode = ({ data, isConnectable, sourcePosition = Position.Bottom }: NodeProps) => (
<>
{data.label}
{data?.label}
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
</>
);
+1 -1
View File
@@ -6,7 +6,7 @@ import { NodeProps, Position } from '../../types';
const OutputNode = ({ data, isConnectable, targetPosition = Position.Top }: NodeProps) => (
<>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
{data.label}
{data?.label}
</>
);
@@ -0,0 +1,25 @@
import { MouseEvent, useCallback } from 'react';
import { GetState } from 'zustand';
import { ReactFlowState, Node } from '../../types';
function useMemoizedMouseHandler(
id: string,
dragging: boolean,
getState: GetState<ReactFlowState>,
handler?: (event: MouseEvent, node: Node) => void
) {
const memoizedHandler = useCallback(
(event: MouseEvent) => {
if (typeof handler !== 'undefined' && !dragging) {
const node = getState().nodeInternals.get(id)!;
handler(event, { ...node });
}
},
[handler, dragging, id]
);
return memoizedHandler;
}
export default useMemoizedMouseHandler;
+14 -16
View File
@@ -13,7 +13,7 @@ export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => {
export const getHandleBoundsByHandleType = (
selector: string,
nodeElement: HTMLDivElement,
parentBounds: ClientRect | DOMRect,
parentBounds: DOMRect,
k: number
): HandleElement[] | null => {
const handles = nodeElement.querySelectorAll(selector);
@@ -24,20 +24,18 @@ export const getHandleBoundsByHandleType = (
const handlesArray = Array.from(handles) as HTMLDivElement[];
return handlesArray.map(
(handle): HandleElement => {
const bounds = handle.getBoundingClientRect();
const dimensions = getDimensions(handle);
const handleId = handle.getAttribute('data-handleid');
const handlePosition = (handle.getAttribute('data-handlepos') as unknown) as Position;
return handlesArray.map((handle): HandleElement => {
const bounds = handle.getBoundingClientRect();
const dimensions = getDimensions(handle);
const handleId = handle.getAttribute('data-handleid');
const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position;
return {
id: handleId,
position: handlePosition,
x: (bounds.left - parentBounds.left) / k,
y: (bounds.top - parentBounds.top) / k,
...dimensions,
};
}
);
return {
id: handleId,
position: handlePosition,
x: (bounds.left - parentBounds.left) / k,
y: (bounds.top - parentBounds.top) / k,
...dimensions,
};
});
};
+125 -124
View File
@@ -1,22 +1,21 @@
import React, {
useEffect,
useLayoutEffect,
useRef,
memo,
ComponentType,
CSSProperties,
useMemo,
MouseEvent,
useCallback,
} from 'react';
import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreActions } from '../../store/hooks';
import { useStore, useStoreApi } from '../../store';
import { Provider } from '../../contexts/NodeIdContext';
import { NodeComponentProps, WrapNodeProps } from '../../types';
import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types';
import useMemoizedMouseHandler from './useMemoizedMouseHandler';
export default (NodeComponent: ComponentType<NodeComponentProps>) => {
const selector = (s: ReactFlowState) => ({
addSelectedNodes: s.addSelectedNodes,
updateNodePosition: s.updateNodePosition,
unselectNodesAndEdges: s.unselectNodesAndEdges,
updateNodeDimensions: s.updateNodeDimensions,
});
export default (NodeComponent: ComponentType<NodeProps>) => {
const NodeWrapper = ({
id,
type,
@@ -42,192 +41,193 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
selectNodesOnDrag,
sourcePosition,
targetPosition,
isHidden,
isInitialized,
hidden,
snapToGrid,
snapGrid,
isDragging,
dragging,
resizeObserver,
dragHandle,
zIndex,
isParent,
noPanClassName,
noDragClassName,
}: WrapNodeProps) => {
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff);
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const store = useStoreApi();
const { addSelectedNodes, unselectNodesAndEdges, updateNodePosition, updateNodeDimensions } = useStore(
selector,
shallow
);
const nodeElement = useRef<HTMLDivElement>(null);
const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]);
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
const prevSourcePosition = useRef(sourcePosition);
const prevTargetPosition = useRef(targetPosition);
const prevType = useRef(type);
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const nodeStyle: CSSProperties = useMemo(
() => ({
zIndex: selected ? 10 : 3,
zIndex,
transform: `translate(${xPos}px,${yPos}px)`,
pointerEvents:
isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none',
// prevents jumping of nodes on start
opacity: isInitialized ? 1 : 0,
pointerEvents: hasPointerEvents ? 'all' : 'none',
...style,
}),
[
selected,
xPos,
yPos,
isSelectable,
isDraggable,
onClick,
isInitialized,
style,
onMouseEnter,
onMouseMove,
onMouseLeave,
]
[zIndex, xPos, yPos, hasPointerEvents, style]
);
const onMouseEnterHandler = useMemo(() => {
if (!onMouseEnter || isDragging) {
return;
}
return (event: MouseEvent) => onMouseEnter(event, node);
}, [onMouseEnter, isDragging, node]);
const grid = useMemo(
() => (snapToGrid ? snapGrid : [1, 1])! as [number, number],
[snapToGrid, snapGrid?.[0], snapGrid?.[1]]
);
const onMouseMoveHandler = useMemo(() => {
if (!onMouseMove || isDragging) {
return;
}
return (event: MouseEvent) => onMouseMove(event, node);
}, [onMouseMove, isDragging, node]);
const onMouseLeaveHandler = useMemo(() => {
if (!onMouseLeave || isDragging) {
return;
}
return (event: MouseEvent) => onMouseLeave(event, node);
}, [onMouseLeave, isDragging, node]);
const onContextMenuHandler = useMemo(() => {
if (!onContextMenu) {
return;
}
return (event: MouseEvent) => onContextMenu(event, node);
}, [onContextMenu, node]);
const onMouseEnterHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseEnter);
const onMouseMoveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseMove);
const onMouseLeaveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseLeave);
const onContextMenuHandler = useMemoizedMouseHandler(id, false, store.getState, onContextMenu);
const onNodeDoubleClickHandler = useMemoizedMouseHandler(id, false, store.getState, onNodeDoubleClick);
const onSelectNodeHandler = useCallback(
(event: MouseEvent) => {
if (!isDraggable) {
if (isSelectable) {
unsetNodesSelection();
store.setState({ nodesSelectionActive: false });
if (!selected) {
addSelectedElements(node);
addSelectedNodes([id]);
}
}
onClick?.(event, node);
if (onClick) {
const node = store.getState().nodeInternals.get(id)!;
onClick(event, { ...node });
}
}
},
[isSelectable, selected, isDraggable, onClick, node]
[isSelectable, selected, isDraggable, onClick, id]
);
const onDragStart = useCallback(
(event: DraggableEvent) => {
onNodeDragStart?.(event as MouseEvent, node);
if (selectNodesOnDrag && isSelectable) {
unsetNodesSelection();
store.setState({ nodesSelectionActive: false });
if (!selected) {
addSelectedElements(node);
addSelectedNodes([id]);
}
} else if (!selectNodesOnDrag && !selected && isSelectable) {
unsetNodesSelection();
addSelectedElements([]);
const { multiSelectionActive } = store.getState();
if (multiSelectionActive) {
addSelectedNodes([id]);
} else {
unselectNodesAndEdges();
store.setState({ nodesSelectionActive: false });
}
}
if (onNodeDragStart) {
const node = store.getState().nodeInternals.get(id)!;
onNodeDragStart(event as MouseEvent, { ...node });
}
},
[node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
[id, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
);
const onDrag = useCallback(
(event: DraggableEvent, draggableData: DraggableData) => {
if (onNodeDrag) {
node.position.x += draggableData.deltaX;
node.position.y += draggableData.deltaY;
onNodeDrag(event as MouseEvent, node);
}
updateNodePosition({ id, dragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } });
updateNodePosDiff({
id,
diff: {
x: draggableData.deltaX,
y: draggableData.deltaY,
},
isDragging: true,
});
if (onNodeDrag) {
const node = store.getState().nodeInternals.get(id)!;
onNodeDrag(event as MouseEvent, {
...node,
dragging: true,
position: {
x: node.position.x + draggableData.deltaX,
y: node.position.y + draggableData.deltaY,
},
positionAbsolute: {
x: (node.positionAbsolute?.x || 0) + draggableData.deltaX,
y: (node.positionAbsolute?.y || 0) + draggableData.deltaY,
},
});
}
},
[id, node, onNodeDrag]
[id, onNodeDrag]
);
const onDragStop = useCallback(
(event: DraggableEvent) => {
// onDragStop also gets called when user just clicks on a node.
// Because of that we set dragging to true inside the onDrag handler and handle the click here
if (!isDragging) {
let node;
if (onClick || onNodeDragStop) {
node = store.getState().nodeInternals.get(id)!;
}
if (!dragging) {
if (isSelectable && !selectNodesOnDrag && !selected) {
addSelectedElements(node);
addSelectedNodes([id]);
}
onClick?.(event as MouseEvent, node);
if (onClick && node) {
onClick(event as MouseEvent, { ...node });
}
return;
}
updateNodePosDiff({
id: node.id,
isDragging: false,
updateNodePosition({
id,
dragging: false,
});
onNodeDragStop?.(event as MouseEvent, node);
if (onNodeDragStop && node) {
onNodeDragStop(event as MouseEvent, { ...node, dragging: false });
}
},
[node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, selected]
[id, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, dragging, selected]
);
const onNodeDoubleClickHandler = useCallback(
(event: MouseEvent) => {
onNodeDoubleClick?.(event, node);
},
[node, onNodeDoubleClick]
);
useLayoutEffect(() => {
if (nodeElement.current && !isHidden) {
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
}
}, [id, isHidden, sourcePosition, targetPosition]);
useEffect(() => {
if (nodeElement.current) {
if (nodeElement.current && !hidden) {
const currNode = nodeElement.current;
resizeObserver?.observe(currNode);
return () => resizeObserver?.unobserve(currNode);
}
}, []);
}, [hidden]);
if (isHidden) {
useEffect(() => {
// when the user programmatically changes the source or handle position, we re-initialize the node
const typeChanged = prevType.current !== type;
const sourcePosChanged = prevSourcePosition.current !== sourcePosition;
const targetPosChanged = prevTargetPosition.current !== targetPosition;
if (nodeElement.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
if (typeChanged) {
prevType.current = type;
}
if (sourcePosChanged) {
prevSourcePosition.current = sourcePosition;
}
if (targetPosChanged) {
prevTargetPosition.current = targetPosition;
}
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
}
}, [id, type, sourcePosition, targetPosition]);
if (hidden) {
return null;
}
const nodeClasses = cc([
'react-flow__node',
`react-flow__node-${type}`,
noPanClassName,
className,
{
selected,
selectable: isSelectable,
parent: isParent,
},
]);
@@ -238,7 +238,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
onStop={onDragStop}
scale={scale}
disabled={!isDraggable}
cancel=".nodrag"
cancel={`.${noDragClassName}`}
nodeRef={nodeElement}
grid={grid}
enableUserSelectHack={false}
@@ -267,8 +267,9 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
isConnectable={isConnectable}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
isDragging={isDragging}
dragging={dragging}
dragHandle={dragHandle}
zIndex={zIndex}
/>
</Provider>
</div>
+46 -52
View File
@@ -1,57 +1,53 @@
/**
* The nodes selection rectangle gets displayed when a user
* made a selectio with on or several nodes
* made a selection with on or several nodes
*/
import React, { useMemo, useCallback, useRef, MouseEvent } from 'react';
import ReactDraggable, { DraggableData } from 'react-draggable';
import React, { memo, useMemo, useCallback, useRef, MouseEvent } from 'react';
import { DraggableCore, DraggableData } from 'react-draggable';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreState, useStoreActions } from '../../store/hooks';
import { isNode } from '../../utils/graph';
import { Node } from '../../types';
import { useStore } from '../../store';
import { Node, ReactFlowState } from '../../types';
import { getRectOfNodes } from '../../utils/graph';
export interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
noPanClassName?: string;
}
// @TODO: work with nodeInternals instead of converting it to an array
const selector = (s: ReactFlowState) => ({
transform: s.transform,
selectedNodesBbox: s.selectedNodesBbox,
userSelectionActive: s.userSelectionActive,
selectedNodes: Array.from(s.nodeInternals)
.filter(([_, n]) => n.selected)
.map(([_, n]) => n),
snapToGrid: s.snapToGrid,
snapGrid: s.snapGrid,
updateNodePosition: s.updateNodePosition,
});
export default ({
function NodesSelection({
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
}: NodesSelectionProps) => {
const [tX, tY, tScale] = useStoreState((state) => state.transform);
const selectedNodesBbox = useStoreState((state) => state.selectedNodesBbox);
const selectionActive = useStoreState((state) => state.selectionActive);
const selectedElements = useStoreState((state) => state.selectedElements);
const snapToGrid = useStoreState((state) => state.snapToGrid);
const snapGrid = useStoreState((state) => state.snapGrid);
const nodes = useStoreState((state) => state.nodes);
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff);
noPanClassName,
}: NodesSelectionProps) {
const { transform, userSelectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } = useStore(
selector,
shallow
);
const [tX, tY, tScale] = transform;
const nodeRef = useRef(null);
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
const selectedNodes = useMemo(
() =>
selectedElements
? selectedElements.filter(isNode).map((selectedNode) => {
const matchingNode = nodes.find((node) => node.id === selectedNode.id);
return {
...matchingNode,
position: matchingNode?.__rf.position,
} as Node;
})
: [],
[selectedElements, nodes]
);
const style = useMemo(
() => ({
transform: `translate(${tX}px,${tY}px) scale(${tScale})`,
@@ -59,6 +55,8 @@ export default ({
[tX, tY, tScale]
);
const selectedNodesBbox = useMemo(() => getRectOfNodes(selectedNodes), [selectedNodes]);
const innerStyle = useMemo(
() => ({
width: selectedNodesBbox.width,
@@ -78,25 +76,23 @@ export default ({
const onDrag = useCallback(
(event: MouseEvent, data: DraggableData) => {
if (onSelectionDrag) {
onSelectionDrag(event, selectedNodes);
}
updateNodePosDiff({
updateNodePosition({
diff: {
x: data.deltaX,
y: data.deltaY,
},
isDragging: true,
dragging: true,
});
onSelectionDrag?.(event, selectedNodes);
},
[onSelectionDrag, selectedNodes, updateNodePosDiff]
[onSelectionDrag, selectedNodes, updateNodePosition]
);
const onStop = useCallback(
(event: MouseEvent) => {
updateNodePosDiff({
isDragging: false,
updateNodePosition({
dragging: false,
});
onSelectionDragStop?.(event, selectedNodes);
@@ -106,22 +102,18 @@ export default ({
const onContextMenu = useCallback(
(event: MouseEvent) => {
const selectedNodes = selectedElements
? selectedElements.filter(isNode).map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)!)
: [];
onSelectionContextMenu?.(event, selectedNodes);
},
[onSelectionContextMenu]
[onSelectionContextMenu, selectedNodes]
);
if (!selectedElements || selectionActive) {
if (!selectedNodes?.length || userSelectionActive) {
return null;
}
return (
<div className="react-flow__nodesselection" style={style}>
<ReactDraggable
<div className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} style={style}>
<DraggableCore
scale={tScale}
grid={grid}
onStart={(event) => onStart(event as MouseEvent)}
@@ -136,7 +128,9 @@ export default ({
onContextMenu={onContextMenu}
style={innerStyle}
/>
</ReactDraggable>
</DraggableCore>
</div>
);
};
}
export default memo(NodesSelection);
+28 -10
View File
@@ -1,20 +1,38 @@
import { useEffect } from 'react';
import { memo, useEffect } from 'react';
import shallow from 'zustand/shallow';
import { Elements } from '../../types';
import { useStoreState } from '../../store/hooks';
import { ReactFlowState, OnSelectionChangeFunc, Node, Edge } from '../../types';
import { useStore } from '../../store';
interface SelectionListenerProps {
onSelectionChange: (elements: Elements | null) => void;
onSelectionChange: OnSelectionChangeFunc;
}
// This is a helper component for calling the onSelectionChange listener
const selector = (s: ReactFlowState) => ({
selectedNodes: Array.from(s.nodeInternals.values()).filter((n) => n.selected),
selectedEdges: s.edges.filter((e) => e.selected),
});
export default ({ onSelectionChange }: SelectionListenerProps) => {
const selectedElements = useStoreState((s) => s.selectedElements);
const areEqual = (objA: any, objB: any) => {
const selectedNodeIdsA = objA.selectedNodes.map((n: Node) => n.id);
const selectedNodeIdsB = objB.selectedNodes.map((n: Node) => n.id);
const selectedEdgeIdsA = objA.selectedEdges.map((e: Edge) => e.id);
const selectedEdgeIdsB = objB.selectedEdges.map((e: Edge) => e.id);
return shallow(selectedNodeIdsA, selectedNodeIdsB) && shallow(selectedEdgeIdsA, selectedEdgeIdsB);
};
// This is just a helper component for calling the onSelectionChange listener.
// @TODO: Now that we have the onNodesChange and on EdgesChange listeners, do we still need this component?
function SelectionListener({ onSelectionChange }: SelectionListenerProps) {
const { selectedNodes, selectedEdges } = useStore(selector, areEqual);
useEffect(() => {
onSelectionChange(selectedElements);
}, [selectedElements]);
onSelectionChange({ nodes: selectedNodes, edges: selectedEdges });
}, [selectedNodes, selectedEdges]);
return null;
};
}
export default memo(SelectionListener);
+161
View File
@@ -0,0 +1,161 @@
import { useEffect } from 'react';
import { SetState } from 'zustand';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
import {
Node,
Edge,
ReactFlowState,
OnConnect,
OnConnectStart,
OnConnectStop,
OnConnectEnd,
CoordinateExtent,
OnNodesChange,
OnEdgesChange,
ConnectionMode,
SnapGrid,
DefaultEdgeOptions,
FitViewOptions,
OnNodesDelete,
OnEdgesDelete
} from '../../types';
interface StoreUpdaterProps {
nodes?: Node[];
edges?: Edge[];
defaultNodes?: Node[];
defaultEdges?: Edge[];
onConnect?: OnConnect;
onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
nodesDraggable?: boolean;
nodesConnectable?: boolean;
minZoom?: number;
maxZoom?: number;
nodeExtent?: CoordinateExtent;
onNodesChange?: OnNodesChange;
onEdgesChange?: OnEdgesChange;
elementsSelectable?: boolean;
connectionMode?: ConnectionMode;
snapToGrid?: boolean;
snapGrid?: SnapGrid;
translateExtent?: CoordinateExtent;
connectOnClick: boolean;
defaultEdgeOptions?: DefaultEdgeOptions;
fitView?: boolean;
fitViewOptions?: FitViewOptions;
onNodesDelete?: OnNodesDelete;
onEdgesDelete?: OnEdgesDelete;
}
const selector = (s: ReactFlowState) => ({
setNodes: s.setNodes,
setEdges: s.setEdges,
setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
setMinZoom: s.setMinZoom,
setMaxZoom: s.setMaxZoom,
setTranslateExtent: s.setTranslateExtent,
setNodeExtent: s.setNodeExtent,
reset: s.reset,
});
function useStoreUpdater<T>(value: T | undefined, setStoreState: (param: T) => void) {
useEffect(() => {
if (typeof value !== 'undefined') {
setStoreState(value);
}
}, [value]);
}
function useDirectStoreUpdater(key: keyof ReactFlowState, value: any, setState: SetState<ReactFlowState>) {
useEffect(() => {
if (typeof value !== 'undefined') {
// @ts-ignore
setState({ [key]: value });
}
}, [value]);
}
const StoreUpdater = ({
nodes,
edges,
defaultNodes,
defaultEdges,
onConnect,
onConnectStart,
onConnectStop,
onConnectEnd,
nodesDraggable,
nodesConnectable,
minZoom,
maxZoom,
nodeExtent,
onNodesChange,
onEdgesChange,
elementsSelectable,
connectionMode,
snapGrid,
snapToGrid,
translateExtent,
connectOnClick,
defaultEdgeOptions,
fitView,
fitViewOptions,
onNodesDelete,
onEdgesDelete,
}: StoreUpdaterProps) => {
const {
setNodes,
setEdges,
setDefaultNodesAndEdges,
setMinZoom,
setMaxZoom,
setTranslateExtent,
setNodeExtent,
reset,
} = useStore(selector, shallow);
const store = useStoreApi();
useEffect(() => {
setDefaultNodesAndEdges(defaultNodes, defaultEdges);
return () => {
reset();
};
}, []);
useDirectStoreUpdater('defaultEdgeOptions', defaultEdgeOptions, store.setState);
useDirectStoreUpdater('connectionMode', connectionMode, store.setState);
useDirectStoreUpdater('onConnect', onConnect, store.setState);
useDirectStoreUpdater('onConnectStart', onConnectStart, store.setState);
useDirectStoreUpdater('onConnectStop', onConnectStop, store.setState);
useDirectStoreUpdater('onConnectEnd', onConnectEnd, store.setState);
useDirectStoreUpdater('nodesDraggable', nodesDraggable, store.setState);
useDirectStoreUpdater('nodesConnectable', nodesConnectable, store.setState);
useDirectStoreUpdater('elementsSelectable', elementsSelectable, store.setState);
useDirectStoreUpdater('snapToGrid', snapToGrid, store.setState);
useDirectStoreUpdater('snapGrid', snapGrid, store.setState);
useDirectStoreUpdater('onNodesChange', onNodesChange, store.setState);
useDirectStoreUpdater('onEdgesChange', onEdgesChange, store.setState);
useDirectStoreUpdater('connectOnClick', connectOnClick, store.setState);
useDirectStoreUpdater('fitViewOnInit', fitView, store.setState);
useDirectStoreUpdater('fitViewOnInitOptions', fitViewOptions, store.setState);
useDirectStoreUpdater('onNodesDelete', onNodesDelete, store.setState);
useDirectStoreUpdater('onEdgesDelete', onEdgesDelete, store.setState);
useStoreUpdater<Node[]>(nodes, setNodes);
useStoreUpdater<Edge[]>(edges, setEdges);
useStoreUpdater<Node[]>(defaultNodes, setNodes);
useStoreUpdater<Edge[]>(defaultEdges, setEdges);
useStoreUpdater<number>(minZoom, setMinZoom);
useStoreUpdater<number>(maxZoom, setMaxZoom);
useStoreUpdater<CoordinateExtent>(translateExtent, setTranslateExtent);
useStoreUpdater<CoordinateExtent>(nodeExtent, setNodeExtent);
return null;
};
export default StoreUpdater;
+125 -65
View File
@@ -2,100 +2,160 @@
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
*/
import React, { memo } from 'react';
import React, { memo, useState, useRef, useCallback } from 'react';
import shallow from 'zustand/shallow';
import { useStoreActions, useStoreState } from '../../store/hooks';
import { XYPosition } from '../../types';
import { useStore, useStoreApi } from '../../store';
import { getSelectionChanges } from '../../utils/changes';
import { XYPosition, ReactFlowState, NodeChange, EdgeChange, Rect } from '../../types';
import { getConnectedEdges, getNodesInside } from '../../utils/graph';
type SelectionRect = Rect & {
startX: number;
startY: number;
draw: boolean;
};
type UserSelectionProps = {
selectionKeyPressed: boolean;
};
function getMousePosition(event: React.MouseEvent): XYPosition | void {
const reactFlowNode = (event.target as Element).closest('.react-flow');
if (!reactFlowNode) {
return;
}
const containerBounds = reactFlowNode.getBoundingClientRect();
function getMousePosition(event: React.MouseEvent, containerBounds: DOMRect): XYPosition {
return {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
};
}
const SelectionRect = () => {
const userSelectionRect = useStoreState((state) => state.userSelectionRect);
const selector = (s: ReactFlowState) => ({
userSelectionActive: s.userSelectionActive,
elementsSelectable: s.elementsSelectable,
});
if (!userSelectionRect.draw) {
return null;
}
return (
<div
className="react-flow__selection"
style={{
width: userSelectionRect.width,
height: userSelectionRect.height,
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
}}
/>
);
const initialRect: SelectionRect = {
startX: 0,
startY: 0,
x: 0,
y: 0,
width: 0,
height: 0,
draw: false,
};
export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
const selectionActive = useStoreState((state) => state.selectionActive);
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
const store = useStoreApi();
const prevSelectedNodesCount = useRef<number>(0);
const prevSelectedEdgesCount = useRef<number>(0);
const containerBounds = useRef<DOMRect>();
const [userSelectionRect, setUserSelectionRect] = useState<SelectionRect>(initialRect);
const { userSelectionActive, elementsSelectable } = useStore(selector, shallow);
const setUserSelection = useStoreActions((actions) => actions.setUserSelection);
const updateUserSelection = useStoreActions((actions) => actions.updateUserSelection);
const unsetUserSelection = useStoreActions((actions) => actions.unsetUserSelection);
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const renderUserSelectionPane = selectionActive || selectionKeyPressed;
const renderUserSelectionPane = userSelectionActive || selectionKeyPressed;
const resetUserSelection = useCallback(() => {
setUserSelectionRect(initialRect);
store.setState({ userSelectionActive: false });
prevSelectedNodesCount.current = 0;
prevSelectedEdgesCount.current = 0;
}, []);
const onMouseDown = useCallback((event: React.MouseEvent): void => {
const reactFlowNode = (event.target as Element).closest('.react-flow')!;
containerBounds.current = reactFlowNode.getBoundingClientRect();
const mousePos = getMousePosition(event, containerBounds.current!);
setUserSelectionRect({
width: 0,
height: 0,
startX: mousePos.x,
startY: mousePos.y,
x: mousePos.x,
y: mousePos.y,
draw: true,
});
store.setState({ userSelectionActive: true, nodesSelectionActive: false });
}, []);
const onMouseMove = (event: React.MouseEvent): void => {
if (!selectionKeyPressed || !userSelectionRect.draw || !containerBounds.current) {
return;
}
const mousePos = getMousePosition(event, containerBounds.current!);
const startX = userSelectionRect.startX ?? 0;
const startY = userSelectionRect.startY ?? 0;
const nextUserSelectRect = {
...userSelectionRect,
x: mousePos.x < startX ? mousePos.x : startX,
y: mousePos.y < startY ? mousePos.y : startY,
width: Math.abs(mousePos.x - startX),
height: Math.abs(mousePos.y - startY),
};
const { nodeInternals, edges, transform, onNodesChange, onEdgesChange } = store.getState();
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const selectedNodes = getNodesInside(nodeInternals, nextUserSelectRect, transform, false, true);
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id);
if (prevSelectedNodesCount.current !== selectedNodeIds.length) {
prevSelectedNodesCount.current = selectedNodeIds.length;
const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
if (changes.length) {
onNodesChange?.(changes);
}
}
if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) {
prevSelectedEdgesCount.current = selectedEdgeIds.length;
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
if (changes.length) {
onEdgesChange?.(changes);
}
}
setUserSelectionRect(nextUserSelectRect);
};
const onMouseUp = useCallback(() => {
store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 });
resetUserSelection();
}, []);
const onMouseLeave = useCallback(() => {
store.setState({ nodesSelectionActive: false });
resetUserSelection();
}, []);
if (!elementsSelectable || !renderUserSelectionPane) {
return null;
}
const onMouseDown = (event: React.MouseEvent): void => {
const mousePos = getMousePosition(event);
if (!mousePos) {
return;
}
setUserSelection(mousePos);
};
const onMouseMove = (event: React.MouseEvent): void => {
if (!selectionKeyPressed || !selectionActive) {
return;
}
const mousePos = getMousePosition(event);
if (!mousePos) {
return;
}
updateUserSelection(mousePos);
};
const onMouseUp = () => unsetUserSelection();
const onMouseLeave = () => {
unsetUserSelection();
unsetNodesSelection();
};
return (
<div
className="react-flow__selectionpane"
className="react-flow__selectionpane react-flow__container"
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
onMouseLeave={onMouseLeave}
>
<SelectionRect />
{userSelectionRect.draw && (
<div
className="react-flow__selection react-flow__container"
style={{
width: userSelectionRect.width,
height: userSelectionRect.height,
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
}}
/>
)}
</div>
);
});
@@ -1,52 +1,81 @@
import React, { ReactNode } from 'react';
import React, { useMemo } from 'react';
interface MarkerProps {
import { useStore } from '../../store';
import { EdgeMarker, ReactFlowState } from '../../types';
import { getMarkerId } from '../../utils/graph';
import { useMarkerSymbol } from './MarkerSymbols';
interface MarkerProps extends EdgeMarker {
id: string;
children: ReactNode;
}
const Marker = ({ id, children }: MarkerProps) => (
<marker
className="react-flow__arrowhead"
id={id}
markerWidth="12.5"
markerHeight="12.5"
viewBox="-10 -10 20 20"
orient="auto"
refX="0"
refY="0"
>
{children}
</marker>
);
interface MarkerDefinitionsProps {
color: string;
defaultColor: string;
}
const MarkerDefinitions = ({ color }: MarkerDefinitionsProps) => {
const Marker = ({
id,
type,
color,
width = 12.5,
height = 12.5,
markerUnits = 'strokeWidth',
strokeWidth,
orient = 'auto',
}: MarkerProps) => {
const Symbol = useMarkerSymbol(type);
return (
<marker
className="react-flow__arrowhead"
id={id}
markerWidth={`${width}`}
markerHeight={`${height}`}
viewBox="-10 -10 20 20"
markerUnits={markerUnits}
orient={orient}
refX="0"
refY="0"
>
<Symbol color={color} strokeWidth={strokeWidth} />
</marker>
);
};
const edgesSelector = (s: ReactFlowState) => s.edges;
const MarkerDefinitions = ({ defaultColor }: MarkerDefinitionsProps) => {
const edges = useStore(edgesSelector);
const markers = useMemo(() => {
const ids: string[] = [];
return edges.reduce<MarkerProps[]>((markers, edge) => {
[edge.markerStart, edge.markerEnd].forEach((marker) => {
if (marker && typeof marker === 'object') {
const markerId = getMarkerId(marker);
if (!ids.includes(markerId)) {
markers.push({ id: markerId, color: marker.color || defaultColor, ...marker });
ids.push(markerId);
}
}
});
return markers.sort((a, b) => a.id.localeCompare(b.id));
}, []);
}, [edges, defaultColor]);
return (
<defs>
<Marker id="react-flow__arrowclosed">
<polyline
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1"
fill={color}
points="-5,-4 0,0 -5,4 -5,-4"
{markers.map((marker: MarkerProps) => (
<Marker
id={marker.id}
key={marker.id}
type={marker.type}
color={marker.color}
width={marker.width}
height={marker.height}
markerUnits={marker.markerUnits}
strokeWidth={marker.strokeWidth}
orient={marker.orient}
/>
</Marker>
<Marker id="react-flow__arrow">
<polyline
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
fill="none"
points="-5,-4 0,0 -5,4"
/>
</Marker>
))}
</defs>
);
};
@@ -0,0 +1,51 @@
import React, { useMemo } from 'react';
import { MarkerType, EdgeMarker } from '../../types';
type SymbolProps = Omit<EdgeMarker, 'type'>;
const ArrowSymbol = ({ color = 'none', strokeWidth = 1 }: SymbolProps) => {
return (
<polyline
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={strokeWidth}
fill="none"
points="-5,-4 0,0 -5,4"
/>
);
};
const ArrowClosedSymbol = ({ color = 'none', strokeWidth = 1 }: SymbolProps) => {
return (
<polyline
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={strokeWidth}
fill={color}
points="-5,-4 0,0 -5,4 -5,-4"
/>
);
};
export const MarkerSymbols = {
[MarkerType.Arrow]: ArrowSymbol,
[MarkerType.ArrowClosed]: ArrowClosedSymbol,
};
export function useMarkerSymbol(type: MarkerType) {
const symbol = useMemo(() => {
const symbolExists = MarkerSymbols.hasOwnProperty(type);
if (!symbolExists) {
console.warn(`marker type "${type}" doesn't exist.`);
return () => null;
}
return MarkerSymbols[type];
}, [type]);
return symbol;
}
export default MarkerSymbols;
+146 -209
View File
@@ -1,34 +1,31 @@
import React, { memo, CSSProperties, useCallback } from 'react';
import React, { memo, CSSProperties } from 'react';
import shallow from 'zustand/shallow';
import cc from 'classcat';
import { useStoreState } from '../../store/hooks';
import { useStore } from '../../store';
import ConnectionLine from '../../components/ConnectionLine/index';
import { isEdge } from '../../utils/graph';
import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getHandle, isEdgeVisible, getSourceTargetNodes } from './utils';
import { getEdgePositions, getHandle, getNodeData } from './utils';
import {
Position,
Edge,
Node,
Elements,
Connection,
ConnectionLineType,
ConnectionLineComponent,
ConnectionMode,
Transform,
OnEdgeUpdateFunc,
HandleType,
ReactFlowState,
} from '../../types';
import useVisibleEdges from '../../hooks/useVisibleEdges';
interface EdgeRendererProps {
edgeTypes: any;
connectionLineType: ConnectionLineType;
connectionLineStyle?: CSSProperties;
connectionLineComponent?: ConnectionLineComponent;
connectionMode?: ConnectionMode;
onElementClick?: (event: React.MouseEvent, element: Node | Edge) => void;
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void;
arrowHeadColor: string;
markerEndId?: string;
defaultMarkerColor: string;
onlyRenderVisibleElements: boolean;
onEdgeUpdate?: OnEdgeUpdateFunc;
onEdgeContextMenu?: (event: React.MouseEvent, edge: Edge) => void;
@@ -38,215 +35,155 @@ interface EdgeRendererProps {
onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void;
edgeUpdaterRadius?: number;
noPanClassName?: string;
}
interface EdgeWrapperProps {
edge: Edge;
props: EdgeRendererProps;
nodes: Node[];
selectedElements: Elements | null;
elementsSelectable: boolean;
transform: Transform;
width: number;
height: number;
onlyRenderVisibleElements: boolean;
connectionMode?: ConnectionMode;
}
const Edge = ({
edge,
props,
nodes,
selectedElements,
elementsSelectable,
transform,
width,
height,
onlyRenderVisibleElements,
connectionMode,
}: EdgeWrapperProps) => {
const sourceHandleId = edge.sourceHandle || null;
const targetHandleId = edge.targetHandle || null;
const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes);
const onConnectEdge = useCallback(
(connection: Connection) => {
props.onEdgeUpdate?.(edge, connection);
},
[edge, props.onEdgeUpdate]
);
if (!sourceNode) {
console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`);
return null;
}
if (!targetNode) {
console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`);
return null;
}
// source and target node need to be initialized
if (!sourceNode.__rf.width || !targetNode.__rf.width) {
return null;
}
const edgeType = edge.type || 'default';
const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default;
const targetNodeBounds = targetNode.__rf.handleBounds;
// when connection type is loose we can define all handles as sources
const targetNodeHandles =
connectionMode === ConnectionMode.Strict
? targetNodeBounds.target
: targetNodeBounds.target || targetNodeBounds.source;
const sourceHandle = getHandle(sourceNode.__rf.handleBounds.source, sourceHandleId);
const targetHandle = getHandle(targetNodeHandles, targetHandleId);
const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom;
const targetPosition = targetHandle ? targetHandle.position : Position.Top;
if (!sourceHandle) {
console.warn(`couldn't create edge for source handle id: ${sourceHandleId}; edge id: ${edge.id}`);
return null;
}
if (!targetHandle) {
console.warn(`couldn't create edge for target handle id: ${targetHandleId}; edge id: ${edge.id}`);
return null;
}
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNode,
sourceHandle,
sourcePosition,
targetNode,
targetHandle,
targetPosition
);
const isVisible = onlyRenderVisibleElements
? isEdgeVisible({
sourcePos: { x: sourceX, y: sourceY },
targetPos: { x: targetX, y: targetY },
width,
height,
transform,
})
: true;
if (!isVisible) {
return null;
}
const isSelected = selectedElements?.some((elm) => isEdge(elm) && elm.id === edge.id) || false;
return (
<EdgeComponent
key={edge.id}
id={edge.id}
className={edge.className}
type={edge.type}
data={edge.data}
onClick={props.onElementClick}
selected={isSelected}
animated={edge.animated}
label={edge.label}
labelStyle={edge.labelStyle}
labelShowBg={edge.labelShowBg}
labelBgStyle={edge.labelBgStyle}
labelBgPadding={edge.labelBgPadding}
labelBgBorderRadius={edge.labelBgBorderRadius}
style={edge.style}
arrowHeadType={edge.arrowHeadType}
source={edge.source}
target={edge.target}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
elementsSelectable={elementsSelectable}
markerEndId={props.markerEndId}
isHidden={edge.isHidden}
onConnectEdge={onConnectEdge}
handleEdgeUpdate={typeof props.onEdgeUpdate !== 'undefined'}
onContextMenu={props.onEdgeContextMenu}
onMouseEnter={props.onEdgeMouseEnter}
onMouseMove={props.onEdgeMouseMove}
onMouseLeave={props.onEdgeMouseLeave}
edgeUpdaterRadius={props.edgeUpdaterRadius}
onEdgeDoubleClick={props.onEdgeDoubleClick}
onEdgeUpdateStart={props.onEdgeUpdateStart}
onEdgeUpdateEnd={props.onEdgeUpdateEnd}
/>
);
};
const selector = (s: ReactFlowState) => ({
connectionNodeId: s.connectionNodeId,
connectionHandleId: s.connectionHandleId,
connectionHandleType: s.connectionHandleType,
connectionPosition: s.connectionPosition,
nodesConnectable: s.nodesConnectable,
elementsSelectable: s.elementsSelectable,
width: s.width,
height: s.height,
connectionMode: s.connectionMode,
nodeInternals: s.nodeInternals,
});
const EdgeRenderer = (props: EdgeRendererProps) => {
const transform = useStoreState((state) => state.transform);
const nodes = useStoreState((state) => state.nodes);
const edges = useStoreState((state) => state.edges);
const connectionNodeId = useStoreState((state) => state.connectionNodeId);
const connectionHandleId = useStoreState((state) => state.connectionHandleId);
const connectionHandleType = useStoreState((state) => state.connectionHandleType);
const connectionPosition = useStoreState((state) => state.connectionPosition);
const selectedElements = useStoreState((state) => state.selectedElements);
const nodesConnectable = useStoreState((state) => state.nodesConnectable);
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
const width = useStoreState((state) => state.width);
const height = useStoreState((state) => state.height);
const {
connectionNodeId,
connectionHandleId,
connectionHandleType,
connectionPosition,
nodesConnectable,
elementsSelectable,
width,
height,
connectionMode,
nodeInternals,
} = useStore(selector, shallow);
const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals);
if (!width) {
return null;
}
const {
connectionLineType,
arrowHeadColor,
connectionLineStyle,
connectionLineComponent,
onlyRenderVisibleElements,
} = props;
const transformStyle = `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`;
const { connectionLineType, defaultMarkerColor, connectionLineStyle, connectionLineComponent } = props;
const renderConnectionLine = connectionNodeId && connectionHandleType;
return (
<svg width={width} height={height} className="react-flow__edges">
<MarkerDefinitions color={arrowHeadColor} />
<g style={{ transform: transformStyle }}>
{edges.map((edge: Edge) => (
<Edge
key={edge.id}
edge={edge}
props={props}
nodes={nodes}
selectedElements={selectedElements}
elementsSelectable={elementsSelectable}
transform={transform}
width={width}
height={height}
onlyRenderVisibleElements={onlyRenderVisibleElements}
/>
))}
{renderConnectionLine && (
<ConnectionLine
nodes={nodes}
connectionNodeId={connectionNodeId!}
connectionHandleId={connectionHandleId}
connectionHandleType={connectionHandleType!}
connectionPositionX={connectionPosition.x}
connectionPositionY={connectionPosition.y}
transform={transform}
connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType}
isConnectable={nodesConnectable}
CustomConnectionLineComponent={connectionLineComponent}
/>
)}
</g>
</svg>
<>
{edgeTree.map(({ level, edges, isMaxLevel }) => (
<svg
key={level}
style={{ zIndex: level }}
width={width}
height={height}
className="react-flow__edges react-flow__container"
>
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} />}
<g>
{edges.map((edge: Edge) => {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals, edge.source);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals, edge.target);
if (!sourceIsValid || !targetIsValid) {
return null;
}
const edgeType = edge.type || 'default';
const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default;
// when connection type is loose we can define all handles as sources
const targetNodeHandles =
connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target
: targetHandleBounds!.target || targetHandleBounds!.source;
const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle || null);
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle || null);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
if (!sourceHandle) {
console.warn(`couldn't create edge for source handle id: ${edge.sourceHandle}; edge id: ${edge.id}`);
return null;
}
if (!targetHandle) {
console.warn(`couldn't create edge for target handle id: ${edge.targetHandle}; edge id: ${edge.id}`);
return null;
}
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNodeRect,
sourceHandle,
sourcePosition,
targetNodeRect,
targetHandle,
targetPosition
);
return (
<EdgeComponent
key={edge.id}
id={edge.id}
className={cc([edge.className, props.noPanClassName])}
type={edgeType}
data={edge.data}
selected={!!edge.selected}
animated={!!edge.animated}
hidden={!!edge.hidden}
label={edge.label}
labelStyle={edge.labelStyle}
labelShowBg={edge.labelShowBg}
labelBgStyle={edge.labelBgStyle}
labelBgPadding={edge.labelBgPadding}
labelBgBorderRadius={edge.labelBgBorderRadius}
style={edge.style}
source={edge.source}
target={edge.target}
sourceHandleId={edge.sourceHandle}
targetHandleId={edge.targetHandle}
markerEnd={edge.markerEnd}
markerStart={edge.markerStart}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
elementsSelectable={elementsSelectable}
onEdgeUpdate={props.onEdgeUpdate}
onContextMenu={props.onEdgeContextMenu}
onMouseEnter={props.onEdgeMouseEnter}
onMouseMove={props.onEdgeMouseMove}
onMouseLeave={props.onEdgeMouseLeave}
onClick={props.onEdgeClick}
edgeUpdaterRadius={props.edgeUpdaterRadius}
onEdgeDoubleClick={props.onEdgeDoubleClick}
onEdgeUpdateStart={props.onEdgeUpdateStart}
onEdgeUpdateEnd={props.onEdgeUpdateEnd}
/>
);
})}
{renderConnectionLine && isMaxLevel && (
<ConnectionLine
connectionNodeId={connectionNodeId!}
connectionHandleId={connectionHandleId}
connectionHandleType={connectionHandleType!}
connectionPositionX={connectionPosition.x}
connectionPositionY={connectionPosition.y}
connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType}
isConnectable={nodesConnectable}
CustomConnectionLineComponent={connectionLineComponent}
/>
)}
</g>
</svg>
))}
</>
);
};
+62 -44
View File
@@ -1,31 +1,32 @@
import { ComponentType } from 'react';
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges';
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge, SimpleBezierEdge } from '../../components/Edges';
import wrapEdge from '../../components/Edges/wrapEdge';
import { rectToBox } from '../../utils/graph';
import {
EdgeTypesType,
EdgeProps,
Position,
Node,
XYPosition,
ElementId,
EdgeTypes,
HandleElement,
NodeHandleBounds,
NodeInternals,
Position,
Rect,
Transform,
Edge,
XYPosition,
} from '../../types';
import { rectToBox } from '../../utils';
export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType {
const standardTypes: EdgeTypesType = {
export type CreateEdgeTypes = (edgeTypes: EdgeTypes) => EdgeTypes;
export function createEdgeTypes(edgeTypes: EdgeTypes): EdgeTypes {
const standardTypes: EdgeTypes = {
default: wrapEdge((edgeTypes.default || BezierEdge) as ComponentType<EdgeProps>),
straight: wrapEdge((edgeTypes.bezier || StraightEdge) as ComponentType<EdgeProps>),
step: wrapEdge((edgeTypes.step || StepEdge) as ComponentType<EdgeProps>),
smoothstep: wrapEdge((edgeTypes.step || SmoothStepEdge) as ComponentType<EdgeProps>),
simplebezier: wrapEdge((edgeTypes.simplebezier || SimpleBezierEdge) as ComponentType<EdgeProps>),
};
const wrappedTypes = {} as EdgeTypesType;
const specialTypes: EdgeTypesType = Object.keys(edgeTypes)
const wrappedTypes = {} as EdgeTypes;
const specialTypes: EdgeTypes = Object.keys(edgeTypes)
.filter((k) => !['default', 'bezier'].includes(k))
.reduce((res, key) => {
res[key] = wrapEdge((edgeTypes[key] || BezierEdge) as ComponentType<EdgeProps>);
@@ -39,11 +40,11 @@ export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType {
};
}
export function getHandlePosition(position: Position, node: Node, handle: any | null = null): XYPosition {
const x = (handle?.x || 0) + node.__rf.position.x;
const y = (handle?.y || 0) + node.__rf.position.y;
const width = handle?.width || node.__rf.width;
const height = handle?.height || node.__rf.height;
export function getHandlePosition(position: Position, nodeRect: Rect, handle: any | null = null): XYPosition {
const x = (handle?.x || 0) + nodeRect.x;
const y = (handle?.y || 0) + nodeRect.y;
const width = handle?.width || nodeRect.width;
const height = handle?.height || nodeRect.height;
switch (position) {
case Position.Top:
@@ -69,7 +70,7 @@ export function getHandlePosition(position: Position, node: Node, handle: any |
}
}
export function getHandle(bounds: HandleElement[], handleId: ElementId | null): HandleElement | null {
export function getHandle(bounds: HandleElement[], handleId: string | null): HandleElement | null {
if (!bounds) {
return null;
}
@@ -94,15 +95,15 @@ interface EdgePositions {
}
export const getEdgePositions = (
sourceNode: Node,
sourceNodeRect: Rect,
sourceHandle: HandleElement | unknown,
sourcePosition: Position,
targetNode: Node,
targetNodeRect: Rect,
targetHandle: HandleElement | unknown,
targetPosition: Position
): EdgePositions => {
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle);
const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle);
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
const targetHandlePos = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
return {
sourceX: sourceHandlePos.x,
@@ -115,17 +116,31 @@ export const getEdgePositions = (
interface IsEdgeVisibleParams {
sourcePos: XYPosition;
targetPos: XYPosition;
sourceWidth: number;
sourceHeight: number;
targetWidth: number;
targetHeight: number;
width: number;
height: number;
transform: Transform;
}
export function isEdgeVisible({ sourcePos, targetPos, width, height, transform }: IsEdgeVisibleParams): boolean {
export function isEdgeVisible({
sourcePos,
targetPos,
sourceWidth,
sourceHeight,
targetWidth,
targetHeight,
width,
height,
transform,
}: IsEdgeVisibleParams): boolean {
const edgeBox = {
x: Math.min(sourcePos.x, targetPos.x),
y: Math.min(sourcePos.y, targetPos.y),
x2: Math.max(sourcePos.x, targetPos.x),
y2: Math.max(sourcePos.y, targetPos.y),
x2: Math.max(sourcePos.x + sourceWidth, targetPos.x + targetWidth),
y2: Math.max(sourcePos.y + sourceHeight, targetPos.y + targetHeight),
};
if (edgeBox.x === edgeBox.x2) {
@@ -150,22 +165,25 @@ export function isEdgeVisible({ sourcePos, targetPos, width, height, transform }
return overlappingArea > 0;
}
type SourceTargetNode = {
sourceNode: Node | null;
targetNode: Node | null;
};
export function getNodeData(nodeInternals: NodeInternals, nodeId: string): [Rect, NodeHandleBounds | null, boolean] {
const node = nodeInternals.get(nodeId);
const handleBounds = node?.handleBounds;
const isInvalid =
!node ||
!node.handleBounds ||
!node.width ||
!node.height ||
typeof node.positionAbsolute?.x === 'undefined' ||
typeof node.positionAbsolute?.y === 'undefined';
export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNode => {
return nodes.reduce(
(res, node) => {
if (node.id === edge.source) {
res.sourceNode = node;
}
if (node.id === edge.target) {
res.targetNode = node;
}
return res;
return [
{
x: node?.positionAbsolute?.x || 0,
y: node?.positionAbsolute?.y || 0,
width: node?.width || 0,
height: node?.height || 0,
},
{ sourceNode: null, targetNode: null } as SourceTargetNode
);
};
handleBounds || null,
!isInvalid,
];
}
+29 -28
View File
@@ -1,18 +1,18 @@
import React, { useCallback, memo, ReactNode, WheelEvent, MouseEvent } from 'react';
import { useStoreActions, useStoreState } from '../../store/hooks';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
import useKeyPress from '../../hooks/useKeyPress';
import { GraphViewProps } from '../GraphView';
import ZoomPane from '../ZoomPane';
import UserSelection from '../../components/UserSelection';
import NodesSelection from '../../components/NodesSelection';
import { ReactFlowState } from '../../types';
interface FlowRendererProps
extends Omit<
GraphViewProps,
| 'elements'
| 'snapToGrid'
| 'nodeTypes'
| 'edgeTypes'
@@ -21,16 +21,21 @@ interface FlowRendererProps
| 'arrowHeadColor'
| 'onlyRenderVisibleElements'
| 'selectNodesOnDrag'
| 'defaultMarkerColor'
> {
children: ReactNode;
}
const selector = (s: ReactFlowState) => ({
resetSelectedElements: s.resetSelectedElements,
nodesSelectionActive: s.nodesSelectionActive,
});
const FlowRenderer = ({
children,
onPaneClick,
onPaneContextMenu,
onPaneScroll,
onElementsRemove,
deleteKeyCode,
onMove,
onMoveStart,
@@ -45,46 +50,35 @@ const FlowRenderer = ({
panOnScrollSpeed,
panOnScrollMode,
zoomOnDoubleClick,
paneMoveable,
panOnDrag,
defaultPosition,
defaultZoom,
translateExtent,
preventScrolling,
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
noWheelClassName,
noPanClassName,
}: FlowRendererProps) => {
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements);
const nodesSelectionActive = useStoreState((state) => state.nodesSelectionActive);
const store = useStoreApi();
const { resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow);
const selectionKeyPressed = useKeyPress(selectionKeyCode);
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode, multiSelectionKeyCode });
useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode });
const onClick = useCallback(
(event: MouseEvent) => {
onPaneClick?.(event);
unsetNodesSelection();
resetSelectedElements();
store.setState({ nodesSelectionActive: false });
},
[onPaneClick]
);
const onContextMenu = useCallback(
(event: MouseEvent) => {
onPaneContextMenu?.(event);
},
[onPaneContextMenu]
);
const onWheel = useCallback(
(event: WheelEvent) => {
onPaneScroll?.(event);
},
[onPaneScroll]
);
const onContextMenu = useCallback((event: MouseEvent) => onPaneContextMenu?.(event), [onPaneContextMenu]);
const onWheel = useCallback((event: WheelEvent) => onPaneScroll?.(event), [onPaneScroll]);
return (
<ZoomPane
@@ -99,12 +93,13 @@ const FlowRenderer = ({
panOnScrollSpeed={panOnScrollSpeed}
panOnScrollMode={panOnScrollMode}
zoomOnDoubleClick={zoomOnDoubleClick}
paneMoveable={paneMoveable}
panOnDrag={panOnDrag}
defaultPosition={defaultPosition}
defaultZoom={defaultZoom}
translateExtent={translateExtent}
zoomActivationKeyCode={zoomActivationKeyCode}
preventScrolling={preventScrolling}
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
>
{children}
<UserSelection selectionKeyPressed={selectionKeyPressed} />
@@ -114,9 +109,15 @@ const FlowRenderer = ({
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
noPanClassName={noPanClassName}
/>
)}
<div className="react-flow__pane" onClick={onClick} onContextMenu={onContextMenu} onWheel={onWheel} />
<div
className="react-flow__pane react-flow__container"
onClick={onClick}
onContextMenu={onContextMenu}
onWheel={onWheel}
/>
</ZoomPane>
);
};
+64 -195
View File
@@ -1,30 +1,27 @@
import React, { useEffect, useRef, memo } from 'react';
import React, { memo } from 'react';
import { useStoreActions, useStore } from '../../store/hooks';
import FlowRenderer from '../FlowRenderer';
import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer';
import { onLoadProject, onLoadGetElements, onLoadToObject } from '../../utils/graph';
import useZoomPanHelper from '../../hooks/useZoomPanHelper';
import Viewport from '../Viewport';
import useOnInitHandler from '../../hooks/useOnInitHandler';
import { NodeTypes, EdgeTypes, ConnectionLineType, KeyCode, ReactFlowProps } from '../../types';
import { ReactFlowProps } from '../ReactFlow';
import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types';
export interface GraphViewProps extends Omit<ReactFlowProps, 'onSelectionChange' | 'elements'> {
nodeTypes: NodeTypesType;
edgeTypes: EdgeTypesType;
selectionKeyCode: KeyCode;
deleteKeyCode: KeyCode;
multiSelectionKeyCode: KeyCode;
export interface GraphViewProps extends Omit<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges'> {
nodeTypes: NodeTypes;
edgeTypes: EdgeTypes;
selectionKeyCode: KeyCode | null;
deleteKeyCode: KeyCode | null;
multiSelectionKeyCode: KeyCode | null;
connectionLineType: ConnectionLineType;
snapToGrid: boolean;
snapGrid: [number, number];
onlyRenderVisibleElements: boolean;
defaultZoom: number;
defaultPosition: [number, number];
arrowHeadColor: string;
defaultMarkerColor: string;
selectNodesOnDrag: boolean;
noDragClassName: string;
noWheelClassName: string;
noPanClassName: string;
}
const GraphView = ({
@@ -33,8 +30,9 @@ const GraphView = ({
onMove,
onMoveStart,
onMoveEnd,
onLoad,
onElementClick,
onInit,
onNodeClick,
onEdgeClick,
onNodeDoubleClick,
onEdgeDoubleClick,
onNodeMouseEnter,
@@ -48,42 +46,27 @@ const GraphView = ({
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
connectionMode,
connectionLineType,
connectionLineStyle,
connectionLineComponent,
selectionKeyCode,
multiSelectionKeyCode,
zoomActivationKeyCode,
onElementsRemove,
deleteKeyCode,
onConnect,
onConnectStart,
onConnectStop,
onConnectEnd,
snapToGrid,
snapGrid,
onlyRenderVisibleElements,
nodesDraggable,
nodesConnectable,
elementsSelectable,
selectNodesOnDrag,
minZoom,
maxZoom,
defaultZoom,
defaultPosition,
translateExtent,
preventScrolling,
nodeExtent,
arrowHeadColor,
markerEndId,
defaultMarkerColor,
zoomOnScroll,
zoomOnPinch,
panOnScroll,
panOnScrollSpeed,
panOnScrollMode,
zoomOnDoubleClick,
paneMoveable,
panOnDrag,
onPaneClick,
onPaneScroll,
onPaneContextMenu,
@@ -95,134 +78,17 @@ const GraphView = ({
edgeUpdaterRadius,
onEdgeUpdateStart,
onEdgeUpdateEnd,
noDragClassName,
noWheelClassName,
noPanClassName,
}: GraphViewProps) => {
const isInitialized = useRef<boolean>(false);
const setOnConnect = useStoreActions((actions) => actions.setOnConnect);
const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart);
const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop);
const setOnConnectEnd = useStoreActions((actions) => actions.setOnConnectEnd);
const setSnapGrid = useStoreActions((actions) => actions.setSnapGrid);
const setSnapToGrid = useStoreActions((actions) => actions.setSnapToGrid);
const setNodesDraggable = useStoreActions((actions) => actions.setNodesDraggable);
const setNodesConnectable = useStoreActions((actions) => actions.setNodesConnectable);
const setElementsSelectable = useStoreActions((actions) => actions.setElementsSelectable);
const setMinZoom = useStoreActions((actions) => actions.setMinZoom);
const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom);
const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent);
const setNodeExtent = useStoreActions((actions) => actions.setNodeExtent);
const setConnectionMode = useStoreActions((actions) => actions.setConnectionMode);
const currentStore = useStore();
const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper();
useEffect(() => {
if (!isInitialized.current && initialized) {
if (onLoad) {
onLoad({
fitView: (params = { padding: 0.1 }) => fitView(params),
zoomIn,
zoomOut,
zoomTo,
setTransform: transform,
project: onLoadProject(currentStore),
getElements: onLoadGetElements(currentStore),
toObject: onLoadToObject(currentStore),
});
}
isInitialized.current = true;
}
}, [onLoad, zoomIn, zoomOut, zoomTo, transform, fitView, initialized]);
useEffect(() => {
if (onConnect) {
setOnConnect(onConnect);
}
}, [onConnect]);
useEffect(() => {
if (onConnectStart) {
setOnConnectStart(onConnectStart);
}
}, [onConnectStart]);
useEffect(() => {
if (onConnectStop) {
setOnConnectStop(onConnectStop);
}
}, [onConnectStop]);
useEffect(() => {
if (onConnectEnd) {
setOnConnectEnd(onConnectEnd);
}
}, [onConnectEnd]);
useEffect(() => {
if (typeof snapToGrid !== 'undefined') {
setSnapToGrid(snapToGrid);
}
}, [snapToGrid]);
useEffect(() => {
if (typeof snapGrid !== 'undefined') {
setSnapGrid(snapGrid);
}
}, [snapGrid]);
useEffect(() => {
if (typeof nodesDraggable !== 'undefined') {
setNodesDraggable(nodesDraggable);
}
}, [nodesDraggable]);
useEffect(() => {
if (typeof nodesConnectable !== 'undefined') {
setNodesConnectable(nodesConnectable);
}
}, [nodesConnectable]);
useEffect(() => {
if (typeof elementsSelectable !== 'undefined') {
setElementsSelectable(elementsSelectable);
}
}, [elementsSelectable]);
useEffect(() => {
if (typeof minZoom !== 'undefined') {
setMinZoom(minZoom);
}
}, [minZoom]);
useEffect(() => {
if (typeof maxZoom !== 'undefined') {
setMaxZoom(maxZoom);
}
}, [maxZoom]);
useEffect(() => {
if (typeof translateExtent !== 'undefined') {
setTranslateExtent(translateExtent);
}
}, [translateExtent]);
useEffect(() => {
if (typeof nodeExtent !== 'undefined') {
setNodeExtent(nodeExtent);
}
}, [nodeExtent]);
useEffect(() => {
if (typeof connectionMode !== 'undefined') {
setConnectionMode(connectionMode);
}
}, [connectionMode]);
useOnInitHandler(onInit);
return (
<FlowRenderer
onPaneClick={onPaneClick}
onPaneContextMenu={onPaneContextMenu}
onPaneScroll={onPaneScroll}
onElementsRemove={onElementsRemove}
deleteKeyCode={deleteKeyCode}
selectionKeyCode={selectionKeyCode}
multiSelectionKeyCode={multiSelectionKeyCode}
@@ -237,52 +103,55 @@ const GraphView = ({
panOnScroll={panOnScroll}
panOnScrollSpeed={panOnScrollSpeed}
panOnScrollMode={panOnScrollMode}
paneMoveable={paneMoveable}
panOnDrag={panOnDrag}
defaultPosition={defaultPosition}
defaultZoom={defaultZoom}
translateExtent={translateExtent}
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
preventScrolling={preventScrolling}
noDragClassName={noDragClassName}
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
>
<NodeRenderer
nodeTypes={nodeTypes}
onElementClick={onElementClick}
onNodeDoubleClick={onNodeDoubleClick}
onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseMove={onNodeMouseMove}
onNodeMouseLeave={onNodeMouseLeave}
onNodeContextMenu={onNodeContextMenu}
onNodeDragStop={onNodeDragStop}
onNodeDrag={onNodeDrag}
onNodeDragStart={onNodeDragStart}
selectNodesOnDrag={selectNodesOnDrag}
snapToGrid={snapToGrid}
snapGrid={snapGrid}
onlyRenderVisibleElements={onlyRenderVisibleElements}
/>
<EdgeRenderer
edgeTypes={edgeTypes}
onElementClick={onElementClick}
onEdgeDoubleClick={onEdgeDoubleClick}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
connectionMode={connectionMode}
arrowHeadColor={arrowHeadColor}
markerEndId={markerEndId}
onEdgeUpdate={onEdgeUpdate}
onlyRenderVisibleElements={onlyRenderVisibleElements}
onEdgeContextMenu={onEdgeContextMenu}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
/>
<Viewport>
<EdgeRenderer
edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
onEdgeUpdate={onEdgeUpdate}
onlyRenderVisibleElements={onlyRenderVisibleElements}
onEdgeContextMenu={onEdgeContextMenu}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
defaultMarkerColor={defaultMarkerColor}
noPanClassName={noPanClassName}
/>
<NodeRenderer
nodeTypes={nodeTypes}
onNodeClick={onNodeClick}
onNodeDoubleClick={onNodeDoubleClick}
onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseMove={onNodeMouseMove}
onNodeMouseLeave={onNodeMouseLeave}
onNodeContextMenu={onNodeContextMenu}
onNodeDragStop={onNodeDragStop}
onNodeDrag={onNodeDrag}
onNodeDragStart={onNodeDragStart}
selectNodesOnDrag={selectNodesOnDrag}
onlyRenderVisibleElements={onlyRenderVisibleElements}
noPanClassName={noPanClassName}
noDragClassName={noDragClassName}
/>
</Viewport>
</FlowRenderer>
);
};
+55 -44
View File
@@ -1,12 +1,14 @@
import React, { memo, useMemo, ComponentType, MouseEvent } from 'react';
import React, { memo, useMemo, ComponentType, MouseEvent, useEffect, useRef } from 'react';
import shallow from 'zustand/shallow';
import useVisibleNodes from '../../hooks/useVisibleNodes';
import { useStore } from '../../store';
import { Node, NodeTypes, Position, ReactFlowState, WrapNodeProps } from '../../types';
import { getNodesInside } from '../../utils/graph';
import { useStoreState, useStoreActions } from '../../store/hooks';
import { Node, NodeTypesType, WrapNodeProps, Edge } from '../../types';
interface NodeRendererProps {
nodeTypes: NodeTypesType;
nodeTypes: NodeTypes;
selectNodesOnDrag: boolean;
onElementClick?: (event: MouseEvent, element: Node | Edge) => void;
onNodeClick?: (event: MouseEvent, element: Node) => void;
onNodeDoubleClick?: (event: MouseEvent, element: Node) => void;
onNodeMouseEnter?: (event: MouseEvent, node: Node) => void;
onNodeMouseMove?: (event: MouseEvent, node: Node) => void;
@@ -15,58 +17,64 @@ interface NodeRendererProps {
onNodeDragStart?: (event: MouseEvent, node: Node) => void;
onNodeDrag?: (event: MouseEvent, node: Node) => void;
onNodeDragStop?: (event: MouseEvent, node: Node) => void;
snapToGrid: boolean;
snapGrid: [number, number];
onlyRenderVisibleElements: boolean;
noPanClassName: string;
noDragClassName: string;
}
const selector = (s: ReactFlowState) => ({
scale: s.transform[2],
nodesDraggable: s.nodesDraggable,
nodesConnectable: s.nodesConnectable,
elementsSelectable: s.elementsSelectable,
updateNodeDimensions: s.updateNodeDimensions,
snapGrid: s.snapGrid,
snapToGrid: s.snapToGrid,
nodeInternals: s.nodeInternals,
});
const NodeRenderer = (props: NodeRendererProps) => {
const transform = useStoreState((state) => state.transform);
const selectedElements = useStoreState((state) => state.selectedElements);
const nodesDraggable = useStoreState((state) => state.nodesDraggable);
const nodesConnectable = useStoreState((state) => state.nodesConnectable);
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
const width = useStoreState((state) => state.width);
const height = useStoreState((state) => state.height);
const nodes = useStoreState((state) => state.nodes);
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
const visibleNodes = props.onlyRenderVisibleElements
? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true)
: nodes;
const transformStyle = useMemo(
() => ({
transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`,
}),
[transform[0], transform[1], transform[2]]
);
const { scale, nodesDraggable, nodesConnectable, elementsSelectable, updateNodeDimensions, snapGrid, snapToGrid } =
useStore(selector, shallow);
const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
const resizeObserverRef = useRef<ResizeObserver>();
const resizeObserver = useMemo(() => {
if (typeof ResizeObserver === 'undefined') {
return null;
}
return new ResizeObserver((entries: ResizeObserverEntry[]) => {
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
const updates = entries.map((entry: ResizeObserverEntry) => ({
id: entry.target.getAttribute('data-id') as string,
nodeElement: entry.target as HTMLDivElement,
forceUpdate: true,
}));
updateNodeDimensions(updates);
});
resizeObserverRef.current = observer;
return observer;
}, []);
useEffect(() => {
return () => {
resizeObserverRef?.current?.disconnect();
};
}, []);
return (
<div className="react-flow__nodes" style={transformStyle}>
{visibleNodes.map((node) => {
<div className="react-flow__nodes react-flow__container">
{nodes.map((node) => {
const nodeType = node.type || 'default';
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
if (!props.nodeTypes[nodeType]) {
console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`);
}
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined'));
const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined'));
@@ -79,17 +87,16 @@ const NodeRenderer = (props: NodeRendererProps) => {
style={node.style}
type={nodeType}
data={node.data}
sourcePosition={node.sourcePosition}
targetPosition={node.targetPosition}
isHidden={node.isHidden}
xPos={node.__rf.position.x}
yPos={node.__rf.position.y}
isDragging={node.__rf.isDragging}
isInitialized={node.__rf.width !== null && node.__rf.height !== null}
snapGrid={props.snapGrid}
snapToGrid={props.snapToGrid}
sourcePosition={node.sourcePosition || Position.Bottom}
targetPosition={node.targetPosition || Position.Top}
hidden={node.hidden}
xPos={node.positionAbsolute?.x ?? 0}
yPos={node.positionAbsolute?.y ?? 0}
dragging={!!node.dragging}
snapGrid={snapGrid}
snapToGrid={snapToGrid}
selectNodesOnDrag={props.selectNodesOnDrag}
onClick={props.onElementClick}
onClick={props.onNodeClick}
onMouseEnter={props.onNodeMouseEnter}
onMouseMove={props.onNodeMouseMove}
onMouseLeave={props.onNodeMouseLeave}
@@ -98,13 +105,17 @@ const NodeRenderer = (props: NodeRendererProps) => {
onNodeDragStart={props.onNodeDragStart}
onNodeDrag={props.onNodeDrag}
onNodeDragStop={props.onNodeDragStop}
scale={transform[2]}
selected={selectedElements?.some(({ id }) => id === node.id) || false}
scale={scale}
selected={!!node.selected}
isDraggable={isDraggable}
isSelectable={isSelectable}
isConnectable={isConnectable}
resizeObserver={resizeObserver}
dragHandle={node.dragHandle}
zIndex={node.z ?? 0}
isParent={!!node.isParent}
noDragClassName={props.noDragClassName}
noPanClassName={props.noPanClassName}
/>
);
})}
+14 -18
View File
@@ -3,29 +3,25 @@ import { ComponentType } from 'react';
import DefaultNode from '../../components/Nodes/DefaultNode';
import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode';
import GroupNode from '../../components/Nodes/GroupNode';
import wrapNode from '../../components/Nodes/wrapNode';
import { NodeTypesType, NodeComponentProps } from '../../types';
import { NodeTypes, NodeProps } from '../../types';
export function createNodeTypes(nodeTypes: NodeTypesType): NodeTypesType {
const standardTypes: NodeTypesType = {
input: wrapNode((nodeTypes.input || InputNode) as ComponentType<
NodeComponentProps
>),
default: wrapNode((nodeTypes.default || DefaultNode) as ComponentType<
NodeComponentProps
>),
output: wrapNode((nodeTypes.output || OutputNode) as ComponentType<
NodeComponentProps
>),
export type CreateNodeTypes = (nodeTypes: NodeTypes) => NodeTypes;
export function createNodeTypes(nodeTypes: NodeTypes): NodeTypes {
const standardTypes: NodeTypes = {
input: wrapNode((nodeTypes.input || InputNode) as ComponentType<NodeProps>),
default: wrapNode((nodeTypes.default || DefaultNode) as ComponentType<NodeProps>),
output: wrapNode((nodeTypes.output || OutputNode) as ComponentType<NodeProps>),
group: wrapNode((nodeTypes.group || GroupNode) as ComponentType<NodeProps>),
};
const wrappedTypes = {} as NodeTypesType;
const specialTypes: NodeTypesType = Object.keys(nodeTypes)
.filter(k => !['input', 'default', 'output'].includes(k))
const wrappedTypes = {} as NodeTypes;
const specialTypes: NodeTypes = Object.keys(nodeTypes)
.filter((k) => !['input', 'default', 'output', 'group'].includes(k))
.reduce((res, key) => {
res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType<
NodeComponentProps
>);
res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType<NodeProps>);
return res;
}, wrappedTypes);
+11 -9
View File
@@ -1,21 +1,23 @@
import React, { FC, useContext, useMemo } from 'react';
import { Provider, ReactReduxContext } from 'react-redux';
import React, { FC } from 'react';
import store from '../../store';
import { Provider, createStore, useStoreApi } from '../../store';
const Wrapper: FC = ({ children }) => {
const contextValue = useContext(ReactReduxContext);
const isWrappedWithReactFlowProvider = useMemo(() => contextValue?.store?.getState()?.reactFlowVersion, [
contextValue,
]);
let isWrapped = true;
if (isWrappedWithReactFlowProvider) {
try {
useStoreApi();
} catch (e) {
isWrapped = false;
}
if (isWrapped) {
// we need to wrap it with a fragment because it's not allowed for children to be a ReactNode
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051
return <>{children}</>;
}
return <Provider store={store}>{children}</Provider>;
return <Provider createStore={createStore}>{children}</Provider>;
};
Wrapper.displayName = 'ReactFlowWrapper';
+86 -144
View File
@@ -1,48 +1,33 @@
import React, {
useMemo,
CSSProperties,
HTMLAttributes,
MouseEvent as ReactMouseEvent,
WheelEvent,
forwardRef,
} from 'react';
import cc from 'classcat';
import GraphView from '../GraphView';
import ElementUpdater from '../../components/ElementUpdater';
import React, { forwardRef } from 'react';
import Attribution from '../../components/Attribution';
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge, SimpleBezierEdge } from '../../components/Edges';
import DefaultNode from '../../components/Nodes/DefaultNode';
import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode';
import { createNodeTypes } from '../NodeRenderer/utils';
import SelectionListener from '../../components/SelectionListener';
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges';
import { createEdgeTypes } from '../EdgeRenderer/utils';
import Wrapper from './Wrapper';
import StoreUpdater from '../../components/StoreUpdater';
import css from '../../style.css';
import theme from '../../theme-default.css';
import {
Elements,
NodeTypesType,
EdgeTypesType,
OnLoadFunc,
Node,
Edge,
Connection,
ConnectionMode,
ConnectionLineType,
ConnectionLineComponent,
FlowTransform,
OnConnectStartFunc,
OnConnectStopFunc,
OnConnectEndFunc,
TranslateExtent,
KeyCode,
ConnectionMode,
EdgeTypes,
NodeTypes,
PanOnScrollMode,
OnEdgeUpdateFunc,
NodeExtent,
HandleType,
ReactFlowProps,
ReactFlowRefType,
} from '../../types';
import { createEdgeTypes } from '../EdgeRenderer/utils';
import GraphView from '../GraphView';
import { createNodeTypes } from '../NodeRenderer/utils';
import injectStyle, { useNodeOrEdgeTypes } from './utils';
import Wrapper from './Wrapper';
import '../../style.css';
import '../../theme-default.css';
if (__INJECT_STYLES__) {
injectStyle(css as unknown as string);
injectStyle(theme as unknown as string);
}
const defaultNodeTypes = {
input: InputNode,
@@ -55,97 +40,28 @@ const defaultEdgeTypes = {
straight: StraightEdge,
step: StepEdge,
smoothstep: SmoothStepEdge,
simplebezier: SimpleBezierEdge,
};
export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onLoad'> {
elements: Elements;
onElementClick?: (event: ReactMouseEvent, element: Node | Edge) => void;
onElementsRemove?: (elements: Elements) => void;
onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void;
onNodeMouseEnter?: (event: ReactMouseEvent, node: Node) => void;
onNodeMouseMove?: (event: ReactMouseEvent, node: Node) => void;
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?: (connection: Edge | Connection) => void;
onConnectStart?: OnConnectStartFunc;
onConnectStop?: OnConnectStopFunc;
onConnectEnd?: OnConnectEndFunc;
onLoad?: OnLoadFunc;
onMove?: (flowTransform?: FlowTransform) => void;
onMoveStart?: (flowTransform?: FlowTransform) => void;
onMoveEnd?: (flowTransform?: FlowTransform) => void;
onSelectionChange?: (elements: Elements | null) => void;
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?: TranslateExtent;
preventScrolling?: boolean;
nodeExtent?: NodeExtent;
arrowHeadColor?: string;
markerEndId?: 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, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void;
edgeUpdaterRadius?: number;
nodeTypesId?: string;
edgeTypesId?: string;
}
export type ReactFlowRefType = HTMLDivElement;
const initSnapGrid: [number, number] = [15, 15];
const initDefaultPosition: [number, number] = [0, 0];
const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
(
{
elements = [],
nodes,
edges,
defaultNodes,
defaultEdges,
className,
nodeTypes = defaultNodeTypes,
edgeTypes = defaultEdgeTypes,
onElementClick,
onLoad,
onNodeClick,
onEdgeClick,
onInit,
onMove,
onMoveStart,
onMoveEnd,
onElementsRemove,
onConnect,
onConnectStart,
onConnectStop,
@@ -158,6 +74,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onNodeDragStart,
onNodeDrag,
onNodeDragStop,
onNodesDelete,
onEdgesDelete,
onSelectionChange,
onSelectionDragStart,
onSelectionDrag,
@@ -172,7 +90,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
multiSelectionKeyCode = 'Meta',
zoomActivationKeyCode = 'Meta',
snapToGrid = false,
snapGrid = [15, 15],
snapGrid = initSnapGrid,
onlyRenderVisibleElements = false,
selectNodesOnDrag = true,
nodesDraggable,
@@ -181,19 +99,18 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
minZoom,
maxZoom,
defaultZoom = 1,
defaultPosition = [0, 0],
defaultPosition = initDefaultPosition,
translateExtent,
preventScrolling = true,
nodeExtent,
arrowHeadColor = '#b1b1b7',
markerEndId,
defaultMarkerColor = '#b1b1b7',
zoomOnScroll = true,
zoomOnPinch = true,
panOnScroll = false,
panOnScrollSpeed = 0.5,
panOnScrollMode = PanOnScrollMode.Free,
zoomOnDoubleClick = true,
paneMoveable = true,
panOnDrag = true,
onPaneClick,
onPaneScroll,
onPaneContextMenu,
@@ -207,25 +124,35 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onEdgeUpdateStart,
onEdgeUpdateEnd,
edgeUpdaterRadius = 10,
nodeTypesId = '1',
edgeTypesId = '1',
onNodesChange,
onEdgesChange,
noDragClassName = 'nodrag',
noWheelClassName = 'nowheel',
noPanClassName = 'nopan',
fitView = false,
fitViewOptions,
connectOnClick = true,
attributionPosition,
proOptions,
defaultEdgeOptions,
...rest
},
ref
) => {
const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), [nodeTypesId]);
const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), [edgeTypesId]);
const nodeTypesParsed = useNodeOrEdgeTypes(nodeTypes, createNodeTypes) as NodeTypes;
const edgeTypesParsed = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes) as EdgeTypes;
const reactFlowClasses = cc(['react-flow', className]);
return (
<div {...rest} ref={ref} className={reactFlowClasses}>
<Wrapper>
<GraphView
onLoad={onLoad}
onInit={onInit}
onMove={onMove}
onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd}
onElementClick={onElementClick}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseMove={onNodeMouseMove}
onNodeMouseLeave={onNodeMouseLeave}
@@ -236,42 +163,25 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onNodeDragStop={onNodeDragStop}
nodeTypes={nodeTypesParsed}
edgeTypes={edgeTypesParsed}
connectionMode={connectionMode}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
selectionKeyCode={selectionKeyCode}
onElementsRemove={onElementsRemove}
deleteKeyCode={deleteKeyCode}
multiSelectionKeyCode={multiSelectionKeyCode}
zoomActivationKeyCode={zoomActivationKeyCode}
onConnect={onConnect}
onConnectStart={onConnectStart}
onConnectStop={onConnectStop}
onConnectEnd={onConnectEnd}
snapToGrid={snapToGrid}
snapGrid={snapGrid}
onlyRenderVisibleElements={onlyRenderVisibleElements}
nodesDraggable={nodesDraggable}
nodesConnectable={nodesConnectable}
elementsSelectable={elementsSelectable}
selectNodesOnDrag={selectNodesOnDrag}
minZoom={minZoom}
maxZoom={maxZoom}
defaultZoom={defaultZoom}
defaultPosition={defaultPosition}
translateExtent={translateExtent}
preventScrolling={preventScrolling}
nodeExtent={nodeExtent}
arrowHeadColor={arrowHeadColor}
markerEndId={markerEndId}
zoomOnScroll={zoomOnScroll}
zoomOnPinch={zoomOnPinch}
zoomOnDoubleClick={zoomOnDoubleClick}
panOnScroll={panOnScroll}
panOnScrollSpeed={panOnScrollSpeed}
panOnScrollMode={panOnScrollMode}
paneMoveable={paneMoveable}
panOnDrag={panOnDrag}
onPaneClick={onPaneClick}
onPaneScroll={onPaneScroll}
onPaneContextMenu={onPaneContextMenu}
@@ -288,10 +198,42 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
defaultMarkerColor={defaultMarkerColor}
noDragClassName={noDragClassName}
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
/>
<StoreUpdater
nodes={nodes}
edges={edges}
defaultNodes={defaultNodes}
defaultEdges={defaultEdges}
onConnect={onConnect}
onConnectStart={onConnectStart}
onConnectStop={onConnectStop}
onConnectEnd={onConnectEnd}
nodesDraggable={nodesDraggable}
nodesConnectable={nodesConnectable}
elementsSelectable={elementsSelectable}
minZoom={minZoom}
maxZoom={maxZoom}
nodeExtent={nodeExtent}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
snapToGrid={snapToGrid}
snapGrid={snapGrid}
connectionMode={connectionMode}
translateExtent={translateExtent}
connectOnClick={connectOnClick}
defaultEdgeOptions={defaultEdgeOptions}
fitView={fitView}
fitViewOptions={fitViewOptions}
onNodesDelete={onNodesDelete}
onEdgesDelete={onEdgesDelete}
/>
<ElementUpdater elements={elements} />
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
{children}
<Attribution proOptions={proOptions} position={attributionPosition} />
</Wrapper>
</div>
);
+40
View File
@@ -0,0 +1,40 @@
import { useMemo, useRef } from 'react';
import shallow from 'zustand/shallow';
import { EdgeTypes, NodeTypes } from '../../types';
import { CreateEdgeTypes } from '../EdgeRenderer/utils';
import { CreateNodeTypes } from '../NodeRenderer/utils';
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypes;
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypes;
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any {
const typesKeysRef = useRef<string[] | null>(null);
const typesParsed = useMemo(() => {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
const typeKeys = Object.keys(nodeOrEdgeTypes);
if (shallow(typesKeysRef.current, typeKeys)) {
console.warn(
"React Flow: It looks like that you created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them."
);
}
typesKeysRef.current = typeKeys;
}
return createTypes(nodeOrEdgeTypes);
}, [nodeOrEdgeTypes]);
return typesParsed;
}
export default function injectStyle(css: string): void {
if (!css || typeof document === 'undefined') return;
const head = document.head || document.getElementsByTagName('head')[0];
const style = document.createElement('style');
head.appendChild(style);
style.appendChild(document.createTextNode(css));
}
+25
View File
@@ -0,0 +1,25 @@
import React, { ReactNode } from 'react';
import { useStore } from '../../store';
import { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => s.transform;
type ViewportProps = {
children: ReactNode;
};
function Viewport({ children }: ViewportProps) {
const transform = useStore(selector);
return (
<div
className="react-flow__viewport react-flow__container"
style={{ transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})` }}
>
{children}
</div>
);
}
export default Viewport;
+54 -61
View File
@@ -1,12 +1,13 @@
import React, { useEffect, useRef, ReactNode } from 'react';
import { zoom, zoomIdentity } from 'd3-zoom';
import { D3ZoomEvent, zoom, zoomIdentity } from 'd3-zoom';
import { select, pointer } from 'd3-selection';
import shallow from 'zustand/shallow';
import { clamp } from '../../utils';
import useKeyPress from '../../hooks/useKeyPress';
import useResizeHandler from '../../hooks/useResizeHandler';
import { useStoreState, useStoreActions, useStore } from '../../store/hooks';
import { FlowTransform, TranslateExtent, PanOnScrollMode, KeyCode } from '../../types';
import { useStore, useStoreApi } from '../../store';
import { Viewport, PanOnScrollMode, KeyCode, ReactFlowState, OnMove, OnMoveStart, OnMoveEnd } from '../../types';
interface ZoomPaneProps {
selectionKeyPressed: boolean;
@@ -17,30 +18,35 @@ interface ZoomPaneProps {
panOnScrollSpeed?: number;
panOnScrollMode?: PanOnScrollMode;
zoomOnDoubleClick?: boolean;
paneMoveable?: boolean;
panOnDrag?: boolean;
defaultPosition?: [number, number];
defaultZoom?: number;
translateExtent?: TranslateExtent;
onMove?: (flowTransform?: FlowTransform) => void;
onMoveStart?: (flowTransform?: FlowTransform) => void;
onMoveEnd?: (flowTransform?: FlowTransform) => void;
onMove?: OnMove;
onMoveStart?: OnMoveStart;
onMoveEnd?: OnMoveEnd;
zoomActivationKeyCode?: KeyCode;
preventScrolling?: boolean;
children: ReactNode;
noWheelClassName: string;
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 hasNoWheelClass = (event: any) => event.target.closest('.nowheel');
const isWrappedWithClass = (event: any, className: string | undefined) => event.target.closest(`.${className}`);
const selector = (s: ReactFlowState) => ({
d3Zoom: s.d3Zoom,
d3Selection: s.d3Selection,
d3ZoomHandler: s.d3ZoomHandler,
});
const ZoomPane = ({
onMove,
@@ -54,44 +60,37 @@ const ZoomPane = ({
zoomOnDoubleClick = true,
selectionKeyPressed,
elementsSelectable,
paneMoveable = true,
panOnDrag = true,
defaultPosition = [0, 0],
defaultZoom = 1,
translateExtent,
zoomActivationKeyCode,
preventScrolling = true,
children,
noWheelClassName,
noPanClassName,
}: ZoomPaneProps) => {
const store = useStoreApi();
const zoomPane = useRef<HTMLDivElement>(null);
const prevTransform = useRef<FlowTransform>({ 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 prevTransform = useRef<Viewport>({ x: 0, y: 0, zoom: 0 });
const { d3Zoom, d3Selection, d3ZoomHandler } = useStore(selector, shallow);
const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode);
useResizeHandler(zoomPane);
useEffect(() => {
if (zoomPane.current) {
const state = store.getState();
const currentTranslateExtent = typeof translateExtent !== 'undefined' ? translateExtent : state.translateExtent;
const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(currentTranslateExtent);
const { minZoom, maxZoom, translateExtent } = store.getState();
const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent);
const selection = select(zoomPane.current as Element).call(d3ZoomInstance);
const clampedX = clamp(defaultPosition[0], currentTranslateExtent[0][0], currentTranslateExtent[1][0]);
const clampedY = clamp(defaultPosition[1], currentTranslateExtent[0][1], currentTranslateExtent[1][1]);
const clampedZoom = clamp(defaultZoom, state.minZoom, state.maxZoom);
const clampedX = clamp(defaultPosition[0], translateExtent[0][0], translateExtent[1][0]);
const clampedY = clamp(defaultPosition[1], translateExtent[0][1], translateExtent[1][1]);
const clampedZoom = clamp(defaultZoom, minZoom, maxZoom);
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom);
d3ZoomInstance.transform(selection, updatedTransform);
initD3Zoom({
store.setState({
d3Zoom: d3ZoomInstance,
d3Selection: selection,
d3ZoomHandler: selection.on('wheel.zoom'),
@@ -106,7 +105,7 @@ const ZoomPane = ({
if (panOnScroll && !zoomActivationKeyPressed) {
d3Selection
.on('wheel', (event: any) => {
if (hasNoWheelClass(event)) {
if (isWrappedWithClass(event, noWheelClassName)) {
return false;
}
event.preventDefault();
@@ -140,7 +139,7 @@ const ZoomPane = ({
} else if (typeof d3ZoomHandler !== 'undefined') {
d3Selection
.on('wheel', (event: any) => {
if (!preventScrolling || hasNoWheelClass(event)) {
if (!preventScrolling || isWrappedWithClass(event, noWheelClassName)) {
return null;
}
@@ -158,6 +157,7 @@ const ZoomPane = ({
zoomActivationKeyPressed,
zoomOnPinch,
preventScrolling,
noWheelClassName,
]);
useEffect(() => {
@@ -165,26 +165,26 @@ const ZoomPane = ({
if (selectionKeyPressed) {
d3Zoom.on('zoom', null);
} else {
d3Zoom.on('zoom', (event: any) => {
updateTransform([event.transform.x, event.transform.y, event.transform.k]);
d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
store.setState({ transform: [event.transform.x, event.transform.y, event.transform.k] });
if (onMove) {
const flowTransform = eventToFlowTransform(event.transform);
onMove(flowTransform);
onMove(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
}
});
}
}
}, [selectionKeyPressed, d3Zoom, updateTransform, onMove]);
}, [selectionKeyPressed, d3Zoom, onMove]);
useEffect(() => {
if (d3Zoom) {
if (onMoveStart) {
d3Zoom.on('start', (event: any) => {
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
const flowTransform = eventToFlowTransform(event.transform);
prevTransform.current = flowTransform;
onMoveStart(flowTransform);
onMoveStart(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
});
} else {
d3Zoom.on('start', null);
@@ -195,12 +195,12 @@ const ZoomPane = ({
useEffect(() => {
if (d3Zoom) {
if (onMoveEnd) {
d3Zoom.on('end', (event: any) => {
d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
if (viewChanged(prevTransform.current, event.transform)) {
const flowTransform = eventToFlowTransform(event.transform);
prevTransform.current = flowTransform;
onMoveEnd(flowTransform);
onMoveEnd(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
}
});
} else {
@@ -216,7 +216,7 @@ const ZoomPane = ({
const pinchZoom = zoomOnPinch && event.ctrlKey;
// if all interactions are disabled, we prevent all zoom events
if (!paneMoveable && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) {
if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) {
return false;
}
@@ -230,20 +230,13 @@ const ZoomPane = ({
return false;
}
if (hasNoWheelClass(event) && event.type === 'wheel') {
// if the target element is inside an element with the nowheel class, we prevent zooming
if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') {
return false;
}
// when the target element is a node, we still allow zooming
if (
(event.target.closest('.react-flow__node') || event.target.closest('.react-flow__edge')) &&
event.type !== 'wheel'
) {
return false;
}
// when the target element is a node selection, we still allow zooming
if (event.target.closest('.react-flow__nodesselection') && event.type !== 'wheel') {
// if the target element is inside an element with the nopan class, we prevent panning
if (isWrappedWithClass(event, noPanClassName) && event.type !== 'wheel') {
return false;
}
@@ -257,7 +250,7 @@ const ZoomPane = ({
}
// if the pane is not movable, we prevent dragging it with mousestart or touchstart
if (!paneMoveable && (event.type === 'mousedown' || event.type === 'touchstart')) {
if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) {
return false;
}
@@ -271,14 +264,14 @@ const ZoomPane = ({
zoomOnPinch,
panOnScroll,
zoomOnDoubleClick,
paneMoveable,
panOnDrag,
selectionKeyPressed,
elementsSelectable,
zoomActivationKeyPressed,
]);
return (
<div className="react-flow__renderer react-flow__zoompane" ref={zoomPane}>
<div className="react-flow__renderer react-flow__container" ref={zoomPane}>
{children}
</div>
);
+1 -3
View File
@@ -1,8 +1,6 @@
import { createContext } from 'react';
import { ElementId } from '../types';
type ContextProps = ElementId | null;
type ContextProps = string | null;
export const NodeIdContext = createContext<Partial<ContextProps>>(null);
export const Provider = NodeIdContext.Provider;
+1
View File
@@ -14,3 +14,4 @@ declare module '*.svg' {
declare var __REACT_FLOW_VERSION__: string;
declare var __ENV__: string;
declare var __INJECT_STYLES__: boolean;
+12
View File
@@ -0,0 +1,12 @@
import { useStore } from '../store';
import { Edge, ReactFlowState } from '../types';
const edgesSelector = (state: ReactFlowState) => state.edges;
function useEdges<EdgeData>(): Edge<EdgeData>[] {
const edges = useStore(edgesSelector);
return edges;
}
export default useEdges;
+76 -24
View File
@@ -1,44 +1,96 @@
import { useEffect } from 'react';
import shallow from 'zustand/shallow';
import { useStore, useStoreActions } from '../store/hooks';
import { useStore, useStoreApi } from '../store';
import useKeyPress from './useKeyPress';
import { isNode, getConnectedEdges } from '../utils/graph';
import { Elements, KeyCode, ElementId, FlowElement } from '../types';
import { getConnectedEdges } from '../utils/graph';
import { EdgeChange, KeyCode, NodeChange, Node, ReactFlowState } from '../types';
interface HookParams {
deleteKeyCode: KeyCode;
multiSelectionKeyCode: KeyCode;
onElementsRemove?: (elements: Elements) => void;
deleteKeyCode: KeyCode | null;
multiSelectionKeyCode: KeyCode | null;
}
export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => {
const store = useStore();
const selector = (s: ReactFlowState) => ({
onNodesChange: s.onNodesChange,
onEdgesChange: s.onEdgesChange,
});
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const setMultiSelectionActive = useStoreActions((actions) => actions.setMultiSelectionActive);
const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements);
export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
const store = useStoreApi();
const { onNodesChange, onEdgesChange } = useStore(selector, shallow);
const deleteKeyPressed = useKeyPress(deleteKeyCode);
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode);
useEffect(() => {
const { edges, selectedElements } = store.getState();
const { nodeInternals, edges, hasDefaultNodes, hasDefaultEdges, onNodesDelete, onEdgesDelete } = store.getState();
const nodes = Array.from(nodeInternals.values());
const nodesToRemove = nodes.reduce<Node[]>((res, node) => {
if (!node.selected && node.parentNode && res.find((n) => n.id === node.parentNode)) {
res.push(node);
} else if (node.selected) {
res.push(node);
}
if (onElementsRemove && deleteKeyPressed && selectedElements) {
const selectedNodes = selectedElements.filter(isNode);
const connectedEdges = getConnectedEdges(selectedNodes, edges);
const elementsToRemove = [...selectedElements, ...connectedEdges].reduce(
(res, item) => res.set(item.id, item),
new Map<ElementId, FlowElement>()
);
return res;
}, []);
const selectedEdges = edges.filter((e) => e.selected);
onElementsRemove(Array.from(elementsToRemove.values()));
unsetNodesSelection();
resetSelectedElements();
if (deleteKeyPressed && (nodesToRemove || selectedEdges)) {
const connectedEdges = getConnectedEdges(nodesToRemove, edges);
const edgesToRemove = [...selectedEdges, ...connectedEdges];
const edgeIdsToRemove = edgesToRemove.reduce<string[]>((res, edge) => {
if (!res.includes(edge.id)) {
res.push(edge.id);
}
return res;
}, []);
if (hasDefaultEdges || hasDefaultNodes) {
if (hasDefaultEdges) {
store.setState({
edges: edges.filter((e) => !edgeIdsToRemove.includes(e.id)),
});
}
if (hasDefaultNodes) {
nodesToRemove.forEach((node) => {
nodeInternals.delete(node.id);
});
store.setState({
nodeInternals: new Map(nodeInternals),
});
}
}
if (edgeIdsToRemove.length > 0) {
onEdgesDelete?.(edgesToRemove);
if (onEdgesChange) {
const edgeChanges: EdgeChange[] = edgeIdsToRemove.map((id) => ({
id,
type: 'remove',
}));
onEdgesChange(edgeChanges);
}
}
if (nodesToRemove.length > 0) {
onNodesDelete?.(nodesToRemove);
if (onNodesChange) {
const nodeChanges: NodeChange[] = nodesToRemove.map((n) => ({ id: n.id, type: 'remove' }));
onNodesChange(nodeChanges);
}
}
store.setState({ nodesSelectionActive: false });
}
}, [deleteKeyPressed]);
}, [deleteKeyPressed, onNodesChange, onEdgesChange]);
useEffect(() => {
setMultiSelectionActive(multiSelectionKeyPressed);
store.setState({ multiSelectionActive: multiSelectionKeyPressed });
}, [multiSelectionKeyPressed]);
};
+88 -14
View File
@@ -1,36 +1,85 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef, useMemo } from 'react';
import { isInputDOMNode } from '../utils';
import { KeyCode } from '../types';
export default (keyCode?: KeyCode): boolean => {
type Keys = Array<string>;
type PressedKeys = Set<string>;
type KeyOrCode = 'key' | 'code';
export interface UseKeyPressOptions {
target: Window | Document | HTMLElement | ShadowRoot | null;
}
const doc = typeof document !== 'undefined' ? document : null;
// the keycode can be a string 'a' or an array of strings ['a', 'a+d']
// a string means a single key 'a' or a combination when '+' is used 'a+d'
// an array means different possibilites. Explainer: ['a', 'd+s'] here the
// user can use the single key 'a' or the combination 'd' + 's'
export default (keyCode: KeyCode | null = null, options: UseKeyPressOptions = { target: doc }): boolean => {
const [keyPressed, setKeyPressed] = useState(false);
useEffect(() => {
if (typeof keyCode !== 'undefined') {
const downHandler = (event: KeyboardEvent) => {
if (!isInputDOMNode(event) && (event.key === keyCode || event.keyCode === keyCode)) {
event.preventDefault();
// we need to remember the pressed keys in order to support combinations
const pressedKeys = useRef<PressedKeys>(new Set([]));
// keyCodes = array with single keys [['a']] or key combinations [['a', 's']]
// keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft']
// used to check if we store event.code or event.key. When the code is in the list of keysToWatch
// we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft"
// and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when
// we can't find it in the list of keysToWatch.
const [keyCodes, keysToWatch] = useMemo<[Array<Keys>, Keys]>(() => {
if (keyCode !== null) {
const keyCodeArr = Array.isArray(keyCode) ? keyCode : [keyCode];
const keys = keyCodeArr.filter((kc) => typeof kc === 'string').map((kc) => kc.split('+'));
const keysFlat = keys.reduce((res: Keys, item) => res.concat(...item), []);
return [keys, keysFlat];
}
return [[], []];
}, [keyCode]);
useEffect(() => {
if (keyCode !== null) {
const downHandler = (event: KeyboardEvent) => {
if (isInputDOMNode(event)) {
return false;
}
const keyOrCode = useKeyOrCode(event.code, keysToWatch);
pressedKeys.current.add(event[keyOrCode]);
if (isMatchingKey(keyCodes, pressedKeys.current, false)) {
event.preventDefault();
setKeyPressed(true);
}
};
const upHandler = (event: KeyboardEvent) => {
if (!isInputDOMNode(event) && (event.key === keyCode || event.keyCode === keyCode)) {
if (isInputDOMNode(event)) {
return false;
}
const keyOrCode = useKeyOrCode(event.code, keysToWatch);
if (isMatchingKey(keyCodes, pressedKeys.current, true)) {
setKeyPressed(false);
pressedKeys.current.clear();
} else {
pressedKeys.current.delete(event[keyOrCode]);
}
};
const resetHandler = () => setKeyPressed(false);
const resetHandler = () => {
pressedKeys.current.clear();
setKeyPressed(false);
};
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
options?.target?.addEventListener('keydown', downHandler as EventListenerOrEventListenerObject);
options?.target?.addEventListener('keyup', upHandler as EventListenerOrEventListenerObject);
window.addEventListener('blur', resetHandler);
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
options?.target?.removeEventListener('keydown', downHandler as EventListenerOrEventListenerObject);
options?.target?.removeEventListener('keyup', upHandler as EventListenerOrEventListenerObject);
window.removeEventListener('blur', resetHandler);
};
}
@@ -38,3 +87,28 @@ export default (keyCode?: KeyCode): boolean => {
return keyPressed;
};
// utils
function isMatchingKey(keyCodes: Array<Keys>, pressedKeys: PressedKeys, isUp: boolean): boolean {
return (
keyCodes
// we only want to compare same sizes of keyCode definitions
// and pressed keys. When the user specified 'Meta' as a key somewhere
// this would also be truthy without this filter when user presses 'Meta' + 'r'
.filter((keys) => isUp || keys.length === pressedKeys.size)
// since we want to support multiple possibilities only one of the
// combinations need to be part of the pressed keys
.some((keys) => keys.every((k) => pressedKeys.has(k)))
);
}
function useKeyOrCode(eventCode: string, keysToWatch: KeyCode): KeyOrCode {
return keysToWatch.includes(eventCode) ? 'code' : 'key';
}
function isInputDOMNode(event: KeyboardEvent): boolean {
const target = event.target as HTMLElement;
return ['INPUT', 'SELECT', 'TEXTAREA'].includes(target?.nodeName) || target?.hasAttribute('contenteditable');
}
+12
View File
@@ -0,0 +1,12 @@
import { useStore } from '../store';
import { Node, ReactFlowState } from '../types';
const nodesSelector = (state: ReactFlowState) => Array.from(state.nodeInternals.values());
function useNodes<NodeData>(): Node<NodeData>[] {
const nodes = useStore(nodesSelector);
return nodes;
}
export default useNodes;
+34
View File
@@ -0,0 +1,34 @@
import { useState, useCallback, SetStateAction, Dispatch } from 'react';
import { applyNodeChanges, applyEdgeChanges } from '../utils/changes';
import { Node, NodeChange, Edge, EdgeChange } from '../types';
type ApplyChanges<ItemType, ChangesType> = (changes: ChangesType[], items: ItemType[]) => ItemType[];
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
// returns a hook that can be used liked this:
// const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
function createUseItemsState(
applyChanges: ApplyChanges<Node, NodeChange>
): <NodeData = any>(
initialItems: Node<NodeData>[]
) => [Node<NodeData>[], Dispatch<SetStateAction<Node<NodeData>[]>>, OnChange<NodeChange>];
function createUseItemsState(
applyChanges: ApplyChanges<Edge, EdgeChange>
): <EdgeData = any>(
initialItems: Edge<EdgeData>[]
) => [Edge<EdgeData>[], Dispatch<SetStateAction<Edge<EdgeData>[]>>, OnChange<EdgeChange>];
function createUseItemsState(
applyChanges: ApplyChanges<any, any>
): (initialItems: any[]) => [any[], Dispatch<SetStateAction<any[]>>, OnChange<any>] {
return (initialItems: any[]) => {
const [items, setItems] = useState(initialItems);
const onItemsChange = useCallback((changes: any[]) => setItems((items: any) => applyChanges(changes, items)), []);
return [items, setItems, onItemsChange];
};
}
export const useNodesState = createUseItemsState(applyNodeChanges);
export const useEdgesState = createUseItemsState(applyEdgeChanges);
+18
View File
@@ -0,0 +1,18 @@
import { useEffect, useRef } from 'react';
import useReactFlow from './useReactFlow';
import { OnInit } from '../types';
function useOnInitHandler(onInit: OnInit<any> | undefined) {
const ReactFlowInstance = useReactFlow();
const isInitialized = useRef<boolean>(false);
useEffect(() => {
if (!isInitialized.current && ReactFlowInstance.viewportInitialized && onInit) {
setTimeout(() => onInit(ReactFlowInstance), 1);
isInitialized.current = true;
}
}, [onInit, ReactFlowInstance.viewportInitialized]);
}
export default useOnInitHandler;
+111
View File
@@ -0,0 +1,111 @@
import { useCallback } from 'react';
import useViewportHelper from './useViewportHelper';
import { useStoreApi } from '../store';
import { ReactFlowInstance, Instance, NodeAddChange, EdgeAddChange, NodeResetChange, EdgeResetChange } from '../types';
export default function useReactFlow<NodeData = any, EdgeData = any>(): 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, hasDefaultNodes, onNodesChange } = store.getState();
const nodes = Array.from(nodeInternals.values());
const nextNodes = typeof payload === 'function' ? payload(nodes) : payload;
if (hasDefaultNodes) {
setNodes(nextNodes);
} else if (onNodesChange) {
const changes = nextNodes.map((node) => ({ item: node, type: 'reset' } as NodeResetChange<NodeData>));
onNodesChange(changes);
}
}, []);
const setEdges = useCallback<Instance.SetEdges<EdgeData>>((payload) => {
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange } = store.getState();
const nextEdges = typeof payload === 'function' ? payload(edges) : payload;
if (hasDefaultEdges) {
setEdges(nextEdges);
} else if (onEdgesChange) {
const changes = nextEdges.map((edge) => ({ item: edge, type: 'reset' } as EdgeResetChange<EdgeData>));
onEdgesChange(changes);
}
}, []);
const addNodes = useCallback<Instance.AddNodes<NodeData>>((payload) => {
const nodes = Array.isArray(payload) ? payload : [payload];
const { nodeInternals, setNodes, hasDefaultNodes, onNodesChange } = store.getState();
if (hasDefaultNodes) {
const currentNodes = Array.from(nodeInternals.values());
const nextNodes = [...currentNodes, ...nodes];
setNodes(nextNodes);
} else if (onNodesChange) {
const changes = nodes.map((node) => ({ item: node, type: 'add' } as NodeAddChange<NodeData>));
onNodesChange(changes);
}
}, []);
const addEdges = useCallback<Instance.AddEdges<EdgeData>>((payload) => {
const nextEdges = Array.isArray(payload) ? payload : [payload];
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange } = store.getState();
if (hasDefaultEdges) {
setEdges([...edges, ...nextEdges]);
} else if (onEdgesChange) {
const changes = nextEdges.map((edge) => ({ item: edge, type: 'add' } as EdgeAddChange<EdgeData>));
onEdgesChange(changes);
}
}, []);
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,
};
}
+7 -5
View File
@@ -1,10 +1,10 @@
import { useEffect, MutableRefObject } from 'react';
import { useStoreActions } from '../store/hooks';
import { useStoreApi } from '../store';
import { getDimensions } from '../utils';
export default (rendererNode: MutableRefObject<HTMLDivElement | null>) => {
const updateSize = useStoreActions((actions) => actions.updateSize);
function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>): void {
const store = useStoreApi();
useEffect(() => {
let resizeObserver: ResizeObserver;
@@ -20,7 +20,7 @@ export default (rendererNode: MutableRefObject<HTMLDivElement | null>) => {
console.warn('The React Flow parent container needs a width and a height to render the graph.');
}
updateSize(size);
store.setState({ width: size.width || 500, height: size.height || 500 });
};
updateDimensions();
@@ -39,4 +39,6 @@ export default (rendererNode: MutableRefObject<HTMLDivElement | null>) => {
}
};
}, []);
};
}
export default useResizeHandler;
+7 -5
View File
@@ -1,13 +1,15 @@
import { useCallback } from 'react';
import { useStoreActions } from '../store/hooks';
import { ElementId, UpdateNodeInternals } from '../types';
import { useStore } from '../store';
import { UpdateNodeInternals, ReactFlowState } from '../types';
const selector = (state: ReactFlowState) => state.updateNodeDimensions;
function useUpdateNodeInternals(): UpdateNodeInternals {
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
const updateNodeDimensions = useStore(selector);
return useCallback<UpdateNodeInternals>((id: ElementId) => {
const nodeElement = document.querySelector(`.react-flow__node[data-id="${id}"]`);
return useCallback<UpdateNodeInternals>((id: string) => {
const nodeElement = document.querySelector(`.react-flow__node[data-id="${id}"]`) as HTMLDivElement;
if (nodeElement) {
updateNodeDimensions([{ id, nodeElement, forceUpdate: true }]);
+18
View File
@@ -0,0 +1,18 @@
import shallow from 'zustand/shallow';
import { useStore } from '../store';
import { Viewport, ReactFlowState } from '../types';
const viewportSelector = (state: ReactFlowState) => ({
x: state.transform[0],
y: state.transform[1],
zoom: state.transform[2],
});
function useViewport(): Viewport {
const viewport = useStore(viewportSelector, shallow);
return viewport;
}
export default useViewport;
+84
View File
@@ -0,0 +1,84 @@
import { useMemo } from 'react';
import { zoomIdentity } from 'd3-zoom';
import shallow from 'zustand/shallow';
import { useStoreApi, useStore } from '../store';
import { pointToRendererPoint, getTransformForBounds, getD3Transition } from '../utils/graph';
import { FitViewOptions, Viewport, ViewportHelperFunctions, ReactFlowState, Rect, XYPosition } from '../types';
import { fitView as fitViewStore } from '../store/utils';
const DEFAULT_PADDING = 0.1;
const initialViewportHelper: ViewportHelperFunctions = {
zoomIn: () => {},
zoomOut: () => {},
zoomTo: (_: number) => {},
getZoom: () => 1,
setViewport: (_: Viewport) => {},
getViewport: () => ({ x: 0, y: 0, zoom: 1 }),
fitView: (_: FitViewOptions = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {},
setCenter: (_: number, __: number) => {},
fitBounds: (_: Rect) => {},
project: (position: XYPosition) => position,
initialized: false,
};
const selector = (s: ReactFlowState) => ({
d3Zoom: s.d3Zoom,
d3Selection: s.d3Selection,
});
const useViewportHelper = (): ViewportHelperFunctions => {
const store = useStoreApi();
const { d3Zoom, d3Selection } = useStore(selector, shallow);
const viewportHelperFunctions = useMemo<ViewportHelperFunctions>(() => {
if (d3Selection && d3Zoom) {
return {
zoomIn: (options) => d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1.2),
zoomOut: (options) => d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1 / 1.2),
zoomTo: (zoomLevel, options) => d3Zoom.scaleTo(getD3Transition(d3Selection, options?.duration), zoomLevel),
getZoom: () => {
const [, , zoom] = store.getState().transform;
return zoom;
},
setViewport: (transform, options) => {
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom);
d3Zoom.transform(getD3Transition(d3Selection, options?.duration), nextTransform);
},
getViewport: () => {
const [x, y, zoom] = store.getState().transform;
return { x, y, zoom };
},
fitView: (options) => fitViewStore(store.getState, options),
setCenter: (x, y, options) => {
const { width, height, maxZoom } = store.getState();
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom;
const centerX = width / 2 - x * nextZoom;
const centerY = height / 2 - y * nextZoom;
const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom);
d3Zoom.transform(getD3Transition(d3Selection, options?.duration), transform);
},
fitBounds: (bounds, options) => {
const { width, height, minZoom, maxZoom } = store.getState();
const [x, y, zoom] = getTransformForBounds(bounds, width, height, minZoom, maxZoom, options?.padding ?? 0.1);
const transform = zoomIdentity.translate(x, y).scale(zoom);
d3Zoom.transform(getD3Transition(d3Selection, options?.duration), transform);
},
project: (position: XYPosition) => {
const { transform, snapToGrid, snapGrid } = store.getState();
return pointToRendererPoint(position, transform, snapToGrid, snapGrid);
},
initialized: true,
};
}
return initialViewportHelper;
}, [d3Zoom, d3Selection]);
return viewportHelperFunctions;
};
export default useViewportHelper;
+83
View File
@@ -0,0 +1,83 @@
import { useCallback } from 'react';
import { useStore } from '../store';
import { isEdgeVisible } from '../container/EdgeRenderer/utils';
import { ReactFlowState, NodeInternals, Edge } from '../types';
import { isNumeric } from '../utils';
const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }];
function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals) {
let maxLevel = -1;
const levelLookup = edges.reduce<Record<string, Edge[]>>((tree, edge) => {
const z = isNumeric(edge.zIndex)
? edge.zIndex
: Math.max(nodeInternals.get(edge.source)?.z || 0, nodeInternals.get(edge.target)?.z || 0);
if (tree[z]) {
tree[z].push(edge);
} else {
tree[z] = [edge];
}
maxLevel = z > maxLevel ? z : maxLevel;
return tree;
}, {});
const edgeTree = Object.entries(levelLookup).map(([key, edges]) => {
const level = +key;
return {
edges,
level,
isMaxLevel: level === maxLevel,
};
});
if (edgeTree.length === 0) {
return defaultEdgeTree;
}
return edgeTree;
}
function useVisibleEdges(onlyRenderVisible: boolean, nodeInternals: NodeInternals) {
const edges = useStore(
useCallback(
(s: ReactFlowState) => {
if (!onlyRenderVisible) {
return s.edges;
}
return s.edges.filter((e) => {
const sourceNode = nodeInternals.get(e.source);
const targetNode = nodeInternals.get(e.target);
return (
sourceNode?.width &&
sourceNode?.height &&
targetNode?.width &&
targetNode?.height &&
isEdgeVisible({
sourcePos: sourceNode.position || { x: 0, y: 0 },
targetPos: targetNode.position || { x: 0, y: 0 },
sourceWidth: sourceNode.width,
sourceHeight: sourceNode.height,
targetWidth: targetNode.width,
targetHeight: targetNode.height,
width: s.width,
height: s.height,
transform: s.transform,
})
);
});
},
[onlyRenderVisible, nodeInternals]
)
);
return groupEdgesByZLevel(edges, nodeInternals);
}
export default useVisibleEdges;
+22
View File
@@ -0,0 +1,22 @@
import { useCallback } from 'react';
import { useStore } from '../store';
import { getNodesInside } from '../utils/graph';
import { ReactFlowState } from '../types';
function useVisibleNodes(onlyRenderVisible: boolean) {
const nodes = useStore(
useCallback(
(s: ReactFlowState) => {
return onlyRenderVisible
? getNodesInside(s.nodeInternals, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true)
: Array.from(s.nodeInternals).map(([_, node]) => node);
},
[onlyRenderVisible]
)
);
return nodes;
}
export default useVisibleNodes;
-94
View File
@@ -1,94 +0,0 @@
import { useMemo } from 'react';
import { zoomIdentity } from 'd3-zoom';
import { Selection as D3Selection } from 'd3';
import { useStoreState, useStore } from '../store/hooks';
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '../utils/graph';
import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, XYPosition } from '../types';
const DEFAULT_PADDING = 0.1;
const initialZoomPanHelper: ZoomPanHelperFunctions = {
zoomIn: () => {},
zoomOut: () => {},
zoomTo: (_: number) => {},
transform: (_: FlowTransform) => {},
fitView: (_: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {},
setCenter: (_: number, __: number) => {},
fitBounds: (_: Rect) => {},
project: (position: XYPosition) => position,
initialized: false,
};
const getTransition = (selection: D3Selection<Element, unknown, null, undefined>, duration: number = 0) => {
return selection.transition().duration(duration);
};
const useZoomPanHelper = (): ZoomPanHelperFunctions => {
const store = useStore();
const d3Zoom = useStoreState((s) => s.d3Zoom);
const d3Selection = useStoreState((s) => s.d3Selection);
const zoomPanHelperFunctions = useMemo<ZoomPanHelperFunctions>(() => {
if (d3Selection && d3Zoom) {
return {
zoomIn: (duration?: number) => d3Zoom.scaleBy(getTransition(d3Selection, duration), 1.2),
zoomOut: (duration?: number) => d3Zoom.scaleBy(getTransition(d3Selection, duration), 1 / 1.2),
zoomTo: (zoomLevel: number, duration?: number) =>
d3Zoom.scaleTo(getTransition(d3Selection, duration), zoomLevel),
transform: (transform: FlowTransform, duration?: number) => {
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom);
d3Zoom.transform(getTransition(d3Selection, duration), nextTransform);
},
fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false, duration: 0 }) => {
const { nodes, width, height, minZoom, maxZoom } = store.getState();
if (!nodes.length) {
return;
}
const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.isHidden));
const [x, y, zoom] = getTransformForBounds(
bounds,
width,
height,
options.minZoom ?? minZoom,
options.maxZoom ?? maxZoom,
options.padding ?? DEFAULT_PADDING
);
const transform = zoomIdentity.translate(x, y).scale(zoom);
d3Zoom.transform(getTransition(d3Selection, options.duration), transform);
},
setCenter: (x: number, y: number, zoom?: number, duration?: number) => {
const { width, height, maxZoom } = store.getState();
const nextZoom = typeof zoom !== 'undefined' ? zoom : maxZoom;
const centerX = width / 2 - x * nextZoom;
const centerY = height / 2 - y * nextZoom;
const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom);
d3Zoom.transform(getTransition(d3Selection, duration), transform);
},
fitBounds: (bounds: Rect, padding = DEFAULT_PADDING, duration?: number) => {
const { width, height, minZoom, maxZoom } = store.getState();
const [x, y, zoom] = getTransformForBounds(bounds, width, height, minZoom, maxZoom, padding);
const transform = zoomIdentity.translate(x, y).scale(zoom);
d3Zoom.transform(getTransition(d3Selection, duration), transform);
},
project: (position: XYPosition) => {
const { transform, snapToGrid, snapGrid } = store.getState();
return pointToRendererPoint(position, transform, snapToGrid, snapGrid);
},
initialized: true,
};
}
return initialZoomPanHelper;
}, [d3Zoom, d3Selection]);
return zoomPanHelperFunctions;
};
export default useZoomPanHelper;
+21 -11
View File
@@ -6,14 +6,22 @@ export { default as Handle } from './components/Handle';
export { default as EdgeText } from './components/Edges/EdgeText';
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 BezierEdge,
getBezierPath,
getBezierCenter as getBezierEdgeCenter,
} from './components/Edges/BezierEdge';
export {
default as SimpleBezierEdge,
getSimpleBezierPath,
getSimpleBezierCenter as getSimpleBezierEdgeCenter,
} from './components/Edges/SimpleBezierEdge';
export { default as SmoothStepEdge, getSmoothStepPath } from './components/Edges/SmoothStepEdge';
export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils';
export * from './additional-components';
export {
isNode,
isEdge,
removeElements,
addEdge,
getOutgoers,
getIncomers,
@@ -22,14 +30,16 @@ export {
getTransformForBounds,
getRectOfNodes,
} from './utils/graph';
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper';
export { applyNodeChanges, applyEdgeChanges } from './utils/changes';
export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils';
export { default as useReactFlow } from './hooks/useReactFlow';
export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals';
export { default as useNodes } from './hooks/useNodes';
export { default as useEdges } from './hooks/useEdges';
export { default as useViewport } from './hooks/useViewport';
export { default as useKeyPress } from './hooks/useKeyPress';
export * from './hooks/useNodesEdgesState';
export { useStore, useStoreApi } from './store';
export * from './additional-components';
export * from './store/hooks';
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';
-163
View File
@@ -1,163 +0,0 @@
import { createAction } from './utils';
import {
Elements,
OnConnectEndFunc,
OnConnectFunc,
OnConnectStartFunc,
OnConnectStopFunc,
NodeDimensionUpdate,
NodePosUpdate,
NodeDiffUpdate,
XYPosition,
Transform,
Dimensions,
InitD3ZoomPayload,
TranslateExtent,
SetConnectionId,
SnapGrid,
ConnectionMode,
NodeExtent,
} from '../types';
import * as constants from './contants';
export const setOnConnect = (onConnect: OnConnectFunc) =>
createAction(constants.SET_ON_CONNECT, {
onConnect,
});
export const setOnConnectStart = (onConnectStart: OnConnectStartFunc) =>
createAction(constants.SET_ON_CONNECT_START, {
onConnectStart,
});
export const setOnConnectStop = (onConnectStop: OnConnectStopFunc) =>
createAction(constants.SET_ON_CONNECT_STOP, {
onConnectStop,
});
export const setOnConnectEnd = (onConnectEnd: OnConnectEndFunc) =>
createAction(constants.SET_ON_CONNECT_END, {
onConnectEnd,
});
export const setElements = (elements: Elements) => createAction(constants.SET_ELEMENTS, elements);
export const updateNodeDimensions = (updates: NodeDimensionUpdate[]) =>
createAction(constants.UPDATE_NODE_DIMENSIONS, updates);
export const updateNodePos = (payload: NodePosUpdate) => createAction(constants.UPDATE_NODE_POS, payload);
export const updateNodePosDiff = (payload: NodeDiffUpdate) => createAction(constants.UPDATE_NODE_POS_DIFF, payload);
export const setUserSelection = (mousePos: XYPosition) => createAction(constants.SET_USER_SELECTION, mousePos);
export const updateUserSelection = (mousePos: XYPosition) => createAction(constants.UPDATE_USER_SELECTION, mousePos);
export const unsetUserSelection = () => createAction(constants.UNSET_USER_SELECTION);
export const setSelection = (selectionActive: boolean) =>
createAction(constants.SET_SELECTION, {
selectionActive,
});
export const unsetNodesSelection = () =>
createAction(constants.UNSET_NODES_SELECTION, {
nodesSelectionActive: false,
});
export const resetSelectedElements = () =>
createAction(constants.RESET_SELECTED_ELEMENTS, {
selectedElements: null,
});
export const setSelectedElements = (elements: Elements) => createAction(constants.SET_SELECTED_ELEMENTS, elements);
export const addSelectedElements = (elements: Elements) => createAction(constants.ADD_SELECTED_ELEMENTS, elements);
export const updateTransform = (transform: Transform) => createAction(constants.UPDATE_TRANSFORM, { transform });
export const updateSize = (size: Dimensions) =>
createAction(constants.UPDATE_SIZE, {
width: size.width || 500,
height: size.height || 500,
});
export const initD3Zoom = (payload: InitD3ZoomPayload) => createAction(constants.INIT_D3ZOOM, payload);
export const setMinZoom = (minZoom: number) => createAction(constants.SET_MINZOOM, minZoom);
export const setMaxZoom = (maxZoom: number) => createAction(constants.SET_MAXZOOM, maxZoom);
export const setTranslateExtent = (translateExtent: TranslateExtent) =>
createAction(constants.SET_TRANSLATEEXTENT, translateExtent);
export const setConnectionPosition = (connectionPosition: XYPosition) =>
createAction(constants.SET_CONNECTION_POSITION, { connectionPosition });
export const setConnectionNodeId = (payload: SetConnectionId) => createAction(constants.SET_CONNECTION_NODEID, payload);
export const setSnapToGrid = (snapToGrid: boolean) => createAction(constants.SET_SNAPTOGRID, { snapToGrid });
export const setSnapGrid = (snapGrid: SnapGrid) => createAction(constants.SET_SNAPGRID, { snapGrid });
export const setInteractive = (isInteractive: boolean) =>
createAction(constants.SET_INTERACTIVE, {
nodesDraggable: isInteractive,
nodesConnectable: isInteractive,
elementsSelectable: isInteractive,
});
export const setNodesDraggable = (nodesDraggable: boolean) =>
createAction(constants.SET_NODES_DRAGGABLE, { nodesDraggable });
export const setNodesConnectable = (nodesConnectable: boolean) =>
createAction(constants.SET_NODES_CONNECTABLE, { nodesConnectable });
export const setElementsSelectable = (elementsSelectable: boolean) =>
createAction(constants.SET_ELEMENTS_SELECTABLE, { elementsSelectable });
export const setMultiSelectionActive = (multiSelectionActive: boolean) =>
createAction(constants.SET_MULTI_SELECTION_ACTIVE, { multiSelectionActive });
export const setConnectionMode = (connectionMode: ConnectionMode) =>
createAction(constants.SET_CONNECTION_MODE, { connectionMode });
export const setNodeExtent = (nodeExtent: NodeExtent) => createAction(constants.SET_NODE_EXTENT, nodeExtent);
export type ReactFlowAction = ReturnType<
| typeof setOnConnect
| typeof setOnConnectStart
| typeof setOnConnectStop
| typeof setOnConnectEnd
| typeof setElements
| typeof updateNodeDimensions
| typeof updateNodePos
| typeof updateNodePosDiff
| typeof setUserSelection
| typeof updateUserSelection
| typeof unsetUserSelection
| typeof setSelection
| typeof unsetNodesSelection
| typeof resetSelectedElements
| typeof setSelectedElements
| typeof addSelectedElements
| typeof updateTransform
| typeof updateSize
| typeof initD3Zoom
| typeof setMinZoom
| typeof setMaxZoom
| typeof setTranslateExtent
| typeof setConnectionPosition
| typeof setConnectionNodeId
| typeof setSnapToGrid
| typeof setSnapGrid
| typeof setInteractive
| typeof setNodesDraggable
| typeof setNodesConnectable
| typeof setElementsSelectable
| typeof setMultiSelectionActive
| typeof setConnectionMode
| typeof setNodeExtent
>;
-10
View File
@@ -1,10 +0,0 @@
import { createStore, Store } from 'redux';
import { ReactFlowState } from '../types';
import { ReactFlowAction } from './actions';
import reactFlowReducer from './reducer';
export default function configureStore(preloadedState: ReactFlowState): Store<ReactFlowState, ReactFlowAction> {
const store = createStore(reactFlowReducer, preloadedState);
return store;
}
-33
View File
@@ -1,33 +0,0 @@
export const SET_ON_CONNECT = 'SET_ON_CONNECT';
export const SET_ON_CONNECT_START = 'SET_ON_CONNECT_START';
export const SET_ON_CONNECT_STOP = 'SET_ON_CONNECT_STOP';
export const SET_ON_CONNECT_END = 'SET_ON_CONNECT_END';
export const SET_ELEMENTS = 'SET_ELEMENTS';
export const UPDATE_NODE_DIMENSIONS = 'UPDATE_NODE_DIMENSIONS';
export const UPDATE_NODE_POS = 'UPDATE_NODE_POS';
export const UPDATE_NODE_POS_DIFF = 'UPDATE_NODE_POS_DIFF';
export const SET_USER_SELECTION = 'SET_USER_SELECTION';
export const UPDATE_USER_SELECTION = 'UPDATE_USER_SELECTION';
export const UNSET_USER_SELECTION = 'UNSET_USER_SELECTION';
export const SET_SELECTION = 'SET_SELECTION';
export const UNSET_NODES_SELECTION = 'UNSET_NODES_SELECTION';
export const SET_SELECTED_ELEMENTS = 'SET_SELECTED_ELEMENTS';
export const RESET_SELECTED_ELEMENTS = 'RESET_SELECTED_ELEMENTS';
export const ADD_SELECTED_ELEMENTS = 'ADD_SELECTED_ELEMENTS';
export const UPDATE_TRANSFORM = 'UPDATE_TRANSFORM';
export const UPDATE_SIZE = 'UPDATE_SIZE';
export const INIT_D3ZOOM = 'INIT_D3ZOOM';
export const SET_MINZOOM = 'SET_MINZOOM';
export const SET_MAXZOOM = 'SET_MAXZOOM';
export const SET_TRANSLATEEXTENT = 'SET_TRANSLATEEXTENT';
export const SET_CONNECTION_POSITION = 'SET_CONNECTION_POSITION';
export const SET_CONNECTION_NODEID = 'SET_CONNECTION_NODEID';
export const SET_SNAPTOGRID = 'SET_SNAPTOGRID';
export const SET_SNAPGRID = 'SET_SNAPGRID';
export const SET_INTERACTIVE = 'SET_INTERACTIVE';
export const SET_NODES_DRAGGABLE = 'SET_NODES_DRAGGABLE';
export const SET_NODES_CONNECTABLE = 'SET_NODES_CONNECTABLE';
export const SET_ELEMENTS_SELECTABLE = 'SET_ELEMENTS_SELECTABLE';
export const SET_MULTI_SELECTION_ACTIVE = 'SET_MULTI_SELECTION_ACTIVE';
export const SET_CONNECTION_MODE = 'SET_CONNECTION_MODE';
export const SET_NODE_EXTENT = 'SET_NODE_EXTENT';
-48
View File
@@ -1,48 +0,0 @@
import { bindActionCreators, Store, ActionCreator, ActionCreatorsMapObject } from 'redux';
import {
useStore as useStoreRedux,
useSelector,
useDispatch as reduxUseDispatch,
TypedUseSelectorHook,
} from 'react-redux';
import { useMemo } from 'react';
import { ReactFlowDispatch } from './index';
import * as actions from './actions';
import { ReactFlowAction } from './actions';
import { ReactFlowState } from '../types';
export const useTypedSelector: TypedUseSelectorHook<ReactFlowState> = useSelector;
export type ActionCreatorSelector<Action> = (acts: typeof actions) => ActionCreator<Action>;
export type ActionMapObjectSelector<Action> = (acts: typeof actions) => ActionCreatorsMapObject<Action>;
export type ActionSelector<Action> = (acts: typeof actions) => ActionCreatorsMapObject<Action> | ActionCreator<Action>;
export function useStoreActions<Action extends ReactFlowAction>(
actionSelector: ActionCreatorSelector<Action>
): ActionCreator<Action>;
export function useStoreActions<Action extends ReactFlowAction>(
actionSelector: ActionMapObjectSelector<Action>
): ActionCreatorsMapObject<Action>;
export function useStoreActions<Action extends ReactFlowAction>(actionSelector: ActionSelector<Action>) {
const dispatch: ReactFlowDispatch = reduxUseDispatch();
const currAction = actionSelector(actions);
const action = useMemo(() => {
// this looks weird but required if both ActionSelector and ActionMapObjectSelector are supported
return typeof currAction === 'function'
? bindActionCreators(currAction, dispatch)
: bindActionCreators(currAction, dispatch);
}, [dispatch, currAction]);
return action;
}
export const useStoreState = useTypedSelector;
export const useStore = (): Store<ReactFlowState, ReactFlowAction> => {
const store = useStoreRedux<ReactFlowState, ReactFlowAction>();
return store;
};
export const useDispatch: ReactFlowDispatch = reduxUseDispatch;
+274 -51
View File
@@ -1,64 +1,287 @@
import { Store } from 'redux';
import configureStore from './configure-store';
import create from 'zustand';
import createContext from 'zustand/context';
import { ReactFlowState, ConnectionMode } from '../types';
import { clampPosition, getDimensions } from '../utils';
import { applyNodeChanges } from '../utils/changes';
export const initialState: ReactFlowState = {
width: 0,
height: 0,
transform: [0, 0, 1],
nodes: [],
edges: [],
selectedElements: null,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
import {
ReactFlowState,
Node,
Edge,
NodeDimensionUpdate,
NodeDiffUpdate,
CoordinateExtent,
NodeDimensionChange,
EdgeSelectionChange,
NodeSelectionChange,
NodePositionChange,
} from '../types';
import { getHandleBounds } from '../components/Nodes/utils';
import { createSelectionChange, getSelectionChanges } from '../utils/changes';
import {
createNodeInternals,
createPositionChange,
handleControlledEdgeSelectionChange,
handleControlledNodeSelectionChange,
isParentSelected,
fitView,
} from './utils';
import initialState from './initialState';
d3Zoom: null,
d3Selection: null,
d3ZoomHandler: undefined,
minZoom: 0.5,
maxZoom: 2,
translateExtent: [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
],
const { Provider, useStore, useStoreApi } = createContext<ReactFlowState>();
nodeExtent: [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
],
const createStore = () =>
create<ReactFlowState>((set, get) => ({
...initialState,
setNodes: (nodes: Node[]) => {
set({ nodeInternals: createNodeInternals(nodes, get().nodeInternals) });
},
setEdges: (edges: Edge[]) => {
const { defaultEdgeOptions } = get();
nodesSelectionActive: false,
selectionActive: false,
if (defaultEdgeOptions) {
set({ edges: edges.map((e) => ({ ...defaultEdgeOptions, ...e })) });
} else {
set({ edges });
}
},
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => {
const hasDefaultNodes = typeof nodes !== 'undefined';
const hasDefaultEdges = typeof edges !== 'undefined';
userSelectionRect: {
startX: 0,
startY: 0,
x: 0,
y: 0,
width: 0,
height: 0,
draw: false,
},
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: 'source',
connectionPosition: { x: 0, y: 0 },
connectionMode: ConnectionMode.Strict,
const nodeInternals = hasDefaultNodes ? createNodeInternals(nodes, new Map()) : new Map();
const nextEdges = hasDefaultEdges ? edges : [];
snapGrid: [15, 15],
snapToGrid: false,
set({ nodeInternals, edges: nextEdges, hasDefaultNodes, hasDefaultEdges });
},
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => {
const { onNodesChange, transform, nodeInternals, fitViewOnInit, fitViewOnInitDone, fitViewOnInitOptions } = get();
nodesDraggable: true,
nodesConnectable: true,
elementsSelectable: true,
const changes: NodeDimensionChange[] = updates.reduce<NodeDimensionChange[]>((res, update) => {
const node = nodeInternals.get(update.id);
multiSelectionActive: false,
if (node) {
const dimensions = getDimensions(update.nodeElement);
const doUpdate = !!(
dimensions.width &&
dimensions.height &&
(node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate)
);
reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-',
};
if (doUpdate) {
const handleBounds = getHandleBounds(update.nodeElement, transform[2]);
nodeInternals.set(node.id, {
...node,
handleBounds,
...dimensions,
});
const store: Store = configureStore(initialState);
res.push({
id: node.id,
type: 'dimensions',
dimensions,
});
}
}
export type ReactFlowDispatch = typeof store.dispatch;
return res;
}, []);
export default store;
const nextFitViewOnInitDone =
fitViewOnInitDone ||
(fitViewOnInit && !fitViewOnInitDone && fitView(get, { initial: true, ...fitViewOnInitOptions }));
set({ nodeInternals: new Map(nodeInternals), fitViewOnInitDone: nextFitViewOnInitDone });
if (changes?.length > 0) {
onNodesChange?.(changes);
}
},
updateNodePosition: ({ id, diff, dragging }: NodeDiffUpdate) => {
const { onNodesChange, nodeExtent, nodeInternals, hasDefaultNodes } = get();
if (hasDefaultNodes || onNodesChange) {
const changes: NodePositionChange[] = [];
nodeInternals.forEach((node) => {
if (node.selected) {
if (!node.parentNode || !isParentSelected(node, nodeInternals)) {
changes.push(createPositionChange({ node, diff, dragging, nodeExtent, nodeInternals }));
}
} else if (node.id === id) {
changes.push(createPositionChange({ node, diff, dragging, nodeExtent, nodeInternals }));
}
});
if (changes?.length) {
if (hasDefaultNodes) {
const nodes = applyNodeChanges(changes, Array.from(nodeInternals.values()));
const nextNodeInternals = createNodeInternals(nodes, nodeInternals);
set({ nodeInternals: nextNodeInternals });
}
onNodesChange?.(changes);
}
}
},
// @TODO: can we unify addSelectedNodes and addSelectedEdges somehow?
addSelectedNodes: (selectedNodeIds: string[]) => {
const {
multiSelectionActive,
onNodesChange,
nodeInternals,
hasDefaultNodes,
onEdgesChange,
hasDefaultEdges,
edges,
} = get();
let changedNodes: NodeSelectionChange[];
let changedEdges: EdgeSelectionChange[] | null = null;
if (multiSelectionActive) {
changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[];
} else {
changedNodes = getSelectionChanges(Array.from(nodeInternals.values()), selectedNodeIds);
changedEdges = getSelectionChanges(edges, []);
}
if (changedNodes.length) {
if (hasDefaultNodes) {
set({ nodeInternals: handleControlledNodeSelectionChange(changedNodes, nodeInternals) });
}
onNodesChange?.(changedNodes);
}
if (changedEdges?.length) {
if (hasDefaultEdges) {
set({ edges: handleControlledEdgeSelectionChange(changedEdges, edges) });
}
onEdgesChange?.(changedEdges);
}
},
addSelectedEdges: (selectedEdgeIds: string[]) => {
const {
multiSelectionActive,
onEdgesChange,
edges,
hasDefaultEdges,
nodeInternals,
hasDefaultNodes,
onNodesChange,
} = get();
let changedEdges: EdgeSelectionChange[];
let changedNodes: NodeSelectionChange[] | null = null;
if (multiSelectionActive) {
changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[];
} else {
changedEdges = getSelectionChanges(edges, selectedEdgeIds);
changedNodes = getSelectionChanges(Array.from(nodeInternals.values()), []);
}
if (changedEdges.length) {
if (hasDefaultEdges) {
set({
edges: handleControlledEdgeSelectionChange(changedEdges, edges),
});
}
onEdgesChange?.(changedEdges);
}
if (changedNodes?.length) {
if (hasDefaultNodes) {
set({ nodeInternals: handleControlledNodeSelectionChange(changedNodes, nodeInternals) });
}
onNodesChange?.(changedNodes);
}
},
unselectNodesAndEdges: () => {
const { nodeInternals, edges, onNodesChange, onEdgesChange, hasDefaultNodes, hasDefaultEdges } = get();
const nodes = Array.from(nodeInternals.values());
const nodesToUnselect = nodes.map((n) => {
n.selected = false;
return createSelectionChange(n.id, false);
}) as NodeSelectionChange[];
const edgesToUnselect = edges.map((edge) => createSelectionChange(edge.id, false)) as EdgeSelectionChange[];
if (nodesToUnselect.length) {
if (hasDefaultNodes) {
set({ nodeInternals: handleControlledNodeSelectionChange(nodesToUnselect, nodeInternals) });
}
onNodesChange?.(nodesToUnselect);
}
if (edgesToUnselect.length) {
if (hasDefaultEdges) {
set({
edges: handleControlledEdgeSelectionChange(edgesToUnselect, edges),
});
}
onEdgesChange?.(edgesToUnselect);
}
},
setMinZoom: (minZoom: number) => {
const { d3Zoom, maxZoom } = get();
d3Zoom?.scaleExtent([minZoom, maxZoom]);
set({ minZoom });
},
setMaxZoom: (maxZoom: number) => {
const { d3Zoom, minZoom } = get();
d3Zoom?.scaleExtent([minZoom, maxZoom]);
set({ maxZoom });
},
setTranslateExtent: (translateExtent: CoordinateExtent) => {
const { d3Zoom } = get();
d3Zoom?.translateExtent(translateExtent);
set({ translateExtent });
},
resetSelectedElements: () => {
const { nodeInternals, edges, onNodesChange, onEdgesChange, hasDefaultNodes, hasDefaultEdges } = get();
const nodes = Array.from(nodeInternals.values());
const nodesToUnselect = nodes
.filter((e) => e.selected)
.map((n) => createSelectionChange(n.id, false)) as NodeSelectionChange[];
const edgesToUnselect = edges
.filter((e) => e.selected)
.map((e) => createSelectionChange(e.id, false)) as EdgeSelectionChange[];
if (nodesToUnselect.length) {
if (hasDefaultNodes) {
set({
nodeInternals: handleControlledNodeSelectionChange(nodesToUnselect, nodeInternals),
});
}
onNodesChange?.(nodesToUnselect);
}
if (edgesToUnselect.length) {
if (hasDefaultEdges) {
set({
edges: handleControlledEdgeSelectionChange(edgesToUnselect, edges),
});
}
onEdgesChange?.(edgesToUnselect);
}
},
setNodeExtent: (nodeExtent: CoordinateExtent) => {
const { nodeInternals } = get();
nodeInternals.forEach((node) => {
node.positionAbsolute = clampPosition(node.position, nodeExtent);
});
set({
nodeExtent,
nodeInternals: new Map(nodeInternals),
});
},
reset: () => set({ ...initialState }),
}));
export { Provider, useStore, createStore, useStoreApi };
+52
View File
@@ -0,0 +1,52 @@
import { CoordinateExtent, ReactFlowStore, ConnectionMode } from '../types';
export const infiniteExtent: CoordinateExtent = [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
];
const initialState: ReactFlowStore = {
width: 0,
height: 0,
transform: [0, 0, 1],
nodeInternals: new Map(),
edges: [],
onNodesChange: null,
onEdgesChange: null,
hasDefaultNodes: false,
hasDefaultEdges: false,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
d3Zoom: null,
d3Selection: null,
d3ZoomHandler: undefined,
minZoom: 0.5,
maxZoom: 2,
translateExtent: infiniteExtent,
nodeExtent: infiniteExtent,
nodesSelectionActive: false,
userSelectionActive: false,
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: 'source',
connectionPosition: { x: 0, y: 0 },
connectionMode: ConnectionMode.Strict,
snapGrid: [15, 15],
snapToGrid: false,
nodesDraggable: true,
nodesConnectable: true,
elementsSelectable: true,
fitViewOnInit: false,
fitViewOnInitDone: false,
fitViewOnInitOptions: undefined,
multiSelectionActive: false,
reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-',
connectionStartHandle: null,
connectOnClick: true,
};
export default initialState;
-341
View File
@@ -1,341 +0,0 @@
import isEqual from 'fast-deep-equal';
import { clampPosition, getDimensions } from '../utils';
import {
getNodesInside,
getConnectedEdges,
getRectOfNodes,
isNode,
isEdge,
parseNode,
parseEdge,
} from '../utils/graph';
import { getHandleBounds } from '../components/Nodes/utils';
import { ReactFlowState, Node, XYPosition, Edge } from '../types';
import * as constants from './contants';
import { ReactFlowAction } from './actions';
import { initialState } from './index';
type NextElements = {
nextNodes: Node[];
nextEdges: Edge[];
};
export default function reactFlowReducer(state = initialState, action: ReactFlowAction): ReactFlowState {
switch (action.type) {
case constants.SET_ELEMENTS: {
const propElements = action.payload;
const nextElements: NextElements = {
nextNodes: [],
nextEdges: [],
};
const { nextNodes, nextEdges } = propElements.reduce((res, propElement): NextElements => {
if (isNode(propElement)) {
const storeNode = state.nodes.find((node) => node.id === propElement.id);
if (storeNode) {
const updatedNode: Node = {
...storeNode,
...propElement,
};
if (storeNode.position.x !== propElement.position.x || storeNode.position.y !== propElement.position.y) {
updatedNode.__rf.position = propElement.position;
}
if (typeof propElement.type !== 'undefined' && propElement.type !== storeNode.type) {
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
// When the type of a node changes it is possible that the number or positions of handles changes too.
updatedNode.__rf.width = null;
}
res.nextNodes.push(updatedNode);
} else {
res.nextNodes.push(parseNode(propElement, state.nodeExtent));
}
} else if (isEdge(propElement)) {
const storeEdge = state.edges.find((se) => se.id === propElement.id);
if (storeEdge) {
res.nextEdges.push({
...storeEdge,
...propElement,
});
} else {
res.nextEdges.push(parseEdge(propElement));
}
}
return res;
}, nextElements);
return { ...state, nodes: nextNodes, edges: nextEdges };
}
case constants.UPDATE_NODE_DIMENSIONS: {
const updatedNodes = state.nodes.map((node) => {
const update = action.payload.find((u) => u.id === node.id);
if (update) {
const dimensions = getDimensions(update.nodeElement);
const doUpdate =
dimensions.width &&
dimensions.height &&
(node.__rf.width !== dimensions.width || node.__rf.height !== dimensions.height || update.forceUpdate);
if (doUpdate) {
const handleBounds = getHandleBounds(update.nodeElement, state.transform[2]);
return {
...node,
__rf: {
...node.__rf,
...dimensions,
handleBounds,
},
};
}
}
return node;
});
return {
...state,
nodes: updatedNodes,
};
}
case constants.UPDATE_NODE_POS: {
const { id, pos } = action.payload;
let position: XYPosition = pos;
if (state.snapToGrid) {
const [gridSizeX, gridSizeY] = state.snapGrid;
position = {
x: gridSizeX * Math.round(pos.x / gridSizeX),
y: gridSizeY * Math.round(pos.y / gridSizeY),
};
}
const nextNodes = state.nodes.map((node) => {
if (node.id === id) {
return {
...node,
__rf: {
...node.__rf,
position,
},
};
}
return node;
});
return { ...state, nodes: nextNodes };
}
case constants.UPDATE_NODE_POS_DIFF: {
const { id, diff, isDragging } = action.payload;
const nextNodes = state.nodes.map((node) => {
if (id === node.id || state.selectedElements?.find((sNode) => sNode.id === node.id)) {
const updatedNode = {
...node,
__rf: {
...node.__rf,
isDragging,
},
};
if (diff) {
updatedNode.__rf.position = {
x: node.__rf.position.x + diff.x,
y: node.__rf.position.y + diff.y,
};
}
return updatedNode;
}
return node;
});
return { ...state, nodes: nextNodes };
}
case constants.SET_USER_SELECTION: {
const mousePos = action.payload;
return {
...state,
selectionActive: true,
userSelectionRect: {
width: 0,
height: 0,
startX: mousePos.x,
startY: mousePos.y,
x: mousePos.x,
y: mousePos.y,
draw: true,
},
};
}
case constants.UPDATE_USER_SELECTION: {
const mousePos = action.payload;
const startX = state.userSelectionRect.startX ?? 0;
const startY = state.userSelectionRect.startY ?? 0;
const nextUserSelectRect = {
...state.userSelectionRect,
x: mousePos.x < startX ? mousePos.x : startX,
y: mousePos.y < startY ? mousePos.y : startY,
width: Math.abs(mousePos.x - startX),
height: Math.abs(mousePos.y - startY),
};
const selectedNodes = getNodesInside(state.nodes, nextUserSelectRect, state.transform, false, true);
const selectedEdges = getConnectedEdges(selectedNodes, state.edges);
const nextSelectedElements = [...selectedNodes, ...selectedEdges];
const selectedElementsChanged = !isEqual(nextSelectedElements, state.selectedElements);
const selectedElementsUpdate = selectedElementsChanged
? {
selectedElements: nextSelectedElements.length > 0 ? nextSelectedElements : null,
}
: {};
return {
...state,
...selectedElementsUpdate,
userSelectionRect: nextUserSelectRect,
};
}
case constants.UNSET_USER_SELECTION: {
const selectedNodes = state.selectedElements?.filter((node) => isNode(node) && node.__rf) as Node[];
const stateUpdate = {
...state,
selectionActive: false,
userSelectionRect: {
...state.userSelectionRect,
draw: false,
},
};
if (!selectedNodes || selectedNodes.length === 0) {
stateUpdate.selectedElements = null;
stateUpdate.nodesSelectionActive = false;
} else {
const selectedNodesBbox = getRectOfNodes(selectedNodes);
stateUpdate.selectedNodesBbox = selectedNodesBbox;
stateUpdate.nodesSelectionActive = true;
}
return stateUpdate;
}
case constants.SET_SELECTED_ELEMENTS: {
const elements = action.payload;
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
const selectedElementsUpdated = !isEqual(selectedElementsArr, state.selectedElements);
const selectedElements = selectedElementsUpdated ? selectedElementsArr : state.selectedElements;
return {
...state,
selectedElements,
};
}
case constants.ADD_SELECTED_ELEMENTS: {
const { multiSelectionActive, selectedElements } = state;
const elements = action.payload;
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
let nextElements = selectedElementsArr;
if (multiSelectionActive) {
nextElements = selectedElements ? [...selectedElements, ...selectedElementsArr] : selectedElementsArr;
}
const selectedElementsUpdated = !isEqual(nextElements, state.selectedElements);
const nextSelectedElements = selectedElementsUpdated ? nextElements : state.selectedElements;
return { ...state, selectedElements: nextSelectedElements };
}
case constants.INIT_D3ZOOM: {
const { d3Zoom, d3Selection, d3ZoomHandler, transform } = action.payload;
return {
...state,
d3Zoom,
d3Selection,
d3ZoomHandler,
transform,
};
}
case constants.SET_MINZOOM: {
const minZoom = action.payload;
state.d3Zoom?.scaleExtent([minZoom, state.maxZoom]);
return {
...state,
minZoom,
};
}
case constants.SET_MAXZOOM: {
const maxZoom = action.payload;
state.d3Zoom?.scaleExtent([state.minZoom, maxZoom]);
return {
...state,
maxZoom,
};
}
case constants.SET_TRANSLATEEXTENT: {
const translateExtent = action.payload;
state.d3Zoom?.translateExtent(translateExtent);
return {
...state,
translateExtent,
};
}
case constants.SET_NODE_EXTENT: {
const nodeExtent = action.payload;
return {
...state,
nodeExtent,
nodes: state.nodes.map((node) => {
return {
...node,
__rf: {
...node.__rf,
position: clampPosition(node.__rf.position, nodeExtent),
},
};
}),
};
}
case constants.SET_ON_CONNECT:
case constants.SET_ON_CONNECT_START:
case constants.SET_ON_CONNECT_STOP:
case constants.SET_ON_CONNECT_END:
case constants.RESET_SELECTED_ELEMENTS:
case constants.UNSET_NODES_SELECTION:
case constants.UPDATE_TRANSFORM:
case constants.UPDATE_SIZE:
case constants.SET_CONNECTION_POSITION:
case constants.SET_CONNECTION_NODEID:
case constants.SET_SNAPTOGRID:
case constants.SET_SNAPGRID:
case constants.SET_INTERACTIVE:
case constants.SET_NODES_DRAGGABLE:
case constants.SET_NODES_CONNECTABLE:
case constants.SET_ELEMENTS_SELECTABLE:
case constants.SET_MULTI_SELECTION_ACTIVE:
case constants.SET_CONNECTION_MODE:
return { ...state, ...action.payload };
default:
return state;
}
}
+218 -4
View File
@@ -1,5 +1,219 @@
export function createAction<T extends string>(type: T): { type: T };
export function createAction<T extends string, P extends any>(type: T, payload: P): { type: T; payload: P };
export function createAction(type: string, payload?: any) {
return { type, payload };
import { zoomIdentity } from 'd3-zoom';
import { GetState } from 'zustand';
import { clampPosition, isNumeric } from '../utils';
import { getD3Transition, getRectOfNodes, getTransformForBounds } from '../utils/graph';
import {
CoordinateExtent,
Edge,
EdgeSelectionChange,
Node,
NodeInternals,
NodePositionChange,
NodeSelectionChange,
ReactFlowState,
XYPosition,
XYZPosition,
FitViewOptions,
} from '../types';
type ParentNodes = Record<string, boolean>;
function calculateXYZPosition(
node: Node,
nodeInternals: NodeInternals,
parentNodes: ParentNodes,
result: XYZPosition
): XYZPosition {
if (!node.parentNode) {
return result;
}
const parentNode = nodeInternals.get(node.parentNode)!;
return calculateXYZPosition(parentNode, nodeInternals, parentNodes, {
x: (result.x ?? 0) + (parentNode.position?.x ?? 0),
y: (result.y ?? 0) + (parentNode.position?.y ?? 0),
z: (parentNode.z ?? 0) > (result.z ?? 0) ? parentNode.z ?? 0 : result.z ?? 0,
});
}
export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals): NodeInternals {
const nextNodeInternals = new Map<string, Node>();
const parentNodes: ParentNodes = {};
nodes.forEach((node) => {
const z = isNumeric(node.zIndex) ? node.zIndex : node.dragging || node.selected ? 1000 : 0;
const internals: Node = {
...nodeInternals.get(node.id),
...node,
positionAbsolute: {
x: node.position.x,
y: node.position.y,
},
z,
};
if (node.parentNode) {
internals.parentNode = node.parentNode;
parentNodes[node.parentNode] = true;
}
nextNodeInternals.set(node.id, internals);
});
nextNodeInternals.forEach((node) => {
if (node.parentNode && !nextNodeInternals.has(node.parentNode)) {
throw new Error(`Parent node ${node.parentNode} not found`);
}
if (node.parentNode || parentNodes[node.id]) {
const { x, y, z } = calculateXYZPosition(node, nextNodeInternals, parentNodes, {
...node.position,
z: node.z ?? 0,
});
node.positionAbsolute = {
x,
y,
};
node.z = z;
if (parentNodes[node.id]) {
node.isParent = true;
}
}
});
return nextNodeInternals;
}
export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean {
if (!node.parentNode) {
return false;
}
const parentNode = nodeInternals.get(node.parentNode);
if (!parentNode) {
return false;
}
if (parentNode.selected) {
return true;
}
return isParentSelected(parentNode, nodeInternals);
}
type CreatePostionChangeParams = {
node: Node;
nodeExtent: CoordinateExtent;
nodeInternals: NodeInternals;
diff?: XYPosition;
dragging?: boolean;
};
export function createPositionChange({
node,
diff,
dragging,
nodeExtent,
nodeInternals,
}: CreatePostionChangeParams): NodePositionChange {
const change: NodePositionChange = {
id: node.id,
type: 'position',
dragging: !!dragging,
};
if (diff) {
const nextPosition = { x: node.position.x + diff.x, y: node.position.y + diff.y };
let currentExtent = node.extent || nodeExtent;
if (node.extent === 'parent') {
if (node.parentNode && node.width && node.height) {
const parent = nodeInternals.get(node.parentNode);
currentExtent =
parent?.width && parent?.height
? [
[0, 0],
[parent.width - node.width, parent.height - node.height],
]
: currentExtent;
} else {
console.warn('Only child nodes can use parent extent');
currentExtent = nodeExtent;
}
}
change.position = currentExtent ? clampPosition(nextPosition, currentExtent as CoordinateExtent) : nextPosition;
}
return change;
}
type InternalFitViewOptions = {
initial?: boolean;
} & FitViewOptions;
export function fitView(get: GetState<ReactFlowState>, options: InternalFitViewOptions = {}) {
let { nodeInternals, width, height, minZoom, maxZoom, d3Zoom, d3Selection, fitViewOnInitDone, fitViewOnInit } = get();
if ((options.initial && !fitViewOnInitDone && fitViewOnInit) || !options.initial) {
if (d3Zoom && d3Selection) {
const nodes = Array.from(nodeInternals.values()).filter((n) =>
options.includeHiddenNodes ? !n.parentNode && n.width && n.height : !n.parentNode && !n.hidden
);
const nodesInitialized = nodes.every((n) => n.width && n.height);
if (nodes.length > 0 && nodesInitialized) {
const bounds = getRectOfNodes(nodes);
const [x, y, zoom] = getTransformForBounds(
bounds,
width,
height,
options.minZoom ?? minZoom,
options.maxZoom ?? maxZoom,
options.padding ?? 0.1
);
const nextTransform = zoomIdentity.translate(x, y).scale(zoom);
if (typeof options.duration === 'number' && options.duration > 0) {
d3Zoom.transform(getD3Transition(d3Selection, options.duration), nextTransform);
} else {
d3Zoom.transform(d3Selection, nextTransform);
}
return true;
}
}
}
return false;
}
export function handleControlledNodeSelectionChange(nodeChanges: NodeSelectionChange[], nodeInternals: NodeInternals) {
nodeChanges.forEach((change) => {
const node = nodeInternals.get(change.id);
if (node) {
nodeInternals.set(node.id, {
...node,
selected: change.selected,
});
}
});
return new Map(nodeInternals);
}
export function handleControlledEdgeSelectionChange(edgeChanges: EdgeSelectionChange[], edges: Edge[]) {
return edges.map((e) => {
const change = edgeChanges.find((change) => change.id === e.id);
if (change) {
e.selected = change.selected;
}
return e;
});
}
+50 -36
View File
@@ -5,12 +5,10 @@
overflow: hidden;
}
.react-flow__renderer,
.react-flow__pane,
.react-flow__selectionpane {
.react-flow__container {
position: absolute;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
@@ -19,6 +17,12 @@
z-index: 1;
}
.react-flow__viewport {
transform-origin: 0 0;
z-index: 2;
pointer-events: none;
}
.react-flow__renderer {
z-index: 4;
}
@@ -27,18 +31,9 @@
z-index: 5;
}
.react-flow__selection {
position: absolute;
top: 0;
left: 0;
}
.react-flow__edges {
position: absolute;
top: 0;
left: 0;
.react-flow .react-flow__edges {
pointer-events: none;
z-index: 2;
overflow: visible;
}
.react-flow__edge {
@@ -82,12 +77,8 @@
}
.react-flow__nodes {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
transform-origin: 0 0;
z-index: 3;
}
.react-flow__node {
@@ -95,15 +86,11 @@
user-select: none;
pointer-events: all;
transform-origin: 0 0;
box-sizing: border-box;
}
.react-flow__nodesselection {
z-index: 3;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
transform-origin: left top;
pointer-events: none;
@@ -154,19 +141,11 @@
/* additional components */
.react-flow__background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.react-flow__controls {
position: absolute;
z-index: 5;
bottom: 10px;
left: 10px;
bottom: 20px;
left: 15px;
&-button {
width: 24px;
@@ -182,6 +161,41 @@
.react-flow__minimap {
position: absolute;
z-index: 5;
bottom: 10px;
right: 10px;
bottom: 20px;
right: 15px;
}
.react-flow__attribution {
font-size: 10px;
position: absolute;
z-index: 1000;
background: rgba(255, 255, 255, 0.5);
padding: 2px 3px;
color: #999;
a {
color: #555;
text-decoration: none;
}
&.top {
top: 0;
}
&.bottom {
bottom: 0;
}
&.left {
left: 0;
}
&.right {
right: 0;
}
&.center {
left: 50%;
transform: translateX(-50%);
}
}
+20 -43
View File
@@ -1,8 +1,3 @@
.react-flow__selection {
background: rgba(0, 89, 220, 0.08);
border: 1px dotted rgba(0, 89, 220, 0.8);
}
.react-flow__edge {
&.selected {
.react-flow__edge-path {
@@ -46,7 +41,8 @@
.react-flow__node-default,
.react-flow__node-input,
.react-flow__node-output {
.react-flow__node-output,
.react-flow__node-group {
padding: 10px;
border-radius: 3px;
width: 150px;
@@ -55,31 +51,6 @@
text-align: center;
border-width: 1px;
border-style: solid;
}
.react-flow__node-default.selectable,
.react-flow__node-input.selectable,
.react-flow__node-output.selectable {
&:hover {
box-shadow: 0 1px 4px 1px rgba(0, 0, 0, 0.08);
}
}
.react-flow__node-input {
background: #fff;
border-color: #0041d0;
&.selected,
&.selected:hover {
box-shadow: 0 0 0 0.5px #0041d0;
}
.react-flow__handle {
background: #0041d0;
}
}
.react-flow__node-default {
background: #fff;
border-color: #1a192b;
@@ -93,21 +64,27 @@
}
}
.react-flow__node-output {
background: #fff;
border-color: #ff0072;
&.selected,
&.selected:hover {
box-shadow: 0 0 0 0.5px #ff0072;
}
.react-flow__handle {
background: #ff0072;
.react-flow__node-default.selectable,
.react-flow__node-input.selectable,
.react-flow__node-output.selectable,
.react-flow__node-group.selectable {
&:hover {
box-shadow: 0 1px 4px 1px rgba(0, 0, 0, 0.08);
}
}
.react-flow__nodesselection-rect {
.react-flow__node-group {
background: rgba(240, 240, 240, 0.25);
border-color: #1a192b;
&.selected,
&.selected:hover {
box-shadow: 0 0 0 0.5px #1a192b;
}
}
.react-flow__nodesselection-rect,
.react-flow__selection {
background: rgba(0, 89, 220, 0.08);
border: 1px dotted rgba(0, 89, 220, 0.8);
}
+58
View File
@@ -0,0 +1,58 @@
import { XYPosition, Dimensions } from './utils';
import { NodeHandleBounds, Node } from './nodes';
import { Edge } from './edges';
export type NodeDimensionChange = {
id: string;
type: 'dimensions';
dimensions: Dimensions;
handleBounds?: NodeHandleBounds;
};
export type NodePositionChange = {
id: string;
type: 'position';
position?: XYPosition;
dragging?: boolean;
};
export type NodeSelectionChange = {
id: string;
type: 'select';
selected: boolean;
};
export type NodeRemoveChange = {
id: string;
type: 'remove';
};
export type NodeAddChange<NodeData = any> = {
item: Node<NodeData>;
type: 'add';
};
export type NodeResetChange<NodeData = any> = {
item: Node<NodeData>;
type: 'reset';
};
export type NodeChange =
| NodeDimensionChange
| NodePositionChange
| NodeSelectionChange
| NodeRemoveChange
| NodeAddChange
| NodeResetChange;
export type EdgeSelectionChange = NodeSelectionChange;
export type EdgeRemoveChange = NodeRemoveChange;
export type EdgeAddChange<EdgeData = any> = {
item: Edge<EdgeData>;
type: 'add';
};
export type EdgeResetChange<EdgeData = any> = {
item: Edge<EdgeData>;
type: 'reset';
};
export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange | EdgeAddChange | EdgeResetChange;
+153
View File
@@ -0,0 +1,153 @@
import React, { CSSProperties, HTMLAttributes, MouseEvent as ReactMouseEvent, WheelEvent } from 'react';
import {
OnSelectionChangeFunc,
NodeTypes,
EdgeTypes,
Node,
Edge,
ConnectionMode,
ConnectionLineType,
ConnectionLineComponent,
OnConnectStart,
OnConnectStop,
OnConnectEnd,
OnConnect,
CoordinateExtent,
KeyCode,
PanOnScrollMode,
OnEdgeUpdateFunc,
OnInit,
ProOptions,
AttributionPosition,
DefaultEdgeOptions,
FitViewOptions,
BackgroundVariant,
OnNodesDelete,
OnEdgesDelete,
OnNodesChange,
OnEdgesChange,
OnMove,
OnMoveStart,
OnMoveEnd,
} from '.';
export interface ReactFlowProps extends HTMLAttributes<HTMLDivElement> {
nodes?: Node[];
edges?: Edge[];
defaultNodes?: Node[];
defaultEdges?: Edge[];
defaultEdgeOptions?: DefaultEdgeOptions;
onNodesChange?: OnNodesChange;
onEdgesChange?: OnEdgesChange;
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;
onNodesDelete?: OnNodesDelete;
onEdgesDelete?: OnEdgesDelete;
onConnect?: OnConnect;
onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
onInit?: OnInit;
onMove?: OnMove;
onMoveStart?: OnMoveStart;
onMoveEnd?: OnMoveEnd;
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?: NodeTypes;
edgeTypes?: EdgeTypes;
connectionMode?: ConnectionMode;
connectionLineType?: ConnectionLineType;
connectionLineStyle?: CSSProperties;
connectionLineComponent?: ConnectionLineComponent;
deleteKeyCode?: KeyCode | null;
selectionKeyCode?: KeyCode | null;
multiSelectionKeyCode?: KeyCode | null;
zoomActivationKeyCode?: KeyCode;
snapToGrid?: boolean;
snapGrid?: [number, number];
onlyRenderVisibleElements?: boolean;
nodesDraggable?: boolean;
nodesConnectable?: boolean;
elementsSelectable?: boolean;
selectNodesOnDrag?: boolean;
panOnDrag?: 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;
fitView?: boolean;
fitViewOptions?: FitViewOptions;
connectOnClick?: boolean;
attributionPosition?: AttributionPosition;
proOptions?: 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;
fitViewOptions?: FitViewOptions;
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;
}
+184
View File
@@ -0,0 +1,184 @@
import { CSSProperties, HTMLAttributes, ReactNode } from 'react';
import { Connection } from './general';
import { HandleElement } from './handles';
import { Node } from './nodes';
import { Position } from './utils';
// interface for the user edge items
export interface Edge<T = any> {
id: string;
type?: string;
source: string;
target: string;
sourceHandle?: string | null;
targetHandle?: string | null;
label?: string | ReactNode;
labelStyle?: CSSProperties;
labelShowBg?: boolean;
labelBgStyle?: CSSProperties;
labelBgPadding?: [number, number];
labelBgBorderRadius?: number;
style?: CSSProperties;
animated?: boolean;
hidden?: boolean;
data?: T;
className?: string;
sourceNode?: Node;
targetNode?: Node;
selected?: boolean;
markerStart?: EdgeMarkerType;
markerEnd?: EdgeMarkerType;
zIndex?: number;
}
export type DefaultEdgeOptions = Omit<
Edge,
'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'sourceNode' | 'targetNode'
>;
// props that get passed to a custom edge
export interface EdgeProps<T = any> {
id: string;
source: string;
target: string;
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
selected?: boolean;
animated?: boolean;
sourcePosition: Position;
targetPosition: Position;
label?: string | ReactNode;
labelStyle?: CSSProperties;
labelShowBg?: boolean;
labelBgStyle?: CSSProperties;
labelBgPadding?: [number, number];
labelBgBorderRadius?: number;
style?: CSSProperties;
data?: T;
sourceHandleId?: string | null;
targetHandleId?: string | null;
markerStart?: string;
markerEnd?: string;
curvature?: number;
}
export type BaseEdgeProps = Pick<
EdgeProps,
| 'label'
| 'labelStyle'
| 'labelShowBg'
| 'labelBgStyle'
| 'labelBgPadding'
| 'labelBgBorderRadius'
| 'style'
| 'markerStart'
| 'markerEnd'
> & {
centerX: number;
centerY: number;
path: string;
};
export type EdgeMouseHandler = (event: React.MouseEvent, edge: Edge) => void;
export interface WrapEdgeProps<T = any> {
id: string;
className?: string;
type: string;
data?: T;
onClick?: EdgeMouseHandler;
onEdgeDoubleClick?: EdgeMouseHandler;
selected: boolean;
animated?: boolean;
label?: string | ReactNode;
labelStyle?: CSSProperties;
labelShowBg?: boolean;
labelBgStyle?: CSSProperties;
labelBgPadding?: [number, number];
labelBgBorderRadius?: number;
style?: CSSProperties;
source: string;
target: string;
sourceHandleId: string | null;
targetHandleId: string | null;
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
elementsSelectable?: boolean;
hidden?: boolean;
onEdgeUpdate: OnEdgeUpdateFunc;
onContextMenu?: EdgeMouseHandler;
onMouseEnter?: EdgeMouseHandler;
onMouseMove?: EdgeMouseHandler;
onMouseLeave?: EdgeMouseHandler;
edgeUpdaterRadius?: number;
onEdgeUpdateStart?: EdgeMouseHandler;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void;
markerStart?: EdgeMarkerType;
markerEnd?: EdgeMarkerType;
}
export interface EdgeSmoothStepProps<T = any> extends EdgeProps<T> {
borderRadius?: number;
}
export interface EdgeTextProps extends HTMLAttributes<SVGElement> {
x: number;
y: number;
label?: string | ReactNode;
labelStyle?: CSSProperties;
labelShowBg?: boolean;
labelBgStyle?: CSSProperties;
labelBgPadding?: [number, number];
labelBgBorderRadius?: number;
}
export enum ConnectionLineType {
Bezier = 'default',
Straight = 'straight',
Step = 'step',
SmoothStep = 'smoothstep',
SimpleBezier = 'simplebezier',
}
export type ConnectionLineComponentProps = {
sourceX: number;
sourceY: number;
sourcePosition?: Position;
targetX: number;
targetY: number;
targetPosition?: Position;
connectionLineStyle?: CSSProperties;
connectionLineType: ConnectionLineType;
fromNode?: Node;
fromHandle?: HandleElement;
// backward compatibility, mark as deprecated?
sourceNode?: Node;
sourceHandle?: HandleElement;
};
export type ConnectionLineComponent = React.ComponentType<ConnectionLineComponentProps>;
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void;
export interface EdgeMarker {
type: MarkerType;
color?: string;
width?: number;
height?: number;
markerUnits?: string;
orient?: string;
strokeWidth?: number;
}
export type EdgeMarkerType = string | EdgeMarker;
export enum MarkerType {
Arrow = 'arrow',
ArrowClosed = 'arrowclosed',
}
+217
View File
@@ -0,0 +1,217 @@
import { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
import { Selection as D3Selection, ZoomBehavior } from 'd3';
import { XYPosition, Rect, Transform, CoordinateExtent } from './utils';
import { NodeChange, EdgeChange } from './changes';
import { Node, NodeInternals, NodeDimensionUpdate, NodeDiffUpdate } from './nodes';
import { Edge } from './edges';
import { HandleType, StartHandle } from './handles';
import { DefaultEdgeOptions } from '.';
import { ReactFlowInstance } from './instance';
export type NodeTypes = { [key: string]: ReactNode };
export type EdgeTypes = NodeTypes;
export type FitView = (fitViewOptions?: FitViewOptions) => void;
export type Project = (position: XYPosition) => XYPosition;
export type OnNodesChange = (nodes: NodeChange[]) => void;
export type OnEdgesChange = (nodes: EdgeChange[]) => void;
export type OnNodesDelete = (nodes: Node[]) => void;
export type OnEdgesDelete = (edges: Edge[]) => void;
export type OnMove = (event: MouseEvent | TouchEvent, viewport: Viewport) => void;
export type OnMoveStart = OnMove;
export type OnMoveEnd = OnMove;
export type ZoomInOut = (options?: ViewportHelperFunctionOptions) => void;
export type ZoomTo = (zoomLevel: number, options?: ViewportHelperFunctionOptions) => void;
export type GetZoom = () => number;
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 OnInit<NodeData = any, EdgeData = any> = (reactFlowInstance: ReactFlowInstance<NodeData, EdgeData>) => void;
export interface Connection {
source: string | null;
target: string | null;
sourceHandle: string | null;
targetHandle: string | null;
}
export enum ConnectionMode {
Strict = 'strict',
Loose = 'loose',
}
export type OnConnect = (connection: Connection) => void;
export type FitViewOptions = {
padding?: number;
includeHiddenNodes?: boolean;
minZoom?: number;
maxZoom?: number;
duration?: number;
};
export type OnConnectStartParams = {
nodeId: string | null;
handleId: string | null;
handleType: HandleType | null;
};
export type OnConnectStart = (event: ReactMouseEvent, params: OnConnectStartParams) => void;
export type OnConnectStop = (event: MouseEvent) => void;
export type OnConnectEnd = (event: MouseEvent) => void;
export enum BackgroundVariant {
Lines = 'lines',
Dots = 'dots',
}
export type Viewport = {
x: number;
y: number;
zoom: number;
};
export type KeyCode = string | Array<string>;
export type SnapGrid = [number, number];
export enum PanOnScrollMode {
Free = 'free',
Vertical = 'vertical',
Horizontal = 'horizontal',
}
export type ViewportHelperFunctionOptions = {
duration?: number;
};
export type SetCenterOptions = ViewportHelperFunctionOptions & {
zoom?: number;
};
export type FitBoundsOptions = ViewportHelperFunctionOptions & {
padding?: number;
};
export interface ViewportHelperFunctions {
zoomIn: ZoomInOut;
zoomOut: ZoomInOut;
zoomTo: ZoomTo;
getZoom: GetZoom;
setViewport: SetViewport;
getViewport: GetViewport;
fitView: FitView;
setCenter: SetCenter;
fitBounds: FitBounds;
project: Project;
initialized: boolean;
}
export type ReactFlowStore = {
width: number;
height: number;
transform: Transform;
nodeInternals: NodeInternals;
edges: Edge[];
selectedNodesBbox: Rect;
onNodesChange: OnNodesChange | null;
onEdgesChange: OnEdgesChange | null;
hasDefaultNodes: boolean;
hasDefaultEdges: boolean;
d3Zoom: ZoomBehavior<Element, unknown> | null;
d3Selection: D3Selection<Element, unknown, null, undefined> | null;
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
minZoom: number;
maxZoom: number;
translateExtent: CoordinateExtent;
nodeExtent: CoordinateExtent;
nodesSelectionActive: boolean;
userSelectionActive: boolean;
connectionNodeId: string | null;
connectionHandleId: string | null;
connectionHandleType: HandleType | null;
connectionPosition: XYPosition;
connectionMode: ConnectionMode;
snapToGrid: boolean;
snapGrid: SnapGrid;
nodesDraggable: boolean;
nodesConnectable: boolean;
elementsSelectable: boolean;
multiSelectionActive: boolean;
reactFlowVersion: string;
connectionStartHandle: StartHandle | null;
onConnect?: OnConnect;
onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
connectOnClick: boolean;
defaultEdgeOptions?: DefaultEdgeOptions;
fitViewOnInit: boolean;
fitViewOnInitDone: boolean;
fitViewOnInitOptions: FitViewOptions | undefined;
onNodesDelete?: OnNodesDelete;
onEdgesDelete?: OnEdgesDelete;
};
export type ReactFlowActions = {
setNodes: (nodes: Node[]) => void;
setEdges: (edges: Edge[]) => void;
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void;
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
updateNodePosition: (update: NodeDiffUpdate) => void;
resetSelectedElements: () => void;
unselectNodesAndEdges: () => void;
addSelectedNodes: (nodeIds: string[]) => void;
addSelectedEdges: (edgeIds: string[]) => void;
setMinZoom: (minZoom: number) => void;
setMaxZoom: (maxZoom: number) => void;
setTranslateExtent: (translateExtent: CoordinateExtent) => void;
setNodeExtent: (nodeExtent: CoordinateExtent) => void;
reset: () => void;
};
export type ReactFlowState = ReactFlowStore & ReactFlowActions;
export type UpdateNodeInternals = (nodeId: string) => void;
export type OnSelectionChangeParams = {
nodes: Node[];
edges: Edge[];
};
export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void;
export type AttributionPosition =
| 'top-left'
| 'top-center'
| 'top-right'
| 'bottom-left'
| 'bottom-center'
| 'bottom-right';
export type ProOptions = {
account: string;
hideAttribution: boolean;
};
+24
View File
@@ -0,0 +1,24 @@
import { XYPosition, Position, Dimensions } from './utils';
import { OnConnect, Connection } from './general';
export type HandleType = 'source' | 'target';
export interface HandleElement extends XYPosition, Dimensions {
id?: string | null;
position: Position;
}
export interface StartHandle {
nodeId: string;
type: HandleType;
handleId?: string | null;
}
export interface HandleProps {
type: HandleType;
position: Position;
isConnectable?: boolean;
onConnect?: OnConnect;
isValidConnection?: (connection: Connection) => boolean;
id?: string;
}
+8 -447
View File
@@ -1,447 +1,8 @@
import React, { CSSProperties, MouseEvent as ReactMouseEvent, HTMLAttributes, ReactNode } from 'react';
import { Selection as D3Selection, ZoomBehavior } from 'd3';
export type ElementId = string;
export type FlowElement<T = any> = Node<T> | Edge<T>;
export type Elements<T = any> = Array<FlowElement<T>>;
export type Transform = [number, number, number];
export enum Position {
Left = 'left',
Top = 'top',
Right = 'right',
Bottom = 'bottom',
}
export interface XYPosition {
x: number;
y: number;
}
export interface Dimensions {
width: number;
height: number;
}
export interface Rect extends Dimensions, XYPosition {}
export interface Box extends XYPosition {
x2: number;
y2: number;
}
export type SnapGrid = [number, number];
export interface Node<T = any> {
id: ElementId;
position: XYPosition;
type?: string;
__rf?: any;
data?: T;
style?: CSSProperties;
className?: string;
targetPosition?: Position;
sourcePosition?: Position;
isHidden?: boolean;
draggable?: boolean;
selectable?: boolean;
connectable?: boolean;
dragHandle?: string;
}
export enum ArrowHeadType {
Arrow = 'arrow',
ArrowClosed = 'arrowclosed',
}
export interface Edge<T = any> {
id: ElementId;
type?: string;
source: ElementId;
target: ElementId;
sourceHandle?: ElementId | null;
targetHandle?: ElementId | null;
label?: string | ReactNode;
labelStyle?: CSSProperties;
labelShowBg?: boolean;
labelBgStyle?: CSSProperties;
labelBgPadding?: [number, number];
labelBgBorderRadius?: number;
style?: CSSProperties;
animated?: boolean;
arrowHeadType?: ArrowHeadType;
isHidden?: boolean;
data?: T;
className?: string;
}
export enum BackgroundVariant {
Lines = 'lines',
Dots = 'dots',
}
export type HandleType = 'source' | 'target';
export type NodeTypesType = { [key: string]: ReactNode };
export type EdgeTypesType = NodeTypesType;
export interface SelectionRect extends Rect {
startX: number;
startY: number;
draw: boolean;
}
export interface WrapEdgeProps<T = any> {
id: ElementId;
className?: string;
type: string;
data?: T;
onClick?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void;
selected: boolean;
animated?: boolean;
label?: string | ReactNode;
labelStyle?: CSSProperties;
labelShowBg?: boolean;
labelBgStyle?: CSSProperties;
labelBgPadding?: [number, number];
labelBgBorderRadius?: number;
style?: CSSProperties;
arrowHeadType?: ArrowHeadType;
source: ElementId;
target: ElementId;
sourceHandleId: ElementId | null;
targetHandleId: ElementId | null;
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
elementsSelectable?: boolean;
markerEndId?: string;
isHidden?: boolean;
handleEdgeUpdate: boolean;
onConnectEdge: OnConnectFunc;
onContextMenu?: (event: React.MouseEvent, edge: Edge) => void;
onMouseEnter?: (event: React.MouseEvent, edge: Edge) => void;
onMouseMove?: (event: React.MouseEvent, edge: Edge) => void;
onMouseLeave?: (event: React.MouseEvent, edge: Edge) => void;
edgeUpdaterRadius?: number;
onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void;
}
export interface EdgeProps<T = any> {
id: ElementId;
source: ElementId;
target: ElementId;
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
selected?: boolean;
animated?: boolean;
sourcePosition: Position;
targetPosition: Position;
label?: string | ReactNode;
labelStyle?: CSSProperties;
labelShowBg?: boolean;
labelBgStyle?: CSSProperties;
labelBgPadding?: [number, number];
labelBgBorderRadius?: number;
style?: CSSProperties;
arrowHeadType?: ArrowHeadType;
markerEndId?: string;
data?: T;
sourceHandleId?: ElementId | null;
targetHandleId?: ElementId | null;
}
export interface EdgeSmoothStepProps<T = any> extends EdgeProps<T> {
borderRadius?: number;
}
export interface EdgeTextProps extends HTMLAttributes<SVGElement> {
x: number;
y: number;
label?: string | ReactNode;
labelStyle?: CSSProperties;
labelShowBg?: boolean;
labelBgStyle?: CSSProperties;
labelBgPadding?: [number, number];
labelBgBorderRadius?: number;
}
export interface NodeProps<T = any> {
id: ElementId;
type: string;
data: T;
selected: boolean;
isConnectable: boolean;
xPos?: number;
yPos?: number;
targetPosition?: Position;
sourcePosition?: Position;
isDragging?: boolean;
dragHandle?: string;
}
export interface NodeComponentProps<T = any> {
id: ElementId;
type: string;
data: T;
selected?: boolean;
isConnectable: boolean;
transform?: Transform;
xPos?: number;
yPos?: number;
targetPosition?: Position;
sourcePosition?: Position;
onClick?: (node: Node) => void;
onNodeDoubleClick?: (node: Node) => void;
onMouseEnter?: (node: Node) => void;
onMouseMove?: (node: Node) => void;
onMouseLeave?: (node: Node) => void;
onContextMenu?: (node: Node) => void;
onNodeDragStart?: (node: Node) => void;
onNodeDrag?: (node: Node) => void;
onNodeDragStop?: (node: Node) => void;
style?: CSSProperties;
isDragging?: boolean;
dragHandle?: string;
}
export interface WrapNodeProps<T = any> {
id: ElementId;
type: string;
data: T;
selected: boolean;
scale: number;
xPos: number;
yPos: number;
isSelectable: boolean;
isDraggable: boolean;
isConnectable: boolean;
selectNodesOnDrag: boolean;
onClick?: (event: ReactMouseEvent, node: Node) => void;
onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void;
onMouseEnter?: (event: ReactMouseEvent, node: Node) => void;
onMouseMove?: (event: ReactMouseEvent, node: Node) => void;
onMouseLeave?: (event: ReactMouseEvent, node: Node) => void;
onContextMenu?: (event: ReactMouseEvent, node: Node) => void;
onNodeDragStart?: (event: ReactMouseEvent, node: Node) => void;
onNodeDrag?: (event: ReactMouseEvent, node: Node) => void;
onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void;
style?: CSSProperties;
className?: string;
sourcePosition?: Position;
targetPosition?: Position;
isHidden?: boolean;
isInitialized?: boolean;
snapToGrid?: boolean;
snapGrid?: SnapGrid;
isDragging?: boolean;
resizeObserver: ResizeObserver | null;
dragHandle?: string;
}
export type FitViewParams = {
padding?: number;
includeHiddenNodes?: boolean;
minZoom?: number;
maxZoom?: number;
duration?: number;
};
export type FlowExportObject<T = any> = {
elements: Elements<T>;
position: [number, number];
zoom: number;
};
export type FitViewFunc = (fitViewOptions?: FitViewParams, duration?: number) => void;
export type ProjectFunc = (position: XYPosition) => XYPosition;
export type ToObjectFunc<T = any> = () => FlowExportObject<T>;
export type OnLoadParams<T = any> = {
zoomIn: () => void;
zoomOut: () => void;
zoomTo: (zoomLevel: number) => void;
fitView: FitViewFunc;
project: ProjectFunc;
getElements: () => Elements<T>;
setTransform: (transform: FlowTransform) => void;
toObject: ToObjectFunc<T>;
};
export type OnLoadFunc<T = any> = (params: OnLoadParams<T>) => void;
export interface Connection {
source: ElementId | null;
target: ElementId | null;
sourceHandle: ElementId | null;
targetHandle: ElementId | null;
}
export enum ConnectionMode {
Strict = 'strict',
Loose = 'loose',
}
export enum ConnectionLineType {
Bezier = 'default',
Straight = 'straight',
Step = 'step',
SmoothStep = 'smoothstep',
}
export type ConnectionLineComponentProps = {
sourceX: number;
sourceY: number;
sourcePosition?: Position;
targetX: number;
targetY: number;
targetPosition?: Position;
connectionLineStyle?: CSSProperties;
connectionLineType: ConnectionLineType;
sourceNode?: Node;
sourceHandle?: HandleElement;
};
export type ConnectionLineComponent = React.ComponentType<ConnectionLineComponentProps>;
export type OnConnectFunc = (connection: Connection) => void;
export type OnConnectStartParams = {
nodeId: ElementId | null;
handleId: ElementId | null;
handleType: HandleType | null;
};
export type OnConnectStartFunc = (event: ReactMouseEvent, params: OnConnectStartParams) => void;
export type OnConnectStopFunc = (event: MouseEvent) => void;
export type OnConnectEndFunc = (event: MouseEvent) => void;
export type SetConnectionId = {
connectionNodeId: ElementId | null;
connectionHandleId: ElementId | null;
connectionHandleType: HandleType | null;
};
export interface HandleElement extends XYPosition, Dimensions {
id?: ElementId | null;
position: Position;
}
export interface HandleProps {
type: HandleType;
position: Position;
isConnectable?: boolean;
onConnect?: OnConnectFunc;
isValidConnection?: (connection: Connection) => boolean;
id?: ElementId;
}
export type NodePosUpdate = {
id: ElementId;
pos: XYPosition;
};
export type NodeDiffUpdate = {
id?: ElementId;
diff?: XYPosition;
isDragging?: boolean;
};
export type FlowTransform = {
x: number;
y: number;
zoom: number;
};
export type TranslateExtent = [[number, number], [number, number]];
export type NodeExtent = TranslateExtent;
export type KeyCode = number | string;
export enum PanOnScrollMode {
Free = 'free',
Vertical = 'vertical',
Horizontal = 'horizontal',
}
export interface ZoomPanHelperFunctions {
zoomIn: (duration?: number) => void;
zoomOut: (duration?: number) => void;
zoomTo: (zoomLevel: number, duration?: number) => void;
transform: (transform: FlowTransform, duration?: number) => void;
fitView: FitViewFunc;
setCenter: (x: number, y: number, zoom?: number, duration?: number) => void;
fitBounds: (bounds: Rect, padding?: number, duration?: number) => void;
project: (position: XYPosition) => XYPosition;
initialized: boolean;
}
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void;
export type NodeDimensionUpdate = {
id: ElementId;
nodeElement: HTMLDivElement;
forceUpdate?: boolean;
};
export type InitD3ZoomPayload = {
d3Zoom: ZoomBehavior<Element, unknown>;
d3Selection: D3Selection<Element, unknown, null, undefined>;
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
transform: Transform;
};
export interface ReactFlowState {
width: number;
height: number;
transform: Transform;
nodes: Node[];
edges: Edge[];
selectedElements: Elements | null;
selectedNodesBbox: Rect;
d3Zoom: ZoomBehavior<Element, unknown> | null;
d3Selection: D3Selection<Element, unknown, null, undefined> | null;
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
minZoom: number;
maxZoom: number;
translateExtent: TranslateExtent;
nodeExtent: NodeExtent;
nodesSelectionActive: boolean;
selectionActive: boolean;
userSelectionRect: SelectionRect;
connectionNodeId: ElementId | null;
connectionHandleId: ElementId | null;
connectionHandleType: HandleType | null;
connectionPosition: XYPosition;
connectionMode: ConnectionMode;
snapToGrid: boolean;
snapGrid: SnapGrid;
nodesDraggable: boolean;
nodesConnectable: boolean;
elementsSelectable: boolean;
multiSelectionActive: boolean;
reactFlowVersion: string;
onConnect?: OnConnectFunc;
onConnectStart?: OnConnectStartFunc;
onConnectStop?: OnConnectStopFunc;
onConnectEnd?: OnConnectEndFunc;
}
export type UpdateNodeInternals = (nodeId: ElementId) => void;
export * from './general';
export * from './nodes';
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'>;
+117
View File
@@ -0,0 +1,117 @@
import { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import { SnapGrid } from './general';
import { XYPosition, Position, CoordinateExtent } from './utils';
import { HandleElement } from './handles';
// interface for the user node items
export interface Node<T = any> {
id: string;
position: XYPosition;
data: T;
type?: string;
style?: CSSProperties;
className?: string;
targetPosition?: Position;
sourcePosition?: Position;
hidden?: boolean;
selected?: boolean;
dragging?: boolean;
draggable?: boolean;
selectable?: boolean;
connectable?: boolean;
dragHandle?: string;
width?: number | null;
height?: number | null;
parentNode?: string;
zIndex?: number;
extent?: 'parent' | CoordinateExtent;
expandParent?: boolean;
// only used internally
positionAbsolute?: XYPosition;
z?: number;
handleBounds?: NodeHandleBounds;
isParent?: boolean;
}
// props that get passed to a custom node
export interface NodeProps<T = any> {
id: string;
type: string;
data: T;
selected: boolean;
isConnectable: boolean;
xPos: number;
yPos: number;
dragging: boolean;
zIndex: number;
targetPosition?: Position;
sourcePosition?: Position;
dragHandle?: string;
}
export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void;
export interface WrapNodeProps<T = any> {
id: string;
type: string;
data: T;
selected: boolean;
isConnectable: boolean;
scale: number;
xPos: number;
yPos: number;
width?: number | null;
height?: number | null;
isSelectable: boolean;
isDraggable: boolean;
selectNodesOnDrag: boolean;
onClick?: NodeMouseHandler;
onNodeDoubleClick?: NodeMouseHandler;
onMouseEnter?: NodeMouseHandler;
onMouseMove?: NodeMouseHandler;
onMouseLeave?: NodeMouseHandler;
onContextMenu?: NodeMouseHandler;
onNodeDragStart?: NodeMouseHandler;
onNodeDrag?: NodeMouseHandler;
onNodeDragStop?: NodeMouseHandler;
style?: CSSProperties;
className?: string;
sourcePosition: Position;
targetPosition: Position;
hidden?: boolean;
snapToGrid?: boolean;
snapGrid?: SnapGrid;
dragging: boolean;
resizeObserver: ResizeObserver | null;
dragHandle?: string;
zIndex: number;
isParent: boolean;
noPanClassName: string;
noDragClassName: string;
}
export type NodeHandleBounds = {
source: HandleElement[] | null;
target: HandleElement[] | null;
};
export type NodeDiffUpdate = {
id?: string;
diff?: XYPosition;
dragging?: boolean;
};
export type NodeDimensionUpdate = {
id: string;
nodeElement: HTMLDivElement;
forceUpdate?: boolean;
};
export type NodeInternals = Map<string, Node>;
export type NodeBounds = XYPosition & {
width: number | null;
height: number | null;
};
+29
View File
@@ -0,0 +1,29 @@
export enum Position {
Left = 'left',
Top = 'top',
Right = 'right',
Bottom = 'bottom',
}
export interface XYPosition {
x: number;
y: number;
}
export type XYZPosition = XYPosition & { z: number };
export interface Dimensions {
width: number;
height: number;
}
export interface Rect extends Dimensions, XYPosition {}
export interface Box extends XYPosition {
x2: number;
y2: number;
}
export type Transform = [number, number, number];
export type CoordinateExtent = [[number, number], [number, number]];
+137
View File
@@ -0,0 +1,137 @@
import { Node, Edge, EdgeChange, NodeChange } from '../types';
function handleParentExpand(res: any[], updateItem: any) {
const parent = res.find((e) => e.id === updateItem.parentNode);
if (parent) {
const extendWidth = updateItem.position.x + updateItem.width - parent.width;
const extendHeight = updateItem.position.y + updateItem.height - parent.height;
if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) {
parent.style = { ...parent.style } || {};
if (extendWidth > 0) {
if (!parent.style.width) {
parent.style.width = parent.width;
}
parent.style.width += extendWidth;
}
if (extendHeight > 0) {
if (!parent.style.height) {
parent.style.height = parent.height;
}
parent.style.height += extendHeight;
}
if (updateItem.position.x < 0) {
const xDiff = Math.abs(updateItem.position.x);
parent.position.x = parent.position.x - xDiff;
parent.style.width += xDiff;
updateItem.position.x = 0;
}
if (updateItem.position.y < 0) {
const yDiff = Math.abs(updateItem.position.y);
parent.position.y = parent.position.y - yDiff;
parent.style.height += yDiff;
updateItem.position.y = 0;
}
parent.width = parent.style.width;
parent.height = parent.style.height;
}
}
}
function applyChanges(changes: any[], elements: any[]): any[] {
// unfortunately we need this hack to handle the setNodes and setEdges function of the
// useReactFlow hook.
if (changes.some((c) => c.type === 'reset')) {
return changes.filter((c) => c.type === 'reset').map((c) => c.item);
}
const initElements: any[] = changes.filter((c) => c.type === 'add').map((c) => c.item);
return elements.reduce((res: any[], item: any) => {
const currentChange = changes.find((c) => c.id === item.id);
if (currentChange) {
switch (currentChange.type) {
case 'select': {
res.push({ ...item, selected: currentChange.selected });
return res;
}
case 'position': {
const updateItem = { ...item };
if (typeof currentChange.position !== 'undefined') {
updateItem.position = currentChange.position;
}
if (typeof currentChange.dragging !== 'undefined') {
updateItem.dragging = currentChange.dragging;
}
if (updateItem.expandParent) {
handleParentExpand(res, updateItem);
}
res.push(updateItem);
return res;
}
case 'dimensions': {
const updateItem = { ...item };
if (typeof currentChange.dimensions !== 'undefined') {
updateItem.width = currentChange.dimensions.width;
updateItem.height = currentChange.dimensions.height;
}
if (updateItem.expandParent) {
handleParentExpand(res, updateItem);
}
res.push(updateItem);
return res;
}
case 'remove': {
return res;
}
}
}
res.push(item);
return res;
}, initElements);
}
export function applyNodeChanges<NodeData = any>(changes: NodeChange[], nodes: Node<NodeData>[]): Node<NodeData>[] {
return applyChanges(changes, nodes) as Node<NodeData>[];
}
export function applyEdgeChanges<EdgeData = any>(changes: EdgeChange[], edges: Edge<EdgeData>[]): Edge<EdgeData>[] {
return applyChanges(changes, edges) as Edge<EdgeData>[];
}
export const createSelectionChange = (id: string, selected: boolean) => ({
id,
type: 'select',
selected,
});
export function getSelectionChanges(items: any[], selectedIds: string[]) {
return items.reduce((res, item) => {
const willBeSelected = selectedIds.includes(item.id);
if (!item.selected && willBeSelected) {
item.selected = true;
res.push(createSelectionChange(item.id, true));
} else if (item.selected && !willBeSelected) {
item.selected = false;
res.push(createSelectionChange(item.id, false));
}
return res;
}, []);
}
+67 -163
View File
@@ -1,21 +1,7 @@
import { Store } from 'redux';
import { Selection as D3Selection } from 'd3';
import { clampPosition, clamp } from '../utils';
import {
ElementId,
Node,
Edge,
Elements,
Transform,
XYPosition,
Rect,
Box,
Connection,
FlowExportObject,
ReactFlowState,
NodeExtent,
} from '../types';
import { boxToRect, clamp, getBoundsOfBoxes, rectToBox } from '../utils';
import { Node, Edge, Connection, EdgeMarkerType, Transform, XYPosition, Rect, NodeInternals } from '../types';
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
'id' in element && 'source' in element && 'target' in element;
@@ -23,44 +9,45 @@ export const isEdge = (element: Node | Connection | Edge): element is Edge =>
export const isNode = (element: Node | Connection | Edge): element is Node =>
'id' in element && !('source' in element) && !('target' in element);
export const getOutgoers = (node: Node, elements: Elements): Node[] => {
export const getOutgoers = <T = any, U extends T = T>(node: Node<U>, nodes: Node<T>[], edges: Edge[]): Node<T>[] => {
if (!isNode(node)) {
return [];
}
const outgoerIds = elements.filter((e) => isEdge(e) && e.source === node.id).map((e) => (e as Edge).target);
return elements.filter((e) => outgoerIds.includes(e.id)) as Node[];
const outgoerIds = edges.filter((e) => e.source === node.id).map((e) => e.target);
return nodes.filter((n) => outgoerIds.includes(n.id));
};
export const getIncomers = (node: Node, elements: Elements): Node[] => {
export const getIncomers = <T = any, U extends T = T>(node: Node<U>, nodes: Node<T>[], edges: Edge[]): Node<T>[] => {
if (!isNode(node)) {
return [];
}
const incomersIds = elements.filter((e) => isEdge(e) && e.target === node.id).map((e) => (e as Edge).source);
return elements.filter((e) => incomersIds.includes(e.id)) as Node[];
const incomersIds = edges.filter((e) => e.target === node.id).map((e) => e.source);
return nodes.filter((n) => incomersIds.includes(n.id));
};
export const removeElements = (elementsToRemove: Elements, elements: Elements): Elements => {
const nodeIdsToRemove = elementsToRemove.map((n) => n.id);
const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection): string =>
`reactflow__edge-${source}${sourceHandle || ''}-${target}${targetHandle || ''}`;
return elements.filter((element) => {
const edgeElement = element as Edge;
return !(
nodeIdsToRemove.includes(element.id) ||
nodeIdsToRemove.includes(edgeElement.target) ||
nodeIdsToRemove.includes(edgeElement.source)
);
});
export const getMarkerId = (marker: EdgeMarkerType | undefined): string => {
if (typeof marker === 'undefined') {
return '';
}
if (typeof marker === 'string') {
return marker;
}
return Object.keys(marker)
.sort()
.map((key: string) => `${key}=${(marker as any)[key]}`)
.join('&');
};
const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection): ElementId =>
`reactflow__edge-${source}${sourceHandle}-${target}${targetHandle}`;
const connectionExists = (edge: Edge, elements: Elements) => {
return elements.some(
const connectionExists = (edge: Edge, edges: Edge[]) => {
return edges.some(
(el) =>
isEdge(el) &&
el.source === edge.source &&
el.target === edge.target &&
(el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) &&
@@ -68,10 +55,10 @@ const connectionExists = (edge: Edge, elements: Elements) => {
);
};
export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elements => {
export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] => {
if (!edgeParams.source || !edgeParams.target) {
console.warn("Can't create edge. An edge needs a source and a target.");
return elements;
return edges;
}
let edge: Edge;
@@ -84,24 +71,24 @@ export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elem
} as Edge;
}
if (connectionExists(edge, elements)) {
return elements;
if (connectionExists(edge, edges)) {
return edges;
}
return elements.concat(edge);
return edges.concat(edge);
};
export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: Elements): Elements => {
export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge[]): Edge[] => {
if (!newConnection.source || !newConnection.target) {
console.warn("Can't create new edge. An edge needs a source and a target.");
return elements;
return edges;
}
const foundEdge = elements.find((e) => isEdge(e) && e.id === oldEdge.id) as Edge;
const foundEdge = edges.find((e) => e.id === oldEdge.id) as Edge;
if (!foundEdge) {
console.warn(`The old edge with id=${oldEdge.id} does not exist.`);
return elements;
return edges;
}
// Remove old edge and create the new edge with parameters of old edge.
@@ -114,7 +101,7 @@ export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: E
targetHandle: newConnection.targetHandle,
} as Edge;
return elements.filter((e) => e.id !== oldEdge.id).concat(edge);
return edges.filter((e) => e.id !== oldEdge.id).concat(edge);
};
export const pointToRendererPoint = (
@@ -138,82 +125,26 @@ export const pointToRendererPoint = (
return position;
};
export const onLoadProject = (currentStore: Store<ReactFlowState>) => {
return (position: XYPosition): XYPosition => {
const { transform, snapToGrid, snapGrid } = currentStore.getState();
return pointToRendererPoint(position, transform, snapToGrid, snapGrid);
};
};
export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => {
return {
...node,
id: node.id.toString(),
type: node.type || 'default',
__rf: {
position: clampPosition(node.position, nodeExtent),
width: node.style?.width || null,
height: node.style?.height || null,
handleBounds: {},
isDragging: false,
},
};
};
export const parseEdge = (edge: Edge): Edge => {
return {
...edge,
source: edge.source.toString(),
target: edge.target.toString(),
sourceHandle: edge.sourceHandle ? edge.sourceHandle.toString() : null,
targetHandle: edge.targetHandle ? edge.targetHandle.toString() : null,
id: edge.id.toString(),
type: edge.type || 'default',
};
};
const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({
x: Math.min(box1.x, box2.x),
y: Math.min(box1.y, box2.y),
x2: Math.max(box1.x2, box2.x2),
y2: Math.max(box1.y2, box2.y2),
});
export const rectToBox = ({ x, y, width, height }: Rect): Box => ({
x,
y,
x2: x + width,
y2: y + height,
});
export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
x,
y,
width: x2 - x,
height: y2 - y,
});
export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect =>
boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)));
export const getRectOfNodes = (nodes: Node[]): Rect => {
const box = nodes.reduce(
(currBox, { __rf: { position, width, height } = {} }) =>
getBoundsOfBoxes(currBox, rectToBox({ ...position, width, height })),
(currBox, { positionAbsolute, position, width, height }) =>
getBoundsOfBoxes(
currBox,
rectToBox({
x: positionAbsolute ? positionAbsolute.x : position.x,
y: positionAbsolute ? positionAbsolute.y : position.y,
width: width || 0,
height: height || 0,
})
),
{ x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }
);
return boxToRect(box);
};
export const graphPosToZoomedPos = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => ({
x: x * tScale + tx,
y: y * tScale + ty,
});
export const getNodesInside = (
nodes: Node[],
nodeInternals: NodeInternals,
rect: Rect,
[tx, ty, tScale]: Transform = [0, 0, 1],
partially: boolean = false,
@@ -227,29 +158,32 @@ export const getNodesInside = (
height: rect.height / tScale,
});
return nodes.filter(({ selectable = true, __rf: { position, width, height, isDragging } }) => {
const visibleNodes: Node[] = [];
nodeInternals.forEach((node) => {
const { positionAbsolute, width, height, dragging, selectable = true } = node;
if (excludeNonSelectableNodes && !selectable) {
return false;
}
const nBox = rectToBox({ ...position, width, height });
const nBox = rectToBox({ ...positionAbsolute!, width: width || 0, height: height || 0 });
const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x));
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y));
const overlappingArea = Math.ceil(xOverlap * yOverlap);
const notInitialized =
typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null || dragging;
if (width === null || height === null || isDragging) {
// nodes are initialized with width and height = null
return true;
const partiallyVisible = partially && overlappingArea > 0;
const area = (width || 0) * (height || 0);
const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
if (isVisible) {
visibleNodes.push(node);
}
if (partially) {
return overlappingArea > 0;
}
const area = width * height;
return overlappingArea >= area;
});
return visibleNodes;
};
export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
@@ -258,40 +192,6 @@ export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target));
};
const parseElements = (nodes: Node[], edges: Edge[]): Elements => {
return [
...nodes.map((node) => {
const n = { ...node };
n.position = n.__rf.position;
delete n.__rf;
return n;
}),
...edges.map((e) => ({ ...e })),
];
};
export const onLoadGetElements = (currentStore: Store<ReactFlowState>) => {
return (): Elements => {
const { nodes = [], edges = [] } = currentStore.getState();
return parseElements(nodes, edges);
};
};
export const onLoadToObject = (currentStore: Store<ReactFlowState>) => {
return (): FlowExportObject => {
const { nodes = [], edges = [], transform } = currentStore.getState();
return {
elements: parseElements(nodes, edges),
position: [transform[0], transform[1]],
zoom: transform[2],
};
};
};
export const getTransformForBounds = (
bounds: Rect,
width: number,
@@ -311,3 +211,7 @@ export const getTransformForBounds = (
return [x, y, clampedZoom];
};
export const getD3Transition = (selection: D3Selection<Element, unknown, null, undefined>, duration: number = 0) => {
return selection.transition().duration(duration);
};
+28 -13
View File
@@ -1,15 +1,4 @@
import { DraggableEvent } from 'react-draggable';
import { MouseEvent as ReactMouseEvent } from 'react';
import { Dimensions, XYPosition, NodeExtent } from '../types';
export const isInputDOMNode = (e: ReactMouseEvent | DraggableEvent | KeyboardEvent) => {
const target = e?.target as HTMLElement;
return (
['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target?.nodeName) || target?.hasAttribute('contenteditable')
);
};
import { Dimensions, XYPosition, CoordinateExtent, Box, Rect } from '../types';
export const getDimensions = (node: HTMLDivElement): Dimensions => ({
width: node.offsetWidth,
@@ -18,10 +7,36 @@ export const getDimensions = (node: HTMLDivElement): Dimensions => ({
export const clamp = (val: number, min: number = 0, max: number = 1): number => Math.min(Math.max(val, min), max);
export const clampPosition = (position: XYPosition, extent: NodeExtent) => ({
export const clampPosition = (position: XYPosition, extent: CoordinateExtent) => ({
x: clamp(position.x, extent[0][0], extent[1][0]),
y: clamp(position.y, extent[0][1], extent[1][1]),
});
export const getHostForElement = (element: HTMLElement): Document | ShadowRoot =>
(element.getRootNode?.() as Document | ShadowRoot) || window?.document;
export const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({
x: Math.min(box1.x, box2.x),
y: Math.min(box1.y, box2.y),
x2: Math.max(box1.x2, box2.x2),
y2: Math.max(box1.y2, box2.y2),
});
export const rectToBox = ({ x, y, width, height }: Rect): Box => ({
x,
y,
x2: x + width,
y2: y + height,
});
export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
x,
y,
width: x2 - x,
height: y2 - y,
});
export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect =>
boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)));
export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n);