ar-design
Version:
AR Design is a (react | nextjs) ui library.
298 lines (297 loc) • 15 kB
JavaScript
import React, { useEffect, useMemo, useRef, useState } from "react";
import "../../../assets/css/components/data-display/diagram/styles.css";
import Grid from "../grid-system";
import Button from "../../form/button";
import Tooltip from "../../feedback/tooltip";
import { ARIcon } from "../../icons";
const { Box } = Grid;
const Diagram = ({ nodes, edges }) => {
// refs
const _arDiagram = useRef(null);
const _content = useRef(null);
const _arNodes = useRef({});
const _path = useRef(null);
// refs -> Start Position
const _dragStartMousePosition = useRef({ x: 0, y: 0 });
const _dragStartNodePosition = useRef({ x: 0, y: 0 });
// refs -> Zoom
const _zoomIntensity = 0.1;
const _maxScale = 4;
const _minScale = 0.1;
// states
const [_nodes, setNodes] = useState(nodes);
const [_edges, setEdges] = useState(edges);
const [trigger, setTrigger] = useState(false);
// states -> Pan
const [pan, setPan] = useState({ x: 0, y: 0 });
const [panning, setPanning] = useState(false);
const [startPan, setStartPan] = useState({ x: 0, y: 0 });
// states -> Zoom
const [scale, setScale] = useState(1);
// states -> Drag
const [draggedNode, setDraggedNode] = useState(null);
// states -> Drawing
const [drawingEdge, setDrawingEdge] = useState(null);
const [mousePos, setMousePos] = useState(null);
// methods
const getPortCenter = (id, port) => {
const node = _arNodes.current[`${id}_${port}`];
const diagram = _arDiagram.current;
if (!node || !diagram)
return null;
const diagramRect = diagram.getBoundingClientRect();
const nodeRect = node.getBoundingClientRect();
return {
x: (nodeRect.left - diagramRect.left + nodeRect.width / 2 - pan.x) / scale,
y: (nodeRect.top - diagramRect.top + nodeRect.height / 2 - pan.y) / scale,
};
};
const getClosestPort = (position, threshold = 20) => {
for (const key in _arNodes.current) {
const el = _arNodes.current[key];
if (!el)
continue;
const [idStr, port] = key.split("_");
const id = parseInt(idStr, 10);
const rect = el.getBoundingClientRect();
const diagramRect = _arDiagram.current.getBoundingClientRect();
const portCenter = {
x: (rect.left - diagramRect.left + rect.width / 2 - pan.x) / scale,
y: (rect.top - diagramRect.top + rect.height / 2 - pan.y) / scale,
};
const distance = Math.hypot(position.x - portCenter.x, position.y - portCenter.y);
if (distance <= threshold)
return { id, port: port };
}
return null;
};
const renderEdges = useMemo(() => {
return _edges.map((edge, index) => {
const from = getPortCenter(edge.from.id, edge.from.port);
const to = getPortCenter(edge.to.id, edge.to.port);
if (!from || !to)
return null;
const dx = to.x - from.x;
const dy = to.y - from.y;
const distance = Math.hypot(dx, dy);
const offset = Math.min(40, distance * 0.25); // maksimum sapma sınırı
// S biçimli kontrol noktaları
const controlPoint1 = {
x: from.x,
y: from.y + (dy < 0 ? -offset : offset),
};
const controlPoint2 = {
x: to.x,
y: to.y + (dy < 0 ? offset : -offset),
};
const pathData = `M${from.x} ${from.y} C${controlPoint1.x} ${controlPoint1.y}, ${controlPoint2.x} ${controlPoint2.y}, ${to.x} ${to.y}`;
return (React.createElement("svg", { key: index, className: "edge" },
React.createElement("path", { ref: _path, d: pathData, fill: "none", stroke: "var(--purple-500)", strokeWidth: 2, strokeDasharray: 10, strokeDashoffset: 10, strokeLinecap: "round" },
React.createElement("animate", { attributeName: "stroke-dashoffset", values: `${20 / scale};0`, dur: "1s", repeatCount: "indefinite" }))));
});
}, [_nodes, _edges, trigger]);
const onPanStart = (e) => {
// Node sürükleniyorsa pan başlatma.
if (draggedNode)
return;
setPanning(true);
setStartPan({ x: e.clientX - pan.x, y: e.clientY - pan.y });
};
const onPanMove = (e) => {
if (panning) {
setPan({ x: e.clientX - startPan.x, y: e.clientY - startPan.y });
}
};
const onPanEnd = () => setPanning(false);
// methods -> Zoom
const handleWheel = (event) => {
const direction = event.deltaY > 0 ? -1 : 1;
let newScale = scale + direction * _zoomIntensity;
newScale = Math.max(_minScale, Math.min(_maxScale, newScale));
// Mouse'un container içindeki konumunu al.
const rect = _content.current.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
// İçerik düzleminde mouse'un bulunduğu noktayı bul.
const zoomPointX = (mouseX - pan.x) / scale;
const zoomPointY = (mouseY - pan.y) / scale;
// Yeni pan değerini hesapla ki zoomPoint sabit kalsın.
const newPanX = mouseX - zoomPointX * newScale;
const newPanY = mouseY - zoomPointY * newScale;
setScale(newScale);
setPan({ x: newPanX, y: newPanY });
};
const handleZoom = (process) => {
let newScale = 0;
if (process === "increment")
newScale = Math.max(_minScale, Math.min(_maxScale, scale + _zoomIntensity));
if (process === "decrement")
newScale = Math.max(_minScale, Math.min(_maxScale, scale - _zoomIntensity));
if (_content.current && _content.current) {
const containerRect = _content.current.getBoundingClientRect();
// Ortadaki noktayı bul (container açısından)
const centerX = containerRect.width / 2;
const centerY = containerRect.height / 2;
// İçerik düzleminde bu noktaya karşılık gelen nokta
const zoomPointX = (centerX - pan.x) / scale;
const zoomPointY = (centerY - pan.y) / scale;
// Yeni pan hesapla ki center aynı yerde kalsın
const newPanX = centerX - zoomPointX * newScale;
const newPanY = centerY - zoomPointY * newScale;
setPan({ x: newPanX, y: newPanY });
setScale(newScale);
}
};
// methods -> Node
const onNodeMouseDown = (event, id, node) => {
event.stopPropagation();
setDraggedNode(id);
_dragStartMousePosition.current = { x: event.clientX, y: event.clientY };
_dragStartNodePosition.current = { x: node.x, y: node.y };
};
const onMouseMove = (event) => {
if (drawingEdge) {
const rect = _arDiagram.current.getBoundingClientRect();
const x = (event.clientX - rect.left - pan.x) / scale;
const y = (event.clientY - rect.top - pan.y) / scale;
setMousePos({ x, y });
}
if (draggedNode) {
const deltaX = (event.clientX - _dragStartMousePosition.current.x) / scale;
const deltaY = (event.clientY - _dragStartMousePosition.current.y) / scale;
const newX = _dragStartNodePosition.current.x + deltaX;
const newY = _dragStartNodePosition.current.y + deltaY;
setNodes((prev) => prev.map((node) => (node.id === draggedNode ? { ...node, position: { x: newX, y: newY } } : node)));
}
};
const onMouseUp = () => {
if (drawingEdge && mousePos) {
const closest = getClosestPort(mousePos);
if (closest) {
// Yakın port varsa, oraya bağla
const newEdge = {
id: crypto.randomUUID(), // _edges[_edges.length - 1]?.id + 1 || 1,
from: { id: drawingEdge.id, port: drawingEdge.port },
to: { id: closest.id, port: closest.port },
};
// Aynı edge daha önce eklenmiş mi kontrol et
const isDuplicate = _edges.some((edge) => {
const samePair = (edge.from.id === newEdge.from.id && edge.to.id === newEdge.to.id) ||
(edge.from.id === newEdge.to.id && edge.to.id === newEdge.from.id);
return samePair;
});
if (!isDuplicate)
setEdges((prev) => [...prev, newEdge]);
}
else {
// Yakın port yoksa yeni node oluştur
const newNodeId = crypto.randomUUID(); // _nodes[_nodes.length - 1]?.id + 1 || 1;
const newNode = {
id: newNodeId,
position: mousePos,
data: React.createElement(React.Fragment, null),
};
const newPort = mousePos.y < drawingEdge.start.y ? "bottom" : "top";
const newEdge = {
id: crypto.randomUUID(), // _edges[_edges.length - 1]?.id + 1 || 1,
from: { id: drawingEdge.id, port: drawingEdge.port },
to: { id: newNodeId, port: newPort },
};
setNodes((prev) => [...prev, newNode]);
setEdges((prev) => [...prev, newEdge]);
}
setDrawingEdge(null);
setMousePos(null);
}
setDraggedNode(null);
setTrigger((prev) => !prev);
};
// useEffects
useEffect(() => {
setEdges([...edges]);
}, []);
return (React.createElement("div", { ref: _arDiagram, className: "ar-diagram", onMouseDown: onPanStart, onMouseMove: (event) => {
onPanMove(event);
onMouseMove(event);
}, onMouseUp: () => {
onMouseUp();
onPanEnd();
} },
React.createElement("div", { ref: _content, className: "content", style: { backgroundPosition: `${pan.x}px ${pan.y}px` }, onWheel: handleWheel },
React.createElement("div", { className: "nodes-wrapper", style: {
transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})`,
} },
React.createElement("div", { className: "edges" },
renderEdges,
drawingEdge && mousePos && (React.createElement("svg", { className: "edge-temp" },
React.createElement("path", { ref: _path, d: `M${drawingEdge.start.x} ${drawingEdge.start.y} L${mousePos.x} ${mousePos.y}`, fill: "none", stroke: "var(--purple-500)", strokeWidth: 2, strokeDasharray: 10, strokeDashoffset: 10, strokeLinecap: "round" })))),
React.createElement("div", { className: "nodes" }, _nodes.map((node, index) => (React.createElement("div", { key: index, className: "node", style: {
left: node.position.x,
top: node.position.y,
}, onMouseDown: (event) => onNodeMouseDown(event, node.id, node.position) },
React.createElement("span", { ref: (el) => {
if (!el)
return;
_arNodes.current[`${node.id}_top`] = el;
}, className: "port top", onMouseDown: (event) => {
event.stopPropagation();
const port = "top";
const from = getPortCenter(node.id, port);
if (from) {
setDrawingEdge({
id: node.id,
port,
start: from,
});
}
} }),
React.createElement("span", { ref: (el) => {
if (!el)
return;
_arNodes.current[`${node.id}_left`] = el;
}, className: "port left", onMouseDown: (event) => {
event.stopPropagation();
const from = getPortCenter(node.id, "left");
if (from) {
setDrawingEdge({ id: node.id, port: "left", start: from });
}
} }),
React.createElement("span", null, node.data),
React.createElement("span", { ref: (el) => {
if (!el)
return;
_arNodes.current[`${node.id}_right`] = el;
}, className: "port right", onMouseDown: (event) => {
event.stopPropagation();
const from = getPortCenter(node.id, "right");
if (from) {
setDrawingEdge({ id: node.id, port: "right", start: from });
}
} }),
React.createElement("span", { ref: (el) => {
if (!el)
return;
_arNodes.current[`${node.id}_bottom`] = el;
}, className: "port bottom", onMouseDown: (event) => {
event.stopPropagation();
const from = getPortCenter(node.id, "bottom");
if (from) {
setDrawingEdge({
id: node.id,
port: "bottom",
start: from,
});
}
} }))))))),
React.createElement("div", { className: "zoom-buttons", onMouseDown: (event) => event.stopPropagation() },
React.createElement(Box, null,
React.createElement(Tooltip, { text: "Zoom Out" },
React.createElement(Button, { variant: "borderless", color: "light", icon: { element: React.createElement(ARIcon, { icon: "Dash", fill: "currentColor" }) }, onClick: () => handleZoom("decrement") })),
React.createElement("div", { className: "zoom-percent" },
Math.round(scale * 100),
"%"),
React.createElement(Tooltip, { text: "Zoom In" },
React.createElement(Button, { variant: "borderless", color: "light", icon: { element: React.createElement(ARIcon, { icon: "Add", fill: "currentColor" }) }, onClick: () => handleZoom("increment") })))),
React.createElement("div", { style: { zIndex: 555 } }, JSON.stringify(drawingEdge))));
};
export default Diagram;