X6 节点工具(Node Tool)实战:自定义按钮、删除按钮、包围框与文本编辑
【免费下载链接】X6🚀 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6
导读
本文基于 X6 官方示例 节点工具(Node Tools) 展开,系统讲解如何在画布节点上挂载交互工具(Tool):包括按钮类工具(自定义按钮、删除按钮)、包围框(Boundary)和文本编辑器(Node Editor),并延伸到连接桩 ToolTip 的进阶玩法。读完本文,你将掌握tools配置、addTools/removeTools/setTools三组 API 的使用方式,理解按钮定位与包围框尺寸的底层计算逻辑,并能直接复用文中的所有示例代码。
一、示例页结构与运行方式
该示例页属于 X6 仓库site/examples/node/下的子模块,入口文档 index.en.md(中文版见 index.zh.md)本身仅声明页面标题与路由重定向,真正的演示内容由 demo 目录 下的五个可运行示例承载,示例清单定义在 meta.json:
| 示例文件 | 主题 | 说明 |
|---|---|---|
| button.ts | Custom Button 自定义按钮 | 按钮点击后随机改变节点填充色 |
| button-remove.ts | Delete Button 删除按钮 | 一键移除节点 |
| boundary.ts | Boundary Box 包围框 | 高亮选中范围 |
| editable.ts | Text Editor 文本编辑 | 双击节点/边直接编辑文本 |
| port-tooltip.tsx | ToolTip For Port 连接桩提示 | 悬停连接桩显示提示气泡 |
运行这些示例无需额外配置,示例均在new Graph({ container, grid: true })基础上直接调用节点 API 完成,可复制到任意 X6 项目中验证。
二、节点工具的挂载机制:tools、addTools、removeTools
所有示例共用一套工具挂载机制。工具既可以随节点创建时静态声明(写入节点配置的tools数组),也可以在运行时通过 Cell 实例方法 动态增删:
node.addTools(tools, options):向节点追加一组工具;node.setTools(tools, options):整体替换工具集合;node.removeTools(options):清空当前节点的所有工具。
示例中典型的写法如下,静态声明与动态挂载两处配置完全一致:
// 静态声明:节点创建时即携带工具 const source = graph.addNode({ x: 180, y: 60, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1 } }, tools: [{ name: 'button', args: { /* ... */ } }], }) // 动态挂载:鼠标进入时添加工具,离开时移除 graph.on('node:mouseenter', ({ node }) => { if (node === target) node.addTools({ name: 'button', args: { /* ... */ } }) }) graph.on('node:mouseleave', ({ cell }) => { if (cell === target) cell.removeTools() })工具项(Tool Item)本身是独立于节点可视区域之外渲染的 SVG/DOM 元素,由src/view/tool/下的 ToolItem 基类 与 ToolView 容器 统一管理,通过args传入的x、y、offset、onClick等参数控制其表现与行为。
三、内置节点工具一览
X6 将工具以注册表(Registry)形式组织,节点工具的全部内置项定义在 src/registry/tool/index.ts 的nodeToolPresets中:
export const nodeToolPresets = { boundary: Boundary, button: Button, 'button-remove': Remove, 'node-editor': NodeEditor, }即节点上内置四类原生工具:boundary(包围框)、button(自定义按钮)、button-remove(删除按钮)、node-editor(文本编辑器)。其中button-remove与node-editor分别是Button与编辑器基类的派生/组合实现,后面逐一展开。工具名可直接写在tools数组的name字段,配合args传参,无需手动register。
四、自定义按钮(button)
4.1 完整示例
button.ts 演示了两种挂载形态:源节点在创建时通过tools静态携带按钮;目标节点在node:mouseenter时动态追加按钮、node:mouseleave时移除。其核心配置如下:
tools: [ { name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 14, stroke: '#fe854f', strokeWidth: 2, fill: 'white', cursor: 'pointer', }, }, { tagName: 'text', textContent: 'Btn', selector: 'icon', attrs: { fill: '#fe854f', fontSize: 10, textAnchor: 'middle', pointerEvents: 'none', y: '0.3em', }, }, ], x: '100%', y: '100%', offset: { x: -20, y: -20 }, onClick({ cell }: { cell: Cell }) { const fill = Color.randomHex() cell.attr({ body: { fill }, label: { fill: Color.invert(fill, true) }, }) }, }, }, ],点击按钮后调用cell.attr(...)修改节点的body与label填充色,实现一键换肤。动态挂载版本额外展示了按钮点击改变描边样式的效果(随机描边色 +strokeDasharray/strokeDashoffset递增的虚线动画效果)。
4.2 参数详解(对应源码 src/registry/tool/button.ts)
| 参数 | 类型 | 说明 |
|---|---|---|
markup | Markup数组 | 按钮的自定义 SVG 结构,可包含多个元素并按selector命名 |
x/y | number \| string | 按钮锚点在节点包围盒内的位置,支持百分比字符串(如'100%'定位到右下角),底层通过NumberExt.normalizePercentage(x, bbox.width)归一化 |
offset | number \| { x, y } | 锚点基础上的额外偏移;传数字时 x、y 同时生效 |
rotate | boolean | 是否跟随节点旋转(结合cell.getAngle()处理) |
useCellGeometry | boolean | 默认true,使用节点几何数据计算包围盒 |
onClick | (args) => void | 点击回调,回调参数含{ e, cell, view, btn },其中btn为按钮工具实例 |
按钮的定位逻辑在getNodeMatrix()中:先取节点包围盒(必要时按角度旋转),再以包围盒中心为原点,将归一化后的x/y与offset组合成 SVG 变换矩阵Dom.transform(...)应用到按钮容器上。onClick在基类的onMouseDown中触发,事件绑定了mousedown与touchstart,因此同时支持鼠标与触屏操作。
五、删除按钮(button-remove)
5.1 使用方式
button-remove.ts 用法与button完全一致,只需把name换成'button-remove':
tools: [ { name: 'button-remove', args: { x: '100%', y: 0, offset: { x: -10, y: 10 }, }, }, ],它默认渲染为一个红色圆形(#FF1D00)内嵌白色“×”图标的删除按钮,点击后节点连同按钮一起从画布移除。
5.2 默认配置与删除逻辑(源码 src/registry/tool/button.ts)
Remove继承自Button,其默认markup为圆形 + 两条交叉线段组成的删除图标,默认点击行为是:
onClick({ view, btn }) { btn.parent.remove() // 移除按钮本身 view.cell.remove({ ui: true, toolId: btn.cid }) // 删除节点 }即先移除按钮所在工具组(ToolView),再以{ ui: true }语义删除对应节点,避免触发 UI 无关的回调;toolId用于在删除时精确跳过该工具自身。其余定位参数(x、y、offset、distance等)与button通用。
六、包围框(boundary)
6.1 使用方式
boundary.ts 用包围框高亮节点,静态挂载与动态挂载并存:
tools: [ { name: 'boundary', args: { padding: 5, attrs: { fill: '#7c68fc', stroke: '#9254de', strokeWidth: 1, fillOpacity: 0.2, }, }, }, ],效果是节点四周出现一圈带透明填充、紫色描边的矩形框。动态版本在鼠标悬停时node.addTools({ name: 'boundary', args: { attrs: { ... } } }),移出时node.removeTools()。
6.2 参数与渲染原理(源码 src/registry/tool/boundary.ts)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
padding | number \| SideOptions | 10 | 包围框与节点边缘的间距,支持{ top, right, bottom, left }分别设置 |
useCellGeometry | boolean | true | 是否基于节点几何数据计算包围盒 |
rotate | boolean | — | 为true时包围框随节点旋转,否则按旋转后的轴对齐包围盒(bbox.bbox(angle)) |
attrs | SimpleAttrs | fill: 'none'、stroke: '#333'、stroke-width: 0.5、stroke-dasharray: '5, 5'、pointer-events: 'none' | 包围框 rect 的样式属性 |
渲染时工具会:
- 通过
Util.getViewBBox(view, useCellGeometry)取得节点包围盒; - 用
NumberExt.normalizeSides(padding)归一化 padding 并调用moveAndExpand外扩矩形; - 若节点有旋转角度,依据
rotate选择“随动旋转”或“轴对齐重算”; - 将最终
bbox写入容器 rect 的x/y/width/height属性。
默认样式是虚线描边且pointer-events: none,因此不会拦截鼠标事件,适合做纯视觉的高亮反馈。
七、文本编辑(node-editor / edge-editor)
7.1 使用方式
editable.ts 展示了节点与边上的文本编辑器:
// 节点 tools: [ { name: 'node-editor', args: { attrs: { backgroundColor: '#EFF4FF' }, }, }, ], // 边(同理) tools: [ { name: 'edge-editor', args: { attrs: { backgroundColor: '#fff' }, }, }, ],挂载后双击节点或边,即可在其上直接输入文本(contentEditable模式),编辑结果写回模型。
7.2 底层实现(源码 src/registry/tool/editor.ts)
NodeEditor/EdgeEditor由编辑器基类CellEditor派生。关键设计点:
- 触发方式:工具渲染时监听
cell:dblclick(onRender中绑定),双击进入编辑态; - 编辑介质:创建一个
contentEditable = 'true'的 HTMLdiv覆盖在节点/边上,样式(fontSize、fontFamily、color、backgroundColor)取自args.attrs; - 定位逻辑:未指定
x/y时编辑器居中于节点包围盒(translate(-50%, -50%)),并经过graph.localToGraph将局部坐标换算为画布坐标,兼容缩放场景(graph.scale()); - 交互边界:绑定
mousedown/touchstart与文档级mouseup/touchend/touchcancel,保证编辑结束、失焦行为的一致处理。
相关行为在 editor 测试 中有覆盖,可作为自定义编辑器行为的参考。
八、连接桩 ToolTip(port-tooltip)
最后一个示例 port-tooltip.tsx 不是内置工具,而是“回调 + 第三方 Tooltip 组件”组合出的交互:利用Graph的onPortRendered回调,在连接桩 DOM 渲染完成后为每个桩注册鼠标事件,再结合 antd 的Tooltip显示提示文本:
const graph = new Graph({ container: this.container, width: 800, height: 600, grid: true, onPortRendered({ contentContainer, port, node }) { const text = node.portProp(port.id, 'tip') as string registerPortTooltip(contentContainer, text) }, }) // 连接桩上自定义 tip 属性 ports: { groups: { bottom: { position: 'bottom', attrs: { circle: { magnet: true } } } }, items: [{ id: 'port-1', group: 'bottom', tip: 'port-1-tip' }], }registerPortTooltip在mouseenter时把portProp(port.id, 'tip')读到的文本写入ant-tooltip-inner,并将气泡定位到鼠标附近;mouseleave时把气泡移到屏幕外(-1000px)。用 React 渲染示例时,Tooltip组件常驻页面并设置overlayClassName="x6-tooltip"作为移动载体。该模式也可迁移到任意 UI 组件库,仅需将Tooltip换成对应实现。
九、常见问题与最佳实践
- 何时用静态
tools数组,何时用addTools?常驻型工具(如始终显示的删除按钮、包围框)适合写在节点配置里;交互型工具(悬停才出现)应放在node:mouseenter/node:mouseleave事件中动态增删,注意用节点引用做判断,避免所有节点共享同一套工具。 removeTools()会清空全部工具:如果只想移除单个工具,需自行管理工具实例或使用setTools整体重设。- 按钮遮挡交互:自定义
markup时,图标元素建议设置pointerEvents: 'none'(示例中的text元素即如此),让点击事件只落在按钮容器上。 - 包围框拦截事件:若包围框影响拖拽,保持默认的
pointer-events: none即可;需要可交互的框再覆盖attrs。 - 百分比定位依赖包围盒:
x: '100%'、y: '100%'按节点宽高归一化,offset再做像素级微调,二者组合可精确控制按钮落在节点边角内侧或外侧。
十、结语
节点工具是 X6 交互体系中最常用的一环:button与button-remove解决“点一下做什么”的入口问题,boundary提供轻量高亮,node-editor让文本就地可编辑,而onPortRendered+ Tooltip 的组合则展示了向连接桩扩展交互的通用思路。结合 节点工具注册表 与各工具源码(button.ts、boundary.ts、editor.ts),你既可以直接复用官方示例,也可以基于ToolItem基类派生自己的业务工具。
【免费下载链接】X6🚀 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考