chore(utils): another cleanup
This commit is contained in:
@@ -29,3 +29,5 @@ export const infiniteExtent: CoordinateExtent = [
|
|||||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const elementSelectionKeys = ['Enter', ' ', 'Escape'];
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import type { Transform, XYPosition, SnapGrid, Dimensions, NodeOrigin, HandleElement, Position } from '../types';
|
||||||
|
import { snapPosition, pointToRendererPoint } from './general';
|
||||||
|
|
||||||
|
export type GetPointerPositionParams = {
|
||||||
|
transform: Transform;
|
||||||
|
snapGrid?: SnapGrid;
|
||||||
|
snapToGrid?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPointerPosition(
|
||||||
|
event: MouseEvent | TouchEvent,
|
||||||
|
{ snapGrid = [0, 0], snapToGrid = false, transform }: GetPointerPositionParams
|
||||||
|
): XYPosition & { xSnapped: number; ySnapped: number } {
|
||||||
|
const { x, y } = getEventPosition(event);
|
||||||
|
const pointerPos = pointToRendererPoint({ x, y }, 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
|
||||||
|
return {
|
||||||
|
xSnapped,
|
||||||
|
ySnapped,
|
||||||
|
...pointerPos,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDimensions = (node: HTMLDivElement): Dimensions => ({
|
||||||
|
width: node.offsetWidth,
|
||||||
|
height: node.offsetHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getHostForElement = (element: HTMLElement): Document | ShadowRoot =>
|
||||||
|
(element.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');
|
||||||
|
// we want to be able to do a multi selection event if we are in an input field
|
||||||
|
const isModifierKey = event.ctrlKey || event.metaKey || event.shiftKey;
|
||||||
|
|
||||||
|
// when an input field is focused we don't want to trigger deletion or movement of nodes
|
||||||
|
return (isInput && !isModifierKey) || !!target?.closest('.nokey');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isMouseEvent = (event: MouseEvent | TouchEvent): event is MouseEvent => 'clientX' in event;
|
||||||
|
|
||||||
|
export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRect) => {
|
||||||
|
const isMouse = isMouseEvent(event);
|
||||||
|
const evtX = isMouse ? event.clientX : event.touches?.[0].clientX;
|
||||||
|
const evtY = isMouse ? event.clientY : event.touches?.[0].clientY;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: evtX - (bounds?.left ?? 0),
|
||||||
|
y: evtY - (bounds?.top ?? 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHandleBounds = (
|
||||||
|
selector: string,
|
||||||
|
nodeElement: HTMLDivElement,
|
||||||
|
zoom: number,
|
||||||
|
nodeOrigin: NodeOrigin = [0, 0]
|
||||||
|
): HandleElement[] | null => {
|
||||||
|
const handles = nodeElement.querySelectorAll(selector);
|
||||||
|
|
||||||
|
if (!handles || !handles.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlesArray = Array.from(handles) as HTMLDivElement[];
|
||||||
|
const nodeBounds = nodeElement.getBoundingClientRect();
|
||||||
|
const nodeOffset = {
|
||||||
|
x: nodeBounds.width * nodeOrigin[0],
|
||||||
|
y: nodeBounds.height * nodeOrigin[1],
|
||||||
|
};
|
||||||
|
|
||||||
|
return handlesArray.map((handle): HandleElement => {
|
||||||
|
const handleBounds = handle.getBoundingClientRect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: handle.getAttribute('data-handleid'),
|
||||||
|
position: handle.getAttribute('data-handlepos') as unknown as Position,
|
||||||
|
x: (handleBounds.left - nodeBounds.left - nodeOffset.x) / zoom,
|
||||||
|
y: (handleBounds.top - nodeBounds.top - nodeOffset.y) / zoom,
|
||||||
|
...getDimensions(handle),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Transform, internalsSymbol } from '../..';
|
import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..';
|
||||||
import { BaseEdge, BaseNode } from '../../types';
|
import { BaseEdge, BaseNode } from '../../types';
|
||||||
import { isNumeric, getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes } from '../utils';
|
import { isNumeric, getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general';
|
||||||
|
|
||||||
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
|
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
|
||||||
export function getEdgeCenter({
|
export function getEdgeCenter({
|
||||||
@@ -107,3 +107,82 @@ export function isEdgeVisible({ sourceNode, targetNode, width, height, transform
|
|||||||
|
|
||||||
return getOverlappingArea(viewRect, boxToRect(edgeBox)) > 0;
|
return getOverlappingArea(viewRect, boxToRect(edgeBox)) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection | BaseEdge): string =>
|
||||||
|
`xyflow__edge-${source}${sourceHandle || ''}-${target}${targetHandle || ''}`;
|
||||||
|
|
||||||
|
const connectionExists = (edge: BaseEdge, edges: BaseEdge[]) => {
|
||||||
|
return edges.some(
|
||||||
|
(el) =>
|
||||||
|
el.source === edge.source &&
|
||||||
|
el.target === edge.target &&
|
||||||
|
(el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) &&
|
||||||
|
(el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addEdgeBase = <EdgeType extends BaseEdge>(
|
||||||
|
edgeParams: EdgeType | Connection,
|
||||||
|
edges: EdgeType[]
|
||||||
|
): EdgeType[] => {
|
||||||
|
if (!edgeParams.source || !edgeParams.target) {
|
||||||
|
devWarn('006', errorMessages['error006']());
|
||||||
|
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
let edge: EdgeType;
|
||||||
|
if (isEdgeBase(edgeParams)) {
|
||||||
|
edge = { ...edgeParams };
|
||||||
|
} else {
|
||||||
|
edge = {
|
||||||
|
...edgeParams,
|
||||||
|
id: getEdgeId(edgeParams),
|
||||||
|
} as EdgeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionExists(edge, edges)) {
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
return edges.concat(edge);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateEdgeOptions = {
|
||||||
|
shouldReplaceId?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateEdgeBase = <EdgeType extends BaseEdge>(
|
||||||
|
oldEdge: EdgeType,
|
||||||
|
newConnection: Connection,
|
||||||
|
edges: EdgeType[],
|
||||||
|
options: UpdateEdgeOptions = { shouldReplaceId: true }
|
||||||
|
): EdgeType[] => {
|
||||||
|
const { id: oldEdgeId, ...rest } = oldEdge;
|
||||||
|
|
||||||
|
if (!newConnection.source || !newConnection.target) {
|
||||||
|
devWarn('006', errorMessages['error006']());
|
||||||
|
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundEdge = edges.find((e) => e.id === oldEdge.id) as EdgeType;
|
||||||
|
|
||||||
|
if (!foundEdge) {
|
||||||
|
devWarn('007', errorMessages['error007'](oldEdgeId));
|
||||||
|
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old edge and create the new edge with parameters of old edge.
|
||||||
|
const edge = {
|
||||||
|
...rest,
|
||||||
|
id: options.shouldReplaceId ? getEdgeId(newConnection) : oldEdgeId,
|
||||||
|
source: newConnection.source,
|
||||||
|
target: newConnection.target,
|
||||||
|
sourceHandle: newConnection.sourceHandle,
|
||||||
|
targetHandle: newConnection.targetHandle,
|
||||||
|
} as EdgeType;
|
||||||
|
|
||||||
|
return edges.filter((e) => e.id !== oldEdgeId).concat(edge);
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import type {
|
||||||
|
Dimensions,
|
||||||
|
XYPosition,
|
||||||
|
CoordinateExtent,
|
||||||
|
Box,
|
||||||
|
Rect,
|
||||||
|
BaseNode,
|
||||||
|
NodeOrigin,
|
||||||
|
SnapGrid,
|
||||||
|
Transform,
|
||||||
|
} from '../types';
|
||||||
|
import { getNodePositionWithOrigin } from './graph';
|
||||||
|
|
||||||
|
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max);
|
||||||
|
|
||||||
|
export const clampPosition = (position: XYPosition = { x: 0, y: 0 }, extent: CoordinateExtent) => ({
|
||||||
|
x: clamp(position.x, extent[0][0], extent[1][0]),
|
||||||
|
y: clamp(position.y, extent[0][1], extent[1][1]),
|
||||||
|
});
|
||||||
|
|
||||||
|
// returns a number between 0 and 1 that represents the velocity of the movement
|
||||||
|
// when the mouse is close to the edge of the canvas
|
||||||
|
const calcAutoPanVelocity = (value: number, min: number, max: number): number => {
|
||||||
|
if (value < min) {
|
||||||
|
return clamp(Math.abs(value - min), 1, 50) / 50;
|
||||||
|
} else if (value > max) {
|
||||||
|
return -clamp(Math.abs(value - max), 1, 50) / 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calcAutoPan = (pos: XYPosition, bounds: Dimensions): number[] => {
|
||||||
|
const xMovement = calcAutoPanVelocity(pos.x, 35, bounds.width - 35) * 20;
|
||||||
|
const yMovement = calcAutoPanVelocity(pos.y, 35, bounds.height - 35) * 20;
|
||||||
|
|
||||||
|
return [xMovement, yMovement];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({
|
||||||
|
x: Math.min(box1.x, box2.x),
|
||||||
|
y: Math.min(box1.y, box2.y),
|
||||||
|
x2: Math.max(box1.x2, box2.x2),
|
||||||
|
y2: Math.max(box1.y2, box2.y2),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rectToBox = ({ x, y, width, height }: Rect): Box => ({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
x2: x + width,
|
||||||
|
y2: y + height,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: x2 - x,
|
||||||
|
height: y2 - y,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const nodeToRect = (node: BaseNode, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
|
||||||
|
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...positionAbsolute,
|
||||||
|
width: node.width || 0,
|
||||||
|
height: node.height || 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const nodeToBox = (node: BaseNode, nodeOrigin: NodeOrigin = [0, 0]): Box => {
|
||||||
|
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...positionAbsolute,
|
||||||
|
x2: positionAbsolute.x + (node.width || 0),
|
||||||
|
y2: positionAbsolute.y + (node.height || 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBoundsOfRects = (rect1: Rect, rect2: Rect): Rect =>
|
||||||
|
boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)));
|
||||||
|
|
||||||
|
export const getOverlappingArea = (rectA: Rect, rectB: Rect): number => {
|
||||||
|
const xOverlap = Math.max(0, Math.min(rectA.x + rectA.width, rectB.x + rectB.width) - Math.max(rectA.x, rectB.x));
|
||||||
|
const yOverlap = Math.max(0, Math.min(rectA.y + rectA.height, rectB.y + rectB.height) - Math.max(rectA.y, rectB.y));
|
||||||
|
|
||||||
|
return Math.ceil(xOverlap * yOverlap);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const isRectObject = (obj: any): obj is Rect =>
|
||||||
|
isNumeric(obj.width) && isNumeric(obj.height) && isNumeric(obj.x) && isNumeric(obj.y);
|
||||||
|
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
|
export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n);
|
||||||
|
|
||||||
|
// used for a11y key board controls for nodes and edges
|
||||||
|
|
||||||
|
export const devWarn = (id: string, message: string) => {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.warn(`[React Flow]: ${message} Help: https://reactflow.dev/error#${id}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPositionWithOrigin = ({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
origin = [0, 0],
|
||||||
|
}: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
origin?: NodeOrigin;
|
||||||
|
}): XYPosition => {
|
||||||
|
if (!width || !height || origin[0] < 0 || origin[1] < 0 || origin[0] > 1 || origin[1] > 1) {
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: x - width * origin[0],
|
||||||
|
y: y - height * origin[1],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function snapPosition(position: XYPosition, snapGrid: SnapGrid = [1, 1]): XYPosition {
|
||||||
|
return {
|
||||||
|
x: snapGrid[0] * Math.round(position.x / snapGrid[0]),
|
||||||
|
y: snapGrid[1] * Math.round(position.y / snapGrid[1]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pointToRendererPoint = (
|
||||||
|
{ x, y }: XYPosition,
|
||||||
|
[tx, ty, tScale]: Transform,
|
||||||
|
snapToGrid = false,
|
||||||
|
snapGrid: SnapGrid = [1, 1]
|
||||||
|
): XYPosition => {
|
||||||
|
const position: XYPosition = {
|
||||||
|
x: (x - tx) / tScale,
|
||||||
|
y: (y - ty) / tScale,
|
||||||
|
};
|
||||||
|
|
||||||
|
return snapToGrid ? snapPosition(position, snapGrid) : position;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => {
|
||||||
|
return {
|
||||||
|
x: x * tScale + tx,
|
||||||
|
y: y * tScale + ty,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTransformForBounds = (
|
||||||
|
bounds: Rect,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
minZoom: number,
|
||||||
|
maxZoom: number,
|
||||||
|
padding = 0.1
|
||||||
|
): Transform => {
|
||||||
|
const xZoom = width / (bounds.width * (1 + padding));
|
||||||
|
const yZoom = height / (bounds.height * (1 + padding));
|
||||||
|
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, clampedZoom];
|
||||||
|
};
|
||||||
@@ -3,14 +3,14 @@ import {
|
|||||||
boxToRect,
|
boxToRect,
|
||||||
clamp,
|
clamp,
|
||||||
clampPosition,
|
clampPosition,
|
||||||
devWarn,
|
|
||||||
getBoundsOfBoxes,
|
getBoundsOfBoxes,
|
||||||
getOverlappingArea,
|
getOverlappingArea,
|
||||||
isNumeric,
|
isNumeric,
|
||||||
rectToBox,
|
rectToBox,
|
||||||
nodeToRect,
|
nodeToRect,
|
||||||
getEventPosition,
|
pointToRendererPoint,
|
||||||
} from './utils';
|
getTransformForBounds,
|
||||||
|
} from './general';
|
||||||
import {
|
import {
|
||||||
type Connection,
|
type Connection,
|
||||||
type Transform,
|
type Transform,
|
||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
type BaseEdge,
|
type BaseEdge,
|
||||||
type FitViewParamsBase,
|
type FitViewParamsBase,
|
||||||
type FitViewOptionsBase,
|
type FitViewOptionsBase,
|
||||||
SnapGrid,
|
|
||||||
NodeDragItem,
|
NodeDragItem,
|
||||||
CoordinateExtent,
|
CoordinateExtent,
|
||||||
OnError,
|
OnError,
|
||||||
@@ -62,106 +61,6 @@ export const getIncomersBase = <NodeType extends BaseNode = BaseNode, EdgeType e
|
|||||||
return nodes.filter((n) => incomersIds.includes(n.id));
|
return nodes.filter((n) => incomersIds.includes(n.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection | BaseEdge): string =>
|
|
||||||
`xyflow__edge-${source}${sourceHandle || ''}-${target}${targetHandle || ''}`;
|
|
||||||
|
|
||||||
const connectionExists = (edge: BaseEdge, edges: BaseEdge[]) => {
|
|
||||||
return edges.some(
|
|
||||||
(el) =>
|
|
||||||
el.source === edge.source &&
|
|
||||||
el.target === edge.target &&
|
|
||||||
(el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) &&
|
|
||||||
(el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addEdgeBase = <EdgeType extends BaseEdge>(
|
|
||||||
edgeParams: EdgeType | Connection,
|
|
||||||
edges: EdgeType[]
|
|
||||||
): EdgeType[] => {
|
|
||||||
if (!edgeParams.source || !edgeParams.target) {
|
|
||||||
devWarn('006', errorMessages['error006']());
|
|
||||||
|
|
||||||
return edges;
|
|
||||||
}
|
|
||||||
|
|
||||||
let edge: EdgeType;
|
|
||||||
if (isEdgeBase(edgeParams)) {
|
|
||||||
edge = { ...edgeParams };
|
|
||||||
} else {
|
|
||||||
edge = {
|
|
||||||
...edgeParams,
|
|
||||||
id: getEdgeId(edgeParams),
|
|
||||||
} as EdgeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectionExists(edge, edges)) {
|
|
||||||
return edges;
|
|
||||||
}
|
|
||||||
|
|
||||||
return edges.concat(edge);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpdateEdgeOptions = {
|
|
||||||
shouldReplaceId?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateEdgeBase = <EdgeType extends BaseEdge>(
|
|
||||||
oldEdge: EdgeType,
|
|
||||||
newConnection: Connection,
|
|
||||||
edges: EdgeType[],
|
|
||||||
options: UpdateEdgeOptions = { shouldReplaceId: true }
|
|
||||||
): EdgeType[] => {
|
|
||||||
const { id: oldEdgeId, ...rest } = oldEdge;
|
|
||||||
|
|
||||||
if (!newConnection.source || !newConnection.target) {
|
|
||||||
devWarn('006', errorMessages['error006']());
|
|
||||||
|
|
||||||
return edges;
|
|
||||||
}
|
|
||||||
|
|
||||||
const foundEdge = edges.find((e) => e.id === oldEdge.id) as EdgeType;
|
|
||||||
|
|
||||||
if (!foundEdge) {
|
|
||||||
devWarn('007', errorMessages['error007'](oldEdgeId));
|
|
||||||
|
|
||||||
return edges;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove old edge and create the new edge with parameters of old edge.
|
|
||||||
const edge = {
|
|
||||||
...rest,
|
|
||||||
id: options.shouldReplaceId ? getEdgeId(newConnection) : oldEdgeId,
|
|
||||||
source: newConnection.source,
|
|
||||||
target: newConnection.target,
|
|
||||||
sourceHandle: newConnection.sourceHandle,
|
|
||||||
targetHandle: newConnection.targetHandle,
|
|
||||||
} as EdgeType;
|
|
||||||
|
|
||||||
return edges.filter((e) => e.id !== oldEdgeId).concat(edge);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const pointToRendererPoint = (
|
|
||||||
{ x, y }: XYPosition,
|
|
||||||
[tx, ty, tScale]: Transform,
|
|
||||||
snapToGrid: boolean,
|
|
||||||
snapGrid: SnapGrid
|
|
||||||
): XYPosition => {
|
|
||||||
const position: XYPosition = {
|
|
||||||
x: (x - tx) / tScale,
|
|
||||||
y: (y - ty) / tScale,
|
|
||||||
};
|
|
||||||
|
|
||||||
return snapToGrid ? snapPosition(position, snapGrid) : position;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => {
|
|
||||||
return {
|
|
||||||
x: x * tScale + tx,
|
|
||||||
y: y * tScale + ty,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNodePositionWithOrigin = (
|
export const getNodePositionWithOrigin = (
|
||||||
node: BaseNode | undefined,
|
node: BaseNode | undefined,
|
||||||
nodeOrigin: NodeOrigin = [0, 0]
|
nodeOrigin: NodeOrigin = [0, 0]
|
||||||
@@ -230,8 +129,7 @@ export const getNodesInside = <NodeType extends BaseNode>(
|
|||||||
nodeOrigin: NodeOrigin = [0, 0]
|
nodeOrigin: NodeOrigin = [0, 0]
|
||||||
): NodeType[] => {
|
): NodeType[] => {
|
||||||
const paneRect = {
|
const paneRect = {
|
||||||
x: (rect.x - tx) / tScale,
|
...pointToRendererPoint(rect, [tx, ty, tScale]),
|
||||||
y: (rect.y - ty) / tScale,
|
|
||||||
width: rect.width / tScale,
|
width: rect.width / tScale,
|
||||||
height: rect.height / tScale,
|
height: rect.height / tScale,
|
||||||
};
|
};
|
||||||
@@ -269,26 +167,6 @@ export const getConnectedEdgesBase = <NodeType extends BaseNode = BaseNode, Edge
|
|||||||
return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target));
|
return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTransformForBounds = (
|
|
||||||
bounds: Rect,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
minZoom: number,
|
|
||||||
maxZoom: number,
|
|
||||||
padding = 0.1
|
|
||||||
): Transform => {
|
|
||||||
const xZoom = width / (bounds.width * (1 + padding));
|
|
||||||
const yZoom = height / (bounds.height * (1 + padding));
|
|
||||||
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, clampedZoom];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function fitView<Params extends FitViewParamsBase<BaseNode>, Options extends FitViewOptionsBase<BaseNode>>(
|
export function fitView<Params extends FitViewParamsBase<BaseNode>, Options extends FitViewOptionsBase<BaseNode>>(
|
||||||
{ nodes, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params,
|
{ nodes, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params,
|
||||||
options?: Options
|
options?: Options
|
||||||
@@ -325,33 +203,6 @@ export function fitView<Params extends FitViewParamsBase<BaseNode>, Options exte
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetPointerPositionParams = {
|
|
||||||
transform: Transform;
|
|
||||||
snapGrid?: SnapGrid;
|
|
||||||
snapToGrid?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getPointerPosition(
|
|
||||||
event: MouseEvent | TouchEvent,
|
|
||||||
{ snapGrid = [0, 0], snapToGrid = false, transform }: GetPointerPositionParams
|
|
||||||
): XYPosition & { xSnapped: number; ySnapped: number } {
|
|
||||||
const { x, y } = getEventPosition(event);
|
|
||||||
|
|
||||||
const pointerPos = {
|
|
||||||
x: (x - transform[0]) / transform[2],
|
|
||||||
y: (y - transform[1]) / transform[2],
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
|
||||||
return {
|
|
||||||
xSnapped,
|
|
||||||
ySnapped,
|
|
||||||
...pointerPos,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calcNextPosition<NodeType extends BaseNode>(
|
export function calcNextPosition<NodeType extends BaseNode>(
|
||||||
node: NodeDragItem | NodeType,
|
node: NodeDragItem | NodeType,
|
||||||
nextPosition: XYPosition,
|
nextPosition: XYPosition,
|
||||||
@@ -409,9 +260,42 @@ export function calcNextPosition<NodeType extends BaseNode>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function snapPosition(position: XYPosition, snapGrid: SnapGrid = [1, 1]) {
|
// helper function to get arrays of nodes and edges that can be deleted
|
||||||
|
// you can pass in a list of nodes and edges that should be deleted
|
||||||
|
// and the function only returns elements that are deletable and also handles connected nodes and child nodes
|
||||||
|
export function getElementsToRemove<NodeType extends BaseNode = BaseNode, EdgeType extends BaseEdge = BaseEdge>({
|
||||||
|
nodesToRemove,
|
||||||
|
edgesToRemove,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
}: {
|
||||||
|
nodesToRemove: Partial<NodeType>[];
|
||||||
|
edgesToRemove: Partial<EdgeType>[];
|
||||||
|
nodes: NodeType[];
|
||||||
|
edges: EdgeType[];
|
||||||
|
}): {
|
||||||
|
matchingNodes: NodeType[];
|
||||||
|
matchingEdges: EdgeType[];
|
||||||
|
} {
|
||||||
|
const nodeIds = nodesToRemove.map((node) => node.id);
|
||||||
|
const edgeIds = edgesToRemove.map((edge) => edge.id);
|
||||||
|
|
||||||
|
const matchingNodes = nodes.reduce<NodeType[]>((res, node) => {
|
||||||
|
const parentHit = !nodeIds.includes(node.id) && node.parentNode && res.find((n) => n.id === node.parentNode);
|
||||||
|
const deletable = typeof node.deletable === 'boolean' ? node.deletable : true;
|
||||||
|
if (deletable && (nodeIds.includes(node.id) || parentHit)) {
|
||||||
|
res.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}, []);
|
||||||
|
const deletableEdges = edges.filter((e) => (typeof e.deletable === 'boolean' ? e.deletable : true));
|
||||||
|
const initialHitEdges = deletableEdges.filter((e) => edgeIds.includes(e.id));
|
||||||
|
const connectedEdges = getConnectedEdgesBase<NodeType, EdgeType>(matchingNodes, deletableEdges);
|
||||||
|
const matchingEdges = [...initialHitEdges, ...connectedEdges];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: snapGrid[0] * Math.round(position.x / snapGrid[0]),
|
matchingEdges,
|
||||||
y: snapGrid[1] * Math.round(position.y / snapGrid[1]),
|
matchingNodes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './graph';
|
export * from './dom';
|
||||||
export * from './utils';
|
|
||||||
export * from './marker';
|
|
||||||
export * from './edges';
|
export * from './edges';
|
||||||
|
export * from './graph';
|
||||||
|
export * from './general';
|
||||||
|
export * from './marker';
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
import type {
|
|
||||||
Dimensions,
|
|
||||||
XYPosition,
|
|
||||||
CoordinateExtent,
|
|
||||||
Box,
|
|
||||||
Rect,
|
|
||||||
BaseNode,
|
|
||||||
BaseEdge,
|
|
||||||
NodeOrigin,
|
|
||||||
HandleElement,
|
|
||||||
Position,
|
|
||||||
} from '../types';
|
|
||||||
import { getConnectedEdgesBase, getNodePositionWithOrigin } from './graph';
|
|
||||||
|
|
||||||
export const getDimensions = (node: HTMLDivElement): Dimensions => ({
|
|
||||||
width: node.offsetWidth,
|
|
||||||
height: node.offsetHeight,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max);
|
|
||||||
|
|
||||||
export const clampPosition = (position: XYPosition = { x: 0, y: 0 }, extent: CoordinateExtent) => ({
|
|
||||||
x: clamp(position.x, extent[0][0], extent[1][0]),
|
|
||||||
y: clamp(position.y, extent[0][1], extent[1][1]),
|
|
||||||
});
|
|
||||||
|
|
||||||
// returns a number between 0 and 1 that represents the velocity of the movement
|
|
||||||
// when the mouse is close to the edge of the canvas
|
|
||||||
const calcAutoPanVelocity = (value: number, min: number, max: number): number => {
|
|
||||||
if (value < min) {
|
|
||||||
return clamp(Math.abs(value - min), 1, 50) / 50;
|
|
||||||
} else if (value > max) {
|
|
||||||
return -clamp(Math.abs(value - max), 1, 50) / 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const calcAutoPan = (pos: XYPosition, bounds: Dimensions): number[] => {
|
|
||||||
const xMovement = calcAutoPanVelocity(pos.x, 35, bounds.width - 35) * 20;
|
|
||||||
const yMovement = calcAutoPanVelocity(pos.y, 35, bounds.height - 35) * 20;
|
|
||||||
|
|
||||||
return [xMovement, yMovement];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getHostForElement = (element: HTMLElement): Document | ShadowRoot =>
|
|
||||||
(element.getRootNode?.() as Document | ShadowRoot) || window?.document;
|
|
||||||
|
|
||||||
export const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({
|
|
||||||
x: Math.min(box1.x, box2.x),
|
|
||||||
y: Math.min(box1.y, box2.y),
|
|
||||||
x2: Math.max(box1.x2, box2.x2),
|
|
||||||
y2: Math.max(box1.y2, box2.y2),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const rectToBox = ({ x, y, width, height }: Rect): Box => ({
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
x2: x + width,
|
|
||||||
y2: y + height,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width: x2 - x,
|
|
||||||
height: y2 - y,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nodeToRect = (node: BaseNode, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
|
|
||||||
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...positionAbsolute,
|
|
||||||
width: node.width || 0,
|
|
||||||
height: node.height || 0,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const nodeToBox = (node: BaseNode, nodeOrigin: NodeOrigin = [0, 0]): Box => {
|
|
||||||
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...positionAbsolute,
|
|
||||||
x2: positionAbsolute.x + (node.width || 0),
|
|
||||||
y2: positionAbsolute.y + (node.height || 0),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBoundsOfRects = (rect1: Rect, rect2: Rect): Rect =>
|
|
||||||
boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)));
|
|
||||||
|
|
||||||
export const getOverlappingArea = (rectA: Rect, rectB: Rect): number => {
|
|
||||||
const xOverlap = Math.max(0, Math.min(rectA.x + rectA.width, rectB.x + rectB.width) - Math.max(rectA.x, rectB.x));
|
|
||||||
const yOverlap = Math.max(0, Math.min(rectA.y + rectA.height, rectB.y + rectB.height) - Math.max(rectA.y, rectB.y));
|
|
||||||
|
|
||||||
return Math.ceil(xOverlap * yOverlap);
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export const isRectObject = (obj: any): obj is Rect =>
|
|
||||||
isNumeric(obj.width) && isNumeric(obj.height) && isNumeric(obj.x) && isNumeric(obj.y);
|
|
||||||
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n);
|
|
||||||
|
|
||||||
// used for a11y key board controls for nodes and edges
|
|
||||||
export const elementSelectionKeys = ['Enter', ' ', 'Escape'];
|
|
||||||
|
|
||||||
export const devWarn = (id: string, message: string) => {
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
console.warn(`[React Flow]: ${message} Help: https://reactflow.dev/error#${id}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isInputDOMNode(event: KeyboardEvent): boolean {
|
|
||||||
// using composed path for handling shadow dom
|
|
||||||
const target = (event.composedPath?.()?.[0] || event.target) as HTMLElement;
|
|
||||||
|
|
||||||
const isInput = ['INPUT', 'SELECT', 'TEXTAREA'].includes(target?.nodeName) || target?.hasAttribute('contenteditable');
|
|
||||||
// we want to be able to do a multi selection event if we are in an input field
|
|
||||||
const isModifierKey = event.ctrlKey || event.metaKey || event.shiftKey;
|
|
||||||
|
|
||||||
// when an input field is focused we don't want to trigger deletion or movement of nodes
|
|
||||||
return (isInput && !isModifierKey) || !!target?.closest('.nokey');
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isMouseEvent = (event: MouseEvent | TouchEvent): event is MouseEvent => 'clientX' in event;
|
|
||||||
|
|
||||||
export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRect) => {
|
|
||||||
const isMouse = isMouseEvent(event);
|
|
||||||
const evtX = isMouse ? event.clientX : event.touches?.[0].clientX;
|
|
||||||
const evtY = isMouse ? event.clientY : event.touches?.[0].clientY;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: evtX - (bounds?.left ?? 0),
|
|
||||||
y: evtY - (bounds?.top ?? 0),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// helper function to get arrays of nodes and edges that can be deleted
|
|
||||||
// you can pass in a list of nodes and edges that should be deleted
|
|
||||||
// and the function only returns elements that are deletable and also handles connected nodes and child nodes
|
|
||||||
export function getElementsToRemove<NodeType extends BaseNode = BaseNode, EdgeType extends BaseEdge = BaseEdge>({
|
|
||||||
nodesToRemove,
|
|
||||||
edgesToRemove,
|
|
||||||
nodes,
|
|
||||||
edges,
|
|
||||||
}: {
|
|
||||||
nodesToRemove: Partial<NodeType>[];
|
|
||||||
edgesToRemove: Partial<EdgeType>[];
|
|
||||||
nodes: NodeType[];
|
|
||||||
edges: EdgeType[];
|
|
||||||
}): {
|
|
||||||
matchingNodes: NodeType[];
|
|
||||||
matchingEdges: EdgeType[];
|
|
||||||
} {
|
|
||||||
const nodeIds = nodesToRemove.map((node) => node.id);
|
|
||||||
const edgeIds = edgesToRemove.map((edge) => edge.id);
|
|
||||||
|
|
||||||
const matchingNodes = nodes.reduce<NodeType[]>((res, node) => {
|
|
||||||
const parentHit = !nodeIds.includes(node.id) && node.parentNode && res.find((n) => n.id === node.parentNode);
|
|
||||||
const deletable = typeof node.deletable === 'boolean' ? node.deletable : true;
|
|
||||||
if (deletable && (nodeIds.includes(node.id) || parentHit)) {
|
|
||||||
res.push(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}, []);
|
|
||||||
const deletableEdges = edges.filter((e) => (typeof e.deletable === 'boolean' ? e.deletable : true));
|
|
||||||
const initialHitEdges = deletableEdges.filter((e) => edgeIds.includes(e.id));
|
|
||||||
const connectedEdges = getConnectedEdgesBase<NodeType, EdgeType>(matchingNodes, deletableEdges);
|
|
||||||
const matchingEdges = [...initialHitEdges, ...connectedEdges];
|
|
||||||
|
|
||||||
return {
|
|
||||||
matchingEdges,
|
|
||||||
matchingNodes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getPositionWithOrigin = ({
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
origin = [0, 0],
|
|
||||||
}: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
origin?: NodeOrigin;
|
|
||||||
}): XYPosition => {
|
|
||||||
if (!width || !height || origin[0] < 0 || origin[1] < 0 || origin[0] > 1 || origin[1] > 1) {
|
|
||||||
return { x, y };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: x - width * origin[0],
|
|
||||||
y: y - height * origin[1],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getHandleBounds = (
|
|
||||||
selector: string,
|
|
||||||
nodeElement: HTMLDivElement,
|
|
||||||
zoom: number,
|
|
||||||
nodeOrigin: NodeOrigin = [0, 0]
|
|
||||||
): HandleElement[] | null => {
|
|
||||||
const handles = nodeElement.querySelectorAll(selector);
|
|
||||||
|
|
||||||
if (!handles || !handles.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlesArray = Array.from(handles) as HTMLDivElement[];
|
|
||||||
const nodeBounds = nodeElement.getBoundingClientRect();
|
|
||||||
const nodeOffset = {
|
|
||||||
x: nodeBounds.width * nodeOrigin[0],
|
|
||||||
y: nodeBounds.height * nodeOrigin[1],
|
|
||||||
};
|
|
||||||
|
|
||||||
return handlesArray.map((handle): HandleElement => {
|
|
||||||
const handleBounds = handle.getBoundingClientRect();
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: handle.getAttribute('data-handleid'),
|
|
||||||
position: handle.getAttribute('data-handlepos') as unknown as Position,
|
|
||||||
x: (handleBounds.left - nodeBounds.left - nodeOffset.x) / zoom,
|
|
||||||
y: (handleBounds.top - nodeBounds.top - nodeOffset.y) / zoom,
|
|
||||||
...getDimensions(handle),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user