chore: setup monorepo using preconstruct

This commit is contained in:
Christopher Möller
2022-07-11 18:04:27 +02:00
parent a47f1c1382
commit 1bb32c8eb7
188 changed files with 2590 additions and 288 deletions
@@ -0,0 +1,73 @@
import React, { memo, FC, useEffect, useState, useRef } from 'react';
import cc from 'classcat';
import { useStore } from '../../store';
import { createGridLinesPath, createGridDotsPath } from './utils';
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,
size = 0.4,
color,
style,
className,
}) => {
const ref = useRef<SVGSVGElement>(null);
const [patternId, setPatternId] = useState<string | null>(null);
const [tX, tY, tScale] = useStore(transformSelector);
useEffect(() => {
// when there are multiple flows on a page we need to make sure that every background gets its own pattern.
const bgs = document.querySelectorAll('.react-flow__background');
const index = Array.from(bgs).findIndex((bg) => bg === ref.current);
setPatternId(`pattern-${index}`);
}, []);
const scaledGap = gap * tScale || 1;
const xOffset = tX % scaledGap;
const yOffset = tY % scaledGap;
const isLines = variant === BackgroundVariant.Lines;
const bgColor = color ? color : defaultColors[variant];
const path = isLines ? createGridLinesPath(scaledGap, size, bgColor) : createGridDotsPath(size * tScale, bgColor);
return (
<svg
className={cc(['react-flow__background', 'react-flow__container', className])}
style={{
...style,
width: '100%',
height: '100%',
}}
ref={ref}
>
{patternId && (
<>
<pattern
id={patternId}
x={xOffset}
y={yOffset}
width={scaledGap}
height={scaledGap}
patternUnits="userSpaceOnUse"
>
{path}
</pattern>
<rect x="0" y="0" width="100%" height="100%" fill={`url(#${patternId})`} />
</>
)}
</svg>
);
};
Background.displayName = 'Background';
export default memo(Background);
@@ -0,0 +1,9 @@
import React from 'react';
export const createGridLinesPath = (size: number, strokeWidth: number, stroke: string): React.ReactElement => {
return <path stroke={stroke} strokeWidth={strokeWidth} d={`M${size / 2} 0 V${size} M0 ${size / 2} H${size}`} />;
};
export const createGridDotsPath = (size: number, fill: string): React.ReactElement => {
return <circle cx={size} cy={size} r={size} fill={fill} />;
};
@@ -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;
@@ -0,0 +1,123 @@
import React, { memo, FC, useEffect, useState, PropsWithChildren } from 'react';
import cc from 'classcat';
import { useStore, useStoreApi } from '../../store';
import useReactFlow from '../../hooks/useReactFlow';
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 { ControlProps, ControlButtonProps, ReactFlowState } from '../../types';
export const ControlButton: FC<PropsWithChildren<ControlButtonProps>> = ({ children, className, ...rest }) => (
<button type="button" className={cc(['react-flow__controls-button', className])} {...rest}>
{children}
</button>
);
const isInteractiveSelector = (s: ReactFlowState) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable;
const Controls: FC<PropsWithChildren<ControlProps>> = ({
style,
showZoom = true,
showFitView = true,
showInteractive = true,
fitViewOptions,
onZoomIn,
onZoomOut,
onFitView,
onInteractiveChange,
className,
children,
}) => {
const store = useStoreApi();
const [isVisible, setIsVisible] = useState<boolean>(false);
const isInteractive = useStore(isInteractiveSelector);
const { zoomIn, zoomOut, fitView } = useReactFlow();
useEffect(() => {
setIsVisible(true);
}, []);
if (!isVisible) {
return null;
}
const onZoomInHandler = () => {
zoomIn?.();
onZoomIn?.();
};
const onZoomOutHandler = () => {
zoomOut?.();
onZoomOut?.();
};
const onFitViewHandler = () => {
fitView?.(fitViewOptions);
onFitView?.();
};
const onToggleInteractivity = () => {
store.setState({
nodesDraggable: !isInteractive,
nodesConnectable: !isInteractive,
elementsSelectable: !isInteractive,
});
onInteractiveChange?.(!isInteractive);
};
return (
<div className={cc(['react-flow__controls', className])} style={style}>
{showZoom && (
<>
<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"
title="zoom out"
aria-label="zoom out"
>
<MinusIcon />
</ControlButton>
</>
)}
{showFitView && (
<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={onToggleInteractivity}
title="toggle interactivity"
aria-label="toggle interactivity"
>
{isInteractive ? <UnlockIcon /> : <LockIcon />}
</ControlButton>
)}
{children}
</div>
);
};
Controls.displayName = 'Controls';
export default memo(Controls);
@@ -0,0 +1,53 @@
import React, { memo, CSSProperties } from 'react';
import cc from 'classcat';
interface MiniMapNodeProps {
x: number;
y: number;
width: number;
height: number;
borderRadius: number;
className: string;
color: string;
shapeRendering: string;
strokeColor: string;
strokeWidth: number;
style?: CSSProperties;
}
const MiniMapNode = ({
x,
y,
width,
height,
style,
color,
strokeColor,
strokeWidth,
className,
borderRadius,
shapeRendering,
}: MiniMapNodeProps) => {
const { background, backgroundColor } = style || {};
const fill = (color || background || backgroundColor) as string;
return (
<rect
className={cc(['react-flow__minimap-node', className])}
x={x}
y={y}
rx={borderRadius}
ry={borderRadius}
width={width}
height={height}
fill={fill}
stroke={strokeColor}
strokeWidth={strokeWidth}
shapeRendering={shapeRendering}
/>
);
};
MiniMapNode.displayName = 'MiniMapNode';
export default memo(MiniMapNode);
@@ -0,0 +1,102 @@
import React, { memo } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import MiniMapNode from './MiniMapNode';
import { useStore } from '../../store';
import { getRectOfNodes } from '../../utils/graph';
import { getBoundsofRects } from '../../utils';
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,
nodes: Array.from(s.nodeInternals.values()),
});
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
const MiniMap = ({
style,
className,
nodeStrokeColor = '#555',
nodeColor = '#fff',
nodeClassName = '',
nodeBorderRadius = 5,
nodeStrokeWidth = 2,
maskColor = 'rgb(240, 242, 243, 0.7)',
}: MiniMapProps) => {
const { width: containerWidth, height: containerHeight, transform, nodes } = useStore(selector, shallow);
const elementWidth = (style?.width as number) ?? defaultWidth;
const elementHeight = (style?.height as number) ?? defaultHeight;
const nodeColorFunc = getAttrFunction(nodeColor);
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
const nodeClassNameFunc = getAttrFunction(nodeClassName);
const viewBB: Rect = {
x: -transform[0] / transform[2],
y: -transform[1] / transform[2],
width: containerWidth / transform[2],
height: containerHeight / transform[2],
};
const boundingRect = nodes.length > 0 ? getBoundsofRects(getRectOfNodes(nodes), viewBB) : viewBB;
const scaledWidth = boundingRect.width / elementWidth;
const scaledHeight = boundingRect.height / elementHeight;
const viewScale = Math.max(scaledWidth, scaledHeight);
const viewWidth = viewScale * elementWidth;
const viewHeight = viewScale * elementHeight;
const offset = 5 * viewScale;
const x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset;
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';
return (
<svg
width={elementWidth}
height={elementHeight}
viewBox={`${x} ${y} ${width} ${height}`}
style={style}
className={cc(['react-flow__minimap', className])}
>
{nodes
.filter((node) => !node.hidden && node.width && node.height)
.map((node) => {
return (
<MiniMapNode
key={node.id}
x={node.positionAbsolute?.x ?? 0}
y={node.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
M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`}
fill={maskColor}
fillRule="evenodd"
/>
</svg>
);
};
MiniMap.displayName = 'MiniMap';
export default memo(MiniMap);
@@ -0,0 +1,11 @@
import React, { FC, PropsWithChildren } from 'react';
import { Provider, createStore } from '../../store';
const ReactFlowProvider: FC<PropsWithChildren<{}>> = ({ children }) => (
<Provider createStore={createStore}>{children}</Provider>
);
ReactFlowProvider.displayName = 'ReactFlowProvider';
export default ReactFlowProvider;
@@ -0,0 +1,7 @@
// These components are not used by React Flow directly
// They can be added as children of a React Flow component
export { default as MiniMap } from './MiniMap';
export { default as Controls, ControlButton } from './Controls';
export { default as Background } from './Background';
export { default as ReactFlowProvider } from './ReactFlowProvider';
@@ -0,0 +1,32 @@
import React from 'react';
import cc from 'classcat';
import { AttributionPosition, ProOptions } from '../../types';
type AttributionProps = {
proOptions?: ProOptions;
position?: AttributionPosition;
};
const accounts = ['paid-pro', 'paid-sponsor', 'paid-enterprise', 'paid-custom'];
function Attribution({ proOptions, position = 'bottom-right' }: AttributionProps) {
if (proOptions?.account && accounts.includes(proOptions?.account) && 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;
@@ -0,0 +1,159 @@
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 { ConnectionLineType, ConnectionLineComponent, HandleType, Node, ReactFlowState, Position } from '../../types';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import { internalsSymbol } from '../../utils';
interface ConnectionLineProps {
connectionNodeId: string;
connectionHandleId: string | null;
connectionHandleType: HandleType;
connectionPositionX: number;
connectionPositionY: number;
connectionLineType: ConnectionLineType;
isConnectable: boolean;
connectionLineStyle?: CSSProperties;
CustomConnectionLineComponent?: ConnectionLineComponent;
}
const selector = (s: ReactFlowState) => ({ nodeInternals: s.nodeInternals, transform: s.transform });
export default ({
connectionNodeId,
connectionHandleId,
connectionHandleType,
connectionLineStyle,
connectionPositionX,
connectionPositionY,
connectionLineType = ConnectionLineType.Bezier,
isConnectable,
CustomConnectionLineComponent,
}: ConnectionLineProps) => {
const nodeId = connectionNodeId;
const handleId = connectionHandleId;
const { nodeInternals, transform } = useStore(selector, shallow);
const fromNode = useRef<Node | undefined>(nodeInternals.get(nodeId));
const fromHandleBounds = fromNode.current?.[internalsSymbol]?.handleBounds;
if (!fromNode.current || !isConnectable || !fromHandleBounds?.[connectionHandleType]) {
return null;
}
const handleBound = fromHandleBounds[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 toX = (connectionPositionX - transform[0]) / transform[2];
const toY = (connectionPositionY - transform[1]) / transform[2];
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 (
<g className="react-flow__connection">
<CustomConnectionLineComponent
sourceX={sourceX}
sourceY={sourceY}
sourcePosition={sourcePosition}
targetX={targetX}
targetY={targetY}
targetPosition={targetPosition}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
fromNode={fromNode.current}
fromHandle={fromHandle}
// backward compatibility, mark as deprecated?
sourceNode={fromNode.current}
sourceHandle={fromHandle}
/>
</g>
);
}
let dAttr: string = '';
const pathParams = {
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
};
if (connectionLineType === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
dAttr = getBezierPath(pathParams);
} else if (connectionLineType === ConnectionLineType.Step) {
dAttr = getSmoothStepPath({
...pathParams,
borderRadius: 0,
});
} else if (connectionLineType === ConnectionLineType.SmoothStep) {
dAttr = getSmoothStepPath(pathParams);
} else if (connectionLineType === ConnectionLineType.SimpleBezier) {
dAttr = getSimpleBezierPath(pathParams);
} else {
dAttr = `M${sourceX},${sourceY} ${targetX},${targetY}`;
}
return (
<g className="react-flow__connection">
<path d={dAttr} className="react-flow__connection-path" style={connectionLineStyle} />
</g>
);
};
@@ -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}
</>
);
};
@@ -0,0 +1,177 @@
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}
/>
);
}
);
@@ -0,0 +1,40 @@
import React, { FC, HTMLAttributes } from 'react';
import cc from 'classcat';
import { Position } from '../../types';
const shiftX = (x: number, shift: number, position: Position): number => {
if (position === Position.Left) return x - shift;
if (position === Position.Right) return x + shift;
return x;
};
const shiftY = (y: number, shift: number, position: Position): number => {
if (position === Position.Top) return y - shift;
if (position === Position.Bottom) return y + shift;
return y;
};
export interface EdgeAnchorProps extends HTMLAttributes<HTMLDivElement> {
position: Position;
centerX: number;
centerY: number;
radius?: number;
}
export const EdgeAnchor: FC<EdgeAnchorProps> = ({
className,
position,
centerX,
centerY,
radius = 10,
}: EdgeAnchorProps) => (
<circle
className={cc(['react-flow__edgeupdater', className])}
cx={shiftX(centerX, radius, position)}
cy={shiftY(centerY, radius, position)}
r={radius}
stroke="transparent"
fill="transparent"
/>
);
@@ -0,0 +1,65 @@
import React, { memo, useRef, useState, useEffect, FC, PropsWithChildren } from 'react';
import cc from 'classcat';
import { EdgeTextProps, Rect } from '../../types';
const EdgeText: FC<PropsWithChildren<EdgeTextProps>> = ({
x,
y,
label,
labelStyle = {},
labelShowBg = true,
labelBgStyle = {},
labelBgPadding = [2, 4],
labelBgBorderRadius = 2,
children,
className,
...rest
}) => {
const edgeRef = useRef<SVGTextElement>(null);
const [edgeTextBbox, setEdgeTextBbox] = useState<Rect>({ x: 0, y: 0, width: 0, height: 0 });
const edgeTextClasses = cc(['react-flow__edge-textwrapper', className]);
useEffect(() => {
if (edgeRef.current) {
const textBbox = edgeRef.current.getBBox();
setEdgeTextBbox({
x: textBbox.x,
y: textBbox.y,
width: textBbox.width,
height: textBbox.height,
});
}
}, [label]);
if (typeof label === 'undefined' || !label) {
return null;
}
return (
<g
transform={`translate(${x - edgeTextBbox.width / 2} ${y - edgeTextBbox.height / 2})`}
className={edgeTextClasses}
{...rest}
>
{labelShowBg && (
<rect
width={edgeTextBbox.width + 2 * labelBgPadding[0]}
x={-labelBgPadding[0]}
y={-labelBgPadding[1]}
height={edgeTextBbox.height + 2 * labelBgPadding[1]}
className="react-flow__edge-textbg"
style={labelBgStyle}
rx={labelBgBorderRadius}
ry={labelBgBorderRadius}
/>
)}
<text className="react-flow__edge-text" y={edgeTextBbox.height / 2} dy="0.3em" ref={edgeRef} style={labelStyle}>
{label}
</text>
{children}
</g>
);
};
export default memo(EdgeText);
@@ -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}
/>
);
}
);
@@ -0,0 +1,166 @@
import React, { memo } from 'react';
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
// from bottom to the left and "leftBottomCorner" goes from left to the bottom.
// We have to consider the direction of the paths because of the animated lines.
const bottomLeftCorner = (x: number, y: number, size: number): string =>
`L ${x},${y - size}Q ${x},${y} ${x + size},${y}`;
const leftBottomCorner = (x: number, y: number, size: number): string =>
`L ${x + size},${y}Q ${x},${y} ${x},${y - size}`;
const bottomRightCorner = (x: number, y: number, size: number): string =>
`L ${x},${y - size}Q ${x},${y} ${x - size},${y}`;
const rightBottomCorner = (x: number, y: number, size: number): string =>
`L ${x - size},${y}Q ${x},${y} ${x},${y - size}`;
const leftTopCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y + size}`;
const topLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x + size},${y}`;
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}`;
export interface GetSmoothStepPathParams {
sourceX: number;
sourceY: number;
sourcePosition?: Position;
targetX: number;
targetY: number;
targetPosition?: Position;
borderRadius?: number;
centerX?: number;
centerY?: number;
}
export function getSmoothStepPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
borderRadius = 5,
centerX,
centerY,
}: GetSmoothStepPathParams): string {
const [_centerX, _centerY, offsetX, offsetY] = getCenter({ sourceX, sourceY, targetX, targetY });
const cornerWidth = Math.min(borderRadius, Math.abs(targetX - sourceX));
const cornerHeight = Math.min(borderRadius, Math.abs(targetY - sourceY));
const cornerSize = Math.min(cornerWidth, cornerHeight, offsetX, offsetY);
const leftAndRight = [Position.Left, Position.Right];
const cX = typeof centerX !== 'undefined' ? centerX : _centerX;
const cY = typeof centerY !== 'undefined' ? centerY : _centerY;
let firstCornerPath = null;
let secondCornerPath = null;
if (sourceX <= targetX) {
firstCornerPath =
sourceY <= targetY ? bottomLeftCorner(sourceX, cY, cornerSize) : topLeftCorner(sourceX, cY, cornerSize);
secondCornerPath =
sourceY <= targetY ? rightTopCorner(targetX, cY, cornerSize) : rightBottomCorner(targetX, cY, cornerSize);
} else {
firstCornerPath =
sourceY < targetY ? bottomRightCorner(sourceX, cY, cornerSize) : topRightCorner(sourceX, cY, cornerSize);
secondCornerPath =
sourceY < targetY ? leftTopCorner(targetX, cY, cornerSize) : leftBottomCorner(targetX, cY, cornerSize);
}
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath =
sourceY <= targetY ? rightTopCorner(cX, sourceY, cornerSize) : rightBottomCorner(cX, sourceY, cornerSize);
secondCornerPath =
sourceY <= targetY ? bottomLeftCorner(cX, targetY, cornerSize) : topLeftCorner(cX, targetY, cornerSize);
} else if (
(sourcePosition === Position.Right && targetPosition === Position.Left) ||
(sourcePosition === Position.Left && targetPosition === Position.Right) ||
(sourcePosition === Position.Left && targetPosition === Position.Left)
) {
// and sourceX > targetX
firstCornerPath =
sourceY <= targetY ? leftTopCorner(cX, sourceY, cornerSize) : leftBottomCorner(cX, sourceY, cornerSize);
secondCornerPath =
sourceY <= targetY ? bottomRightCorner(cX, targetY, cornerSize) : topRightCorner(cX, targetY, cornerSize);
}
} else if (leftAndRight.includes(sourcePosition) && !leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath =
sourceY <= targetY
? rightTopCorner(targetX, sourceY, cornerSize)
: rightBottomCorner(targetX, sourceY, cornerSize);
} else {
firstCornerPath =
sourceY <= targetY
? leftTopCorner(targetX, sourceY, cornerSize)
: leftBottomCorner(targetX, sourceY, cornerSize);
}
secondCornerPath = '';
} else if (!leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath =
sourceY <= targetY
? bottomLeftCorner(sourceX, targetY, cornerSize)
: topLeftCorner(sourceX, targetY, cornerSize);
} else {
firstCornerPath =
sourceY <= targetY
? bottomRightCorner(sourceX, targetY, cornerSize)
: topRightCorner(sourceX, targetY, cornerSize);
}
secondCornerPath = '';
}
return `M ${sourceX},${sourceY}${firstCornerPath}${secondCornerPath}L ${targetX},${targetY}`;
}
export default memo(
({
sourceX,
sourceY,
targetX,
targetY,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
markerEnd,
markerStart,
borderRadius = 5,
}: EdgeSmoothStepProps) => {
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
const path = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
borderRadius,
});
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}
/>
);
}
);
@@ -0,0 +1,8 @@
import React, { memo } from 'react';
import { EdgeSmoothStepProps } from '../../types';
import SmoothStepEdge from './SmoothStepEdge';
export default memo((props: EdgeSmoothStepProps) => {
return <SmoothStepEdge {...props} borderRadius={0} />;
});
@@ -0,0 +1,46 @@
import React, { memo } from 'react';
import BaseEdge from './BaseEdge';
import { EdgeProps } from '../../types';
export default memo(
({
sourceX,
sourceY,
targetX,
targetY,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
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 path = `M ${sourceX},${sourceY}L ${targetX},${targetY}`;
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}
/>
);
}
);
@@ -0,0 +1,5 @@
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';
@@ -0,0 +1,70 @@
import { MouseEvent as ReactMouseEvent } from 'react';
import { GetState } from 'zustand';
import { Edge, MarkerType, Position, ReactFlowState } from '../../types';
export const getMarkerEnd = (markerType?: MarkerType, markerEndId?: string): string => {
if (typeof markerEndId !== 'undefined' && markerEndId) {
return `url(#${markerEndId})`;
}
return typeof markerType !== 'undefined' ? `url(#react-flow__${markerType})` : 'none';
};
export interface GetCenterParams {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition?: Position;
targetPosition?: Position;
}
const LeftOrRight = [Position.Left, Position.Right];
export const getCenter = ({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
}: GetCenterParams): [number, number, number, number] => {
const sourceIsLeftOrRight = LeftOrRight.includes(sourcePosition);
const targetIsLeftOrRight = LeftOrRight.includes(targetPosition);
// we expect flows to be horizontal or vertical (all handles left or right respectively top or bottom)
// a mixed edge is when one the source is on the left and the target is on the top for example.
const mixedEdge = (sourceIsLeftOrRight && !targetIsLeftOrRight) || (targetIsLeftOrRight && !sourceIsLeftOrRight);
if (mixedEdge) {
const xOffset = sourceIsLeftOrRight ? Math.abs(targetX - sourceX) : 0;
const centerX = sourceX > targetX ? sourceX - xOffset : sourceX + xOffset;
const yOffset = sourceIsLeftOrRight ? 0 : Math.abs(targetY - sourceY);
const centerY = sourceY < targetY ? sourceY + yOffset : sourceY - yOffset;
return [centerX, centerY, xOffset, yOffset];
}
const xOffset = Math.abs(targetX - sourceX) / 2;
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
return [centerX, centerY, xOffset, yOffset];
};
export function getMouseHandler(
id: string,
getState: GetState<ReactFlowState>,
handler?: (event: ReactMouseEvent<SVGGElement, MouseEvent>, edge: Edge) => void
) {
return handler === undefined
? handler
: (event: ReactMouseEvent<SVGGElement, MouseEvent>) => {
const edge = getState().edges.find((e) => e.id === id)!;
handler(event, { ...edge });
};
}
@@ -0,0 +1,200 @@
import React, { memo, ComponentType, useState, useMemo } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
import { EdgeProps, WrapEdgeProps, ReactFlowState, Connection } from '../../types';
import { handleMouseDown } from '../../components/Handle/handler';
import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph';
import { getMouseHandler } from './utils';
const selector = (s: ReactFlowState) => ({
addSelectedEdges: s.addSelectedEdges,
connectionMode: s.connectionMode,
});
export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({
id,
className,
type,
data,
onClick,
onEdgeDoubleClick,
selected,
animated,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
source,
target,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
elementsSelectable,
hidden,
sourceHandleId,
targetHandleId,
onContextMenu,
onMouseEnter,
onMouseMove,
onMouseLeave,
edgeUpdaterRadius,
onEdgeUpdate,
onEdgeUpdateStart,
onEdgeUpdateEnd,
markerEnd,
markerStart,
rfId,
}: WrapEdgeProps): JSX.Element | null => {
const [updating, setUpdating] = useState<boolean>(false);
const { addSelectedEdges, connectionMode } = useStore(selector, shallow);
const store = useStoreApi();
const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
const edge = store.getState().edges.find((e) => e.id === id)!;
if (elementsSelectable) {
store.setState({ nodesSelectionActive: false });
addSelectedEdges([id]);
}
onClick?.(event, edge);
};
const onEdgeDoubleClickHandler = getMouseHandler(id, store.getState, onEdgeDoubleClick);
const onEdgeContextMenu = getMouseHandler(id, store.getState, onContextMenu);
const onEdgeMouseEnter = getMouseHandler(id, store.getState, onMouseEnter);
const onEdgeMouseMove = getMouseHandler(id, store.getState, onMouseMove);
const onEdgeMouseLeave = getMouseHandler(id, store.getState, onMouseLeave);
const handleEdgeUpdater = (event: React.MouseEvent<SVGGElement, MouseEvent>, isSourceHandle: boolean) => {
const nodeId = isSourceHandle ? target : source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source';
const isValidConnection = () => true;
const isTarget = isSourceHandle;
const edge = store.getState().edges.find((e) => e.id === id)!;
onEdgeUpdateStart?.(event, edge, handleType);
const _onEdgeUpdate = onEdgeUpdateEnd
? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edge, handleType)
: undefined;
const onConnectEdge = (connection: Connection) => {
const { edges } = store.getState();
const edge = edges.find((e) => e.id === id);
if (edge && onEdgeUpdate) {
onEdgeUpdate(edge, connection);
}
};
handleMouseDown(
event,
handleId,
nodeId,
store.setState,
onConnectEdge,
isTarget,
isValidConnection,
connectionMode,
handleType,
_onEdgeUpdate,
store.getState
);
};
const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, true);
const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, false);
const onEdgeUpdaterMouseEnter = () => setUpdating(true);
const onEdgeUpdaterMouseOut = () => setUpdating(false);
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]);
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]);
if (hidden) {
return null;
}
const inactive = !elementsSelectable && !onClick;
const handleEdgeUpdate = typeof onEdgeUpdate !== 'undefined';
const edgeClasses = cc([
'react-flow__edge',
`react-flow__edge-${type}`,
className,
{ selected, animated, inactive, updating },
]);
return (
<g
className={edgeClasses}
onClick={onEdgeClick}
onDoubleClick={onEdgeDoubleClickHandler}
onContextMenu={onEdgeContextMenu}
onMouseEnter={onEdgeMouseEnter}
onMouseMove={onEdgeMouseMove}
onMouseLeave={onEdgeMouseLeave}
>
<EdgeComponent
id={id}
source={source}
target={target}
selected={selected}
animated={animated}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
data={data}
style={style}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
markerStart={markerStartUrl}
markerEnd={markerEndUrl}
/>
{handleEdgeUpdate && (
<g
onMouseDown={onEdgeUpdaterSourceMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
>
<EdgeAnchor position={sourcePosition} centerX={sourceX} centerY={sourceY} radius={edgeUpdaterRadius} />
</g>
)}
{handleEdgeUpdate && (
<g
onMouseDown={onEdgeUpdaterTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
>
<EdgeAnchor position={targetPosition} centerX={targetX} centerY={targetY} radius={edgeUpdaterRadius} />
</g>
)}
</g>
);
};
EdgeWrapper.displayName = 'EdgeWrapper';
return memo(EdgeWrapper);
};
@@ -0,0 +1,199 @@
import { MouseEvent as ReactMouseEvent } from 'react';
import { SetState } from 'zustand';
import { getHostForElement } from '../../utils';
import {
OnConnect,
OnConnectStart,
OnConnectStop,
OnConnectEnd,
ConnectionMode,
Connection,
HandleType,
ReactFlowState,
} from '../../types';
type ValidConnectionFunc = (connection: Connection) => boolean;
type Result = {
elementBelow: Element | null;
isValid: boolean;
connection: Connection;
isHoveringHandle: boolean;
};
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
export function checkElementBelowIsValid(
event: MouseEvent,
connectionMode: ConnectionMode,
isTarget: boolean,
nodeId: string,
handleId: string | null,
isValidConnection: ValidConnectionFunc,
doc: Document | ShadowRoot
) {
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
const elementBelowIsTarget = elementBelow?.classList.contains('target') || false;
const elementBelowIsSource = elementBelow?.classList.contains('source') || false;
const result: Result = {
elementBelow,
isValid: false,
connection: { source: null, target: null, sourceHandle: null, targetHandle: null },
isHoveringHandle: false,
};
if (elementBelow && (elementBelowIsTarget || elementBelowIsSource)) {
result.isHoveringHandle = true;
// in strict mode we don't allow target to target or source to source connections
const isValid =
connectionMode === ConnectionMode.Strict
? (isTarget && elementBelowIsSource) || (!isTarget && elementBelowIsTarget)
: true;
if (isValid) {
const elementBelowNodeId = elementBelow.getAttribute('data-nodeid');
const elementBelowHandleId = elementBelow.getAttribute('data-handleid');
const connection: Connection = isTarget
? {
source: elementBelowNodeId,
sourceHandle: elementBelowHandleId,
target: nodeId,
targetHandle: handleId,
}
: {
source: nodeId,
sourceHandle: handleId,
target: elementBelowNodeId,
targetHandle: elementBelowHandleId,
};
result.connection = connection;
result.isValid = isValidConnection(connection);
}
}
return result;
}
function resetRecentHandle(hoveredHandle: Element): void {
hoveredHandle?.classList.remove('react-flow__handle-valid');
hoveredHandle?.classList.remove('react-flow__handle-connecting');
}
export function handleMouseDown(
event: ReactMouseEvent,
handleId: string | null,
nodeId: string,
setState: SetState<ReactFlowState>,
onConnect: OnConnect,
isTarget: boolean,
isValidConnection: ValidConnectionFunc,
connectionMode: ConnectionMode,
elementEdgeUpdaterType?: HandleType,
onEdgeUpdateEnd?: (evt: MouseEvent) => void,
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
const doc = getHostForElement(event.target as HTMLElement);
if (!doc) {
return;
}
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
const elementBelowIsTarget = elementBelow?.classList.contains('target');
const elementBelowIsSource = elementBelow?.classList.contains('source');
if (!reactFlowNode || (!elementBelowIsTarget && !elementBelowIsSource && !elementEdgeUpdaterType)) {
return;
}
const handleType = elementEdgeUpdaterType ? elementEdgeUpdaterType : elementBelowIsTarget ? 'target' : 'source';
const containerBounds = reactFlowNode.getBoundingClientRect();
let recentHoveredHandle: Element;
setState({
connectionPosition: {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
},
connectionNodeId: nodeId,
connectionHandleId: handleId,
connectionHandleType: handleType,
});
onConnectStart?.(event, { nodeId, handleId, handleType });
function onMouseMove(event: MouseEvent) {
setState({
connectionPosition: {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
},
});
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
event,
connectionMode,
isTarget,
nodeId,
handleId,
isValidConnection,
doc
);
if (!isHoveringHandle) {
return resetRecentHandle(recentHoveredHandle);
}
const isOwnHandle = connection.source === connection.target;
if (!isOwnHandle && elementBelow) {
recentHoveredHandle = elementBelow;
elementBelow.classList.add('react-flow__handle-connecting');
elementBelow.classList.toggle('react-flow__handle-valid', isValid);
}
}
function onMouseUp(event: MouseEvent) {
const { connection, isValid } = checkElementBelowIsValid(
event,
connectionMode,
isTarget,
nodeId,
handleId,
isValidConnection,
doc
);
onConnectStop?.(event);
if (isValid) {
onConnect?.(connection);
}
onConnectEnd?.(event);
if (elementEdgeUpdaterType && onEdgeUpdateEnd) {
onEdgeUpdateEnd(event);
}
resetRecentHandle(recentHoveredHandle);
setState({
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: null,
});
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject);
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject);
}
doc.addEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject);
doc.addEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject);
}
@@ -0,0 +1,166 @@
import React, { memo, useContext, HTMLAttributes, forwardRef } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
import NodeIdContext from '../../contexts/NodeIdContext';
import { HandleProps, Connection, ReactFlowState, Position } from '../../types';
import { checkElementBelowIsValid, handleMouseDown } 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,
onClickConnectStart: s.onClickConnectStart,
onClickConnectStop: s.onClickConnectStop,
onClickConnectEnd: s.onClickConnectEnd,
connectionMode: s.connectionMode,
connectionStartHandle: s.connectionStartHandle,
connectOnClick: s.connectOnClick,
hasDefaultEdges: s.hasDefaultEdges,
});
const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
(
{
type = 'source',
position = Position.Top,
isValidConnection = alwaysValid,
isConnectable = true,
id,
onConnect,
children,
className,
onMouseDown,
...rest
},
ref
) => {
const store = useStoreApi();
const nodeId = useContext(NodeIdContext) as string;
const {
onConnectAction,
onConnectStart,
onConnectStop,
onConnectEnd,
onClickConnectStart,
onClickConnectStop,
onClickConnectEnd,
connectionMode,
connectionStartHandle,
connectOnClick,
hasDefaultEdges,
} = useStore(selector, shallow);
const handleId = id || null;
const isTarget = type === 'target';
const onConnectExtended = (params: Connection) => {
const { defaultEdgeOptions } = store.getState();
const edgeParams = {
...defaultEdgeOptions,
...params,
};
if (hasDefaultEdges) {
const { edges } = store.getState();
store.setState({ edges: addEdge(edgeParams, edges) });
}
onConnectAction?.(edgeParams);
onConnect?.(edgeParams);
};
const onMouseDownHandler = (event: React.MouseEvent<HTMLDivElement>) => {
if (event.button === 0) {
handleMouseDown(
event,
handleId,
nodeId,
store.setState,
onConnectExtended,
isTarget,
isValidConnection,
connectionMode,
undefined,
undefined,
onConnectStart,
onConnectStop,
onConnectEnd
);
}
onMouseDown?.(event);
};
const onClick = (event: React.MouseEvent) => {
if (!connectionStartHandle) {
onClickConnectStart?.(event, { nodeId, handleId, handleType: type });
store.setState({ connectionStartHandle: { nodeId, type, handleId } });
return;
}
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
);
onClickConnectStop?.(event as unknown as MouseEvent);
if (isValid) {
onConnectExtended(connection);
}
onClickConnectEnd?.(event as unknown as MouseEvent);
store.setState({ connectionStartHandle: null });
};
const handleClasses = cc([
'react-flow__handle',
`react-flow__handle-${position}`,
'nodrag',
className,
{
source: !isTarget,
target: isTarget,
connectable: isConnectable,
connecting:
connectionStartHandle?.nodeId === nodeId &&
connectionStartHandle?.handleId === handleId &&
connectionStartHandle?.type === type,
},
]);
return (
<div
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
className={handleClasses}
onMouseDown={onMouseDownHandler}
onClick={connectOnClick ? onClick : undefined}
ref={ref}
{...rest}
>
{children}
</div>
);
}
);
Handle.displayName = 'Handle';
export default memo(Handle);
@@ -0,0 +1,23 @@
import React, { memo } from 'react';
import Handle from '../../components/Handle';
import { NodeProps, Position } from '../../types';
const DefaultNode = ({
data,
isConnectable,
targetPosition = Position.Top,
sourcePosition = Position.Bottom,
}: NodeProps) => {
return (
<>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
{data?.label}
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
</>
);
};
DefaultNode.displayName = 'DefaultNode';
export default memo(DefaultNode);
@@ -0,0 +1,5 @@
const GroupNode = () => null;
GroupNode.displayName = 'GroupNode';
export default GroupNode;
@@ -0,0 +1,15 @@
import React, { memo } from 'react';
import Handle from '../../components/Handle';
import { NodeProps, Position } from '../../types';
const InputNode = ({ data, isConnectable, sourcePosition = Position.Bottom }: NodeProps) => (
<>
{data?.label}
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
</>
);
InputNode.displayName = 'InputNode';
export default memo(InputNode);
@@ -0,0 +1,15 @@
import React, { memo } from 'react';
import Handle from '../../components/Handle';
import { NodeProps, Position } from '../../types';
const OutputNode = ({ data, isConnectable, targetPosition = Position.Top }: NodeProps) => (
<>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
{data?.label}
</>
);
OutputNode.displayName = 'OutputNode';
export default memo(OutputNode);
@@ -0,0 +1,83 @@
import { MouseEvent } from 'react';
import { GetState, SetState } from 'zustand';
import { HandleElement, Node, Position, ReactFlowState } from '../../types';
import { getDimensions } from '../../utils';
export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => {
const bounds = nodeElement.getBoundingClientRect();
return {
source: getHandleBoundsByHandleType('.source', nodeElement, bounds, scale),
target: getHandleBoundsByHandleType('.target', nodeElement, bounds, scale),
};
};
export const getHandleBoundsByHandleType = (
selector: string,
nodeElement: HTMLDivElement,
parentBounds: DOMRect,
k: number
): HandleElement[] | null => {
const handles = nodeElement.querySelectorAll(selector);
if (!handles || !handles.length) {
return null;
}
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 {
id: handleId,
position: handlePosition,
x: (bounds.left - parentBounds.left) / k,
y: (bounds.top - parentBounds.top) / k,
...dimensions,
};
});
};
export function getMouseHandler(
id: string,
getState: GetState<ReactFlowState>,
handler?: (event: MouseEvent, node: Node) => void
) {
return handler === undefined
? handler
: (event: MouseEvent) => {
const node = getState().nodeInternals.get(id)!;
handler(event, { ...node });
};
}
// this handler is called by
// 1. the click handler when node is not draggable or selectNodesOnDrag = false
// or
// 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
export function handleNodeClick({
id,
store,
}: {
id: string;
store: {
getState: GetState<ReactFlowState>;
setState: SetState<ReactFlowState>;
};
}) {
const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodeInternals } = store.getState();
const node = nodeInternals.get(id)!;
store.setState({ nodesSelectionActive: false });
if (!node.selected) {
addSelectedNodes([id]);
} else if (node.selected && multiSelectionActive) {
unselectNodesAndEdges({ nodes: [node] });
}
}
@@ -0,0 +1,172 @@
import React, { useEffect, useRef, memo, ComponentType, MouseEvent } from 'react';
import cc from 'classcat';
import { useStore, useStoreApi } from '../../store';
import { Provider } from '../../contexts/NodeIdContext';
import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types';
import useDrag from '../../hooks/useDrag';
import { getMouseHandler, handleNodeClick } from './utils';
const selector = (s: ReactFlowState) => s.updateNodeDimensions;
export default (NodeComponent: ComponentType<NodeProps>) => {
const NodeWrapper = ({
id,
type,
data,
xPos,
yPos,
selected,
onClick,
onMouseEnter,
onMouseMove,
onMouseLeave,
onContextMenu,
onDoubleClick,
onDragStart,
onDrag,
onDragStop,
style,
className,
isDraggable,
isSelectable,
isConnectable,
selectNodesOnDrag,
sourcePosition,
targetPosition,
hidden,
resizeObserver,
dragHandle,
zIndex,
isParent,
noPanClassName,
noDragClassName,
initialized,
}: WrapNodeProps) => {
const store = useStoreApi();
const updateNodeDimensions = useStore(selector);
const nodeRef = useRef<HTMLDivElement>(null);
const prevSourcePosition = useRef(sourcePosition);
const prevTargetPosition = useRef(targetPosition);
const prevType = useRef(type);
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const onMouseEnterHandler = getMouseHandler(id, store.getState, onMouseEnter);
const onMouseMoveHandler = getMouseHandler(id, store.getState, onMouseMove);
const onMouseLeaveHandler = getMouseHandler(id, store.getState, onMouseLeave);
const onContextMenuHandler = getMouseHandler(id, store.getState, onContextMenu);
const onDoubleClickHandler = getMouseHandler(id, store.getState, onDoubleClick);
const onSelectNodeHandler = (event: MouseEvent) => {
if (isSelectable && (!selectNodesOnDrag || !isDraggable)) {
// this handler gets called within the drag start event when selectNodesOnDrag=true
handleNodeClick({
id,
store,
});
}
if (onClick) {
const node = store.getState().nodeInternals.get(id)!;
onClick(event, { ...node });
}
};
useEffect(() => {
if (nodeRef.current && !hidden) {
const currNode = nodeRef.current;
resizeObserver?.observe(currNode);
return () => resizeObserver?.unobserve(currNode);
}
}, [hidden]);
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 (nodeRef.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
if (typeChanged) {
prevType.current = type;
}
if (sourcePosChanged) {
prevSourcePosition.current = sourcePosition;
}
if (targetPosChanged) {
prevTargetPosition.current = targetPosition;
}
updateNodeDimensions([{ id, nodeElement: nodeRef.current, forceUpdate: true }]);
}
}, [id, type, sourcePosition, targetPosition]);
const dragging = useDrag({
onStart: onDragStart,
onDrag: onDrag,
onStop: onDragStop,
nodeRef,
disabled: hidden || !isDraggable,
noDragClassName,
handleSelector: dragHandle,
nodeId: id,
isSelectable,
selectNodesOnDrag,
});
if (hidden) {
return null;
}
return (
<div
className={cc([
'react-flow__node',
`react-flow__node-${type}`,
noPanClassName,
className,
{
selected,
selectable: isSelectable,
parent: isParent,
},
])}
ref={nodeRef}
style={{
zIndex,
transform: `translate(${xPos}px,${yPos}px)`,
pointerEvents: hasPointerEvents ? 'all' : 'none',
visibility: initialized ? 'visible' : 'hidden',
...style,
}}
onMouseEnter={onMouseEnterHandler}
onMouseMove={onMouseMoveHandler}
onMouseLeave={onMouseLeaveHandler}
onContextMenu={onContextMenuHandler}
onClick={onSelectNodeHandler}
onDoubleClick={onDoubleClickHandler}
data-id={id}
>
<Provider value={id}>
<NodeComponent
id={id}
data={data}
type={type}
xPos={xPos}
yPos={yPos}
selected={selected}
isConnectable={isConnectable}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
dragging={dragging}
dragHandle={dragHandle}
zIndex={zIndex}
/>
</Provider>
</div>
);
};
NodeWrapper.displayName = 'NodeWrapper';
return memo(NodeWrapper);
};
@@ -0,0 +1,94 @@
/**
* The nodes selection rectangle gets displayed when a user
* made a selection with on or several nodes
*/
import React, { memo, useCallback, useRef, MouseEvent } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
import { Node, ReactFlowState } from '../../types';
import { getRectOfNodes } from '../../utils/graph';
import useDrag from '../../hooks/useDrag';
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;
}
const selector = (s: ReactFlowState) => ({
transform: s.transform,
userSelectionActive: s.userSelectionActive,
});
const bboxSelector = (s: ReactFlowState) => {
const selectedNodes = Array.from(s.nodeInternals.values()).filter((n) => n.selected);
return getRectOfNodes(selectedNodes);
};
function useGetMemoizedHandler(handler?: (event: MouseEvent, nodes: Node[]) => void) {
return useCallback((event: MouseEvent, _: Node, nodes: Node[]) => handler?.(event, nodes), [handler]);
}
function NodesSelection({
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
noPanClassName,
}: NodesSelectionProps) {
const store = useStoreApi();
const { transform, userSelectionActive } = useStore(selector, shallow);
const { width, height, x: left, y: top } = useStore(bboxSelector, shallow);
const nodeRef = useRef(null);
// it's important that these handlers are memoized to avoid multiple creation of d3 drag handler
const onStart = useGetMemoizedHandler(onSelectionDragStart);
const onDrag = useGetMemoizedHandler(onSelectionDrag);
const onStop = useGetMemoizedHandler(onSelectionDragStop);
useDrag({
onStart,
onDrag,
onStop,
nodeRef,
});
if (userSelectionActive || !width || !height) {
return null;
}
const onContextMenu = onSelectionContextMenu
? (event: MouseEvent) => {
const selectedNodes = Array.from(store.getState().nodeInternals.values()).filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes);
}
: undefined;
return (
<div
className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])}
style={{
transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`,
}}
>
<div
ref={nodeRef}
className="react-flow__nodesselection-rect"
onContextMenu={onContextMenu}
style={{
width,
height,
top,
left,
}}
/>
</div>
);
}
export default memo(NodesSelection);
@@ -0,0 +1,38 @@
import { memo, useEffect } from 'react';
import shallow from 'zustand/shallow';
import { ReactFlowState, OnSelectionChangeFunc, Node, Edge } from '../../types';
import { useStore } from '../../store';
interface SelectionListenerProps {
onSelectionChange: OnSelectionChangeFunc;
}
const selector = (s: ReactFlowState) => ({
selectedNodes: Array.from(s.nodeInternals.values()).filter((n) => n.selected),
selectedEdges: s.edges.filter((e) => e.selected),
});
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({ nodes: selectedNodes, edges: selectedEdges });
}, [selectedNodes, selectedEdges]);
return null;
}
export default memo(SelectionListener);
@@ -0,0 +1,170 @@
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;
onClickConnectStart?: OnConnectStart;
onClickConnectStop?: OnConnectStop;
onClickConnectEnd?: 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,
onClickConnectStart,
onClickConnectStop,
onClickConnectEnd,
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('onClickConnectStart', onClickConnectStart, store.setState);
useDirectStoreUpdater('onClickConnectStop', onClickConnectStop, store.setState);
useDirectStoreUpdater('onClickConnectEnd', onClickConnectEnd, 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;
@@ -0,0 +1,159 @@
/**
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
*/
import React, { memo, useState, useRef } from 'react';
import shallow from 'zustand/shallow';
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, containerBounds: DOMRect): XYPosition {
return {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
};
}
const selector = (s: ReactFlowState) => ({
userSelectionActive: s.userSelectionActive,
elementsSelectable: s.elementsSelectable,
});
const initialRect: SelectionRect = {
startX: 0,
startY: 0,
x: 0,
y: 0,
width: 0,
height: 0,
draw: false,
};
export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
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 renderUserSelectionPane = userSelectionActive || selectionKeyPressed;
if (!elementsSelectable || !renderUserSelectionPane) {
return null;
}
const resetUserSelection = () => {
setUserSelectionRect(initialRect);
store.setState({ userSelectionActive: false });
prevSelectedNodesCount.current = 0;
prevSelectedEdgesCount.current = 0;
};
const onMouseDown = (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.values());
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 = () => {
store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 });
resetUserSelection();
};
const onMouseLeave = () => {
store.setState({ nodesSelectionActive: false });
resetUserSelection();
};
return (
<div
className="react-flow__selectionpane react-flow__container"
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
onMouseLeave={onMouseLeave}
>
{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>
);
});
@@ -0,0 +1,96 @@
import React, { memo, useCallback } from 'react';
import { useStore } from '../../store';
import { EdgeMarker, ReactFlowState } from '../../types';
import { getMarkerId } from '../../utils/graph';
import { useMarkerSymbol } from './MarkerSymbols';
interface MarkerProps extends EdgeMarker {
id: string;
}
interface MarkerDefinitionsProps {
defaultColor: string;
rfId?: string;
}
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 markerSelector =
({ defaultColor, rfId }: { defaultColor: string; rfId?: string }) =>
(s: ReactFlowState) => {
const ids: string[] = [];
return s.edges
.reduce<MarkerProps[]>((markers, edge) => {
[edge.markerStart, edge.markerEnd].forEach((marker) => {
if (marker && typeof marker === 'object') {
const markerId = getMarkerId(marker, rfId);
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));
};
// when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore
// when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper
// that we can then use for creating our unique marker ids
const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => {
const markers = useStore(
useCallback(markerSelector({ defaultColor, rfId }), [defaultColor, rfId]),
// the id includes all marker options, so we just need to look at that part of the marker
(a, b) => !(a.length !== b.length || a.some((m, i) => m.id !== b[i].id))
);
return (
<defs>
{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}
/>
))}
</defs>
);
};
MarkerDefinitions.displayName = 'MarkerDefinitions';
export default memo(MarkerDefinitions);
@@ -0,0 +1,54 @@
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) {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(`[React Flow]: Marker type "${type}" doesn't exist. Help: https://reactflow.dev/error#900`);
}
return () => null;
}
return MarkerSymbols[type];
}, [type]);
return symbol;
}
export default MarkerSymbols;
@@ -0,0 +1,221 @@
import React, { memo, CSSProperties } from 'react';
import shallow from 'zustand/shallow';
import cc from 'classcat';
import { useStore } from '../../store';
import ConnectionLine from '../../components/ConnectionLine/index';
import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getHandle, getNodeData } from './utils';
import useVisibleEdges from '../../hooks/useVisibleEdges';
import {
Position,
Edge,
ConnectionLineType,
ConnectionLineComponent,
ConnectionMode,
OnEdgeUpdateFunc,
HandleType,
ReactFlowState,
EdgeTypesWrapped,
} from '../../types';
interface EdgeRendererProps {
edgeTypes: EdgeTypesWrapped;
connectionLineType: ConnectionLineType;
connectionLineStyle?: CSSProperties;
connectionLineComponent?: ConnectionLineComponent;
connectionLineContainerStyle?: CSSProperties;
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void;
defaultMarkerColor: string;
onlyRenderVisibleElements: boolean;
onEdgeUpdate?: OnEdgeUpdateFunc;
onEdgeContextMenu?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeMouseEnter?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeMouseMove?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeMouseLeave?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void;
edgeUpdaterRadius?: number;
noPanClassName?: string;
elevateEdgesOnSelect: boolean;
rfId?: string;
}
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 {
connectionNodeId,
connectionHandleId,
connectionHandleType,
connectionPosition,
nodesConnectable,
elementsSelectable,
width,
height,
connectionMode,
nodeInternals,
} = useStore(selector, shallow);
const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals, props.elevateEdgesOnSelect);
if (!width) {
return null;
}
const {
connectionLineType,
defaultMarkerColor,
connectionLineStyle,
connectionLineComponent,
connectionLineContainerStyle,
} = props;
const renderConnectionLine = connectionNodeId && connectionHandleType;
return (
<>
{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} rfId={props.rfId} />}
<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) {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
`[React Flow]: Couldn't create edge for source handle id: ${edge.sourceHandle}; edge id: ${edge.id}. Help: https://reactflow.dev/error#800`
);
}
return null;
}
if (!targetHandle) {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
`[React Flow]: Couldn't create edge for target handle id: ${edge.targetHandle}; edge id: ${edge.id}. Help: https://reactflow.dev/error#800`
);
}
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}
rfId={props.rfId}
/>
);
})}
</g>
</svg>
))}
{renderConnectionLine && (
<svg
style={connectionLineContainerStyle}
width={width}
height={height}
className="react-flow__edges react-flow__connectionline react-flow__container"
>
<ConnectionLine
connectionNodeId={connectionNodeId!}
connectionHandleId={connectionHandleId}
connectionHandleType={connectionHandleType!}
connectionPositionX={connectionPosition.x}
connectionPositionY={connectionPosition.y}
connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType}
isConnectable={nodesConnectable}
CustomConnectionLineComponent={connectionLineComponent}
/>
</svg>
)}
</>
);
};
EdgeRenderer.displayName = 'EdgeRenderer';
export default memo(EdgeRenderer);
@@ -0,0 +1,191 @@
import { ComponentType } from 'react';
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge, SimpleBezierEdge } from '../../components/Edges';
import wrapEdge from '../../components/Edges/wrapEdge';
import {
EdgeProps,
EdgeTypes,
EdgeTypesWrapped,
HandleElement,
NodeHandleBounds,
NodeInternals,
Position,
Rect,
Transform,
XYPosition,
} from '../../types';
import { internalsSymbol, rectToBox } from '../../utils';
export type CreateEdgeTypes = (edgeTypes: EdgeTypes) => EdgeTypesWrapped;
export function createEdgeTypes(edgeTypes: EdgeTypes): EdgeTypesWrapped {
const standardTypes: EdgeTypesWrapped = {
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 EdgeTypesWrapped;
const specialTypes: EdgeTypesWrapped = Object.keys(edgeTypes)
.filter((k) => !['default', 'bezier'].includes(k))
.reduce((res, key) => {
res[key] = wrapEdge((edgeTypes[key] || BezierEdge) as ComponentType<EdgeProps>);
return res;
}, wrappedTypes);
return {
...standardTypes,
...specialTypes,
};
}
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:
return {
x: x + width / 2,
y,
};
case Position.Right:
return {
x: x + width,
y: y + height / 2,
};
case Position.Bottom:
return {
x: x + width / 2,
y: y + height,
};
case Position.Left:
return {
x,
y: y + height / 2,
};
}
}
export function getHandle(bounds: HandleElement[], handleId: string | null): HandleElement | null {
if (!bounds) {
return null;
}
// there is no handleId when there are no multiple handles/ handles with ids
// so we just pick the first one
let handle = null;
if (bounds.length === 1 || !handleId) {
handle = bounds[0];
} else if (handleId) {
handle = bounds.find((d) => d.id === handleId);
}
return typeof handle === 'undefined' ? null : handle;
}
interface EdgePositions {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
}
export const getEdgePositions = (
sourceNodeRect: Rect,
sourceHandle: HandleElement | unknown,
sourcePosition: Position,
targetNodeRect: Rect,
targetHandle: HandleElement | unknown,
targetPosition: Position
): EdgePositions => {
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
const targetHandlePos = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
return {
sourceX: sourceHandlePos.x,
sourceY: sourceHandlePos.y,
targetX: targetHandlePos.x,
targetY: targetHandlePos.y,
};
};
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,
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 + sourceWidth, targetPos.x + targetWidth),
y2: Math.max(sourcePos.y + sourceHeight, targetPos.y + targetHeight),
};
if (edgeBox.x === edgeBox.x2) {
edgeBox.x2 += 1;
}
if (edgeBox.y === edgeBox.y2) {
edgeBox.y2 += 1;
}
const viewBox = rectToBox({
x: (0 - transform[0]) / transform[2],
y: (0 - transform[1]) / transform[2],
width: width / transform[2],
height: height / transform[2],
});
const xOverlap = Math.max(0, Math.min(viewBox.x2, edgeBox.x2) - Math.max(viewBox.x, edgeBox.x));
const yOverlap = Math.max(0, Math.min(viewBox.y2, edgeBox.y2) - Math.max(viewBox.y, edgeBox.y));
const overlappingArea = Math.ceil(xOverlap * yOverlap);
return overlappingArea > 0;
}
export function getNodeData(nodeInternals: NodeInternals, nodeId: string): [Rect, NodeHandleBounds | null, boolean] {
const node = nodeInternals.get(nodeId);
const handleBounds = node?.[internalsSymbol]?.handleBounds || null;
const isInvalid =
!node ||
!handleBounds ||
!node.width ||
!node.height ||
typeof node.positionAbsolute?.x === 'undefined' ||
typeof node.positionAbsolute?.y === 'undefined';
return [
{
x: node?.positionAbsolute?.x || 0,
y: node?.positionAbsolute?.y || 0,
width: node?.width || 0,
height: node?.height || 0,
},
handleBounds,
!isInvalid,
];
}
@@ -0,0 +1,124 @@
import React, { memo, ReactNode, WheelEvent, MouseEvent } from 'react';
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';
export type FlowRendererProps = Omit<
GraphViewProps,
| 'snapToGrid'
| 'nodeTypes'
| 'edgeTypes'
| 'snapGrid'
| 'connectionLineType'
| 'connectionLineContainerStyle'
| 'arrowHeadColor'
| 'onlyRenderVisibleElements'
| 'selectNodesOnDrag'
| 'defaultMarkerColor'
> & {
children: ReactNode;
};
const selector = (s: ReactFlowState) => ({
resetSelectedElements: s.resetSelectedElements,
nodesSelectionActive: s.nodesSelectionActive,
});
const FlowRenderer = ({
children,
onPaneClick,
onPaneContextMenu,
onPaneScroll,
deleteKeyCode,
onMove,
onMoveStart,
onMoveEnd,
selectionKeyCode,
multiSelectionKeyCode,
zoomActivationKeyCode,
elementsSelectable,
zoomOnScroll,
zoomOnPinch,
panOnScroll,
panOnScrollSpeed,
panOnScrollMode,
zoomOnDoubleClick,
panOnDrag,
defaultPosition,
defaultZoom,
preventScrolling,
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
noWheelClassName,
noPanClassName,
}: FlowRendererProps) => {
const store = useStoreApi();
const { resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow);
const selectionKeyPressed = useKeyPress(selectionKeyCode);
useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode });
const onClick = (event: MouseEvent) => {
onPaneClick?.(event);
resetSelectedElements();
store.setState({ nodesSelectionActive: false });
};
const onContextMenu = (event: MouseEvent) => onPaneContextMenu?.(event);
const onWheel = (event: WheelEvent) => onPaneScroll?.(event);
return (
<ZoomPane
onMove={onMove}
onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd}
selectionKeyPressed={selectionKeyPressed}
elementsSelectable={elementsSelectable}
zoomOnScroll={zoomOnScroll}
zoomOnPinch={zoomOnPinch}
panOnScroll={panOnScroll}
panOnScrollSpeed={panOnScrollSpeed}
panOnScrollMode={panOnScrollMode}
zoomOnDoubleClick={zoomOnDoubleClick}
panOnDrag={panOnDrag}
defaultPosition={defaultPosition}
defaultZoom={defaultZoom}
zoomActivationKeyCode={zoomActivationKeyCode}
preventScrolling={preventScrolling}
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
>
{children}
<UserSelection selectionKeyPressed={selectionKeyPressed} />
{nodesSelectionActive && (
<NodesSelection
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
noPanClassName={noPanClassName}
/>
)}
<div
className="react-flow__pane react-flow__container"
onClick={onClick}
onContextMenu={onContextMenu}
onWheel={onWheel}
/>
</ZoomPane>
);
};
FlowRenderer.displayName = 'FlowRenderer';
export default memo(FlowRenderer);
@@ -0,0 +1,168 @@
import React, { memo } from 'react';
import FlowRenderer from '../FlowRenderer';
import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer';
import Viewport from '../Viewport';
import useOnInitHandler from '../../hooks/useOnInitHandler';
import { NodeTypesWrapped, EdgeTypesWrapped, ConnectionLineType, KeyCode, ReactFlowProps } from '../../types';
export interface GraphViewProps
extends Omit<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes'> {
nodeTypes: NodeTypesWrapped;
edgeTypes: EdgeTypesWrapped;
selectionKeyCode: KeyCode | null;
deleteKeyCode: KeyCode | null;
multiSelectionKeyCode: KeyCode | null;
connectionLineType: ConnectionLineType;
onlyRenderVisibleElements: boolean;
defaultZoom: number;
defaultPosition: [number, number];
defaultMarkerColor: string;
selectNodesOnDrag: boolean;
noDragClassName: string;
noWheelClassName: string;
noPanClassName: string;
}
const GraphView = ({
nodeTypes,
edgeTypes,
onMove,
onMoveStart,
onMoveEnd,
onInit,
onNodeClick,
onEdgeClick,
onNodeDoubleClick,
onEdgeDoubleClick,
onNodeMouseEnter,
onNodeMouseMove,
onNodeMouseLeave,
onNodeContextMenu,
onNodeDragStart,
onNodeDrag,
onNodeDragStop,
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
connectionLineType,
connectionLineStyle,
connectionLineComponent,
connectionLineContainerStyle,
selectionKeyCode,
multiSelectionKeyCode,
zoomActivationKeyCode,
deleteKeyCode,
onlyRenderVisibleElements,
elementsSelectable,
selectNodesOnDrag,
defaultZoom,
defaultPosition,
preventScrolling,
defaultMarkerColor,
zoomOnScroll,
zoomOnPinch,
panOnScroll,
panOnScrollSpeed,
panOnScrollMode,
zoomOnDoubleClick,
panOnDrag,
onPaneClick,
onPaneScroll,
onPaneContextMenu,
onEdgeUpdate,
onEdgeContextMenu,
onEdgeMouseEnter,
onEdgeMouseMove,
onEdgeMouseLeave,
edgeUpdaterRadius,
onEdgeUpdateStart,
onEdgeUpdateEnd,
noDragClassName,
noWheelClassName,
noPanClassName,
elevateEdgesOnSelect,
id,
}: GraphViewProps) => {
useOnInitHandler(onInit);
return (
<FlowRenderer
onPaneClick={onPaneClick}
onPaneContextMenu={onPaneContextMenu}
onPaneScroll={onPaneScroll}
deleteKeyCode={deleteKeyCode}
selectionKeyCode={selectionKeyCode}
multiSelectionKeyCode={multiSelectionKeyCode}
zoomActivationKeyCode={zoomActivationKeyCode}
elementsSelectable={elementsSelectable}
onMove={onMove}
onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd}
zoomOnScroll={zoomOnScroll}
zoomOnPinch={zoomOnPinch}
zoomOnDoubleClick={zoomOnDoubleClick}
panOnScroll={panOnScroll}
panOnScrollSpeed={panOnScrollSpeed}
panOnScrollMode={panOnScrollMode}
panOnDrag={panOnDrag}
defaultPosition={defaultPosition}
defaultZoom={defaultZoom}
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
preventScrolling={preventScrolling}
noDragClassName={noDragClassName}
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
>
<Viewport>
<EdgeRenderer
edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
connectionLineContainerStyle={connectionLineContainerStyle}
onEdgeUpdate={onEdgeUpdate}
onlyRenderVisibleElements={onlyRenderVisibleElements}
onEdgeContextMenu={onEdgeContextMenu}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
defaultMarkerColor={defaultMarkerColor}
noPanClassName={noPanClassName}
elevateEdgesOnSelect={!!elevateEdgesOnSelect}
rfId={id}
/>
<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>
);
};
GraphView.displayName = 'GraphView';
export default memo(GraphView);
@@ -0,0 +1,135 @@
import React, { memo, useMemo, ComponentType, useEffect, useRef } from 'react';
import shallow from 'zustand/shallow';
import useVisibleNodes from '../../hooks/useVisibleNodes';
import { useStore } from '../../store';
import {
NodeDragHandler,
NodeMouseHandler,
NodeTypesWrapped,
Position,
ReactFlowState,
WrapNodeProps,
} from '../../types';
import { internalsSymbol } from '../../utils';
interface NodeRendererProps {
nodeTypes: NodeTypesWrapped;
selectNodesOnDrag: boolean;
onNodeClick?: NodeMouseHandler;
onNodeDoubleClick?: NodeMouseHandler;
onNodeMouseEnter?: NodeMouseHandler;
onNodeMouseMove?: NodeMouseHandler;
onNodeMouseLeave?: NodeMouseHandler;
onNodeContextMenu?: NodeMouseHandler;
onNodeDragStart?: NodeDragHandler;
onNodeDrag?: NodeDragHandler;
onNodeDragStop?: NodeDragHandler;
onlyRenderVisibleElements: boolean;
noPanClassName: string;
noDragClassName: string;
}
const selector = (s: ReactFlowState) => ({
nodesDraggable: s.nodesDraggable,
nodesConnectable: s.nodesConnectable,
elementsSelectable: s.elementsSelectable,
updateNodeDimensions: s.updateNodeDimensions,
});
const NodeRenderer = (props: NodeRendererProps) => {
const { nodesDraggable, nodesConnectable, elementsSelectable, updateNodeDimensions } = useStore(selector, shallow);
const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
const resizeObserverRef = useRef<ResizeObserver>();
const resizeObserver = useMemo(() => {
if (typeof ResizeObserver === 'undefined') {
return null;
}
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 react-flow__container">
{nodes.map((node) => {
let nodeType = node.type || 'default';
if (!props.nodeTypes[nodeType]) {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
`[React Flow]: Node type "${nodeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300`
);
}
nodeType = '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'));
return (
<NodeComponent
key={node.id}
id={node.id}
className={node.className}
style={node.style}
type={nodeType}
data={node.data}
sourcePosition={node.sourcePosition || Position.Bottom}
targetPosition={node.targetPosition || Position.Top}
hidden={node.hidden}
xPos={node.positionAbsolute?.x ?? 0}
yPos={node.positionAbsolute?.y ?? 0}
selectNodesOnDrag={props.selectNodesOnDrag}
onClick={props.onNodeClick}
onMouseEnter={props.onNodeMouseEnter}
onMouseMove={props.onNodeMouseMove}
onMouseLeave={props.onNodeMouseLeave}
onContextMenu={props.onNodeContextMenu}
onDoubleClick={props.onNodeDoubleClick}
onDragStart={props.onNodeDragStart}
onDrag={props.onNodeDrag}
onDragStop={props.onNodeDragStop}
selected={!!node.selected}
isDraggable={isDraggable}
isSelectable={isSelectable}
isConnectable={isConnectable}
resizeObserver={resizeObserver}
dragHandle={node.dragHandle}
zIndex={node[internalsSymbol]?.z ?? 0}
isParent={!!node[internalsSymbol]?.isParent}
noDragClassName={props.noDragClassName}
noPanClassName={props.noPanClassName}
initialized={!!node.width && !!node.height}
/>
);
})}
</div>
);
};
NodeRenderer.displayName = 'NodeRenderer';
export default memo(NodeRenderer);
@@ -0,0 +1,33 @@
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 { NodeTypes, NodeProps, NodeTypesWrapped } from '../../types';
export type CreateNodeTypes = (nodeTypes: NodeTypes) => NodeTypesWrapped;
export function createNodeTypes(nodeTypes: NodeTypes): NodeTypesWrapped {
const standardTypes: NodeTypesWrapped = {
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 NodeTypesWrapped;
const specialTypes: NodeTypesWrapped = Object.keys(nodeTypes)
.filter((k) => !['input', 'default', 'output', 'group'].includes(k))
.reduce((res, key) => {
res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType<NodeProps>);
return res;
}, wrappedTypes);
return {
...standardTypes,
...specialTypes,
};
}
@@ -0,0 +1,25 @@
import React, { FC, PropsWithChildren } from 'react';
import { Provider, createStore, useStoreApi } from '../../store';
const Wrapper: FC<PropsWithChildren<{}>> = ({ children }) => {
let isWrapped = true;
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 createStore={createStore}>{children}</Provider>;
};
Wrapper.displayName = 'ReactFlowWrapper';
export default Wrapper;
@@ -0,0 +1,258 @@
import cc from 'classcat';
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 SelectionListener from '../../components/SelectionListener';
import StoreUpdater from '../../components/StoreUpdater';
// import css from '../../style.css';
// import theme from '../../theme-default.css';
import {
ConnectionLineType,
ConnectionMode,
EdgeTypes,
EdgeTypesWrapped,
NodeTypes,
NodeTypesWrapped,
PanOnScrollMode,
ReactFlowProps,
ReactFlowRefType,
} from '../../types';
import { createEdgeTypes } from '../EdgeRenderer/utils';
import GraphView from '../GraphView';
import { createNodeTypes } from '../NodeRenderer/utils';
import { useNodeOrEdgeTypes } from './utils';
import Wrapper from './Wrapper';
// if (__INJECT_STYLES__) {
// injectStyle(css as unknown as string);
// injectStyle(theme as unknown as string);
// }
const defaultNodeTypes: NodeTypes = {
input: InputNode,
default: DefaultNode,
output: OutputNode,
};
const defaultEdgeTypes: EdgeTypes = {
default: BezierEdge,
straight: StraightEdge,
step: StepEdge,
smoothstep: SmoothStepEdge,
simplebezier: SimpleBezierEdge,
};
const initSnapGrid: [number, number] = [15, 15];
const initDefaultPosition: [number, number] = [0, 0];
const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
(
{
nodes,
edges,
defaultNodes,
defaultEdges,
className,
nodeTypes = defaultNodeTypes,
edgeTypes = defaultEdgeTypes,
onNodeClick,
onEdgeClick,
onInit,
onMove,
onMoveStart,
onMoveEnd,
onConnect,
onConnectStart,
onConnectStop,
onConnectEnd,
onClickConnectStart,
onClickConnectStop,
onClickConnectEnd,
onNodeMouseEnter,
onNodeMouseMove,
onNodeMouseLeave,
onNodeContextMenu,
onNodeDoubleClick,
onNodeDragStart,
onNodeDrag,
onNodeDragStop,
onNodesDelete,
onEdgesDelete,
onSelectionChange,
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
connectionMode = ConnectionMode.Strict,
connectionLineType = ConnectionLineType.Bezier,
connectionLineStyle,
connectionLineComponent,
connectionLineContainerStyle,
deleteKeyCode = 'Backspace',
selectionKeyCode = 'Shift',
multiSelectionKeyCode = 'Meta',
zoomActivationKeyCode = 'Meta',
snapToGrid = false,
snapGrid = initSnapGrid,
onlyRenderVisibleElements = false,
selectNodesOnDrag = true,
nodesDraggable,
nodesConnectable,
elementsSelectable,
minZoom,
maxZoom,
defaultZoom = 1,
defaultPosition = initDefaultPosition,
translateExtent,
preventScrolling = true,
nodeExtent,
defaultMarkerColor = '#b1b1b7',
zoomOnScroll = true,
zoomOnPinch = true,
panOnScroll = false,
panOnScrollSpeed = 0.5,
panOnScrollMode = PanOnScrollMode.Free,
zoomOnDoubleClick = true,
panOnDrag = true,
onPaneClick,
onPaneScroll,
onPaneContextMenu,
children,
onEdgeUpdate,
onEdgeContextMenu,
onEdgeDoubleClick,
onEdgeMouseEnter,
onEdgeMouseMove,
onEdgeMouseLeave,
onEdgeUpdateStart,
onEdgeUpdateEnd,
edgeUpdaterRadius = 10,
onNodesChange,
onEdgesChange,
noDragClassName = 'nodrag',
noWheelClassName = 'nowheel',
noPanClassName = 'nopan',
fitView = false,
fitViewOptions,
connectOnClick = true,
attributionPosition,
proOptions,
defaultEdgeOptions,
elevateEdgesOnSelect = false,
...rest
},
ref
) => {
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes) as NodeTypesWrapped;
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes) as EdgeTypesWrapped;
const reactFlowClasses = cc(['react-flow', className]);
return (
<div {...rest} ref={ref} className={reactFlowClasses}>
<Wrapper>
<GraphView
onInit={onInit}
onMove={onMove}
onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseMove={onNodeMouseMove}
onNodeMouseLeave={onNodeMouseLeave}
onNodeContextMenu={onNodeContextMenu}
onNodeDoubleClick={onNodeDoubleClick}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
nodeTypes={nodeTypesWrapped}
edgeTypes={edgeTypesWrapped}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
connectionLineContainerStyle={connectionLineContainerStyle}
selectionKeyCode={selectionKeyCode}
deleteKeyCode={deleteKeyCode}
multiSelectionKeyCode={multiSelectionKeyCode}
zoomActivationKeyCode={zoomActivationKeyCode}
onlyRenderVisibleElements={onlyRenderVisibleElements}
selectNodesOnDrag={selectNodesOnDrag}
defaultZoom={defaultZoom}
defaultPosition={defaultPosition}
preventScrolling={preventScrolling}
zoomOnScroll={zoomOnScroll}
zoomOnPinch={zoomOnPinch}
zoomOnDoubleClick={zoomOnDoubleClick}
panOnScroll={panOnScroll}
panOnScrollSpeed={panOnScrollSpeed}
panOnScrollMode={panOnScrollMode}
panOnDrag={panOnDrag}
onPaneClick={onPaneClick}
onPaneScroll={onPaneScroll}
onPaneContextMenu={onPaneContextMenu}
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
onEdgeUpdate={onEdgeUpdate}
onEdgeContextMenu={onEdgeContextMenu}
onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
defaultMarkerColor={defaultMarkerColor}
noDragClassName={noDragClassName}
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
elevateEdgesOnSelect={elevateEdgesOnSelect}
id={rest?.id}
/>
<StoreUpdater
nodes={nodes}
edges={edges}
defaultNodes={defaultNodes}
defaultEdges={defaultEdges}
onConnect={onConnect}
onConnectStart={onConnectStart}
onConnectStop={onConnectStop}
onConnectEnd={onConnectEnd}
onClickConnectStart={onClickConnectStart}
onClickConnectStop={onClickConnectStop}
onClickConnectEnd={onClickConnectEnd}
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}
/>
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
{children}
<Attribution proOptions={proOptions} position={attributionPosition} />
</Wrapper>
</div>
);
}
);
ReactFlow.displayName = 'ReactFlow';
export default ReactFlow;
@@ -0,0 +1,40 @@
import { useMemo, useRef } from 'react';
import shallow from 'zustand/shallow';
import { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
import { CreateEdgeTypes } from '../EdgeRenderer/utils';
import { CreateNodeTypes } from '../NodeRenderer/utils';
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped;
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped;
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 you have 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. Help: https://reactflow.dev/error#200"
);
}
typesKeysRef.current = typeKeys;
}
return createTypes(nodeOrEdgeTypes);
}, [nodeOrEdgeTypes]);
return typesParsed;
}
export 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));
}
@@ -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;
@@ -0,0 +1,264 @@
import React, { useEffect, useRef } from 'react';
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 { useStore, useStoreApi } from '../../store';
import { Viewport, PanOnScrollMode, ReactFlowState } from '../../types';
import { FlowRendererProps } from '../FlowRenderer';
type ZoomPaneProps = Omit<
FlowRendererProps,
'deleteKeyCode' | 'selectionKeyCode' | 'multiSelectionKeyCode' | 'noDragClassName'
> & { selectionKeyPressed: boolean };
const viewChanged = (prevViewport: Viewport, eventViewport: any): boolean =>
prevViewport.x !== eventViewport.x || prevViewport.y !== eventViewport.y || prevViewport.zoom !== eventViewport.k;
const eventToFlowTransform = (eventViewport: any): Viewport => ({
x: eventViewport.x,
y: eventViewport.y,
zoom: eventViewport.k,
});
const isWrappedWithClass = (event: any, className: string | undefined) => event.target.closest(`.${className}`);
const selector = (s: ReactFlowState) => ({
d3Zoom: s.d3Zoom,
d3Selection: s.d3Selection,
d3ZoomHandler: s.d3ZoomHandler,
});
const ZoomPane = ({
onMove,
onMoveStart,
onMoveEnd,
zoomOnScroll = true,
zoomOnPinch = true,
panOnScroll = false,
panOnScrollSpeed = 0.5,
panOnScrollMode = PanOnScrollMode.Free,
zoomOnDoubleClick = true,
selectionKeyPressed,
elementsSelectable,
panOnDrag = true,
defaultPosition = [0, 0],
defaultZoom = 1,
zoomActivationKeyCode,
preventScrolling = true,
children,
noWheelClassName,
noPanClassName,
}: ZoomPaneProps) => {
const store = useStoreApi();
const zoomPane = useRef<HTMLDivElement>(null);
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 { 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], 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);
store.setState({
d3Zoom: d3ZoomInstance,
d3Selection: selection,
d3ZoomHandler: selection.on('wheel.zoom'),
// we need to pass transform because zoom handler is not registered when we set the initial transform
transform: [clampedX, clampedY, clampedZoom],
});
}
}, []);
useEffect(() => {
if (d3Selection && d3Zoom) {
if (panOnScroll && !zoomActivationKeyPressed) {
d3Selection
.on('wheel', (event: any) => {
if (isWrappedWithClass(event, noWheelClassName)) {
return false;
}
event.preventDefault();
event.stopImmediatePropagation();
const currentZoom = d3Selection.property('__zoom').k || 1;
if (event.ctrlKey && zoomOnPinch) {
const point = pointer(event);
// taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js
const pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10;
const zoom = currentZoom * Math.pow(2, pinchDelta);
d3Zoom.scaleTo(d3Selection, zoom, point);
return;
}
// increase scroll speed in firefox
// firefox: deltaMode === 1; chrome: deltaMode === 0
const deltaNormalize = event.deltaMode === 1 ? 20 : 1;
const deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize;
const deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;
d3Zoom.translateBy(
d3Selection,
-(deltaX / currentZoom) * panOnScrollSpeed,
-(deltaY / currentZoom) * panOnScrollSpeed
);
})
.on('wheel.zoom', null);
} else if (typeof d3ZoomHandler !== 'undefined') {
d3Selection
.on('wheel', (event: any) => {
if (!preventScrolling || isWrappedWithClass(event, noWheelClassName)) {
return null;
}
event.preventDefault();
})
.on('wheel.zoom', d3ZoomHandler);
}
}
}, [
panOnScroll,
panOnScrollMode,
d3Selection,
d3Zoom,
d3ZoomHandler,
zoomActivationKeyPressed,
zoomOnPinch,
preventScrolling,
noWheelClassName,
]);
useEffect(() => {
if (d3Zoom) {
if (selectionKeyPressed) {
d3Zoom.on('zoom', null);
} else {
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(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
}
});
}
}
}, [selectionKeyPressed, d3Zoom, onMove]);
useEffect(() => {
if (d3Zoom) {
if (onMoveStart) {
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
const flowTransform = eventToFlowTransform(event.transform);
prevTransform.current = flowTransform;
onMoveStart(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
});
} else {
d3Zoom.on('start', null);
}
}
}, [d3Zoom, onMoveStart]);
useEffect(() => {
if (d3Zoom) {
if (onMoveEnd) {
d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
if (viewChanged(prevTransform.current, event.transform)) {
const flowTransform = eventToFlowTransform(event.transform);
prevTransform.current = flowTransform;
onMoveEnd(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
}
});
} else {
d3Zoom.on('end', null);
}
}
}, [d3Zoom, onMoveEnd]);
useEffect(() => {
if (d3Zoom) {
d3Zoom.filter((event: any) => {
const zoomScroll = zoomActivationKeyPressed || zoomOnScroll;
const pinchZoom = zoomOnPinch && event.ctrlKey;
// if all interactions are disabled, we prevent all zoom events
if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) {
return false;
}
// during a selection we prevent all other interactions
if (selectionKeyPressed) {
return false;
}
// if zoom on double click is disabled, we prevent the double click event
if (!zoomOnDoubleClick && event.type === 'dblclick') {
return false;
}
// if the target element is inside an element with the nowheel class, we prevent zooming
if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') {
return false;
}
// if the target element is inside an element with the nopan class, we prevent panning
if (isWrappedWithClass(event, noPanClassName) && event.type !== 'wheel') {
return false;
}
if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') {
return false;
}
// when there is no scroll handling enabled, we prevent all wheel events
if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') {
return false;
}
// if the pane is not movable, we prevent dragging it with mousestart or touchstart
if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) {
return false;
}
// default filter for d3-zoom
return (!event.ctrlKey || event.type === 'wheel') && !event.button;
});
}
}, [
d3Zoom,
zoomOnScroll,
zoomOnPinch,
panOnScroll,
zoomOnDoubleClick,
panOnDrag,
selectionKeyPressed,
elementsSelectable,
zoomActivationKeyPressed,
]);
return (
<div className="react-flow__renderer react-flow__container" ref={zoomPane}>
{children}
</div>
);
};
export default ZoomPane;
@@ -0,0 +1,7 @@
import { createContext } from 'react';
export const NodeIdContext = createContext<string | null>(null);
export const Provider = NodeIdContext.Provider;
export const Consumer = NodeIdContext.Consumer;
export default NodeIdContext;
+17
View File
@@ -0,0 +1,17 @@
declare module '*.css' {
const content: { [className: string]: string };
export default content;
}
interface SvgrComponent extends React.FunctionComponent<React.SVGAttributes<SVGElement>> {}
declare module '*.svg' {
const svgUrl: string;
const svgComponent: SvgrComponent;
export default svgUrl;
export { svgComponent as ReactComponent };
}
declare var __REACT_FLOW_VERSION__: string;
declare var __ENV__: string;
declare var __INJECT_STYLES__: boolean;
+166
View File
@@ -0,0 +1,166 @@
import { RefObject, useEffect, useRef, MouseEvent, useState, useCallback } from 'react';
import { D3DragEvent, drag, SubjectPosition } from 'd3-drag';
import { select } from 'd3-selection';
import { useStoreApi } from '../../store';
import { pointToRendererPoint } from '../../utils/graph';
import { NodeDragItem, NodeDragHandler } from '../../types';
import { getDragItems, getEventHandlerParams, hasSelector, updatePosition } from './utils';
import { handleNodeClick } from '../../components/Nodes/utils';
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
export type UseDragData = { dx: number; dy: number };
type UseDragParams = {
nodeRef: RefObject<Element>;
onStart?: NodeDragHandler;
onDrag?: NodeDragHandler;
onStop?: NodeDragHandler;
disabled?: boolean;
noDragClassName?: string;
handleSelector?: string;
nodeId?: string;
isSelectable?: boolean;
selectNodesOnDrag?: boolean;
};
function useDrag({
onStart,
onDrag,
onStop,
nodeRef,
disabled = false,
noDragClassName,
handleSelector,
nodeId,
isSelectable,
selectNodesOnDrag,
}: UseDragParams) {
const [dragging, setDragging] = useState<boolean>(false);
const store = useStoreApi();
const dragItems = useRef<NodeDragItem[]>();
const lastPos = useRef<{ x: number | null; y: number | null }>({ x: null, y: null });
// returns the pointer position projected to the RF coordinate system
const getPointerPosition = useCallback(({ sourceEvent }: UseDragEvent) => {
const { transform, snapGrid, snapToGrid } = store.getState();
const x = sourceEvent.touches ? sourceEvent.touches[0].clientX : sourceEvent.clientX;
const y = sourceEvent.touches ? sourceEvent.touches[0].clientY : sourceEvent.clientY;
const pointerPos = pointToRendererPoint({ x, y }, transform, snapToGrid, snapGrid);
return pointerPos;
}, []);
useEffect(() => {
if (nodeRef?.current) {
const selection = select(nodeRef.current);
if (disabled) {
selection.on('.drag', null);
} else {
const dragHandler = drag()
.on('start', (event: UseDragEvent) => {
const { nodeInternals, multiSelectionActive, unselectNodesAndEdges } = store.getState();
if (!selectNodesOnDrag && !multiSelectionActive && nodeId) {
if (!nodeInternals.get(nodeId)?.selected) {
// we need to reset selected nodes when selectNodesOnDrag=false
unselectNodesAndEdges();
}
}
if (nodeId && isSelectable && selectNodesOnDrag) {
handleNodeClick({
id: nodeId,
store,
});
}
const pointerPos = getPointerPosition(event);
dragItems.current = getDragItems(nodeInternals, pointerPos, nodeId);
if (onStart && dragItems.current) {
const [currentNode, nodes] = getEventHandlerParams({
nodeId,
dragItems: dragItems.current,
nodeInternals,
});
onStart(event.sourceEvent as MouseEvent, currentNode, nodes);
}
})
.on('drag', (event: UseDragEvent) => {
const { updateNodePositions, nodeInternals, nodeExtent } = store.getState();
const pointerPos = getPointerPosition(event);
// skip events without movement
if ((lastPos.current.x !== pointerPos.x || lastPos.current.y !== pointerPos.y) && dragItems.current) {
lastPos.current = pointerPos;
dragItems.current = dragItems.current.map((n) =>
updatePosition(n, pointerPos, nodeInternals, nodeExtent)
);
updateNodePositions(dragItems.current, true, true);
setDragging(true);
if (onDrag) {
const [currentNode, nodes] = getEventHandlerParams({
nodeId,
dragItems: dragItems.current,
nodeInternals,
});
onDrag(event.sourceEvent as MouseEvent, currentNode, nodes);
}
}
event.on('end', (event) => {
setDragging(false);
if (dragItems.current) {
updateNodePositions(dragItems.current, false, false);
if (onStop) {
const [currentNode, nodes] = getEventHandlerParams({
nodeId,
dragItems: dragItems.current,
nodeInternals,
});
onStop(event.sourceEvent as MouseEvent, currentNode, nodes);
}
}
});
})
.filter((event: MouseEvent) => {
const target = event.target as HTMLDivElement;
const isDraggable =
!event.button &&
(!noDragClassName || !hasSelector(target, `.${noDragClassName}`, nodeRef)) &&
(!handleSelector || hasSelector(target, handleSelector, nodeRef));
return isDraggable;
});
selection.call(dragHandler);
return () => {
selection.on('.drag', null);
};
}
}
}, [
onStart,
onDrag,
onStop,
nodeRef,
disabled,
noDragClassName,
handleSelector,
isSelectable,
store,
nodeId,
selectNodesOnDrag,
getPointerPosition,
]);
return dragging;
}
export default useDrag;
+123
View File
@@ -0,0 +1,123 @@
import { RefObject } from 'react';
import { CoordinateExtent, Node, NodeDragItem, NodeInternals, XYPosition } from '../../types';
import { clampPosition } from '../../utils';
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);
}
export function hasSelector(target: Element, selector: string, nodeRef: RefObject<Element>): boolean {
let current = target;
do {
if (current?.matches(selector)) return true;
if (current === nodeRef.current) return false;
current = current.parentElement as Element;
} while (current);
return false;
}
// looks for all selected nodes and created a NodeDragItem for each of them
export function getDragItems(nodeInternals: NodeInternals, mousePos: XYPosition, nodeId?: string): NodeDragItem[] {
return Array.from(nodeInternals.values())
.filter((n) => (n.selected || n.id === nodeId) && (!n.parentNode || !isParentSelected(n, nodeInternals)))
.map((n) => ({
id: n.id,
position: n.positionAbsolute || { x: 0, y: 0 },
distance: {
x: mousePos.x - (n.positionAbsolute?.x ?? 0),
y: mousePos.y - (n.positionAbsolute?.y ?? 0),
},
delta: {
x: 0,
y: 0,
},
extent: n.extent,
parentNode: n.parentNode,
width: n.width,
height: n.height,
}));
}
export function updatePosition(
dragItem: NodeDragItem,
mousePos: XYPosition,
nodeInternals: NodeInternals,
nodeExtent?: CoordinateExtent
): NodeDragItem {
let currentExtent = dragItem.extent || nodeExtent;
const nextPosition = { x: mousePos.x - dragItem.distance.x, y: mousePos.y - dragItem.distance.y };
if (dragItem.extent === 'parent') {
if (dragItem.parentNode && dragItem.width && dragItem.height) {
const parent = nodeInternals.get(dragItem.parentNode);
currentExtent =
parent?.positionAbsolute && parent?.width && parent?.height
? [
[parent.positionAbsolute.x, parent.positionAbsolute.y],
[
parent.positionAbsolute.x + parent.width - dragItem.width,
parent.positionAbsolute.y + parent.height - dragItem.height,
],
]
: currentExtent;
} else {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn('[React Flow]: Only child nodes can use a parent extent. Help: https://reactflow.dev/error#500');
}
currentExtent = nodeExtent;
}
} else if (dragItem.extent && dragItem.parentNode) {
const parent = nodeInternals.get(dragItem.parentNode);
const parentX = parent?.positionAbsolute?.x ?? 0;
const parentY = parent?.positionAbsolute?.y ?? 0;
currentExtent = [
[dragItem.extent[0][0] + parentX, dragItem.extent[0][1] + parentY],
[dragItem.extent[1][0] + parentX, dragItem.extent[1][1] + parentY],
];
}
dragItem.position = currentExtent ? clampPosition(nextPosition, currentExtent as CoordinateExtent) : nextPosition;
return dragItem;
}
// returns two params:
// 1. the dragged node (or the first of the list, if we are dragging a node selection)
// 2. array of selected nodes (handy when multi selection is active)
export function getEventHandlerParams({
nodeId,
dragItems,
nodeInternals,
}: {
nodeId?: string;
dragItems: NodeDragItem[];
nodeInternals: NodeInternals;
}): [Node, Node[]] {
const extentedDragItems: Node[] = dragItems.map((n) => {
const node = nodeInternals.get(n.id)!;
return {
...node,
};
});
return [nodeId ? extentedDragItems.find((n) => n.id === nodeId)! : extentedDragItems[0], extentedDragItems];
}
+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;
@@ -0,0 +1,96 @@
import { useEffect } from 'react';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../store';
import useKeyPress from './useKeyPress';
import { getConnectedEdges } from '../utils/graph';
import { EdgeChange, KeyCode, NodeChange, Node, ReactFlowState } from '../types';
interface HookParams {
deleteKeyCode: KeyCode | null;
multiSelectionKeyCode: KeyCode | null;
}
const selector = (s: ReactFlowState) => ({
onNodesChange: s.onNodesChange,
onEdgesChange: s.onEdgesChange,
});
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 { 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);
}
return res;
}, []);
const selectedEdges = edges.filter((e) => e.selected);
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, onNodesChange, onEdgesChange]);
useEffect(() => {
store.setState({ multiSelectionActive: multiSelectionKeyPressed });
}, [multiSelectionKeyPressed]);
};
+114
View File
@@ -0,0 +1,114 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import { KeyCode } from '../types';
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);
// 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)) {
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 = () => {
pressedKeys.current.clear();
setKeyPressed(false);
};
options?.target?.addEventListener('keydown', downHandler as EventListenerOrEventListenerObject);
options?.target?.addEventListener('keyup', upHandler as EventListenerOrEventListenerObject);
window.addEventListener('blur', resetHandler);
return () => {
options?.target?.removeEventListener('keydown', downHandler as EventListenerOrEventListenerObject);
options?.target?.removeEventListener('keyup', upHandler as EventListenerOrEventListenerObject);
window.removeEventListener('blur', resetHandler);
};
}
}, [keyCode, setKeyPressed]);
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;
@@ -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);
@@ -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;
+126
View File
@@ -0,0 +1,126 @@
import { useCallback } from 'react';
import useViewportHelper from './useViewportHelper';
import { useStoreApi } from '../store';
import {
ReactFlowInstance,
Instance,
NodeAddChange,
EdgeAddChange,
NodeResetChange,
EdgeResetChange,
NodeRemoveChange,
EdgeRemoveChange,
} 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.length === 0
? nodes.map((node) => ({ type: 'remove', id: node.id } as NodeRemoveChange))
: 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.length === 0
? edges.map((edge) => ({ type: 'remove', id: edge.id } as EdgeRemoveChange))
: 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,
};
}
@@ -0,0 +1,49 @@
import { useEffect, MutableRefObject } from 'react';
import { useStoreApi } from '../store';
import { getDimensions } from '../utils';
function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>): void {
const store = useStoreApi();
useEffect(() => {
let resizeObserver: ResizeObserver;
const updateDimensions = () => {
if (!rendererNode.current) {
return;
}
const size = getDimensions(rendererNode.current);
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
if (size.height === 0 || size.width === 0) {
console.warn(
'[React Flow]: The React Flow parent container needs a width and a height to render the graph. Help: https://reactflow.dev/error#400'
);
}
}
store.setState({ width: size.width || 500, height: size.height || 500 });
};
updateDimensions();
window.onresize = updateDimensions;
if (rendererNode.current) {
resizeObserver = new ResizeObserver(() => updateDimensions());
resizeObserver.observe(rendererNode.current);
}
return () => {
window.onresize = null;
if (resizeObserver && rendererNode.current) {
resizeObserver.unobserve(rendererNode.current!);
}
};
}, []);
}
export default useResizeHandler;
@@ -0,0 +1,20 @@
import { useCallback } from 'react';
import { useStore } from '../store';
import { UpdateNodeInternals, ReactFlowState } from '../types';
const selector = (state: ReactFlowState) => state.updateNodeDimensions;
function useUpdateNodeInternals(): UpdateNodeInternals {
const updateNodeDimensions = useStore(selector);
return useCallback<UpdateNodeInternals>((id: string) => {
const nodeElement = document.querySelector(`.react-flow__node[data-id="${id}"]`) as HTMLDivElement;
if (nodeElement) {
updateNodeDimensions([{ id, nodeElement, forceUpdate: true }]);
}
}, []);
}
export default useUpdateNodeInternals;
+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;
@@ -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;
@@ -0,0 +1,92 @@
import { useCallback } from 'react';
import { useStore } from '../store';
import { isEdgeVisible } from '../container/EdgeRenderer/utils';
import { ReactFlowState, NodeInternals, Edge } from '../types';
import { internalsSymbol, isNumeric } from '../utils';
const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }];
function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals, elevateEdgesOnSelect: boolean = false) {
let maxLevel = -1;
const levelLookup = edges.reduce<Record<string, Edge[]>>((tree, edge) => {
const hasZIndex = isNumeric(edge.zIndex);
let z = hasZIndex ? edge.zIndex! : 0;
if (elevateEdgesOnSelect) {
z = hasZIndex
? edge.zIndex!
: Math.max(
nodeInternals.get(edge.source)?.[internalsSymbol]?.z || 0,
nodeInternals.get(edge.target)?.[internalsSymbol]?.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, elevateEdgesOnSelect: boolean) {
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.positionAbsolute || { x: 0, y: 0 },
targetPos: targetNode.positionAbsolute || { 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, elevateEdgesOnSelect);
}
export default useVisibleEdges;
@@ -0,0 +1,23 @@
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.values());
},
[onlyRenderVisible]
)
);
return nodes;
}
export default useVisibleNodes;
+46
View File
@@ -0,0 +1,46 @@
import ReactFlow from './container/ReactFlow';
export default ReactFlow;
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,
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 * from './additional-components';
export { internalsSymbol } from './utils';
export {
isNode,
isEdge,
addEdge,
getOutgoers,
getIncomers,
getConnectedEdges,
updateEdge,
getTransformForBounds,
getRectOfNodes,
} from './utils/graph';
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 './types';
+237
View File
@@ -0,0 +1,237 @@
import create from 'zustand';
import createContext from 'zustand/context';
import { clampPosition, getDimensions, internalsSymbol } from '../utils';
import { applyNodeChanges } from '../utils/changes';
import {
ReactFlowState,
Node,
Edge,
NodeDimensionUpdate,
CoordinateExtent,
NodeDimensionChange,
EdgeSelectionChange,
NodeSelectionChange,
NodePositionChange,
NodeDragItem,
UnselectNodesAndEdgesParams,
} from '../types';
import { getHandleBounds } from '../components/Nodes/utils';
import { createSelectionChange, getSelectionChanges } from '../utils/changes';
import { createNodeInternals, fitView, updateNodesAndEdgesSelections } from './utils';
import initialState from './initialState';
const { Provider, useStore, useStoreApi } = createContext<ReactFlowState>();
const createStore = () =>
create<ReactFlowState>((set, get) => ({
...initialState,
setNodes: (nodes: Node[]) => {
set({ nodeInternals: createNodeInternals(nodes, get().nodeInternals) });
},
setEdges: (edges: Edge[]) => {
const { defaultEdgeOptions = {} } = get();
set({ edges: edges.map((e) => ({ ...defaultEdgeOptions, ...e })) });
},
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => {
const hasDefaultNodes = typeof nodes !== 'undefined';
const hasDefaultEdges = typeof edges !== 'undefined';
const nodeInternals = hasDefaultNodes ? createNodeInternals(nodes, new Map()) : new Map();
const nextEdges = hasDefaultEdges ? edges : [];
set({ nodeInternals, edges: nextEdges, hasDefaultNodes, hasDefaultEdges });
},
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => {
const { onNodesChange, transform, nodeInternals, fitViewOnInit, fitViewOnInitDone, fitViewOnInitOptions } = get();
const changes: NodeDimensionChange[] = updates.reduce<NodeDimensionChange[]>((res, update) => {
const node = nodeInternals.get(update.id);
if (node) {
const dimensions = getDimensions(update.nodeElement);
const doUpdate = !!(
dimensions.width &&
dimensions.height &&
(node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate)
);
if (doUpdate) {
const handleBounds = getHandleBounds(update.nodeElement, transform[2]);
nodeInternals.set(node.id, {
...node,
[internalsSymbol]: {
...node[internalsSymbol],
handleBounds,
},
...dimensions,
});
res.push({
id: node.id,
type: 'dimensions',
dimensions,
});
}
}
return res;
}, []);
const nextFitViewOnInitDone =
fitViewOnInitDone ||
(fitViewOnInit && !fitViewOnInitDone && fitView(get, { initial: true, ...fitViewOnInitOptions }));
set({ nodeInternals: new Map(nodeInternals), fitViewOnInitDone: nextFitViewOnInitDone });
if (changes?.length > 0) {
onNodesChange?.(changes);
}
},
updateNodePositions: (nodeDragItems: NodeDragItem[], positionChanged = true, dragging = false) => {
const { onNodesChange, nodeInternals, hasDefaultNodes } = get();
if (hasDefaultNodes || onNodesChange) {
const changes = nodeDragItems.map((node) => {
const change: NodePositionChange = {
id: node.id,
type: 'position',
dragging,
};
if (positionChanged) {
change.positionAbsolute = node.position;
change.position = node.position;
if (node.parentNode) {
const parentNode = nodeInternals.get(node.parentNode);
change.position = {
x: change.position.x - (parentNode?.positionAbsolute?.x ?? 0),
y: change.position.y - (parentNode?.positionAbsolute?.y ?? 0),
};
}
}
return change;
});
if (changes?.length) {
if (hasDefaultNodes) {
const nodes = applyNodeChanges(changes, Array.from(nodeInternals.values()));
const nextNodeInternals = createNodeInternals(nodes, nodeInternals);
set({ nodeInternals: nextNodeInternals });
}
onNodesChange?.(changes);
}
}
},
addSelectedNodes: (selectedNodeIds: string[]) => {
const { multiSelectionActive, nodeInternals, 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, []);
}
updateNodesAndEdgesSelections({
changedNodes,
changedEdges,
get,
set,
});
},
addSelectedEdges: (selectedEdgeIds: string[]) => {
const { multiSelectionActive, edges, nodeInternals } = 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()), []);
}
updateNodesAndEdgesSelections({
changedNodes,
changedEdges,
get,
set,
});
},
unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => {
const { nodeInternals, edges: storeEdges } = get();
const nodesToUnselect = nodes ? nodes : Array.from(nodeInternals.values());
const edgesToUnselect = edges ? edges : storeEdges;
const changedNodes = nodesToUnselect.map((n) => {
n.selected = false;
return createSelectionChange(n.id, false);
}) as NodeSelectionChange[];
const changedEdges = edgesToUnselect.map((edge) =>
createSelectionChange(edge.id, false)
) as EdgeSelectionChange[];
updateNodesAndEdgesSelections({
changedNodes,
changedEdges,
get,
set,
});
},
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 } = 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[];
updateNodesAndEdgesSelections({
changedNodes: nodesToUnselect,
changedEdges: edgesToUnselect,
get,
set,
});
},
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 };
+51
View File
@@ -0,0 +1,51 @@
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,
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;
+190
View File
@@ -0,0 +1,190 @@
import { zoomIdentity } from 'd3-zoom';
import { GetState, SetState } from 'zustand';
import { internalsSymbol, isNumeric } from '../utils';
import { getD3Transition, getRectOfNodes, getTransformForBounds } from '../utils/graph';
import {
Edge,
EdgeSelectionChange,
Node,
NodeInternals,
NodeSelectionChange,
ReactFlowState,
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[internalsSymbol]?.z ?? 0) > (result.z ?? 0) ? parentNode[internalsSymbol]?.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.selected ? 1000 : 0;
const currInternals = nodeInternals.get(node.id);
const internals: Node = {
width: currInternals?.width,
height: currInternals?.height,
...node,
positionAbsolute: {
x: node.position.x,
y: node.position.y,
},
};
if (node.parentNode) {
internals.parentNode = node.parentNode;
parentNodes[node.parentNode] = true;
}
Object.defineProperty(internals, internalsSymbol, {
enumerable: false,
value: {
handleBounds: currInternals?.[internalsSymbol]?.handleBounds,
z,
},
});
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[internalsSymbol]?.z ?? 0,
});
node.positionAbsolute = {
x,
y,
};
node[internalsSymbol]!.z = z;
if (parentNodes[node.id]) {
node[internalsSymbol]!.isParent = true;
}
}
});
return nextNodeInternals;
}
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.width && n.height : !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,
[internalsSymbol]: node[internalsSymbol],
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;
});
}
type UpdateNodesAndEdgesParams = {
changedNodes: NodeSelectionChange[] | null;
changedEdges: EdgeSelectionChange[] | null;
get: GetState<ReactFlowState>;
set: SetState<ReactFlowState>;
};
export function updateNodesAndEdgesSelections({ changedNodes, changedEdges, get, set }: UpdateNodesAndEdgesParams) {
const { nodeInternals, edges, onNodesChange, onEdgesChange, hasDefaultNodes, hasDefaultEdges } = get();
if (changedNodes?.length) {
if (hasDefaultNodes) {
set({ nodeInternals: handleControlledNodeSelectionChange(changedNodes, nodeInternals) });
}
onNodesChange?.(changedNodes);
}
if (changedEdges?.length) {
if (hasDefaultEdges) {
set({ edges: handleControlledEdgeSelectionChange(changedEdges, edges) });
}
onEdgesChange?.(changedEdges);
}
}
+206
View File
@@ -0,0 +1,206 @@
.react-flow {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.react-flow__container {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.react-flow__pane {
z-index: 1;
}
.react-flow__viewport {
transform-origin: 0 0;
z-index: 2;
pointer-events: none;
}
.react-flow__renderer {
z-index: 4;
}
.react-flow__selectionpane {
z-index: 5;
}
.react-flow .react-flow__edges {
pointer-events: none;
overflow: visible;
}
.react-flow .react-flow__connectionline {
z-index: 1001;
}
.react-flow__edge {
pointer-events: visibleStroke;
&.inactive {
pointer-events: none;
}
}
@keyframes dashdraw {
from {
stroke-dashoffset: 10;
}
}
.react-flow__edge-path {
fill: none;
}
.react-flow__edge-textwrapper {
pointer-events: all;
}
.react-flow__edge-text {
pointer-events: none;
user-select: none;
}
.react-flow__connection {
pointer-events: none;
.animated {
stroke-dasharray: 5;
animation: dashdraw 0.5s linear infinite;
}
}
.react-flow__connection-path {
fill: none;
}
.react-flow__nodes {
pointer-events: none;
transform-origin: 0 0;
}
.react-flow__node {
position: absolute;
user-select: none;
pointer-events: all;
transform-origin: 0 0;
box-sizing: border-box;
}
.react-flow__nodesselection {
z-index: 3;
transform-origin: left top;
pointer-events: none;
&-rect {
position: absolute;
pointer-events: all;
cursor: grab;
}
}
.react-flow__handle {
position: absolute;
pointer-events: none;
&.connectable {
pointer-events: all;
}
}
.react-flow__handle-bottom {
top: auto;
left: 50%;
bottom: -4px;
transform: translate(-50%, 0);
}
.react-flow__handle-top {
left: 50%;
top: -4px;
transform: translate(-50%, 0);
}
.react-flow__handle-left {
top: 50%;
left: -4px;
transform: translate(0, -50%);
}
.react-flow__handle-right {
right: -4px;
top: 50%;
transform: translate(0, -50%);
}
.react-flow__edgeupdater {
cursor: move;
pointer-events: all;
}
/* additional components */
.react-flow__controls {
position: absolute;
z-index: 5;
bottom: 20px;
left: 15px;
&-button {
width: 24px;
height: 24px;
border: none;
svg {
width: 100%;
}
}
}
.react-flow__minimap {
position: absolute;
z-index: 5;
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%);
}
}
+131
View File
@@ -0,0 +1,131 @@
.react-flow__edge {
&.selected {
.react-flow__edge-path {
stroke: #555;
}
}
&.animated path {
stroke-dasharray: 5;
animation: dashdraw 0.5s linear infinite;
}
&.updating {
.react-flow__edge-path {
stroke: #777;
}
}
}
.react-flow__edge-path {
stroke: #b1b1b7;
stroke-width: 1;
}
.react-flow__edge-text {
font-size: 10px;
}
.react-flow__edge-textbg {
fill: white;
}
.react-flow__connection-path {
stroke: #b1b1b7;
stroke-width: 1;
}
.react-flow__node {
cursor: grab;
}
.react-flow__node-default,
.react-flow__node-input,
.react-flow__node-output,
.react-flow__node-group {
padding: 10px;
border-radius: 3px;
width: 150px;
font-size: 12px;
color: #222;
text-align: center;
border-width: 1px;
border-style: solid;
background: #fff;
border-color: #1a192b;
&.selected {
box-shadow: 0 0 0 0.5px #1a192b;
}
.react-flow__handle {
background: #1a192b;
}
}
.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);
}
&.selected {
box-shadow: 0 0 0 0.5px #1a192b;
}
}
.react-flow__node-group {
background: rgba(240, 240, 240, 0.25);
border-color: #1a192b;
}
.react-flow__nodesselection-rect,
.react-flow__selection {
background: rgba(0, 89, 220, 0.08);
border: 1px dotted rgba(0, 89, 220, 0.8);
}
.react-flow__handle {
width: 6px;
height: 6px;
background: #555;
border: 1px solid white;
border-radius: 100%;
&.connectable {
cursor: crosshair;
}
}
.react-flow__minimap {
background-color: #fff;
}
.react-flow__controls {
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.08);
&-button {
background: #fefefe;
border-bottom: 1px solid #eee;
box-sizing: content-box;
display: flex;
justify-content: center;
align-items: center;
width: 16px;
height: 16px;
cursor: pointer;
user-select: none;
padding: 5px;
svg {
max-width: 12px;
max-height: 12px;
}
&:hover {
background: #f4f4f4;
}
}
}
+58
View File
@@ -0,0 +1,58 @@
import { XYPosition, Dimensions } from './utils';
import { Node } from './nodes';
import { Edge } from './edges';
export type NodeDimensionChange = {
id: string;
type: 'dimensions';
dimensions: Dimensions;
};
export type NodePositionChange = {
id: string;
type: 'position';
position?: XYPosition;
positionAbsolute?: 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;
+167
View File
@@ -0,0 +1,167 @@
import React, {
ButtonHTMLAttributes,
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,
NodeDragHandler,
NodeMouseHandler,
} from '.';
import { HandleType } from './handles';
export interface ReactFlowProps extends HTMLAttributes<HTMLDivElement> {
nodes?: Node[];
edges?: Edge[];
defaultNodes?: Node[];
defaultEdges?: Edge[];
defaultEdgeOptions?: DefaultEdgeOptions;
onNodesChange?: OnNodesChange;
onEdgesChange?: OnEdgesChange;
onNodeClick?: NodeMouseHandler;
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
onNodeDoubleClick?: NodeMouseHandler;
onNodeMouseEnter?: NodeMouseHandler;
onNodeMouseMove?: NodeMouseHandler;
onNodeMouseLeave?: NodeMouseHandler;
onNodeContextMenu?: NodeMouseHandler;
onNodeDragStart?: NodeDragHandler;
onNodeDrag?: NodeDragHandler;
onNodeDragStop?: NodeDragHandler;
onNodesDelete?: OnNodesDelete;
onEdgesDelete?: OnEdgesDelete;
onConnect?: OnConnect;
onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
onClickConnectStart?: OnConnectStart;
onClickConnectStop?: OnConnectStop;
onClickConnectEnd?: 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;
connectionLineContainerStyle?: CSSProperties;
deleteKeyCode?: KeyCode | null;
selectionKeyCode?: KeyCode | null;
multiSelectionKeyCode?: KeyCode | null;
zoomActivationKeyCode?: KeyCode | null;
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, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void;
edgeUpdaterRadius?: number;
noDragClassName?: string;
noWheelClassName?: string;
noPanClassName?: string;
fitView?: boolean;
fitViewOptions?: FitViewOptions;
connectOnClick?: boolean;
attributionPosition?: AttributionPosition;
proOptions?: ProOptions;
elevateEdgesOnSelect?: boolean;
}
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 ButtonHTMLAttributes<HTMLButtonElement> {}
export interface BackgroundProps extends HTMLAttributes<SVGElement> {
variant?: BackgroundVariant;
gap?: number;
color?: string;
size?: number;
}
+185
View File
@@ -0,0 +1,185 @@
import React, { CSSProperties, ComponentType, HTMLAttributes, ReactNode } from 'react';
import { Connection } from './general';
import { HandleElement, HandleType } 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?: (event: React.MouseEvent, edge: Edge, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void;
markerStart?: EdgeMarkerType;
markerEnd?: EdgeMarkerType;
rfId?: string;
}
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 = 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',
}
+227
View File
@@ -0,0 +1,227 @@
import { MouseEvent as ReactMouseEvent, ComponentType, MemoExoticComponent } 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, NodeProps, WrapNodeProps, NodeDragItem } from './nodes';
import { Edge, EdgeProps, WrapEdgeProps } from './edges';
import { HandleType, StartHandle } from './handles';
import { DefaultEdgeOptions } from '.';
import { ReactFlowInstance } from './instance';
export type NodeTypes = { [key: string]: ComponentType<NodeProps> };
export type NodeTypesWrapped = { [key: string]: MemoExoticComponent<ComponentType<WrapNodeProps>> };
export type EdgeTypes = { [key: string]: ComponentType<EdgeProps> };
export type EdgeTypesWrapped = { [key: string]: MemoExoticComponent<ComponentType<WrapEdgeProps>> };
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 type UnselectNodesAndEdgesParams = {
nodes?: Node[];
edges?: Edge[];
};
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[];
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;
onClickConnectStart?: OnConnectStart;
onClickConnectStop?: OnConnectStop;
onClickConnectEnd?: 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;
updateNodePositions: (nodeDragItems: NodeDragItem[], positionChanged: boolean, dragging: boolean) => void;
resetSelectedElements: () => void;
unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => 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
View File
@@ -0,0 +1,8 @@
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'>;
+122
View File
@@ -0,0 +1,122 @@
import { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import { XYPosition, Position, CoordinateExtent } from './utils';
import { HandleElement } from './handles';
import { internalsSymbol } from '../utils';
// 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;
positionAbsolute?: XYPosition;
// only used internally
[internalsSymbol]?: {
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 type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void;
export interface WrapNodeProps<T = any> {
id: string;
type: string;
data: T;
selected: boolean;
isConnectable: boolean;
xPos: number;
yPos: number;
initialized: boolean;
isSelectable: boolean;
isDraggable: boolean;
selectNodesOnDrag: boolean;
onClick?: NodeMouseHandler;
onDoubleClick?: NodeMouseHandler;
onMouseEnter?: NodeMouseHandler;
onMouseMove?: NodeMouseHandler;
onMouseLeave?: NodeMouseHandler;
onContextMenu?: NodeMouseHandler;
onDragStart?: NodeDragHandler;
onDrag?: NodeDragHandler;
onDragStop?: NodeDragHandler;
style?: CSSProperties;
className?: string;
sourcePosition: Position;
targetPosition: Position;
hidden?: 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 NodeDimensionUpdate = {
id: string;
nodeElement: HTMLDivElement;
forceUpdate?: boolean;
};
export type NodeInternals = Map<string, Node>;
export type NodeBounds = XYPosition & {
width: number | null;
height: number | null;
};
export type NodeDragItem = {
id: string;
// relative node position
position: XYPosition;
// distance from the mouse cursor to the node when start dragging
distance: XYPosition;
width?: number | null;
height?: number | null;
extent?: 'parent' | CoordinateExtent;
parentNode?: string;
dragging?: boolean;
};
+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]];
+139
View File
@@ -0,0 +1,139 @@
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[] {
// we need this hack to handle the setNodes and setEdges function of the useReactFlow hook for controlled flows
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.positionAbsolute !== 'undefined') {
updateItem.positionAbsolute = currentChange.positionAbsolute;
}
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;
}, []);
}
+238
View File
@@ -0,0 +1,238 @@
import { Selection as D3Selection } from 'd3';
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;
export const isNode = (element: Node | Connection | Edge): element is Node =>
'id' in element && !('source' in element) && !('target' in element);
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 = edges.filter((e) => e.source === node.id).map((e) => e.target);
return nodes.filter((n) => outgoerIds.includes(n.id));
};
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 = edges.filter((e) => e.target === node.id).map((e) => e.source);
return nodes.filter((n) => incomersIds.includes(n.id));
};
const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection): string =>
`reactflow__edge-${source}${sourceHandle || ''}-${target}${targetHandle || ''}`;
export const getMarkerId = (marker: EdgeMarkerType | undefined, rfId?: string): string => {
if (typeof marker === 'undefined') {
return '';
}
if (typeof marker === 'string') {
return marker;
}
const idPrefix = rfId ? `${rfId}__` : '';
return `${idPrefix}${Object.keys(marker)
.sort()
.map((key: string) => `${key}=${(marker as any)[key]}`)
.join('&')}`;
};
const connectionExists = (edge: Edge, edges: Edge[]) => {
return edges.some(
(el) =>
el.source === edge.source &&
el.target === edge.target &&
(el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) &&
(el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle))
);
};
export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] => {
if (!edgeParams.source || !edgeParams.target) {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
"[React Flow]: Can't create edge. An edge needs a source and a target. Help: https://reactflow.dev/error#600"
);
}
return edges;
}
let edge: Edge;
if (isEdge(edgeParams)) {
edge = { ...edgeParams };
} else {
edge = {
...edgeParams,
id: getEdgeId(edgeParams),
} as Edge;
}
if (connectionExists(edge, edges)) {
return edges;
}
return edges.concat(edge);
};
export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge[]): Edge[] => {
if (!newConnection.source || !newConnection.target) {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
"[React Flow]: Can't create a new edge. An edge needs a source and a target. Help: https://reactflow.dev/error#600"
);
}
return edges;
}
const foundEdge = edges.find((e) => e.id === oldEdge.id) as Edge;
if (!foundEdge) {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
`[React Flow]: The old edge with id=${oldEdge.id} does not exist. Help: https://reactflow.dev/error#700`
);
}
return edges;
}
// Remove old edge and create the new edge with parameters of old edge.
const edge = {
...oldEdge,
id: getEdgeId(newConnection),
source: newConnection.source,
target: newConnection.target,
sourceHandle: newConnection.sourceHandle,
targetHandle: newConnection.targetHandle,
} as Edge;
return edges.filter((e) => e.id !== oldEdge.id).concat(edge);
};
export const pointToRendererPoint = (
{ x, y }: XYPosition,
[tx, ty, tScale]: Transform,
snapToGrid: boolean,
[snapX, snapY]: [number, number]
): XYPosition => {
const position: XYPosition = {
x: (x - tx) / tScale,
y: (y - ty) / tScale,
};
if (snapToGrid) {
return {
x: snapX * Math.round(position.x / snapX),
y: snapY * Math.round(position.y / snapY),
};
}
return position;
};
export const getRectOfNodes = (nodes: Node[]): Rect => {
if (nodes.length === 0) {
return { x: 0, y: 0, width: 0, height: 0 };
}
const box = nodes.reduce(
(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 getNodesInside = (
nodeInternals: NodeInternals,
rect: Rect,
[tx, ty, tScale]: Transform = [0, 0, 1],
partially: boolean = false,
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
excludeNonSelectableNodes: boolean = false
): Node[] => {
const rBox = rectToBox({
x: (rect.x - tx) / tScale,
y: (rect.y - ty) / tScale,
width: rect.width / tScale,
height: rect.height / tScale,
});
const visibleNodes: Node[] = [];
nodeInternals.forEach((node) => {
const { positionAbsolute, width, height, selectable = true } = node;
if (excludeNonSelectableNodes && !selectable) {
return false;
}
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;
const partiallyVisible = partially && overlappingArea > 0;
const area = (width || 0) * (height || 0);
const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
if (isVisible) {
visibleNodes.push(node);
}
});
return visibleNodes;
};
export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
const nodeIds = nodes.map((node) => node.id);
return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target));
};
export const getTransformForBounds = (
bounds: Rect,
width: number,
height: number,
minZoom: number,
maxZoom: number,
padding: number = 0.1
): Transform => {
const xZoom = width / (bounds.width * (1 + padding));
const yZoom = height / (bounds.height * (1 + padding));
const zoom = Math.min(xZoom, yZoom);
const clampedZoom = clamp(zoom, minZoom, maxZoom);
const boundsCenterX = bounds.x + bounds.width / 2;
const boundsCenterY = bounds.y + bounds.height / 2;
const x = width / 2 - boundsCenterX * clampedZoom;
const y = height / 2 - boundsCenterY * clampedZoom;
return [x, y, clampedZoom];
};
export const getD3Transition = (selection: D3Selection<Element, unknown, null, undefined>, duration: number = 0) => {
return selection.transition().duration(duration);
};
+44
View File
@@ -0,0 +1,44 @@
import { Dimensions, XYPosition, CoordinateExtent, Box, Rect } from '../types';
export const getDimensions = (node: HTMLDivElement): Dimensions => ({
width: node.offsetWidth,
height: node.offsetHeight,
});
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: 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);
export const internalsSymbol = Symbol('internals');