UNPKG

@delove/reaflow

Version:

Node-based Visualizations for React

1 lines 169 kB
{"version":3,"file":"index.umd.cjs","sources":["../src/types.ts","../src/layout/utils.ts","../src/layout/elkLayout.ts","../src/layout/useLayout.ts","../src/utils/useEdgeDrag.ts","../src/utils/useZoom.ts","../src/utils/CanvasProvider.tsx","../src/utils/helpers.ts","../src/utils/useNodeDrag.ts","../src/symbols/Port/Port.tsx","../src/symbols/Label/Label.tsx","../src/symbols/Remove/Remove.tsx","../src/symbols/Edge/utils.ts","../src/symbols/Add/Add.tsx","../src/symbols/Edge/Edge.tsx","../src/symbols/Node/Node.tsx","../src/symbols/Arrow/Arrow.tsx","../src/symbols/Arrow/MarkerArrow.tsx","../src/Canvas.tsx","../src/symbols/Icon/Icon.tsx","../src/helpers/crudHelpers.ts","../src/helpers/useSelection.ts","../src/helpers/useUndo.ts","../src/helpers/useProximity.ts","../src/helpers/graphHelpers.ts"],"sourcesContent":["import { ElkNodeLayoutOptions } from './layout';\n\nexport enum CanvasPosition {\n CENTER = 'center',\n TOP = 'top',\n LEFT = 'left',\n RIGHT = 'right',\n BOTTOM = 'bottom'\n}\n\nexport interface NodeData<T = any> {\n /**\n * Unique ID for the node.\n */\n id: string;\n\n /**\n * Whether the node is disabled or not.\n */\n disabled?: boolean;\n\n /**\n * Text label for the node.\n */\n text?: any;\n\n /**\n * Optional height attribute. If not passed with calculate\n * default sizes using text.\n */\n height?: number;\n\n /**\n * Optional width attribute. If not passed with calculate\n * default sizes using text.\n */\n width?: number;\n\n /**\n * Parent node id for nesting.\n */\n parent?: string;\n\n /**\n * List of ports.\n */\n ports?: PortData[];\n\n /**\n * Icon for the node.\n */\n icon?: IconData;\n\n /**\n * Padding for the node.\n */\n nodePadding?: number | [number, number] | [number, number, number, number];\n\n /**\n * Data for the node.\n */\n data?: T;\n\n /**\n * CSS classname for the node.\n */\n className?: string;\n\n /**\n * ELK layout options.\n */\n layoutOptions?: ElkNodeLayoutOptions;\n\n /**\n * Whether the node can be clicked.\n */\n selectionDisabled?: boolean;\n}\n\nexport interface LayoutNodeData extends NodeData {\n x: number;\n y: number;\n children?: LayoutNodeData[];\n}\n\nexport interface IconData {\n /**\n * URL for the icon.\n */\n url: string;\n\n /**\n * Height of the icon.\n */\n height: number;\n\n /**\n * Width of the icon.\n */\n width: number;\n}\n\nexport interface EdgeData<T = any> {\n /**\n * Unique ID of the edge.\n */\n id: string;\n\n /**\n * Whether the edge is disabled or not.\n */\n disabled?: boolean;\n\n /**\n * Text label for the edge.\n */\n text?: any;\n\n /**\n * ID of the from node.\n */\n from?: string;\n\n /**\n * ID of the to node.\n */\n to?: string;\n\n /**\n * Optional ID of the from port.\n */\n fromPort?: string;\n\n /**\n * Optional ID of the to port.\n */\n toPort?: string;\n\n /**\n * Data about the edge.\n */\n data?: T;\n\n /**\n * CSS class name for the edge (\"path\" element).\n */\n className?: string;\n\n /**\n * CSS class name for the edge (main \"g\" element).\n */\n containerClassName?: string;\n\n /**\n * Optional arrow head type.\n */\n arrowHeadType?: any;\n\n /**\n * Parent of the edge for nesting.\n */\n parent?: string;\n\n /**\n * Whether the edge can be clicked.\n */\n selectionDisabled?: boolean;\n}\n\nexport type PortSide = 'NORTH' | 'SOUTH' | 'EAST' | 'WEST';\n\nexport interface PortData {\n /**\n * Unique ID of the port.\n */\n id: string;\n\n /**\n * Port is disabled.\n */\n disabled?: boolean;\n\n /**\n * Height of the port.\n */\n height: number;\n\n /**\n * Width of the port.\n */\n width: number;\n\n /**\n * Whether the port is visually hidden or not.\n */\n hidden?: boolean;\n\n /**\n * Classname for the port.\n */\n className?: string;\n\n /**\n * Alignment of the port.\n */\n alignment?: 'CENTER';\n\n /**\n * Side the port is located.\n */\n side: PortSide;\n}\n","import calculateSize from 'calculate-size';\nimport { LayoutNodeData, NodeData } from '../types';\nimport ellipsize from 'ellipsize';\n\nconst MAX_CHAR_COUNT = 35;\nconst MIN_NODE_WIDTH = 50;\nconst DEFAULT_NODE_HEIGHT = 50;\nconst NODE_PADDING = 30;\nconst ICON_PADDING = 10;\n\nexport function measureText(text: string) {\n let result = { height: 0, width: 0 };\n\n if (text) {\n // Reference: https://github.com/reaviz/reaflow/pull/229\n // @ts-ignore\n const fn = typeof calculateSize === 'function' ? calculateSize : calculateSize.default;\n result = fn(text, {\n font: 'Arial, sans-serif',\n fontSize: '14px'\n });\n }\n\n return result;\n}\n\nexport function parsePadding(padding: NodeData['nodePadding']) {\n let top = 50;\n let right = 50;\n let bottom = 50;\n let left = 50;\n\n if (Array.isArray(padding)) {\n if (padding.length === 2) {\n top = padding[0];\n bottom = padding[0];\n left = padding[1];\n right = padding[1];\n } else if (padding.length === 4) {\n top = padding[0];\n right = padding[1];\n bottom = padding[2];\n left = padding[3];\n }\n } else if (padding !== undefined) {\n top = padding;\n right = padding;\n bottom = padding;\n left = padding;\n }\n\n return {\n top,\n right,\n bottom,\n left\n };\n}\n\nexport function formatText(node: NodeData) {\n const text = node.text ? ellipsize(node.text, MAX_CHAR_COUNT) : node.text;\n\n const labelDim = measureText(text);\n const nodePadding = parsePadding(node.nodePadding);\n\n let width = node.width;\n if (width === undefined) {\n if (text && node.icon) {\n width = labelDim.width + node.icon.width + NODE_PADDING + ICON_PADDING;\n } else {\n if (text) {\n width = labelDim.width + NODE_PADDING;\n } else if (node.icon) {\n width = node.icon.width + NODE_PADDING;\n }\n\n width = Math.max(width, MIN_NODE_WIDTH);\n }\n }\n\n let height = node.height;\n if (height === undefined) {\n if (text && node.icon) {\n height = labelDim.height + node.icon.height;\n } else if (text) {\n height = labelDim.height + NODE_PADDING;\n } else if (node.icon) {\n height = node.icon.height + NODE_PADDING;\n }\n\n height = Math.max(height, DEFAULT_NODE_HEIGHT);\n }\n\n return {\n text,\n originalText: node.text,\n width,\n height,\n nodePadding,\n labelHeight: labelDim.height,\n labelWidth: labelDim.width\n };\n}\n\n/**\n * Finds a node in a tree of nodes\n * @param nodes - The nodes to search through\n * @param nodeId - The id of the node to find\n * @returns The node if found, undefined otherwise\n */\nexport const findNode = (nodes: LayoutNodeData[], nodeId: string): any | undefined => {\n for (const node of nodes) {\n if (node.id === nodeId) {\n return node;\n }\n if (node.children) {\n const foundNode = findNode(node.children, nodeId);\n if (foundNode) {\n return foundNode;\n }\n }\n }\n return undefined;\n};\n\n/**\n * Finds the number of nested children a node has\n * @param node - The node to search through\n * @returns The number of children\n */\nexport const getChildCount = (node: LayoutNodeData): number => {\n return (\n node.children?.reduce((acc, child) => {\n if (child.children) {\n return acc + 1 + getChildCount(child);\n }\n return acc + 1;\n }, 0) ?? 0\n );\n};\n\n/**\n * Calculates the zoom for a group of nodes when fitting to the viewport\n * @param nodes - The nodes to calculate the zoom for\n * @param viewportWidth - The width of the viewport\n * @param viewportHeight - The height of the viewport\n * @param maxViewportCoverage - The maximum percentage of the viewport that the node group will take up\n * @param minViewportCoverage - The minimum percentage of the viewport that the node group will take up\n * @returns The zoom\n */\nexport const calculateZoom = ({ nodes, viewportWidth, viewportHeight, maxViewportCoverage = 0.9, minViewportCoverage = 0.2 }: { nodes: LayoutNodeData[]; viewportWidth: number; viewportHeight: number; maxViewportCoverage?: number; minViewportCoverage?: number }) => {\n const maxChildren = Math.max(\n 0,\n nodes.map(getChildCount).reduce((acc, curr) => acc + curr, 0)\n );\n const boundingBox = getNodesBoundingBox(nodes);\n const boundingBoxWidth = boundingBox.x1 - boundingBox.x0;\n const boundingBoxHeight = boundingBox.y1 - boundingBox.y0;\n\n // calculate the maximum zoom to ensure no single node takes up more than 20% of the viewport\n const maxNodeWidth = Math.max(...nodes.map((node) => node.width));\n const maxNodeHeight = Math.max(...nodes.map((node) => node.height));\n // if a node has children, let it take up an extra 10% per child\n const maxNodeZoomX = ((0.2 + maxChildren * 0.1) * viewportWidth) / maxNodeWidth;\n const maxNodeZoomY = ((0.2 + maxChildren * 0.1) * viewportHeight) / maxNodeHeight;\n const maxNodeZoom = Math.min(maxNodeZoomX, maxNodeZoomY);\n\n const viewportCoverage = Math.max(Math.min(maxViewportCoverage, maxNodeZoom), minViewportCoverage);\n\n const updatedHorizontalZoom = (viewportCoverage * viewportWidth) / boundingBoxWidth;\n const updatedVerticalZoom = (viewportCoverage * viewportHeight) / boundingBoxHeight;\n const updatedZoom = Math.min(updatedHorizontalZoom, updatedVerticalZoom, maxNodeZoom);\n\n return updatedZoom;\n};\n\n/**\n * Calculates the scroll position for the canvas when fitting nodes to the viewport - assumes the chart is centered\n * @param nodes - The nodes to calculate the zoom and position for\n * @param viewportWidth - The width of the viewport\n * @param viewportHeight - The height of the viewport\n * @param canvasWidth - The width of the canvas\n * @param canvasHeight - The height of the canvas\n * @param chartWidth - The width of the chart\n * @param chartHeight - The height of the chart\n * @param zoom - The zoom level of the canvas\n * @returns The scroll position\n */\nexport const calculateScrollPosition = ({ nodes, viewportWidth, viewportHeight, canvasWidth, canvasHeight, chartWidth, chartHeight, zoom }: { nodes: LayoutNodeData[]; viewportWidth: number; viewportHeight: number; canvasWidth: number; canvasHeight: number; chartWidth: number; chartHeight: number; zoom: number }): [number, number] => {\n const { x0, y0, x1, y1 } = getNodesBoundingBox(nodes);\n const boundingBoxWidth = (x1 - x0) * zoom;\n const boundingBoxHeight = (y1 - y0) * zoom;\n\n // the chart is centered so we can assume the x and y positions\n const chartPosition = {\n x: (canvasWidth - chartWidth * zoom) / 2,\n y: (canvasHeight - chartHeight * zoom) / 2\n };\n\n const boxXPosition = chartPosition.x + x0 * zoom;\n const boxYPosition = chartPosition.y + y0 * zoom;\n\n const boxCenterXPosition = boxXPosition + boundingBoxWidth / 2;\n const boxCenterYPosition = boxYPosition + boundingBoxHeight / 2;\n\n // scroll to the spot that centers the node in the viewport\n const scrollX = boxCenterXPosition - viewportWidth / 2;\n const scrollY = boxCenterYPosition - viewportHeight / 2;\n\n return [scrollX, scrollY];\n};\n\n/**\n * Calculates the bounding box of a group of nodes\n * @param nodes - The nodes to calculate the bounding box for\n * @returns The bounding box\n */\nexport const getNodesBoundingBox = (nodes: LayoutNodeData[]) => {\n return nodes.reduce(\n (acc, node) => ({\n x0: Math.min(acc.x0, node.x),\n y0: Math.min(acc.y0, node.y),\n x1: Math.max(acc.x1, node.x + node.width),\n y1: Math.max(acc.y1, node.y + node.height)\n }),\n { x0: nodes[0].x, y0: nodes[0].y, x1: nodes[0].x + nodes[0].width, y1: nodes[0].y + nodes[0].height }\n );\n};\n","import { EdgeData, NodeData } from '../types';\nimport ELK, { ElkNode } from 'elkjs/lib/elk.bundled.js';\nimport PCancelable from 'p-cancelable';\nimport { formatText, measureText } from './utils';\n\nexport type CanvasDirection = 'LEFT' | 'RIGHT' | 'DOWN' | 'UP';\n\n/**\n * ELKjs layout options for the Canvas.\n *\n * Unfortunately, the ELKjs documentation is not straightforward.\n * You'll likely need to take a look at the ELK options reference to see all available options.\n *\n * @see https://github.com/kieler/elkjs#layout-options\n * @see https://www.eclipse.org/elk/reference/options.html\n */\nexport interface ElkCanvasLayoutOptions {\n 'elk.direction'?: CanvasDirection;\n [key: string]: string;\n}\n\n/**\n * ELKjs layout option for a node.\n *\n * TODO add reference link, I don't know what are the available options.\n *\n * @see https://www.eclipse.org/elk/reference/options.html\n */\nexport interface ElkNodeLayoutOptions {\n [key: string]: string;\n\n /**\n * @example [left=12, top=12, right=12, bottom=12]\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-padding.html\n */\n 'elk.padding': string;\n\n /**\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-portConstraints.html\n */\n portConstraints: 'UNDEFINED' | 'FREE' | 'FIXED_SIDE' | 'FIXED_ORDER' | 'FIXED_RATIO' | 'FIXED_POS';\n}\n\n/**\n * ELK layout options applied by default, unless overridden through <Canvas layoutOptions> property.\n *\n * XXX Not to be confounded with ELK \"defaultLayoutOptions\" property, which is meant to be used as fallback, when no layout option is provided.\n *\n * @see https://www.eclipse.org/elk/reference/options.html\n */\nconst defaultLayoutOptions: ElkCanvasLayoutOptions = {\n /**\n * Hints for where node labels are to be placed; if empty, the node label’s position is not modified.\n *\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-nodeLabels-placement.html\n */\n 'elk.nodeLabels.placement': 'INSIDE V_CENTER H_RIGHT',\n\n /**\n * Select a specific layout algorithm.\n *\n * Uses \"layered\" strategy.\n * It emphasizes the direction of edges by pointing as many edges as possible into the same direction.\n * The nodes are arranged in layers, which are sometimes called “hierarchies”,\n * and then reordered such that the number of edge crossings is minimized.\n * Afterwards, concrete coordinates are computed for the nodes and edge bend points.\n *\n * @see https://www.eclipse.org/elk/reference/algorithms.html\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-algorithm.html\n * @see https://www.eclipse.org/elk/reference/algorithms/org-eclipse-elk-layered.html\n */\n 'elk.algorithm': 'org.eclipse.elk.layered',\n\n /**\n * Overall direction of edges: horizontal (right / left) or vertical (down / up).\n *\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-direction.html\n */\n 'elk.direction': 'DOWN',\n\n /**\n * The node order given by the model does not change to produce a better layout.\n * E.g. if node A is before node B in the model this is not changed during crossing minimization.\n * This assumes that the node model order is already respected before crossing minimization. This\n * can be achieved by setting considerModelOrder.strategy to NODES_AND_EDGES.\n *\n * @see https://eclipse.dev/elk/reference/options/org-eclipse-elk-layered-crossingMinimization-forceNodeModelOrder.html\n */\n 'layered.crossingMinimization.forceNodeModelOrder': 'true',\n\n /**\n * Strategy for node layering.\n *\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-layered-layering-strategy.html\n */\n 'org.eclipse.elk.layered.layering.strategy': 'INTERACTIVE',\n\n /**\n * What kind of edge routing style should be applied for the content of a parent node.\n * Algorithms may also set this option to single edges in order to mark them as splines.\n * The bend point list of edges with this option set to SPLINES\n * must be interpreted as control points for a piecewise cubic spline.\n *\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-edgeRouting.html\n */\n 'org.eclipse.elk.edgeRouting': 'ORTHOGONAL',\n\n /**\n * Adds bend points even if an edge does not change direction.\n * If true, each long edge dummy will contribute a bend point to its edges\n * and hierarchy-crossing edges will always get a bend point where they cross hierarchy boundaries.\n * By default, bend points are only added where an edge changes direction.\n *\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-layered-unnecessaryBendpoints.html\n */\n 'elk.layered.unnecessaryBendpoints': 'true',\n\n /**\n * The spacing to be preserved between nodes and edges that are routed next to the node’s layer.\n * For the spacing between nodes and edges that cross the node’s layer ‘spacing.edgeNode’ is used.\n *\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-layered-spacing-edgeNodeBetweenLayers.html\n */\n 'elk.layered.spacing.edgeNodeBetweenLayers': '50',\n\n /**\n * Tells the BK node placer to use a certain alignment (out of its four)\n * instead of the one producing the smallest height, or the combination of all four.\n *\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-layered-nodePlacement-bk-fixedAlignment.html\n */\n 'org.eclipse.elk.layered.nodePlacement.bk.fixedAlignment': 'BALANCED',\n\n /**\n * Strategy for cycle breaking.\n *\n * Cycle breaking looks for cycles in the graph and determines which edges to reverse to break the cycles.\n * Reversed edges will end up pointing to the opposite direction of regular edges\n * (that is, reversed edges will point left if edges usually point right).\n *\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-layered-cycleBreaking-strategy.html\n */\n 'org.eclipse.elk.layered.cycleBreaking.strategy': 'DEPTH_FIRST',\n\n /**\n * Whether this node allows to route self loops inside of it instead of around it.\n *\n * If set to true, this will make the node a compound node if it isn’t already,\n * and will require the layout algorithm to support compound nodes with hierarchical ports.\n *\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-insideSelfLoops-activate.html\n */\n 'org.eclipse.elk.insideSelfLoops.activate': 'true',\n\n /**\n * Whether each connected component should be processed separately.\n *\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-separateConnectedComponents.html\n */\n separateConnectedComponents: 'false',\n\n /**\n * Spacing to be preserved between pairs of connected components.\n * This option is only relevant if ‘separateConnectedComponents’ is activated.\n *\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-spacing-componentComponent.html\n */\n 'spacing.componentComponent': '70',\n\n /**\n * TODO: Should be spacing.baseValue?\n * An optional base value for all other layout options of the ‘spacing’ group.\n * It can be used to conveniently alter the overall ‘spaciousness’ of the drawing.\n * Whenever an explicit value is set for the other layout options, this base value will have no effect.\n * The base value is not inherited, i.e. it must be set for each hierarchical node.\n *\n * @see https://www.eclipse.org/elk/reference/groups/org-eclipse-elk-layered-spacing.html\n */\n spacing: '75',\n\n /**\n * The spacing to be preserved between any pair of nodes of two adjacent layers.\n * Note that ‘spacing.nodeNode’ is used for the spacing between nodes within the layer itself.\n *\n * @see https://www.eclipse.org/elk/reference/options/org-eclipse-elk-layered-spacing-nodeNodeBetweenLayers.html\n */\n 'spacing.nodeNodeBetweenLayers': '70'\n};\n\nfunction mapNode(nodes: NodeData[], edges: EdgeData[], node: NodeData) {\n const { text, width, height, labelHeight, labelWidth, nodePadding, originalText } = formatText(node);\n\n const children = nodes.filter((n) => n.parent === node.id).map((n) => mapNode(nodes, edges, n));\n\n const childEdges = edges.filter((e) => e.parent === node.id).map((e) => mapEdge({ edge: e }));\n\n const nodeLayoutOptions: ElkNodeLayoutOptions = {\n 'elk.padding': `[left=${nodePadding.left}, top=${nodePadding.top}, right=${nodePadding.right}, bottom=${nodePadding.bottom}]`,\n portConstraints: 'FIXED_ORDER',\n ...(node.layoutOptions || {})\n };\n\n return {\n id: node.id,\n height,\n width,\n children,\n edges: childEdges,\n ports: node.ports\n ? node.ports.map((port) => ({\n id: port.id,\n properties: {\n ...port,\n 'port.side': port.side,\n 'port.alignment': port.alignment || 'CENTER'\n }\n }))\n : [],\n layoutOptions: nodeLayoutOptions,\n properties: {\n ...node\n },\n labels: text\n ? [\n {\n width: labelWidth,\n height: -(labelHeight / 2),\n text,\n originalText\n // layoutOptions: { 'elk.nodeLabels.placement': 'INSIDE V_CENTER H_CENTER' }\n }\n ]\n : []\n };\n}\n\nfunction mapEdge({ edge: { data, ...edge }, direction }: { edge: EdgeData; direction?: CanvasDirection }) {\n const labelDim = measureText(edge.text);\n const validEdgeData = data ? { data } : {};\n let labelWidth = labelDim.width / 2;\n\n if (direction === 'LEFT' || direction === 'RIGHT') {\n labelWidth = labelDim.width;\n }\n\n return {\n id: edge.id,\n source: edge.from,\n target: edge.to,\n properties: {\n ...edge\n },\n ...validEdgeData,\n sourcePort: edge.fromPort,\n targetPort: edge.toPort,\n labels: edge.text\n ? [\n {\n width: labelWidth,\n height: -(labelDim.height / 2),\n text: edge.text,\n layoutOptions: {\n 'elk.edgeLabels.placement': 'INSIDE V_CENTER H_CENTER'\n }\n }\n ]\n : []\n };\n}\n\nfunction mapInput({ nodes, edges, direction }: { nodes: NodeData[]; edges: EdgeData[]; direction?: CanvasDirection }) {\n const children = [];\n const mappedEdges = [];\n\n for (const node of nodes) {\n if (!node.parent) {\n const mappedNode = mapNode(nodes, edges, node);\n if (mappedNode !== null) {\n children.push(mappedNode);\n }\n }\n }\n\n for (const edge of edges) {\n if (!edge.parent) {\n const mappedEdge = mapEdge({ edge, direction });\n if (mappedEdge !== null) {\n mappedEdges.push(mappedEdge);\n }\n }\n }\n\n return {\n children,\n edges: mappedEdges\n };\n}\n\nfunction postProcessNode(nodes: any[]): any[] {\n for (const node of nodes) {\n const hasLabels = node.labels?.length > 0;\n\n if (hasLabels && node.properties.icon) {\n const [label] = node.labels;\n label.x = node.properties.icon.width + 25;\n node.properties.icon.x = 25;\n node.properties.icon.y = node.height / 2;\n } else if (hasLabels) {\n const [label] = node.labels;\n label.x = (node.width - label.width) / 2;\n } else if (node.properties.icon) {\n node.properties.icon.x = node.width / 2;\n node.properties.icon.y = node.height / 2;\n }\n\n if (node.children) {\n postProcessNode(node.children);\n }\n }\n\n return nodes;\n}\n\nexport const elkLayout = (nodes: NodeData[], edges: EdgeData[], options: ElkCanvasLayoutOptions) => {\n const graph = new ELK();\n const layoutOptions: ElkCanvasLayoutOptions = {\n ...defaultLayoutOptions,\n ...options\n };\n\n return new PCancelable<ElkNode>((resolve, reject) => {\n graph\n .layout(\n {\n id: 'root',\n ...mapInput({ nodes, edges, direction: layoutOptions?.['elk.direction'] })\n },\n {\n layoutOptions: layoutOptions\n }\n )\n .then((data) => {\n resolve({\n ...data,\n children: postProcessNode(data.children)\n });\n })\n .catch(reject);\n });\n};\n","import { RefObject, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport useDimensions from 'react-cool-dimensions';\nimport isEqual from 'react-fast-compare';\nimport { CanvasPosition, EdgeData, NodeData } from '../types';\nimport { CanvasDirection, ElkCanvasLayoutOptions, elkLayout } from './elkLayout';\nimport { calculateScrollPosition, calculateZoom, findNode } from './utils';\n\nexport interface ElkRoot {\n x?: number;\n y?: number;\n width?: number;\n height?: number;\n children?: any[];\n edges?: any[];\n direction?: CanvasDirection;\n}\n\nexport interface LayoutProps {\n maxHeight: number;\n maxWidth: number;\n nodes: NodeData[];\n edges: EdgeData[];\n pannable: boolean;\n defaultPosition: CanvasPosition;\n fit: boolean;\n zoom: number;\n layoutOptions?: ElkCanvasLayoutOptions;\n direction: CanvasDirection;\n setZoom: (factor: number) => void;\n onLayoutChange: (layout: ElkRoot) => void;\n}\n\nexport interface LayoutResult {\n /**\n * X/Y offset.\n */\n xy: [number, number];\n\n /**\n * Scroll offset.\n */\n scrollXY: [number, number];\n\n /**\n * ELK Layout object.\n */\n layout: ElkRoot;\n\n /**\n * Ref to container div.\n */\n containerRef: RefObject<HTMLDivElement | null>;\n\n /**\n * Height of the svg.\n */\n canvasHeight?: number;\n\n /**\n * Width of the svg.\n */\n canvasWidth?: number;\n\n /**\n * Width of the container div.\n */\n containerWidth?: number;\n\n /**\n * Height of the container div.\n */\n containerHeight?: number;\n\n /**\n * Positions the canvas to the viewport.\n */\n positionCanvas?: (position: CanvasPosition, animated?: boolean) => void;\n\n /**\n * Fit the canvas to the viewport.\n */\n fitCanvas?: (animated?: boolean) => void;\n\n /**\n * Fit a group of nodes to the viewport.\n */\n fitNodes?: (nodeIds: string | string[], animated?: boolean) => void;\n\n /**\n * Scroll to X/Y\n */\n setScrollXY?: (xy: [number, number], animated?: boolean) => void;\n\n observe: (el: HTMLDivElement) => void;\n}\n\nexport const useLayout = ({ maxWidth, maxHeight, nodes = [], edges = [], fit, pannable, defaultPosition, direction, layoutOptions = {}, zoom, setZoom, onLayoutChange }: LayoutProps) => {\n const scrolled = useRef<boolean>(false);\n const ref = useRef<HTMLDivElement>();\n const { observe, width, height } = useDimensions<HTMLDivElement>();\n const [layout, setLayout] = useState<ElkRoot | null>(null);\n const [xy, setXY] = useState<[number, number]>([0, 0]);\n const [scrollXY, setScrollXY] = useState<[number, number]>([0, 0]);\n const canvasHeight = pannable ? maxHeight : height;\n const canvasWidth = pannable ? maxWidth : width;\n\n const scrollToXY = (xy: [number, number], animated = false) => {\n ref.current.scrollTo({ left: xy[0], top: xy[1], behavior: animated ? 'smooth' : 'auto' });\n setScrollXY(xy);\n };\n\n useEffect(() => {\n const promise = elkLayout(nodes, edges, {\n 'elk.direction': direction,\n ...layoutOptions\n });\n\n promise\n .then((result) => {\n if (!isEqual(layout, result)) {\n setLayout(result);\n onLayoutChange(result);\n }\n })\n .catch((err) => {\n if (err.name !== 'CancelError') {\n console.error('Layout Error:', err);\n }\n });\n\n return () => promise.cancel();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [nodes, edges]);\n\n const positionVector = useCallback(\n (position: CanvasPosition) => {\n if (layout) {\n const centerX = (canvasWidth - layout.width * zoom) / 2;\n const centerY = (canvasHeight - layout.height * zoom) / 2;\n switch (position) {\n case CanvasPosition.CENTER:\n setXY([centerX, centerY]);\n break;\n case CanvasPosition.TOP:\n setXY([centerX, 0]);\n break;\n case CanvasPosition.LEFT:\n setXY([0, centerY]);\n break;\n case CanvasPosition.RIGHT:\n setXY([canvasWidth - layout.width * zoom, centerY]);\n break;\n case CanvasPosition.BOTTOM:\n setXY([centerX, canvasHeight - layout.height * zoom]);\n break;\n }\n }\n },\n [canvasWidth, canvasHeight, layout, zoom]\n );\n\n const positionScroll = useCallback(\n (position: CanvasPosition, animated = false) => {\n const scrollCenterX = (canvasWidth - width) / 2;\n const scrollCenterY = (canvasHeight - height) / 2;\n if (pannable) {\n switch (position) {\n case CanvasPosition.CENTER:\n scrollToXY([scrollCenterX, scrollCenterY], animated);\n break;\n case CanvasPosition.TOP:\n scrollToXY([scrollCenterX, 0], animated);\n break;\n case CanvasPosition.LEFT:\n scrollToXY([0, scrollCenterY], animated);\n break;\n case CanvasPosition.RIGHT:\n scrollToXY([canvasWidth - width, scrollCenterY], animated);\n break;\n case CanvasPosition.BOTTOM:\n scrollToXY([scrollCenterX, canvasHeight - height], animated);\n break;\n }\n }\n },\n [canvasWidth, canvasHeight, width, height, pannable]\n );\n\n const positionCanvas = useCallback(\n (position: CanvasPosition, animated = false) => {\n positionVector(position);\n positionScroll(position, animated);\n },\n [positionScroll, positionVector]\n );\n\n useEffect(() => {\n if (scrolled.current && defaultPosition) {\n positionVector(defaultPosition);\n }\n }, [positionVector, zoom, defaultPosition]);\n\n const fitCanvas = useCallback(\n (animated = false) => {\n if (layout) {\n const heightZoom = height / layout.height;\n const widthZoom = width / layout.width;\n const scale = Math.min(heightZoom, widthZoom, 1);\n setZoom(scale - 1);\n positionCanvas(CanvasPosition.CENTER, animated);\n }\n },\n [height, layout, width, setZoom, positionCanvas]\n );\n\n /**\n * This centers the chart on the canvas, zooms in to fit the specified nodes, and scrolls to center the nodes in the viewport\n */\n const fitNodes = useCallback(\n (nodeIds: string | string[], animated = true) => {\n if (layout && layout.children) {\n const nodes = Array.isArray(nodeIds) ? nodeIds.map((nodeId) => findNode(layout.children, nodeId)) : [findNode(layout.children, nodeIds)];\n\n if (nodes) {\n // center the chart\n positionVector(CanvasPosition.CENTER);\n\n const updatedZoom = calculateZoom({ nodes, viewportWidth: width, viewportHeight: height, maxViewportCoverage: 0.9, minViewportCoverage: 0.2 });\n const scrollPosition = calculateScrollPosition({ nodes, viewportWidth: width, viewportHeight: height, canvasWidth, canvasHeight, chartWidth: layout.width, chartHeight: layout.height, zoom: updatedZoom });\n\n setZoom(updatedZoom - 1);\n scrollToXY(scrollPosition, animated);\n }\n }\n },\n [canvasHeight, canvasWidth, height, layout, positionVector, setZoom, width]\n );\n\n useLayoutEffect(() => {\n const scroller = ref.current;\n if (scroller && !scrolled.current && layout && height && width) {\n if (fit) {\n fitCanvas();\n } else if (defaultPosition) {\n positionCanvas(defaultPosition);\n }\n\n scrolled.current = true;\n }\n }, [canvasWidth, pannable, canvasHeight, layout, height, fit, width, defaultPosition, positionCanvas, fitCanvas, ref]);\n\n useLayoutEffect(() => {\n function onResize() {\n if (fit) {\n fitCanvas();\n } else if (defaultPosition) {\n positionCanvas(defaultPosition);\n }\n }\n\n window.addEventListener('resize', onResize);\n\n return () => window.removeEventListener('resize', onResize);\n }, [fit, positionCanvas, defaultPosition, fitCanvas]);\n\n return {\n xy,\n observe,\n containerRef: ref,\n canvasHeight,\n canvasWidth,\n containerWidth: width,\n containerHeight: height,\n layout,\n scrollXY,\n positionCanvas,\n fitCanvas,\n fitNodes,\n setScrollXY: scrollToXY\n } as LayoutResult;\n};\n","import React, { useCallback, useState } from 'react';\nimport { EdgeSections } from '../symbols/Edge';\nimport { NodeData, PortData } from '../types';\nimport { DragEvent, NodeDragEvents, Position } from './useNodeDrag';\nimport { Point2D } from 'kld-affine';\nimport { NodeDragType } from '../symbols/Node';\n\nexport interface EdgeDragResult extends NodeDragEvents {\n dragCoords: EdgeSections[] | null;\n canLinkNode: boolean | null;\n dragNode: NodeData | null;\n dragPort: PortData | null;\n enteredNode: NodeData | null;\n onEnter?: (\n event: React.MouseEvent<SVGGElement, MouseEvent>,\n data: NodeData | PortData\n ) => void;\n onLeave?: (\n event: React.MouseEvent<SVGGElement, MouseEvent>,\n data: NodeData | PortData\n ) => void;\n}\n\nexport const useEdgeDrag = ({\n onNodeLink,\n onNodeLinkCheck\n}): EdgeDragResult => {\n const [dragNode, setDragNode] = useState<NodeData | null>(null);\n const [dragPort, setDragPort] = useState<PortData | null>(null);\n const [dragType, setDragType] = useState<NodeDragType | null>(null);\n const [enteredNode, setEnteredNode] = useState<NodeData | null>(null);\n const [dragCoords, setDragCoords] = useState<EdgeSections[] | null>(null);\n const [canLinkNode, setCanLinkNode] = useState<boolean | null>(null);\n\n const onDragStart = useCallback(\n (state: DragEvent, _initial: Position, node: NodeData, port?: PortData) => {\n setDragType(state.dragType);\n setDragNode(node);\n setDragPort(port);\n },\n []\n );\n\n const onDrag = useCallback(\n ({ memo: [matrix], xy: [x, y] }: DragEvent, [ix, iy]: Position) => {\n const endPoint = new Point2D(x, y).transform(matrix);\n setDragCoords([\n {\n startPoint: {\n x: ix,\n y: iy\n },\n endPoint\n }\n ]);\n },\n []\n );\n\n const onDragEnd = useCallback(\n (event: DragEvent) => {\n if (dragNode && enteredNode && canLinkNode) {\n onNodeLink(event, dragNode, enteredNode, dragPort);\n }\n\n setDragNode(null);\n setDragPort(null);\n setEnteredNode(null);\n setDragCoords(null);\n },\n [canLinkNode, dragNode, dragPort, enteredNode, onNodeLink]\n );\n\n const onEnter = useCallback(\n (event: React.MouseEvent<SVGGElement, MouseEvent>, node: NodeData) => {\n if (dragNode && node) {\n setEnteredNode(node);\n const canLink = onNodeLinkCheck(event, dragNode, node, dragPort);\n const result =\n (canLink === undefined || canLink) &&\n (dragNode.parent === node.parent || dragType === 'node');\n\n setCanLinkNode(result);\n }\n },\n [dragNode, dragPort, dragType, onNodeLinkCheck]\n );\n\n const onLeave = useCallback(\n (event: React.MouseEvent<SVGGElement, MouseEvent>, node: NodeData) => {\n if (dragNode && node) {\n setEnteredNode(null);\n setCanLinkNode(null);\n }\n },\n [dragNode]\n );\n\n return {\n dragCoords,\n canLinkNode,\n dragNode,\n dragPort,\n enteredNode,\n onDragStart,\n onDrag,\n onDragEnd,\n onEnter,\n onLeave\n };\n};\n","import { RefObject, useCallback, useRef, useState } from 'react';\nimport { useGesture } from 'react-use-gesture';\n\nconst limit = (scale: number, min: number, max: number) => (scale < max ? (scale > min ? scale : min) : max);\n\nexport interface ZoomProps {\n disabled?: boolean;\n zoom?: number;\n minZoom?: number;\n maxZoom?: number;\n onZoomChange: (zoom: number) => void;\n}\n\nexport interface ZoomResult {\n /**\n * Factor of zoom.\n */\n zoom: number;\n\n /**\n * SVG Ref for the Canvas.\n */\n svgRef: RefObject<SVGSVGElement | null>;\n\n /**\n * Set a zoom factor of the canvas.\n */\n setZoom?: (factor: number) => void;\n\n /**\n * Zoom in on the canvas.\n */\n zoomIn?: (zoomFactor?: number) => void;\n\n /**\n * Zoom out on the canvas.\n */\n zoomOut?: (zoomFactor?: number) => void;\n}\n\nexport const useZoom = ({ disabled = false, zoom = 1, minZoom = -0.5, maxZoom = 1, onZoomChange }: ZoomProps) => {\n const [factor, setFactor] = useState<number>(zoom - 1);\n const svgRef = useRef<SVGSVGElement | null>(null);\n\n useGesture(\n {\n onPinch: ({ offset: [d], event }) => {\n event.preventDefault();\n // TODO: Set X/Y on center of zoom\n const next = limit(d / 100, minZoom, maxZoom);\n setFactor(next);\n onZoomChange(next + 1);\n }\n },\n {\n enabled: !disabled,\n domTarget: svgRef,\n eventOptions: { passive: false }\n }\n );\n\n const setZoom = useCallback(\n (f: number) => {\n const next = limit(f, minZoom, maxZoom);\n setFactor(next);\n onZoomChange(next + 1);\n },\n [maxZoom, minZoom, onZoomChange]\n );\n\n const zoomIn = useCallback(\n (zoomFactor: number = 0.1) => {\n setZoom(factor + zoomFactor);\n },\n [factor, setZoom]\n );\n\n const zoomOut = useCallback(\n (zoomFactor: number = -0.1) => {\n setZoom(factor + zoomFactor);\n },\n [factor, setZoom]\n );\n\n return {\n svgRef,\n zoom: factor + 1,\n setZoom,\n zoomIn,\n zoomOut\n } as ZoomResult;\n};\n","import React, { createContext, useContext } from 'react';\nimport { LayoutResult, useLayout } from '../layout/useLayout';\nimport { NodeData, PortData } from '../types';\nimport { EdgeDragResult, useEdgeDrag } from './useEdgeDrag';\nimport { useZoom, ZoomResult } from './useZoom';\n\nexport interface CanvasProviderValue\n extends EdgeDragResult,\n LayoutResult,\n ZoomResult {\n selections?: string[];\n readonly?: boolean;\n pannable: boolean;\n panType: 'scroll' | 'drag'; \n}\n\nexport const CanvasContext = createContext<CanvasProviderValue>({} as any);\n\nexport interface CanvasProviderProps {\n onNodeLink?: (\n event: any,\n fromNode: NodeData,\n toNode: NodeData,\n fromPort?: PortData\n ) => void;\n onNodeLinkCheck?: (\n event: any,\n fromNode: NodeData,\n toNode: NodeData,\n fromPort?: PortData\n ) => undefined | boolean;\n}\n\nexport const CanvasProvider = ({\n selections,\n onNodeLink,\n readonly,\n children,\n nodes,\n edges,\n maxHeight,\n fit,\n maxWidth,\n direction,\n layoutOptions,\n pannable,\n panType,\n defaultPosition,\n zoomable,\n zoom,\n minZoom,\n maxZoom,\n onNodeLinkCheck,\n onLayoutChange,\n onZoomChange\n}) => {\n const zoomProps = useZoom({\n zoom,\n minZoom,\n maxZoom,\n disabled: !zoomable,\n onZoomChange\n });\n\n const layoutProps = useLayout({\n nodes,\n edges,\n maxHeight,\n maxWidth,\n direction,\n pannable,\n panType,\n defaultPosition,\n fit,\n layoutOptions,\n zoom: zoomProps.zoom,\n setZoom: zoomProps.setZoom,\n onLayoutChange\n });\n\n const dragProps = useEdgeDrag({\n onNodeLink,\n onNodeLinkCheck\n });\n\n return (\n <CanvasContext.Provider\n value={{\n selections,\n readonly,\n pannable,\n panType,\n ...layoutProps,\n ...zoomProps,\n ...dragProps\n }}\n >\n {children}\n </CanvasContext.Provider>\n );\n};\n\nexport const useCanvas = () => {\n const context = useContext(CanvasContext);\n\n if (context === undefined) {\n throw new Error(\n '`useCanvas` hook must be used within a `CanvasContext` component'\n );\n }\n\n return context;\n};\n","import { RefObject } from 'react';\nimport { NodeData } from '../types';\nimport { Matrix2D } from 'kld-affine';\n\n/**\n * Checks if the node can be linked or not.\n */\nexport function checkNodeLinkable(\n curNode: NodeData,\n enteredNode: NodeData | null,\n canLinkNode: boolean | null\n) {\n if (canLinkNode === null || !enteredNode) {\n return null;\n }\n\n if (!enteredNode || !curNode) {\n return false;\n }\n\n // TODO: Revisit how to do self-linking better...\n return !(canLinkNode === false && enteredNode.id === curNode.id);\n}\n\nexport interface CoordProps {\n zoom: number;\n layoutXY: [number, number];\n containerRef: RefObject<HTMLDivElement | null>;\n}\n\n/**\n * Given various dimensions and positions, create a matrix\n * used for determining position.\n */\nexport function getCoords({ zoom, layoutXY, containerRef }: CoordProps) {\n const { top, left } = containerRef.current.getBoundingClientRect();\n const tx = layoutXY[0] - containerRef.current.scrollLeft + left;\n const ty = layoutXY[1] - containerRef.current.scrollTop + top;\n\n return new Matrix2D().translate(tx, ty).scale(zoom).inverse();\n}\n\n/**\n * Given a nodeId to find, a list of nodes to check against, and an optional parentId of the node\n * find the node from the list of nodes\n */\nexport function findNestedNode(\n nodeId: string,\n children: any[],\n parentId?: string\n): { [key: string]: any } {\n if (!nodeId || !children) {\n return {};\n }\n\n const foundNode = children.find((n) => n.id === nodeId);\n if (foundNode) {\n return foundNode;\n }\n\n if (parentId) {\n const parentNode = children.find((n) => n.id === parentId);\n if (parentNode?.children) {\n return findNestedNode(nodeId, parentNode.children, parentId);\n }\n }\n\n // Check for nested children\n const nodesWithChildren = children.filter((n) => n.children?.length);\n // Iterate over all nested nodes and check if any of them contain the node\n for (const n of nodesWithChildren) {\n const foundChild = findNestedNode(nodeId, n.children, parentId);\n\n if (foundChild && Object.keys(foundChild).length) {\n return foundChild;\n }\n }\n\n return {};\n}\n\n/**\n * Return the layout node that is currently being dragged on the Canvas\n */\nexport function getDragNodeData(\n dragNode: NodeData,\n children: any[] = []\n): { [key: string]: any } {\n if (!dragNode) {\n return {};\n }\n\n const { parent } = dragNode;\n if (!parent) {\n return children?.find((n) => n.id === dragNode.id) || {};\n }\n\n return findNestedNode(dragNode.id, children, parent);\n}\n","import { useRef } from 'react';\nimport { useDrag } from 'react-use-gesture';\nimport { State } from 'react-use-gesture/dist/types';\nimport { NodeDragType } from 'symbols';\nimport { NodeData } from '../types';\nimport { useCanvas } from './CanvasProvider';\nimport { getCoords } from './helpers';\n\nexport type DragEvent = State['drag'] & { dragType?: NodeDragType };\nexport type Position = [number, number];\n\nexport interface NodeDragEvents<T = any, TT = any | undefined> {\n onDrag?: (event: DragEvent, initial: Position, data: T, extra?: TT) => void;\n onDragEnd?: (\n event: DragEvent,\n initial: Position,\n data: T,\n extra?: TT\n ) => void;\n onDragStart?: (\n event: DragEvent,\n initial: Position,\n data: T,\n extra?: TT\n ) => void;\n}\n\nexport interface NodeDragProps extends NodeDragEvents {\n node: NodeData;\n height: number;\n width: number;\n x: number;\n y: number;\n disabled: boolean;\n}\n\nexport const useNodeDrag = ({\n x,\n y,\n height,\n width,\n onDrag,\n onDragEnd,\n onDragStart,\n node,\n disabled\n}: NodeDragProps) => {\n const initial: Position = [width / 2 + x, height + y];\n const targetRef = useRef<EventTarget | null>(null);\n const { zoom, xy, containerRef } = useCanvas();\n\n const bind = useDrag(\n (state) => {\n if (state.event.type === 'pointerdown') {\n targetRef.current = state.event.currentTarget;\n }\n\n if (!state.intentional || !targetRef.current) {\n return;\n }\n\n if (state.first) {\n const matrix = getCoords({\n containerRef,\n zoom,\n layoutXY: xy\n });\n\n // memo will hold the difference between the\n // first point of impact and the origin\n const memo = [matrix];\n\n onDragStart({ ...state, memo }, initial, node);\n\n return memo;\n }\n\n onDrag(state, initial, node);\n\n if (state.last) {\n targetRef.current = null;\n onDragEnd(state, initial, node);\n }\n },\n {\n enabled: !disabled,\n triggerAllEvents: true,\n threshold: 5\n }\n );\n\n return bind;\n};\n","import React, { forwardRef, Fragment, ReactNode, Ref, useState } from 'react';\nimport { motion } from 'motion/react';\nimport { PortData } from '../../types';\nimport { NodeDragEvents, DragEvent, useNodeDrag, Position } from '../../utils/useNodeDrag';\nimport classNames from 'classnames';\nimport { useCanvas } from '../../utils/CanvasProvider';\nimport css from './Port.module.css';\n\nexport interface ElkPortProperties {\n index: number;\n width: number;\n height: number;\n 'port.side': string;\n 'port.alignment': string;\n}\n\nexport interface PortChildProps {\n port: PortData;\n isDisabled: boolean;\n isDragging: boolean;\n isHovered: boolean;\n x: number;\n y: number;\n rx: number;\n ry: number;\n offsetX: number;\n offsetY: number;\n}\n\nexport type PortChildrenAsFunction = (portChildProps: PortChildProps) => ReactNode;\n\nexport interface PortProps extends NodeDragEvents<PortData> {\n id: string;\n x: number;\n y: number;\n rx: number;\n ry: number;\n offsetX: number;\n offsetY: number;\n disabled?: boolean;\n className?: string;\n properties: ElkPortProperties & PortData;\n style?: any;\n children?: ReactNode | PortChildrenAsFunction;\n active?: boolean;\n onEnter?: (event: React.MouseEvent<SVGGElement, MouseEvent>, port: PortData) => void;\n onLeave?: (event: React.MouseEvent<SVGGElement, MouseEvent>, port: PortData) => void;\n onClick?: (event: React.MouseEvent<SVGGElement, MouseEvent>, port: PortData) => void;\n}\n\nexport const Port = 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 }: Partial<PortProps>, ref: Ref<SVGRectElement>) => {\n const { readonly } = useCanvas();\n const [isDragging, setIsDragging] = useState<boolean>(false);\n const [isHovered, setIsHovered] = useState<boolean>(false);\n const newX = x - properties.width / 2;\n const newY = y - properties.height / 2;\n\n const onDragStartInternal = (event: DragEvent, initial: Position) => {\n onDragStart(event, initial, properties);\n setIsDragging(true);\n };\n\n const onDragEndInternal = (event: DragEvent, initial: Position) => {\n onDragEnd(event, initial, properties);\n setIsDragging(false);\n };\n\n const bind = useNodeDrag({\n x: newX + offsetX,\n y: newY + offsetY,\n height: properties.height,\n width: properties.width,\n disabled: disabled || readonly || properties?.disabled,\n node: properties,\n onDrag,\n onDragStart: onDragStartInternal,\n onDragEnd: onDragEndInternal\n });\n\n if (properties.hidden) {\n return null;\n }\n\n const isDisabled = properties.disabled || disabled;\n\n const portChildProps: PortChildProps = {\n port: properties,\n isDragging,\n isHovered,\n isDisabled,\n x,\n y,\n rx,\n ry,\n offsetX,\n offsetY\n };\n\n return (\n <g id={id}>\n <rect\n {...bind()}\n ref={ref}\n height={properties.height + 14}\n width={properties.width + 14}\n x={newX - 7}\n y={newY - 7}\n className={classNames(css.clicker, { [css.disabled]: isDisabled })}\n onMouseEnter={(event) => {\n event.stopPropagation();\n if (!isDisabled) {\n setIsHovered(true);\n onEnter(event, properties);\n }\n }}\n onMouseLeave={(event) => {\n event.stopPropagation();\n if (!isDisabled) {\n setIsHovered(false);\n onLeave(event, properties);\n }\n }}\n onClick={(event) => {\n event.stopPropagation();\n if (!isDisabled) {\n onClick(event, properties);\n }\n }}\n />\n <motion.rect\n key={`${x}-${y}`}\n style={style}\n className={classNames(css.port, className, properties?.className)}\n height={properties.height}\n width={properties.width}\n rx={rx}\n ry={ry}\n initial={{\n scale: 0,\n opacity: 0,\n x: newX,\n y: newY\n }}\n animate={{\n x: newX,\n y: newY,\n scale: (isDragging || active || isHovered) && !isDisabled ? 1.5 : 1,\n opacity: 1\n }}\n />\n {children && <Fragment>{typeof children === 'function' ? (children as PortChildrenAsFunction)(portChildProps) : children}</Fragment>}\n </g>\n );\n});\n","import React, { FC } from 'react';\nimport classNames from 'classnames';\nimport css from './Label.module.css';\n\nexport interface LabelProps {\n x: number;\n y: number;\n height: number;\n width: number;\n text: string;\n style?: any;\n className?: string;\n originalText?: string;\n}\n\nexport const Label: FC<Partial<LabelProps>> = ({ text, x, y, style, className, originalText }) => {\n const isString = typeof originalText === 'string';\n return (\n <>\n {isString && <title>{originalText}</title>}\n <g transform={`translate(${x}, ${y})`}>\n <text className={classNames(css.text, className)} style={style}>\n {text}\n </text>\n </g>\n </>\n );\n};\n","import React, { FC } from 'react';\nimport classNames from 'classnames';\nimpor