UNPKG

@delove/reaflow

Version:

Node-based Visualizations for React

1,379 lines (1,378 loc) 85.5 kB
(function() { "use strict"; try { if (typeof document != "undefined") { var elementStyle = document.createElement("style"); elementStyle.appendChild(document.createTextNode("._port_1r6fw_1 {\n stroke: #0d0e17;\n fill: #3e405a;\n stroke-width: 2px;\n shape-rendering: geometricPrecision;\n pointer-events: none;\n}\n\n._clicker_1r6fw_9 {\n opacity: 0;\n}\n\n._clicker_1r6fw_9:not(._disabled_1r6fw_12) {\n cursor: crosshair;\n }\n._text_fhkx6_1 {\n fill: #d6e7ff;\n pointer-events: none;\n font-size: 14px;\n text-rendering: geometricPrecision;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n}\n._deleteX_nxq8k_1 {\n stroke: black;\n pointer-events: none;\n}\n\n._container_nxq8k_6 {\n will-change: transform, opacity;\n}\n\n._drop_nxq8k_10 {\n cursor: pointer;\n opacity: 0;\n}\n\n._rect_nxq8k_15 {\n shape-rendering: geometricPrecision;\n fill: #ff005d;\n border-radius: 2px;\n pointer-events: none;\n}\n._plus_1qsm8_1 {\n stroke: black;\n pointer-events: none;\n}\n\n._container_1qsm8_6 {\n will-change: transform, opacity;\n}\n\n._drop_1qsm8_10 {\n cursor: pointer;\n opacity: 0;\n}\n\n._rect_1qsm8_15 {\n shape-rendering: geometricPrecision;\n fill: #46FECB;\n border-radius: 2px;\n pointer-events: none;\n}\n._edge_v5z62_1._disabled_v5z62_2 {\n pointer-events: none;\n }\n ._edge_v5z62_1:not(._selectionDisabled_v5z62_6):not(._disabled_v5z62_2):hover ._path_v5z62_8 {\n stroke: #a5a9e2;\n }\n ._edge_v5z62_1:not(._selectionDisabled_v5z62_6):not(._disabled_v5z62_2):hover ._path_v5z62_8._active_v5z62_11 {\n stroke: #46fecb;\n }\n ._edge_v5z62_1:not(._selectionDisabled_v5z62_6):not(._disabled_v5z62_2):hover ._path_v5z62_8._deleteHovered_v5z62_15 {\n stroke: #ff005d;\n stroke-dasharray: 4 2;\n }\n ._edge_v5z62_1:not(._selectionDisabled_v5z62_6):not(._disabled_v5z62_2) ._clicker_v5z62_22 {\n cursor: pointer;\n }\n\n._path_v5z62_8 {\n fill: transparent;\n stroke: #485a74;\n pointer-events: none;\n shape-rendering: geometricPrecision;\n stroke-width: 1pt;\n}\n\n._clicker_v5z62_22 {\n fill: none;\n stroke: transparent;\n stroke-width: 15px;\n}\n\n._clicker_v5z62_22:focus {\n outline: none;\n }\n._rect_1b6xi_1 {\n fill: #2b2c3e;\n transition: stroke 100ms ease-in-out;\n stroke: #475872;\n shape-rendering: geometricPrecision;\n stroke-width: 1pt;\n}\n\n ._rect_1b6xi_1:not(._selectionDisabled_1b6xi_8):not(._disabled_1b6xi_8) {\n cursor: pointer;\n }\n\n ._rect_1b6xi_1:not(._selectionDisabled_1b6xi_8):not(._disabled_1b6xi_8):hover {\n stroke: #a5a9e2;\n }\n\n ._rect_1b6xi_1:not(._selectionDisabled_1b6xi_8):not(._disabled_1b6xi_8)._dragging_1b6xi_15 {\n stroke: #a5a9e2;\n }\n\n ._rect_1b6xi_1:not(._selectionDisabled_1b6xi_8):not(._disabled_1b6xi_8)._active_1b6xi_19 {\n stroke: #46fecb;\n }\n\n ._rect_1b6xi_1:not(._selectionDisabled_1b6xi_8):not(._disabled_1b6xi_8)._unlinkable_1b6xi_23 {\n stroke: #ff005d;\n }\n\n ._rect_1b6xi_1:not(._selectionDisabled_1b6xi_8):not(._disabled_1b6xi_8)._deleteHovered_1b6xi_27 {\n stroke: #ff005d !important;\n stroke-dasharray: 4 2;\n }\n\n ._rect_1b6xi_1:focus {\n outline: none;\n }\n\n ._rect_1b6xi_1._children_1b6xi_37 {\n fill: transparent;\n stroke: #475872;\n }\n._arrow_4r5xg_1 {\n pointer-events: none;\n shape-rendering: geometricPrecision;\n fill: #485a74;\n}\n._container_1ryvh_1._pannable_1ryvh_2 {\n overflow: auto;\n }\n ._container_1ryvh_1:focus {\n outline: none;\n }\n\n.dragging {\n -webkit-touch-callout: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n}\n\n._dragNode_1ryvh_20 {\n pointer-events: none;\n}\n\n._draggable_1ryvh_24 {\n scrollbar-width: none; /* Firefox */\n -ms-overflow-style: none; /* Internet Explorer 10+ */\n cursor: grab;\n}\n\n._draggable_1ryvh_24::-webkit-scrollbar {\n display: none; /* WebKit */\n }\n\n._draggable_1ryvh_24:active {\n cursor: grabbing;\n }\n._icon_6o39n_1 {\n pointer-events: none;\n}")); document.head.appendChild(elementStyle); } } catch (e) { console.error("vite-plugin-css-injected-by-js", e); } })(); import { jsx, jsxs, Fragment as Fragment$1 } from "react/jsx-runtime"; import React, { useRef, useState, useEffect, useCallback, useLayoutEffect, createContext, useContext, forwardRef, Fragment, useMemo, useImperativeHandle } from "react"; import { CloneElement, useId } from "reablocks"; import { useGesture, useDrag } from "react-use-gesture"; import { motion, useAnimation } from "motion/react"; import useDimensions from "react-cool-dimensions"; import isEqual from "react-fast-compare"; import ELK from "elkjs/lib/elk.bundled.js"; import PCancelable from "p-cancelable"; import calculateSize from "calculate-size"; import ellipsize from "ellipsize"; import { Point2D, Matrix2D } from "kld-affine"; import classNames from "classnames"; import { line, curveBundle } from "d3-shape"; import { useHotkeys } from "reakeys"; import Undoo from "undoo"; import { IntersectionQuery } from "kld-intersections"; var CanvasPosition = /* @__PURE__ */ ((CanvasPosition2) => { CanvasPosition2["CENTER"] = "center"; CanvasPosition2["TOP"] = "top"; CanvasPosition2["LEFT"] = "left"; CanvasPosition2["RIGHT"] = "right"; CanvasPosition2["BOTTOM"] = "bottom"; return CanvasPosition2; })(CanvasPosition || {}); const MAX_CHAR_COUNT = 35; const MIN_NODE_WIDTH = 50; const DEFAULT_NODE_HEIGHT = 50; const NODE_PADDING = 30; const ICON_PADDING = 10; function measureText(text2) { let result = { height: 0, width: 0 }; if (text2) { const fn = typeof calculateSize === "function" ? calculateSize : calculateSize.default; result = fn(text2, { font: "Arial, sans-serif", fontSize: "14px" }); } return result; } function parsePadding(padding) { let top = 50; let right = 50; let bottom = 50; let left = 50; if (Array.isArray(padding)) { if (padding.length === 2) { top = padding[0]; bottom = padding[0]; left = padding[1]; right = padding[1]; } else if (padding.length === 4) { top = padding[0]; right = padding[1]; bottom = padding[2]; left = padding[3]; } } else if (padding !== void 0) { top = padding; right = padding; bottom = padding; left = padding; } return { top, right, bottom, left }; } function formatText(node) { const text2 = node.text ? ellipsize(node.text, MAX_CHAR_COUNT) : node.text; const labelDim = measureText(text2); const nodePadding = parsePadding(node.nodePadding); let width = node.width; if (width === void 0) { if (text2 && node.icon) { width = labelDim.width + node.icon.width + NODE_PADDING + ICON_PADDING; } else { if (text2) { width = labelDim.width + NODE_PADDING; } else if (node.icon) { width = node.icon.width + NODE_PADDING; } width = Math.max(width, MIN_NODE_WIDTH); } } let height = node.height; if (height === void 0) { if (text2 && node.icon) { height = labelDim.height + node.icon.height; } else if (text2) { height = labelDim.height + NODE_PADDING; } else if (node.icon) { height = node.icon.height + NODE_PADDING; } height = Math.max(height, DEFAULT_NODE_HEIGHT); } return { text: text2, originalText: node.text, width, height, nodePadding, labelHeight: labelDim.height, labelWidth: labelDim.width }; } const findNode = (nodes, nodeId) => { for (const node of nodes) { if (node.id === nodeId) { return node; } if (node.children) { const foundNode = findNode(node.children, nodeId); if (foundNode) { return foundNode; } } } return void 0; }; const getChildCount = (node) => { var _a; return ((_a = node.children) == null ? void 0 : _a.reduce((acc, child) => { if (child.children) { return acc + 1 + getChildCount(child); } return acc + 1; }, 0)) ?? 0; }; const calculateZoom = ({ nodes, viewportWidth, viewportHeight, maxViewportCoverage = 0.9, minViewportCoverage = 0.2 }) => { const maxChildren = Math.max( 0, nodes.map(getChildCount).reduce((acc, curr) => acc + curr, 0) ); const boundingBox = getNodesBoundingBox(nodes); const boundingBoxWidth = boundingBox.x1 - boundingBox.x0; const boundingBoxHeight = boundingBox.y1 - boundingBox.y0; const maxNodeWidth = Math.max(...nodes.map((node) => node.width)); const maxNodeHeight = Math.max(...nodes.map((node) => node.height)); const maxNodeZoomX = (0.2 + maxChildren * 0.1) * viewportWidth / maxNodeWidth; const maxNodeZoomY = (0.2 + maxChildren * 0.1) * viewportHeight / maxNodeHeight; const maxNodeZoom = Math.min(maxNodeZoomX, maxNodeZoomY); const viewportCoverage = Math.max(Math.min(maxViewportCoverage, maxNodeZoom), minViewportCoverage); const updatedHorizontalZoom = viewportCoverage * viewportWidth / boundingBoxWidth; const updatedVerticalZoom = viewportCoverage * viewportHeight / boundingBoxHeight; const updatedZoom = Math.min(updatedHorizontalZoom, updatedVerticalZoom, maxNodeZoom); return updatedZoom; }; const calculateScrollPosition = ({ nodes, viewportWidth, viewportHeight, canvasWidth, canvasHeight, chartWidth, chartHeight, zoom }) => { const { x0, y0, x1, y1 } = getNodesBoundingBox(nodes); const boundingBoxWidth = (x1 - x0) * zoom; const boundingBoxHeight = (y1 - y0) * zoom; const chartPosition = { x: (canvasWidth - chartWidth * zoom) / 2, y: (canvasHeight - chartHeight * zoom) / 2 }; const boxXPosition = chartPosition.x + x0 * zoom; const boxYPosition = chartPosition.y + y0 * zoom; const boxCenterXPosition = boxXPosition + boundingBoxWidth / 2; const boxCenterYPosition = boxYPosition + boundingBoxHeight / 2; const scrollX = boxCenterXPosition - viewportWidth / 2; const scrollY = boxCenterYPosition - viewportHeight / 2; return [scrollX, scrollY]; }; const getNodesBoundingBox = (nodes) => { return nodes.reduce( (acc, node) => ({ x0: Math.min(acc.x0, node.x), y0: Math.min(acc.y0, node.y), x1: Math.max(acc.x1, node.x + node.width), y1: Math.max(acc.y1, node.y + node.height) }), { x0: nodes[0].x, y0: nodes[0].y, x1: nodes[0].x + nodes[0].width, y1: nodes[0].y + nodes[0].height } ); }; const defaultLayoutOptions = { /** * Hints for where node labels are to be placed; if empty, the node label’s position is not modified. * * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-nodeLabels-placement.html */ "elk.nodeLabels.placement": "INSIDE V_CENTER H_RIGHT", /** * Select a specific layout algorithm. * * Uses "layered" strategy. * It emphasizes the direction of edges by pointing as many edges as possible into the same direction. * The nodes are arranged in layers, which are sometimes called “hierarchies”, * and then reordered such that the number of edge crossings is minimized. * Afterwards, concrete coordinates are computed for the nodes and edge bend points. * * @see https://www.eclipse.org/elk/reference/algorithms.html * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-algorithm.html * @see https://www.eclipse.org/elk/reference/algorithms/org-eclipse-elk-layered.html */ "elk.algorithm": "org.eclipse.elk.layered", /** * Overall direction of edges: horizontal (right / left) or vertical (down / up). * * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-direction.html */ "elk.direction": "DOWN", /** * The node order given by the model does not change to produce a better layout. * E.g. if node A is before node B in the model this is not changed during crossing minimization. * This assumes that the node model order is already respected before crossing minimization. This * can be achieved by setting considerModelOrder.strategy to NODES_AND_EDGES. * * @see https://eclipse.dev/elk/reference/options/org-eclipse-elk-layered-crossingMinimization-forceNodeModelOrder.html */ "layered.crossingMinimization.forceNodeModelOrder": "true", /** * Strategy for node layering. * * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-layered-layering-strategy.html */ "org.eclipse.elk.layered.layering.strategy": "INTERACTIVE", /** * What kind of edge routing style should be applied for the content of a parent node. * Algorithms may also set this option to single edges in order to mark them as splines. * The bend point list of edges with this option set to SPLINES * must be interpreted as control points for a piecewise cubic spline. * * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-edgeRouting.html */ "org.eclipse.elk.edgeRouting": "ORTHOGONAL", /** * Adds bend points even if an edge does not change direction. * If true, each long edge dummy will contribute a bend point to its edges * and hierarchy-crossing edges will always get a bend point where they cross hierarchy boundaries. * By default, bend points are only added where an edge changes direction. * * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-layered-unnecessaryBendpoints.html */ "elk.layered.unnecessaryBendpoints": "true", /** * The spacing to be preserved between nodes and edges that are routed next to the node’s layer. * For the spacing between nodes and edges that cross the node’s layer ‘spacing.edgeNode’ is used. * * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-layered-spacing-edgeNodeBetweenLayers.html */ "elk.layered.spacing.edgeNodeBetweenLayers": "50", /** * Tells the BK node placer to use a certain alignment (out of its four) * instead of the one producing the smallest height, or the combination of all four. * * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-layered-nodePlacement-bk-fixedAlignment.html */ "org.eclipse.elk.layered.nodePlacement.bk.fixedAlignment": "BALANCED", /** * Strategy for cycle breaking. * * Cycle breaking looks for cycles in the graph and determines which edges to reverse to break the cycles. * Reversed edges will end up pointing to the opposite direction of regular edges * (that is, reversed edges will point left if edges usually point right). * * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-layered-cycleBreaking-strategy.html */ "org.eclipse.elk.layered.cycleBreaking.strategy": "DEPTH_FIRST", /** * Whether this node allows to route self loops inside of it instead of around it. * * If set to true, this will make the node a compound node if it isn’t already, * and will require the layout algorithm to support compound nodes with hierarchical ports. * * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-insideSelfLoops-activate.html */ "org.eclipse.elk.insideSelfLoops.activate": "true", /** * Whether each connected component should be processed separately. * * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-separateConnectedComponents.html */ separateConnectedComponents: "false", /** * Spacing to be preserved between pairs of connected components. * This option is only relevant if ‘separateConnectedComponents’ is activated. * * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-spacing-componentComponent.html */ "spacing.componentComponent": "70", /** * TODO: Should be spacing.baseValue? * An optional base value for all other layout options of the ‘spacing’ group. * It can be used to conveniently alter the overall ‘spaciousness’ of the drawing. * Whenever an explicit value is set for the other layout options, this base value will have no effect. * The base value is not inherited, i.e. it must be set for each hierarchical node. * * @see https://www.eclipse.org/elk/reference/groups/org-eclipse-elk-layered-spacing.html */ spacing: "75", /** * The spacing to be preserved between any pair of nodes of two adjacent layers. * Note that ‘spacing.nodeNode’ is used for the spacing between nodes within the layer itself. * * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-layered-spacing-nodeNodeBetweenLayers.html */ "spacing.nodeNodeBetweenLayers": "70" }; function mapNode(nodes, edges, node) { const { text: text2, width, height, labelHeight, labelWidth, nodePadding, originalText } = formatText(node); const children2 = nodes.filter((n) => n.parent === node.id).map((n) => mapNode(nodes, edges, n)); const childEdges = edges.filter((e) => e.parent === node.id).map((e) => mapEdge({ edge: e })); const nodeLayoutOptions = { "elk.padding": `[left=${nodePadding.left}, top=${nodePadding.top}, right=${nodePadding.right}, bottom=${nodePadding.bottom}]`, portConstraints: "FIXED_ORDER", ...node.layoutOptions || {} }; return { id: node.id, height, width, children: children2, edges: childEdges, ports: node.ports ? node.ports.map((port2) => ({ id: port2.id, properties: { ...port2, "port.side": port2.side, "port.alignment": port2.alignment || "CENTER" } })) : [], layoutOptions: nodeLayoutOptions, properties: { ...node }, labels: text2 ? [ { width: labelWidth, height: -(labelHeight / 2), text: text2, originalText // layoutOptions: { 'elk.nodeLabels.placement': 'INSIDE V_CENTER H_CENTER' } } ] : [] }; } function mapEdge({ edge: { data, ...edge2 }, direction }) { const labelDim = measureText(edge2.text); const validEdgeData = data ? { data } : {}; let labelWidth = labelDim.width / 2; if (direction === "LEFT" || direction === "RIGHT") { labelWidth = labelDim.width; } return { id: edge2.id, source: edge2.from, target: edge2.to, properties: { ...edge2 }, ...validEdgeData, sourcePort: edge2.fromPort, targetPort: edge2.toPort, labels: edge2.text ? [ { width: labelWidth, height: -(labelDim.height / 2), text: edge2.text, layoutOptions: { "elk.edgeLabels.placement": "INSIDE V_CENTER H_CENTER" } } ] : [] }; } function mapInput({ nodes, edges, direction }) { const children2 = []; const mappedEdges = []; for (const node of nodes) { if (!node.parent) { const mappedNode = mapNode(nodes, edges, node); if (mappedNode !== null) { children2.push(mappedNode); } } } for (const edge2 of edges) { if (!edge2.parent) { const mappedEdge = mapEdge({ edge: edge2, direction }); if (mappedEdge !== null) { mappedEdges.push(mappedEdge); } } } return { children: children2, edges: mappedEdges }; } function postProcessNode(nodes) { var _a; for (const node of nodes) { const hasLabels = ((_a = node.labels) == null ? void 0 : _a.length) > 0; if (hasLabels && node.properties.icon) { const [label] = node.labels; label.x = node.properties.icon.width + 25; node.properties.icon.x = 25; node.properties.icon.y = node.height / 2; } else if (hasLabels) { const [label] = node.labels; label.x = (node.width - label.width) / 2; } else if (node.properties.icon) { node.properties.icon.x = node.width / 2; node.properties.icon.y = node.height / 2; } if (node.children) { postProcessNode(node.children); } } return nodes; } const elkLayout = (nodes, edges, options) => { const graph = new ELK(); const layoutOptions = { ...defaultLayoutOptions, ...options }; return new PCancelable((resolve, reject) => { graph.layout( { id: "root", ...mapInput({ nodes, edges, direction: layoutOptions == null ? void 0 : layoutOptions["elk.direction"] }) }, { layoutOptions } ).then((data) => { resolve({ ...data, children: postProcessNode(data.children) }); }).catch(reject); }); }; const useLayout = ({ maxWidth, maxHeight, nodes = [], edges = [], fit, pannable: pannable2, defaultPosition, direction, layoutOptions = {}, zoom, setZoom, onLayoutChange }) => { const scrolled = useRef(false); const ref = useRef(); const { observe, width, height } = useDimensions(); const [layout, setLayout] = useState(null); const [xy, setXY] = useState([0, 0]); const [scrollXY, setScrollXY] = useState([0, 0]); const canvasHeight = pannable2 ? maxHeight : height; const canvasWidth = pannable2 ? maxWidth : width; const scrollToXY = (xy2, animated = false) => { ref.current.scrollTo({ left: xy2[0], top: xy2[1], behavior: animated ? "smooth" : "auto" }); setScrollXY(xy2); }; useEffect(() => { const promise = elkLayout(nodes, edges, { "elk.direction": direction, ...layoutOptions }); promise.then((result) => { if (!isEqual(layout, result)) { setLayout(result); onLayoutChange(result); } }).catch((err) => { if (err.name !== "CancelError") { console.error("Layout Error:", err); } }); return () => promise.cancel(); }, [nodes, edges]); const positionVector = useCallback( (position) => { if (layout) { const centerX = (canvasWidth - layout.width * zoom) / 2; const centerY = (canvasHeight - layout.height * zoom) / 2; switch (position) { case CanvasPosition.CENTER: setXY([centerX, centerY]); break; case CanvasPosition.TOP: setXY([centerX, 0]); break; case CanvasPosition.LEFT: setXY([0, centerY]); break; case CanvasPosition.RIGHT: setXY([canvasWidth - layout.width * zoom, centerY]); break; case CanvasPosition.BOTTOM: setXY([centerX, canvasHeight - layout.height * zoom]); break; } } }, [canvasWidth, canvasHeight, layout, zoom] ); const positionScroll = useCallback( (position, animated = false) => { const scrollCenterX = (canvasWidth - width) / 2; const scrollCenterY = (canvasHeight - height) / 2; if (pannable2) { switch (position) { case CanvasPosition.CENTER: scrollToXY([scrollCenterX, scrollCenterY], animated); break; case CanvasPosition.TOP: scrollToXY([scrollCenterX, 0], animated); break; case CanvasPosition.LEFT: scrollToXY([0, scrollCenterY], animated); break; case CanvasPosition.RIGHT: scrollToXY([canvasWidth - width, scrollCenterY], animated); break; case CanvasPosition.BOTTOM: scrollToXY([scrollCenterX, canvasHeight - height], animated); break; } } }, [canvasWidth, canvasHeight, width, height, pannable2] ); const positionCanvas = useCallback( (position, animated = false) => { positionVector(position); positionScroll(position, animated); }, [positionScroll, positionVector] ); useEffect(() => { if (scrolled.current && defaultPosition) { positionVector(defaultPosition); } }, [positionVector, zoom, defaultPosition]); const fitCanvas = useCallback( (animated = false) => { if (layout) { const heightZoom = height / layout.height; const widthZoom = width / layout.width; const scale = Math.min(heightZoom, widthZoom, 1); setZoom(scale - 1); positionCanvas(CanvasPosition.CENTER, animated); } }, [height, layout, width, setZoom, positionCanvas] ); const fitNodes = useCallback( (nodeIds, animated = true) => { if (layout && layout.children) { const nodes2 = Array.isArray(nodeIds) ? nodeIds.map((nodeId) => findNode(layout.children, nodeId)) : [findNode(layout.children, nodeIds)]; if (nodes2) { positionVector(CanvasPosition.CENTER); const updatedZoom = calculateZoom({ nodes: nodes2, viewportWidth: width, viewportHeight: height, maxViewportCoverage: 0.9, minViewportCoverage: 0.2 }); const scrollPosition = calculateScrollPosition({ nodes: nodes2, viewportWidth: width, viewportHeight: height, canvasWidth, canvasHeight, chartWidth: layout.width, chartHeight: layout.height, zoom: updatedZoom }); setZoom(updatedZoom - 1); scrollToXY(scrollPosition, animated); } } }, [canvasHeight, canvasWidth, height, layout, positionVector, setZoom, width] ); useLayoutEffect(() => { const scroller = ref.current; if (scroller && !scrolled.current && layout && height && width) { if (fit) { fitCanvas(); } else if (defaultPosition) { positionCanvas(defaultPosition); } scrolled.current = true; } }, [canvasWidth, pannable2, canvasHeight, layout, height, fit, width, defaultPosition, positionCanvas, fitCanvas, ref]); useLayoutEffect(() => { function onResize() { if (fit) { fitCanvas(); } else if (defaultPosition) { positionCanvas(defaultPosition); } } window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); }, [fit, positionCanvas, defaultPosition, fitCanvas]); return { xy, observe, containerRef: ref, canvasHeight, canvasWidth, containerWidth: width, containerHeight: height, layout, scrollXY, positionCanvas, fitCanvas, fitNodes, setScrollXY: scrollToXY }; }; const useEdgeDrag = ({ onNodeLink, onNodeLinkCheck }) => { const [dragNode2, setDragNode] = useState(null); const [dragPort, setDragPort] = useState(null); const [dragType, setDragType] = useState(null); const [enteredNode, setEnteredNode] = useState(null); const [dragCoords, setDragCoords] = useState(null); const [canLinkNode, setCanLinkNode] = useState(null); const onDragStart = useCallback( (state, _initial, node, port2) => { setDragType(state.dragType); setDragNode(node); setDragPort(port2); }, [] ); const onDrag = useCallback( ({ memo: [matrix], xy: [x, y] }, [ix, iy]) => { const endPoint = new Point2D(x, y).transform(matrix); setDragCoords([ { startPoint: { x: ix, y: iy }, endPoint } ]); }, [] ); const onDragEnd = useCallback( (event) => { if (dragNode2 && enteredNode && canLinkNode) { onNodeLink(event, dragNode2, enteredNode, dragPort); } setDragNode(null); setDragPort(null); setEnteredNode(null); setDragCoords(null); }, [canLinkNode, dragNode2, dragPort, enteredNode, onNodeLink] ); const onEnter = useCallback( (event, node) => { if (dragNode2 && node) { setEnteredNode(node); const canLink = onNodeLinkCheck(event, dragNode2, node, dragPort); const result = (canLink === void 0 || canLink) && (dragNode2.parent === node.parent || dragType === "node"); setCanLinkNode(result); } }, [dragNode2, dragPort, dragType, onNodeLinkCheck] ); const onLeave = useCallback( (event, node) => { if (dragNode2 && node) { setEnteredNode(null); setCanLinkNode(null); } }, [dragNode2] ); return { dragCoords, canLinkNode, dragNode: dragNode2, dragPort, enteredNode, onDragStart, onDrag, onDragEnd, onEnter, onLeave }; }; const limit = (scale, min, max) => scale < max ? scale > min ? scale : min : max; const useZoom = ({ disabled: disabled2 = false, zoom = 1, minZoom = -0.5, maxZoom = 1, onZoomChange }) => { const [factor, setFactor] = useState(zoom - 1); const svgRef = useRef(null); useGesture( { onPinch: ({ offset: [d], event }) => { event.preventDefault(); const next = limit(d / 100, minZoom, maxZoom); setFactor(next); onZoomChange(next + 1); } }, { enabled: !disabled2, domTarget: svgRef, eventOptions: { passive: false } } ); const setZoom = useCallback( (f) => { const next = limit(f, minZoom, maxZoom); setFactor(next); onZoomChange(next + 1); }, [maxZoom, minZoom, onZoomChange] ); const zoomIn = useCallback( (zoomFactor = 0.1) => { setZoom(factor + zoomFactor); }, [factor, setZoom] ); const zoomOut = useCallback( (zoomFactor = -0.1) => { setZoom(factor + zoomFactor); }, [factor, setZoom] ); return { svgRef, zoom: factor + 1, setZoom, zoomIn, zoomOut }; }; const CanvasContext = createContext({}); const CanvasProvider = ({ selections, onNodeLink, readonly, children: children2, nodes, edges, maxHeight, fit, maxWidth, direction, layoutOptions, pannable: pannable2, panType, defaultPosition, zoomable, zoom, minZoom, maxZoom, onNodeLinkCheck, onLayoutChange, onZoomChange }) => { const zoomProps = useZoom({ zoom, minZoom, maxZoom, disabled: !zoomable, onZoomChange }); const layoutProps = useLayout({ nodes, edges, maxHeight, maxWidth, direction, pannable: pannable2, panType, defaultPosition, fit, layoutOptions, zoom: zoomProps.zoom, setZoom: zoomProps.setZoom, onLayoutChange }); const dragProps = useEdgeDrag({ onNodeLink, onNodeLinkCheck }); return /* @__PURE__ */ jsx( CanvasContext.Provider, { value: { selections, readonly, pannable: pannable2, panType, ...layoutProps, ...zoomProps, ...dragProps }, children: children2 } ); }; const useCanvas = () => { const context = useContext(CanvasContext); if (context === void 0) { throw new Error( "`useCanvas` hook must be used within a `CanvasContext` component" ); } return context; }; function checkNodeLinkable(curNode, enteredNode, canLinkNode) { if (canLinkNode === null || !enteredNode) { return null; } if (!enteredNode || !curNode) { return false; } return !(canLinkNode === false && enteredNode.id === curNode.id); } function getCoords({ zoom, layoutXY, containerRef }) { const { top, left } = containerRef.current.getBoundingClientRect(); const tx = layoutXY[0] - containerRef.current.scrollLeft + left; const ty = layoutXY[1] - containerRef.current.scrollTop + top; return new Matrix2D().translate(tx, ty).scale(zoom).inverse(); } function findNestedNode(nodeId, children2, parentId) { if (!nodeId || !children2) { return {}; } const foundNode = children2.find((n) => n.id === nodeId); if (foundNode) { return foundNode; } if (parentId) { const parentNode = children2.find((n) => n.id === parentId); if (parentNode == null ? void 0 : parentNode.children) { return findNestedNode(nodeId, parentNode.children, parentId); } } const nodesWithChildren = children2.filter((n) => { var _a; return (_a = n.children) == null ? void 0 : _a.length; }); for (const n of nodesWithChildren) { const foundChild = findNestedNode(nodeId, n.children, parentId); if (foundChild && Object.keys(foundChild).length) { return foundChild; } } return {}; } function getDragNodeData(dragNode2, children2 = []) { if (!dragNode2) { return {}; } const { parent } = dragNode2; if (!parent) { return (children2 == null ? void 0 : children2.find((n) => n.id === dragNode2.id)) || {}; } return findNestedNode(dragNode2.id, children2, parent); } const useNodeDrag = ({ x, y, height, width, onDrag, onDragEnd, onDragStart, node, disabled: disabled2 }) => { const initial = [width / 2 + x, height + y]; const targetRef = useRef(null); const { zoom, xy, containerRef } = useCanvas(); const bind = useDrag( (state) => { if (state.event.type === "pointerdown") { targetRef.current = state.event.currentTarget; } if (!state.intentional || !targetRef.current) { return; } if (state.first) { const matrix = getCoords({ containerRef, zoom, layoutXY: xy }); const memo = [matrix]; onDragStart({ ...state, memo }, initial, node); return memo; } onDrag(state, initial, node); if (state.last) { targetRef.current = null; onDragEnd(state, initial, node); } }, { enabled: !disabled2, triggerAllEvents: true, threshold: 5 } ); return bind; }; const port = "_port_1r6fw_1"; const clicker$1 = "_clicker_1r6fw_9"; const disabled$2 = "_disabled_1r6fw_12"; const css$8 = { port, clicker: clicker$1, disabled: disabled$2 }; const Port = forwardRef(({ id, x, y, rx, ry, disabled: disabled2, style, children: children2, properties, offsetX, offsetY, className, active: active2, onDrag = () => void 0, onDragStart = () => void 0, onDragEnd = () => void 0, onEnter = () => void 0, onLeave = () => void 0, onClick = () => void 0 }, ref) => { const { readonly } = useCanvas(); const [isDragging, setIsDragging] = useState(false); const [isHovered, setIsHovered] = useState(false); const newX = x - properties.width / 2; const newY = y - properties.height / 2; const onDragStartInternal = (event, initial) => { onDragStart(event, initial, properties); setIsDragging(true); }; const onDragEndInternal = (event, initial) => { onDragEnd(event, initial, properties); setIsDragging(false); }; const bind = useNodeDrag({ x: newX + offsetX, y: newY + offsetY, height: properties.height, width: properties.width, disabled: disabled2 || readonly || (properties == null ? void 0 : properties.disabled), node: properties, onDrag, onDragStart: onDragStartInternal, onDragEnd: onDragEndInternal }); if (properties.hidden) { return null; } const isDisabled = properties.disabled || disabled2; const portChildProps = { port: properties, isDragging, isHovered, isDisabled, x, y, rx, ry, offsetX, offsetY }; return /* @__PURE__ */ jsxs("g", { id, children: [ /* @__PURE__ */ jsx( "rect", { ...bind(), ref, height: properties.height + 14, width: properties.width + 14, x: newX - 7, y: newY - 7, className: classNames(css$8.clicker, { [css$8.disabled]: isDisabled }), onMouseEnter: (event) => { event.stopPropagation(); if (!isDisabled) { setIsHovered(true); onEnter(event, properties); } }, onMouseLeave: (event) => { event.stopPropagation(); if (!isDisabled) { setIsHovered(false); onLeave(event, properties); } }, onClick: (event) => { event.stopPropagation(); if (!isDisabled) { onClick(event, properties); } } } ), /* @__PURE__ */ jsx( motion.rect, { style, className: classNames(css$8.port, className, properties == null ? void 0 : properties.className), height: properties.height, width: properties.width, rx, ry, initial: { scale: 0, opacity: 0, x: newX, y: newY }, animate: { x: newX, y: newY, scale: (isDragging || active2 || isHovered) && !isDisabled ? 1.5 : 1, opacity: 1 } }, `${x}-${y}` ), children2 && /* @__PURE__ */ jsx(Fragment, { children: typeof children2 === "function" ? children2(portChildProps) : children2 }) ] }); }); const text = "_text_fhkx6_1"; const css$7 = { text }; const Label = ({ text: text2, x, y, style, className, originalText }) => { const isString = typeof originalText === "string"; return /* @__PURE__ */ jsxs(Fragment$1, { children: [ isString && /* @__PURE__ */ jsx("title", { children: originalText }), /* @__PURE__ */ jsx("g", { transform: `translate(${x}, ${y})`, children: /* @__PURE__ */ jsx("text", { className: classNames(css$7.text, className), style, children: text2 }) }) ] }); }; const deleteX = "_deleteX_nxq8k_1"; const container$2 = "_container_nxq8k_6"; const drop$1 = "_drop_nxq8k_10"; const rect$2 = "_rect_nxq8k_15"; const css$6 = { deleteX, container: container$2, drop: drop$1, rect: rect$2 }; const Remove = ({ size = 15, className, hidden, x, y, onClick = () => void 0, onEnter = () => void 0, onLeave = () => void 0 }) => { if (hidden) { return null; } const half = size / 2; const translateX = x - half; const translateY = y - half; return /* @__PURE__ */ jsxs(motion.g, { className: classNames(className, css$6.container), initial: { scale: 0, opacity: 0, translateX, translateY }, animate: { scale: 1, opacity: 1, translateX, translateY }, whileHover: { scale: 1.2 }, whileTap: { scale: 0.8 }, children: [ /* @__PURE__ */ jsx( "rect", { height: size * 1.5, width: size * 1.5, className: css$6.drop, onMouseEnter: onEnter, onMouseLeave: onLeave, onClick: (event) => { event.preventDefault(); event.stopPropagation(); onClick(event); } } ), /* @__PURE__ */ jsx("rect", { height: size, width: size, className: css$6.rect }), /* @__PURE__ */ jsx("line", { x1: "2", y1: size - 2, x2: size - 2, y2: "2", className: css$6.deleteX, strokeWidth: "1" }), /* @__PURE__ */ jsx("line", { x1: "2", y1: "2", x2: size - 2, y2: size - 2, className: css$6.deleteX, strokeWidth: "1" }) ] }); }; function getBezierCenter({ 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 getBezierPath({ sourceX, sourceY, sourcePosition = "bottom", targetX, targetY, targetPosition = "top" }) { const leftAndRight = ["left", "right"]; const [centerX, centerY] = getBezierCenter({ sourceX, sourceY, targetX, targetY }); let path2 = `M${sourceX},${sourceY} C${sourceX},${centerY} ${targetX},${centerY} ${targetX},${targetY}`; if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) { path2 = `M${sourceX},${sourceY} C${centerX},${sourceY} ${centerX},${targetY} ${targetX},${targetY}`; } else if (leftAndRight.includes(targetPosition)) { path2 = `M${sourceX},${sourceY} C${sourceX},${targetY} ${sourceX},${targetY} ${targetX},${targetY}`; } else if (leftAndRight.includes(sourcePosition)) { path2 = `M${sourceX},${sourceY} C${targetX},${sourceY} ${targetX},${sourceY} ${targetX},${targetY}`; } return path2; } function getCenter(pathElm) { const pLength = pathElm.getTotalLength(); const pieceSize = pLength / 2; const { x, y } = pathElm.getPointAtLength(pieceSize); const angle = Math.atan2(x, y) * 180 / Math.PI; return { x, y, angle }; } function getAngle(source, target) { const dx = source.x - target.x; const dy = source.y - target.y; let theta = Math.atan2(-dy, -dx); theta *= 180 / Math.PI; if (theta < 0) { theta += 360; } return theta; } function getPathCenter(pathElm, firstPoint, lastPoint) { if (!pathElm) { return null; } const angle = getAngle(firstPoint, lastPoint); const point = getCenter(pathElm); return { ...point, angle }; } const plus = "_plus_1qsm8_1"; const container$1 = "_container_1qsm8_6"; const drop = "_drop_1qsm8_10"; const rect$1 = "_rect_1qsm8_15"; const css$5 = { plus, container: container$1, drop, rect: rect$1 }; const Add = ({ x, y, className, size = 15, hidden = true, onEnter = () => void 0, onLeave = () => void 0, onClick = () => void 0 }) => { if (hidden) { return null; } const half = size / 2; const translateX = x - half; const translateY = y - half; return /* @__PURE__ */ jsxs(motion.g, { className: classNames(className, css$5.container), initial: { scale: 0, opacity: 0, translateX, translateY }, animate: { scale: 1, opacity: 1, translateX, translateY }, whileHover: { scale: 1.2 }, whileTap: { scale: 0.8 }, children: [ /* @__PURE__ */ jsx( "rect", { height: size * 2, width: size * 2, className: css$5.drop, onClick: (event) => { event.preventDefault(); event.stopPropagation(); onClick(event); }, onMouseEnter: onEnter, onMouseLeave: onLeave } ), /* @__PURE__ */ jsx("rect", { height: size, width: size, className: css$5.rect }), /* @__PURE__ */ jsx("line", { x1: "2", x2: size - 2, y1: half, y2: half, className: css$5.plus, strokeWidth: "1" }), /* @__PURE__ */ jsx("line", { x1: half, x2: half, y1: "2", y2: size - 2, className: css$5.plus, strokeWidth: "1" }) ] }); }; const edge = "_edge_v5z62_1"; const disabled$1 = "_disabled_v5z62_2"; const selectionDisabled$1 = "_selectionDisabled_v5z62_6"; const path = "_path_v5z62_8"; const active$1 = "_active_v5z62_11"; const deleteHovered$1 = "_deleteHovered_v5z62_15"; const clicker = "_clicker_v5z62_22"; const css$4 = { edge, disabled: disabled$1, selectionDisabled: selectionDisabled$1, path, active: active$1, deleteHovered: deleteHovered$1, clicker }; const Edge = ({ sections, interpolation = "curved", properties, labels, className, containerClassName, disabled: disabled2, removable = true, selectable = true, upsertable = true, style, children: children2, add = /* @__PURE__ */ jsx(Add, {}), remove = /* @__PURE__ */ jsx(Remove, {}), label = /* @__PURE__ */ jsx(Label, {}), onClick = () => void 0, onKeyDown = () => void 0, onEnter = () => void 0, onLeave = () => void 0, onRemove = () => void 0, onAdd = () => void 0 }) => { const pathRef = useRef(null); const [deleteHovered2, setDeleteHovered] = useState(false); const [center, setCenter] = useState(null); const { selections, readonly } = useCanvas(); const isActive = (selections == null ? void 0 : selections.length) ? selections.includes(properties == null ? void 0 : properties.id) : false; const isDisabled = disabled2 || (properties == null ? void 0 : properties.disabled); const canSelect = selectable && !(properties == null ? void 0 : properties.selectionDisabled); const d = useMemo(() => { if (!(sections == null ? void 0 : sections.length)) { return null; } if (sections[0].bendPoints) { const points = sections ? [sections[0].startPoint, ...sections[0].bendPoints || [], sections[0].endPoint] : []; let pathFn = line().x((d2) => d2.x).y((d2) => d2.y); if (interpolation !== "linear") { pathFn = interpolation === "curved" ? pathFn.curve(curveBundle.beta(1)) : interpolation; } return pathFn(points); } else { return getBezierPath({ sourceX: sections[0].startPoint.x, sourceY: sections[0].startPoint.y, targetX: sections[0].endPoint.x, targetY: sections[0].endPoint.y }); } }, [interpolation, sections]); useEffect(() => { if ((sections == null ? void 0 : sections.length) > 0) { setCenter(getPathCenter(pathRef.current, sections[0].startPoint, sections[0].endPoint)); } }, [sections]); const edgeChildProps = { edge: properties, center, pathRef }; return /* @__PURE__ */ jsxs( "g", { className: classNames(css$4.edge, containerClassName, { [css$4.disabled]: isDisabled, [css$4.selectionDisabled]: !canSelect }), children: [ /* @__PURE__ */ jsx( "path", { ref: pathRef, style, className: classNames(css$4.path, properties == null ? void 0 : properties.className, className, { [css$4.active]: isActive, [css$4.deleteHovered]: deleteHovered2 }), d, markerEnd: "url(#end-arrow)" } ), /* @__PURE__ */ jsx( "path", { className: css$4.clicker, d, tabIndex: -1, onClick: (event) => { event.preventDefault(); event.stopPropagation(); if (!isDisabled && canSelect) { onClick(event, properties); } }, onKeyDown: (event) => { event.preventDefault(); event.stopPropagation(); if (!isDisabled) { onKeyDown(event, properties); } }, onMouseEnter: (event) => { event.stopPropagation(); if (!isDisabled) { onEnter(event, properties); } }, onMouseLeave: (event) => { event.stopPropagation(); if (!isDisabled) { onLeave(event, properties); } } } ), children2 && /* @__PURE__ */ jsx(Fragment, { children: typeof children2 === "function" ? children2(edgeChildProps) : children2 }), (labels == null ? void 0 : labels.length) > 0 && labels.map((l, index) => /* @__PURE__ */ jsx(CloneElement, { element: label, edgeChildProps, ...l }, index)), !isDisabled && center && !readonly && remove && removable && /* @__PURE__ */ jsx( CloneElement, { element: remove, ...center, hidden: remove.props.hidden !== void 0 ? remove.props.hidden : !isActive, onClick: (event) => { event.preventDefault(); event.stopPropagation(); onRemove(event, properties); setDeleteHovered(false); }, onEnter: () => setDeleteHovered(true), onLeave: () => setDeleteHovered(false) } ), !isDisabled && center && !readonly && add && upsertable && /* @__PURE__ */ jsx( CloneElement, { element: add, ...center, onClick: (event) => { event.preventDefault(); event.stopPropagation(); onAdd(event, properties); } } ) ] } ); }; const rect = "_rect_1b6xi_1"; const selectionDisabled = "_selectionDisabled_1b6xi_8"; const disabled = "_disabled_1b6xi_8"; const dragging = "_dragging_1b6xi_15"; const active = "_active_1b6xi_19"; const unlinkable = "_unlinkable_1b6xi_23"; const deleteHovered = "_deleteHovered_1b6xi_27"; const children = "_children_1b6xi_37"; const css$3 = { rect, selectionDisabled, disabled, dragging, active, unlinkable, deleteHovered, children }; const Node = ({ id, x, y, ports, labels, height, width, properties, animated, className, rx = 2, ry = 2, offsetX = 0, offsetY = 0, icon: icon2, disabled: disabled2, style, children: children2, nodes, edges, draggable: draggable2 = true, linkable = true, selectable = true, removable = true, dragType = "multiportOnly", dragCursor = "crosshair", childEdge = /* @__PURE__ */ jsx(Edge, {}), childNode = /* @__PURE__ */ jsx(Node, {}), remove = /* @__PURE__ */ jsx(Remove, {}), port: port2 = /* @__PURE__ */ jsx(Port, {}), label = /* @__PURE__ */ jsx(Label, {}), tooltip: Tooltip = React.Fragment, onRemove, onDrag, onDragStart, onDragEnd, onClick, onKeyDown, onEnter, onLeave }) => { const nodeRef = useRef(null); const controls = useAnimation(); const { canLinkNode, enteredNode, selections, readonly, ...canvas } = useCanvas(); const [deleteHovered2, setDeleteHovered] = useState(false); const [dragging2, setDragging] = useState(false); const [isLinkable, setIsLinkable] = useState(true); const isActive = (selections == null ? void 0 : selections.length) ? selections.includes(properties.id) : null; const isNodeDrag = id.includes("node-drag"); const newX = x + offsetX; const newY = y + offsetY; const isMultiPort = dragType === "multiportOnly" && (ports == null ? void 0 : ports.filter((p) => { var _a; return !((_a = p.properties) == null ? void 0 : _a.hidden); }).length) > 1; const isDisabled = disabled2 || (properties == null ? void 0 : properties.disabled); const canDrag = ["port", "multiportOnly"].includes(dragType) ? linkable : draggable2; const canSelect = selectable && !(properties == null ? void 0 : properties.selectionDisabled); const getDragType = useCallback( (hasPort) => { let activeDragType = null; if (!hasPort) { if (dragType === "all" || dragType === "node") { activeDragType = "node"; } else if (!isMultiPort) { activeDragType = "port"; } } else { if (dragType === "all" || dragType === "port" || isMultiPort) { activeDragType = "port"; } } return activeDragType; }, [dragType, isMultiPort] ); const setDragCursor = useCallback((dragType2) => { if (dragType2) { document.body.classList.add("dragging"); document.body.style.cursor = dragType2 === "node" ? "grab" : "crosshair"; } else { document.body.classList.remove("dragging"); document.body.style.cursor = "auto"; } }, []); const bind = useNodeDrag({ x: newX, y: newY, height, width,