"use client"
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
import { createContext, useContext, useMemo, forwardRef, useEffect, useRef, useState, useLayoutEffect, useCallback, memo } from 'react';
import cc from 'classcat';
import { errorMessages, mergeAriaLabelConfig, infiniteExtent, isInputDOMNode, getViewportForBounds, pointToRendererPoint, rendererPointToPoint, isNodeBase, isEdgeBase, getElementsToRemove, isRectObject, nodeToRect, getOverlappingArea, getNodesBounds, withResolvers, evaluateAbsolutePosition, getDimensions, XYPanZoom, PanOnScrollMode, SelectionMode, getEventPosition, getNodesInside, areSetsEqual, XYDrag, snapPosition, calculateNodePosition, Position, ConnectionMode, isMouseEvent, XYHandle, getHostForElement, addEdge, getInternalNodesBounds, isNumeric, nodeHasDimensions, getNodeDimensions, elementSelectionKeys, isEdgeVisible, MarkerType, createMarkerIds, getBezierEdgeCenter, getSmoothStepPath, getStraightPath, getBezierPath, getEdgePosition, getElevatedEdgeZIndex, getMarkerId, getConnectionStatus, ConnectionLineType, updateConnectionLookup, adoptUserNodes, initialConnection, devWarn, defaultAriaLabelConfig, updateNodeInternals, updateAbsolutePositions, getHandlePosition, handleExpandParent, panBy, fitViewport, isMacOs, areConnectionMapsEqual, handleConnectionChange, shallowNodeData, XYMinimap, getBoundsOfRects, ResizeControlVariant, XYResizer, XY_RESIZER_LINE_POSITIONS, XY_RESIZER_HANDLE_POSITIONS, getNodeToolbarTransform, getEdgeToolbarTransform } from '@xyflow/system';
export { ConnectionLineType, ConnectionMode, MarkerType, PanOnScrollMode, Position, ResizeControlVariant, SelectionMode, addEdge, getBezierEdgeCenter, getBezierPath, getConnectedEdges, getEdgeCenter, getIncomers, getNodesBounds, getOutgoers, getSmoothStepPath, getStraightPath, getViewportForBounds, reconnectEdge } from '@xyflow/system';
import { useStoreWithEqualityFn, createWithEqualityFn } from 'zustand/traditional';
import { shallow } from 'zustand/shallow';
import { createPortal } from 'react-dom';
const StoreContext = createContext(null);
const Provider$1 = StoreContext.Provider;
const zustandErrorMessage = errorMessages['error001']();
/**
* 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 - A selector function that returns a slice of the flow's internal state.
* Extracting or transforming just the state you need is a good practice to avoid unnecessary
* re-renders.
* @param equalityFn - A function to compare the previous and next value. This is incredibly useful
* for preventing unnecessary re-renders. Good sensible defaults are using `Object.is` or importing
* `zustand/shallow`, but you can be as granular as you like.
* @returns The selected state slice.
*
* @example
* ```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(selector, equalityFn) {
const store = useContext(StoreContext);
if (store === null) {
throw new Error(zustandErrorMessage);
}
return useStoreWithEqualityFn(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() {
const store = useContext(StoreContext);
if (store === null) {
throw new Error(zustandErrorMessage);
}
return useMemo(() => ({
getState: store.getState,
setState: store.setState,
subscribe: store.subscribe,
}), [store]);
}
const style = { display: 'none' };
const ariaLiveStyle = {
position: 'absolute',
width: 1,
height: 1,
margin: -1,
border: 0,
padding: 0,
overflow: 'hidden',
clip: 'rect(0px, 0px, 0px, 0px)',
clipPath: 'inset(100%)',
};
const ARIA_NODE_DESC_KEY = 'react-flow__node-desc';
const ARIA_EDGE_DESC_KEY = 'react-flow__edge-desc';
const ARIA_LIVE_MESSAGE = 'react-flow__aria-live';
const ariaLiveSelector = (s) => s.ariaLiveMessage;
const ariaLabelConfigSelector = (s) => s.ariaLabelConfig;
function AriaLiveMessage({ rfId }) {
const ariaLiveMessage = useStore(ariaLiveSelector);
return (jsx("div", { id: `${ARIA_LIVE_MESSAGE}-${rfId}`, "aria-live": "assertive", "aria-atomic": "true", style: ariaLiveStyle, children: ariaLiveMessage }));
}
function A11yDescriptions({ rfId, disableKeyboardA11y }) {
const ariaLabelConfig = useStore(ariaLabelConfigSelector);
return (jsxs(Fragment, { children: [jsx("div", { id: `${ARIA_NODE_DESC_KEY}-${rfId}`, style: style, children: disableKeyboardA11y
? ariaLabelConfig['node.a11yDescription.default']
: ariaLabelConfig['node.a11yDescription.keyboardDisabled'] }), jsx("div", { id: `${ARIA_EDGE_DESC_KEY}-${rfId}`, style: style, children: ariaLabelConfig['edge.a11yDescription.default'] }), !disableKeyboardA11y && jsx(AriaLiveMessage, { rfId: rfId })] }));
}
/**
* The `` component helps you position content above the viewport.
* It is used internally by the [``](/api-reference/components/minimap)
* and [``](/api-reference/components/controls) components.
*
* @public
*
* @example
* ```jsx
*import { ReactFlow, Background, Panel } from '@xyflow/react';
*
*export default function Flow() {
* return (
*
* top-left
* top-center
* top-right
* bottom-left
* bottom-center
* bottom-right
*
* );
*}
*```
*/
const Panel = forwardRef(({ position = 'top-left', children, className, style, ...rest }, ref) => {
const positionClasses = `${position}`.split('-');
return (jsx("div", { className: cc(['react-flow__panel', className, ...positionClasses]), style: style, ref: ref, ...rest, children: children }));
});
Panel.displayName = 'Panel';
function Attribution({ proOptions, position = 'bottom-right' }) {
if (proOptions?.hideAttribution) {
return null;
}
return (jsx(Panel, { position: position, className: "react-flow__attribution", "data-message": "Please only hide this attribution when you are subscribed to React Flow Pro: https://pro.reactflow.dev", children: jsx("a", { href: "https://reactflow.dev", target: "_blank", rel: "noopener noreferrer", "aria-label": "React Flow attribution", children: "React Flow" }) }));
}
const selector$m = (s) => {
const selectedNodes = [];
const selectedEdges = [];
for (const [, node] of s.nodeLookup) {
if (node.selected) {
selectedNodes.push(node.internals.userNode);
}
}
for (const [, edge] of s.edgeLookup) {
if (edge.selected) {
selectedEdges.push(edge);
}
}
return { selectedNodes, selectedEdges };
};
const selectId = (obj) => obj.id;
function areEqual(a, b) {
return (shallow(a.selectedNodes.map(selectId), b.selectedNodes.map(selectId)) &&
shallow(a.selectedEdges.map(selectId), b.selectedEdges.map(selectId)));
}
function SelectionListenerInner({ onSelectionChange, }) {
const store = useStoreApi();
const { selectedNodes, selectedEdges } = useStore(selector$m, areEqual);
useEffect(() => {
const params = { nodes: selectedNodes, edges: selectedEdges };
onSelectionChange?.(params);
store.getState().onSelectionChangeHandlers.forEach((fn) => fn(params));
}, [selectedNodes, selectedEdges, onSelectionChange]);
return null;
}
const changeSelector = (s) => !!s.onSelectionChangeHandlers;
function SelectionListener({ onSelectionChange, }) {
const storeHasSelectionChangeHandlers = useStore(changeSelector);
if (onSelectionChange || storeHasSelectionChangeHandlers) {
return jsx(SelectionListenerInner, { onSelectionChange: onSelectionChange });
}
return null;
}
const defaultNodeOrigin = [0, 0];
const defaultViewport = { x: 0, y: 0, zoom: 1 };
/*
* This component helps us to update the store with the values coming from the user.
* We distinguish between values we can update directly with `useDirectStoreUpdater` (like `snapGrid`)
* and values that have a dedicated setter function in the store (like `setNodes`).
*/
// These fields exist in the global store, and we need to keep them up to date
const reactFlowFieldsToTrack = [
'nodes',
'edges',
'defaultNodes',
'defaultEdges',
'onConnect',
'onConnectStart',
'onConnectEnd',
'onClickConnectStart',
'onClickConnectEnd',
'nodesDraggable',
'autoPanOnNodeFocus',
'nodesConnectable',
'nodesFocusable',
'edgesFocusable',
'edgesReconnectable',
'elevateNodesOnSelect',
'elevateEdgesOnSelect',
'minZoom',
'maxZoom',
'nodeExtent',
'onNodesChange',
'onEdgesChange',
'elementsSelectable',
'connectionMode',
'snapGrid',
'snapToGrid',
'translateExtent',
'connectOnClick',
'defaultEdgeOptions',
'fitView',
'fitViewOptions',
'onNodesDelete',
'onEdgesDelete',
'onDelete',
'onNodeDrag',
'onNodeDragStart',
'onNodeDragStop',
'onSelectionDrag',
'onSelectionDragStart',
'onSelectionDragStop',
'onMoveStart',
'onMove',
'onMoveEnd',
'noPanClassName',
'nodeOrigin',
'autoPanOnConnect',
'autoPanOnNodeDrag',
'onError',
'connectionRadius',
'isValidConnection',
'selectNodesOnDrag',
'nodeDragThreshold',
'connectionDragThreshold',
'onBeforeDelete',
'debug',
'autoPanSpeed',
'ariaLabelConfig',
'zIndexMode',
];
// rfId doesn't exist in ReactFlowProps, but it's one of the fields we want to update
const fieldsToTrack = [...reactFlowFieldsToTrack, 'rfId'];
const selector$l = (s) => ({
setNodes: s.setNodes,
setEdges: s.setEdges,
setMinZoom: s.setMinZoom,
setMaxZoom: s.setMaxZoom,
setTranslateExtent: s.setTranslateExtent,
setNodeExtent: s.setNodeExtent,
reset: s.reset,
setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
});
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.
*/
translateExtent: infiniteExtent,
nodeOrigin: defaultNodeOrigin,
minZoom: 0.5,
maxZoom: 2,
elementsSelectable: true,
noPanClassName: 'nopan',
rfId: '1',
};
function StoreUpdater(props) {
const { setNodes, setEdges, setMinZoom, setMaxZoom, setTranslateExtent, setNodeExtent, reset, setDefaultNodesAndEdges, } = useStore(selector$l, shallow);
const store = useStoreApi();
useEffect(() => {
setDefaultNodesAndEdges(props.defaultNodes, props.defaultEdges);
return () => {
// when we reset the store we also need to reset the previous fields
previousFields.current = initPrevValues;
reset();
};
}, []);
const previousFields = useRef(initPrevValues);
useEffect(() => {
for (const fieldName of fieldsToTrack) {
const fieldValue = props[fieldName];
const previousFieldValue = previousFields.current[fieldName];
if (fieldValue === previousFieldValue)
continue;
if (typeof props[fieldName] === 'undefined')
continue;
// Custom handling with dedicated setters for some fields
if (fieldName === 'nodes')
setNodes(fieldValue);
else if (fieldName === 'edges')
setEdges(fieldValue);
else if (fieldName === 'minZoom')
setMinZoom(fieldValue);
else if (fieldName === 'maxZoom')
setMaxZoom(fieldValue);
else if (fieldName === 'translateExtent')
setTranslateExtent(fieldValue);
else if (fieldName === 'nodeExtent')
setNodeExtent(fieldValue);
else if (fieldName === 'ariaLabelConfig')
store.setState({ ariaLabelConfig: mergeAriaLabelConfig(fieldValue) });
// Renamed fields
else if (fieldName === 'fitView')
store.setState({ fitViewQueued: fieldValue });
else if (fieldName === 'fitViewOptions')
store.setState({ fitViewOptions: fieldValue });
// General case
else
store.setState({ [fieldName]: fieldValue });
}
previousFields.current = props;
},
// Only re-run the effect if one of the fields we track changes
fieldsToTrack.map((fieldName) => props[fieldName]));
return null;
}
function getMediaQuery() {
if (typeof window === 'undefined' || !window.matchMedia) {
return null;
}
return window.matchMedia('(prefers-color-scheme: dark)');
}
/**
* Hook for receiving the current color mode class 'dark' or 'light'.
*
* @internal
* @param colorMode - The color mode to use ('dark', 'light' or 'system')
*/
function useColorModeClass(colorMode) {
const [colorModeClass, setColorModeClass] = useState(colorMode === 'system' ? null : colorMode);
useEffect(() => {
if (colorMode !== 'system') {
setColorModeClass(colorMode);
return;
}
const mediaQuery = getMediaQuery();
const updateColorModeClass = () => setColorModeClass(mediaQuery?.matches ? 'dark' : 'light');
updateColorModeClass();
mediaQuery?.addEventListener('change', updateColorModeClass);
return () => {
mediaQuery?.removeEventListener('change', updateColorModeClass);
};
}, [colorMode]);
return colorModeClass !== null ? colorModeClass : getMediaQuery()?.matches ? 'dark' : 'light';
}
const defaultDoc = typeof document !== 'undefined' ? document : null;
/**
* This hook lets you listen for specific key codes and tells you whether they are
* currently pressed or not.
*
* @public
* @param options - Options
*
* @example
* ```tsx
*import { useKeyPress } from '@xyflow/react';
*
*export default function () {
* const spacePressed = useKeyPress('Space');
* const cmdAndSPressed = useKeyPress(['Meta+s', 'Strg+s']);
*
* return (
*
* {spacePressed &&
Space pressed!
}
* {cmdAndSPressed &&
Cmd + S pressed!
}
*
* );
*}
*```
*/
function useKeyPress(
/**
* The key code (string or array of strings) specifies which key(s) should trigger
* an action.
*
* A **string** can represent:
* - A **single key**, e.g. `'a'`
* - A **key combination**, using `'+'` to separate keys, e.g. `'a+d'`
*
* An **array of strings** represents **multiple possible key inputs**. For example, `['a', 'd+s']`
* means the user can press either the single key `'a'` or the combination of `'d'` and `'s'`.
* @default null
*/
keyCode = null, options = { target: defaultDoc, actInsideInputWithModifier: true }) {
const [keyPressed, setKeyPressed] = useState(false);
// we need to remember if a modifier key is pressed in order to track it
const modifierPressed = useRef(false);
// we need to remember the pressed keys in order to support combinations
const pressedKeys = useRef(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.
*/
const [keyCodes, keysToWatch] = useMemo(() => {
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
*/
.map((kc) => kc.replace('+', '\n').replace('\n\n', '\n+').split('\n'));
const keysFlat = keys.reduce((res, item) => res.concat(...item), []);
return [keys, keysFlat];
}
return [[], []];
}, [keyCode]);
useEffect(() => {
const target = options?.target ?? defaultDoc;
const actInsideInputWithModifier = options?.actInsideInputWithModifier ?? true;
if (keyCode !== null) {
const downHandler = (event) => {
modifierPressed.current = event.ctrlKey || event.metaKey || event.shiftKey || event.altKey;
const preventAction = (!modifierPressed.current || (modifierPressed.current && !actInsideInputWithModifier)) &&
isInputDOMNode(event);
if (preventAction) {
return false;
}
const keyOrCode = useKeyOrCode(event.code, keysToWatch);
pressedKeys.current.add(event[keyOrCode]);
if (isMatchingKey(keyCodes, pressedKeys.current, false)) {
const target = (event.composedPath?.()?.[0] || event.target);
const isInteractiveElement = target?.nodeName === 'BUTTON' || target?.nodeName === 'A';
if (options.preventDefault !== false && (modifierPressed.current || !isInteractiveElement)) {
event.preventDefault();
}
setKeyPressed(true);
}
};
const upHandler = (event) => {
const keyOrCode = useKeyOrCode(event.code, keysToWatch);
if (isMatchingKey(keyCodes, pressedKeys.current, true)) {
setKeyPressed(false);
pressedKeys.current.clear();
}
else {
pressedKeys.current.delete(event[keyOrCode]);
}
// fix for Mac: when cmd key is pressed, keyup is not triggered for any other key, see: https://stackoverflow.com/questions/27380018/when-cmd-key-is-kept-pressed-keyup-is-not-triggered-for-any-other-key
if (event.key === 'Meta') {
pressedKeys.current.clear();
}
modifierPressed.current = false;
};
const resetHandler = () => {
pressedKeys.current.clear();
setKeyPressed(false);
};
target?.addEventListener('keydown', downHandler);
target?.addEventListener('keyup', upHandler);
window.addEventListener('blur', resetHandler);
window.addEventListener('contextmenu', resetHandler);
return () => {
target?.removeEventListener('keydown', downHandler);
target?.removeEventListener('keyup', upHandler);
window.removeEventListener('blur', resetHandler);
window.removeEventListener('contextmenu', resetHandler);
};
}
}, [keyCode, setKeyPressed]);
return keyPressed;
}
// utils
function isMatchingKey(keyCodes, pressedKeys, isUp) {
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'
*/
.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
*/
.some((keys) => keys.every((k) => pressedKeys.has(k))));
}
function useKeyOrCode(eventCode, keysToWatch) {
return keysToWatch.includes(eventCode) ? 'code' : 'key';
}
/**
* Hook for getting viewport helper functions.
*
* @internal
* @returns viewport helper functions
*/
const useViewportHelper = () => {
const store = useStoreApi();
return useMemo(() => {
return {
zoomIn: (options) => {
const { panZoom } = store.getState();
return panZoom ? panZoom.scaleBy(1.2, { duration: options?.duration }) : Promise.resolve(false);
},
zoomOut: (options) => {
const { panZoom } = store.getState();
return panZoom ? panZoom.scaleBy(1 / 1.2, { duration: options?.duration }) : Promise.resolve(false);
},
zoomTo: (zoomLevel, options) => {
const { panZoom } = store.getState();
return panZoom ? panZoom.scaleTo(zoomLevel, { duration: options?.duration }) : Promise.resolve(false);
},
getZoom: () => store.getState().transform[2],
setViewport: async (viewport, options) => {
const { transform: [tX, tY, tZoom], panZoom, } = store.getState();
if (!panZoom) {
return Promise.resolve(false);
}
await panZoom.setViewport({
x: viewport.x ?? tX,
y: viewport.y ?? tY,
zoom: viewport.zoom ?? tZoom,
}, options);
return Promise.resolve(true);
},
getViewport: () => {
const [x, y, zoom] = store.getState().transform;
return { x, y, zoom };
},
setCenter: async (x, y, options) => {
return store.getState().setCenter(x, y, options);
},
fitBounds: async (bounds, options) => {
const { width, height, minZoom, maxZoom, panZoom } = store.getState();
const viewport = getViewportForBounds(bounds, width, height, minZoom, maxZoom, options?.padding ?? 0.1);
if (!panZoom) {
return Promise.resolve(false);
}
await panZoom.setViewport(viewport, {
duration: options?.duration,
ease: options?.ease,
interpolate: options?.interpolate,
});
return Promise.resolve(true);
},
screenToFlowPosition: (clientPosition, options = {}) => {
const { transform, snapGrid, snapToGrid, domNode } = store.getState();
if (!domNode) {
return clientPosition;
}
const { x: domX, y: domY } = domNode.getBoundingClientRect();
const correctedPosition = {
x: clientPosition.x - domX,
y: clientPosition.y - domY,
};
const _snapGrid = options.snapGrid ?? snapGrid;
const _snapToGrid = options.snapToGrid ?? snapToGrid;
return pointToRendererPoint(correctedPosition, transform, _snapToGrid, _snapGrid);
},
flowToScreenPosition: (flowPosition) => {
const { transform, domNode } = store.getState();
if (!domNode) {
return flowPosition;
}
const { x: domX, y: domY } = domNode.getBoundingClientRect();
const rendererPosition = rendererPointToPoint(flowPosition, transform);
return {
x: rendererPosition.x + domX,
y: rendererPosition.y + domY,
};
},
};
}, []);
};
/*
* 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, elements) {
const updatedElements = [];
/*
* 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();
const addItemChanges = [];
for (const change of changes) {
if (change.type === 'add') {
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!
*/
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.
*/
elementChanges.push(change);
}
else {
changesMap.set(change.id, [change]);
}
}
}
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.
*/
if (!changes) {
updatedElements.push(element);
continue;
}
// If we have a 'remove' change queued, it'll be the only change in the array
if (changes[0].type === 'remove') {
continue;
}
if (changes[0].type === 'replace') {
updatedElements.push({ ...changes[0].item });
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.
*/
const updatedElement = { ...element };
for (const change of changes) {
applyChange(change, updatedElement);
}
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
*/
if (addItemChanges.length) {
addItemChanges.forEach((change) => {
if (change.index !== undefined) {
updatedElements.splice(change.index, 0, { ...change.item });
}
else {
updatedElements.push({ ...change.item });
}
});
}
return updatedElements;
}
// Applies a single change to an element. This is a *mutable* update.
function applyChange(change, element) {
switch (change.type) {
case 'select': {
element.selected = change.selected;
break;
}
case 'position': {
if (typeof change.position !== 'undefined') {
element.position = change.position;
}
if (typeof change.dragging !== 'undefined') {
element.dragging = change.dragging;
}
break;
}
case 'dimensions': {
if (typeof change.dimensions !== 'undefined') {
element.measured = {
...change.dimensions,
};
if (change.setAttributes) {
if (change.setAttributes === true || change.setAttributes === 'width') {
element.width = change.dimensions.width;
}
if (change.setAttributes === true || change.setAttributes === 'height') {
element.height = change.dimensions.height;
}
}
}
if (typeof change.resizing === 'boolean') {
element.resizing = change.resizing;
}
break;
}
}
}
/**
* Drop in function that applies node changes to an array of nodes.
* @public
* @param changes - Array of changes to apply.
* @param nodes - Array of nodes to apply the changes to.
* @returns Array of updated nodes.
* @example
*```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([]);
* const [edges, setEdges] = useState([]);
* const onNodesChange: OnNodesChange = useCallback(
* (changes) => {
* setNodes((oldNodes) => applyNodeChanges(changes, oldNodes));
* },
* [setNodes],
* );
*
* return (
*
* );
*}
*```
* @remarks Various events on the 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.
*/
function applyNodeChanges(changes, nodes) {
return applyChanges(changes, nodes);
}
/**
* Drop in function that applies edge changes to an array of edges.
* @public
* @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 (
*
* );
*}
*```
* @remarks Various events on the 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.
*/
function applyEdgeChanges(changes, edges) {
return applyChanges(changes, edges);
}
function createSelectionChange(id, selected) {
return {
id,
type: 'select',
selected,
};
}
function getSelectionChanges(items, selectedIds = new Set(), mutateItem = false) {
const changes = [];
for (const [id, item] of items) {
const willBeSelected = selectedIds.has(id);
// 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 :/
*/
item.selected = willBeSelected;
}
changes.push(createSelectionChange(item.id, willBeSelected));
}
}
return changes;
}
function getElementsDiffChanges({ items = [], lookup, }) {
const changes = [];
const itemsLookup = new Map(items.map((item) => [item.id, item]));
for (const [index, item] of items.entries()) {
const lookupItem = lookup.get(item.id);
const storeItem = lookupItem?.internals?.userNode ?? lookupItem;
if (storeItem !== undefined && storeItem !== item) {
changes.push({ id: item.id, item: item, type: 'replace' });
}
if (storeItem === undefined) {
changes.push({ item: item, type: 'add', index });
}
}
for (const [id] of lookup) {
const nextNode = itemsLookup.get(id);
if (nextNode === undefined) {
changes.push({ id, type: 'remove' });
}
}
return changes;
}
function elementToRemoveChange(item) {
return {
id: item.id,
type: 'remove',
};
}
/**
* Test whether an object is usable 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 Tests whether the provided value can be used as a `Node`. If you're using TypeScript,
* this function acts as a type guard and will narrow the type of the value to `Node` if it returns
* `true`.
*
* @example
* ```js
*import { isNode } from '@xyflow/react';
*
*if (isNode(node)) {
* // ...
*}
*```
*/
const isNode = (element) => isNodeBase(element);
/**
* Test whether an object is usable 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 Tests whether the provided value can be used as an `Edge`. If you're using TypeScript,
* this function acts as a type guard and will narrow the type of the value to `Edge` if it returns
* `true`.
*
* @example
* ```js
*import { isEdge } from '@xyflow/react';
*
*if (isEdge(edge)) {
* // ...
*}
*```
*/
const isEdge = (element) => isEdgeBase(element);
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
function fixedForwardRef(render) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return forwardRef(render);
}
// we need this hook to prevent a warning when using react-flow in SSR
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
/**
* This hook returns a queue that can be used to batch updates.
*
* @param runQueue - a function that gets called when the queue is flushed
* @internal
*
* @returns a Queue object
*/
function useQueue(runQueue) {
/*
* 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.
*/
const [queue] = useState(() => createQueue(() => 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`).
*/
useIsomorphicLayoutEffect(() => {
const queueItems = queue.get();
if (queueItems.length) {
runQueue(queueItems);
queue.reset();
}
}, [serial]);
return queue;
}
function createQueue(cb) {
let queue = [];
return {
get: () => queue,
reset: () => {
queue = [];
},
push: (item) => {
queue.push(item);
cb();
},
};
}
const BatchContext = createContext(null);
/**
* This is a context provider that holds and processes the node and edge update queues
* that are needed to handle setNodes, addNodes, setEdges and addEdges.
*
* @internal
*/
function BatchProvider({ children, }) {
const store = useStoreApi();
const nodeQueueHandler = useCallback((queueItems) => {
const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup, fitViewQueued, onNodesChangeMiddlewareMap, } = 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.
*/
let next = nodes;
for (const payload of queueItems) {
next = typeof payload === 'function' ? payload(next) : payload;
}
let changes = getElementsDiffChanges({
items: next,
lookup: nodeLookup,
});
for (const middleware of onNodesChangeMiddlewareMap.values()) {
changes = middleware(changes);
}
if (hasDefaultNodes) {
setNodes(next);
}
// We only want to fire onNodesChange if there are changes to the nodes
if (changes.length > 0) {
onNodesChange?.(changes);
}
else if (fitViewQueued) {
// If there are no changes to the nodes, we still need to call setNodes
// to trigger a re-render and fitView.
window.requestAnimationFrame(() => {
const { fitViewQueued, nodes, setNodes } = store.getState();
if (fitViewQueued) {
setNodes(nodes);
}
});
}
}, []);
const nodeQueue = useQueue(nodeQueueHandler);
const edgeQueueHandler = useCallback((queueItems) => {
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange, edgeLookup } = store.getState();
let next = edges;
for (const payload of queueItems) {
next = typeof payload === 'function' ? payload(next) : payload;
}
if (hasDefaultEdges) {
setEdges(next);
}
else if (onEdgesChange) {
onEdgesChange(getElementsDiffChanges({
items: next,
lookup: edgeLookup,
}));
}
}, []);
const edgeQueue = useQueue(edgeQueueHandler);
const value = useMemo(() => ({ nodeQueue, edgeQueue }), []);
return jsx(BatchContext.Provider, { value: value, children: children });
}
function useBatchContext() {
const batchContext = useContext(BatchContext);
if (!batchContext) {
throw new Error('useBatchContext must be used within a BatchProvider');
}
return batchContext;
}
const selector$k = (s) => !!s.panZoom;
/**
* 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
* @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 (
*
*
*
There are {count} nodes in the flow.
*
* );
*}
*```
*/
function useReactFlow() {
const viewportHelper = useViewportHelper();
const store = useStoreApi();
const batchContext = useBatchContext();
const viewportInitialized = useStore(selector$k);
const generalHelper = useMemo(() => {
const getInternalNode = (id) => store.getState().nodeLookup.get(id);
const setNodes = (payload) => {
batchContext.nodeQueue.push(payload);
};
const setEdges = (payload) => {
batchContext.edgeQueue.push(payload);
};
const getNodeRect = (node) => {
const { nodeLookup, nodeOrigin } = store.getState();
const nodeToUse = isNode(node) ? node : nodeLookup.get(node.id);
const position = nodeToUse.parentId
? evaluateAbsolutePosition(nodeToUse.position, nodeToUse.measured, nodeToUse.parentId, nodeLookup, nodeOrigin)
: nodeToUse.position;
const nodeWithPosition = {
...nodeToUse,
position,
width: nodeToUse.measured?.width ?? nodeToUse.width,
height: nodeToUse.measured?.height ?? nodeToUse.height,
};
return nodeToRect(nodeWithPosition);
};
const updateNode = (id, nodeUpdate, options = { replace: false }) => {
setNodes((prevNodes) => prevNodes.map((node) => {
if (node.id === id) {
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node) : nodeUpdate;
return options.replace && isNode(nextNode) ? nextNode : { ...node, ...nextNode };
}
return node;
}));
};
const updateEdge = (id, edgeUpdate, options = { replace: false }) => {
setEdges((prevEdges) => prevEdges.map((edge) => {
if (edge.id === id) {
const nextEdge = typeof edgeUpdate === 'function' ? edgeUpdate(edge) : edgeUpdate;
return options.replace && isEdge(nextEdge) ? nextEdge : { ...edge, ...nextEdge };
}
return edge;
}));
};
return {
getNodes: () => store.getState().nodes.map((n) => ({ ...n })),
getNode: (id) => getInternalNode(id)?.internals.userNode,
getInternalNode,
getEdges: () => {
const { edges = [] } = store.getState();
return edges.map((e) => ({ ...e }));
},
getEdge: (id) => store.getState().edgeLookup.get(id),
setNodes,
setEdges,
addNodes: (payload) => {
const newNodes = Array.isArray(payload) ? payload : [payload];
batchContext.nodeQueue.push((nodes) => [...nodes, ...newNodes]);
},
addEdges: (payload) => {
const newEdges = Array.isArray(payload) ? payload : [payload];
batchContext.edgeQueue.push((edges) => [...edges, ...newEdges]);
},
toObject: () => {
const { nodes = [], edges = [], transform } = store.getState();
const [x, y, zoom] = transform;
return {
nodes: nodes.map((n) => ({ ...n })),
edges: edges.map((e) => ({ ...e })),
viewport: {
x,
y,
zoom,
},
};
},
deleteElements: async ({ nodes: nodesToRemove = [], edges: edgesToRemove = [] }) => {
const { nodes, edges, onNodesDelete, onEdgesDelete, triggerNodeChanges, triggerEdgeChanges, onDelete, onBeforeDelete, } = store.getState();
const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({
nodesToRemove,
edgesToRemove,
nodes,
edges,
onBeforeDelete,
});
const hasMatchingEdges = matchingEdges.length > 0;
const hasMatchingNodes = matchingNodes.length > 0;
if (hasMatchingEdges) {
const edgeChanges = matchingEdges.map(elementToRemoveChange);
onEdgesDelete?.(matchingEdges);
triggerEdgeChanges(edgeChanges);
}
if (hasMatchingNodes) {
const nodeChanges = matchingNodes.map(elementToRemoveChange);
onNodesDelete?.(matchingNodes);
triggerNodeChanges(nodeChanges);
}
if (hasMatchingNodes || hasMatchingEdges) {
onDelete?.({ nodes: matchingNodes, edges: matchingEdges });
}
return { deletedNodes: matchingNodes, deletedEdges: matchingEdges };
},
/**
* Partial is defined as "the 2 nodes/areas are intersecting partially".
* If a is contained in b or b is contained in a, they are both
* considered fully intersecting.
*/
getIntersectingNodes: (nodeOrRect, partially = true, nodes) => {
const isRect = isRectObject(nodeOrRect);
const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect);
const hasNodesOption = nodes !== undefined;
if (!nodeRect) {
return [];
}
return (nodes || store.getState().nodes).filter((n) => {
const internalNode = store.getState().nodeLookup.get(n.id);
if (internalNode && !isRect && (n.id === nodeOrRect.id || !internalNode.internals.positionAbsolute)) {
return false;
}
const currNodeRect = nodeToRect(hasNodesOption ? n : internalNode);
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
const partiallyVisible = partially && overlappingArea > 0;
return (partiallyVisible ||
overlappingArea >= currNodeRect.width * currNodeRect.height ||
overlappingArea >= nodeRect.width * nodeRect.height);
});
},
isNodeIntersecting: (nodeOrRect, area, partially = true) => {
const isRect = isRectObject(nodeOrRect);
const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect);
if (!nodeRect) {
return false;
}
const overlappingArea = getOverlappingArea(nodeRect, area);
const partiallyVisible = partially && overlappingArea > 0;
return (partiallyVisible ||
overlappingArea >= area.width * area.height ||
overlappingArea >= nodeRect.width * nodeRect.height);
},
updateNode,
updateNodeData: (id, dataUpdate, options = { replace: false }) => {
updateNode(id, (node) => {
const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate;
return options.replace ? { ...node, data: nextData } : { ...node, data: { ...node.data, ...nextData } };
}, options);
},
updateEdge,
updateEdgeData: (id, dataUpdate, options = { replace: false }) => {
updateEdge(id, (edge) => {
const nextData = typeof dataUpdate === 'function' ? dataUpdate(edge) : dataUpdate;
return options.replace ? { ...edge, data: nextData } : { ...edge, data: { ...edge.data, ...nextData } };
}, options);
},
getNodesBounds: (nodes) => {
const { nodeLookup, nodeOrigin } = store.getState();
return getNodesBounds(nodes, { nodeLookup, nodeOrigin });
},
getHandleConnections: ({ type, id, nodeId }) => Array.from(store
.getState()
.connectionLookup.get(`${nodeId}-${type}${id ? `-${id}` : ''}`)
?.values() ?? []),
getNodeConnections: ({ type, handleId, nodeId }) => Array.from(store
.getState()
.connectionLookup.get(`${nodeId}${type ? (handleId ? `-${type}-${handleId}` : `-${type}`) : ''}`)
?.values() ?? []),
fitView: async (options) => {
// We either create a new Promise or reuse the existing one
// Even if fitView is called multiple times in a row, we only end up with a single Promise
const fitViewResolver = store.getState().fitViewResolver ?? withResolvers();
// We schedule a fitView by setting fitViewQueued and triggering a setNodes
store.setState({ fitViewQueued: true, fitViewOptions: options, fitViewResolver });
batchContext.nodeQueue.push((nodes) => [...nodes]);
return fitViewResolver.promise;
},
};
}, []);
return useMemo(() => {
return {
...generalHelper,
...viewportHelper,
viewportInitialized,
};
}, [viewportInitialized]);
}
const selected = (item) => item.selected;
const win$1 = typeof window !== 'undefined' ? window : undefined;
/**
* Hook for handling global key events.
*
* @internal
*/
function useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode, }) {
const store = useStoreApi();
const { deleteElements } = useReactFlow();
const deleteKeyPressed = useKeyPress(deleteKeyCode, { actInsideInputWithModifier: false });
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode, { target: win$1 });
useEffect(() => {
if (deleteKeyPressed) {
const { edges, nodes } = store.getState();
deleteElements({ nodes: nodes.filter(selected), edges: edges.filter(selected) });
store.setState({ nodesSelectionActive: false });
}
}, [deleteKeyPressed]);
useEffect(() => {
store.setState({ multiSelectionActive: multiSelectionKeyPressed });
}, [multiSelectionKeyPressed]);
}
/**
* Hook for handling resize events.
*
* @internal
*/
function useResizeHandler(domNode) {
const store = useStoreApi();
useEffect(() => {
const updateDimensions = () => {
if (!domNode.current || !(domNode.current.checkVisibility?.() ?? true)) {
return false;
}
const size = getDimensions(domNode.current);
if (size.height === 0 || size.width === 0) {
store.getState().onError?.('004', errorMessages['error004']());
}
store.setState({ width: size.width || 500, height: size.height || 500 });
};
if (domNode.current) {
updateDimensions();
window.addEventListener('resize', updateDimensions);
const resizeObserver = new ResizeObserver(() => updateDimensions());
resizeObserver.observe(domNode.current);
return () => {
window.removeEventListener('resize', updateDimensions);
if (resizeObserver && domNode.current) {
resizeObserver.unobserve(domNode.current);
}
};
}
}, []);
}
const containerStyle = {
position: 'absolute',
width: '100%',
height: '100%',
top: 0,
left: 0,
};
const selector$j = (s) => ({
userSelectionActive: s.userSelectionActive,
lib: s.lib,
connectionInProgress: s.connection.inProgress,
});
function ZoomPane({ onPaneContextMenu, zoomOnScroll = true, zoomOnPinch = true, panOnScroll = false, panOnScrollSpeed = 0.5, panOnScrollMode = PanOnScrollMode.Free, zoomOnDoubleClick = true, panOnDrag = true, defaultViewport, translateExtent, minZoom, maxZoom, zoomActivationKeyCode, preventScrolling = true, children, noWheelClassName, noPanClassName, onViewportChange, isControlledViewport, paneClickDistance, selectionOnDrag, }) {
const store = useStoreApi();
const zoomPane = useRef(null);
const { userSelectionActive, lib, connectionInProgress } = useStore(selector$j, shallow);
const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode);
const panZoom = useRef();
useResizeHandler(zoomPane);
const onTransformChange = useCallback((transform) => {
onViewportChange?.({ x: transform[0], y: transform[1], zoom: transform[2] });
if (!isControlledViewport) {
store.setState({ transform });
}
}, [onViewportChange, isControlledViewport]);
useEffect(() => {
if (zoomPane.current) {
panZoom.current = XYPanZoom({
domNode: zoomPane.current,
minZoom,
maxZoom,
translateExtent,
viewport: defaultViewport,
onDraggingChange: (paneDragging) => store.setState({ paneDragging }),
onPanZoomStart: (event, vp) => {
const { onViewportChangeStart, onMoveStart } = store.getState();
onMoveStart?.(event, vp);
onViewportChangeStart?.(vp);
},
onPanZoom: (event, vp) => {
const { onViewportChange, onMove } = store.getState();
onMove?.(event, vp);
onViewportChange?.(vp);
},
onPanZoomEnd: (event, vp) => {
const { onViewportChangeEnd, onMoveEnd } = store.getState();
onMoveEnd?.(event, vp);
onViewportChangeEnd?.(vp);
},
});
const { x, y, zoom } = panZoom.current.getViewport();
store.setState({
panZoom: panZoom.current,
transform: [x, y, zoom],
domNode: zoomPane.current.closest('.react-flow'),
});
return () => {
panZoom.current?.destroy();
};
}
}, []);
useEffect(() => {
panZoom.current?.update({
onPaneContextMenu,
zoomOnScroll,
zoomOnPinch,
panOnScroll,
panOnScrollSpeed,
panOnScrollMode,
zoomOnDoubleClick,
panOnDrag,
zoomActivationKeyPressed,
preventScrolling,
noPanClassName,
userSelectionActive,
noWheelClassName,
lib,
onTransformChange,
connectionInProgress,
selectionOnDrag,
paneClickDistance,
});
}, [
onPaneContextMenu,
zoomOnScroll,
zoomOnPinch,
panOnScroll,
panOnScrollSpeed,
panOnScrollMode,
zoomOnDoubleClick,
panOnDrag,
zoomActivationKeyPressed,
preventScrolling,
noPanClassName,
userSelectionActive,
noWheelClassName,
lib,
onTransformChange,
connectionInProgress,
selectionOnDrag,
paneClickDistance,
]);
return (jsx("div", { className: "react-flow__renderer", ref: zoomPane, style: containerStyle, children: children }));
}
const selector$i = (s) => ({
userSelectionActive: s.userSelectionActive,
userSelectionRect: s.userSelectionRect,
});
function UserSelection() {
const { userSelectionActive, userSelectionRect } = useStore(selector$i, shallow);
const isActive = userSelectionActive && userSelectionRect;
if (!isActive) {
return null;
}
return (jsx("div", { className: "react-flow__selection react-flow__container", style: {
width: userSelectionRect.width,
height: userSelectionRect.height,
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
} }));
}
const wrapHandler = (handler, containerRef) => {
return (event) => {
if (event.target !== containerRef.current) {
return;
}
handler?.(event);
};
};
const selector$h = (s) => ({
userSelectionActive: s.userSelectionActive,
elementsSelectable: s.elementsSelectable,
connectionInProgress: s.connection.inProgress,
dragging: s.paneDragging,
});
function Pane({ isSelecting, selectionKeyPressed, selectionMode = SelectionMode.Full, panOnDrag, paneClickDistance, selectionOnDrag, onSelectionStart, onSelectionEnd, onPaneClick, onPaneContextMenu, onPaneScroll, onPaneMouseEnter, onPaneMouseMove, onPaneMouseLeave, children, }) {
const store = useStoreApi();
const { userSelectionActive, elementsSelectable, dragging, connectionInProgress } = useStore(selector$h, shallow);
const isSelectionEnabled = elementsSelectable && (isSelecting || userSelectionActive);
const container = useRef(null);
const containerBounds = useRef();
const selectedNodeIds = useRef(new Set());
const selectedEdgeIds = useRef(new Set());
// Used to prevent click events when the user lets go of the selectionKey during a selection
const selectionInProgress = useRef(false);
const onClick = (event) => {
// We prevent click events when the user let go of the selectionKey during a selection
// We also prevent click events when a connection is in progress
if (selectionInProgress.current || connectionInProgress) {
selectionInProgress.current = false;
return;
}
onPaneClick?.(event);
store.getState().resetSelectedElements();
store.setState({ nodesSelectionActive: false });
};
const onContextMenu = (event) => {
if (Array.isArray(panOnDrag) && panOnDrag?.includes(2)) {
event.preventDefault();
return;
}
onPaneContextMenu?.(event);
};
const onWheel = onPaneScroll ? (event) => onPaneScroll(event) : undefined;
const onClickCapture = (event) => {
if (selectionInProgress.current) {
event.stopPropagation();
selectionInProgress.current = false;
}
};
// We are using capture here in order to prevent other pointer events
// to be able to create a selection above a node or an edge
const onPointerDownCapture = (event) => {
const { domNode } = store.getState();
containerBounds.current = domNode?.getBoundingClientRect();
if (!containerBounds.current)
return;
const eventTargetIsContainer = event.target === container.current;
// if a child element has the 'nokey' class, we don't want to swallow the event and don't start a selection
const isNoKeyEvent = !eventTargetIsContainer && !!event.target.closest('.nokey');
const isSelectionActive = (selectionOnDrag && eventTargetIsContainer) || selectionKeyPressed;
if (isNoKeyEvent || !isSelecting || !isSelectionActive || event.button !== 0 || !event.isPrimary) {
return;
}
event.target?.setPointerCapture?.(event.pointerId);
selectionInProgress.current = false;
const { x, y } = getEventPosition(event.nativeEvent, containerBounds.current);
store.setState({
userSelectionRect: {
width: 0,
height: 0,
startX: x,
startY: y,
x,
y,
},
});
if (!eventTargetIsContainer) {
event.stopPropagation();
event.preventDefault();
}
};
const onPointerMove = (event) => {
const { userSelectionRect, transform, nodeLookup, edgeLookup, connectionLookup, triggerNodeChanges, triggerEdgeChanges, defaultEdgeOptions, resetSelectedElements, } = store.getState();
if (!containerBounds.current || !userSelectionRect) {
return;
}
const { x: mouseX, y: mouseY } = getEventPosition(event.nativeEvent, containerBounds.current);
const { startX, startY } = userSelectionRect;
if (!selectionInProgress.current) {
const requiredDistance = selectionKeyPressed ? 0 : paneClickDistance;
const distance = Math.hypot(mouseX - startX, mouseY - startY);
if (distance <= requiredDistance) {
return;
}
resetSelectedElements();
onSelectionStart?.(event);
}
selectionInProgress.current = true;
const nextUserSelectRect = {
startX,
startY,
x: mouseX < startX ? mouseX : startX,
y: mouseY < startY ? mouseY : startY,
width: Math.abs(mouseX - startX),
height: Math.abs(mouseY - startY),
};
const prevSelectedNodeIds = selectedNodeIds.current;
const prevSelectedEdgeIds = selectedEdgeIds.current;
selectedNodeIds.current = new Set(getNodesInside(nodeLookup, nextUserSelectRect, transform, selectionMode === SelectionMode.Partial, true).map((node) => node.id));
selectedEdgeIds.current = new Set();
const edgesSelectable = defaultEdgeOptions?.selectable ?? true;
// We look for all edges connected to the selected nodes
for (const nodeId of selectedNodeIds.current) {
const connections = connectionLookup.get(nodeId);
if (!connections)
continue;
for (const { edgeId } of connections.values()) {
const edge = edgeLookup.get(edgeId);
if (edge && (edge.selectable ?? edgesSelectable)) {
selectedEdgeIds.current.add(edgeId);
}
}
}
if (!areSetsEqual(prevSelectedNodeIds, selectedNodeIds.current)) {
const changes = getSelectionChanges(nodeLookup, selectedNodeIds.current, true);
triggerNodeChanges(changes);
}
if (!areSetsEqual(prevSelectedEdgeIds, selectedEdgeIds.current)) {
const changes = getSelectionChanges(edgeLookup, selectedEdgeIds.current);
triggerEdgeChanges(changes);
}
store.setState({
userSelectionRect: nextUserSelectRect,
userSelectionActive: true,
nodesSelectionActive: false,
});
};
const onPointerUp = (event) => {
if (event.button !== 0) {
return;
}
event.target?.releasePointerCapture?.(event.pointerId);
/*
* We only want to trigger click functions when in selection mode if
* the user did not move the mouse.
*/
if (!userSelectionActive && event.target === container.current && store.getState().userSelectionRect) {
onClick?.(event);
}
store.setState({
userSelectionActive: false,
userSelectionRect: null,
});
if (selectionInProgress.current) {
onSelectionEnd?.(event);
store.setState({
nodesSelectionActive: selectedNodeIds.current.size > 0,
});
}
};
const draggable = panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0));
return (jsxs("div", { className: cc(['react-flow__pane', { draggable, dragging, selection: isSelecting }]), onClick: isSelectionEnabled ? undefined : wrapHandler(onClick, container), onContextMenu: wrapHandler(onContextMenu, container), onWheel: wrapHandler(onWheel, container), onPointerEnter: isSelectionEnabled ? undefined : onPaneMouseEnter, onPointerMove: isSelectionEnabled ? onPointerMove : onPaneMouseMove, onPointerUp: isSelectionEnabled ? onPointerUp : undefined, onPointerDownCapture: isSelectionEnabled ? onPointerDownCapture : undefined, onClickCapture: isSelectionEnabled ? onClickCapture : undefined, onPointerLeave: onPaneMouseLeave, ref: container, style: containerStyle, children: [children, jsx(UserSelection, {})] }));
}
/*
* 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
*/
function handleNodeClick({ id, store, unselect = false, nodeRef, }) {
const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodeLookup, onError } = store.getState();
const node = nodeLookup.get(id);
if (!node) {
onError?.('012', errorMessages['error012'](id));
return;
}
store.setState({ nodesSelectionActive: false });
if (!node.selected) {
addSelectedNodes([id]);
}
else if (unselect || (node.selected && multiSelectionActive)) {
unselectNodesAndEdges({ nodes: [node], edges: [] });
requestAnimationFrame(() => nodeRef?.current?.blur());
}
}
/**
* Hook for calling XYDrag helper from @xyflow/system.
*
* @internal
*/
function useDrag({ nodeRef, disabled = false, noDragClassName, handleSelector, nodeId, isSelectable, nodeClickDistance, }) {
const store = useStoreApi();
const [dragging, setDragging] = useState(false);
const xyDrag = useRef();
useEffect(() => {
xyDrag.current = XYDrag({
getStoreItems: () => store.getState(),
onNodeMouseDown: (id) => {
handleNodeClick({
id,
store,
nodeRef,
});
},
onDragStart: () => {
setDragging(true);
},
onDragStop: () => {
setDragging(false);
},
});
}, []);
useEffect(() => {
if (disabled) {
xyDrag.current?.destroy();
}
else if (nodeRef.current) {
xyDrag.current?.update({
noDragClassName,
handleSelector,
domNode: nodeRef.current,
isSelectable,
nodeId,
nodeClickDistance,
});
return () => {
xyDrag.current?.destroy();
};
}
}, [noDragClassName, handleSelector, disabled, isSelectable, nodeRef, nodeId]);
return dragging;
}
const selectedAndDraggable = (nodesDraggable) => (n) => n.selected && (n.draggable || (nodesDraggable && typeof n.draggable === 'undefined'));
/**
* Hook for updating node positions by passing a direction and factor
*
* @internal
* @returns function for updating node positions
*/
function useMoveSelectedNodes() {
const store = useStoreApi();
const moveSelectedNodes = useCallback((params) => {
const { nodeExtent, snapToGrid, snapGrid, nodesDraggable, onError, updateNodePositions, nodeLookup, nodeOrigin } = store.getState();
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
*/
const xVelo = snapToGrid ? snapGrid[0] : 5;
const yVelo = snapToGrid ? snapGrid[1] : 5;
const xDiff = params.direction.x * xVelo * params.factor;
const yDiff = params.direction.y * yVelo * params.factor;
for (const [, node] of nodeLookup) {
if (!isSelected(node)) {
continue;
}
let nextPosition = {
x: node.internals.positionAbsolute.x + xDiff,
y: node.internals.positionAbsolute.y + yDiff,
};
if (snapToGrid) {
nextPosition = snapPosition(nextPosition, snapGrid);
}
const { position, positionAbsolute } = calculateNodePosition({
nodeId: node.id,
nextPosition,
nodeLookup,
nodeExtent,
nodeOrigin,
onError,
});
node.position = position;
node.internals.positionAbsolute = positionAbsolute;
nodeUpdates.set(node.id, node);
}
updateNodePositions(nodeUpdates);
}, []);
return moveSelectedNodes;
}
const NodeIdContext = createContext(null);
const Provider = NodeIdContext.Provider;
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 The id for a node in the flow.
*
* @example
*```jsx
*import { useNodeId } from '@xyflow/react';
*
*export default function CustomNode() {
* return (
*
*
*
*
* >
* );
*};
*```
*/
const Handle = memo(fixedForwardRef(HandleComponent));
function InputNode({ data, isConnectable, sourcePosition = Position.Bottom }) {
return (jsxs(Fragment, { children: [data?.label, jsx(Handle, { type: "source", position: sourcePosition, isConnectable: isConnectable })] }));
}
function DefaultNode({ data, isConnectable, targetPosition = Position.Top, sourcePosition = Position.Bottom, }) {
return (jsxs(Fragment, { children: [jsx(Handle, { type: "target", position: targetPosition, isConnectable: isConnectable }), data?.label, jsx(Handle, { type: "source", position: sourcePosition, isConnectable: isConnectable })] }));
}
function GroupNode() {
return null;
}
function OutputNode({ data, isConnectable, targetPosition = Position.Top }) {
return (jsxs(Fragment, { children: [jsx(Handle, { type: "target", position: targetPosition, isConnectable: isConnectable }), data?.label] }));
}
const arrowKeyDiffs = {
ArrowUp: { x: 0, y: -1 },
ArrowDown: { x: 0, y: 1 },
ArrowLeft: { x: -1, y: 0 },
ArrowRight: { x: 1, y: 0 },
};
const builtinNodeTypes = {
input: InputNode,
default: DefaultNode,
output: OutputNode,
group: GroupNode,
};
function getNodeInlineStyleDimensions(node) {
if (node.internals.handleBounds === undefined) {
return {
width: node.width ?? node.initialWidth ?? node.style?.width,
height: node.height ?? node.initialHeight ?? node.style?.height,
};
}
return {
width: node.width ?? node.style?.width,
height: node.height ?? node.style?.height,
};
}
const selector$f = (s) => {
const { width, height, x, y } = getInternalNodesBounds(s.nodeLookup, {
filter: (node) => !!node.selected,
});
return {
width: isNumeric(width) ? width : null,
height: isNumeric(height) ? height : null,
userSelectionActive: s.userSelectionActive,
transformString: `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]}) translate(${x}px,${y}px)`,
};
};
function NodesSelection({ onSelectionContextMenu, noPanClassName, disableKeyboardA11y, }) {
const store = useStoreApi();
const { width, height, transformString, userSelectionActive } = useStore(selector$f, shallow);
const moveSelectedNodes = useMoveSelectedNodes();
const nodeRef = useRef(null);
useEffect(() => {
if (!disableKeyboardA11y) {
nodeRef.current?.focus({
preventScroll: true,
});
}
}, [disableKeyboardA11y]);
useDrag({
nodeRef,
});
if (userSelectionActive || !width || !height) {
return null;
}
const onContextMenu = onSelectionContextMenu
? (event) => {
const selectedNodes = store.getState().nodes.filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes);
}
: undefined;
const onKeyDown = (event) => {
if (Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) {
event.preventDefault();
moveSelectedNodes({
direction: arrowKeyDiffs[event.key],
factor: event.shiftKey ? 4 : 1,
});
}
};
return (jsx("div", { className: cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName]), style: {
transform: transformString,
}, children: jsx("div", { ref: nodeRef, className: "react-flow__nodesselection-rect", onContextMenu: onContextMenu, tabIndex: disableKeyboardA11y ? undefined : -1, onKeyDown: disableKeyboardA11y ? undefined : onKeyDown, style: {
width,
height,
} }) }));
}
const win = typeof window !== 'undefined' ? window : undefined;
const selector$e = (s) => {
return { nodesSelectionActive: s.nodesSelectionActive, userSelectionActive: s.userSelectionActive };
};
function FlowRendererComponent({ children, onPaneClick, onPaneMouseEnter, onPaneMouseMove, onPaneMouseLeave, onPaneContextMenu, onPaneScroll, paneClickDistance, deleteKeyCode, selectionKeyCode, selectionOnDrag, selectionMode, onSelectionStart, onSelectionEnd, multiSelectionKeyCode, panActivationKeyCode, zoomActivationKeyCode, elementsSelectable, zoomOnScroll, zoomOnPinch, panOnScroll: _panOnScroll, panOnScrollSpeed, panOnScrollMode, zoomOnDoubleClick, panOnDrag: _panOnDrag, defaultViewport, translateExtent, minZoom, maxZoom, preventScrolling, onSelectionContextMenu, noWheelClassName, noPanClassName, disableKeyboardA11y, onViewportChange, isControlledViewport, }) {
const { nodesSelectionActive, userSelectionActive } = useStore(selector$e, shallow);
const selectionKeyPressed = useKeyPress(selectionKeyCode, { target: win });
const panActivationKeyPressed = useKeyPress(panActivationKeyCode, { target: win });
const panOnDrag = panActivationKeyPressed || _panOnDrag;
const panOnScroll = panActivationKeyPressed || _panOnScroll;
const _selectionOnDrag = selectionOnDrag && panOnDrag !== true;
const isSelecting = selectionKeyPressed || userSelectionActive || _selectionOnDrag;
useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode });
return (jsx(ZoomPane, { onPaneContextMenu: onPaneContextMenu, elementsSelectable: elementsSelectable, zoomOnScroll: zoomOnScroll, zoomOnPinch: zoomOnPinch, panOnScroll: panOnScroll, panOnScrollSpeed: panOnScrollSpeed, panOnScrollMode: panOnScrollMode, zoomOnDoubleClick: zoomOnDoubleClick, panOnDrag: !selectionKeyPressed && panOnDrag, defaultViewport: defaultViewport, translateExtent: translateExtent, minZoom: minZoom, maxZoom: maxZoom, zoomActivationKeyCode: zoomActivationKeyCode, preventScrolling: preventScrolling, noWheelClassName: noWheelClassName, noPanClassName: noPanClassName, onViewportChange: onViewportChange, isControlledViewport: isControlledViewport, paneClickDistance: paneClickDistance, selectionOnDrag: _selectionOnDrag, children: jsxs(Pane, { onSelectionStart: onSelectionStart, onSelectionEnd: onSelectionEnd, onPaneClick: onPaneClick, onPaneMouseEnter: onPaneMouseEnter, onPaneMouseMove: onPaneMouseMove, onPaneMouseLeave: onPaneMouseLeave, onPaneContextMenu: onPaneContextMenu, onPaneScroll: onPaneScroll, panOnDrag: panOnDrag, isSelecting: !!isSelecting, selectionMode: selectionMode, selectionKeyPressed: selectionKeyPressed, paneClickDistance: paneClickDistance, selectionOnDrag: _selectionOnDrag, children: [children, nodesSelectionActive && (jsx(NodesSelection, { onSelectionContextMenu: onSelectionContextMenu, noPanClassName: noPanClassName, disableKeyboardA11y: disableKeyboardA11y }))] }) }));
}
FlowRendererComponent.displayName = 'FlowRenderer';
const FlowRenderer = memo(FlowRendererComponent);
const selector$d = (onlyRenderVisible) => (s) => {
return onlyRenderVisible
? getNodesInside(s.nodeLookup, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map((node) => node.id)
: Array.from(s.nodeLookup.keys());
};
/**
* Hook for getting the visible node ids from the store.
*
* @internal
* @param onlyRenderVisible
* @returns array with visible node ids
*/
function useVisibleNodeIds(onlyRenderVisible) {
const nodeIds = useStore(useCallback(selector$d(onlyRenderVisible), [onlyRenderVisible]), shallow);
return nodeIds;
}
const selector$c = (s) => s.updateNodeInternals;
function useResizeObserver() {
const updateNodeInternals = useStore(selector$c);
const [resizeObserver] = useState(() => {
if (typeof ResizeObserver === 'undefined') {
return null;
}
return new ResizeObserver((entries) => {
const updates = new Map();
entries.forEach((entry) => {
const id = entry.target.getAttribute('data-id');
updates.set(id, {
id,
nodeElement: entry.target,
force: true,
});
});
updateNodeInternals(updates);
});
});
useEffect(() => {
return () => {
resizeObserver?.disconnect();
};
}, [resizeObserver]);
return resizeObserver;
}
/**
* Hook to handle the resize observation + internal updates for the passed node.
*
* @internal
* @returns nodeRef - reference to the node element
*/
function useNodeObserver({ node, nodeType, hasDimensions, resizeObserver, }) {
const store = useStoreApi();
const nodeRef = useRef(null);
const observedNode = useRef(null);
const prevSourcePosition = useRef(node.sourcePosition);
const prevTargetPosition = useRef(node.targetPosition);
const prevType = useRef(nodeType);
const isInitialized = hasDimensions && !!node.internals.handleBounds;
useEffect(() => {
if (nodeRef.current && !node.hidden && (!isInitialized || observedNode.current !== nodeRef.current)) {
if (observedNode.current) {
resizeObserver?.unobserve(observedNode.current);
}
resizeObserver?.observe(nodeRef.current);
observedNode.current = nodeRef.current;
}
}, [isInitialized, node.hidden]);
useEffect(() => {
return () => {
if (observedNode.current) {
resizeObserver?.unobserve(observedNode.current);
observedNode.current = null;
}
};
}, []);
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
*/
const typeChanged = prevType.current !== nodeType;
const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition;
const targetPosChanged = prevTargetPosition.current !== node.targetPosition;
if (typeChanged || sourcePosChanged || targetPosChanged) {
prevType.current = nodeType;
prevSourcePosition.current = node.sourcePosition;
prevTargetPosition.current = node.targetPosition;
store
.getState()
.updateNodeInternals(new Map([[node.id, { id: node.id, nodeElement: nodeRef.current, force: true }]]));
}
}
}, [node.id, nodeType, node.sourcePosition, node.targetPosition]);
return nodeRef;
}
function NodeWrapper({ id, onClick, onMouseEnter, onMouseMove, onMouseLeave, onContextMenu, onDoubleClick, nodesDraggable, elementsSelectable, nodesConnectable, nodesFocusable, resizeObserver, noDragClassName, noPanClassName, disableKeyboardA11y, rfId, nodeTypes, nodeClickDistance, onError, }) {
const { node, internals, isParent } = useStore((s) => {
const node = s.nodeLookup.get(id);
const isParent = s.parentLookup.has(id);
return {
node,
internals: node.internals,
isParent,
};
}, shallow);
let nodeType = node.type || 'default';
let NodeComponent = nodeTypes?.[nodeType] || builtinNodeTypes[nodeType];
if (NodeComponent === undefined) {
onError?.('003', errorMessages['error003'](nodeType));
nodeType = 'default';
NodeComponent = nodeTypes?.['default'] || builtinNodeTypes.default;
}
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined'));
const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined'));
const isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined'));
const store = useStoreApi();
const hasDimensions = nodeHasDimensions(node);
const nodeRef = useNodeObserver({ node, nodeType, hasDimensions, resizeObserver });
const dragging = useDrag({
nodeRef,
disabled: node.hidden || !isDraggable,
noDragClassName,
handleSelector: node.dragHandle,
nodeId: id,
isSelectable,
nodeClickDistance,
});
const moveSelectedNodes = useMoveSelectedNodes();
if (node.hidden) {
return null;
}
const nodeDimensions = getNodeDimensions(node);
const inlineDimensions = getNodeInlineStyleDimensions(node);
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const onMouseEnterHandler = onMouseEnter
? (event) => onMouseEnter(event, { ...internals.userNode })
: undefined;
const onMouseMoveHandler = onMouseMove
? (event) => onMouseMove(event, { ...internals.userNode })
: undefined;
const onMouseLeaveHandler = onMouseLeave
? (event) => onMouseLeave(event, { ...internals.userNode })
: undefined;
const onContextMenuHandler = onContextMenu
? (event) => onContextMenu(event, { ...internals.userNode })
: undefined;
const onDoubleClickHandler = onDoubleClick
? (event) => onDoubleClick(event, { ...internals.userNode })
: undefined;
const onSelectNodeHandler = (event) => {
const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) {
/*
* this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
* here we only need to call it when selectNodesOnDrag=false
*/
handleNodeClick({
id,
store,
nodeRef,
});
}
if (onClick) {
onClick(event, { ...internals.userNode });
}
};
const onKeyDown = (event) => {
if (isInputDOMNode(event.nativeEvent) || disableKeyboardA11y) {
return;
}
if (elementSelectionKeys.includes(event.key) && isSelectable) {
const unselect = event.key === 'Escape';
handleNodeClick({
id,
store,
unselect,
nodeRef,
});
}
else if (isDraggable && node.selected && Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) {
// prevent default scrolling behavior on arrow key press when node is moved
event.preventDefault();
const { ariaLabelConfig } = store.getState();
store.setState({
ariaLiveMessage: ariaLabelConfig['node.a11yDescription.ariaLiveMessage']({
direction: event.key.replace('Arrow', '').toLowerCase(),
x: ~~internals.positionAbsolute.x,
y: ~~internals.positionAbsolute.y,
}),
});
moveSelectedNodes({
direction: arrowKeyDiffs[event.key],
factor: event.shiftKey ? 4 : 1,
});
}
};
const onFocus = () => {
if (disableKeyboardA11y || !nodeRef.current?.matches(':focus-visible')) {
return;
}
const { transform, width, height, autoPanOnNodeFocus, setCenter } = store.getState();
if (!autoPanOnNodeFocus) {
return;
}
const withinViewport = getNodesInside(new Map([[id, node]]), { x: 0, y: 0, width, height }, transform, true).length > 0;
if (!withinViewport) {
setCenter(node.position.x + nodeDimensions.width / 2, node.position.y + nodeDimensions.height / 2, {
zoom: transform[2],
});
}
};
return (jsx("div", { className: cc([
'react-flow__node',
`react-flow__node-${nodeType}`,
{
// this is overwritable by passing `nopan` as a class name
[noPanClassName]: isDraggable,
},
node.className,
{
selected: node.selected,
selectable: isSelectable,
parent: isParent,
draggable: isDraggable,
dragging,
},
]), ref: nodeRef, style: {
zIndex: internals.z,
transform: `translate(${internals.positionAbsolute.x}px,${internals.positionAbsolute.y}px)`,
pointerEvents: hasPointerEvents ? 'all' : 'none',
visibility: hasDimensions ? 'visible' : 'hidden',
...node.style,
...inlineDimensions,
}, "data-id": id, "data-testid": `rf__node-${id}`, onMouseEnter: onMouseEnterHandler, onMouseMove: onMouseMoveHandler, onMouseLeave: onMouseLeaveHandler, onContextMenu: onContextMenuHandler, onClick: onSelectNodeHandler, onDoubleClick: onDoubleClickHandler, onKeyDown: isFocusable ? onKeyDown : undefined, tabIndex: isFocusable ? 0 : undefined, onFocus: isFocusable ? onFocus : undefined, role: node.ariaRole ?? (isFocusable ? 'group' : undefined), "aria-roledescription": "node", "aria-describedby": disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`, "aria-label": node.ariaLabel, ...node.domAttributes, children: jsx(Provider, { value: id, children: jsx(NodeComponent, { id: id, data: node.data, type: nodeType, positionAbsoluteX: internals.positionAbsolute.x, positionAbsoluteY: internals.positionAbsolute.y, selected: node.selected ?? false, selectable: isSelectable, draggable: isDraggable, deletable: node.deletable ?? true, isConnectable: isConnectable, sourcePosition: node.sourcePosition, targetPosition: node.targetPosition, dragging: dragging, dragHandle: node.dragHandle, zIndex: internals.z, parentId: node.parentId, ...nodeDimensions }) }) }));
}
var NodeWrapper$1 = memo(NodeWrapper);
const selector$b = (s) => ({
nodesDraggable: s.nodesDraggable,
nodesConnectable: s.nodesConnectable,
nodesFocusable: s.nodesFocusable,
elementsSelectable: s.elementsSelectable,
onError: s.onError,
});
function NodeRendererComponent(props) {
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, onError } = useStore(selector$b, shallow);
const nodeIds = useVisibleNodeIds(props.onlyRenderVisibleElements);
const resizeObserver = useResizeObserver();
return (jsx("div", { className: "react-flow__nodes", style: containerStyle, children: nodeIds.map((nodeId) => {
return (
/*
* The split of responsibilities between NodeRenderer and
* NodeComponentWrapper may appear weird. However, it’s designed to
* minimize the cost of updates when individual nodes change.
*
* For example, when you’re dragging a single node, that node gets
* updated multiple times per second. If `NodeRenderer` were to update
* every time, it would have to re-run the `nodes.map()` loop every
* time. This gets pricey with hundreds of nodes, especially if every
* loop cycle does more than just rendering a JSX element!
*
* As a result of this choice, we took the following implementation
* decisions:
* - NodeRenderer subscribes *only* to node IDs – and therefore
* rerender *only* when visible nodes are added or removed.
* - NodeRenderer performs all operations the result of which can be
* shared between nodes (such as creating the `ResizeObserver`
* instance, or subscribing to `selector`). This means extra prop
* drilling into `NodeComponentWrapper`, but it means we need to run
* these operations only once – instead of once per node.
* - Any operations that you’d normally write inside `nodes.map` are
* moved into `NodeComponentWrapper`. This ensures they are
* memorized – so if `NodeRenderer` *has* to rerender, it only
* needs to regenerate the list of nodes, nothing else.
*/
jsx(NodeWrapper$1, { id: nodeId, nodeTypes: props.nodeTypes, nodeExtent: props.nodeExtent, onClick: props.onNodeClick, onMouseEnter: props.onNodeMouseEnter, onMouseMove: props.onNodeMouseMove, onMouseLeave: props.onNodeMouseLeave, onContextMenu: props.onNodeContextMenu, onDoubleClick: props.onNodeDoubleClick, noDragClassName: props.noDragClassName, noPanClassName: props.noPanClassName, rfId: props.rfId, disableKeyboardA11y: props.disableKeyboardA11y, resizeObserver: resizeObserver, nodesDraggable: nodesDraggable, nodesConnectable: nodesConnectable, nodesFocusable: nodesFocusable, elementsSelectable: elementsSelectable, nodeClickDistance: props.nodeClickDistance, onError: onError }, nodeId));
}) }));
}
NodeRendererComponent.displayName = 'NodeRenderer';
const NodeRenderer = memo(NodeRendererComponent);
/**
* Hook for getting the visible edge ids from the store.
*
* @internal
* @param onlyRenderVisible
* @returns array with visible edge ids
*/
function useVisibleEdgeIds(onlyRenderVisible) {
const edgeIds = useStore(useCallback((s) => {
if (!onlyRenderVisible) {
return s.edges.map((edge) => edge.id);
}
const visibleEdgeIds = [];
if (s.width && s.height) {
for (const edge of s.edges) {
const sourceNode = s.nodeLookup.get(edge.source);
const targetNode = s.nodeLookup.get(edge.target);
if (sourceNode &&
targetNode &&
isEdgeVisible({
sourceNode,
targetNode,
width: s.width,
height: s.height,
transform: s.transform,
})) {
visibleEdgeIds.push(edge.id);
}
}
}
return visibleEdgeIds;
}, [onlyRenderVisible]), shallow);
return edgeIds;
}
const ArrowSymbol = ({ color = 'none', strokeWidth = 1 }) => {
const style = {
strokeWidth,
...(color && { stroke: color }),
};
return (jsx("polyline", { className: "arrow", style: style, strokeLinecap: "round", fill: "none", strokeLinejoin: "round", points: "-5,-4 0,0 -5,4" }));
};
const ArrowClosedSymbol = ({ color = 'none', strokeWidth = 1 }) => {
const style = {
strokeWidth,
...(color && { stroke: color, fill: color }),
};
return (jsx("polyline", { className: "arrowclosed", style: style, strokeLinecap: "round", strokeLinejoin: "round", points: "-5,-4 0,0 -5,4 -5,-4" }));
};
const MarkerSymbols = {
[MarkerType.Arrow]: ArrowSymbol,
[MarkerType.ArrowClosed]: ArrowClosedSymbol,
};
function useMarkerSymbol(type) {
const store = useStoreApi();
const symbol = useMemo(() => {
const symbolExists = Object.prototype.hasOwnProperty.call(MarkerSymbols, type);
if (!symbolExists) {
store.getState().onError?.('009', errorMessages['error009'](type));
return null;
}
return MarkerSymbols[type];
}, [type]);
return symbol;
}
const Marker = ({ id, type, color, width = 12.5, height = 12.5, markerUnits = 'strokeWidth', strokeWidth, orient = 'auto-start-reverse', }) => {
const Symbol = useMarkerSymbol(type);
if (!Symbol) {
return null;
}
return (jsx("marker", { className: "react-flow__arrowhead", id: id, markerWidth: `${width}`, markerHeight: `${height}`, viewBox: "-10 -10 20 20", markerUnits: markerUnits, orient: orient, refX: "0", refY: "0", children: jsx(Symbol, { color: color, strokeWidth: strokeWidth }) }));
};
/*
* 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 }) => {
const edges = useStore((s) => s.edges);
const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions);
const markers = useMemo(() => {
const markers = createMarkerIds(edges, {
id: rfId,
defaultColor,
defaultMarkerStart: defaultEdgeOptions?.markerStart,
defaultMarkerEnd: defaultEdgeOptions?.markerEnd,
});
return markers;
}, [edges, defaultEdgeOptions, rfId, defaultColor]);
if (!markers.length) {
return null;
}
return (jsx("svg", { className: "react-flow__marker", "aria-hidden": "true", children: jsx("defs", { children: markers.map((marker) => (jsx(Marker, { id: marker.id, type: marker.type, color: marker.color, width: marker.width, height: marker.height, markerUnits: marker.markerUnits, strokeWidth: marker.strokeWidth, orient: marker.orient }, marker.id))) }) }));
};
MarkerDefinitions.displayName = 'MarkerDefinitions';
var MarkerDefinitions$1 = memo(MarkerDefinitions);
function EdgeTextComponent({ x, y, label, labelStyle, labelShowBg = true, labelBgStyle, labelBgPadding = [2, 4], labelBgBorderRadius = 2, children, className, ...rest }) {
const [edgeTextBbox, setEdgeTextBbox] = useState({ x: 1, y: 0, width: 0, height: 0 });
const edgeTextClasses = cc(['react-flow__edge-textwrapper', className]);
const edgeTextRef = useRef(null);
useEffect(() => {
if (edgeTextRef.current) {
const textBbox = edgeTextRef.current.getBBox();
setEdgeTextBbox({
x: textBbox.x,
y: textBbox.y,
width: textBbox.width,
height: textBbox.height,
});
}
}, [label]);
if (!label) {
return null;
}
return (jsxs("g", { transform: `translate(${x - edgeTextBbox.width / 2} ${y - edgeTextBbox.height / 2})`, className: edgeTextClasses, visibility: edgeTextBbox.width ? 'visible' : 'hidden', ...rest, children: [labelShowBg && (jsx("rect", { width: edgeTextBbox.width + 2 * labelBgPadding[0], x: -labelBgPadding[0], y: -labelBgPadding[1], height: edgeTextBbox.height + 2 * labelBgPadding[1], className: "react-flow__edge-textbg", style: labelBgStyle, rx: labelBgBorderRadius, ry: labelBgBorderRadius })), jsx("text", { className: "react-flow__edge-text", y: edgeTextBbox.height / 2, dy: "0.3em", ref: edgeTextRef, style: labelStyle, children: label }), children] }));
}
EdgeTextComponent.displayName = 'EdgeText';
/**
* You can use the `` 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 (
*
* );
* }
*```
*/
const EdgeText = memo(EdgeTextComponent);
/**
* The `` 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 ;
*}
*```
*
* @remarks If you want to use an edge marker with the [``](/api-reference/components/base-edge) component,
* you can pass the `markerStart` or `markerEnd` props passed to your custom edge
* through to the [``](/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.
*/
function BaseEdge({ path, labelX, labelY, label, labelStyle, labelShowBg, labelBgStyle, labelBgPadding, labelBgBorderRadius, interactionWidth = 20, ...props }) {
return (jsxs(Fragment, { children: [jsx("path", { ...props, d: path, fill: "none", className: cc(['react-flow__edge-path', props.className]) }), interactionWidth ? (jsx("path", { d: path, fill: "none", strokeOpacity: 0, strokeWidth: interactionWidth, className: "react-flow__edge-interaction" })) : null, label && isNumeric(labelX) && isNumeric(labelY) ? (jsx(EdgeText, { x: labelX, y: labelY, label: label, labelStyle: labelStyle, labelShowBg: labelShowBg, labelBgStyle: labelBgStyle, labelBgPadding: labelBgPadding, labelBgBorderRadius: labelBgBorderRadius })) : null] }));
}
function getControl({ pos, x1, y1, x2, y2 }) {
if (pos === Position.Left || pos === Position.Right) {
return [0.5 * (x1 + x2), y1];
}
return [x1, 0.5 * (y1 + y2)];
}
/**
* The `getSimpleBezierPath` util returns everything you need to render a simple
* bezier edge between two nodes.
* @public
* @returns
* - `path`: the path to use in an SVG `` element.
* - `labelX`: the `x` position you can use to render a label for this edge.
* - `labelY`: the `y` position you can use to render a label for this edge.
* - `offsetX`: the absolute difference between the source `x` position and the `x` position of the
* middle of this path.
* - `offsetY`: the absolute difference between the source `y` position and the `y` position of the
* middle of this path.
*/
function getSimpleBezierPath({ sourceX, sourceY, sourcePosition = Position.Bottom, targetX, targetY, targetPosition = Position.Top, }) {
const [sourceControlX, sourceControlY] = getControl({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
});
const [targetControlX, targetControlY] = getControl({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
});
const [labelX, labelY, offsetX, offsetY] = getBezierEdgeCenter({
sourceX,
sourceY,
targetX,
targetY,
sourceControlX,
sourceControlY,
targetControlX,
targetControlY,
});
return [
`M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`,
labelX,
labelY,
offsetX,
offsetY,
];
}
function createSimpleBezierEdge(params) {
// eslint-disable-next-line react/display-name
return memo(({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, label, labelStyle, labelShowBg, labelBgStyle, labelBgPadding, labelBgBorderRadius, style, markerEnd, markerStart, interactionWidth, }) => {
const [path, labelX, labelY] = getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const _id = params.isInternal ? undefined : id;
return (jsx(BaseEdge, { id: _id, path: path, labelX: labelX, labelY: labelY, label: label, labelStyle: labelStyle, labelShowBg: labelShowBg, labelBgStyle: labelBgStyle, labelBgPadding: labelBgPadding, labelBgBorderRadius: labelBgBorderRadius, style: style, markerEnd: markerEnd, markerStart: markerStart, interactionWidth: interactionWidth }));
});
}
const SimpleBezierEdge = createSimpleBezierEdge({ isInternal: false });
const SimpleBezierEdgeInternal = createSimpleBezierEdge({ isInternal: true });
SimpleBezierEdge.displayName = 'SimpleBezierEdge';
SimpleBezierEdgeInternal.displayName = 'SimpleBezierEdgeInternal';
function createSmoothStepEdge(params) {
// eslint-disable-next-line react/display-name
return memo(({ id, sourceX, sourceY, targetX, targetY, label, labelStyle, labelShowBg, labelBgStyle, labelBgPadding, labelBgBorderRadius, style, sourcePosition = Position.Bottom, targetPosition = Position.Top, markerEnd, markerStart, pathOptions, interactionWidth, }) => {
const [path, labelX, labelY] = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
borderRadius: pathOptions?.borderRadius,
offset: pathOptions?.offset,
stepPosition: pathOptions?.stepPosition,
});
const _id = params.isInternal ? undefined : id;
return (jsx(BaseEdge, { id: _id, path: path, labelX: labelX, labelY: labelY, label: label, labelStyle: labelStyle, labelShowBg: labelShowBg, labelBgStyle: labelBgStyle, labelBgPadding: labelBgPadding, labelBgBorderRadius: labelBgBorderRadius, style: style, markerEnd: markerEnd, markerStart: markerStart, interactionWidth: interactionWidth }));
});
}
/**
* Component that can be used inside a custom edge to render a smooth step edge.
*
* @public
* @example
*
* ```tsx
* import { SmoothStepEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
*
* );
* }
* ```
*/
const SmoothStepEdge = createSmoothStepEdge({ isInternal: false });
/**
* @internal
*/
const SmoothStepEdgeInternal = createSmoothStepEdge({ isInternal: true });
SmoothStepEdge.displayName = 'SmoothStepEdge';
SmoothStepEdgeInternal.displayName = 'SmoothStepEdgeInternal';
function createStepEdge(params) {
// eslint-disable-next-line react/display-name
return memo(({ id, ...props }) => {
const _id = params.isInternal ? undefined : id;
return (jsx(SmoothStepEdge, { ...props, id: _id, pathOptions: useMemo(() => ({ borderRadius: 0, offset: props.pathOptions?.offset }), [props.pathOptions?.offset]) }));
});
}
/**
* Component that can be used inside a custom edge to render a step edge.
*
* @public
* @example
*
* ```tsx
* import { StepEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
*
* );
* }
* ```
*/
const StepEdge = createStepEdge({ isInternal: false });
/**
* @internal
*/
const StepEdgeInternal = createStepEdge({ isInternal: true });
StepEdge.displayName = 'StepEdge';
StepEdgeInternal.displayName = 'StepEdgeInternal';
function createStraightEdge(params) {
// eslint-disable-next-line react/display-name
return memo(({ id, sourceX, sourceY, targetX, targetY, label, labelStyle, labelShowBg, labelBgStyle, labelBgPadding, labelBgBorderRadius, style, markerEnd, markerStart, interactionWidth, }) => {
const [path, labelX, labelY] = getStraightPath({ sourceX, sourceY, targetX, targetY });
const _id = params.isInternal ? undefined : id;
return (jsx(BaseEdge, { id: _id, path: path, labelX: labelX, labelY: labelY, label: label, labelStyle: labelStyle, labelShowBg: labelShowBg, labelBgStyle: labelBgStyle, labelBgPadding: labelBgPadding, labelBgBorderRadius: labelBgBorderRadius, style: style, markerEnd: markerEnd, markerStart: markerStart, interactionWidth: interactionWidth }));
});
}
/**
* Component that can be used inside a custom edge to render a straight line.
*
* @public
* @example
*
* ```tsx
* import { StraightEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY }) {
* return (
*
* );
* }
* ```
*/
const StraightEdge = createStraightEdge({ isInternal: false });
/**
* @internal
*/
const StraightEdgeInternal = createStraightEdge({ isInternal: true });
StraightEdge.displayName = 'StraightEdge';
StraightEdgeInternal.displayName = 'StraightEdgeInternal';
function createBezierEdge(params) {
// eslint-disable-next-line react/display-name
return memo(({ id, sourceX, sourceY, targetX, targetY, sourcePosition = Position.Bottom, targetPosition = Position.Top, label, labelStyle, labelShowBg, labelBgStyle, labelBgPadding, labelBgBorderRadius, style, markerEnd, markerStart, pathOptions, interactionWidth, }) => {
const [path, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
curvature: pathOptions?.curvature,
});
const _id = params.isInternal ? undefined : id;
return (jsx(BaseEdge, { id: _id, path: path, labelX: labelX, labelY: labelY, label: label, labelStyle: labelStyle, labelShowBg: labelShowBg, labelBgStyle: labelBgStyle, labelBgPadding: labelBgPadding, labelBgBorderRadius: labelBgBorderRadius, style: style, markerEnd: markerEnd, markerStart: markerStart, interactionWidth: interactionWidth }));
});
}
/**
* Component that can be used inside a custom edge to render a bezier curve.
*
* @public
* @example
*
* ```tsx
* import { BezierEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
*
* );
* }
* ```
*/
const BezierEdge = createBezierEdge({ isInternal: false });
/**
* @internal
*/
const BezierEdgeInternal = createBezierEdge({ isInternal: true });
BezierEdge.displayName = 'BezierEdge';
BezierEdgeInternal.displayName = 'BezierEdgeInternal';
const builtinEdgeTypes = {
default: BezierEdgeInternal,
straight: StraightEdgeInternal,
step: StepEdgeInternal,
smoothstep: SmoothStepEdgeInternal,
simplebezier: SimpleBezierEdgeInternal,
};
const nullPosition = {
sourceX: null,
sourceY: null,
targetX: null,
targetY: null,
sourcePosition: null,
targetPosition: null,
};
const shiftX = (x, shift, position) => {
if (position === Position.Left)
return x - shift;
if (position === Position.Right)
return x + shift;
return x;
};
const shiftY = (y, shift, position) => {
if (position === Position.Top)
return y - shift;
if (position === Position.Bottom)
return y + shift;
return y;
};
const EdgeUpdaterClassName = 'react-flow__edgeupdater';
/**
* @internal
*/
function EdgeAnchor({ position, centerX, centerY, radius = 10, onMouseDown, onMouseEnter, onMouseOut, type, }) {
return (jsx("circle", { onMouseDown: onMouseDown, onMouseEnter: onMouseEnter, onMouseOut: onMouseOut, className: cc([EdgeUpdaterClassName, `${EdgeUpdaterClassName}-${type}`]), cx: shiftX(centerX, radius, position), cy: shiftY(centerY, radius, position), r: radius, stroke: "transparent", fill: "transparent" }));
}
function EdgeUpdateAnchors({ isReconnectable, reconnectRadius, edge, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, onReconnect, onReconnectStart, onReconnectEnd, setReconnecting, setUpdateHover, }) {
const store = useStoreApi();
const handleEdgeUpdater = (event, oppositeHandle) => {
// avoid triggering edge updater if mouse btn is not left
if (event.button !== 0) {
return;
}
const { autoPanOnConnect, domNode, isValidConnection, connectionMode, connectionRadius, lib, onConnectStart, onConnectEnd, cancelConnection, nodeLookup, rfId: flowId, panBy, updateConnection, } = store.getState();
const isTarget = oppositeHandle.type === 'target';
const _onReconnectEnd = (evt, connectionState) => {
setReconnecting(false);
onReconnectEnd?.(evt, edge, oppositeHandle.type, connectionState);
};
const onConnectEdge = (connection) => onReconnect?.(edge, connection);
const _onConnectStart = (_event, params) => {
setReconnecting(true);
onReconnectStart?.(event, edge, oppositeHandle.type);
onConnectStart?.(_event, params);
};
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect,
connectionMode,
connectionRadius,
domNode,
handleId: oppositeHandle.id,
nodeId: oppositeHandle.nodeId,
nodeLookup,
isTarget,
edgeUpdaterType: oppositeHandle.type,
lib,
flowId,
cancelConnection,
panBy,
isValidConnection,
onConnect: onConnectEdge,
onConnectStart: _onConnectStart,
onConnectEnd,
onReconnectEnd: _onReconnectEnd,
updateConnection,
getTransform: () => store.getState().transform,
getFromHandle: () => store.getState().connection.fromHandle,
dragThreshold: store.getState().connectionDragThreshold,
handleDomNode: event.currentTarget,
});
};
const onReconnectSourceMouseDown = (event) => handleEdgeUpdater(event, { nodeId: edge.target, id: edge.targetHandle ?? null, type: 'target' });
const onReconnectTargetMouseDown = (event) => handleEdgeUpdater(event, { nodeId: edge.source, id: edge.sourceHandle ?? null, type: 'source' });
const onReconnectMouseEnter = () => setUpdateHover(true);
const onReconnectMouseOut = () => setUpdateHover(false);
return (jsxs(Fragment, { children: [(isReconnectable === true || isReconnectable === 'source') && (jsx(EdgeAnchor, { position: sourcePosition, centerX: sourceX, centerY: sourceY, radius: reconnectRadius, onMouseDown: onReconnectSourceMouseDown, onMouseEnter: onReconnectMouseEnter, onMouseOut: onReconnectMouseOut, type: "source" })), (isReconnectable === true || isReconnectable === 'target') && (jsx(EdgeAnchor, { position: targetPosition, centerX: targetX, centerY: targetY, radius: reconnectRadius, onMouseDown: onReconnectTargetMouseDown, onMouseEnter: onReconnectMouseEnter, onMouseOut: onReconnectMouseOut, type: "target" }))] }));
}
function EdgeWrapper({ id, edgesFocusable, edgesReconnectable, elementsSelectable, onClick, onDoubleClick, onContextMenu, onMouseEnter, onMouseMove, onMouseLeave, reconnectRadius, onReconnect, onReconnectStart, onReconnectEnd, rfId, edgeTypes, noPanClassName, onError, disableKeyboardA11y, }) {
let edge = useStore((s) => s.edgeLookup.get(id));
const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions);
edge = defaultEdgeOptions ? { ...defaultEdgeOptions, ...edge } : edge;
let edgeType = edge.type || 'default';
let EdgeComponent = edgeTypes?.[edgeType] || builtinEdgeTypes[edgeType];
if (EdgeComponent === undefined) {
onError?.('011', errorMessages['error011'](edgeType));
edgeType = 'default';
EdgeComponent = edgeTypes?.['default'] || builtinEdgeTypes.default;
}
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
const isReconnectable = typeof onReconnect !== 'undefined' &&
(edge.reconnectable || (edgesReconnectable && typeof edge.reconnectable === 'undefined'));
const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined'));
const edgeRef = useRef(null);
const [updateHover, setUpdateHover] = useState(false);
const [reconnecting, setReconnecting] = useState(false);
const store = useStoreApi();
const { zIndex, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = useStore(useCallback((store) => {
const sourceNode = store.nodeLookup.get(edge.source);
const targetNode = store.nodeLookup.get(edge.target);
if (!sourceNode || !targetNode) {
return {
zIndex: edge.zIndex,
...nullPosition,
};
}
const edgePosition = getEdgePosition({
id,
sourceNode,
targetNode,
sourceHandle: edge.sourceHandle || null,
targetHandle: edge.targetHandle || null,
connectionMode: store.connectionMode,
onError,
});
const zIndex = getElevatedEdgeZIndex({
selected: edge.selected,
zIndex: edge.zIndex,
sourceNode,
targetNode,
elevateOnSelect: store.elevateEdgesOnSelect,
zIndexMode: store.zIndexMode,
});
return {
zIndex,
...(edgePosition || nullPosition),
};
}, [edge.source, edge.target, edge.sourceHandle, edge.targetHandle, edge.selected, edge.zIndex]), shallow);
const markerStartUrl = useMemo(() => (edge.markerStart ? `url('#${getMarkerId(edge.markerStart, rfId)}')` : undefined), [edge.markerStart, rfId]);
const markerEndUrl = useMemo(() => (edge.markerEnd ? `url('#${getMarkerId(edge.markerEnd, rfId)}')` : undefined), [edge.markerEnd, rfId]);
if (edge.hidden || sourceX === null || sourceY === null || targetX === null || targetY === null) {
return null;
}
const onEdgeClick = (event) => {
const { addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState();
if (isSelectable) {
store.setState({ nodesSelectionActive: false });
if (edge.selected && multiSelectionActive) {
unselectNodesAndEdges({ nodes: [], edges: [edge] });
edgeRef.current?.blur();
}
else {
addSelectedEdges([id]);
}
}
if (onClick) {
onClick(event, edge);
}
};
const onEdgeDoubleClick = onDoubleClick
? (event) => {
onDoubleClick(event, { ...edge });
}
: undefined;
const onEdgeContextMenu = onContextMenu
? (event) => {
onContextMenu(event, { ...edge });
}
: undefined;
const onEdgeMouseEnter = onMouseEnter
? (event) => {
onMouseEnter(event, { ...edge });
}
: undefined;
const onEdgeMouseMove = onMouseMove
? (event) => {
onMouseMove(event, { ...edge });
}
: undefined;
const onEdgeMouseLeave = onMouseLeave
? (event) => {
onMouseLeave(event, { ...edge });
}
: undefined;
const onKeyDown = (event) => {
if (!disableKeyboardA11y && elementSelectionKeys.includes(event.key) && isSelectable) {
const { unselectNodesAndEdges, addSelectedEdges } = store.getState();
const unselect = event.key === 'Escape';
if (unselect) {
edgeRef.current?.blur();
unselectNodesAndEdges({ edges: [edge] });
}
else {
addSelectedEdges([id]);
}
}
};
return (jsx("svg", { style: { zIndex }, children: jsxs("g", { className: cc([
'react-flow__edge',
`react-flow__edge-${edgeType}`,
edge.className,
noPanClassName,
{
selected: edge.selected,
animated: edge.animated,
inactive: !isSelectable && !onClick,
updating: updateHover,
selectable: isSelectable,
},
]), onClick: onEdgeClick, onDoubleClick: onEdgeDoubleClick, onContextMenu: onEdgeContextMenu, onMouseEnter: onEdgeMouseEnter, onMouseMove: onEdgeMouseMove, onMouseLeave: onEdgeMouseLeave, onKeyDown: isFocusable ? onKeyDown : undefined, tabIndex: isFocusable ? 0 : undefined, role: edge.ariaRole ?? (isFocusable ? 'group' : 'img'), "aria-roledescription": "edge", "data-id": id, "data-testid": `rf__edge-${id}`, "aria-label": edge.ariaLabel === null ? undefined : edge.ariaLabel || `Edge from ${edge.source} to ${edge.target}`, "aria-describedby": isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined, ref: edgeRef, ...edge.domAttributes, children: [!reconnecting && (jsx(EdgeComponent, { id: id, source: edge.source, target: edge.target, type: edge.type, selected: edge.selected, animated: edge.animated, selectable: isSelectable, deletable: edge.deletable ?? true, label: edge.label, labelStyle: edge.labelStyle, labelShowBg: edge.labelShowBg, labelBgStyle: edge.labelBgStyle, labelBgPadding: edge.labelBgPadding, labelBgBorderRadius: edge.labelBgBorderRadius, sourceX: sourceX, sourceY: sourceY, targetX: targetX, targetY: targetY, sourcePosition: sourcePosition, targetPosition: targetPosition, data: edge.data, style: edge.style, sourceHandleId: edge.sourceHandle, targetHandleId: edge.targetHandle, markerStart: markerStartUrl, markerEnd: markerEndUrl, pathOptions: 'pathOptions' in edge ? edge.pathOptions : undefined, interactionWidth: edge.interactionWidth })), isReconnectable && (jsx(EdgeUpdateAnchors, { edge: edge, isReconnectable: isReconnectable, reconnectRadius: reconnectRadius, onReconnect: onReconnect, onReconnectStart: onReconnectStart, onReconnectEnd: onReconnectEnd, sourceX: sourceX, sourceY: sourceY, targetX: targetX, targetY: targetY, sourcePosition: sourcePosition, targetPosition: targetPosition, setUpdateHover: setUpdateHover, setReconnecting: setReconnecting }))] }) }));
}
var EdgeWrapper$1 = memo(EdgeWrapper);
const selector$a = (s) => ({
edgesFocusable: s.edgesFocusable,
edgesReconnectable: s.edgesReconnectable,
elementsSelectable: s.elementsSelectable,
connectionMode: s.connectionMode,
onError: s.onError,
});
function EdgeRendererComponent({ defaultMarkerColor, onlyRenderVisibleElements, rfId, edgeTypes, noPanClassName, onReconnect, onEdgeContextMenu, onEdgeMouseEnter, onEdgeMouseMove, onEdgeMouseLeave, onEdgeClick, reconnectRadius, onEdgeDoubleClick, onReconnectStart, onReconnectEnd, disableKeyboardA11y, }) {
const { edgesFocusable, edgesReconnectable, elementsSelectable, onError } = useStore(selector$a, shallow);
const edgeIds = useVisibleEdgeIds(onlyRenderVisibleElements);
return (jsxs("div", { className: "react-flow__edges", children: [jsx(MarkerDefinitions$1, { defaultColor: defaultMarkerColor, rfId: rfId }), edgeIds.map((id) => {
return (jsx(EdgeWrapper$1, { id: id, edgesFocusable: edgesFocusable, edgesReconnectable: edgesReconnectable, elementsSelectable: elementsSelectable, noPanClassName: noPanClassName, onReconnect: onReconnect, onContextMenu: onEdgeContextMenu, onMouseEnter: onEdgeMouseEnter, onMouseMove: onEdgeMouseMove, onMouseLeave: onEdgeMouseLeave, onClick: onEdgeClick, reconnectRadius: reconnectRadius, onDoubleClick: onEdgeDoubleClick, onReconnectStart: onReconnectStart, onReconnectEnd: onReconnectEnd, rfId: rfId, onError: onError, edgeTypes: edgeTypes, disableKeyboardA11y: disableKeyboardA11y }, id));
})] }));
}
EdgeRendererComponent.displayName = 'EdgeRenderer';
const EdgeRenderer = memo(EdgeRendererComponent);
const selector$9 = (s) => `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]})`;
function Viewport({ children }) {
const transform = useStore(selector$9);
return (jsx("div", { className: "react-flow__viewport xyflow__viewport react-flow__container", style: { transform }, children: children }));
}
/**
* Hook for calling onInit handler.
*
* @internal
*/
function useOnInitHandler(onInit) {
const rfInstance = useReactFlow();
const isInitialized = useRef(false);
useEffect(() => {
if (!isInitialized.current && rfInstance.viewportInitialized && onInit) {
setTimeout(() => onInit(rfInstance), 1);
isInitialized.current = true;
}
}, [onInit, rfInstance.viewportInitialized]);
}
const selector$8 = (state) => state.panZoom?.syncViewport;
/**
* Hook for syncing the viewport with the panzoom instance.
*
* @internal
* @param viewport
*/
function useViewportSync(viewport) {
const syncViewport = useStore(selector$8);
const store = useStoreApi();
useEffect(() => {
if (viewport) {
syncViewport?.(viewport);
store.setState({ transform: [viewport.x, viewport.y, viewport.zoom] });
}
}, [viewport, syncViewport]);
return null;
}
function storeSelector$1(s) {
return s.connection.inProgress
? { ...s.connection, to: pointToRendererPoint(s.connection.to, s.transform) }
: { ...s.connection };
}
function getSelector(connectionSelector) {
if (connectionSelector) {
const combinedSelector = (s) => {
const connection = storeSelector$1(s);
return connectionSelector(connection);
};
return combinedSelector;
}
return storeSelector$1;
}
/**
* 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
* @param connectionSelector - An optional selector function used to extract a slice of the
* `ConnectionState` data. Using a selector can prevent component re-renders where data you don't
* otherwise care about might change. If a selector is not provided, the entire `ConnectionState`
* object is returned unchanged.
* @example
*
* ```tsx
*import { useConnection } from '@xyflow/react';
*
*function App() {
* const connection = useConnection();
*
* return (
*
{connection ? `Someone is trying to make a connection from ${connection.fromNode} to this one.` : 'There are currently no incoming connections!'}
*
*
* );
* }
* ```
*
* @returns ConnectionState
*/
function useConnection(connectionSelector) {
const combinedSelector = getSelector(connectionSelector);
return useStore(combinedSelector, shallow);
}
const selector$7 = (s) => ({
nodesConnectable: s.nodesConnectable,
isValid: s.connection.isValid,
inProgress: s.connection.inProgress,
width: s.width,
height: s.height,
});
function ConnectionLineWrapper({ containerStyle, style, type, component, }) {
const { nodesConnectable, width, height, isValid, inProgress } = useStore(selector$7, shallow);
const renderConnection = !!(width && nodesConnectable && inProgress);
if (!renderConnection) {
return null;
}
return (jsx("svg", { style: containerStyle, width: width, height: height, className: "react-flow__connectionline react-flow__container", children: jsx("g", { className: cc(['react-flow__connection', getConnectionStatus(isValid)]), children: jsx(ConnectionLine, { style: style, type: type, CustomComponent: component, isValid: isValid }) }) }));
}
const ConnectionLine = ({ style, type = ConnectionLineType.Bezier, CustomComponent, isValid, }) => {
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition, pointer } = useConnection();
if (!inProgress) {
return;
}
if (CustomComponent) {
return (jsx(CustomComponent, { connectionLineType: type, connectionLineStyle: style, fromNode: fromNode, fromHandle: fromHandle, fromX: from.x, fromY: from.y, toX: to.x, toY: to.y, fromPosition: fromPosition, toPosition: toPosition, connectionStatus: getConnectionStatus(isValid), toNode: toNode, toHandle: toHandle, pointer: pointer }));
}
let path = '';
const pathParams = {
sourceX: from.x,
sourceY: from.y,
sourcePosition: fromPosition,
targetX: to.x,
targetY: to.y,
targetPosition: toPosition,
};
switch (type) {
case ConnectionLineType.Bezier:
[path] = getBezierPath(pathParams);
break;
case ConnectionLineType.SimpleBezier:
[path] = getSimpleBezierPath(pathParams);
break;
case ConnectionLineType.Step:
[path] = getSmoothStepPath({
...pathParams,
borderRadius: 0,
});
break;
case ConnectionLineType.SmoothStep:
[path] = getSmoothStepPath(pathParams);
break;
default:
[path] = getStraightPath(pathParams);
}
return jsx("path", { d: path, fill: "none", className: "react-flow__connection-path", style: style });
};
ConnectionLine.displayName = 'ConnectionLine';
const emptyTypes = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes = emptyTypes) {
const typesRef = useRef(nodeOrEdgeTypes);
const store = useStoreApi();
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
const usedKeys = new Set([...Object.keys(typesRef.current), ...Object.keys(nodeOrEdgeTypes)]);
for (const key of usedKeys) {
if (typesRef.current[key] !== nodeOrEdgeTypes[key]) {
store.getState().onError?.('002', errorMessages['error002']());
break;
}
}
typesRef.current = nodeOrEdgeTypes;
}
}, [nodeOrEdgeTypes]);
}
function useStylesLoadedWarning() {
const store = useStoreApi();
const checked = useRef(false);
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
if (!checked.current) {
const pane = document.querySelector('.react-flow__pane');
if (pane && !(window.getComputedStyle(pane).zIndex === '1')) {
store.getState().onError?.('013', errorMessages['error013']('react'));
}
checked.current = true;
}
}
}, []);
}
function GraphViewComponent({ nodeTypes, edgeTypes, onInit, onNodeClick, onEdgeClick, onNodeDoubleClick, onEdgeDoubleClick, onNodeMouseEnter, onNodeMouseMove, onNodeMouseLeave, onNodeContextMenu, onSelectionContextMenu, onSelectionStart, onSelectionEnd, connectionLineType, connectionLineStyle, connectionLineComponent, connectionLineContainerStyle, selectionKeyCode, selectionOnDrag, selectionMode, multiSelectionKeyCode, panActivationKeyCode, zoomActivationKeyCode, deleteKeyCode, onlyRenderVisibleElements, elementsSelectable, defaultViewport, translateExtent, minZoom, maxZoom, preventScrolling, defaultMarkerColor, zoomOnScroll, zoomOnPinch, panOnScroll, panOnScrollSpeed, panOnScrollMode, zoomOnDoubleClick, panOnDrag, onPaneClick, onPaneMouseEnter, onPaneMouseMove, onPaneMouseLeave, onPaneScroll, onPaneContextMenu, paneClickDistance, nodeClickDistance, onEdgeContextMenu, onEdgeMouseEnter, onEdgeMouseMove, onEdgeMouseLeave, reconnectRadius, onReconnect, onReconnectStart, onReconnectEnd, noDragClassName, noWheelClassName, noPanClassName, disableKeyboardA11y, nodeExtent, rfId, viewport, onViewportChange, }) {
useNodeOrEdgeTypesWarning(nodeTypes);
useNodeOrEdgeTypesWarning(edgeTypes);
useStylesLoadedWarning();
useOnInitHandler(onInit);
useViewportSync(viewport);
return (jsx(FlowRenderer, { onPaneClick: onPaneClick, onPaneMouseEnter: onPaneMouseEnter, onPaneMouseMove: onPaneMouseMove, onPaneMouseLeave: onPaneMouseLeave, onPaneContextMenu: onPaneContextMenu, onPaneScroll: onPaneScroll, paneClickDistance: paneClickDistance, deleteKeyCode: deleteKeyCode, selectionKeyCode: selectionKeyCode, selectionOnDrag: selectionOnDrag, selectionMode: selectionMode, onSelectionStart: onSelectionStart, onSelectionEnd: onSelectionEnd, multiSelectionKeyCode: multiSelectionKeyCode, panActivationKeyCode: panActivationKeyCode, zoomActivationKeyCode: zoomActivationKeyCode, elementsSelectable: elementsSelectable, zoomOnScroll: zoomOnScroll, zoomOnPinch: zoomOnPinch, zoomOnDoubleClick: zoomOnDoubleClick, panOnScroll: panOnScroll, panOnScrollSpeed: panOnScrollSpeed, panOnScrollMode: panOnScrollMode, panOnDrag: panOnDrag, defaultViewport: defaultViewport, translateExtent: translateExtent, minZoom: minZoom, maxZoom: maxZoom, onSelectionContextMenu: onSelectionContextMenu, preventScrolling: preventScrolling, noDragClassName: noDragClassName, noWheelClassName: noWheelClassName, noPanClassName: noPanClassName, disableKeyboardA11y: disableKeyboardA11y, onViewportChange: onViewportChange, isControlledViewport: !!viewport, children: jsxs(Viewport, { children: [jsx(EdgeRenderer, { edgeTypes: edgeTypes, onEdgeClick: onEdgeClick, onEdgeDoubleClick: onEdgeDoubleClick, onReconnect: onReconnect, onReconnectStart: onReconnectStart, onReconnectEnd: onReconnectEnd, onlyRenderVisibleElements: onlyRenderVisibleElements, onEdgeContextMenu: onEdgeContextMenu, onEdgeMouseEnter: onEdgeMouseEnter, onEdgeMouseMove: onEdgeMouseMove, onEdgeMouseLeave: onEdgeMouseLeave, reconnectRadius: reconnectRadius, defaultMarkerColor: defaultMarkerColor, noPanClassName: noPanClassName, disableKeyboardA11y: disableKeyboardA11y, rfId: rfId }), jsx(ConnectionLineWrapper, { style: connectionLineStyle, type: connectionLineType, component: connectionLineComponent, containerStyle: connectionLineContainerStyle }), jsx("div", { className: "react-flow__edgelabel-renderer" }), jsx(NodeRenderer, { nodeTypes: nodeTypes, onNodeClick: onNodeClick, onNodeDoubleClick: onNodeDoubleClick, onNodeMouseEnter: onNodeMouseEnter, onNodeMouseMove: onNodeMouseMove, onNodeMouseLeave: onNodeMouseLeave, onNodeContextMenu: onNodeContextMenu, nodeClickDistance: nodeClickDistance, onlyRenderVisibleElements: onlyRenderVisibleElements, noPanClassName: noPanClassName, noDragClassName: noDragClassName, disableKeyboardA11y: disableKeyboardA11y, nodeExtent: nodeExtent, rfId: rfId }), jsx("div", { className: "react-flow__viewport-portal" })] }) }));
}
GraphViewComponent.displayName = 'GraphView';
const GraphView = memo(GraphViewComponent);
const getInitialState = ({ nodes, edges, defaultNodes, defaultEdges, width, height, fitView, fitViewOptions, minZoom = 0.5, maxZoom = 2, nodeOrigin, nodeExtent, zIndexMode = 'basic', } = {}) => {
const nodeLookup = new Map();
const parentLookup = new Map();
const connectionLookup = new Map();
const edgeLookup = new Map();
const storeEdges = defaultEdges ?? edges ?? [];
const storeNodes = defaultNodes ?? nodes ?? [];
const storeNodeOrigin = nodeOrigin ?? [0, 0];
const storeNodeExtent = nodeExtent ?? infiniteExtent;
updateConnectionLookup(connectionLookup, edgeLookup, storeEdges);
const nodesInitialized = adoptUserNodes(storeNodes, nodeLookup, parentLookup, {
nodeOrigin: storeNodeOrigin,
nodeExtent: storeNodeExtent,
zIndexMode,
});
let transform = [0, 0, 1];
if (fitView && width && height) {
const bounds = getInternalNodesBounds(nodeLookup, {
filter: (node) => !!((node.width || node.initialWidth) && (node.height || node.initialHeight)),
});
const { x, y, zoom } = getViewportForBounds(bounds, width, height, minZoom, maxZoom, fitViewOptions?.padding ?? 0.1);
transform = [x, y, zoom];
}
return {
rfId: '1',
width: width ?? 0,
height: height ?? 0,
transform,
nodes: storeNodes,
nodesInitialized,
nodeLookup,
parentLookup,
edges: storeEdges,
edgeLookup,
connectionLookup,
onNodesChange: null,
onEdgesChange: null,
hasDefaultNodes: defaultNodes !== undefined,
hasDefaultEdges: defaultEdges !== undefined,
panZoom: null,
minZoom,
maxZoom,
translateExtent: infiniteExtent,
nodeExtent: storeNodeExtent,
nodesSelectionActive: false,
userSelectionActive: false,
userSelectionRect: null,
connectionMode: ConnectionMode.Strict,
domNode: null,
paneDragging: false,
noPanClassName: 'nopan',
nodeOrigin: storeNodeOrigin,
nodeDragThreshold: 1,
connectionDragThreshold: 1,
snapGrid: [15, 15],
snapToGrid: false,
nodesDraggable: true,
nodesConnectable: true,
nodesFocusable: true,
edgesFocusable: true,
edgesReconnectable: true,
elementsSelectable: true,
elevateNodesOnSelect: true,
elevateEdgesOnSelect: true,
selectNodesOnDrag: true,
multiSelectionActive: false,
fitViewQueued: fitView ?? false,
fitViewOptions,
fitViewResolver: null,
connection: { ...initialConnection },
connectionClickStartHandle: null,
connectOnClick: true,
ariaLiveMessage: '',
autoPanOnConnect: true,
autoPanOnNodeDrag: true,
autoPanOnNodeFocus: true,
autoPanSpeed: 15,
connectionRadius: 20,
onError: devWarn,
isValidConnection: undefined,
onSelectionChangeHandlers: [],
lib: 'react',
debug: false,
ariaLabelConfig: defaultAriaLabelConfig,
zIndexMode,
onNodesChangeMiddlewareMap: new Map(),
onEdgesChangeMiddlewareMap: new Map(),
};
};
const createStore = ({ nodes, edges, defaultNodes, defaultEdges, width, height, fitView, fitViewOptions, minZoom, maxZoom, nodeOrigin, nodeExtent, zIndexMode, }) => createWithEqualityFn((set, get) => {
async function resolveFitView() {
const { nodeLookup, panZoom, fitViewOptions, fitViewResolver, width, height, minZoom, maxZoom } = get();
if (!panZoom) {
return;
}
await fitViewport({
nodes: nodeLookup,
width,
height,
panZoom,
minZoom,
maxZoom,
}, fitViewOptions);
fitViewResolver?.resolve(true);
/**
* wait for the fitViewport to resolve before deleting the resolver,
* we want to reuse the old resolver if the user calls fitView again in the mean time
*/
set({ fitViewResolver: null });
}
return {
...getInitialState({
nodes,
edges,
width,
height,
fitView,
fitViewOptions,
minZoom,
maxZoom,
nodeOrigin,
nodeExtent,
defaultNodes,
defaultEdges,
zIndexMode,
}),
setNodes: (nodes) => {
const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued, zIndexMode } = get();
/*
* setNodes() is called exclusively in response to user actions:
* - either when the `` 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.
*/
const nodesInitialized = adoptUserNodes(nodes, nodeLookup, parentLookup, {
nodeOrigin,
nodeExtent,
elevateNodesOnSelect,
checkEquality: true,
zIndexMode,
});
if (fitViewQueued && nodesInitialized) {
resolveFitView();
set({ nodes, nodesInitialized, fitViewQueued: false, fitViewOptions: undefined });
}
else {
set({ nodes, nodesInitialized });
}
},
setEdges: (edges) => {
const { connectionLookup, edgeLookup } = get();
updateConnectionLookup(connectionLookup, edgeLookup, edges);
set({ edges });
},
setDefaultNodesAndEdges: (nodes, edges) => {
if (nodes) {
const { setNodes } = get();
setNodes(nodes);
set({ hasDefaultNodes: true });
}
if (edges) {
const { setEdges } = get();
setEdges(edges);
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.
*/
updateNodeInternals: (updates) => {
const { triggerNodeChanges, nodeLookup, parentLookup, domNode, nodeOrigin, nodeExtent, debug, fitViewQueued, zIndexMode, } = get();
const { changes, updatedInternals } = updateNodeInternals(updates, nodeLookup, parentLookup, domNode, nodeOrigin, nodeExtent, zIndexMode);
if (!updatedInternals) {
return;
}
updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent, zIndexMode });
if (fitViewQueued) {
resolveFitView();
set({ fitViewQueued: false, fitViewOptions: undefined });
}
else {
// we always want to trigger useStore calls whenever updateNodeInternals is called
set({});
}
if (changes?.length > 0) {
if (debug) {
console.log('React Flow: trigger node changes', changes);
}
triggerNodeChanges?.(changes);
}
},
updateNodePositions: (nodeDragItems, dragging = false) => {
const parentExpandChildren = [];
let changes = [];
const { nodeLookup, triggerNodeChanges, connection, updateConnection, onNodesChangeMiddlewareMap } = get();
for (const [id, dragItem] of nodeDragItems) {
// we are using the nodelookup to be sure to use the current expandParent and parentId value
const node = nodeLookup.get(id);
const expandParent = !!(node?.expandParent && node?.parentId && dragItem?.position);
const change = {
id,
type: 'position',
position: expandParent
? {
x: Math.max(0, dragItem.position.x),
y: Math.max(0, dragItem.position.y),
}
: dragItem.position,
dragging,
};
if (node && connection.inProgress && connection.fromNode.id === node.id) {
const updatedFrom = getHandlePosition(node, connection.fromHandle, Position.Left, true);
updateConnection({ ...connection, from: updatedFrom });
}
if (expandParent && node.parentId) {
parentExpandChildren.push({
id,
parentId: node.parentId,
rect: {
...dragItem.internals.positionAbsolute,
width: dragItem.measured.width ?? 0,
height: dragItem.measured.height ?? 0,
},
});
}
changes.push(change);
}
if (parentExpandChildren.length > 0) {
const { parentLookup, nodeOrigin } = get();
const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin);
changes.push(...parentExpandChanges);
}
for (const middleware of onNodesChangeMiddlewareMap.values()) {
changes = middleware(changes);
}
triggerNodeChanges(changes);
},
triggerNodeChanges: (changes) => {
const { onNodesChange, setNodes, nodes, hasDefaultNodes, debug } = get();
if (changes?.length) {
if (hasDefaultNodes) {
const updatedNodes = applyNodeChanges(changes, nodes);
setNodes(updatedNodes);
}
if (debug) {
console.log('React Flow: trigger node changes', changes);
}
onNodesChange?.(changes);
}
},
triggerEdgeChanges: (changes) => {
const { onEdgesChange, setEdges, edges, hasDefaultEdges, debug } = get();
if (changes?.length) {
if (hasDefaultEdges) {
const updatedEdges = applyEdgeChanges(changes, edges);
setEdges(updatedEdges);
}
if (debug) {
console.log('React Flow: trigger edge changes', changes);
}
onEdgesChange?.(changes);
}
},
addSelectedNodes: (selectedNodeIds) => {
const { multiSelectionActive, edgeLookup, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get();
if (multiSelectionActive) {
const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true));
triggerNodeChanges(nodeChanges);
return;
}
triggerNodeChanges(getSelectionChanges(nodeLookup, new Set([...selectedNodeIds]), true));
triggerEdgeChanges(getSelectionChanges(edgeLookup));
},
addSelectedEdges: (selectedEdgeIds) => {
const { multiSelectionActive, edgeLookup, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get();
if (multiSelectionActive) {
const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true));
triggerEdgeChanges(changedEdges);
return;
}
triggerEdgeChanges(getSelectionChanges(edgeLookup, new Set([...selectedEdgeIds])));
triggerNodeChanges(getSelectionChanges(nodeLookup, new Set(), true));
},
unselectNodesAndEdges: ({ nodes, edges } = {}) => {
const { edges: storeEdges, nodes: storeNodes, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get();
const nodesToUnselect = nodes ? nodes : storeNodes;
const edgesToUnselect = edges ? edges : storeEdges;
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
*/
internalNode.selected = false;
}
return createSelectionChange(n.id, false);
});
const edgeChanges = edgesToUnselect.map((edge) => createSelectionChange(edge.id, false));
triggerNodeChanges(nodeChanges);
triggerEdgeChanges(edgeChanges);
},
setMinZoom: (minZoom) => {
const { panZoom, maxZoom } = get();
panZoom?.setScaleExtent([minZoom, maxZoom]);
set({ minZoom });
},
setMaxZoom: (maxZoom) => {
const { panZoom, minZoom } = get();
panZoom?.setScaleExtent([minZoom, maxZoom]);
set({ maxZoom });
},
setTranslateExtent: (translateExtent) => {
get().panZoom?.setTranslateExtent(translateExtent);
set({ translateExtent });
},
resetSelectedElements: () => {
const { edges, nodes, triggerNodeChanges, triggerEdgeChanges, elementsSelectable } = get();
if (!elementsSelectable) {
return;
}
const nodeChanges = nodes.reduce((res, node) => (node.selected ? [...res, createSelectionChange(node.id, false)] : res), []);
const edgeChanges = edges.reduce((res, edge) => (edge.selected ? [...res, createSelectionChange(edge.id, false)] : res), []);
triggerNodeChanges(nodeChanges);
triggerEdgeChanges(edgeChanges);
},
setNodeExtent: (nextNodeExtent) => {
const { nodes, nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, nodeExtent, zIndexMode } = get();
if (nextNodeExtent[0][0] === nodeExtent[0][0] &&
nextNodeExtent[0][1] === nodeExtent[0][1] &&
nextNodeExtent[1][0] === nodeExtent[1][0] &&
nextNodeExtent[1][1] === nodeExtent[1][1]) {
return;
}
adoptUserNodes(nodes, nodeLookup, parentLookup, {
nodeOrigin,
nodeExtent: nextNodeExtent,
elevateNodesOnSelect,
checkEquality: false,
zIndexMode,
});
set({ nodeExtent: nextNodeExtent });
},
panBy: (delta) => {
const { transform, width, height, panZoom, translateExtent } = get();
return panBy({ delta, panZoom, transform, translateExtent, width, height });
},
setCenter: async (x, y, options) => {
const { width, height, maxZoom, panZoom } = get();
if (!panZoom) {
return Promise.resolve(false);
}
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom;
await panZoom.setViewport({
x: width / 2 - x * nextZoom,
y: height / 2 - y * nextZoom,
zoom: nextZoom,
}, { duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate });
return Promise.resolve(true);
},
cancelConnection: () => {
set({
connection: { ...initialConnection },
});
},
updateConnection: (connection) => {
set({ connection });
},
reset: () => set({ ...getInitialState() }),
};
}, Object.is);
/**
* The `` 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
* [``](/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 (
*
*
*
*
* );
*}
*
*function Sidebar() {
* // This hook will only work if the component it's used in is a child of a
* // .
* const nodes = useNodes()
*
* return ;
*}
*```
*
* @remarks If you're using a router and want your flow's state to persist across routes,
* it's vital that you place the `` component _outside_ of
* your router. If you have multiple flows on the same page you will need to use a separate
* `` for each flow.
*/
function ReactFlowProvider({ initialNodes: nodes, initialEdges: edges, defaultNodes, defaultEdges, initialWidth: width, initialHeight: height, initialMinZoom: minZoom, initialMaxZoom: maxZoom, initialFitViewOptions: fitViewOptions, fitView, nodeOrigin, nodeExtent, zIndexMode, children, }) {
const [store] = useState(() => createStore({
nodes,
edges,
defaultNodes,
defaultEdges,
width,
height,
fitView,
minZoom,
maxZoom,
fitViewOptions,
nodeOrigin,
nodeExtent,
zIndexMode,
}));
return (jsx(Provider$1, { value: store, children: jsx(BatchProvider, { children: children }) }));
}
function Wrapper({ children, nodes, edges, defaultNodes, defaultEdges, width, height, fitView, fitViewOptions, minZoom, maxZoom, nodeOrigin, nodeExtent, zIndexMode, }) {
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
*/
return jsx(Fragment, { children: children });
}
return (jsx(ReactFlowProvider, { initialNodes: nodes, initialEdges: edges, defaultNodes: defaultNodes, defaultEdges: defaultEdges, initialWidth: width, initialHeight: height, fitView: fitView, initialFitViewOptions: fitViewOptions, initialMinZoom: minZoom, initialMaxZoom: maxZoom, nodeOrigin: nodeOrigin, nodeExtent: nodeExtent, zIndexMode: zIndexMode, children: children }));
}
const wrapperStyle = {
width: '100%',
height: '100%',
overflow: 'hidden',
position: 'relative',
zIndex: 0,
};
function ReactFlow({ nodes, edges, defaultNodes, defaultEdges, className, nodeTypes, edgeTypes, onNodeClick, onEdgeClick, onInit, onMove, onMoveStart, onMoveEnd, onConnect, onConnectStart, onConnectEnd, onClickConnectStart, onClickConnectEnd, onNodeMouseEnter, onNodeMouseMove, onNodeMouseLeave, onNodeContextMenu, onNodeDoubleClick, onNodeDragStart, onNodeDrag, onNodeDragStop, onNodesDelete, onEdgesDelete, onDelete, onSelectionChange, onSelectionDragStart, onSelectionDrag, onSelectionDragStop, onSelectionContextMenu, onSelectionStart, onSelectionEnd, onBeforeDelete, connectionMode, connectionLineType = ConnectionLineType.Bezier, connectionLineStyle, connectionLineComponent, connectionLineContainerStyle, deleteKeyCode = 'Backspace', selectionKeyCode = 'Shift', selectionOnDrag = false, selectionMode = SelectionMode.Full, panActivationKeyCode = 'Space', multiSelectionKeyCode = isMacOs() ? 'Meta' : 'Control', zoomActivationKeyCode = isMacOs() ? 'Meta' : 'Control', snapToGrid, snapGrid, onlyRenderVisibleElements = false, selectNodesOnDrag, nodesDraggable, autoPanOnNodeFocus, nodesConnectable, nodesFocusable, nodeOrigin = defaultNodeOrigin, edgesFocusable, edgesReconnectable, elementsSelectable = true, defaultViewport: defaultViewport$1 = defaultViewport, minZoom = 0.5, maxZoom = 2, translateExtent = infiniteExtent, preventScrolling = true, nodeExtent, defaultMarkerColor = '#b1b1b7', zoomOnScroll = true, zoomOnPinch = true, panOnScroll = false, panOnScrollSpeed = 0.5, panOnScrollMode = PanOnScrollMode.Free, zoomOnDoubleClick = true, panOnDrag = true, onPaneClick, onPaneMouseEnter, onPaneMouseMove, onPaneMouseLeave, onPaneScroll, onPaneContextMenu, paneClickDistance = 1, nodeClickDistance = 0, children, onReconnect, onReconnectStart, onReconnectEnd, onEdgeContextMenu, onEdgeDoubleClick, onEdgeMouseEnter, onEdgeMouseMove, onEdgeMouseLeave, reconnectRadius = 10, onNodesChange, onEdgesChange, noDragClassName = 'nodrag', noWheelClassName = 'nowheel', noPanClassName = 'nopan', fitView, fitViewOptions, connectOnClick, attributionPosition, proOptions, defaultEdgeOptions, elevateNodesOnSelect = true, elevateEdgesOnSelect = false, disableKeyboardA11y = false, autoPanOnConnect, autoPanOnNodeDrag, autoPanSpeed, connectionRadius, isValidConnection, onError, style, id, nodeDragThreshold, connectionDragThreshold, viewport, onViewportChange, width, height, colorMode = 'light', debug, onScroll, ariaLabelConfig, zIndexMode = 'basic', ...rest }, ref) {
const rfId = id || '1';
const colorModeClassName = useColorModeClass(colorMode);
// Undo scroll events, preventing viewport from shifting when nodes outside of it are focused
const wrapperOnScroll = useCallback((e) => {
e.currentTarget.scrollTo({ top: 0, left: 0, behavior: 'instant' });
onScroll?.(e);
}, [onScroll]);
return (jsx("div", { "data-testid": "rf__wrapper", ...rest, onScroll: wrapperOnScroll, style: { ...style, ...wrapperStyle }, ref: ref, className: cc(['react-flow', className, colorModeClassName]), id: id, role: "application", children: jsxs(Wrapper, { nodes: nodes, edges: edges, width: width, height: height, fitView: fitView, fitViewOptions: fitViewOptions, minZoom: minZoom, maxZoom: maxZoom, nodeOrigin: nodeOrigin, nodeExtent: nodeExtent, zIndexMode: zIndexMode, children: [jsx(GraphView, { onInit: onInit, onNodeClick: onNodeClick, onEdgeClick: onEdgeClick, onNodeMouseEnter: onNodeMouseEnter, onNodeMouseMove: onNodeMouseMove, onNodeMouseLeave: onNodeMouseLeave, onNodeContextMenu: onNodeContextMenu, onNodeDoubleClick: onNodeDoubleClick, nodeTypes: nodeTypes, edgeTypes: edgeTypes, connectionLineType: connectionLineType, connectionLineStyle: connectionLineStyle, connectionLineComponent: connectionLineComponent, connectionLineContainerStyle: connectionLineContainerStyle, selectionKeyCode: selectionKeyCode, selectionOnDrag: selectionOnDrag, selectionMode: selectionMode, deleteKeyCode: deleteKeyCode, multiSelectionKeyCode: multiSelectionKeyCode, panActivationKeyCode: panActivationKeyCode, zoomActivationKeyCode: zoomActivationKeyCode, onlyRenderVisibleElements: onlyRenderVisibleElements, defaultViewport: defaultViewport$1, translateExtent: translateExtent, minZoom: minZoom, maxZoom: maxZoom, preventScrolling: preventScrolling, zoomOnScroll: zoomOnScroll, zoomOnPinch: zoomOnPinch, zoomOnDoubleClick: zoomOnDoubleClick, panOnScroll: panOnScroll, panOnScrollSpeed: panOnScrollSpeed, panOnScrollMode: panOnScrollMode, panOnDrag: panOnDrag, onPaneClick: onPaneClick, onPaneMouseEnter: onPaneMouseEnter, onPaneMouseMove: onPaneMouseMove, onPaneMouseLeave: onPaneMouseLeave, onPaneScroll: onPaneScroll, onPaneContextMenu: onPaneContextMenu, paneClickDistance: paneClickDistance, nodeClickDistance: nodeClickDistance, onSelectionContextMenu: onSelectionContextMenu, onSelectionStart: onSelectionStart, onSelectionEnd: onSelectionEnd, onReconnect: onReconnect, onReconnectStart: onReconnectStart, onReconnectEnd: onReconnectEnd, onEdgeContextMenu: onEdgeContextMenu, onEdgeDoubleClick: onEdgeDoubleClick, onEdgeMouseEnter: onEdgeMouseEnter, onEdgeMouseMove: onEdgeMouseMove, onEdgeMouseLeave: onEdgeMouseLeave, reconnectRadius: reconnectRadius, defaultMarkerColor: defaultMarkerColor, noDragClassName: noDragClassName, noWheelClassName: noWheelClassName, noPanClassName: noPanClassName, rfId: rfId, disableKeyboardA11y: disableKeyboardA11y, nodeExtent: nodeExtent, viewport: viewport, onViewportChange: onViewportChange }), jsx(StoreUpdater, { nodes: nodes, edges: edges, defaultNodes: defaultNodes, defaultEdges: defaultEdges, onConnect: onConnect, onConnectStart: onConnectStart, onConnectEnd: onConnectEnd, onClickConnectStart: onClickConnectStart, onClickConnectEnd: onClickConnectEnd, nodesDraggable: nodesDraggable, autoPanOnNodeFocus: autoPanOnNodeFocus, nodesConnectable: nodesConnectable, nodesFocusable: nodesFocusable, edgesFocusable: edgesFocusable, edgesReconnectable: edgesReconnectable, elementsSelectable: elementsSelectable, elevateNodesOnSelect: elevateNodesOnSelect, elevateEdgesOnSelect: elevateEdgesOnSelect, minZoom: minZoom, maxZoom: maxZoom, nodeExtent: nodeExtent, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, snapToGrid: snapToGrid, snapGrid: snapGrid, connectionMode: connectionMode, translateExtent: translateExtent, connectOnClick: connectOnClick, defaultEdgeOptions: defaultEdgeOptions, fitView: fitView, fitViewOptions: fitViewOptions, onNodesDelete: onNodesDelete, onEdgesDelete: onEdgesDelete, onDelete: onDelete, onNodeDragStart: onNodeDragStart, onNodeDrag: onNodeDrag, onNodeDragStop: onNodeDragStop, onSelectionDrag: onSelectionDrag, onSelectionDragStart: onSelectionDragStart, onSelectionDragStop: onSelectionDragStop, onMove: onMove, onMoveStart: onMoveStart, onMoveEnd: onMoveEnd, noPanClassName: noPanClassName, nodeOrigin: nodeOrigin, rfId: rfId, autoPanOnConnect: autoPanOnConnect, autoPanOnNodeDrag: autoPanOnNodeDrag, autoPanSpeed: autoPanSpeed, onError: onError, connectionRadius: connectionRadius, isValidConnection: isValidConnection, selectNodesOnDrag: selectNodesOnDrag, nodeDragThreshold: nodeDragThreshold, connectionDragThreshold: connectionDragThreshold, onBeforeDelete: onBeforeDelete, debug: debug, ariaLabelConfig: ariaLabelConfig, zIndexMode: zIndexMode }), jsx(SelectionListener, { onSelectionChange: onSelectionChange }), children, jsx(Attribution, { proOptions: proOptions, position: attributionPosition }), jsx(A11yDescriptions, { rfId: rfId, disableKeyboardA11y: disableKeyboardA11y })] }) }));
}
/**
* The `` 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 ();
*}
*```
*/
var index = fixedForwardRef(ReactFlow);
const selector$6 = (s) => s.domNode?.querySelector('.react-flow__edgelabel-renderer');
/**
* Edges are SVG-based. If you want to render more complex labels you can use the
* `` component to access a div based renderer. This component
* is a portal that renders the label in a `` that is positioned on top of
* the edges. You can see an example usage of the component in the
* [edge label renderer example](/examples/edges/edge-label-renderer).
* @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 (
* <>
*
*
*
* {data.label}
*
*
* >
* );
* };
* ```
*
* @remarks The `` 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.
*/
function EdgeLabelRenderer({ children }) {
const edgeLabelRenderer = useStore(selector$6);
if (!edgeLabelRenderer) {
return null;
}
return createPortal(children, edgeLabelRenderer);
}
const selector$5 = (s) => s.domNode?.querySelector('.react-flow__viewport-portal');
/**
* The `` 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 (
*
*
* This div is positioned at [100, 100] on the flow.
*
*
* );
*}
*```
*/
function ViewportPortal({ children }) {
const viewPortalDiv = useStore(selector$5);
if (!viewPortalDiv) {
return null;
}
return createPortal(children, viewPortalDiv);
}
/**
* 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 Use this function to tell React Flow to update the internal state of one or more nodes
* that you have changed programmatically.
*
* @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) => (
*
* ))}
*
*
*
*
There are {handleCount} handles on this node.
*
* >
* );
*}
*```
* @remarks This hook can only be used in a component that is a child of a
*{@link ReactFlowProvider} or a {@link ReactFlow} component.
*/
function useUpdateNodeInternals() {
const store = useStoreApi();
return useCallback((id) => {
const { domNode, updateNodeInternals } = store.getState();
const updateIds = Array.isArray(id) ? id : [id];
const updates = new Map();
updateIds.forEach((updateId) => {
const nodeElement = domNode?.querySelector(`.react-flow__node[data-id="${updateId}"]`);
if (nodeElement) {
updates.set(updateId, { id: updateId, nodeElement, force: true });
}
});
requestAnimationFrame(() => updateNodeInternals(updates, { triggerFitView: false }));
}, []);
}
const nodesSelector = (state) => state.nodes;
/**
* 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 all nodes currently in the flow.
*
* @example
* ```jsx
*import { useNodes } from '@xyflow/react';
*
*export default function() {
* const nodes = useNodes();
*
* return
There are currently {nodes.length} nodes!
;
*}
*```
*/
function useNodes() {
const nodes = useStore(nodesSelector, shallow);
return nodes;
}
const edgesSelector = (state) => state.edges;
/**
* 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 all edges currently in the flow.
*
* @example
* ```tsx
*import { useEdges } from '@xyflow/react';
*
*export default function () {
* const edges = useEdges();
*
* return
There are currently {edges.length} edges!
;
*}
*```
*/
function useEdges() {
const edges = useStore(edgesSelector, shallow);
return edges;
}
const viewportSelector = (state) => ({
x: state.transform[0],
y: state.transform[1],
zoom: state.transform[2],
});
/**
* 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 (
*
*
* The viewport is currently at ({x}, {y}) and zoomed to {zoom}.
*
*
* );
*}
*```
*
* @remarks This hook can only be used in a component that is a child of a
*{@link ReactFlowProvider} or a {@link ReactFlow} component.
*/
function useViewport() {
const viewport = useStore(viewportSelector, shallow);
return viewport;
}
/**
* 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
* @returns
* - `nodes`: The current array of nodes. You might pass this directly to the `nodes` prop of your
* `` component, or you may want to manipulate it first to perform some layouting,
* for example.
* - `setNodes`: A function that you can use to update the nodes. You can pass it a new array of
* nodes or a callback that receives the current array of nodes and returns a new array of nodes.
* This is the same as the second element of the tuple returned by React's `useState` hook.
* - `onNodesChange`: A handy callback that can take an array of `NodeChanges` and update the nodes
* state accordingly. You'll typically pass this directly to the `onNodesChange` prop of your
* `` component.
* @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 (
*
* );
*}
*```
*
* @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.
*
*/
function useNodesState(initialNodes) {
const [nodes, setNodes] = useState(initialNodes);
const onNodesChange = useCallback((changes) => setNodes((nds) => applyNodeChanges(changes, nds)), []);
return [nodes, setNodes, onNodesChange];
}
/**
* 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
* @returns
* - `edges`: The current array of edges. You might pass this directly to the `edges` prop of your
* `` component, or you may want to manipulate it first to perform some layouting,
* for example.
*
* - `setEdges`: A function that you can use to update the edges. You can pass it a new array of
* edges or a callback that receives the current array of edges and returns a new array of edges.
* This is the same as the second element of the tuple returned by React's `useState` hook.
*
* - `onEdgesChange`: A handy callback that can take an array of `EdgeChanges` and update the edges
* state accordingly. You'll typically pass this directly to the `onEdgesChange` prop of your
* `` component.
* @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 (
*
* );
*}
*```
*
* @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.
*
*/
function useEdgesState(initialEdges) {
const [edges, setEdges] = useState(initialEdges);
const onEdgesChange = useCallback((changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), []);
return [edges, setEdges, onEdgesChange];
}
/**
* 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
* @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;
*}
*```
*/
function useOnViewportChange({ onStart, onChange, onEnd }) {
const store = useStoreApi();
useEffect(() => {
store.setState({ onViewportChangeStart: onStart });
}, [onStart]);
useEffect(() => {
store.setState({ onViewportChange: onChange });
}, [onChange]);
useEffect(() => {
store.setState({ onViewportChangeEnd: onEnd });
}, [onEnd]);
}
/**
* 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
* @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 (
*
*
Selected nodes: {selectedNodes.join(', ')}
*
Selected edges: {selectedEdges.join(', ')}
*
* );
*}
*```
*
* @remarks You need to memoize the passed `onChange` handler, otherwise the hook will not work correctly.
*/
function useOnSelectionChange({ onChange, }) {
const store = useStoreApi();
useEffect(() => {
const nextOnSelectionChangeHandlers = [...store.getState().onSelectionChangeHandlers, onChange];
store.setState({ onSelectionChangeHandlers: nextOnSelectionChangeHandlers });
return () => {
const nextHandlers = store.getState().onSelectionChangeHandlers.filter((fn) => fn !== onChange);
store.setState({ onSelectionChangeHandlers: nextHandlers });
};
}, [onChange]);
}
const selector$4 = (options) => (s) => {
if (!options.includeHiddenNodes) {
return s.nodesInitialized;
}
if (s.nodeLookup.size === 0) {
return false;
}
for (const [, { internals }] of s.nodeLookup) {
if (internals.handleBounds === undefined || !nodeHasDimensions(internals.userNode)) {
return false;
}
}
return true;
};
/**
* 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
* @returns Whether or not the nodes have been initialized by the `` component and
* given a width and height.
*
* @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;
*}
*```
*/
function useNodesInitialized(options = {
includeHiddenNodes: false,
}) {
const initialized = useStore(selector$4(options));
return initialized;
}
/**
* Hook to check if a is connected to another and get the connections.
*
* @public
* @deprecated Use `useNodeConnections` instead.
* @returns An array with handle connections.
*/
function useHandleConnections({ type, id, nodeId, onConnect, onDisconnect, }) {
console.warn('[DEPRECATED] `useHandleConnections` is deprecated. Instead use `useNodeConnections` https://reactflow.dev/api-reference/hooks/useNodeConnections');
const _nodeId = useNodeId();
const currentNodeId = nodeId ?? _nodeId;
const prevConnections = useRef(null);
const connections = useStore((state) => state.connectionLookup.get(`${currentNodeId}-${type}${id ? `-${id}` : ''}`), areConnectionMapsEqual);
useEffect(() => {
// @todo dicuss if onConnect/onDisconnect should be called when the component mounts/unmounts
if (prevConnections.current && prevConnections.current !== connections) {
const _connections = connections ?? new Map();
handleConnectionChange(prevConnections.current, _connections, onDisconnect);
handleConnectionChange(_connections, prevConnections.current, onConnect);
}
prevConnections.current = connections ?? new Map();
}, [connections, onConnect, onDisconnect]);
return useMemo(() => Array.from(connections?.values() ?? []), [connections]);
}
const error014 = errorMessages['error014']();
/**
* This hook returns an array of connections on a specific node, handle type ('source', 'target') or handle ID.
*
* @public
* @returns An array with connections.
*
* @example
* ```jsx
*import { useNodeConnections } from '@xyflow/react';
*
*export default function () {
* const connections = useNodeConnections({
* handleType: 'target',
* handleId: 'my-handle',
* });
*
* return (
*
There are currently {connections.length} incoming connections!
* );
*}
*```
*/
function useNodeConnections({ id, handleType, handleId, onConnect, onDisconnect, } = {}) {
const nodeId = useNodeId();
const currentNodeId = id ?? nodeId;
if (!currentNodeId) {
throw new Error(error014);
}
const prevConnections = useRef(null);
const connections = useStore((state) => state.connectionLookup.get(`${currentNodeId}${handleType ? (handleId ? `-${handleType}-${handleId}` : `-${handleType}`) : ''}`), areConnectionMapsEqual);
useEffect(() => {
// @todo discuss if onConnect/onDisconnect should be called when the component mounts/unmounts
if (prevConnections.current && prevConnections.current !== connections) {
const _connections = connections ?? new Map();
handleConnectionChange(prevConnections.current, _connections, onDisconnect);
handleConnectionChange(_connections, prevConnections.current, onConnect);
}
prevConnections.current = connections ?? new Map();
}, [connections, onConnect, onDisconnect]);
return useMemo(() => Array.from(connections?.values() ?? []), [connections]);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function useNodesData(nodeIds) {
const nodesData = useStore(useCallback((s) => {
const data = [];
const isArrayOfIds = Array.isArray(nodeIds);
const _nodeIds = isArrayOfIds ? nodeIds : [nodeIds];
for (const nodeId of _nodeIds) {
const node = s.nodeLookup.get(nodeId);
if (node) {
data.push({
id: node.id,
type: node.type,
data: node.data,
});
}
}
return isArrayOfIds ? data : data[0] ?? null;
}, [nodeIds]), shallowNodeData);
return nodesData;
}
/**
* 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 - The ID of a node you want to observe.
* @returns The `InternalNode` object for the node with the given ID.
*
* @example
* ```tsx
*import { useInternalNode } from '@xyflow/react';
*
*export default function () {
* const internalNode = useInternalNode('node-1');
* const absolutePosition = internalNode.internals.positionAbsolute;
*
* return (
*
* The absolute position of the node is at:
*
x: {absolutePosition.x}
*
y: {absolutePosition.y}
*
* );
*}
*```
*/
function useInternalNode(id) {
const node = useStore(useCallback((s) => s.nodeLookup.get(id), [id]), shallow);
return node;
}
/**
* Registers a middleware function to transform node changes.
*
* @public
* @param fn - Middleware function. Should be memoized with useCallback to avoid re-registration.
*/
function experimental_useOnNodesChangeMiddleware(fn) {
const store = useStoreApi();
const [symbol] = useState(() => Symbol());
useEffect(() => {
const { onNodesChangeMiddlewareMap } = store.getState();
onNodesChangeMiddlewareMap.set(symbol, fn);
}, [fn]);
useEffect(() => {
const { onNodesChangeMiddlewareMap } = store.getState();
return () => {
onNodesChangeMiddlewareMap.delete(symbol);
};
}, []);
}
/**
* Registers a middleware function to transform edge changes.
*
* @public
* @param fn - Middleware function. Should be memoized with useCallback to avoid re-registration.
*/
function experimental_useOnEdgesChangeMiddleware(fn) {
const store = useStoreApi();
const [symbol] = useState(() => Symbol());
useEffect(() => {
const { onEdgesChangeMiddlewareMap } = store.getState();
onEdgesChangeMiddlewareMap.set(symbol, fn);
}, [fn]);
useEffect(() => {
const { onEdgesChangeMiddlewareMap } = store.getState();
return () => {
onEdgesChangeMiddlewareMap.delete(symbol);
};
}, []);
}
function LinePattern({ dimensions, lineWidth, variant, className }) {
return (jsx("path", { strokeWidth: lineWidth, d: `M${dimensions[0] / 2} 0 V${dimensions[1]} M0 ${dimensions[1] / 2} H${dimensions[0]}`, className: cc(['react-flow__background-pattern', variant, className]) }));
}
function DotPattern({ radius, className }) {
return (jsx("circle", { cx: radius, cy: radius, r: radius, className: cc(['react-flow__background-pattern', 'dots', className]) }));
}
/**
* 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
*/
var BackgroundVariant;
(function (BackgroundVariant) {
BackgroundVariant["Lines"] = "lines";
BackgroundVariant["Dots"] = "dots";
BackgroundVariant["Cross"] = "cross";
})(BackgroundVariant || (BackgroundVariant = {}));
const defaultSize = {
[BackgroundVariant.Dots]: 1,
[BackgroundVariant.Lines]: 1,
[BackgroundVariant.Cross]: 6,
};
const selector$3 = (s) => ({ transform: s.transform, patternId: `pattern-${s.rfId}` });
function BackgroundComponent({ id, variant = BackgroundVariant.Dots,
// only used for dots and cross
gap = 20,
// only used for lines and cross
size, lineWidth = 1, offset = 0, color, bgColor, style, className, patternClassName, }) {
const ref = useRef(null);
const { transform, patternId } = useStore(selector$3, shallow);
const patternSize = size || defaultSize[variant];
const isDots = variant === BackgroundVariant.Dots;
const isCross = variant === BackgroundVariant.Cross;
const gapXY = Array.isArray(gap) ? gap : [gap, gap];
const scaledGap = [gapXY[0] * transform[2] || 1, gapXY[1] * transform[2] || 1];
const scaledSize = patternSize * transform[2];
const offsetXY = Array.isArray(offset) ? offset : [offset, offset];
const patternDimensions = isCross ? [scaledSize, scaledSize] : scaledGap;
const scaledOffset = [
offsetXY[0] * transform[2] || 1 + patternDimensions[0] / 2,
offsetXY[1] * transform[2] || 1 + patternDimensions[1] / 2,
];
const _patternId = `${patternId}${id ? id : ''}`;
return (jsxs("svg", { className: cc(['react-flow__background', className]), style: {
...style,
...containerStyle,
'--xy-background-color-props': bgColor,
'--xy-background-pattern-color-props': color,
}, ref: ref, "data-testid": "rf__background", children: [jsx("pattern", { id: _patternId, x: transform[0] % scaledGap[0], y: transform[1] % scaledGap[1], width: scaledGap[0], height: scaledGap[1], patternUnits: "userSpaceOnUse", patternTransform: `translate(-${scaledOffset[0]},-${scaledOffset[1]})`, children: isDots ? (jsx(DotPattern, { radius: scaledSize / 2, className: patternClassName })) : (jsx(LinePattern, { dimensions: patternDimensions, lineWidth: lineWidth, variant: variant, className: patternClassName })) }), jsx("rect", { x: "0", y: "0", width: "100%", height: "100%", fill: `url(#${_patternId})` })] }));
}
BackgroundComponent.displayName = 'Background';
/**
* The `` 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 (
*
*
*
* );
* }
* ```
*
* @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 (
*
*
*
*
* );
* }
* ```
*
* @remarks
*
* When combining multiple components it’s important to give each of them a unique id prop!
*
*/
const Background = memo(BackgroundComponent);
function PlusIcon() {
return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 32 32", children: jsx("path", { d: "M32 18.133H18.133V32h-4.266V18.133H0v-4.266h13.867V0h4.266v13.867H32z" }) }));
}
function MinusIcon() {
return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 32 5", children: jsx("path", { d: "M0 0h32v4.2H0z" }) }));
}
function FitViewIcon() {
return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 32 30", children: jsx("path", { d: "M3.692 4.63c0-.53.4-.938.939-.938h5.215V0H4.708C2.13 0 0 2.054 0 4.63v5.216h3.692V4.631zM27.354 0h-5.2v3.692h5.17c.53 0 .984.4.984.939v5.215H32V4.631A4.624 4.624 0 0027.354 0zm.954 24.83c0 .532-.4.94-.939.94h-5.215v3.768h5.215c2.577 0 4.631-2.13 4.631-4.707v-5.139h-3.692v5.139zm-23.677.94c-.531 0-.939-.4-.939-.94v-5.138H0v5.139c0 2.577 2.13 4.707 4.708 4.707h5.138V25.77H4.631z" }) }));
}
function LockIcon() {
return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 25 32", children: jsx("path", { d: "M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0 8 0 4.571 3.429 4.571 7.619v3.048H3.048A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047zm4.724-13.866H7.467V7.619c0-2.59 2.133-4.724 4.723-4.724 2.591 0 4.724 2.133 4.724 4.724v3.048z" }) }));
}
function UnlockIcon() {
return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 25 32", children: jsx("path", { d: "M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0c-4.114 1.828-1.37 2.133.305 2.438 1.676.305 4.42 2.59 4.42 5.181v3.048H3.047A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047z" }) }));
}
/**
* You can add buttons to the control panel by using the `` component
* and pass it as a child to the [``](/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 (
*
*
* alert('Something magical just happened. ✨')}>
*
*
*
*
* )
*}
*```
*/
function ControlButton({ children, className, ...rest }) {
return (jsx("button", { type: "button", className: cc(['react-flow__controls-button', className]), ...rest, children: children }));
}
const selector$2 = (s) => ({
isInteractive: s.nodesDraggable || s.nodesConnectable || s.elementsSelectable,
minZoomReached: s.transform[2] <= s.minZoom,
maxZoomReached: s.transform[2] >= s.maxZoom,
ariaLabelConfig: s.ariaLabelConfig,
});
function ControlsComponent({ style, showZoom = true, showFitView = true, showInteractive = true, fitViewOptions, onZoomIn, onZoomOut, onFitView, onInteractiveChange, className, children, position = 'bottom-left', orientation = 'vertical', 'aria-label': ariaLabel, }) {
const store = useStoreApi();
const { isInteractive, minZoomReached, maxZoomReached, ariaLabelConfig } = useStore(selector$2, shallow);
const { zoomIn, zoomOut, fitView } = useReactFlow();
const onZoomInHandler = () => {
zoomIn();
onZoomIn?.();
};
const onZoomOutHandler = () => {
zoomOut();
onZoomOut?.();
};
const onFitViewHandler = () => {
fitView(fitViewOptions);
onFitView?.();
};
const onToggleInteractivity = () => {
store.setState({
nodesDraggable: !isInteractive,
nodesConnectable: !isInteractive,
elementsSelectable: !isInteractive,
});
onInteractiveChange?.(!isInteractive);
};
const orientationClass = orientation === 'horizontal' ? 'horizontal' : 'vertical';
return (jsxs(Panel, { className: cc(['react-flow__controls', orientationClass, className]), position: position, style: style, "data-testid": "rf__controls", "aria-label": ariaLabel ?? ariaLabelConfig['controls.ariaLabel'], children: [showZoom && (jsxs(Fragment, { children: [jsx(ControlButton, { onClick: onZoomInHandler, className: "react-flow__controls-zoomin", title: ariaLabelConfig['controls.zoomIn.ariaLabel'], "aria-label": ariaLabelConfig['controls.zoomIn.ariaLabel'], disabled: maxZoomReached, children: jsx(PlusIcon, {}) }), jsx(ControlButton, { onClick: onZoomOutHandler, className: "react-flow__controls-zoomout", title: ariaLabelConfig['controls.zoomOut.ariaLabel'], "aria-label": ariaLabelConfig['controls.zoomOut.ariaLabel'], disabled: minZoomReached, children: jsx(MinusIcon, {}) })] })), showFitView && (jsx(ControlButton, { className: "react-flow__controls-fitview", onClick: onFitViewHandler, title: ariaLabelConfig['controls.fitView.ariaLabel'], "aria-label": ariaLabelConfig['controls.fitView.ariaLabel'], children: jsx(FitViewIcon, {}) })), showInteractive && (jsx(ControlButton, { className: "react-flow__controls-interactive", onClick: onToggleInteractivity, title: ariaLabelConfig['controls.interactive.ariaLabel'], "aria-label": ariaLabelConfig['controls.interactive.ariaLabel'], children: isInteractive ? jsx(UnlockIcon, {}) : jsx(LockIcon, {}) })), children] }));
}
ControlsComponent.displayName = 'Controls';
/**
* The `` 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 (
*
*
*
* )
*}
*```
*
* @remarks To extend or customise the controls, you can use the [``](/api-reference/components/control-button) component
*
*/
const Controls = memo(ControlsComponent);
function MiniMapNodeComponent({ id, x, y, width, height, style, color, strokeColor, strokeWidth, className, borderRadius, shapeRendering, selected, onClick, }) {
const { background, backgroundColor } = style || {};
const fill = (color || background || backgroundColor);
return (jsx("rect", { className: cc(['react-flow__minimap-node', { selected }, className]), x: x, y: y, rx: borderRadius, ry: borderRadius, width: width, height: height, style: {
fill,
stroke: strokeColor,
strokeWidth,
}, shapeRendering: shapeRendering, onClick: onClick ? (event) => onClick(event, id) : undefined }));
}
const MiniMapNode = memo(MiniMapNodeComponent);
const selectorNodeIds = (s) => s.nodes.map((node) => node.id);
const getAttrFunction = (func) => func instanceof Function ? func : () => func;
function MiniMapNodes({ nodeStrokeColor, nodeColor, nodeClassName = '', nodeBorderRadius = 5, nodeStrokeWidth,
/*
* We need to rename the prop to be `CapitalCase` so that JSX will render it as
* a component properly.
*/
nodeComponent: NodeComponent = MiniMapNode, onClick, }) {
const nodeIds = useStore(selectorNodeIds, shallow);
const nodeColorFunc = getAttrFunction(nodeColor);
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
const nodeClassNameFunc = getAttrFunction(nodeClassName);
const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
return (jsx(Fragment, { children: nodeIds.map((nodeId) => (
/*
* The split of responsibilities between MiniMapNodes and
* NodeComponentWrapper may appear weird. However, it’s designed to
* minimize the cost of updates when individual nodes change.
*
* For more details, see a similar commit in `NodeRenderer/index.tsx`.
*/
jsx(NodeComponentWrapper, { id: nodeId, nodeColorFunc: nodeColorFunc, nodeStrokeColorFunc: nodeStrokeColorFunc, nodeClassNameFunc: nodeClassNameFunc, nodeBorderRadius: nodeBorderRadius, nodeStrokeWidth: nodeStrokeWidth, NodeComponent: NodeComponent, onClick: onClick, shapeRendering: shapeRendering }, nodeId))) }));
}
function NodeComponentWrapperInner({ id, nodeColorFunc, nodeStrokeColorFunc, nodeClassNameFunc, nodeBorderRadius, nodeStrokeWidth, shapeRendering, NodeComponent, onClick, }) {
const { node, x, y, width, height } = useStore((s) => {
const { internals } = s.nodeLookup.get(id);
const node = internals.userNode;
const { x, y } = internals.positionAbsolute;
const { width, height } = getNodeDimensions(node);
return {
node,
x,
y,
width,
height,
};
}, shallow);
if (!node || node.hidden || !nodeHasDimensions(node)) {
return null;
}
return (jsx(NodeComponent, { x: x, y: y, width: width, height: height, style: node.style, selected: !!node.selected, className: nodeClassNameFunc(node), color: nodeColorFunc(node), borderRadius: nodeBorderRadius, strokeColor: nodeStrokeColorFunc(node), strokeWidth: nodeStrokeWidth, shapeRendering: shapeRendering, onClick: onClick, id: node.id }));
}
const NodeComponentWrapper = memo(NodeComponentWrapperInner);
var MiniMapNodes$1 = memo(MiniMapNodes);
const defaultWidth = 200;
const defaultHeight = 150;
const filterHidden = (node) => !node.hidden;
const selector$1 = (s) => {
const viewBB = {
x: -s.transform[0] / s.transform[2],
y: -s.transform[1] / s.transform[2],
width: s.width / s.transform[2],
height: s.height / s.transform[2],
};
return {
viewBB,
boundingRect: s.nodeLookup.size > 0
? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup, { filter: filterHidden }), viewBB)
: viewBB,
rfId: s.rfId,
panZoom: s.panZoom,
translateExtent: s.translateExtent,
flowWidth: s.width,
flowHeight: s.height,
ariaLabelConfig: s.ariaLabelConfig,
};
};
const ARIA_LABEL_KEY = 'react-flow__minimap-desc';
function MiniMapComponent({ style, className, nodeStrokeColor, nodeColor, nodeClassName = '', nodeBorderRadius = 5, nodeStrokeWidth,
/*
* We need to rename the prop to be `CapitalCase` so that JSX will render it as
* a component properly.
*/
nodeComponent, bgColor, maskColor, maskStrokeColor, maskStrokeWidth, position = 'bottom-right', onClick, onNodeClick, pannable = false, zoomable = false, ariaLabel, inversePan, zoomStep = 1, offsetScale = 5, }) {
const store = useStoreApi();
const svg = useRef(null);
const { boundingRect, viewBB, rfId, panZoom, translateExtent, flowWidth, flowHeight, ariaLabelConfig } = useStore(selector$1, shallow);
const elementWidth = style?.width ?? defaultWidth;
const elementHeight = style?.height ?? defaultHeight;
const scaledWidth = boundingRect.width / elementWidth;
const scaledHeight = boundingRect.height / elementHeight;
const viewScale = Math.max(scaledWidth, scaledHeight);
const viewWidth = viewScale * elementWidth;
const viewHeight = viewScale * elementHeight;
const offset = offsetScale * viewScale;
const x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset;
const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset;
const width = viewWidth + offset * 2;
const height = viewHeight + offset * 2;
const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`;
const viewScaleRef = useRef(0);
const minimapInstance = useRef();
viewScaleRef.current = viewScale;
useEffect(() => {
if (svg.current && panZoom) {
minimapInstance.current = XYMinimap({
domNode: svg.current,
panZoom,
getTransform: () => store.getState().transform,
getViewScale: () => viewScaleRef.current,
});
return () => {
minimapInstance.current?.destroy();
};
}
}, [panZoom]);
useEffect(() => {
minimapInstance.current?.update({
translateExtent,
width: flowWidth,
height: flowHeight,
inversePan,
pannable,
zoomStep,
zoomable,
});
}, [pannable, zoomable, inversePan, zoomStep, translateExtent, flowWidth, flowHeight]);
const onSvgClick = onClick
? (event) => {
const [x, y] = minimapInstance.current?.pointer(event) || [0, 0];
onClick(event, { x, y });
}
: undefined;
const onSvgNodeClick = onNodeClick
? useCallback((event, nodeId) => {
const node = store.getState().nodeLookup.get(nodeId).internals.userNode;
onNodeClick(event, node);
}, [])
: undefined;
const _ariaLabel = ariaLabel ?? ariaLabelConfig['minimap.ariaLabel'];
return (jsx(Panel, { position: position, style: {
...style,
'--xy-minimap-background-color-props': typeof bgColor === 'string' ? bgColor : undefined,
'--xy-minimap-mask-background-color-props': typeof maskColor === 'string' ? maskColor : undefined,
'--xy-minimap-mask-stroke-color-props': typeof maskStrokeColor === 'string' ? maskStrokeColor : undefined,
'--xy-minimap-mask-stroke-width-props': typeof maskStrokeWidth === 'number' ? maskStrokeWidth * viewScale : undefined,
'--xy-minimap-node-background-color-props': typeof nodeColor === 'string' ? nodeColor : undefined,
'--xy-minimap-node-stroke-color-props': typeof nodeStrokeColor === 'string' ? nodeStrokeColor : undefined,
'--xy-minimap-node-stroke-width-props': typeof nodeStrokeWidth === 'number' ? nodeStrokeWidth : undefined,
}, className: cc(['react-flow__minimap', className]), "data-testid": "rf__minimap", children: jsxs("svg", { width: elementWidth, height: elementHeight, viewBox: `${x} ${y} ${width} ${height}`, className: "react-flow__minimap-svg", role: "img", "aria-labelledby": labelledBy, ref: svg, onClick: onSvgClick, children: [_ariaLabel && jsx("title", { id: labelledBy, children: _ariaLabel }), jsx(MiniMapNodes$1, { onClick: onSvgNodeClick, nodeColor: nodeColor, nodeStrokeColor: nodeStrokeColor, nodeBorderRadius: nodeBorderRadius, nodeClassName: nodeClassName, nodeStrokeWidth: nodeStrokeWidth, nodeComponent: nodeComponent }), jsx("path", { className: "react-flow__minimap-mask", d: `M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`, fillRule: "evenodd", pointerEvents: "none" })] }) }));
}
MiniMapComponent.displayName = 'MiniMap';
/**
* The `` 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 (
*
*
*
* );
*}
*```
*/
const MiniMap = memo(MiniMapComponent);
const scaleSelector = (calculateScale) => (store) => calculateScale ? `${Math.max(1 / store.transform[2], 1)}` : undefined;
const defaultPositions = {
[ResizeControlVariant.Line]: 'right',
[ResizeControlVariant.Handle]: 'bottom-right',
};
function ResizeControl({ nodeId, position, variant = ResizeControlVariant.Handle, className, style = undefined, children, color, minWidth = 10, minHeight = 10, maxWidth = Number.MAX_VALUE, maxHeight = Number.MAX_VALUE, keepAspectRatio = false, resizeDirection, autoScale = true, shouldResize, onResizeStart, onResize, onResizeEnd, }) {
const contextNodeId = useNodeId();
const id = typeof nodeId === 'string' ? nodeId : contextNodeId;
const store = useStoreApi();
const resizeControlRef = useRef(null);
const isHandleControl = variant === ResizeControlVariant.Handle;
const scale = useStore(useCallback(scaleSelector(isHandleControl && autoScale), [isHandleControl, autoScale]), shallow);
const resizer = useRef(null);
const controlPosition = position ?? defaultPositions[variant];
useEffect(() => {
if (!resizeControlRef.current || !id) {
return;
}
if (!resizer.current) {
resizer.current = XYResizer({
domNode: resizeControlRef.current,
nodeId: id,
getStoreItems: () => {
const { nodeLookup, transform, snapGrid, snapToGrid, nodeOrigin, domNode } = store.getState();
return {
nodeLookup,
transform,
snapGrid,
snapToGrid,
nodeOrigin,
paneDomNode: domNode,
};
},
onChange: (change, childChanges) => {
const { triggerNodeChanges, nodeLookup, parentLookup, nodeOrigin } = store.getState();
const changes = [];
const nextPosition = { x: change.x, y: change.y };
const node = nodeLookup.get(id);
if (node && node.expandParent && node.parentId) {
const origin = node.origin ?? nodeOrigin;
const width = change.width ?? node.measured.width ?? 0;
const height = change.height ?? node.measured.height ?? 0;
const child = {
id: node.id,
parentId: node.parentId,
rect: {
width,
height,
...evaluateAbsolutePosition({
x: change.x ?? node.position.x,
y: change.y ?? node.position.y,
}, { width, height }, node.parentId, nodeLookup, origin),
},
};
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
*/
nextPosition.x = change.x ? Math.max(origin[0] * width, change.x) : undefined;
nextPosition.y = change.y ? Math.max(origin[1] * height, change.y) : undefined;
}
if (nextPosition.x !== undefined && nextPosition.y !== undefined) {
const positionChange = {
id,
type: 'position',
position: { ...nextPosition },
};
changes.push(positionChange);
}
if (change.width !== undefined && change.height !== undefined) {
const setAttributes = !resizeDirection ? true : resizeDirection === 'horizontal' ? 'width' : 'height';
const dimensionChange = {
id,
type: 'dimensions',
resizing: true,
setAttributes,
dimensions: {
width: change.width,
height: change.height,
},
};
changes.push(dimensionChange);
}
for (const childChange of childChanges) {
const positionChange = {
...childChange,
type: 'position',
};
changes.push(positionChange);
}
triggerNodeChanges(changes);
},
onEnd: ({ width, height }) => {
const dimensionChange = {
id: id,
type: 'dimensions',
resizing: false,
dimensions: {
width,
height,
},
};
store.getState().triggerNodeChanges([dimensionChange]);
},
});
}
resizer.current.update({
controlPosition,
boundaries: {
minWidth,
minHeight,
maxWidth,
maxHeight,
},
keepAspectRatio,
resizeDirection,
onResizeStart,
onResize,
onResizeEnd,
shouldResize,
});
return () => {
resizer.current?.destroy();
};
}, [
controlPosition,
minWidth,
minHeight,
maxWidth,
maxHeight,
keepAspectRatio,
onResizeStart,
onResize,
onResizeEnd,
shouldResize,
]);
const positionClassNames = controlPosition.split('-');
return (jsx("div", { className: cc(['react-flow__resize-control', 'nodrag', ...positionClassNames, variant, className]), ref: resizeControlRef, style: {
...style,
scale,
...(color && { [isHandleControl ? 'backgroundColor' : 'borderColor']: color }),
}, children: children }));
}
/**
* To create your own resizing UI, you can use the `NodeResizeControl` component where you can pass children (such as icons).
* @public
*
*/
const NodeResizeControl = memo(ResizeControl);
/**
* The `` 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 (
* <>
*
*
*
{data.label}
*
* >
* );
*};
*
*export default memo(ResizableNode);
*```
*/
function NodeResizer({ nodeId, isVisible = true, handleClassName, handleStyle, lineClassName, lineStyle, color, minWidth = 10, minHeight = 10, maxWidth = Number.MAX_VALUE, maxHeight = Number.MAX_VALUE, keepAspectRatio = false, autoScale = true, shouldResize, onResizeStart, onResize, onResizeEnd, }) {
if (!isVisible) {
return null;
}
return (jsxs(Fragment, { children: [XY_RESIZER_LINE_POSITIONS.map((position) => (jsx(NodeResizeControl, { className: lineClassName, style: lineStyle, nodeId: nodeId, position: position, variant: ResizeControlVariant.Line, color: color, minWidth: minWidth, minHeight: minHeight, maxWidth: maxWidth, maxHeight: maxHeight, onResizeStart: onResizeStart, keepAspectRatio: keepAspectRatio, autoScale: autoScale, shouldResize: shouldResize, onResize: onResize, onResizeEnd: onResizeEnd }, position))), XY_RESIZER_HANDLE_POSITIONS.map((position) => (jsx(NodeResizeControl, { className: handleClassName, style: handleStyle, nodeId: nodeId, position: position, color: color, minWidth: minWidth, minHeight: minHeight, maxWidth: maxWidth, maxHeight: maxHeight, onResizeStart: onResizeStart, keepAspectRatio: keepAspectRatio, autoScale: autoScale, shouldResize: shouldResize, onResize: onResize, onResizeEnd: onResizeEnd }, position)))] }));
}
const selector = (state) => state.domNode?.querySelector('.react-flow__renderer');
function NodeToolbarPortal({ children }) {
const wrapperRef = useStore(selector);
if (!wrapperRef) {
return null;
}
return createPortal(children, wrapperRef);
}
const nodeEqualityFn = (a, b) => a?.internals.positionAbsolute.x !== b?.internals.positionAbsolute.x ||
a?.internals.positionAbsolute.y !== b?.internals.positionAbsolute.y ||
a?.measured.width !== b?.measured.width ||
a?.measured.height !== b?.measured.height ||
a?.selected !== b?.selected ||
a?.internals.z !== b?.internals.z;
const nodesEqualityFn = (a, b) => {
if (a.size !== b.size) {
return false;
}
for (const [key, node] of a) {
if (nodeEqualityFn(node, b.get(key))) {
return false;
}
}
return true;
};
const storeSelector = (state) => ({
x: state.transform[0],
y: state.transform[1],
zoom: state.transform[2],
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 (
* <>
*
*
*
*
*
*
*
* {data.label}
*
*
*
*
* >
* );
*};
*
*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`.
*/
function NodeToolbar({ nodeId, children, className, style, isVisible, position = Position.Top, offset = 10, align = 'center', ...rest }) {
const contextNodeId = useNodeId();
const nodesSelector = useCallback((state) => {
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || ''];
const internalNodes = nodeIds.reduce((res, id) => {
const node = state.nodeLookup.get(id);
if (node) {
res.set(node.id, node);
}
return res;
}, new Map());
return internalNodes;
}, [nodeId, contextNodeId]);
const nodes = useStore(nodesSelector, nodesEqualityFn);
const { x, y, zoom, selectedNodesCount } = useStore(storeSelector, shallow);
// if isVisible is not set, we show the toolbar only if its node is selected and no other node is selected
const isActive = typeof isVisible === 'boolean'
? isVisible
: nodes.size === 1 && nodes.values().next().value?.selected && selectedNodesCount === 1;
if (!isActive || !nodes.size) {
return null;
}
const nodeRect = getInternalNodesBounds(nodes);
const nodesArray = Array.from(nodes.values());
const zIndex = Math.max(...nodesArray.map((node) => node.internals.z + 1));
const wrapperStyle = {
position: 'absolute',
transform: getNodeToolbarTransform(nodeRect, { x, y, zoom }, position, offset, align),
zIndex,
...style,
};
return (jsx(NodeToolbarPortal, { children: jsx("div", { style: wrapperStyle, className: cc(['react-flow__node-toolbar', className]), ...rest, "data-id": nodesArray.reduce((acc, node) => `${acc}${node.id} `, '').trim(), children: children }) }));
}
const zoomSelector = (state) => state.transform[2];
/**
* This component can render a toolbar or tooltip to one side of a custom edge. This
* toolbar doesn't scale with the viewport so that the content stays the same size.
*
* @public
* @example
* ```jsx
* import { EdgeToolbar, BaseEdge, getBezierPath, type EdgeProps } from "@xyflow/react";
*
* export function CustomEdge({ id, data, ...props }: EdgeProps) {
* const [edgePath, centerX, centerY] = getBezierPath(props);
*
* return (
* <>
*
*
*
*
* >
* );
* }
* ```
*/
function EdgeToolbar({ edgeId, x, y, children, className, style, isVisible, alignX = 'center', alignY = 'center', ...rest }) {
const edgeSelector = useCallback((state) => state.edgeLookup.get(edgeId), [edgeId]);
const edge = useStore(edgeSelector, shallow);
const isActive = typeof isVisible === 'boolean' ? isVisible : edge?.selected;
const zoom = useStore(zoomSelector);
if (!isActive) {
return null;
}
const zIndex = (edge?.zIndex ?? 0) + 1;
const transform = getEdgeToolbarTransform(x, y, zoom, alignX, alignY);
return (jsx(EdgeLabelRenderer, { children: jsx("div", { style: {
position: 'absolute',
transform,
zIndex,
pointerEvents: 'all',
transformOrigin: '0 0',
...style,
}, className: cc(['react-flow__edge-toolbar', className]), "data-id": edge?.id ?? '', ...rest, children: children }) }));
}
export { Background, BackgroundVariant, BaseEdge, BezierEdge, ControlButton, Controls, EdgeLabelRenderer, EdgeText, EdgeToolbar, Handle, MiniMap, MiniMapNode, NodeResizeControl, NodeResizer, NodeToolbar, Panel, index as ReactFlow, ReactFlowProvider, SimpleBezierEdge, SmoothStepEdge, StepEdge, StraightEdge, ViewportPortal, applyEdgeChanges, applyNodeChanges, experimental_useOnEdgesChangeMiddleware, experimental_useOnNodesChangeMiddleware, getSimpleBezierPath, isEdge, isNode, useConnection, useEdges, useEdgesState, useHandleConnections, useInternalNode, useKeyPress, useNodeConnections, useNodeId, useNodes, useNodesData, useNodesInitialized, useNodesState, useOnSelectionChange, useOnViewportChange, useReactFlow, useStore, useStoreApi, useUpdateNodeInternals, useViewport };