news 2026/9/24 13:50:51

G6 上下文菜单插件(Contextmenu)实战指南:右键菜单配置、事件回调与源码实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
G6 上下文菜单插件(Contextmenu)实战指南:右键菜单配置、事件回调与源码实现解析
  • 数据可视化
  • 前端
  • 图表库

【免费下载链接】G6

♾ A Graph Visualization Framework in JavaScript.

项目地址:https://gitcode.com/gh_mirrors/g6/G6
点击查看免费下载

导读

本文基于 G6(A Graph Visualization Framework in JavaScript)当前仓库的 Contextmenu.zh.md 官方文档,系统讲解内置插件contextmenu的完整用法。你会掌握:如何通过plugins配置快速为图元素挂载右键菜单、使用getItems/getContent两种方式生成菜单内容、通过enable精确控制菜单在节点/边/Combo 上的生效范围,以及借助onClick回调拿到value、DOM 节点与当前元素实现「查看详情」「删除节点」等具体操作。文章同时结合 contextmenu 插件源码 与单元测试,说明菜单定位、边界约束、事件绑定与隐藏逻辑的底层实现,帮助你从「会用」进阶到「知其所以然」。

概述:什么是上下文菜单

上下文菜单(Contextmenu),也被称为右键菜单,是当用户在某个特定区域(画布、节点、边或 Combo)上点击后出现的一个菜单。它支持在点击前后触发自定义事件,允许你针对某一元素进行按需的单独控制。

在 G6 中,contextmenu是一个内置插件,通过plugins配置项注册即可使用。它适用于元素的各种交互操作场景,例如:

  • 查看节点详情
  • 删除节点
  • 变更边的起点
  • 对 Combo 进行展开/收起等批量操作

核心价值在于:把「对某一项元素的特定操作」集中到一个浮层菜单中,无需占用画布空间,也不依赖额外的工具栏 UI。

基本用法:三步挂载右键菜单

在 Graph 的plugins配置中声明type: 'contextmenu'即可。一个最简示例:

const graph = new Graph({ plugins: [ { type: 'contextmenu', // 只在节点上开启右键菜单,默认全部元素都开启 enable: (e) => e.targetType === 'node', getItems: () => { return [{ name: '查看详情', value: 'detail' }]; }, onClick: (value) => { if (value === 'detail') console.log('展示节点详情'); }, }, ], });

从源码 Contextmenu 类 可以看到,插件在构造时即完成三件事:

  1. initElement():通过 createPluginContainer 创建容器 DOM,追加className(默认g6-contextmenu),并注入内置样式CONTEXTMENU_CSSdocument.head
  2. 绑定事件:分别监听canvasnodeedgecombo四类目标的contextmenu(或click)事件(见bindEvents);
  3. 调用update(options)合并默认配置。

事件监听的具体实现在 bindEvents 中:

graph.on(`canvas:${trigger}`, this.onTriggerEvent); graph.on(`node:${trigger}`, this.onTriggerEvent); graph.on(`edge:${trigger}`, this.onTriggerEvent); graph.on(`combo:${trigger}`, this.onTriggerEvent);

也就是说,菜单默认在画布、节点、边、Combo 上全部生效,enable配置用于按需收窄生效范围。事件类型常量可参考 节点事件枚举(如NodeEvent.CONTEXT_MENU = 'node:contextmenu')。

配置项详解

下表为contextmenu插件的完整配置项(与 ContextmenuOptions 接口一一对应):

属性描述类型默认值必选
className给菜单的 DOM 追加的类名,用于自定义样式stringg6-contextmenu
trigger如何触发右键菜单:contextmenu表示右键触发,click表示点击触发click|contextmenucontextmenu
offset菜单显式 X、Y 方向的偏移量[number, number][4, 4]
onClick当菜单被点击后,触发的回调方法(value: string, target: HTMLElement, current: Element) => void-
getItems返回菜单的项目列表,支持Promise类型的返回值,是getContent的快捷配置(event: IElementEvent) => Item[] | Promise<Item[]>-
getContent返回菜单的内容,支持Promise类型的返回值,也可以使用getItems进行快捷配置(event: IElementEvent) => HTMLElement | string | Promise<HTMLElement | string>-
loadingContentgetContent返回一个Promise时,使用的菜单内容(加载占位)HTMLElement | stringLoading...
enable是否可用,通过参数判断是否支持右键菜单,默认是全部可用boolean | (event: IElementEvent) => booleantrue

源码中的默认值(defaultOptions)为:

static defaultOptions: Partial<ContextmenuOptions> = { trigger: 'contextmenu', offset: [4, 4], loadingContent: '<div class="g6-contextmenu-loading">Loading...</div>', getContent: () => 'It is a empty context menu.', enable: () => true, };

几点补充说明:

  • 若未配置getItemsgetContentgetContent的默认实现会返回空菜单占位文案;
  • loadingContent只在getContent返回Promise时作为加载态展示;
  • enable接收的事件类型是 IElementEvent,其targetType取值为'canvas' | 'node' | 'edge' | 'combo'(见 TargetedEvent),这是精确控制生效范围的关键字段。

Item:菜单项结构

每个菜单项目(Item)仅含两个字段:

属性描述类型必选
name菜单项显示的名字string
value菜单项对应的值string

类型定义见 util.ts 中的 Item 类型。当使用getItems时,插件内部通过 getContentFromItems 将数组渲染为 HTML 字符串:

export function getContentFromItems(items: Item[]) { return ` <ul class="g6-contextmenu-ul"> ${items.map((item) => `<li class="g6-contextmenu-li" value="${item.value}">${item.name}</li>`).join('')} </ul> `; }

生成的每个<li class="g6-contextmenu-li">节点的value属性即菜单项的value,点击时会被onClick回调读取。

onClick:点击回调的三个参数

点击菜单项后触发onClick,函数有三个参数:

  • value:对应菜单项的value
  • target:对应菜单项容器的 DOM 节点(即被点击的<li>元素);
  • current:对应触发菜单的元素(如节点/边/Combo),可通过它获取元素信息(如id)或直接对元素进行修改。

onClick的触发实现在源码 onMenuItemClick:点击事件冒泡到document后,判断event.target是否包含g6-contextmenu-li类名,若命中则取出value属性并调用onClick?.(value, event.target, this.targetElement!),随后自动隐藏菜单;如果trigger !== 'click',任何外部点击也会隐藏菜单。

代码示例

基础右键菜单

同时展示「查看详情」与「删除」两个菜单项,trigger使用默认的右键触发:

const data = { nodes: [ { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } }, { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } }, ], edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }], }; const graph = new Graph({ data, layout: { type: 'grid' }, plugins: [ { type: 'contextmenu', trigger: 'contextmenu', // 'click' or 'contextmenu' onClick: (value, target, current) => { alert('You have clicked the「' + value + '」item'); }, getItems: () => { return [ { name: '查看详情', value: 'detail' }, { name: '删除', value: 'delete' }, ]; }, }, ], });

注意:原文档示例中回调参数v未定义,实际应使用第一个参数valueonClick的参数名,如上所示。

边的右键菜单

通过enable: (e) => e.targetType === 'edge'将菜单限定在边上,例如实现「变更起点」操作:

const data = { nodes: [ { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } }, { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } }, ], edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }], }; const graph = new Graph({ data, layout: { type: 'grid' }, plugins: [ { type: 'contextmenu', trigger: 'contextmenu', getItems: () => { return [{ name: '变更起点', value: 'change' }]; }, onClick: (value) => { if (value === 'change') console.log('这里执行变更起点操作'); }, // 仅在边上开启右键菜单 enable: (e) => e.targetType === 'edge', }, ], });

异步加载菜单项

getItems支持返回Promise,可从服务器或其他异步源动态获取菜单配置,适合菜单项需要按权限或业务动态下发的场景:

const data = { nodes: [ { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } }, { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } }, ], edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }], }; const graph = new Graph({ data, layout: { type: 'grid' }, plugins: [ { type: 'contextmenu', trigger: 'contextmenu', getItems: async () => { // 可以从服务器或其他异步源获取菜单配置 const response = await fetch('/api/contextmenu-config'); const items = await response.json(); return items; }, // 仅在节点上开启右键菜单 enable: (e) => e.targetType === 'node', }, ], });

异步场景下,getContent同样支持Promise<HTMLElement | string>,此时可配合loadingContent展示加载占位。菜单内容渲染逻辑见 getDOMContent:

private async getDOMContent(event: IElementEvent) { const { getContent, getItems } = this.options; if (getItems) { return getContentFromItems(await getItems(event)); } return await getContent(event); }

动态控制菜单项

利用getItems的回调参数event,可以根据被触发元素的不同(event.targetevent.target.type)返回不同的菜单项,实现真正意义上的「按元素动态生成菜单」:

const data = { nodes: [ { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } }, { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } }, ], edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }], }; const graph = new Graph({ data, layout: { type: 'grid' }, plugins: [ { type: 'contextmenu', trigger: 'contextmenu', getItems: (e) => { if (e.target.id === 'node-1') { return [ { name: '删除节点', value: 'delete', }, ]; } if (e.target.type === 'edge') { return [ { name: '移动边', value: 'move', }, ]; } return []; }, }, ], });

实际案例:带交互的完整示例

以下是一个可直接运行的完整案例(对应仓库 plugin-contextmenu demo 的简化版):构建一个以node-0为中心的星型图,右键任意节点弹出「展开一度关系 / 查看详情」菜单,并配合zoom-canvasdrag-canvasdrag-element三个内置交互:

import { Graph } from '@antv/g6'; const data = { nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }], edges: [ { source: 'node-0', target: 'node-1' }, { source: 'node-0', target: 'node-2' }, { source: 'node-0', target: 'node-3' }, { source: 'node-0', target: 'node-4' }, { source: 'node-1', target: 'node-0' }, { source: 'node-2', target: 'node-0' }, { source: 'node-3', target: 'node-0' }, { source: 'node-4', target: 'node-0' }, { source: 'node-5', target: 'node-0' }, ], }; const graph = new Graph({ container: 'container', data, layout: { type: 'grid', }, behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'], plugins: [ { type: 'contextmenu', trigger: 'contextmenu', // 'click' or 'contextmenu' onClick: (v) => { alert('You have clicked the「' + v + '」item'); }, getItems: () => { return [ { name: '展开一度关系', value: 'spread' }, { name: '查看详情', value: 'detail' }, ]; }, enable: (e) => e.targetType === 'node', }, ], }); graph.render();

源码级原理剖析

菜单显示与边界约束

show方法是菜单出现的核心(show),其流程为:

  1. 先执行enable判断:函数形式返回false或布尔值为false时直接隐藏并返回;
  2. 等待getDOMContent产出的内容(HTMLElement 或 HTML 字符串),写入菜单容器;
  3. 依据事件client坐标与画布容器getBoundingClientRect计算菜单位置,加上offset偏移量;
  4. 对菜单位置做边界约束——Math.max(padding, Math.min(left, containerWidth - menuWidth - padding)),确保菜单不会溢出画布容器(padding 为 4px);
  5. 记录targetElementonClick使用。
let left = event.client.x - clientRect.left + offset[0]; let top = event.client.y - clientRect.top + offset[1]; // 限制菜单位于画布容器范围内 left = Math.max(padding, Math.min(left, containerWidth - menuWidth - padding)); top = Math.max(padding, Math.min(top, containerHeight - menuHeight - padding));

事件绑定与默认行为处理

bindEvents中监听的事件回调 onTriggerEvent 会调用event.preventDefault?.()

  • trigger: 'contextmenu'时,阻止浏览器原生右键菜单弹出(否则会出现「双层菜单」);
  • trigger: 'click'时无需阻止默认行为。

此外,容器还注册了wheel事件处理器wheelHandlere.stopPropagation()),阻止滚轮事件冒泡到画布,避免误触画布的zoom-canvas缩放拦截逻辑(见 initElement)。

菜单隐藏策略

菜单隐藏有两个触发时机(onMenuItemClick):

  • 点击任意菜单项后立即隐藏;
  • trigger !== 'click'时,点击document任意位置都会隐藏菜单(document.addEventListener('click', this.onMenuItemClick))。

hide()方法将容器display置为none并清空targetElement引用。单元测试 contextmenu.spec.ts 完整验证了这套行为:模拟NodeEvent.CONTEXT_MENU事件后断言.g6-contextmenu-ul与两个.g6-contextmenu-li出现、点击菜单项后onClick被调用且菜单隐藏、document.body.click()后菜单再次隐藏。

自定义样式

插件通过 CONTEXTMENU_CSS 注入默认样式:白色半透明背景、圆角、阴影、悬停高亮(.g6-contextmenu-li:hover背景变灰)、最大宽度 256px / 最小宽度 96px、自定义滚动条等。你可以通过className追加自定义类覆盖这些样式,或直接针对.g6-contextmenu.g6-contextmenu-li编写全局 CSS。

动态更新与实例 API

contextmenu插件遵循 G6 插件体系的通用管理 API(见 runtime/plugin.ts 与 Graph 的插件方法):

  • graph.setPlugins(plugins):整体替换插件列表,可传入函数基于旧配置增量修改;
  • graph.updatePlugin({ key, ... }):按key更新某个插件的部分配置——注意更新前必须在插件配置中声明key
  • graph.getPluginInstance('key'):获取插件实例,从而调用show(event)hide()update(options)等公开方法,实现「手动弹出/隐藏菜单」等高级控制;
  • graph.getPlugins():获取当前插件配置列表。

例如动态切换触发方式:

graph.updatePlugin({ key: 'contextmenu', trigger: 'click', // 从右键触发切换为点击触发 });

update方法内部会先unbindEvents()解绑旧事件,再合并配置并重新bindEvents()(见 update),因此触发方式、菜单内容等均可运行时热更新。

常见使用提示

  • 优先用getItems而非getContent:前者声明式、支持Promise、自动渲染为统一风格列表,适合绝大多数菜单场景;只有需要完全自定义 DOM(如带图标、分组、富文本的菜单)时才使用getContent
  • enable的三层能力:可传布尔值(全局开关)、按targetType区分元素类型、或按event.target.id精确到某个具体元素;
  • 注意区分targetTypetarget.typetargetType表示事件命中的元素类别(canvas/node/edge/combo),target.type表示元素的具体类型(如circlerect);
  • 菜单溢出画布:插件已内置边界约束,但若你的画布容器有特殊布局(如被裁剪),请合理设置offset或使用className定制定位;
  • 不要忘记graph.render():所有plugins配置在渲染后才完整生效(demo 与测试中均在render后交互)。

相关资源

  • 插件官方文档:Contextmenu.zh.md
  • 插件核心实现:contextmenu/index.ts
  • 菜单项类型与样式定义:contextmenu/util.ts
  • 插件容器创建工具:plugins/utils/dom.ts
  • 单元测试:contextmenu.spec.ts
  • 可运行示例:plugin-contextmenu.ts
  • 插件管理 API:runtime/plugin.ts、Graph 插件方法
  • 事件类型定义:types/event.ts
  • 数据可视化
  • 前端
  • 图表库

【免费下载链接】G6

♾ A Graph Visualization Framework in JavaScript.

项目地址:https://gitcode.com/gh_mirrors/g6/G6
点击查看免费下载

相关推荐

上一篇:【亲测免费】 Bootstrap Datepicker 安装和配置指南
下一篇:深入CircleImageView源码:BitmapShader实现原理

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

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

【Coze】【视频】踏马爽文工作流

今天给大家演示一个 踏马爽文视频 Coze 工作流。该工作流结合了大语言模型、批处理、语音合成和剪映小助手等功能节点,能够从输入的爽文主题出发,自动生成符合爽文文风的文案,再将文案转化为音频、字幕并组合到视频草稿中,最终实现一键生成爽文短视频的效果。通过这个工作流…

作者头像 李华
网站建设 2026/9/24 13:46:49

Flask 即插视图高级应用

在使用 Flask 框架构建 Web 应用时,即插视图(Pluggable Views)是一种结构化管理视图函数的重要方式。通过将视图逻辑封装进类中,不仅提升了代码的可读性和复用性,也更容易与大型项目架构兼容。尤其在构建 RESTful 接口、模块化开发等场景中,即插视图能极大简化开发流程,…

作者头像 李华
网站建设 2026/9/24 13:46:36

Flask 扩展 Moment 本地化日期和时间

Web 应用中时间显示是一个常见需求,而本地化展示时间更是提升用户体验的关键细节。不同地区的用户希望看到符合其文化习惯的时间格式,比如“2025年4月7日 上午10:30”这样的格式对中文用户更友好,而美国用户则更习惯“April 7, 2025, 10:30 AM”。 Flask-Moment 是 Flask 的…

作者头像 李华
网站建设 2026/9/24 13:46:31

Flask Cookies 本地数据

在Web开发中,Cookies 是实现用户会话管理、偏好设置保存以及简易身份识别的重要手段。Flask作为一个轻量级的Python Web框架,提供了简单直观的方式来处理Cookies。掌握Cookies的用法不仅有助于构建更智能的Web应用,也是在构建用户体验、处理用户状态以及提高系统安全性方面的…

作者头像 李华
网站建设 2026/9/24 13:46:31

Flask 扩展 SQLalchemy 操作数据库

Flask 本身是一个轻量级框架,默认并不内置 ORM 功能。为了提供数据库支持,可以通过扩展集成 SQLAlchemy。SQLAlchemy 是 Python 中功能强大的数据库工具,具备 ORM(对象关系映射)和 SQL 表达式语言两种能力,能够让代码更贴近对象操作的思维方式,同时也不失对底层 SQL 的控…

作者头像 李华