Merge branch 'next' into refactor/edge-rendering
This commit is contained in:
@@ -55,8 +55,8 @@ function Background({
|
||||
{
|
||||
...style,
|
||||
...containerStyle,
|
||||
'--background-color-props': bgColor,
|
||||
'--background-pattern-color-props': color,
|
||||
'--xy-background-color-props': bgColor,
|
||||
'--xy-background-pattern-color-props': color,
|
||||
} as CSSProperties
|
||||
}
|
||||
ref={ref}
|
||||
|
||||
@@ -26,6 +26,6 @@ type DotPatternProps = {
|
||||
|
||||
export function DotPattern({ radius, className }: DotPatternProps) {
|
||||
return (
|
||||
<circle cx={radius} cy={radius} r={radius} className={cc(['react-flow__background-pattern', 'dot', className])} />
|
||||
<circle cx={radius} cy={radius} r={radius} className={cc(['react-flow__background-pattern', 'dots', className])} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { memo, useEffect, useRef, type MouseEvent, useCallback } from 'react';
|
||||
import { memo, useEffect, useRef, type MouseEvent, useCallback, CSSProperties } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system';
|
||||
@@ -40,15 +40,15 @@ const ARIA_LABEL_KEY = 'react-flow__minimap-desc';
|
||||
function MiniMap({
|
||||
style,
|
||||
className,
|
||||
nodeStrokeColor = 'transparent',
|
||||
nodeColor = '#e2e2e2',
|
||||
nodeStrokeColor,
|
||||
nodeColor,
|
||||
nodeClassName = '',
|
||||
nodeBorderRadius = 5,
|
||||
nodeStrokeWidth = 2,
|
||||
nodeStrokeWidth,
|
||||
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
|
||||
// a component properly.
|
||||
nodeComponent,
|
||||
maskColor = 'rgb(240, 240, 240, 0.6)',
|
||||
maskColor,
|
||||
maskStrokeColor = 'none',
|
||||
maskStrokeWidth = 1,
|
||||
position = 'bottom-right',
|
||||
@@ -126,7 +126,15 @@ function MiniMap({
|
||||
return (
|
||||
<Panel
|
||||
position={position}
|
||||
style={style}
|
||||
style={
|
||||
{
|
||||
...style,
|
||||
'--xy-minimap-mask-color-props': typeof maskColor === 'string' ? maskColor : undefined,
|
||||
'--xy-minimap-node-background-color-props': typeof nodeColor === 'string' ? nodeColor : undefined,
|
||||
'--xy-minimap-node-stroke-color-props': typeof nodeStrokeColor === 'string' ? nodeStrokeColor : undefined,
|
||||
'--xy-minimap-node-stroke-width-props': typeof nodeStrokeWidth === 'string' ? nodeStrokeWidth : undefined,
|
||||
} as CSSProperties
|
||||
}
|
||||
className={cc(['react-flow__minimap', className])}
|
||||
data-testid="rf__minimap"
|
||||
>
|
||||
@@ -153,7 +161,6 @@ function MiniMap({
|
||||
className="react-flow__minimap-mask"
|
||||
d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
|
||||
M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`}
|
||||
fill={maskColor}
|
||||
fillRule="evenodd"
|
||||
stroke={maskStrokeColor}
|
||||
strokeWidth={maskStrokeWidth}
|
||||
|
||||
@@ -31,9 +31,11 @@ function MiniMapNode({
|
||||
ry={borderRadius}
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={strokeWidth}
|
||||
style={{
|
||||
fill,
|
||||
stroke: strokeColor,
|
||||
strokeWidth,
|
||||
}}
|
||||
shapeRendering={shapeRendering}
|
||||
onClick={onClick ? (event) => onClick(event, id) : undefined}
|
||||
/>
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { memo } from 'react';
|
||||
import { ComponentType, memo } from 'react';
|
||||
import { NodeOrigin, getNodePositionWithOrigin } from '@xyflow/system';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getNodePositionWithOrigin } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
import MiniMapNode from './MiniMapNode';
|
||||
import type { MiniMapNodes, GetMiniMapNodeAttribute } from './types';
|
||||
import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types';
|
||||
|
||||
declare const window: any;
|
||||
|
||||
const selector = (s: ReactFlowState) => s.nodeOrigin;
|
||||
const selectorNodes = (s: ReactFlowState) => s.nodes.filter((node) => !node.hidden && node.width && node.height);
|
||||
const selectorNodeIds = (s: ReactFlowState) => s.nodes.map((node) => node.id);
|
||||
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
|
||||
|
||||
function MiniMapNodes({
|
||||
nodeStrokeColor = 'transparent',
|
||||
nodeColor = '#e2e2e2',
|
||||
nodeStrokeColor,
|
||||
nodeColor,
|
||||
nodeClassName = '',
|
||||
nodeBorderRadius = 5,
|
||||
nodeStrokeWidth = 2,
|
||||
nodeStrokeWidth,
|
||||
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
|
||||
// a component properly.
|
||||
nodeComponent: NodeComponent = MiniMapNode,
|
||||
onClick,
|
||||
}: MiniMapNodes) {
|
||||
const nodes = useStore(selectorNodes, shallow);
|
||||
}: MiniMapNodesProps) {
|
||||
const nodeIds = useStore(selectorNodeIds, shallow);
|
||||
const nodeOrigin = useStore(selector);
|
||||
const nodeColorFunc = getAttrFunction(nodeColor);
|
||||
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
|
||||
@@ -36,31 +36,78 @@ function MiniMapNodes({
|
||||
|
||||
return (
|
||||
<>
|
||||
{nodes.map((node) => {
|
||||
const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
|
||||
|
||||
return (
|
||||
<NodeComponent
|
||||
key={node.id}
|
||||
x={x}
|
||||
y={y}
|
||||
width={node.width!}
|
||||
height={node.height!}
|
||||
style={node.style}
|
||||
selected={!!node.selected}
|
||||
className={nodeClassNameFunc(node)}
|
||||
color={nodeColorFunc(node)}
|
||||
borderRadius={nodeBorderRadius}
|
||||
strokeColor={nodeStrokeColorFunc(node)}
|
||||
strokeWidth={nodeStrokeWidth}
|
||||
shapeRendering={shapeRendering}
|
||||
onClick={onClick}
|
||||
id={node.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{nodeIds.map((nodeId) => (
|
||||
// The split of responsibilities between MiniMapNodes and
|
||||
// NodeComponentWrapper may appear weird. However, it’s designed to
|
||||
// minimize the cost of updates when individual nodes change.
|
||||
//
|
||||
// For more details, see a similar commit in `NodeRenderer/index.tsx`.
|
||||
<NodeComponentWrapper
|
||||
key={nodeId}
|
||||
id={nodeId}
|
||||
nodeOrigin={nodeOrigin}
|
||||
nodeColorFunc={nodeColorFunc}
|
||||
nodeStrokeColorFunc={nodeStrokeColorFunc}
|
||||
nodeClassNameFunc={nodeClassNameFunc}
|
||||
nodeBorderRadius={nodeBorderRadius}
|
||||
nodeStrokeWidth={nodeStrokeWidth}
|
||||
NodeComponent={NodeComponent}
|
||||
onClick={onClick}
|
||||
shapeRendering={shapeRendering}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const NodeComponentWrapper = memo(function NodeComponentWrapper({
|
||||
id,
|
||||
nodeOrigin,
|
||||
nodeColorFunc,
|
||||
nodeStrokeColorFunc,
|
||||
nodeClassNameFunc,
|
||||
nodeBorderRadius,
|
||||
nodeStrokeWidth,
|
||||
shapeRendering,
|
||||
NodeComponent,
|
||||
onClick,
|
||||
}: {
|
||||
id: string;
|
||||
nodeOrigin: NodeOrigin;
|
||||
nodeColorFunc: GetMiniMapNodeAttribute;
|
||||
nodeStrokeColorFunc: GetMiniMapNodeAttribute;
|
||||
nodeClassNameFunc: GetMiniMapNodeAttribute;
|
||||
nodeBorderRadius: number;
|
||||
nodeStrokeWidth?: number;
|
||||
NodeComponent: ComponentType<MiniMapNodeProps>;
|
||||
onClick: MiniMapNodesProps['onClick'];
|
||||
shapeRendering: string;
|
||||
}) {
|
||||
const node = useStore((s) => s.nodeLookup.get(id));
|
||||
if (!node || node.hidden || !(node.computed?.width || node.width) || !(node.computed?.height || node.height)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const positionOrigin = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
|
||||
|
||||
return (
|
||||
<NodeComponent
|
||||
x={positionOrigin.x}
|
||||
y={positionOrigin.y}
|
||||
width={node.computed?.width ?? node.width ?? 0}
|
||||
height={node.computed?.height ?? node.height ?? 0}
|
||||
style={node.style}
|
||||
selected={!!node.selected}
|
||||
className={nodeClassNameFunc(node)}
|
||||
color={nodeColorFunc(node)}
|
||||
borderRadius={nodeBorderRadius}
|
||||
strokeColor={nodeStrokeColorFunc(node)}
|
||||
strokeWidth={nodeStrokeWidth}
|
||||
shapeRendering={shapeRendering}
|
||||
onClick={onClick}
|
||||
id={node.id}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default memo(MiniMapNodes);
|
||||
|
||||
@@ -42,10 +42,10 @@ export type MiniMapNodeProps = {
|
||||
height: number;
|
||||
borderRadius: number;
|
||||
className: string;
|
||||
color: string;
|
||||
color?: string;
|
||||
shapeRendering: string;
|
||||
strokeColor: string;
|
||||
strokeWidth: number;
|
||||
strokeColor?: string;
|
||||
strokeWidth?: number;
|
||||
style?: CSSProperties;
|
||||
selected: boolean;
|
||||
onClick?: (event: MouseEvent, id: string) => void;
|
||||
|
||||
@@ -70,8 +70,8 @@ function ResizeControl({
|
||||
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
|
||||
prevValues.current = {
|
||||
width: node?.width ?? 0,
|
||||
height: node?.height ?? 0,
|
||||
width: node?.computed?.width ?? 0,
|
||||
height: node?.computed?.height ?? 0,
|
||||
x: node?.position.x ?? 0,
|
||||
y: node?.position.y ?? 0,
|
||||
};
|
||||
|
||||
@@ -1,74 +1,40 @@
|
||||
import { useCallback, CSSProperties } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getNodesBounds, Transform, Rect, Position, internalsSymbol } from '@xyflow/system';
|
||||
import { getNodesBounds, Rect, Position, internalsSymbol, getNodeToolbarTransform } from '@xyflow/system';
|
||||
|
||||
import { Node, ReactFlowState } from '../../types';
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { useNodeId } from '../../contexts/NodeIdContext';
|
||||
import NodeToolbarPortal from './NodeToolbarPortal';
|
||||
import { Align, NodeToolbarProps } from './types';
|
||||
import { NodeToolbarProps } from './types';
|
||||
|
||||
const nodeEqualityFn = (a: Node | undefined, b: Node | undefined) =>
|
||||
a?.positionAbsolute?.x === b?.positionAbsolute?.x &&
|
||||
a?.positionAbsolute?.y === b?.positionAbsolute?.y &&
|
||||
a?.width === b?.width &&
|
||||
a?.height === b?.height &&
|
||||
a?.selected === b?.selected &&
|
||||
a?.[internalsSymbol]?.z === b?.[internalsSymbol]?.z;
|
||||
const nodeEqualityFn = (a?: Node, b?: Node) =>
|
||||
a?.computed?.positionAbsolute?.x !== b?.computed?.positionAbsolute?.x ||
|
||||
a?.computed?.positionAbsolute?.y !== b?.computed?.positionAbsolute?.y ||
|
||||
a?.computed?.width !== b?.computed?.width ||
|
||||
a?.computed?.height !== b?.computed?.height ||
|
||||
a?.selected !== b?.selected ||
|
||||
a?.[internalsSymbol]?.z !== b?.[internalsSymbol]?.z;
|
||||
|
||||
const nodesEqualityFn = (a: Node[], b: Node[]) => {
|
||||
return a.length === b.length && a.every((node, i) => nodeEqualityFn(node, b[i]));
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !a.some((node, i) => nodeEqualityFn(node, b[i]));
|
||||
};
|
||||
|
||||
const storeSelector = (state: ReactFlowState) => ({
|
||||
transform: state.transform,
|
||||
viewport: {
|
||||
x: state.transform[0],
|
||||
y: state.transform[1],
|
||||
zoom: state.transform[2],
|
||||
},
|
||||
nodeOrigin: state.nodeOrigin,
|
||||
selectedNodesCount: state.nodes.filter((node) => node.selected).length,
|
||||
});
|
||||
|
||||
function getTransform(nodeRect: Rect, transform: Transform, position: Position, offset: number, align: Align): string {
|
||||
let alignmentOffset = 0.5;
|
||||
|
||||
if (align === 'start') {
|
||||
alignmentOffset = 0;
|
||||
} else if (align === 'end') {
|
||||
alignmentOffset = 1;
|
||||
}
|
||||
|
||||
// position === Position.Top
|
||||
// we set the x any y position of the toolbar based on the nodes position
|
||||
let pos = [
|
||||
(nodeRect.x + nodeRect.width * alignmentOffset) * transform[2] + transform[0],
|
||||
nodeRect.y * transform[2] + transform[1] - offset,
|
||||
];
|
||||
// and than shift it based on the alignment. The shift values are in %.
|
||||
let shift = [-100 * alignmentOffset, -100];
|
||||
|
||||
switch (position) {
|
||||
case Position.Right:
|
||||
pos = [
|
||||
(nodeRect.x + nodeRect.width) * transform[2] + transform[0] + offset,
|
||||
(nodeRect.y + nodeRect.height * alignmentOffset) * transform[2] + transform[1],
|
||||
];
|
||||
shift = [0, -100 * alignmentOffset];
|
||||
break;
|
||||
case Position.Bottom:
|
||||
pos[1] = (nodeRect.y + nodeRect.height) * transform[2] + transform[1] + offset;
|
||||
shift[1] = 0;
|
||||
break;
|
||||
case Position.Left:
|
||||
pos = [
|
||||
nodeRect.x * transform[2] + transform[0] - offset,
|
||||
(nodeRect.y + nodeRect.height * alignmentOffset) * transform[2] + transform[1],
|
||||
];
|
||||
shift = [-100, -100 * alignmentOffset];
|
||||
break;
|
||||
}
|
||||
|
||||
return `translate(${pos[0]}px, ${pos[1]}px) translate(${shift[0]}%, ${shift[1]}%)`;
|
||||
}
|
||||
|
||||
function NodeToolbar({
|
||||
nodeId,
|
||||
children,
|
||||
@@ -97,7 +63,9 @@ function NodeToolbar({
|
||||
[nodeId, contextNodeId]
|
||||
);
|
||||
const nodes = useStore(nodesSelector, nodesEqualityFn);
|
||||
const { transform, nodeOrigin, selectedNodesCount } = useStore(storeSelector, shallow);
|
||||
const { viewport, nodeOrigin, selectedNodesCount } = useStore(storeSelector, shallow);
|
||||
|
||||
// if isVisible is not set, we show the toolbar only if its node is selected and no other node is selected
|
||||
const isActive =
|
||||
typeof isVisible === 'boolean' ? isVisible : nodes.length === 1 && nodes[0].selected && selectedNodesCount === 1;
|
||||
|
||||
@@ -110,14 +78,19 @@ function NodeToolbar({
|
||||
|
||||
const wrapperStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
transform: getTransform(nodeRect, transform, position, offset, align),
|
||||
transform: getNodeToolbarTransform(nodeRect, viewport, position, offset, align),
|
||||
zIndex,
|
||||
...style,
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeToolbarPortal>
|
||||
<div style={wrapperStyle} className={cc(['react-flow__node-toolbar', className])} {...rest}>
|
||||
<div
|
||||
style={wrapperStyle}
|
||||
className={cc(['react-flow__node-toolbar', className])}
|
||||
{...rest}
|
||||
data-id={nodes.reduce((acc, node) => `${acc}${node.id} `, '').trim()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</NodeToolbarPortal>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import type { Position } from '@xyflow/system';
|
||||
import type { Position, Align } from '@xyflow/system';
|
||||
|
||||
export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & {
|
||||
nodeId?: string | string[];
|
||||
@@ -8,5 +8,3 @@ export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & {
|
||||
offset?: number;
|
||||
align?: Align;
|
||||
};
|
||||
|
||||
export type Align = 'center' | 'start' | 'end';
|
||||
|
||||
@@ -65,10 +65,10 @@ const ConnectionLine = ({
|
||||
}
|
||||
|
||||
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 fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.computed?.width ?? 0) / 2;
|
||||
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.computed?.height ?? 0;
|
||||
const fromX = (fromNode.computed?.positionAbsolute?.x ?? 0) + fromHandleX;
|
||||
const fromY = (fromNode.computed?.positionAbsolute?.y ?? 0) + fromHandleY;
|
||||
const fromPosition = fromHandle?.position;
|
||||
const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
|
||||
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
import { memo, useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import {
|
||||
getMarkerId,
|
||||
elementSelectionKeys,
|
||||
XYHandle,
|
||||
type Connection,
|
||||
getEdgePosition,
|
||||
errorMessages,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStoreApi, useStore } from '../../hooks/useStore';
|
||||
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
|
||||
import { EdgeAnchor } from '../Edges/EdgeAnchor';
|
||||
import { getMouseHandler } from '../Edges/utils';
|
||||
import type { EdgeWrapperProps } from '../../types';
|
||||
import { builtinEdgeTypes } from './utils';
|
||||
|
||||
function EdgeWrapper({
|
||||
id,
|
||||
className,
|
||||
type,
|
||||
data,
|
||||
onClick,
|
||||
onEdgeDoubleClick,
|
||||
selected,
|
||||
animated,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
source,
|
||||
target,
|
||||
isSelectable,
|
||||
hidden,
|
||||
sourceHandleId,
|
||||
targetHandleId,
|
||||
onContextMenu,
|
||||
onMouseEnter,
|
||||
onMouseMove,
|
||||
onMouseLeave,
|
||||
edgeUpdaterRadius,
|
||||
onEdgeUpdate,
|
||||
onEdgeUpdateStart,
|
||||
onEdgeUpdateEnd,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
rfId,
|
||||
ariaLabel,
|
||||
isFocusable,
|
||||
isUpdatable,
|
||||
pathOptions,
|
||||
interactionWidth,
|
||||
edgeTypes,
|
||||
zIndex,
|
||||
onError,
|
||||
}: EdgeWrapperProps): JSX.Element | null {
|
||||
let edgeType = type || 'default';
|
||||
let EdgeComponent = edgeTypes?.[edgeType] || builtinEdgeTypes[edgeType];
|
||||
|
||||
if (EdgeComponent === undefined) {
|
||||
onError?.('011', errorMessages['error011'](edgeType));
|
||||
edgeType = 'default';
|
||||
EdgeComponent = builtinEdgeTypes.default;
|
||||
}
|
||||
|
||||
const edgeRef = useRef<SVGGElement>(null);
|
||||
const [updateHover, setUpdateHover] = useState<boolean>(false);
|
||||
const [updating, setUpdating] = useState<boolean>(false);
|
||||
const store = useStoreApi();
|
||||
const edgePosition = useStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const sourceNode = state.nodeLookup.get(source);
|
||||
const targetNode = state.nodeLookup.get(target);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getEdgePosition({
|
||||
id,
|
||||
sourceNode,
|
||||
targetNode,
|
||||
sourceHandle: sourceHandleId || null,
|
||||
targetHandle: targetHandleId || null,
|
||||
connectionMode: state.connectionMode,
|
||||
onError: state.onError,
|
||||
});
|
||||
},
|
||||
[source, target]
|
||||
),
|
||||
shallow
|
||||
);
|
||||
|
||||
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]);
|
||||
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]);
|
||||
|
||||
if (hidden || !edgePosition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
|
||||
const { edges, addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState();
|
||||
const edge = edges.find((e) => e.id === id);
|
||||
|
||||
if (!edge) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelectable) {
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
|
||||
if (edge.selected && multiSelectionActive) {
|
||||
unselectNodesAndEdges({ nodes: [], edges: [edge] });
|
||||
edgeRef.current?.blur();
|
||||
} else {
|
||||
addSelectedEdges([id]);
|
||||
}
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
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,
|
||||
connectionMode,
|
||||
connectionRadius,
|
||||
lib,
|
||||
onConnectStart,
|
||||
onConnectEnd,
|
||||
cancelConnection,
|
||||
nodes,
|
||||
panBy,
|
||||
updateConnection,
|
||||
} = store.getState();
|
||||
const nodeId = isSourceHandle ? target : source;
|
||||
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
|
||||
const handleType = isSourceHandle ? 'target' : 'source';
|
||||
|
||||
const isTarget = isSourceHandle;
|
||||
const edge = edges.find((e) => e.id === id)!;
|
||||
|
||||
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,
|
||||
lib,
|
||||
cancelConnection,
|
||||
panBy,
|
||||
isValidConnection,
|
||||
onConnect: onConnectEdge,
|
||||
onConnectStart,
|
||||
onConnectEnd,
|
||||
onEdgeUpdateEnd: _onEdgeUpdateEnd,
|
||||
updateConnection,
|
||||
getTransform: () => store.getState().transform,
|
||||
});
|
||||
};
|
||||
|
||||
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 (
|
||||
<svg style={{ zIndex }}>
|
||||
<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' : 'img'}
|
||||
data-id={id}
|
||||
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={edgePosition.sourceX}
|
||||
sourceY={edgePosition.sourceY}
|
||||
targetX={edgePosition.targetX}
|
||||
targetY={edgePosition.targetY}
|
||||
sourcePosition={edgePosition.sourcePosition}
|
||||
targetPosition={edgePosition.targetPosition}
|
||||
sourceHandleId={sourceHandleId}
|
||||
targetHandleId={targetHandleId}
|
||||
markerStart={markerStartUrl}
|
||||
markerEnd={markerEndUrl}
|
||||
pathOptions={pathOptions}
|
||||
interactionWidth={interactionWidth}
|
||||
/>
|
||||
)}
|
||||
{isUpdatable && (
|
||||
<>
|
||||
{(isUpdatable === 'source' || isUpdatable === true) && (
|
||||
<EdgeAnchor
|
||||
position={edgePosition.sourcePosition}
|
||||
centerX={edgePosition.sourceX}
|
||||
centerY={edgePosition.sourceY}
|
||||
radius={edgeUpdaterRadius}
|
||||
onMouseDown={onEdgeUpdaterSourceMouseDown}
|
||||
onMouseEnter={onEdgeUpdaterMouseEnter}
|
||||
onMouseOut={onEdgeUpdaterMouseOut}
|
||||
type="source"
|
||||
/>
|
||||
)}
|
||||
{(isUpdatable === 'target' || isUpdatable === true) && (
|
||||
<EdgeAnchor
|
||||
position={edgePosition.targetPosition}
|
||||
centerX={edgePosition.targetX}
|
||||
centerY={edgePosition.targetY}
|
||||
radius={edgeUpdaterRadius}
|
||||
onMouseDown={onEdgeUpdaterTargetMouseDown}
|
||||
onMouseEnter={onEdgeUpdaterMouseEnter}
|
||||
onMouseOut={onEdgeUpdaterMouseOut}
|
||||
type="target"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
EdgeWrapper.displayName = 'EdgeWrapper';
|
||||
|
||||
export default memo(EdgeWrapper);
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import type { EdgeProps, EdgeTypes } from '../../types';
|
||||
import {
|
||||
BezierEdgeInternal,
|
||||
StraightEdgeInternal,
|
||||
StepEdgeInternal,
|
||||
SmoothStepEdgeInternal,
|
||||
SimpleBezierEdgeInternal,
|
||||
} from '../Edges';
|
||||
|
||||
export const builtinEdgeTypes: EdgeTypes = {
|
||||
default: BezierEdgeInternal as ComponentType<EdgeProps>,
|
||||
straight: StraightEdgeInternal as ComponentType<EdgeProps>,
|
||||
step: StepEdgeInternal as ComponentType<EdgeProps>,
|
||||
smoothstep: SmoothStepEdgeInternal as ComponentType<EdgeProps>,
|
||||
simplebezier: SimpleBezierEdgeInternal as ComponentType<EdgeProps>,
|
||||
};
|
||||
@@ -4,56 +4,67 @@ 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({
|
||||
function createBezierEdge(params: { isInternal: boolean }) {
|
||||
// eslint-disable-next-line react/display-name
|
||||
return memo(
|
||||
({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
curvature: pathOptions?.curvature,
|
||||
});
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
const _id = params.isInternal ? undefined : id;
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
id={_id}
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const BezierEdge = createBezierEdge({ isInternal: false });
|
||||
const BezierEdgeInternal = createBezierEdge({ isInternal: true });
|
||||
|
||||
BezierEdge.displayName = 'BezierEdge';
|
||||
BezierEdgeInternal.displayName = 'BezierEdgeInternal';
|
||||
|
||||
export default BezierEdge;
|
||||
export { BezierEdge, BezierEdgeInternal };
|
||||
|
||||
@@ -2,7 +2,7 @@ import { memo } from 'react';
|
||||
import { Position, getBezierEdgeCenter } from '@xyflow/system';
|
||||
|
||||
import BaseEdge from './BaseEdge';
|
||||
import type { EdgeProps } from '../../types';
|
||||
import type { SimpleBezierEdgeProps } from '../../types';
|
||||
|
||||
export interface GetSimpleBezierPathParams {
|
||||
sourceX: number;
|
||||
@@ -71,54 +71,65 @@ export function getSimpleBezierPath({
|
||||
];
|
||||
}
|
||||
|
||||
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({
|
||||
function createSimpleBezierEdge(params: { isInternal: boolean }) {
|
||||
// eslint-disable-next-line react/display-name
|
||||
return memo(
|
||||
({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
sourcePosition = Position.Bottom,
|
||||
targetPosition = Position.Top,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
interactionWidth,
|
||||
}: SimpleBezierEdgeProps) => {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
const _id = params.isInternal ? undefined : id;
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
id={_id}
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const SimpleBezierEdge = createSimpleBezierEdge({ isInternal: false });
|
||||
const SimpleBezierEdgeInternal = createSimpleBezierEdge({ isInternal: true });
|
||||
|
||||
SimpleBezierEdge.displayName = 'SimpleBezierEdge';
|
||||
SimpleBezierEdgeInternal.displayName = 'SimpleBezierEdgeInternal';
|
||||
|
||||
export default SimpleBezierEdge;
|
||||
export { SimpleBezierEdge, SimpleBezierEdgeInternal };
|
||||
|
||||
@@ -4,57 +4,68 @@ 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({
|
||||
function createSmoothStepEdge(params: { isInternal: boolean }) {
|
||||
// eslint-disable-next-line react/display-name
|
||||
return memo(
|
||||
({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
borderRadius: pathOptions?.borderRadius,
|
||||
offset: pathOptions?.offset,
|
||||
});
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
const _id = params.isInternal ? undefined : id;
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
id={_id}
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const SmoothStepEdge = createSmoothStepEdge({ isInternal: false });
|
||||
const SmoothStepEdgeInternal = createSmoothStepEdge({ isInternal: true });
|
||||
|
||||
SmoothStepEdge.displayName = 'SmoothStepEdge';
|
||||
SmoothStepEdgeInternal.displayName = 'SmoothStepEdgeInternal';
|
||||
|
||||
export default SmoothStepEdge;
|
||||
export { SmoothStepEdge, SmoothStepEdgeInternal };
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
import SmoothStepEdge from './SmoothStepEdge';
|
||||
import type { SmoothStepEdgeProps } from '../../types';
|
||||
import { SmoothStepEdge } from './SmoothStepEdge';
|
||||
import type { StepEdgeProps } from '../../types';
|
||||
|
||||
const StepEdge = memo((props: SmoothStepEdgeProps) => (
|
||||
<SmoothStepEdge
|
||||
{...props}
|
||||
pathOptions={useMemo(() => ({ borderRadius: 0, offset: props.pathOptions?.offset }), [props.pathOptions?.offset])}
|
||||
/>
|
||||
));
|
||||
function createStepEdge(params: { isInternal: boolean }) {
|
||||
// eslint-disable-next-line react/display-name
|
||||
return memo(({ id, ...props }: StepEdgeProps) => {
|
||||
const _id = params.isInternal ? undefined : id;
|
||||
|
||||
return (
|
||||
<SmoothStepEdge
|
||||
{...props}
|
||||
id={_id}
|
||||
pathOptions={useMemo(
|
||||
() => ({ borderRadius: 0, offset: props.pathOptions?.offset }),
|
||||
[props.pathOptions?.offset]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const StepEdge = createStepEdge({ isInternal: false });
|
||||
const StepEdgeInternal = createStepEdge({ isInternal: true });
|
||||
|
||||
StepEdge.displayName = 'StepEdge';
|
||||
StepEdgeInternal.displayName = 'StepEdgeInternal';
|
||||
|
||||
export default StepEdge;
|
||||
export { StepEdge, StepEdgeInternal };
|
||||
|
||||
@@ -2,47 +2,58 @@ import { memo } from 'react';
|
||||
import { getStraightPath } from '@xyflow/system';
|
||||
|
||||
import BaseEdge from './BaseEdge';
|
||||
import type { EdgeProps } from '../../types';
|
||||
import type { StraightEdgeProps } 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 });
|
||||
function createStraightEdge(params: { isInternal: boolean }) {
|
||||
// eslint-disable-next-line react/display-name
|
||||
return memo(
|
||||
({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
interactionWidth,
|
||||
}: StraightEdgeProps) => {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
const _id = params.isInternal ? undefined : id;
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
id={_id}
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const StraightEdge = createStraightEdge({ isInternal: false });
|
||||
const StraightEdgeInternal = createStraightEdge({ isInternal: true });
|
||||
|
||||
StraightEdge.displayName = 'StraightEdge';
|
||||
StraightEdgeInternal.displayName = 'StraightEdgeInternal';
|
||||
|
||||
export default StraightEdge;
|
||||
export { StraightEdge, StraightEdgeInternal };
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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';
|
||||
// We distinguish between internal and exported edges
|
||||
// The internal edges are used directly like custom edges and always get an id, source and target props
|
||||
// If you import an edge from the library, the id is optional and source and target are not used at all
|
||||
|
||||
export { SimpleBezierEdge, SimpleBezierEdgeInternal } from './SimpleBezierEdge';
|
||||
export { SmoothStepEdge, SmoothStepEdgeInternal } from './SmoothStepEdge';
|
||||
export { StepEdge, StepEdgeInternal } from './StepEdge';
|
||||
export { StraightEdge, StraightEdgeInternal } from './StraightEdge';
|
||||
export { BezierEdge, BezierEdgeInternal } from './BezierEdge';
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent, useCallback } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getMarkerId, elementSelectionKeys, XYHandle, type Connection, getEdgePosition } from '@xyflow/system';
|
||||
|
||||
import { useStoreApi, useStore } from '../../hooks/useStore';
|
||||
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
|
||||
import { EdgeAnchor } from './EdgeAnchor';
|
||||
import { getMouseHandler } from './utils';
|
||||
import type { EdgeProps, WrapEdgeProps } from '../../types';
|
||||
|
||||
export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
const EdgeWrapper = ({
|
||||
id,
|
||||
className,
|
||||
type,
|
||||
data,
|
||||
onClick,
|
||||
onEdgeDoubleClick,
|
||||
selected,
|
||||
animated,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
zIndex,
|
||||
source,
|
||||
target,
|
||||
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 edgePosition = useStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const sourceNode = state.nodeLookup.get(source);
|
||||
const targetNode = state.nodeLookup.get(target);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getEdgePosition({
|
||||
id,
|
||||
sourceNode,
|
||||
targetNode,
|
||||
sourceHandle: sourceHandleId || null,
|
||||
targetHandle: targetHandleId || null,
|
||||
connectionMode: state.connectionMode,
|
||||
onError: state.onError,
|
||||
});
|
||||
},
|
||||
[source, target]
|
||||
),
|
||||
shallow
|
||||
);
|
||||
|
||||
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]);
|
||||
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]);
|
||||
|
||||
if (hidden || !edgePosition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
|
||||
const { edges, addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState();
|
||||
const edge = edges.find((e) => e.id === id);
|
||||
|
||||
if (!edge) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelectable) {
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
|
||||
if (edge.selected && multiSelectionActive) {
|
||||
unselectNodesAndEdges({ nodes: [], edges: [edge] });
|
||||
edgeRef.current?.blur();
|
||||
} else {
|
||||
addSelectedEdges([id]);
|
||||
}
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
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,
|
||||
connectionMode,
|
||||
connectionRadius,
|
||||
lib,
|
||||
onConnectStart,
|
||||
onConnectEnd,
|
||||
cancelConnection,
|
||||
nodes,
|
||||
panBy,
|
||||
updateConnection,
|
||||
} = store.getState();
|
||||
const nodeId = isSourceHandle ? target : source;
|
||||
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
|
||||
const handleType = isSourceHandle ? 'target' : 'source';
|
||||
|
||||
const isTarget = isSourceHandle;
|
||||
const edge = edges.find((e) => e.id === id)!;
|
||||
|
||||
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,
|
||||
lib,
|
||||
cancelConnection,
|
||||
panBy,
|
||||
isValidConnection,
|
||||
onConnect: onConnectEdge,
|
||||
onConnectStart,
|
||||
onConnectEnd,
|
||||
onEdgeUpdateEnd: _onEdgeUpdateEnd,
|
||||
updateConnection,
|
||||
getTransform: () => store.getState().transform,
|
||||
});
|
||||
};
|
||||
|
||||
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 (
|
||||
<svg style={{ zIndex }}>
|
||||
<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' : 'img'}
|
||||
data-id={id}
|
||||
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={edgePosition.sourceX}
|
||||
sourceY={edgePosition.sourceY}
|
||||
targetX={edgePosition.targetX}
|
||||
targetY={edgePosition.targetY}
|
||||
sourcePosition={edgePosition.sourcePosition}
|
||||
targetPosition={edgePosition.targetPosition}
|
||||
sourceHandleId={sourceHandleId}
|
||||
targetHandleId={targetHandleId}
|
||||
markerStart={markerStartUrl}
|
||||
markerEnd={markerEndUrl}
|
||||
pathOptions={pathOptions}
|
||||
interactionWidth={interactionWidth}
|
||||
/>
|
||||
)}
|
||||
{isUpdatable && (
|
||||
<>
|
||||
{(isUpdatable === 'source' || isUpdatable === true) && (
|
||||
<EdgeAnchor
|
||||
position={edgePosition.sourcePosition}
|
||||
centerX={edgePosition.sourceX}
|
||||
centerY={edgePosition.sourceY}
|
||||
radius={edgeUpdaterRadius}
|
||||
onMouseDown={onEdgeUpdaterSourceMouseDown}
|
||||
onMouseEnter={onEdgeUpdaterMouseEnter}
|
||||
onMouseOut={onEdgeUpdaterMouseOut}
|
||||
type="source"
|
||||
/>
|
||||
)}
|
||||
{(isUpdatable === 'target' || isUpdatable === true) && (
|
||||
<EdgeAnchor
|
||||
position={edgePosition.targetPosition}
|
||||
centerX={edgePosition.targetX}
|
||||
centerY={edgePosition.targetY}
|
||||
radius={edgeUpdaterRadius}
|
||||
onMouseDown={onEdgeUpdaterTargetMouseDown}
|
||||
onMouseEnter={onEdgeUpdaterMouseEnter}
|
||||
onMouseOut={onEdgeUpdaterMouseOut}
|
||||
type="target"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
EdgeWrapper.displayName = 'EdgeWrapper';
|
||||
|
||||
return memo(EdgeWrapper);
|
||||
};
|
||||
@@ -171,7 +171,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
|
||||
lib,
|
||||
});
|
||||
|
||||
if (isValid) {
|
||||
if (isValid && connection) {
|
||||
onConnectExtended(connection);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
import { useEffect, useRef, memo, type MouseEvent, type KeyboardEvent } from 'react';
|
||||
import cc from 'classcat';
|
||||
import {
|
||||
clampPosition,
|
||||
elementSelectionKeys,
|
||||
errorMessages,
|
||||
getPositionWithOrigin,
|
||||
internalsSymbol,
|
||||
isInputDOMNode,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStore, 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 { handleNodeClick } from '../Nodes/utils';
|
||||
import type { NodeWrapperProps } from '../../types';
|
||||
import { arrowKeyDiffs, builtinNodeTypes } from './utils';
|
||||
|
||||
const NodeWrapper = ({
|
||||
id,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseMove,
|
||||
onMouseLeave,
|
||||
onContextMenu,
|
||||
onDoubleClick,
|
||||
nodesDraggable,
|
||||
elementsSelectable,
|
||||
nodesConnectable,
|
||||
nodesFocusable,
|
||||
resizeObserver,
|
||||
noDragClassName,
|
||||
noPanClassName,
|
||||
disableKeyboardA11y,
|
||||
rfId,
|
||||
nodeTypes,
|
||||
nodeExtent,
|
||||
nodeOrigin,
|
||||
onError,
|
||||
}: NodeWrapperProps) => {
|
||||
const node = useStore((s) => s.nodeLookup.get(id)!);
|
||||
|
||||
let nodeType = node.type || 'default';
|
||||
let NodeComponent = nodeTypes?.[nodeType] || builtinNodeTypes[nodeType];
|
||||
|
||||
if (NodeComponent === undefined) {
|
||||
onError?.('003', errorMessages['error003'](nodeType));
|
||||
nodeType = 'default';
|
||||
NodeComponent = builtinNodeTypes.default;
|
||||
}
|
||||
|
||||
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
|
||||
const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined'));
|
||||
const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined'));
|
||||
const isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined'));
|
||||
|
||||
const store = useStoreApi();
|
||||
const nodeRef = useRef<HTMLDivElement>(null);
|
||||
const prevSourcePosition = useRef(node.sourcePosition);
|
||||
const prevTargetPosition = useRef(node.targetPosition);
|
||||
const prevType = useRef(nodeType);
|
||||
|
||||
const updatePositions = useUpdateNodePositions();
|
||||
|
||||
useEffect(() => {
|
||||
if (nodeRef.current && !node.hidden) {
|
||||
const currNode = nodeRef.current;
|
||||
resizeObserver?.observe(currNode);
|
||||
|
||||
return () => resizeObserver?.unobserve(currNode);
|
||||
}
|
||||
}, [node.hidden]);
|
||||
|
||||
useEffect(() => {
|
||||
// when the user programmatically changes the source or handle position, we re-initialize the node
|
||||
const typeChanged = prevType.current !== nodeType;
|
||||
const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition;
|
||||
const targetPosChanged = prevTargetPosition.current !== node.targetPosition;
|
||||
|
||||
if (nodeRef.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
|
||||
if (typeChanged) {
|
||||
prevType.current = nodeType;
|
||||
}
|
||||
if (sourcePosChanged) {
|
||||
prevSourcePosition.current = node.sourcePosition;
|
||||
}
|
||||
if (targetPosChanged) {
|
||||
prevTargetPosition.current = node.targetPosition;
|
||||
}
|
||||
store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]]));
|
||||
}
|
||||
}, [id, nodeType, node.sourcePosition, node.targetPosition]);
|
||||
|
||||
const dragging = useDrag({
|
||||
nodeRef,
|
||||
disabled: node.hidden || !isDraggable,
|
||||
noDragClassName,
|
||||
handleSelector: node.dragHandle,
|
||||
nodeId: id,
|
||||
isSelectable,
|
||||
});
|
||||
|
||||
if (node.hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const width = node.width ?? undefined;
|
||||
const height = node.height ?? undefined;
|
||||
const computedWidth = node.computed?.width;
|
||||
const computedHeight = node.computed?.height;
|
||||
const positionAbsolute = nodeExtent
|
||||
? clampPosition(node.computed?.positionAbsolute, nodeExtent)
|
||||
: node.computed?.positionAbsolute || { x: 0, y: 0 };
|
||||
const positionAbsoluteOrigin = getPositionWithOrigin({
|
||||
x: positionAbsolute.x,
|
||||
y: positionAbsolute.y,
|
||||
width: computedWidth ?? width ?? 0,
|
||||
height: computedHeight ?? height ?? 0,
|
||||
origin: node.origin || nodeOrigin,
|
||||
});
|
||||
const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height);
|
||||
const zIndex = node[internalsSymbol]?.z ?? 0;
|
||||
const isParent = !!node[internalsSymbol]?.isParent;
|
||||
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
|
||||
|
||||
const onMouseEnterHandler =
|
||||
onMouseEnter === undefined ? undefined : (event: MouseEvent) => onMouseEnter(event, { ...node });
|
||||
const onMouseMoveHandler =
|
||||
onMouseMove === undefined ? undefined : (event: MouseEvent) => onMouseMove(event, { ...node });
|
||||
const onMouseLeaveHandler =
|
||||
onMouseLeave === undefined ? undefined : (event: MouseEvent) => onMouseLeave(event, { ...node });
|
||||
const onContextMenuHandler =
|
||||
onContextMenu === undefined ? undefined : (event: MouseEvent) => onContextMenu(event, { ...node });
|
||||
const onDoubleClickHandler =
|
||||
onDoubleClick === undefined ? undefined : (event: MouseEvent) => onDoubleClick(event, { ...node });
|
||||
|
||||
const onSelectNodeHandler = (event: MouseEvent) => {
|
||||
const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
|
||||
|
||||
if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) {
|
||||
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
|
||||
// here we only need to call it when selectNodesOnDrag=false
|
||||
handleNodeClick({
|
||||
id,
|
||||
store,
|
||||
nodeRef,
|
||||
});
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
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 &&
|
||||
node.selected &&
|
||||
Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)
|
||||
) {
|
||||
store.setState({
|
||||
ariaLiveMessage: `Moved selected node ${event.key
|
||||
.replace('Arrow', '')
|
||||
.toLowerCase()}. New position, x: ${~~positionAbsolute.x}, y: ${~~positionAbsolute.y}`,
|
||||
});
|
||||
|
||||
updatePositions({
|
||||
x: arrowKeyDiffs[event.key].x,
|
||||
y: arrowKeyDiffs[event.key].y,
|
||||
isShiftPressed: event.shiftKey,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cc([
|
||||
'react-flow__node',
|
||||
`react-flow__node-${nodeType}`,
|
||||
{
|
||||
// this is overwritable by passing `nopan` as a class name
|
||||
[noPanClassName]: isDraggable,
|
||||
},
|
||||
node.className,
|
||||
{
|
||||
selected: node.selected,
|
||||
selectable: isSelectable,
|
||||
parent: isParent,
|
||||
dragging,
|
||||
},
|
||||
])}
|
||||
ref={nodeRef}
|
||||
style={{
|
||||
zIndex,
|
||||
transform: `translate(${positionAbsoluteOrigin.x}px,${positionAbsoluteOrigin.y}px)`,
|
||||
pointerEvents: hasPointerEvents ? 'all' : 'none',
|
||||
visibility: initialized ? 'visible' : 'hidden',
|
||||
width,
|
||||
height,
|
||||
...node.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={node.ariaLabel}
|
||||
>
|
||||
<Provider value={id}>
|
||||
<NodeComponent
|
||||
id={id}
|
||||
data={node.data}
|
||||
type={nodeType}
|
||||
width={computedWidth}
|
||||
height={computedHeight}
|
||||
positionAbsoluteX={positionAbsolute.x}
|
||||
positionAbsoluteY={positionAbsolute.y}
|
||||
selected={node.selected}
|
||||
isConnectable={isConnectable}
|
||||
sourcePosition={node.sourcePosition}
|
||||
targetPosition={node.targetPosition}
|
||||
dragging={dragging}
|
||||
dragHandle={node.dragHandle}
|
||||
zIndex={zIndex}
|
||||
/>
|
||||
</Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NodeWrapper.displayName = 'NodeWrapper';
|
||||
|
||||
export default memo(NodeWrapper);
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import type { NodeProps, XYPosition } from '@xyflow/system';
|
||||
|
||||
import InputNode from '../Nodes/InputNode';
|
||||
import DefaultNode from '../Nodes/DefaultNode';
|
||||
import GroupNode from '../Nodes/GroupNode';
|
||||
import OutputNode from '../Nodes/OutputNode';
|
||||
import type { NodeTypes } 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 const builtinNodeTypes: NodeTypes = {
|
||||
input: InputNode as ComponentType<NodeProps>,
|
||||
default: DefaultNode as ComponentType<NodeProps>,
|
||||
output: OutputNode as ComponentType<NodeProps>,
|
||||
group: GroupNode as ComponentType<NodeProps>,
|
||||
};
|
||||
@@ -1,22 +1,9 @@
|
||||
import type { MouseEvent, RefObject } from 'react';
|
||||
import type { RefObject } from 'react';
|
||||
import type { StoreApi } from 'zustand';
|
||||
|
||||
import type { Node, ReactFlowState } from '../../types';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
import { errorMessages } from '@xyflow/system';
|
||||
|
||||
export function getMouseHandler(
|
||||
id: string,
|
||||
getState: StoreApi<ReactFlowState>['getState'],
|
||||
handler?: (event: MouseEvent, node: Node) => void
|
||||
) {
|
||||
return handler === undefined
|
||||
? handler
|
||||
: (event: MouseEvent) => {
|
||||
const node = getState().nodeLookup.get(id)!;
|
||||
handler(event, { ...node });
|
||||
};
|
||||
}
|
||||
|
||||
// this handler is called by
|
||||
// 1. the click handler when node is not draggable or selectNodesOnDrag = false
|
||||
// or
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
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,
|
||||
sizeWidth,
|
||||
sizeHeight,
|
||||
}: 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, nodeDragThreshold } = store.getState();
|
||||
|
||||
if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) {
|
||||
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
|
||||
// here we only need to call it when selectNodesOnDrag=false
|
||||
handleNodeClick({
|
||||
id,
|
||||
store,
|
||||
nodeRef,
|
||||
});
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
const node = store.getState().nodes.find((n) => n.id === 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(new Map([[id, { 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',
|
||||
width: sizeWidth,
|
||||
height: sizeHeight,
|
||||
...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);
|
||||
};
|
||||
@@ -10,9 +10,9 @@ import { getNodesBounds } 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';
|
||||
import { arrowKeyDiffs } from '../NodeWrapper/utils';
|
||||
|
||||
export type NodesSelectionProps = {
|
||||
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
|
||||
|
||||
@@ -3,67 +3,72 @@
|
||||
* We distinguish between values we can update directly with `useDirectStoreUpdater` (like `snapGrid`)
|
||||
* and values that have a dedicated setter function in the store (like `setNodes`).
|
||||
*/
|
||||
import { useEffect } from 'react';
|
||||
import { StoreApi } from 'zustand';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { devWarn, type CoordinateExtent } from '@xyflow/system';
|
||||
import { type CoordinateExtent } from '@xyflow/system';
|
||||
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import type { Node, Edge, ReactFlowState, ReactFlowProps, ReactFlowStore } from '../../types';
|
||||
import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } 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'
|
||||
| 'onDelete'
|
||||
| 'onNodeDragStart'
|
||||
| 'onNodeDrag'
|
||||
| 'onNodeDragStop'
|
||||
| 'onSelectionDragStart'
|
||||
| 'onSelectionDrag'
|
||||
| 'onSelectionDragStop'
|
||||
| 'onMove'
|
||||
| 'onMoveStart'
|
||||
| 'onMoveEnd'
|
||||
| 'noPanClassName'
|
||||
| 'nodeOrigin'
|
||||
| 'elevateNodesOnSelect'
|
||||
| 'autoPanOnConnect'
|
||||
| 'autoPanOnNodeDrag'
|
||||
| 'onError'
|
||||
| 'connectionRadius'
|
||||
| 'isValidConnection'
|
||||
| 'selectNodesOnDrag'
|
||||
| 'nodeDragThreshold'
|
||||
> & { rfId: string };
|
||||
// these fields exist in the global store and we need to keep them up to date
|
||||
const reactFlowFieldsToTrack = [
|
||||
'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',
|
||||
'onDelete',
|
||||
'onNodeDrag',
|
||||
'onNodeDragStart',
|
||||
'onNodeDragStop',
|
||||
'onSelectionDrag',
|
||||
'onSelectionDragStart',
|
||||
'onSelectionDragStop',
|
||||
'onMoveStart',
|
||||
'onMove',
|
||||
'onMoveEnd',
|
||||
'noPanClassName',
|
||||
'nodeOrigin',
|
||||
'autoPanOnConnect',
|
||||
'autoPanOnNodeDrag',
|
||||
'onError',
|
||||
'connectionRadius',
|
||||
'isValidConnection',
|
||||
'selectNodesOnDrag',
|
||||
'nodeDragThreshold',
|
||||
] as const;
|
||||
|
||||
type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number];
|
||||
type StoreUpdaterProps = Pick<ReactFlowProps, ReactFlowFieldsToTrack> & { rfId: string };
|
||||
|
||||
// rfId doesn't exist in ReactFlowProps, but it's one of the fields we want to update
|
||||
const fieldsToTrack = [...reactFlowFieldsToTrack, 'rfId'] as const;
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
setNodes: s.setNodes,
|
||||
@@ -76,80 +81,7 @@ const selector = (s: ReactFlowState) => ({
|
||||
reset: s.reset,
|
||||
});
|
||||
|
||||
function useStoreUpdater<T>(value: T | undefined, setStoreAction: (param: T) => void) {
|
||||
useEffect(() => {
|
||||
if (typeof value !== 'undefined') {
|
||||
setStoreAction(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,
|
||||
onDelete,
|
||||
onNodeDrag,
|
||||
onNodeDragStart,
|
||||
onNodeDragStop,
|
||||
onSelectionDrag,
|
||||
onSelectionDragStart,
|
||||
onSelectionDragStop,
|
||||
onMoveStart,
|
||||
onMove,
|
||||
onMoveEnd,
|
||||
noPanClassName,
|
||||
nodeOrigin,
|
||||
rfId,
|
||||
autoPanOnConnect,
|
||||
autoPanOnNodeDrag,
|
||||
onError,
|
||||
connectionRadius,
|
||||
isValidConnection,
|
||||
selectNodesOnDrag,
|
||||
nodeDragThreshold,
|
||||
}: StoreUpdaterProps) => {
|
||||
const StoreUpdater = (props: StoreUpdaterProps) => {
|
||||
const {
|
||||
setNodes,
|
||||
setEdges,
|
||||
@@ -163,64 +95,44 @@ const StoreUpdater = ({
|
||||
const store = useStoreApi();
|
||||
|
||||
useEffect(() => {
|
||||
const edgesWithDefaults = defaultEdges?.map((e) => ({ ...e, ...defaultEdgeOptions }));
|
||||
setDefaultNodesAndEdges(defaultNodes, edgesWithDefaults);
|
||||
const edgesWithDefaults = props.defaultEdges?.map((e) => ({ ...e, ...props.defaultEdgeOptions }));
|
||||
setDefaultNodesAndEdges(props.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('onDelete', onDelete, 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 || devWarn, store.setState);
|
||||
useDirectStoreUpdater('connectionRadius', connectionRadius, store.setState);
|
||||
useDirectStoreUpdater('isValidConnection', isValidConnection, store.setState);
|
||||
useDirectStoreUpdater('selectNodesOnDrag', selectNodesOnDrag, store.setState);
|
||||
useDirectStoreUpdater('nodeDragThreshold', nodeDragThreshold, store.setState);
|
||||
const previousFields = useRef<Partial<StoreUpdaterProps>>({});
|
||||
|
||||
useStoreUpdater<Node[]>(nodes, setNodes);
|
||||
useStoreUpdater<Edge[]>(edges, setEdges);
|
||||
useStoreUpdater<number>(minZoom, setMinZoom);
|
||||
useStoreUpdater<number>(maxZoom, setMaxZoom);
|
||||
useStoreUpdater<CoordinateExtent>(translateExtent, setTranslateExtent);
|
||||
useStoreUpdater<CoordinateExtent>(nodeExtent, setNodeExtent);
|
||||
useEffect(
|
||||
() => {
|
||||
for (const fieldName of fieldsToTrack) {
|
||||
const fieldValue = props[fieldName];
|
||||
const previousFieldValue = previousFields.current[fieldName];
|
||||
|
||||
if (fieldValue === previousFieldValue) continue;
|
||||
if (typeof props[fieldName] === 'undefined') continue;
|
||||
|
||||
// Custom handling with dedicated setters for some fields
|
||||
if (fieldName === 'nodes') setNodes(fieldValue as Node[]);
|
||||
else if (fieldName === 'edges') setEdges(fieldValue as Edge[]);
|
||||
else if (fieldName === 'minZoom') setMinZoom(fieldValue as number);
|
||||
else if (fieldName === 'maxZoom') setMaxZoom(fieldValue as number);
|
||||
else if (fieldName === 'translateExtent') setTranslateExtent(fieldValue as CoordinateExtent);
|
||||
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
|
||||
// Renamed fields
|
||||
else if (fieldName === 'fitView') store.setState({ fitViewOnInit: fieldValue as boolean });
|
||||
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOnInitOptions: fieldValue as FitViewOptions });
|
||||
// General case
|
||||
else store.setState({ [fieldName]: fieldValue });
|
||||
}
|
||||
|
||||
previousFields.current = props;
|
||||
},
|
||||
// Only re-run the effect if one of the fields we track changes
|
||||
fieldsToTrack.map((fieldName) => props[fieldName])
|
||||
);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { memo, ReactNode } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import cc from 'classcat';
|
||||
import { errorMessages } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import useVisibleEdges from '../../hooks/useVisibleEdges';
|
||||
import MarkerDefinitions from './MarkerDefinitions';
|
||||
import { GraphViewProps } from '../GraphView';
|
||||
import type { EdgeTypesWrapped, ReactFlowState } from '../../types';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
import EdgeWrapper from '../../components/EdgeWrapper';
|
||||
|
||||
type EdgeRendererProps = Pick<
|
||||
GraphViewProps,
|
||||
@@ -27,8 +27,8 @@ type EdgeRendererProps = Pick<
|
||||
| 'elevateEdgesOnSelect'
|
||||
| 'rfId'
|
||||
| 'disableKeyboardA11y'
|
||||
| 'edgeTypes'
|
||||
> & {
|
||||
edgeTypes: EdgeTypesWrapped;
|
||||
elevateEdgesOnSelect: boolean;
|
||||
children: ReactNode;
|
||||
};
|
||||
@@ -72,14 +72,6 @@ const EdgeRenderer = ({
|
||||
</svg>
|
||||
|
||||
{edges.map((edge) => {
|
||||
let edgeType = edge.type || 'default';
|
||||
|
||||
if (!edgeTypes[edgeType]) {
|
||||
onError?.('011', errorMessages['error011'](edgeType));
|
||||
edgeType = 'default';
|
||||
}
|
||||
|
||||
const EdgeComponent = edgeTypes[edgeType];
|
||||
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
|
||||
const isUpdatable =
|
||||
typeof onEdgeUpdate !== 'undefined' &&
|
||||
@@ -87,15 +79,16 @@ const EdgeRenderer = ({
|
||||
const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined'));
|
||||
|
||||
return (
|
||||
<EdgeComponent
|
||||
<EdgeWrapper
|
||||
key={edge.id}
|
||||
id={edge.id}
|
||||
className={cc([edge.className, noPanClassName])}
|
||||
type={edgeType}
|
||||
type={edge.type}
|
||||
data={edge.data}
|
||||
selected={!!edge.selected}
|
||||
animated={!!edge.animated}
|
||||
hidden={!!edge.hidden}
|
||||
zIndex={edge.zIndex}
|
||||
label={edge.label}
|
||||
labelStyle={edge.labelStyle}
|
||||
labelShowBg={edge.labelShowBg}
|
||||
@@ -103,7 +96,6 @@ const EdgeRenderer = ({
|
||||
labelBgPadding={edge.labelBgPadding}
|
||||
labelBgBorderRadius={edge.labelBgBorderRadius}
|
||||
style={edge.style}
|
||||
zIndex={edge.zIndex}
|
||||
source={edge.source}
|
||||
target={edge.target}
|
||||
sourceHandleId={edge.sourceHandle}
|
||||
@@ -127,10 +119,11 @@ const EdgeRenderer = ({
|
||||
isUpdatable={isUpdatable}
|
||||
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
|
||||
interactionWidth={edge.interactionWidth}
|
||||
onError={onError}
|
||||
edgeTypes={edgeTypes}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { ComponentType } from 'react';
|
||||
|
||||
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge, SimpleBezierEdge } from '../../components/Edges';
|
||||
import wrapEdge from '../../components/Edges/wrapEdge';
|
||||
import type { EdgeProps, EdgeTypes, EdgeTypesWrapped } from '../../types';
|
||||
|
||||
export type CreateEdgeTypes = (edgeTypes: EdgeTypes) => EdgeTypesWrapped;
|
||||
|
||||
export function createEdgeTypes(edgeTypes: EdgeTypes): EdgeTypesWrapped {
|
||||
const standardTypes: EdgeTypesWrapped = {
|
||||
default: wrapEdge((edgeTypes.default || BezierEdge) as ComponentType<EdgeProps>),
|
||||
straight: wrapEdge((edgeTypes.bezier || StraightEdge) as ComponentType<EdgeProps>),
|
||||
step: wrapEdge((edgeTypes.step || StepEdge) as ComponentType<EdgeProps>),
|
||||
smoothstep: wrapEdge((edgeTypes.step || SmoothStepEdge) as ComponentType<EdgeProps>),
|
||||
simplebezier: wrapEdge((edgeTypes.simplebezier || SimpleBezierEdge) as ComponentType<EdgeProps>),
|
||||
};
|
||||
|
||||
const wrappedTypes = {} as EdgeTypesWrapped;
|
||||
const specialTypes: EdgeTypesWrapped = Object.keys(edgeTypes)
|
||||
.filter((k) => !['default', 'bezier'].includes(k))
|
||||
.reduce((res, key) => {
|
||||
res[key] = wrapEdge((edgeTypes[key] || BezierEdge) as ComponentType<EdgeProps>);
|
||||
|
||||
return res;
|
||||
}, wrappedTypes);
|
||||
|
||||
return {
|
||||
...standardTypes,
|
||||
...specialTypes,
|
||||
};
|
||||
}
|
||||
@@ -8,19 +8,15 @@ import useOnInitHandler from '../../hooks/useOnInitHandler';
|
||||
import useViewportSync from '../../hooks/useViewportSync';
|
||||
import ConnectionLine from '../../components/ConnectionLine';
|
||||
import type { ReactFlowProps } from '../../types';
|
||||
import { createNodeTypes } from '../NodeRenderer/utils';
|
||||
import { createEdgeTypes } from '../EdgeRenderer/utils';
|
||||
import { useNodeOrEdgeTypes } from './utils';
|
||||
import useNodeOrEdgeTypesWarning from './useNodeOrEdgeTypesWarning';
|
||||
|
||||
export type GraphViewProps = Omit<
|
||||
ReactFlowProps,
|
||||
'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes' | 'onMove' | 'onMoveStart' | 'onMoveEnd'
|
||||
'onSelectionChange' | 'nodes' | 'edges' | 'onMove' | 'onMoveStart' | 'onMoveEnd'
|
||||
> &
|
||||
Required<
|
||||
Pick<
|
||||
ReactFlowProps,
|
||||
| 'nodeTypes'
|
||||
| 'edgeTypes'
|
||||
| 'selectionKeyCode'
|
||||
| 'deleteKeyCode'
|
||||
| 'multiSelectionKeyCode'
|
||||
@@ -108,8 +104,8 @@ const GraphView = ({
|
||||
viewport,
|
||||
onViewportChange,
|
||||
}: GraphViewProps) => {
|
||||
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes);
|
||||
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes);
|
||||
useNodeOrEdgeTypesWarning(nodeTypes);
|
||||
useNodeOrEdgeTypesWarning(edgeTypes);
|
||||
|
||||
useOnInitHandler(onInit);
|
||||
useViewportSync(viewport);
|
||||
@@ -154,7 +150,7 @@ const GraphView = ({
|
||||
>
|
||||
<ViewportWrapper>
|
||||
<EdgeRenderer
|
||||
edgeTypes={edgeTypesWrapped}
|
||||
edgeTypes={edgeTypes}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onEdgeDoubleClick={onEdgeDoubleClick}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
@@ -182,7 +178,7 @@ const GraphView = ({
|
||||
<div className="react-flow__edgelabel-renderer" />
|
||||
|
||||
<NodeRenderer
|
||||
nodeTypes={nodeTypesWrapped}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onNodeMouseEnter={onNodeMouseEnter}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { errorMessages } from '@xyflow/system';
|
||||
|
||||
import type { EdgeTypes, NodeTypes } from '../../types';
|
||||
import { useStoreApi } from '../../hooks/useStore';
|
||||
|
||||
const emptyTypes = {};
|
||||
|
||||
/*
|
||||
* This hook warns the user if node or edgeTypes change.
|
||||
*/
|
||||
export function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes?: NodeTypes): void;
|
||||
export function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes?: EdgeTypes): void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes: any = emptyTypes): any {
|
||||
const updateCount = useRef(0);
|
||||
const store = useStoreApi();
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (updateCount.current > 1) {
|
||||
store.getState().onError?.('002', errorMessages['error002']());
|
||||
}
|
||||
updateCount.current += 1;
|
||||
}
|
||||
}, [nodeOrEdgeTypes]);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { errorMessages } from '@xyflow/system';
|
||||
|
||||
import { CreateEdgeTypes } from '../EdgeRenderer/utils';
|
||||
import { CreateNodeTypes } from '../NodeRenderer/utils';
|
||||
import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
|
||||
import { useStoreApi } from '../../hooks/useStore';
|
||||
|
||||
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped;
|
||||
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any {
|
||||
const typesKeysRef = useRef<string[] | null>(null);
|
||||
const store = useStoreApi();
|
||||
|
||||
const typesParsed = useMemo(() => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const typeKeys = Object.keys(nodeOrEdgeTypes);
|
||||
|
||||
if (shallow(typesKeysRef.current, typeKeys)) {
|
||||
store.getState().onError?.('002', errorMessages['error002']());
|
||||
}
|
||||
|
||||
typesKeysRef.current = typeKeys;
|
||||
}
|
||||
return createTypes(nodeOrEdgeTypes);
|
||||
}, [nodeOrEdgeTypes]);
|
||||
|
||||
return typesParsed;
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import { memo, useMemo, useEffect, useRef, type ComponentType } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { internalsSymbol, errorMessages, Position, clampPosition, getPositionWithOrigin } from '@xyflow/system';
|
||||
|
||||
import useVisibleNodes from '../../hooks/useVisibleNodes';
|
||||
import useVisibleNodesIds from '../../hooks/useVisibleNodes';
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { containerStyle } from '../../styles/utils';
|
||||
import { GraphViewProps } from '../GraphView';
|
||||
import type { NodeTypesWrapped, ReactFlowState, WrapNodeProps } from '../../types';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
import useResizeObserver from './useResizeObserver';
|
||||
import NodeWrapper from '../../components/NodeWrapper';
|
||||
|
||||
type NodeRendererProps = Pick<
|
||||
export type NodeRendererProps = Pick<
|
||||
GraphViewProps,
|
||||
| 'onNodeClick'
|
||||
| 'onNodeDoubleClick'
|
||||
@@ -23,126 +24,71 @@ type NodeRendererProps = Pick<
|
||||
| 'disableKeyboardA11y'
|
||||
| 'nodeOrigin'
|
||||
| 'nodeExtent'
|
||||
> & {
|
||||
nodeTypes: NodeTypesWrapped;
|
||||
};
|
||||
| 'nodeTypes'
|
||||
>;
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
nodesDraggable: s.nodesDraggable,
|
||||
nodesConnectable: s.nodesConnectable,
|
||||
nodesFocusable: s.nodesFocusable,
|
||||
elementsSelectable: s.elementsSelectable,
|
||||
updateNodeDimensions: s.updateNodeDimensions,
|
||||
onError: s.onError,
|
||||
});
|
||||
|
||||
const NodeRenderer = (props: NodeRendererProps) => {
|
||||
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions, onError } =
|
||||
useStore(selector, shallow);
|
||||
const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
|
||||
const resizeObserverRef = useRef<ResizeObserver>();
|
||||
|
||||
const resizeObserver = useMemo(() => {
|
||||
if (typeof ResizeObserver === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
const updates = new Map();
|
||||
|
||||
entries.forEach((entry: ResizeObserverEntry) => {
|
||||
const id = entry.target.getAttribute('data-id') as string;
|
||||
updates.set(id, {
|
||||
id,
|
||||
nodeElement: entry.target as HTMLDivElement,
|
||||
forceUpdate: true,
|
||||
});
|
||||
});
|
||||
|
||||
updateNodeDimensions(updates);
|
||||
});
|
||||
|
||||
resizeObserverRef.current = observer;
|
||||
|
||||
return observer;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
resizeObserverRef?.current?.disconnect();
|
||||
};
|
||||
}, []);
|
||||
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, onError } = useStore(selector, shallow);
|
||||
const nodeIds = useVisibleNodesIds(props.onlyRenderVisibleElements);
|
||||
const resizeObserver = useResizeObserver();
|
||||
|
||||
return (
|
||||
<div className="react-flow__nodes" style={containerStyle}>
|
||||
{nodes.map((node) => {
|
||||
let nodeType = node.type || 'default';
|
||||
|
||||
if (!props.nodeTypes[nodeType]) {
|
||||
onError?.('003', errorMessages['error003'](nodeType));
|
||||
|
||||
nodeType = 'default';
|
||||
}
|
||||
|
||||
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
|
||||
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
|
||||
const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined'));
|
||||
const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined'));
|
||||
const isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined'));
|
||||
|
||||
const clampedPosition = props.nodeExtent
|
||||
? clampPosition(node.positionAbsolute, props.nodeExtent)
|
||||
: node.positionAbsolute;
|
||||
|
||||
const posX = clampedPosition?.x ?? 0;
|
||||
const posY = clampedPosition?.y ?? 0;
|
||||
const posOrigin = getPositionWithOrigin({
|
||||
x: posX,
|
||||
y: posY,
|
||||
width: node.width ?? 0,
|
||||
height: node.height ?? 0,
|
||||
origin: node.origin || props.nodeOrigin,
|
||||
});
|
||||
const initialized = (!!node.width && !!node.height) || (!!node.size?.width && !!node.size?.height);
|
||||
|
||||
{nodeIds.map((nodeId) => {
|
||||
return (
|
||||
<NodeComponent
|
||||
key={node.id}
|
||||
id={node.id}
|
||||
className={node.className}
|
||||
style={node.style}
|
||||
sizeWidth={node.size?.width}
|
||||
sizeHeight={node.size?.height}
|
||||
type={nodeType}
|
||||
data={node.data}
|
||||
sourcePosition={node.sourcePosition || Position.Bottom}
|
||||
targetPosition={node.targetPosition || Position.Top}
|
||||
hidden={node.hidden}
|
||||
xPos={posX}
|
||||
yPos={posY}
|
||||
xPosOrigin={posOrigin.x}
|
||||
yPosOrigin={posOrigin.y}
|
||||
// The split of responsibilities between NodeRenderer and
|
||||
// NodeComponentWrapper may appear weird. However, it’s designed to
|
||||
// minimize the cost of updates when individual nodes change.
|
||||
//
|
||||
// For example, when you’re dragging a single node, that node gets
|
||||
// updated multiple times per second. If `NodeRenderer` were to update
|
||||
// every time, it would have to re-run the `nodes.map()` loop every
|
||||
// time. This gets pricey with hundreds of nodes, especially if every
|
||||
// loop cycle does more than just rendering a JSX element!
|
||||
//
|
||||
// As a result of this choice, we took the following implementation
|
||||
// decisions:
|
||||
// - NodeRenderer subscribes *only* to node IDs – and therefore
|
||||
// rerender *only* when visible nodes are added or removed.
|
||||
// - NodeRenderer performs all operations the result of which can be
|
||||
// shared between nodes (such as creating the `ResizeObserver`
|
||||
// instance, or subscribing to `selector`). This means extra prop
|
||||
// drilling into `NodeComponentWrapper`, but it means we need to run
|
||||
// these operations only once – instead of once per node.
|
||||
// - Any operations that you’d normally write inside `nodes.map` are
|
||||
// moved into `NodeComponentWrapper`. This ensures they are
|
||||
// memorized – so if `NodeRenderer` *has* to rerender, it only
|
||||
// needs to regenerate the list of nodes, nothing else.
|
||||
<NodeWrapper
|
||||
key={nodeId}
|
||||
id={nodeId}
|
||||
nodeTypes={props.nodeTypes}
|
||||
nodeExtent={props.nodeExtent}
|
||||
nodeOrigin={props.nodeOrigin}
|
||||
onClick={props.onNodeClick}
|
||||
onMouseEnter={props.onNodeMouseEnter}
|
||||
onMouseMove={props.onNodeMouseMove}
|
||||
onMouseLeave={props.onNodeMouseLeave}
|
||||
onContextMenu={props.onNodeContextMenu}
|
||||
onDoubleClick={props.onNodeDoubleClick}
|
||||
selected={!!node.selected}
|
||||
isDraggable={isDraggable}
|
||||
isSelectable={isSelectable}
|
||||
isConnectable={isConnectable}
|
||||
isFocusable={isFocusable}
|
||||
resizeObserver={resizeObserver}
|
||||
dragHandle={node.dragHandle}
|
||||
zIndex={node[internalsSymbol]?.z ?? 0}
|
||||
isParent={!!node[internalsSymbol]?.isParent}
|
||||
noDragClassName={props.noDragClassName}
|
||||
noPanClassName={props.noPanClassName}
|
||||
initialized={initialized}
|
||||
rfId={props.rfId}
|
||||
disableKeyboardA11y={props.disableKeyboardA11y}
|
||||
ariaLabel={node.ariaLabel}
|
||||
resizeObserver={resizeObserver}
|
||||
nodesDraggable={nodesDraggable}
|
||||
nodesConnectable={nodesConnectable}
|
||||
nodesFocusable={nodesFocusable}
|
||||
elementsSelectable={elementsSelectable}
|
||||
onError={onError}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { ReactFlowState } from '../../types';
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
|
||||
const selector = (s: ReactFlowState) => s.updateNodeDimensions;
|
||||
|
||||
export default function useResizeObserver() {
|
||||
const updateNodeDimensions = useStore(selector);
|
||||
const resizeObserverRef = useRef<ResizeObserver>();
|
||||
|
||||
const resizeObserver = useMemo(() => {
|
||||
if (typeof ResizeObserver === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
const updates = new Map();
|
||||
|
||||
entries.forEach((entry: ResizeObserverEntry) => {
|
||||
const id = entry.target.getAttribute('data-id') as string;
|
||||
updates.set(id, {
|
||||
id,
|
||||
nodeElement: entry.target as HTMLDivElement,
|
||||
forceUpdate: true,
|
||||
});
|
||||
});
|
||||
|
||||
updateNodeDimensions(updates);
|
||||
});
|
||||
|
||||
resizeObserverRef.current = observer;
|
||||
|
||||
return observer;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
resizeObserverRef?.current?.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return resizeObserver;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import type { NodeProps } from '@xyflow/system';
|
||||
|
||||
import DefaultNode from '../../components/Nodes/DefaultNode';
|
||||
import InputNode from '../../components/Nodes/InputNode';
|
||||
import OutputNode from '../../components/Nodes/OutputNode';
|
||||
import GroupNode from '../../components/Nodes/GroupNode';
|
||||
import wrapNode from '../../components/Nodes/wrapNode';
|
||||
import type { NodeTypes, NodeTypesWrapped } from '../../types';
|
||||
|
||||
export type CreateNodeTypes = (nodeTypes: NodeTypes) => NodeTypesWrapped;
|
||||
|
||||
export function createNodeTypes(nodeTypes: NodeTypes): NodeTypesWrapped {
|
||||
const standardTypes: NodeTypesWrapped = {
|
||||
input: wrapNode((nodeTypes.input || InputNode) as ComponentType<NodeProps>),
|
||||
default: wrapNode((nodeTypes.default || DefaultNode) as ComponentType<NodeProps>),
|
||||
output: wrapNode((nodeTypes.output || OutputNode) as ComponentType<NodeProps>),
|
||||
group: wrapNode((nodeTypes.group || GroupNode) as ComponentType<NodeProps>),
|
||||
};
|
||||
|
||||
const wrappedTypes = {} as NodeTypesWrapped;
|
||||
const specialTypes: NodeTypesWrapped = Object.keys(nodeTypes)
|
||||
.filter((k) => !['input', 'default', 'output', 'group'].includes(k))
|
||||
.reduce((res, key) => {
|
||||
res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType<NodeProps>);
|
||||
|
||||
return res;
|
||||
}, wrappedTypes);
|
||||
|
||||
return {
|
||||
...standardTypes,
|
||||
...specialTypes,
|
||||
};
|
||||
}
|
||||
@@ -12,32 +12,14 @@ import {
|
||||
} from '@xyflow/system';
|
||||
|
||||
import Attribution from '../../components/Attribution';
|
||||
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge, SimpleBezierEdge } from '../../components/Edges';
|
||||
import DefaultNode from '../../components/Nodes/DefaultNode';
|
||||
import InputNode from '../../components/Nodes/InputNode';
|
||||
import OutputNode from '../../components/Nodes/OutputNode';
|
||||
import GroupNode from '../../components/Nodes/GroupNode';
|
||||
|
||||
import SelectionListener from '../../components/SelectionListener';
|
||||
import StoreUpdater from '../../components/StoreUpdater';
|
||||
import A11yDescriptions from '../../components/A11yDescriptions';
|
||||
import GraphView from '../GraphView';
|
||||
import Wrapper from './Wrapper';
|
||||
import type { EdgeTypes, NodeTypes, ReactFlowProps, ReactFlowRefType } from '../../types';
|
||||
|
||||
const defaultNodeTypes: NodeTypes = {
|
||||
input: InputNode,
|
||||
default: DefaultNode,
|
||||
output: OutputNode,
|
||||
group: GroupNode,
|
||||
};
|
||||
|
||||
const defaultEdgeTypes: EdgeTypes = {
|
||||
default: BezierEdge,
|
||||
straight: StraightEdge,
|
||||
step: StepEdge,
|
||||
smoothstep: SmoothStepEdge,
|
||||
simplebezier: SimpleBezierEdge,
|
||||
};
|
||||
import type { ReactFlowProps, ReactFlowRefType } from '../../types';
|
||||
import useColorModeClass from '../../hooks/useColorModeClass';
|
||||
|
||||
const initNodeOrigin: NodeOrigin = [0, 0];
|
||||
const initSnapGrid: [number, number] = [15, 15];
|
||||
@@ -59,8 +41,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
||||
defaultNodes,
|
||||
defaultEdges,
|
||||
className,
|
||||
nodeTypes = defaultNodeTypes,
|
||||
edgeTypes = defaultEdgeTypes,
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
onNodeClick,
|
||||
onEdgeClick,
|
||||
onInit,
|
||||
@@ -169,18 +151,20 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
||||
onViewportChange,
|
||||
width,
|
||||
height,
|
||||
colorMode = 'light',
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const rfId = id || '1';
|
||||
const colorModeClassName = useColorModeClass(colorMode);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
style={{ ...style, ...wrapperStyle }}
|
||||
ref={ref}
|
||||
className={cc(['react-flow', className])}
|
||||
className={cc(['react-flow', className, colorModeClassName])}
|
||||
data-testid="rf__wrapper"
|
||||
id={id}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { ColorMode, ColorModeClass } from '@xyflow/system';
|
||||
|
||||
function getMediaQuery() {
|
||||
if (typeof window === 'undefined' || !window.matchMedia) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.matchMedia('(prefers-color-scheme: dark)');
|
||||
}
|
||||
|
||||
export default function useColorModeClass(colorMode: ColorMode): ColorModeClass {
|
||||
const [colorModeClass, setColorModeClass] = useState<ColorModeClass | null>(
|
||||
colorMode === 'system' ? null : colorMode
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (colorMode !== 'system') {
|
||||
setColorModeClass(colorMode);
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaQuery = getMediaQuery();
|
||||
const updateColorModeClass = () => setColorModeClass(mediaQuery?.matches ? 'dark' : 'light');
|
||||
|
||||
updateColorModeClass();
|
||||
mediaQuery?.addEventListener('change', updateColorModeClass);
|
||||
|
||||
return () => {
|
||||
mediaQuery?.removeEventListener('change', updateColorModeClass);
|
||||
};
|
||||
}, [colorMode]);
|
||||
|
||||
return colorModeClass !== null ? colorModeClass : getMediaQuery()?.matches ? 'dark' : 'light';
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { Connection, HandleType, areConnectionMapsEqual, handleConnectionChange } from '@xyflow/system';
|
||||
|
||||
import { useStore } from './useStore';
|
||||
import { useNodeId } from '../contexts/NodeIdContext';
|
||||
|
||||
type useHandleConnectionsParams = {
|
||||
type: HandleType;
|
||||
id?: string | null;
|
||||
nodeId?: string;
|
||||
onConnect?: (connections: Connection[]) => void;
|
||||
onDisconnect?: (connections: Connection[]) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to check if a <Handle /> is connected to another <Handle /> and get the connections.
|
||||
*
|
||||
* @public
|
||||
* @param param.type - handle type 'source' or 'target'
|
||||
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
|
||||
* @param param.nodeId - node id - if not provided, the node id from the NodeIdContext is used
|
||||
* @param param.onConnect - gets called when a connection is established
|
||||
* @param param.onDisconnect - gets called when a connection is removed
|
||||
* @returns an array with connections
|
||||
*/
|
||||
export function useHandleConnections({
|
||||
type,
|
||||
id = null,
|
||||
nodeId,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
}: useHandleConnectionsParams): Connection[] {
|
||||
const _nodeId = useNodeId();
|
||||
const prevConnections = useRef<Map<string, Connection> | null>(null);
|
||||
const currentNodeId = nodeId || _nodeId;
|
||||
|
||||
const connections = useStore(
|
||||
(state) => state.connectionLookup.get(`${currentNodeId}-${type}-${id}`),
|
||||
areConnectionMapsEqual
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// @todo dicuss if onConnect/onDisconnect should be called when the component mounts/unmounts
|
||||
if (prevConnections.current && prevConnections.current !== connections) {
|
||||
const _connections = connections ?? new Map();
|
||||
handleConnectionChange(prevConnections.current, _connections, onDisconnect);
|
||||
handleConnectionChange(_connections, prevConnections.current, onConnect);
|
||||
}
|
||||
|
||||
prevConnections.current = connections ?? new Map();
|
||||
}, [connections, onConnect, onDisconnect]);
|
||||
|
||||
return useMemo(() => Array.from(connections?.values() ?? []), [connections]);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useCallback } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useStore } from '../hooks/useStore';
|
||||
import type { Node } from '../types';
|
||||
|
||||
export function useNodesData<NodeType extends Node = Node>(nodeId: string): NodeType['data'] | null;
|
||||
export function useNodesData<NodeType extends Node = Node>(nodeIds: string[]): NodeType['data'][];
|
||||
export function useNodesData<NodeType extends Node = Node>(
|
||||
nodeIds: string[],
|
||||
guard: (node: Node) => node is NodeType
|
||||
): NodeType['data'][];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function useNodesData(nodeIds: any): any {
|
||||
const nodesData = useStore(
|
||||
useCallback(
|
||||
(s) => {
|
||||
if (!Array.isArray(nodeIds)) {
|
||||
return s.nodeLookup.get(nodeIds)?.data || null;
|
||||
}
|
||||
|
||||
const data = [];
|
||||
|
||||
for (const nodeId of nodeIds) {
|
||||
const nodeData = s.nodeLookup.get(nodeId)?.data;
|
||||
|
||||
if (nodeData) {
|
||||
data.push(nodeData);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
[nodeIds]
|
||||
),
|
||||
shallow
|
||||
);
|
||||
|
||||
return nodesData;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@xyflow/system';
|
||||
|
||||
import useViewportHelper from './useViewportHelper';
|
||||
import { useStoreApi } from '../hooks/useStore';
|
||||
import { useStoreApi } from './useStore';
|
||||
import type {
|
||||
ReactFlowInstance,
|
||||
Instance,
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
Node,
|
||||
Edge,
|
||||
} from '../types';
|
||||
import { isNode } from '../utils';
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlowInstance<NodeData, EdgeData> {
|
||||
@@ -206,7 +207,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
||||
}
|
||||
|
||||
return (nodes || store.getState().nodes).filter((n) => {
|
||||
if (!isRect && (n.id === node!.id || !n.positionAbsolute)) {
|
||||
if (!isRect && (n.id === node!.id || !n.computed?.positionAbsolute)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -271,6 +272,36 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
||||
return getOutgoersBase(node, nodes, edges);
|
||||
}, []);
|
||||
|
||||
const updateNode = useCallback<Instance.UpdateNode>(
|
||||
(id, nodeUpdate, options = { replace: true }) => {
|
||||
setNodes((prevNodes) =>
|
||||
prevNodes.map((node) => {
|
||||
if (node.id === id) {
|
||||
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as Node) : nodeUpdate;
|
||||
return options.replace && isNode(nextNode) ? nextNode : { ...node, ...nextNode };
|
||||
}
|
||||
|
||||
return node;
|
||||
})
|
||||
);
|
||||
},
|
||||
[setNodes]
|
||||
);
|
||||
|
||||
const updateNodeData = useCallback<Instance.UpdateNodeData>(
|
||||
(id, dataUpdate, options = { replace: false }) => {
|
||||
updateNode(
|
||||
id,
|
||||
(node) => {
|
||||
const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate;
|
||||
return options.replace ? { ...node, data: nextData } : { ...node, data: { ...node.data, ...nextData } };
|
||||
},
|
||||
options
|
||||
);
|
||||
},
|
||||
[updateNode]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
...viewportHelper,
|
||||
@@ -289,6 +320,8 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
||||
getConnectedEdges,
|
||||
getIncomers,
|
||||
getOutgoers,
|
||||
updateNode,
|
||||
updateNodeData,
|
||||
};
|
||||
}, [
|
||||
viewportHelper,
|
||||
|
||||
@@ -23,8 +23,11 @@ function useUpdateNodePositions() {
|
||||
const yDiff = params.y * yVelo * factor;
|
||||
|
||||
const nodeUpdates = selectedNodes.map((node) => {
|
||||
if (node.positionAbsolute) {
|
||||
let nextPosition = { x: node.positionAbsolute.x + xDiff, y: node.positionAbsolute.y + yDiff };
|
||||
if (node.computed?.positionAbsolute) {
|
||||
let nextPosition = {
|
||||
x: node.computed?.positionAbsolute.x + xDiff,
|
||||
y: node.computed?.positionAbsolute.y + yDiff,
|
||||
};
|
||||
|
||||
if (snapToGrid) {
|
||||
nextPosition = snapPosition(nextPosition, snapGrid);
|
||||
@@ -40,7 +43,10 @@ function useUpdateNodePositions() {
|
||||
);
|
||||
|
||||
node.position = position;
|
||||
node.positionAbsolute = positionAbsolute;
|
||||
if (!node.computed) {
|
||||
node.computed = {};
|
||||
}
|
||||
node.computed.positionAbsolute = positionAbsolute;
|
||||
}
|
||||
|
||||
return node;
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { useCallback } from 'react';
|
||||
import { getNodesInside } from '@xyflow/system';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useStore } from '../hooks/useStore';
|
||||
import type { Node, ReactFlowState } from '../types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
function useVisibleNodes(onlyRenderVisible: boolean) {
|
||||
const nodes = useStore(
|
||||
useCallback(
|
||||
(s: ReactFlowState) =>
|
||||
onlyRenderVisible
|
||||
? getNodesInside<Node>(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true)
|
||||
: s.nodes,
|
||||
[onlyRenderVisible]
|
||||
)
|
||||
);
|
||||
const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
|
||||
return onlyRenderVisible
|
||||
? getNodesInside<Node>(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
|
||||
(node) => node.id
|
||||
)
|
||||
: Array.from(s.nodeLookup.keys());
|
||||
};
|
||||
|
||||
return nodes;
|
||||
function useVisibleNodeIds(onlyRenderVisible: boolean) {
|
||||
const nodeIds = useStore(useCallback(selector(onlyRenderVisible), [onlyRenderVisible]), shallow);
|
||||
|
||||
return nodeIds;
|
||||
}
|
||||
|
||||
export default useVisibleNodes;
|
||||
export default useVisibleNodeIds;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export { default as ReactFlow } from './container/ReactFlow';
|
||||
export { default as Handle } from './components/Handle';
|
||||
export { default as Handle, type HandleComponentProps } from './components/Handle';
|
||||
export { default as EdgeText } from './components/Edges/EdgeText';
|
||||
export { default as StraightEdge } from './components/Edges/StraightEdge';
|
||||
export { default as StepEdge } from './components/Edges/StepEdge';
|
||||
export { default as BezierEdge } from './components/Edges/BezierEdge';
|
||||
export { default as SimpleBezierEdge, getSimpleBezierPath } from './components/Edges/SimpleBezierEdge';
|
||||
export { default as SmoothStepEdge } from './components/Edges/SmoothStepEdge';
|
||||
export { StraightEdge } from './components/Edges/StraightEdge';
|
||||
export { StepEdge } from './components/Edges/StepEdge';
|
||||
export { BezierEdge } from './components/Edges/BezierEdge';
|
||||
export { SimpleBezierEdge, getSimpleBezierPath } from './components/Edges/SimpleBezierEdge';
|
||||
export { SmoothStepEdge } from './components/Edges/SmoothStepEdge';
|
||||
export { default as BaseEdge } from './components/Edges/BaseEdge';
|
||||
export { default as ReactFlowProvider } from './components/ReactFlowProvider';
|
||||
export { default as Panel, type PanelProps } from './components/Panel';
|
||||
@@ -22,6 +22,8 @@ export { useStore, useStoreApi } from './hooks/useStore';
|
||||
export { default as useOnViewportChange, type UseOnViewportChangeOptions } from './hooks/useOnViewportChange';
|
||||
export { default as useOnSelectionChange, type UseOnSelectionChangeOptions } from './hooks/useOnSelectionChange';
|
||||
export { default as useNodesInitialized, type UseNodesInitializedOptions } from './hooks/useNodesInitialized';
|
||||
export { useHandleConnections } from './hooks/useHandleConnections';
|
||||
export { useNodesData } from './hooks/useNodesData';
|
||||
export { useNodeId } from './contexts/NodeIdContext';
|
||||
|
||||
export { applyNodeChanges, applyEdgeChanges, handleParentExpand } from './utils/changes';
|
||||
@@ -73,6 +75,8 @@ export {
|
||||
type Box,
|
||||
type Transform,
|
||||
type CoordinateExtent,
|
||||
type ColorMode,
|
||||
type ColorModeClass,
|
||||
} from '@xyflow/system';
|
||||
|
||||
// system utils
|
||||
|
||||
@@ -2,11 +2,12 @@ import { createWithEqualityFn } from 'zustand/traditional';
|
||||
import {
|
||||
clampPosition,
|
||||
fitView as fitViewSystem,
|
||||
updateNodes,
|
||||
adoptUserProvidedNodes,
|
||||
updateAbsolutePositions,
|
||||
panBy as panBySystem,
|
||||
Dimensions,
|
||||
updateNodeDimensions as updateNodeDimensionsSystem,
|
||||
updateConnectionLookup,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
|
||||
@@ -42,15 +43,24 @@ const createRFStore = ({
|
||||
...getInitialState({ nodes, edges, width, height, fitView }),
|
||||
setNodes: (nodes: Node[]) => {
|
||||
const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get();
|
||||
// Whenver new nodes are set, we need to calculate the absolute positions of the nodes
|
||||
// and update the nodeLookup.
|
||||
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
|
||||
// setNodes() is called exclusively in response to user actions:
|
||||
// - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup,
|
||||
// - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup.
|
||||
//
|
||||
// When this happens, we take the note objects passed by the user and extend them with fields
|
||||
// relevant for internal React Flow operations.
|
||||
// TODO: consider updating the types to reflect the distinction between user-provided nodes and internal nodes.
|
||||
const nodesWithInternalData = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
|
||||
|
||||
set({ nodes: nextNodes });
|
||||
set({ nodes: nodesWithInternalData });
|
||||
},
|
||||
setEdges: (edges: Edge[]) => {
|
||||
const { defaultEdgeOptions = {} } = get();
|
||||
set({ edges: edges.map((e) => ({ ...defaultEdgeOptions, ...e })) });
|
||||
const { defaultEdgeOptions = {}, connectionLookup } = get();
|
||||
const nextEdges = edges.map((e) => ({ ...defaultEdgeOptions, ...e }));
|
||||
|
||||
updateConnectionLookup(connectionLookup, nextEdges);
|
||||
|
||||
set({ edges: nextEdges });
|
||||
},
|
||||
// when the user works with an uncontrolled flow,
|
||||
// we set a flag `hasDefaultNodes` / `hasDefaultEdges`
|
||||
@@ -69,7 +79,8 @@ const createRFStore = ({
|
||||
};
|
||||
|
||||
if (hasDefaultNodes) {
|
||||
nextState.nodes = updateNodes(nodes, new Map(), {
|
||||
const { nodeLookup } = get();
|
||||
nextState.nodes = adoptUserProvidedNodes(nodes, nodeLookup, {
|
||||
nodeOrigin: get().nodeOrigin,
|
||||
elevateNodesOnSelect: get().elevateNodesOnSelect,
|
||||
});
|
||||
@@ -147,7 +158,7 @@ const createRFStore = ({
|
||||
};
|
||||
|
||||
if (positionChanged) {
|
||||
change.positionAbsolute = node.positionAbsolute;
|
||||
change.positionAbsolute = node.computed?.positionAbsolute;
|
||||
change.position = node.position;
|
||||
}
|
||||
|
||||
@@ -163,7 +174,7 @@ const createRFStore = ({
|
||||
if (changes?.length) {
|
||||
if (hasDefaultNodes) {
|
||||
const updatedNodes = applyNodeChanges(changes, nodes);
|
||||
const nextNodes = updateNodes(updatedNodes, nodeLookup, {
|
||||
const nextNodes = adoptUserProvidedNodes(updatedNodes, nodeLookup, {
|
||||
nodeOrigin,
|
||||
elevateNodesOnSelect,
|
||||
});
|
||||
@@ -276,7 +287,10 @@ const createRFStore = ({
|
||||
|
||||
return {
|
||||
...node,
|
||||
positionAbsolute,
|
||||
computed: {
|
||||
...node.computed,
|
||||
positionAbsolute,
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -323,6 +337,7 @@ const createRFStore = ({
|
||||
|
||||
set(currentConnection);
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
// @todo: what should we do about this? Do we still need it?
|
||||
// if you are on a SPA with multiple flows, we want to make sure that the store gets resetted
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {
|
||||
infiniteExtent,
|
||||
ConnectionMode,
|
||||
updateNodes,
|
||||
adoptUserProvidedNodes,
|
||||
getNodesBounds,
|
||||
getViewportForBounds,
|
||||
Transform,
|
||||
updateConnectionLookup,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { Edge, Node, ReactFlowStore } from '../types';
|
||||
@@ -22,17 +23,17 @@ const getInitialState = ({
|
||||
height?: number;
|
||||
fitView?: boolean;
|
||||
} = {}): ReactFlowStore => {
|
||||
const nodeLookup = new Map<string, Node>();
|
||||
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
|
||||
const nodeLookup = new Map();
|
||||
const connectionLookup = updateConnectionLookup(new Map(), edges);
|
||||
const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, {
|
||||
nodeOrigin: [0, 0],
|
||||
elevateNodesOnSelect: false,
|
||||
});
|
||||
|
||||
let transform: Transform = [0, 0, 1];
|
||||
|
||||
if (fitView && width && height) {
|
||||
const nodesWithDimensions = nextNodes.map((node) => ({
|
||||
...node,
|
||||
width: node.size?.width,
|
||||
height: node.size?.height,
|
||||
}));
|
||||
const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height);
|
||||
const bounds = getNodesBounds(nodesWithDimensions, [0, 0]);
|
||||
const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
|
||||
transform = [x, y, zoom];
|
||||
@@ -46,6 +47,7 @@ const getInitialState = ({
|
||||
nodes: nextNodes,
|
||||
nodeLookup,
|
||||
edges: edges,
|
||||
connectionLookup,
|
||||
onNodesChange: null,
|
||||
onEdgesChange: null,
|
||||
hasDefaultNodes: false,
|
||||
@@ -65,7 +67,7 @@ const getInitialState = ({
|
||||
paneDragging: false,
|
||||
noPanClassName: 'nopan',
|
||||
nodeOrigin: [0, 0],
|
||||
nodeDragThreshold: 0,
|
||||
nodeDragThreshold: 1,
|
||||
|
||||
snapGrid: [15, 15],
|
||||
snapToGrid: false,
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/* this will be exported as base.css and can be used for a basic styling */
|
||||
@import '../../../system/src/styles/init.css';
|
||||
@import '../../../system/src/styles/base.css';
|
||||
|
||||
.react-flow {
|
||||
--edge-label-background-color-default: #ffffff;
|
||||
--edge-label-color-default: inherit;
|
||||
}
|
||||
|
||||
.react-flow.dark {
|
||||
--edge-label-background-color-default: #141414;
|
||||
--edge-label-color-default: #f8f8f8;
|
||||
}
|
||||
|
||||
.react-flow__edge-textbg {
|
||||
fill: var(--edge-label-background-color, var(--edge-label-background-color-default));
|
||||
}
|
||||
|
||||
.react-flow__edge-text {
|
||||
fill: var(--edge-label-color, var(--edge-label-color-default));
|
||||
}
|
||||
|
||||
@@ -2,3 +2,21 @@
|
||||
@import '../../../system/src/styles/init.css';
|
||||
@import '../../../system/src/styles/style.css';
|
||||
@import '../../../system/src/styles/node-resizer.css';
|
||||
|
||||
.react-flow {
|
||||
--edge-label-background-color-default: #ffffff;
|
||||
--edge-label-color-default: inherit;
|
||||
}
|
||||
|
||||
.react-flow.dark {
|
||||
--edge-label-background-color-default: #141414;
|
||||
--edge-label-color-default: #f8f8f8;
|
||||
}
|
||||
|
||||
.react-flow__edge-textbg {
|
||||
fill: var(--edge-label-background-color, var(--edge-label-background-color-default));
|
||||
}
|
||||
|
||||
.react-flow__edge-text {
|
||||
fill: var(--edge-label-color, var(--edge-label-color-default));
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
SelectionMode,
|
||||
OnError,
|
||||
IsValidConnection,
|
||||
ColorMode,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
@@ -155,6 +156,7 @@ export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & {
|
||||
nodeDragThreshold?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
colorMode?: ColorMode;
|
||||
};
|
||||
|
||||
export type ReactFlowRefType = HTMLDivElement;
|
||||
|
||||
@@ -13,9 +13,12 @@ import type {
|
||||
HandleElement,
|
||||
ConnectionStatus,
|
||||
EdgePosition,
|
||||
Optional,
|
||||
StepPathOptions,
|
||||
OnError,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { Node } from '.';
|
||||
import { EdgeTypes, Node } from '.';
|
||||
|
||||
export type EdgeLabelOptions = {
|
||||
label?: string | ReactNode;
|
||||
@@ -46,11 +49,16 @@ type BezierEdgeType<T> = DefaultEdge<T> & {
|
||||
pathOptions?: BezierPathOptions;
|
||||
};
|
||||
|
||||
export type Edge<T = any> = DefaultEdge<T> | SmoothStepEdgeType<T> | BezierEdgeType<T>;
|
||||
type StepEdgeType<T> = DefaultEdge<T> & {
|
||||
type: 'step';
|
||||
pathOptions?: StepPathOptions;
|
||||
};
|
||||
|
||||
export type Edge<T = any> = DefaultEdge<T> | SmoothStepEdgeType<T> | BezierEdgeType<T> | StepEdgeType<T>;
|
||||
|
||||
export type EdgeMouseHandler = (event: ReactMouseEvent, edge: Edge) => void;
|
||||
|
||||
export type WrapEdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandle'> & {
|
||||
export type EdgeWrapperProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandle'> & {
|
||||
onClick?: EdgeMouseHandler;
|
||||
onEdgeDoubleClick?: EdgeMouseHandler;
|
||||
sourceHandleId?: string | null;
|
||||
@@ -68,6 +76,8 @@ export type WrapEdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandl
|
||||
isUpdatable: EdgeUpdatable;
|
||||
isSelectable: boolean;
|
||||
pathOptions?: BezierPathOptions | SmoothStepPathOptions;
|
||||
edgeTypes?: EdgeTypes;
|
||||
onError?: OnError;
|
||||
};
|
||||
|
||||
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
|
||||
@@ -83,7 +93,7 @@ export type EdgeProps<T = any> = Pick<
|
||||
Edge<T>,
|
||||
'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target'
|
||||
> &
|
||||
Pick<WrapEdgeProps, 'sourceHandleId' | 'targetHandleId' | 'interactionWidth'> &
|
||||
Pick<EdgeWrapperProps, 'sourceHandleId' | 'targetHandleId' | 'interactionWidth'> &
|
||||
EdgePosition &
|
||||
EdgeLabelOptions & {
|
||||
markerStart?: string;
|
||||
@@ -100,14 +110,24 @@ export type BaseEdgeProps = Pick<EdgeProps, 'style' | 'markerStart' | 'markerEnd
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type SmoothStepEdgeProps<T = any> = EdgeProps<T> & {
|
||||
export type EdgeComponentProps<T = any> = Optional<Omit<EdgeProps<T>, 'source' | 'target'>, 'id'>;
|
||||
|
||||
export type StraightEdgeProps<T = any> = Omit<EdgeComponentProps<T>, 'sourcePosition' | 'targetPosition'>;
|
||||
|
||||
export type SmoothStepEdgeProps<T = any> = EdgeComponentProps<T> & {
|
||||
pathOptions?: SmoothStepPathOptions;
|
||||
};
|
||||
|
||||
export type BezierEdgeProps<T = any> = EdgeProps<T> & {
|
||||
export type BezierEdgeProps<T = any> = EdgeComponentProps<T> & {
|
||||
pathOptions?: BezierPathOptions;
|
||||
};
|
||||
|
||||
export type StepEdgeProps<T = any> = EdgeComponentProps<T> & {
|
||||
pathOptions?: StepPathOptions;
|
||||
};
|
||||
|
||||
export type SimpleBezierEdgeProps<T = any> = EdgeComponentProps<T>;
|
||||
|
||||
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void;
|
||||
|
||||
export type ConnectionLineComponentProps = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { ComponentType, MemoExoticComponent } from 'react';
|
||||
import type { ComponentType } from 'react';
|
||||
import {
|
||||
FitViewParamsBase,
|
||||
FitViewOptionsBase,
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
XYPosition,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { NodeChange, EdgeChange, Node, WrapNodeProps, Edge, EdgeProps, WrapEdgeProps, ReactFlowInstance } from '.';
|
||||
import type { NodeChange, EdgeChange, Node, Edge, EdgeProps, ReactFlowInstance } from '.';
|
||||
|
||||
export type OnNodesChange = (changes: NodeChange[]) => void;
|
||||
export type OnEdgesChange = (changes: EdgeChange[]) => void;
|
||||
@@ -24,9 +24,7 @@ export type OnEdgesDelete = (edges: Edge[]) => void;
|
||||
export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void;
|
||||
|
||||
export type NodeTypes = { [key: string]: ComponentType<NodeProps> };
|
||||
export type NodeTypesWrapped = { [key: string]: MemoExoticComponent<ComponentType<WrapNodeProps>> };
|
||||
export type EdgeTypes = { [key: string]: ComponentType<EdgeProps> };
|
||||
export type EdgeTypesWrapped = { [key: string]: MemoExoticComponent<ComponentType<WrapEdgeProps>> };
|
||||
|
||||
export type UnselectNodesAndEdgesParams = {
|
||||
nodes?: Node[];
|
||||
|
||||
@@ -45,6 +45,17 @@ export namespace Instance {
|
||||
export type getConnectedEdges = (id: string | (Node | { id: Node['id'] })[]) => Edge[];
|
||||
export type getIncomers = (node: string | Node | { id: Node['id'] }) => Node[];
|
||||
export type getOutgoers = (node: string | Node | { id: Node['id'] }) => Node[];
|
||||
|
||||
export type UpdateNode = (
|
||||
id: string,
|
||||
dataUpdate: Partial<Node> | ((node: Node) => Partial<Node>),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
export type UpdateNodeData = (
|
||||
id: string,
|
||||
dataUpdate: object | ((node: Node) => object),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type ReactFlowInstance<NodeData = any, EdgeData = any> = {
|
||||
@@ -60,5 +71,7 @@ export type ReactFlowInstance<NodeData = any, EdgeData = any> = {
|
||||
deleteElements: Instance.DeleteElements;
|
||||
getIntersectingNodes: Instance.GetIntersectingNodes<NodeData>;
|
||||
isNodeIntersecting: Instance.IsNodeIntersecting<NodeData>;
|
||||
updateNode: Instance.UpdateNode;
|
||||
updateNodeData: Instance.UpdateNodeData;
|
||||
viewportInitialized: boolean;
|
||||
} & Omit<ViewportHelperFunctions, 'initialized'>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import type { NodeBase } from '@xyflow/system';
|
||||
import type { CoordinateExtent, NodeBase, NodeOrigin, OnError } from '@xyflow/system';
|
||||
import { NodeTypes } from './general';
|
||||
|
||||
export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase<
|
||||
NodeData,
|
||||
@@ -14,32 +15,25 @@ export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void;
|
||||
export type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void;
|
||||
export type SelectionDragHandler = (event: ReactMouseEvent, nodes: Node[]) => void;
|
||||
|
||||
export type WrapNodeProps<NodeData = any> = Pick<
|
||||
Node<NodeData>,
|
||||
'id' | 'data' | 'style' | 'className' | 'dragHandle' | 'sourcePosition' | 'targetPosition' | 'hidden' | 'ariaLabel'
|
||||
> &
|
||||
Required<Pick<Node<NodeData>, 'selected' | 'type' | 'zIndex'>> & {
|
||||
isConnectable: boolean;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
xPosOrigin: number;
|
||||
yPosOrigin: number;
|
||||
initialized: boolean;
|
||||
isSelectable: boolean;
|
||||
isDraggable: boolean;
|
||||
isFocusable: boolean;
|
||||
onClick?: NodeMouseHandler;
|
||||
onDoubleClick?: NodeMouseHandler;
|
||||
onMouseEnter?: NodeMouseHandler;
|
||||
onMouseMove?: NodeMouseHandler;
|
||||
onMouseLeave?: NodeMouseHandler;
|
||||
onContextMenu?: NodeMouseHandler;
|
||||
resizeObserver: ResizeObserver | null;
|
||||
isParent: boolean;
|
||||
noDragClassName: string;
|
||||
noPanClassName: string;
|
||||
rfId: string;
|
||||
disableKeyboardA11y: boolean;
|
||||
sizeWidth?: number;
|
||||
sizeHeight?: number;
|
||||
};
|
||||
export type NodeWrapperProps = {
|
||||
id: string;
|
||||
nodesConnectable: boolean;
|
||||
elementsSelectable: boolean;
|
||||
nodesDraggable: boolean;
|
||||
nodesFocusable: boolean;
|
||||
onClick?: NodeMouseHandler;
|
||||
onDoubleClick?: NodeMouseHandler;
|
||||
onMouseEnter?: NodeMouseHandler;
|
||||
onMouseMove?: NodeMouseHandler;
|
||||
onMouseLeave?: NodeMouseHandler;
|
||||
onContextMenu?: NodeMouseHandler;
|
||||
resizeObserver: ResizeObserver | null;
|
||||
noDragClassName: string;
|
||||
noPanClassName: string;
|
||||
rfId: string;
|
||||
disableKeyboardA11y: boolean;
|
||||
nodeTypes?: NodeTypes;
|
||||
nodeExtent?: CoordinateExtent;
|
||||
nodeOrigin: NodeOrigin;
|
||||
onError?: OnError;
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
type OnMoveEnd,
|
||||
type IsValidConnection,
|
||||
type UpdateConnection,
|
||||
Connection,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
@@ -49,6 +50,8 @@ export type ReactFlowStore = {
|
||||
nodes: Node[];
|
||||
nodeLookup: Map<string, Node>;
|
||||
edges: Edge[];
|
||||
connectionLookup: Map<string, Map<string, Connection>>;
|
||||
|
||||
onNodesChange: OnNodesChange | null;
|
||||
onEdgesChange: OnEdgesChange | null;
|
||||
hasDefaultNodes: boolean;
|
||||
|
||||
@@ -5,14 +5,17 @@ export function handleParentExpand(res: any[], updateItem: any) {
|
||||
const parent = res.find((e) => e.id === updateItem.parentNode);
|
||||
|
||||
if (parent) {
|
||||
const extendWidth = updateItem.position.x + updateItem.width - parent.width;
|
||||
const extendHeight = updateItem.position.y + updateItem.height - parent.height;
|
||||
if (!parent.computed) {
|
||||
parent.computed = {};
|
||||
}
|
||||
const extendWidth = updateItem.position.x + updateItem.computed.width - parent.computed.width;
|
||||
const extendHeight = updateItem.position.y + updateItem.computed.height - parent.computed.height;
|
||||
|
||||
if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) {
|
||||
parent.style = { ...parent.style } || {};
|
||||
|
||||
parent.style.width = parent.style.width ?? parent.width;
|
||||
parent.style.height = parent.style.height ?? parent.height;
|
||||
parent.style.width = parent.style.width ?? parent.computed.width;
|
||||
parent.style.height = parent.style.height ?? parent.computed.height;
|
||||
|
||||
if (extendWidth > 0) {
|
||||
parent.style.width += extendWidth;
|
||||
@@ -36,8 +39,8 @@ export function handleParentExpand(res: any[], updateItem: any) {
|
||||
updateItem.position.y = 0;
|
||||
}
|
||||
|
||||
parent.width = parent.style.width;
|
||||
parent.height = parent.style.height;
|
||||
parent.computed.width = parent.style.width;
|
||||
parent.computed.height = parent.style.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,14 +55,17 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
}
|
||||
|
||||
let remainingChanges = changes;
|
||||
const initElements: any[] = changes.filter((c) => c.type === 'add').map((c) => c.item);
|
||||
const updatedElements: any[] = [];
|
||||
|
||||
return elements.reduce((res: any[], item: any) => {
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const nextChanges: any[] = [];
|
||||
const _remainingChanges: any[] = [];
|
||||
const item = elements[i];
|
||||
|
||||
remainingChanges.forEach((c) => {
|
||||
if (c.id === item.id) {
|
||||
if (c.type === 'add') {
|
||||
updatedElements.push(c.item);
|
||||
} else if (c.id === item.id) {
|
||||
nextChanges.push(c);
|
||||
} else {
|
||||
_remainingChanges.push(c);
|
||||
@@ -68,8 +74,8 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
remainingChanges = _remainingChanges;
|
||||
|
||||
if (nextChanges.length === 0) {
|
||||
res.push(item);
|
||||
return res;
|
||||
updatedElements.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
const updateItem = { ...item };
|
||||
@@ -87,7 +93,10 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
}
|
||||
|
||||
if (typeof currentChange.positionAbsolute !== 'undefined') {
|
||||
updateItem.positionAbsolute = currentChange.positionAbsolute;
|
||||
if (!updateItem.computed) {
|
||||
updateItem.computed = {};
|
||||
}
|
||||
updateItem.computed.positionAbsolute = currentChange.positionAbsolute;
|
||||
}
|
||||
|
||||
if (typeof currentChange.dragging !== 'undefined') {
|
||||
@@ -95,14 +104,17 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
}
|
||||
|
||||
if (updateItem.expandParent) {
|
||||
handleParentExpand(res, updateItem);
|
||||
handleParentExpand(updatedElements, updateItem);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'dimensions': {
|
||||
if (typeof currentChange.dimensions !== 'undefined') {
|
||||
updateItem.width = currentChange.dimensions.width;
|
||||
updateItem.height = currentChange.dimensions.height;
|
||||
if (!updateItem.computed) {
|
||||
updateItem.computed = {};
|
||||
}
|
||||
updateItem.computed.width = currentChange.dimensions.width;
|
||||
updateItem.computed.height = currentChange.dimensions.height;
|
||||
}
|
||||
|
||||
if (typeof currentChange.updateStyle !== 'undefined') {
|
||||
@@ -114,20 +126,20 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
}
|
||||
|
||||
if (updateItem.expandParent) {
|
||||
handleParentExpand(res, updateItem);
|
||||
handleParentExpand(updatedElements, updateItem);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'remove': {
|
||||
return res;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
updatedElements.push(updateItem);
|
||||
}
|
||||
}
|
||||
|
||||
res.push(updateItem);
|
||||
return res;
|
||||
}, initElements);
|
||||
return updatedElements;
|
||||
}
|
||||
|
||||
export function applyNodeChanges<NodeData = any>(changes: NodeChange[], nodes: Node<NodeData>[]): Node<NodeData>[] {
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
|
||||
import type { Edge, Node } from '../types';
|
||||
|
||||
export const isNode = isNodeBase<Node, Edge>;
|
||||
export const isEdge = isEdgeBase<Node, Edge>;
|
||||
export const isNode = isNodeBase<Node>;
|
||||
export const isEdge = isEdgeBase<Edge>;
|
||||
export const getOutgoers = getOutgoersBase<Node, Edge>;
|
||||
export const getIncomers = getIncomersBase<Node, Edge>;
|
||||
export const addEdge = addEdgeBase<Edge>;
|
||||
|
||||
Reference in New Issue
Block a user