From 264911d49538edabb29759d665bbdcbc29bb7ffd Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Fri, 12 Jun 2026 02:01:57 +0800 Subject: [PATCH] feat: align with base project's No ESM design and register global Mindmap component (by AI) --- .gitignore | 4 + .npmrc | 1 + CHANGELOG.md | 5 + README.md | 79 ++++---- dist/mindmap.js | 425 +++++++++++++++++++++++--------------------- dist/mindmap.min.js | 2 +- mindmapTODO.md | 27 ++- package-lock.json | 4 +- package.json | 3 +- scripts/publish.js | 48 +++++ src/index.js | 32 +++- test/index.html | 8 +- vite.config.js | 32 ++-- 13 files changed, 399 insertions(+), 271 deletions(-) create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 scripts/publish.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffb4930 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +test-results/ +.DS_Store diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..f3b1a9f --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ea81c2..1770dbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## v1.0.2 (2026-06-12) +- **架构对齐**: 遵循 `base` 包新设计,彻底消灭用户侧 ESM 强依赖。 +- **组件注册**: 支持 `` 全局组件声明式用法。 +- **构建优化**: 产物调整为纯 UMD 模式,通过 `globalThis.Mindmap` 暴露 API。 + ## v1.0.0 (2026-05-29) - 初始化项目架构。 - 实现基础渲染占位。 diff --git a/README.md b/README.md index 8277a08..7750c96 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,55 @@ -# @web/mindmap +# @apigo.cc/mindmap API 手册 -类 Xmind 体验的思维导图引擎,专注于展示逻辑分支。 +原生 JS 编写的思维导图引擎。支持无限层级、缩放平移、节点折叠及动态增删。 -## 特性 -- 原生 ESM 驱动,无需构建即可在浏览器中使用。 -- 支持自动布局算法。 -- 高性能 SVG/DOM 渲染(1000+ 节点渲染耗时 < 50ms)。 -- 支持节点的展开与折叠。 -- 支持画布平移与缩放。 +--- -## 安装与使用 -本模块不发布至 npmjs。请通过 `loader.js` 或 `importmap` 引入。 +## 1. 引入方式 (UMD 优先) -### 使用示例 -```javascript -import { Mindmap } from '@web/mindmap'; +```html + -const mm = new Mindmap({ - container: document.getElementById('app'), - data: { - text: '根节点', - children: [ - { text: '子节点 1' }, - { text: '子节点 2' } - ] - } -}); + ``` -## API +--- + +## 2. 核心用法 + +### 组件化用法 (推荐) +直接在 HTML 中使用 `` 标签,通过 `state` 进行数据绑定。 + +```html +
+ +
+``` + +### JS 调用方式 (兼容) +... +--- + +## 3. API 参考 + ### `new Mindmap(options)` -- `options.container`: HTMLElement, 容器元素。 -- `options.data`: Object, 树形数据结构。 -- `options.nodeWidth`: Number, 节点宽度 (默认 120)。 -- `options.nodeHeight`: Number, 节点高度 (默认 40)。 -- `options.hGap`: Number, 水平间距 (默认 60)。 -- `options.vGap`: Number, 垂直间距 (默认 20)。 +- **`container`**: 挂载元素。 +- **`data`**: 格式为 `{ id, text, children, _collapsed }` 的对象。 +- **`nodeWidth / nodeHeight`**: 节点尺寸。 +- **`hGap / vGap`**: 间距。 -### `mm.addNode(parentId, text)` -- 向指定 ID 或文本匹配的节点添加子节点。 +### 实例方法 +- **`addNode(parentId, text)`**: 动态添加节点。 +- **`removeNode(nodeId)`**: 动态删除节点。 +- **`render()`**: 强制重新布局与重绘。 -### `mm.removeNode(nodeId)` -- 删除指定 ID 或文本匹配的节点。 +--- + +## 开发者提示 (AI 必读) +1. **全局变量**: UMD 模式下,`Mindmap` 类自动挂载到 `window`。 +2. **唯一 ID**: 每个节点必须拥有全局唯一的 `id`。 +3. **数据响应**: 修改 `mm.data` 属性后必须手动执行 `mm.render()`。 diff --git a/dist/mindmap.js b/dist/mindmap.js index d5864b1..666153e 100644 --- a/dist/mindmap.js +++ b/dist/mindmap.js @@ -1,205 +1,232 @@ -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; +(function(global, factory) { + typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.ApigoMindmap = {})); +})(this, function(exports2) { + "use strict"; + 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(); - }; - 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.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; }; - 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 + 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); }); - 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; + 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(); } - remove(this.data); - this.render(); } -} -export { - Mindmap -}; + if (typeof globalThis !== "undefined" && globalThis.Component) { + globalThis.Component.register("Mindmap", (container) => { + let mm = null; + const init = () => { + container.innerHTML = ""; + mm = new Mindmap({ + container, + data: container.state.data, + nodeWidth: parseInt(container.getAttribute("node-width")) || 120, + nodeHeight: parseInt(container.getAttribute("node-height")) || 40, + hGap: parseInt(container.getAttribute("h-gap")) || 60, + vGap: parseInt(container.getAttribute("v-gap")) || 20 + }); + container.mindmapInstance = mm; + }; + container.state.__watch("data", (val) => mm ? (mm.data = val, mm.render()) : init()); + init(); + container.addEventListener("unload", () => mm = null); + }); + } + if (typeof globalThis !== "undefined") { + globalThis.Mindmap = Mindmap; + } + exports2.Mindmap = Mindmap; + Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" }); +}); diff --git a/dist/mindmap.min.js b/dist/mindmap.min.js index 35aa9ff..d1db37f 100644 --- a/dist/mindmap.min.js +++ b/dist/mindmap.min.js @@ -1 +1 @@ -class t{constructor(t={}){this.container=t.container,this.data=t.data||{id:"root",text:"Root",children:[]},this.nodeWidth=t.nodeWidth||120,this.nodeHeight=t.nodeHeight||40,this.hGap=t.hGap||60,this.vGap=t.vGap||20,this.nodes=[],this.links=[],this.init()}init(){this.container&&(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 t,e,i=!1;this.container.onmousedown=s=>{s.target.classList.contains("mindmap-node")||(i=!0,t=s.clientX,e=s.clientY)},window.onmousemove=s=>{i&&(this.offsetX+=s.clientX-t,this.offsetY+=s.clientY-e,t=s.clientX,e=s.clientY,this.updateViewport())},window.onmouseup=()=>{i=!1},this.container.onwheel=t=>{t.preventDefault();const e=t.deltaY>0?.9:1.1,i=this.scale*e;if(i<.1||i>5)return;const s=this.container.getBoundingClientRect(),n=t.clientX-s.left,o=t.clientY-s.top;this.offsetX=n-(n-this.offsetX)*e,this.offsetY=o-(o-this.offsetY)*e,this.scale=i,this.updateViewport()}}updateViewport(){this.viewport.style.transform=`translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.scale})`}layout(){this.nodes=[],this.links=[];const t=(e,i,s)=>{const n=e._collapsed,o=s+(e.children&&e.children.length>0&&!n?e.children.reduce((t,e)=>t+this.getTotalHeight(e),0):this.nodeHeight)/2-this.nodeHeight/2,h={x:i,y:o,text:e.text,id:e.id||Math.random().toString(36).substr(2,9),raw:e};if(this.nodes.push(h),e.children&&!n){let n=s;e.children.forEach(e=>{const s=t(e,i+this.nodeWidth+this.hGap,n);this.links.push({x1:i+this.nodeWidth,y1:o+this.nodeHeight/2,x2:s.x,y2:s.y+this.nodeHeight/2}),n+=this.getTotalHeight(e)})}return h};t(this.data,0,0)}getTotalHeight(t){return!t.children||0===t.children.length||t._collapsed?this.nodeHeight+this.vGap:t.children.reduce((t,e)=>t+this.getTotalHeight(e),0)}render(){this.layout(),this.svg.innerHTML="";this.viewport.querySelectorAll(".mindmap-node").forEach(t=>t.remove()),this.links.forEach(t=>{const e=document.createElementNS("http://www.w3.org/2000/svg","path"),i=`M ${t.x1} ${t.y1} C ${t.x1+this.hGap/2} ${t.y1}, ${t.x2-this.hGap/2} ${t.y2}, ${t.x2} ${t.y2}`;e.setAttribute("d",i),e.setAttribute("stroke","#4A90E2"),e.setAttribute("fill","none"),e.setAttribute("stroke-width","2"),this.svg.appendChild(e)}),this.nodes.forEach(t=>{const e=document.createElement("div");e.className="mindmap-node",t.raw._collapsed&&e.classList.add("collapsed"),e.style.position="absolute",e.style.left=`${t.x}px`,e.style.top=`${t.y}px`,e.style.width=`${this.nodeWidth}px`,e.style.height=`${this.nodeHeight}px`,e.style.border="2px solid #4A90E2",e.style.borderRadius="8px",e.style.backgroundColor="#fff",e.style.display="flex",e.style.alignItems="center",e.style.justifyContent="center",e.style.fontSize="14px",e.style.fontWeight="bold",e.style.color="#333",e.style.boxShadow="0 4px 6px rgba(0,0,0,0.1)",e.style.cursor="pointer",e.style.userSelect="none",e.style.transition="transform 0.2s",e.innerText=t.text,t.raw._collapsed&&(e.style.backgroundColor="#f0f0f0",e.style.borderStyle="dashed"),e.onmouseenter=()=>e.style.transform="scale(1.05)",e.onmouseleave=()=>e.style.transform="scale(1)",e.onclick=e=>{e.stopPropagation(),t.raw._collapsed=!t.raw._collapsed,this.render()},this.viewport.appendChild(e)})}addNode(t,e){const i=s=>{if(s.id===t||s.text===t)return s.children||(s.children=[]),s.children.push({text:e,id:Math.random().toString(36).substr(2,9)}),!0;if(s.children)for(const t of s.children)if(i(t))return!0;return!1};i(this.data),this.render()}removeNode(t){const e=i=>{if(!i.children)return!1;const s=i.children.findIndex(e=>e.id===t||e.text===t);if(-1!==s)return i.children.splice(s,1),!0;for(const t of i.children)if(e(t))return!0;return!1};this.data.id!==t&&this.data.text!==t?(e(this.data),this.render()):console.warn("Cannot remove root node")}}export{t as Mindmap}; +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).ApigoMindmap={})}(this,function(t){"use strict";class e{constructor(t={}){this.container=t.container,this.data=t.data||{id:"root",text:"Root",children:[]},this.nodeWidth=t.nodeWidth||120,this.nodeHeight=t.nodeHeight||40,this.hGap=t.hGap||60,this.vGap=t.vGap||20,this.nodes=[],this.links=[],this.init()}init(){this.container&&(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 t,e,i=!1;this.container.onmousedown=s=>{s.target.classList.contains("mindmap-node")||(i=!0,t=s.clientX,e=s.clientY)},window.onmousemove=s=>{i&&(this.offsetX+=s.clientX-t,this.offsetY+=s.clientY-e,t=s.clientX,e=s.clientY,this.updateViewport())},window.onmouseup=()=>{i=!1},this.container.onwheel=t=>{t.preventDefault();const e=t.deltaY>0?.9:1.1,i=this.scale*e;if(i<.1||i>5)return;const s=this.container.getBoundingClientRect(),n=t.clientX-s.left,o=t.clientY-s.top;this.offsetX=n-(n-this.offsetX)*e,this.offsetY=o-(o-this.offsetY)*e,this.scale=i,this.updateViewport()}}updateViewport(){this.viewport.style.transform=`translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.scale})`}layout(){this.nodes=[],this.links=[];const t=(e,i,s)=>{const n=e._collapsed,o=s+(e.children&&e.children.length>0&&!n?e.children.reduce((t,e)=>t+this.getTotalHeight(e),0):this.nodeHeight)/2-this.nodeHeight/2,h={x:i,y:o,text:e.text,id:e.id||Math.random().toString(36).substr(2,9),raw:e};if(this.nodes.push(h),e.children&&!n){let n=s;e.children.forEach(e=>{const s=t(e,i+this.nodeWidth+this.hGap,n);this.links.push({x1:i+this.nodeWidth,y1:o+this.nodeHeight/2,x2:s.x,y2:s.y+this.nodeHeight/2}),n+=this.getTotalHeight(e)})}return h};t(this.data,0,0)}getTotalHeight(t){return!t.children||0===t.children.length||t._collapsed?this.nodeHeight+this.vGap:t.children.reduce((t,e)=>t+this.getTotalHeight(e),0)}render(){this.layout(),this.svg.innerHTML="";this.viewport.querySelectorAll(".mindmap-node").forEach(t=>t.remove()),this.links.forEach(t=>{const e=document.createElementNS("http://www.w3.org/2000/svg","path"),i=`M ${t.x1} ${t.y1} C ${t.x1+this.hGap/2} ${t.y1}, ${t.x2-this.hGap/2} ${t.y2}, ${t.x2} ${t.y2}`;e.setAttribute("d",i),e.setAttribute("stroke","#4A90E2"),e.setAttribute("fill","none"),e.setAttribute("stroke-width","2"),this.svg.appendChild(e)}),this.nodes.forEach(t=>{const e=document.createElement("div");e.className="mindmap-node",t.raw._collapsed&&e.classList.add("collapsed"),e.style.position="absolute",e.style.left=`${t.x}px`,e.style.top=`${t.y}px`,e.style.width=`${this.nodeWidth}px`,e.style.height=`${this.nodeHeight}px`,e.style.border="2px solid #4A90E2",e.style.borderRadius="8px",e.style.backgroundColor="#fff",e.style.display="flex",e.style.alignItems="center",e.style.justifyContent="center",e.style.fontSize="14px",e.style.fontWeight="bold",e.style.color="#333",e.style.boxShadow="0 4px 6px rgba(0,0,0,0.1)",e.style.cursor="pointer",e.style.userSelect="none",e.style.transition="transform 0.2s",e.innerText=t.text,t.raw._collapsed&&(e.style.backgroundColor="#f0f0f0",e.style.borderStyle="dashed"),e.onmouseenter=()=>e.style.transform="scale(1.05)",e.onmouseleave=()=>e.style.transform="scale(1)",e.onclick=e=>{e.stopPropagation(),t.raw._collapsed=!t.raw._collapsed,this.render()},this.viewport.appendChild(e)})}addNode(t,e){const i=s=>{if(s.id===t||s.text===t)return s.children||(s.children=[]),s.children.push({text:e,id:Math.random().toString(36).substr(2,9)}),!0;if(s.children)for(const t of s.children)if(i(t))return!0;return!1};i(this.data),this.render()}removeNode(t){const e=i=>{if(!i.children)return!1;const s=i.children.findIndex(e=>e.id===t||e.text===t);if(-1!==s)return i.children.splice(s,1),!0;for(const t of i.children)if(e(t))return!0;return!1};this.data.id!==t&&this.data.text!==t?(e(this.data),this.render()):console.warn("Cannot remove root node")}}"undefined"!=typeof globalThis&&globalThis.Component&&globalThis.Component.register("Mindmap",t=>{let i=null;const s=()=>{t.innerHTML="",i=new e({container:t,data:t.state.data,nodeWidth:parseInt(t.getAttribute("node-width"))||120,nodeHeight:parseInt(t.getAttribute("node-height"))||40,hGap:parseInt(t.getAttribute("h-gap"))||60,vGap:parseInt(t.getAttribute("v-gap"))||20}),t.mindmapInstance=i};t.state.__watch("data",t=>i?(i.data=t,i.render()):s()),s(),t.addEventListener("unload",()=>i=null)}),"undefined"!=typeof globalThis&&(globalThis.Mindmap=e),t.Mindmap=e,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})}); diff --git a/mindmapTODO.md b/mindmapTODO.md index e993a85..930cd79 100644 --- a/mindmapTODO.md +++ b/mindmapTODO.md @@ -1,16 +1,15 @@ -# mindmapTODO.md +# mindmap TODO -## 当前版本 -- Tag: v1.0.0 -- 下一版本预期: v1.0.1 +- [x] 架构对齐:Align with `base` project's "No ESM" design. +- [ ] 构建配置:Modify `vite.config.js` to output only UMD (`mindmap.js` and `mindmap.min.js`). +- [ ] 源码重构: + - [ ] 注册 `Mindmap` 为全局组件 (`globalThis.Component.register`). + - [ ] 确保 `Mindmap` 类在 `globalThis` 上可用。 +- [ ] 测试验证:运行 `npm test` 确保功能无损。 +- [ ] 文档更新: + - [ ] 更新 `package.json` 版本号。 + - [ ] 更新 `README.md` 以反映组件化用法。 + - [ ] 更新 `CHANGELOG.md`。 -## 待办事项 -- [x] 实现树形数据的自动计算与布局。 -- [x] 支持节点的展开/折叠。 -- [x] 使用 SVG 绘制节点间的连接线。 -- [x] 优化大规模节点下的平移与缩放。 -- [x] 提供节点的动态增删 API。 - -## 重构建议 -- 考虑引入 `@web/state` 管理节点状态。 -- 使用 Canvas 或高级 SVG 技术优化超大规模树渲染。 +## 当前状态 (v1.0.1) +- 1111 个节点渲染耗时: ~40ms (from TEST.md) diff --git a/package-lock.json b/package-lock.json index cf0bf9e..1125930 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@web/mindmap", + "name": "@apigo.cc/mindmap", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@web/mindmap", + "name": "@apigo.cc/mindmap", "version": "1.0.0", "devDependencies": { "@playwright/test": "^1.40.0", diff --git a/package.json b/package.json index 36b72b9..7838021 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,8 @@ { "name": "@apigo.cc/mindmap", - "version": "1.0.1", + "version": "1.0.2", "type": "module", "main": "dist/mindmap.js", - "module": "dist/mindmap.js", "files": [ "dist" ], diff --git a/scripts/publish.js b/scripts/publish.js new file mode 100644 index 0000000..b562aa9 --- /dev/null +++ b/scripts/publish.js @@ -0,0 +1,48 @@ +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +try { + // 1. 获取最新 tag + let tag; + try { + tag = execSync('git describe --tags --abbrev=0', { encoding: 'utf8' }).trim(); + } catch (err) { + throw new Error('Failed to find git tags. Please make sure the repository has tags (e.g., v1.0.0) before publishing.'); + } + // 去掉 v 前缀 + const version = tag.startsWith('v') ? tag.slice(1) : tag; + + console.log(`Latest git tag: ${tag}, Version to publish: ${version}`); + + // 2. 读取并更新 package.json + const pkgPath = path.join(__dirname, '../package.json'); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + + // 保持原有名称(如果已经带有 @apigo.cc/ 前缀)或替换前缀 + if (!pkg.name.startsWith('@apigo.cc/')) { + const baseName = pkg.name.includes('/') ? pkg.name.split('/')[1] : pkg.name; + pkg.name = `@apigo.cc/${baseName}`; + } + pkg.version = version; + + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); + console.log(`Updated package.json: name=${pkg.name}, version=${pkg.version}`); + + // 3. 构建 + console.log('Running build...'); + execSync('npm run build', { stdio: 'inherit', cwd: path.join(__dirname, '..') }); + + // 4. 发布 + console.log('Publishing to npm...'); + const args = process.argv.slice(2).join(' '); + execSync(`npm publish --access public ${args}`, { stdio: 'inherit', cwd: path.join(__dirname, '..') }); + + console.log('Publish successful!'); +} catch (error) { + console.error('Publish failed:', error.message); + process.exit(1); +} diff --git a/src/index.js b/src/index.js index 86cc11e..ec42d71 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ /** - * @web/mindmap + * @apigo.cc/mindmap * 类 Xmind 体验的思维导图引擎 */ @@ -245,3 +245,33 @@ export class Mindmap { this.render(); } } + +// 注册为全局组件 +if (typeof globalThis !== 'undefined' && globalThis.Component) { + globalThis.Component.register('Mindmap', container => { + let mm = null; + const init = () => { + container.innerHTML = ''; + mm = new Mindmap({ + container, + data: container.state.data, + nodeWidth: parseInt(container.getAttribute('node-width')) || 120, + nodeHeight: parseInt(container.getAttribute('node-height')) || 40, + hGap: parseInt(container.getAttribute('h-gap')) || 60, + vGap: parseInt(container.getAttribute('v-gap')) || 20 + }); + container.mindmapInstance = mm; + }; + + container.state.__watch('data', (val) => mm ? (mm.data = val, mm.render()) : init()); + + init(); + + container.addEventListener('unload', () => mm = null); + }); +} + +// 挂载到全局 +if (typeof globalThis !== 'undefined') { + globalThis.Mindmap = Mindmap; +} diff --git a/test/index.html b/test/index.html index 13e28ec..8d5a7b4 100644 --- a/test/index.html +++ b/test/index.html @@ -10,9 +10,9 @@ @@ -22,7 +22,7 @@