mindmap/dist/mindmap.js

206 lines
7.0 KiB
JavaScript

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
};