Merge branch 'main' into patch-1
This commit is contained in:
@@ -24,6 +24,10 @@ export const errorMessages = {
|
||||
`Node with id "${id}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`,
|
||||
error013: (lib: string = 'react') =>
|
||||
`It seems that you haven't loaded the styles. Please import '@xyflow/${lib}/dist/style.css' or base.css to make sure everything is working properly.`,
|
||||
error014: () =>
|
||||
'useNodeConnections: No node ID found. Call useNodeConnections inside a custom Node or provide a node ID.',
|
||||
error015: () =>
|
||||
'It seems that you are trying to drag a node that is not initialized. Please use onNodesChange as explained in the docs.',
|
||||
};
|
||||
|
||||
export const infiniteExtent: CoordinateExtent = [
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
--xy-attribution-background-color-default: rgba(255, 255, 255, 0.5);
|
||||
|
||||
--xy-minimap-background-color-default: #fff;
|
||||
--xy-minimap-mask-background-color-default: rgb(240, 240, 240, 0.6);
|
||||
--xy-minimap-mask-background-color-default: rgba(240, 240, 240, 0.6);
|
||||
--xy-minimap-mask-stroke-color-default: transparent;
|
||||
--xy-minimap-mask-stroke-width-default: 1;
|
||||
--xy-minimap-node-background-color-default: #e2e2e2;
|
||||
@@ -37,7 +37,7 @@
|
||||
--xy-attribution-background-color-default: rgba(150, 150, 150, 0.25);
|
||||
|
||||
--xy-minimap-background-color-default: #141414;
|
||||
--xy-minimap-mask-background-color-default: rgb(60, 60, 60, 0.6);
|
||||
--xy-minimap-mask-background-color-default: rgba(60, 60, 60, 0.6);
|
||||
--xy-minimap-mask-stroke-color-default: transparent;
|
||||
--xy-minimap-mask-stroke-width-default: 1;
|
||||
--xy-minimap-node-background-color-default: #2b2b2b;
|
||||
@@ -55,7 +55,7 @@
|
||||
}
|
||||
|
||||
.xy-flow__background {
|
||||
background-color: var(--xy-background-color, var(--xy-background-color-props, var(--xy-background-color-default)));
|
||||
background-color: var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));
|
||||
}
|
||||
|
||||
.xy-flow__container {
|
||||
@@ -278,6 +278,14 @@ svg.xy-flow__connectionline {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&.top,
|
||||
&.bottom {
|
||||
&.center {
|
||||
left: 50%;
|
||||
transform: translateX(-15px) translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&.left {
|
||||
left: 0;
|
||||
}
|
||||
@@ -286,9 +294,12 @@ svg.xy-flow__connectionline {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.center {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
&.left,
|
||||
&.right {
|
||||
&.center {
|
||||
top: 50%;
|
||||
transform: translateY(-15px) translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -158,4 +158,17 @@
|
||||
&-button:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.horizontal &-button {
|
||||
border-bottom: none;
|
||||
border-right: 1px solid
|
||||
var(
|
||||
--xy-controls-button-border-color-props,
|
||||
var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default))
|
||||
);
|
||||
}
|
||||
|
||||
&.horizontal &-button:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export type NodeDimensionChange = {
|
||||
/* if this is true, the node is currently being resized via the NodeResizer */
|
||||
resizing?: boolean;
|
||||
/* if this is true, we will set width and height of the node and not just the measured dimensions */
|
||||
setAttributes?: boolean;
|
||||
setAttributes?: boolean | 'width' | 'height';
|
||||
};
|
||||
|
||||
export type NodePositionChange = {
|
||||
@@ -42,7 +42,10 @@ export type NodeReplaceChange<NodeType extends NodeBase = NodeBase> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Union type of all possible node changes.
|
||||
* The [`onNodesChange`](/api-reference/react-flow#on-nodes-change) callback takes
|
||||
*an array of `NodeChange` objects that you should use to update your flow's state.
|
||||
*The `NodeChange` type is a union of six different object types that represent that
|
||||
*various ways an node can change in a flow.
|
||||
* @public
|
||||
*/
|
||||
export type NodeChange<NodeType extends NodeBase = NodeBase> =
|
||||
@@ -67,6 +70,14 @@ export type EdgeReplaceChange<EdgeType extends EdgeBase = EdgeBase> = {
|
||||
type: 'replace';
|
||||
};
|
||||
|
||||
/**
|
||||
* The [`onEdgesChange`](/api-reference/react-flow#on-edges-change) callback takes
|
||||
*an array of `EdgeChange` objects that you should use to update your flow's state.
|
||||
*The `EdgeChange` type is a union of four different object types that represent that
|
||||
*various ways an edge can change in a flow.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EdgeChange<EdgeType extends EdgeBase = EdgeBase> =
|
||||
| EdgeSelectionChange
|
||||
| EdgeRemoveChange
|
||||
|
||||
@@ -4,40 +4,41 @@ export type EdgeBase<
|
||||
EdgeData extends Record<string, unknown> = Record<string, unknown>,
|
||||
EdgeType extends string | undefined = string | undefined
|
||||
> = {
|
||||
/** Unique id of an edge */
|
||||
/** Unique id of an edge. */
|
||||
id: string;
|
||||
/** Type of an edge defined in edgeTypes */
|
||||
/** Type of edge defined in `edgeTypes`. */
|
||||
type?: EdgeType;
|
||||
/** Id of source node */
|
||||
/** Id of source node. */
|
||||
source: string;
|
||||
/** Id of target node */
|
||||
/** Id of target node. */
|
||||
target: string;
|
||||
/** Id of source handle
|
||||
* only needed if there are multiple handles per node
|
||||
*/
|
||||
/** Id of source handle, only needed if there are multiple handles per node. */
|
||||
sourceHandle?: string | null;
|
||||
/** Id of target handle
|
||||
* only needed if there are multiple handles per node
|
||||
*/
|
||||
/** Id of target handle, only needed if there are multiple handles per node. */
|
||||
targetHandle?: string | null;
|
||||
animated?: boolean;
|
||||
hidden?: boolean;
|
||||
deletable?: boolean;
|
||||
selectable?: boolean;
|
||||
/** Arbitrary data passed to an edge */
|
||||
/** Arbitrary data passed to an edge. */
|
||||
data?: EdgeData;
|
||||
selected?: boolean;
|
||||
/** Set the marker on the beginning of an edge
|
||||
/**
|
||||
* Set the marker on the beginning of an edge.
|
||||
* @example 'arrow', 'arrowclosed' or custom marker
|
||||
*/
|
||||
markerStart?: EdgeMarkerType;
|
||||
/** Set the marker on the end of an edge
|
||||
/**
|
||||
* Set the marker on the end of an edge.
|
||||
* @example 'arrow', 'arrowclosed' or custom marker
|
||||
*/
|
||||
markerEnd?: EdgeMarkerType;
|
||||
zIndex?: number;
|
||||
ariaLabel?: string;
|
||||
/** Padding around the edge where interaction is still possible */
|
||||
/**
|
||||
* ReactFlow renders an invisible path around each edge to make them easier to click or tap on.
|
||||
* This property sets the width of that invisible path.
|
||||
*/
|
||||
interactionWidth?: number;
|
||||
};
|
||||
|
||||
@@ -54,11 +55,24 @@ export type BezierPathOptions = {
|
||||
curvature?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type DefaultEdgeOptionsBase<EdgeType extends EdgeBase> = Omit<
|
||||
EdgeType,
|
||||
'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'selected'
|
||||
>;
|
||||
|
||||
/**
|
||||
* If you set the `connectionLineType` prop on your [`<ReactFlow />`](/api-reference/react-flow#connection-connectionLineType)
|
||||
*component, it will dictate the style of connection line rendered when creating
|
||||
*new edges.
|
||||
*
|
||||
* @public
|
||||
*
|
||||
* @remarks If you choose to render a custom connection line component, this value will be
|
||||
*passed to your component as part of its [`ConnectionLineComponentProps`](/api-reference/types/connection-line-component-props).
|
||||
*/
|
||||
export enum ConnectionLineType {
|
||||
Bezier = 'default',
|
||||
Straight = 'straight',
|
||||
@@ -67,6 +81,13 @@ export enum ConnectionLineType {
|
||||
SimpleBezier = 'simplebezier',
|
||||
}
|
||||
|
||||
/**
|
||||
* Edges can optionally have markers at the start and end of an edge. The `EdgeMarker`
|
||||
*type is used to configure those markers! Check the docs for [`MarkerType`](/api-reference/types/marker-type)
|
||||
*for details on what types of edge marker are available.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EdgeMarker = {
|
||||
type: MarkerType;
|
||||
color?: string;
|
||||
@@ -79,6 +100,12 @@ export type EdgeMarker = {
|
||||
|
||||
export type EdgeMarkerType = string | EdgeMarker;
|
||||
|
||||
/**
|
||||
* Edges may optionally have a marker on either end. The MarkerType type enumerates
|
||||
* the options available to you when configuring a given marker.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export enum MarkerType {
|
||||
Arrow = 'arrow',
|
||||
ArrowClosed = 'arrowclosed',
|
||||
@@ -88,6 +115,9 @@ export type MarkerProps = EdgeMarker & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type EdgePosition = {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
import type { Selection as D3Selection } from 'd3-selection';
|
||||
import type { D3DragEvent, SubjectPosition } from 'd3-drag';
|
||||
import type { ZoomBehavior } from 'd3-zoom';
|
||||
// this is needed for the Selection type to include the transition function :/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- this is needed for the Selection type to include the transition function :/
|
||||
import type { Transition } from 'd3-transition';
|
||||
|
||||
import type { XYPosition, Rect, Position } from './utils';
|
||||
@@ -26,17 +25,46 @@ export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionO
|
||||
export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => Promise<boolean>;
|
||||
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => Promise<boolean>;
|
||||
|
||||
/**
|
||||
* The `Connection` type is the basic minimal description of an [`Edge`](/api-reference/types/edge)
|
||||
* between two nodes. The [`addEdge`](/api-reference/utils/add-edge) util can be used to upgrade
|
||||
* a `Connection` to an [`Edge`](/api-reference/types/edge).
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type Connection = {
|
||||
/** The id of the node this connection originates from. */
|
||||
source: string;
|
||||
/** The id of the node this connection terminates at. */
|
||||
target: string;
|
||||
/** When not `null`, the id of the handle on the source node that this connection originates from. */
|
||||
sourceHandle: string | null;
|
||||
/** When not `null`, the id of the handle on the target node that this connection terminates at. */
|
||||
targetHandle: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The `HandleConnection` type is an extension of a basic [Connection](/api-reference/types/connection) that includes the `edgeId`.
|
||||
*/
|
||||
export type HandleConnection = Connection & {
|
||||
edgeId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The `NodeConnection` type is an extension of a basic [Connection](/api-reference/types/connection) that includes the `edgeId`.
|
||||
*
|
||||
*/
|
||||
export type NodeConnection = Connection & {
|
||||
edgeId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The `ConnectionMode` is used to set the mode of connection between nodes.
|
||||
* The `Strict` mode is the default one and only allows source to target edges.
|
||||
* `Loose` mode allows source to source and target to target edges as well.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export enum ConnectionMode {
|
||||
Strict = 'strict',
|
||||
Loose = 'loose',
|
||||
@@ -52,8 +80,24 @@ export type OnConnectStart = (event: MouseEvent | TouchEvent, params: OnConnectS
|
||||
export type OnConnect = (connection: Connection) => void;
|
||||
export type OnConnectEnd = (event: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => void;
|
||||
|
||||
export type OnReconnect<EdgeType extends EdgeBase = EdgeBase> = (oldEdge: EdgeType, newConnection: Connection) => void;
|
||||
export type OnReconnectStart<EdgeType extends EdgeBase = EdgeBase> = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
edge: EdgeType,
|
||||
handleType: HandleType
|
||||
) => void;
|
||||
export type OnReconnectEnd<EdgeType extends EdgeBase = EdgeBase> = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
edge: EdgeType,
|
||||
handleType: HandleType,
|
||||
connectionState: FinalConnectionState
|
||||
) => void;
|
||||
|
||||
export type IsValidConnection = (edge: EdgeBase | Connection) => boolean;
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type FitViewParamsBase<NodeType extends NodeBase> = {
|
||||
nodes: Map<string, InternalNodeBase<NodeType>>;
|
||||
width: number;
|
||||
@@ -63,15 +107,45 @@ export type FitViewParamsBase<NodeType extends NodeBase> = {
|
||||
maxZoom: number;
|
||||
};
|
||||
|
||||
export type PaddingUnit = 'px' | '%';
|
||||
export type PaddingWithUnit = `${number}${PaddingUnit}` | number;
|
||||
|
||||
export type Padding =
|
||||
| PaddingWithUnit
|
||||
| {
|
||||
top?: PaddingWithUnit;
|
||||
right?: PaddingWithUnit;
|
||||
bottom?: PaddingWithUnit;
|
||||
left?: PaddingWithUnit;
|
||||
x?: PaddingWithUnit;
|
||||
y?: PaddingWithUnit;
|
||||
};
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type FitViewOptionsBase<NodeType extends NodeBase = NodeBase> = {
|
||||
padding?: number;
|
||||
padding?: Padding;
|
||||
includeHiddenNodes?: boolean;
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
duration?: number;
|
||||
ease?: (t: number) => number;
|
||||
interpolate?: 'smooth' | 'linear';
|
||||
nodes?: (NodeType | { id: string })[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Internally, React Flow maintains a coordinate system that is independent of the
|
||||
* rest of the page. The `Viewport` type tells you where in that system your flow
|
||||
* is currently being display at and how zoomed in or out it is.
|
||||
*
|
||||
* @public
|
||||
* @remarks A `Transform` has the same properties as the viewport, but they represent
|
||||
* different things. Make sure you don't get them muddled up or things will start
|
||||
* to look weird!
|
||||
*
|
||||
*/
|
||||
export type Viewport = {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -82,14 +156,27 @@ export type KeyCode = string | Array<string>;
|
||||
|
||||
export type SnapGrid = [number, number];
|
||||
|
||||
/**
|
||||
* This enum is used to set the different modes of panning the viewport when the
|
||||
* user scrolls. The `Free` mode allows the user to pan in any direction by scrolling
|
||||
* with a device like a trackpad. The `Vertical` and `Horizontal` modes restrict
|
||||
* scroll panning to only the vertical or horizontal axis, respectively.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export enum PanOnScrollMode {
|
||||
Free = 'free',
|
||||
Vertical = 'vertical',
|
||||
Horizontal = 'horizontal',
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type ViewportHelperFunctionOptions = {
|
||||
duration?: number;
|
||||
ease?: (t: number) => number;
|
||||
interpolate?: 'smooth' | 'linear';
|
||||
};
|
||||
|
||||
export type SetCenterOptions = ViewportHelperFunctionOptions & {
|
||||
@@ -108,7 +195,23 @@ export type D3ZoomHandler = (this: Element, event: any, d: unknown) => void;
|
||||
|
||||
export type UpdateNodeInternals = (nodeId: string | string[]) => void;
|
||||
|
||||
export type PanelPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
||||
/**
|
||||
* This type is mostly used to help position things on top of the flow viewport. For
|
||||
* example both the [`<MiniMap />`](/api-reference/components/minimap) and
|
||||
* [`<Controls />`](/api-reference/components/controls) components take a `position`
|
||||
* prop of this type.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PanelPosition =
|
||||
| 'top-left'
|
||||
| 'top-center'
|
||||
| 'top-right'
|
||||
| 'bottom-left'
|
||||
| 'bottom-center'
|
||||
| 'bottom-right'
|
||||
| 'center-left'
|
||||
| 'center-right';
|
||||
|
||||
export type ProOptions = {
|
||||
account?: string;
|
||||
@@ -158,17 +261,37 @@ export type NoConnection = {
|
||||
toNode: null;
|
||||
};
|
||||
export type ConnectionInProgress<NodeType extends InternalNodeBase = InternalNodeBase> = {
|
||||
/** Indicates whether a connection is currently in progress. */
|
||||
inProgress: true;
|
||||
/**
|
||||
* If an ongoing connection is above a handle or inside the connection radius, this will be `true`
|
||||
* or `false`, otherwise `null`.
|
||||
*/
|
||||
isValid: boolean | null;
|
||||
/** Returns the xy start position or `null` if no connection is in progress. */
|
||||
from: XYPosition;
|
||||
/** Returns the start handle or `null` if no connection is in progress. */
|
||||
fromHandle: Handle;
|
||||
/** Returns the side (called position) of the start handle or `null` if no connection is in progress. */
|
||||
fromPosition: Position;
|
||||
/** Returns the start node or `null` if no connection is in progress. */
|
||||
fromNode: NodeType;
|
||||
/** Returns the xy end position or `null` if no connection is in progress. */
|
||||
to: XYPosition;
|
||||
/** Returns the end handle or `null` if no connection is in progress. */
|
||||
toHandle: Handle | null;
|
||||
/** Returns the side (called position) of the end handle or `null` if no connection is in progress. */
|
||||
toPosition: Position;
|
||||
/** Returns the end node or `null` if no connection is in progress. */
|
||||
toNode: NodeType | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The `ConnectionState` type bundles all information about an ongoing connection.
|
||||
* It is returned by the [`useConnection`](/api-reference/hooks/use-connection) hook.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ConnectionState<NodeType extends InternalNodeBase = InternalNodeBase> =
|
||||
| ConnectionInProgress<NodeType>
|
||||
| NoConnection;
|
||||
|
||||
@@ -14,27 +14,45 @@ export type Handle = {
|
||||
};
|
||||
|
||||
export type HandleProps = {
|
||||
/** Type of the handle
|
||||
/**
|
||||
* Type of the handle.
|
||||
* @default "source"
|
||||
* @example HandleType.Source, HandleType.Target
|
||||
*/
|
||||
type: HandleType;
|
||||
/** Position of the handle
|
||||
* @example Position.TopLeft, Position.TopRight,
|
||||
* Position.BottomLeft, Position.BottomRight
|
||||
/**
|
||||
* The position of the handle relative to the node. In a horizontal flow source handles are
|
||||
* typically `Position.Right` and in a vertical flow they are typically `Position.Top`.
|
||||
* @default Position.Top
|
||||
* @example Position.TopLeft, Position.TopRight, Position.BottomLeft, Position.BottomRight
|
||||
*/
|
||||
position: Position;
|
||||
/** Should you be able to connect to/from this handle */
|
||||
/**
|
||||
* Should you be able to connect to/from this handle.
|
||||
* @default true
|
||||
*/
|
||||
isConnectable?: boolean;
|
||||
/** Should you be able to connect from this handle */
|
||||
/**
|
||||
* Dictates whether a connection can start from this handle.
|
||||
* @default true
|
||||
*/
|
||||
isConnectableStart?: boolean;
|
||||
/** Should you be able to connect to this handle */
|
||||
/**
|
||||
* Dictates whether a connection can end on this handle.
|
||||
* @default true
|
||||
*/
|
||||
isConnectableEnd?: boolean;
|
||||
/** Callback if connection is valid
|
||||
/**
|
||||
* Called when a connection is dragged to this handle. You can use this callback to perform some
|
||||
* custom validation logic based on the connection target and source, for example. Where possible,
|
||||
* we recommend you move this logic to the `isValidConnection` prop on the main ReactFlow
|
||||
* component for performance reasons.
|
||||
* @remarks connection becomes an edge if isValidConnection returns true
|
||||
*/
|
||||
isValidConnection?: IsValidConnection;
|
||||
/** Id of the handle
|
||||
/**
|
||||
* Id of the handle.
|
||||
* @remarks optional if there is only one handle of this type
|
||||
*/
|
||||
id?: string;
|
||||
id?: string | null;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Optional } from '../utils/types';
|
||||
/**
|
||||
* Framework independent node data structure.
|
||||
*
|
||||
* @inline
|
||||
* @typeParam NodeData - type of the node data
|
||||
* @typeParam NodeType - type of the node
|
||||
*/
|
||||
@@ -11,45 +12,60 @@ export type NodeBase<
|
||||
NodeData extends Record<string, unknown> = Record<string, unknown>,
|
||||
NodeType extends string | undefined = string | undefined
|
||||
> = {
|
||||
/** Unique id of a node */
|
||||
/** Unique id of a node. */
|
||||
id: string;
|
||||
/** Position of a node on the pane
|
||||
/**
|
||||
* Position of a node on the pane.
|
||||
* @example { x: 0, y: 0 }
|
||||
*/
|
||||
position: XYPosition;
|
||||
/** Arbitrary data passed to a node */
|
||||
/** Arbitrary data passed to a node. */
|
||||
data: NodeData;
|
||||
/** Only relevant for default, source, target nodeType. controls source position
|
||||
/**
|
||||
* Only relevant for default, source, target nodeType. Controls source position.
|
||||
* @example 'right', 'left', 'top', 'bottom'
|
||||
*/
|
||||
sourcePosition?: Position;
|
||||
/** Only relevant for default, source, target nodeType. controls target position
|
||||
/**
|
||||
* Only relevant for default, source, target nodeType. Controls target position.
|
||||
* @example 'right', 'left', 'top', 'bottom'
|
||||
*/
|
||||
targetPosition?: Position;
|
||||
/** Whether or not the node should be visible on the canvas. */
|
||||
hidden?: boolean;
|
||||
selected?: boolean;
|
||||
/** True, if node is being dragged */
|
||||
/** Whether or not the node is currently being dragged. */
|
||||
dragging?: boolean;
|
||||
/** Whether or not the node is able to be dragged. */
|
||||
draggable?: boolean;
|
||||
selectable?: boolean;
|
||||
connectable?: boolean;
|
||||
deletable?: boolean;
|
||||
/**
|
||||
* A class name that can be applied to elements inside the node that allows those elements to act
|
||||
* as drag handles, letting the user drag the node by clicking and dragging on those elements.
|
||||
*/
|
||||
dragHandle?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
initialWidth?: number;
|
||||
initialHeight?: number;
|
||||
/** Parent node id, used for creating sub-flows */
|
||||
/** Parent node id, used for creating sub-flows. */
|
||||
parentId?: string;
|
||||
zIndex?: number;
|
||||
/** Boundary a node can be moved in
|
||||
/**
|
||||
* Boundary a node can be moved in.
|
||||
* @example 'parent' or [[0, 0], [100, 100]]
|
||||
*/
|
||||
extent?: 'parent' | CoordinateExtent;
|
||||
/**
|
||||
* When `true`, the parent node will automatically expand if this node is dragged to the edge of
|
||||
* the parent node's bounds.
|
||||
*/
|
||||
expandParent?: boolean;
|
||||
ariaLabel?: string;
|
||||
/** Origin of the node relative to it's position
|
||||
/**
|
||||
* Origin of the node relative to its position.
|
||||
* @example
|
||||
* [0.5, 0.5] // centers the node
|
||||
* [0, 0] // top left
|
||||
@@ -71,7 +87,7 @@ export type NodeBase<
|
||||
type: NodeType;
|
||||
});
|
||||
|
||||
export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = NodeType & {
|
||||
export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = Omit<NodeType, 'measured'> & {
|
||||
measured: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
@@ -79,8 +95,10 @@ export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = NodeType &
|
||||
internals: {
|
||||
positionAbsolute: XYPosition;
|
||||
z: number;
|
||||
/** Holds a reference to the original node object provided by the user.
|
||||
* Used as an optimization to avoid certain operations. */
|
||||
/**
|
||||
* Holds a reference to the original node object provided by the user.
|
||||
* Used as an optimization to avoid certain operations.
|
||||
*/
|
||||
userNode: NodeType;
|
||||
handleBounds?: NodeHandleBounds;
|
||||
bounds?: NodeBounds;
|
||||
@@ -88,31 +106,20 @@ export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = NodeType &
|
||||
};
|
||||
|
||||
/**
|
||||
* The node data structure that gets used for the nodes prop.
|
||||
* The node data structure that gets used for the custom nodes props.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type NodeProps<NodeType extends NodeBase> = Pick<
|
||||
NodeType,
|
||||
| 'id'
|
||||
| 'data'
|
||||
| 'width'
|
||||
| 'height'
|
||||
| 'sourcePosition'
|
||||
| 'targetPosition'
|
||||
| 'selected'
|
||||
| 'dragHandle'
|
||||
| 'selectable'
|
||||
| 'deletable'
|
||||
| 'draggable'
|
||||
| 'parentId'
|
||||
'id' | 'data' | 'width' | 'height' | 'sourcePosition' | 'targetPosition' | 'dragHandle' | 'parentId'
|
||||
> &
|
||||
Required<Pick<NodeType, 'type' | 'dragging' | 'zIndex'>> & {
|
||||
/** whether a node is connectable or not */
|
||||
Required<Pick<NodeType, 'type' | 'dragging' | 'zIndex' | 'selectable' | 'deletable' | 'selected' | 'draggable'>> & {
|
||||
/** Whether a node is connectable or not. */
|
||||
isConnectable: boolean;
|
||||
/** position absolute x value */
|
||||
/** Position absolute x value. */
|
||||
positionAbsoluteX: number;
|
||||
/** position absolute x value */
|
||||
/** Position absolute y value. */
|
||||
positionAbsoluteY: number;
|
||||
};
|
||||
|
||||
@@ -151,10 +158,22 @@ export type NodeDragItem = {
|
||||
expandParent?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* The origin of a Node determines how it is placed relative to its own coordinates.
|
||||
* `[0, 0]` places it at the top left corner, `[0.5, 0.5]` right in the center and
|
||||
* `[1, 1]` at the bottom right of its position.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type NodeOrigin = [number, number];
|
||||
|
||||
export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void;
|
||||
|
||||
/**
|
||||
* Type for the handles of a node
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type NodeHandle = Omit<Optional<Handle, 'width' | 'height'>, 'nodeId'>;
|
||||
|
||||
export type Align = 'center' | 'start' | 'end';
|
||||
|
||||
@@ -20,6 +20,8 @@ export type PanZoomParams = {
|
||||
|
||||
export type PanZoomTransformOptions = {
|
||||
duration?: number;
|
||||
ease?: (t: number) => number;
|
||||
interpolate?: 'smooth' | 'linear';
|
||||
};
|
||||
|
||||
export type OnPanZoom = (event: MouseEvent | TouchEvent | null, viewport: Viewport) => void;
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* While [`PanelPosition`](/api-reference/types/panel-position) can be used to place a
|
||||
* component in the corners of a container, the `Position` enum is less precise and used
|
||||
* primarily in relation to edges and handles.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export enum Position {
|
||||
Left = 'left',
|
||||
Top = 'top',
|
||||
@@ -12,6 +19,11 @@ export const oppositePosition = {
|
||||
[Position.Bottom]: Position.Top,
|
||||
};
|
||||
|
||||
/**
|
||||
* All positions are stored in an object with x and y coordinates.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type XYPosition = {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -33,4 +45,14 @@ export type Box = XYPosition & {
|
||||
|
||||
export type Transform = [number, number, number];
|
||||
|
||||
/**
|
||||
* A coordinate extent represents two points in a coordinate system: one in the top
|
||||
* left corner and one in the bottom right corner. It is used to represent the
|
||||
* bounds of nodes in the flow or the bounds of the viewport.
|
||||
*
|
||||
* @public
|
||||
*
|
||||
* @remarks Props that expect a `CoordinateExtent` usually default to `[[-∞, -∞], [+∞, +∞]]`
|
||||
* to represent an unbounded extent.
|
||||
*/
|
||||
export type CoordinateExtent = [[number, number], [number, number]];
|
||||
|
||||
@@ -5,14 +5,18 @@ export type GetPointerPositionParams = {
|
||||
transform: Transform;
|
||||
snapGrid?: SnapGrid;
|
||||
snapToGrid?: boolean;
|
||||
containerBounds: DOMRect | null;
|
||||
};
|
||||
|
||||
export function getPointerPosition(
|
||||
event: MouseEvent | TouchEvent,
|
||||
{ snapGrid = [0, 0], snapToGrid = false, transform }: GetPointerPositionParams
|
||||
{ snapGrid = [0, 0], snapToGrid = false, transform, containerBounds }: GetPointerPositionParams
|
||||
): XYPosition & { xSnapped: number; ySnapped: number } {
|
||||
const { x, y } = getEventPosition(event);
|
||||
const pointerPos = pointToRendererPoint({ x, y }, transform);
|
||||
const pointerPos = pointToRendererPoint(
|
||||
{ x: x - (containerBounds?.left ?? 0), y: y - (containerBounds?.top ?? 0) },
|
||||
transform
|
||||
);
|
||||
const { x: xSnapped, y: ySnapped } = snapToGrid ? snapPosition(pointerPos, snapGrid) : pointerPos;
|
||||
|
||||
// we need the snapped position in order to be able to skip unnecessary drag events
|
||||
@@ -28,18 +32,20 @@ export const getDimensions = (node: HTMLDivElement): Dimensions => ({
|
||||
height: node.offsetHeight,
|
||||
});
|
||||
|
||||
export const getHostForElement = (element: HTMLElement): Document | ShadowRoot =>
|
||||
(element.getRootNode?.() as Document | ShadowRoot) || window?.document;
|
||||
export const getHostForElement = (element: HTMLElement | EventTarget | null): Document | ShadowRoot =>
|
||||
((element as Partial<HTMLElement> | null)?.getRootNode?.() as Document | ShadowRoot) || window?.document;
|
||||
|
||||
const inputTags = ['INPUT', 'SELECT', 'TEXTAREA'];
|
||||
|
||||
export function isInputDOMNode(event: KeyboardEvent): boolean {
|
||||
// using composed path for handling shadow dom
|
||||
const target = (event.composedPath?.()?.[0] || event.target) as HTMLElement;
|
||||
const isInput = inputTags.includes(target?.nodeName) || target?.hasAttribute('contenteditable');
|
||||
const target = (event.composedPath?.()?.[0] || event.target) as Element | null;
|
||||
if (target?.nodeType !== 1 /* Node.ELEMENT_NODE */) return false;
|
||||
|
||||
const isInput = inputTags.includes(target.nodeName) || target.hasAttribute('contenteditable');
|
||||
|
||||
// when an input field is focused we don't want to trigger deletion or movement of nodes
|
||||
return isInput || !!target?.closest('.nokey');
|
||||
return isInput || !!target.closest('.nokey');
|
||||
}
|
||||
|
||||
export const isMouseEvent = (event: MouseEvent | TouchEvent): event is MouseEvent => 'clientX' in event;
|
||||
@@ -55,9 +61,11 @@ export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRec
|
||||
};
|
||||
};
|
||||
|
||||
// The handle bounds are calculated relative to the node element.
|
||||
// We store them in the internals object of the node in order to avoid
|
||||
// unnecessary recalculations.
|
||||
/*
|
||||
* The handle bounds are calculated relative to the node element.
|
||||
* We store them in the internals object of the node in order to avoid
|
||||
* unnecessary recalculations.
|
||||
*/
|
||||
export const getHandleBounds = (
|
||||
type: 'source' | 'target',
|
||||
nodeElement: HTMLDivElement,
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import { Position } from '../../types';
|
||||
|
||||
export type GetBezierPathParams = {
|
||||
/** The `x` position of the source handle. */
|
||||
sourceX: number;
|
||||
/** The `y` position of the source handle. */
|
||||
sourceY: number;
|
||||
/**
|
||||
* The position of the source handle.
|
||||
* @default Position.Bottom
|
||||
*/
|
||||
sourcePosition?: Position;
|
||||
/** The `x` position of the target handle. */
|
||||
targetX: number;
|
||||
/** The `y` position of the target handle. */
|
||||
targetY: number;
|
||||
/**
|
||||
* The position of the target handle.
|
||||
* @default Position.Top
|
||||
*/
|
||||
targetPosition?: Position;
|
||||
/**
|
||||
* The curvature of the bezier edge.
|
||||
* @default 0.25
|
||||
*/
|
||||
curvature?: number;
|
||||
};
|
||||
|
||||
@@ -38,8 +54,10 @@ export function getBezierEdgeCenter({
|
||||
targetControlX: number;
|
||||
targetControlY: number;
|
||||
}): [number, number, number, number] {
|
||||
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
|
||||
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
|
||||
/*
|
||||
* cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
|
||||
* https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
|
||||
*/
|
||||
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125;
|
||||
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125;
|
||||
const offsetX = Math.abs(centerX - sourceX);
|
||||
@@ -70,27 +88,35 @@ function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurva
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bezier path from source to target handle
|
||||
* @param params.sourceX - The x position of the source handle
|
||||
* @param params.sourceY - The y position of the source handle
|
||||
* @param params.sourcePosition - The position of the source handle (default: Position.Bottom)
|
||||
* @param params.targetX - The x position of the target handle
|
||||
* @param params.targetY - The y position of the target handle
|
||||
* @param params.targetPosition - The position of the target handle (default: Position.Top)
|
||||
* @param params.curvature - The curvature of the bezier edge
|
||||
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
|
||||
* The `getBezierPath` util returns everything you need to render a bezier edge
|
||||
*between two nodes.
|
||||
* @public
|
||||
* @returns A path string you can use in an SVG, the `labelX` and `labelY` position (center of path)
|
||||
* and `offsetX`, `offsetY` between source handle and label.
|
||||
* - `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.
|
||||
* @example
|
||||
* ```js
|
||||
* const source = { x: 0, y: 20 };
|
||||
const target = { x: 150, y: 100 };
|
||||
|
||||
const [path, labelX, labelY, offsetX, offsetY] = getBezierPath({
|
||||
sourceX: source.x,
|
||||
sourceY: source.y,
|
||||
sourcePosition: Position.Right,
|
||||
targetX: target.x,
|
||||
targetY: target.y,
|
||||
targetPosition: Position.Left,
|
||||
});
|
||||
* const target = { x: 150, y: 100 };
|
||||
*
|
||||
* const [path, labelX, labelY, offsetX, offsetY] = getBezierPath({
|
||||
* sourceX: source.x,
|
||||
* sourceY: source.y,
|
||||
* sourcePosition: Position.Right,
|
||||
* targetX: target.x,
|
||||
* targetY: target.y,
|
||||
* targetPosition: Position.Left,
|
||||
*});
|
||||
*```
|
||||
*
|
||||
* @remarks This function returns a tuple (aka a fixed-size array) to make it easier to
|
||||
*work with multiple edge paths at once.
|
||||
*/
|
||||
export function getBezierPath({
|
||||
sourceX,
|
||||
|
||||
@@ -90,12 +90,16 @@ const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* This util is a convenience function to add a new Edge to an array of edges
|
||||
* @remarks It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one.
|
||||
* This util is a convenience function to add a new Edge to an array of edges. It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one.
|
||||
* @public
|
||||
* @param edgeParams - Either an Edge or a Connection you want to add
|
||||
* @param edges - The array of all current edges
|
||||
* @returns A new array of edges with the new edge added
|
||||
* @param edgeParams - Either an `Edge` or a `Connection` you want to add.
|
||||
* @param edges - The array of all current edges.
|
||||
* @returns A new array of edges with the new edge added.
|
||||
*
|
||||
* @remarks If an edge with the same `target` and `source` already exists (and the same
|
||||
*`targetHandle` and `sourceHandle` if those are set), then this util won't add
|
||||
*a new edge even if the `id` property is different.
|
||||
*
|
||||
*/
|
||||
export const addEdge = <EdgeType extends EdgeBase>(
|
||||
edgeParams: EdgeType | Connection,
|
||||
@@ -133,16 +137,28 @@ export const addEdge = <EdgeType extends EdgeBase>(
|
||||
};
|
||||
|
||||
export type ReconnectEdgeOptions = {
|
||||
/**
|
||||
* Should the id of the old edge be replaced with the new connection id.
|
||||
* @default true
|
||||
*/
|
||||
shouldReplaceId?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A handy utility to reconnect an existing edge with new properties
|
||||
* @param oldEdge - The edge you want to update
|
||||
* @param newConnection - The new connection you want to update the edge with
|
||||
* @param edges - The array of all current edges
|
||||
* @param options.shouldReplaceId - should the id of the old edge be replaced with the new connection id
|
||||
* @returns the updated edges array
|
||||
* A handy utility to update an existing [`Edge`](/api-reference/types/edge) with new properties.
|
||||
*This searches your edge array for an edge with a matching `id` and updates its
|
||||
*properties with the connection you provide.
|
||||
* @public
|
||||
* @param oldEdge - The edge you want to update.
|
||||
* @param newConnection - The new connection you want to update the edge with.
|
||||
* @param edges - The array of all current edges.
|
||||
* @returns The updated edges array.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*const onReconnect = useCallback(
|
||||
* (oldEdge: Edge, newConnection: Connection) => setEdges((els) => reconnectEdge(oldEdge, newConnection, els)),[]);
|
||||
*```
|
||||
*/
|
||||
export const reconnectEdge = <EdgeType extends EdgeBase>(
|
||||
oldEdge: EdgeType,
|
||||
|
||||
@@ -2,15 +2,29 @@ import { getEdgeCenter } from './general';
|
||||
import { Position, type XYPosition } from '../../types';
|
||||
|
||||
export interface GetSmoothStepPathParams {
|
||||
/** The `x` position of the source handle. */
|
||||
sourceX: number;
|
||||
/** The `y` position of the source handle. */
|
||||
sourceY: number;
|
||||
/**
|
||||
* The position of the source handle.
|
||||
* @default Position.Bottom
|
||||
*/
|
||||
sourcePosition?: Position;
|
||||
/** The `x` position of the target handle. */
|
||||
targetX: number;
|
||||
/** The `y` position of the target handle. */
|
||||
targetY: number;
|
||||
/**
|
||||
* The position of the target handle.
|
||||
* @default Position.Top
|
||||
*/
|
||||
targetPosition?: Position;
|
||||
/** @default 5 */
|
||||
borderRadius?: number;
|
||||
centerX?: number;
|
||||
centerY?: number;
|
||||
/** @default 20 */
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
@@ -38,8 +52,10 @@ const getDirection = ({
|
||||
|
||||
const distance = (a: XYPosition, b: XYPosition) => Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
|
||||
|
||||
// ith this function we try to mimic a orthogonal edge routing behaviour
|
||||
// It's not as good as a real orthogonal edge routing but it's faster and good enough as a default for step and smooth step edges
|
||||
/*
|
||||
* With this function we try to mimic an orthogonal edge routing behaviour
|
||||
* It's not as good as a real orthogonal edge routing, but it's faster and good enough as a default for step and smooth step edges
|
||||
*/
|
||||
function getPoints({
|
||||
source,
|
||||
sourcePosition = Position.Bottom,
|
||||
@@ -83,16 +99,20 @@ function getPoints({
|
||||
if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) {
|
||||
centerX = center.x ?? defaultCenterX;
|
||||
centerY = center.y ?? defaultCenterY;
|
||||
// --->
|
||||
// |
|
||||
// >---
|
||||
/*
|
||||
* --->
|
||||
* |
|
||||
* >---
|
||||
*/
|
||||
const verticalSplit: XYPosition[] = [
|
||||
{ x: centerX, y: sourceGapped.y },
|
||||
{ x: centerX, y: targetGapped.y },
|
||||
];
|
||||
// |
|
||||
// ---
|
||||
// |
|
||||
/*
|
||||
* |
|
||||
* ---
|
||||
* |
|
||||
*/
|
||||
const horizontalSplit: XYPosition[] = [
|
||||
{ x: sourceGapped.x, y: centerY },
|
||||
{ x: targetGapped.x, y: centerY },
|
||||
@@ -191,26 +211,35 @@ function getBend(a: XYPosition, b: XYPosition, c: XYPosition, size: number): str
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a smooth step path from source to target handle
|
||||
* @param params.sourceX - The x position of the source handle
|
||||
* @param params.sourceY - The y position of the source handle
|
||||
* @param params.sourcePosition - The position of the source handle (default: Position.Bottom)
|
||||
* @param params.targetX - The x position of the target handle
|
||||
* @param params.targetY - The y position of the target handle
|
||||
* @param params.targetPosition - The position of the target handle (default: Position.Top)
|
||||
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
|
||||
* The `getSmoothStepPath` util returns everything you need to render a stepped path
|
||||
* between two nodes. The `borderRadius` property can be used to choose how rounded
|
||||
* the corners of those steps are.
|
||||
* @public
|
||||
* @returns A path string you can use in an SVG, the `labelX` and `labelY` position (center of path)
|
||||
* and `offsetX`, `offsetY` between source handle and label.
|
||||
*
|
||||
* - `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.
|
||||
* @example
|
||||
* ```js
|
||||
* const source = { x: 0, y: 20 };
|
||||
const target = { x: 150, y: 100 };
|
||||
|
||||
const [path, labelX, labelY, offsetX, offsetY] = getSmoothStepPath({
|
||||
sourceX: source.x,
|
||||
sourceY: source.y,
|
||||
sourcePosition: Position.Right,
|
||||
targetX: target.x,
|
||||
targetY: target.y,
|
||||
targetPosition: Position.Left,
|
||||
});
|
||||
* const target = { x: 150, y: 100 };
|
||||
*
|
||||
* const [path, labelX, labelY, offsetX, offsetY] = getSmoothStepPath({
|
||||
* sourceX: source.x,
|
||||
* sourceY: source.y,
|
||||
* sourcePosition: Position.Right,
|
||||
* targetX: target.x,
|
||||
* targetY: target.y,
|
||||
* targetPosition: Position.Left,
|
||||
* });
|
||||
* ```
|
||||
* @remarks This function returns a tuple (aka a fixed-size array) to make it easier to work with multiple edge paths at once.
|
||||
*/
|
||||
export function getSmoothStepPath({
|
||||
sourceX,
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
import { getEdgeCenter } from './general';
|
||||
|
||||
export type GetStraightPathParams = {
|
||||
/** The `x` position of the source handle. */
|
||||
sourceX: number;
|
||||
/** The `y` position of the source handle. */
|
||||
sourceY: number;
|
||||
/** The `x` position of the target handle. */
|
||||
targetX: number;
|
||||
/** The `y` position of the target handle. */
|
||||
targetY: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a straight path from source to target handle
|
||||
* @param params.sourceX - The x position of the source handle
|
||||
* @param params.sourceY - The y position of the source handle
|
||||
* @param params.targetX - The x position of the target handle
|
||||
* @param params.targetY - The y position of the target handle
|
||||
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
|
||||
* Calculates the straight line path between two points.
|
||||
* @public
|
||||
* @returns A path string you can use in an SVG, the `labelX` and `labelY` position (center of path)
|
||||
* and `offsetX`, `offsetY` between source handle and label.
|
||||
*
|
||||
* - `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.
|
||||
* @example
|
||||
* ```js
|
||||
* const source = { x: 0, y: 20 };
|
||||
const target = { x: 150, y: 100 };
|
||||
|
||||
const [path, labelX, labelY, offsetX, offsetY] = getStraightPath({
|
||||
sourceX: source.x,
|
||||
sourceY: source.y,
|
||||
sourcePosition: Position.Right,
|
||||
targetX: target.x,
|
||||
targetY: target.y,
|
||||
targetPosition: Position.Left,
|
||||
});
|
||||
* const target = { x: 150, y: 100 };
|
||||
*
|
||||
* const [path, labelX, labelY, offsetX, offsetY] = getStraightPath({
|
||||
* sourceX: source.x,
|
||||
* sourceY: source.y,
|
||||
* sourcePosition: Position.Right,
|
||||
* targetX: target.x,
|
||||
* targetY: target.y,
|
||||
* targetPosition: Position.Left,
|
||||
* });
|
||||
* ```
|
||||
* @remarks This function returns a tuple (aka a fixed-size array) to make it easier to work with multiple edge paths at once.
|
||||
*/
|
||||
export function getStraightPath({
|
||||
sourceX,
|
||||
|
||||
@@ -10,6 +10,8 @@ import type {
|
||||
Transform,
|
||||
InternalNodeBase,
|
||||
NodeLookup,
|
||||
Padding,
|
||||
PaddingWithUnit,
|
||||
} from '../types';
|
||||
import { type Viewport } from '../types';
|
||||
import { getNodePositionWithOrigin, isInternalNodeBase } from './graph';
|
||||
@@ -174,20 +176,119 @@ export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Tra
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a viewport that encloses the given bounds with optional padding.
|
||||
* Parses a single padding value to a number
|
||||
* @internal
|
||||
* @param padding - Padding to parse
|
||||
* @param viewport - Width or height of the viewport
|
||||
* @returns The padding in pixels
|
||||
*/
|
||||
function parsePadding(padding: PaddingWithUnit, viewport: number): number {
|
||||
if (typeof padding === 'number') {
|
||||
return Math.floor((viewport - viewport / (1 + padding)) * 0.5);
|
||||
}
|
||||
|
||||
if (typeof padding === 'string' && padding.endsWith('px')) {
|
||||
const paddingValue = parseFloat(padding);
|
||||
if (!Number.isNaN(paddingValue)) {
|
||||
return Math.floor(paddingValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof padding === 'string' && padding.endsWith('%')) {
|
||||
const paddingValue = parseFloat(padding);
|
||||
if (!Number.isNaN(paddingValue)) {
|
||||
return Math.floor(viewport * paddingValue * 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
console.error(
|
||||
`[React Flow] The padding value "${padding}" is invalid. Please provide a number or a string with a valid unit (px or %).`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the paddings to an object with top, right, bottom, left, x and y paddings
|
||||
* @internal
|
||||
* @param padding - Padding to parse
|
||||
* @param width - Width of the viewport
|
||||
* @param height - Height of the viewport
|
||||
* @returns An object with the paddings in pixels
|
||||
*/
|
||||
function parsePaddings(
|
||||
padding: Padding,
|
||||
width: number,
|
||||
height: number
|
||||
): { top: number; bottom: number; left: number; right: number; x: number; y: number } {
|
||||
if (typeof padding === 'string' || typeof padding === 'number') {
|
||||
const paddingY = parsePadding(padding, height);
|
||||
const paddingX = parsePadding(padding, width);
|
||||
return {
|
||||
top: paddingY,
|
||||
right: paddingX,
|
||||
bottom: paddingY,
|
||||
left: paddingX,
|
||||
x: paddingX * 2,
|
||||
y: paddingY * 2,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof padding === 'object') {
|
||||
const top = parsePadding(padding.top ?? padding.y ?? 0, height);
|
||||
const bottom = parsePadding(padding.bottom ?? padding.y ?? 0, height);
|
||||
const left = parsePadding(padding.left ?? padding.x ?? 0, width);
|
||||
const right = parsePadding(padding.right ?? padding.x ?? 0, width);
|
||||
return { top, right, bottom, left, x: left + right, y: top + bottom };
|
||||
}
|
||||
|
||||
return { top: 0, right: 0, bottom: 0, left: 0, x: 0, y: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the resulting paddings if the new viewport is applied
|
||||
* @internal
|
||||
* @param bounds - Bounds to fit inside viewport
|
||||
* @param x - X position of the viewport
|
||||
* @param y - Y position of the viewport
|
||||
* @param zoom - Zoom level of the viewport
|
||||
* @param width - Width of the viewport
|
||||
* @param height - Height of the viewport
|
||||
* @returns An object with the minimum padding required to fit the bounds inside the viewport
|
||||
*/
|
||||
function calculateAppliedPaddings(bounds: Rect, x: number, y: number, zoom: number, width: number, height: number) {
|
||||
const { x: left, y: top } = rendererPointToPoint(bounds, [x, y, zoom]);
|
||||
|
||||
const { x: boundRight, y: boundBottom } = rendererPointToPoint(
|
||||
{ x: bounds.x + bounds.width, y: bounds.y + bounds.height },
|
||||
[x, y, zoom]
|
||||
);
|
||||
|
||||
const right = width - boundRight;
|
||||
const bottom = height - boundBottom;
|
||||
|
||||
return {
|
||||
left: Math.floor(left),
|
||||
top: Math.floor(top),
|
||||
right: Math.floor(right),
|
||||
bottom: Math.floor(bottom),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a viewport that encloses the given bounds with padding.
|
||||
* @public
|
||||
* @remarks You can determine bounds of nodes with {@link getNodesBounds} and {@link getBoundsOfRects}
|
||||
* @param bounds - Bounds to fit inside viewport
|
||||
* @param width - Width of the viewport
|
||||
* @param height - Height of the viewport
|
||||
* @param minZoom - Minimum zoom level of the resulting viewport
|
||||
* @param maxZoom - Maximum zoom level of the resulting viewport
|
||||
* @param padding - Optional padding around the bounds
|
||||
* @returns A transforned {@link Viewport} that encloses the given bounds which you can pass to e.g. {@link setViewport}
|
||||
* @param bounds - Bounds to fit inside viewport.
|
||||
* @param width - Width of the viewport.
|
||||
* @param height - Height of the viewport.
|
||||
* @param minZoom - Minimum zoom level of the resulting viewport.
|
||||
* @param maxZoom - Maximum zoom level of the resulting viewport.
|
||||
* @param padding - Padding around the bounds.
|
||||
* @returns A transformed {@link Viewport} that encloses the given bounds which you can pass to e.g. {@link setViewport}.
|
||||
* @example
|
||||
* const { x, y, zoom } = getViewportForBounds(
|
||||
{ x: 0, y: 0, width: 100, height: 100},
|
||||
1200, 800, 0.5, 2);
|
||||
* { x: 0, y: 0, width: 100, height: 100},
|
||||
* 1200, 800, 0.5, 2);
|
||||
*/
|
||||
export const getViewportForBounds = (
|
||||
bounds: Rect,
|
||||
@@ -195,18 +296,39 @@ export const getViewportForBounds = (
|
||||
height: number,
|
||||
minZoom: number,
|
||||
maxZoom: number,
|
||||
padding: number
|
||||
padding: Padding
|
||||
): Viewport => {
|
||||
const xZoom = width / (bounds.width * (1 + padding));
|
||||
const yZoom = height / (bounds.height * (1 + padding));
|
||||
// First we resolve all the paddings to actual pixel values
|
||||
const p = parsePaddings(padding, width, height);
|
||||
|
||||
const xZoom = (width - p.x) / bounds.width;
|
||||
const yZoom = (height - p.y) / bounds.height;
|
||||
|
||||
// We calculate the new x, y, zoom for a centered view
|
||||
const zoom = Math.min(xZoom, yZoom);
|
||||
const clampedZoom = clamp(zoom, minZoom, maxZoom);
|
||||
|
||||
const boundsCenterX = bounds.x + bounds.width / 2;
|
||||
const boundsCenterY = bounds.y + bounds.height / 2;
|
||||
const x = width / 2 - boundsCenterX * clampedZoom;
|
||||
const y = height / 2 - boundsCenterY * clampedZoom;
|
||||
|
||||
return { x, y, zoom: clampedZoom };
|
||||
// Then we calculate the minimum padding, to respect asymmetric paddings
|
||||
const newPadding = calculateAppliedPaddings(bounds, x, y, clampedZoom, width, height);
|
||||
|
||||
// We only want to have an offset if the newPadding is smaller than the required padding
|
||||
const offset = {
|
||||
left: Math.min(newPadding.left - p.left, 0),
|
||||
top: Math.min(newPadding.top - p.top, 0),
|
||||
right: Math.min(newPadding.right - p.right, 0),
|
||||
bottom: Math.min(newPadding.bottom - p.bottom, 0),
|
||||
};
|
||||
|
||||
return {
|
||||
x: x - offset.left + offset.right,
|
||||
y: y - offset.top + offset.bottom,
|
||||
zoom: clampedZoom,
|
||||
};
|
||||
};
|
||||
|
||||
export const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0;
|
||||
@@ -252,19 +374,47 @@ export function evaluateAbsolutePosition(
|
||||
nodeLookup: NodeLookup,
|
||||
nodeOrigin: NodeOrigin
|
||||
): XYPosition {
|
||||
let nextParentId: string | undefined = parentId;
|
||||
const positionAbsolute = { ...position };
|
||||
|
||||
while (nextParentId) {
|
||||
const parent = nodeLookup.get(nextParentId);
|
||||
nextParentId = parent?.parentId;
|
||||
|
||||
if (parent) {
|
||||
const origin = parent.origin || nodeOrigin;
|
||||
positionAbsolute.x += parent.internals.positionAbsolute.x - (dimensions.width ?? 0) * origin[0];
|
||||
positionAbsolute.y += parent.internals.positionAbsolute.y - (dimensions.height ?? 0) * origin[1];
|
||||
}
|
||||
const parent = nodeLookup.get(parentId);
|
||||
if (parent) {
|
||||
const origin = parent.origin || nodeOrigin;
|
||||
positionAbsolute.x += parent.internals.positionAbsolute.x - (dimensions.width ?? 0) * origin[0];
|
||||
positionAbsolute.y += parent.internals.positionAbsolute.y - (dimensions.height ?? 0) * origin[1];
|
||||
}
|
||||
|
||||
return positionAbsolute;
|
||||
}
|
||||
|
||||
export function areSetsEqual(a: Set<string>, b: Set<string>) {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const item of a) {
|
||||
if (!b.has(item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyfill for Promise.withResolvers until we can use it in all browsers
|
||||
* @internal
|
||||
*/
|
||||
export function withResolvers<T>(): {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T | PromiseLike<T>) => void;
|
||||
reject: (reason?: unknown) => void;
|
||||
} {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import { errorMessages } from '../constants';
|
||||
|
||||
/**
|
||||
* Test whether an object is useable as an Edge
|
||||
* Test whether an object is usable as an Edge
|
||||
* @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
|
||||
@@ -40,7 +40,7 @@ export const isEdgeBase = <EdgeType extends EdgeBase = EdgeBase>(element: any):
|
||||
'id' in element && 'source' in element && 'target' in element;
|
||||
|
||||
/**
|
||||
* Test whether an object is useable as a Node
|
||||
* Test whether an object is usable as a Node
|
||||
* @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
|
||||
@@ -54,12 +54,27 @@ export const isInternalNodeBase = <NodeType extends InternalNodeBase = InternalN
|
||||
): element is NodeType => 'id' in element && 'internals' in element && !('source' in element) && !('target' in element);
|
||||
|
||||
/**
|
||||
* Pass in a node, and get connected nodes where edge.source === node.id
|
||||
* This util is used to tell you what nodes, if any, are connected to the given node
|
||||
* as the _target_ of an edge.
|
||||
* @public
|
||||
* @param node - The node to get the connected nodes from
|
||||
* @param nodes - The array of all nodes
|
||||
* @param edges - The array of all edges
|
||||
* @returns An array of nodes that are connected over eges where the source is the given node
|
||||
* @param node - The node to get the connected nodes from.
|
||||
* @param nodes - The array of all nodes.
|
||||
* @param edges - The array of all edges.
|
||||
* @returns An array of nodes that are connected over edges where the source is the given node.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
*import { getOutgoers } from '@xyflow/react';
|
||||
*
|
||||
*const nodes = [];
|
||||
*const edges = [];
|
||||
*
|
||||
*const outgoers = getOutgoers(
|
||||
* { id: '1', position: { x: 0, y: 0 }, data: { label: 'node' } },
|
||||
* nodes,
|
||||
* edges,
|
||||
*);
|
||||
*```
|
||||
*/
|
||||
export const getOutgoers = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
|
||||
node: NodeType | { id: string },
|
||||
@@ -81,12 +96,27 @@ export const getOutgoers = <NodeType extends NodeBase = NodeBase, EdgeType exten
|
||||
};
|
||||
|
||||
/**
|
||||
* Pass in a node, and get connected nodes where edge.target === node.id
|
||||
* This util is used to tell you what nodes, if any, are connected to the given node
|
||||
* as the _source_ of an edge.
|
||||
* @public
|
||||
* @param node - The node to get the connected nodes from
|
||||
* @param nodes - The array of all nodes
|
||||
* @param edges - The array of all edges
|
||||
* @returns An array of nodes that are connected over eges where the target is the given node
|
||||
* @param node - The node to get the connected nodes from.
|
||||
* @param nodes - The array of all nodes.
|
||||
* @param edges - The array of all edges.
|
||||
* @returns An array of nodes that are connected over edges where the target is the given node.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
*import { getIncomers } from '@xyflow/react';
|
||||
*
|
||||
*const nodes = [];
|
||||
*const edges = [];
|
||||
*
|
||||
*const incomers = getIncomers(
|
||||
* { id: '1', position: { x: 0, y: 0 }, data: { label: 'node' } },
|
||||
* nodes,
|
||||
* edges,
|
||||
*);
|
||||
*```
|
||||
*/
|
||||
export const getIncomers = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
|
||||
node: NodeType | { id: string },
|
||||
@@ -119,21 +149,52 @@ export const getNodePositionWithOrigin = (node: NodeBase, nodeOrigin: NodeOrigin
|
||||
};
|
||||
|
||||
export type GetNodesBoundsParams<NodeType extends NodeBase = NodeBase> = {
|
||||
/**
|
||||
* Origin of the nodes: `[0, 0]` for top-left, `[0.5, 0.5]` for center.
|
||||
* @default [0, 0]
|
||||
*/
|
||||
nodeOrigin?: NodeOrigin;
|
||||
nodeLookup?: NodeLookup<InternalNodeBase<NodeType>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal function for determining a bounding box that contains all given nodes in an array.
|
||||
* Returns the bounding box that contains all the given nodes in an array. This can
|
||||
* be useful when combined with [`getViewportForBounds`](/api-reference/utils/get-viewport-for-bounds)
|
||||
* to calculate the correct transform to fit the given nodes in a viewport.
|
||||
* @public
|
||||
* @remarks Useful when combined with {@link getViewportForBounds} to calculate the correct transform to fit the given nodes in a viewport.
|
||||
* @param nodes - Nodes to calculate the bounds for
|
||||
* @param params.nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center
|
||||
* @returns Bounding box enclosing all nodes
|
||||
* @param nodes - Nodes to calculate the bounds for.
|
||||
* @returns Bounding box enclosing all nodes.
|
||||
*
|
||||
* @remarks This function was previously called `getRectOfNodes`
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*import { getNodesBounds } from '@xyflow/react';
|
||||
*
|
||||
*const nodes = [
|
||||
* {
|
||||
* id: 'a',
|
||||
* position: { x: 0, y: 0 },
|
||||
* data: { label: 'a' },
|
||||
* width: 50,
|
||||
* height: 25,
|
||||
* },
|
||||
* {
|
||||
* id: 'b',
|
||||
* position: { x: 100, y: 100 },
|
||||
* data: { label: 'b' },
|
||||
* width: 50,
|
||||
* height: 25,
|
||||
* },
|
||||
*];
|
||||
*
|
||||
*const bounds = getNodesBounds(nodes);
|
||||
*```
|
||||
*/
|
||||
export const getNodesBounds = <NodeType extends NodeBase = NodeBase>(
|
||||
nodes: (NodeType | InternalNodeBase<NodeType> | string)[],
|
||||
params: GetNodesBoundsParams<NodeType> = { nodeOrigin: [0, 0], nodeLookup: undefined }
|
||||
params: GetNodesBoundsParams<NodeType> = { nodeOrigin: [0, 0] }
|
||||
): Rect => {
|
||||
if (process.env.NODE_ENV === 'development' && !params.nodeLookup) {
|
||||
console.warn(
|
||||
@@ -238,10 +299,30 @@ export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all connecting edges for a given set of nodes
|
||||
* @param nodes - Nodes you want to get the connected edges for
|
||||
* @param edges - All edges
|
||||
* @returns Array of edges that connect any of the given nodes with each other
|
||||
* This utility filters an array of edges, keeping only those where either the source or target
|
||||
* node is present in the given array of nodes.
|
||||
* @public
|
||||
* @param nodes - Nodes you want to get the connected edges for.
|
||||
* @param edges - All edges.
|
||||
* @returns Array of edges that connect any of the given nodes with each other.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
*import { getConnectedEdges } from '@xyflow/react';
|
||||
*
|
||||
*const nodes = [
|
||||
* { id: 'a', position: { x: 0, y: 0 } },
|
||||
* { id: 'b', position: { x: 100, y: 0 } },
|
||||
*];
|
||||
*
|
||||
*const edges = [
|
||||
* { id: 'a->c', source: 'a', target: 'c' },
|
||||
* { id: 'c->d', source: 'c', target: 'd' },
|
||||
*];
|
||||
*
|
||||
*const connectedEdges = getConnectedEdges(nodes, edges);
|
||||
* // => [{ id: 'a->c', source: 'a', target: 'c' }]
|
||||
*```
|
||||
*/
|
||||
export const getConnectedEdges = <NodeType extends NodeBase = NodeBase, EdgeType extends EdgeBase = EdgeBase>(
|
||||
nodes: NodeType[],
|
||||
@@ -255,10 +336,10 @@ export const getConnectedEdges = <NodeType extends NodeBase = NodeBase, EdgeType
|
||||
return edges.filter((edge) => nodeIds.has(edge.source) || nodeIds.has(edge.target));
|
||||
};
|
||||
|
||||
export function getFitViewNodes<
|
||||
function getFitViewNodes<
|
||||
Params extends NodeLookup<InternalNodeBase<NodeBase>>,
|
||||
Options extends FitViewOptionsBase<NodeBase>
|
||||
>(nodeLookup: Params, options?: Pick<Options, 'nodes' | 'includeHiddenNodes'>) {
|
||||
>(nodeLookup: Params, options?: Options) {
|
||||
const fitViewNodes: NodeLookup = new Map();
|
||||
const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null;
|
||||
|
||||
@@ -273,15 +354,20 @@ export function getFitViewNodes<
|
||||
return fitViewNodes;
|
||||
}
|
||||
|
||||
export async function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>(
|
||||
export async function fitViewport<
|
||||
Params extends FitViewParamsBase<NodeBase>,
|
||||
Options extends FitViewOptionsBase<NodeBase>
|
||||
>(
|
||||
{ nodes, width, height, panZoom, minZoom, maxZoom }: Params,
|
||||
options?: Omit<Options, 'nodes' | 'includeHiddenNodes'>
|
||||
): Promise<boolean> {
|
||||
if (nodes.size === 0) {
|
||||
return Promise.resolve(false);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
const bounds = getInternalNodesBounds(nodes);
|
||||
const nodesToFit = getFitViewNodes(nodes, options);
|
||||
|
||||
const bounds = getInternalNodesBounds(nodesToFit);
|
||||
|
||||
const viewport = getViewportForBounds(
|
||||
bounds,
|
||||
@@ -292,7 +378,11 @@ export async function fitView<Params extends FitViewParamsBase<NodeBase>, Option
|
||||
options?.padding ?? 0.1
|
||||
);
|
||||
|
||||
await panZoom.setViewport(viewport, { duration: options?.duration });
|
||||
await panZoom.setViewport(viewport, {
|
||||
duration: options?.duration,
|
||||
ease: options?.ease,
|
||||
interpolate: options?.interpolate,
|
||||
});
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
@@ -350,10 +440,14 @@ export function calculateNodePosition<NodeType extends NodeBase>({
|
||||
? clampPosition(nextPosition, extent, node.measured)
|
||||
: nextPosition;
|
||||
|
||||
if (node.measured.width === undefined || node.measured.height === undefined) {
|
||||
onError?.('015', errorMessages['error015']());
|
||||
}
|
||||
|
||||
return {
|
||||
position: {
|
||||
x: positionAbsolute.x - parentX + node.measured.width! * origin[0],
|
||||
y: positionAbsolute.y - parentY + node.measured.height! * origin[1],
|
||||
x: positionAbsolute.x - parentX + (node.measured.width ?? 0) * origin[0],
|
||||
y: positionAbsolute.y - parentY + (node.measured.height ?? 0) * origin[1],
|
||||
},
|
||||
positionAbsolute,
|
||||
};
|
||||
|
||||
@@ -15,8 +15,10 @@ export function getNodeToolbarTransform(
|
||||
alignmentOffset = 1;
|
||||
}
|
||||
|
||||
// position === Position.Top
|
||||
// we set the x any y position of the toolbar based on the nodes position
|
||||
/*
|
||||
* position === Position.Top
|
||||
* we set the x any y position of the toolbar based on the nodes position
|
||||
*/
|
||||
let pos = [
|
||||
(nodeRect.x + nodeRect.width * alignmentOffset) * viewport.zoom + viewport.x,
|
||||
nodeRect.y * viewport.zoom + viewport.y - offset,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { infiniteExtent } from '..';
|
||||
import { HandleConnection, infiniteExtent } from '..';
|
||||
import {
|
||||
NodeBase,
|
||||
CoordinateExtent,
|
||||
@@ -42,7 +42,7 @@ const adoptUserNodesDefaultOptions = {
|
||||
checkEquality: true,
|
||||
};
|
||||
|
||||
function mergeObjects<T extends Record<string, any>>(base: T, incoming?: Partial<T>): T {
|
||||
function mergeObjects<T extends Record<string, unknown>>(base: T, incoming?: Partial<T>): T {
|
||||
const result = { ...base };
|
||||
for (const key in incoming) {
|
||||
if (incoming[key] !== undefined) {
|
||||
@@ -61,11 +61,14 @@ export function updateAbsolutePositions<NodeType extends NodeBase>(
|
||||
) {
|
||||
const _options = mergeObjects(defaultOptions, options);
|
||||
for (const node of nodeLookup.values()) {
|
||||
if (!node.parentId) {
|
||||
continue;
|
||||
if (node.parentId) {
|
||||
updateChildNode(node, nodeLookup, parentLookup, _options);
|
||||
} else {
|
||||
const positionWithOrigin = getNodePositionWithOrigin(node, _options.nodeOrigin);
|
||||
const extent = isCoordinateExtent(node.extent) ? node.extent : _options.nodeExtent;
|
||||
const clampedPosition = clampPosition(positionWithOrigin, extent, getNodeDimensions(node));
|
||||
node.internals.positionAbsolute = clampedPosition;
|
||||
}
|
||||
|
||||
updateChildNode(node, nodeLookup, parentLookup, _options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +85,10 @@ export function adoptUserNodes<NodeType extends NodeBase>(
|
||||
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
|
||||
parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
|
||||
options?: UpdateNodesOptions<NodeType>
|
||||
) {
|
||||
): boolean {
|
||||
const _options = mergeObjects(adoptUserNodesDefaultOptions, options);
|
||||
|
||||
let nodesInitialized = nodes.length > 0;
|
||||
const tmpLookup = new Map(nodeLookup);
|
||||
const selectedNodeZ: number = _options?.elevateNodesOnSelect ? 1000 : 0;
|
||||
|
||||
@@ -119,10 +124,21 @@ export function adoptUserNodes<NodeType extends NodeBase>(
|
||||
nodeLookup.set(userNode.id, internalNode);
|
||||
}
|
||||
|
||||
if (
|
||||
(internalNode.measured === undefined ||
|
||||
internalNode.measured.width === undefined ||
|
||||
internalNode.measured.height === undefined) &&
|
||||
!internalNode.hidden
|
||||
) {
|
||||
nodesInitialized = false;
|
||||
}
|
||||
|
||||
if (userNode.parentId) {
|
||||
updateChildNode(internalNode, nodeLookup, parentLookup, options);
|
||||
}
|
||||
}
|
||||
|
||||
return nodesInitialized;
|
||||
}
|
||||
|
||||
function updateParentLookup<NodeType extends NodeBase>(
|
||||
@@ -170,11 +186,15 @@ function updateChildNode<NodeType extends NodeBase>(
|
||||
const positionChanged = x !== positionAbsolute.x || y !== positionAbsolute.y;
|
||||
|
||||
if (positionChanged || z !== node.internals.z) {
|
||||
node.internals = {
|
||||
...node.internals,
|
||||
positionAbsolute: positionChanged ? { x, y } : positionAbsolute,
|
||||
z,
|
||||
};
|
||||
// we create a new object to mark the node as updated
|
||||
nodeLookup.set(node.id, {
|
||||
...node,
|
||||
internals: {
|
||||
...node.internals,
|
||||
positionAbsolute: positionChanged ? { x, y } : positionAbsolute,
|
||||
z,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,8 +288,10 @@ export function handleExpandParent(
|
||||
},
|
||||
});
|
||||
|
||||
// We move all child nodes in the oppsite direction
|
||||
// so the x,y changes of the parent do not move the children
|
||||
/*
|
||||
* We move all child nodes in the oppsite direction
|
||||
* so the x,y changes of the parent do not move the children
|
||||
*/
|
||||
parentLookup.get(parentId)?.forEach((childNode) => {
|
||||
if (!children.some((child) => child.id === childNode.id)) {
|
||||
changes.push({
|
||||
@@ -330,60 +352,70 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
|
||||
}
|
||||
|
||||
if (node.hidden) {
|
||||
node.internals = {
|
||||
...node.internals,
|
||||
handleBounds: undefined,
|
||||
};
|
||||
nodeLookup.set(node.id, {
|
||||
...node,
|
||||
internals: {
|
||||
...node.internals,
|
||||
handleBounds: undefined,
|
||||
},
|
||||
});
|
||||
updatedInternals = true;
|
||||
} else {
|
||||
const dimensions = getDimensions(update.nodeElement);
|
||||
const dimensionChanged = node.measured.width !== dimensions.width || node.measured.height !== dimensions.height;
|
||||
const doUpdate = !!(
|
||||
dimensions.width &&
|
||||
dimensions.height &&
|
||||
(dimensionChanged || !node.internals.handleBounds || update.force)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (doUpdate) {
|
||||
const nodeBounds = update.nodeElement.getBoundingClientRect();
|
||||
const extent = isCoordinateExtent(node.extent) ? node.extent : nodeExtent;
|
||||
let { positionAbsolute } = node.internals;
|
||||
const dimensions = getDimensions(update.nodeElement);
|
||||
const dimensionChanged = node.measured.width !== dimensions.width || node.measured.height !== dimensions.height;
|
||||
const doUpdate = !!(
|
||||
dimensions.width &&
|
||||
dimensions.height &&
|
||||
(dimensionChanged || !node.internals.handleBounds || update.force)
|
||||
);
|
||||
|
||||
if (node.parentId && node.extent === 'parent') {
|
||||
positionAbsolute = clampPositionToParent(positionAbsolute, dimensions, nodeLookup.get(node.parentId)!);
|
||||
} else if (extent) {
|
||||
positionAbsolute = clampPosition(positionAbsolute, extent, dimensions);
|
||||
}
|
||||
if (doUpdate) {
|
||||
const nodeBounds = update.nodeElement.getBoundingClientRect();
|
||||
const extent = isCoordinateExtent(node.extent) ? node.extent : nodeExtent;
|
||||
let { positionAbsolute } = node.internals;
|
||||
|
||||
node.measured = dimensions;
|
||||
node.internals = {
|
||||
if (node.parentId && node.extent === 'parent') {
|
||||
positionAbsolute = clampPositionToParent(positionAbsolute, dimensions, nodeLookup.get(node.parentId)!);
|
||||
} else if (extent) {
|
||||
positionAbsolute = clampPosition(positionAbsolute, extent, dimensions);
|
||||
}
|
||||
|
||||
const newNode = {
|
||||
...node,
|
||||
measured: dimensions,
|
||||
internals: {
|
||||
...node.internals,
|
||||
positionAbsolute,
|
||||
handleBounds: {
|
||||
source: getHandleBounds('source', update.nodeElement, nodeBounds, zoom, node.id),
|
||||
target: getHandleBounds('target', update.nodeElement, nodeBounds, zoom, node.id),
|
||||
},
|
||||
};
|
||||
if (node.parentId) {
|
||||
updateChildNode(node, nodeLookup, parentLookup, { nodeOrigin });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
updatedInternals = true;
|
||||
nodeLookup.set(node.id, newNode);
|
||||
|
||||
if (dimensionChanged) {
|
||||
changes.push({
|
||||
if (node.parentId) {
|
||||
updateChildNode(newNode, nodeLookup, parentLookup, { nodeOrigin });
|
||||
}
|
||||
|
||||
updatedInternals = true;
|
||||
|
||||
if (dimensionChanged) {
|
||||
changes.push({
|
||||
id: node.id,
|
||||
type: 'dimensions',
|
||||
dimensions,
|
||||
});
|
||||
|
||||
if (node.expandParent && node.parentId) {
|
||||
parentExpandChildren.push({
|
||||
id: node.id,
|
||||
type: 'dimensions',
|
||||
dimensions,
|
||||
parentId: node.parentId,
|
||||
rect: nodeToRect(newNode, nodeOrigin),
|
||||
});
|
||||
|
||||
if (node.expandParent && node.parentId) {
|
||||
parentExpandChildren.push({
|
||||
id: node.id,
|
||||
parentId: node.parentId,
|
||||
rect: nodeToRect(node, nodeOrigin),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -436,22 +468,58 @@ export async function panBy({
|
||||
return Promise.resolve(transformChanged);
|
||||
}
|
||||
|
||||
/**
|
||||
* this function adds the connection to the connectionLookup
|
||||
* at the following keys: nodeId-type-handleId, nodeId-type and nodeId
|
||||
* @param type type of the connection
|
||||
* @param connection connection that should be added to the lookup
|
||||
* @param connectionKey at which key the connection should be added
|
||||
* @param connectionLookup reference to the connection lookup
|
||||
* @param nodeId nodeId of the connection
|
||||
* @param handleId handleId of the conneciton
|
||||
*/
|
||||
function addConnectionToLookup(
|
||||
type: 'source' | 'target',
|
||||
connection: HandleConnection,
|
||||
connectionKey: string,
|
||||
connectionLookup: ConnectionLookup,
|
||||
nodeId: string,
|
||||
handleId: string | null
|
||||
) {
|
||||
/*
|
||||
* We add the connection to the connectionLookup at the following keys
|
||||
* 1. nodeId, 2. nodeId-type, 3. nodeId-type-handleId
|
||||
* If the key already exists, we add the connection to the existing map
|
||||
*/
|
||||
let key = nodeId;
|
||||
const nodeMap = connectionLookup.get(key) || new Map();
|
||||
connectionLookup.set(key, nodeMap.set(connectionKey, connection));
|
||||
|
||||
key = `${nodeId}-${type}`;
|
||||
const typeMap = connectionLookup.get(key) || new Map();
|
||||
connectionLookup.set(key, typeMap.set(connectionKey, connection));
|
||||
|
||||
if (handleId) {
|
||||
key = `${nodeId}-${type}-${handleId}`;
|
||||
const handleMap = connectionLookup.get(key) || new Map();
|
||||
connectionLookup.set(key, handleMap.set(connectionKey, connection));
|
||||
}
|
||||
}
|
||||
|
||||
export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) {
|
||||
connectionLookup.clear();
|
||||
edgeLookup.clear();
|
||||
|
||||
for (const edge of edges) {
|
||||
const { source, target, sourceHandle = null, targetHandle = null } = edge;
|
||||
const { source: sourceNode, target: targetNode, sourceHandle = null, targetHandle = null } = edge;
|
||||
|
||||
const sourceKey = `${source}-source-${sourceHandle}`;
|
||||
const targetKey = `${target}-target-${targetHandle}`;
|
||||
const connection = { edgeId: edge.id, source: sourceNode, target: targetNode, sourceHandle, targetHandle };
|
||||
const sourceKey = `${sourceNode}-${sourceHandle}--${targetNode}-${targetHandle}`;
|
||||
const targetKey = `${targetNode}-${targetHandle}--${sourceNode}-${sourceHandle}`;
|
||||
|
||||
const prevSource = connectionLookup.get(sourceKey) || new Map();
|
||||
const prevTarget = connectionLookup.get(targetKey) || new Map();
|
||||
const connection = { edgeId: edge.id, source, target, sourceHandle, targetHandle };
|
||||
addConnectionToLookup('source', connection, targetKey, connectionLookup, sourceNode, sourceHandle);
|
||||
addConnectionToLookup('target', connection, sourceKey, connectionLookup, targetNode, targetHandle);
|
||||
|
||||
edgeLookup.set(edge.id, edge);
|
||||
connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection));
|
||||
connectionLookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,13 +139,23 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
}
|
||||
|
||||
for (const [id, dragItem] of dragItems) {
|
||||
if (!nodeLookup.has(id)) {
|
||||
/*
|
||||
* if the node is not in the nodeLookup anymore, it was probably deleted while dragging
|
||||
* and we don't need to update it anymore
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
|
||||
let nextPosition = { x: x - dragItem.distance.x, y: y - dragItem.distance.y };
|
||||
if (snapToGrid) {
|
||||
nextPosition = snapPosition(nextPosition, snapGrid);
|
||||
}
|
||||
|
||||
// if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node
|
||||
// based on its position so that the node stays at it's position relative to the selection.
|
||||
/*
|
||||
* if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node
|
||||
* based on its position so that the node stays at it's position relative to the selection.
|
||||
*/
|
||||
let adjustedNodeExtent: CoordinateExtent = [
|
||||
[nodeExtent[0][0], nodeExtent[0][1]],
|
||||
[nodeExtent[1][0], nodeExtent[1][1]],
|
||||
@@ -208,7 +218,13 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
return;
|
||||
}
|
||||
|
||||
const { transform, panBy, autoPanSpeed } = getStoreItems();
|
||||
const { transform, panBy, autoPanSpeed, autoPanOnNodeDrag } = getStoreItems();
|
||||
|
||||
if (!autoPanOnNodeDrag) {
|
||||
autoPanStarted = false;
|
||||
cancelAnimationFrame(autoPanId);
|
||||
return;
|
||||
}
|
||||
|
||||
const [xMovement, yMovement] = calcAutoPan(mousePosition, containerBounds, autoPanSpeed);
|
||||
|
||||
@@ -251,7 +267,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
onNodeMouseDown?.(nodeId);
|
||||
}
|
||||
|
||||
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid, containerBounds });
|
||||
lastPos = pointerPos;
|
||||
dragItems = getDragItems(nodeLookup, nodesDraggable, pointerPos, nodeId);
|
||||
|
||||
@@ -275,6 +291,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
.clickDistance(nodeClickDistance)
|
||||
.on('start', (event: UseDragEvent) => {
|
||||
const { domNode, nodeDragThreshold, transform, snapGrid, snapToGrid } = getStoreItems();
|
||||
containerBounds = domNode?.getBoundingClientRect() || null;
|
||||
|
||||
abortDrag = false;
|
||||
|
||||
@@ -282,16 +299,19 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
startDrag(event);
|
||||
}
|
||||
|
||||
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid, containerBounds });
|
||||
lastPos = pointerPos;
|
||||
containerBounds = domNode?.getBoundingClientRect() || null;
|
||||
mousePosition = getEventPosition(event.sourceEvent, containerBounds!);
|
||||
})
|
||||
.on('drag', (event: UseDragEvent) => {
|
||||
const { autoPanOnNodeDrag, transform, snapGrid, snapToGrid, nodeDragThreshold } = getStoreItems();
|
||||
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
const { autoPanOnNodeDrag, transform, snapGrid, snapToGrid, nodeDragThreshold, nodeLookup } = getStoreItems();
|
||||
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid, containerBounds });
|
||||
|
||||
if (event.sourceEvent.type === 'touchmove' && event.sourceEvent.touches.length > 1) {
|
||||
if (
|
||||
(event.sourceEvent.type === 'touchmove' && event.sourceEvent.touches.length > 1) ||
|
||||
// if user deletes a node while dragging, we need to abort the drag to prevent errors
|
||||
(nodeId && !nodeLookup.has(nodeId))
|
||||
) {
|
||||
abortDrag = true;
|
||||
}
|
||||
|
||||
@@ -354,7 +374,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
}
|
||||
})
|
||||
.filter((event: MouseEvent) => {
|
||||
const target = event.target as HTMLDivElement;
|
||||
const target = event.target;
|
||||
const isDraggable =
|
||||
!event.button &&
|
||||
(!noDragClassName || !hasSelector(target, `.${noDragClassName}`, domNode)) &&
|
||||
|
||||
@@ -18,13 +18,13 @@ export function isParentSelected<NodeType extends NodeBase>(node: NodeType, node
|
||||
return isParentSelected(parentNode, nodeLookup);
|
||||
}
|
||||
|
||||
export function hasSelector(target: Element, selector: string, domNode: Element): boolean {
|
||||
let current = target;
|
||||
export function hasSelector(target: Element | EventTarget | null, selector: string, domNode: Element): boolean {
|
||||
let current = target as Partial<Element> | null | undefined;
|
||||
|
||||
do {
|
||||
if (current?.matches(selector)) return true;
|
||||
if (current?.matches?.(selector)) return true;
|
||||
if (current === domNode) return false;
|
||||
current = current.parentElement as Element;
|
||||
current = current?.parentElement;
|
||||
} while (current);
|
||||
|
||||
return false;
|
||||
@@ -74,9 +74,11 @@ export function getDragItems<NodeType extends NodeBase>(
|
||||
return dragItems;
|
||||
}
|
||||
|
||||
// returns two params:
|
||||
// 1. the dragged node (or the first of the list, if we are dragging a node selection)
|
||||
// 2. array of selected nodes (for multi selections)
|
||||
/*
|
||||
* returns two params:
|
||||
* 1. the dragged node (or the first of the list, if we are dragging a node selection)
|
||||
* 2. array of selected nodes (for multi selections)
|
||||
*/
|
||||
export function getEventHandlerParams<NodeType extends InternalNodeBase>({
|
||||
nodeId,
|
||||
dragItems,
|
||||
@@ -106,14 +108,16 @@ export function getEventHandlerParams<NodeType extends InternalNodeBase>({
|
||||
return [nodesFromDragItems[0], nodesFromDragItems];
|
||||
}
|
||||
|
||||
const node = nodeLookup.get(nodeId)!.internals.userNode;
|
||||
const node = nodeLookup.get(nodeId)?.internals.userNode;
|
||||
|
||||
return [
|
||||
{
|
||||
...node,
|
||||
position: dragItems.get(nodeId)?.position || node.position,
|
||||
dragging,
|
||||
},
|
||||
!node
|
||||
? nodesFromDragItems[0]
|
||||
: {
|
||||
...node,
|
||||
position: dragItems.get(nodeId)?.position || node.position,
|
||||
dragging,
|
||||
},
|
||||
nodesFromDragItems,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ function onPointerDown(
|
||||
}: OnPointerDownParams
|
||||
) {
|
||||
// when xyflow is used inside a shadow root we can't use document
|
||||
const doc = getHostForElement(event.target as HTMLElement);
|
||||
const doc = getHostForElement(event.target);
|
||||
let autoPanId = 0;
|
||||
let closestHandle: Handle | null;
|
||||
|
||||
@@ -157,16 +157,18 @@ function onPointerDown(
|
||||
...previousConnection,
|
||||
isValid,
|
||||
to:
|
||||
closestHandle && isValid
|
||||
? rendererPointToPoint({ x: closestHandle.x, y: closestHandle.y }, transform)
|
||||
result.toHandle && isValid
|
||||
? rendererPointToPoint({ x: result.toHandle.x, y: result.toHandle.y }, transform)
|
||||
: position,
|
||||
toHandle: result.toHandle,
|
||||
toPosition: isValid && result.toHandle ? result.toHandle.position : oppositePosition[fromHandle.position],
|
||||
toNode: result.toHandle ? nodeLookup.get(result.toHandle.nodeId)! : null,
|
||||
};
|
||||
|
||||
// we don't want to trigger an update when the connection
|
||||
// is snapped to the same handle as before
|
||||
/*
|
||||
* we don't want to trigger an update when the connection
|
||||
* is snapped to the same handle as before
|
||||
*/
|
||||
if (
|
||||
isValid &&
|
||||
closestHandle &&
|
||||
@@ -190,8 +192,10 @@ function onPointerDown(
|
||||
onConnect?.(connection);
|
||||
}
|
||||
|
||||
// it's important to get a fresh reference from the store here
|
||||
// in order to get the latest state of onConnectEnd
|
||||
/*
|
||||
* it's important to get a fresh reference from the store here
|
||||
* in order to get the latest state of onConnectEnd
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { inProgress, ...connectionState } = previousConnection;
|
||||
const finalConnectionState = {
|
||||
@@ -248,8 +252,10 @@ function isValidHandle(
|
||||
|
||||
const { x, y } = getEventPosition(event);
|
||||
const handleBelow = doc.elementFromPoint(x, y);
|
||||
// we always want to prioritize the handle below the mouse cursor over the closest distance handle,
|
||||
// because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
|
||||
/*
|
||||
* we always want to prioritize the handle below the mouse cursor over the closest distance handle,
|
||||
* because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
|
||||
*/
|
||||
const handleToCheck = handleBelow?.classList.contains(`${lib}-flow__handle`) ? handleBelow : handleDomNode;
|
||||
|
||||
const result: Result = {
|
||||
@@ -289,7 +295,7 @@ function isValidHandle(
|
||||
|
||||
result.isValid = isValid && isValidConnection(connection);
|
||||
|
||||
result.toHandle = getHandle(handleNodeId, handleType, handleId, nodeLookup, connectionMode, false);
|
||||
result.toHandle = getHandle(handleNodeId, handleType, handleId, nodeLookup, connectionMode, true);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
type UpdateConnection,
|
||||
type IsValidConnection,
|
||||
NodeLookup,
|
||||
ConnectionState,
|
||||
FinalConnectionState,
|
||||
} from '../types';
|
||||
|
||||
|
||||
@@ -19,8 +19,10 @@ function getNodesWithinDistance(position: XYPosition, nodeLookup: NodeLookup, di
|
||||
return nodes;
|
||||
}
|
||||
|
||||
// this distance is used for the area around the user pointer
|
||||
// while doing a connection for finding the closest nodes
|
||||
/*
|
||||
* this distance is used for the area around the user pointer
|
||||
* while doing a connection for finding the closest nodes
|
||||
*/
|
||||
const ADDITIONAL_DISTANCE = 250;
|
||||
|
||||
export function getClosestHandle(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type ZoomTransform, zoom, zoomTransform } from 'd3-zoom';
|
||||
import { select } from 'd3-selection';
|
||||
import { interpolateZoom, interpolate } from 'd3-interpolate';
|
||||
|
||||
import {
|
||||
type CoordinateExtent,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
createZoomOnScrollHandler,
|
||||
} from './eventhandler';
|
||||
import { createFilter } from './filter';
|
||||
import { transition } from 'd3-transition';
|
||||
|
||||
export type ZoomPanValues = {
|
||||
isZoomingOrPanning: boolean;
|
||||
@@ -78,8 +80,8 @@ export function XYPanZoom({
|
||||
function setTransform(transform: ZoomTransform, options?: PanZoomTransformOptions) {
|
||||
if (d3Selection) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
d3ZoomInstance?.transform(
|
||||
getD3Transition(d3Selection, options?.duration, () => resolve(true)),
|
||||
d3ZoomInstance?.interpolate(options?.interpolate === 'linear' ? interpolate : interpolateZoom).transform(
|
||||
getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)),
|
||||
transform
|
||||
);
|
||||
});
|
||||
@@ -178,9 +180,11 @@ export function XYPanZoom({
|
||||
});
|
||||
d3ZoomInstance.filter(filter);
|
||||
|
||||
// We cannot add zoomOnDoubleClick to the filter above because
|
||||
// double tapping on touch screens circumvents the filter and
|
||||
// dblclick.zoom is fired on the selection directly
|
||||
/*
|
||||
* We cannot add zoomOnDoubleClick to the filter above because
|
||||
* double tapping on touch screens circumvents the filter and
|
||||
* dblclick.zoom is fired on the selection directly
|
||||
*/
|
||||
if (zoomOnDoubleClick) {
|
||||
d3Selection.on('dblclick.zoom', d3DblClickZoomHandler);
|
||||
} else {
|
||||
@@ -240,8 +244,8 @@ export function XYPanZoom({
|
||||
function scaleTo(zoom: number, options?: PanZoomTransformOptions) {
|
||||
if (d3Selection) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
d3ZoomInstance?.scaleTo(
|
||||
getD3Transition(d3Selection, options?.duration, () => resolve(true)),
|
||||
d3ZoomInstance?.interpolate(options?.interpolate === 'linear' ? interpolate : interpolateZoom).scaleTo(
|
||||
getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)),
|
||||
zoom
|
||||
);
|
||||
});
|
||||
@@ -253,8 +257,8 @@ export function XYPanZoom({
|
||||
function scaleBy(factor: number, options?: PanZoomTransformOptions) {
|
||||
if (d3Selection) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
d3ZoomInstance?.scaleBy(
|
||||
getD3Transition(d3Selection, options?.duration, () => resolve(true)),
|
||||
d3ZoomInstance?.interpolate(options?.interpolate === 'linear' ? interpolate : interpolateZoom).scaleBy(
|
||||
getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)),
|
||||
factor
|
||||
);
|
||||
});
|
||||
|
||||
@@ -90,8 +90,10 @@ export function createPanOnScrollHandler({
|
||||
return;
|
||||
}
|
||||
|
||||
// increase scroll speed in firefox
|
||||
// firefox: deltaMode === 1; chrome: deltaMode === 0
|
||||
/*
|
||||
* increase scroll speed in firefox
|
||||
* firefox: deltaMode === 1; chrome: deltaMode === 0
|
||||
*/
|
||||
const deltaNormalize = event.deltaMode === 1 ? 20 : 1;
|
||||
let deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize;
|
||||
let deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;
|
||||
@@ -114,9 +116,11 @@ export function createPanOnScrollHandler({
|
||||
|
||||
clearTimeout(zoomPanValues.panScrollTimeout);
|
||||
|
||||
// for pan on scroll we need to handle the event calls on our own
|
||||
// we can't use the start, zoom and end events from d3-zoom
|
||||
// because start and move gets called on every scroll event and not once at the beginning
|
||||
/*
|
||||
* for pan on scroll we need to handle the event calls on our own
|
||||
* we can't use the start, zoom and end events from d3-zoom
|
||||
* because start and move gets called on every scroll event and not once at the beginning
|
||||
*/
|
||||
if (!zoomPanValues.isPanScrolling) {
|
||||
zoomPanValues.isPanScrolling = true;
|
||||
|
||||
@@ -137,10 +141,17 @@ export function createPanOnScrollHandler({
|
||||
|
||||
export function createZoomOnScrollHandler({ noWheelClassName, preventScrolling, d3ZoomHandler }: ZoomOnScrollParams) {
|
||||
return function (this: Element, event: any, d: unknown) {
|
||||
const isWheel = event.type === 'wheel';
|
||||
// we still want to enable pinch zooming even if preventScrolling is set to false
|
||||
const preventZoom = !preventScrolling && event.type === 'wheel' && !event.ctrlKey;
|
||||
const preventZoom = !preventScrolling && isWheel && !event.ctrlKey;
|
||||
const hasNoWheelClass = isWrappedWithClass(event, noWheelClassName);
|
||||
|
||||
if (preventZoom || isWrappedWithClass(event, noWheelClassName)) {
|
||||
// if user is pinch zooming above a nowheel element, we don't want the browser to zoom
|
||||
if (event.ctrlKey && isWheel && hasNoWheelClass) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (preventZoom || hasNoWheelClass) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
|
||||
import { transition } from 'd3-transition';
|
||||
|
||||
import { type D3SelectionInstance, type Viewport } from '../types';
|
||||
import { isMacOs } from '../utils';
|
||||
@@ -21,14 +22,17 @@ export const isWrappedWithClass = (event: any, className: string | undefined) =>
|
||||
export const isRightClickPan = (panOnDrag: boolean | number[], usedButton: number) =>
|
||||
usedButton === 2 && Array.isArray(panOnDrag) && panOnDrag.includes(2);
|
||||
|
||||
export const getD3Transition = (selection: D3SelectionInstance, duration = 0, onEnd = () => {}) => {
|
||||
// taken from d3-ease: https://github.com/d3/d3-ease/blob/main/src/cubic.js
|
||||
const defaultEase = (t: number) => ((t *= 2) <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2;
|
||||
|
||||
export const getD3Transition = (selection: D3SelectionInstance, duration = 0, ease = defaultEase, onEnd = () => {}) => {
|
||||
const hasDuration = typeof duration === 'number' && duration > 0;
|
||||
|
||||
if (!hasDuration) {
|
||||
onEnd();
|
||||
}
|
||||
|
||||
return hasDuration ? selection.transition().duration(duration).on('end', onEnd) : selection;
|
||||
return hasDuration ? selection.transition().duration(duration).ease(ease).on('end', onEnd) : selection;
|
||||
};
|
||||
|
||||
export const wheelDelta = (event: any) => {
|
||||
|
||||
@@ -12,7 +12,15 @@ import type {
|
||||
Transform,
|
||||
XYPosition,
|
||||
} from '../types';
|
||||
import type { OnResize, OnResizeEnd, OnResizeStart, ResizeDragEvent, ShouldResize, ControlPosition } from './types';
|
||||
import type {
|
||||
OnResize,
|
||||
OnResizeEnd,
|
||||
OnResizeStart,
|
||||
ResizeDragEvent,
|
||||
ShouldResize,
|
||||
ControlPosition,
|
||||
ResizeControlDirection,
|
||||
} from './types';
|
||||
|
||||
const initPrevValues = { width: 0, height: 0, x: 0, y: 0 };
|
||||
|
||||
@@ -45,9 +53,10 @@ type XYResizerParams = {
|
||||
snapGrid?: [number, number];
|
||||
snapToGrid: boolean;
|
||||
nodeOrigin: NodeOrigin;
|
||||
paneDomNode: HTMLDivElement | null;
|
||||
};
|
||||
onChange: (changes: XYResizerChange, childChanges: XYResizerChildChange[]) => void;
|
||||
onEnd?: () => void;
|
||||
onEnd?: (change: Required<XYResizerChange>) => void;
|
||||
};
|
||||
|
||||
type XYResizerUpdateParams = {
|
||||
@@ -59,6 +68,7 @@ type XYResizerUpdateParams = {
|
||||
maxHeight: number;
|
||||
};
|
||||
keepAspectRatio: boolean;
|
||||
resizeDirection?: ResizeControlDirection;
|
||||
onResizeStart: OnResizeStart | undefined;
|
||||
onResize: OnResize | undefined;
|
||||
onResizeEnd: OnResizeEnd | undefined;
|
||||
@@ -98,6 +108,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
|
||||
controlPosition,
|
||||
boundaries,
|
||||
keepAspectRatio,
|
||||
resizeDirection,
|
||||
onResizeStart,
|
||||
onResize,
|
||||
onResizeEnd,
|
||||
@@ -109,6 +120,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
|
||||
const controlDirection = getControlDirection(controlPosition);
|
||||
|
||||
let node: InternalNodeBase | undefined = undefined;
|
||||
let containerBounds: DOMRect | null = null;
|
||||
let childNodes: XYResizerChildChange[] = [];
|
||||
let parentNode: InternalNodeBase | undefined = undefined; // Needed to fix expandParent
|
||||
let parentExtent: CoordinateExtent | undefined = undefined;
|
||||
@@ -116,14 +128,20 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
|
||||
|
||||
const dragHandler = drag<HTMLDivElement, unknown>()
|
||||
.on('start', (event: ResizeDragEvent) => {
|
||||
const { nodeLookup, transform, snapGrid, snapToGrid, nodeOrigin } = getStoreItems();
|
||||
const { nodeLookup, transform, snapGrid, snapToGrid, nodeOrigin, paneDomNode } = getStoreItems();
|
||||
node = nodeLookup.get(nodeId);
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
containerBounds = paneDomNode?.getBoundingClientRect() ?? null;
|
||||
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, {
|
||||
transform,
|
||||
snapGrid,
|
||||
snapToGrid,
|
||||
containerBounds,
|
||||
});
|
||||
|
||||
prevValues = {
|
||||
width: node.measured.width ?? 0,
|
||||
@@ -146,8 +164,10 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
|
||||
parentExtent = parentNode && node.extent === 'parent' ? nodeToParentExtent(parentNode) : undefined;
|
||||
}
|
||||
|
||||
// Collect all child nodes to correct their relative positions when top/left changes
|
||||
// Determine largest minimal extent the parent node is allowed to resize to
|
||||
/*
|
||||
* Collect all child nodes to correct their relative positions when top/left changes
|
||||
* Determine largest minimal extent the parent node is allowed to resize to
|
||||
*/
|
||||
childNodes = [];
|
||||
childExtent = undefined;
|
||||
|
||||
@@ -178,7 +198,13 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
|
||||
})
|
||||
.on('drag', (event: ResizeDragEvent) => {
|
||||
const { transform, snapGrid, snapToGrid, nodeOrigin: storeNodeOrigin } = getStoreItems();
|
||||
const pointerPosition = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
const pointerPosition = getPointerPosition(event.sourceEvent, {
|
||||
transform,
|
||||
snapGrid,
|
||||
snapToGrid,
|
||||
containerBounds,
|
||||
});
|
||||
|
||||
const childChanges: XYResizerChildChange[] = [];
|
||||
|
||||
if (!node) {
|
||||
@@ -216,8 +242,10 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
|
||||
prevValues.x = change.x;
|
||||
prevValues.y = change.y;
|
||||
|
||||
// when top/left changes, correct the relative positions of child nodes
|
||||
// so that they stay in the same position
|
||||
/*
|
||||
* when top/left changes, correct the relative positions of child nodes
|
||||
* so that they stay in the same position
|
||||
*/
|
||||
if (childNodes.length > 0) {
|
||||
const xChange = x - prevX;
|
||||
const yChange = y - prevY;
|
||||
@@ -233,8 +261,10 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
|
||||
}
|
||||
|
||||
if (isWidthChange || isHeightChange) {
|
||||
change.width = isWidthChange ? width : prevValues.width;
|
||||
change.height = isHeightChange ? height : prevValues.height;
|
||||
change.width =
|
||||
isWidthChange && (!resizeDirection || resizeDirection === 'horizontal') ? width : prevValues.width;
|
||||
change.height =
|
||||
isHeightChange && (!resizeDirection || resizeDirection === 'vertical') ? height : prevValues.height;
|
||||
prevValues.width = change.width;
|
||||
prevValues.height = change.height;
|
||||
}
|
||||
@@ -276,7 +306,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
|
||||
})
|
||||
.on('end', (event: ResizeDragEvent) => {
|
||||
onResizeEnd?.(event, { ...prevValues });
|
||||
onEnd?.();
|
||||
onEnd?.({ ...prevValues });
|
||||
});
|
||||
selection.call(dragHandler);
|
||||
}
|
||||
|
||||
@@ -11,15 +11,36 @@ export type ResizeParamsWithDirection = ResizeParams & {
|
||||
direction: number[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to determine the control line position of the NodeResizer
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right';
|
||||
|
||||
/**
|
||||
* Used to determine the control position of the NodeResizer
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ControlPosition = ControlLinePosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
||||
|
||||
/**
|
||||
* Used to determine the variant of the resize control
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export enum ResizeControlVariant {
|
||||
Line = 'line',
|
||||
Handle = 'handle',
|
||||
}
|
||||
|
||||
/**
|
||||
* The direction the user can resize the node.
|
||||
* @public
|
||||
*/
|
||||
export type ResizeControlDirection = 'horizontal' | 'vertical';
|
||||
|
||||
export const XY_RESIZER_HANDLE_POSITIONS: ControlPosition[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
||||
export const XY_RESIZER_LINE_POSITIONS: ControlLinePosition[] = ['top', 'right', 'bottom', 'left'];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user