- 数据可视化
- 前端
- 图表库
【免费下载链接】G6
♾ A Graph Visualization Framework in JavaScript.
导读
本文基于 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 类 可以看到,插件在构造时即完成三件事:
initElement():通过 createPluginContainer 创建容器 DOM,追加className(默认g6-contextmenu),并注入内置样式CONTEXTMENU_CSS到document.head;- 绑定事件:分别监听
canvas、node、edge、combo四类目标的contextmenu(或click)事件(见bindEvents); - 调用
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 追加的类名,用于自定义样式 | string | g6-contextmenu | |
| trigger | 如何触发右键菜单:contextmenu表示右键触发,click表示点击触发 | click|contextmenu | contextmenu | |
| 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> | - | |
| loadingContent | 当getContent返回一个Promise时,使用的菜单内容(加载占位) | HTMLElement | string | Loading... | |
| enable | 是否可用,通过参数判断是否支持右键菜单,默认是全部可用 | boolean | (event: IElementEvent) => boolean | true |
源码中的默认值(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, };几点补充说明:
- 若未配置
getItems与getContent,getContent的默认实现会返回空菜单占位文案; 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未定义,实际应使用第一个参数value或onClick的参数名,如上所示。
边的右键菜单
通过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.target、event.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-canvas、drag-canvas、drag-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),其流程为:
- 先执行
enable判断:函数形式返回false或布尔值为false时直接隐藏并返回; - 等待
getDOMContent产出的内容(HTMLElement 或 HTML 字符串),写入菜单容器; - 依据事件
client坐标与画布容器getBoundingClientRect计算菜单位置,加上offset偏移量; - 对菜单位置做边界约束——
Math.max(padding, Math.min(left, containerWidth - menuWidth - padding)),确保菜单不会溢出画布容器(padding 为 4px); - 记录
targetElement供onClick使用。
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事件处理器wheelHandler(e.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精确到某个具体元素;- 注意区分
targetType与target.type:targetType表示事件命中的元素类别(canvas/node/edge/combo),target.type表示元素的具体类型(如circle、rect); - 菜单溢出画布:插件已内置边界约束,但若你的画布容器有特殊布局(如被裁剪),请合理设置
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.
相关推荐
G6 Contextmenu 上下文菜单插件实战指南:配置、事件机制与源码解析
G6 Contextmenu 上下文菜单插件实战指南:配置、事件机制与源码解析 导读:本文围绕 G6 官方插件 Contextmenu(上下文菜单,即右键菜单)
数据可视化前端图表库G6 Contextmenu 右键菜单插件:配置项详解与源码级实战指南
G6 Contextmenu 右键菜单插件:配置项详解与源码级实战指南 本文围绕 G6(AntV 图可视化框架)内置的 Contextmenu 插件展开,系统讲
数据可视化前端图表库CICFlowMeter终极指南:3步构建专业级网络流量分析工具
CICFlowMeter终极指南:3步构建专业级网络流量分析工具 你是否曾经需要分析网络流量数据却苦于缺乏专业工具?CICFlowMeter正是为解决这一痛点而
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考