Merge pull request #5010 from xyflow/tsdocs-update

TSDoc update
This commit is contained in:
Moritz Klack
2025-02-12 16:49:36 +01:00
committed by GitHub
89 changed files with 1876 additions and 416 deletions

View File

@@ -0,0 +1,7 @@
---
'@xyflow/react': patch
'@xyflow/svelte': patch
'@xyflow/system': patch
---
Add more TSDocs to components, hooks, utils funcs and types

View File

@@ -10,7 +10,6 @@ import {
useEdgesState,
useOnSelectionChange,
OnSelectionChangeParams,
OnSelectionChangeFunc,
} from '@xyflow/react';
const initialNodes: Node[] = [

View File

@@ -90,4 +90,57 @@ function BackgroundComponent({
BackgroundComponent.displayName = 'Background';
/**
* The `<Background />` component makes it convenient to render different types of backgrounds common in node-based UIs. It comes with three variants: lines, dots and cross.
*
* @example
*
* A simple example of how to use the Background component.
*
* ```tsx
* import { useState } from 'react';
* import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react';
*
* export default function Flow() {
* return (
* <ReactFlow defaultNodes={[...]} defaultEdges={[...]}>
* <Background color="#ccc" variant={BackgroundVariant.Dots} />
* </ReactFlow>
* );
* }
* ```
*
* @example
*
* In this example you can see how to combine multiple backgrounds
*
* ```tsx
* import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react';
* import '@xyflow/react/dist/style.css';
*
* export default function Flow() {
* return (
* <ReactFlow defaultNodes={[...]} defaultEdges={[...]}>
* <Background
* id="1"
* gap={10}
* color="#f1f1f1"
* variant={BackgroundVariant.Lines}
* />
* <Background
* id="2"
* gap={100}
* color="#ccc"
* variant={BackgroundVariant.Lines}
* />
* </ReactFlow>
* );
* }
* ```
*
* @remarks
*
* When combining multiple <Background /> components its important to give each of them a unique id prop!
*
*/
export const Background = memo(BackgroundComponent);

View File

@@ -1,11 +1,20 @@
import { CSSProperties } from 'react';
/**
* The three variants are exported as an enum for convenience. You can either import
* the enum and use it like `BackgroundVariant.Lines` or you can use the raw string
* value directly.
* @public
*/
export enum BackgroundVariant {
Lines = 'lines',
Dots = 'dots',
Cross = 'cross',
}
/**
* @expand
*/
export type BackgroundProps = {
id?: string;
/** Color of the pattern */
@@ -24,7 +33,8 @@ export type BackgroundProps = {
offset?: number | [number, number];
/** Line width of the Line pattern */
lineWidth?: number;
/** Variant of the pattern
/**
* Variant of the pattern
* @example BackgroundVariant.Lines, BackgroundVariant.Dots, BackgroundVariant.Cross
* 'lines', 'dots', 'cross'
*/

View File

@@ -2,6 +2,29 @@ import cc from 'classcat';
import type { ControlButtonProps } from './types';
/**
* You can add buttons to the control panel by using the `<ControlButton />` component
* and pass it as a child to the [`<Controls />`](/api-reference/components/controls) component.
*
* @public
* @example
*```jsx
*import { MagicWand } from '@radix-ui/react-icons'
*import { ReactFlow, Controls, ControlButton } from '@xyflow/react'
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[...]} edges={[...]}>
* <Controls>
* <ControlButton onClick={() => alert('Something magical just happened. ✨')}>
* <MagicWand />
* </ControlButton>
* </Controls>
* </ReactFlow>
* )
*}
*```
*/
export function ControlButton({ children, className, ...rest }: ControlButtonProps) {
return (
<button type="button" className={cc(['react-flow__controls-button', className])} {...rest}>

View File

@@ -125,4 +125,25 @@ function ControlsComponent({
ControlsComponent.displayName = 'Controls';
/**
* The `<Controls />` component renders a small panel that contains convenient
* buttons to zoom in, zoom out, fit the view, and lock the viewport.
*
* @public
* @example
*```tsx
*import { ReactFlow, Controls } from '@xyflow/react'
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[...]} edges={[...]}>
* <Controls />
* </ReactFlow>
* )
*}
*```
*
* @remarks To extend or customise the controls, you can use the [`<ControlButton />`](/api-reference/components/control-button) component
*
*/
export const Controls = memo(ControlsComponent);

View File

@@ -3,6 +3,9 @@ import type { PanelPosition } from '@xyflow/system';
import type { FitViewOptions } from '../../types';
/**
* @expand
*/
export type ControlProps = {
/** Show button for zoom in/out */
showZoom?: boolean;
@@ -20,7 +23,8 @@ export type ControlProps = {
onFitView?: () => void;
/** Callback when interactivity is toggled */
onInteractiveChange?: (interactiveStatus: boolean) => void;
/** Position of the controls on the pane
/**
* Position of the controls on the pane
* @example PanelPosition.TopLeft, PanelPosition.TopRight,
* PanelPosition.BottomLeft, PanelPosition.BottomRight
*/
@@ -34,4 +38,7 @@ export type ControlProps = {
orientation?: 'horizontal' | 'vertical';
};
/**
* @expand
*/
export type ControlButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;

View File

@@ -44,8 +44,10 @@ function MiniMapComponent<NodeType extends Node = Node>({
nodeClassName = '',
nodeBorderRadius = 5,
nodeStrokeWidth,
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
// a component properly.
/*
* We need to rename the prop to be `CapitalCase` so that JSX will render it as
* a component properly.
*/
nodeComponent,
bgColor,
maskColor,
@@ -111,16 +113,16 @@ function MiniMapComponent<NodeType extends Node = Node>({
const onSvgClick = onClick
? (event: MouseEvent) => {
const [x, y] = minimapInstance.current?.pointer(event) || [0, 0];
onClick(event, { x, y });
}
const [x, y] = minimapInstance.current?.pointer(event) || [0, 0];
onClick(event, { x, y });
}
: undefined;
const onSvgNodeClick = onNodeClick
? useCallback((event: MouseEvent, nodeId: string) => {
const node = store.getState().nodeLookup.get(nodeId)!;
onNodeClick(event, node);
}, [])
const node = store.getState().nodeLookup.get(nodeId)!;
onNodeClick(event, node);
}, [])
: undefined;
return (
@@ -176,4 +178,24 @@ function MiniMapComponent<NodeType extends Node = Node>({
MiniMapComponent.displayName = 'MiniMap';
/**
* The `<MiniMap />` component can be used to render an overview of your flow. It
* renders each node as an SVG element and visualizes where the current viewport is
* in relation to the rest of the flow.
*
* @public
* @example
*
* ```jsx
*import { ReactFlow, MiniMap } from '@xyflow/react';
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[...]]} edges={[...]]}>
* <MiniMap nodeStrokeWidth={3} />
* </ReactFlow>
* );
*}
*```
*/
export const MiniMap = memo(MiniMapComponent) as typeof MiniMapComponent;

View File

@@ -21,8 +21,10 @@ function MiniMapNodes<NodeType extends Node>({
nodeClassName = '',
nodeBorderRadius = 5,
nodeStrokeWidth,
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
// a component properly.
/*
* We need to rename the prop to be `CapitalCase` so that JSX will render it as
* a component properly.
*/
nodeComponent: NodeComponent = MiniMapNode,
onClick,
}: MiniMapNodesProps<NodeType>) {
@@ -36,11 +38,13 @@ function MiniMapNodes<NodeType extends Node>({
return (
<>
{nodeIds.map((nodeId) => (
// The split of responsibilities between MiniMapNodes and
// NodeComponentWrapper may appear weird. However, its designed to
// minimize the cost of updates when individual nodes change.
//
// For more details, see a similar commit in `NodeRenderer/index.tsx`.
/*
* The split of responsibilities between MiniMapNodes and
* NodeComponentWrapper may appear weird. However, its designed to
* minimize the cost of updates when individual nodes change.
*
* For more details, see a similar commit in `NodeRenderer/index.tsx`.
*/
<NodeComponentWrapper<NodeType>
key={nodeId}
id={nodeId}

View File

@@ -6,6 +6,9 @@ import type { Node } from '../../types';
export type GetMiniMapNodeAttribute<NodeType extends Node = Node> = (node: NodeType) => string;
/**
* @expand
*/
export type MiniMapProps<NodeType extends Node = Node> = Omit<HTMLAttributes<SVGSVGElement>, 'onClick'> & {
/** Color of nodes on minimap */
nodeColor?: string | GetMiniMapNodeAttribute<NodeType>;
@@ -27,7 +30,8 @@ export type MiniMapProps<NodeType extends Node = Node> = Omit<HTMLAttributes<SVG
maskStrokeColor?: string;
/** Stroke width of mask representing viewport */
maskStrokeWidth?: number;
/** Position of minimap on pane
/**
* Position of minimap on pane
* @example PanelPosition.TopLeft, PanelPosition.TopRight,
* PanelPosition.BottomLeft, PanelPosition.BottomRight
*/
@@ -57,6 +61,12 @@ export type MiniMapNodes<NodeType extends Node = Node> = Pick<
onClick?: (event: MouseEvent, nodeId: string) => void;
};
/**
* The props that are passed to the MiniMapNode component
*
* @public
* @expand
*/
export type MiniMapNodeProps = {
id: string;
x: number;

View File

@@ -99,8 +99,10 @@ function ResizeControl({
const parentExpandChanges = handleExpandParent([child], nodeLookup, parentLookup, nodeOrigin);
changes.push(...parentExpandChanges);
// when the parent was expanded by the child node, its position will be clamped at
// 0,0 when node origin is 0,0 and to width, height if it's 1,1
/*
* when the parent was expanded by the child node, its position will be clamped at
* 0,0 when node origin is 0,0 and to width, height if it's 1,1
*/
nextPosition.x = change.x ? Math.max(origin[0] * width, change.x) : undefined;
nextPosition.y = change.y ? Math.max(origin[1] * height, change.y) : undefined;
}
@@ -201,4 +203,9 @@ export function ResizeControlLine(props: ResizeControlLineProps) {
return <ResizeControl {...props} variant={ResizeControlVariant.Line} />;
}
/**
* To create your own resizing UI, you can use the `NodeResizeControl` component where you can pass children (such as icons).
* @public
*
*/
export const NodeResizeControl = memo(ResizeControl);

View File

@@ -3,6 +3,30 @@ import { ResizeControlVariant, XY_RESIZER_HANDLE_POSITIONS, XY_RESIZER_LINE_POSI
import { NodeResizeControl } from './NodeResizeControl';
import type { NodeResizerProps } from './types';
/**
* The `<NodeResizer />` component can be used to add a resize functionality to your
* nodes. It renders draggable controls around the node to resize in all directions.
* @public
*
* @example
*```jsx
*import { memo } from 'react';
*import { Handle, Position, NodeResizer } from '@xyflow/react';
*
*function ResizableNode({ data }) {
* return (
* <>
* <NodeResizer minWidth={100} minHeight={30} />
* <Handle type="target" position={Position.Left} />
* <div style={{ padding: 10 }}>{data.label}</div>
* <Handle type="source" position={Position.Right} />
* </>
* );
*};
*
*export default memo(ResizableNode);
*```
*/
export function NodeResizer({
nodeId,
isVisible = true,

View File

@@ -9,8 +9,12 @@ import type {
OnResizeEnd,
} from '@xyflow/system';
/**
* @expand
*/
export type NodeResizerProps = {
/** Id of the node it is resizing
/**
* Id of the node it is resizing
* @remarks optional if used inside custom node
*/
nodeId?: string;
@@ -46,6 +50,9 @@ export type NodeResizerProps = {
onResizeEnd?: OnResizeEnd;
};
/**
* @expand
*/
export type ResizeControlProps = Pick<
NodeResizerProps,
| 'nodeId'
@@ -60,12 +67,14 @@ export type ResizeControlProps = Pick<
| 'onResize'
| 'onResizeEnd'
> & {
/** Position of the control
/**
* Position of the control
* @example ControlPosition.TopLeft, ControlPosition.TopRight,
* ControlPosition.BottomLeft, ControlPosition.BottomRight
*/
position?: ControlPosition;
/** Variant of the control
/**
* Variant of the control
* @example ResizeControlVariant.Handle, ResizeControlVariant.Line
*/
variant?: ResizeControlVariant;
@@ -74,6 +83,9 @@ export type ResizeControlProps = Pick<
children?: ReactNode;
};
/**
* @expand
*/
export type ResizeControlLineProps = ResizeControlProps & {
position?: ControlLinePosition;
};

View File

@@ -38,6 +38,41 @@ const storeSelector = (state: ReactFlowState) => ({
selectedNodesCount: state.nodes.filter((node) => node.selected).length,
});
/**
* This component can render a toolbar or tooltip to one side of a custom node. This
* toolbar doesn't scale with the viewport so that the content is always visible.
*
* @public
* @example
* ```jsx
*import { memo } from 'react';
*import { Handle, Position, NodeToolbar } from '@xyflow/react';
*
*function CustomNode({ data }) {
* return (
* <>
* <NodeToolbar isVisible={data.toolbarVisible} position={data.toolbarPosition}>
* <button>delete</button>
* <button>copy</button>
* <button>expand</button>
* </NodeToolbar>
*
* <div style={{ padding: '10px 20px' }}>
* {data.label}
* </div>
*
* <Handle type="target" position={Position.Left} />
* <Handle type="source" position={Position.Right} />
* </>
* );
*};
*
*export default memo(CustomNode);
*```
* @remarks By default, the toolbar is only visible when a node is selected. If multiple
* nodes are selected it will not be visible to prevent overlapping toolbars or
* clutter. You can override this behavior by setting the `isVisible` prop to `true`.
*/
export function NodeToolbar({
nodeId,
children,
@@ -74,7 +109,7 @@ export function NodeToolbar({
const isActive =
typeof isVisible === 'boolean'
? isVisible
: nodes.size === 1 && nodes.values().next().value.selected && selectedNodesCount === 1;
: nodes.size === 1 && nodes.values().next().value?.selected && selectedNodesCount === 1;
if (!isActive || !nodes.size) {
return null;

View File

@@ -1,19 +1,24 @@
import type { HTMLAttributes } from 'react';
import type { Position, Align } from '@xyflow/system';
/**
* @expand
*/
export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & {
/** Id of the node, or array of ids the toolbar should be displayed at */
nodeId?: string | string[];
/** If true, node toolbar is visible even if node is not selected */
isVisible?: boolean;
/** Position of the toolbar relative to the node
/**
* Position of the toolbar relative to the node
* @example Position.TopLeft, Position.TopRight,
* Position.BottomLeft, Position.BottomRight
*/
position?: Position;
/** Offset the toolbar from the node */
offset?: number;
/** Align the toolbar relative to the node
/**
* Align the toolbar relative to the node
* @example Align.Start, Align.Center, Align.End
*/
align?: Align;

View File

@@ -30,9 +30,11 @@ export function BatchProvider<NodeType extends Node = Node, EdgeType extends Edg
const nodeQueueHandler = useCallback((queueItems: QueueItem<NodeType>[]) => {
const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState();
// This is essentially an `Array.reduce` in imperative clothing. Processing
// this queue is a relatively hot path so we'd like to avoid the overhead of
// array methods where we can.
/*
* This is essentially an `Array.reduce` in imperative clothing. Processing
* this queue is a relatively hot path so we'd like to avoid the overhead of
* array methods where we can.
*/
let next = nodes as NodeType[];
for (const payload of queueItems) {
next = typeof payload === 'function' ? payload(next) : payload;

View File

@@ -12,21 +12,27 @@ import { Queue, QueueItem } from './types';
* @returns a Queue object
*/
export function useQueue<T>(runQueue: (items: QueueItem<T>[]) => void) {
// Because we're using a ref above, we need some way to let React know when to
// actually process the queue. We increment this number any time we mutate the
// queue, creating a new state to trigger the layout effect below.
// Using a boolean dirty flag here instead would lead to issues related to
// automatic batching. (https://github.com/xyflow/xyflow/issues/4779)
/*
* Because we're using a ref above, we need some way to let React know when to
* actually process the queue. We increment this number any time we mutate the
* queue, creating a new state to trigger the layout effect below.
* Using a boolean dirty flag here instead would lead to issues related to
* automatic batching. (https://github.com/xyflow/xyflow/issues/4779)
*/
const [serial, setSerial] = useState(BigInt(0));
// A reference of all the batched updates to process before the next render. We
// want a reference here so multiple synchronous calls to `setNodes` etc can be
// batched together.
/*
* A reference of all the batched updates to process before the next render. We
* want a reference here so multiple synchronous calls to `setNodes` etc can be
* batched together.
*/
const [queue] = useState(() => createQueue<T>(() => setSerial(n => n + BigInt(1))));
// Layout effects are guaranteed to run before the next render which means we
// shouldn't run into any issues with stale state or weird issues that come from
// rendering things one frame later than expected (we used to use `setTimeout`).
/*
* Layout effects are guaranteed to run before the next render which means we
* shouldn't run into any issues with stale state or weird issues that come from
* rendering things one frame later than expected (we used to use `setTimeout`).
*/
useIsomorphicLayoutEffect(() => {
const queueItems = queue.get();

View File

@@ -6,6 +6,46 @@ import type { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__edgelabel-renderer');
/**
* Edges are SVG-based. If you want to render more complex labels you can use the
* `<EdgeLabelRenderer />` component to access a div based renderer. This component
* is a portal that renders the label in a `<div />` that is positioned on top of
* the edges. You can see an example usage of the component in the [edge label renderer](/examples/edges/edge-label-renderer) example.
* @public
*
* @example
*```jsx
*import React from 'react';
*import { getBezierPath, EdgeLabelRenderer, BaseEdge } from '@xyflow/react';
*
*export function CustomEdge({ id, data, ...props }) {
* const [edgePath, labelX, labelY] = getBezierPath(props);
*
* return (
* <>
* <BaseEdge id={id} path={edgePath} />
* <EdgeLabelRenderer>
* <div
* style={{
* position: 'absolute',
* transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
* background: '#ffcc00',
* padding: 10,
* }}
* className="nodrag nopan"
* >
* {data.label}
* </div>
* </EdgeLabelRenderer>
* </>
* );
*};
*```
*
* @remarks The `<EdgeLabelRenderer />` has no pointer events by default. If you want to
* add mouse interactions you need to set the style `pointerEvents: all` and add
* the `nopan` class on the label or the element you want to interact with.
*/
export function EdgeLabelRenderer({ children }: { children: ReactNode }) {
const edgeLabelRenderer = useStore(selector);

View File

@@ -136,28 +136,28 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
const onEdgeDoubleClick = onDoubleClick
? (event: React.MouseEvent) => {
onDoubleClick(event, { ...edge });
}
onDoubleClick(event, { ...edge });
}
: undefined;
const onEdgeContextMenu = onContextMenu
? (event: React.MouseEvent) => {
onContextMenu(event, { ...edge });
}
onContextMenu(event, { ...edge });
}
: undefined;
const onEdgeMouseEnter = onMouseEnter
? (event: React.MouseEvent) => {
onMouseEnter(event, { ...edge });
}
onMouseEnter(event, { ...edge });
}
: undefined;
const onEdgeMouseMove = onMouseMove
? (event: React.MouseEvent) => {
onMouseMove(event, { ...edge });
}
onMouseMove(event, { ...edge });
}
: undefined;
const onEdgeMouseLeave = onMouseLeave
? (event: React.MouseEvent) => {
onMouseLeave(event, { ...edge });
}
onMouseLeave(event, { ...edge });
}
: undefined;
const onKeyDown = (event: KeyboardEvent) => {

View File

@@ -4,6 +4,33 @@ import cc from 'classcat';
import { EdgeText } from './EdgeText';
import type { BaseEdgeProps } from '../../types';
/**
* The `<BaseEdge />` component gets used internally for all the edges. It can be
* used inside a custom edge and handles the invisible helper edge and the edge label
* for you.
*
* @public
* @example
* ```jsx
*import { BaseEdge } from '@xyflow/react';
*
*export function CustomEdge({ sourceX, sourceY, targetX, targetY, ...props }) {
* const [edgePath] = getStraightPath({
* sourceX,
* sourceY,
* targetX,
* targetY,
* });
*
* return <BaseEdge path={edgePath} {...props} />;
*}
*```
*
* @remarks If you want to use an edge marker with the [`<BaseEdge />`](/api-reference/components/base-edge) component,
* you can pass the `markerStart` or `markerEnd` props passed to your custom edge
* through to the [`<BaseEdge />`](/api-reference/components/base-edge) component.
* You can see all the props passed to a custom edge by looking at the [`EdgeProps`](/api-reference/types/edge-props) type.
*/
export function BaseEdge({
path,
labelX,

View File

@@ -73,4 +73,30 @@ function EdgeTextComponent({
EdgeTextComponent.displayName = 'EdgeText';
/**
* You can use the `<EdgeText />` component as a helper component to display text
* within your custom edges.
*
*@public
*
*@example
*```jsx
*import { EdgeText } from '@xyflow/react';
*
*export function CustomEdgeLabel({ label }) {
* return (
* <EdgeText
* x={100}
* y={100}
* label={label}
* labelStyle={{ fill: 'white' }}
* labelShowBg
* labelBgStyle={{ fill: 'red' }}
* labelBgPadding={[2, 4]}
* labelBgBorderRadius={2}
* />
* );
*}
*```
*/
export const EdgeText = memo(EdgeTextComponent);

View File

@@ -29,6 +29,11 @@ function getControl({ pos, x1, y1, x2, y2 }: GetControlParams): [number, number]
return [x1, 0.5 * (y1 + y2)];
}
/**
* The `getSimpleBezierPath` util returns everything you need to render a simple
* bezier edge between two nodes.
* @public
*/
export function getSimpleBezierPath({
sourceX,
sourceY,

View File

@@ -1,6 +1,8 @@
// 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
/*
* 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';

View File

@@ -28,10 +28,14 @@ import { useNodeId } from '../../contexts/NodeIdContext';
import { type ReactFlowState } from '../../types';
import { fixedForwardRef } from '../../utils';
export interface HandleProps extends HandlePropsSystem, Omit<HTMLAttributes<HTMLDivElement>, 'id'> {
/** Callback called when connection is made */
onConnect?: OnConnect;
}
/**
* @expand
*/
export type HandleProps = HandlePropsSystem &
Omit<HTMLAttributes<HTMLDivElement>, 'id'> & {
/** Callback called when connection is made */
onConnect?: OnConnect;
};
const selector = (s: ReactFlowState) => ({
connectOnClick: s.connectOnClick,
@@ -228,8 +232,10 @@ function HandleComponent(
connectingfrom: connectingFrom,
connectingto: connectingTo,
valid,
// shows where you can start a connection from
// and where you can end it while connecting
/*
* shows where you can start a connection from
* and where you can end it while connecting
*/
connectionindicator:
isConnectable &&
(!connectionInProcess || isPossibleEndHandle) &&
@@ -248,6 +254,28 @@ function HandleComponent(
}
/**
* The Handle component is a UI element that is used to connect nodes.
* The `<Handle />` component is used in your [custom nodes](/learn/customization/custom-nodes)
* to define connection points.
*
*@public
*
*@example
*
*```jsx
*import { Handle, Position } from '@xyflow/react';
*
*export function CustomNode({ data }) {
* return (
* <>
* <div style={{ padding: '10px 20px' }}>
* {data.label}
* </div>
*
* <Handle type="target" position={Position.Left} />
* <Handle type="source" position={Position.Right} />
* </>
* );
*};
*```
*/
export const Handle = memo(fixedForwardRef(HandleComponent));

View File

@@ -108,8 +108,10 @@ export function NodeWrapper<NodeType extends Node>({
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
/*
* 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,

View File

@@ -49,8 +49,10 @@ export function useNodeObserver({
useEffect(() => {
if (nodeRef.current) {
// when the user programmatically changes the source or handle position, we need to update the internals
// to make sure the edges are updated correctly
/*
* when the user programmatically changes the source or handle position, we need to update the internals
* to make sure the edges are updated correctly
*/
const typeChanged = prevType.current !== nodeType;
const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition;
const targetPosChanged = prevTargetPosition.current !== node.targetPosition;

View File

@@ -23,9 +23,9 @@ export const builtinNodeTypes: NodeTypes = {
export function getNodeInlineStyleDimensions<NodeType extends Node = Node>(
node: InternalNode<NodeType>
): {
width: number | string | undefined;
height: number | string | undefined;
} {
width: number | string | undefined;
height: number | string | undefined;
} {
if (node.internals.handleBounds === undefined) {
return {
width: node.width ?? node.initialWidth ?? node.style?.width,

View File

@@ -4,10 +4,12 @@ import { errorMessages } from '@xyflow/system';
import type { ReactFlowState } from '../../types';
// this handler is called by
// 1. the click handler when node is not draggable or selectNodesOnDrag = false
// or
// 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
/*
* this handler is called by
* 1. the click handler when node is not draggable or selectNodesOnDrag = false
* or
* 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
*/
export function handleNodeClick({
id,
store,

View File

@@ -61,9 +61,9 @@ export function NodesSelection<NodeType extends Node>({
const onContextMenu = onSelectionContextMenu
? (event: MouseEvent) => {
const selectedNodes = store.getState().nodes.filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes);
}
const selectedNodes = store.getState().nodes.filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes);
}
: undefined;
const onKeyDown = (event: KeyboardEvent) => {

View File

@@ -5,9 +5,12 @@ import type { PanelPosition } from '@xyflow/system';
import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types';
/**
* @expand
*/
export type PanelProps = HTMLAttributes<HTMLDivElement> & {
/** Set position of the panel
* @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
/**
* The position of the panel
*/
position?: PanelPosition;
children: ReactNode;
@@ -15,6 +18,31 @@ export type PanelProps = HTMLAttributes<HTMLDivElement> & {
const selector = (s: ReactFlowState) => (s.userSelectionActive ? 'none' : 'all');
/**
* The `<Panel />` component helps you position content above the viewport.
* It is used internally by the [`<MiniMap />`](/api-reference/components/minimap)
* and [`<Controls />`](/api-reference/components/controls) components.
*
* @public
*
* @example
* ```jsx
*import { ReactFlow, Background, Panel } from '@xyflow/react';
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[]} fitView>
* <Panel position="top-left">top-left</Panel>
* <Panel position="top-center">top-center</Panel>
* <Panel position="top-right">top-right</Panel>
* <Panel position="bottom-left">bottom-left</Panel>
* <Panel position="bottom-center">bottom-center</Panel>
* <Panel position="bottom-right">bottom-right</Panel>
* </ReactFlow>
* );
*}
*```
*/
export const Panel = forwardRef<HTMLDivElement, PanelProps>(
({ position = 'top-left', children, className, style, ...rest }, ref) => {
const pointerEvents = useStore(selector);
@@ -33,4 +61,4 @@ export const Panel = forwardRef<HTMLDivElement, PanelProps>(
}
);
Panel.displayName = 'Panel'
Panel.displayName = 'Panel';

View File

@@ -19,6 +19,40 @@ export type ReactFlowProviderProps = {
children: ReactNode;
};
/**
* The `<ReactFlowProvider />` component is a [context provider](https://react.dev/learn/passing-data-deeply-with-context#)
* that makes it possible to access a flow's internal state outside of the
* [`<ReactFlow />`](/api-reference/react-flow) component. Many of the hooks we
* provide rely on this component to work.
* @public
*
* @example
* ```tsx
*import { ReactFlow, ReactFlowProvider, useNodes } from '@xyflow/react'
*
*export default function Flow() {
* return (
* <ReactFlowProvider>
* <ReactFlow nodes={...} edges={...} />
* <Sidebar />
* </ReactFlowProvider>
* );
*}
*
*function Sidebar() {
* // This hook will only work if the component it's used in is a child of a
* // <ReactFlowProvider />.
* const nodes = useNodes()
*
* return <aside>do something with nodes</aside>;
*}
*```
*
* @remarks If you're using a router and want your flow's state to persist across routes,
* it's vital that you place the `<ReactFlowProvider />` component _outside_ of
* your router. If you have multiple flows on the same page you will need to use a separate
* `<ReactFlowProvider />` for each flow.
*/
export function ReactFlowProvider({
initialNodes: nodes,
initialEdges: edges,

View File

@@ -94,9 +94,11 @@ const selector = (s: ReactFlowState) => ({
});
const initPrevValues = {
// these are values that are also passed directly to other components
// than the StoreUpdater. We can reduce the number of setStore calls
// by setting the same values here as prev fields.
/*
* these are values that are also passed directly to other components
* than the StoreUpdater. We can reduce the number of setStore calls
* by setting the same values here as prev fields.
*/
translateExtent: infiniteExtent,
nodeOrigin: defaultNodeOrigin,
minZoom: 0.5,

View File

@@ -6,6 +6,31 @@ import type { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__viewport-portal');
/**
* The `<ViewportPortal />` component can be used to add components to the same viewport
* of the flow where nodes and edges are rendered. This is useful when you want to render
* your own components that are adhere to the same coordinate system as the nodes & edges
* and are also affected by zooming and panning
* @public
* @example
*
* ```jsx
*import React from 'react';
*import { ViewportPortal } from '@xyflow/react';
*
*export default function () {
* return (
* <ViewportPortal>
* <div
* style={{ transform: 'translate(100px, 100px)', position: 'absolute' }}
* >
* This div is positioned at [100, 100] on the flow.
* </div>
* </ViewportPortal>
* );
*}
*```
*/
export function ViewportPortal({ children }: { children: ReactNode }) {
const viewPortalDiv = useStore(selector);

View File

@@ -42,9 +42,11 @@ const Marker = ({
);
};
// when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore
// when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper
// that we can then use for creating our unique marker ids
/*
* when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore
* when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper
* that we can then use for creating our unique marker ids
*/
const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => {
const edges = useStore((s) => s.edges);
const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions);

View File

@@ -44,29 +44,31 @@ function NodeRendererComponent<NodeType extends Node>(props: NodeRendererProps<N
<div className="react-flow__nodes" style={containerStyle}>
{nodeIds.map((nodeId) => {
return (
// The split of responsibilities between NodeRenderer and
// NodeComponentWrapper may appear weird. However, its designed to
// minimize the cost of updates when individual nodes change.
//
// For example, when youre 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 youd 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.
/*
* The split of responsibilities between NodeRenderer and
* NodeComponentWrapper may appear weird. However, its designed to
* minimize the cost of updates when individual nodes change.
*
* For example, when youre 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 youd 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<NodeType>
key={nodeId}
id={nodeId}

View File

@@ -233,8 +233,10 @@ export function Pane({
(event.target as Partial<Element>)?.releasePointerCapture?.(event.pointerId);
const { userSelectionRect } = store.getState();
// We only want to trigger click functions when in selection mode if
// the user did not move the mouse.
/*
* We only want to trigger click functions when in selection mode if
* the user did not move the mouse.
*/
if (!userSelectionActive && userSelectionRect && event.target === container.current) {
onClick?.(event);
}
@@ -246,8 +248,10 @@ export function Pane({
});
onSelectionEnd?.(event);
// If the user kept holding the selectionKey during the selection,
// we need to reset the selectionInProgress, so the next click event is not prevented
/*
* If the user kept holding the selectionKey during the selection,
* we need to reset the selectionInProgress, so the next click event is not prevented
*/
if (selectionKeyPressed || selectionOnDrag) {
selectionInProgress.current = false;
}

View File

@@ -31,8 +31,10 @@ export function Wrapper({
const isWrapped = useContext(StoreContext);
if (isWrapped) {
// we need to wrap it with a fragment because it's not allowed for children to be a ReactNode
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051
/*
* we need to wrap it with a fragment because it's not allowed for children to be a ReactNode
* https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051
*/
return <>{children}</>;
}

View File

@@ -312,4 +312,24 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
);
}
/**
* The `<ReactFlow />` component is the heart of your React Flow application.
* It renders your nodes and edges and handles user interaction
*
* @public
*
* @example
* ```tsx
*import { ReactFlow } from '@xyflow/react'
*
*export default function Flow() {
* return (<ReactFlow
* nodes={...}
* edges={...}
* onNodesChange={...}
* ...
* />);
*}
*```
*/
export default fixedForwardRef(ReactFlow);

View File

@@ -4,6 +4,34 @@ export const NodeIdContext = createContext<string | null>(null);
export const Provider = NodeIdContext.Provider;
export const Consumer = NodeIdContext.Consumer;
/**
* You can use this hook to get the id of the node it is used inside. It is useful
* if you need the node's id deeper in the render tree but don't want to manually
* drill down the id as a prop.
*
* @public
* @returns id of the node
*
* @example
*```jsx
*import { useNodeId } from '@xyflow/react';
*
*export default function CustomNode() {
* return (
* <div>
* <span>This node has an id of </span>
* <NodeIdDisplay />
* </div>
* );
*}
*
*function NodeIdDisplay() {
* const nodeId = useNodeId();
*
* return <span>{nodeId}</span>;
*}
*```
*/
export const useNodeId = (): string | null => {
const nodeId = useContext(NodeIdContext);
return nodeId;

View File

@@ -24,9 +24,28 @@ function getSelector<NodeType extends Node = Node, SelectorReturn = ConnectionSt
return storeSelector;
}
/**
* Hook for accessing the connection state.
* The `useConnection` hook returns the current connection when there is an active
* connection interaction. If no connection interaction is active, it returns null
* for every property. A typical use case for this hook is to colorize handles
* based on a certain condition (e.g. if the connection is valid or not).
*
* @public
* @example
*
* ```tsx
*import { useConnection } from '@xyflow/react';
*
*function App() {
* const connection = useConnection();
*
* return (
* <div> {connection ? `Someone is trying to make a connection from ${connection.fromNode} to this one.` : 'There are currently no incoming connections!'}
*
* </div>
* );
* }
* ```
*
* @returns ConnectionState
*/
export function useConnection<NodeType extends Node = Node, SelectorReturn = ConnectionState<InternalNode<NodeType>>>(

View File

@@ -6,10 +6,22 @@ import type { Edge, ReactFlowState } from '../types';
const edgesSelector = (state: ReactFlowState) => state.edges;
/**
* Hook for getting the current edges from the store.
* This hook returns an array of the current edges. Components that use this hook
* will re-render **whenever any edge changes**.
*
* @public
* @returns An array of edges
*
* @example
* ```tsx
*import { useEdges } from '@xyflow/react';
*
*export default function () {
* const edges = useEdges();
*
* return <div>There are currently {edges.length} edges!</div>;
*}
*```
*/
export function useEdges<EdgeType extends Edge = Edge>(): EdgeType[] {
const edges = useStore(edgesSelector, shallow) as EdgeType[];

View File

@@ -5,11 +5,31 @@ import { useStore } from './useStore';
import type { InternalNode, Node } from '../types';
/**
* Hook for getting an internal node by id
* This hook returns the internal representation of a specific node.
* Components that use this hook will re-render **whenever the node changes**,
* including when a node is selected or moved.
*
* @public
* @param id - id of the node
* @returns array with visible node ids
*
* @example
* ```tsx
*import { useInternalNode } from '@xyflow/react';
*
*export default function () {
* const internalNode = useInternalNode('node-1');
* const absolutePosition = internalNode.internals.positionAbsolute;
*
* return (
* <div>
* The absolute position of the node is at:
* <p>x: {absolutePosition.x}</p>
* <p>y: {absolutePosition.y}</p>
* </div>
* );
*}
*```
*/
export function useInternalNode<NodeType extends Node = Node>(id: string): InternalNode<NodeType> | undefined {
const node = useStore(

View File

@@ -13,18 +13,38 @@ export type UseKeyPressOptions = {
const defaultDoc = typeof document !== 'undefined' ? document : null;
/**
* Hook for handling key events.
* This hook lets you listen for specific key codes and tells you whether they are
* currently pressed or not.
*
* @public
* @param param.keyCode - The key code (string or array of strings) to use
* @param param.options - Options
* @returns boolean
*
* @example
* ```tsx
*import { useKeyPress } from '@xyflow/react';
*
*export default function () {
* const spacePressed = useKeyPress('Space');
* const cmdAndSPressed = useKeyPress(['Meta+s', 'Strg+s']);
*
* return (
* <div>
* {spacePressed && <p>Space pressed!</p>}
* {cmdAndSPressed && <p>Cmd + S pressed!</p>}
* </div>
* );
*}
*```
*/
export function useKeyPress(
// the keycode can be a string 'a' or an array of strings ['a', 'a+d']
// a string means a single key 'a' or a combination when '+' is used 'a+d'
// an array means different possibilites. Explainer: ['a', 'd+s'] here the
// user can use the single key 'a' or the combination 'd' + 's'
/*
* the keycode can be a string 'a' or an array of strings ['a', 'a+d']
* a string means a single key 'a' or a combination when '+' is used 'a+d'
* an array means different possibilites. Explainer: ['a', 'd+s'] here the
* user can use the single key 'a' or the combination 'd' + 's'
*/
keyCode: KeyCode | null = null,
options: UseKeyPressOptions = { target: defaultDoc, actInsideInputWithModifier: true }
): boolean {
@@ -36,20 +56,24 @@ export function useKeyPress(
// we need to remember the pressed keys in order to support combinations
const pressedKeys = useRef<PressedKeys>(new Set([]));
// keyCodes = array with single keys [['a']] or key combinations [['a', 's']]
// keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft']
// used to check if we store event.code or event.key. When the code is in the list of keysToWatch
// we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft"
// and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when
// we can't find it in the list of keysToWatch.
/*
* keyCodes = array with single keys [['a']] or key combinations [['a', 's']]
* keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft']
* used to check if we store event.code or event.key. When the code is in the list of keysToWatch
* we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft"
* and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when
* we can't find it in the list of keysToWatch.
*/
const [keyCodes, keysToWatch] = useMemo<[Array<Keys>, Keys]>(() => {
if (keyCode !== null) {
const keyCodeArr = Array.isArray(keyCode) ? keyCode : [keyCode];
const keys = keyCodeArr
.filter((kc) => typeof kc === 'string')
// we first replace all '+' with '\n' which we will use to split the keys on
// then we replace '\n\n' with '\n+', this way we can also support the combination 'key++'
// in the end we simply split on '\n' to get the key array
/*
* we first replace all '+' with '\n' which we will use to split the keys on
* then we replace '\n\n' with '\n+', this way we can also support the combination 'key++'
* in the end we simply split on '\n' to get the key array
*/
.map((kc) => kc.replace('+', '\n').replace('\n\n', '\n+').split('\n'));
const keysFlat = keys.reduce((res: Keys, item) => res.concat(...item), []);
@@ -133,12 +157,16 @@ export function useKeyPress(
function isMatchingKey(keyCodes: Array<Keys>, pressedKeys: PressedKeys, isUp: boolean): boolean {
return (
keyCodes
// we only want to compare same sizes of keyCode definitions
// and pressed keys. When the user specified 'Meta' as a key somewhere
// this would also be truthy without this filter when user presses 'Meta' + 'r'
/*
* we only want to compare same sizes of keyCode definitions
* and pressed keys. When the user specified 'Meta' as a key somewhere
* this would also be truthy without this filter when user presses 'Meta' + 'r'
*/
.filter((keys) => isUp || keys.length === pressedKeys.size)
// since we want to support multiple possibilities only one of the
// combinations need to be part of the pressed keys
/*
* since we want to support multiple possibilities only one of the
* combinations need to be part of the pressed keys
*/
.some((keys) => keys.every((k) => pressedKeys.has(k)))
);
}

View File

@@ -22,8 +22,10 @@ export function useMoveSelectedNodes() {
const nodeUpdates = new Map();
const isSelected = selectedAndDraggable(nodesDraggable);
// by default a node moves 5px on each key press
// if snap grid is enabled, we use that for the velocity
/*
* by default a node moves 5px on each key press
* if snap grid is enabled, we use that for the velocity
*/
const xVelo = snapToGrid ? snapGrid[0] : 5;
const yVelo = snapToGrid ? snapGrid[1] : 5;

View File

@@ -22,7 +22,7 @@ type UseNodeConnectionsParams = {
};
/**
* Hook to retrieve all edges connected to a node. Can be filtered by handle type and id.
* This hook returns an array of connections on a specific node, handle type ('source', 'target') or handle ID.
*
* @public
* @param param.id - node id - optional if called inside a custom node
@@ -31,6 +31,22 @@ type UseNodeConnectionsParams = {
* @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
*
* @example
* ```jsx
*import { useNodeConnections } from '@xyflow/react';
*
*export default function () {
* const connections = useNodeConnections({
* type: 'target',
* handleId: 'my-handle',
* });
*
* return (
* <div>There are currently {connections.length} incoming connections!</div>
* );
*}
*```
*/
export function useNodeConnections({
id,

View File

@@ -6,10 +6,23 @@ import type { Node, ReactFlowState } from '../types';
const nodesSelector = (state: ReactFlowState) => state.nodes;
/**
* Hook for getting the current nodes from the store.
* This hook returns an array of the current nodes. Components that use this hook
* will re-render **whenever any node changes**, including when a node is selected
* or moved.
*
* @public
* @returns An array of nodes
*
* @example
* ```jsx
*import { useNodes } from '@xyflow/react';
*
*export default function() {
* const nodes = useNodes();
*
* return <div>There are currently {nodes.length} nodes!</div>;
*}
*```
*/
export function useNodes<NodeType extends Node = Node>(): NodeType[] {
const nodes = useStore(nodesSelector, shallow) as NodeType[];

View File

@@ -5,12 +5,24 @@ import { useStore } from '../hooks/useStore';
import type { Node } from '../types';
/**
* Hook for receiving data of one or multiple nodes
* This hook lets you subscribe to changes of a specific nodes `data` object.
*
* @public
* @param nodeId - The id (or ids) of the node to get the data from
* @param guard - Optional guard function to narrow down the node type
* @returns An object (or array of object) with {id, type, data} representing each node
*
* @example
*
*```jsx
*import { useNodesData } from '@xyflow/react';
*
*export default function() {
* const nodeData = useNodesData('nodeId-1');
* const nodesData = useNodesData(['nodeId-1', 'nodeId-2']);
*
* return null;
*}
*```
*/
export function useNodesData<NodeType extends Node = Node>(
nodeId: string

View File

@@ -4,11 +4,41 @@ import { applyNodeChanges, applyEdgeChanges } from '../utils/changes';
import type { Node, Edge, OnNodesChange, OnEdgesChange } from '../types';
/**
* Hook for managing the state of nodes - should only be used for prototyping / simple use cases.
* This hook makes it easy to prototype a controlled flow where you manage the
* state of nodes and edges outside the `ReactFlowInstance`. You can think of it
* like React's `useState` hook with an additional helper callback.
*
* @public
* @param initialNodes
* @returns an array [nodes, setNodes, onNodesChange]
* @example
*
*```tsx
*import { ReactFlow, useNodesState, useEdgesState } from '@xyflow/react';
*
*const initialNodes = [];
*const initialEdges = [];
*
*export default function () {
* const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
* const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
*
* return (
* <ReactFlow
* nodes={nodes}
* edges={edges}
* onNodesChange={onNodesChange}
* onEdgesChange={onEdgesChange}
* />
* );
*}
*```
*
* @remarks This hook was created to make prototyping easier and our documentation
* examples clearer. Although it is OK to use this hook in production, in
* practice you may want to use a more sophisticated state management solution
* like Zustand {@link https://reactflow.dev/docs/guides/state-management/} instead.
*
*/
export function useNodesState<NodeType extends Node>(
initialNodes: NodeType[]
@@ -23,11 +53,41 @@ export function useNodesState<NodeType extends Node>(
}
/**
* Hook for managing the state of edges - should only be used for prototyping / simple use cases.
* This hook makes it easy to prototype a controlled flow where you manage the
* state of nodes and edges outside the `ReactFlowInstance`. You can think of it
* like React's `useState` hook with an additional helper callback.
*
* @public
* @param initialEdges
* @returns an array [edges, setEdges, onEdgesChange]
* @example
*
*```tsx
*import { ReactFlow, useNodesState, useEdgesState } from '@xyflow/react';
*
*const initialNodes = [];
*const initialEdges = [];
*
*export default function () {
* const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
* const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
*
* return (
* <ReactFlow
* nodes={nodes}
* edges={edges}
* onNodesChange={onNodesChange}
* onEdgesChange={onEdgesChange}
* />
* );
*}
*```
*
* @remarks This hook was created to make prototyping easier and our documentation
* examples clearer. Although it is OK to use this hook in production, in
* practice you may want to use a more sophisticated state management solution
* like Zustand {@link https://reactflow.dev/docs/guides/state-management/} instead.
*
*/
export function useEdgesState<EdgeType extends Edge = Edge>(
initialEdges: EdgeType[]

View File

@@ -1,6 +1,7 @@
import { nodeHasDimensions } from '@xyflow/system';
import { useStore } from './useStore';
import type { ReactFlowState } from '../types';
import { nodeHasDimensions } from '@xyflow/system';
export type UseNodesInitializedOptions = {
includeHiddenNodes?: boolean;
@@ -22,18 +23,44 @@ const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) =>
return true;
};
const defaultOptions = {
includeHiddenNodes: false,
};
/**
* Hook which returns true when all nodes are initialized.
* This hook tells you whether all the nodes in a flow have been measured and given
*a width and height. When you add a node to the flow, this hook will return
*`false` and then `true` again once the node has been measured.
*
* @public
* @param options.includeHiddenNodes - defaults to false
* @returns boolean indicating whether all nodes are initialized
*
* @example
* ```jsx
*import { useReactFlow, useNodesInitialized } from '@xyflow/react';
*import { useEffect, useState } from 'react';
*
*const options = {
* includeHiddenNodes: false,
*};
*
*export default function useLayout() {
* const { getNodes } = useReactFlow();
* const nodesInitialized = useNodesInitialized(options);
* const [layoutedNodes, setLayoutedNodes] = useState(getNodes());
*
* useEffect(() => {
* if (nodesInitialized) {
* setLayoutedNodes(yourLayoutingFunction(getNodes()));
* }
* }, [nodesInitialized]);
*
* return layoutedNodes;
*}
*```
*/
export function useNodesInitialized(options: UseNodesInitializedOptions = defaultOptions): boolean {
export function useNodesInitialized(
options: UseNodesInitializedOptions = {
includeHiddenNodes: false,
}
): boolean {
const initialized = useStore(selector(options));
return initialized;

View File

@@ -8,10 +8,42 @@ export type UseOnSelectionChangeOptions = {
};
/**
* Hook for registering an onSelectionChange handler.
* This hook lets you listen for changes to both node and edge selection. As the
*name implies, the callback you provide will be called whenever the selection of
*_either_ nodes or edges changes.
*
* @public
* @param params.onChange - The handler to register
*
* @example
* ```jsx
*import { useState } from 'react';
*import { ReactFlow, useOnSelectionChange } from '@xyflow/react';
*
*function SelectionDisplay() {
* const [selectedNodes, setSelectedNodes] = useState([]);
* const [selectedEdges, setSelectedEdges] = useState([]);
*
* // the passed handler has to be memoized, otherwise the hook will not work correctly
* const onChange = useCallback(({ nodes, edges }) => {
* setSelectedNodes(nodes.map((node) => node.id));
* setSelectedEdges(edges.map((edge) => edge.id));
* }, []);
*
* useOnSelectionChange({
* onChange,
* });
*
* return (
* <div>
* <p>Selected nodes: {selectedNodes.join(', ')}</p>
* <p>Selected edges: {selectedEdges.join(', ')}</p>
* </div>
* );
*}
*```
*
* @remarks You need to memoize the passed `onChange` handler, otherwise the hook will not work correctly.
*/
export function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) {
const store = useStoreApi();

View File

@@ -10,12 +10,30 @@ export type UseOnViewportChangeOptions = {
};
/**
* Hook for registering an onViewportChange handler.
* The `useOnViewportChange` hook lets you listen for changes to the viewport such
*as panning and zooming. You can provide a callback for each phase of a viewport
*change: `onStart`, `onChange`, and `onEnd`.
*
* @public
* @param params.onStart - gets called when the viewport starts changing
* @param params.onChange - gets called when the viewport changes
* @param params.onEnd - gets called when the viewport stops changing
*
* @example
* ```jsx
*import { useCallback } from 'react';
*import { useOnViewportChange } from '@xyflow/react';
*
*function ViewportChangeLogger() {
* useOnViewportChange({
* onStart: (viewport: Viewport) => console.log('start', viewport),
* onChange: (viewport: Viewport) => console.log('change', viewport),
* onEnd: (viewport: Viewport) => console.log('end', viewport),
* });
*
* return null;
*}
*```
*/
export function useOnViewportChange({ onStart, onChange, onEnd }: UseOnViewportChangeOptions) {
const store = useStoreApi();

View File

@@ -20,10 +20,33 @@ import type { ReactFlowInstance, Node, Edge, InternalNode, ReactFlowState, Gener
const selector = (s: ReactFlowState) => !!s.panZoom;
/**
* Hook for accessing the ReactFlow instance.
* This hook returns a ReactFlowInstance that can be used to update nodes and edges, manipulate the viewport, or query the current state of the flow.
*
* @public
* @returns ReactFlowInstance
*
* @example
* ```jsx
*import { useCallback, useState } from 'react';
*import { useReactFlow } from '@xyflow/react';
*
*export function NodeCounter() {
* const reactFlow = useReactFlow();
* const [count, setCount] = useState(0);
* const countNodes = useCallback(() => {
* setCount(reactFlow.getNodes().length);
* // you need to pass it as a dependency if you are using it with useEffect or useCallback
* // because at the first render, it's not initialized yet and some functions might not work.
* }, [reactFlow]);
*
* return (
* <div>
* <button onClick={countNodes}>Update count</button>
* <p>There are {count} nodes in the flow.</p>
* </div>
* );
*}
*```
*/
export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(): ReactFlowInstance<
NodeType,

View File

@@ -9,7 +9,9 @@ import type { Edge, Node, ReactFlowState } from '../types';
const zustandErrorMessage = errorMessages['error001']();
/**
* Hook for accessing the internal store. Should only be used in rare cases.
* This hook can be used to subscribe to internal state changes of the React Flow
* component. The `useStore` hook is re-exported from the [Zustand](https://github.com/pmndrs/zustand)
* state management library, so you should check out their docs for more details.
*
* @public
* @param selector
@@ -17,8 +19,13 @@ const zustandErrorMessage = errorMessages['error001']();
* @returns The selected state slice
*
* @example
* const nodes = useStore((state: ReactFlowState<MyNodeType>) => state.nodes);
* ```ts
* const nodes = useStore((state) => state.nodes);
* ```
*
* @remarks This hook should only be used if there is no other way to access the internal
* state. For many of the common use cases, there are dedicated hooks available
* such as {@link useReactFlow}, {@link useViewport}, etc.
*/
function useStore<StateSlice = unknown>(
selector: (state: ReactFlowState) => StateSlice,
@@ -33,6 +40,20 @@ function useStore<StateSlice = unknown>(
return useZustandStore(store, selector, equalityFn);
}
/**
* In some cases, you might need to access the store directly. This hook returns the store object which can be used on demand to access the state or dispatch actions.
*
* @returns The store object
*
* @example
* ```ts
* const store = useStoreApi();
* ```
*
* @remarks This hook should only be used if there is no other way to access the internal
* state. For many of the common use cases, there are dedicated hooks available
* such as {@link useReactFlow}, {@link useViewport}, etc.
*/
function useStoreApi<NodeType extends Node = Node, EdgeType extends Edge = Edge>() {
const store = useContext(StoreContext) as UseBoundStoreWithEqualityFn<
StoreApi<ReactFlowState<NodeType, EdgeType>>

View File

@@ -4,10 +4,48 @@ import type { UpdateNodeInternals, InternalNodeUpdate } from '@xyflow/system';
import { useStoreApi } from '../hooks/useStore';
/**
* Hook for updating node internals.
* When you programmatically add or remove handles to a node or update a node's
*handle position, you need to let React Flow know about it using this hook. This
*will update the internal dimensions of the node and properly reposition handles
*on the canvas if necessary.
*
* @public
* @returns function for updating node internals
*
* @example
* ```jsx
*import { useCallback, useState } from 'react';
*import { Handle, useUpdateNodeInternals } from '@xyflow/react';
*
*export default function RandomHandleNode({ id }) {
* const updateNodeInternals = useUpdateNodeInternals();
* const [handleCount, setHandleCount] = useState(0);
* const randomizeHandleCount = useCallback(() => {
* setHandleCount(Math.floor(Math.random() * 10));
* updateNodeInternals(id);
* }, [id, updateNodeInternals]);
*
* return (
* <>
* {Array.from({ length: handleCount }).map((_, index) => (
* <Handle
* key={index}
* type="target"
* position="left"
* id={`handle-${index}`}
* />
* ))}
*
* <div>
* <button onClick={randomizeHandleCount}>Randomize handle count</button>
* <p>There are {handleCount} handles on this node.</p>
* </div>
* </>
* );
*}
*```
* @remarks This hook can only be used in a component that is a child of a
*{@link ReactFlowProvider} or a {@link ReactFlow} component.
*/
export function useUpdateNodeInternals(): UpdateNodeInternals {
const store = useStoreApi();

View File

@@ -11,10 +11,33 @@ const viewportSelector = (state: ReactFlowState) => ({
});
/**
* Hook for getting the current viewport from the store.
* The `useViewport` hook is a convenient way to read the current state of the
*{@link Viewport} in a component. Components that use this hook
*will re-render **whenever the viewport changes**.
*
* @public
* @returns The current viewport
*
* @example
*
*```jsx
*import { useViewport } from '@xyflow/react';
*
*export default function ViewportDisplay() {
* const { x, y, zoom } = useViewport();
*
* return (
* <div>
* <p>
* The viewport is currently at ({x}, {y}) and zoomed to {zoom}.
* </p>
* </div>
* );
*}
*```
*
* @remarks This hook can only be used in a component that is a child of a
*{@link ReactFlowProvider} or a {@link ReactFlow} component.
*/
export function useViewport(): Viewport {
const viewport = useStore(viewportSelector, shallow);

View File

@@ -8,8 +8,8 @@ import type { Node, ReactFlowState } from '../types';
const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
return onlyRenderVisible
? getNodesInside<Node>(s.nodeLookup, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
(node) => node.id
)
(node) => node.id
)
: Array.from(s.nodeLookup.keys());
};

View File

@@ -47,12 +47,14 @@ const createStore = ({
...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent, defaultNodes, defaultEdges }),
setNodes: (nodes: Node[]) => {
const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get();
// 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.
/*
* 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.
*/
adoptUserNodes(nodes, nodeLookup, parentLookup, {
nodeOrigin,
nodeExtent,
@@ -81,9 +83,11 @@ const createStore = ({
set({ hasDefaultEdges: true });
}
},
// Every node gets registerd at a ResizeObserver. Whenever a node
// changes its dimensions, this function is called to measure the
// new dimensions and update the nodes.
/*
* Every node gets registerd at a ResizeObserver. Whenever a node
* changes its dimensions, this function is called to measure the
* new dimensions and update the nodes.
*/
updateNodeInternals: (updates, params = { triggerFitView: true }) => {
const {
triggerNodeChanges,
@@ -125,11 +129,13 @@ const createStore = ({
});
}
// here we are cirmumventing the onNodesChange handler
// in order to be able to display nodes even if the user
// has not provided an onNodesChange handler.
// Nodes are only rendered if they have a width and height
// attribute which they get from this handler.
/*
* here we are cirmumventing the onNodesChange handler
* in order to be able to display nodes even if the user
* has not provided an onNodesChange handler.
* Nodes are only rendered if they have a width and height
* attribute which they get from this handler.
*/
set({ fitViewDone: nextFitViewDone });
} else {
// we always want to trigger useStore calls whenever updateNodeInternals is called
@@ -155,9 +161,9 @@ const createStore = ({
type: 'position',
position: expandParent
? {
x: Math.max(0, dragItem.position.x),
y: Math.max(0, dragItem.position.y),
}
x: Math.max(0, dragItem.position.x),
y: Math.max(0, dragItem.position.y),
}
: dragItem.position,
dragging,
};
@@ -248,8 +254,10 @@ const createStore = ({
const nodeChanges = nodesToUnselect.map((n) => {
const internalNode = nodeLookup.get(n.id);
if (internalNode) {
// we need to unselect the internal node that was selected previously before we
// send the change to the user to prevent it to be selected while dragging the new node
/*
* we need to unselect the internal node that was selected previously before we
* send the change to the user to prevent it to be selected while dragging the new node
*/
internalNode.selected = false;
}
@@ -342,8 +350,10 @@ const createStore = ({
options
);
},
// we can't call an asnychronous function in updateNodeInternals
// for that we created this sync version of fitView
/*
* we can't call an asnychronous function in updateNodeInternals
* for that we created this sync version of fitView
*/
fitViewSync: (options?: FitViewOptions): boolean => {
const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get();

View File

@@ -52,7 +52,8 @@ import type {
*/
export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends Edge = Edge>
extends Omit<HTMLAttributes<HTMLDivElement>, 'onError'> {
/** An array of nodes to render in a controlled flow.
/**
* An array of nodes to render in a controlled flow.
* @example
* const nodes = [
* {
@@ -64,7 +65,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* ];
*/
nodes?: NodeType[];
/** An array of edges to render in a controlled flow.
/**
* An array of edges to render in a controlled flow.
* @example
* const edges = [
* {
@@ -79,7 +81,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
defaultNodes?: NodeType[];
/** The initial edges to render in an uncontrolled flow. */
defaultEdges?: EdgeType[];
/** Defaults to be applied to all new edges that are added to the flow.
/**
* Defaults to be applied to all new edges that are added to the flow.
*
* Properties on a new edge will override these defaults if they exist.
* @example
@@ -132,7 +135,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
onReconnect?: OnReconnect<EdgeType>;
onReconnectStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void;
onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void;
/** This event handler is called when a Node is updated
/**
* This event handler is called when a Node is updated
* @example // Use NodesState hook to create edges and get onNodesChange handler
* import ReactFlow, { useNodesState } from '@xyflow/react';
* const [edges, setNodes, onNodesChange] = useNodesState(initialNodes);
@@ -149,7 +153,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* return (<ReactFlow onNodeChange={onNodeChange} {...rest} />)
*/
onNodesChange?: OnNodesChange<NodeType>;
/** This event handler is called when a Edge is updated
/**
* This event handler is called when a Edge is updated
* @example // Use EdgesState hook to create edges and get onEdgesChange handler
* import ReactFlow, { useEdgesState } from '@xyflow/react';
* const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -181,7 +186,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
onSelectionStart?: (event: ReactMouseEvent) => void;
onSelectionEnd?: (event: ReactMouseEvent) => void;
onSelectionContextMenu?: (event: ReactMouseEvent, nodes: NodeType[]) => void;
/** When a connection line is completed and two nodes are connected by the user, this event fires with the new connection.
/**
* When a connection line is completed and two nodes are connected by the user, this event fires with the new connection.
*
* You can use the addEdge utility to convert the connection to a complete edge.
* @example // Use helper function to update edges onConnect
@@ -223,17 +229,20 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
onPaneMouseMove?: (event: ReactMouseEvent) => void;
/** This event handler gets called when mouse leaves the pane */
onPaneMouseLeave?: (event: ReactMouseEvent) => void;
/** Distance that the mouse can move between mousedown/up that will trigger a click
/**
* Distance that the mouse can move between mousedown/up that will trigger a click
* @default 0
*/
paneClickDistance?: number;
/** Distance that the mouse can move between mousedown/up that will trigger a click
/**
* Distance that the mouse can move between mousedown/up that will trigger a click
* @default 0
*/
nodeClickDistance?: number;
/** This handler gets called before the user deletes nodes or edges and provides a way to abort the deletion by returning false. */
onBeforeDelete?: OnBeforeDelete<NodeType, EdgeType>;
/** Custom node types to be available in a flow.
/**
* Custom node types to be available in a flow.
*
* React Flow matches a node's type to a component in the nodeTypes object.
* @example
@@ -242,7 +251,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* const nodeTypes = { nameOfNodeType: CustomNode };
*/
nodeTypes?: NodeTypes;
/** Custom edge types to be available in a flow.
/**
* Custom edge types to be available in a flow.
*
* React Flow matches an edge's type to a component in the edgeTypes object.
* @example
@@ -251,7 +261,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* const edgeTypes = { nameOfEdgeType: CustomEdge };
*/
edgeTypes?: EdgeTypes;
/** The type of edge path to use for connection lines.
/**
* The type of edge path to use for connection lines.
*
* Although created edges can be of any type, React Flow needs to know what type of path to render for the connection line before the edge is created!
*/
@@ -262,17 +273,20 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
connectionLineComponent?: ConnectionLineComponent<NodeType>;
/** Styles to be applied to the container of the connection line */
connectionLineContainerStyle?: CSSProperties;
/** 'strict' connection mode will only allow you to connect source handles to target handles.
/**
* 'strict' connection mode will only allow you to connect source handles to target handles.
*
* 'loose' connection mode will allow you to connect handles of any type to one another.
* @default 'strict'
*/
connectionMode?: ConnectionMode;
/** Pressing down this key deletes all selected nodes & edges.
/**
* Pressing down this key deletes all selected nodes & edges.
* @default 'Backspace'
*/
deleteKeyCode?: KeyCode | null;
/** If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false.
/**
* If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false.
*
* By setting this prop to null you can disable this functionality.
* @default 'Space'
@@ -280,91 +294,110 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
selectionKeyCode?: KeyCode | null;
/** Select multiple elements with a selection box, without pressing down selectionKey */
selectionOnDrag?: boolean;
/** When set to "partial", when the user creates a selection box by click and dragging nodes that are only partially in the box are still selected.
/**
* When set to "partial", when the user creates a selection box by click and dragging nodes that are only partially in the box are still selected.
* @default 'full'
*/
selectionMode?: SelectionMode;
/** If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false.
/**
* If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false.
*
* By setting this prop to null you can disable this functionality.
* @default 'Space'
*/
panActivationKeyCode?: KeyCode | null;
/** Pressing down this key you can select multiple elements by clicking.
/**
* Pressing down this key you can select multiple elements by clicking.
* @default 'Meta' for macOS, "Ctrl" for other systems
*/
multiSelectionKeyCode?: KeyCode | null;
/** If a key is set, you can zoom the viewport while that key is held down even if panOnScroll is set to false.
/**
* If a key is set, you can zoom the viewport while that key is held down even if panOnScroll is set to false.
*
* By setting this prop to null you can disable this functionality.
* @default 'Meta' for macOS, "Ctrl" for other systems
* */
*
*/
zoomActivationKeyCode?: KeyCode | null;
/** Set this prop to make the flow snap to the grid */
snapToGrid?: boolean;
/** Grid all nodes will snap to
/**
* Grid all nodes will snap to
* @example [20, 20]
*/
snapGrid?: SnapGrid;
/** You can enable this optimisation to instruct Svelte Flow to only render nodes and edges that would be visible in the viewport.
/**
* You can enable this optimisation to instruct Svelte Flow to only render nodes and edges that would be visible in the viewport.
*
* This might improve performance when you have a large number of nodes and edges but also adds an overhead.
* @default false
*/
onlyRenderVisibleElements?: boolean;
/** Controls if all nodes should be draggable
/**
* Controls if all nodes should be draggable
* @default true
*/
nodesDraggable?: boolean;
/** Controls if all nodes should be connectable to each other
/**
* Controls if all nodes should be connectable to each other
* @default true
*/
nodesConnectable?: boolean;
/** Controls if all nodes should be focusable
/**
* Controls if all nodes should be focusable
* @default true
*/
nodesFocusable?: boolean;
/** Defines nodes relative position to its coordinates
/**
* Defines nodes relative position to its coordinates
* @example
* [0, 0] // default, top left
* [0.5, 0.5] // center
* [1, 1] // bottom right
*/
nodeOrigin?: NodeOrigin;
/** Controls if all edges should be focusable
/**
* Controls if all edges should be focusable
* @default true
*/
edgesFocusable?: boolean;
/** Controls if all edges should be updateable
/**
* Controls if all edges should be updateable
* @default true
*/
edgesReconnectable?: boolean;
/** Controls if all elements should (nodes & edges) be selectable
/**
* Controls if all elements should (nodes & edges) be selectable
* @default true
*/
elementsSelectable?: boolean;
/** If true, nodes get selected on drag
/**
* If true, nodes get selected on drag
* @default true
*/
selectNodesOnDrag?: boolean;
/** Enableing this prop allows users to pan the viewport by clicking and dragging.
/**
* Enableing this prop allows users to pan the viewport by clicking and dragging.
*
* You can also set this prop to an array of numbers to limit which mouse buttons can activate panning.
* @example [0, 2] // allows panning with the left and right mouse buttons
* [0, 1, 2, 3, 4] // allows panning with all mouse buttons
*/
panOnDrag?: boolean | number[];
/** Minimum zoom level
/**
* Minimum zoom level
* @default 0.5
*/
minZoom?: number;
/** Maximum zoom level
/**
* Maximum zoom level
* @default 2
*/
maxZoom?: number;
/** Controlled viewport to be used instead of internal one */
viewport?: Viewport;
/** Sets the initial position and zoom of the viewport.
/**
* Sets the initial position and zoom of the viewport.
*
* If a default viewport is provided but fitView is enabled, the default viewport will be ignored.
* @example
@@ -378,23 +411,27 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* Gets called when the viewport changes.
*/
onViewportChange?: (viewport: Viewport) => void;
/** By default the viewport extends infinitely. You can use this prop to set a boundary.
/**
* By default the viewport extends infinitely. You can use this prop to set a boundary.
*
* The first pair of coordinates is the top left boundary and the second pair is the bottom right.
* @example [[-1000, -10000], [1000, 1000]]
*/
translateExtent?: CoordinateExtent;
/** Disabling this prop will allow the user to scroll the page even when their pointer is over the flow.
/**
* Disabling this prop will allow the user to scroll the page even when their pointer is over the flow.
* @default true
*/
preventScrolling?: boolean;
/** By default nodes can be placed on an infinite flow. You can use this prop to set a boundary.
/**
* By default nodes can be placed on an infinite flow. You can use this prop to set a boundary.
*
* The first pair of coordinates is the top left boundary and the second pair is the bottom right.
* @example [[-1000, -10000], [1000, 1000]]
*/
nodeExtent?: CoordinateExtent;
/** Color of edge markers
/**
* Color of edge markers
* @example "#b1b1b7"
*/
defaultMarkerColor?: string;
@@ -402,17 +439,20 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
zoomOnScroll?: boolean;
/** Controls if the viewport should zoom by pinching on a touch screen */
zoomOnPinch?: boolean;
/** Controls if the viewport should pan by scrolling inside the container
/**
* Controls if the viewport should pan by scrolling inside the container
*
* Can be limited to a specific direction with panOnScrollMode
*/
panOnScroll?: boolean;
/** Controls how fast viewport should be panned on scroll.
/**
* Controls how fast viewport should be panned on scroll.
*
* Use togther with panOnScroll prop.
*/
panOnScrollSpeed?: number;
/** This prop is used to limit the direction of panning when panOnScroll is enabled.
/**
* This prop is used to limit the direction of panning when panOnScroll is enabled.
*
* The "free" option allows panning in any direction.
* @default "free"
@@ -427,7 +467,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
noPanClassName?: string;
/** If set, initial viewport will show all nodes & edges */
fitView?: boolean;
/** Options to be used in combination with fitView
/**
* Options to be used in combination with fitView
* @example
* const fitViewOptions = {
* padding: 0.1,
@@ -439,30 +480,35 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* };
*/
fitViewOptions?: FitViewOptions;
/**The connectOnClick option lets you click or tap on a source handle to start a connection
/**
*The connectOnClick option lets you click or tap on a source handle to start a connection
* and then click on a target handle to complete the connection.
*
* If you set this option to false, users will need to drag the connection line to the target
* handle to create a connection.
*/
connectOnClick?: boolean;
/** Set position of the attribution
/**
* Set position of the attribution
* @default 'bottom-right'
* @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
*/
attributionPosition?: PanelPosition;
/** By default, we render a small attribution in the corner of your flows that links back to the project.
/**
* By default, we render a small attribution in the corner of your flows that links back to the project.
*
* Anyone is free to remove this attribution whether they're a Pro subscriber or not
* but we ask that you take a quick look at our {@link https://reactflow.dev/learn/troubleshooting/remove-attribution | removing attribution guide}
* before doing so.
*/
proOptions?: ProOptions;
/** Enabling this option will raise the z-index of nodes when they are selected.
/**
* Enabling this option will raise the z-index of nodes when they are selected.
* @default true
*/
elevateNodesOnSelect?: boolean;
/** Enabling this option will raise the z-index of edges when they are selected.
/**
* Enabling this option will raise the z-index of edges when they are selected.
* @default true
*/
elevateEdgesOnSelect?: boolean;
@@ -471,36 +517,43 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* @default false
*/
disableKeyboardA11y?: boolean;
/** You can enable this prop to automatically pan the viewport while dragging a node.
/**
* You can enable this prop to automatically pan the viewport while dragging a node.
* @default true
*/
autoPanOnNodeDrag?: boolean;
/** You can enable this prop to automatically pan the viewport while dragging a node.
/**
* You can enable this prop to automatically pan the viewport while dragging a node.
* @default true
*/
autoPanOnConnect?: boolean;
/** The speed at which the viewport pans while dragging a node or a selection box.
/**
* The speed at which the viewport pans while dragging a node or a selection box.
* @default 15
*/
autoPanSpeed?: number;
/** You can enable this prop to automatically pan the viewport while making a new connection.
/**
* You can enable this prop to automatically pan the viewport while making a new connection.
* @default true
*/
connectionRadius?: number;
/** Ocassionally something may happen that causes Svelte Flow to throw an error.
/**
* Ocassionally something may happen that causes Svelte Flow to throw an error.
*
* Instead of exploding your application, we log a message to the console and then call this event handler.
* You might use it for additional logging or to show a message to the user.
*/
onError?: OnError;
/** This callback can be used to validate a new connection
/**
* This callback can be used to validate a new connection
*
* If you return false, the edge will not be added to your flow.
* If you have custom connection logic its preferred to use this callback over the isValidConnection prop on the handle component for performance reasons.
* @default (connection: Connection) => true
*/
isValidConnection?: IsValidConnection<EdgeType>;
/** With a threshold greater than zero you can control the distinction between node drag and click events.
/**
* With a threshold greater than zero you can control the distinction between node drag and click events.
*
* If threshold equals 1, you need to drag the node 1 pixel before a drag event is fired.
* @default 1
@@ -510,12 +563,14 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
width?: number;
/** Sets a fixed height for the flow */
height?: number;
/** Controls color scheme used for styling the flow
/**
* Controls color scheme used for styling the flow
* @default 'system'
* @example 'system' | 'light' | 'dark'
*/
colorMode?: ColorMode;
/** If set true, some debug information will be logged to the console like which events are fired.
/**
* If set true, some debug information will be logged to the console like which events are fired.
*
* @default undefined
*/

View File

@@ -19,6 +19,9 @@ import type {
import { EdgeTypes, InternalNode, Node } from '.';
/**
* @inline
*/
export type EdgeLabelOptions = {
label?: string | ReactNode;
labelStyle?: CSSProperties;
@@ -29,7 +32,8 @@ export type EdgeLabelOptions = {
};
/**
* The Edge type is mainly used for the `edges` that get passed to the ReactFlow component
* An `Edge` is the complete description with everything React Flow needs
* to know in order to render it.
* @public
*/
export type Edge<
@@ -91,6 +95,11 @@ export type EdgeWrapperProps<EdgeType extends Edge = Edge> = {
disableKeyboardA11y?: boolean;
};
/**
* Many properties on an [`Edge`](/api-reference/types/edge) are optional. When a new edge is created,
* the properties that are not provided will be filled in with the default values
* passed to the `defaultEdgeOptions` prop of the [`<ReactFlow />`](/api-reference/react-flow#defaultedgeoptions) component.
*/
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
export type EdgeTextProps = SVGAttributes<SVGElement> &
@@ -100,8 +109,10 @@ export type EdgeTextProps = SVGAttributes<SVGElement> &
};
/**
* Custom edge component props
* When you implement a custom edge it is wrapped in a component that enables some
* basic functionality. The `EdgeProps` type is the props that are passed to this.
* @public
* @expand
*/
export type EdgeProps<EdgeType extends Edge = Edge> = Pick<
EdgeType,
@@ -121,6 +132,7 @@ export type EdgeProps<EdgeType extends Edge = Edge> = Pick<
/**
* BaseEdge component props
* @public
* @expand
*/
export type BaseEdgeProps = Omit<SVGAttributes<SVGPathElement>, 'd'> &
EdgeLabelOptions & {
@@ -137,6 +149,7 @@ export type BaseEdgeProps = Omit<SVGAttributes<SVGPathElement>, 'd'> &
/**
* Helper type for edge components that get exported by the library
* @public
* @expand
*/
export type EdgeComponentProps = EdgePosition &
EdgeLabelOptions & {
@@ -156,35 +169,47 @@ export type EdgeComponentWithPathOptions<PathOptions> = EdgeComponentProps & {
/**
* BezierEdge component props
* @public
* @expand
*/
export type BezierEdgeProps = EdgeComponentWithPathOptions<BezierPathOptions>;
/**
* SmoothStepEdge component props
* @public
* @expand
*/
export type SmoothStepEdgeProps = EdgeComponentWithPathOptions<SmoothStepPathOptions>;
/**
* StepEdge component props
* @public
* @expand
*/
export type StepEdgeProps = EdgeComponentWithPathOptions<StepPathOptions>;
/**
* StraightEdge component props
* @public
* @expand
*/
export type StraightEdgeProps = Omit<EdgeComponentProps, 'sourcePosition' | 'targetPosition'>;
/**
* SimpleBezier component props
* @public
* @expand
*/
export type SimpleBezierEdgeProps = EdgeComponentProps;
export type OnReconnect<EdgeType extends Edge = Edge> = (oldEdge: EdgeType, newConnection: Connection) => void;
/**
* If you want to render a custom component for connection lines, you can set the
* `connectionLineComponent` prop on the [`<ReactFlow />`](/api-reference/react-flow#connection-connectionLineComponent)
* component. The `ConnectionLineComponentProps` are passed to your custom component.
*
* @public
*/
export type ConnectionLineComponentProps<NodeType extends Node = Node> = {
connectionLineStyle?: CSSProperties;
connectionLineType: ConnectionLineType;

View File

@@ -18,11 +18,44 @@ import {
import type { Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.';
/**
* This type can be used to type the `onNodesChange` function with a custom node type.
*
* @public
*
* @example
*
* ```ts
* const onNodesChange: OnNodesChange<MyNodeType> = useCallback((changes) => {
* setNodes((nodes) => applyNodeChanges(nodes, changes));
* },[]);
* ```
*/
export type OnNodesChange<NodeType extends Node = Node> = (changes: NodeChange<NodeType>[]) => void;
/**
* This type can be used to type the `onEdgesChange` function with a custom edge type.
*
* @public
*
* @example
*
* ```ts
* const onEdgesChange: OnEdgesChange<MyEdgeType> = useCallback((changes) => {
* setEdges((edges) => applyEdgeChanges(edges, changes));
* },[]);
* ```
*/
export type OnEdgesChange<EdgeType extends Edge = Edge> = (changes: EdgeChange<EdgeType>[]) => void;
export type OnNodesDelete<NodeType extends Node = Node> = (nodes: NodeType[]) => void;
export type OnEdgesDelete<EdgeType extends Edge = Edge> = (edges: EdgeType[]) => void;
/**
* This type can be used to type the `onDelete` function with a custom node and edge type.
*
* @public
*/
export type OnDelete<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (params: {
nodes: NodeType[];
edges: EdgeType[];
@@ -39,6 +72,7 @@ export type NodeTypes = Record<
}
>
>;
export type EdgeTypes = Record<
string,
ComponentType<
@@ -64,12 +98,23 @@ export type OnSelectionChangeParams = {
export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void;
export type FitViewParams<NodeType extends Node = Node> = FitViewParamsBase<NodeType>;
/**
* When calling [`fitView`](/api-reference/types/react-flow-instance#fitview) these options
* can be used to customize the behaviour. For example, the `duration` option can be used to
* transform the viewport smoothly over a given amount of time.
*
* @public
*/
export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>;
export type FitView = (fitViewOptions?: FitViewOptions) => Promise<boolean>;
export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (
reactFlowInstance: ReactFlowInstance<NodeType, EdgeType>
) => void;
/**
* @inline
*/
export type ViewportHelperFunctions = {
/**
* Zooms viewport in by 1.2.

View File

@@ -13,6 +13,9 @@ export type DeleteElementsOptions = {
edges?: (Edge | { id: Edge['id'] })[];
};
/**
* @inline
*/
export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
/**
* Returns nodes.
@@ -217,6 +220,14 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
}) => NodeConnection[];
};
/**
* The `ReactFlowInstance` provides a collection of methods to query and manipulate
* the internal state of your flow. You can get an instance by using the
* [`useReactFlow`](/api-reference/hooks/use-react-flow) hook or attaching a listener
* to the [`onInit`](/api-reference/react-flow#event-oninit) event.
*
* @public
*/
export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = GeneralHelpers<
NodeType,
EdgeType

View File

@@ -4,7 +4,10 @@ import type { CoordinateExtent, NodeBase, OnError, NodeProps as NodePropsBase, I
import { NodeTypes } from './general';
/**
* The node data structure that gets used for the nodes prop.
* The `Node` type represents everything React Flow needs to know about a given node.
* Whenever you want to update a certain attribute of a node, you need to create a new
* node object.
*
* @public
*/
export type Node<
@@ -18,9 +21,10 @@ export type Node<
};
/**
* The node data structure that gets used for internal nodes.
* There are some data structures added under node.internal
* that are needed for tracking some properties
* The `InternalNode` type is identical to the base [`Node`](/api-references/types/node)
* type but is extended with some additional properties used internally.
* Some functions and callbacks that return nodes may return an `InternalNode`.
*
* @public
*/
export type InternalNode<NodeType extends Node = Node> = InternalNodeBase<NodeType>;
@@ -56,8 +60,46 @@ export type NodeWrapperProps<NodeType extends Node> = {
nodeClickDistance?: number;
};
/**
* The `BuiltInNode` type represents the built-in node types that are available in React Flow.
* You can use this type to extend your custom node type if you still want ot use the built-in ones.
*
* @public
* @example
* ```ts
* type CustomNode = Node<{ value: number }, 'custom'>;
* type MyAppNode = CustomNode | BuiltInNode;
* ```
*/
export type BuiltInNode =
| Node<{ label: string }, 'input' | 'output' | 'default'>
| Node<Record<string, never>, 'group'>;
/**
* When you implement a [custom node](/learn/customization/custom-nodes) it is
* wrapped in a component that enables basic functionality like selection and
* dragging. Your custom node receives `NodeProps` as props.
*
* @public
* @example
* ```tsx
*import { useState } from 'react';
*import { NodeProps, Node } from '@xyflow/react';
*
*export type CounterNode = Node<{ initialCount?: number }, 'counter'>;
*
*export default function CounterNode(props: NodeProps<CounterNode>) {
* const [count, setCount] = useState(props.data?.initialCount ?? 0);
*
* return (
* <div>
* <p>Count: {count}</p>
* <button className="nodrag" onClick={() => setCount(count + 1)}>
* Increment
* </button>
* </div>
* );
*}
*```
*/
export type NodeProps<NodeType extends Node = Node> = NodePropsBase<NodeType>;

View File

@@ -11,13 +11,17 @@ import {
} from '@xyflow/system';
import type { Node, Edge, InternalNode } from '../types';
// This function applies changes to nodes or edges that are triggered by React Flow internally.
// When you drag a node for example, React Flow will send a position change update.
// This function then applies the changes and returns the updated elements.
/*
* This function applies changes to nodes or edges that are triggered by React Flow internally.
* When you drag a node for example, React Flow will send a position change update.
* This function then applies the changes and returns the updated elements.
*/
function applyChanges(changes: any[], elements: any[]): any[] {
const updatedElements: any[] = [];
// By storing a map of changes for each element, we can a quick lookup as we
// iterate over the elements array!
/*
* By storing a map of changes for each element, we can a quick lookup as we
* iterate over the elements array!
*/
const changesMap = new Map<any, any[]>();
const addItemChanges: any[] = [];
@@ -26,15 +30,19 @@ function applyChanges(changes: any[], elements: any[]): any[] {
addItemChanges.push(change);
continue;
} else if (change.type === 'remove' || change.type === 'replace') {
// For a 'remove' change we can safely ignore any other changes queued for
// the same element, it's going to be removed anyway!
/*
* For a 'remove' change we can safely ignore any other changes queued for
* the same element, it's going to be removed anyway!
*/
changesMap.set(change.id, [change]);
} else {
const elementChanges = changesMap.get(change.id);
if (elementChanges) {
// If we have some changes queued already, we can do a mutable update of
// that array and save ourselves some copying.
/*
* If we have some changes queued already, we can do a mutable update of
* that array and save ourselves some copying.
*/
elementChanges.push(change);
} else {
changesMap.set(change.id, [change]);
@@ -45,8 +53,10 @@ function applyChanges(changes: any[], elements: any[]): any[] {
for (const element of elements) {
const changes = changesMap.get(element.id);
// When there are no changes for an element we can just push it unmodified,
// no need to copy it.
/*
* When there are no changes for an element we can just push it unmodified,
* no need to copy it.
*/
if (!changes) {
updatedElements.push(element);
continue;
@@ -62,9 +72,11 @@ function applyChanges(changes: any[], elements: any[]): any[] {
continue;
}
// For other types of changes, we want to start with a shallow copy of the
// object so React knows this element has changed. Sequential changes will
/// each _mutate_ this object, so there's only ever one copy.
/**
* For other types of changes, we want to start with a shallow copy of the
* object so React knows this element has changed. Sequential changes will
* each _mutate_ this object, so there's only ever one copy.
*/
const updatedElement = { ...element };
for (const change of changes) {
@@ -74,8 +86,10 @@ function applyChanges(changes: any[], elements: any[]): any[] {
updatedElements.push(updatedElement);
}
// we need to wait for all changes to be applied before adding new items
// to be able to add them at the correct index
/*
* we need to wait for all changes to be applied before adding new items
* to be able to add them at the correct index
*/
if (addItemChanges.length) {
addItemChanges.forEach((change) => {
if (change.index !== undefined) {
@@ -133,22 +147,33 @@ function applyChange(change: any, element: any): any {
/**
* Drop in function that applies node changes to an array of nodes.
* @public
* @remarks Various events on the <ReactFlow /> component can produce an {@link NodeChange} that describes how to update the edges of your flow in some way.
If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges.
* @param changes - Array of changes to apply
* @param nodes - Array of nodes to apply the changes to
* @returns Array of updated nodes
* @example
* const onNodesChange = useCallback(
(changes) => {
setNodes((oldNodes) => applyNodeChanges(changes, oldNodes));
},
[setNodes],
);
return (
<ReactFLow nodes={nodes} edges={edges} onNodesChange={onNodesChange} />
);
*```tsx
*import { useState, useCallback } from 'react';
*import { ReactFlow, applyNodeChanges, type Node, type Edge, type OnNodesChange } from '@xyflow/react';
*
*export default function Flow() {
* const [nodes, setNodes] = useState<Node[]>([]);
* const [edges, setEdges] = useState<Edge[]>([]);
* const onNodesChange: OnNodesChange = useCallback(
* (changes) => {
* setNodes((oldNodes) => applyNodeChanges(changes, oldNodes));
* },
* [setNodes],
* );
*
* return (
* <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} />
* );
*}
*```
* @remarks Various events on the <ReactFlow /> component can produce an {@link NodeChange}
* that describes how to update the edges of your flow in some way.
* If you don't need any custom behaviour, this util can be used to take an array
* of these changes and apply them to your edges.
*/
export function applyNodeChanges<NodeType extends Node = Node>(
changes: NodeChange<NodeType>[],
@@ -160,22 +185,33 @@ export function applyNodeChanges<NodeType extends Node = Node>(
/**
* Drop in function that applies edge changes to an array of edges.
* @public
* @remarks Various events on the <ReactFlow /> component can produce an {@link EdgeChange} that describes how to update the edges of your flow in some way.
If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges.
* @param changes - Array of changes to apply
* @param edges - Array of edge to apply the changes to
* @returns Array of updated edges
* @example
* ```tsx
*import { useState, useCallback } from 'react';
*import { ReactFlow, applyEdgeChanges } from '@xyflow/react';
*
*export default function Flow() {
* const [nodes, setNodes] = useState([]);
* const [edges, setEdges] = useState([]);
* const onEdgesChange = useCallback(
(changes) => {
setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges));
},
[setEdges],
);
return (
<ReactFlow nodes={nodes} edges={edges} onEdgesChange={onEdgesChange} />
);
* (changes) => {
* setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges));
* },
* [setEdges],
* );
*
* return (
* <ReactFlow nodes={nodes} edges={edges} onEdgesChange={onEdgesChange} />
* );
*}
*```
* @remarks Various events on the <ReactFlow /> component can produce an {@link EdgeChange}
* that describes how to update the edges of your flow in some way.
* If you don't need any custom behaviour, this util can be used to take an array
* of these changes and apply them to your edges.
*/
export function applyEdgeChanges<EdgeType extends Edge = Edge>(
changes: EdgeChange<EdgeType>[],
@@ -205,9 +241,11 @@ export function getSelectionChanges(
// we don't want to set all items to selected=false on the first selection
if (!(item.selected === undefined && !willBeSelected) && item.selected !== willBeSelected) {
if (mutateItem) {
// this hack is needed for nodes. When the user dragged a node, it's selected.
// When another node gets dragged, we need to deselect the previous one,
// in order to have only one selected node at a time - the onNodesChange callback comes too late here :/
/*
* this hack is needed for nodes. When the user dragged a node, it's selected.
* When another node gets dragged, we need to deselect the previous one,
* in order to have only one selected node at a time - the onNodesChange callback comes too late here :/
*/
item.selected = willBeSelected;
}
changes.push(createSelectionChange(item.id, willBeSelected));

View File

@@ -4,21 +4,45 @@ import { isNodeBase, isEdgeBase } from '@xyflow/system';
import type { Edge, Node } from '../types';
/**
* Test whether an object is useable as a Node
* Test whether an object is useable as an [`Node`](/api-reference/types/node).
* In TypeScript this is a type guard that will narrow the type of whatever you pass in to
* [`Node`](/api-reference/types/node) if it returns `true`.
*
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true
* @param element - The element to test
* @returns A boolean indicating whether the element is an Node
*
* @example
* ```js
*import { isNode } from '@xyflow/react';
*
*if (isNode(node)) {
* // ..
*}
*```
*/
export const isNode = <NodeType extends Node = Node>(element: unknown): element is NodeType =>
isNodeBase<NodeType>(element);
/**
* Test whether an object is useable as an Edge
* Test whether an object is useable as an [`Edge`](/api-reference/types/edge).
* In TypeScript this is a type guard that will narrow the type of whatever you pass in to
* [`Edge`](/api-reference/types/edge) if it returns `true`.
*
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true
* @param element - The element to test
* @returns A boolean indicating whether the element is an Edge
*
* @example
* ```js
*import { isEdge } from '@xyflow/react';
*
*if (isEdge(edge)) {
* // ..
*}
*```
*/
export const isEdge = <EdgeType extends Edge = Edge>(element: unknown): element is EdgeType =>
isEdgeBase<EdgeType>(element);

View File

@@ -11,7 +11,9 @@ import type {
import type { Node } from '$lib/types';
/**
* The Edge type is mainly used for the `edges` that get passed to the SvelteFlow component.
* An `Edge` is the complete description with everything React Flow needs
*to know in order to render it.
* @public
*/
export type Edge<
EdgeData extends Record<string, unknown> = Record<string, unknown>,

View File

@@ -42,7 +42,10 @@ export type NodeReplaceChange<NodeType extends NodeBase = NodeBase> = {
};
/**
* Union type of all possible node changes.
* The [`onNodesChange`](/api-reference/react-flow#on-nodes-change) callback takes
*an array of `NodeChange` objects that you should use to update your flow's state.
*The `NodeChange` type is a union of six different object types that represent that
*various ways an node can change in a flow.
* @public
*/
export type NodeChange<NodeType extends NodeBase = NodeBase> =
@@ -67,6 +70,14 @@ export type EdgeReplaceChange<EdgeType extends EdgeBase = EdgeBase> = {
type: 'replace';
};
/**
* The [`onEdgesChange`](/api-reference/react-flow#on-edges-change) callback takes
*an array of `EdgeChange` objects that you should use to update your flow's state.
*The `EdgeChange` type is a union of four different object types that represent that
*various ways an edge can change in a flow.
*
* @public
*/
export type EdgeChange<EdgeType extends EdgeBase = EdgeBase> =
| EdgeSelectionChange
| EdgeRemoveChange

View File

@@ -12,11 +12,13 @@ export type EdgeBase<
source: string;
/** Id of target node */
target: string;
/** Id of source handle
/**
* Id of source handle
* only needed if there are multiple handles per node
*/
sourceHandle?: string | null;
/** Id of target handle
/**
* Id of target handle
* only needed if there are multiple handles per node
*/
targetHandle?: string | null;
@@ -27,11 +29,13 @@ export type EdgeBase<
/** Arbitrary data passed to an edge */
data?: EdgeData;
selected?: boolean;
/** Set the marker on the beginning of an edge
/**
* Set the marker on the beginning of an edge
* @example 'arrow', 'arrowclosed' or custom marker
*/
markerStart?: EdgeMarkerType;
/** Set the marker on the end of an edge
/**
* Set the marker on the end of an edge
* @example 'arrow', 'arrowclosed' or custom marker
*/
markerEnd?: EdgeMarkerType;
@@ -54,11 +58,24 @@ export type BezierPathOptions = {
curvature?: number;
};
/**
* @inline
*/
export type DefaultEdgeOptionsBase<EdgeType extends EdgeBase> = Omit<
EdgeType,
'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'selected'
>;
/**
* If you set the `connectionLineType` prop on your [`<ReactFlow />`](/api-reference/react-flow#connection-connectionLineType)
*component, it will dictate the style of connection line rendered when creating
*new edges.
*
* @public
*
* @remarks If you choose to render a custom connection line component, this value will be
*passed to your component as part of its [`ConnectionLineComponentProps`](/api-reference/types/connection-line-component-props).
*/
export enum ConnectionLineType {
Bezier = 'default',
Straight = 'straight',
@@ -67,6 +84,13 @@ export enum ConnectionLineType {
SimpleBezier = 'simplebezier',
}
/**
* Edges can optionally have markers at the start and end of an edge. The `EdgeMarker`
*type is used to configure those markers! Check the docs for [`MarkerType`](/api-reference/types/marker-type)
*for details on what types of edge marker are available.
*
* @public
*/
export type EdgeMarker = {
type: MarkerType;
color?: string;
@@ -79,6 +103,12 @@ export type EdgeMarker = {
export type EdgeMarkerType = string | EdgeMarker;
/**
* Edges may optionally have a marker on either end. The MarkerType type enumerates
* the options available to you when configuring a given marker.
*
* @public
*/
export enum MarkerType {
Arrow = 'arrow',
ArrowClosed = 'arrowclosed',
@@ -88,6 +118,9 @@ export type MarkerProps = EdgeMarker & {
id: string;
};
/**
* @inline
*/
export type EdgePosition = {
sourceX: number;
sourceY: number;

View File

@@ -26,6 +26,13 @@ export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionO
export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => Promise<boolean>;
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => Promise<boolean>;
/**
* The `Connection` type is the basic minimal description of an [`Edge`](/api-reference/types/edge)
* between two nodes. The [`addEdge`](/api-reference/utils/add-edge) util can be used to upgrade
* a `Connection` to an [`Edge`](/api-reference/types/edge).
*
* @public
*/
export type Connection = {
source: string;
target: string;
@@ -33,15 +40,28 @@ export type Connection = {
targetHandle: string | null;
};
// TODO: remove in next version
/**
* The `HandleConnection` type is an extention of a basic [Connection](/api-reference/types/connection) that includes the `edgeId`.
*/
export type HandleConnection = Connection & {
edgeId: string;
};
/**
* The `NodeConnection` type is an extention of a basic [Connection](/api-reference/types/connection) that includes the `edgeId`.
*
*/
export type NodeConnection = Connection & {
edgeId: string;
};
/**
* The `ConnectionMode` is used to set the mode of connection between nodes.
* The `Strict` mode is the default one and only allows source to target edges.
* `Loose` mode allows source to source and target to target edges as well.
*
* @public
*/
export enum ConnectionMode {
Strict = 'strict',
Loose = 'loose',
@@ -59,6 +79,9 @@ export type OnConnectEnd = (event: MouseEvent | TouchEvent, connectionState: Fin
export type IsValidConnection = (edge: EdgeBase | Connection) => boolean;
/**
* @inline
*/
export type FitViewParamsBase<NodeType extends NodeBase> = {
nodes: Map<string, InternalNodeBase<NodeType>>;
width: number;
@@ -68,6 +91,9 @@ export type FitViewParamsBase<NodeType extends NodeBase> = {
maxZoom: number;
};
/**
* @inline
*/
export type FitViewOptionsBase<NodeType extends NodeBase = NodeBase> = {
padding?: number;
includeHiddenNodes?: boolean;
@@ -77,6 +103,17 @@ export type FitViewOptionsBase<NodeType extends NodeBase = NodeBase> = {
nodes?: (NodeType | { id: string })[];
};
/**
* Internally, React Flow maintains a coordinate system that is independent of the
* rest of the page. The `Viewport` type tells you where in that system your flow
* is currently being display at and how zoomed in or out it is.
*
* @public
* @remarks A `Transform` has the same properties as the viewport, but they represent
* different things. Make sure you don't get them muddled up or things will start
* to look weird!
*
*/
export type Viewport = {
x: number;
y: number;
@@ -87,12 +124,23 @@ export type KeyCode = string | Array<string>;
export type SnapGrid = [number, number];
/**
* This enum is used to set the different modes of panning the viewport when the
* user scrolls. The `Free` mode allows the user to pan in any direction by scrolling
* with a device like a trackpad. The `Vertical` and `Horizontal` modes restrict
* scroll panning to only the vertical or horizontal axis, respectively.
*
* @public
*/
export enum PanOnScrollMode {
Free = 'free',
Vertical = 'vertical',
Horizontal = 'horizontal',
}
/**
* @inline
*/
export type ViewportHelperFunctionOptions = {
duration?: number;
};
@@ -113,6 +161,14 @@ export type D3ZoomHandler = (this: Element, event: any, d: unknown) => void;
export type UpdateNodeInternals = (nodeId: string | string[]) => void;
/**
* This type is mostly used to help position things on top of the flow viewport. For
* example both the [`<MiniMap />`](/api-reference/components/minimap) and
* [`<Controls />`](/api-reference/components/controls) components take a `position`
* prop of this type.
*
* @public
*/
export type PanelPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
export type ProOptions = {
@@ -174,6 +230,13 @@ export type ConnectionInProgress<NodeType extends InternalNodeBase = InternalNod
toPosition: Position;
toNode: NodeType | null;
};
/**
* The `ConnectionState` type bundles all information about an ongoing connection.
* It is returned by the [`useConnection`](/api-reference/hooks/use-connection) hook.
*
* @public
*/
export type ConnectionState<NodeType extends InternalNodeBase = InternalNodeBase> =
| ConnectionInProgress<NodeType>
| NoConnection;

View File

@@ -14,11 +14,13 @@ export type Handle = {
};
export type HandleProps = {
/** Type of the handle
/**
* Type of the handle
* @example HandleType.Source, HandleType.Target
*/
type: HandleType;
/** Position of the handle
/**
* Position of the handle
* @example Position.TopLeft, Position.TopRight,
* Position.BottomLeft, Position.BottomRight
*/
@@ -29,11 +31,13 @@ export type HandleProps = {
isConnectableStart?: boolean;
/** Should you be able to connect to this handle */
isConnectableEnd?: boolean;
/** Callback if connection is valid
/**
* Callback if connection is valid
* @remarks connection becomes an edge if isValidConnection returns true
*/
isValidConnection?: IsValidConnection;
/** Id of the handle
/**
* Id of the handle
* @remarks optional if there is only one handle of this type
*/
id?: string | null;

View File

@@ -4,6 +4,7 @@ import { Optional } from '../utils/types';
/**
* Framework independent node data structure.
*
* @inline
* @typeParam NodeData - type of the node data
* @typeParam NodeType - type of the node
*/
@@ -13,7 +14,8 @@ export type NodeBase<
> = {
/** Unique id of a node */
id: string;
/** Position of a node on the pane
/**
* Position of a node on the pane
* @example { x: 0, y: 0 }
*/
position: XYPosition;
@@ -21,11 +23,13 @@ export type NodeBase<
data: NodeData;
/** Type of node defined in nodeTypes */
type?: NodeType;
/** Only relevant for default, source, target nodeType. controls source position
/**
* Only relevant for default, source, target nodeType. controls source position
* @example 'right', 'left', 'top', 'bottom'
*/
sourcePosition?: Position;
/** Only relevant for default, source, target nodeType. controls target position
/**
* Only relevant for default, source, target nodeType. controls target position
* @example 'right', 'left', 'top', 'bottom'
*/
targetPosition?: Position;
@@ -45,13 +49,15 @@ export type NodeBase<
/** Parent node id, used for creating sub-flows */
parentId?: string;
zIndex?: number;
/** Boundary a node can be moved in
/**
* Boundary a node can be moved in
* @example 'parent' or [[0, 0], [100, 100]]
*/
extent?: 'parent' | CoordinateExtent;
expandParent?: boolean;
ariaLabel?: string;
/** Origin of the node relative to it's position
/**
* Origin of the node relative to it's position
* @example
* [0.5, 0.5] // centers the node
* [0, 0] // top left
@@ -73,8 +79,10 @@ export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = NodeType &
internals: {
positionAbsolute: XYPosition;
z: number;
/** Holds a reference to the original node object provided by the user.
* Used as an optimization to avoid certain operations. */
/**
* Holds a reference to the original node object provided by the user.
* Used as an optimization to avoid certain operations.
*/
userNode: NodeType;
handleBounds?: NodeHandleBounds;
bounds?: NodeBounds;
@@ -82,7 +90,7 @@ export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = NodeType &
};
/**
* The node data structure that gets used for the nodes prop.
* The node data structure that gets used for the custom nodes props.
*
* @public
*/
@@ -134,10 +142,22 @@ export type NodeDragItem = {
expandParent?: boolean;
};
/**
* The origin of a Node determines how it is placed relative to its own coordinates.
* `[0, 0]` places it at the top left corner, `[0.5, 0.5]` right in the center and
* `[1, 1]` at the bottom right of its position.
*
* @public
*/
export type NodeOrigin = [number, number];
export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void;
/**
* Type for the handles of a node
*
* @public
*/
export type NodeHandle = Omit<Optional<Handle, 'width' | 'height'>, 'nodeId'>;
export type Align = 'center' | 'start' | 'end';

View File

@@ -1,3 +1,10 @@
/**
* While [`PanelPosition`](/api-reference/types/panel-position) can be used to place a
* component in the corners of a container, the `Position` enum is less precise and used
* primarily in relation to edges and handles.
*
* @public
*/
export enum Position {
Left = 'left',
Top = 'top',
@@ -12,6 +19,11 @@ export const oppositePosition = {
[Position.Bottom]: Position.Top,
};
/**
* All positions are stored in an object with x and y coordinates.
*
* @public
*/
export type XYPosition = {
x: number;
y: number;
@@ -33,4 +45,14 @@ export type Box = XYPosition & {
export type Transform = [number, number, number];
/**
* A coordinate extent represents two points in a coordinate system: one in the top
* left corner and one in the bottom right corner. It is used to represent the
* bounds of nodes in the flow or the bounds of the viewport.
*
* @public
*
* @remarks Props that expect a `CoordinateExtent` usually default to `[[-∞, -∞], [+∞, +∞]]`
* to represent an unbounded extent.
*/
export type CoordinateExtent = [[number, number], [number, number]];

View File

@@ -61,9 +61,11 @@ export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRec
};
};
// The handle bounds are calculated relative to the node element.
// We store them in the internals object of the node in order to avoid
// unnecessary recalculations.
/*
* The handle bounds are calculated relative to the node element.
* We store them in the internals object of the node in order to avoid
* unnecessary recalculations.
*/
export const getHandleBounds = (
type: 'source' | 'target',
nodeElement: HTMLDivElement,

View File

@@ -38,8 +38,10 @@ export function getBezierEdgeCenter({
targetControlX: number;
targetControlY: number;
}): [number, number, number, number] {
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
/*
* cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
* https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
*/
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125;
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125;
const offsetX = Math.abs(centerX - sourceX);
@@ -70,7 +72,9 @@ function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurva
}
/**
* Get a bezier path from source to target handle
* The `getBezierPath` util returns everything you need to render a bezier edge
*between two nodes.
* @public
* @param params.sourceX - The x position of the source handle
* @param params.sourceY - The y position of the source handle
* @param params.sourcePosition - The position of the source handle (default: Position.Bottom)
@@ -80,17 +84,22 @@ function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurva
* @param params.curvature - The curvature of the bezier edge
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
* @example
* ```js
* const source = { x: 0, y: 20 };
const target = { x: 150, y: 100 };
const [path, labelX, labelY, offsetX, offsetY] = getBezierPath({
sourceX: source.x,
sourceY: source.y,
sourcePosition: Position.Right,
targetX: target.x,
targetY: target.y,
targetPosition: Position.Left,
});
* const target = { x: 150, y: 100 };
*
* const [path, labelX, labelY, offsetX, offsetY] = getBezierPath({
* sourceX: source.x,
* sourceY: source.y,
* sourcePosition: Position.Right,
* targetX: target.x,
* targetY: target.y,
* targetPosition: Position.Left,
*});
*```
*
* @remarks This function returns a tuple (aka a fixed-size array) to make it easier to
*work with multiple edge paths at once.
*/
export function getBezierPath({
sourceX,

View File

@@ -90,12 +90,16 @@ const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => {
};
/**
* This util is a convenience function to add a new Edge to an array of edges
* @remarks It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one.
* This util is a convenience function to add a new Edge to an array of edges. It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one.
* @public
* @param edgeParams - Either an Edge or a Connection you want to add
* @param edges - The array of all current edges
* @returns A new array of edges with the new edge added
*
* @remarks If an edge with the same `target` and `source` already exists (and the same
*`targetHandle` and `sourceHandle` if those are set), then this util won't add
*a new edge even if the `id` property is different.
*
*/
export const addEdge = <EdgeType extends EdgeBase>(
edgeParams: EdgeType | Connection,
@@ -137,12 +141,21 @@ export type ReconnectEdgeOptions = {
};
/**
* A handy utility to reconnect an existing edge with new properties
* A handy utility to update an existing [`Edge`](/api-reference/types/edge) with new properties.
*This searches your edge array for an edge with a matching `id` and updates its
*properties with the connection you provide.
* @public
* @param oldEdge - The edge you want to update
* @param newConnection - The new connection you want to update the edge with
* @param edges - The array of all current edges
* @param options.shouldReplaceId - should the id of the old edge be replaced with the new connection id
* @returns the updated edges array
*
* @example
* ```js
*const onReconnect = useCallback(
* (oldEdge: Edge, newConnection: Connection) => setEdges((els) => reconnectEdge(oldEdge, newConnection, els)),[]);
*```
*/
export const reconnectEdge = <EdgeType extends EdgeBase>(
oldEdge: EdgeType,

View File

@@ -38,8 +38,10 @@ const getDirection = ({
const distance = (a: XYPosition, b: XYPosition) => Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
// ith this function we try to mimic a orthogonal edge routing behaviour
// It's not as good as a real orthogonal edge routing but it's faster and good enough as a default for step and smooth step edges
/*
* ith this function we try to mimic a orthogonal edge routing behaviour
* It's not as good as a real orthogonal edge routing but it's faster and good enough as a default for step and smooth step edges
*/
function getPoints({
source,
sourcePosition = Position.Bottom,
@@ -83,16 +85,20 @@ function getPoints({
if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) {
centerX = center.x ?? defaultCenterX;
centerY = center.y ?? defaultCenterY;
// --->
// |
// >---
/*
* --->
* |
* >---
*/
const verticalSplit: XYPosition[] = [
{ x: centerX, y: sourceGapped.y },
{ x: centerX, y: targetGapped.y },
];
// |
// ---
// |
/*
* |
* ---
* |
*/
const horizontalSplit: XYPosition[] = [
{ x: sourceGapped.x, y: centerY },
{ x: targetGapped.x, y: centerY },
@@ -191,7 +197,10 @@ function getBend(a: XYPosition, b: XYPosition, c: XYPosition, size: number): str
}
/**
* Get a smooth step path from source to target handle
* The `getSmoothStepPath` util returns everything you need to render a stepped path
*between two nodes. The `borderRadius` property can be used to choose how rounded
*the corners of those steps are.
* @public
* @param params.sourceX - The x position of the source handle
* @param params.sourceY - The y position of the source handle
* @param params.sourcePosition - The position of the source handle (default: Position.Bottom)
@@ -200,17 +209,20 @@ function getBend(a: XYPosition, b: XYPosition, c: XYPosition, size: number): str
* @param params.targetPosition - The position of the target handle (default: Position.Top)
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
* @example
* ```js
* const source = { x: 0, y: 20 };
const target = { x: 150, y: 100 };
const [path, labelX, labelY, offsetX, offsetY] = getSmoothStepPath({
sourceX: source.x,
sourceY: source.y,
sourcePosition: Position.Right,
targetX: target.x,
targetY: target.y,
targetPosition: Position.Left,
});
* const target = { x: 150, y: 100 };
*
* const [path, labelX, labelY, offsetX, offsetY] = getSmoothStepPath({
* sourceX: source.x,
* sourceY: source.y,
* sourcePosition: Position.Right,
* targetX: target.x,
* targetY: target.y,
* targetPosition: Position.Left,
* });
* ```
* @remarks This function returns a tuple (aka a fixed-size array) to make it easier to work with multiple edge paths at once.
*/
export function getSmoothStepPath({
sourceX,

View File

@@ -8,24 +8,28 @@ export type GetStraightPathParams = {
};
/**
* Get a straight path from source to target handle
* Calculates the straight line path between two points.
* @public
* @param params.sourceX - The x position of the source handle
* @param params.sourceY - The y position of the source handle
* @param params.targetX - The x position of the target handle
* @param params.targetY - The y position of the target handle
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
* @example
* ```js
* const source = { x: 0, y: 20 };
const target = { x: 150, y: 100 };
const [path, labelX, labelY, offsetX, offsetY] = getStraightPath({
sourceX: source.x,
sourceY: source.y,
sourcePosition: Position.Right,
targetX: target.x,
targetY: target.y,
targetPosition: Position.Left,
});
* const target = { x: 150, y: 100 };
*
* const [path, labelX, labelY, offsetX, offsetY] = getStraightPath({
* sourceX: source.x,
* sourceY: source.y,
* sourcePosition: Position.Right,
* targetX: target.x,
* targetY: target.y,
* targetPosition: Position.Left,
* });
* ```
* @remarks This function returns a tuple (aka a fixed-size array) to make it easier to work with multiple edge paths at once.
*/
export function getStraightPath({
sourceX,

View File

@@ -186,8 +186,8 @@ export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Tra
* @returns A transforned {@link Viewport} that encloses the given bounds which you can pass to e.g. {@link setViewport}
* @example
* const { x, y, zoom } = getViewportForBounds(
{ x: 0, y: 0, width: 100, height: 100},
1200, 800, 0.5, 2);
*{ x: 0, y: 0, width: 100, height: 100},
*1200, 800, 0.5, 2);
*/
export const getViewportForBounds = (
bounds: Rect,

View File

@@ -54,12 +54,27 @@ export const isInternalNodeBase = <NodeType extends InternalNodeBase = InternalN
): element is NodeType => 'id' in element && 'internals' in element && !('source' in element) && !('target' in element);
/**
* Pass in a node, and get connected nodes where edge.source === node.id
* This util is used to tell you what nodes, if any, are connected to the given node
* as the _target_ of an edge.
* @public
* @param node - The node to get the connected nodes from
* @param nodes - The array of all nodes
* @param edges - The array of all edges
* @returns An array of nodes that are connected over eges where the source is the given node
*
* @example
* ```ts
*import { getOutgoers } from '@xyflow/react';
*
*const nodes = [];
*const edges = [];
*
*const outgoers = getOutgoers(
* { id: '1', position: { x: 0, y: 0 }, data: { label: 'node' } },
* nodes,
* edges,
*);
*```
*/
export const getOutgoers = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
node: NodeType | { id: string },
@@ -81,12 +96,27 @@ export const getOutgoers = <NodeType extends NodeBase = NodeBase, EdgeType exten
};
/**
* Pass in a node, and get connected nodes where edge.target === node.id
* This util is used to tell you what nodes, if any, are connected to the given node
* as the _source_ of an edge.
* @public
* @param node - The node to get the connected nodes from
* @param nodes - The array of all nodes
* @param edges - The array of all edges
* @returns An array of nodes that are connected over eges where the target is the given node
*
* @example
* ```ts
*import { getIncomers } from '@xyflow/react';
*
*const nodes = [];
*const edges = [];
*
*const incomers = getIncomers(
* { id: '1', position: { x: 0, y: 0 }, data: { label: 'node' } },
* nodes,
* edges,
*);
*```
*/
export const getIncomers = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
node: NodeType | { id: string },
@@ -124,12 +154,40 @@ export type GetNodesBoundsParams<NodeType extends NodeBase = NodeBase> = {
};
/**
* Internal function for determining a bounding box that contains all given nodes in an array.
* Returns the bounding box that contains all the given nodes in an array. This can
* be useful when combined with [`getViewportForBounds`](/api-reference/utils/get-viewport-for-bounds)
* to calculate the correct transform to fit the given nodes in a viewport.
* @public
* @remarks Useful when combined with {@link getViewportForBounds} to calculate the correct transform to fit the given nodes in a viewport.
* @param nodes - Nodes to calculate the bounds for
* @param params.nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center
* @returns Bounding box enclosing all nodes
*
* @remarks This function was previously called `getRectOfNodes`
*
* @example
* ```js
*import { getNodesBounds } from '@xyflow/react';
*
*const nodes = [
* {
* id: 'a',
* position: { x: 0, y: 0 },
* data: { label: 'a' },
* width: 50,
* height: 25,
* },
* {
* id: 'b',
* position: { x: 100, y: 100 },
* data: { label: 'b' },
* width: 50,
* height: 25,
* },
*];
*
*const bounds = getNodesBounds(nodes);
*```
*/
export const getNodesBounds = <NodeType extends NodeBase = NodeBase>(
nodes: (NodeType | InternalNodeBase<NodeType> | string)[],
@@ -238,10 +296,30 @@ export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
};
/**
* Get all connecting edges for a given set of nodes
* This utility filters an array of edges, keeping only those where either the source or target
* node is present in the given array of nodes.
* @public
* @param nodes - Nodes you want to get the connected edges for
* @param edges - All edges
* @returns Array of edges that connect any of the given nodes with each other
*
* @example
* ```js
*import { getConnectedEdges } from '@xyflow/react';
*
*const nodes = [
* { id: 'a', position: { x: 0, y: 0 } },
* { id: 'b', position: { x: 100, y: 0 } },
*];
*
*const edges = [
* { id: 'a->c', source: 'a', target: 'c' },
* { id: 'c->d', source: 'c', target: 'd' },
*];
*
*const connectedEdges = getConnectedEdges(nodes, edges);
* // => [{ id: 'a->c', source: 'a', target: 'c' }]
*```
*/
export const getConnectedEdges = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
nodes: NodeType[],

View File

@@ -15,8 +15,10 @@ export function getNodeToolbarTransform(
alignmentOffset = 1;
}
// position === Position.Top
// we set the x any y position of the toolbar based on the nodes position
/*
* 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) * viewport.zoom + viewport.x,
nodeRect.y * viewport.zoom + viewport.y - offset,

View File

@@ -276,8 +276,10 @@ export function handleExpandParent(
},
});
// We move all child nodes in the oppsite direction
// so the x,y changes of the parent do not move the children
/*
* We move all child nodes in the oppsite direction
* so the x,y changes of the parent do not move the children
*/
parentLookup.get(parentId)?.forEach((childNode) => {
if (!children.some((child) => child.id === childNode.id)) {
changes.push({
@@ -472,9 +474,11 @@ function addConnectionToLookup(
nodeId: string,
handleId: string | null
) {
// We add the connection to the connectionLookup at the following keys
// 1. nodeId, 2. nodeId-type, 3. nodeId-type-handleId
// If the key already exists, we add the connection to the existing map
/*
* We add the connection to the connectionLookup at the following keys
* 1. nodeId, 2. nodeId-type, 3. nodeId-type-handleId
* If the key already exists, we add the connection to the existing map
*/
let key = nodeId;
const nodeMap = connectionLookup.get(key) || new Map();
connectionLookup.set(key, nodeMap.set(connectionKey, connection));

View File

@@ -140,8 +140,10 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
for (const [id, dragItem] of dragItems) {
if (!nodeLookup.has(id)) {
// if the node is not in the nodeLookup anymore, it was probably deleted while dragging
// and we don't need to update it anymore
/*
* if the node is not in the nodeLookup anymore, it was probably deleted while dragging
* and we don't need to update it anymore
*/
continue;
}
@@ -150,8 +152,10 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
nextPosition = snapPosition(nextPosition, snapGrid);
}
// if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node
// based on its position so that the node stays at it's position relative to the selection.
/*
* if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node
* based on its position so that the node stays at it's position relative to the selection.
*/
let adjustedNodeExtent: CoordinateExtent = [
[nodeExtent[0][0], nodeExtent[0][1]],
[nodeExtent[1][0], nodeExtent[1][1]],

View File

@@ -74,9 +74,11 @@ export function getDragItems<NodeType extends NodeBase>(
return dragItems;
}
// returns two params:
// 1. the dragged node (or the first of the list, if we are dragging a node selection)
// 2. array of selected nodes (for multi selections)
/*
* returns two params:
* 1. the dragged node (or the first of the list, if we are dragging a node selection)
* 2. array of selected nodes (for multi selections)
*/
export function getEventHandlerParams<NodeType extends InternalNodeBase>({
nodeId,
dragItems,
@@ -112,10 +114,10 @@ export function getEventHandlerParams<NodeType extends InternalNodeBase>({
!node
? nodesFromDragItems[0]
: {
...node,
position: dragItems.get(nodeId)?.position || node.position,
dragging,
},
...node,
position: dragItems.get(nodeId)?.position || node.position,
dragging,
},
nodesFromDragItems,
];
}

View File

@@ -165,8 +165,10 @@ function onPointerDown(
toNode: result.toHandle ? nodeLookup.get(result.toHandle.nodeId)! : null,
};
// we don't want to trigger an update when the connection
// is snapped to the same handle as before
/*
* we don't want to trigger an update when the connection
* is snapped to the same handle as before
*/
if (
isValid &&
closestHandle &&
@@ -190,8 +192,10 @@ function onPointerDown(
onConnect?.(connection);
}
// it's important to get a fresh reference from the store here
// in order to get the latest state of onConnectEnd
/*
* it's important to get a fresh reference from the store here
* in order to get the latest state of onConnectEnd
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { inProgress, ...connectionState } = previousConnection;
const finalConnectionState = {
@@ -248,8 +252,10 @@ function isValidHandle(
const { x, y } = getEventPosition(event);
const handleBelow = doc.elementFromPoint(x, y);
// we always want to prioritize the handle below the mouse cursor over the closest distance handle,
// because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
/*
* we always want to prioritize the handle below the mouse cursor over the closest distance handle,
* because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
*/
const handleToCheck = handleBelow?.classList.contains(`${lib}-flow__handle`) ? handleBelow : handleDomNode;
const result: Result = {

View File

@@ -19,8 +19,10 @@ function getNodesWithinDistance(position: XYPosition, nodeLookup: NodeLookup, di
return nodes;
}
// this distance is used for the area around the user pointer
// while doing a connection for finding the closest nodes
/*
* this distance is used for the area around the user pointer
* while doing a connection for finding the closest nodes
*/
const ADDITIONAL_DISTANCE = 250;
export function getClosestHandle(

View File

@@ -114,22 +114,22 @@ export function XYPanZoom({
const wheelHandler = isPanOnScroll
? createPanOnScrollHandler({
zoomPanValues,
noWheelClassName,
d3Selection,
d3Zoom: d3ZoomInstance,
panOnScrollMode,
panOnScrollSpeed,
zoomOnPinch,
onPanZoomStart,
onPanZoom,
onPanZoomEnd,
})
zoomPanValues,
noWheelClassName,
d3Selection,
d3Zoom: d3ZoomInstance,
panOnScrollMode,
panOnScrollSpeed,
zoomOnPinch,
onPanZoomStart,
onPanZoom,
onPanZoomEnd,
})
: createZoomOnScrollHandler({
noWheelClassName,
preventScrolling,
d3ZoomHandler,
});
noWheelClassName,
preventScrolling,
d3ZoomHandler,
});
d3Selection.on('wheel.zoom', wheelHandler, { passive: false });
@@ -178,9 +178,11 @@ export function XYPanZoom({
});
d3ZoomInstance.filter(filter);
// We cannot add zoomOnDoubleClick to the filter above because
// double tapping on touch screens circumvents the filter and
// dblclick.zoom is fired on the selection directly
/*
* We cannot add zoomOnDoubleClick to the filter above because
* double tapping on touch screens circumvents the filter and
* dblclick.zoom is fired on the selection directly
*/
if (zoomOnDoubleClick) {
d3Selection.on('dblclick.zoom', d3DblClickZoomHandler);
} else {

View File

@@ -90,8 +90,10 @@ export function createPanOnScrollHandler({
return;
}
// increase scroll speed in firefox
// firefox: deltaMode === 1; chrome: deltaMode === 0
/*
* increase scroll speed in firefox
* firefox: deltaMode === 1; chrome: deltaMode === 0
*/
const deltaNormalize = event.deltaMode === 1 ? 20 : 1;
let deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize;
let deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;
@@ -114,9 +116,11 @@ export function createPanOnScrollHandler({
clearTimeout(zoomPanValues.panScrollTimeout);
// for pan on scroll we need to handle the event calls on our own
// we can't use the start, zoom and end events from d3-zoom
// because start and move gets called on every scroll event and not once at the beginning
/*
* for pan on scroll we need to handle the event calls on our own
* we can't use the start, zoom and end events from d3-zoom
* because start and move gets called on every scroll event and not once at the beginning
*/
if (!zoomPanValues.isPanScrolling) {
zoomPanValues.isPanScrolling = true;

View File

@@ -154,8 +154,10 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
parentExtent = parentNode && node.extent === 'parent' ? nodeToParentExtent(parentNode) : undefined;
}
// Collect all child nodes to correct their relative positions when top/left changes
// Determine largest minimal extent the parent node is allowed to resize to
/*
* Collect all child nodes to correct their relative positions when top/left changes
* Determine largest minimal extent the parent node is allowed to resize to
*/
childNodes = [];
childExtent = undefined;
@@ -230,8 +232,10 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
prevValues.x = change.x;
prevValues.y = change.y;
// when top/left changes, correct the relative positions of child nodes
// so that they stay in the same position
/*
* when top/left changes, correct the relative positions of child nodes
* so that they stay in the same position
*/
if (childNodes.length > 0) {
const xChange = x - prevX;
const yChange = y - prevY;

View File

@@ -11,10 +11,25 @@ export type ResizeParamsWithDirection = ResizeParams & {
direction: number[];
};
/**
* Used to determine the control line position of the NodeResizer
*
* @public
*/
export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right';
/**
* Used to determine the control position of the NodeResizer
*
* @public
*/
export type ControlPosition = ControlLinePosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
/**
* Used to determine the variant of the resize control
*
* @public
*/
export enum ResizeControlVariant {
Line = 'line',
Handle = 'handle',

View File

@@ -5,11 +5,11 @@
"license": "MIT",
"main": "src/index.js",
"devDependencies": {
"eslint-config-prettier": "^10.0.1",
"eslint-config-turbo": "^2.4.0",
"eslint-plugin-react": "^7.37.4",
"@typescript-eslint/eslint-plugin": "^8.23.0",
"@typescript-eslint/parser": "^8.23.0",
"eslint-plugin-prettier": "^4.2.1"
"eslint-config-prettier": "^10.0.1",
"eslint-config-turbo": "^2.4.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.37.4"
}
}