refactor(packages): change package structure

This commit is contained in:
moklick
2023-06-05 15:40:18 +02:00
parent b2e296901e
commit 8354759f57
332 changed files with 980 additions and 3244 deletions
@@ -0,0 +1,51 @@
import { CSSProperties } from 'react';
import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types';
const style: CSSProperties = { display: 'none' };
const ariaLiveStyle: CSSProperties = {
position: 'absolute',
width: 1,
height: 1,
margin: -1,
border: 0,
padding: 0,
overflow: 'hidden',
clip: 'rect(0px, 0px, 0px, 0px)',
clipPath: 'inset(100%)',
};
export const ARIA_NODE_DESC_KEY = 'react-flow__node-desc';
export const ARIA_EDGE_DESC_KEY = 'react-flow__edge-desc';
export const ARIA_LIVE_MESSAGE = 'react-flow__aria-live';
const selector = (s: ReactFlowState) => s.ariaLiveMessage;
function AriaLiveMessage({ rfId }: { rfId: string }) {
const ariaLiveMessage = useStore(selector);
return (
<div id={`${ARIA_LIVE_MESSAGE}-${rfId}`} aria-live="assertive" aria-atomic="true" style={ariaLiveStyle}>
{ariaLiveMessage}
</div>
);
}
function A11yDescriptions({ rfId, disableKeyboardA11y }: { rfId: string; disableKeyboardA11y: boolean }) {
return (
<>
<div id={`${ARIA_NODE_DESC_KEY}-${rfId}`} style={style}>
Press enter or space to select a node.
{!disableKeyboardA11y && 'You can then use the arrow keys to move the node around.'} Press delete to remove it
and escape to cancel.{' '}
</div>
<div id={`${ARIA_EDGE_DESC_KEY}-${rfId}`} style={style}>
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.
</div>
{!disableKeyboardA11y && <AriaLiveMessage rfId={rfId} />}
</>
);
}
export default A11yDescriptions;
@@ -0,0 +1,28 @@
import type { PanelPosition, ProOptions } from '@xyflow/system';
import Panel from '../Panel';
type AttributionProps = {
proOptions?: ProOptions;
position?: PanelPosition;
};
function Attribution({ proOptions, position = 'bottom-right' }: AttributionProps) {
if (proOptions?.hideAttribution) {
return null;
}
return (
<Panel
position={position}
className="react-flow__attribution"
data-message="Please only hide this attribution when you are subscribed to React Flow Pro: https://pro.reactflow.dev"
>
<a href="https://reactflow.dev" target="_blank" rel="noopener noreferrer" aria-label="React Flow attribution">
React Flow
</a>
</Panel>
);
}
export default Attribution;
@@ -0,0 +1,174 @@
import { CSSProperties, useCallback } from 'react';
import { shallow } from 'zustand/shallow';
import cc from 'classcat';
import {
internalsSymbol,
Position,
ConnectionLineType,
ConnectionMode,
getBezierPath,
getSmoothStepPath,
type ConnectionStatus,
type HandleType,
} from '@xyflow/system';
import { useStore } from '../../hooks/useStore';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import type { ConnectionLineComponent, ReactFlowState, ReactFlowStore } from '../../types';
type ConnectionLineProps = {
nodeId: string;
handleType: HandleType;
type: ConnectionLineType;
style?: CSSProperties;
CustomComponent?: ConnectionLineComponent;
connectionStatus: ConnectionStatus | null;
};
const oppositePosition = {
[Position.Left]: Position.Right,
[Position.Right]: Position.Left,
[Position.Top]: Position.Bottom,
[Position.Bottom]: Position.Top,
};
const ConnectionLine = ({
nodeId,
handleType,
style,
type = ConnectionLineType.Bezier,
CustomComponent,
connectionStatus,
}: ConnectionLineProps) => {
const { fromNode, handleId, toX, toY, connectionMode } = useStore(
useCallback(
(s: ReactFlowStore) => ({
fromNode: s.nodeInternals.get(nodeId),
handleId: s.connectionStartHandle?.handleId,
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
connectionMode: s.connectionMode,
}),
[nodeId]
),
shallow
);
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
let handleBounds = fromHandleBounds?.[handleType];
if (connectionMode === ConnectionMode.Loose) {
handleBounds = handleBounds ? handleBounds : fromHandleBounds?.[handleType === 'source' ? 'target' : 'source'];
}
if (!fromNode || !handleBounds) {
return null;
}
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.height ?? 0;
const fromX = (fromNode.positionAbsolute?.x ?? 0) + fromHandleX;
const fromY = (fromNode.positionAbsolute?.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
if (!fromPosition || !toPosition) {
return null;
}
if (CustomComponent) {
return (
<CustomComponent
connectionLineType={type}
connectionLineStyle={style}
fromNode={fromNode}
fromHandle={fromHandle}
fromX={fromX}
fromY={fromY}
toX={toX}
toY={toY}
fromPosition={fromPosition}
toPosition={toPosition}
connectionStatus={connectionStatus}
/>
);
}
let dAttr = '';
const pathParams = {
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: toX,
targetY: toY,
targetPosition: toPosition,
};
if (type === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
[dAttr] = getBezierPath(pathParams);
} else if (type === ConnectionLineType.Step) {
[dAttr] = getSmoothStepPath({
...pathParams,
borderRadius: 0,
});
} else if (type === ConnectionLineType.SmoothStep) {
[dAttr] = getSmoothStepPath(pathParams);
} else if (type === ConnectionLineType.SimpleBezier) {
[dAttr] = getSimpleBezierPath(pathParams);
} else {
dAttr = `M${fromX},${fromY} ${toX},${toY}`;
}
return <path d={dAttr} fill="none" className="react-flow__connection-path" style={style} />;
};
ConnectionLine.displayName = 'ConnectionLine';
type ConnectionLineWrapperProps = {
type: ConnectionLineType;
component?: ConnectionLineComponent;
containerStyle?: CSSProperties;
style?: CSSProperties;
};
const selector = (s: ReactFlowState) => ({
nodeId: s.connectionStartHandle?.nodeId,
handleType: s.connectionStartHandle?.type,
nodesConnectable: s.nodesConnectable,
connectionStatus: s.connectionStatus,
width: s.width,
height: s.height,
});
function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) {
const { nodeId, handleType, nodesConnectable, width, height, connectionStatus } = useStore(selector, shallow);
const isValid = !!(nodeId && handleType && width && nodesConnectable);
if (!isValid) {
return null;
}
return (
<svg
style={containerStyle}
width={width}
height={height}
className="react-flow__edges react-flow__connectionline react-flow__container"
>
<g className={cc(['react-flow__connection', connectionStatus])}>
<ConnectionLine
nodeId={nodeId}
handleType={handleType}
style={style}
type={type}
CustomComponent={component}
connectionStatus={connectionStatus}
/>
</g>
</svg>
);
}
export default ConnectionLineWrapper;
@@ -0,0 +1,19 @@
import type { ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__edgelabel-renderer');
function EdgeLabelRenderer({ children }: { children: ReactNode }) {
const edgeLabelRenderer = useStore(selector);
if (!edgeLabelRenderer) {
return null;
}
return createPortal(children, edgeLabelRenderer);
}
export default EdgeLabelRenderer;
@@ -0,0 +1,61 @@
import { isNumeric } from '@xyflow/system';
import type { BaseEdgeProps } from '../../types';
import EdgeText from './EdgeText';
const BaseEdge = ({
id,
path,
labelX,
labelY,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
markerEnd,
markerStart,
interactionWidth = 20,
}: BaseEdgeProps) => {
return (
<>
<path
id={id}
style={style}
d={path}
fill="none"
className="react-flow__edge-path"
markerEnd={markerEnd}
markerStart={markerStart}
/>
{interactionWidth && (
<path
d={path}
fill="none"
strokeOpacity={0}
strokeWidth={interactionWidth}
className="react-flow__edge-interaction"
/>
)}
{label && isNumeric(labelX) && isNumeric(labelY) ? (
<EdgeText
x={labelX}
y={labelY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
/>
) : null}
</>
);
};
BaseEdge.displayName = 'BaseEdge';
export default BaseEdge;
@@ -0,0 +1,59 @@
import { memo } from 'react';
import { Position, getBezierPath } from '@xyflow/system';
import BaseEdge from './BaseEdge';
import type { BezierEdgeProps } from '../../types';
const BezierEdge = memo(
({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
markerEnd,
markerStart,
pathOptions,
interactionWidth,
}: BezierEdgeProps) => {
const [path, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
curvature: pathOptions?.curvature,
});
return (
<BaseEdge
path={path}
labelX={labelX}
labelY={labelY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
style={style}
markerEnd={markerEnd}
markerStart={markerStart}
interactionWidth={interactionWidth}
/>
);
}
);
BezierEdge.displayName = 'BezierEdge';
export default BezierEdge;
@@ -0,0 +1,51 @@
import type { FC, MouseEvent as ReactMouseEvent, SVGAttributes } from 'react';
import cc from 'classcat';
import { Position } from '@xyflow/system';
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 SVGAttributes<SVGGElement> {
position: Position;
centerX: number;
centerY: number;
radius?: number;
onMouseDown: (event: ReactMouseEvent<SVGGElement, MouseEvent>) => void;
onMouseEnter: (event: ReactMouseEvent<SVGGElement, MouseEvent>) => void;
onMouseOut: (event: ReactMouseEvent<SVGGElement, MouseEvent>) => void;
type: string;
}
const EdgeUpdaterClassName = 'react-flow__edgeupdater';
export const EdgeAnchor: FC<EdgeAnchorProps> = ({
position,
centerX,
centerY,
radius = 10,
onMouseDown,
onMouseEnter,
onMouseOut,
type,
}: EdgeAnchorProps) => (
<circle
onMouseDown={onMouseDown}
onMouseEnter={onMouseEnter}
onMouseOut={onMouseOut}
className={cc([EdgeUpdaterClassName, `${EdgeUpdaterClassName}-${type}`])}
cx={shiftX(centerX, radius, position)}
cy={shiftY(centerY, radius, position)}
r={radius}
stroke="transparent"
fill="transparent"
/>
);
@@ -0,0 +1,67 @@
import { memo, useRef, useState, useEffect, type FC, type PropsWithChildren } from 'react';
import cc from 'classcat';
import type { Rect } from '@xyflow/system';
import type { EdgeTextProps } 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}
visibility={edgeTextBbox.width ? 'visible' : 'hidden'}
{...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,124 @@
import { memo } from 'react';
import { Position, getBezierEdgeCenter } from '@xyflow/system';
import BaseEdge from './BaseEdge';
import type { EdgeProps } from '../../types';
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] {
if (pos === Position.Left || pos === Position.Right) {
return [0.5 * (x1 + x2), y1];
}
return [x1, 0.5 * (y1 + y2)];
}
export function getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
}: GetSimpleBezierPathParams): [path: string, labelX: number, labelY: number, offsetX: number, offsetY: 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,
});
const [labelX, labelY, offsetX, offsetY] = getBezierEdgeCenter({
sourceX,
sourceY,
targetX,
targetY,
sourceControlX,
sourceControlY,
targetControlX,
targetControlY,
});
return [
`M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`,
labelX,
labelY,
offsetX,
offsetY,
];
}
const SimpleBezierEdge = memo(
({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
markerEnd,
markerStart,
interactionWidth,
}: EdgeProps) => {
const [path, labelX, labelY] = getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<BaseEdge
path={path}
labelX={labelX}
labelY={labelY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
style={style}
markerEnd={markerEnd}
markerStart={markerStart}
interactionWidth={interactionWidth}
/>
);
}
);
SimpleBezierEdge.displayName = 'SimpleBezierEdge';
export default SimpleBezierEdge;
@@ -0,0 +1,60 @@
import { memo } from 'react';
import { Position, getSmoothStepPath } from '@xyflow/system';
import BaseEdge from './BaseEdge';
import type { SmoothStepEdgeProps } from '../../types';
const SmoothStepEdge = memo(
({
sourceX,
sourceY,
targetX,
targetY,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
markerEnd,
markerStart,
pathOptions,
interactionWidth,
}: SmoothStepEdgeProps) => {
const [path, labelX, labelY] = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
borderRadius: pathOptions?.borderRadius,
offset: pathOptions?.offset,
});
return (
<BaseEdge
path={path}
labelX={labelX}
labelY={labelY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
style={style}
markerEnd={markerEnd}
markerStart={markerStart}
interactionWidth={interactionWidth}
/>
);
}
);
SmoothStepEdge.displayName = 'SmoothStepEdge';
export default SmoothStepEdge;
@@ -0,0 +1,15 @@
import { memo, useMemo } from 'react';
import SmoothStepEdge from './SmoothStepEdge';
import type { SmoothStepEdgeProps } from '../../types';
const StepEdge = memo((props: SmoothStepEdgeProps) => (
<SmoothStepEdge
{...props}
pathOptions={useMemo(() => ({ borderRadius: 0, offset: props.pathOptions?.offset }), [props.pathOptions?.offset])}
/>
));
StepEdge.displayName = 'StepEdge';
export default StepEdge;
@@ -0,0 +1,48 @@
import { memo } from 'react';
import { getStraightPath } from '@xyflow/system';
import BaseEdge from './BaseEdge';
import type { EdgeProps } from '../../types';
const StraightEdge = memo(
({
sourceX,
sourceY,
targetX,
targetY,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
markerEnd,
markerStart,
interactionWidth,
}: EdgeProps) => {
const [path, labelX, labelY] = getStraightPath({ sourceX, sourceY, targetX, targetY });
return (
<BaseEdge
path={path}
labelX={labelX}
labelY={labelY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
style={style}
markerEnd={markerEnd}
markerStart={markerStart}
interactionWidth={interactionWidth}
/>
);
}
);
StraightEdge.displayName = 'StraightEdge';
export default StraightEdge;
@@ -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,20 @@
import type { MouseEvent as ReactMouseEvent } from 'react';
import type { StoreApi } from 'zustand';
import type { Edge, ReactFlowState } from '../../types';
export function getMouseHandler(
id: string,
getState: StoreApi<ReactFlowState>['getState'],
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);
if (edge) {
handler(event, { ...edge });
}
};
}
@@ -0,0 +1,265 @@
import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent } from 'react';
import cc from 'classcat';
import { getMarkerId, elementSelectionKeys, XYHandle, type Connection } from '@xyflow/system';
import { useStoreApi } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import { EdgeAnchor } from './EdgeAnchor';
import { getMouseHandler } from './utils';
import type { EdgeProps, WrapEdgeProps } from '../../types';
const alwaysValidConnection = () => true;
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,
isSelectable,
hidden,
sourceHandleId,
targetHandleId,
onContextMenu,
onMouseEnter,
onMouseMove,
onMouseLeave,
edgeUpdaterRadius,
onEdgeUpdate,
onEdgeUpdateStart,
onEdgeUpdateEnd,
markerEnd,
markerStart,
rfId,
ariaLabel,
isFocusable,
isUpdatable,
pathOptions,
interactionWidth,
}: WrapEdgeProps): JSX.Element | null => {
const edgeRef = useRef<SVGGElement>(null);
const [updateHover, setUpdateHover] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(false);
const store = useStoreApi();
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]);
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]);
if (hidden) {
return null;
}
const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
const { edges, addSelectedEdges } = store.getState();
if (isSelectable) {
store.setState({ nodesSelectionActive: false });
addSelectedEdges([id]);
}
if (onClick) {
const edge = edges.find((e) => e.id === 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) => {
// avoid triggering edge updater if mouse btn is not left
if (event.button !== 0) {
return;
}
const {
autoPanOnConnect,
domNode,
edges,
isValidConnection: isValidConnectionStore,
connectionMode,
connectionRadius,
transform,
lib,
onConnectStart,
onConnectEnd,
cancelConnection,
getNodes,
panBy,
updateConnection,
} = store.getState();
const nodeId = isSourceHandle ? target : source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source';
const isValidConnection = isValidConnectionStore || alwaysValidConnection;
const isTarget = isSourceHandle;
const edge = edges.find((e) => e.id === id)!;
const nodes = getNodes();
setUpdating(true);
onEdgeUpdateStart?.(event, edge, handleType);
const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => {
setUpdating(false);
onEdgeUpdateEnd?.(evt, edge, handleType);
};
const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection);
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect,
connectionMode,
connectionRadius,
domNode,
handleId,
nodeId,
nodes,
isTarget,
edgeUpdaterType: handleType,
transform,
lib,
cancelConnection,
panBy,
isValidConnection,
onConnect: onConnectEdge,
onConnectStart,
onConnectEnd,
onEdgeUpdateEnd: _onEdgeUpdateEnd,
updateConnection,
});
};
const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, true);
const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, false);
const onEdgeUpdaterMouseEnter = () => setUpdateHover(true);
const onEdgeUpdaterMouseOut = () => setUpdateHover(false);
const inactive = !isSelectable && !onClick;
const onKeyDown = (event: KeyboardEvent) => {
if (elementSelectionKeys.includes(event.key) && isSelectable) {
const { unselectNodesAndEdges, addSelectedEdges, edges } = store.getState();
const unselect = event.key === 'Escape';
if (unselect) {
edgeRef.current?.blur();
unselectNodesAndEdges({ edges: [edges.find((e) => e.id === id)!] });
} else {
addSelectedEdges([id]);
}
}
};
return (
<g
className={cc([
'react-flow__edge',
`react-flow__edge-${type}`,
className,
{ selected, animated, inactive, updating: updateHover },
])}
onClick={onEdgeClick}
onDoubleClick={onEdgeDoubleClickHandler}
onContextMenu={onEdgeContextMenu}
onMouseEnter={onEdgeMouseEnter}
onMouseMove={onEdgeMouseMove}
onMouseLeave={onEdgeMouseLeave}
onKeyDown={isFocusable ? onKeyDown : undefined}
tabIndex={isFocusable ? 0 : undefined}
role={isFocusable ? 'button' : undefined}
data-testid={`rf__edge-${id}`}
aria-label={ariaLabel === null ? undefined : ariaLabel ? ariaLabel : `Edge from ${source} to ${target}`}
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
ref={edgeRef}
>
{!updating && (
<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}
pathOptions={pathOptions}
interactionWidth={interactionWidth}
/>
)}
{isUpdatable && (
<>
{(isUpdatable === 'source' || isUpdatable === true) && (
<EdgeAnchor
position={sourcePosition}
centerX={sourceX}
centerY={sourceY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterSourceMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="source"
/>
)}
{(isUpdatable === 'target' || isUpdatable === true) && (
<EdgeAnchor
position={targetPosition}
centerX={targetX}
centerY={targetY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="target"
/>
)}
</>
)}
</g>
);
};
EdgeWrapper.displayName = 'EdgeWrapper';
return memo(EdgeWrapper);
};
@@ -0,0 +1,216 @@
import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import {
errorMessages,
Position,
XYHandle,
getHostForElement,
isMouseEvent,
type HandleProps,
type Connection,
type HandleType,
} from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext';
import { addEdge } from '../../utils/';
import { type ReactFlowState } from '../../types';
export type HandleComponentProps = HandleProps & Omit<HTMLAttributes<HTMLDivElement>, 'id'>;
const selector = (s: ReactFlowState) => ({
connectionStartHandle: s.connectionStartHandle,
connectOnClick: s.connectOnClick,
noPanClassName: s.noPanClassName,
});
const connectingSelector =
(nodeId: string | null, handleId: string | null, type: HandleType) => (state: ReactFlowState) => {
const {
connectionStartHandle: startHandle,
connectionEndHandle: endHandle,
connectionClickStartHandle: clickHandle,
} = state;
return {
connecting:
(startHandle?.nodeId === nodeId && startHandle?.handleId === handleId && startHandle?.type === type) ||
(endHandle?.nodeId === nodeId && endHandle?.handleId === handleId && endHandle?.type === type),
clickConnecting:
clickHandle?.nodeId === nodeId && clickHandle?.handleId === handleId && clickHandle?.type === type,
};
};
const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
(
{
type = 'source',
position = Position.Top,
isValidConnection,
isConnectable = true,
isConnectableStart = true,
isConnectableEnd = true,
id,
onConnect,
children,
className,
onMouseDown,
onTouchStart,
...rest
},
ref
) => {
const handleId = id || null;
const isTarget = type === 'target';
const store = useStoreApi();
const nodeId = useNodeId();
const { connectOnClick, noPanClassName } = useStore(selector, shallow);
const { connecting, clickConnecting } = useStore(connectingSelector(nodeId, handleId, type), shallow);
if (!nodeId) {
store.getState().onError?.('010', errorMessages['error010']());
}
const onConnectExtended = (params: Connection) => {
const { defaultEdgeOptions, onConnect: onConnectAction, hasDefaultEdges } = store.getState();
const edgeParams = {
...defaultEdgeOptions,
...params,
};
if (hasDefaultEdges) {
const { edges, setEdges } = store.getState();
setEdges(addEdge(edgeParams, edges));
}
onConnectAction?.(edgeParams);
onConnect?.(edgeParams);
};
const onPointerDown = (event: ReactMouseEvent<HTMLDivElement> | ReactTouchEvent<HTMLDivElement>) => {
if (!nodeId) {
return;
}
const isMouseTriggered = isMouseEvent(event.nativeEvent);
if (
isConnectableStart &&
((isMouseTriggered && (event as ReactMouseEvent<HTMLDivElement>).button === 0) || !isMouseTriggered)
) {
const currentStore = store.getState();
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect: currentStore.autoPanOnConnect,
connectionMode: currentStore.connectionMode,
connectionRadius: currentStore.connectionRadius,
domNode: currentStore.domNode,
nodes: currentStore.getNodes(),
lib: currentStore.lib,
isTarget,
handleId,
nodeId,
panBy: currentStore.panBy,
cancelConnection: currentStore.cancelConnection,
onConnectStart: currentStore.onConnectStart,
onConnectEnd: currentStore.onConnectEnd,
updateConnection: currentStore.updateConnection,
onConnect: onConnectExtended,
isValidConnection: isValidConnection || currentStore.isValidConnection,
getTransform: () => store.getState().transform,
});
}
if (isMouseTriggered) {
onMouseDown?.(event as ReactMouseEvent<HTMLDivElement>);
} else {
onTouchStart?.(event as ReactTouchEvent<HTMLDivElement>);
}
};
const onClick = (event: ReactMouseEvent) => {
const {
onClickConnectStart,
onClickConnectEnd,
connectionClickStartHandle,
connectionMode,
isValidConnection: isValidConnectionStore,
lib,
} = store.getState();
if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
return;
}
if (!connectionClickStartHandle) {
onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type });
store.setState({ connectionClickStartHandle: { nodeId, type, handleId } });
return;
}
const doc = getHostForElement(event.target as HTMLElement);
const isValidConnectionHandler = isValidConnection || isValidConnectionStore;
const { connection, isValid } = XYHandle.isValid(event.nativeEvent, {
handle: {
nodeId,
id: handleId,
type,
},
connectionMode,
fromNodeId: connectionClickStartHandle.nodeId,
fromHandleId: connectionClickStartHandle.handleId || null,
fromType: connectionClickStartHandle.type,
isValidConnection: isValidConnectionHandler,
doc,
lib,
});
if (isValid) {
onConnectExtended(connection);
}
onClickConnectEnd?.(event as unknown as MouseEvent);
store.setState({ connectionClickStartHandle: null });
};
return (
<div
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
data-id={`${nodeId}-${handleId}-${type}`}
className={cc([
'react-flow__handle',
`react-flow__handle-${position}`,
'nodrag',
noPanClassName,
className,
{
source: !isTarget,
target: isTarget,
connectable: isConnectable,
connectablestart: isConnectableStart,
connectableend: isConnectableEnd,
connecting: clickConnecting,
// this class is used to style the handle when the user is connecting
connectionindicator:
isConnectable && ((isConnectableStart && !connecting) || (isConnectableEnd && connecting)),
},
])}
onMouseDown={onPointerDown}
onTouchStart={onPointerDown}
onClick={connectOnClick ? onClick : undefined}
ref={ref}
{...rest}
>
{children}
</div>
);
}
);
Handle.displayName = 'Handle';
export default memo(Handle);
@@ -0,0 +1,23 @@
import { memo } from 'react';
import { Position, type NodeProps } from '@xyflow/system';
import Handle from '../../components/Handle';
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 { memo } from 'react';
import { Position, type NodeProps } from '@xyflow/system';
import Handle from '../../components/Handle';
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 { memo } from 'react';
import { Position, type NodeProps } from '@xyflow/system';
import Handle from '../../components/Handle';
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,49 @@
import type { MouseEvent, RefObject } from 'react';
import type { StoreApi } from 'zustand';
import type { Node, ReactFlowState } from '../../types';
export function getMouseHandler(
id: string,
getState: StoreApi<ReactFlowState>['getState'],
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,
unselect = false,
nodeRef,
}: {
id: string;
store: {
getState: StoreApi<ReactFlowState>['getState'];
setState: StoreApi<ReactFlowState>['setState'];
};
unselect?: boolean;
nodeRef?: RefObject<HTMLDivElement>;
}) {
const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodeInternals } = store.getState();
const node = nodeInternals.get(id)!;
store.setState({ nodesSelectionActive: false });
if (!node.selected) {
addSelectedNodes([id]);
} else if (unselect || (node.selected && multiSelectionActive)) {
unselectNodesAndEdges({ nodes: [node] });
requestAnimationFrame(() => nodeRef?.current?.blur());
}
}
@@ -0,0 +1,224 @@
import { useEffect, useRef, memo, type ComponentType, type MouseEvent, type KeyboardEvent } from 'react';
import cc from 'classcat';
import { elementSelectionKeys, isInputDOMNode, type NodeProps, type XYPosition } from '@xyflow/system';
import { useStoreApi } from '../../hooks/useStore';
import { Provider } from '../../contexts/NodeIdContext';
import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions';
import useDrag from '../../hooks/useDrag';
import useUpdateNodePositions from '../../hooks/useUpdateNodePositions';
import { getMouseHandler, handleNodeClick } from './utils';
import type { WrapNodeProps } from '../../types';
export const arrowKeyDiffs: Record<string, XYPosition> = {
ArrowUp: { x: 0, y: -1 },
ArrowDown: { x: 0, y: 1 },
ArrowLeft: { x: -1, y: 0 },
ArrowRight: { x: 1, y: 0 },
};
export default (NodeComponent: ComponentType<NodeProps>) => {
const NodeWrapper = ({
id,
type,
data,
xPos,
yPos,
xPosOrigin,
yPosOrigin,
selected,
onClick,
onMouseEnter,
onMouseMove,
onMouseLeave,
onContextMenu,
onDoubleClick,
style,
className,
isDraggable,
isSelectable,
isConnectable,
isFocusable,
sourcePosition,
targetPosition,
hidden,
resizeObserver,
dragHandle,
zIndex,
isParent,
noDragClassName,
noPanClassName,
initialized,
disableKeyboardA11y,
ariaLabel,
rfId,
}: WrapNodeProps) => {
const store = useStoreApi();
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 updatePositions = useUpdateNodePositions();
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) => {
const { selectNodesOnDrag } = store.getState();
if (isSelectable && (!selectNodesOnDrag || !isDraggable)) {
// this handler gets called within the drag start event when selectNodesOnDrag=true
handleNodeClick({
id,
store,
nodeRef,
});
}
if (onClick) {
const node = store.getState().nodeInternals.get(id)!;
onClick(event, { ...node });
}
};
const onKeyDown = (event: KeyboardEvent) => {
if (isInputDOMNode(event.nativeEvent)) {
return;
}
if (elementSelectionKeys.includes(event.key) && isSelectable) {
const unselect = event.key === 'Escape';
handleNodeClick({
id,
store,
unselect,
nodeRef,
});
} else if (
!disableKeyboardA11y &&
isDraggable &&
selected &&
Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)
) {
store.setState({
ariaLiveMessage: `Moved selected node ${event.key
.replace('Arrow', '')
.toLowerCase()}. New position, x: ${~~xPos}, y: ${~~yPos}`,
});
updatePositions({
x: arrowKeyDiffs[event.key].x,
y: arrowKeyDiffs[event.key].y,
isShiftPressed: event.shiftKey,
});
}
};
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;
}
store.getState().updateNodeDimensions([{ id, nodeElement: nodeRef.current, forceUpdate: true }]);
}
}, [id, type, sourcePosition, targetPosition]);
const dragging = useDrag({
nodeRef,
disabled: hidden || !isDraggable,
noDragClassName,
handleSelector: dragHandle,
nodeId: id,
isSelectable,
});
if (hidden) {
return null;
}
return (
<div
className={cc([
'react-flow__node',
`react-flow__node-${type}`,
{
// this is overwritable by passing `nopan` as a class name
[noPanClassName]: isDraggable,
},
className,
{
selected,
selectable: isSelectable,
parent: isParent,
dragging,
},
])}
ref={nodeRef}
style={{
zIndex,
transform: `translate(${xPosOrigin}px,${yPosOrigin}px)`,
pointerEvents: hasPointerEvents ? 'all' : 'none',
visibility: initialized ? 'visible' : 'hidden',
...style,
}}
data-id={id}
data-testid={`rf__node-${id}`}
onMouseEnter={onMouseEnterHandler}
onMouseMove={onMouseMoveHandler}
onMouseLeave={onMouseLeaveHandler}
onContextMenu={onContextMenuHandler}
onClick={onSelectNodeHandler}
onDoubleClick={onDoubleClickHandler}
onKeyDown={isFocusable ? onKeyDown : undefined}
tabIndex={isFocusable ? 0 : undefined}
role={isFocusable ? 'button' : undefined}
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`}
aria-label={ariaLabel}
>
<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,99 @@
/**
* The nodes selection rectangle gets displayed when a user
* made a selection with on or several nodes
*/
import { memo, useRef, useEffect, type MouseEvent, type KeyboardEvent } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import { getRectOfNodes } from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore';
import useDrag from '../../hooks/useDrag';
import { arrowKeyDiffs } from '../Nodes/wrapNode';
import useUpdateNodePositions from '../../hooks/useUpdateNodePositions';
import type { Node, ReactFlowState } from '../../types';
export interface NodesSelectionProps {
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
noPanClassName?: string;
disableKeyboardA11y: boolean;
}
const selector = (s: ReactFlowState) => {
const selectedNodes = s.getNodes().filter((n) => n.selected);
return {
...getRectOfNodes(selectedNodes, s.nodeOrigin),
transformString: `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]})`,
userSelectionActive: s.userSelectionActive,
};
};
function NodesSelection({ onSelectionContextMenu, noPanClassName, disableKeyboardA11y }: NodesSelectionProps) {
const store = useStoreApi();
const { width, height, x: left, y: top, transformString, userSelectionActive } = useStore(selector, shallow);
const updatePositions = useUpdateNodePositions();
const nodeRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!disableKeyboardA11y) {
nodeRef.current?.focus({
preventScroll: true,
});
}
}, [disableKeyboardA11y]);
useDrag({
nodeRef,
});
if (userSelectionActive || !width || !height) {
return null;
}
const onContextMenu = onSelectionContextMenu
? (event: MouseEvent) => {
const selectedNodes = store
.getState()
.getNodes()
.filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes);
}
: undefined;
const onKeyDown = (event: KeyboardEvent) => {
if (Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) {
updatePositions({
x: arrowKeyDiffs[event.key].x,
y: arrowKeyDiffs[event.key].y,
isShiftPressed: event.shiftKey,
});
}
};
return (
<div
className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])}
style={{
transform: transformString,
}}
>
<div
ref={nodeRef}
className="react-flow__nodesselection-rect"
onContextMenu={onContextMenu}
tabIndex={disableKeyboardA11y ? undefined : -1}
onKeyDown={disableKeyboardA11y ? undefined : onKeyDown}
style={{
width,
height,
top,
left,
}}
/>
</div>
);
}
export default memo(NodesSelection);
@@ -0,0 +1,30 @@
import type { HTMLAttributes, ReactNode } from 'react';
import cc from 'classcat';
import type { PanelPosition } from '@xyflow/system';
import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types';
export type PanelProps = HTMLAttributes<HTMLDivElement> & {
position: PanelPosition;
children: ReactNode;
};
const selector = (s: ReactFlowState) => (s.userSelectionActive ? 'none' : 'all');
function Panel({ position, children, className, style, ...rest }: PanelProps) {
const pointerEvents = useStore(selector);
const positionClasses = `${position}`.split('-');
return (
<div
className={cc(['react-flow__panel', className, ...positionClasses])}
style={{ ...style, pointerEvents }}
{...rest}
>
{children}
</div>
);
}
export default Panel;
@@ -0,0 +1,20 @@
import { useRef, type FC, type PropsWithChildren } from 'react';
import { type StoreApi } from 'zustand';
import { Provider } from '../../contexts/RFStoreContext';
import { createRFStore } from '../../store';
import type { ReactFlowState } from '../../types';
const ReactFlowProvider: FC<PropsWithChildren<unknown>> = ({ children }) => {
const storeRef = useRef<StoreApi<ReactFlowState> | null>(null);
if (!storeRef.current) {
storeRef.current = createRFStore();
}
return <Provider value={storeRef.current}>{children}</Provider>;
};
ReactFlowProvider.displayName = 'ReactFlowProvider';
export default ReactFlowProvider;
@@ -0,0 +1,56 @@
import { memo, useEffect } from 'react';
import { shallow } from 'zustand/shallow';
import { useStore, useStoreApi } from '../../hooks/useStore';
import type { ReactFlowState, OnSelectionChangeFunc, Node, Edge } from '../../types';
type SelectionListenerProps = {
onSelectionChange?: OnSelectionChangeFunc;
};
const selector = (s: ReactFlowState) => ({
selectedNodes: s.getNodes().filter((n) => n.selected),
selectedEdges: s.edges.filter((e) => e.selected),
});
type SelectorSlice = ReturnType<typeof selector>;
const selectId = (obj: Node | Edge) => obj.id;
function areEqual(a: SelectorSlice, b: SelectorSlice) {
return (
shallow(a.selectedNodes.map(selectId), b.selectedNodes.map(selectId)) &&
shallow(a.selectedEdges.map(selectId), b.selectedEdges.map(selectId))
);
}
// 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?
const SelectionListener = memo(({ onSelectionChange }: SelectionListenerProps) => {
const store = useStoreApi();
const { selectedNodes, selectedEdges } = useStore(selector, areEqual);
useEffect(() => {
const params = { nodes: selectedNodes, edges: selectedEdges };
onSelectionChange?.(params);
store.getState().onSelectionChange?.(params);
}, [selectedNodes, selectedEdges, onSelectionChange]);
return null;
});
SelectionListener.displayName = 'SelectionListener';
const changeSelector = (s: ReactFlowState) => !!s.onSelectionChange;
function Wrapper({ onSelectionChange }: SelectionListenerProps) {
const storeHasSelectionChange = useStore(changeSelector);
if (onSelectionChange || storeHasSelectionChange) {
return <SelectionListener onSelectionChange={onSelectionChange} />;
}
return null;
}
export default Wrapper;
@@ -0,0 +1,217 @@
import { useEffect } from 'react';
import { StoreApi } from 'zustand';
import { shallow } from 'zustand/shallow';
import type { CoordinateExtent } from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore';
import type { Node, Edge, ReactFlowState, ReactFlowProps, ReactFlowStore } from '../../types';
type StoreUpdaterProps = Pick<
ReactFlowProps,
| 'nodes'
| 'edges'
| 'defaultNodes'
| 'defaultEdges'
| 'onConnect'
| 'onConnectStart'
| 'onConnectEnd'
| 'onClickConnectStart'
| 'onClickConnectEnd'
| 'nodesDraggable'
| 'nodesConnectable'
| 'nodesFocusable'
| 'edgesFocusable'
| 'edgesUpdatable'
| 'minZoom'
| 'maxZoom'
| 'nodeExtent'
| 'onNodesChange'
| 'onEdgesChange'
| 'elementsSelectable'
| 'connectionMode'
| 'snapToGrid'
| 'snapGrid'
| 'translateExtent'
| 'connectOnClick'
| 'defaultEdgeOptions'
| 'fitView'
| 'fitViewOptions'
| 'onNodesDelete'
| 'onEdgesDelete'
| 'onNodeDragStart'
| 'onNodeDrag'
| 'onNodeDragStop'
| 'onSelectionDragStart'
| 'onSelectionDrag'
| 'onSelectionDragStop'
| 'onMove'
| 'onMoveStart'
| 'onMoveEnd'
| 'noPanClassName'
| 'nodeOrigin'
| 'elevateNodesOnSelect'
| 'autoPanOnConnect'
| 'autoPanOnNodeDrag'
| 'onError'
| 'connectionRadius'
| 'isValidConnection'
| 'selectNodesOnDrag'
> & { rfId: string };
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]);
}
// updates with values in store that don't have a dedicated setter function
function useDirectStoreUpdater(
key: keyof ReactFlowStore,
value: unknown,
setState: StoreApi<ReactFlowState>['setState']
) {
useEffect(() => {
if (typeof value !== 'undefined') {
setState({ [key]: value });
}
}, [value]);
}
const StoreUpdater = ({
nodes,
edges,
defaultNodes,
defaultEdges,
onConnect,
onConnectStart,
onConnectEnd,
onClickConnectStart,
onClickConnectEnd,
nodesDraggable,
nodesConnectable,
nodesFocusable,
edgesFocusable,
edgesUpdatable,
elevateNodesOnSelect,
minZoom,
maxZoom,
nodeExtent,
onNodesChange,
onEdgesChange,
elementsSelectable,
connectionMode,
snapGrid,
snapToGrid,
translateExtent,
connectOnClick,
defaultEdgeOptions,
fitView,
fitViewOptions,
onNodesDelete,
onEdgesDelete,
onNodeDrag,
onNodeDragStart,
onNodeDragStop,
onSelectionDrag,
onSelectionDragStart,
onSelectionDragStop,
onMoveStart,
onMove,
onMoveEnd,
noPanClassName,
nodeOrigin,
rfId,
autoPanOnConnect,
autoPanOnNodeDrag,
onError,
connectionRadius,
isValidConnection,
selectNodesOnDrag,
}: StoreUpdaterProps) => {
const {
setNodes,
setEdges,
setDefaultNodesAndEdges,
setMinZoom,
setMaxZoom,
setTranslateExtent,
setNodeExtent,
reset,
} = useStore(selector, shallow);
const store = useStoreApi();
useEffect(() => {
const edgesWithDefaults = defaultEdges?.map((e) => ({ ...e, ...defaultEdgeOptions }));
setDefaultNodesAndEdges(defaultNodes, edgesWithDefaults);
return () => {
reset();
};
}, []);
useDirectStoreUpdater('defaultEdgeOptions', defaultEdgeOptions, store.setState);
useDirectStoreUpdater('connectionMode', connectionMode, store.setState);
useDirectStoreUpdater('onConnect', onConnect, store.setState);
useDirectStoreUpdater('onConnectStart', onConnectStart, store.setState);
useDirectStoreUpdater('onConnectEnd', onConnectEnd, store.setState);
useDirectStoreUpdater('onClickConnectStart', onClickConnectStart, store.setState);
useDirectStoreUpdater('onClickConnectEnd', onClickConnectEnd, store.setState);
useDirectStoreUpdater('nodesDraggable', nodesDraggable, store.setState);
useDirectStoreUpdater('nodesConnectable', nodesConnectable, store.setState);
useDirectStoreUpdater('nodesFocusable', nodesFocusable, store.setState);
useDirectStoreUpdater('edgesFocusable', edgesFocusable, store.setState);
useDirectStoreUpdater('edgesUpdatable', edgesUpdatable, store.setState);
useDirectStoreUpdater('elementsSelectable', elementsSelectable, store.setState);
useDirectStoreUpdater('elevateNodesOnSelect', elevateNodesOnSelect, 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);
useDirectStoreUpdater('onNodeDrag', onNodeDrag, store.setState);
useDirectStoreUpdater('onNodeDragStart', onNodeDragStart, store.setState);
useDirectStoreUpdater('onNodeDragStop', onNodeDragStop, store.setState);
useDirectStoreUpdater('onSelectionDrag', onSelectionDrag, store.setState);
useDirectStoreUpdater('onSelectionDragStart', onSelectionDragStart, store.setState);
useDirectStoreUpdater('onSelectionDragStop', onSelectionDragStop, store.setState);
useDirectStoreUpdater('onMove', onMove, store.setState);
useDirectStoreUpdater('onMoveStart', onMoveStart, store.setState);
useDirectStoreUpdater('onMoveEnd', onMoveEnd, store.setState);
useDirectStoreUpdater('noPanClassName', noPanClassName, store.setState);
useDirectStoreUpdater('nodeOrigin', nodeOrigin, store.setState);
useDirectStoreUpdater('rfId', rfId, store.setState);
useDirectStoreUpdater('autoPanOnConnect', autoPanOnConnect, store.setState);
useDirectStoreUpdater('autoPanOnNodeDrag', autoPanOnNodeDrag, store.setState);
useDirectStoreUpdater('onError', onError, store.setState);
useDirectStoreUpdater('connectionRadius', connectionRadius, store.setState);
useDirectStoreUpdater('isValidConnection', isValidConnection, store.setState);
useDirectStoreUpdater('selectNodesOnDrag', selectNodesOnDrag, store.setState);
useStoreUpdater<Node[]>(nodes, setNodes);
useStoreUpdater<Edge[]>(edges, 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,31 @@
import { shallow } from 'zustand/shallow';
import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => ({
userSelectionActive: s.userSelectionActive,
userSelectionRect: s.userSelectionRect,
});
function UserSelection() {
const { userSelectionActive, userSelectionRect } = useStore(selector, shallow);
const isActive = userSelectionActive && userSelectionRect;
if (!isActive) {
return null;
}
return (
<div
className="react-flow__selection react-flow__container"
style={{
width: userSelectionRect.width,
height: userSelectionRect.height,
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
}}
/>
);
}
export default UserSelection;