|
import { drag } from 'd3-drag'; |
|
import { select, pointer } from 'd3-selection'; |
|
import { zoom, zoomIdentity, zoomTransform } from 'd3-zoom'; |
|
|
|
const errorMessages = { |
|
error001: () => '[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#001', |
|
error002: () => "It looks like you've created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them.", |
|
error003: (nodeType) => `Node type "${nodeType}" not found. Using fallback type "default".`, |
|
error004: () => 'The React Flow parent container needs a width and a height to render the graph.', |
|
error005: () => 'Only child nodes can use a parent extent.', |
|
error006: () => "Can't create edge. An edge needs a source and a target.", |
|
error007: (id) => `The old edge with id=${id} does not exist.`, |
|
error009: (type) => `Marker type "${type}" doesn't exist.`, |
|
error008: (handleType, { id, sourceHandle, targetHandle }) => `Couldn't create edge for ${handleType} handle id: "${handleType === 'source' ? sourceHandle : targetHandle}", edge id: ${id}.`, |
|
error010: () => 'Handle: No node id found. Make sure to only use a Handle inside a custom Node.', |
|
error011: (edgeType) => `Edge type "${edgeType}" not found. Using fallback type "default".`, |
|
error012: (id) => `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 = '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.`, |
|
}; |
|
const infiniteExtent = [ |
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], |
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], |
|
]; |
|
const elementSelectionKeys = ['Enter', ' ', 'Escape']; |
|
|
|
var ConnectionMode; |
|
(function (ConnectionMode) { |
|
ConnectionMode["Strict"] = "strict"; |
|
ConnectionMode["Loose"] = "loose"; |
|
})(ConnectionMode || (ConnectionMode = {})); |
|
var PanOnScrollMode; |
|
(function (PanOnScrollMode) { |
|
PanOnScrollMode["Free"] = "free"; |
|
PanOnScrollMode["Vertical"] = "vertical"; |
|
PanOnScrollMode["Horizontal"] = "horizontal"; |
|
})(PanOnScrollMode || (PanOnScrollMode = {})); |
|
var SelectionMode; |
|
(function (SelectionMode) { |
|
SelectionMode["Partial"] = "partial"; |
|
SelectionMode["Full"] = "full"; |
|
})(SelectionMode || (SelectionMode = {})); |
|
const initialConnection = { |
|
inProgress: false, |
|
isValid: null, |
|
from: null, |
|
fromHandle: null, |
|
fromPosition: null, |
|
fromNode: null, |
|
to: null, |
|
toHandle: null, |
|
toPosition: null, |
|
toNode: null, |
|
}; |
|
|
|
var ConnectionLineType; |
|
(function (ConnectionLineType) { |
|
ConnectionLineType["Bezier"] = "default"; |
|
ConnectionLineType["Straight"] = "straight"; |
|
ConnectionLineType["Step"] = "step"; |
|
ConnectionLineType["SmoothStep"] = "smoothstep"; |
|
ConnectionLineType["SimpleBezier"] = "simplebezier"; |
|
})(ConnectionLineType || (ConnectionLineType = {})); |
|
var MarkerType; |
|
(function (MarkerType) { |
|
MarkerType["Arrow"] = "arrow"; |
|
MarkerType["ArrowClosed"] = "arrowclosed"; |
|
})(MarkerType || (MarkerType = {})); |
|
|
|
var Position; |
|
(function (Position) { |
|
Position["Left"] = "left"; |
|
Position["Top"] = "top"; |
|
Position["Right"] = "right"; |
|
Position["Bottom"] = "bottom"; |
|
})(Position || (Position = {})); |
|
const oppositePosition = { |
|
[Position.Left]: Position.Right, |
|
[Position.Right]: Position.Left, |
|
[Position.Top]: Position.Bottom, |
|
[Position.Bottom]: Position.Top, |
|
}; |
|
|
|
|
|
|
|
|
|
function areConnectionMapsEqual(a, b) { |
|
if (!a && !b) { |
|
return true; |
|
} |
|
if (!a || !b || a.size !== b.size) { |
|
return false; |
|
} |
|
if (!a.size && !b.size) { |
|
return true; |
|
} |
|
for (const key of a.keys()) { |
|
if (!b.has(key)) { |
|
return false; |
|
} |
|
} |
|
return true; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function handleConnectionChange(a, b, cb) { |
|
if (!cb) { |
|
return; |
|
} |
|
const diff = []; |
|
a.forEach((connection, key) => { |
|
if (!b?.has(key)) { |
|
diff.push(connection); |
|
} |
|
}); |
|
if (diff.length) { |
|
cb(diff); |
|
} |
|
} |
|
function getConnectionStatus(isValid) { |
|
return isValid === null ? null : isValid ? 'valid' : 'invalid'; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isEdgeBase = (element) => 'id' in element && 'source' in element && 'target' in element; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isNodeBase = (element) => 'id' in element && 'position' in element && !('source' in element) && !('target' in element); |
|
const isInternalNodeBase = (element) => 'id' in element && 'internals' in element && !('source' in element) && !('target' in element); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const getOutgoers = (node, nodes, edges) => { |
|
if (!node.id) { |
|
return []; |
|
} |
|
const outgoerIds = new Set(); |
|
edges.forEach((edge) => { |
|
if (edge.source === node.id) { |
|
outgoerIds.add(edge.target); |
|
} |
|
}); |
|
return nodes.filter((n) => outgoerIds.has(n.id)); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const getIncomers = (node, nodes, edges) => { |
|
if (!node.id) { |
|
return []; |
|
} |
|
const incomersIds = new Set(); |
|
edges.forEach((edge) => { |
|
if (edge.target === node.id) { |
|
incomersIds.add(edge.source); |
|
} |
|
}); |
|
return nodes.filter((n) => incomersIds.has(n.id)); |
|
}; |
|
const getNodePositionWithOrigin = (node, nodeOrigin = [0, 0]) => { |
|
const { width, height } = getNodeDimensions(node); |
|
const origin = node.origin ?? nodeOrigin; |
|
const offsetX = width * origin[0]; |
|
const offsetY = height * origin[1]; |
|
return { |
|
x: node.position.x - offsetX, |
|
y: node.position.y - offsetY, |
|
}; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const getNodesBounds = (nodes, params = { nodeOrigin: [0, 0], nodeLookup: undefined }) => { |
|
if (process.env.NODE_ENV === 'development' && !params.nodeLookup) { |
|
console.warn('Please use `getNodesBounds` from `useReactFlow`/`useSvelteFlow` hook to ensure correct values for sub flows. If not possible, you have to provide a nodeLookup to support sub flows.'); |
|
} |
|
if (nodes.length === 0) { |
|
return { x: 0, y: 0, width: 0, height: 0 }; |
|
} |
|
const box = nodes.reduce((currBox, nodeOrId) => { |
|
const isId = typeof nodeOrId === 'string'; |
|
let currentNode = !params.nodeLookup && !isId ? nodeOrId : undefined; |
|
if (params.nodeLookup) { |
|
currentNode = isId |
|
? params.nodeLookup.get(nodeOrId) |
|
: !isInternalNodeBase(nodeOrId) |
|
? params.nodeLookup.get(nodeOrId.id) |
|
: nodeOrId; |
|
} |
|
const nodeBox = currentNode ? nodeToBox(currentNode, params.nodeOrigin) : { x: 0, y: 0, x2: 0, y2: 0 }; |
|
return getBoundsOfBoxes(currBox, nodeBox); |
|
}, { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }); |
|
return boxToRect(box); |
|
}; |
|
|
|
|
|
|
|
|
|
const getInternalNodesBounds = (nodeLookup, params = {}) => { |
|
if (nodeLookup.size === 0) { |
|
return { x: 0, y: 0, width: 0, height: 0 }; |
|
} |
|
let box = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }; |
|
nodeLookup.forEach((node) => { |
|
if (params.filter === undefined || params.filter(node)) { |
|
const nodeBox = nodeToBox(node); |
|
box = getBoundsOfBoxes(box, nodeBox); |
|
} |
|
}); |
|
return boxToRect(box); |
|
}; |
|
const getNodesInside = (nodes, rect, [tx, ty, tScale] = [0, 0, 1], partially = false, |
|
|
|
excludeNonSelectableNodes = false) => { |
|
const paneRect = { |
|
...pointToRendererPoint(rect, [tx, ty, tScale]), |
|
width: rect.width / tScale, |
|
height: rect.height / tScale, |
|
}; |
|
const visibleNodes = []; |
|
for (const node of nodes.values()) { |
|
const { measured, selectable = true, hidden = false } = node; |
|
if ((excludeNonSelectableNodes && !selectable) || hidden) { |
|
continue; |
|
} |
|
const width = measured.width ?? node.width ?? node.initialWidth ?? null; |
|
const height = measured.height ?? node.height ?? node.initialHeight ?? null; |
|
const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node)); |
|
const area = (width ?? 0) * (height ?? 0); |
|
const partiallyVisible = partially && overlappingArea > 0; |
|
const forceInitialRender = !node.internals.handleBounds; |
|
const isVisible = forceInitialRender || partiallyVisible || overlappingArea >= area; |
|
if (isVisible || node.dragging) { |
|
visibleNodes.push(node); |
|
} |
|
} |
|
return visibleNodes; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
const getConnectedEdges = (nodes, edges) => { |
|
const nodeIds = new Set(); |
|
nodes.forEach((node) => { |
|
nodeIds.add(node.id); |
|
}); |
|
return edges.filter((edge) => nodeIds.has(edge.source) || nodeIds.has(edge.target)); |
|
}; |
|
function getFitViewNodes(nodeLookup, options) { |
|
const fitViewNodes = new Map(); |
|
const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null; |
|
nodeLookup.forEach((n) => { |
|
const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden); |
|
if (isVisible && (!optionNodeIds || optionNodeIds.has(n.id))) { |
|
fitViewNodes.set(n.id, n); |
|
} |
|
}); |
|
return fitViewNodes; |
|
} |
|
async function fitView({ nodes, width, height, panZoom, minZoom, maxZoom }, options) { |
|
if (nodes.size === 0) { |
|
return Promise.resolve(false); |
|
} |
|
const bounds = getInternalNodesBounds(nodes); |
|
const viewport = getViewportForBounds(bounds, width, height, options?.minZoom ?? minZoom, options?.maxZoom ?? maxZoom, options?.padding ?? 0.1); |
|
await panZoom.setViewport(viewport, { duration: options?.duration }); |
|
return Promise.resolve(true); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function calculateNodePosition({ nodeId, nextPosition, nodeLookup, nodeOrigin = [0, 0], nodeExtent, onError, }) { |
|
const node = nodeLookup.get(nodeId); |
|
const parentNode = node.parentId ? nodeLookup.get(node.parentId) : undefined; |
|
const { x: parentX, y: parentY } = parentNode ? parentNode.internals.positionAbsolute : { x: 0, y: 0 }; |
|
const origin = node.origin ?? nodeOrigin; |
|
let extent = nodeExtent; |
|
if (node.extent === 'parent' && !node.expandParent) { |
|
if (!parentNode) { |
|
onError?.('005', errorMessages['error005']()); |
|
} |
|
else { |
|
const parentWidth = parentNode.measured.width; |
|
const parentHeight = parentNode.measured.height; |
|
if (parentWidth && parentHeight) { |
|
extent = [ |
|
[parentX, parentY], |
|
[parentX + parentWidth, parentY + parentHeight], |
|
]; |
|
} |
|
} |
|
} |
|
else if (parentNode && isCoordinateExtent(node.extent)) { |
|
extent = [ |
|
[node.extent[0][0] + parentX, node.extent[0][1] + parentY], |
|
[node.extent[1][0] + parentX, node.extent[1][1] + parentY], |
|
]; |
|
} |
|
const positionAbsolute = isCoordinateExtent(extent) |
|
? clampPosition(nextPosition, extent, node.measured) |
|
: nextPosition; |
|
return { |
|
position: { |
|
x: positionAbsolute.x - parentX + node.measured.width * origin[0], |
|
y: positionAbsolute.y - parentY + node.measured.height * origin[1], |
|
}, |
|
positionAbsolute, |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function getElementsToRemove({ nodesToRemove = [], edgesToRemove = [], nodes, edges, onBeforeDelete, }) { |
|
const nodeIds = new Set(nodesToRemove.map((node) => node.id)); |
|
const matchingNodes = []; |
|
for (const node of nodes) { |
|
if (node.deletable === false) { |
|
continue; |
|
} |
|
const isIncluded = nodeIds.has(node.id); |
|
const parentHit = !isIncluded && node.parentId && matchingNodes.find((n) => n.id === node.parentId); |
|
if (isIncluded || parentHit) { |
|
matchingNodes.push(node); |
|
} |
|
} |
|
const edgeIds = new Set(edgesToRemove.map((edge) => edge.id)); |
|
const deletableEdges = edges.filter((edge) => edge.deletable !== false); |
|
const connectedEdges = getConnectedEdges(matchingNodes, deletableEdges); |
|
const matchingEdges = connectedEdges; |
|
for (const edge of deletableEdges) { |
|
const isIncluded = edgeIds.has(edge.id); |
|
if (isIncluded && !matchingEdges.find((e) => e.id === edge.id)) { |
|
matchingEdges.push(edge); |
|
} |
|
} |
|
if (!onBeforeDelete) { |
|
return { |
|
edges: matchingEdges, |
|
nodes: matchingNodes, |
|
}; |
|
} |
|
const onBeforeDeleteResult = await onBeforeDelete({ |
|
nodes: matchingNodes, |
|
edges: matchingEdges, |
|
}); |
|
if (typeof onBeforeDeleteResult === 'boolean') { |
|
return onBeforeDeleteResult ? { edges: matchingEdges, nodes: matchingNodes } : { edges: [], nodes: [] }; |
|
} |
|
return onBeforeDeleteResult; |
|
} |
|
|
|
const clamp = (val, min = 0, max = 1) => Math.min(Math.max(val, min), max); |
|
const clampPosition = (position = { x: 0, y: 0 }, extent, dimensions) => ({ |
|
x: clamp(position.x, extent[0][0], extent[1][0] - (dimensions?.width ?? 0)), |
|
y: clamp(position.y, extent[0][1], extent[1][1] - (dimensions?.height ?? 0)), |
|
}); |
|
function clampPositionToParent(childPosition, childDimensions, parent) { |
|
const { width: parentWidth, height: parentHeight } = getNodeDimensions(parent); |
|
const { x: parentX, y: parentY } = parent.internals.positionAbsolute; |
|
return clampPosition(childPosition, [ |
|
[parentX, parentY], |
|
[parentX + parentWidth, parentY + parentHeight], |
|
], childDimensions); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const calcAutoPanVelocity = (value, min, max) => { |
|
if (value < min) { |
|
return clamp(Math.abs(value - min), 1, min) / min; |
|
} |
|
else if (value > max) { |
|
return -clamp(Math.abs(value - max), 1, min) / min; |
|
} |
|
return 0; |
|
}; |
|
const calcAutoPan = (pos, bounds, speed = 15, distance = 40) => { |
|
const xMovement = calcAutoPanVelocity(pos.x, distance, bounds.width - distance) * speed; |
|
const yMovement = calcAutoPanVelocity(pos.y, distance, bounds.height - distance) * speed; |
|
return [xMovement, yMovement]; |
|
}; |
|
const getBoundsOfBoxes = (box1, box2) => ({ |
|
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), |
|
}); |
|
const rectToBox = ({ x, y, width, height }) => ({ |
|
x, |
|
y, |
|
x2: x + width, |
|
y2: y + height, |
|
}); |
|
const boxToRect = ({ x, y, x2, y2 }) => ({ |
|
x, |
|
y, |
|
width: x2 - x, |
|
height: y2 - y, |
|
}); |
|
const nodeToRect = (node, nodeOrigin = [0, 0]) => { |
|
const { x, y } = isInternalNodeBase(node) |
|
? node.internals.positionAbsolute |
|
: getNodePositionWithOrigin(node, nodeOrigin); |
|
return { |
|
x, |
|
y, |
|
width: node.measured?.width ?? node.width ?? node.initialWidth ?? 0, |
|
height: node.measured?.height ?? node.height ?? node.initialHeight ?? 0, |
|
}; |
|
}; |
|
const nodeToBox = (node, nodeOrigin = [0, 0]) => { |
|
const { x, y } = isInternalNodeBase(node) |
|
? node.internals.positionAbsolute |
|
: getNodePositionWithOrigin(node, nodeOrigin); |
|
return { |
|
x, |
|
y, |
|
x2: x + (node.measured?.width ?? node.width ?? node.initialWidth ?? 0), |
|
y2: y + (node.measured?.height ?? node.height ?? node.initialHeight ?? 0), |
|
}; |
|
}; |
|
const getBoundsOfRects = (rect1, rect2) => boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2))); |
|
const getOverlappingArea = (rectA, rectB) => { |
|
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); |
|
}; |
|
|
|
const isRectObject = (obj) => isNumeric(obj.width) && isNumeric(obj.height) && isNumeric(obj.x) && isNumeric(obj.y); |
|
|
|
const isNumeric = (n) => !isNaN(n) && isFinite(n); |
|
|
|
const devWarn = (id, message) => { |
|
if (process.env.NODE_ENV === 'development') { |
|
console.warn(`[React Flow]: ${message} Help: https://reactflow.dev/error#${id}`); |
|
} |
|
}; |
|
const snapPosition = (position, snapGrid = [1, 1]) => { |
|
return { |
|
x: snapGrid[0] * Math.round(position.x / snapGrid[0]), |
|
y: snapGrid[1] * Math.round(position.y / snapGrid[1]), |
|
}; |
|
}; |
|
const pointToRendererPoint = ({ x, y }, [tx, ty, tScale], snapToGrid = false, snapGrid = [1, 1]) => { |
|
const position = { |
|
x: (x - tx) / tScale, |
|
y: (y - ty) / tScale, |
|
}; |
|
return snapToGrid ? snapPosition(position, snapGrid) : position; |
|
}; |
|
const rendererPointToPoint = ({ x, y }, [tx, ty, tScale]) => { |
|
return { |
|
x: x * tScale + tx, |
|
y: y * tScale + ty, |
|
}; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const getViewportForBounds = (bounds, width, height, minZoom, maxZoom, padding) => { |
|
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, zoom: clampedZoom }; |
|
}; |
|
const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0; |
|
function isCoordinateExtent(extent) { |
|
return extent !== undefined && extent !== 'parent'; |
|
} |
|
function getNodeDimensions(node) { |
|
return { |
|
width: node.measured?.width ?? node.width ?? node.initialWidth ?? 0, |
|
height: node.measured?.height ?? node.height ?? node.initialHeight ?? 0, |
|
}; |
|
} |
|
function nodeHasDimensions(node) { |
|
return ((node.measured?.width ?? node.width ?? node.initialWidth) !== undefined && |
|
(node.measured?.height ?? node.height ?? node.initialHeight) !== undefined); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function evaluateAbsolutePosition(position, dimensions = { width: 0, height: 0 }, parentId, nodeLookup, nodeOrigin) { |
|
let nextParentId = 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]; |
|
} |
|
} |
|
return positionAbsolute; |
|
} |
|
|
|
function getPointerPosition(event, { snapGrid = [0, 0], snapToGrid = false, transform }) { |
|
const { x, y } = getEventPosition(event); |
|
const pointerPos = pointToRendererPoint({ x, y }, transform); |
|
const { x: xSnapped, y: ySnapped } = snapToGrid ? snapPosition(pointerPos, snapGrid) : pointerPos; |
|
|
|
return { |
|
xSnapped, |
|
ySnapped, |
|
...pointerPos, |
|
}; |
|
} |
|
const getDimensions = (node) => ({ |
|
width: node.offsetWidth, |
|
height: node.offsetHeight, |
|
}); |
|
const getHostForElement = (element) => element.getRootNode?.() || window?.document; |
|
const inputTags = ['INPUT', 'SELECT', 'TEXTAREA']; |
|
function isInputDOMNode(event) { |
|
|
|
const target = (event.composedPath?.()?.[0] || event.target); |
|
const isInput = inputTags.includes(target?.nodeName) || target?.hasAttribute('contenteditable'); |
|
|
|
return isInput || !!target?.closest('.nokey'); |
|
} |
|
const isMouseEvent = (event) => 'clientX' in event; |
|
const getEventPosition = (event, bounds) => { |
|
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), |
|
}; |
|
}; |
|
|
|
|
|
|
|
const getHandleBounds = (type, nodeElement, nodeBounds, zoom, nodeId) => { |
|
const handles = nodeElement.querySelectorAll(`.${type}`); |
|
if (!handles || !handles.length) { |
|
return null; |
|
} |
|
return Array.from(handles).map((handle) => { |
|
const handleBounds = handle.getBoundingClientRect(); |
|
return { |
|
id: handle.getAttribute('data-handleid'), |
|
type, |
|
nodeId, |
|
position: handle.getAttribute('data-handlepos'), |
|
x: (handleBounds.left - nodeBounds.left) / zoom, |
|
y: (handleBounds.top - nodeBounds.top) / zoom, |
|
...getDimensions(handle), |
|
}; |
|
}); |
|
}; |
|
|
|
function getBezierEdgeCenter({ sourceX, sourceY, targetX, targetY, sourceControlX, sourceControlY, targetControlX, targetControlY, }) { |
|
|
|
|
|
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); |
|
const offsetY = Math.abs(centerY - sourceY); |
|
return [centerX, centerY, offsetX, offsetY]; |
|
} |
|
function calculateControlOffset(distance, curvature) { |
|
if (distance >= 0) { |
|
return 0.5 * distance; |
|
} |
|
return curvature * 25 * Math.sqrt(-distance); |
|
} |
|
function getControlWithCurvature({ pos, x1, y1, x2, y2, c }) { |
|
switch (pos) { |
|
case Position.Left: |
|
return [x1 - calculateControlOffset(x1 - x2, c), y1]; |
|
case Position.Right: |
|
return [x1 + calculateControlOffset(x2 - x1, c), y1]; |
|
case Position.Top: |
|
return [x1, y1 - calculateControlOffset(y1 - y2, c)]; |
|
case Position.Bottom: |
|
return [x1, y1 + calculateControlOffset(y2 - y1, c)]; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getBezierPath({ sourceX, sourceY, sourcePosition = Position.Bottom, targetX, targetY, targetPosition = Position.Top, curvature = 0.25, }) { |
|
const [sourceControlX, sourceControlY] = getControlWithCurvature({ |
|
pos: sourcePosition, |
|
x1: sourceX, |
|
y1: sourceY, |
|
x2: targetX, |
|
y2: targetY, |
|
c: curvature, |
|
}); |
|
const [targetControlX, targetControlY] = getControlWithCurvature({ |
|
pos: targetPosition, |
|
x1: targetX, |
|
y1: targetY, |
|
x2: sourceX, |
|
y2: sourceY, |
|
c: curvature, |
|
}); |
|
const [labelX, labelY, offsetX, offsetY] = getBezierEdgeCenter({ |
|
sourceX, |
|
sourceY, |
|
targetX, |
|
targetY, |
|
sourceControlX, |
|
sourceControlY, |
|
targetControlX, |
|
targetControlY, |
|
}); |
|
return [ |
|
`M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`, |
|
labelX, |
|
labelY, |
|
offsetX, |
|
offsetY, |
|
]; |
|
} |
|
|
|
|
|
function getEdgeCenter({ sourceX, sourceY, targetX, targetY, }) { |
|
const xOffset = Math.abs(targetX - sourceX) / 2; |
|
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset; |
|
const yOffset = Math.abs(targetY - sourceY) / 2; |
|
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset; |
|
return [centerX, centerY, xOffset, yOffset]; |
|
} |
|
function getElevatedEdgeZIndex({ sourceNode, targetNode, selected = false, zIndex = 0, elevateOnSelect = false, }) { |
|
if (!elevateOnSelect) { |
|
return zIndex; |
|
} |
|
const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected; |
|
const selectedZIndex = Math.max(sourceNode.internals.z || 0, targetNode.internals.z || 0, 1000); |
|
return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0); |
|
} |
|
function isEdgeVisible({ sourceNode, targetNode, width, height, transform }) { |
|
const edgeBox = getBoundsOfBoxes(nodeToBox(sourceNode), nodeToBox(targetNode)); |
|
if (edgeBox.x === edgeBox.x2) { |
|
edgeBox.x2 += 1; |
|
} |
|
if (edgeBox.y === edgeBox.y2) { |
|
edgeBox.y2 += 1; |
|
} |
|
const viewRect = { |
|
x: -transform[0] / transform[2], |
|
y: -transform[1] / transform[2], |
|
width: width / transform[2], |
|
height: height / transform[2], |
|
}; |
|
return getOverlappingArea(viewRect, boxToRect(edgeBox)) > 0; |
|
} |
|
const getEdgeId = ({ source, sourceHandle, target, targetHandle }) => `xy-edge__${source}${sourceHandle || ''}-${target}${targetHandle || ''}`; |
|
const connectionExists = (edge, edges) => { |
|
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))); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const addEdge = (edgeParams, edges) => { |
|
if (!edgeParams.source || !edgeParams.target) { |
|
devWarn('006', errorMessages['error006']()); |
|
return edges; |
|
} |
|
let edge; |
|
if (isEdgeBase(edgeParams)) { |
|
edge = { ...edgeParams }; |
|
} |
|
else { |
|
edge = { |
|
...edgeParams, |
|
id: getEdgeId(edgeParams), |
|
}; |
|
} |
|
if (connectionExists(edge, edges)) { |
|
return edges; |
|
} |
|
if (edge.sourceHandle === null) { |
|
delete edge.sourceHandle; |
|
} |
|
if (edge.targetHandle === null) { |
|
delete edge.targetHandle; |
|
} |
|
return edges.concat(edge); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const reconnectEdge = (oldEdge, newConnection, edges, options = { shouldReplaceId: true }) => { |
|
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); |
|
if (!foundEdge) { |
|
devWarn('007', errorMessages['error007'](oldEdgeId)); |
|
return edges; |
|
} |
|
|
|
const edge = { |
|
...rest, |
|
id: options.shouldReplaceId ? getEdgeId(newConnection) : oldEdgeId, |
|
source: newConnection.source, |
|
target: newConnection.target, |
|
sourceHandle: newConnection.sourceHandle, |
|
targetHandle: newConnection.targetHandle, |
|
}; |
|
return edges.filter((e) => e.id !== oldEdgeId).concat(edge); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getStraightPath({ sourceX, sourceY, targetX, targetY, }) { |
|
const [labelX, labelY, offsetX, offsetY] = getEdgeCenter({ |
|
sourceX, |
|
sourceY, |
|
targetX, |
|
targetY, |
|
}); |
|
return [`M ${sourceX},${sourceY}L ${targetX},${targetY}`, labelX, labelY, offsetX, offsetY]; |
|
} |
|
|
|
const handleDirections = { |
|
[Position.Left]: { x: -1, y: 0 }, |
|
[Position.Right]: { x: 1, y: 0 }, |
|
[Position.Top]: { x: 0, y: -1 }, |
|
[Position.Bottom]: { x: 0, y: 1 }, |
|
}; |
|
const getDirection = ({ source, sourcePosition = Position.Bottom, target, }) => { |
|
if (sourcePosition === Position.Left || sourcePosition === Position.Right) { |
|
return source.x < target.x ? { x: 1, y: 0 } : { x: -1, y: 0 }; |
|
} |
|
return source.y < target.y ? { x: 0, y: 1 } : { x: 0, y: -1 }; |
|
}; |
|
const distance = (a, b) => Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2)); |
|
|
|
|
|
function getPoints({ source, sourcePosition = Position.Bottom, target, targetPosition = Position.Top, center, offset, }) { |
|
const sourceDir = handleDirections[sourcePosition]; |
|
const targetDir = handleDirections[targetPosition]; |
|
const sourceGapped = { x: source.x + sourceDir.x * offset, y: source.y + sourceDir.y * offset }; |
|
const targetGapped = { x: target.x + targetDir.x * offset, y: target.y + targetDir.y * offset }; |
|
const dir = getDirection({ |
|
source: sourceGapped, |
|
sourcePosition, |
|
target: targetGapped, |
|
}); |
|
const dirAccessor = dir.x !== 0 ? 'x' : 'y'; |
|
const currDir = dir[dirAccessor]; |
|
let points = []; |
|
let centerX, centerY; |
|
const sourceGapOffset = { x: 0, y: 0 }; |
|
const targetGapOffset = { x: 0, y: 0 }; |
|
const [defaultCenterX, defaultCenterY, defaultOffsetX, defaultOffsetY] = getEdgeCenter({ |
|
sourceX: source.x, |
|
sourceY: source.y, |
|
targetX: target.x, |
|
targetY: target.y, |
|
}); |
|
|
|
if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) { |
|
centerX = center.x ?? defaultCenterX; |
|
centerY = center.y ?? defaultCenterY; |
|
|
|
|
|
|
|
const verticalSplit = [ |
|
{ x: centerX, y: sourceGapped.y }, |
|
{ x: centerX, y: targetGapped.y }, |
|
]; |
|
|
|
|
|
|
|
const horizontalSplit = [ |
|
{ x: sourceGapped.x, y: centerY }, |
|
{ x: targetGapped.x, y: centerY }, |
|
]; |
|
if (sourceDir[dirAccessor] === currDir) { |
|
points = dirAccessor === 'x' ? verticalSplit : horizontalSplit; |
|
} |
|
else { |
|
points = dirAccessor === 'x' ? horizontalSplit : verticalSplit; |
|
} |
|
} |
|
else { |
|
|
|
const sourceTarget = [{ x: sourceGapped.x, y: targetGapped.y }]; |
|
const targetSource = [{ x: targetGapped.x, y: sourceGapped.y }]; |
|
|
|
if (dirAccessor === 'x') { |
|
points = sourceDir.x === currDir ? targetSource : sourceTarget; |
|
} |
|
else { |
|
points = sourceDir.y === currDir ? sourceTarget : targetSource; |
|
} |
|
if (sourcePosition === targetPosition) { |
|
const diff = Math.abs(source[dirAccessor] - target[dirAccessor]); |
|
|
|
if (diff <= offset) { |
|
const gapOffset = Math.min(offset - 1, offset - diff); |
|
if (sourceDir[dirAccessor] === currDir) { |
|
sourceGapOffset[dirAccessor] = (sourceGapped[dirAccessor] > source[dirAccessor] ? -1 : 1) * gapOffset; |
|
} |
|
else { |
|
targetGapOffset[dirAccessor] = (targetGapped[dirAccessor] > target[dirAccessor] ? -1 : 1) * gapOffset; |
|
} |
|
} |
|
} |
|
|
|
if (sourcePosition !== targetPosition) { |
|
const dirAccessorOpposite = dirAccessor === 'x' ? 'y' : 'x'; |
|
const isSameDir = sourceDir[dirAccessor] === targetDir[dirAccessorOpposite]; |
|
const sourceGtTargetOppo = sourceGapped[dirAccessorOpposite] > targetGapped[dirAccessorOpposite]; |
|
const sourceLtTargetOppo = sourceGapped[dirAccessorOpposite] < targetGapped[dirAccessorOpposite]; |
|
const flipSourceTarget = (sourceDir[dirAccessor] === 1 && ((!isSameDir && sourceGtTargetOppo) || (isSameDir && sourceLtTargetOppo))) || |
|
(sourceDir[dirAccessor] !== 1 && ((!isSameDir && sourceLtTargetOppo) || (isSameDir && sourceGtTargetOppo))); |
|
if (flipSourceTarget) { |
|
points = dirAccessor === 'x' ? sourceTarget : targetSource; |
|
} |
|
} |
|
const sourceGapPoint = { x: sourceGapped.x + sourceGapOffset.x, y: sourceGapped.y + sourceGapOffset.y }; |
|
const targetGapPoint = { x: targetGapped.x + targetGapOffset.x, y: targetGapped.y + targetGapOffset.y }; |
|
const maxXDistance = Math.max(Math.abs(sourceGapPoint.x - points[0].x), Math.abs(targetGapPoint.x - points[0].x)); |
|
const maxYDistance = Math.max(Math.abs(sourceGapPoint.y - points[0].y), Math.abs(targetGapPoint.y - points[0].y)); |
|
|
|
if (maxXDistance >= maxYDistance) { |
|
centerX = (sourceGapPoint.x + targetGapPoint.x) / 2; |
|
centerY = points[0].y; |
|
} |
|
else { |
|
centerX = points[0].x; |
|
centerY = (sourceGapPoint.y + targetGapPoint.y) / 2; |
|
} |
|
} |
|
const pathPoints = [ |
|
source, |
|
{ x: sourceGapped.x + sourceGapOffset.x, y: sourceGapped.y + sourceGapOffset.y }, |
|
...points, |
|
{ x: targetGapped.x + targetGapOffset.x, y: targetGapped.y + targetGapOffset.y }, |
|
target, |
|
]; |
|
return [pathPoints, centerX, centerY, defaultOffsetX, defaultOffsetY]; |
|
} |
|
function getBend(a, b, c, size) { |
|
const bendSize = Math.min(distance(a, b) / 2, distance(b, c) / 2, size); |
|
const { x, y } = b; |
|
|
|
if ((a.x === x && x === c.x) || (a.y === y && y === c.y)) { |
|
return `L${x} ${y}`; |
|
} |
|
|
|
if (a.y === y) { |
|
const xDir = a.x < c.x ? -1 : 1; |
|
const yDir = a.y < c.y ? 1 : -1; |
|
return `L ${x + bendSize * xDir},${y}Q ${x},${y} ${x},${y + bendSize * yDir}`; |
|
} |
|
const xDir = a.x < c.x ? 1 : -1; |
|
const yDir = a.y < c.y ? -1 : 1; |
|
return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getSmoothStepPath({ sourceX, sourceY, sourcePosition = Position.Bottom, targetX, targetY, targetPosition = Position.Top, borderRadius = 5, centerX, centerY, offset = 20, }) { |
|
const [points, labelX, labelY, offsetX, offsetY] = getPoints({ |
|
source: { x: sourceX, y: sourceY }, |
|
sourcePosition, |
|
target: { x: targetX, y: targetY }, |
|
targetPosition, |
|
center: { x: centerX, y: centerY }, |
|
offset, |
|
}); |
|
const path = points.reduce((res, p, i) => { |
|
let segment = ''; |
|
if (i > 0 && i < points.length - 1) { |
|
segment = getBend(points[i - 1], p, points[i + 1], borderRadius); |
|
} |
|
else { |
|
segment = `${i === 0 ? 'M' : 'L'}${p.x} ${p.y}`; |
|
} |
|
res += segment; |
|
return res; |
|
}, ''); |
|
return [path, labelX, labelY, offsetX, offsetY]; |
|
} |
|
|
|
function isNodeInitialized(node) { |
|
return (node && |
|
!!(node.internals.handleBounds || node.handles?.length) && |
|
!!(node.measured.width || node.width || node.initialWidth)); |
|
} |
|
function getEdgePosition(params) { |
|
const { sourceNode, targetNode } = params; |
|
if (!isNodeInitialized(sourceNode) || !isNodeInitialized(targetNode)) { |
|
return null; |
|
} |
|
const sourceHandleBounds = sourceNode.internals.handleBounds || toHandleBounds(sourceNode.handles); |
|
const targetHandleBounds = targetNode.internals.handleBounds || toHandleBounds(targetNode.handles); |
|
const sourceHandle = getHandle$1(sourceHandleBounds?.source ?? [], params.sourceHandle); |
|
const targetHandle = getHandle$1( |
|
|
|
params.connectionMode === ConnectionMode.Strict |
|
? targetHandleBounds?.target ?? [] |
|
: (targetHandleBounds?.target ?? []).concat(targetHandleBounds?.source ?? []), params.targetHandle); |
|
if (!sourceHandle || !targetHandle) { |
|
params.onError?.('008', errorMessages['error008'](!sourceHandle ? 'source' : 'target', { |
|
id: params.id, |
|
sourceHandle: params.sourceHandle, |
|
targetHandle: params.targetHandle, |
|
})); |
|
return null; |
|
} |
|
const sourcePosition = sourceHandle?.position || Position.Bottom; |
|
const targetPosition = targetHandle?.position || Position.Top; |
|
const source = getHandlePosition(sourceNode, sourceHandle, sourcePosition); |
|
const target = getHandlePosition(targetNode, targetHandle, targetPosition); |
|
return { |
|
sourceX: source.x, |
|
sourceY: source.y, |
|
targetX: target.x, |
|
targetY: target.y, |
|
sourcePosition, |
|
targetPosition, |
|
}; |
|
} |
|
function toHandleBounds(handles) { |
|
if (!handles) { |
|
return null; |
|
} |
|
const source = []; |
|
const target = []; |
|
for (const handle of handles) { |
|
handle.width = handle.width ?? 1; |
|
handle.height = handle.height ?? 1; |
|
if (handle.type === 'source') { |
|
source.push(handle); |
|
} |
|
else if (handle.type === 'target') { |
|
target.push(handle); |
|
} |
|
} |
|
return { |
|
source, |
|
target, |
|
}; |
|
} |
|
function getHandlePosition(node, handle, fallbackPosition = Position.Left, center = false) { |
|
const x = (handle?.x ?? 0) + node.internals.positionAbsolute.x; |
|
const y = (handle?.y ?? 0) + node.internals.positionAbsolute.y; |
|
const { width, height } = handle ?? getNodeDimensions(node); |
|
if (center) { |
|
return { x: x + width / 2, y: y + height / 2 }; |
|
} |
|
const position = handle?.position ?? fallbackPosition; |
|
switch (position) { |
|
case Position.Top: |
|
return { x: x + width / 2, y }; |
|
case Position.Right: |
|
return { x: x + width, y: y + height / 2 }; |
|
case Position.Bottom: |
|
return { x: x + width / 2, y: y + height }; |
|
case Position.Left: |
|
return { x, y: y + height / 2 }; |
|
} |
|
} |
|
function getHandle$1(bounds, handleId) { |
|
if (!bounds) { |
|
return null; |
|
} |
|
|
|
return (!handleId ? bounds[0] : bounds.find((d) => d.id === handleId)) || null; |
|
} |
|
|
|
function getMarkerId(marker, id) { |
|
if (!marker) { |
|
return ''; |
|
} |
|
if (typeof marker === 'string') { |
|
return marker; |
|
} |
|
const idPrefix = id ? `${id}__` : ''; |
|
return `${idPrefix}${Object.keys(marker) |
|
.sort() |
|
.map((key) => `${key}=${marker[key]}`) |
|
.join('&')}`; |
|
} |
|
function createMarkerIds(edges, { id, defaultColor, defaultMarkerStart, defaultMarkerEnd, }) { |
|
const ids = new Set(); |
|
return edges |
|
.reduce((markers, edge) => { |
|
[edge.markerStart || defaultMarkerStart, edge.markerEnd || defaultMarkerEnd].forEach((marker) => { |
|
if (marker && typeof marker === 'object') { |
|
const markerId = getMarkerId(marker, id); |
|
if (!ids.has(markerId)) { |
|
markers.push({ id: markerId, color: marker.color || defaultColor, ...marker }); |
|
ids.add(markerId); |
|
} |
|
} |
|
}); |
|
return markers; |
|
}, []) |
|
.sort((a, b) => a.id.localeCompare(b.id)); |
|
} |
|
|
|
function getNodeToolbarTransform(nodeRect, viewport, position, offset, align) { |
|
let alignmentOffset = 0.5; |
|
if (align === 'start') { |
|
alignmentOffset = 0; |
|
} |
|
else if (align === 'end') { |
|
alignmentOffset = 1; |
|
} |
|
|
|
|
|
let pos = [ |
|
(nodeRect.x + nodeRect.width * alignmentOffset) * viewport.zoom + viewport.x, |
|
nodeRect.y * viewport.zoom + viewport.y - offset, |
|
]; |
|
|
|
let shift = [-100 * alignmentOffset, -100]; |
|
switch (position) { |
|
case Position.Right: |
|
pos = [ |
|
(nodeRect.x + nodeRect.width) * viewport.zoom + viewport.x + offset, |
|
(nodeRect.y + nodeRect.height * alignmentOffset) * viewport.zoom + viewport.y, |
|
]; |
|
shift = [0, -100 * alignmentOffset]; |
|
break; |
|
case Position.Bottom: |
|
pos[1] = (nodeRect.y + nodeRect.height) * viewport.zoom + viewport.y + offset; |
|
shift[1] = 0; |
|
break; |
|
case Position.Left: |
|
pos = [ |
|
nodeRect.x * viewport.zoom + viewport.x - offset, |
|
(nodeRect.y + nodeRect.height * alignmentOffset) * viewport.zoom + viewport.y, |
|
]; |
|
shift = [-100, -100 * alignmentOffset]; |
|
break; |
|
} |
|
return `translate(${pos[0]}px, ${pos[1]}px) translate(${shift[0]}%, ${shift[1]}%)`; |
|
} |
|
|
|
const defaultOptions = { |
|
nodeOrigin: [0, 0], |
|
nodeExtent: infiniteExtent, |
|
elevateNodesOnSelect: true, |
|
defaults: {}, |
|
}; |
|
const adoptUserNodesDefaultOptions = { |
|
...defaultOptions, |
|
checkEquality: true, |
|
}; |
|
function mergeObjects(base, incoming) { |
|
const result = { ...base }; |
|
for (const key in incoming) { |
|
if (incoming[key] !== undefined) { |
|
|
|
result[key] = incoming[key]; |
|
} |
|
} |
|
return result; |
|
} |
|
function updateAbsolutePositions(nodeLookup, parentLookup, options) { |
|
const _options = mergeObjects(defaultOptions, options); |
|
for (const node of nodeLookup.values()) { |
|
if (!node.parentId) { |
|
continue; |
|
} |
|
updateChildNode(node, nodeLookup, parentLookup, _options); |
|
} |
|
} |
|
function adoptUserNodes(nodes, nodeLookup, parentLookup, options) { |
|
const _options = mergeObjects(adoptUserNodesDefaultOptions, options); |
|
const tmpLookup = new Map(nodeLookup); |
|
const selectedNodeZ = _options?.elevateNodesOnSelect ? 1000 : 0; |
|
nodeLookup.clear(); |
|
parentLookup.clear(); |
|
for (const userNode of nodes) { |
|
let internalNode = tmpLookup.get(userNode.id); |
|
if (_options.checkEquality && userNode === internalNode?.internals.userNode) { |
|
nodeLookup.set(userNode.id, internalNode); |
|
} |
|
else { |
|
const positionWithOrigin = getNodePositionWithOrigin(userNode, _options.nodeOrigin); |
|
const extent = isCoordinateExtent(userNode.extent) ? userNode.extent : _options.nodeExtent; |
|
const clampedPosition = clampPosition(positionWithOrigin, extent, getNodeDimensions(userNode)); |
|
internalNode = { |
|
..._options.defaults, |
|
...userNode, |
|
measured: { |
|
width: userNode.measured?.width, |
|
height: userNode.measured?.height, |
|
}, |
|
internals: { |
|
positionAbsolute: clampedPosition, |
|
|
|
handleBounds: !userNode.measured ? undefined : internalNode?.internals.handleBounds, |
|
z: calculateZ(userNode, selectedNodeZ), |
|
userNode, |
|
}, |
|
}; |
|
nodeLookup.set(userNode.id, internalNode); |
|
} |
|
if (userNode.parentId) { |
|
updateChildNode(internalNode, nodeLookup, parentLookup, options); |
|
} |
|
} |
|
} |
|
function updateParentLookup(node, parentLookup) { |
|
if (!node.parentId) { |
|
return; |
|
} |
|
const childNodes = parentLookup.get(node.parentId); |
|
if (childNodes) { |
|
childNodes.set(node.id, node); |
|
} |
|
else { |
|
parentLookup.set(node.parentId, new Map([[node.id, node]])); |
|
} |
|
} |
|
|
|
|
|
|
|
function updateChildNode(node, nodeLookup, parentLookup, options) { |
|
const { elevateNodesOnSelect, nodeOrigin, nodeExtent } = mergeObjects(defaultOptions, options); |
|
const parentId = node.parentId; |
|
const parentNode = nodeLookup.get(parentId); |
|
if (!parentNode) { |
|
console.warn(`Parent node ${parentId} not found. Please make sure that parent nodes are in front of their child nodes in the nodes array.`); |
|
return; |
|
} |
|
updateParentLookup(node, parentLookup); |
|
const selectedNodeZ = elevateNodesOnSelect ? 1000 : 0; |
|
const { x, y, z } = calculateChildXYZ(node, parentNode, nodeOrigin, nodeExtent, selectedNodeZ); |
|
const { positionAbsolute } = node.internals; |
|
const positionChanged = x !== positionAbsolute.x || y !== positionAbsolute.y; |
|
if (positionChanged || z !== node.internals.z) { |
|
node.internals = { |
|
...node.internals, |
|
positionAbsolute: positionChanged ? { x, y } : positionAbsolute, |
|
z, |
|
}; |
|
} |
|
} |
|
function calculateZ(node, selectedNodeZ) { |
|
return (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0); |
|
} |
|
function calculateChildXYZ(childNode, parentNode, nodeOrigin, nodeExtent, selectedNodeZ) { |
|
const { x: parentX, y: parentY } = parentNode.internals.positionAbsolute; |
|
const childDimensions = getNodeDimensions(childNode); |
|
const positionWithOrigin = getNodePositionWithOrigin(childNode, nodeOrigin); |
|
const clampedPosition = isCoordinateExtent(childNode.extent) |
|
? clampPosition(positionWithOrigin, childNode.extent, childDimensions) |
|
: positionWithOrigin; |
|
let absolutePosition = clampPosition({ x: parentX + clampedPosition.x, y: parentY + clampedPosition.y }, nodeExtent, childDimensions); |
|
if (childNode.extent === 'parent') { |
|
absolutePosition = clampPositionToParent(absolutePosition, childDimensions, parentNode); |
|
} |
|
const childZ = calculateZ(childNode, selectedNodeZ); |
|
const parentZ = parentNode.internals.z ?? 0; |
|
return { |
|
x: absolutePosition.x, |
|
y: absolutePosition.y, |
|
z: parentZ > childZ ? parentZ : childZ, |
|
}; |
|
} |
|
function handleExpandParent(children, nodeLookup, parentLookup, nodeOrigin = [0, 0]) { |
|
const changes = []; |
|
const parentExpansions = new Map(); |
|
|
|
for (const child of children) { |
|
const parent = nodeLookup.get(child.parentId); |
|
if (!parent) { |
|
continue; |
|
} |
|
const parentRect = parentExpansions.get(child.parentId)?.expandedRect ?? nodeToRect(parent); |
|
const expandedRect = getBoundsOfRects(parentRect, child.rect); |
|
parentExpansions.set(child.parentId, { expandedRect, parent }); |
|
} |
|
if (parentExpansions.size > 0) { |
|
parentExpansions.forEach(({ expandedRect, parent }, parentId) => { |
|
|
|
const positionAbsolute = parent.internals.positionAbsolute; |
|
const dimensions = getNodeDimensions(parent); |
|
const origin = parent.origin ?? nodeOrigin; |
|
|
|
const xChange = expandedRect.x < positionAbsolute.x ? Math.round(Math.abs(positionAbsolute.x - expandedRect.x)) : 0; |
|
const yChange = expandedRect.y < positionAbsolute.y ? Math.round(Math.abs(positionAbsolute.y - expandedRect.y)) : 0; |
|
const newWidth = Math.max(dimensions.width, Math.round(expandedRect.width)); |
|
const newHeight = Math.max(dimensions.height, Math.round(expandedRect.height)); |
|
const widthChange = (newWidth - dimensions.width) * origin[0]; |
|
const heightChange = (newHeight - dimensions.height) * origin[1]; |
|
|
|
if (xChange > 0 || yChange > 0 || widthChange || heightChange) { |
|
changes.push({ |
|
id: parentId, |
|
type: 'position', |
|
position: { |
|
x: parent.position.x - xChange + widthChange, |
|
y: parent.position.y - yChange + heightChange, |
|
}, |
|
}); |
|
|
|
|
|
parentLookup.get(parentId)?.forEach((childNode) => { |
|
if (!children.some((child) => child.id === childNode.id)) { |
|
changes.push({ |
|
id: childNode.id, |
|
type: 'position', |
|
position: { |
|
x: childNode.position.x + xChange, |
|
y: childNode.position.y + yChange, |
|
}, |
|
}); |
|
} |
|
}); |
|
} |
|
|
|
if (dimensions.width < expandedRect.width || dimensions.height < expandedRect.height || xChange || yChange) { |
|
changes.push({ |
|
id: parentId, |
|
type: 'dimensions', |
|
setAttributes: true, |
|
dimensions: { |
|
width: newWidth + (xChange ? origin[0] * xChange - widthChange : 0), |
|
height: newHeight + (yChange ? origin[1] * yChange - heightChange : 0), |
|
}, |
|
}); |
|
} |
|
}); |
|
} |
|
return changes; |
|
} |
|
function updateNodeInternals(updates, nodeLookup, parentLookup, domNode, nodeOrigin, nodeExtent) { |
|
const viewportNode = domNode?.querySelector('.xyflow__viewport'); |
|
let updatedInternals = false; |
|
if (!viewportNode) { |
|
return { changes: [], updatedInternals }; |
|
} |
|
const changes = []; |
|
const style = window.getComputedStyle(viewportNode); |
|
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform); |
|
|
|
const parentExpandChildren = []; |
|
for (const update of updates.values()) { |
|
const node = nodeLookup.get(update.id); |
|
if (!node) { |
|
continue; |
|
} |
|
if (node.hidden) { |
|
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)); |
|
if (doUpdate) { |
|
const nodeBounds = update.nodeElement.getBoundingClientRect(); |
|
const extent = isCoordinateExtent(node.extent) ? node.extent : nodeExtent; |
|
let { positionAbsolute } = node.internals; |
|
if (node.parentId && node.extent === 'parent') { |
|
positionAbsolute = clampPositionToParent(positionAbsolute, dimensions, nodeLookup.get(node.parentId)); |
|
} |
|
else if (extent) { |
|
positionAbsolute = clampPosition(positionAbsolute, extent, dimensions); |
|
} |
|
node.measured = dimensions; |
|
node.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; |
|
if (dimensionChanged) { |
|
changes.push({ |
|
id: node.id, |
|
type: 'dimensions', |
|
dimensions, |
|
}); |
|
if (node.expandParent && node.parentId) { |
|
parentExpandChildren.push({ |
|
id: node.id, |
|
parentId: node.parentId, |
|
rect: nodeToRect(node, nodeOrigin), |
|
}); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
if (parentExpandChildren.length > 0) { |
|
const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin); |
|
changes.push(...parentExpandChanges); |
|
} |
|
return { changes, updatedInternals }; |
|
} |
|
async function panBy({ delta, panZoom, transform, translateExtent, width, height, }) { |
|
if (!panZoom || (!delta.x && !delta.y)) { |
|
return Promise.resolve(false); |
|
} |
|
const nextViewport = await panZoom.setViewportConstrained({ |
|
x: transform[0] + delta.x, |
|
y: transform[1] + delta.y, |
|
zoom: transform[2], |
|
}, [ |
|
[0, 0], |
|
[width, height], |
|
], translateExtent); |
|
const transformChanged = !!nextViewport && |
|
(nextViewport.x !== transform[0] || nextViewport.y !== transform[1] || nextViewport.k !== transform[2]); |
|
return Promise.resolve(transformChanged); |
|
} |
|
function updateConnectionLookup(connectionLookup, edgeLookup, edges) { |
|
connectionLookup.clear(); |
|
edgeLookup.clear(); |
|
for (const edge of edges) { |
|
const { source, target, sourceHandle = null, targetHandle = null } = edge; |
|
const sourceKey = `${source}-source-${sourceHandle}`; |
|
const targetKey = `${target}-target-${targetHandle}`; |
|
const prevSource = connectionLookup.get(sourceKey) || new Map(); |
|
const prevTarget = connectionLookup.get(targetKey) || new Map(); |
|
const connection = { edgeId: edge.id, source, target, sourceHandle, targetHandle }; |
|
edgeLookup.set(edge.id, edge); |
|
connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection)); |
|
connectionLookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection)); |
|
} |
|
} |
|
|
|
function shallowNodeData(a, b) { |
|
if (a === null || b === null) { |
|
return false; |
|
} |
|
const _a = Array.isArray(a) ? a : [a]; |
|
const _b = Array.isArray(b) ? b : [b]; |
|
if (_a.length !== _b.length) { |
|
return false; |
|
} |
|
for (let i = 0; i < _a.length; i++) { |
|
if (_a[i].id !== _b[i].id || _a[i].type !== _b[i].type || !Object.is(_a[i].data, _b[i].data)) { |
|
return false; |
|
} |
|
} |
|
return true; |
|
} |
|
|
|
function isParentSelected(node, nodeLookup) { |
|
if (!node.parentId) { |
|
return false; |
|
} |
|
const parentNode = nodeLookup.get(node.parentId); |
|
if (!parentNode) { |
|
return false; |
|
} |
|
if (parentNode.selected) { |
|
return true; |
|
} |
|
return isParentSelected(parentNode, nodeLookup); |
|
} |
|
function hasSelector(target, selector, domNode) { |
|
let current = target; |
|
do { |
|
if (current?.matches(selector)) |
|
return true; |
|
if (current === domNode) |
|
return false; |
|
current = current.parentElement; |
|
} while (current); |
|
return false; |
|
} |
|
|
|
function getDragItems(nodeLookup, nodesDraggable, mousePos, nodeId) { |
|
const dragItems = new Map(); |
|
for (const [id, node] of nodeLookup) { |
|
if ((node.selected || node.id === nodeId) && |
|
(!node.parentId || !isParentSelected(node, nodeLookup)) && |
|
(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'))) { |
|
const internalNode = nodeLookup.get(id); |
|
if (internalNode) { |
|
dragItems.set(id, { |
|
id, |
|
position: internalNode.position || { x: 0, y: 0 }, |
|
distance: { |
|
x: mousePos.x - internalNode.internals.positionAbsolute.x, |
|
y: mousePos.y - internalNode.internals.positionAbsolute.y, |
|
}, |
|
extent: internalNode.extent, |
|
parentId: internalNode.parentId, |
|
origin: internalNode.origin, |
|
expandParent: internalNode.expandParent, |
|
internals: { |
|
positionAbsolute: internalNode.internals.positionAbsolute || { x: 0, y: 0 }, |
|
}, |
|
measured: { |
|
width: internalNode.measured.width ?? 0, |
|
height: internalNode.measured.height ?? 0, |
|
}, |
|
}); |
|
} |
|
} |
|
} |
|
return dragItems; |
|
} |
|
|
|
|
|
|
|
function getEventHandlerParams({ nodeId, dragItems, nodeLookup, dragging = true, }) { |
|
const nodesFromDragItems = []; |
|
for (const [id, dragItem] of dragItems) { |
|
const node = nodeLookup.get(id)?.internals.userNode; |
|
if (node) { |
|
nodesFromDragItems.push({ |
|
...node, |
|
position: dragItem.position, |
|
dragging, |
|
}); |
|
} |
|
} |
|
if (!nodeId) { |
|
return [nodesFromDragItems[0], nodesFromDragItems]; |
|
} |
|
const node = nodeLookup.get(nodeId).internals.userNode; |
|
return [ |
|
{ |
|
...node, |
|
position: dragItems.get(nodeId)?.position || node.position, |
|
dragging, |
|
}, |
|
nodesFromDragItems, |
|
]; |
|
} |
|
|
|
|
|
function XYDrag({ onNodeMouseDown, getStoreItems, onDragStart, onDrag, onDragStop, }) { |
|
let lastPos = { x: null, y: null }; |
|
let autoPanId = 0; |
|
let dragItems = new Map(); |
|
let autoPanStarted = false; |
|
let mousePosition = { x: 0, y: 0 }; |
|
let containerBounds = null; |
|
let dragStarted = false; |
|
let d3Selection = null; |
|
let abortDrag = false; |
|
|
|
function update({ noDragClassName, handleSelector, domNode, isSelectable, nodeId, nodeClickDistance = 0, }) { |
|
d3Selection = select(domNode); |
|
function updateNodes({ x, y }, dragEvent) { |
|
const { nodeLookup, nodeExtent, snapGrid, snapToGrid, nodeOrigin, onNodeDrag, onSelectionDrag, onError, updateNodePositions, } = getStoreItems(); |
|
lastPos = { x, y }; |
|
let hasChange = false; |
|
let nodesBox = { x: 0, y: 0, x2: 0, y2: 0 }; |
|
if (dragItems.size > 1 && nodeExtent) { |
|
const rect = getInternalNodesBounds(dragItems); |
|
nodesBox = rectToBox(rect); |
|
} |
|
for (const [id, dragItem] of dragItems) { |
|
let nextPosition = { x: x - dragItem.distance.x, y: y - dragItem.distance.y }; |
|
if (snapToGrid) { |
|
nextPosition = snapPosition(nextPosition, snapGrid); |
|
} |
|
|
|
|
|
let adjustedNodeExtent = [ |
|
[nodeExtent[0][0], nodeExtent[0][1]], |
|
[nodeExtent[1][0], nodeExtent[1][1]], |
|
]; |
|
if (dragItems.size > 1 && nodeExtent && !dragItem.extent) { |
|
const { positionAbsolute } = dragItem.internals; |
|
const x1 = positionAbsolute.x - nodesBox.x + nodeExtent[0][0]; |
|
const x2 = positionAbsolute.x + dragItem.measured.width - nodesBox.x2 + nodeExtent[1][0]; |
|
const y1 = positionAbsolute.y - nodesBox.y + nodeExtent[0][1]; |
|
const y2 = positionAbsolute.y + dragItem.measured.height - nodesBox.y2 + nodeExtent[1][1]; |
|
adjustedNodeExtent = [ |
|
[x1, y1], |
|
[x2, y2], |
|
]; |
|
} |
|
const { position, positionAbsolute } = calculateNodePosition({ |
|
nodeId: id, |
|
nextPosition, |
|
nodeLookup, |
|
nodeExtent: adjustedNodeExtent, |
|
nodeOrigin, |
|
onError, |
|
}); |
|
|
|
hasChange = hasChange || dragItem.position.x !== position.x || dragItem.position.y !== position.y; |
|
dragItem.position = position; |
|
dragItem.internals.positionAbsolute = positionAbsolute; |
|
} |
|
if (!hasChange) { |
|
return; |
|
} |
|
updateNodePositions(dragItems, true); |
|
if (dragEvent && (onDrag || onNodeDrag || (!nodeId && onSelectionDrag))) { |
|
const [currentNode, currentNodes] = getEventHandlerParams({ |
|
nodeId, |
|
dragItems, |
|
nodeLookup, |
|
}); |
|
onDrag?.(dragEvent, dragItems, currentNode, currentNodes); |
|
onNodeDrag?.(dragEvent, currentNode, currentNodes); |
|
if (!nodeId) { |
|
onSelectionDrag?.(dragEvent, currentNodes); |
|
} |
|
} |
|
} |
|
async function autoPan() { |
|
if (!containerBounds) { |
|
return; |
|
} |
|
const { transform, panBy, autoPanSpeed } = getStoreItems(); |
|
const [xMovement, yMovement] = calcAutoPan(mousePosition, containerBounds, autoPanSpeed); |
|
if (xMovement !== 0 || yMovement !== 0) { |
|
lastPos.x = (lastPos.x ?? 0) - xMovement / transform[2]; |
|
lastPos.y = (lastPos.y ?? 0) - yMovement / transform[2]; |
|
if (await panBy({ x: xMovement, y: yMovement })) { |
|
updateNodes(lastPos, null); |
|
} |
|
} |
|
autoPanId = requestAnimationFrame(autoPan); |
|
} |
|
function startDrag(event) { |
|
const { nodeLookup, multiSelectionActive, nodesDraggable, transform, snapGrid, snapToGrid, selectNodesOnDrag, onNodeDragStart, onSelectionDragStart, unselectNodesAndEdges, } = getStoreItems(); |
|
dragStarted = true; |
|
if ((!selectNodesOnDrag || !isSelectable) && !multiSelectionActive && nodeId) { |
|
if (!nodeLookup.get(nodeId)?.selected) { |
|
|
|
unselectNodesAndEdges(); |
|
} |
|
} |
|
if (isSelectable && selectNodesOnDrag && nodeId) { |
|
onNodeMouseDown?.(nodeId); |
|
} |
|
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); |
|
lastPos = pointerPos; |
|
dragItems = getDragItems(nodeLookup, nodesDraggable, pointerPos, nodeId); |
|
if (dragItems.size > 0 && (onDragStart || onNodeDragStart || (!nodeId && onSelectionDragStart))) { |
|
const [currentNode, currentNodes] = getEventHandlerParams({ |
|
nodeId, |
|
dragItems, |
|
nodeLookup, |
|
}); |
|
onDragStart?.(event.sourceEvent, dragItems, currentNode, currentNodes); |
|
onNodeDragStart?.(event.sourceEvent, currentNode, currentNodes); |
|
if (!nodeId) { |
|
onSelectionDragStart?.(event.sourceEvent, currentNodes); |
|
} |
|
} |
|
} |
|
const d3DragInstance = drag() |
|
.clickDistance(nodeClickDistance) |
|
.on('start', (event) => { |
|
const { domNode, nodeDragThreshold, transform, snapGrid, snapToGrid } = getStoreItems(); |
|
abortDrag = false; |
|
if (nodeDragThreshold === 0) { |
|
startDrag(event); |
|
} |
|
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); |
|
lastPos = pointerPos; |
|
containerBounds = domNode?.getBoundingClientRect() || null; |
|
mousePosition = getEventPosition(event.sourceEvent, containerBounds); |
|
}) |
|
.on('drag', (event) => { |
|
const { autoPanOnNodeDrag, transform, snapGrid, snapToGrid, nodeDragThreshold } = getStoreItems(); |
|
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); |
|
if (event.sourceEvent.type === 'touchmove' && event.sourceEvent.touches.length > 1) { |
|
abortDrag = true; |
|
} |
|
if (abortDrag) { |
|
return; |
|
} |
|
if (!autoPanStarted && autoPanOnNodeDrag && dragStarted) { |
|
autoPanStarted = true; |
|
autoPan(); |
|
} |
|
if (!dragStarted) { |
|
const x = pointerPos.xSnapped - (lastPos.x ?? 0); |
|
const y = pointerPos.ySnapped - (lastPos.y ?? 0); |
|
const distance = Math.sqrt(x * x + y * y); |
|
if (distance > nodeDragThreshold) { |
|
startDrag(event); |
|
} |
|
} |
|
|
|
if ((lastPos.x !== pointerPos.xSnapped || lastPos.y !== pointerPos.ySnapped) && dragItems && dragStarted) { |
|
|
|
mousePosition = getEventPosition(event.sourceEvent, containerBounds); |
|
updateNodes(pointerPos, event.sourceEvent); |
|
} |
|
}) |
|
.on('end', (event) => { |
|
if (!dragStarted || abortDrag) { |
|
return; |
|
} |
|
autoPanStarted = false; |
|
dragStarted = false; |
|
cancelAnimationFrame(autoPanId); |
|
if (dragItems.size > 0) { |
|
const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems(); |
|
updateNodePositions(dragItems, false); |
|
if (onDragStop || onNodeDragStop || (!nodeId && onSelectionDragStop)) { |
|
const [currentNode, currentNodes] = getEventHandlerParams({ |
|
nodeId, |
|
dragItems, |
|
nodeLookup, |
|
dragging: false, |
|
}); |
|
onDragStop?.(event.sourceEvent, dragItems, currentNode, currentNodes); |
|
onNodeDragStop?.(event.sourceEvent, currentNode, currentNodes); |
|
if (!nodeId) { |
|
onSelectionDragStop?.(event.sourceEvent, currentNodes); |
|
} |
|
} |
|
} |
|
}) |
|
.filter((event) => { |
|
const target = event.target; |
|
const isDraggable = !event.button && |
|
(!noDragClassName || !hasSelector(target, `.${noDragClassName}`, domNode)) && |
|
(!handleSelector || hasSelector(target, handleSelector, domNode)); |
|
return isDraggable; |
|
}); |
|
d3Selection.call(d3DragInstance); |
|
} |
|
function destroy() { |
|
d3Selection?.on('.drag', null); |
|
} |
|
return { |
|
update, |
|
destroy, |
|
}; |
|
} |
|
|
|
function getNodesWithinDistance(position, nodeLookup, distance) { |
|
const nodes = []; |
|
const rect = { |
|
x: position.x - distance, |
|
y: position.y - distance, |
|
width: distance * 2, |
|
height: distance * 2, |
|
}; |
|
for (const node of nodeLookup.values()) { |
|
if (getOverlappingArea(rect, nodeToRect(node)) > 0) { |
|
nodes.push(node); |
|
} |
|
} |
|
return nodes; |
|
} |
|
|
|
|
|
const ADDITIONAL_DISTANCE = 250; |
|
function getClosestHandle(position, connectionRadius, nodeLookup, fromHandle) { |
|
let closestHandles = []; |
|
let minDistance = Infinity; |
|
const closeNodes = getNodesWithinDistance(position, nodeLookup, connectionRadius + ADDITIONAL_DISTANCE); |
|
for (const node of closeNodes) { |
|
const allHandles = [...(node.internals.handleBounds?.source ?? []), ...(node.internals.handleBounds?.target ?? [])]; |
|
for (const handle of allHandles) { |
|
|
|
if (fromHandle.nodeId === handle.nodeId && fromHandle.type === handle.type && fromHandle.id === handle.id) { |
|
continue; |
|
} |
|
|
|
const { x, y } = getHandlePosition(node, handle, handle.position, true); |
|
const distance = Math.sqrt(Math.pow(x - position.x, 2) + Math.pow(y - position.y, 2)); |
|
if (distance > connectionRadius) { |
|
continue; |
|
} |
|
if (distance < minDistance) { |
|
closestHandles = [{ ...handle, x, y }]; |
|
minDistance = distance; |
|
} |
|
else if (distance === minDistance) { |
|
|
|
closestHandles.push({ ...handle, x, y }); |
|
} |
|
} |
|
} |
|
if (!closestHandles.length) { |
|
return null; |
|
} |
|
|
|
if (closestHandles.length > 1) { |
|
const oppositeHandleType = fromHandle.type === 'source' ? 'target' : 'source'; |
|
return closestHandles.find((handle) => handle.type === oppositeHandleType) ?? closestHandles[0]; |
|
} |
|
return closestHandles[0]; |
|
} |
|
function getHandle(nodeId, handleType, handleId, nodeLookup, connectionMode, withAbsolutePosition = false) { |
|
const node = nodeLookup.get(nodeId); |
|
if (!node) { |
|
return null; |
|
} |
|
const handles = connectionMode === 'strict' |
|
? node.internals.handleBounds?.[handleType] |
|
: [...(node.internals.handleBounds?.source ?? []), ...(node.internals.handleBounds?.target ?? [])]; |
|
const handle = (handleId ? handles?.find((h) => h.id === handleId) : handles?.[0]) ?? null; |
|
return handle && withAbsolutePosition |
|
? { ...handle, ...getHandlePosition(node, handle, handle.position, true) } |
|
: handle; |
|
} |
|
function getHandleType(edgeUpdaterType, handleDomNode) { |
|
if (edgeUpdaterType) { |
|
return edgeUpdaterType; |
|
} |
|
else if (handleDomNode?.classList.contains('target')) { |
|
return 'target'; |
|
} |
|
else if (handleDomNode?.classList.contains('source')) { |
|
return 'source'; |
|
} |
|
return null; |
|
} |
|
function isConnectionValid(isInsideConnectionRadius, isHandleValid) { |
|
let isValid = null; |
|
if (isHandleValid) { |
|
isValid = true; |
|
} |
|
else if (isInsideConnectionRadius && !isHandleValid) { |
|
isValid = false; |
|
} |
|
return isValid; |
|
} |
|
|
|
const alwaysValid = () => true; |
|
function onPointerDown(event, { connectionMode, connectionRadius, handleId, nodeId, edgeUpdaterType, isTarget, domNode, nodeLookup, lib, autoPanOnConnect, flowId, panBy, cancelConnection, onConnectStart, onConnect, onConnectEnd, isValidConnection = alwaysValid, onReconnectEnd, updateConnection, getTransform, getFromHandle, autoPanSpeed, }) { |
|
|
|
const doc = getHostForElement(event.target); |
|
let autoPanId = 0; |
|
let closestHandle; |
|
const { x, y } = getEventPosition(event); |
|
const clickedHandle = doc?.elementFromPoint(x, y); |
|
const handleType = getHandleType(edgeUpdaterType, clickedHandle); |
|
const containerBounds = domNode?.getBoundingClientRect(); |
|
if (!containerBounds || !handleType) { |
|
return; |
|
} |
|
const fromHandleInternal = getHandle(nodeId, handleType, handleId, nodeLookup, connectionMode); |
|
if (!fromHandleInternal) { |
|
return; |
|
} |
|
let position = getEventPosition(event, containerBounds); |
|
let autoPanStarted = false; |
|
let connection = null; |
|
let isValid = false; |
|
let handleDomNode = null; |
|
|
|
function autoPan() { |
|
if (!autoPanOnConnect || !containerBounds) { |
|
return; |
|
} |
|
const [x, y] = calcAutoPan(position, containerBounds, autoPanSpeed); |
|
panBy({ x, y }); |
|
autoPanId = requestAnimationFrame(autoPan); |
|
} |
|
|
|
const fromHandle = { |
|
...fromHandleInternal, |
|
nodeId, |
|
type: handleType, |
|
position: fromHandleInternal.position, |
|
}; |
|
const fromNodeInternal = nodeLookup.get(nodeId); |
|
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true); |
|
const newConnection = { |
|
inProgress: true, |
|
isValid: null, |
|
from, |
|
fromHandle, |
|
fromPosition: fromHandle.position, |
|
fromNode: fromNodeInternal, |
|
to: position, |
|
toHandle: null, |
|
toPosition: oppositePosition[fromHandle.position], |
|
toNode: null, |
|
}; |
|
updateConnection(newConnection); |
|
let previousConnection = newConnection; |
|
onConnectStart?.(event, { nodeId, handleId, handleType }); |
|
function onPointerMove(event) { |
|
if (!getFromHandle() || !fromHandle) { |
|
onPointerUp(event); |
|
return; |
|
} |
|
const transform = getTransform(); |
|
position = getEventPosition(event, containerBounds); |
|
closestHandle = getClosestHandle(pointToRendererPoint(position, transform, false, [1, 1]), connectionRadius, nodeLookup, fromHandle); |
|
if (!autoPanStarted) { |
|
autoPan(); |
|
autoPanStarted = true; |
|
} |
|
const result = isValidHandle(event, { |
|
handle: closestHandle, |
|
connectionMode, |
|
fromNodeId: nodeId, |
|
fromHandleId: handleId, |
|
fromType: isTarget ? 'target' : 'source', |
|
isValidConnection, |
|
doc, |
|
lib, |
|
flowId, |
|
nodeLookup, |
|
}); |
|
handleDomNode = result.handleDomNode; |
|
connection = result.connection; |
|
isValid = isConnectionValid(!!closestHandle, result.isValid); |
|
const newConnection = { |
|
|
|
...previousConnection, |
|
isValid, |
|
to: closestHandle && isValid |
|
? rendererPointToPoint({ x: closestHandle.x, y: closestHandle.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, |
|
}; |
|
|
|
|
|
if (isValid && |
|
closestHandle && |
|
previousConnection.toHandle && |
|
newConnection.toHandle && |
|
previousConnection.toHandle.type === newConnection.toHandle.type && |
|
previousConnection.toHandle.nodeId === newConnection.toHandle.nodeId && |
|
previousConnection.toHandle.id === newConnection.toHandle.id && |
|
previousConnection.to.x === newConnection.to.x && |
|
previousConnection.to.y === newConnection.to.y) { |
|
return; |
|
} |
|
updateConnection(newConnection); |
|
previousConnection = newConnection; |
|
} |
|
function onPointerUp(event) { |
|
if ((closestHandle || handleDomNode) && connection && isValid) { |
|
onConnect?.(connection); |
|
} |
|
|
|
|
|
|
|
const { inProgress, ...connectionState } = previousConnection; |
|
const finalConnectionState = { |
|
...connectionState, |
|
toPosition: previousConnection.toHandle ? previousConnection.toPosition : null, |
|
}; |
|
onConnectEnd?.(event, finalConnectionState); |
|
if (edgeUpdaterType) { |
|
onReconnectEnd?.(event, finalConnectionState); |
|
} |
|
cancelConnection(); |
|
cancelAnimationFrame(autoPanId); |
|
autoPanStarted = false; |
|
isValid = false; |
|
connection = null; |
|
handleDomNode = null; |
|
doc.removeEventListener('mousemove', onPointerMove); |
|
doc.removeEventListener('mouseup', onPointerUp); |
|
doc.removeEventListener('touchmove', onPointerMove); |
|
doc.removeEventListener('touchend', onPointerUp); |
|
} |
|
doc.addEventListener('mousemove', onPointerMove); |
|
doc.addEventListener('mouseup', onPointerUp); |
|
doc.addEventListener('touchmove', onPointerMove); |
|
doc.addEventListener('touchend', onPointerUp); |
|
} |
|
|
|
function isValidHandle(event, { handle, connectionMode, fromNodeId, fromHandleId, fromType, doc, lib, flowId, isValidConnection = alwaysValid, nodeLookup, }) { |
|
const isTarget = fromType === 'target'; |
|
const handleDomNode = handle |
|
? doc.querySelector(`.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]`) |
|
: null; |
|
const { x, y } = getEventPosition(event); |
|
const handleBelow = doc.elementFromPoint(x, y); |
|
|
|
|
|
const handleToCheck = handleBelow?.classList.contains(`${lib}-flow__handle`) ? handleBelow : handleDomNode; |
|
const result = { |
|
handleDomNode: handleToCheck, |
|
isValid: false, |
|
connection: null, |
|
toHandle: null, |
|
}; |
|
if (handleToCheck) { |
|
const handleType = getHandleType(undefined, handleToCheck); |
|
const handleNodeId = handleToCheck.getAttribute('data-nodeid'); |
|
const handleId = handleToCheck.getAttribute('data-handleid'); |
|
const connectable = handleToCheck.classList.contains('connectable'); |
|
const connectableEnd = handleToCheck.classList.contains('connectableend'); |
|
if (!handleNodeId || !handleType) { |
|
return result; |
|
} |
|
const connection = { |
|
source: isTarget ? handleNodeId : fromNodeId, |
|
sourceHandle: isTarget ? handleId : fromHandleId, |
|
target: isTarget ? fromNodeId : handleNodeId, |
|
targetHandle: isTarget ? fromHandleId : handleId, |
|
}; |
|
result.connection = connection; |
|
const isConnectable = connectable && connectableEnd; |
|
|
|
const isValid = isConnectable && |
|
(connectionMode === ConnectionMode.Strict |
|
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target') |
|
: handleNodeId !== fromNodeId || handleId !== fromHandleId); |
|
result.isValid = isValid && isValidConnection(connection); |
|
result.toHandle = getHandle(handleNodeId, handleType, handleId, nodeLookup, connectionMode, false); |
|
} |
|
return result; |
|
} |
|
const XYHandle = { |
|
onPointerDown, |
|
isValid: isValidHandle, |
|
}; |
|
|
|
function XYMinimap({ domNode, panZoom, getTransform, getViewScale }) { |
|
const selection = select(domNode); |
|
function update({ translateExtent, width, height, zoomStep = 10, pannable = true, zoomable = true, inversePan = false, }) { |
|
|
|
const zoomHandler = (event) => { |
|
const transform = getTransform(); |
|
if (event.sourceEvent.type !== 'wheel' || !panZoom) { |
|
return; |
|
} |
|
const pinchDelta = -event.sourceEvent.deltaY * |
|
(event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) * |
|
zoomStep; |
|
const nextZoom = transform[2] * Math.pow(2, pinchDelta); |
|
panZoom.scaleTo(nextZoom); |
|
}; |
|
let panStart = [0, 0]; |
|
|
|
const panStartHandler = (event) => { |
|
if (event.sourceEvent.type === 'mousedown' || event.sourceEvent.type === 'touchstart') { |
|
panStart = [ |
|
event.sourceEvent.clientX ?? event.sourceEvent.touches[0].clientX, |
|
event.sourceEvent.clientY ?? event.sourceEvent.touches[0].clientY, |
|
]; |
|
} |
|
}; |
|
|
|
const panHandler = (event) => { |
|
const transform = getTransform(); |
|
if ((event.sourceEvent.type !== 'mousemove' && event.sourceEvent.type !== 'touchmove') || !panZoom) { |
|
return; |
|
} |
|
const panCurrent = [ |
|
event.sourceEvent.clientX ?? event.sourceEvent.touches[0].clientX, |
|
event.sourceEvent.clientY ?? event.sourceEvent.touches[0].clientY, |
|
]; |
|
const panDelta = [panCurrent[0] - panStart[0], panCurrent[1] - panStart[1]]; |
|
panStart = panCurrent; |
|
const moveScale = getViewScale() * Math.max(transform[2], Math.log(transform[2])) * (inversePan ? -1 : 1); |
|
const position = { |
|
x: transform[0] - panDelta[0] * moveScale, |
|
y: transform[1] - panDelta[1] * moveScale, |
|
}; |
|
const extent = [ |
|
[0, 0], |
|
[width, height], |
|
]; |
|
panZoom.setViewportConstrained({ |
|
x: position.x, |
|
y: position.y, |
|
zoom: transform[2], |
|
}, extent, translateExtent); |
|
}; |
|
const zoomAndPanHandler = zoom() |
|
.on('start', panStartHandler) |
|
|
|
|
|
.on('zoom', pannable ? panHandler : null) |
|
|
|
|
|
.on('zoom.wheel', zoomable ? zoomHandler : null); |
|
selection.call(zoomAndPanHandler, {}); |
|
} |
|
function destroy() { |
|
selection.on('zoom', null); |
|
} |
|
return { |
|
update, |
|
destroy, |
|
pointer, |
|
}; |
|
} |
|
|
|
|
|
const viewChanged = (prevViewport, eventViewport) => prevViewport.x !== eventViewport.x || prevViewport.y !== eventViewport.y || prevViewport.zoom !== eventViewport.k; |
|
const transformToViewport = (transform) => ({ |
|
x: transform.x, |
|
y: transform.y, |
|
zoom: transform.k, |
|
}); |
|
const viewportToTransform = ({ x, y, zoom }) => zoomIdentity.translate(x, y).scale(zoom); |
|
const isWrappedWithClass = (event, className) => event.target.closest(`.${className}`); |
|
const isRightClickPan = (panOnDrag, usedButton) => usedButton === 2 && Array.isArray(panOnDrag) && panOnDrag.includes(2); |
|
const getD3Transition = (selection, duration = 0, onEnd = () => { }) => { |
|
const hasDuration = typeof duration === 'number' && duration > 0; |
|
if (!hasDuration) { |
|
onEnd(); |
|
} |
|
return hasDuration ? selection.transition().duration(duration).on('end', onEnd) : selection; |
|
}; |
|
const wheelDelta = (event) => { |
|
const factor = event.ctrlKey && isMacOs() ? 10 : 1; |
|
return -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * factor; |
|
}; |
|
|
|
function createPanOnScrollHandler({ zoomPanValues, noWheelClassName, d3Selection, d3Zoom, panOnScrollMode, panOnScrollSpeed, zoomOnPinch, onPanZoomStart, onPanZoom, onPanZoomEnd, }) { |
|
return (event) => { |
|
if (isWrappedWithClass(event, noWheelClassName)) { |
|
return false; |
|
} |
|
event.preventDefault(); |
|
event.stopImmediatePropagation(); |
|
const currentZoom = d3Selection.property('__zoom').k || 1; |
|
|
|
if (event.ctrlKey && zoomOnPinch) { |
|
const point = pointer(event); |
|
const pinchDelta = wheelDelta(event); |
|
const zoom = currentZoom * Math.pow(2, pinchDelta); |
|
|
|
d3Zoom.scaleTo(d3Selection, zoom, point, event); |
|
return; |
|
} |
|
|
|
|
|
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; |
|
|
|
if (!isMacOs() && event.shiftKey && panOnScrollMode !== PanOnScrollMode.Vertical) { |
|
deltaX = event.deltaY * deltaNormalize; |
|
deltaY = 0; |
|
} |
|
d3Zoom.translateBy(d3Selection, -(deltaX / currentZoom) * panOnScrollSpeed, -(deltaY / currentZoom) * panOnScrollSpeed, |
|
|
|
{ internal: true }); |
|
const nextViewport = transformToViewport(d3Selection.property('__zoom')); |
|
clearTimeout(zoomPanValues.panScrollTimeout); |
|
|
|
|
|
|
|
if (!zoomPanValues.isPanScrolling) { |
|
zoomPanValues.isPanScrolling = true; |
|
onPanZoomStart?.(event, nextViewport); |
|
} |
|
if (zoomPanValues.isPanScrolling) { |
|
onPanZoom?.(event, nextViewport); |
|
zoomPanValues.panScrollTimeout = setTimeout(() => { |
|
onPanZoomEnd?.(event, nextViewport); |
|
zoomPanValues.isPanScrolling = false; |
|
}, 150); |
|
} |
|
}; |
|
} |
|
function createZoomOnScrollHandler({ noWheelClassName, preventScrolling, d3ZoomHandler }) { |
|
return function (event, d) { |
|
|
|
const preventZoom = !preventScrolling && event.type === 'wheel' && !event.ctrlKey; |
|
if (preventZoom || isWrappedWithClass(event, noWheelClassName)) { |
|
return null; |
|
} |
|
event.preventDefault(); |
|
d3ZoomHandler.call(this, event, d); |
|
}; |
|
} |
|
function createPanZoomStartHandler({ zoomPanValues, onDraggingChange, onPanZoomStart }) { |
|
return (event) => { |
|
if (event.sourceEvent?.internal) { |
|
return; |
|
} |
|
const viewport = transformToViewport(event.transform); |
|
|
|
zoomPanValues.mouseButton = event.sourceEvent?.button || 0; |
|
zoomPanValues.isZoomingOrPanning = true; |
|
zoomPanValues.prevViewport = viewport; |
|
if (event.sourceEvent?.type === 'mousedown') { |
|
onDraggingChange(true); |
|
} |
|
if (onPanZoomStart) { |
|
onPanZoomStart?.(event.sourceEvent, viewport); |
|
} |
|
}; |
|
} |
|
function createPanZoomHandler({ zoomPanValues, panOnDrag, onPaneContextMenu, onTransformChange, onPanZoom, }) { |
|
return (event) => { |
|
zoomPanValues.usedRightMouseButton = !!(onPaneContextMenu && isRightClickPan(panOnDrag, zoomPanValues.mouseButton ?? 0)); |
|
if (!event.sourceEvent?.sync) { |
|
onTransformChange([event.transform.x, event.transform.y, event.transform.k]); |
|
} |
|
if (onPanZoom && !event.sourceEvent?.internal) { |
|
onPanZoom?.(event.sourceEvent, transformToViewport(event.transform)); |
|
} |
|
}; |
|
} |
|
function createPanZoomEndHandler({ zoomPanValues, panOnDrag, panOnScroll, onDraggingChange, onPanZoomEnd, onPaneContextMenu, }) { |
|
return (event) => { |
|
if (event.sourceEvent?.internal) { |
|
return; |
|
} |
|
zoomPanValues.isZoomingOrPanning = false; |
|
if (onPaneContextMenu && |
|
isRightClickPan(panOnDrag, zoomPanValues.mouseButton ?? 0) && |
|
!zoomPanValues.usedRightMouseButton && |
|
event.sourceEvent) { |
|
onPaneContextMenu(event.sourceEvent); |
|
} |
|
zoomPanValues.usedRightMouseButton = false; |
|
onDraggingChange(false); |
|
if (onPanZoomEnd && viewChanged(zoomPanValues.prevViewport, event.transform)) { |
|
const viewport = transformToViewport(event.transform); |
|
zoomPanValues.prevViewport = viewport; |
|
clearTimeout(zoomPanValues.timerId); |
|
zoomPanValues.timerId = setTimeout(() => { |
|
onPanZoomEnd?.(event.sourceEvent, viewport); |
|
}, |
|
|
|
panOnScroll ? 150 : 0); |
|
} |
|
}; |
|
} |
|
|
|
|
|
function createFilter({ zoomActivationKeyPressed, zoomOnScroll, zoomOnPinch, panOnDrag, panOnScroll, zoomOnDoubleClick, userSelectionActive, noWheelClassName, noPanClassName, lib, }) { |
|
return (event) => { |
|
const zoomScroll = zoomActivationKeyPressed || zoomOnScroll; |
|
const pinchZoom = zoomOnPinch && event.ctrlKey; |
|
if (event.button === 1 && |
|
event.type === 'mousedown' && |
|
(isWrappedWithClass(event, `${lib}-flow__node`) || isWrappedWithClass(event, `${lib}-flow__edge`))) { |
|
return true; |
|
} |
|
|
|
if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) { |
|
return false; |
|
} |
|
|
|
if (userSelectionActive) { |
|
return false; |
|
} |
|
|
|
if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') { |
|
return false; |
|
} |
|
|
|
if (isWrappedWithClass(event, noPanClassName) && |
|
(event.type !== 'wheel' || (panOnScroll && event.type === 'wheel' && !zoomActivationKeyPressed))) { |
|
return false; |
|
} |
|
if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') { |
|
return false; |
|
} |
|
if (!zoomOnPinch && event.type === 'touchstart' && event.touches?.length > 1) { |
|
event.preventDefault(); |
|
return false; |
|
} |
|
|
|
if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') { |
|
return false; |
|
} |
|
|
|
if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) { |
|
return false; |
|
} |
|
|
|
if (Array.isArray(panOnDrag) && !panOnDrag.includes(event.button) && event.type === 'mousedown') { |
|
return false; |
|
} |
|
|
|
const buttonAllowed = (Array.isArray(panOnDrag) && panOnDrag.includes(event.button)) || !event.button || event.button <= 1; |
|
|
|
return (!event.ctrlKey || event.type === 'wheel') && buttonAllowed; |
|
}; |
|
} |
|
|
|
function XYPanZoom({ domNode, minZoom, maxZoom, paneClickDistance, translateExtent, viewport, onPanZoom, onPanZoomStart, onPanZoomEnd, onDraggingChange, }) { |
|
const zoomPanValues = { |
|
isZoomingOrPanning: false, |
|
usedRightMouseButton: false, |
|
prevViewport: { x: 0, y: 0, zoom: 0 }, |
|
mouseButton: 0, |
|
timerId: undefined, |
|
panScrollTimeout: undefined, |
|
isPanScrolling: false, |
|
}; |
|
const bbox = domNode.getBoundingClientRect(); |
|
const d3ZoomInstance = zoom() |
|
.clickDistance(!isNumeric(paneClickDistance) || paneClickDistance < 0 ? 0 : paneClickDistance) |
|
.scaleExtent([minZoom, maxZoom]) |
|
.translateExtent(translateExtent); |
|
const d3Selection = select(domNode).call(d3ZoomInstance); |
|
setViewportConstrained({ |
|
x: viewport.x, |
|
y: viewport.y, |
|
zoom: clamp(viewport.zoom, minZoom, maxZoom), |
|
}, [ |
|
[0, 0], |
|
[bbox.width, bbox.height], |
|
], translateExtent); |
|
const d3ZoomHandler = d3Selection.on('wheel.zoom'); |
|
const d3DblClickZoomHandler = d3Selection.on('dblclick.zoom'); |
|
d3ZoomInstance.wheelDelta(wheelDelta); |
|
function setTransform(transform, options) { |
|
if (d3Selection) { |
|
return new Promise((resolve) => { |
|
d3ZoomInstance?.transform(getD3Transition(d3Selection, options?.duration, () => resolve(true)), transform); |
|
}); |
|
} |
|
return Promise.resolve(false); |
|
} |
|
|
|
function update({ noWheelClassName, noPanClassName, onPaneContextMenu, userSelectionActive, panOnScroll, panOnDrag, panOnScrollMode, panOnScrollSpeed, preventScrolling, zoomOnPinch, zoomOnScroll, zoomOnDoubleClick, zoomActivationKeyPressed, lib, onTransformChange, }) { |
|
if (userSelectionActive && !zoomPanValues.isZoomingOrPanning) { |
|
destroy(); |
|
} |
|
const isPanOnScroll = panOnScroll && !zoomActivationKeyPressed && !userSelectionActive; |
|
const wheelHandler = isPanOnScroll |
|
? createPanOnScrollHandler({ |
|
zoomPanValues, |
|
noWheelClassName, |
|
d3Selection, |
|
d3Zoom: d3ZoomInstance, |
|
panOnScrollMode, |
|
panOnScrollSpeed, |
|
zoomOnPinch, |
|
onPanZoomStart, |
|
onPanZoom, |
|
onPanZoomEnd, |
|
}) |
|
: createZoomOnScrollHandler({ |
|
noWheelClassName, |
|
preventScrolling, |
|
d3ZoomHandler, |
|
}); |
|
d3Selection.on('wheel.zoom', wheelHandler, { passive: false }); |
|
if (!userSelectionActive) { |
|
|
|
const startHandler = createPanZoomStartHandler({ |
|
zoomPanValues, |
|
onDraggingChange, |
|
onPanZoomStart, |
|
}); |
|
d3ZoomInstance.on('start', startHandler); |
|
|
|
const panZoomHandler = createPanZoomHandler({ |
|
zoomPanValues, |
|
panOnDrag, |
|
onPaneContextMenu: !!onPaneContextMenu, |
|
onPanZoom, |
|
onTransformChange, |
|
}); |
|
d3ZoomInstance.on('zoom', panZoomHandler); |
|
|
|
const panZoomEndHandler = createPanZoomEndHandler({ |
|
zoomPanValues, |
|
panOnDrag, |
|
panOnScroll, |
|
onPaneContextMenu, |
|
onPanZoomEnd, |
|
onDraggingChange, |
|
}); |
|
d3ZoomInstance.on('end', panZoomEndHandler); |
|
} |
|
const filter = createFilter({ |
|
zoomActivationKeyPressed, |
|
panOnDrag, |
|
zoomOnScroll, |
|
panOnScroll, |
|
zoomOnDoubleClick, |
|
zoomOnPinch, |
|
userSelectionActive, |
|
noPanClassName, |
|
noWheelClassName, |
|
lib, |
|
}); |
|
d3ZoomInstance.filter(filter); |
|
|
|
|
|
|
|
if (zoomOnDoubleClick) { |
|
d3Selection.on('dblclick.zoom', d3DblClickZoomHandler); |
|
} |
|
else { |
|
d3Selection.on('dblclick.zoom', null); |
|
} |
|
} |
|
function destroy() { |
|
d3ZoomInstance.on('zoom', null); |
|
} |
|
async function setViewportConstrained(viewport, extent, translateExtent) { |
|
const nextTransform = viewportToTransform(viewport); |
|
const contrainedTransform = d3ZoomInstance?.constrain()(nextTransform, extent, translateExtent); |
|
if (contrainedTransform) { |
|
await setTransform(contrainedTransform); |
|
} |
|
return new Promise((resolve) => resolve(contrainedTransform)); |
|
} |
|
async function setViewport(viewport, options) { |
|
const nextTransform = viewportToTransform(viewport); |
|
await setTransform(nextTransform, options); |
|
return new Promise((resolve) => resolve(nextTransform)); |
|
} |
|
function syncViewport(viewport) { |
|
if (d3Selection) { |
|
const nextTransform = viewportToTransform(viewport); |
|
const currentTransform = d3Selection.property('__zoom'); |
|
if (currentTransform.k !== viewport.zoom || |
|
currentTransform.x !== viewport.x || |
|
currentTransform.y !== viewport.y) { |
|
|
|
|
|
d3ZoomInstance?.transform(d3Selection, nextTransform, null, { sync: true }); |
|
} |
|
} |
|
} |
|
function getViewport() { |
|
const transform = d3Selection ? zoomTransform(d3Selection.node()) : { x: 0, y: 0, k: 1 }; |
|
return { x: transform.x, y: transform.y, zoom: transform.k }; |
|
} |
|
function scaleTo(zoom, options) { |
|
if (d3Selection) { |
|
return new Promise((resolve) => { |
|
d3ZoomInstance?.scaleTo(getD3Transition(d3Selection, options?.duration, () => resolve(true)), zoom); |
|
}); |
|
} |
|
return Promise.resolve(false); |
|
} |
|
function scaleBy(factor, options) { |
|
if (d3Selection) { |
|
return new Promise((resolve) => { |
|
d3ZoomInstance?.scaleBy(getD3Transition(d3Selection, options?.duration, () => resolve(true)), factor); |
|
}); |
|
} |
|
return Promise.resolve(false); |
|
} |
|
function setScaleExtent(scaleExtent) { |
|
d3ZoomInstance?.scaleExtent(scaleExtent); |
|
} |
|
function setTranslateExtent(translateExtent) { |
|
d3ZoomInstance?.translateExtent(translateExtent); |
|
} |
|
function setClickDistance(distance) { |
|
const validDistance = !isNumeric(distance) || distance < 0 ? 0 : distance; |
|
d3ZoomInstance?.clickDistance(validDistance); |
|
} |
|
return { |
|
update, |
|
destroy, |
|
setViewport, |
|
setViewportConstrained, |
|
getViewport, |
|
scaleTo, |
|
scaleBy, |
|
setScaleExtent, |
|
setTranslateExtent, |
|
syncViewport, |
|
setClickDistance, |
|
}; |
|
} |
|
|
|
var ResizeControlVariant; |
|
(function (ResizeControlVariant) { |
|
ResizeControlVariant["Line"] = "line"; |
|
ResizeControlVariant["Handle"] = "handle"; |
|
})(ResizeControlVariant || (ResizeControlVariant = {})); |
|
const XY_RESIZER_HANDLE_POSITIONS = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; |
|
const XY_RESIZER_LINE_POSITIONS = ['top', 'right', 'bottom', 'left']; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getResizeDirection({ width, prevWidth, height, prevHeight, affectsX, affectsY, }) { |
|
const deltaWidth = width - prevWidth; |
|
const deltaHeight = height - prevHeight; |
|
const direction = [deltaWidth > 0 ? 1 : deltaWidth < 0 ? -1 : 0, deltaHeight > 0 ? 1 : deltaHeight < 0 ? -1 : 0]; |
|
if (deltaWidth && affectsX) { |
|
direction[0] = direction[0] * -1; |
|
} |
|
if (deltaHeight && affectsY) { |
|
direction[1] = direction[1] * -1; |
|
} |
|
return direction; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function getControlDirection(controlPosition) { |
|
const isHorizontal = controlPosition.includes('right') || controlPosition.includes('left'); |
|
const isVertical = controlPosition.includes('bottom') || controlPosition.includes('top'); |
|
const affectsX = controlPosition.includes('left'); |
|
const affectsY = controlPosition.includes('top'); |
|
return { |
|
isHorizontal, |
|
isVertical, |
|
affectsX, |
|
affectsY, |
|
}; |
|
} |
|
function getLowerExtentClamp(lowerExtent, lowerBound) { |
|
return Math.max(0, lowerBound - lowerExtent); |
|
} |
|
function getUpperExtentClamp(upperExtent, upperBound) { |
|
return Math.max(0, upperExtent - upperBound); |
|
} |
|
function getSizeClamp(size, minSize, maxSize) { |
|
return Math.max(0, minSize - size, size - maxSize); |
|
} |
|
function xor(a, b) { |
|
return a ? !b : b; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getDimensionsAfterResize(startValues, controlDirection, pointerPosition, boundaries, keepAspectRatio, nodeOrigin, extent, childExtent) { |
|
let { affectsX, affectsY } = controlDirection; |
|
const { isHorizontal, isVertical } = controlDirection; |
|
const isDiagonal = isHorizontal && isVertical; |
|
const { xSnapped, ySnapped } = pointerPosition; |
|
const { minWidth, maxWidth, minHeight, maxHeight } = boundaries; |
|
const { x: startX, y: startY, width: startWidth, height: startHeight, aspectRatio } = startValues; |
|
let distX = Math.floor(isHorizontal ? xSnapped - startValues.pointerX : 0); |
|
let distY = Math.floor(isVertical ? ySnapped - startValues.pointerY : 0); |
|
const newWidth = startWidth + (affectsX ? -distX : distX); |
|
const newHeight = startHeight + (affectsY ? -distY : distY); |
|
const originOffsetX = -nodeOrigin[0] * startWidth; |
|
const originOffsetY = -nodeOrigin[1] * startHeight; |
|
|
|
let clampX = getSizeClamp(newWidth, minWidth, maxWidth); |
|
let clampY = getSizeClamp(newHeight, minHeight, maxHeight); |
|
|
|
if (extent) { |
|
let xExtentClamp = 0; |
|
let yExtentClamp = 0; |
|
if (affectsX && distX < 0) { |
|
xExtentClamp = getLowerExtentClamp(startX + distX + originOffsetX, extent[0][0]); |
|
} |
|
else if (!affectsX && distX > 0) { |
|
xExtentClamp = getUpperExtentClamp(startX + newWidth + originOffsetX, extent[1][0]); |
|
} |
|
if (affectsY && distY < 0) { |
|
yExtentClamp = getLowerExtentClamp(startY + distY + originOffsetY, extent[0][1]); |
|
} |
|
else if (!affectsY && distY > 0) { |
|
yExtentClamp = getUpperExtentClamp(startY + newHeight + originOffsetY, extent[1][1]); |
|
} |
|
clampX = Math.max(clampX, xExtentClamp); |
|
clampY = Math.max(clampY, yExtentClamp); |
|
} |
|
|
|
if (childExtent) { |
|
let xExtentClamp = 0; |
|
let yExtentClamp = 0; |
|
if (affectsX && distX > 0) { |
|
xExtentClamp = getUpperExtentClamp(startX + distX, childExtent[0][0]); |
|
} |
|
else if (!affectsX && distX < 0) { |
|
xExtentClamp = getLowerExtentClamp(startX + newWidth, childExtent[1][0]); |
|
} |
|
if (affectsY && distY > 0) { |
|
yExtentClamp = getUpperExtentClamp(startY + distY, childExtent[0][1]); |
|
} |
|
else if (!affectsY && distY < 0) { |
|
yExtentClamp = getLowerExtentClamp(startY + newHeight, childExtent[1][1]); |
|
} |
|
clampX = Math.max(clampX, xExtentClamp); |
|
clampY = Math.max(clampY, yExtentClamp); |
|
} |
|
|
|
if (keepAspectRatio) { |
|
if (isHorizontal) { |
|
|
|
const aspectHeightClamp = getSizeClamp(newWidth / aspectRatio, minHeight, maxHeight) * aspectRatio; |
|
clampX = Math.max(clampX, aspectHeightClamp); |
|
|
|
if (extent) { |
|
let aspectExtentClamp = 0; |
|
if ((!affectsX && !affectsY) || (affectsX && !affectsY && isDiagonal)) { |
|
aspectExtentClamp = |
|
getUpperExtentClamp(startY + originOffsetY + newWidth / aspectRatio, extent[1][1]) * aspectRatio; |
|
} |
|
else { |
|
aspectExtentClamp = |
|
getLowerExtentClamp(startY + originOffsetY + (affectsX ? distX : -distX) / aspectRatio, extent[0][1]) * |
|
aspectRatio; |
|
} |
|
clampX = Math.max(clampX, aspectExtentClamp); |
|
} |
|
|
|
if (childExtent) { |
|
let aspectExtentClamp = 0; |
|
if ((!affectsX && !affectsY) || (affectsX && !affectsY && isDiagonal)) { |
|
aspectExtentClamp = getLowerExtentClamp(startY + newWidth / aspectRatio, childExtent[1][1]) * aspectRatio; |
|
} |
|
else { |
|
aspectExtentClamp = |
|
getUpperExtentClamp(startY + (affectsX ? distX : -distX) / aspectRatio, childExtent[0][1]) * aspectRatio; |
|
} |
|
clampX = Math.max(clampX, aspectExtentClamp); |
|
} |
|
} |
|
|
|
if (isVertical) { |
|
const aspectWidthClamp = getSizeClamp(newHeight * aspectRatio, minWidth, maxWidth) / aspectRatio; |
|
clampY = Math.max(clampY, aspectWidthClamp); |
|
if (extent) { |
|
let aspectExtentClamp = 0; |
|
if ((!affectsX && !affectsY) || (affectsY && !affectsX && isDiagonal)) { |
|
aspectExtentClamp = |
|
getUpperExtentClamp(startX + newHeight * aspectRatio + originOffsetX, extent[1][0]) / aspectRatio; |
|
} |
|
else { |
|
aspectExtentClamp = |
|
getLowerExtentClamp(startX + (affectsY ? distY : -distY) * aspectRatio + originOffsetX, extent[0][0]) / |
|
aspectRatio; |
|
} |
|
clampY = Math.max(clampY, aspectExtentClamp); |
|
} |
|
if (childExtent) { |
|
let aspectExtentClamp = 0; |
|
if ((!affectsX && !affectsY) || (affectsY && !affectsX && isDiagonal)) { |
|
aspectExtentClamp = getLowerExtentClamp(startX + newHeight * aspectRatio, childExtent[1][0]) / aspectRatio; |
|
} |
|
else { |
|
aspectExtentClamp = |
|
getUpperExtentClamp(startX + (affectsY ? distY : -distY) * aspectRatio, childExtent[0][0]) / aspectRatio; |
|
} |
|
clampY = Math.max(clampY, aspectExtentClamp); |
|
} |
|
} |
|
} |
|
distY = distY + (distY < 0 ? clampY : -clampY); |
|
distX = distX + (distX < 0 ? clampX : -clampX); |
|
if (keepAspectRatio) { |
|
if (isDiagonal) { |
|
if (newWidth > newHeight * aspectRatio) { |
|
distY = (xor(affectsX, affectsY) ? -distX : distX) / aspectRatio; |
|
} |
|
else { |
|
distX = (xor(affectsX, affectsY) ? -distY : distY) * aspectRatio; |
|
} |
|
} |
|
else { |
|
if (isHorizontal) { |
|
distY = distX / aspectRatio; |
|
affectsY = affectsX; |
|
} |
|
else { |
|
distX = distY * aspectRatio; |
|
affectsX = affectsY; |
|
} |
|
} |
|
} |
|
const x = affectsX ? startX + distX : startX; |
|
const y = affectsY ? startY + distY : startY; |
|
return { |
|
width: startWidth + (affectsX ? -distX : distX), |
|
height: startHeight + (affectsY ? -distY : distY), |
|
x: nodeOrigin[0] * distX * (!affectsX ? 1 : -1) + x, |
|
y: nodeOrigin[1] * distY * (!affectsY ? 1 : -1) + y, |
|
}; |
|
} |
|
|
|
const initPrevValues = { width: 0, height: 0, x: 0, y: 0 }; |
|
const initStartValues = { |
|
...initPrevValues, |
|
pointerX: 0, |
|
pointerY: 0, |
|
aspectRatio: 1, |
|
}; |
|
function nodeToParentExtent(node) { |
|
return [ |
|
[0, 0], |
|
[node.measured.width, node.measured.height], |
|
]; |
|
} |
|
function nodeToChildExtent(child, parent, nodeOrigin) { |
|
const x = parent.position.x + child.position.x; |
|
const y = parent.position.y + child.position.y; |
|
const width = child.measured.width ?? 0; |
|
const height = child.measured.height ?? 0; |
|
const originOffsetX = nodeOrigin[0] * width; |
|
const originOffsetY = nodeOrigin[1] * height; |
|
return [ |
|
[x - originOffsetX, y - originOffsetY], |
|
[x + width - originOffsetX, y + height - originOffsetY], |
|
]; |
|
} |
|
function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }) { |
|
const selection = select(domNode); |
|
function update({ controlPosition, boundaries, keepAspectRatio, onResizeStart, onResize, onResizeEnd, shouldResize, }) { |
|
let prevValues = { ...initPrevValues }; |
|
let startValues = { ...initStartValues }; |
|
const controlDirection = getControlDirection(controlPosition); |
|
let node = undefined; |
|
let childNodes = []; |
|
let parentNode = undefined; |
|
let parentExtent = undefined; |
|
let childExtent = undefined; |
|
const dragHandler = drag() |
|
.on('start', (event) => { |
|
const { nodeLookup, transform, snapGrid, snapToGrid, nodeOrigin } = getStoreItems(); |
|
node = nodeLookup.get(nodeId); |
|
if (!node) { |
|
return; |
|
} |
|
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); |
|
prevValues = { |
|
width: node.measured.width ?? 0, |
|
height: node.measured.height ?? 0, |
|
x: node.position.x ?? 0, |
|
y: node.position.y ?? 0, |
|
}; |
|
startValues = { |
|
...prevValues, |
|
pointerX: xSnapped, |
|
pointerY: ySnapped, |
|
aspectRatio: prevValues.width / prevValues.height, |
|
}; |
|
parentNode = undefined; |
|
if (node.parentId && (node.extent === 'parent' || node.expandParent)) { |
|
parentNode = nodeLookup.get(node.parentId); |
|
parentExtent = parentNode && node.extent === 'parent' ? nodeToParentExtent(parentNode) : undefined; |
|
} |
|
|
|
|
|
childNodes = []; |
|
childExtent = undefined; |
|
for (const [childId, child] of nodeLookup) { |
|
if (child.parentId === nodeId) { |
|
childNodes.push({ |
|
id: childId, |
|
position: { ...child.position }, |
|
extent: child.extent, |
|
}); |
|
if (child.extent === 'parent' || child.expandParent) { |
|
const extent = nodeToChildExtent(child, node, child.origin ?? nodeOrigin); |
|
if (childExtent) { |
|
childExtent = [ |
|
[Math.min(extent[0][0], childExtent[0][0]), Math.min(extent[0][1], childExtent[0][1])], |
|
[Math.max(extent[1][0], childExtent[1][0]), Math.max(extent[1][1], childExtent[1][1])], |
|
]; |
|
} |
|
else { |
|
childExtent = extent; |
|
} |
|
} |
|
} |
|
} |
|
onResizeStart?.(event, { ...prevValues }); |
|
}) |
|
.on('drag', (event) => { |
|
const { transform, snapGrid, snapToGrid, nodeOrigin: storeNodeOrigin } = getStoreItems(); |
|
const pointerPosition = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); |
|
const childChanges = []; |
|
if (!node) { |
|
return; |
|
} |
|
const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues; |
|
const change = {}; |
|
const nodeOrigin = node.origin ?? storeNodeOrigin; |
|
const { width, height, x, y } = getDimensionsAfterResize(startValues, controlDirection, pointerPosition, boundaries, keepAspectRatio, nodeOrigin, parentExtent, childExtent); |
|
const isWidthChange = width !== prevWidth; |
|
const isHeightChange = height !== prevHeight; |
|
const isXPosChange = x !== prevX && isWidthChange; |
|
const isYPosChange = y !== prevY && isHeightChange; |
|
if (!isXPosChange && !isYPosChange && !isWidthChange && !isHeightChange) { |
|
return; |
|
} |
|
if (isXPosChange || isYPosChange || nodeOrigin[0] === 1 || nodeOrigin[1] === 1) { |
|
change.x = isXPosChange ? x : prevValues.x; |
|
change.y = isYPosChange ? y : prevValues.y; |
|
prevValues.x = change.x; |
|
prevValues.y = change.y; |
|
|
|
|
|
if (childNodes.length > 0) { |
|
const xChange = x - prevX; |
|
const yChange = y - prevY; |
|
for (const childNode of childNodes) { |
|
childNode.position = { |
|
x: childNode.position.x - xChange + nodeOrigin[0] * (width - prevWidth), |
|
y: childNode.position.y - yChange + nodeOrigin[1] * (height - prevHeight), |
|
}; |
|
childChanges.push(childNode); |
|
} |
|
} |
|
} |
|
if (isWidthChange || isHeightChange) { |
|
change.width = isWidthChange ? width : prevValues.width; |
|
change.height = isHeightChange ? height : prevValues.height; |
|
prevValues.width = change.width; |
|
prevValues.height = change.height; |
|
} |
|
|
|
if (parentNode && node.expandParent) { |
|
const xLimit = nodeOrigin[0] * (change.width ?? 0); |
|
if (change.x && change.x < xLimit) { |
|
prevValues.x = xLimit; |
|
startValues.x = startValues.x - (change.x - xLimit); |
|
} |
|
const yLimit = nodeOrigin[1] * (change.height ?? 0); |
|
if (change.y && change.y < yLimit) { |
|
prevValues.y = yLimit; |
|
startValues.y = startValues.y - (change.y - yLimit); |
|
} |
|
} |
|
const direction = getResizeDirection({ |
|
width: prevValues.width, |
|
prevWidth, |
|
height: prevValues.height, |
|
prevHeight, |
|
affectsX: controlDirection.affectsX, |
|
affectsY: controlDirection.affectsY, |
|
}); |
|
const nextValues = { ...prevValues, direction }; |
|
const callResize = shouldResize?.(event, nextValues); |
|
if (callResize === false) { |
|
return; |
|
} |
|
onResize?.(event, nextValues); |
|
onChange(change, childChanges); |
|
}) |
|
.on('end', (event) => { |
|
onResizeEnd?.(event, { ...prevValues }); |
|
onEnd?.(); |
|
}); |
|
selection.call(dragHandler); |
|
} |
|
function destroy() { |
|
selection.on('.drag', null); |
|
} |
|
return { |
|
update, |
|
destroy, |
|
}; |
|
} |
|
|
|
export { ConnectionLineType, ConnectionMode, MarkerType, PanOnScrollMode, Position, ResizeControlVariant, SelectionMode, XYDrag, XYHandle, XYMinimap, XYPanZoom, XYResizer, XY_RESIZER_HANDLE_POSITIONS, XY_RESIZER_LINE_POSITIONS, addEdge, adoptUserNodes, areConnectionMapsEqual, boxToRect, calcAutoPan, calculateNodePosition, clamp, clampPosition, clampPositionToParent, createMarkerIds, devWarn, elementSelectionKeys, errorMessages, evaluateAbsolutePosition, fitView, getBezierEdgeCenter, getBezierPath, getBoundsOfBoxes, getBoundsOfRects, getConnectedEdges, getConnectionStatus, getDimensions, getEdgeCenter, getEdgePosition, getElementsToRemove, getElevatedEdgeZIndex, getEventPosition, getFitViewNodes, getHandleBounds, getHandlePosition, getHostForElement, getIncomers, getInternalNodesBounds, getMarkerId, getNodeDimensions, getNodePositionWithOrigin, getNodeToolbarTransform, getNodesBounds, getNodesInside, getOutgoers, getOverlappingArea, getPointerPosition, getSmoothStepPath, getStraightPath, getViewportForBounds, handleConnectionChange, handleExpandParent, infiniteExtent, initialConnection, isCoordinateExtent, isEdgeBase, isEdgeVisible, isInputDOMNode, isInternalNodeBase, isMacOs, isMouseEvent, isNodeBase, isNumeric, isRectObject, nodeHasDimensions, nodeToBox, nodeToRect, oppositePosition, panBy, pointToRendererPoint, reconnectEdge, rectToBox, rendererPointToPoint, shallowNodeData, snapPosition, updateAbsolutePositions, updateConnectionLookup, updateNodeInternals }; |
|
|