implemented new fitView logic
This commit is contained in:
@@ -28,33 +28,49 @@ export function BatchProvider<NodeType extends Node = Node, EdgeType extends Edg
|
||||
const store = useStoreApi<NodeType, EdgeType>();
|
||||
|
||||
const nodeQueueHandler = useCallback((queueItems: QueueItem<NodeType>[]) => {
|
||||
const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState();
|
||||
const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup, fitViewQueued } = 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 as NodeType[];
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
if (hasDefaultNodes) {
|
||||
setNodes(next);
|
||||
} else if (onNodesChange) {
|
||||
onNodesChange(
|
||||
getElementsDiffChanges({
|
||||
items: next,
|
||||
lookup: nodeLookup,
|
||||
}) as NodeChange<NodeType>[]
|
||||
);
|
||||
} else {
|
||||
// When a controlled flow is used we need to collect the changes
|
||||
const changes = getElementsDiffChanges({
|
||||
items: next,
|
||||
lookup: nodeLookup,
|
||||
}) as NodeChange<NodeType>[];
|
||||
|
||||
// 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<NodeType>(nodeQueueHandler);
|
||||
|
||||
const edgeQueueHandler = useCallback((queueItems: QueueItem<EdgeType>[]) => {
|
||||
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange, edgeLookup } = store.getState();
|
||||
|
||||
let next = edges as EdgeType[];
|
||||
let next = edges;
|
||||
for (const payload of queueItems) {
|
||||
next = typeof payload === 'function' ? payload(next) : payload;
|
||||
}
|
||||
|
||||
@@ -12,21 +12,27 @@ import { Queue, QueueItem } from './types';
|
||||
* @returns a Queue object
|
||||
*/
|
||||
export function useQueue<T>(runQueue: (items: QueueItem<T>[]) => void) {
|
||||
// Because we're using a ref above, we need some way to let React know when to
|
||||
// actually process the queue. We increment this number any time we mutate the
|
||||
// queue, creating a new state to trigger the layout effect below.
|
||||
// Using a boolean dirty flag here instead would lead to issues related to
|
||||
// automatic batching. (https://github.com/xyflow/xyflow/issues/4779)
|
||||
/*
|
||||
* Because we're using a ref above, we need some way to let React know when to
|
||||
* actually process the queue. We increment this number any time we mutate the
|
||||
* queue, creating a new state to trigger the layout effect below.
|
||||
* Using a boolean dirty flag here instead would lead to issues related to
|
||||
* automatic batching. (https://github.com/xyflow/xyflow/issues/4779)
|
||||
*/
|
||||
const [serial, setSerial] = useState(BigInt(0));
|
||||
|
||||
// A reference of all the batched updates to process before the next render. We
|
||||
// want a reference here so multiple synchronous calls to `setNodes` etc can be
|
||||
// batched together.
|
||||
/*
|
||||
* A reference of all the batched updates to process before the next render. We
|
||||
* want a reference here so multiple synchronous calls to `setNodes` etc can be
|
||||
* batched together.
|
||||
*/
|
||||
const [queue] = useState(() => createQueue<T>(() => setSerial(n => n + BigInt(1))));
|
||||
|
||||
// Layout effects are guaranteed to run before the next render which means we
|
||||
// shouldn't run into any issues with stale state or weird issues that come from
|
||||
// rendering things one frame later than expected (we used to use `setTimeout`).
|
||||
/*
|
||||
* Layout effects are guaranteed to run before the next render which means we
|
||||
* shouldn't run into any issues with stale state or weird issues that come from
|
||||
* rendering things one frame later than expected (we used to use `setTimeout`).
|
||||
*/
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const queueItems = queue.get();
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
|
||||
import type { ConnectionLineComponent, ReactFlowState } from '../../types';
|
||||
import type { ConnectionLineComponent, Node, ReactFlowState } from '../../types';
|
||||
import { useConnection } from '../../hooks/useConnection';
|
||||
|
||||
type ConnectionLineWrapperProps = {
|
||||
type ConnectionLineWrapperProps<NodeType extends Node = Node> = {
|
||||
type: ConnectionLineType;
|
||||
component?: ConnectionLineComponent;
|
||||
component?: ConnectionLineComponent<NodeType>;
|
||||
containerStyle?: CSSProperties;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
@@ -29,7 +29,12 @@ const selector = (s: ReactFlowState) => ({
|
||||
height: s.height,
|
||||
});
|
||||
|
||||
export function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) {
|
||||
export function ConnectionLineWrapper<NodeType extends Node = Node>({
|
||||
containerStyle,
|
||||
style,
|
||||
type,
|
||||
component,
|
||||
}: ConnectionLineWrapperProps<NodeType>) {
|
||||
const { nodesConnectable, width, height, isValid, inProgress } = useStore(selector, shallow);
|
||||
const renderConnection = !!(width && nodesConnectable && inProgress);
|
||||
|
||||
@@ -45,21 +50,27 @@ export function ConnectionLineWrapper({ containerStyle, style, type, component }
|
||||
className="react-flow__connectionline react-flow__container"
|
||||
>
|
||||
<g className={cc(['react-flow__connection', getConnectionStatus(isValid)])}>
|
||||
<ConnectionLine style={style} type={type} CustomComponent={component} isValid={isValid} />
|
||||
<ConnectionLine<NodeType> style={style} type={type} CustomComponent={component} isValid={isValid} />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
type ConnectionLineProps = {
|
||||
type ConnectionLineProps<NodeType extends Node = Node> = {
|
||||
type: ConnectionLineType;
|
||||
style?: CSSProperties;
|
||||
CustomComponent?: ConnectionLineComponent;
|
||||
CustomComponent?: ConnectionLineComponent<NodeType>;
|
||||
isValid: boolean | null;
|
||||
};
|
||||
|
||||
const ConnectionLine = ({ style, type = ConnectionLineType.Bezier, CustomComponent, isValid }: ConnectionLineProps) => {
|
||||
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } = useConnection();
|
||||
const ConnectionLine = <NodeType extends Node = Node>({
|
||||
style,
|
||||
type = ConnectionLineType.Bezier,
|
||||
CustomComponent,
|
||||
isValid,
|
||||
}: ConnectionLineProps<NodeType>) => {
|
||||
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } =
|
||||
useConnection<NodeType>();
|
||||
|
||||
if (!inProgress) {
|
||||
return;
|
||||
|
||||
@@ -6,7 +6,52 @@ import type { ReactFlowState } from '../../types';
|
||||
|
||||
const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__edgelabel-renderer');
|
||||
|
||||
export function EdgeLabelRenderer({ children }: { children: ReactNode }) {
|
||||
export type EdgeLabelRendererProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Edges are SVG-based. If you want to render more complex labels you can use the
|
||||
* `<EdgeLabelRenderer />` component to access a div based renderer. This component
|
||||
* is a portal that renders the label in a `<div />` that is positioned on top of
|
||||
* the edges. You can see an example usage of the component in the
|
||||
* [edge label renderer 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 (
|
||||
* <>
|
||||
* <BaseEdge id={id} path={edgePath} />
|
||||
* <EdgeLabelRenderer>
|
||||
* <div
|
||||
* style={{
|
||||
* position: 'absolute',
|
||||
* transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
* background: '#ffcc00',
|
||||
* padding: 10,
|
||||
* }}
|
||||
* className="nodrag nopan"
|
||||
* >
|
||||
* {data.label}
|
||||
* </div>
|
||||
* </EdgeLabelRenderer>
|
||||
* </>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @remarks The `<EdgeLabelRenderer />` has no pointer events by default. If you want to
|
||||
* add mouse interactions you need to set the style `pointerEvents: all` and add
|
||||
* the `nopan` class on the label or the element you want to interact with.
|
||||
*/
|
||||
export function EdgeLabelRenderer({ children }: EdgeLabelRendererProps) {
|
||||
const edgeLabelRenderer = useStore(selector);
|
||||
|
||||
if (!edgeLabelRenderer) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react';
|
||||
import { useState, useMemo, useRef, type KeyboardEvent, useCallback, JSX } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import {
|
||||
@@ -136,28 +136,28 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
||||
|
||||
const onEdgeDoubleClick = onDoubleClick
|
||||
? (event: React.MouseEvent) => {
|
||||
onDoubleClick(event, { ...edge });
|
||||
}
|
||||
onDoubleClick(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
const onEdgeContextMenu = onContextMenu
|
||||
? (event: React.MouseEvent) => {
|
||||
onContextMenu(event, { ...edge });
|
||||
}
|
||||
onContextMenu(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
const onEdgeMouseEnter = onMouseEnter
|
||||
? (event: React.MouseEvent) => {
|
||||
onMouseEnter(event, { ...edge });
|
||||
}
|
||||
onMouseEnter(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
const onEdgeMouseMove = onMouseMove
|
||||
? (event: React.MouseEvent) => {
|
||||
onMouseMove(event, { ...edge });
|
||||
}
|
||||
onMouseMove(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
const onEdgeMouseLeave = onMouseLeave
|
||||
? (event: React.MouseEvent) => {
|
||||
onMouseLeave(event, { ...edge });
|
||||
}
|
||||
onMouseLeave(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
|
||||
@@ -4,6 +4,33 @@ import cc from 'classcat';
|
||||
import { EdgeText } from './EdgeText';
|
||||
import type { BaseEdgeProps } from '../../types';
|
||||
|
||||
/**
|
||||
* The `<BaseEdge />` component gets used internally for all the edges. It can be
|
||||
* used inside a custom edge and handles the invisible helper edge and the edge label
|
||||
* for you.
|
||||
*
|
||||
* @public
|
||||
* @example
|
||||
* ```jsx
|
||||
*import { BaseEdge } from '@xyflow/react';
|
||||
*
|
||||
*export function CustomEdge({ sourceX, sourceY, targetX, targetY, ...props }) {
|
||||
* const [edgePath] = getStraightPath({
|
||||
* sourceX,
|
||||
* sourceY,
|
||||
* targetX,
|
||||
* targetY,
|
||||
* });
|
||||
*
|
||||
* return <BaseEdge path={edgePath} {...props} />;
|
||||
*}
|
||||
*```
|
||||
*
|
||||
* @remarks If you want to use an edge marker with the [`<BaseEdge />`](/api-reference/components/base-edge) component,
|
||||
* you can pass the `markerStart` or `markerEnd` props passed to your custom edge
|
||||
* through to the [`<BaseEdge />`](/api-reference/components/base-edge) component.
|
||||
* You can see all the props passed to a custom edge by looking at the [`EdgeProps`](/api-reference/types/edge-props) type.
|
||||
*/
|
||||
export function BaseEdge({
|
||||
path,
|
||||
labelX,
|
||||
|
||||
@@ -61,7 +61,34 @@ function createBezierEdge(params: { isInternal: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
* <BezierEdge
|
||||
* sourceX={sourceX}
|
||||
* sourceY={sourceY}
|
||||
* targetX={targetX}
|
||||
* targetY={targetY}
|
||||
* sourcePosition={sourcePosition}
|
||||
* targetPosition={targetPosition}
|
||||
* />
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
const BezierEdge = createBezierEdge({ isInternal: false });
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
const BezierEdgeInternal = createBezierEdge({ isInternal: true });
|
||||
|
||||
BezierEdge.displayName = 'BezierEdge';
|
||||
|
||||
@@ -27,6 +27,9 @@ export interface EdgeAnchorProps extends SVGAttributes<SVGGElement> {
|
||||
|
||||
const EdgeUpdaterClassName = 'react-flow__edgeupdater';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function EdgeAnchor({
|
||||
position,
|
||||
centerX,
|
||||
|
||||
@@ -8,9 +8,9 @@ function EdgeTextComponent({
|
||||
x,
|
||||
y,
|
||||
label,
|
||||
labelStyle = {},
|
||||
labelStyle,
|
||||
labelShowBg = true,
|
||||
labelBgStyle = {},
|
||||
labelBgStyle,
|
||||
labelBgPadding = [2, 4],
|
||||
labelBgBorderRadius = 2,
|
||||
children,
|
||||
@@ -34,7 +34,7 @@ function EdgeTextComponent({
|
||||
}
|
||||
}, [label]);
|
||||
|
||||
if (typeof label === 'undefined' || !label) {
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -73,4 +73,30 @@ function EdgeTextComponent({
|
||||
|
||||
EdgeTextComponent.displayName = 'EdgeText';
|
||||
|
||||
/**
|
||||
* You can use the `<EdgeText />` component as a helper component to display text
|
||||
* within your custom edges.
|
||||
*
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* import { EdgeText } from '@xyflow/react';
|
||||
*
|
||||
* export function CustomEdgeLabel({ label }) {
|
||||
* return (
|
||||
* <EdgeText
|
||||
* x={100}
|
||||
* y={100}
|
||||
* label={label}
|
||||
* labelStyle={{ fill: 'white' }}
|
||||
* labelShowBg
|
||||
* labelBgStyle={{ fill: 'red' }}
|
||||
* labelBgPadding={[2, 4]}
|
||||
* labelBgBorderRadius={2}
|
||||
* />
|
||||
* );
|
||||
* }
|
||||
*```
|
||||
*/
|
||||
export const EdgeText = memo(EdgeTextComponent);
|
||||
|
||||
@@ -7,9 +7,11 @@ import type { SimpleBezierEdgeProps } from '../../types';
|
||||
export interface GetSimpleBezierPathParams {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
/** @default Position.Bottom */
|
||||
sourcePosition?: Position;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
/** @default Position.Top */
|
||||
targetPosition?: Position;
|
||||
}
|
||||
|
||||
@@ -29,6 +31,19 @@ function getControl({ pos, x1, y1, x2, y2 }: GetControlParams): [number, number]
|
||||
return [x1, 0.5 * (y1 + y2)];
|
||||
}
|
||||
|
||||
/**
|
||||
* The `getSimpleBezierPath` util returns everything you need to render a simple
|
||||
* bezier edge between two nodes.
|
||||
* @public
|
||||
* @returns
|
||||
* - `path`: the path to use in an SVG `<path>` 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.
|
||||
*/
|
||||
export function getSimpleBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
@@ -80,8 +95,8 @@ function createSimpleBezierEdge(params: { isInternal: boolean }) {
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetPosition = Position.Top,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
|
||||
@@ -62,7 +62,34 @@ function createSmoothStepEdge(params: { isInternal: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
* <SmoothStepEdge
|
||||
* sourceX={sourceX}
|
||||
* sourceY={sourceY}
|
||||
* targetX={targetX}
|
||||
* targetY={targetY}
|
||||
* sourcePosition={sourcePosition}
|
||||
* targetPosition={targetPosition}
|
||||
* />
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
const SmoothStepEdge = createSmoothStepEdge({ isInternal: false });
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
const SmoothStepEdgeInternal = createSmoothStepEdge({ isInternal: true });
|
||||
|
||||
SmoothStepEdge.displayName = 'SmoothStepEdge';
|
||||
|
||||
@@ -21,7 +21,34 @@ function createStepEdge(params: { isInternal: boolean }) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
* <StepEdge
|
||||
* sourceX={sourceX}
|
||||
* sourceY={sourceY}
|
||||
* targetX={targetX}
|
||||
* targetY={targetY}
|
||||
* sourcePosition={sourcePosition}
|
||||
* targetPosition={targetPosition}
|
||||
* />
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
const StepEdge = createStepEdge({ isInternal: false });
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
const StepEdgeInternal = createStepEdge({ isInternal: true });
|
||||
|
||||
StepEdge.displayName = 'StepEdge';
|
||||
|
||||
@@ -50,7 +50,32 @@ function createStraightEdge(params: { isInternal: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
* <StraightEdge
|
||||
* sourceX={sourceX}
|
||||
* sourceY={sourceY}
|
||||
* targetX={targetX}
|
||||
* targetY={targetY}
|
||||
* />
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
const StraightEdge = createStraightEdge({ isInternal: false });
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
const StraightEdgeInternal = createStraightEdge({ isInternal: true });
|
||||
|
||||
StraightEdge.displayName = 'StraightEdge';
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// We distinguish between internal and exported edges
|
||||
// The internal edges are used directly like custom edges and always get an id, source and target props
|
||||
// If you import an edge from the library, the id is optional and source and target are not used at all
|
||||
/*
|
||||
* We distinguish between internal and exported edges
|
||||
* The internal edges are used directly like custom edges and always get an id, source and target props
|
||||
* If you import an edge from the library, the id is optional and source and target are not used at all
|
||||
*/
|
||||
|
||||
export { SimpleBezierEdge, SimpleBezierEdgeInternal } from './SimpleBezierEdge';
|
||||
export { SmoothStepEdge, SmoothStepEdgeInternal } from './SmoothStepEdge';
|
||||
|
||||
@@ -28,10 +28,14 @@ import { useNodeId } from '../../contexts/NodeIdContext';
|
||||
import { type ReactFlowState } from '../../types';
|
||||
import { fixedForwardRef } from '../../utils';
|
||||
|
||||
export interface HandleProps extends HandlePropsSystem, Omit<HTMLAttributes<HTMLDivElement>, 'id'> {
|
||||
/** Callback called when connection is made */
|
||||
onConnect?: OnConnect;
|
||||
}
|
||||
/**
|
||||
* @expand
|
||||
*/
|
||||
export type HandleProps = HandlePropsSystem &
|
||||
Omit<HTMLAttributes<HTMLDivElement>, 'id'> & {
|
||||
/** Callback called when connection is made */
|
||||
onConnect?: OnConnect;
|
||||
};
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
connectOnClick: s.connectOnClick,
|
||||
@@ -42,9 +46,7 @@ const selector = (s: ReactFlowState) => ({
|
||||
const connectingSelector =
|
||||
(nodeId: string | null, handleId: string | null, type: HandleType) => (state: ReactFlowState) => {
|
||||
const { connectionClickStartHandle: clickHandle, connectionMode, connection } = state;
|
||||
|
||||
const { fromHandle, toHandle, isValid } = connection;
|
||||
|
||||
const connectingTo = toHandle?.nodeId === nodeId && toHandle?.id === handleId && toHandle?.type === type;
|
||||
|
||||
return {
|
||||
@@ -56,6 +58,7 @@ const connectingSelector =
|
||||
? fromHandle?.type !== type
|
||||
: nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id,
|
||||
connectionInProcess: !!fromHandle,
|
||||
clickConnectionInProcess: !!clickHandle,
|
||||
valid: connectingTo && isValid,
|
||||
};
|
||||
};
|
||||
@@ -83,11 +86,15 @@ function HandleComponent(
|
||||
const store = useStoreApi();
|
||||
const nodeId = useNodeId();
|
||||
const { connectOnClick, noPanClassName, rfId } = useStore(selector, shallow);
|
||||
const { connectingFrom, connectingTo, clickConnecting, isPossibleEndHandle, connectionInProcess, valid } = useStore(
|
||||
connectingSelector(nodeId, handleId, type),
|
||||
shallow
|
||||
);
|
||||
|
||||
const {
|
||||
connectingFrom,
|
||||
connectingTo,
|
||||
clickConnecting,
|
||||
isPossibleEndHandle,
|
||||
connectionInProcess,
|
||||
clickConnectionInProcess,
|
||||
valid,
|
||||
} = useStore(connectingSelector(nodeId, handleId, type), shallow);
|
||||
if (!nodeId) {
|
||||
store.getState().onError?.('010', errorMessages['error010']());
|
||||
}
|
||||
@@ -228,12 +235,14 @@ function HandleComponent(
|
||||
connectingfrom: connectingFrom,
|
||||
connectingto: connectingTo,
|
||||
valid,
|
||||
// shows where you can start a connection from
|
||||
// and where you can end it while connecting
|
||||
/*
|
||||
* shows where you can start a connection from
|
||||
* and where you can end it while connecting
|
||||
*/
|
||||
connectionindicator:
|
||||
isConnectable &&
|
||||
(!connectionInProcess || isPossibleEndHandle) &&
|
||||
(connectionInProcess ? isConnectableEnd : isConnectableStart),
|
||||
(connectionInProcess || clickConnectionInProcess ? isConnectableEnd : isConnectableStart),
|
||||
},
|
||||
])}
|
||||
onMouseDown={onPointerDown}
|
||||
@@ -248,6 +257,28 @@ function HandleComponent(
|
||||
}
|
||||
|
||||
/**
|
||||
* The Handle component is a UI element that is used to connect nodes.
|
||||
* The `<Handle />` component is used in your [custom nodes](/learn/customization/custom-nodes)
|
||||
* to define connection points.
|
||||
*
|
||||
*@public
|
||||
*
|
||||
*@example
|
||||
*
|
||||
*```jsx
|
||||
*import { Handle, Position } from '@xyflow/react';
|
||||
*
|
||||
*export function CustomNode({ data }) {
|
||||
* return (
|
||||
* <>
|
||||
* <div style={{ padding: '10px 20px' }}>
|
||||
* {data.label}
|
||||
* </div>
|
||||
*
|
||||
* <Handle type="target" position={Position.Left} />
|
||||
* <Handle type="source" position={Position.Right} />
|
||||
* </>
|
||||
* );
|
||||
*};
|
||||
*```
|
||||
*/
|
||||
export const Handle = memo(fixedForwardRef(HandleComponent));
|
||||
|
||||
@@ -37,7 +37,6 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
disableKeyboardA11y,
|
||||
rfId,
|
||||
nodeTypes,
|
||||
nodeExtent,
|
||||
nodeClickDistance,
|
||||
onError,
|
||||
}: NodeWrapperProps<NodeType>) {
|
||||
@@ -109,8 +108,10 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
|
||||
|
||||
if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) {
|
||||
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
|
||||
// here we only need to call it when selectNodesOnDrag=false
|
||||
/*
|
||||
* this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
|
||||
* here we only need to call it when selectNodesOnDrag=false
|
||||
*/
|
||||
handleNodeClick({
|
||||
id,
|
||||
store,
|
||||
|
||||
@@ -49,8 +49,10 @@ export function useNodeObserver({
|
||||
|
||||
useEffect(() => {
|
||||
if (nodeRef.current) {
|
||||
// when the user programmatically changes the source or handle position, we need to update the internals
|
||||
// to make sure the edges are updated correctly
|
||||
/*
|
||||
* when the user programmatically changes the source or handle position, we need to update the internals
|
||||
* to make sure the edges are updated correctly
|
||||
*/
|
||||
const typeChanged = prevType.current !== nodeType;
|
||||
const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition;
|
||||
const targetPosChanged = prevTargetPosition.current !== node.targetPosition;
|
||||
|
||||
@@ -23,9 +23,9 @@ export const builtinNodeTypes: NodeTypes = {
|
||||
export function getNodeInlineStyleDimensions<NodeType extends Node = Node>(
|
||||
node: InternalNode<NodeType>
|
||||
): {
|
||||
width: number | string | undefined;
|
||||
height: number | string | undefined;
|
||||
} {
|
||||
width: number | string | undefined;
|
||||
height: number | string | undefined;
|
||||
} {
|
||||
if (node.internals.handleBounds === undefined) {
|
||||
return {
|
||||
width: node.width ?? node.initialWidth ?? node.style?.width,
|
||||
|
||||
@@ -4,10 +4,12 @@ import { errorMessages } from '@xyflow/system';
|
||||
|
||||
import type { ReactFlowState } from '../../types';
|
||||
|
||||
// this handler is called by
|
||||
// 1. the click handler when node is not draggable or selectNodesOnDrag = false
|
||||
// or
|
||||
// 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
|
||||
/*
|
||||
* this handler is called by
|
||||
* 1. the click handler when node is not draggable or selectNodesOnDrag = false
|
||||
* or
|
||||
* 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
|
||||
*/
|
||||
export function handleNodeClick({
|
||||
id,
|
||||
store,
|
||||
|
||||
@@ -61,9 +61,9 @@ export function NodesSelection<NodeType extends Node>({
|
||||
|
||||
const onContextMenu = onSelectionContextMenu
|
||||
? (event: MouseEvent) => {
|
||||
const selectedNodes = store.getState().nodes.filter((n) => n.selected);
|
||||
onSelectionContextMenu(event, selectedNodes);
|
||||
}
|
||||
const selectedNodes = store.getState().nodes.filter((n) => n.selected);
|
||||
onSelectionContextMenu(event, selectedNodes);
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
|
||||
@@ -1,20 +1,48 @@
|
||||
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
|
||||
import { HTMLAttributes, forwardRef } from 'react';
|
||||
import cc from 'classcat';
|
||||
import type { PanelPosition } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
|
||||
/**
|
||||
* @expand
|
||||
*/
|
||||
export type PanelProps = HTMLAttributes<HTMLDivElement> & {
|
||||
/** Set position of the panel
|
||||
* @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
|
||||
/**
|
||||
* The position of the panel.
|
||||
* @default "top-left"
|
||||
*/
|
||||
position?: PanelPosition;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const selector = (s: ReactFlowState) => (s.userSelectionActive ? 'none' : 'all');
|
||||
|
||||
/**
|
||||
* The `<Panel />` component helps you position content above the viewport.
|
||||
* It is used internally by the [`<MiniMap />`](/api-reference/components/minimap)
|
||||
* and [`<Controls />`](/api-reference/components/controls) components.
|
||||
*
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
*import { ReactFlow, Background, Panel } from '@xyflow/react';
|
||||
*
|
||||
*export default function Flow() {
|
||||
* return (
|
||||
* <ReactFlow nodes={[]} fitView>
|
||||
* <Panel position="top-left">top-left</Panel>
|
||||
* <Panel position="top-center">top-center</Panel>
|
||||
* <Panel position="top-right">top-right</Panel>
|
||||
* <Panel position="bottom-left">bottom-left</Panel>
|
||||
* <Panel position="bottom-center">bottom-center</Panel>
|
||||
* <Panel position="bottom-right">bottom-right</Panel>
|
||||
* </ReactFlow>
|
||||
* );
|
||||
*}
|
||||
*```
|
||||
*/
|
||||
export const Panel = forwardRef<HTMLDivElement, PanelProps>(
|
||||
({ position = 'top-left', children, className, style, ...rest }, ref) => {
|
||||
const pointerEvents = useStore(selector);
|
||||
@@ -32,3 +60,5 @@ export const Panel = forwardRef<HTMLDivElement, PanelProps>(
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Panel.displayName = 'Panel';
|
||||
|
||||
@@ -19,6 +19,40 @@ export type ReactFlowProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* The `<ReactFlowProvider />` component is a [context provider](https://react.dev/learn/passing-data-deeply-with-context#)
|
||||
* that makes it possible to access a flow's internal state outside of the
|
||||
* [`<ReactFlow />`](/api-reference/react-flow) component. Many of the hooks we
|
||||
* provide rely on this component to work.
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
*import { ReactFlow, ReactFlowProvider, useNodes } from '@xyflow/react'
|
||||
*
|
||||
*export default function Flow() {
|
||||
* return (
|
||||
* <ReactFlowProvider>
|
||||
* <ReactFlow nodes={...} edges={...} />
|
||||
* <Sidebar />
|
||||
* </ReactFlowProvider>
|
||||
* );
|
||||
*}
|
||||
*
|
||||
*function Sidebar() {
|
||||
* // This hook will only work if the component it's used in is a child of a
|
||||
* // <ReactFlowProvider />.
|
||||
* const nodes = useNodes()
|
||||
*
|
||||
* return <aside>do something with nodes</aside>;
|
||||
*}
|
||||
*```
|
||||
*
|
||||
* @remarks If you're using a router and want your flow's state to persist across routes,
|
||||
* it's vital that you place the `<ReactFlowProvider />` component _outside_ of
|
||||
* your router. If you have multiple flows on the same page you will need to use a separate
|
||||
* `<ReactFlowProvider />` for each flow.
|
||||
*/
|
||||
export function ReactFlowProvider({
|
||||
initialNodes: nodes,
|
||||
initialEdges: edges,
|
||||
|
||||
@@ -10,8 +10,8 @@ import { shallow } from 'zustand/shallow';
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import type { ReactFlowState, OnSelectionChangeFunc, Node, Edge } from '../../types';
|
||||
|
||||
type SelectionListenerProps = {
|
||||
onSelectionChange?: OnSelectionChangeFunc;
|
||||
type SelectionListenerProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
onSelectionChange?: OnSelectionChangeFunc<NodeType, EdgeType>;
|
||||
};
|
||||
|
||||
const selector = (s: ReactFlowState) => {
|
||||
@@ -44,12 +44,14 @@ function areEqual(a: SelectorSlice, b: SelectorSlice) {
|
||||
);
|
||||
}
|
||||
|
||||
function SelectionListenerInner({ onSelectionChange }: SelectionListenerProps) {
|
||||
const store = useStoreApi();
|
||||
function SelectionListenerInner<NodeType extends Node = Node, EdgeType extends Edge = Edge>({
|
||||
onSelectionChange,
|
||||
}: SelectionListenerProps<NodeType, EdgeType>) {
|
||||
const store = useStoreApi<NodeType, EdgeType>();
|
||||
const { selectedNodes, selectedEdges } = useStore(selector, areEqual);
|
||||
|
||||
useEffect(() => {
|
||||
const params = { nodes: selectedNodes, edges: selectedEdges };
|
||||
const params = { nodes: selectedNodes as NodeType[], edges: selectedEdges as EdgeType[] };
|
||||
|
||||
onSelectionChange?.(params);
|
||||
store.getState().onSelectionChangeHandlers.forEach((fn) => fn(params));
|
||||
@@ -60,11 +62,13 @@ function SelectionListenerInner({ onSelectionChange }: SelectionListenerProps) {
|
||||
|
||||
const changeSelector = (s: ReactFlowState) => !!s.onSelectionChangeHandlers;
|
||||
|
||||
export function SelectionListener({ onSelectionChange }: SelectionListenerProps) {
|
||||
export function SelectionListener<NodeType extends Node = Node, EdgeType extends Edge = Edge>({
|
||||
onSelectionChange,
|
||||
}: SelectionListenerProps<NodeType, EdgeType>) {
|
||||
const storeHasSelectionChangeHandlers = useStore(changeSelector);
|
||||
|
||||
if (onSelectionChange || storeHasSelectionChangeHandlers) {
|
||||
return <SelectionListenerInner onSelectionChange={onSelectionChange} />;
|
||||
return <SelectionListenerInner<NodeType, EdgeType> onSelectionChange={onSelectionChange} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types';
|
||||
import { defaultNodeOrigin } from '../../container/ReactFlow/init-values';
|
||||
|
||||
// these fields exist in the global store and we need to keep them up to date
|
||||
// These fields exist in the global store, and we need to keep them up to date
|
||||
const reactFlowFieldsToTrack = [
|
||||
'nodes',
|
||||
'edges',
|
||||
@@ -94,9 +94,11 @@ const selector = (s: ReactFlowState) => ({
|
||||
});
|
||||
|
||||
const initPrevValues = {
|
||||
// these are values that are also passed directly to other components
|
||||
// than the StoreUpdater. We can reduce the number of setStore calls
|
||||
// by setting the same values here as prev fields.
|
||||
/*
|
||||
* these are values that are also passed directly to other components
|
||||
* than the StoreUpdater. We can reduce the number of setStore calls
|
||||
* by setting the same values here as prev fields.
|
||||
*/
|
||||
translateExtent: infiniteExtent,
|
||||
nodeOrigin: defaultNodeOrigin,
|
||||
minZoom: 0.5,
|
||||
@@ -152,8 +154,8 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
|
||||
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
|
||||
else if (fieldName === 'paneClickDistance') setPaneClickDistance(fieldValue as number);
|
||||
// Renamed fields
|
||||
else if (fieldName === 'fitView') store.setState({ fitViewOnInit: fieldValue as boolean });
|
||||
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOnInitOptions: fieldValue as FitViewOptions });
|
||||
else if (fieldName === 'fitView') store.setState({ fitViewQueued: fieldValue as boolean });
|
||||
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOptions: fieldValue as FitViewOptions });
|
||||
// General case
|
||||
else store.setState({ [fieldName]: fieldValue });
|
||||
}
|
||||
|
||||
@@ -6,6 +6,31 @@ import type { ReactFlowState } from '../../types';
|
||||
|
||||
const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__viewport-portal');
|
||||
|
||||
/**
|
||||
* The `<ViewportPortal />` component can be used to add components to the same viewport
|
||||
* of the flow where nodes and edges are rendered. This is useful when you want to render
|
||||
* your own components that are adhere to the same coordinate system as the nodes & edges
|
||||
* and are also affected by zooming and panning
|
||||
* @public
|
||||
* @example
|
||||
*
|
||||
* ```jsx
|
||||
*import React from 'react';
|
||||
*import { ViewportPortal } from '@xyflow/react';
|
||||
*
|
||||
*export default function () {
|
||||
* return (
|
||||
* <ViewportPortal>
|
||||
* <div
|
||||
* style={{ transform: 'translate(100px, 100px)', position: 'absolute' }}
|
||||
* >
|
||||
* This div is positioned at [100, 100] on the flow.
|
||||
* </div>
|
||||
* </ViewportPortal>
|
||||
* );
|
||||
*}
|
||||
*```
|
||||
*/
|
||||
export function ViewportPortal({ children }: { children: ReactNode }) {
|
||||
const viewPortalDiv = useStore(selector);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user