class Mindmap { constructor(options = {}) { this.container = options.container; this.data = options.data || { id: "root", text: "Root", children: [] }; this.nodeWidth = options.nodeWidth || 120; this.nodeHeight = options.nodeHeight || 40; this.hGap = options.hGap || 60; this.vGap = options.vGap || 20; this.nodes = []; this.links = []; this.init(); } init() { if (!this.container) return; this.container.style.position = "relative"; this.container.style.overflow = "hidden"; this.container.style.backgroundColor = "#f5f5f5"; this.viewport = document.createElement("div"); this.viewport.style.position = "absolute"; this.viewport.style.width = "10000px"; this.viewport.style.height = "10000px"; this.viewport.style.transformOrigin = "0 0"; this.container.appendChild(this.viewport); this.scale = 1; this.offsetX = this.container.clientWidth / 2 - 100; this.offsetY = this.container.clientHeight / 2 - 50; this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); this.svg.style.position = "absolute"; this.svg.style.top = "0"; this.svg.style.left = "0"; this.svg.style.width = "100%"; this.svg.style.height = "100%"; this.svg.style.pointerEvents = "none"; this.viewport.appendChild(this.svg); this.initEvents(); this.updateViewport(); this.render(); } initEvents() { let isDragging = false; let lastX, lastY; this.container.onmousedown = (e) => { if (e.target.classList.contains("mindmap-node")) return; isDragging = true; lastX = e.clientX; lastY = e.clientY; }; window.onmousemove = (e) => { if (!isDragging) return; this.offsetX += e.clientX - lastX; this.offsetY += e.clientY - lastY; lastX = e.clientX; lastY = e.clientY; this.updateViewport(); }; window.onmouseup = () => { isDragging = false; }; this.container.onwheel = (e) => { e.preventDefault(); const delta = e.deltaY > 0 ? 0.9 : 1.1; const newScale = this.scale * delta; if (newScale < 0.1 || newScale > 5) return; const rect = this.container.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; this.offsetX = mouseX - (mouseX - this.offsetX) * delta; this.offsetY = mouseY - (mouseY - this.offsetY) * delta; this.scale = newScale; this.updateViewport(); }; } updateViewport() { this.viewport.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.scale})`; } layout() { this.nodes = []; this.links = []; const compute = (node, x, startY) => { const isCollapsed = node._collapsed; const childrenHeight = node.children && node.children.length > 0 && !isCollapsed ? node.children.reduce((acc, child) => acc + this.getTotalHeight(child), 0) : this.nodeHeight; const y = startY + childrenHeight / 2 - this.nodeHeight / 2; const nodeInfo = { x, y, text: node.text, id: node.id || Math.random().toString(36).substr(2, 9), raw: node }; this.nodes.push(nodeInfo); if (node.children && !isCollapsed) { let currentY = startY; node.children.forEach((child) => { const childInfo = compute(child, x + this.nodeWidth + this.hGap, currentY); this.links.push({ x1: x + this.nodeWidth, y1: y + this.nodeHeight / 2, x2: childInfo.x, y2: childInfo.y + this.nodeHeight / 2 }); currentY += this.getTotalHeight(child); }); } return nodeInfo; }; compute(this.data, 0, 0); } getTotalHeight(node) { if (!node.children || node.children.length === 0 || node._collapsed) { return this.nodeHeight + this.vGap; } return node.children.reduce((acc, child) => acc + this.getTotalHeight(child), 0); } render() { this.layout(); this.svg.innerHTML = ""; const existingNodes = this.viewport.querySelectorAll(".mindmap-node"); existingNodes.forEach((n) => n.remove()); this.links.forEach((link) => { const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); const d = `M ${link.x1} ${link.y1} C ${link.x1 + this.hGap / 2} ${link.y1}, ${link.x2 - this.hGap / 2} ${link.y2}, ${link.x2} ${link.y2}`; path.setAttribute("d", d); path.setAttribute("stroke", "#4A90E2"); path.setAttribute("fill", "none"); path.setAttribute("stroke-width", "2"); this.svg.appendChild(path); }); this.nodes.forEach((node) => { const el = document.createElement("div"); el.className = "mindmap-node"; if (node.raw._collapsed) el.classList.add("collapsed"); el.style.position = "absolute"; el.style.left = `${node.x}px`; el.style.top = `${node.y}px`; el.style.width = `${this.nodeWidth}px`; el.style.height = `${this.nodeHeight}px`; el.style.border = "2px solid #4A90E2"; el.style.borderRadius = "8px"; el.style.backgroundColor = "#fff"; el.style.display = "flex"; el.style.alignItems = "center"; el.style.justifyContent = "center"; el.style.fontSize = "14px"; el.style.fontWeight = "bold"; el.style.color = "#333"; el.style.boxShadow = "0 4px 6px rgba(0,0,0,0.1)"; el.style.cursor = "pointer"; el.style.userSelect = "none"; el.style.transition = "transform 0.2s"; el.innerText = node.text; if (node.raw._collapsed) { el.style.backgroundColor = "#f0f0f0"; el.style.borderStyle = "dashed"; } el.onmouseenter = () => el.style.transform = "scale(1.05)"; el.onmouseleave = () => el.style.transform = "scale(1)"; el.onclick = (e) => { e.stopPropagation(); node.raw._collapsed = !node.raw._collapsed; this.render(); }; this.viewport.appendChild(el); }); } addNode(parentId, text) { const findAndAdd = (node) => { if (node.id === parentId || node.text === parentId) { if (!node.children) node.children = []; node.children.push({ text, id: Math.random().toString(36).substr(2, 9) }); return true; } if (node.children) { for (const child of node.children) { if (findAndAdd(child)) return true; } } return false; }; findAndAdd(this.data); this.render(); } removeNode(nodeId) { const remove = (node) => { if (!node.children) return false; const index = node.children.findIndex((child) => child.id === nodeId || child.text === nodeId); if (index !== -1) { node.children.splice(index, 1); return true; } for (const child of node.children) { if (remove(child)) return true; } return false; }; if (this.data.id === nodeId || this.data.text === nodeId) { console.warn("Cannot remove root node"); return; } remove(this.data); this.render(); } } export { Mindmap };