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>;
|
||||
|
||||
@@ -1,6 +1,49 @@
|
||||
## 0.0.29
|
||||
|
||||
Another huge update for Svelte Flow 🙏 Handling data flows will be way easier with the new hooks and functions. You can now subscribe to connected nodes, receive data and update nodes more easily. We fix a big issue about the `<Handle />` component. No more `on:connect` that only worked for target `<Handle />` components but `onconnect` and `ondisconnect` that works for every `<Handle />`.
|
||||
|
||||
### Features
|
||||
|
||||
- add `useHandleConnections` hook for receiving connected node and handle ids for a specific handle
|
||||
- add `useNodesData(ids: string | string[])` hook for receiving data from other nodes
|
||||
- export `updateNode` and `updateNodeData` from `useSvelteFlow` to update a node or the data object
|
||||
- add `onedgecreate` function for passing a certain id or other attributes to a newly created edge
|
||||
|
||||
### ⚠️ Breaking
|
||||
|
||||
- replace `on:connect`, `on:connectstart` and `on:connectend` with `onconnect`, `onconnectstart` and `onconnectend`, no need to forward `on:connect..` anymore
|
||||
|
||||
### Fixes and minor changes
|
||||
|
||||
- `onconnect` and `ondisconnect` callback work for `<Handle />` component
|
||||
- don't delete a node when user presses Backspace inside an input/textarea/.nokey element
|
||||
- `bgColor` prop for Background didn't work
|
||||
- prefix css vars with "xy-"
|
||||
- don't update nodes and edges on pane click if not necessary
|
||||
- cleaner types for exported edges
|
||||
- fix `getIntersectingNodes` bug when passing `Rect`
|
||||
|
||||
## 0.0.28
|
||||
|
||||
This is a huge update! We added a new `<NodeToolbar />` component and a new `colorMode` ('light' | 'dark' | 'system') prop for toggling dark/light mode.
|
||||
|
||||
There are also some breaking changes again (sorry!) but we are very close to the final API for Svelte Flow 1.0.0. The biggest change is that we group node attriubutes (`width`, `height`, `positionAbsolute`) that are added by the library under `node.computed`. This makes it easier to understand, that this stuff comes from the library itself. `node.width` and `node.height` is still an optional node option and can be used to set certain dimensions for SSR or on the client.
|
||||
|
||||
- add `<NodeToolbar />` component
|
||||
- add `on:selectionclick` and `on:selectioncontextmenu` event handlers
|
||||
- add `ondelete({ nodes, edges })` handler
|
||||
- add `zoomActivationKey` prop
|
||||
- add `width` and `height` prop to custom `NodeProps` type
|
||||
- add `colorMode` prop ('light' | 'dark' | 'system')
|
||||
- ⚠️ replace `xPos` and `yPos` with `positionAbsolute` prop to custom `NodeProps` type
|
||||
- ⚠️ `node.width/height` and `node.positionAbsolute` can now be found under `node.computed.width/height/positionAbsolute`
|
||||
- ⚠️ `node.width/height` is still optional an can be used for forcing certain dimensions and SSR
|
||||
- refactor keys: you can now disable keys by setting them to `null` (e.g. `selectionKey={null}`)
|
||||
- performance optimization with internal node lookup
|
||||
|
||||
## 0.0.27
|
||||
|
||||
- add `selectionOnDrag` prop - can be used to create figma-like controls in combination with `panOnDrag={false}` / `panOnDrag={[1, 2]}` + `panOnScroll={true}`
|
||||
- add `selectionOnDrag` prop - can be used to create figma-like controls in combination with `panOnDrag={false}` / `panOnDrag={[1, 2]}` + `panOnScroll={true}`
|
||||
- ⚠️ rename `screenToFlowCoordinate` to `screenToFlowPosition`
|
||||
- ⚠️ rename `flowToScreenCoordinate` to `flowToScreenPosition`
|
||||
- ⚠️ rename `getTransformForBounds` to `getViewportForBounds` (return `{ x: number, y: number, zoom: number }` instead of `[number, number, number]`)
|
||||
@@ -49,7 +92,7 @@
|
||||
|
||||
## 0.0.22
|
||||
|
||||
- add `connectionLine` slot for rendering a custom connection line
|
||||
- add `connectionLine` slot for rendering a custom connection line
|
||||
- add `connectionLineStyle` and `connectionLineContainerStyle` props
|
||||
- add `useConnection` hook
|
||||
- add `nodeDragThreshold` prop
|
||||
@@ -82,7 +125,7 @@
|
||||
|
||||
## 0.0.18
|
||||
|
||||
- add `nodesDraggable` prop
|
||||
- add `nodesDraggable` prop
|
||||
- minimap: add default background
|
||||
|
||||
## 0.0.17
|
||||
@@ -95,7 +138,7 @@
|
||||
|
||||
## 0.0.15
|
||||
|
||||
- fix wrongly displayed connectionline
|
||||
- fix wrongly displayed connectionline
|
||||
|
||||
## 0.0.14
|
||||
|
||||
@@ -134,14 +177,14 @@ this version is broken because of a wrong path in the package.json
|
||||
|
||||
- add `connectionRadius`
|
||||
|
||||
## 0.0.1
|
||||
## 0.0.1
|
||||
|
||||
Svelte Flow alpha is here 🔥 You can expect some changes until we reach 1.0.0 but we try to stick as close as possible to the React Flow API. There are no docs yet, but we are working on it! For now the easiest way is to use the autocomplete of your IDE, lookup the props in the SvelteFlow component or check out the React Flow docs.
|
||||
Svelte Flow alpha is here 🔥 You can expect some changes until we reach 1.0.0 but we try to stick as close as possible to the React Flow API. There are no docs yet, but we are working on it! For now the easiest way is to use the autocomplete of your IDE, lookup the props in the SvelteFlow component or check out the React Flow docs.
|
||||
|
||||
This very first release comes with lots of features already:
|
||||
|
||||
- pass `nodes` and `edges` as writables
|
||||
- draggable, selectable and deletable nodes
|
||||
- pass `nodes` and `edges` as writables
|
||||
- draggable, selectable and deletable nodes
|
||||
- support for custom `nodeTypes` and `edgeTypes`
|
||||
- basic viewport settings like `fitView`, `minZoom` and `maxZoom`
|
||||
- additional components: `<MiniMap />`, `<Controls />` & `<Background />`
|
||||
- additional components: `<MiniMap />`, `<Controls />` & `<Background />`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xyflow/svelte",
|
||||
"version": "0.0.27",
|
||||
"version": "0.0.29",
|
||||
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
|
||||
"keywords": [
|
||||
"svelte",
|
||||
@@ -41,7 +41,7 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@svelte-put/shortcut": "^3.0.0",
|
||||
"@svelte-put/shortcut": "^3.1.0",
|
||||
"@xyflow/system": "workspace:*",
|
||||
"classcat": "^5.0.4"
|
||||
},
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
export default function (node: Element, target = 'body') {
|
||||
const targetEl = document.querySelector(target);
|
||||
type PortalOptions = {
|
||||
target?: string;
|
||||
domNode: Element | null;
|
||||
};
|
||||
|
||||
function tryToMount(node: Element, domNode: Element | null, target: string | undefined) {
|
||||
if (!domNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetEl = target ? domNode.querySelector(target) : domNode;
|
||||
|
||||
if (targetEl) {
|
||||
targetEl.appendChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
export default function (node: Element, { target, domNode }: PortalOptions) {
|
||||
tryToMount(node, domNode, target);
|
||||
|
||||
return {
|
||||
async update({ target, domNode }: PortalOptions) {
|
||||
tryToMount(node, domNode, target);
|
||||
},
|
||||
destroy() {
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts">
|
||||
import portal from '$lib/actions/portal';
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
type $$Props = {};
|
||||
|
||||
const { domNode } = useStore();
|
||||
</script>
|
||||
|
||||
<div use:portal={'.svelte-flow__edgelabel-renderer'}>
|
||||
<div use:portal={{ target: '.svelte-flow__edgelabel-renderer', domNode: $domNode }}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { errorMessages, getMarkerId } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import BezierEdge from '$lib/components/edges/BezierEdge.svelte';
|
||||
import { BezierEdgeInternal } from '$lib/components/edges';
|
||||
import type { EdgeLayouted, Edge } from '$lib/types';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
edgecontextmenu: { edge: Edge; event: MouseEvent };
|
||||
}>();
|
||||
|
||||
$: edgeComponent = $edgeTypes[type!] || BezierEdge;
|
||||
$: edgeComponent = $edgeTypes[type!] || BezierEdgeInternal;
|
||||
$: markerStartUrl = markerStart ? `url(#${getMarkerId(markerStart, $flowId)})` : undefined;
|
||||
$: markerEndUrl = markerEnd ? `url(#${getMarkerId(markerEnd, $flowId)})` : undefined;
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import cc from 'classcat';
|
||||
import {
|
||||
Position,
|
||||
XYHandle,
|
||||
isMouseEvent,
|
||||
type Connection,
|
||||
type HandleType
|
||||
areConnectionMapsEqual,
|
||||
handleConnectionChange
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { HandleComponentProps } from '$lib/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
type $$Props = HandleComponentProps;
|
||||
|
||||
@@ -20,6 +21,8 @@
|
||||
export let position: $$Props['position'] = Position.Top;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
export let onconnect: $$Props['onconnect'] = undefined;
|
||||
export let ondisconnect: $$Props['ondisconnect'] = undefined;
|
||||
// export let isConnectableStart: $$Props['isConnectableStart'] = undefined;
|
||||
// export let isConnectableEnd: $$Props['isConnectableEnd'] = undefined;
|
||||
|
||||
@@ -32,18 +35,6 @@
|
||||
$: handleConnectable = isConnectable !== undefined ? isConnectable : $connectable;
|
||||
|
||||
const handleId = id || null;
|
||||
const dispatch = createEventDispatcher<{
|
||||
connect: { connection: Connection };
|
||||
connectstart: {
|
||||
event: MouseEvent | TouchEvent;
|
||||
nodeId: string | null;
|
||||
handleId: string | null;
|
||||
handleType: HandleType | null;
|
||||
};
|
||||
connectend: {
|
||||
event: MouseEvent | TouchEvent;
|
||||
};
|
||||
}>();
|
||||
|
||||
const store = useStore();
|
||||
const {
|
||||
@@ -55,10 +46,16 @@
|
||||
isValidConnection,
|
||||
lib,
|
||||
addEdge,
|
||||
onedgecreate,
|
||||
panBy,
|
||||
cancelConnection,
|
||||
updateConnection,
|
||||
autoPanOnConnect
|
||||
autoPanOnConnect,
|
||||
edges,
|
||||
connectionLookup,
|
||||
onconnect: onConnectAction,
|
||||
onconnectstart: onConnectStartAction,
|
||||
onconnectend: onConnectEndAction
|
||||
} = store;
|
||||
|
||||
function onPointerDown(event: MouseEvent | TouchEvent) {
|
||||
@@ -80,28 +77,50 @@
|
||||
cancelConnection,
|
||||
panBy,
|
||||
onConnect: (connection) => {
|
||||
addEdge(connection);
|
||||
const edge = $onedgecreate ? $onedgecreate(connection) : connection;
|
||||
|
||||
// @todo: should we change/ improve the stuff we are passing here?
|
||||
// instead of source/target we could pass fromNodeId, fromHandleId, etc
|
||||
dispatch('connect', { connection });
|
||||
if (!edge) {
|
||||
return;
|
||||
}
|
||||
|
||||
addEdge(edge);
|
||||
$onConnectAction?.(connection);
|
||||
},
|
||||
onConnectStart: (event, startParams) => {
|
||||
dispatch('connectstart', {
|
||||
event,
|
||||
$onConnectStartAction?.(event, {
|
||||
nodeId: startParams.nodeId,
|
||||
handleId: startParams.handleId,
|
||||
handleType: startParams.handleType
|
||||
});
|
||||
},
|
||||
onConnectEnd: (event) => {
|
||||
dispatch('connectend', { event });
|
||||
$onConnectEndAction?.(event);
|
||||
},
|
||||
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let prevConnections: Map<string, Connection> | null = null;
|
||||
let connections: Map<string, Connection> | undefined;
|
||||
|
||||
$: if (onconnect || ondisconnect) {
|
||||
// connectionLookup is not reactive, so we use edges to get notified about updates
|
||||
$edges;
|
||||
connections = $connectionLookup.get(`${nodeId}-${type}-${id || null}`);
|
||||
}
|
||||
|
||||
$: {
|
||||
if (prevConnections && !areConnectionMapsEqual(connections, prevConnections)) {
|
||||
const _connections = connections ?? new Map();
|
||||
|
||||
handleConnectionChange(prevConnections, _connections, ondisconnect);
|
||||
handleConnectionChange(_connections, prevConnections, onconnect);
|
||||
}
|
||||
|
||||
prevConnections = connections ?? new Map();
|
||||
}
|
||||
|
||||
// @todo implement connectablestart, connectableend
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { shortcut, type ShortcutModifierDefinition } from '@svelte-put/shortcut';
|
||||
import { isInputDOMNode, isMacOs } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { KeyHandlerProps } from './types';
|
||||
import type { KeyDefinition, KeyDefinitionObject } from '$lib/types';
|
||||
import { isMacOs } from '@xyflow/system';
|
||||
|
||||
type $$Props = KeyHandlerProps;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
} = useStore();
|
||||
|
||||
function isKeyObject(key?: KeyDefinition | null): key is KeyDefinitionObject {
|
||||
return typeof key === 'object';
|
||||
return key !== null && typeof key === 'object';
|
||||
}
|
||||
|
||||
function getModifier(key?: KeyDefinition | null): ShortcutModifierDefinition {
|
||||
@@ -71,16 +71,19 @@
|
||||
trigger: [
|
||||
{
|
||||
...selectionKeyDefinition,
|
||||
callback: () => selectionKeyDefinition.key && selectionKeyPressed.set(true)
|
||||
enabled: selectionKeyDefinition.key !== null,
|
||||
callback: () => selectionKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
|
||||
type: 'keydown'
|
||||
}}
|
||||
use:shortcut={{
|
||||
trigger: [
|
||||
{
|
||||
...selectionKeyDefinition,
|
||||
callback: () => selectionKeyDefinition.key && selectionKeyPressed.set(false)
|
||||
enabled: selectionKeyDefinition.key !== null,
|
||||
callback: () => selectionKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
@@ -89,7 +92,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...multiSelectionKeyDefinition,
|
||||
callback: () => multiSelectionKeyDefinition.key && multiselectionKeyPressed.set(true)
|
||||
enabled: multiSelectionKeyDefinition.key !== null,
|
||||
callback: () => multiselectionKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
type: 'keydown'
|
||||
@@ -98,7 +102,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...multiSelectionKeyDefinition,
|
||||
callback: () => multiSelectionKeyDefinition.key && multiselectionKeyPressed.set(false)
|
||||
enabled: multiSelectionKeyDefinition.key !== null,
|
||||
callback: () => multiselectionKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
@@ -107,7 +112,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...deleteKeyDefinition,
|
||||
callback: () => deleteKeyDefinition.key && deleteKeyPressed.set(true)
|
||||
enabled: deleteKeyDefinition.key !== null,
|
||||
callback: (detail) => !isInputDOMNode(detail.originalEvent) && deleteKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
type: 'keydown'
|
||||
@@ -116,7 +122,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...deleteKeyDefinition,
|
||||
callback: () => deleteKeyDefinition.key && deleteKeyPressed.set(false)
|
||||
enabled: deleteKeyDefinition.key !== null,
|
||||
callback: () => deleteKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
@@ -125,7 +132,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...panActivationKeyDefinition,
|
||||
callback: () => panActivationKeyDefinition.key && panActivationKeyPressed.set(true)
|
||||
enabled: panActivationKeyDefinition.key !== null,
|
||||
callback: () => panActivationKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
type: 'keydown'
|
||||
@@ -134,7 +142,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...panActivationKeyDefinition,
|
||||
callback: () => panActivationKeyDefinition.key && panActivationKeyPressed.set(false)
|
||||
enabled: panActivationKeyDefinition.key !== null,
|
||||
callback: () => panActivationKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
@@ -143,7 +152,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...zoomActivationKeyDefinition,
|
||||
callback: () => zoomActivationKeyDefinition.key && zoomActivationKeyPressed.set(true)
|
||||
enabled: zoomActivationKeyDefinition.key !== null,
|
||||
callback: () => zoomActivationKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
type: 'keydown'
|
||||
@@ -152,7 +162,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...zoomActivationKeyDefinition,
|
||||
callback: () => zoomActivationKeyDefinition.key && zoomActivationKeyPressed.set(false)
|
||||
enabled: zoomActivationKeyDefinition.key !== null,
|
||||
callback: () => zoomActivationKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { getNodesBounds } from '@xyflow/system';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import { Selection } from '$lib/components/Selection';
|
||||
import drag from '$lib/actions/drag';
|
||||
import type { Node } from '$lib/types';
|
||||
|
||||
const store = useStore();
|
||||
const { selectionRectMode, nodes } = store;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
selectioncontextmenu: { nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
selectionclick: { nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
}>();
|
||||
|
||||
$: selectedNodes = $nodes.filter((n) => n.selected);
|
||||
$: bounds = getNodesBounds(selectedNodes);
|
||||
|
||||
function onContextMenu(event: MouseEvent | TouchEvent) {
|
||||
dispatch('selectioncontextmenu', { nodes: selectedNodes, event });
|
||||
}
|
||||
|
||||
function onClick(event: MouseEvent | TouchEvent) {
|
||||
dispatch('selectionclick', { nodes: selectedNodes, event });
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if selectedNodes && $selectionRectMode === 'nodes'}
|
||||
@@ -17,6 +32,8 @@
|
||||
class="selection-wrapper nopan"
|
||||
style="width: {bounds.width}px; height: {bounds.height}px; transform: translate({bounds.x}px, {bounds.y}px)"
|
||||
use:drag={{ disabled: false, store }}
|
||||
on:contextmenu={onContextMenu}
|
||||
on:click={onClick}
|
||||
>
|
||||
<Selection width="100%" height="100%" x={0} y={0} />
|
||||
</div>
|
||||
|
||||
@@ -27,11 +27,15 @@
|
||||
export let style: NodeWrapperProps['style'] = undefined;
|
||||
export let type: NodeWrapperProps['type'] = 'default';
|
||||
export let isParent: NodeWrapperProps['isParent'] = false;
|
||||
export let positionAbsolute: NodeWrapperProps['positionAbsolute'] = undefined;
|
||||
export let positionOrigin: NodeWrapperProps['positionOrigin'] = undefined;
|
||||
export let positionX: NodeWrapperProps['positionX'];
|
||||
export let positionY: NodeWrapperProps['positionY'];
|
||||
export let positionOriginX: NodeWrapperProps['positionOriginX'];
|
||||
export let positionOriginY: NodeWrapperProps['positionOriginY'];
|
||||
export let sourcePosition: NodeWrapperProps['sourcePosition'] = undefined;
|
||||
export let targetPosition: NodeWrapperProps['targetPosition'] = undefined;
|
||||
export let zIndex: NodeWrapperProps['zIndex'];
|
||||
export let width: NodeWrapperProps['width'] = undefined;
|
||||
export let height: NodeWrapperProps['height'] = undefined;
|
||||
export let dragHandle: NodeWrapperProps['dragHandle'] = undefined;
|
||||
export let initialized: NodeWrapperProps['initialized'] = false;
|
||||
let className: string = '';
|
||||
@@ -164,11 +168,11 @@
|
||||
class:nopan={draggable}
|
||||
class:parent={isParent}
|
||||
style:z-index={zIndex}
|
||||
style:transform="translate({positionOrigin?.x ?? 0}px, {positionOrigin?.y ?? 0}px)"
|
||||
style:transform="translate({positionOriginX}px, {positionOriginY}px)"
|
||||
style:visibility={initialized ? 'visible' : 'hidden'}
|
||||
style="{style} {node.size?.width ? `;width=${node.size?.width}px` : ''} {node.size?.height
|
||||
? `;height=${node.size?.height}px;`
|
||||
: ''}"
|
||||
style="{style ?? ''}; {!width ? '' : `width:${width}px;`} {!height
|
||||
? ''
|
||||
: `height:${height}px;`}"
|
||||
on:click={onSelectNodeHandler}
|
||||
on:mouseenter={(event) => dispatch('nodemouseenter', { node, event })}
|
||||
on:mouseleave={(event) => dispatch('nodemouseleave', { node, event })}
|
||||
@@ -187,11 +191,10 @@
|
||||
{dragging}
|
||||
{dragHandle}
|
||||
isConnectable={connectable}
|
||||
xPos={positionAbsolute?.x ?? 0}
|
||||
yPos={positionAbsolute?.y ?? 0}
|
||||
on:connectstart
|
||||
on:connect
|
||||
on:connectend
|
||||
positionAbsoluteX={positionX}
|
||||
positionAbsoluteY={positionY}
|
||||
{width}
|
||||
{height}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { XYPosition } from '@xyflow/system';
|
||||
import type { Node } from '$lib/types';
|
||||
|
||||
export type NodeWrapperProps = Pick<
|
||||
@@ -9,19 +8,22 @@ export type NodeWrapperProps = Pick<
|
||||
| 'data'
|
||||
| 'draggable'
|
||||
| 'dragging'
|
||||
| 'positionAbsolute'
|
||||
| 'selected'
|
||||
| 'selectable'
|
||||
| 'style'
|
||||
| 'type'
|
||||
| 'width'
|
||||
| 'height'
|
||||
| 'sourcePosition'
|
||||
| 'targetPosition'
|
||||
| 'dragHandle'
|
||||
| 'hidden'
|
||||
> & {
|
||||
positionOrigin?: XYPosition;
|
||||
width?: number;
|
||||
height?: number;
|
||||
type: string;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
positionOriginX: number;
|
||||
positionOriginY: number;
|
||||
'on:nodeclick'?: (event: MouseEvent) => void;
|
||||
resizeObserver?: ResizeObserver | null;
|
||||
isParent?: boolean;
|
||||
|
||||
@@ -1,30 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { getBezierPath } from '@xyflow/system';
|
||||
|
||||
import type { EdgeProps } from '$lib/types';
|
||||
import type { BezierEdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = EdgeProps;
|
||||
type $$Props = BezierEdgeProps;
|
||||
|
||||
export let id: $$Props['id'] = undefined;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let pathOptions: $$Props['pathOptions'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
|
||||
$: [path, labelX, labelY] = getBezierPath({
|
||||
sourceX: $$props.sourceX,
|
||||
sourceY: $$props.sourceY,
|
||||
targetX: $$props.targetX,
|
||||
targetY: $$props.targetY,
|
||||
sourcePosition: $$props.sourcePosition,
|
||||
targetPosition: $$props.targetPosition
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
curvature: pathOptions?.curvature
|
||||
});
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
{id}
|
||||
{path}
|
||||
{labelX}
|
||||
{labelY}
|
||||
id={$$props.id}
|
||||
label={$$props.label}
|
||||
labelStyle={$$props.labelStyle}
|
||||
markerStart={$$props.markerStart}
|
||||
markerEnd={$$props.markerEnd}
|
||||
interactionWidth={$$props.interactionWidth}
|
||||
style={$$props.style}
|
||||
{label}
|
||||
{labelStyle}
|
||||
{markerStart}
|
||||
{markerEnd}
|
||||
{interactionWidth}
|
||||
{style}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import { getBezierPath } from '@xyflow/system';
|
||||
|
||||
import type { EdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = EdgeProps;
|
||||
|
||||
// these props are not used in this edge, but passed to every custom edge component
|
||||
export let id: $$Props['id'] = '';
|
||||
export let source: $$Props['source'] = '';
|
||||
export let target: $$Props['target'] = '';
|
||||
export let animated: $$Props['animated'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let data: $$Props['data'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
export let targetHandleId: $$Props['targetHandleId'] = undefined;
|
||||
|
||||
$: [path, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition
|
||||
});
|
||||
|
||||
// hopefully with Svelte5, we don't need this kind of workaround anymore
|
||||
id;
|
||||
source;
|
||||
target;
|
||||
animated;
|
||||
selected;
|
||||
data;
|
||||
sourceHandleId;
|
||||
targetHandleId;
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
{path}
|
||||
{labelX}
|
||||
{labelY}
|
||||
{label}
|
||||
{labelStyle}
|
||||
{markerStart}
|
||||
{markerEnd}
|
||||
{interactionWidth}
|
||||
{style}
|
||||
/>
|
||||
@@ -1,30 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { getSmoothStepPath } from '@xyflow/system';
|
||||
|
||||
import type { EdgeProps } from '$lib/types';
|
||||
import type { SmoothStepEdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = EdgeProps;
|
||||
type $$Props = SmoothStepEdgeProps;
|
||||
|
||||
export let id: $$Props['id'] = undefined;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let pathOptions: $$Props['pathOptions'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
|
||||
$: [path, labelX, labelY] = getSmoothStepPath({
|
||||
sourceX: $$props.sourceX,
|
||||
sourceY: $$props.sourceY,
|
||||
targetX: $$props.targetX,
|
||||
targetY: $$props.targetY,
|
||||
sourcePosition: $$props.sourcePosition,
|
||||
targetPosition: $$props.targetPosition
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
borderRadius: pathOptions?.borderRadius,
|
||||
offset: pathOptions?.offset
|
||||
});
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
{id}
|
||||
{path}
|
||||
{labelX}
|
||||
{labelY}
|
||||
id={$$props.id}
|
||||
label={$$props.label}
|
||||
labelStyle={$$props.labelStyle}
|
||||
markerStart={$$props.markerStart}
|
||||
markerEnd={$$props.markerEnd}
|
||||
interactionWidth={$$props.interactionWidth}
|
||||
style={$$props.style}
|
||||
{label}
|
||||
{labelStyle}
|
||||
{markerStart}
|
||||
{markerEnd}
|
||||
{interactionWidth}
|
||||
{style}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import { getSmoothStepPath } from '@xyflow/system';
|
||||
|
||||
import type { EdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = EdgeProps;
|
||||
|
||||
// these props are not used in this edge, but passed to every custom edge component
|
||||
export let id: $$Props['id'] = '';
|
||||
export let source: $$Props['source'] = '';
|
||||
export let target: $$Props['target'] = '';
|
||||
|
||||
export let animated: $$Props['animated'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let data: $$Props['data'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
export let targetHandleId: $$Props['targetHandleId'] = undefined;
|
||||
|
||||
$: [path, labelX, labelY] = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition
|
||||
});
|
||||
|
||||
id;
|
||||
source;
|
||||
target;
|
||||
animated;
|
||||
selected;
|
||||
data;
|
||||
sourceHandleId;
|
||||
targetHandleId;
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
{path}
|
||||
{labelX}
|
||||
{labelY}
|
||||
{label}
|
||||
{labelStyle}
|
||||
{markerStart}
|
||||
{markerEnd}
|
||||
{interactionWidth}
|
||||
{style}
|
||||
/>
|
||||
@@ -1,31 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { getSmoothStepPath } from '@xyflow/system';
|
||||
|
||||
import type { EdgeProps } from '$lib/types';
|
||||
import type { StepEdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = EdgeProps;
|
||||
type $$Props = StepEdgeProps;
|
||||
|
||||
export let id: $$Props['id'] = undefined;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let pathOptions: $$Props['pathOptions'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
|
||||
$: [path, labelX, labelY] = getSmoothStepPath({
|
||||
sourceX: $$props.sourceX,
|
||||
sourceY: $$props.sourceY,
|
||||
targetX: $$props.targetX,
|
||||
targetY: $$props.targetY,
|
||||
sourcePosition: $$props.sourcePosition,
|
||||
targetPosition: $$props.targetPosition,
|
||||
borderRadius: 0
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
borderRadius: 0,
|
||||
offset: pathOptions?.offset
|
||||
});
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
{id}
|
||||
{path}
|
||||
{labelX}
|
||||
{labelY}
|
||||
id={$$props.id}
|
||||
label={$$props.label}
|
||||
labelStyle={$$props.labelStyle}
|
||||
markerStart={$$props.markerStart}
|
||||
markerEnd={$$props.markerEnd}
|
||||
interactionWidth={$$props.interactionWidth}
|
||||
style={$$props.style}
|
||||
{label}
|
||||
{labelStyle}
|
||||
{markerStart}
|
||||
{markerEnd}
|
||||
{interactionWidth}
|
||||
{style}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { getSmoothStepPath } from '@xyflow/system';
|
||||
|
||||
import type { EdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = EdgeProps;
|
||||
|
||||
// these props are not used in this edge, but passed to every custom edge component
|
||||
export let id: $$Props['id'] = '';
|
||||
export let source: $$Props['source'] = '';
|
||||
export let target: $$Props['target'] = '';
|
||||
|
||||
export let animated: $$Props['animated'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let data: $$Props['data'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
export let targetHandleId: $$Props['targetHandleId'] = undefined;
|
||||
|
||||
$: [path, labelX, labelY] = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
borderRadius: 0
|
||||
});
|
||||
|
||||
id;
|
||||
source;
|
||||
target;
|
||||
animated;
|
||||
selected;
|
||||
data;
|
||||
sourceHandleId;
|
||||
targetHandleId;
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
{path}
|
||||
{labelX}
|
||||
{labelY}
|
||||
{label}
|
||||
{labelStyle}
|
||||
{markerStart}
|
||||
{markerEnd}
|
||||
{interactionWidth}
|
||||
{style}
|
||||
/>
|
||||
@@ -1,28 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { getStraightPath } from '@xyflow/system';
|
||||
|
||||
import type { EdgeProps } from '$lib/types';
|
||||
import type { StraightEdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = EdgeProps;
|
||||
type $$Props = StraightEdgeProps;
|
||||
|
||||
export let id: $$Props['id'] = undefined;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
|
||||
$: [path, labelX, labelY] = getStraightPath({
|
||||
sourceX: $$props.sourceX,
|
||||
sourceY: $$props.sourceY,
|
||||
targetX: $$props.targetX,
|
||||
targetY: $$props.targetY
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY
|
||||
});
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
{id}
|
||||
{path}
|
||||
{labelX}
|
||||
{labelY}
|
||||
id={$$props.id}
|
||||
label={$$props.label}
|
||||
labelStyle={$$props.labelStyle}
|
||||
markerStart={$$props.markerStart}
|
||||
markerEnd={$$props.markerEnd}
|
||||
interactionWidth={$$props.interactionWidth}
|
||||
style={$$props.style}
|
||||
{label}
|
||||
{labelStyle}
|
||||
{markerStart}
|
||||
{markerEnd}
|
||||
{interactionWidth}
|
||||
{style}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import { getStraightPath } from '@xyflow/system';
|
||||
|
||||
import type { EdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = EdgeProps;
|
||||
|
||||
// these props are not used in this edge, but passed to every custom edge component
|
||||
export let id: $$Props['id'] = '';
|
||||
export let source: $$Props['source'] = '';
|
||||
export let target: $$Props['target'] = '';
|
||||
|
||||
export let animated: $$Props['animated'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let data: $$Props['data'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
export let targetHandleId: $$Props['targetHandleId'] = undefined;
|
||||
|
||||
$: [path, labelX, labelY] = getStraightPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY
|
||||
});
|
||||
|
||||
id;
|
||||
source;
|
||||
target;
|
||||
animated;
|
||||
selected;
|
||||
data;
|
||||
sourcePosition;
|
||||
targetPosition;
|
||||
sourceHandleId;
|
||||
targetHandleId;
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
{path}
|
||||
{labelX}
|
||||
{labelY}
|
||||
{label}
|
||||
{labelStyle}
|
||||
{markerStart}
|
||||
{markerEnd}
|
||||
{interactionWidth}
|
||||
{style}
|
||||
/>
|
||||
@@ -1,4 +1,17 @@
|
||||
// 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
|
||||
|
||||
// @todo: how can we prevent this duplication in ...Edge/ ...EdgeInternal?
|
||||
// both are quite similar, it's just about 1-2 props that are different
|
||||
export { default as BezierEdge } from './BezierEdge.svelte';
|
||||
export { default as StraightEdge } from './StraightEdge.svelte';
|
||||
export { default as BezierEdgeInternal } from './BezierEdgeInternal.svelte';
|
||||
|
||||
export { default as SmoothStepEdge } from './SmoothStepEdge.svelte';
|
||||
export { default as SmoothStepEdgeInternal } from './SmoothStepEdgeInternal.svelte';
|
||||
|
||||
export { default as StraightEdge } from './StraightEdge.svelte';
|
||||
export { default as StraightEdgeInternal } from './StraightEdgeInternal.svelte';
|
||||
|
||||
export { default as StepEdge } from './StepEdge.svelte';
|
||||
export { default as StepEdgeInternal } from './StepEdgeInternal.svelte';
|
||||
|
||||
@@ -11,27 +11,31 @@
|
||||
|
||||
// unused props - we need to list them here in order to prevent warnings
|
||||
export let id: $$Props['id'] = '';
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let type: $$Props['type'] = undefined;
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
export let dragging: $$Props['dragging'] = false;
|
||||
export let dragHandle: $$Props['dragHandle'] = undefined;
|
||||
export let xPos: $$Props['xPos'] = 0;
|
||||
export let yPos: $$Props['yPos'] = 0;
|
||||
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
|
||||
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
|
||||
// @todo: there must be a better way to do this
|
||||
id;
|
||||
width;
|
||||
height;
|
||||
selected;
|
||||
type;
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
xPos;
|
||||
yPos;
|
||||
positionAbsoluteX;
|
||||
positionAbsoluteY;
|
||||
isConnectable;
|
||||
</script>
|
||||
|
||||
<Handle type="target" position={targetPosition} on:connectstart on:connect on:connectend />
|
||||
<Handle type="target" position={targetPosition} />
|
||||
{data?.label}
|
||||
<Handle type="source" position={sourcePosition} on:connectstart on:connect on:connectend />
|
||||
<Handle type="source" position={sourcePosition} />
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
// unused props - we need to list them here in order to prevent warnings
|
||||
export let id: $$Props['id'] = '';
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let data: $$Props['data'] = {};
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let sourcePosition: $$Props['sourcePosition'] = undefined;
|
||||
@@ -13,12 +15,14 @@
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
export let dragging: $$Props['dragging'] = false;
|
||||
export let dragHandle: $$Props['dragHandle'] = undefined;
|
||||
export let xPos: $$Props['xPos'] = 0;
|
||||
export let yPos: $$Props['yPos'] = 0;
|
||||
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
|
||||
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
|
||||
// @todo: there must be a better way to do this
|
||||
id;
|
||||
width;
|
||||
height;
|
||||
data;
|
||||
selected;
|
||||
sourcePosition;
|
||||
@@ -27,7 +31,7 @@
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
xPos;
|
||||
yPos;
|
||||
positionAbsoluteX;
|
||||
positionAbsoluteY;
|
||||
isConnectable;
|
||||
</script>
|
||||
|
||||
@@ -10,28 +10,32 @@
|
||||
|
||||
// unused props - we need to list them here in order to prevent warnings
|
||||
export let id: $$Props['id'] = '';
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let targetPosition: $$Props['targetPosition'] = undefined;
|
||||
export let type: $$Props['type'] = undefined;
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
export let dragging: $$Props['dragging'] = false;
|
||||
export let dragHandle: $$Props['dragHandle'] = undefined;
|
||||
export let xPos: $$Props['xPos'] = 0;
|
||||
export let yPos: $$Props['yPos'] = 0;
|
||||
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
|
||||
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
|
||||
// @todo: there must be a better way to do this
|
||||
id;
|
||||
width;
|
||||
height;
|
||||
selected;
|
||||
targetPosition;
|
||||
type;
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
xPos;
|
||||
yPos;
|
||||
positionAbsoluteX;
|
||||
positionAbsoluteY;
|
||||
isConnectable;
|
||||
</script>
|
||||
|
||||
{data?.label}
|
||||
<Handle type="source" position={sourcePosition} on:connectstart on:connect on:connectend />
|
||||
<Handle type="source" position={sourcePosition} />
|
||||
|
||||
@@ -10,28 +10,32 @@
|
||||
|
||||
// unused props - we need to list them here in order to prevent warnings
|
||||
export let id: $$Props['id'] = '';
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let sourcePosition: $$Props['sourcePosition'] = undefined;
|
||||
export let type: $$Props['type'] = undefined;
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
export let dragging: $$Props['dragging'] = false;
|
||||
export let dragHandle: $$Props['dragHandle'] = undefined;
|
||||
export let xPos: $$Props['xPos'] = 0;
|
||||
export let yPos: $$Props['yPos'] = 0;
|
||||
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
|
||||
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
|
||||
// @todo: there must be a better way to do this
|
||||
id;
|
||||
width;
|
||||
height;
|
||||
selected;
|
||||
sourcePosition;
|
||||
type;
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
xPos;
|
||||
yPos;
|
||||
positionAbsoluteX;
|
||||
positionAbsoluteY;
|
||||
isConnectable;
|
||||
</script>
|
||||
|
||||
{data?.label}
|
||||
<Handle type="target" position={targetPosition} on:connectstart on:connect on:connectend />
|
||||
<Handle type="target" position={targetPosition} />
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
<div class="svelte-flow__nodes">
|
||||
{#each $visibleNodes as node (node.id)}
|
||||
{@const posOrigin = getPositionWithOrigin({
|
||||
x: node.positionAbsolute?.x ?? 0,
|
||||
y: node.positionAbsolute?.y ?? 0,
|
||||
width: (node.size?.width || node.width) ?? 0,
|
||||
height: (node.size?.height || node.height) ?? 0,
|
||||
x: node.computed?.positionAbsolute?.x ?? 0,
|
||||
y: node.computed?.positionAbsolute?.y ?? 0,
|
||||
width: node.computed?.width ?? node.width ?? 0,
|
||||
height: node.computed?.height ?? node.height ?? 0,
|
||||
origin: node.origin
|
||||
})}
|
||||
<NodeWrapper
|
||||
@@ -61,26 +61,28 @@
|
||||
node.connectable ||
|
||||
($nodesConnectable && typeof node.connectable === 'undefined')
|
||||
)}
|
||||
positionAbsolute={node.positionAbsolute}
|
||||
positionOrigin={posOrigin}
|
||||
positionX={node.computed?.positionAbsolute?.x ?? 0}
|
||||
positionY={node.computed?.positionAbsolute?.y ?? 0}
|
||||
positionOriginX={posOrigin.x ?? 0}
|
||||
positionOriginY={posOrigin.y ?? 0}
|
||||
isParent={!!node[internalsSymbol]?.isParent}
|
||||
style={node.style}
|
||||
class={node.class}
|
||||
type={node.type}
|
||||
type={node.type ?? 'default'}
|
||||
sourcePosition={node.sourcePosition}
|
||||
targetPosition={node.targetPosition}
|
||||
dragging={node.dragging}
|
||||
zIndex={node[internalsSymbol]?.z ?? 0}
|
||||
dragHandle={node.dragHandle}
|
||||
initialized={(!!node.width && !!node.height) || (!!node.size?.width && !!node.size?.height)}
|
||||
width={node.width ?? undefined}
|
||||
height={node.height ?? undefined}
|
||||
initialized={(!!node.computed?.width && !!node.computed?.height) ||
|
||||
(!!node.width && !!node.height)}
|
||||
{resizeObserver}
|
||||
on:nodeclick
|
||||
on:nodemouseenter
|
||||
on:nodemousemove
|
||||
on:nodemouseleave
|
||||
on:connectstart
|
||||
on:connect
|
||||
on:connectend
|
||||
on:nodedrag
|
||||
on:nodedragstart
|
||||
on:nodedragstop
|
||||
|
||||
@@ -15,8 +15,14 @@
|
||||
import { Attribution } from '$lib/components/Attribution';
|
||||
import { key, useStore, createStoreContext } from '$lib/store';
|
||||
import type { SvelteFlowProps } from './types';
|
||||
import { updateStore, updateStoreByKeys, type UpdatableStoreProps } from './utils';
|
||||
import {
|
||||
updateStore,
|
||||
updateStoreByKeys,
|
||||
type UpdatableStoreProps,
|
||||
getColorModeClass
|
||||
} from './utils';
|
||||
import { get } from 'svelte/store';
|
||||
import { useColorModeClass } from '$lib/hooks/useColorModeClass';
|
||||
|
||||
type $$Props = SvelteFlowProps;
|
||||
|
||||
@@ -65,11 +71,16 @@
|
||||
export let autoPanOnNodeDrag: $$Props['autoPanOnNodeDrag'] = true;
|
||||
export let onerror: $$Props['onerror'] = undefined;
|
||||
export let ondelete: $$Props['ondelete'] = undefined;
|
||||
export let onedgecreate: $$Props['onedgecreate'] = undefined;
|
||||
export let attributionPosition: $$Props['attributionPosition'] = undefined;
|
||||
export let proOptions: $$Props['proOptions'] = undefined;
|
||||
export let defaultEdgeOptions: $$Props['defaultEdgeOptions'] = undefined;
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let colorMode: $$Props['colorMode'] = 'light';
|
||||
export let onconnect: $$Props['onconnect'] = undefined;
|
||||
export let onconnectstart: $$Props['onconnectstart'] = undefined;
|
||||
export let onconnectend: $$Props['onconnectend'] = undefined;
|
||||
|
||||
export let defaultMarkerColor = '#b1b1b7';
|
||||
|
||||
@@ -142,8 +153,12 @@
|
||||
autoPanOnNodeDrag,
|
||||
onerror,
|
||||
ondelete,
|
||||
onedgecreate,
|
||||
connectionMode,
|
||||
nodeDragThreshold
|
||||
nodeDragThreshold,
|
||||
onconnect,
|
||||
onconnectstart,
|
||||
onconnectend
|
||||
};
|
||||
|
||||
updateStoreByKeys(store, updatableProps);
|
||||
@@ -156,6 +171,8 @@
|
||||
maxZoom,
|
||||
translateExtent
|
||||
});
|
||||
|
||||
$: colorModeClass = useColorModeClass(colorMode);
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -163,7 +180,7 @@
|
||||
bind:clientWidth
|
||||
bind:clientHeight
|
||||
{style}
|
||||
class={cc(['svelte-flow', className])}
|
||||
class={cc(['svelte-flow', className, $colorModeClass])}
|
||||
data-testid="svelte-flow__wrapper"
|
||||
on:dragover
|
||||
on:drop
|
||||
@@ -206,15 +223,12 @@
|
||||
on:nodemouseenter
|
||||
on:nodemousemove
|
||||
on:nodemouseleave
|
||||
on:connectstart
|
||||
on:connect
|
||||
on:connectend
|
||||
on:nodedragstart
|
||||
on:nodedrag
|
||||
on:nodedragstop
|
||||
on:nodecontextmenu
|
||||
/>
|
||||
<NodeSelection />
|
||||
<NodeSelection on:selectionclick on:selectioncontextmenu />
|
||||
</ViewportComponent>
|
||||
<UserSelection />
|
||||
</Pane>
|
||||
|
||||
@@ -14,7 +14,11 @@ import type {
|
||||
OnError,
|
||||
ConnectionMode,
|
||||
PanelPosition,
|
||||
ProOptions
|
||||
ProOptions,
|
||||
ColorMode,
|
||||
OnConnect,
|
||||
OnConnectStart,
|
||||
OnConnectEnd
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
@@ -25,7 +29,8 @@ import type {
|
||||
EdgeTypes,
|
||||
DefaultEdgeOptions,
|
||||
FitViewOptions,
|
||||
OnDelete
|
||||
OnDelete,
|
||||
OnEdgeCreate
|
||||
} from '$lib/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
@@ -75,6 +80,7 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
|
||||
defaultEdgeOptions?: DefaultEdgeOptions;
|
||||
width?: number;
|
||||
height?: number;
|
||||
colorMode?: ColorMode;
|
||||
|
||||
class?: string;
|
||||
style?: string;
|
||||
@@ -87,4 +93,10 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
|
||||
onMoveEnd?: OnMoveEnd;
|
||||
onerror?: OnError;
|
||||
ondelete?: OnDelete;
|
||||
|
||||
onedgecreate?: OnEdgeCreate;
|
||||
|
||||
onconnect?: OnConnect;
|
||||
onconnectstart?: OnConnectStart;
|
||||
onconnectend?: OnConnectEnd;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import type { EdgeTypes, NodeTypes } from '$lib/types';
|
||||
import type { CoordinateExtent } from '@xyflow/system';
|
||||
import type { ColorMode, CoordinateExtent } from '@xyflow/system';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
// this is helper function for updating the store
|
||||
@@ -64,7 +64,11 @@ export type UpdatableStoreProps = {
|
||||
connectionMode?: UnwrapWritable<SvelteFlowStore['connectionMode']>;
|
||||
onerror?: UnwrapWritable<SvelteFlowStore['onerror']>;
|
||||
ondelete?: UnwrapWritable<SvelteFlowStore['ondelete']>;
|
||||
onedgecreate?: UnwrapWritable<SvelteFlowStore['onedgecreate']>;
|
||||
nodeDragThreshold?: UnwrapWritable<SvelteFlowStore['nodeDragThreshold']>;
|
||||
onconnect?: UnwrapWritable<SvelteFlowStore['onconnect']>;
|
||||
onconnectstart?: UnwrapWritable<SvelteFlowStore['onconnectstart']>;
|
||||
onconnectend?: UnwrapWritable<SvelteFlowStore['onconnectend']>;
|
||||
};
|
||||
|
||||
export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStoreProps) {
|
||||
@@ -77,3 +81,15 @@ export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStorePr
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getColorModeClass(colorMode?: ColorMode) {
|
||||
if (colorMode !== 'system') {
|
||||
return colorMode;
|
||||
}
|
||||
|
||||
if (!colorMode || typeof window === 'undefined' || !window.matchMedia) {
|
||||
return 'light';
|
||||
}
|
||||
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { ColorMode, ColorModeClass } from '@xyflow/system';
|
||||
import { readable, type Readable } from 'svelte/store';
|
||||
|
||||
function getMediaQuery() {
|
||||
if (typeof window === 'undefined' || !window.matchMedia) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.matchMedia('(prefers-color-scheme: dark)');
|
||||
}
|
||||
|
||||
export function useColorModeClass(colorMode: ColorMode = 'light'): Readable<ColorModeClass> {
|
||||
const colorModeClass = readable<ColorModeClass>('light', (set) => {
|
||||
if (colorMode !== 'system') {
|
||||
set(colorMode);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaQuery = getMediaQuery();
|
||||
const updateColorModeClass = () => set(mediaQuery?.matches ? 'dark' : 'light');
|
||||
|
||||
set(mediaQuery?.matches ? 'dark' : 'light');
|
||||
mediaQuery?.addEventListener('change', updateColorModeClass);
|
||||
|
||||
return () => {
|
||||
mediaQuery?.removeEventListener('change', updateColorModeClass);
|
||||
};
|
||||
});
|
||||
|
||||
return colorModeClass;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { derived } from 'svelte/store';
|
||||
import { areConnectionMapsEqual, type Connection, type HandleType } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
export type useHandleConnectionsParams = {
|
||||
nodeId: string;
|
||||
type: HandleType;
|
||||
id?: string | null;
|
||||
};
|
||||
|
||||
const initialConnections: Connection[] = [];
|
||||
|
||||
export function useHandleConnections({ nodeId, type, id = null }: useHandleConnectionsParams) {
|
||||
const { edges, connectionLookup } = useStore();
|
||||
let prevConnections: Map<string, Connection> | undefined = undefined;
|
||||
|
||||
return derived(
|
||||
[edges, connectionLookup],
|
||||
([, connectionLookup], set) => {
|
||||
const nextConnections = connectionLookup.get(`${nodeId}-${type}-${id || null}`);
|
||||
|
||||
if (!areConnectionMapsEqual(nextConnections, prevConnections)) {
|
||||
prevConnections = nextConnections;
|
||||
set(Array.from(prevConnections?.values() || []));
|
||||
}
|
||||
},
|
||||
initialConnections
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { derived, type Readable } from 'svelte/store';
|
||||
|
||||
import type { Node } from '$lib/types';
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
function areNodesDataEqual(a: Node['data'][] | null, b: Node['data'][] | null) {
|
||||
if ((!a && !b) || (!a?.length && !b?.length)) {
|
||||
true;
|
||||
}
|
||||
|
||||
if (!a || !b || a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function useNodesData<NodeType extends Node = Node>(
|
||||
nodeId: string
|
||||
): Readable<NodeType['data'] | null>;
|
||||
export function useNodesData<NodeType extends Node = Node>(
|
||||
nodeIds: string[]
|
||||
): Readable<NodeType['data'][]>;
|
||||
export function useNodesData<NodeType extends Node = Node>(
|
||||
nodeIds: string[],
|
||||
guard: (node: Node) => node is NodeType
|
||||
): Readable<NodeType['data'][]>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function useNodesData(nodeIds: any): any {
|
||||
const { nodes, nodeLookup } = useStore();
|
||||
let prevNodesData: (Node['data'] | null)[] | null = null;
|
||||
|
||||
return derived([nodes, nodeLookup], ([, nodeLookup], set) => {
|
||||
let nextNodesData: (Node['data'] | null)[] | null = null;
|
||||
const nodeIdArray = Array.isArray(nodeIds);
|
||||
|
||||
if (!nodeIdArray) {
|
||||
nextNodesData = [nodeLookup.get(nodeIds)?.data || null];
|
||||
} else {
|
||||
const data = [];
|
||||
|
||||
for (const nodeId of nodeIds) {
|
||||
const nodeData = nodeLookup.get(nodeId)?.data;
|
||||
|
||||
if (nodeData) {
|
||||
data.push(nodeData);
|
||||
}
|
||||
}
|
||||
|
||||
nextNodesData = data;
|
||||
}
|
||||
|
||||
if (!areNodesDataEqual(nextNodesData, prevNodesData)) {
|
||||
prevNodesData = nextNodesData;
|
||||
set(nodeIdArray ? nextNodesData : nextNodesData[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { get, type Writable } from 'svelte/store';
|
||||
import {
|
||||
getIncomersBase,
|
||||
getOutgoersBase,
|
||||
getOverlappingArea,
|
||||
isRectObject,
|
||||
nodeToRect,
|
||||
@@ -20,6 +18,7 @@ import {
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { Edge, FitViewOptions, Node } from '$lib/types';
|
||||
import { isNode } from '$lib/utils';
|
||||
|
||||
export function useSvelteFlow(): {
|
||||
zoomIn: ZoomInOut;
|
||||
@@ -48,9 +47,16 @@ export function useSvelteFlow(): {
|
||||
screenToFlowPosition: (position: XYPosition) => XYPosition;
|
||||
flowToScreenPosition: (position: XYPosition) => XYPosition;
|
||||
viewport: Writable<Viewport>;
|
||||
getConnectedEdges: (id: string | (Node | { id: Node['id'] })[]) => Edge[];
|
||||
getIncomers: (node: string | Node | { id: Node['id'] }) => Node[];
|
||||
getOutgoers: (node: string | Node | { id: Node['id'] }) => Node[];
|
||||
updateNode: (
|
||||
id: string,
|
||||
nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
updateNodeData: (
|
||||
id: string,
|
||||
dataUpdate: object | ((node: Node) => object),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
toObject: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
|
||||
} {
|
||||
const {
|
||||
@@ -84,6 +90,24 @@ export function useSvelteFlow(): {
|
||||
return [nodeRect, node, isRect];
|
||||
};
|
||||
|
||||
const updateNode = (
|
||||
id: string,
|
||||
nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>),
|
||||
options: { replace: boolean } = { replace: false }
|
||||
) => {
|
||||
nodes.update((nds) =>
|
||||
nds.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;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
@@ -136,12 +160,12 @@ export function useSvelteFlow(): {
|
||||
) => {
|
||||
const [nodeRect, node, isRect] = getNodeRect(nodeOrRect);
|
||||
|
||||
if (!nodeRect || !node) {
|
||||
if (!nodeRect) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (nodesToIntersect || get(nodes)).filter((n) => {
|
||||
if (!isRect && (n.id === node.id || !n.positionAbsolute)) {
|
||||
if (!isRect && (n.id === node!.id || !n.computed?.positionAbsolute)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -232,29 +256,6 @@ export function useSvelteFlow(): {
|
||||
y: rendererPosition.y + domY
|
||||
};
|
||||
},
|
||||
getConnectedEdges: (node) => {
|
||||
const nodeIds = new Set();
|
||||
|
||||
if (typeof node === 'string') {
|
||||
nodeIds.add(node);
|
||||
} else if (node.length >= 1) {
|
||||
node.forEach((n) => {
|
||||
nodeIds.add(n.id);
|
||||
});
|
||||
}
|
||||
|
||||
return get(edges).filter((edge) => nodeIds.has(edge.source) || nodeIds.has(edge.target));
|
||||
},
|
||||
getIncomers: (node) => {
|
||||
const _node = typeof node === 'string' ? { id: node } : node;
|
||||
|
||||
return getIncomersBase(_node, get(nodes), get(edges));
|
||||
},
|
||||
getOutgoers: (node) => {
|
||||
const _node = typeof node === 'string' ? { id: node } : node;
|
||||
|
||||
return getOutgoersBase(_node, get(nodes), get(edges));
|
||||
},
|
||||
toObject: () => {
|
||||
return {
|
||||
nodes: get(nodes).map((node) => ({
|
||||
@@ -268,6 +269,16 @@ export function useSvelteFlow(): {
|
||||
viewport: { ...get(viewport) }
|
||||
};
|
||||
},
|
||||
updateNode,
|
||||
updateNodeData: (id, dataUpdate, options) => {
|
||||
updateNode(id, (node) => {
|
||||
const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate;
|
||||
|
||||
return options?.replace
|
||||
? { ...node, data: nextData }
|
||||
: { ...node, data: { ...node.data, ...nextData } };
|
||||
});
|
||||
},
|
||||
viewport
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { get } from 'svelte/store';
|
||||
import type { UpdateNodeInternals, NodeDimensionUpdate } from '@xyflow/system';
|
||||
import type { UpdateNodeInternals } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
|
||||
@@ -7,13 +7,14 @@ export * from '$lib/container/Panel';
|
||||
export * from '$lib/components/SvelteFlowProvider';
|
||||
export * from '$lib/components/EdgeLabelRenderer';
|
||||
export * from '$lib/components/BaseEdge';
|
||||
export * from '$lib/components/edges';
|
||||
export { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '$lib/components/edges';
|
||||
export * from '$lib/components/Handle';
|
||||
|
||||
// plugins
|
||||
export * from '$lib/plugins/Controls';
|
||||
export * from '$lib/plugins/Background';
|
||||
export * from '$lib/plugins/Minimap';
|
||||
export * from '$lib/plugins/NodeToolbar';
|
||||
|
||||
// store
|
||||
export { useStore } from '$lib/store';
|
||||
@@ -26,9 +27,20 @@ export * from '$lib/hooks/useSvelteFlow';
|
||||
export * from '$lib/hooks/useUpdateNodeInternals';
|
||||
export * from '$lib/hooks/useConnection';
|
||||
export * from '$lib/hooks/useNodesEdges';
|
||||
export * from '$lib/hooks/useHandleConnections';
|
||||
export * from '$lib/hooks/useNodesData';
|
||||
|
||||
// types
|
||||
export type { Edge, EdgeProps, EdgeTypes, DefaultEdgeOptions } from '$lib/types/edges';
|
||||
export type {
|
||||
Edge,
|
||||
EdgeProps,
|
||||
BezierEdgeProps,
|
||||
SmoothStepEdgeProps,
|
||||
StepEdgeProps,
|
||||
StraightEdgeProps,
|
||||
EdgeTypes,
|
||||
DefaultEdgeOptions
|
||||
} from '$lib/types/edges';
|
||||
export type { HandleComponentProps, FitViewOptions } from '$lib/types/general';
|
||||
export type { Node, NodeTypes, DefaultNodeOptions } from '$lib/types/nodes';
|
||||
export type { SvelteFlowStore } from '$lib/store/types';
|
||||
@@ -74,7 +86,9 @@ export {
|
||||
type Rect,
|
||||
type Box,
|
||||
type Transform,
|
||||
type CoordinateExtent
|
||||
type CoordinateExtent,
|
||||
type ColorMode,
|
||||
type ColorModeClass
|
||||
} from '@xyflow/system';
|
||||
|
||||
// system utils
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
<svg
|
||||
class={cc(['svelte-flow__background', className])}
|
||||
data-testid="svelte-flow__background"
|
||||
style:--background-color-props={bgColor}
|
||||
style:--background-pattern-color-props={patternColor}
|
||||
style:--xy-background-color-props={bgColor}
|
||||
style:--xy-background-pattern-color-props={patternColor}
|
||||
>
|
||||
<pattern
|
||||
id={patternId}
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
type="button"
|
||||
on:click
|
||||
class={cc(['svelte-flow__controls-button', className])}
|
||||
style:--controls-button-background-color-props={bgColor}
|
||||
style:--controls-button-background-color-hover-props={bgColorHover}
|
||||
style:--controls-button-color-props={color}
|
||||
style:--controls-button-color-hover-props={colorHover}
|
||||
style:--controls-button-border-color-props={borderColor}
|
||||
style:--xy-controls-button-background-color-props={bgColor}
|
||||
style:--xy-controls-button-background-color-hover-props={bgColorHover}
|
||||
style:--xy-controls-button-color-props={color}
|
||||
style:--xy-controls-button-color-hover-props={colorHover}
|
||||
style:--xy-controls-button-border-color-props={borderColor}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot class="button-svg" />
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
export let position: $$Props['position'] = 'bottom-right';
|
||||
export let ariaLabel: $$Props['ariaLabel'] = 'Mini map';
|
||||
export let nodeStrokeColor: $$Props['nodeStrokeColor'] = 'transparent';
|
||||
export let nodeColor: $$Props['nodeColor'] = '#e2e2e2';
|
||||
export let nodeColor: $$Props['nodeColor'] = undefined;
|
||||
export let nodeClass: $$Props['nodeClass'] = '';
|
||||
export let nodeBorderRadius: $$Props['nodeBorderRadius'] = 5;
|
||||
export let nodeStrokeWidth: $$Props['nodeStrokeWidth'] = 2;
|
||||
@@ -51,7 +51,7 @@
|
||||
translateExtent
|
||||
} = useStore();
|
||||
|
||||
const nodeColorFunc = getAttrFunction(nodeColor);
|
||||
const nodeColorFunc = nodeColor === undefined ? undefined : getAttrFunction(nodeColor);
|
||||
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
|
||||
const nodeClassFunc = getAttrFunction(nodeClass);
|
||||
const shapeRendering =
|
||||
@@ -94,10 +94,10 @@
|
||||
viewBox="{x} {y} {viewboxWidth} {viewboxHeight}"
|
||||
role="img"
|
||||
aria-labelledby={labelledBy}
|
||||
style:--minimap-background-color-props={bgColor}
|
||||
style:--minimap-mask-color-props={maskColor}
|
||||
style:--minimap-mask-stroke-color-props={maskStrokeColor}
|
||||
style:--minimap-mask-stroke-width-props={maskStrokeWidth}
|
||||
style:--xy-minimap-background-color-props={bgColor}
|
||||
style:--xy-minimap-mask-color-props={maskColor}
|
||||
style:--xy-minimap-mask-stroke-color-props={maskStrokeColor}
|
||||
style:--xy-minimap-mask-stroke-width-props={maskStrokeWidth}
|
||||
use:interactive={{
|
||||
panZoom: $panZoom,
|
||||
viewport,
|
||||
@@ -114,15 +114,15 @@
|
||||
{#if ariaLabel}<title id={labelledBy}>{ariaLabel}</title>{/if}
|
||||
|
||||
{#each $nodes as node (node.id)}
|
||||
{#if node.width && node.height}
|
||||
{#if (node.computed?.width || node?.width) && (node.computed?.height || node.height)}
|
||||
{@const pos = getNodePositionWithOrigin(node).positionAbsolute}
|
||||
<MinimapNode
|
||||
x={pos.x}
|
||||
y={pos.y}
|
||||
width={node.width}
|
||||
height={node.height}
|
||||
width={node.computed?.width ?? node.width ?? 0}
|
||||
height={node.computed?.height ?? node.height ?? 0}
|
||||
selected={node.selected}
|
||||
color={nodeColorFunc(node)}
|
||||
color={nodeColorFunc?.(node)}
|
||||
borderRadius={nodeBorderRadius}
|
||||
strokeColor={nodeStrokeColorFunc(node)}
|
||||
strokeWidth={nodeStrokeWidth}
|
||||
@@ -142,27 +142,3 @@
|
||||
</svg>
|
||||
{/if}
|
||||
</Panel>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
background-color: var(
|
||||
--minimap-background-color-props,
|
||||
var(--minimap-background-color, var(--minimap-background-color-default))
|
||||
);
|
||||
}
|
||||
|
||||
.svelte-flow__minimap-mask {
|
||||
fill: var(
|
||||
--minimap-mask-color-props,
|
||||
var(--minimap-mask-color, var(--minimap-mask-color-default))
|
||||
);
|
||||
stroke: var(
|
||||
--minimap-mask-stroke-color-props,
|
||||
var(--minimap-mask-stroke-color, var(--minimap-mask-stroke-color-default))
|
||||
);
|
||||
stroke-width: var(
|
||||
--minimap-mask-stroke-width-props,
|
||||
var(--minimap-mask-stroke-width, var(--minimap-mask-stroke-width-default))
|
||||
);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
export let width: number = 0;
|
||||
export let height: number = 0;
|
||||
export let borderRadius: number = 5;
|
||||
export let color: string;
|
||||
export let color: string | undefined = undefined;
|
||||
export let shapeRendering: string;
|
||||
export let strokeColor: string;
|
||||
export let strokeColor: string | undefined = undefined;
|
||||
export let strokeWidth: number = 2;
|
||||
export let selected: boolean = false;
|
||||
let className: string = '';
|
||||
@@ -24,8 +24,8 @@
|
||||
ry={borderRadius}
|
||||
{width}
|
||||
{height}
|
||||
fill={color}
|
||||
stroke={strokeColor}
|
||||
stroke-width={strokeWidth}
|
||||
style={`${color ? `fill: ${color};` : ''}${strokeColor ? `stroke: ${strokeColor};` : ''}${
|
||||
strokeWidth ? `stroke-width: ${strokeWidth};` : ''
|
||||
}`}
|
||||
shape-rendering={shapeRendering}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import {
|
||||
getNodesBounds,
|
||||
Position,
|
||||
type Rect,
|
||||
internalsSymbol,
|
||||
getNodeToolbarTransform
|
||||
} from '@xyflow/system';
|
||||
import portal from '$lib/actions/portal';
|
||||
import type { Node } from '$lib/types';
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
import type { NodeToolbarProps } from './types';
|
||||
|
||||
type $$Props = NodeToolbarProps;
|
||||
|
||||
export let nodeId: $$Props['nodeId'] = undefined;
|
||||
export let position: $$Props['position'] = undefined;
|
||||
export let align: $$Props['align'] = undefined;
|
||||
export let offset: $$Props['offset'] = undefined;
|
||||
export let isVisible: $$Props['isVisible'] = undefined;
|
||||
|
||||
const { domNode, viewport, nodeLookup, nodes, nodeOrigin } = useStore();
|
||||
const contextNodeId = getContext<string>('svelteflow__node_id');
|
||||
|
||||
let transform: string;
|
||||
let toolbarNodes: Node[] = [];
|
||||
let _offset = offset !== undefined ? offset : 10;
|
||||
let _position = position !== undefined ? position : Position.Top;
|
||||
let _align = align !== undefined ? align : 'center';
|
||||
|
||||
$: {
|
||||
// $nodes only needed to trigger updates, $nodeLookup is just a helper that does not trigger any updates
|
||||
if ($nodes) {
|
||||
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId];
|
||||
|
||||
toolbarNodes = nodeIds.reduce<Node[]>((res, nodeId) => {
|
||||
const node = $nodeLookup.get(nodeId);
|
||||
|
||||
if (node) {
|
||||
res.push(node);
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
let nodeRect: Rect | undefined = undefined;
|
||||
|
||||
if (toolbarNodes.length === 1) {
|
||||
const toolbarNode = toolbarNodes[0];
|
||||
nodeRect = {
|
||||
...toolbarNode.position,
|
||||
width: toolbarNode.computed?.width ?? toolbarNode.width ?? 0,
|
||||
height: toolbarNode.computed?.height ?? toolbarNode.height ?? 0
|
||||
};
|
||||
} else if (toolbarNodes.length > 1) {
|
||||
nodeRect = getNodesBounds(toolbarNodes, $nodeOrigin);
|
||||
}
|
||||
|
||||
if (nodeRect) {
|
||||
transform = getNodeToolbarTransform(nodeRect, $viewport, _position, _offset, _align);
|
||||
}
|
||||
}
|
||||
|
||||
$: zIndex =
|
||||
toolbarNodes.length === 0
|
||||
? 1
|
||||
: Math.max(...toolbarNodes.map((node) => (node[internalsSymbol]?.z || 5) + 1));
|
||||
|
||||
//FIXME: Possible performance bottleneck
|
||||
$: selectedNodesCount = $nodes.filter((node) => node.selected).length;
|
||||
|
||||
// if isVisible is not set, we show the toolbar only if its node is selected and no other node is selected
|
||||
$: isActive =
|
||||
typeof isVisible === 'boolean'
|
||||
? isVisible
|
||||
: toolbarNodes.length === 1 && toolbarNodes[0].selected && selectedNodesCount === 1;
|
||||
</script>
|
||||
|
||||
{#if $domNode && isActive && toolbarNodes}
|
||||
<div
|
||||
data-id={toolbarNodes.reduce((acc, node) => `${acc}${node.id} `, '').trim()}
|
||||
class="svelte-flow__node-toolbar"
|
||||
use:portal={{ domNode: $domNode }}
|
||||
style:position="absolute"
|
||||
style:transform
|
||||
style:z-index={zIndex}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as NodeToolbar } from './NodeToolbar.svelte';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Position, Align } from '@xyflow/system';
|
||||
|
||||
export type NodeToolbarProps = {
|
||||
nodeId?: string | string[];
|
||||
position?: Position;
|
||||
align?: Align;
|
||||
offset?: number;
|
||||
isVisible?: boolean;
|
||||
};
|
||||
@@ -80,10 +80,12 @@ export function getDerivedConnectionProps(
|
||||
: 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;
|
||||
: (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] : undefined;
|
||||
|
||||
|
||||
@@ -66,22 +66,25 @@ export function createStore({
|
||||
|
||||
const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => {
|
||||
store.nodes.update((nds) => {
|
||||
return nds.map((n) => {
|
||||
return nds.map((node) => {
|
||||
const nodeDragItem = (nodeDragItems as Array<NodeBase | NodeDragItem>).find(
|
||||
(ndi) => ndi.id === n.id
|
||||
(ndi) => ndi.id === node.id
|
||||
);
|
||||
|
||||
if (nodeDragItem) {
|
||||
return {
|
||||
...n,
|
||||
[internalsSymbol]: n[internalsSymbol],
|
||||
...node,
|
||||
dragging,
|
||||
positionAbsolute: nodeDragItem.positionAbsolute,
|
||||
position: nodeDragItem.position
|
||||
position: nodeDragItem.position,
|
||||
computed: {
|
||||
...node.computed,
|
||||
positionAbsolute: nodeDragItem.computed?.positionAbsolute
|
||||
},
|
||||
[internalsSymbol]: node[internalsSymbol]
|
||||
};
|
||||
}
|
||||
|
||||
return n;
|
||||
return node;
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -189,15 +192,20 @@ export function createStore({
|
||||
}
|
||||
|
||||
function unselectNodesAndEdges(params?: { nodes?: Node[]; edges?: Edge[] }) {
|
||||
const nodeIdsToUnselect = (params?.nodes ? params.nodes : get(store.nodes)).map(
|
||||
(item) => item.id
|
||||
);
|
||||
const edgeIdsToUnselect = (params?.edges ? params.edges : get(store.edges)).map(
|
||||
(item) => item.id
|
||||
);
|
||||
const selectedNodeIds = (params?.nodes ? params.nodes : get(store.nodes))
|
||||
.filter((node) => node.selected)
|
||||
.map((node) => node.id);
|
||||
const selectedEdgeIds = (params?.edges ? params.edges : get(store.edges))
|
||||
.filter((edge) => edge.selected)
|
||||
.map((edge) => edge.id);
|
||||
|
||||
store.nodes.update((ns) => ns.map(resetSelectedItem(nodeIdsToUnselect)));
|
||||
store.edges.update((es) => es.map(resetSelectedItem(edgeIdsToUnselect)));
|
||||
if (selectedNodeIds.length) {
|
||||
store.nodes.update((ns) => ns.map(resetSelectedItem(selectedNodeIds)));
|
||||
}
|
||||
|
||||
if (selectedEdgeIds.length) {
|
||||
store.edges.update((es) => es.map(resetSelectedItem(selectedEdgeIds)));
|
||||
}
|
||||
}
|
||||
|
||||
store.deleteKeyPressed.subscribe((deleteKeyPressed) => {
|
||||
|
||||
@@ -14,19 +14,28 @@ import {
|
||||
type OnError,
|
||||
devWarn,
|
||||
type Viewport,
|
||||
updateNodes,
|
||||
adoptUserProvidedNodes,
|
||||
getNodesBounds,
|
||||
getViewportForBounds
|
||||
getViewportForBounds,
|
||||
updateConnectionLookup,
|
||||
type ConnectionLookup,
|
||||
type OnConnect,
|
||||
type OnConnectStart,
|
||||
type OnConnectEnd
|
||||
} from '@xyflow/system';
|
||||
|
||||
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
||||
import InputNode from '$lib/components/nodes/InputNode.svelte';
|
||||
import OutputNode from '$lib/components/nodes/OutputNode.svelte';
|
||||
import GroupNode from '$lib/components/nodes/GroupNode.svelte';
|
||||
import BezierEdge from '$lib/components/edges/BezierEdge.svelte';
|
||||
import StraightEdge from '$lib/components/edges/StraightEdge.svelte';
|
||||
import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte';
|
||||
import StepEdge from '$lib/components/edges/StepEdge.svelte';
|
||||
|
||||
import {
|
||||
BezierEdgeInternal,
|
||||
SmoothStepEdgeInternal,
|
||||
StraightEdgeInternal,
|
||||
StepEdgeInternal
|
||||
} from '$lib/components/edges';
|
||||
|
||||
import type {
|
||||
NodeTypes,
|
||||
EdgeTypes,
|
||||
@@ -34,7 +43,8 @@ import type {
|
||||
Node,
|
||||
Edge,
|
||||
FitViewOptions,
|
||||
OnDelete
|
||||
OnDelete,
|
||||
OnEdgeCreate
|
||||
} from '$lib/types';
|
||||
import { createNodesStore, createEdgesStore } from './utils';
|
||||
import { initConnectionProps, type ConnectionProps } from './derived-connection-props';
|
||||
@@ -47,10 +57,10 @@ export const initialNodeTypes = {
|
||||
};
|
||||
|
||||
export const initialEdgeTypes = {
|
||||
straight: StraightEdge,
|
||||
smoothstep: SmoothStepEdge,
|
||||
default: BezierEdge,
|
||||
step: StepEdge
|
||||
straight: StraightEdgeInternal,
|
||||
smoothstep: SmoothStepEdgeInternal,
|
||||
default: BezierEdgeInternal,
|
||||
step: StepEdgeInternal
|
||||
};
|
||||
|
||||
export const getInitialStore = ({
|
||||
@@ -66,20 +76,17 @@ export const getInitialStore = ({
|
||||
height?: number;
|
||||
fitView?: boolean;
|
||||
}) => {
|
||||
const nodeLookup = new Map<string, Node>();
|
||||
const nextNodes = updateNodes(nodes, nodeLookup, {
|
||||
const nodeLookup = new Map();
|
||||
const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, {
|
||||
nodeOrigin: [0, 0],
|
||||
elevateNodesOnSelect: false
|
||||
});
|
||||
const connectionLookup = updateConnectionLookup(new Map(), edges);
|
||||
|
||||
let viewport: Viewport = { x: 0, y: 0, zoom: 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]);
|
||||
viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
|
||||
}
|
||||
@@ -89,14 +96,15 @@ export const getInitialStore = ({
|
||||
nodes: createNodesStore(nextNodes, nodeLookup),
|
||||
nodeLookup: readable<Map<string, Node>>(nodeLookup),
|
||||
visibleNodes: readable<Node[]>([]),
|
||||
edges: createEdgesStore(edges),
|
||||
edges: createEdgesStore(edges, connectionLookup),
|
||||
visibleEdges: readable<EdgeLayouted[]>([]),
|
||||
connectionLookup: readable<ConnectionLookup>(connectionLookup),
|
||||
height: writable<number>(500),
|
||||
width: writable<number>(500),
|
||||
minZoom: writable<number>(0.5),
|
||||
maxZoom: writable<number>(2),
|
||||
nodeOrigin: writable<NodeOrigin>([0, 0]),
|
||||
nodeDragThreshold: writable<number>(0),
|
||||
nodeDragThreshold: writable<number>(1),
|
||||
nodeExtent: writable<CoordinateExtent>(infiniteExtent),
|
||||
translateExtent: writable<CoordinateExtent>(infiniteExtent),
|
||||
autoPanOnNodeDrag: writable<boolean>(true),
|
||||
@@ -133,6 +141,10 @@ export const getInitialStore = ({
|
||||
lib: readable<string>('svelte'),
|
||||
onlyRenderVisibleElements: writable<boolean>(false),
|
||||
onerror: writable<OnError>(devWarn),
|
||||
ondelete: writable<OnDelete>(undefined)
|
||||
ondelete: writable<OnDelete>(undefined),
|
||||
onedgecreate: writable<OnEdgeCreate>(undefined),
|
||||
onconnect: writable<OnConnect>(undefined),
|
||||
onconnectstart: writable<OnConnectStart>(undefined),
|
||||
onconnectend: writable<OnConnectEnd>(undefined)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,7 +6,13 @@ import {
|
||||
type Writable,
|
||||
get
|
||||
} from 'svelte/store';
|
||||
import { updateNodes, type Viewport, type PanZoomInstance } from '@xyflow/system';
|
||||
import {
|
||||
adoptUserProvidedNodes,
|
||||
updateConnectionLookup,
|
||||
type Viewport,
|
||||
type PanZoomInstance,
|
||||
type ConnectionLookup
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';
|
||||
|
||||
@@ -133,7 +139,7 @@ export const createNodesStore = (
|
||||
let elevateNodesOnSelect = true;
|
||||
|
||||
const _set = (nds: Node[]): Node[] => {
|
||||
const nextNodes = updateNodes(nds, nodeLookup, {
|
||||
const nextNodes = adoptUserProvidedNodes(nds, nodeLookup, {
|
||||
elevateNodesOnSelect,
|
||||
defaults
|
||||
});
|
||||
@@ -168,6 +174,7 @@ export const createNodesStore = (
|
||||
|
||||
export const createEdgesStore = (
|
||||
edges: Edge[],
|
||||
connectionLookup: ConnectionLookup,
|
||||
defaultOptions?: DefaultEdgeOptions
|
||||
): Writable<Edge[]> & { setDefaultOptions: (opts: DefaultEdgeOptions) => void } => {
|
||||
const { subscribe, set, update } = writable<Edge[]>([]);
|
||||
@@ -176,6 +183,9 @@ export const createEdgesStore = (
|
||||
|
||||
const _set: typeof set = (eds: Edge[]) => {
|
||||
const nextEdges = defaults ? eds.map((edge) => ({ ...defaults, ...edge })) : eds;
|
||||
|
||||
updateConnectionLookup(connectionLookup, nextEdges);
|
||||
|
||||
value = nextEdges;
|
||||
set(value);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,9 @@ import type {
|
||||
BezierPathOptions,
|
||||
DefaultEdgeOptionsBase,
|
||||
EdgePosition,
|
||||
SmoothStepPathOptions
|
||||
SmoothStepPathOptions,
|
||||
Optional,
|
||||
StepPathOptions
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { Node } from '$lib/types';
|
||||
@@ -29,6 +31,7 @@ type BezierEdgeType<T> = DefaultEdge<T> & {
|
||||
|
||||
type StepEdgeType<T> = DefaultEdge<T> & {
|
||||
type: 'step';
|
||||
pathOptions?: StepPathOptions;
|
||||
};
|
||||
|
||||
export type Edge<T = any> =
|
||||
@@ -37,7 +40,7 @@ export type Edge<T = any> =
|
||||
| BezierEdgeType<T>
|
||||
| StepEdgeType<T>;
|
||||
|
||||
export type EdgeProps = Omit<Edge, 'sourceHandle' | 'targetHandle'> &
|
||||
export type EdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandle' | 'type'> &
|
||||
EdgePosition & {
|
||||
markerStart?: string;
|
||||
markerEnd?: string;
|
||||
@@ -45,6 +48,31 @@ export type EdgeProps = Omit<Edge, 'sourceHandle' | 'targetHandle'> &
|
||||
targetHandleId?: string | null;
|
||||
};
|
||||
|
||||
export type EdgeComponentProps<T = any> = Optional<
|
||||
Omit<
|
||||
EdgeProps<T>,
|
||||
'source' | 'target' | 'sourceHandleId' | 'targetHandleId' | 'animated' | 'selected' | 'data'
|
||||
>,
|
||||
'id'
|
||||
>;
|
||||
|
||||
export type BezierEdgeProps<T = any> = EdgeComponentProps<T> & {
|
||||
pathOptions?: BezierPathOptions;
|
||||
};
|
||||
|
||||
export type SmoothStepEdgeProps<T = any> = EdgeComponentProps<T> & {
|
||||
pathOptions?: SmoothStepPathOptions;
|
||||
};
|
||||
|
||||
export type StepEdgeProps<T = any> = EdgeComponentProps<T> & {
|
||||
pathOptions?: StepPathOptions;
|
||||
};
|
||||
|
||||
export type StraightEdgeProps<T = any> = Omit<
|
||||
EdgeComponentProps<T>,
|
||||
'sourcePosition' | 'targetPosition'
|
||||
>;
|
||||
|
||||
export type EdgeTypes = Record<string, ComponentType<SvelteComponent<EdgeProps>>>;
|
||||
|
||||
export type DefaultEdgeOptions = Omit<DefaultEdgeOptionsBase<Edge>, 'focusable'>;
|
||||
|
||||
@@ -4,7 +4,8 @@ import type {
|
||||
HandleType,
|
||||
Position,
|
||||
XYPosition,
|
||||
ConnectingHandle
|
||||
ConnectingHandle,
|
||||
Connection
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { Node } from './nodes';
|
||||
@@ -30,8 +31,11 @@ export type HandleComponentProps = {
|
||||
isConnectable?: boolean;
|
||||
isConnectableStart?: boolean;
|
||||
isConnectableEnd?: boolean;
|
||||
onconnect?: (connections: Connection[]) => void;
|
||||
ondisconnect?: (connections: Connection[]) => void;
|
||||
};
|
||||
|
||||
export type FitViewOptions = FitViewOptionsBase<Node>;
|
||||
|
||||
export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void;
|
||||
export type OnEdgeCreate = (connection: Connection) => Edge | Connection | void;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user