UNPKG

@ichigo_san/graphing

Version:

A lightweight UML-style diagram editor built with React Flow and Tailwind CSS

585 lines (557 loc) 22.2 kB
"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;