UNPKG

reaflow

Version:

Node-based Visualizations for React

1,093 lines (1,059 loc) 91 kB
Object.defineProperty(exports, '__esModule', { value: true }); var jsxRuntime = require('react/jsx-runtime'); var react = require('react'); var rdk = require('rdk'); var framerMotion = require('framer-motion'); var reactUseGesture = require('react-use-gesture'); var ELK = require('elkjs/lib/elk.bundled'); var PCancelable = require('p-cancelable'); var calculateSize = require('calculate-size'); var ellipsize = require('ellipsize'); var useDimensions = require('react-cool-dimensions'); var isEqual = require('react-fast-compare'); var kldAffine = require('kld-affine'); var classNames = require('classnames'); var d3Shape = require('d3-shape'); var reakeys = require('reakeys'); var Undoo = require('undoo'); var kldIntersections = require('kld-intersections'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var ELK__default = /*#__PURE__*/_interopDefaultLegacy(ELK); var PCancelable__default = /*#__PURE__*/_interopDefaultLegacy(PCancelable); var calculateSize__default = /*#__PURE__*/_interopDefaultLegacy(calculateSize); var ellipsize__default = /*#__PURE__*/_interopDefaultLegacy(ellipsize); var useDimensions__default = /*#__PURE__*/_interopDefaultLegacy(useDimensions); var isEqual__default = /*#__PURE__*/_interopDefaultLegacy(isEqual); var classNames__default = /*#__PURE__*/_interopDefaultLegacy(classNames); var Undoo__default = /*#__PURE__*/_interopDefaultLegacy(Undoo); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } 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(text) { let result = { height: 0, width: 0 }; if (text) { result = calculateSize__default['default'](text, { 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 !== undefined) { top = padding; right = padding; bottom = padding; left = padding; } return { top, right, bottom, left }; } function formatText(node) { const text = node.text ? ellipsize__default['default'](node.text, MAX_CHAR_COUNT) : node.text; const labelDim = measureText(text); const nodePadding = parsePadding(node.nodePadding); let width = node.width; if (width === undefined) { if (text && node.icon) { width = labelDim.width + node.icon.width + NODE_PADDING + ICON_PADDING; } else { if (text) { 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 === undefined) { if (text && node.icon) { height = labelDim.height + node.icon.height; } else if (text) { height = labelDim.height + NODE_PADDING; } else if (node.icon) { height = node.icon.height + NODE_PADDING; } height = Math.max(height, DEFAULT_NODE_HEIGHT); } return { text, originalText: node.text, width, height, nodePadding, labelHeight: labelDim.height, labelWidth: labelDim.width }; } /** * ELK layout options applied by default, unless overridden through <Canvas layoutOptions> property. * * XXX Not to be confounded with ELK "defaultLayoutOptions" property, which is meant to be used as fallback, when no layout option is provided. * * @see https://www.eclipse.org/elk/reference/options.html */ 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', /** * 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, width, height, labelHeight, labelWidth, nodePadding, originalText } = formatText(node); const children = 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(e)); const nodeLayoutOptions = Object.assign({ '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, edges: childEdges, ports: node.ports ? node.ports.map((port) => ({ id: port.id, properties: Object.assign(Object.assign({}, port), { 'port.side': port.side, 'port.alignment': port.alignment || 'CENTER' }) })) : [], layoutOptions: nodeLayoutOptions, properties: Object.assign({}, node), labels: text ? [ { width: labelWidth, height: -(labelHeight / 2), text, originalText // layoutOptions: { 'elk.nodeLabels.placement': 'INSIDE V_CENTER H_CENTER' } } ] : [] }; } function mapEdge(edge) { const labelDim = measureText(edge.text); return { id: edge.id, source: edge.from, target: edge.to, properties: Object.assign({}, edge), sourcePort: edge.fromPort, targetPort: edge.toPort, labels: edge.text ? [ { width: labelDim.width / 2, height: -(labelDim.height / 2), text: edge.text, layoutOptions: { 'elk.edgeLabels.placement': 'INSIDE V_CENTER H_CENTER' } } ] : [] }; } function mapInput(nodes, edges) { const children = []; const mappedEdges = []; for (const node of nodes) { if (!node.parent) { const mappedNode = mapNode(nodes, edges, node); if (mappedNode !== null) { children.push(mappedNode); } } } for (const edge of edges) { if (!edge.parent) { const mappedEdge = mapEdge(edge); if (mappedEdge !== null) { mappedEdges.push(mappedEdge); } } } return { children, edges: mappedEdges }; } function postProcessNode(nodes) { var _a; for (const node of nodes) { const hasLabels = ((_a = node.labels) === null || _a === void 0 ? 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__default['default'](); const layoutOptions = Object.assign(Object.assign({}, defaultLayoutOptions), options); return new PCancelable__default['default']((resolve, reject) => { graph .layout(Object.assign({ id: 'root' }, mapInput(nodes, edges)), { layoutOptions: layoutOptions }) .then((data) => { resolve(Object.assign(Object.assign({}, data), { children: postProcessNode(data.children) })); }) .catch(reject); }); }; const useLayout = ({ maxWidth, maxHeight, nodes = [], edges = [], fit, pannable, center, direction, layoutOptions = {}, zoom, setZoom, onLayoutChange }) => { const scrolled = react.useRef(false); const ref = react.useRef(); const { observe, width, height } = useDimensions__default['default'](); const [layout, setLayout] = react.useState(null); const [xy, setXY] = react.useState([0, 0]); const [scrollXY, setScrollXY] = react.useState([0, 0]); const canvasHeight = pannable ? maxHeight : height; const canvasWidth = pannable ? maxWidth : width; react.useEffect(() => { const promise = elkLayout(nodes, edges, Object.assign({ 'elk.direction': direction }, layoutOptions)); promise .then(result => { if (!isEqual__default['default'](layout, result)) { setLayout(result); onLayoutChange(result); } }) .catch(err => { if (err.name !== 'CancelError') { console.error('Layout Error:', err); } }); return () => promise.cancel(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [nodes, edges]); const centerVector = react.useCallback(() => { if (center && layout) { // @ts-ignore const x = (canvasWidth - layout.width * zoom) / 2; // @ts-ignore const y = (canvasHeight - layout.height * zoom) / 2; setXY([x, y]); } }, [canvasWidth, canvasHeight, layout, zoom, center]); const centerScroll = react.useCallback(() => { const scrollX = (canvasWidth - width) / 2; const scrollY = (canvasHeight - height) / 2; if (pannable) { setScrollXY([scrollX, scrollY]); } }, [canvasWidth, canvasHeight, width, height, pannable]); const centerCanvas = react.useCallback(() => { centerVector(); centerScroll(); }, [centerScroll, centerVector]); react.useEffect(() => { var _a; (_a = ref === null || ref === void 0 ? void 0 : ref.current) === null || _a === void 0 ? void 0 : _a.scrollTo(scrollXY[0], scrollXY[1]); }, [scrollXY, ref]); react.useEffect(() => { if (scrolled.current) { centerVector(); } }, [centerVector, zoom]); const fitCanvas = react.useCallback(() => { if (layout) { const heightZoom = height / layout.height; const widthZoom = width / layout.width; const scale = Math.min(heightZoom, widthZoom, 1); setZoom(scale - 1); centerCanvas(); } }, [height, layout, width, setZoom, centerCanvas]); react.useLayoutEffect(() => { const scroller = ref.current; if (scroller && !scrolled.current && layout && height && width) { if (fit) { fitCanvas(); } else { centerCanvas(); } scrolled.current = true; } }, [ canvasWidth, pannable, canvasHeight, layout, height, fit, width, center, centerCanvas, fitCanvas, ref ]); react.useLayoutEffect(() => { function onResize() { if (fit) { fitCanvas(); } else { centerCanvas(); } } window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, [fit, centerCanvas, fitCanvas]); return { xy, observe, containerRef: ref, canvasHeight, canvasWidth, containerWidth: width, containerHeight: height, layout, scrollXY, centerCanvas, fitCanvas }; }; const useEdgeDrag = ({ onNodeLink, onNodeLinkCheck }) => { const [dragNode, setDragNode] = react.useState(null); const [dragPort, setDragPort] = react.useState(null); const [dragType, setDragType] = react.useState(null); const [enteredNode, setEnteredNode] = react.useState(null); const [dragCoords, setDragCoords] = react.useState(null); const [canLinkNode, setCanLinkNode] = react.useState(null); const onDragStart = (state, _initial, node, port) => { setDragType(state.dragType); setDragNode(node); setDragPort(port); }; const onDrag = ({ memo: [matrix], xy: [x, y] }, [ix, iy]) => { const endPoint = new kldAffine.Point2D(x, y).transform(matrix); setDragCoords([ { startPoint: { x: ix, y: iy }, endPoint } ]); }; const onDragEnd = (event) => { if (dragNode && enteredNode && canLinkNode) { onNodeLink(event, dragNode, enteredNode, dragPort); } setDragNode(null); setDragPort(null); setEnteredNode(null); setDragCoords(null); }; const onEnter = (event, node) => { setEnteredNode(node); if (dragNode && node) { const canLink = onNodeLinkCheck(event, dragNode, node, dragPort); const result = (canLink === undefined || canLink) && (dragNode.parent === node.parent || dragType === 'node'); setCanLinkNode(result); } }; const onLeave = () => { setEnteredNode(null); setCanLinkNode(null); }; return { dragCoords, canLinkNode, dragNode, dragPort, enteredNode, onDragStart, onDrag, onDragEnd, onEnter, onLeave }; }; const limit = (scale, min, max) => scale < max ? (scale > min ? scale : min) : max; const useZoom = ({ disabled = false, zoom = 1, minZoom = -0.5, maxZoom = 1, onZoomChange }) => { const [factor, setFactor] = react.useState(zoom - 1); const svgRef = react.useRef(null); reactUseGesture.useGesture({ onPinch: ({ offset: [d], event }) => { event.preventDefault(); // TODO: Set X/Y on center of zoom const next = limit(d / 100, minZoom, maxZoom); setFactor(next); onZoomChange(next + 1); } }, { enabled: !disabled, domTarget: svgRef, eventOptions: { passive: false } }); const setZoom = (f) => { const next = limit(f, minZoom, maxZoom); setFactor(next); onZoomChange(next + 1); }; const zoomIn = () => { setZoom(factor + 0.1); }; const zoomOut = () => { setZoom(factor - 0.1); }; return { svgRef, zoom: factor + 1, setZoom, zoomIn, zoomOut }; }; const CanvasContext = react.createContext({}); const CanvasProvider = ({ selections, onNodeLink, readonly, children, nodes, edges, maxHeight, fit, maxWidth, direction, layoutOptions, pannable, center, 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, center, fit, layoutOptions, zoom: zoomProps.zoom, setZoom: zoomProps.setZoom, onLayoutChange }); const dragProps = useEdgeDrag({ onNodeLink, onNodeLinkCheck }); return (jsxRuntime.jsx(CanvasContext.Provider, Object.assign({ value: Object.assign(Object.assign(Object.assign({ selections, readonly, pannable }, layoutProps), zoomProps), dragProps) }, { children: children }), void 0)); }; const useCanvas = () => { const context = react.useContext(CanvasContext); if (context === undefined) { throw new Error('`useCanvas` hook must be used within a `CanvasContext` component'); } return context; }; /** * Checks if the node can be linked or not. */ function checkNodeLinkable(curNode, enteredNode, canLinkNode) { if (canLinkNode === null || !enteredNode) { return null; } if (!enteredNode || !curNode) { return false; } // TODO: Revisit how to do self-linking better... if (canLinkNode === false && enteredNode.id === curNode.id) { return false; } return true; } /** * Given various dimensions and positions, create a matrix * used for determining position. */ function getCoords({ zoom, scrollXY, layout, containerWidth, containerHeight, containerRef }) { const { top, left } = containerRef.current.getBoundingClientRect(); const offsetX = scrollXY[0] - containerRef.current.scrollLeft; const offsetY = scrollXY[1] - containerRef.current.scrollTop; const tx = (containerWidth - layout.width * zoom) / 2 + offsetX + left; const ty = (containerHeight - layout.height * zoom) / 2 + offsetY + top; return new kldAffine.Matrix2D().translate(tx, ty).scale(zoom).inverse(); } /** * Given a nodeId to find, a list of nodes to check against, and an optional parentId of the node * find the node from the list of nodes */ function findNestedNode(nodeId, children, parentId) { if (!nodeId || !children) { return {}; } const foundNode = children.find(n => n.id === nodeId); if (foundNode) { return foundNode; } if (parentId) { const parentNode = children.find(n => n.id === parentId); if (parentNode === null || parentNode === void 0 ? void 0 : parentNode.children) { return findNestedNode(nodeId, parentNode.children, parentId); } } // Check for nested children const nodesWithChildren = children.filter(n => { var _a; return (_a = n.children) === null || _a === void 0 ? void 0 : _a.length; }); // Iterate over all nested nodes and check if any of them contain the node for (const n of nodesWithChildren) { const foundChild = findNestedNode(nodeId, n.children, parentId); if (foundChild && Object.keys(foundChild).length) { return foundChild; } } return {}; } /** * Return the layout node that is currently being dragged on the Canvas */ function getDragNodeData(dragNode, children = []) { if (!dragNode) { return {}; } const { parent } = dragNode; if (!parent) { return (children === null || children === void 0 ? void 0 : children.find(n => n.id === dragNode.id)) || {}; } return findNestedNode(dragNode.id, children, parent); } const useNodeDrag = ({ x, y, height, width, onDrag, onDragEnd, onDragStart, node, disabled }) => { const initial = [width / 2 + x, height + y]; const targetRef = react.useRef(null); const { zoom, scrollXY, layout, containerWidth, containerRef, containerHeight } = useCanvas(); const bind = reactUseGesture.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, layout, scrollXY, containerHeight, containerWidth }); // memo will hold the difference between the // first point of impact and the origin const memo = [matrix]; onDragStart(Object.assign(Object.assign({}, state), { memo }), initial, node); return memo; } onDrag(state, initial, node); if (state.last) { targetRef.current = null; onDragEnd(state, initial, node); } }, { enabled: !disabled, triggerAllEvents: true, threshold: 5 }); return bind; }; function styleInject(css, ref) { if ( ref === void 0 ) ref = {}; var insertAt = ref.insertAt; if (!css || typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css_248z = ".Port-module_port__30o1q {\n stroke: #0d0e17;\n fill: #3e405a;\n stroke-width: 2px;\n shape-rendering: geometricPrecision;\n pointer-events: none;\n}\n\n.Port-module_clicker__ZivO1 {\n opacity: 0;\n}\n\n.Port-module_clicker__ZivO1:not(.Port-module_disabled__1SC1t) {\n cursor: crosshair;\n }\n"; var css = {"port":"Port-module_port__30o1q","clicker":"Port-module_clicker__ZivO1","disabled":"Port-module_disabled__1SC1t"}; styleInject(css_248z); const Port = react.forwardRef(({ id, x, y, rx, ry, disabled, style, children, properties, offsetX, offsetY, className, active, onDrag = () => undefined, onDragStart = () => undefined, onDragEnd = () => undefined, onEnter = () => undefined, onLeave = () => undefined, onClick = () => undefined }, ref) => { const { readonly } = useCanvas(); const [isDragging, setIsDragging] = react.useState(false); const [isHovered, setIsHovered] = react.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: disabled || readonly || (properties === null || properties === void 0 ? void 0 : properties.disabled), node: properties, onDrag, onDragStart: onDragStartInternal, onDragEnd: onDragEndInternal }); if (properties.hidden) { return null; } const isDisabled = properties.disabled || disabled; const portChildProps = { port: properties, isDragging, isHovered, isDisabled, x, y, rx, ry, offsetX, offsetY }; return (jsxRuntime.jsxs("g", Object.assign({ id: id }, { children: [jsxRuntime.jsx("rect", Object.assign({}, bind(), { ref: ref, height: properties.height + 14, width: properties.width + 14, x: newX - 7, y: newY - 7, className: classNames__default['default'](css.clicker, { [css.disabled]: isDisabled }), onMouseEnter: (event) => { event.stopPropagation(); setIsHovered(true); onEnter(event, properties); }, onMouseLeave: (event) => { event.stopPropagation(); setIsHovered(false); onLeave(event, properties); }, onClick: (event) => { event.stopPropagation(); onClick(event, properties); } }), void 0), jsxRuntime.jsx(framerMotion.motion.rect, { style: style, className: classNames__default['default'](css.port, className, properties === null || properties === void 0 ? void 0 : properties.className), height: properties.height, width: properties.width, rx: rx, ry: ry, initial: { scale: 0, opacity: 0, x: newX, y: newY }, animate: { x: newX, y: newY, scale: (isDragging || active || isHovered) && !isDisabled ? 1.5 : 1, opacity: 1 } }, `${x}-${y}`), children && (jsxRuntime.jsx(react.Fragment, { children: typeof children === 'function' ? children(portChildProps) : children }, void 0))] }), void 0)); }); var css_248z$1 = ".Label-module_text__126dD {\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 -ms-user-select: none;\n user-select: none;\n}\n"; var css$1 = {"text":"Label-module_text__126dD"}; styleInject(css_248z$1); const Label = ({ text, x, y, style, className, originalText }) => (jsxRuntime.jsxs("g", Object.assign({ transform: `translate(${x}, ${y})` }, { children: [jsxRuntime.jsx("title", { children: originalText }, void 0), jsxRuntime.jsx("text", Object.assign({ className: classNames__default['default'](css$1.text, className), style: style }, { children: text }), void 0)] }), void 0)); var css_248z$2 = ".Remove-module_deleteX__2x11N {\n stroke: black;\n pointer-events: none;\n}\n\n.Remove-module_container__19Jsk {\n will-change: transform, opacity;\n}\n\n.Remove-module_drop__2AwP8 {\n cursor: pointer;\n opacity: 0;\n}\n\n.Remove-module_rect__3vmn2 {\n shape-rendering: geometricPrecision;\n fill: #ff005d;\n border-radius: 2px;\n pointer-events: none;\n}\n"; var css$2 = {"deleteX":"Remove-module_deleteX__2x11N","container":"Remove-module_container__19Jsk","drop":"Remove-module_drop__2AwP8","rect":"Remove-module_rect__3vmn2"}; styleInject(css_248z$2); const Remove = ({ size = 15, className, hidden, x, y, onClick = () => undefined, onEnter = () => undefined, onLeave = () => undefined }) => { if (hidden) { return null; } const half = size / 2; const translateX = x - half; const translateY = y - half; return (jsxRuntime.jsxs(framerMotion.motion.g, Object.assign({ className: classNames__default['default'](className, css$2.container), initial: { scale: 0, opacity: 0, translateX, translateY }, animate: { scale: 1, opacity: 1, translateX, translateY }, whileHover: { scale: 1.2 }, whileTap: { scale: 0.8 } }, { children: [jsxRuntime.jsx("rect", { height: size * 1.5, width: size * 1.5, className: css$2.drop, onMouseEnter: onEnter, onMouseLeave: onLeave, onClick: (event) => { event.preventDefault(); event.stopPropagation(); onClick(event); } }, void 0), jsxRuntime.jsx("rect", { height: size, width: size, className: css$2.rect }, void 0), jsxRuntime.jsx("line", { x1: "2", y1: size - 2, x2: size - 2, y2: "2", className: css$2.deleteX, strokeWidth: "1" }, void 0), jsxRuntime.jsx("line", { x1: "2", y1: "2", x2: size - 2, y2: size - 2, className: css$2.deleteX, strokeWidth: "1" }, void 0)] }), void 0)); }; /** * Center helper. * Ref: https://github.com/wbkd/react-flow/blob/main/src/components/Edges/utils.ts#L18 */ 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]; } /** * Path helper utils. * Ref: https://github.com/wbkd/react-flow/blob/main/src/components/Edges/BezierEdge.tsx#L19 */ function getBezierPath({ sourceX, sourceY, sourcePosition = 'bottom', targetX, targetY, targetPosition = 'top' }) { const leftAndRight = ['left', 'right']; const [centerX, centerY] = getBezierCenter({ sourceX, sourceY, targetX, targetY }); let path = `M${sourceX},${sourceY} C${sourceX},${centerY} ${targetX},${centerY} ${targetX},${targetY}`; if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) { path = `M${sourceX},${sourceY} C${centerX},${sourceY} ${centerX},${targetY} ${targetX},${targetY}`; } else if (leftAndRight.includes(targetPosition)) { path = `M${sourceX},${sourceY} C${sourceX},${targetY} ${sourceX},${targetY} ${targetX},${targetY}`; } else if (leftAndRight.includes(sourcePosition)) { path = `M${sourceX},${sourceY} C${targetX},${sourceY} ${targetX},${sourceY} ${targetX},${targetY}`; } return path; } /** * Calculate actual center for a path element. */ 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 }; } /** * Get the angle for the path. */ 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; } /** * Get the center for the path element. */ function getPathCenter(pathElm, firstPoint, lastPoint) { if (!pathElm) { return null; } const angle = getAngle(firstPoint, lastPoint); const point = getCenter(pathElm); return Object.assign(Object.assign({}, point), { angle }); } var css_248z$3 = ".Add-module_plus__30sKV {\n stroke: black;\n pointer-events: none;\n}\n\n.Add-module_container__1RcJg {\n will-change: transform, opacity;\n}\n\n.Add-module_drop__W1fxu {\n cursor: pointer;\n opacity: 0;\n}\n\n.Add-module_rect__3JiVn {\n shape-rendering: geometricPrecision;\n fill: #46FECB;\n border-radius: 2px;\n pointer-events: none;\n}\n"; var css$3 = {"plus":"Add-module_plus__30sKV","container":"Add-module_container__1RcJg","drop":"Add-module_drop__W1fxu","rect":"Add-module_rect__3JiVn"}; styleInject(css_248z$3); const Add = ({ x, y, className, size = 15, hidden = true, onEnter = () => undefined, onLeave = () => undefined, onClick = () => undefined }) => { if (hidden) { return null; } const half = size / 2; const translateX = x - half; const translateY = y - half; return (jsxRuntime.jsxs(framerMotion.motion.g, Object.assign({ className: classNames__default['default'](className, css$3.container), initial: { scale: 0, opacity: 0, translateX, translateY }, animate: { scale: 1, opacity: 1, translateX, translateY }, whileHover: { scale: 1.2 }, whileTap: { scale: 0.8 } }, { children: [jsxRuntime.jsx("rect", { height: size * 2, width: size * 2, className: css$3.drop, onClick: (event) => { event.preventDefault(); event.stopPropagation(); onClick(event); }, onMouseEnter: onEnter, onMouseLeave: onLeave }, void 0), jsxRuntime.jsx("rect", { height: size, width: size, className: css$3.rect }, void 0), jsxRuntime.jsx("line", { x1: "2", x2: size - 2, y1: half, y2: half, className: css$3.plus, strokeWidth: "1" }, void 0), jsxRuntime.jsx("line", { x1: half, x2: half, y1: "2", y2: size - 2, className: css$3.plus, strokeWidth: "1" }, void 0)] }), void 0)); }; var css_248z$4 = ".Edge-module_edge__RbmGh.Edge-module_disabled__20Zof {\n pointer-events: none;\n }\n .Edge-module_edge__RbmGh:not(.Edge-module_selectionDisabled__MVwAS):not(.Edge-module_disabled__20Zof):hover .Edge-module_path__u3LT2 {\n stroke: #a5a9e2;\n }\n .Edge-module_edge__RbmGh:not(.Edge-module_selectionDisabled__MVwAS):not(.Edge-module_disabled__20Zof):hover .Edge-module_path__u3LT2.Edge-module_active__2dt5E {\n stroke: #46fecb;\n }\n .Edge-module_edge__RbmGh:not(.Edge-module_selectionDisabled__MVwAS):not(.Edge-module_disabled__20Zof):hover .Edge-module_path__u3LT2.Edge-module_deleteHovered__ZG2og {\n stroke: #ff005d;\n stroke-dasharray: 4 2;\n }\n .Edge-module_edge__RbmGh:not(.Edge-module_selectionDisabled__MVwAS):not(.Edge-module_disabled__20Zof) .Edge-module_clicker__3cdd0 {\n cursor: pointer;\n }\n .Edge-module_edge__RbmGh .Edge-module_path__u3LT2 {\n fill: transparent;\n stroke: #485a74;\n pointer-events: none;\n shape-rendering: geometricPrecision;\n stroke-width: 1pt;\n }\n .Edge-module_edge__RbmGh .Edge-module_clicker__3cdd0 {\n fill: none;\n stroke: transparent;\n stroke-width: 15px;\n }\n .Edge-module_edge__RbmGh .Edge-module_clicker__3cdd0:focus {\n outline: none;\n }\n"; var css$4 = {"edge":"Edge-module_edge__RbmGh","disabled":"Edge-module_disabled__20Zof","selectionDisabled":"Edge-module_selectionDisabled__MVwAS","path":"Edge-module_path__u3LT2","active":"Edge-module_active__2dt5E","deleteHovered":"Edge-module_deleteHovered__ZG2og","clicker":"Edge-module_clicker__3cdd0"}; styleInject(css_248z$4); const Edge = ({ sections, properties, labels, className, disabled, style, children, add = jsxRuntime.jsx(Add, {}, void 0), remove = jsxRuntime.jsx(Remove, {}, void 0), label = jsxRuntime.jsx(Label, {}, void 0), onClick = () => undefined, onKeyDown = () => undefined, onEnter = () => undefined, onLeave = () => undefined, onRemove = () => undefined, onAdd = () => undefined }) => { const pathRef = react.useRef(null); const [deleteHovered, setDeleteHovered] = react.useState(false); const [center, setCenter] = react.useState(null); const { selections, readonly } = useCanvas(); const isActive = (selections === null || selections === void 0 ? void 0 : selections.length) ? selections.includes(properties === null || properties === void 0 ? void 0 : properties.id) : false; // The "d" attribute defines a path to be drawn. See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d const d = react.useMemo(() => { if (!(sections === null || sections === void 0 ? void 0 : sections.length)) { return null; } // Handle bend points that elk gives // us separately from drag points if (sections[0].bendPoints) { const points = sections ? [ sections[0].startPoint, ...(sections[0].bendPoints || []), sections[0].endPoint ] : []; const pathFn = d3Shape.line() .x((d) => d.x) .y((d) => d.y) .curve(d3Shape.curveBundle.beta(1)); 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 }); } }, [sections]); react.useEffect(() => { if ((sections === null || sections === void 0 ? void 0 : sections.length) > 0) { setCenter(getPathCenter(pathRef.current, sections[0].startPoint, sections[0].endPoint)); } }, [sections]); const edgeChildProps = { edge: properties, center, pathRef }; return (jsxRuntime.jsxs("g", Object.assign({ className: classNames__default['default'](css$4.edge, { [css$4.disabled]: disabled || (properties === null || properties === void 0 ? void 0 : properties.disabled), [css$4.selectionDisabled]: properties === null || properties === void 0 ? void 0 : properties.selectionDisabled }) }, { children: [jsxRuntime.jsx("path", { ref: pathRef, style: style, className: classNames__default['default'](className, properties === null || properties === void 0 ? void 0 : properties.className, css$4.path, { [css$4.active]: isActive, [css$4.deleteHovered]: deleteHovered }), d: d, markerEnd: "url(#end-arrow)" }, void 0), jsxRuntime.jsx("path", { className: css$4.clicker, d: d, tabIndex: -1, onClick: (event) => { event.preventDefault(); event.stopPropagation(); onClick(event, properties); }, onKeyDown: (event) => { event.preventDefault(); event.stopPropagation(); onKeyDown(event, properties); }, onMouseEnter: (event) => { event.stopPropagation(); onEnter(event, properties); }, onMouseLeave: (event) => { event.stopPropagation(); onLeave(event, properties); } }, void 0), children && (jsxRuntime.jsx(react.Fragment, { children: typeof children === 'function' ? children(edgeChildProps) : children }, void 0)), (labels === null || labels === void 0 ? void 0 : labels.length) > 0 && labels.map((l, index) => (jsxRuntime.jsx(rdk.CloneElement, Object.assign({ element: label }, l), index))), !disabled && center && !readonly && remove && (jsxRuntime.jsx(rdk.CloneElement, Object.assign({ element: remove }, center, { hidden: remove.props.hidden !== undefined ? remove.props.hidden : !isActive, onClick: (event) => { event.preventDefault(); event.stopPropagation(); onRemove(event, properties); setDeleteHovered(false); }, onEnter: () => setDeleteHovered(true), onLeave: () => setDeleteHovered(false) }), void 0)), !disabled && center && !readonly && add && (jsxRuntime.jsx(rdk.CloneElement, Object.assign({ element: add }, center, { onClick: (event) => { event.preventDefault(); event.stopPropagation(); onAdd(event, properties); } }), void 0))] }), void 0)); }; var css_248z$5 = ".Node-module_rect__1Eal3 {\n fill: #2b2c3e;\n transition: stroke 100ms ease-in-out;\n stroke: #475872;\n shape-rendering: geometricPrecision;\n stroke-width: 1pt;\n}\n\n .Node-module_rect__1Eal3:not(.Node-module_selectionDisabled__2zufG):not(.Node-module_disabled__bZzf8) {\n cursor: pointer;\n }\n\n .Node-module_rect__1Eal3:not(.Node-module_selectionDisabled__2zufG):not(.Node-module_disabled__bZzf8):hover {\n stroke: #a5a9e2;\n }\n\n .Node-module_rect__1Eal3:not(.Node-module_selectionDisabled__2zufG):not(.Node-module_disabled__bZzf8).Node-module_dragging__24Avg {\n stroke: #a5a9e2;\n }\n\n .Node-module_rect__1Eal3:not(.Node-module_selectionDisabled__2zufG):not(.Node-module_disabled__bZzf8).Node-module_active__1b2HC {\n stroke: #46fecb;\n }\n\n .Node-module_rect__1Eal3:not(.Node-module_selectionDisabled__2zufG):not(.Node-module_disabled__bZzf8).Node-module_unlinkable__p6w5x {\n stroke: #ff005d;\n }\n\n .Node-module_rect__1Eal3:not(.Node-module_selectionDisabled__2zufG):not(.Node-module_disabled__bZzf8).Node-module_deleteHovered__36-dV {\n stroke: #ff005d !important;\n stroke-dasharray: 4 2;\n }\n\n .Node-module_rect__1Eal3:focus {\n outline: none;\n }\n\n .Node-module_rect__1Eal3.Node-module_children__1kq65 {\n fill: transparent;\n stroke: #475872;\n }\n"; var css$5 = {"rect":"Node-module_rect__1Eal3","selectionDisabled":"Node-module_selectionDisabled__2zufG","disabled":"Node-module_disabled__bZzf8","dragging":"Node-module_dragging__24Avg","active":"Node-module_active__1b2HC","unlinkable":"Node-module_unlinkable__p6w5x","deleteHovered":"Node-module_deleteHovered__36-dV","children":"Node-module_children__1kq65"}; styleInject(css_248z$5); const Node = ({ id, x, y, ports, labels, height, width, properties, animated, className, rx = 2, ry = 2, offsetX = 0, offsetY = 0, icon, disabled, style, children, nodes, edges, dragType = 'multiportOnly', dragCursor = 'crosshair', childEdge = jsxRuntime.jsx(Edge, {}, void 0), childNode = jsxRuntime.jsx(Node, {}, void 0), remove = jsxRuntime.jsx(Remove, {}, void 0), port = jsxRuntime.jsx(Port, {}, void 0), label = jsxRuntime.jsx(Label, {}, void 0), onRemove = () => undefined, onDrag = () => undefined, onDragStart = () => undefined, onDragEnd = () => undefined, onClick = () => undefined, onKeyDown = () => undefined, onEnter = () => undefined, onLeave = () => undefined }) => { const controls = framerMotion.useAnimation(); const _a = useCanvas(), { canLinkNode, enteredNode, selections, readonly } = _a, canvas = __rest(_a, ["canLinkNode", "enteredNode", "selections", "readonly"]); const [deleteHovered, setDeleteHovered] = react.useState(false); const [dragging, setDragging] = react.useState(false); const isActive = (selections === null || selections === void 0 ? void 0 : selections.length) ? selections.includes(properties.id) : null; const isNodeDrag = id.includes('node-drag'); const newX = x + offsetX; const newY = y + offsetY; const isLinkable = checkNodeLinkable(properties, enteredNode, canLinkNode); const isMultiPort = dragType === 'multiportOnly' && (ports === null || ports === void 0 ? void 0 : ports.filter(p => { var _a; return !((_a = p.properties) === null || _a === void 0 ? void 0 : _a.hidden); }).length) > 1; const getDragType = (hasPort) => { let activeDragType = null; if (!hasPort) { if (dragType === 'all' || dragType === 'node') { activeDragType = 'node'; } else if (!isMultiPort) { activeDragType = 'port'; } } else {