reaflow
Version:
Node-based Visualizations for React
1,035 lines (1,006 loc) • 99.5 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react/jsx-runtime'), require('react'), require('rdk'), require('framer-motion'), require('react-use-gesture'), require('elkjs/lib/elk.bundled'), require('p-cancelable'), require('calculate-size'), require('ellipsize'), require('react-cool-dimensions'), require('react-fast-compare'), require('kld-affine'), require('classnames'), require('d3-shape'), require('reakeys'), require('undoo'), require('kld-intersections')) :
typeof define === 'function' && define.amd ? define(['exports', 'react/jsx-runtime', 'react', 'rdk', 'framer-motion', 'react-use-gesture', 'elkjs/lib/elk.bundled', 'p-cancelable', 'calculate-size', 'ellipsize', 'react-cool-dimensions', 'react-fast-compare', 'kld-affine', 'classnames', 'd3-shape', 'reakeys', 'undoo', 'kld-intersections'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.reaflow = {}, global.jsxRuntime, global.react, global.rdk, global.framerMotion, global.reactUseGesture, global.ELK, global.PCancelable, global.calculateSize, global.ellipsize, global.useDimensions, global.isEqual, global.kldAffine, global.classNames, global.d3Shape, global.reakeys, global.Undoo, global.kldIntersections));
}(this, (function (exports, jsxRuntime, react, rdk, framerMotion, reactUseGesture, ELK, PCancelable, calculateSize, ellipsize, useDimensions, isEqual, kldAffine, classNames, d3Shape, reakeys, Undoo, kldIntersections) {
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 === nul