news 2026/9/17 3:19:56

X6 节点工具(NodeTool)完全指南:内置工具、事件交互与自定义注册

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
X6 节点工具(NodeTool)完全指南:内置工具、事件交互与自定义注册

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)逐层剖析buttonbutton-removeboundarynode-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的基类。

配置项

参数名类型默认值说明
xnumber | string0相对于节点的左上角 X 轴的坐标,小数和百分比表示相对位置。
ynumber | string0相对于节点的左上角 Y 轴的坐标,小数和百分比表示相对位置。
offsetnumber |{ x: number, y: number }0xy基础上的偏移量。
rotateboolean-是否跟随节点旋转。
useCellGeometrybooleantrue是否使用几何计算的方式来计算元素包围盒,开启后会有性能上的提升,如果出现计算准度问题,请将它设置为false
markupMarkup.JSONMarkup-渲染按钮的 Markup 定义。
onClick(args: {e: Dom.MouseDownEvent, cell: Cell, view: CellView }) => void-点击按钮的回调函数。

其中xy支持小数与百分比:小数表示相对节点包围盒宽/高的比例,百分比写法如'100%'同样表示相对比例。在源码实现中,它们统一通过NumberExt.normalizePercentage(x, bbox.width)归一化为实际像素值(src/registry/tool/button.ts)。

offset若为数字,则同时作用于 X、Y 两个方向;若为对象则分别指定xy偏移(src/registry/tool/button.ts)。

rotate控制按钮是否跟随节点旋转。当rotatefalse时,定位矩阵会先对包围盒做一次反向旋转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),其默认配置声明了mousedowntouchstart两类事件绑定。定位时,它根据xyoffsetrotateuseCellGeometry组合出 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),随后阻止事件冒泡,最后以cellViewthis上下文调用onClick回调,回调参数中额外携带btn指向按钮工具实例自身。

官方在线示例可参考 site/src/api/node-tool/button/index.tsx。

button-remove:开箱即用的删除按钮

button-removebutton的一个特例,在指定位置渲染一个删除按钮,点击即删除对应节点。它支持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、填充色为#FF1D00cursor: 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根据节点的包围盒渲染一个包围节点的矩形。注意,该工具仅仅渲染一个矩形,不带任何交互,适合做选中态、聚焦态的视觉提示。

配置项

参数名类型默认值说明
tagNamestringrect使用何种图形渲染。
rotateboolean-图形是否跟随节点旋转。
paddingSideOptions10边距。
attrsKeyValueobject图形属性。
useCellGeometrybooleantrue是否使用几何计算的方式来计算元素包围盒,开启后会有性能上的提升,如果出现计算准度问题,请将它设置为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):

  1. 通过NumberExt.normalizeSides(options.padding)SideOptions(数字或四方向对象)归一化为{ left, top, right, bottom }结构;
  2. 调用Util.getViewBBox(view, useCellGeometry)获取包围盒,若useCellGeometrytrue则走几何计算快速路径;
  3. bbox.moveAndExpand({ x: -padding.left, y: -padding.top, ... })按边距向外扩张;
  4. 若节点带旋转角度,rotatetrue时对容器做绝对旋转,false时则直接对包围盒取旋转后的外接矩形bbox.bbox(angle)
  5. 最终通过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)。

配置项

参数名类型默认值说明
xnumber | string-相对于节点的左上角 X 轴的坐标,小数和百分比表示相对位置
ynumber | string-相对于节点的左上角 Y 轴的坐标,小数和百分比表示相对位置
attrs/fontSizestring14编辑文本字体大小
attrs/colorstring#000编辑文本字体颜色
attrs/fontFamilystringArial, helvetica, sans-serif编辑文本的字体
attrs/backgroundColorstring#fff编辑区域的背景色
getTextstring |(this: CellView, args: {cell: Cell}) => string-获取原文本方法,在自定义markup场景需要自定义getText方法
setTextstring |(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,往往需要自定义getTextsetText方法来正确获取和设置编辑文本。这两个配置都支持函数字符串两种形式:

  • 函数形式:直接返回/写入文本,逻辑最灵活;
  • 字符串形式:本质是文本所在属性路径。官方建议优先使用字符串形式,因为函数无法序列化,而字符串形式能保证图数据完全可序列化——否则可能出现渲染画布后文本编辑功能异常。

例如:

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-iddata-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为数据载体:内置的buttonbutton-removeboundarynode-editor覆盖了按钮交互、删除、包围盒提示、文本编辑四大高频场景,而define+inherit的组合则让自定义工具变得声明式且可序列化。在实际项目中,优先复用内置工具并微调args,遇到特殊交互时再走自定义注册路径,即可高效完成大部分节点级交互需求。

【免费下载链接】X6🚀 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 3:18:29

火电机组协调控制Simulink高保真建模与工程落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 3:15:39

npm.ps1 无法加载?TaoToken 这样让 Codex 改执行策略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 3:15:33

OpenClaw 报 401?TaoToken 的 Base URL 别带 /v1

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华