@ichigo_san/graphing
Version:
A lightweight UML-style diagram editor built with React Flow and Tailwind CSS
585 lines (557 loc) • 22.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _reactflow = require("reactflow");
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
const getConnectionPoint = (x, y, width, height, position) => {
switch (position) {
case _reactflow.Position.Top:
return {
x: x + width / 2,
y
};
case _reactflow.Position.Right:
return {
x: x + width,
y: y + height / 2
};
case _reactflow.Position.Bottom:
return {
x: x + width / 2,
y: y + height
};
case _reactflow.Position.Left:
return {
x,
y: y + height / 2
};
default:
return {
x: x + width / 2,
y: y + height / 2
};
}
};
const AdjustableEdge = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerStart,
markerEnd,
data,
selected
}) => {
const {
screenToFlowPosition,
setEdges,
getEdges,
getNodes
} = (0, _reactflow.useReactFlow)();
const [hoveredSegmentInfo, setHoveredSegmentInfo] = (0, _react.useState)(null);
const [draggedSegmentIndex, setDraggedSegmentIndex] = (0, _react.useState)(null);
// Get node dimensions and positions for source/target
const nodes = getNodes();
const sourceNode = nodes.find(n => n.id === (data === null || data === void 0 ? void 0 : data.source) || n.id === (data === null || data === void 0 ? void 0 : data.sourceNode) || n.id === (data === null || data === void 0 ? void 0 : data.sourceId));
const targetNode = nodes.find(n => n.id === (data === null || data === void 0 ? void 0 : data.target) || n.id === (data === null || data === void 0 ? void 0 : data.targetNode) || n.id === (data === null || data === void 0 ? void 0 : data.targetId));
const sourcePoint = (0, _react.useMemo)(() => {
return sourceNode && sourcePosition ? getConnectionPoint(sourceNode.position.x, sourceNode.position.y, sourceNode.width, sourceNode.height, sourcePosition) : {
x: sourceX,
y: sourceY
};
}, [sourceNode, sourcePosition, sourceX, sourceY]);
const targetPoint = (0, _react.useMemo)(() => {
return targetNode && targetPosition ? getConnectionPoint(targetNode.position.x, targetNode.position.y, targetNode.width, targetNode.height, targetPosition) : {
x: targetX,
y: targetY
};
}, [targetNode, targetPosition, targetX, targetY]);
// Calculate orthogonal bends for new edges - always use connection points
const calculateOrthogonalBends = (source, target) => {
const dx = target.x - source.x;
const dy = target.y - source.y;
const needsTwoBends = Math.abs(dx) > 50 && Math.abs(dy) > 50;
if (needsTwoBends) {
// L or Z shape
if (Math.abs(dx) > Math.abs(dy)) {
// Horizontal first
const midX = source.x + dx / 2;
return [{
x: midX,
y: source.y
}, {
x: midX,
y: target.y
}];
} else {
// Vertical first
return [{
x: source.x,
y: source.y + dy / 2
}, {
x: target.x,
y: target.y - dy / 2
}];
}
} else {
// Single bend
if (Math.abs(dx) > Math.abs(dy)) {
const midX = source.x + dx / 2;
return [{
x: midX,
y: source.y
}];
} else {
return [{
x: source.x,
y: source.y + dy / 2
}];
}
}
};
// Use the waypoints from the edge's data, or start with no waypoints for clean direct connection
const waypoints = (0, _react.useMemo)(() => {
if (data !== null && data !== void 0 && data.waypoints && data.waypoints.length > 0) {
const validWaypoints = data.waypoints.filter(wp => wp && typeof wp.x === 'number' && typeof wp.y === 'number' && !isNaN(wp.x) && !isNaN(wp.y) && isFinite(wp.x) && isFinite(wp.y));
return validWaypoints;
}
// Start with no waypoints - direct connection
return [];
}, [data === null || data === void 0 ? void 0 : data.waypoints]);
// Don't auto-save waypoints - let user create them by dragging segments
// This function generates the SVG path string for the edge.
const path = (0, _react.useMemo)(() => {
const points = [sourcePoint, ...waypoints, targetPoint];
let pathString = `M ${points[0].x},${points[0].y}`;
for (let i = 1; i < points.length; i++) {
pathString += ` L ${points[i].x},${points[i].y}`;
}
return pathString;
}, [sourcePoint, targetPoint, waypoints]);
// Draw.io-style segment dragging - moves the entire segment orthogonally
const handleSegmentMouseDown = (0, _react.useCallback)((event, segmentIndex) => {
var _clickedEdge$data;
event.stopPropagation();
event.preventDefault();
console.log('Segment mouse down triggered:', segmentIndex);
setDraggedSegmentIndex(segmentIndex);
const initialEdges = getEdges();
const clickedEdge = initialEdges.find(e => e.id === id);
if (!clickedEdge) return;
// Get current waypoints or create them if they don't exist
let currentWaypoints = ((_clickedEdge$data = clickedEdge.data) === null || _clickedEdge$data === void 0 ? void 0 : _clickedEdge$data.waypoints) || [];
// If no waypoints exist, create simple orthogonal waypoints when user drags
if (currentWaypoints.length === 0) {
const dx = targetPoint.x - sourcePoint.x;
const dy = targetPoint.y - sourcePoint.y;
// Create simple L-shaped path
if (Math.abs(dx) > Math.abs(dy)) {
// Horizontal then vertical
currentWaypoints = [{
x: sourcePoint.x + dx * 0.5,
y: sourcePoint.y
}, {
x: sourcePoint.x + dx * 0.5,
y: targetPoint.y
}];
} else {
// Vertical then horizontal
currentWaypoints = [{
x: sourcePoint.x,
y: sourcePoint.y + dy * 0.5
}, {
x: targetPoint.x,
y: sourcePoint.y + dy * 0.5
}];
}
}
const points = [sourcePoint, ...currentWaypoints, targetPoint];
const p1 = points[segmentIndex];
const p2 = points[segmentIndex + 1];
if (!p1 || !p2) return;
// Determine if this is a horizontal or vertical segment
const isHorizontal = Math.abs(p1.y - p2.y) < 10;
const isVertical = Math.abs(p1.x - p2.x) < 10;
console.log('Dragging segment:', segmentIndex, 'isHorizontal:', isHorizontal, 'isVertical:', isVertical, 'points:', points);
const onMouseMove = moveEvent => {
const position = screenToFlowPosition({
x: moveEvent.clientX,
y: moveEvent.clientY
});
console.log('Mouse position:', position);
setEdges(eds => eds.map(e => {
if (e.id === id) {
var _e$data;
// Get waypoints from multiple possible sources
let currentWaypoints = ((_e$data = e.data) === null || _e$data === void 0 ? void 0 : _e$data.waypoints) || [];
// If waypoints are empty, create them based on current drag action
if (currentWaypoints.length === 0) {
const dx = targetPoint.x - sourcePoint.x;
const dy = targetPoint.y - sourcePoint.y;
// Create simple L-shaped path based on drag direction
if (Math.abs(dx) > Math.abs(dy)) {
currentWaypoints = [{
x: sourcePoint.x + dx * 0.5,
y: sourcePoint.y
}, {
x: sourcePoint.x + dx * 0.5,
y: targetPoint.y
}];
} else {
currentWaypoints = [{
x: sourcePoint.x,
y: sourcePoint.y + dy * 0.5
}, {
x: targetPoint.x,
y: sourcePoint.y + dy * 0.5
}];
}
console.log('Created waypoints for dragging:', currentWaypoints);
}
console.log('Current waypoints:', currentWaypoints);
const newWaypoints = [...currentWaypoints];
if (isHorizontal) {
// Move horizontal segment vertically - maintain orthogonality
const newY = position.y;
console.log('Moving horizontal segment to Y:', newY);
// Update the waypoint before this segment (if it exists)
if (segmentIndex > 0 && newWaypoints[segmentIndex - 1]) {
newWaypoints[segmentIndex - 1] = {
...newWaypoints[segmentIndex - 1],
y: newY
};
console.log('Updated waypoint', segmentIndex - 1, 'to Y:', newY);
}
// Update the waypoint after this segment (if it exists)
if (segmentIndex < newWaypoints.length && newWaypoints[segmentIndex]) {
newWaypoints[segmentIndex] = {
...newWaypoints[segmentIndex],
y: newY
};
console.log('Updated waypoint', segmentIndex, 'to Y:', newY);
}
// Maintain orthogonality by ensuring adjacent segments are perpendicular
if (segmentIndex > 0 && newWaypoints[segmentIndex - 1]) {
// Keep the previous waypoint's X coordinate to maintain vertical segment
const prevWaypoint = newWaypoints[segmentIndex - 1];
newWaypoints[segmentIndex - 1] = {
x: prevWaypoint.x,
y: newY
};
}
if (segmentIndex < newWaypoints.length && newWaypoints[segmentIndex]) {
// Keep the next waypoint's X coordinate to maintain vertical segment
const nextWaypoint = newWaypoints[segmentIndex];
newWaypoints[segmentIndex] = {
x: nextWaypoint.x,
y: newY
};
}
} else if (isVertical) {
// Move vertical segment horizontally - maintain orthogonality
const newX = position.x;
console.log('Moving vertical segment to X:', newX);
// Update the waypoint before this segment (if it exists)
if (segmentIndex > 0 && newWaypoints[segmentIndex - 1]) {
newWaypoints[segmentIndex - 1] = {
...newWaypoints[segmentIndex - 1],
x: newX
};
console.log('Updated waypoint', segmentIndex - 1, 'to X:', newX);
}
// Update the waypoint after this segment (if it exists)
if (segmentIndex < newWaypoints.length && newWaypoints[segmentIndex]) {
newWaypoints[segmentIndex] = {
...newWaypoints[segmentIndex],
x: newX
};
console.log('Updated waypoint', segmentIndex, 'to X:', newX);
}
// Maintain orthogonality by ensuring adjacent segments are perpendicular
if (segmentIndex > 0 && newWaypoints[segmentIndex - 1]) {
// Keep the previous waypoint's Y coordinate to maintain horizontal segment
const prevWaypoint = newWaypoints[segmentIndex - 1];
newWaypoints[segmentIndex - 1] = {
x: newX,
y: prevWaypoint.y
};
}
if (segmentIndex < newWaypoints.length && newWaypoints[segmentIndex]) {
// Keep the next waypoint's Y coordinate to maintain horizontal segment
const nextWaypoint = newWaypoints[segmentIndex];
newWaypoints[segmentIndex] = {
x: newX,
y: nextWaypoint.y
};
}
}
console.log('New waypoints:', newWaypoints);
return {
...e,
data: {
...e.data,
waypoints: newWaypoints
}
};
}
return e;
}));
};
const onMouseUp = () => {
console.log('Mouse up - stopping drag');
setDraggedSegmentIndex(null);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
// Add event listeners to window
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}, [id, sourcePoint, targetPoint, getEdges, setEdges, screenToFlowPosition]);
// Draw.io-style waypoint dragging - maintains orthogonality
const onWaypointMouseDown = (0, _react.useCallback)((event, index) => {
event.stopPropagation();
event.preventDefault();
console.log('Waypoint mouse down triggered:', index);
const onMouseMove = e => {
const position = screenToFlowPosition({
x: e.clientX,
y: e.clientY
});
setEdges(eds => eds.map(edge => {
if (edge.id === id) {
var _edge$data;
let currentWaypoints = ((_edge$data = edge.data) === null || _edge$data === void 0 ? void 0 : _edge$data.waypoints) || [];
// If waypoints are empty but we have points, extract waypoints from the points
if (currentWaypoints.length === 0) {
var _edge$data2;
const points = [sourcePoint, ...(((_edge$data2 = edge.data) === null || _edge$data2 === void 0 ? void 0 : _edge$data2.waypoints) || []), targetPoint];
if (points && points.length > 2) {
currentWaypoints = points.slice(1, -1);
console.log('Extracted waypoints for dragging:', currentWaypoints);
}
}
if (!currentWaypoints || currentWaypoints.length === 0 || index >= currentWaypoints.length) {
return edge;
}
const newWaypoints = [...currentWaypoints];
// Move the waypoint
newWaypoints[index] = position;
// Maintain orthogonality by adjusting adjacent waypoints
if (index > 0) {
const prevWaypoint = newWaypoints[index - 1];
const currentWaypoint = newWaypoints[index];
// Determine if the segment to the left is horizontal or vertical
const isPrevHorizontal = Math.abs(prevWaypoint.y - currentWaypoint.y) < 5;
if (isPrevHorizontal) {
// Keep the previous waypoint at the same Y level
newWaypoints[index - 1] = {
...prevWaypoint,
y: currentWaypoint.y
};
} else {
// Keep the previous waypoint at the same X level
newWaypoints[index - 1] = {
...prevWaypoint,
x: currentWaypoint.x
};
}
}
if (index < newWaypoints.length - 1) {
const nextWaypoint = newWaypoints[index + 1];
const currentWaypoint = newWaypoints[index];
// Determine if the segment to the right is horizontal or vertical
const isNextHorizontal = Math.abs(currentWaypoint.y - nextWaypoint.y) < 5;
if (isNextHorizontal) {
// Keep the next waypoint at the same Y level
newWaypoints[index + 1] = {
...nextWaypoint,
y: currentWaypoint.y
};
} else {
// Keep the next waypoint at the same X level
newWaypoints[index + 1] = {
...nextWaypoint,
x: currentWaypoint.x
};
}
}
return {
...edge,
data: {
...edge.data,
waypoints: newWaypoints
}
};
}
return edge;
}));
};
const onMouseUp = () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}, [id, setEdges, screenToFlowPosition, sourcePoint, targetPoint]);
// Waypoint double-click handler - removes waypoint
const onWaypointDoubleClick = (0, _react.useCallback)((event, index) => {
event.stopPropagation();
event.preventDefault();
console.log('Waypoint double-click triggered:', index);
setEdges(eds => eds.map(edge => {
if (edge.id === id) {
var _edge$data3;
const currentWaypoints = ((_edge$data3 = edge.data) === null || _edge$data3 === void 0 ? void 0 : _edge$data3.waypoints) || [];
if (!currentWaypoints || currentWaypoints.length === 0 || index >= currentWaypoints.length) {
return edge;
}
const newWaypoints = [...currentWaypoints];
newWaypoints.splice(index, 1);
return {
...edge,
data: {
...edge.data,
waypoints: newWaypoints
}
};
}
return edge;
}));
}, [id, setEdges]);
// Calculate label position
const labelPosition = (0, _react.useMemo)(() => {
const points = [sourcePoint, ...waypoints, targetPoint];
if (points.length < 2) return {
x: (sourcePoint.x + targetPoint.x) / 2,
y: (sourcePoint.y + targetPoint.y) / 2
};
const midIndex = Math.floor(points.length / 2);
const p1 = points[midIndex - 1];
const p2 = points[midIndex];
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2
};
}, [sourcePoint, targetPoint, waypoints]);
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_reactflow.BaseEdge, {
id: id,
path: path,
style: style,
markerStart: markerStart,
markerEnd: markerEnd
}), /*#__PURE__*/_react.default.createElement(_reactflow.EdgeLabelRenderer, null, /*#__PURE__*/_react.default.createElement("div", {
style: {
position: 'absolute',
left: labelPosition.x,
top: labelPosition.y,
transform: 'translate(-50%, -50%)',
fontSize: 12,
fontWeight: 500,
color: '#333',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
padding: '2px 6px',
borderRadius: '4px',
border: '1px solid rgba(0, 0, 0, 0.1)',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
pointerEvents: 'all',
whiteSpace: 'nowrap',
zIndex: 10,
backdropFilter: 'blur(4px)'
},
className: "nodrag nopan edge-label"
}, (data === null || data === void 0 ? void 0 : data.label) || '')), (() => {
const points = [sourcePoint, ...waypoints, targetPoint];
return points.slice(0, -1).map((p1, i) => {
const p2 = points[i + 1];
if (!p1 || !p2) return null;
const isHorizontal = Math.abs(p1.y - p2.y) < 5;
const isDragging = draggedSegmentIndex === i;
const isHovered = (hoveredSegmentInfo === null || hoveredSegmentInfo === void 0 ? void 0 : hoveredSegmentInfo.segmentIndex) === i;
return /*#__PURE__*/_react.default.createElement("g", {
key: `segment-${i}`
}, /*#__PURE__*/_react.default.createElement("line", {
x1: p1.x,
y1: p1.y,
x2: p2.x,
y2: p2.y,
stroke: isDragging ? "rgba(59, 130, 246, 0.8)" : isHovered ? "rgba(59, 130, 246, 0.6)" : "rgba(59, 130, 246, 0.3)",
strokeWidth: isDragging ? 4 : isHovered ? 3 : 2,
strokeDasharray: isDragging || isHovered ? "5,5" : "none",
style: {
cursor: isHorizontal ? 'ns-resize' : 'ew-resize'
},
onMouseDown: event => handleSegmentMouseDown(event, i),
onMouseEnter: () => setHoveredSegmentInfo({
segmentIndex: i,
distance: 0
}),
onMouseLeave: () => setHoveredSegmentInfo(null)
}), /*#__PURE__*/_react.default.createElement("line", {
x1: p1.x,
y1: p1.y,
x2: p2.x,
y2: p2.y,
stroke: "transparent",
strokeWidth: 12,
style: {
cursor: isHorizontal ? 'ns-resize' : 'ew-resize'
},
onMouseDown: event => handleSegmentMouseDown(event, i),
onMouseEnter: () => setHoveredSegmentInfo({
segmentIndex: i,
distance: 0
}),
onMouseLeave: () => setHoveredSegmentInfo(null)
}));
});
})(), hoveredSegmentInfo && !draggedSegmentIndex && (() => {
const points = [sourcePoint, ...waypoints, targetPoint];
const p1 = points[hoveredSegmentInfo.segmentIndex];
const p2 = points[hoveredSegmentInfo.segmentIndex + 1];
if (!p1 || !p2) return null;
const midX = (p1.x + p2.x) / 2;
const midY = (p1.y + p2.y) / 2;
return /*#__PURE__*/_react.default.createElement("foreignObject", {
x: midX + 10,
y: midY - 20,
width: 80,
height: 40,
style: {
overflow: 'visible'
}
}, /*#__PURE__*/_react.default.createElement("div", {
style: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: 'white',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
fontFamily: 'monospace',
pointerEvents: 'none',
whiteSpace: 'nowrap',
zIndex: 1000
}
}, Math.round(midX), ", ", Math.round(midY)));
})(), waypoints.map((wp, i) => /*#__PURE__*/_react.default.createElement("g", {
key: i,
className: "react-flow__custom-edge-waypoint"
}, /*#__PURE__*/_react.default.createElement("circle", {
cx: wp.x,
cy: wp.y,
r: 5,
fill: "rgb(59, 130, 246)",
stroke: "white",
strokeWidth: 2,
className: "cursor-move",
onMouseDown: event => onWaypointMouseDown(event, i),
onDoubleClick: event => onWaypointDoubleClick(event, i)
}))));
};
var _default = exports.default = AdjustableEdge;