diff --git a/docs/assets/guide/en/plugin/context-menu.md b/docs/assets/guide/en/plugin/context-menu.md index 28bb38173b..28b549192e 100644 --- a/docs/assets/guide/en/plugin/context-menu.md +++ b/docs/assets/guide/en/plugin/context-menu.md @@ -11,6 +11,7 @@ ContextMenu provides rich right-click menu features for VTable tables, supportin - Support custom menu items and separators - Support menu item grouping and submenus - Support custom menu item click callbacks +- Support listening to menu item click events with menu item and cell position details - Support menu item icons and shortcut hints - Support numeric input in menu items (e.g., insert multiple rows/columns) @@ -132,6 +133,48 @@ const contextMenuPlugin = new ContextMenuPlugin({ }); ``` +## Listen to Menu Item Click Events + +If you need to observe user actions after a right-click menu item is clicked, listen to the `context_menu_click` event fired by `ContextMenuPlugin`. This event is only fired after the right-click menu plugin is installed and enabled, and it is fired after the corresponding menu item handling logic finishes. Note that when `menuClickCallback` is configured as a function, the plugin runs that callback and skips the built-in menu handling. Built-in operations such as freeze, insert, and delete are only executed when `menuClickCallback` is not configured, or when it is configured as an object and the current `menuKey` does not match a custom callback. + +```typescript +import * as VTable from '@visactor/vtable'; +import { ContextMenuPlugin } from '@visactor/vtable-plugins'; + +const contextMenuPlugin = new ContextMenuPlugin(); + +const tableInstance = new VTable.ListTable({ + container: document.getElementById('container'), + columns, + records, + plugins: [contextMenuPlugin] +}); + +tableInstance.on(VTable.TABLE_EVENT_TYPE.CONTEXT_MENU_CLICK, args => { + const { col, row, contextMenu } = args; + + console.log('Context menu click position:', col, row); + console.log('Menu item:', contextMenu.menuKey, contextMenu.menuText); + + if (contextMenu.menuKey === 'freeze_to_this_row_and_column') { + console.log('Current frozen counts:', tableInstance.frozenRowCount, tableInstance.frozenColCount); + } +}); +``` + +Event argument description: + +| Parameter | Type | Description | +| --- | --- | --- | +| `col` | number | Column index of the menu trigger position; `-1` when there is no corresponding cell | +| `row` | number | Row index of the menu trigger position; `-1` when there is no corresponding cell | +| `contextMenu.menuKey` | string | Unique key of the clicked menu item | +| `contextMenu.menuText` | string | Display text of the clicked menu item | +| `contextMenu.rowIndex` | number \| undefined | Row index of the menu trigger position; may be `undefined` when no row context exists | +| `contextMenu.colIndex` | number \| undefined | Column index of the menu trigger position; may be `undefined` when no column context exists | +| `contextMenu.cellValue` | any | Cell value at the menu trigger position | +| `contextMenu.inputValue` | number \| string | Input value of an input menu item | + ## Complete Example Here is a complete example showing how to create a table with right-click menu functionality: @@ -244,6 +287,10 @@ const generateTestData = (count) => { // Create table instance const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); + + tableInstance.on(VTable.TABLE_EVENT_TYPE.CONTEXT_MENU_CLICK, args => { + console.log('Context menu item clicked:', args.contextMenu); + }); ``` diff --git a/docs/assets/guide/zh/plugin/context-menu.md b/docs/assets/guide/zh/plugin/context-menu.md index 63289ec6f9..34804adc1b 100644 --- a/docs/assets/guide/zh/plugin/context-menu.md +++ b/docs/assets/guide/zh/plugin/context-menu.md @@ -11,6 +11,7 @@ - 支持自定义菜单项和分隔线 - 支持菜单项分组和子菜单 - 支持自定义菜单项点击回调 +- 支持监听菜单项点击事件,获取菜单项和单元格位置信息 - 支持菜单项图标和快捷键提示 - 支持菜单项中的数字输入(如插入多行/列) @@ -132,6 +133,48 @@ const contextMenuPlugin = new ContextMenuPlugin({ }); ``` +## 监听菜单项点击事件 + +如果需要在右键菜单项点击后统一感知用户操作,可以监听 `ContextMenuPlugin` 触发的 `context_menu_click` 事件。该事件只有安装并启用右键菜单插件后才会触发,并且会在菜单项对应的处理逻辑完成后触发。需要注意的是,如果 `menuClickCallback` 配置为函数,插件会执行该回调并跳过内置菜单处理;如果未配置 `menuClickCallback`,或配置为对象但当前 `menuKey` 未命中自定义回调,才会执行内置的冻结、插入、删除等菜单操作。 + +```typescript +import * as VTable from '@visactor/vtable'; +import { ContextMenuPlugin } from '@visactor/vtable-plugins'; + +const contextMenuPlugin = new ContextMenuPlugin(); + +const tableInstance = new VTable.ListTable({ + container: document.getElementById('container'), + columns, + records, + plugins: [contextMenuPlugin] +}); + +tableInstance.on(VTable.TABLE_EVENT_TYPE.CONTEXT_MENU_CLICK, args => { + const { col, row, contextMenu } = args; + + console.log('右键菜单点击位置:', col, row); + console.log('菜单项信息:', contextMenu.menuKey, contextMenu.menuText); + + if (contextMenu.menuKey === 'freeze_to_this_row_and_column') { + console.log('当前冻结行列数:', tableInstance.frozenRowCount, tableInstance.frozenColCount); + } +}); +``` + +事件参数说明: + +| 参数 | 类型 | 描述 | +| --- | --- | --- | +| `col` | number | 菜单触发位置对应的列号;没有对应单元格时为 `-1` | +| `row` | number | 菜单触发位置对应的行号;没有对应单元格时为 `-1` | +| `contextMenu.menuKey` | string | 被点击菜单项的唯一标识 | +| `contextMenu.menuText` | string | 被点击菜单项的展示文本 | +| `contextMenu.rowIndex` | number \| undefined | 菜单触发位置对应的行号;没有对应行时可能为 `undefined` | +| `contextMenu.colIndex` | number \| undefined | 菜单触发位置对应的列号;没有对应列时可能为 `undefined` | +| `contextMenu.cellValue` | any | 菜单触发位置对应的单元格值 | +| `contextMenu.inputValue` | number \| string | 输入型菜单项中的输入值 | + ## 完整示例 以下是一个完整的示例,展示了如何创建具有右键菜单功能的表格: @@ -244,6 +287,10 @@ const generateTestData = (count) => { // 创建表格实例 const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); + + tableInstance.on(VTable.TABLE_EVENT_TYPE.CONTEXT_MENU_CLICK, args => { + console.log('右键菜单项点击:', args.contextMenu); + }); ``` diff --git a/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts index b9579f9562..1e4f639506 100644 --- a/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts +++ b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts @@ -134,6 +134,45 @@ describe('Context menu canvas option', () => { ); }); + test('ContextMenuPlugin emits context_menu_click after menu item click', () => { + const container = createDiv(); + const contextMenuPlugin = new ContextMenuPlugin(); + + table = new ListTable({ + container, + columns: [{ field: 'id', title: 'ID' }], + records: [{ id: 1 }], + plugins: [contextMenuPlugin] + }); + + const fireListeners = jest.spyOn(table, 'fireListeners'); + + contextMenuPlugin['handleMenuClickCallback']( + { + menuKey: 'freeze_to_this_row_and_column', + menuText: '冻结到本行本列', + rowIndex: 1, + colIndex: 0, + cellValue: 1 + }, + table + ); + + expect(table.frozenRowCount).toBe(2); + expect(table.frozenColCount).toBe(1); + expect(fireListeners).toHaveBeenCalledWith(ListTable.EVENT_TYPE.CONTEXT_MENU_CLICK, { + col: 0, + row: 1, + contextMenu: { + menuKey: 'freeze_to_this_row_and_column', + menuText: '冻结到本行本列', + rowIndex: 1, + colIndex: 0, + cellValue: 1 + } + }); + }); + test('ContextMenuPlugin init uses new options when added through updateOption', () => { const container = createDiv(); const contextMenuPlugin = new ContextMenuPlugin({ diff --git a/packages/vtable-plugins/demo/context-menu/issue-4655-context-menu-click.ts b/packages/vtable-plugins/demo/context-menu/issue-4655-context-menu-click.ts new file mode 100644 index 0000000000..cf5f5934ce --- /dev/null +++ b/packages/vtable-plugins/demo/context-menu/issue-4655-context-menu-click.ts @@ -0,0 +1,85 @@ +import * as VTable from '@visactor/vtable'; +import { ContextMenuPlugin } from '../../src/context-menu'; + +const CONTAINER_ID = 'vTable'; + +function createRecords() { + return Array.from({ length: 12 }, (_, index) => ({ + id: index + 1, + name: `name-${index + 1}`, + city: ['Beijing', 'Shanghai', 'Shenzhen'][index % 3], + value: Math.round((index + 1) * 12.6) + })); +} + +function updateStatus(table: VTable.ListTable, status: HTMLElement, eventArgs?: any) { + const lines = [ + `frozenRowCount: ${table.frozenRowCount}`, + `frozenColCount: ${table.frozenColCount}`, + eventArgs + ? `last context_menu_click: ${JSON.stringify(eventArgs.contextMenu)}` + : 'last context_menu_click: waiting for menu click' + ]; + status.textContent = lines.join('\n'); +} + +export function createTableInstance(status: HTMLElement) { + const plugin = new ContextMenuPlugin(); + const option: VTable.ListTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + records: createRecords(), + columns: [ + { field: 'id', title: 'ID', width: 80 }, + { field: 'name', title: 'Name', width: 160 }, + { field: 'city', title: 'City', width: 160 }, + { field: 'value', title: 'Value', width: 120 } + ], + defaultRowHeight: 40, + defaultHeaderRowHeight: 40, + widthMode: 'standard', + heightMode: 'standard', + plugins: [plugin] + }; + + const tableInstance = new VTable.ListTable(option); + tableInstance.on(VTable.TABLE_EVENT_TYPE.CONTEXT_MENU_CLICK, args => { + updateStatus(tableInstance, status, args); + // eslint-disable-next-line no-console + console.log('context_menu_click', args); + }); + + updateStatus(tableInstance, status); + window.tableInstance = tableInstance; + return tableInstance; +} + +export function createTable() { + const info = document.createElement('div'); + info.style.margin = '10px'; + info.style.padding = '10px'; + info.style.border = '1px solid #ddd'; + info.style.borderRadius = '4px'; + info.style.backgroundColor = '#f9f9f9'; + info.innerHTML = ` +

Issue 4655: context menu click event

+

Right-click a body cell, open the freeze submenu, then click freeze or unfreeze.

+

The table should emit context_menu_click with menu details in contextMenu.

+ `; + document.body.appendChild(info); + + const status = document.createElement('pre'); + status.style.margin = '10px'; + status.style.padding = '10px'; + status.style.border = '1px solid #ddd'; + status.style.borderRadius = '4px'; + status.style.backgroundColor = '#fff'; + document.body.appendChild(status); + + const container = document.createElement('div'); + container.id = CONTAINER_ID; + container.style.width = '100%'; + container.style.height = '460px'; + document.body.appendChild(container); + + return createTableInstance(status); +} diff --git a/packages/vtable-plugins/demo/menu.ts b/packages/vtable-plugins/demo/menu.ts index f110a5cb4b..da8063e917 100644 --- a/packages/vtable-plugins/demo/menu.ts +++ b/packages/vtable-plugins/demo/menu.ts @@ -156,6 +156,10 @@ export const menus = [ path: 'context-menu', name: 'context-menu' }, + { + path: 'context-menu', + name: 'issue-4655-context-menu-click' + }, { path: 'context-menu', name: 'issue-5215-context-menu-canvas' diff --git a/packages/vtable-plugins/src/context-menu.ts b/packages/vtable-plugins/src/context-menu.ts index 47f9cf06bc..1e8aa5d7c3 100644 --- a/packages/vtable-plugins/src/context-menu.ts +++ b/packages/vtable-plugins/src/context-menu.ts @@ -249,8 +249,22 @@ export class ContextMenuPlugin implements pluginsDefinition.IVTablePlugin { // 菜单项处理逻辑 this.handleMenuClick(args, table); } + this.fireContextMenuClick(args, table); }; + private fireContextMenuClick(args: MenuClickEventArgs, table: ListTable): void { + const { colIndex, rowIndex, ...contextMenu } = args; + table.fireListeners(TABLE_EVENT_TYPE.CONTEXT_MENU_CLICK, { + col: colIndex ?? -1, + row: rowIndex ?? -1, + contextMenu: { + ...contextMenu, + colIndex, + rowIndex + } + }); + } + private showContextMenu(menuItems: MenuItemOrSeparator[], x: number, y: number, col: number, row: number): void { // 显示菜单 this.menuManager.showMenu( diff --git a/packages/vtable/src/core/TABLE_EVENT_TYPE.ts b/packages/vtable/src/core/TABLE_EVENT_TYPE.ts index 7011c481b0..074c4972f2 100644 --- a/packages/vtable/src/core/TABLE_EVENT_TYPE.ts +++ b/packages/vtable/src/core/TABLE_EVENT_TYPE.ts @@ -68,6 +68,10 @@ export interface TableEvents { * 画布右键事件 */ CONTEXTMENU_CANVAS: 'contextmenu_canvas'; + /** + * 右键菜单项点击事件 + */ + CONTEXT_MENU_CLICK: 'context_menu_click'; /** * 列宽调整事件 */ @@ -287,6 +291,7 @@ export const TABLE_EVENT_TYPE: TableEvents = { MOUSELEAVE_CELL: 'mouseleave_cell', CONTEXTMENU_CELL: 'contextmenu_cell', CONTEXTMENU_CANVAS: 'contextmenu_canvas', + CONTEXT_MENU_CLICK: 'context_menu_click', RESIZE_COLUMN: 'resize_column', RESIZE_COLUMN_END: 'resize_column_end', RESIZE_ROW: 'resize_row', diff --git a/packages/vtable/src/ts-types/events.ts b/packages/vtable/src/ts-types/events.ts index bab6a41093..5ceee38e19 100644 --- a/packages/vtable/src/ts-types/events.ts +++ b/packages/vtable/src/ts-types/events.ts @@ -7,7 +7,7 @@ import type { ListTableConstructorOptions, PivotTableConstructorOptions } from './table-engine'; -import type { DropDownMenuEventArgs, MenuListItem, PivotInfo } from './menu'; +import type { ContextMenuClickEventArgs, DropDownMenuEventArgs, MenuListItem, PivotInfo } from './menu'; import type { IDimensionInfo, MergeCellInfo, RectProps, SortOrder } from './common'; import type { IconFuncTypeEnum, CellInfo, HierarchyState, ColumnsDefine } from '.'; @@ -84,6 +84,7 @@ export interface TableEventHandlersEventArgumentMap { mouseup_cell: MousePointerCellEvent; contextmenu_cell: MousePointerMultiCellEvent; contextmenu_canvas: MousePointerCellEvent; + context_menu_click: ContextMenuClickEventArgs; before_keydown: KeydownEvent; keydown: KeydownEvent; scroll: { @@ -395,6 +396,7 @@ export interface TableEventHandlersReturnMap { mouseup_cell: void; contextmenu_cell: void; contextmenu_canvas: void; + context_menu_click: void; before_keydown: void; keydown: void; scroll: void; diff --git a/packages/vtable/src/ts-types/menu.ts b/packages/vtable/src/ts-types/menu.ts index a19dcfc0ac..1751602e1e 100644 --- a/packages/vtable/src/ts-types/menu.ts +++ b/packages/vtable/src/ts-types/menu.ts @@ -75,6 +75,19 @@ export type DropDownMenuEventArgs = { highlight: boolean; } & DropDownMenuEventInfo; +export type ContextMenuClickEventArgs = { + col: number; + row: number; + contextMenu: { + menuKey: string; + menuText: string; + rowIndex?: number; + colIndex?: number; + cellValue?: any; + inputValue?: number | string; + }; +}; + export type DropDownMenuEventInfo = { field?: FieldDef; /**format之后的值 */