X6 节点工具(NodeTool)完全指南:内置工具、事件交互与自定义注册
【免费下载链接】X6🚀 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6
节点小工具(NodeTool)是渲染在节点上的小型组件,通常附带交互能力——比如删除按钮、包围盒指示器、文本编辑器等。本文以 X6 官方文档 node-tool.zh.md 为骨架,结合仓库源码(src/registry/tool、src/view/tool、src/model/cell.ts)逐层剖析button、button-remove、boundary、node-editor四大内置工具的完整配置、底层实现原理,以及两种自定义工具注册方式。读完本文,你将掌握在 X6 中任意添加、删除、定制节点交互组件的完整实战方案。
什么是节点工具:在节点上挂载小组件
节点工具是附加在节点视图之上的"小组件",它们与节点的渲染内容相互独立,通常负责承载交互功能。最典型的例子是删除按钮:当鼠标悬停在节点上时显示一个删除按钮,点击后删除对应节点。
在 X6 中,工具与节点的绑定方式非常灵活,既可以在创建节点时声明,也可以在节点创建后动态增删:
// 创建节点时添加小工具 graph.addNode({ ..., tools: [ { name: 'button-remove', args: { x: 10, y: 10 }, }, ], }) // 创建节点后添加小工具 node.addTools([ { name: 'button-remove', args: { x: 10, y: 10 }, }, ]) // 删除工具 graph.on("node:mouseleave", ({ node }) => { if (node.hasTool("button-remove")) { node.removeTool("button-remove"); } });从源码结构看,节点工具的增删最终都落到Cell模型层的工具管理 API 上(src/model/cell.ts):
addTools(items, options?):向节点追加工具列表。内部会将单数形式的工具包装为数组,并通过store.set('tools', ...)写入模型数据;removeTools(options?):移除全部工具;hasTool(name)/hasTools(name?):按名称(或索引)判断工具是否存在;removeTool(nameOrIndex, options?):按名称或索引移除指定工具。
由于工具信息存储在 Cell 的store中,它是可随图数据序列化的,这也是工具配置能被graph.addNode声明式写入的原因。
X6 默认提供了四个内置节点工具(源码注册于 src/registry/tool/index.ts):
- button 在指定位置处渲染一个按钮,支持自定义按钮的点击交互。
- button-remove 在指定的位置处,渲染一个删除按钮,点击时删除对应的节点。
- boundary 根据节点的包围盒渲染一个包围节点的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互。
- node-editor 提供节点文本编辑功能。
button:可完全自定义的交互按钮
button工具在指定位置渲染一个按钮,并通过onClick回调支持任意点击交互。它也是后面button-remove的基类。
配置项
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| x | number | string | 0 | 相对于节点的左上角 X 轴的坐标,小数和百分比表示相对位置。 |
| y | number | string | 0 | 相对于节点的左上角 Y 轴的坐标,小数和百分比表示相对位置。 |
| offset | number |{ x: number, y: number } | 0 | 在x和y基础上的偏移量。 |
| rotate | boolean | - | 是否跟随节点旋转。 |
| useCellGeometry | boolean | true | 是否使用几何计算的方式来计算元素包围盒,开启后会有性能上的提升,如果出现计算准度问题,请将它设置为false。 |
| markup | Markup.JSONMarkup | - | 渲染按钮的 Markup 定义。 |
| onClick | (args: {e: Dom.MouseDownEvent, cell: Cell, view: CellView }) => void | - | 点击按钮的回调函数。 |
其中x、y支持小数与百分比:小数表示相对节点包围盒宽/高的比例,百分比写法如'100%'同样表示相对比例。在源码实现中,它们统一通过NumberExt.normalizePercentage(x, bbox.width)归一化为实际像素值(src/registry/tool/button.ts)。
offset若为数字,则同时作用于 X、Y 两个方向;若为对象则分别指定x、y偏移(src/registry/tool/button.ts)。
rotate控制按钮是否跟随节点旋转。当rotate为false时,定位矩阵会先对包围盒做一次反向旋转bbox.bbox(angle),保证按钮始终保持水平;为true时则在矩阵中叠加matrix.rotate(angle)(src/registry/tool/button.ts)。
useCellGeometry决定包围盒的计算方式:开启时直接使用cell.getBBox()的几何计算,性能更优;关闭时则基于真实渲染元素计算包围盒,结果更精确(src/registry/tool/util.ts)。
典型用法:Hover 时动态添加与移除
// 鼠标 Hover 时添加按钮 graph.on('node:mouseenter', ({ node }) => { node.addTools({ name: 'button', args: { markup: ..., x: 0, y: 0, offset: { x: 18, y: 18 }, onClick({ view }) { ... }, }, }) }) // 鼠标移开时删除按钮 graph.on('node:mouseleave', ({ node }) => { node.removeTools() // 删除所有的工具 })源码解析:定位矩阵与点击守卫
Button类继承自ToolItem(src/registry/tool/button.ts),其默认配置声明了mousedown、touchstart两类事件绑定。定位时,它根据x、y、offset、rotate、useCellGeometry组合出 SVG 变换矩阵,并通过Dom.transform(container, matrix, { absolute: true })应用到容器上(src/registry/tool/button.ts)。
点击交互的关键在onMouseDown处理器(src/registry/tool/button.ts):
protected onMouseDown(e: Dom.MouseDownEvent) { if (this.guard(e)) { return } e.stopPropagation() e.preventDefault() const onClick = this.options.onClick if (typeof onClick === 'function') { FunctionExt.call(onClick, this.cellView, { e, view: this.cellView, cell: this.cellView.cell, btn: this, }) } }它首先通过guard(e)做事件守卫(由graph.view.guard实现,见 src/view/tool/tool-item.ts),随后阻止事件冒泡,最后以cellView为this上下文调用onClick回调,回调参数中额外携带btn指向按钮工具实例自身。
官方在线示例可参考 site/src/api/node-tool/button/index.tsx。
button-remove:开箱即用的删除按钮
button-remove是button的一个特例,在指定位置渲染一个删除按钮,点击即删除对应节点。它支持button的全部配置项。
const source = graph.addNode({ ..., // 添加一个始终显示的删除按钮 tools: [ { name: 'button-remove', args: { x: '100%', y: 0, offset: { x: -10, y: 10 }, }, }, ], })源码解析:内建 Markup 与默认点击行为
Remove类直接继承Button(src/registry/tool/button.ts),其默认配置内置了两段 Markup:
- 一个
circle(selector:button),半径为 7、填充色为#FF1D00、cursor: pointer; - 一个
path(selector:icon),绘制M -3 -3 3 3 M -3 3 3 -3的叉号图形,白色描边、不响应指针事件。
默认的onClick实现为:
onClick({ view, btn }) { btn.parent.remove() view.cell.remove({ ui: true, toolId: btn.cid }) }即先移除所属的ToolsView,再以{ ui: true, toolId }参数删除对应 Cell——ui: true表示该删除动作由用户交互触发,可被历史记录等机制识别,toolId用于关联本次工具触发的删除。
官方在线示例可参考 site/src/api/node-tool/button-remove/index.tsx。
boundary:纯渲染的包围盒指示器
boundary根据节点的包围盒渲染一个包围节点的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互,适合做选中态、聚焦态的视觉提示。
配置项
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| tagName | string | rect | 使用何种图形渲染。 |
| rotate | boolean | - | 图形是否跟随节点旋转。 |
| padding | SideOptions | 10 | 边距。 |
| attrs | KeyValue | object | 图形属性。 |
| useCellGeometry | boolean | true | 是否使用几何计算的方式来计算元素包围盒,开启后会有性能上的提升,如果出现计算准度问题,请将它设置为false。 |
其中attrs的默认值(默认样式)为:
{ fill: 'none', stroke: '#333', 'stroke-width': 0.5, 'stroke-dasharray': '5, 5', 'pointer-events': 'none', }SideOptions的类型定义如下:
type SideOptions = | number | { vertical?: number horizontal?: number left?: number top?: number right?: number bottom?: number }源码解析:边距归一化与旋转处理
Boundary类的默认配置(src/registry/tool/boundary.ts)与文档表格完全一致:tagName: 'rect'、padding: 10、默认attrs为虚线边框。
update()方法内部的处理流程值得关注(src/registry/tool/boundary.ts):
- 通过
NumberExt.normalizeSides(options.padding)将SideOptions(数字或四方向对象)归一化为{ left, top, right, bottom }结构; - 调用
Util.getViewBBox(view, useCellGeometry)获取包围盒,若useCellGeometry为true则走几何计算快速路径; - 用
bbox.moveAndExpand({ x: -padding.left, y: -padding.top, ... })按边距向外扩张; - 若节点带旋转角度,
rotate为true时对容器做绝对旋转,false时则直接对包围盒取旋转后的外接矩形bbox.bbox(angle); - 最终通过
Dom.attr(container, bbox.toJSON())把矩形的位置与尺寸写回 DOM。
使用示例
const source = graph.addNode({ ..., tools: [ { name: 'boundary', args: { padding: 5, attrs: { fill: '#7c68fc', stroke: '#333', 'stroke-width': 1, 'fill-opacity': 0.2, }, }, }, ], })官方在线示例可参考 site/src/api/node-tool/boundary/index.tsx。
node-editor:节点文本就地编辑
node-editor提供节点上文本编辑功能。它的实现类是NodeEditor,继承自CellEditor(src/registry/tool/editor.ts),底层创建一个contentEditable = 'true'的 HTML 编辑区,并自动监听节点的cell:dblclick事件进入编辑态(src/registry/tool/editor.ts)。
配置项
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| x | number | string | - | 相对于节点的左上角 X 轴的坐标,小数和百分比表示相对位置 |
| y | number | string | - | 相对于节点的左上角 Y 轴的坐标,小数和百分比表示相对位置 |
| attrs/fontSize | string | 14 | 编辑文本字体大小 |
| attrs/color | string | #000 | 编辑文本字体颜色 |
| attrs/fontFamily | string | Arial, helvetica, sans-serif | 编辑文本的字体 |
| attrs/backgroundColor | string | #fff | 编辑区域的背景色 |
| getText | string |(this: CellView, args: {cell: Cell}) => string | - | 获取原文本方法,在自定义markup场景需要自定义getText方法 |
| setText | string |(this: CellView, args: {cell: Cell, value: string}) => void | - | 设置新文本,在自定义markup场景需要自定义setText方法 |
2.8.0 版本行为变化
:::warning{title=注意} 需要注意的是,2.8.0 版本后不需要在双击事件中去动态添加工具,也就不需要传入事件参数。 :::
// 2.8.0 版本之前使用方式 graph.on('node:dblclick', ({ node, e }) => { node.addTools({ name: 'node-editor', args: { event: e, }, }) }) // 2.8.0 版本之后使用方式 node.addTools({ name: 'node-editor', })2.8.0 之后,CellEditor在渲染时会自动在cellView上监听cell:dblclick(src/registry/tool/editor.ts):首次双击即创建编辑元素、填充原文本、自动聚焦并全选文本;编辑结束后(点击编辑器外部或再次双击)通过updateCell()写回文本并移除工具(src/registry/tool/editor.ts)。
自定义 markup 场景:getText 与 setText 的两种形式
如果在节点中自定义了markup,往往需要自定义getText和setText方法来正确获取和设置编辑文本。这两个配置都支持函数和字符串两种形式:
- 函数形式:直接返回/写入文本,逻辑最灵活;
- 字符串形式:本质是文本所在属性路径。官方建议优先使用字符串形式,因为函数无法序列化,而字符串形式能保证图数据完全可序列化——否则可能出现渲染画布后文本编辑功能异常。
例如:
node.addTools({ name: 'node-editor', args: { getText: 'a/b', setText: 'c/d', }, })上面配置表示:
- 获取编辑文本:
node.attr('a/b') - 设置编辑文本:
node.attr('c/d', value)
在源码实现中,字符串形式的getText/setText会在内部通过cell.attr(path)与cell.attr(path, value)完成读写;而节点的默认配置恰好是getText: 'text/text'、setText: 'text/text'(src/registry/tool/editor.ts),即默认读取与写入node.attr('text/text')。
官方在线示例可参考 site/src/api/node-tool/node-editor/index.tsx。
自定义工具:两种注册方式
当内置工具无法满足需求时,可以通过以下两种方式注册自定义节点工具。
方式一:继承 ToolItem 实现工具类
继承ToolItem实现一个工具类,难度较高,要求对 ToolItem 类都有所了解,可以参考上述内置工具的源码(button.ts、boundary.ts、editor.ts)。
Graph.registerNodeTool('button', Button)方式二:继承已注册工具并快速修改配置
继承已经注册的工具,在继承基础上修改配置。ToolItem基类上提供了一个静态方法define来快速实现继承并修改配置(src/view/tool/tool-item.ts)。define内部通过ObjectExt.createClass创建子类,并把传入的配置合并到新类的defaults上。
const MyButton = Button.define<Button.Options>({ name: 'my-btn', markup: ..., onClick({ view }) { ... }, }) Graph.registerNodeTool('my-btn', MyButton, true)同时,Graph.registerNodeTool方法提供了一种快速继承并指定默认选项的声明式写法——直接在注册对象中指定inherit字段:
Graph.registerNodeTool('my-btn', { inherit:'button', // 基类名称,使用已经注册的工具名称。 markup: ..., onClick: ..., })这种写法的底层实现位于注册表 src/registry/tool/index.ts:注册时若检测到inherit字段,会先通过this.get(inherit)取出基类,再调用parent.define.call(parent, others)完成继承。Graph.registerNodeTool本身正是nodeToolRegistry.register的静态别名(src/graph/graph.ts)。
自定义工具后,节点的tools配置中即可直接引用新名称:
graph.addNode({ tools: [{ name: 'my-btn', args: { ... } }], })官方在线示例可参考 site/src/api/node-tool/custom-button/index.tsx。
工具生命周期与渲染机制补充
理解工具的生命周期有助于排查定位与显示问题。在 X6 中,每个 Cell 的工具由ToolsView统一管理(src/view/tool/tool-view.ts):
ToolsView会同时创建 SVG 容器(<g>)与 HTML 容器(<div>)两套容器,并打上data-cell-id、data-tools-name标记;SVG 类工具渲染进svgContainer,HTML 类工具(如node-editor的编辑区)渲染进htmlContainer;- 每个工具项实例是
ToolItem子类。ToolItem默认以<g>(tagName: 'g')作为容器,支持markupJSON 解析、events/documentEvents事件委托、show()/hide()/focus()/blur()等控制方法(src/view/tool/tool-item.ts); - 工具配置写入 Cell 的
store,因此graph.addNode声明式添加与node.addTools命令式添加最终殊途同归。
与工具相关的单元测试位于tests/view/tool/tool-item.spec.ts 与tests/view/tool/tool-view.spec.ts,可用于验证工具渲染、事件委托与增删行为。
综上,X6 节点工具体系以ToolItem为基类、以注册表nodeToolRegistry为中枢、以 Cell 的store为数据载体:内置的button、button-remove、boundary、node-editor覆盖了按钮交互、删除、包围盒提示、文本编辑四大高频场景,而define+inherit的组合则让自定义工具变得声明式且可序列化。在实际项目中,优先复用内置工具并微调args,遇到特殊交互时再走自定义注册路径,即可高效完成大部分节点级交互需求。
【免费下载链接】X6🚀 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考