114 lines
3.4 KiB
React
114 lines
3.4 KiB
React
import React, { useState } from 'react';
|
|||
|
|
import { EdgeLabelRenderer, BaseEdge } from 'reactflow';
|
||
|
|
import { X } from 'lucide-react';
|
||
|
|
|
||
|
|
export default function RemovableEdge({
|
||
|
|
id,
|
||
|
|
sourceX,
|
||
|
|
sourceY,
|
||
|
|
targetX,
|
||
|
|
targetY,
|
||
|
|
style = {},
|
||
|
|
markerEnd,
|
||
|
|
data,
|
||
|
|
}) {
|
||
|
|
const [isHovered, setIsHovered] = useState(false);
|
||
|
|
|
||
|
|
// Cubic Bezier with horizontal control points max(50, |dx|/2)
|
||
|
|
const dx = Math.max(50, Math.abs(targetX - sourceX) / 2);
|
||
|
|
const edgePath = `M${sourceX},${sourceY} C${sourceX + dx},${sourceY} ${targetX - dx},${targetY} ${targetX},${targetY}`;
|
||
|
|
|
||
|
|
const midX = (sourceX + targetX) / 2;
|
||
|
|
const midY = (sourceY + targetY) / 2;
|
||
|
|
|
||
|
|
const handleDelete = (evt) => {
|
||
|
|
if (evt) evt.stopPropagation();
|
||
|
|
if (data && data.onDeleteEdge) {
|
||
|
|
data.onDeleteEdge(id);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDoubleClick = (evt) => {
|
||
|
|
evt.stopPropagation();
|
||
|
|
handleDelete(evt);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
{/* VISIBLE BEZIER EDGE */}
|
||
|
|
<BaseEdge
|
||
|
|
path={edgePath}
|
||
|
|
markerEnd={markerEnd}
|
||
|
|
style={{
|
||
|
|
...style,
|
||
|
|
stroke: isHovered ? 'var(--acc)' : style.stroke || 'var(--muted)',
|
||
|
|
strokeWidth: isHovered ? 2.5 : style.strokeWidth || 2,
|
||
|
|
transition: 'stroke 0.15s ease, stroke-width 0.15s ease',
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* INVISIBLE WIDE HIT AREA FOR HOVER AND DOUBLE CLICK TO DELETE */}
|
||
|
|
<path
|
||
|
|
d={edgePath}
|
||
|
|
fill="none"
|
||
|
|
stroke="transparent"
|
||
|
|
strokeWidth={22}
|
||
|
|
style={{ cursor: 'pointer' }}
|
||
|
|
onMouseEnter={() => setIsHovered(true)}
|
||
|
|
onMouseLeave={() => setIsHovered(false)}
|
||
|
|
onDoubleClick={handleDoubleClick}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* OPTIONAL HOVER DELETE BUTTON IN CENTER */}
|
||
|
|
{isHovered && (
|
||
|
|
<EdgeLabelRenderer>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
position: 'absolute',
|
||
|
|
transform: `translate(-50%, -50%) translate(${midX}px,${midY}px)`,
|
||
|
|
fontSize: 12,
|
||
|
|
pointerEvents: 'all',
|
||
|
|
zIndex: 10,
|
||
|
|
}}
|
||
|
|
className="nodrag nopan"
|
||
|
|
onMouseEnter={() => setIsHovered(true)}
|
||
|
|
onMouseLeave={() => setIsHovered(false)}
|
||
|
|
>
|
||
|
|
<button
|
||
|
|
onClick={handleDelete}
|
||
|
|
title="Click or double-click line to delete connection"
|
||
|
|
aria-label="Delete connection"
|
||
|
|
style={{
|
||
|
|
width: '18px',
|
||
|
|
height: '18px',
|
||
|
|
borderRadius: '50%',
|
||
|
|
background: 'var(--surface)',
|
||
|
|
color: 'var(--muted)',
|
||
|
|
border: '1px solid var(--line)',
|
||
|
|
display: 'flex',
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: 'center',
|
||
|
|
cursor: 'pointer',
|
||
|
|
boxShadow: 'var(--shadow)',
|
||
|
|
padding: 0,
|
||
|
|
}}
|
||
|
|
onMouseEnter={(e) => {
|
||
|
|
e.currentTarget.style.color = '#ffffff';
|
||
|
|
e.currentTarget.style.background = 'var(--err)';
|
||
|
|
e.currentTarget.style.borderColor = 'var(--err)';
|
||
|
|
}}
|
||
|
|
onMouseLeave={(e) => {
|
||
|
|
e.currentTarget.style.color = 'var(--muted)';
|
||
|
|
e.currentTarget.style.background = 'var(--surface)';
|
||
|
|
e.currentTarget.style.borderColor = 'var(--line)';
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<X size={10} strokeWidth={2.5} />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</EdgeLabelRenderer>
|
||
|
|
)}
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|