Merge branch 'main' into feature/edgeUpdateWithMoreInfo-1961
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import cc from 'classcat';
|
||||
|
||||
import { AttributionPosition, ProOptions } from '../../types';
|
||||
|
||||
type AttributionProps = {
|
||||
proOptions?: ProOptions;
|
||||
position?: AttributionPosition;
|
||||
};
|
||||
|
||||
function Attribution({ proOptions, position = 'bottom-right' }: AttributionProps) {
|
||||
if (
|
||||
(proOptions?.account === 'paid-sponsor' ||
|
||||
proOptions?.account === 'paid-enterprise' ||
|
||||
proOptions?.account === 'paid-custom') &&
|
||||
proOptions?.hideAttribution
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const positionClasses = `${position}`.split('-');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cc(['react-flow__attribution', ...positionClasses])}
|
||||
data-message="Please only hide this attribution when you are subscribed to React Flow Pro: https://pro.reactflow.dev/pricing"
|
||||
>
|
||||
<a href="https://reactflow.dev" target="_blank" rel="noopener noreferrer">
|
||||
React Flow
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Attribution;
|
||||
@@ -1,39 +1,25 @@
|
||||
import React, { useEffect, useState, CSSProperties } from 'react';
|
||||
import React, { useRef, CSSProperties } from 'react';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { useStore } from '../../store';
|
||||
import { getBezierPath } from '../Edges/BezierEdge';
|
||||
import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
|
||||
import {
|
||||
ElementId,
|
||||
Node,
|
||||
Transform,
|
||||
HandleElement,
|
||||
Position,
|
||||
ConnectionLineType,
|
||||
ConnectionLineComponent,
|
||||
HandleType,
|
||||
} from '../../types';
|
||||
import { ConnectionLineType, ConnectionLineComponent, HandleType, Node, ReactFlowState, Position } from '../../types';
|
||||
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
|
||||
|
||||
interface ConnectionLineProps {
|
||||
connectionNodeId: ElementId;
|
||||
connectionHandleId: ElementId | null;
|
||||
connectionNodeId: string;
|
||||
connectionHandleId: string | null;
|
||||
connectionHandleType: HandleType;
|
||||
connectionPositionX: number;
|
||||
connectionPositionY: number;
|
||||
connectionLineType: ConnectionLineType;
|
||||
nodes: Node[];
|
||||
transform: Transform;
|
||||
isConnectable: boolean;
|
||||
connectionLineStyle?: CSSProperties;
|
||||
CustomConnectionLineComponent?: ConnectionLineComponent;
|
||||
}
|
||||
|
||||
const getSourceHandle = (handleId: ElementId | null, sourceNode: Node, connectionHandleType: HandleType) => {
|
||||
const handleTypeInverted = connectionHandleType === 'source' ? 'target' : 'source';
|
||||
const handleBound =
|
||||
sourceNode.__rf.handleBounds[connectionHandleType] || sourceNode.__rf.handleBounds[handleTypeInverted];
|
||||
|
||||
return handleId ? handleBound.find((d: HandleElement) => d.id === handleId) : handleBound[0];
|
||||
};
|
||||
const selector = (s: ReactFlowState) => ({ nodeInternals: s.nodeInternals, transform: s.transform });
|
||||
|
||||
export default ({
|
||||
connectionNodeId,
|
||||
@@ -43,35 +29,76 @@ export default ({
|
||||
connectionPositionX,
|
||||
connectionPositionY,
|
||||
connectionLineType = ConnectionLineType.Bezier,
|
||||
nodes = [],
|
||||
transform,
|
||||
isConnectable,
|
||||
CustomConnectionLineComponent,
|
||||
}: ConnectionLineProps) => {
|
||||
const [sourceNode, setSourceNode] = useState<Node | null>(null);
|
||||
const nodeId = connectionNodeId;
|
||||
const handleId = connectionHandleId;
|
||||
|
||||
useEffect(() => {
|
||||
const nextSourceNode = nodes.find((n) => n.id === nodeId) || null;
|
||||
setSourceNode(nextSourceNode);
|
||||
}, []);
|
||||
const { nodeInternals, transform } = useStore(selector, shallow);
|
||||
const fromNode = useRef<Node | undefined>(nodeInternals.get(nodeId));
|
||||
|
||||
if (!sourceNode || !isConnectable) {
|
||||
if (!fromNode.current || !isConnectable || !fromNode.current.handleBounds?.[connectionHandleType]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceHandle = getSourceHandle(handleId, sourceNode, connectionHandleType);
|
||||
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rf.width / 2;
|
||||
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rf.height;
|
||||
const sourceX = sourceNode.__rf.position.x + sourceHandleX;
|
||||
const sourceY = sourceNode.__rf.position.y + sourceHandleY;
|
||||
const handleBound = fromNode.current.handleBounds?.[connectionHandleType];
|
||||
const fromHandle = handleId ? handleBound?.find((d) => d.id === handleId) : handleBound?.[0];
|
||||
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.current?.width ?? 0) / 2;
|
||||
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.current?.height ?? 0;
|
||||
const fromX = (fromNode.current.positionAbsolute?.x || 0) + fromHandleX;
|
||||
const fromY = (fromNode.current.positionAbsolute?.y || 0) + fromHandleY;
|
||||
|
||||
const targetX = (connectionPositionX - transform[0]) / transform[2];
|
||||
const targetY = (connectionPositionY - transform[1]) / transform[2];
|
||||
const toX = (connectionPositionX - transform[0]) / transform[2];
|
||||
const toY = (connectionPositionY - transform[1]) / transform[2];
|
||||
|
||||
const isRightOrLeft = sourceHandle?.position === Position.Left || sourceHandle?.position === Position.Right;
|
||||
const targetPosition = isRightOrLeft ? Position.Left : Position.Top;
|
||||
const fromPosition = fromHandle?.position;
|
||||
|
||||
let toPosition: Position | undefined;
|
||||
switch (fromPosition) {
|
||||
case Position.Left:
|
||||
toPosition = Position.Right;
|
||||
break;
|
||||
case Position.Right:
|
||||
toPosition = Position.Left;
|
||||
break;
|
||||
case Position.Top:
|
||||
toPosition = Position.Bottom;
|
||||
break;
|
||||
case Position.Bottom:
|
||||
toPosition = Position.Top;
|
||||
break;
|
||||
}
|
||||
|
||||
let sourceX: number,
|
||||
sourceY: number,
|
||||
sourcePosition: Position | undefined,
|
||||
targetX: number,
|
||||
targetY: number,
|
||||
targetPosition: Position | undefined;
|
||||
|
||||
switch (connectionHandleType) {
|
||||
case 'source':
|
||||
{
|
||||
sourceX = fromX;
|
||||
sourceY = fromY;
|
||||
sourcePosition = fromPosition;
|
||||
targetX = toX;
|
||||
targetY = toY;
|
||||
targetPosition = toPosition;
|
||||
}
|
||||
break;
|
||||
case 'target':
|
||||
{
|
||||
sourceX = toX;
|
||||
sourceY = toY;
|
||||
sourcePosition = toPosition;
|
||||
targetX = fromX;
|
||||
targetY = fromY;
|
||||
targetPosition = fromPosition;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (CustomConnectionLineComponent) {
|
||||
return (
|
||||
@@ -79,14 +106,17 @@ export default ({
|
||||
<CustomConnectionLineComponent
|
||||
sourceX={sourceX}
|
||||
sourceY={sourceY}
|
||||
sourcePosition={sourceHandle?.position}
|
||||
sourcePosition={sourcePosition}
|
||||
targetX={targetX}
|
||||
targetY={targetY}
|
||||
targetPosition={targetPosition}
|
||||
connectionLineType={connectionLineType}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
sourceNode={sourceNode}
|
||||
sourceHandle={sourceHandle}
|
||||
fromNode={fromNode.current}
|
||||
fromHandle={fromHandle}
|
||||
// backward compatibility, mark as deprecated?
|
||||
sourceNode={fromNode.current}
|
||||
sourceHandle={fromHandle}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
@@ -94,34 +124,27 @@ export default ({
|
||||
|
||||
let dAttr: string = '';
|
||||
|
||||
const pathParams = {
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
};
|
||||
|
||||
if (connectionLineType === ConnectionLineType.Bezier) {
|
||||
dAttr = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
// we assume the destination position is opposite to the source position
|
||||
dAttr = getBezierPath(pathParams);
|
||||
} else if (connectionLineType === ConnectionLineType.Step) {
|
||||
dAttr = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
...pathParams,
|
||||
borderRadius: 0,
|
||||
});
|
||||
} else if (connectionLineType === ConnectionLineType.SmoothStep) {
|
||||
dAttr = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
dAttr = getSmoothStepPath(pathParams);
|
||||
} else if (connectionLineType === ConnectionLineType.SimpleBezier) {
|
||||
dAttr = getSimpleBezierPath(pathParams);
|
||||
} else {
|
||||
dAttr = `M${sourceX},${sourceY} ${targetX},${targetY}`;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,98 +1,177 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import EdgeText from './EdgeText';
|
||||
|
||||
import { getMarkerEnd, getCenter } from './utils';
|
||||
import { EdgeProps, Position } from '../../types';
|
||||
|
||||
interface GetBezierPathParams {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
sourcePosition?: Position;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
targetPosition?: Position;
|
||||
centerX?: number;
|
||||
centerY?: number;
|
||||
}
|
||||
|
||||
export function getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
centerX,
|
||||
centerY,
|
||||
}: GetBezierPathParams): string {
|
||||
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY });
|
||||
const leftAndRight = [Position.Left, Position.Right];
|
||||
|
||||
const cX = typeof centerX !== 'undefined' ? centerX : _centerX;
|
||||
const cY = typeof centerY !== 'undefined' ? centerY : _centerY;
|
||||
|
||||
let path = `M${sourceX},${sourceY} C${sourceX},${cY} ${targetX},${cY} ${targetX},${targetY}`;
|
||||
|
||||
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
|
||||
path = `M${sourceX},${sourceY} C${cX},${sourceY} ${cX},${targetY} ${targetX},${targetY}`;
|
||||
} else if (leftAndRight.includes(targetPosition)) {
|
||||
path = `M${sourceX},${sourceY} Q${sourceX},${targetY} ${targetX},${targetY}`;
|
||||
} else if (leftAndRight.includes(sourcePosition)) {
|
||||
path = `M${sourceX},${sourceY} Q${targetX},${sourceY} ${targetX},${targetY}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
export default memo(
|
||||
({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetPosition = Position.Top,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
arrowHeadType,
|
||||
markerEndId,
|
||||
}: EdgeProps) => {
|
||||
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
|
||||
const path = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
const text = label ? (
|
||||
<EdgeText
|
||||
x={centerX}
|
||||
y={centerY}
|
||||
label={label}
|
||||
labelStyle={labelStyle}
|
||||
labelShowBg={labelShowBg}
|
||||
labelBgStyle={labelBgStyle}
|
||||
labelBgPadding={labelBgPadding}
|
||||
labelBgBorderRadius={labelBgBorderRadius}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<path style={style} d={path} className="react-flow__edge-path" markerEnd={markerEnd} />
|
||||
{text}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
import React, { memo } from 'react';
|
||||
import { EdgeProps, Position } from '../../types';
|
||||
import BaseEdge from './BaseEdge';
|
||||
|
||||
export interface GetBezierPathParams {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
sourcePosition?: Position;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
targetPosition?: Position;
|
||||
curvature?: number;
|
||||
}
|
||||
|
||||
interface GetControlWithCurvatureParams {
|
||||
pos: Position;
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
c: number;
|
||||
}
|
||||
|
||||
function calculateControlOffset(distance: number, curvature: number): number {
|
||||
if (distance >= 0) {
|
||||
return 0.5 * distance;
|
||||
} else {
|
||||
return curvature * 25 * Math.sqrt(-distance);
|
||||
}
|
||||
}
|
||||
|
||||
function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurvatureParams): [number, number] {
|
||||
let ctX: number, ctY: number;
|
||||
switch (pos) {
|
||||
case Position.Left:
|
||||
{
|
||||
ctX = x1 - calculateControlOffset(x1 - x2, c);
|
||||
ctY = y1;
|
||||
}
|
||||
break;
|
||||
case Position.Right:
|
||||
{
|
||||
ctX = x1 + calculateControlOffset(x2 - x1, c);
|
||||
ctY = y1;
|
||||
}
|
||||
break;
|
||||
case Position.Top:
|
||||
{
|
||||
ctX = x1;
|
||||
ctY = y1 - calculateControlOffset(y1 - y2, c);
|
||||
}
|
||||
break;
|
||||
case Position.Bottom:
|
||||
{
|
||||
ctX = x1;
|
||||
ctY = y1 + calculateControlOffset(y2 - y1, c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return [ctX, ctY];
|
||||
}
|
||||
|
||||
export function getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
curvature = 0.25,
|
||||
}: GetBezierPathParams): string {
|
||||
const [sourceControlX, sourceControlY] = getControlWithCurvature({
|
||||
pos: sourcePosition,
|
||||
x1: sourceX,
|
||||
y1: sourceY,
|
||||
x2: targetX,
|
||||
y2: targetY,
|
||||
c: curvature,
|
||||
});
|
||||
const [targetControlX, targetControlY] = getControlWithCurvature({
|
||||
pos: targetPosition,
|
||||
x1: targetX,
|
||||
y1: targetY,
|
||||
x2: sourceX,
|
||||
y2: sourceY,
|
||||
c: curvature,
|
||||
});
|
||||
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`;
|
||||
}
|
||||
|
||||
// @TODO: this function will recalculate the control points
|
||||
// one option is to let getXXXPath() return center points
|
||||
// but will introduce breaking changes
|
||||
// the getCenter() of other types of edges might need to change, too
|
||||
export function getBezierCenter({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
curvature = 0.25,
|
||||
}: GetBezierPathParams): [number, number, number, number] {
|
||||
const [sourceControlX, sourceControlY] = getControlWithCurvature({
|
||||
pos: sourcePosition,
|
||||
x1: sourceX,
|
||||
y1: sourceY,
|
||||
x2: targetX,
|
||||
y2: targetY,
|
||||
c: curvature,
|
||||
});
|
||||
const [targetControlX, targetControlY] = getControlWithCurvature({
|
||||
pos: targetPosition,
|
||||
x1: targetX,
|
||||
y1: targetY,
|
||||
x2: sourceX,
|
||||
y2: sourceY,
|
||||
c: curvature,
|
||||
});
|
||||
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
|
||||
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
|
||||
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125;
|
||||
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125;
|
||||
const xOffset = Math.abs(centerX - sourceX);
|
||||
const yOffset = Math.abs(centerY - sourceY);
|
||||
return [centerX, centerY, xOffset, yOffset];
|
||||
}
|
||||
|
||||
export default memo(
|
||||
({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetPosition = Position.Top,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
curvature,
|
||||
}: EdgeProps) => {
|
||||
const params = {
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
curvature,
|
||||
};
|
||||
const path = getBezierPath(params);
|
||||
const [centerX, centerY] = getBezierCenter(params);
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
path={path}
|
||||
centerX={centerX}
|
||||
centerY={centerY}
|
||||
label={label}
|
||||
labelStyle={labelStyle}
|
||||
labelShowBg={labelShowBg}
|
||||
labelBgStyle={labelBgStyle}
|
||||
labelBgPadding={labelBgPadding}
|
||||
labelBgBorderRadius={labelBgBorderRadius}
|
||||
style={style}
|
||||
markerEnd={markerEnd}
|
||||
markerStart={markerStart}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import EdgeText from './EdgeText';
|
||||
import { getMarkerEnd, getCenter } from './utils';
|
||||
import { getCenter } from './utils';
|
||||
import { EdgeSmoothStepProps, Position } from '../../types';
|
||||
import BaseEdge from './BaseEdge';
|
||||
|
||||
// These are some helper methods for drawing the round corners
|
||||
// The name indicates the direction of the path. "bottomLeftCorner" goes
|
||||
@@ -21,7 +21,7 @@ const topLeftCorner = (x: number, y: number, size: number): string => `L ${x},${
|
||||
const topRightCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x - size},${y}`;
|
||||
const rightTopCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y + size}`;
|
||||
|
||||
interface GetSmoothStepPathParams {
|
||||
export interface GetSmoothStepPathParams {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
sourcePosition?: Position;
|
||||
@@ -130,8 +130,8 @@ export default memo(
|
||||
style,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetPosition = Position.Top,
|
||||
arrowHeadType,
|
||||
markerEndId,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
borderRadius = 5,
|
||||
}: EdgeSmoothStepProps) => {
|
||||
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
|
||||
@@ -146,26 +146,21 @@ export default memo(
|
||||
borderRadius,
|
||||
});
|
||||
|
||||
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
|
||||
|
||||
const text = label ? (
|
||||
<EdgeText
|
||||
x={centerX}
|
||||
y={centerY}
|
||||
return (
|
||||
<BaseEdge
|
||||
path={path}
|
||||
centerX={centerX}
|
||||
centerY={centerY}
|
||||
label={label}
|
||||
labelStyle={labelStyle}
|
||||
labelShowBg={labelShowBg}
|
||||
labelBgStyle={labelBgStyle}
|
||||
labelBgPadding={labelBgPadding}
|
||||
labelBgBorderRadius={labelBgBorderRadius}
|
||||
style={style}
|
||||
markerEnd={markerEnd}
|
||||
markerStart={markerStart}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<path style={style} className="react-flow__edge-path" d={path} markerEnd={markerEnd} />
|
||||
{text}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import EdgeText from './EdgeText';
|
||||
import { getMarkerEnd } from './utils';
|
||||
import BaseEdge from './BaseEdge';
|
||||
import { EdgeProps } from '../../types';
|
||||
|
||||
export default memo(
|
||||
@@ -17,39 +16,31 @@ export default memo(
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
arrowHeadType,
|
||||
markerEndId,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
}: EdgeProps) => {
|
||||
const yOffset = Math.abs(targetY - sourceY) / 2;
|
||||
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
||||
|
||||
const xOffset = Math.abs(targetX - sourceX) / 2;
|
||||
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
|
||||
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
|
||||
const path = `M ${sourceX},${sourceY}L ${targetX},${targetY}`;
|
||||
|
||||
const text = label ? (
|
||||
<EdgeText
|
||||
x={centerX}
|
||||
y={centerY}
|
||||
return (
|
||||
<BaseEdge
|
||||
path={path}
|
||||
centerX={centerX}
|
||||
centerY={centerY}
|
||||
label={label}
|
||||
labelStyle={labelStyle}
|
||||
labelShowBg={labelShowBg}
|
||||
labelBgStyle={labelBgStyle}
|
||||
labelBgPadding={labelBgPadding}
|
||||
labelBgBorderRadius={labelBgBorderRadius}
|
||||
style={style}
|
||||
markerEnd={markerEnd}
|
||||
markerStart={markerStart}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<path
|
||||
style={style}
|
||||
className="react-flow__edge-path"
|
||||
d={`M ${sourceX},${sourceY}L ${targetX},${targetY}`}
|
||||
markerEnd={markerEnd}
|
||||
/>
|
||||
{text}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { default as BezierEdge } from './BezierEdge';
|
||||
export { default as StepEdge } from './StepEdge';
|
||||
export { default as SimpleBezierEdge } from './SimpleBezierEdge';
|
||||
export { default as SmoothStepEdge } from './SmoothStepEdge';
|
||||
export { default as StepEdge } from './StepEdge';
|
||||
export { default as StraightEdge } from './StraightEdge';
|
||||
export { default as BezierEdge } from './BezierEdge';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ArrowHeadType, Position } from '../../types';
|
||||
import { MarkerType, Position } from '../../types';
|
||||
|
||||
export const getMarkerEnd = (arrowHeadType?: ArrowHeadType, markerEndId?: string): string => {
|
||||
export const getMarkerEnd = (markerType?: MarkerType, markerEndId?: string): string => {
|
||||
if (typeof markerEndId !== 'undefined' && markerEndId) {
|
||||
return `url(#${markerEndId})`;
|
||||
}
|
||||
|
||||
return typeof arrowHeadType !== 'undefined' ? `url(#react-flow__${arrowHeadType})` : 'none';
|
||||
return typeof markerType !== 'undefined' ? `url(#react-flow__${markerType})` : 'none';
|
||||
};
|
||||
|
||||
export interface GetCenterParams {
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import React, { memo, ComponentType, useCallback, useState, useMemo } from 'react';
|
||||
import cc from 'classcat';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { useStoreActions, useStoreState } from '../../store/hooks';
|
||||
import { Edge, EdgeProps, WrapEdgeProps } from '../../types';
|
||||
import { useStore, useStoreApi } from '../../store';
|
||||
import { Edge, EdgeProps, WrapEdgeProps, ReactFlowState, Connection } from '../../types';
|
||||
import { onMouseDown } from '../../components/Handle/handler';
|
||||
import { EdgeAnchor } from './EdgeAnchor';
|
||||
import { getMarkerId } from '../../utils/graph';
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
addSelectedEdges: s.addSelectedEdges,
|
||||
connectionMode: s.connectionMode,
|
||||
});
|
||||
|
||||
export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
const EdgeWrapper = ({
|
||||
@@ -23,7 +30,6 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
arrowHeadType,
|
||||
source,
|
||||
target,
|
||||
sourceX,
|
||||
@@ -33,29 +39,27 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
elementsSelectable,
|
||||
markerEndId,
|
||||
isHidden,
|
||||
hidden,
|
||||
sourceHandleId,
|
||||
targetHandleId,
|
||||
handleEdgeUpdate,
|
||||
onConnectEdge,
|
||||
onContextMenu,
|
||||
onMouseEnter,
|
||||
onMouseMove,
|
||||
onMouseLeave,
|
||||
edgeUpdaterRadius,
|
||||
onEdgeUpdate,
|
||||
onEdgeUpdateStart,
|
||||
onEdgeUpdateEnd,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
}: WrapEdgeProps): JSX.Element | null => {
|
||||
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
|
||||
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
|
||||
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
||||
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
|
||||
const connectionMode = useStoreState((state) => state.connectionMode);
|
||||
const store = useStoreApi();
|
||||
const { addSelectedEdges, connectionMode } = useStore(selector, shallow);
|
||||
|
||||
const [updating, setUpdating] = useState<boolean>(false);
|
||||
|
||||
const inactive = !elementsSelectable && !onClick;
|
||||
const handleEdgeUpdate = typeof onEdgeUpdate !== 'undefined';
|
||||
const edgeClasses = cc([
|
||||
'react-flow__edge',
|
||||
`react-flow__edge-${type}`,
|
||||
@@ -89,8 +93,8 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
const onEdgeClick = useCallback(
|
||||
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
|
||||
if (elementsSelectable) {
|
||||
unsetNodesSelection();
|
||||
addSelectedElements(edgeElement);
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
addSelectedEdges([edgeElement.id]);
|
||||
}
|
||||
|
||||
onClick?.(event, edgeElement);
|
||||
@@ -147,21 +151,30 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edgeElement, handleType)
|
||||
: undefined;
|
||||
|
||||
const onConnectEdge = (connection: Connection) => {
|
||||
const { edges } = store.getState();
|
||||
const edge = edges.find((e) => e.id === id);
|
||||
|
||||
if (edge && onEdgeUpdate) {
|
||||
onEdgeUpdate(edge, connection);
|
||||
}
|
||||
};
|
||||
|
||||
onMouseDown(
|
||||
event,
|
||||
handleId,
|
||||
nodeId,
|
||||
setConnectionNodeId,
|
||||
setPosition,
|
||||
store.setState,
|
||||
onConnectEdge,
|
||||
isTarget,
|
||||
isValidConnection,
|
||||
connectionMode,
|
||||
handleType,
|
||||
_onEdgeUpdate
|
||||
store.getState
|
||||
);
|
||||
},
|
||||
[id, source, target, type, sourceHandleId, targetHandleId, setConnectionNodeId, setPosition, edgeElement, onConnectEdge]
|
||||
[id, source, target, type, sourceHandleId, targetHandleId, edgeElement, onEdgeUpdate]
|
||||
);
|
||||
|
||||
const onEdgeUpdaterSourceMouseDown = useCallback(
|
||||
@@ -180,8 +193,10 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
|
||||
const onEdgeUpdaterMouseEnter = useCallback(() => setUpdating(true), [setUpdating]);
|
||||
const onEdgeUpdaterMouseOut = useCallback(() => setUpdating(false), [setUpdating]);
|
||||
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart)})`, [markerStart]);
|
||||
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd)})`, [markerEnd]);
|
||||
|
||||
if (isHidden) {
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -209,16 +224,16 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
labelBgBorderRadius={labelBgBorderRadius}
|
||||
data={data}
|
||||
style={style}
|
||||
arrowHeadType={arrowHeadType}
|
||||
sourceX={sourceX}
|
||||
sourceY={sourceY}
|
||||
targetX={targetX}
|
||||
targetY={targetY}
|
||||
sourcePosition={sourcePosition}
|
||||
targetPosition={targetPosition}
|
||||
markerEndId={markerEndId}
|
||||
sourceHandleId={sourceHandleId}
|
||||
targetHandleId={targetHandleId}
|
||||
markerStart={markerStartUrl}
|
||||
markerEnd={markerEndUrl}
|
||||
/>
|
||||
{handleEdgeUpdate && (
|
||||
<g
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useStoreActions } from '../../store/hooks';
|
||||
import { Elements } from '../../types';
|
||||
|
||||
interface ElementUpdaterProps {
|
||||
elements: Elements;
|
||||
}
|
||||
|
||||
const ElementUpdater = ({ elements }: ElementUpdaterProps) => {
|
||||
const setElements = useStoreActions((actions) => actions.setElements);
|
||||
|
||||
useEffect(() => {
|
||||
setElements(elements);
|
||||
}, [elements]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ElementUpdater;
|
||||
@@ -1,24 +1,19 @@
|
||||
import { MouseEvent as ReactMouseEvent } from 'react';
|
||||
import { SetState } from 'zustand';
|
||||
|
||||
import { getHostForElement } from '../../utils';
|
||||
|
||||
import {
|
||||
ElementId,
|
||||
XYPosition,
|
||||
OnConnectFunc,
|
||||
OnConnectStartFunc,
|
||||
OnConnectStopFunc,
|
||||
OnConnectEndFunc,
|
||||
OnConnect,
|
||||
OnConnectStart,
|
||||
OnConnectStop,
|
||||
OnConnectEnd,
|
||||
ConnectionMode,
|
||||
SetConnectionId,
|
||||
Connection,
|
||||
HandleType,
|
||||
ReactFlowState,
|
||||
} from '../../types';
|
||||
|
||||
type ValidConnectionFunc = (connection: Connection) => boolean;
|
||||
export type SetSourceIdFunc = (params: SetConnectionId) => void;
|
||||
|
||||
export type SetPosition = (pos: XYPosition) => void;
|
||||
|
||||
type Result = {
|
||||
elementBelow: Element | null;
|
||||
@@ -28,17 +23,15 @@ type Result = {
|
||||
};
|
||||
|
||||
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
|
||||
function checkElementBelowIsValid(
|
||||
export function checkElementBelowIsValid(
|
||||
event: MouseEvent,
|
||||
connectionMode: ConnectionMode,
|
||||
isTarget: boolean,
|
||||
nodeId: ElementId,
|
||||
handleId: ElementId | null,
|
||||
nodeId: string,
|
||||
handleId: string | null,
|
||||
isValidConnection: ValidConnectionFunc,
|
||||
doc: Document | ShadowRoot
|
||||
) {
|
||||
// TODO: why does this throw an error? elementFromPoint should be available for ShadowRoot too
|
||||
// @ts-ignore
|
||||
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
|
||||
const elementBelowIsTarget = elementBelow?.classList.contains('target') || false;
|
||||
const elementBelowIsSource = elementBelow?.classList.contains('source') || false;
|
||||
@@ -91,19 +84,18 @@ function resetRecentHandle(hoveredHandle: Element): void {
|
||||
|
||||
export function onMouseDown(
|
||||
event: ReactMouseEvent,
|
||||
handleId: ElementId | null,
|
||||
nodeId: ElementId,
|
||||
setConnectionNodeId: SetSourceIdFunc,
|
||||
setPosition: SetPosition,
|
||||
onConnect: OnConnectFunc,
|
||||
handleId: string | null,
|
||||
nodeId: string,
|
||||
setState: SetState<ReactFlowState>,
|
||||
onConnect: OnConnect,
|
||||
isTarget: boolean,
|
||||
isValidConnection: ValidConnectionFunc,
|
||||
connectionMode: ConnectionMode,
|
||||
elementEdgeUpdaterType?: HandleType,
|
||||
onEdgeUpdateEnd?: (evt: MouseEvent) => void,
|
||||
onConnectStart?: OnConnectStartFunc,
|
||||
onConnectStop?: OnConnectStopFunc,
|
||||
onConnectEnd?: OnConnectEndFunc
|
||||
onConnectStart?: OnConnectStart,
|
||||
onConnectStop?: OnConnectStop,
|
||||
onConnectEnd?: OnConnectEnd
|
||||
): void {
|
||||
const reactFlowNode = (event.target as Element).closest('.react-flow');
|
||||
// when react-flow is used inside a shadow root we can't use document
|
||||
@@ -113,7 +105,6 @@ export function onMouseDown(
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
|
||||
const elementBelowIsTarget = elementBelow?.classList.contains('target');
|
||||
const elementBelowIsSource = elementBelow?.classList.contains('source');
|
||||
@@ -126,18 +117,24 @@ export function onMouseDown(
|
||||
const containerBounds = reactFlowNode.getBoundingClientRect();
|
||||
let recentHoveredHandle: Element;
|
||||
|
||||
setPosition({
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
setState({
|
||||
connectionPosition: {
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
},
|
||||
connectionNodeId: nodeId,
|
||||
connectionHandleId: handleId,
|
||||
connectionHandleType: handleType,
|
||||
});
|
||||
|
||||
setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType });
|
||||
onConnectStart?.(event, { nodeId, handleId, handleType });
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
setPosition({
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
setState({
|
||||
connectionPosition: {
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
},
|
||||
});
|
||||
|
||||
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
|
||||
@@ -187,7 +184,11 @@ export function onMouseDown(
|
||||
}
|
||||
|
||||
resetRecentHandle(recentHoveredHandle);
|
||||
setConnectionNodeId({ connectionNodeId: null, connectionHandleId: null, connectionHandleType: null });
|
||||
setState({
|
||||
connectionNodeId: null,
|
||||
connectionHandleId: null,
|
||||
connectionHandleType: null,
|
||||
});
|
||||
|
||||
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject);
|
||||
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject);
|
||||
|
||||
+107
-33
@@ -1,16 +1,29 @@
|
||||
import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react';
|
||||
import cc from 'classcat';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { useStoreActions, useStoreState } from '../../store/hooks';
|
||||
import { useStore, useStoreApi } from '../../store';
|
||||
import NodeIdContext from '../../contexts/NodeIdContext';
|
||||
import { HandleProps, Connection, ElementId, Position } from '../../types';
|
||||
|
||||
import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler';
|
||||
import { HandleProps, Connection, ReactFlowState, Position } from '../../types';
|
||||
import { checkElementBelowIsValid, onMouseDown } from './handler';
|
||||
import { getHostForElement } from '../../utils';
|
||||
import { addEdge } from '../../utils/graph';
|
||||
|
||||
const alwaysValid = () => true;
|
||||
|
||||
export type HandleComponentProps = HandleProps & Omit<HTMLAttributes<HTMLDivElement>, 'id'>;
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
onConnectAction: s.onConnect,
|
||||
onConnectStart: s.onConnectStart,
|
||||
onConnectStop: s.onConnectStop,
|
||||
onConnectEnd: s.onConnectEnd,
|
||||
connectionMode: s.connectionMode,
|
||||
connectionStartHandle: s.connectionStartHandle,
|
||||
connectOnClick: s.connectOnClick,
|
||||
hasDefaultEdges: s.hasDefaultEdges,
|
||||
});
|
||||
|
||||
const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
|
||||
(
|
||||
{
|
||||
@@ -26,49 +39,64 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const nodeId = useContext(NodeIdContext) as ElementId;
|
||||
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
|
||||
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
|
||||
const onConnectAction = useStoreState((state) => state.onConnect);
|
||||
const onConnectStart = useStoreState((state) => state.onConnectStart);
|
||||
const onConnectStop = useStoreState((state) => state.onConnectStop);
|
||||
const onConnectEnd = useStoreState((state) => state.onConnectEnd);
|
||||
const connectionMode = useStoreState((state) => state.connectionMode);
|
||||
const store = useStoreApi();
|
||||
const nodeId = useContext(NodeIdContext) as string;
|
||||
const {
|
||||
onConnectAction,
|
||||
onConnectStart,
|
||||
onConnectStop,
|
||||
onConnectEnd,
|
||||
connectionMode,
|
||||
connectionStartHandle,
|
||||
connectOnClick,
|
||||
hasDefaultEdges,
|
||||
} = useStore(selector, shallow);
|
||||
|
||||
const handleId = id || null;
|
||||
const isTarget = type === 'target';
|
||||
|
||||
const onConnectExtended = useCallback(
|
||||
(params: Connection) => {
|
||||
onConnectAction?.(params);
|
||||
onConnect?.(params);
|
||||
const { defaultEdgeOptions } = store.getState();
|
||||
|
||||
const edgeParams = {
|
||||
...defaultEdgeOptions,
|
||||
...params,
|
||||
};
|
||||
if (hasDefaultEdges) {
|
||||
const { edges } = store.getState();
|
||||
store.setState({ edges: addEdge(edgeParams, edges) });
|
||||
}
|
||||
|
||||
onConnectAction?.(edgeParams);
|
||||
onConnect?.(edgeParams);
|
||||
},
|
||||
[onConnectAction, onConnect]
|
||||
[hasDefaultEdges, onConnectAction, onConnect]
|
||||
);
|
||||
|
||||
const onMouseDownHandler = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
onMouseDown(
|
||||
event,
|
||||
handleId,
|
||||
nodeId,
|
||||
setConnectionNodeId as unknown as SetSourceIdFunc,
|
||||
setPosition as unknown as SetPosition,
|
||||
onConnectExtended,
|
||||
isTarget,
|
||||
isValidConnection,
|
||||
connectionMode,
|
||||
undefined,
|
||||
undefined,
|
||||
onConnectStart,
|
||||
onConnectStop,
|
||||
onConnectEnd
|
||||
);
|
||||
if (event.button === 0) {
|
||||
onMouseDown(
|
||||
event,
|
||||
handleId,
|
||||
nodeId,
|
||||
store.setState,
|
||||
onConnectExtended,
|
||||
isTarget,
|
||||
isValidConnection,
|
||||
connectionMode,
|
||||
undefined,
|
||||
undefined,
|
||||
onConnectStart,
|
||||
onConnectStop,
|
||||
onConnectEnd
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
handleId,
|
||||
nodeId,
|
||||
setConnectionNodeId,
|
||||
setPosition,
|
||||
onConnectExtended,
|
||||
isTarget,
|
||||
isValidConnection,
|
||||
@@ -79,6 +107,47 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
|
||||
]
|
||||
);
|
||||
|
||||
const onClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (!connectionStartHandle) {
|
||||
onConnectStart?.(event, { nodeId, handleId, handleType: type });
|
||||
store.setState({ connectionStartHandle: { nodeId, type, handleId } });
|
||||
} else {
|
||||
const doc = getHostForElement(event.target as HTMLElement);
|
||||
const { connection, isValid } = checkElementBelowIsValid(
|
||||
event as unknown as MouseEvent,
|
||||
connectionMode,
|
||||
connectionStartHandle.type === 'target',
|
||||
connectionStartHandle.nodeId,
|
||||
connectionStartHandle.handleId || null,
|
||||
isValidConnection,
|
||||
doc
|
||||
);
|
||||
|
||||
onConnectStop?.(event as unknown as MouseEvent);
|
||||
|
||||
if (isValid) {
|
||||
onConnectExtended(connection);
|
||||
}
|
||||
|
||||
onConnectEnd?.(event as unknown as MouseEvent);
|
||||
|
||||
store.setState({ connectionStartHandle: null });
|
||||
}
|
||||
},
|
||||
[
|
||||
connectionStartHandle,
|
||||
onConnectStart,
|
||||
onConnectExtended,
|
||||
onConnectStop,
|
||||
onConnectEnd,
|
||||
isTarget,
|
||||
nodeId,
|
||||
handleId,
|
||||
type,
|
||||
]
|
||||
);
|
||||
|
||||
const handleClasses = cc([
|
||||
'react-flow__handle',
|
||||
`react-flow__handle-${position}`,
|
||||
@@ -88,6 +157,10 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
|
||||
source: !isTarget,
|
||||
target: isTarget,
|
||||
connectable: isConnectable,
|
||||
connecting:
|
||||
connectionStartHandle?.nodeId === nodeId &&
|
||||
connectionStartHandle?.handleId === handleId &&
|
||||
connectionStartHandle?.type === type,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -98,6 +171,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
|
||||
data-handlepos={position}
|
||||
className={handleClasses}
|
||||
onMouseDown={onMouseDownHandler}
|
||||
onClick={connectOnClick ? onClick : undefined}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
>
|
||||
|
||||
@@ -8,13 +8,15 @@ const DefaultNode = ({
|
||||
isConnectable,
|
||||
targetPosition = Position.Top,
|
||||
sourcePosition = Position.Bottom,
|
||||
}: NodeProps) => (
|
||||
<>
|
||||
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
|
||||
{data.label}
|
||||
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
|
||||
</>
|
||||
);
|
||||
}: NodeProps) => {
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
|
||||
{data?.label}
|
||||
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DefaultNode.displayName = 'DefaultNode';
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const GroupNode = () => null;
|
||||
|
||||
GroupNode.displayName = 'GroupNode';
|
||||
|
||||
export default GroupNode;
|
||||
@@ -5,7 +5,7 @@ import { NodeProps, Position } from '../../types';
|
||||
|
||||
const InputNode = ({ data, isConnectable, sourcePosition = Position.Bottom }: NodeProps) => (
|
||||
<>
|
||||
{data.label}
|
||||
{data?.label}
|
||||
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NodeProps, Position } from '../../types';
|
||||
const OutputNode = ({ data, isConnectable, targetPosition = Position.Top }: NodeProps) => (
|
||||
<>
|
||||
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
|
||||
{data.label}
|
||||
{data?.label}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { MouseEvent, useCallback } from 'react';
|
||||
import { GetState } from 'zustand';
|
||||
|
||||
import { ReactFlowState, Node } from '../../types';
|
||||
|
||||
function useMemoizedMouseHandler(
|
||||
id: string,
|
||||
dragging: boolean,
|
||||
getState: GetState<ReactFlowState>,
|
||||
handler?: (event: MouseEvent, node: Node) => void
|
||||
) {
|
||||
const memoizedHandler = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (typeof handler !== 'undefined' && !dragging) {
|
||||
const node = getState().nodeInternals.get(id)!;
|
||||
handler(event, { ...node });
|
||||
}
|
||||
},
|
||||
[handler, dragging, id]
|
||||
);
|
||||
|
||||
return memoizedHandler;
|
||||
}
|
||||
|
||||
export default useMemoizedMouseHandler;
|
||||
@@ -13,7 +13,7 @@ export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => {
|
||||
export const getHandleBoundsByHandleType = (
|
||||
selector: string,
|
||||
nodeElement: HTMLDivElement,
|
||||
parentBounds: ClientRect | DOMRect,
|
||||
parentBounds: DOMRect,
|
||||
k: number
|
||||
): HandleElement[] | null => {
|
||||
const handles = nodeElement.querySelectorAll(selector);
|
||||
@@ -24,20 +24,18 @@ export const getHandleBoundsByHandleType = (
|
||||
|
||||
const handlesArray = Array.from(handles) as HTMLDivElement[];
|
||||
|
||||
return handlesArray.map(
|
||||
(handle): HandleElement => {
|
||||
const bounds = handle.getBoundingClientRect();
|
||||
const dimensions = getDimensions(handle);
|
||||
const handleId = handle.getAttribute('data-handleid');
|
||||
const handlePosition = (handle.getAttribute('data-handlepos') as unknown) as Position;
|
||||
return handlesArray.map((handle): HandleElement => {
|
||||
const bounds = handle.getBoundingClientRect();
|
||||
const dimensions = getDimensions(handle);
|
||||
const handleId = handle.getAttribute('data-handleid');
|
||||
const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position;
|
||||
|
||||
return {
|
||||
id: handleId,
|
||||
position: handlePosition,
|
||||
x: (bounds.left - parentBounds.left) / k,
|
||||
y: (bounds.top - parentBounds.top) / k,
|
||||
...dimensions,
|
||||
};
|
||||
}
|
||||
);
|
||||
return {
|
||||
id: handleId,
|
||||
position: handlePosition,
|
||||
x: (bounds.left - parentBounds.left) / k,
|
||||
y: (bounds.top - parentBounds.top) / k,
|
||||
...dimensions,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
+125
-124
@@ -1,22 +1,21 @@
|
||||
import React, {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
memo,
|
||||
ComponentType,
|
||||
CSSProperties,
|
||||
useMemo,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
|
||||
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable';
|
||||
import cc from 'classcat';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { useStoreActions } from '../../store/hooks';
|
||||
import { useStore, useStoreApi } from '../../store';
|
||||
import { Provider } from '../../contexts/NodeIdContext';
|
||||
import { NodeComponentProps, WrapNodeProps } from '../../types';
|
||||
import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types';
|
||||
import useMemoizedMouseHandler from './useMemoizedMouseHandler';
|
||||
|
||||
export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
addSelectedNodes: s.addSelectedNodes,
|
||||
updateNodePosition: s.updateNodePosition,
|
||||
unselectNodesAndEdges: s.unselectNodesAndEdges,
|
||||
updateNodeDimensions: s.updateNodeDimensions,
|
||||
});
|
||||
|
||||
export default (NodeComponent: ComponentType<NodeProps>) => {
|
||||
const NodeWrapper = ({
|
||||
id,
|
||||
type,
|
||||
@@ -42,192 +41,193 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
||||
selectNodesOnDrag,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
isHidden,
|
||||
isInitialized,
|
||||
hidden,
|
||||
snapToGrid,
|
||||
snapGrid,
|
||||
isDragging,
|
||||
dragging,
|
||||
resizeObserver,
|
||||
dragHandle,
|
||||
zIndex,
|
||||
isParent,
|
||||
noPanClassName,
|
||||
noDragClassName,
|
||||
}: WrapNodeProps) => {
|
||||
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
|
||||
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
|
||||
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff);
|
||||
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
||||
|
||||
const store = useStoreApi();
|
||||
const { addSelectedNodes, unselectNodesAndEdges, updateNodePosition, updateNodeDimensions } = useStore(
|
||||
selector,
|
||||
shallow
|
||||
);
|
||||
const nodeElement = useRef<HTMLDivElement>(null);
|
||||
|
||||
const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]);
|
||||
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
|
||||
|
||||
const prevSourcePosition = useRef(sourcePosition);
|
||||
const prevTargetPosition = useRef(targetPosition);
|
||||
const prevType = useRef(type);
|
||||
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
|
||||
const nodeStyle: CSSProperties = useMemo(
|
||||
() => ({
|
||||
zIndex: selected ? 10 : 3,
|
||||
zIndex,
|
||||
transform: `translate(${xPos}px,${yPos}px)`,
|
||||
pointerEvents:
|
||||
isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none',
|
||||
// prevents jumping of nodes on start
|
||||
opacity: isInitialized ? 1 : 0,
|
||||
pointerEvents: hasPointerEvents ? 'all' : 'none',
|
||||
...style,
|
||||
}),
|
||||
[
|
||||
selected,
|
||||
xPos,
|
||||
yPos,
|
||||
isSelectable,
|
||||
isDraggable,
|
||||
onClick,
|
||||
isInitialized,
|
||||
style,
|
||||
onMouseEnter,
|
||||
onMouseMove,
|
||||
onMouseLeave,
|
||||
]
|
||||
[zIndex, xPos, yPos, hasPointerEvents, style]
|
||||
);
|
||||
const onMouseEnterHandler = useMemo(() => {
|
||||
if (!onMouseEnter || isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (event: MouseEvent) => onMouseEnter(event, node);
|
||||
}, [onMouseEnter, isDragging, node]);
|
||||
const grid = useMemo(
|
||||
() => (snapToGrid ? snapGrid : [1, 1])! as [number, number],
|
||||
[snapToGrid, snapGrid?.[0], snapGrid?.[1]]
|
||||
);
|
||||
|
||||
const onMouseMoveHandler = useMemo(() => {
|
||||
if (!onMouseMove || isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (event: MouseEvent) => onMouseMove(event, node);
|
||||
}, [onMouseMove, isDragging, node]);
|
||||
|
||||
const onMouseLeaveHandler = useMemo(() => {
|
||||
if (!onMouseLeave || isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (event: MouseEvent) => onMouseLeave(event, node);
|
||||
}, [onMouseLeave, isDragging, node]);
|
||||
|
||||
const onContextMenuHandler = useMemo(() => {
|
||||
if (!onContextMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (event: MouseEvent) => onContextMenu(event, node);
|
||||
}, [onContextMenu, node]);
|
||||
const onMouseEnterHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseEnter);
|
||||
const onMouseMoveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseMove);
|
||||
const onMouseLeaveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseLeave);
|
||||
const onContextMenuHandler = useMemoizedMouseHandler(id, false, store.getState, onContextMenu);
|
||||
const onNodeDoubleClickHandler = useMemoizedMouseHandler(id, false, store.getState, onNodeDoubleClick);
|
||||
|
||||
const onSelectNodeHandler = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (!isDraggable) {
|
||||
if (isSelectable) {
|
||||
unsetNodesSelection();
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
|
||||
if (!selected) {
|
||||
addSelectedElements(node);
|
||||
addSelectedNodes([id]);
|
||||
}
|
||||
}
|
||||
|
||||
onClick?.(event, node);
|
||||
if (onClick) {
|
||||
const node = store.getState().nodeInternals.get(id)!;
|
||||
onClick(event, { ...node });
|
||||
}
|
||||
}
|
||||
},
|
||||
[isSelectable, selected, isDraggable, onClick, node]
|
||||
[isSelectable, selected, isDraggable, onClick, id]
|
||||
);
|
||||
|
||||
const onDragStart = useCallback(
|
||||
(event: DraggableEvent) => {
|
||||
onNodeDragStart?.(event as MouseEvent, node);
|
||||
|
||||
if (selectNodesOnDrag && isSelectable) {
|
||||
unsetNodesSelection();
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
|
||||
if (!selected) {
|
||||
addSelectedElements(node);
|
||||
addSelectedNodes([id]);
|
||||
}
|
||||
} else if (!selectNodesOnDrag && !selected && isSelectable) {
|
||||
unsetNodesSelection();
|
||||
addSelectedElements([]);
|
||||
const { multiSelectionActive } = store.getState();
|
||||
if (multiSelectionActive) {
|
||||
addSelectedNodes([id]);
|
||||
} else {
|
||||
unselectNodesAndEdges();
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
}
|
||||
}
|
||||
|
||||
if (onNodeDragStart) {
|
||||
const node = store.getState().nodeInternals.get(id)!;
|
||||
onNodeDragStart(event as MouseEvent, { ...node });
|
||||
}
|
||||
},
|
||||
[node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
|
||||
[id, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
|
||||
);
|
||||
|
||||
const onDrag = useCallback(
|
||||
(event: DraggableEvent, draggableData: DraggableData) => {
|
||||
if (onNodeDrag) {
|
||||
node.position.x += draggableData.deltaX;
|
||||
node.position.y += draggableData.deltaY;
|
||||
onNodeDrag(event as MouseEvent, node);
|
||||
}
|
||||
updateNodePosition({ id, dragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } });
|
||||
|
||||
updateNodePosDiff({
|
||||
id,
|
||||
diff: {
|
||||
x: draggableData.deltaX,
|
||||
y: draggableData.deltaY,
|
||||
},
|
||||
isDragging: true,
|
||||
});
|
||||
if (onNodeDrag) {
|
||||
const node = store.getState().nodeInternals.get(id)!;
|
||||
onNodeDrag(event as MouseEvent, {
|
||||
...node,
|
||||
dragging: true,
|
||||
position: {
|
||||
x: node.position.x + draggableData.deltaX,
|
||||
y: node.position.y + draggableData.deltaY,
|
||||
},
|
||||
positionAbsolute: {
|
||||
x: (node.positionAbsolute?.x || 0) + draggableData.deltaX,
|
||||
y: (node.positionAbsolute?.y || 0) + draggableData.deltaY,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[id, node, onNodeDrag]
|
||||
[id, onNodeDrag]
|
||||
);
|
||||
|
||||
const onDragStop = useCallback(
|
||||
(event: DraggableEvent) => {
|
||||
// onDragStop also gets called when user just clicks on a node.
|
||||
// Because of that we set dragging to true inside the onDrag handler and handle the click here
|
||||
if (!isDragging) {
|
||||
let node;
|
||||
|
||||
if (onClick || onNodeDragStop) {
|
||||
node = store.getState().nodeInternals.get(id)!;
|
||||
}
|
||||
|
||||
if (!dragging) {
|
||||
if (isSelectable && !selectNodesOnDrag && !selected) {
|
||||
addSelectedElements(node);
|
||||
addSelectedNodes([id]);
|
||||
}
|
||||
|
||||
onClick?.(event as MouseEvent, node);
|
||||
if (onClick && node) {
|
||||
onClick(event as MouseEvent, { ...node });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateNodePosDiff({
|
||||
id: node.id,
|
||||
isDragging: false,
|
||||
updateNodePosition({
|
||||
id,
|
||||
dragging: false,
|
||||
});
|
||||
|
||||
onNodeDragStop?.(event as MouseEvent, node);
|
||||
if (onNodeDragStop && node) {
|
||||
onNodeDragStop(event as MouseEvent, { ...node, dragging: false });
|
||||
}
|
||||
},
|
||||
[node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, selected]
|
||||
[id, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, dragging, selected]
|
||||
);
|
||||
|
||||
const onNodeDoubleClickHandler = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
onNodeDoubleClick?.(event, node);
|
||||
},
|
||||
[node, onNodeDoubleClick]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (nodeElement.current && !isHidden) {
|
||||
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
|
||||
}
|
||||
}, [id, isHidden, sourcePosition, targetPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
if (nodeElement.current) {
|
||||
if (nodeElement.current && !hidden) {
|
||||
const currNode = nodeElement.current;
|
||||
resizeObserver?.observe(currNode);
|
||||
|
||||
return () => resizeObserver?.unobserve(currNode);
|
||||
}
|
||||
}, []);
|
||||
}, [hidden]);
|
||||
|
||||
if (isHidden) {
|
||||
useEffect(() => {
|
||||
// when the user programmatically changes the source or handle position, we re-initialize the node
|
||||
const typeChanged = prevType.current !== type;
|
||||
const sourcePosChanged = prevSourcePosition.current !== sourcePosition;
|
||||
const targetPosChanged = prevTargetPosition.current !== targetPosition;
|
||||
|
||||
if (nodeElement.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
|
||||
if (typeChanged) {
|
||||
prevType.current = type;
|
||||
}
|
||||
if (sourcePosChanged) {
|
||||
prevSourcePosition.current = sourcePosition;
|
||||
}
|
||||
if (targetPosChanged) {
|
||||
prevTargetPosition.current = targetPosition;
|
||||
}
|
||||
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
|
||||
}
|
||||
}, [id, type, sourcePosition, targetPosition]);
|
||||
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodeClasses = cc([
|
||||
'react-flow__node',
|
||||
`react-flow__node-${type}`,
|
||||
noPanClassName,
|
||||
className,
|
||||
{
|
||||
selected,
|
||||
selectable: isSelectable,
|
||||
parent: isParent,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -238,7 +238,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
||||
onStop={onDragStop}
|
||||
scale={scale}
|
||||
disabled={!isDraggable}
|
||||
cancel=".nodrag"
|
||||
cancel={`.${noDragClassName}`}
|
||||
nodeRef={nodeElement}
|
||||
grid={grid}
|
||||
enableUserSelectHack={false}
|
||||
@@ -267,8 +267,9 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
||||
isConnectable={isConnectable}
|
||||
sourcePosition={sourcePosition}
|
||||
targetPosition={targetPosition}
|
||||
isDragging={isDragging}
|
||||
dragging={dragging}
|
||||
dragHandle={dragHandle}
|
||||
zIndex={zIndex}
|
||||
/>
|
||||
</Provider>
|
||||
</div>
|
||||
|
||||
@@ -1,57 +1,53 @@
|
||||
/**
|
||||
* The nodes selection rectangle gets displayed when a user
|
||||
* made a selectio with on or several nodes
|
||||
* made a selection with on or several nodes
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback, useRef, MouseEvent } from 'react';
|
||||
import ReactDraggable, { DraggableData } from 'react-draggable';
|
||||
import React, { memo, useMemo, useCallback, useRef, MouseEvent } from 'react';
|
||||
import { DraggableCore, DraggableData } from 'react-draggable';
|
||||
import cc from 'classcat';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { useStoreState, useStoreActions } from '../../store/hooks';
|
||||
import { isNode } from '../../utils/graph';
|
||||
import { Node } from '../../types';
|
||||
import { useStore } from '../../store';
|
||||
import { Node, ReactFlowState } from '../../types';
|
||||
import { getRectOfNodes } from '../../utils/graph';
|
||||
|
||||
export interface NodesSelectionProps {
|
||||
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
|
||||
onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void;
|
||||
onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void;
|
||||
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
|
||||
noPanClassName?: string;
|
||||
}
|
||||
// @TODO: work with nodeInternals instead of converting it to an array
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
transform: s.transform,
|
||||
selectedNodesBbox: s.selectedNodesBbox,
|
||||
userSelectionActive: s.userSelectionActive,
|
||||
selectedNodes: Array.from(s.nodeInternals)
|
||||
.filter(([_, n]) => n.selected)
|
||||
.map(([_, n]) => n),
|
||||
snapToGrid: s.snapToGrid,
|
||||
snapGrid: s.snapGrid,
|
||||
updateNodePosition: s.updateNodePosition,
|
||||
});
|
||||
|
||||
export default ({
|
||||
function NodesSelection({
|
||||
onSelectionDragStart,
|
||||
onSelectionDrag,
|
||||
onSelectionDragStop,
|
||||
onSelectionContextMenu,
|
||||
}: NodesSelectionProps) => {
|
||||
const [tX, tY, tScale] = useStoreState((state) => state.transform);
|
||||
const selectedNodesBbox = useStoreState((state) => state.selectedNodesBbox);
|
||||
const selectionActive = useStoreState((state) => state.selectionActive);
|
||||
const selectedElements = useStoreState((state) => state.selectedElements);
|
||||
const snapToGrid = useStoreState((state) => state.snapToGrid);
|
||||
const snapGrid = useStoreState((state) => state.snapGrid);
|
||||
const nodes = useStoreState((state) => state.nodes);
|
||||
|
||||
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff);
|
||||
|
||||
noPanClassName,
|
||||
}: NodesSelectionProps) {
|
||||
const { transform, userSelectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } = useStore(
|
||||
selector,
|
||||
shallow
|
||||
);
|
||||
const [tX, tY, tScale] = transform;
|
||||
const nodeRef = useRef(null);
|
||||
|
||||
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
|
||||
|
||||
const selectedNodes = useMemo(
|
||||
() =>
|
||||
selectedElements
|
||||
? selectedElements.filter(isNode).map((selectedNode) => {
|
||||
const matchingNode = nodes.find((node) => node.id === selectedNode.id);
|
||||
|
||||
return {
|
||||
...matchingNode,
|
||||
position: matchingNode?.__rf.position,
|
||||
} as Node;
|
||||
})
|
||||
: [],
|
||||
[selectedElements, nodes]
|
||||
);
|
||||
|
||||
const style = useMemo(
|
||||
() => ({
|
||||
transform: `translate(${tX}px,${tY}px) scale(${tScale})`,
|
||||
@@ -59,6 +55,8 @@ export default ({
|
||||
[tX, tY, tScale]
|
||||
);
|
||||
|
||||
const selectedNodesBbox = useMemo(() => getRectOfNodes(selectedNodes), [selectedNodes]);
|
||||
|
||||
const innerStyle = useMemo(
|
||||
() => ({
|
||||
width: selectedNodesBbox.width,
|
||||
@@ -78,25 +76,23 @@ export default ({
|
||||
|
||||
const onDrag = useCallback(
|
||||
(event: MouseEvent, data: DraggableData) => {
|
||||
if (onSelectionDrag) {
|
||||
onSelectionDrag(event, selectedNodes);
|
||||
}
|
||||
|
||||
updateNodePosDiff({
|
||||
updateNodePosition({
|
||||
diff: {
|
||||
x: data.deltaX,
|
||||
y: data.deltaY,
|
||||
},
|
||||
isDragging: true,
|
||||
dragging: true,
|
||||
});
|
||||
|
||||
onSelectionDrag?.(event, selectedNodes);
|
||||
},
|
||||
[onSelectionDrag, selectedNodes, updateNodePosDiff]
|
||||
[onSelectionDrag, selectedNodes, updateNodePosition]
|
||||
);
|
||||
|
||||
const onStop = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
updateNodePosDiff({
|
||||
isDragging: false,
|
||||
updateNodePosition({
|
||||
dragging: false,
|
||||
});
|
||||
|
||||
onSelectionDragStop?.(event, selectedNodes);
|
||||
@@ -106,22 +102,18 @@ export default ({
|
||||
|
||||
const onContextMenu = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
const selectedNodes = selectedElements
|
||||
? selectedElements.filter(isNode).map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)!)
|
||||
: [];
|
||||
|
||||
onSelectionContextMenu?.(event, selectedNodes);
|
||||
},
|
||||
[onSelectionContextMenu]
|
||||
[onSelectionContextMenu, selectedNodes]
|
||||
);
|
||||
|
||||
if (!selectedElements || selectionActive) {
|
||||
if (!selectedNodes?.length || userSelectionActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="react-flow__nodesselection" style={style}>
|
||||
<ReactDraggable
|
||||
<div className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} style={style}>
|
||||
<DraggableCore
|
||||
scale={tScale}
|
||||
grid={grid}
|
||||
onStart={(event) => onStart(event as MouseEvent)}
|
||||
@@ -136,7 +128,9 @@ export default ({
|
||||
onContextMenu={onContextMenu}
|
||||
style={innerStyle}
|
||||
/>
|
||||
</ReactDraggable>
|
||||
</DraggableCore>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default memo(NodesSelection);
|
||||
|
||||
@@ -1,20 +1,38 @@
|
||||
import { useEffect } from 'react';
|
||||
import { memo, useEffect } from 'react';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { Elements } from '../../types';
|
||||
import { useStoreState } from '../../store/hooks';
|
||||
import { ReactFlowState, OnSelectionChangeFunc, Node, Edge } from '../../types';
|
||||
import { useStore } from '../../store';
|
||||
|
||||
interface SelectionListenerProps {
|
||||
onSelectionChange: (elements: Elements | null) => void;
|
||||
onSelectionChange: OnSelectionChangeFunc;
|
||||
}
|
||||
|
||||
// This is a helper component for calling the onSelectionChange listener
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
selectedNodes: Array.from(s.nodeInternals.values()).filter((n) => n.selected),
|
||||
selectedEdges: s.edges.filter((e) => e.selected),
|
||||
});
|
||||
|
||||
export default ({ onSelectionChange }: SelectionListenerProps) => {
|
||||
const selectedElements = useStoreState((s) => s.selectedElements);
|
||||
const areEqual = (objA: any, objB: any) => {
|
||||
const selectedNodeIdsA = objA.selectedNodes.map((n: Node) => n.id);
|
||||
const selectedNodeIdsB = objB.selectedNodes.map((n: Node) => n.id);
|
||||
|
||||
const selectedEdgeIdsA = objA.selectedEdges.map((e: Edge) => e.id);
|
||||
const selectedEdgeIdsB = objB.selectedEdges.map((e: Edge) => e.id);
|
||||
|
||||
return shallow(selectedNodeIdsA, selectedNodeIdsB) && shallow(selectedEdgeIdsA, selectedEdgeIdsB);
|
||||
};
|
||||
|
||||
// This is just a helper component for calling the onSelectionChange listener.
|
||||
// @TODO: Now that we have the onNodesChange and on EdgesChange listeners, do we still need this component?
|
||||
function SelectionListener({ onSelectionChange }: SelectionListenerProps) {
|
||||
const { selectedNodes, selectedEdges } = useStore(selector, areEqual);
|
||||
|
||||
useEffect(() => {
|
||||
onSelectionChange(selectedElements);
|
||||
}, [selectedElements]);
|
||||
onSelectionChange({ nodes: selectedNodes, edges: selectedEdges });
|
||||
}, [selectedNodes, selectedEdges]);
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export default memo(SelectionListener);
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import { useEffect } from 'react';
|
||||
import { SetState } from 'zustand';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { useStore, useStoreApi } from '../../store';
|
||||
import {
|
||||
Node,
|
||||
Edge,
|
||||
ReactFlowState,
|
||||
OnConnect,
|
||||
OnConnectStart,
|
||||
OnConnectStop,
|
||||
OnConnectEnd,
|
||||
CoordinateExtent,
|
||||
OnNodesChange,
|
||||
OnEdgesChange,
|
||||
ConnectionMode,
|
||||
SnapGrid,
|
||||
DefaultEdgeOptions,
|
||||
FitViewOptions,
|
||||
OnNodesDelete,
|
||||
OnEdgesDelete
|
||||
} from '../../types';
|
||||
|
||||
interface StoreUpdaterProps {
|
||||
nodes?: Node[];
|
||||
edges?: Edge[];
|
||||
defaultNodes?: Node[];
|
||||
defaultEdges?: Edge[];
|
||||
onConnect?: OnConnect;
|
||||
onConnectStart?: OnConnectStart;
|
||||
onConnectStop?: OnConnectStop;
|
||||
onConnectEnd?: OnConnectEnd;
|
||||
nodesDraggable?: boolean;
|
||||
nodesConnectable?: boolean;
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
nodeExtent?: CoordinateExtent;
|
||||
onNodesChange?: OnNodesChange;
|
||||
onEdgesChange?: OnEdgesChange;
|
||||
elementsSelectable?: boolean;
|
||||
connectionMode?: ConnectionMode;
|
||||
snapToGrid?: boolean;
|
||||
snapGrid?: SnapGrid;
|
||||
translateExtent?: CoordinateExtent;
|
||||
connectOnClick: boolean;
|
||||
defaultEdgeOptions?: DefaultEdgeOptions;
|
||||
fitView?: boolean;
|
||||
fitViewOptions?: FitViewOptions;
|
||||
onNodesDelete?: OnNodesDelete;
|
||||
onEdgesDelete?: OnEdgesDelete;
|
||||
}
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
setNodes: s.setNodes,
|
||||
setEdges: s.setEdges,
|
||||
setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
|
||||
setMinZoom: s.setMinZoom,
|
||||
setMaxZoom: s.setMaxZoom,
|
||||
setTranslateExtent: s.setTranslateExtent,
|
||||
setNodeExtent: s.setNodeExtent,
|
||||
reset: s.reset,
|
||||
});
|
||||
|
||||
function useStoreUpdater<T>(value: T | undefined, setStoreState: (param: T) => void) {
|
||||
useEffect(() => {
|
||||
if (typeof value !== 'undefined') {
|
||||
setStoreState(value);
|
||||
}
|
||||
}, [value]);
|
||||
}
|
||||
|
||||
function useDirectStoreUpdater(key: keyof ReactFlowState, value: any, setState: SetState<ReactFlowState>) {
|
||||
useEffect(() => {
|
||||
if (typeof value !== 'undefined') {
|
||||
// @ts-ignore
|
||||
setState({ [key]: value });
|
||||
}
|
||||
}, [value]);
|
||||
}
|
||||
|
||||
const StoreUpdater = ({
|
||||
nodes,
|
||||
edges,
|
||||
defaultNodes,
|
||||
defaultEdges,
|
||||
onConnect,
|
||||
onConnectStart,
|
||||
onConnectStop,
|
||||
onConnectEnd,
|
||||
nodesDraggable,
|
||||
nodesConnectable,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
nodeExtent,
|
||||
onNodesChange,
|
||||
onEdgesChange,
|
||||
elementsSelectable,
|
||||
connectionMode,
|
||||
snapGrid,
|
||||
snapToGrid,
|
||||
translateExtent,
|
||||
connectOnClick,
|
||||
defaultEdgeOptions,
|
||||
fitView,
|
||||
fitViewOptions,
|
||||
onNodesDelete,
|
||||
onEdgesDelete,
|
||||
}: StoreUpdaterProps) => {
|
||||
const {
|
||||
setNodes,
|
||||
setEdges,
|
||||
setDefaultNodesAndEdges,
|
||||
setMinZoom,
|
||||
setMaxZoom,
|
||||
setTranslateExtent,
|
||||
setNodeExtent,
|
||||
reset,
|
||||
} = useStore(selector, shallow);
|
||||
const store = useStoreApi();
|
||||
|
||||
useEffect(() => {
|
||||
setDefaultNodesAndEdges(defaultNodes, defaultEdges);
|
||||
|
||||
return () => {
|
||||
reset();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useDirectStoreUpdater('defaultEdgeOptions', defaultEdgeOptions, store.setState);
|
||||
useDirectStoreUpdater('connectionMode', connectionMode, store.setState);
|
||||
useDirectStoreUpdater('onConnect', onConnect, store.setState);
|
||||
useDirectStoreUpdater('onConnectStart', onConnectStart, store.setState);
|
||||
useDirectStoreUpdater('onConnectStop', onConnectStop, store.setState);
|
||||
useDirectStoreUpdater('onConnectEnd', onConnectEnd, store.setState);
|
||||
useDirectStoreUpdater('nodesDraggable', nodesDraggable, store.setState);
|
||||
useDirectStoreUpdater('nodesConnectable', nodesConnectable, store.setState);
|
||||
useDirectStoreUpdater('elementsSelectable', elementsSelectable, store.setState);
|
||||
useDirectStoreUpdater('snapToGrid', snapToGrid, store.setState);
|
||||
useDirectStoreUpdater('snapGrid', snapGrid, store.setState);
|
||||
useDirectStoreUpdater('onNodesChange', onNodesChange, store.setState);
|
||||
useDirectStoreUpdater('onEdgesChange', onEdgesChange, store.setState);
|
||||
useDirectStoreUpdater('connectOnClick', connectOnClick, store.setState);
|
||||
useDirectStoreUpdater('fitViewOnInit', fitView, store.setState);
|
||||
useDirectStoreUpdater('fitViewOnInitOptions', fitViewOptions, store.setState);
|
||||
useDirectStoreUpdater('onNodesDelete', onNodesDelete, store.setState);
|
||||
useDirectStoreUpdater('onEdgesDelete', onEdgesDelete, store.setState);
|
||||
|
||||
useStoreUpdater<Node[]>(nodes, setNodes);
|
||||
useStoreUpdater<Edge[]>(edges, setEdges);
|
||||
useStoreUpdater<Node[]>(defaultNodes, setNodes);
|
||||
useStoreUpdater<Edge[]>(defaultEdges, setEdges);
|
||||
useStoreUpdater<number>(minZoom, setMinZoom);
|
||||
useStoreUpdater<number>(maxZoom, setMaxZoom);
|
||||
useStoreUpdater<CoordinateExtent>(translateExtent, setTranslateExtent);
|
||||
useStoreUpdater<CoordinateExtent>(nodeExtent, setNodeExtent);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default StoreUpdater;
|
||||
@@ -2,100 +2,160 @@
|
||||
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useState, useRef, useCallback } from 'react';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { useStoreActions, useStoreState } from '../../store/hooks';
|
||||
import { XYPosition } from '../../types';
|
||||
import { useStore, useStoreApi } from '../../store';
|
||||
import { getSelectionChanges } from '../../utils/changes';
|
||||
import { XYPosition, ReactFlowState, NodeChange, EdgeChange, Rect } from '../../types';
|
||||
import { getConnectedEdges, getNodesInside } from '../../utils/graph';
|
||||
|
||||
type SelectionRect = Rect & {
|
||||
startX: number;
|
||||
startY: number;
|
||||
draw: boolean;
|
||||
};
|
||||
|
||||
type UserSelectionProps = {
|
||||
selectionKeyPressed: boolean;
|
||||
};
|
||||
|
||||
function getMousePosition(event: React.MouseEvent): XYPosition | void {
|
||||
const reactFlowNode = (event.target as Element).closest('.react-flow');
|
||||
if (!reactFlowNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const containerBounds = reactFlowNode.getBoundingClientRect();
|
||||
|
||||
function getMousePosition(event: React.MouseEvent, containerBounds: DOMRect): XYPosition {
|
||||
return {
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
};
|
||||
}
|
||||
|
||||
const SelectionRect = () => {
|
||||
const userSelectionRect = useStoreState((state) => state.userSelectionRect);
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
userSelectionActive: s.userSelectionActive,
|
||||
elementsSelectable: s.elementsSelectable,
|
||||
});
|
||||
|
||||
if (!userSelectionRect.draw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="react-flow__selection"
|
||||
style={{
|
||||
width: userSelectionRect.width,
|
||||
height: userSelectionRect.height,
|
||||
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const initialRect: SelectionRect = {
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
draw: false,
|
||||
};
|
||||
|
||||
export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
|
||||
const selectionActive = useStoreState((state) => state.selectionActive);
|
||||
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
|
||||
const store = useStoreApi();
|
||||
const prevSelectedNodesCount = useRef<number>(0);
|
||||
const prevSelectedEdgesCount = useRef<number>(0);
|
||||
const containerBounds = useRef<DOMRect>();
|
||||
const [userSelectionRect, setUserSelectionRect] = useState<SelectionRect>(initialRect);
|
||||
const { userSelectionActive, elementsSelectable } = useStore(selector, shallow);
|
||||
|
||||
const setUserSelection = useStoreActions((actions) => actions.setUserSelection);
|
||||
const updateUserSelection = useStoreActions((actions) => actions.updateUserSelection);
|
||||
const unsetUserSelection = useStoreActions((actions) => actions.unsetUserSelection);
|
||||
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
||||
const renderUserSelectionPane = selectionActive || selectionKeyPressed;
|
||||
const renderUserSelectionPane = userSelectionActive || selectionKeyPressed;
|
||||
|
||||
const resetUserSelection = useCallback(() => {
|
||||
setUserSelectionRect(initialRect);
|
||||
|
||||
store.setState({ userSelectionActive: false });
|
||||
|
||||
prevSelectedNodesCount.current = 0;
|
||||
prevSelectedEdgesCount.current = 0;
|
||||
}, []);
|
||||
|
||||
const onMouseDown = useCallback((event: React.MouseEvent): void => {
|
||||
const reactFlowNode = (event.target as Element).closest('.react-flow')!;
|
||||
containerBounds.current = reactFlowNode.getBoundingClientRect();
|
||||
|
||||
const mousePos = getMousePosition(event, containerBounds.current!);
|
||||
|
||||
setUserSelectionRect({
|
||||
width: 0,
|
||||
height: 0,
|
||||
startX: mousePos.x,
|
||||
startY: mousePos.y,
|
||||
x: mousePos.x,
|
||||
y: mousePos.y,
|
||||
draw: true,
|
||||
});
|
||||
|
||||
store.setState({ userSelectionActive: true, nodesSelectionActive: false });
|
||||
}, []);
|
||||
|
||||
const onMouseMove = (event: React.MouseEvent): void => {
|
||||
if (!selectionKeyPressed || !userSelectionRect.draw || !containerBounds.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mousePos = getMousePosition(event, containerBounds.current!);
|
||||
const startX = userSelectionRect.startX ?? 0;
|
||||
const startY = userSelectionRect.startY ?? 0;
|
||||
|
||||
const nextUserSelectRect = {
|
||||
...userSelectionRect,
|
||||
x: mousePos.x < startX ? mousePos.x : startX,
|
||||
y: mousePos.y < startY ? mousePos.y : startY,
|
||||
width: Math.abs(mousePos.x - startX),
|
||||
height: Math.abs(mousePos.y - startY),
|
||||
};
|
||||
|
||||
const { nodeInternals, edges, transform, onNodesChange, onEdgesChange } = store.getState();
|
||||
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
|
||||
const selectedNodes = getNodesInside(nodeInternals, nextUserSelectRect, transform, false, true);
|
||||
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
|
||||
const selectedNodeIds = selectedNodes.map((n) => n.id);
|
||||
|
||||
if (prevSelectedNodesCount.current !== selectedNodeIds.length) {
|
||||
prevSelectedNodesCount.current = selectedNodeIds.length;
|
||||
const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
|
||||
if (changes.length) {
|
||||
onNodesChange?.(changes);
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) {
|
||||
prevSelectedEdgesCount.current = selectedEdgeIds.length;
|
||||
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
|
||||
if (changes.length) {
|
||||
onEdgesChange?.(changes);
|
||||
}
|
||||
}
|
||||
|
||||
setUserSelectionRect(nextUserSelectRect);
|
||||
};
|
||||
|
||||
const onMouseUp = useCallback(() => {
|
||||
store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 });
|
||||
|
||||
resetUserSelection();
|
||||
}, []);
|
||||
|
||||
const onMouseLeave = useCallback(() => {
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
|
||||
resetUserSelection();
|
||||
}, []);
|
||||
|
||||
if (!elementsSelectable || !renderUserSelectionPane) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onMouseDown = (event: React.MouseEvent): void => {
|
||||
const mousePos = getMousePosition(event);
|
||||
if (!mousePos) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUserSelection(mousePos);
|
||||
};
|
||||
|
||||
const onMouseMove = (event: React.MouseEvent): void => {
|
||||
if (!selectionKeyPressed || !selectionActive) {
|
||||
return;
|
||||
}
|
||||
const mousePos = getMousePosition(event);
|
||||
|
||||
if (!mousePos) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateUserSelection(mousePos);
|
||||
};
|
||||
|
||||
const onMouseUp = () => unsetUserSelection();
|
||||
|
||||
const onMouseLeave = () => {
|
||||
unsetUserSelection();
|
||||
unsetNodesSelection();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="react-flow__selectionpane"
|
||||
className="react-flow__selectionpane react-flow__container"
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<SelectionRect />
|
||||
{userSelectionRect.draw && (
|
||||
<div
|
||||
className="react-flow__selection react-flow__container"
|
||||
style={{
|
||||
width: userSelectionRect.width,
|
||||
height: userSelectionRect.height,
|
||||
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user