上下文菜单 Contextmenu
概述
上下文菜单(Contextmenu),也被称为右键菜单,是当用户在某个特定区域上点击后出现的一个菜单。支持在点击前后,触发自定义事件。通过上下文菜单,可以将一些具体元素的操作集成在其中,方便对某一项来进行需要时的单独控制。
使用场景
这一插件主要用于:
- 元素的各种交互:查看节点、查看边、删除节点等等交互操作
基本用法
以下是一个简单的 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('展示节点详情');
},
},
],
});
配置项
| 属性 | 描述 | 类型 | 默认值 | 必选 |
|---|---|---|---|---|
| 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 | - | |
| enable | 是否可用,通过参数判断是否支持右键菜单,默认是全部可用 | boolean | (event: IElementEvent) => boolean | true |
Item
每个菜单项目 (Item) 包含以下属性:
| 属性 | 描述 | 类型 | 必选 |
|---|---|---|---|
| name | 菜单项显示的名字 | string |
✓ |
| value | 菜单项对应的值 | string |
✓ |
onClick
点击菜单项后会触发该函数,函数有三个参数:
- value: 对应菜单项的 value
- target: 对应菜单项容器的 dom 节点
- current: 对应触发菜单项的元素,例如是节点则可以通过
current来获取到节点的信息(id)等,或者来对元素进行修改
代码示例
基础右键菜单
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「' + v + '」item');
},
getItems: () => {
return [
{ name: '查看详情', value: 'detail' },
{ name: '删除', value: 'delete' },
];
},
},
],
});
边的右键菜单
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',
},
],
});
异步加载菜单项
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',
},
],
});
动态控制菜单项
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 [];
},
},
],
});
实际案例
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();
import { Graph } from '@antv/g6';
import GUI from 'lil-gui';
export function addPanel(renderPanel: (gui: GUI) => void) {
const gui = new GUI({ container: document.body });
gui.title('Control');
Object.assign(gui.domElement.style, { position: 'absolute', top: '0', right: '0', zIndex: '10' });
renderPanel(gui);
addEventListener('pagehide', () => gui.destroy(), { once: true });
}
export async function createGraph(
options: ConstructorParameters<typeof Graph>[0],
size: { width?: number; height?: number } = {},
renderPanel?: (gui: GUI, graph: Graph) => void,
) {
const container = document.createElement('div');
Object.assign(container.style, {
width: '100%',
maxWidth: `${size.width || 600}px`,
height: `${size.height || 400}px`,
});
document.getElementById('container')!.append(container);
const graph = new Graph({ ...size, ...options, width: container.clientWidth, container, autoResize: true });
addEventListener('pagehide', () => graph.destroy(), { once: true });
await graph.render();
if (renderPanel) addPanel((gui) => renderPanel(gui, graph));
return container;
}