Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "fix: support context menu plugin on canvas blank area",
"type": "patch",
"packageName": "@visactor/vtable-plugins"
}
],
"packageName": "@visactor/vtable-plugins",
"email": "github@visactor.io"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "fix: allow plugins to initialize before first render",
"type": "patch",
"packageName": "@visactor/vtable"
}
],
"packageName": "@visactor/vtable",
"email": "github@visactor.io"
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @ts-nocheck
import { ListTable } from '@visactor/vtable';
import { createDiv } from '../../../vtable/__tests__/dom';
import { ContextMenuPlugin } from '../../src/context-menu';
import { MenuHandler } from '../../src/contextmenu/handle-menu-helper';
import { TableSeriesNumber } from '../../src/table-series-number';

Expand Down Expand Up @@ -63,3 +64,130 @@ describe('Context menu row deletion', () => {
expect(table.records.map(record => record.id)).toEqual([0, 4]);
});
});

describe('Context menu canvas option', () => {
let table: ListTable;

afterEach(() => {
table?.release();
document.body.innerHTML = '';
});

test('ContextMenuPlugin enables canvas context menu through contextMenuWorkOnlyCell', () => {
const container = createDiv();
const contextMenuPlugin = new ContextMenuPlugin({
contextMenuWorkOnlyCell: false
});

table = new ListTable({
container,
columns: [{ field: 'id', title: 'ID' }],
records: [{ id: 1 }],
plugins: [contextMenuPlugin]
});

expect(table.options.menu.contextMenuWorkOnlyCell).toBe(false);
expect(contextMenuPlugin.runTime).toContain(ListTable.EVENT_TYPE.CONTEXTMENU_CANVAS);
});

test('ContextMenuPlugin shows canvasMenuItems on CONTEXTMENU_CANVAS event', () => {
const container = createDiv();
const contextMenuPlugin = new ContextMenuPlugin({
contextMenuWorkOnlyCell: false,
bodyCellMenuItems: [{ text: 'Body Item', menuKey: 'body_item' }],
canvasMenuItems: [{ text: 'Canvas Item', menuKey: 'canvas_item' }]
});
const showMenu = jest.spyOn(contextMenuPlugin['menuManager'], 'showMenu');
const preventDefault = jest.fn();

table = new ListTable({
container,
columns: [{ field: 'id', title: 'ID' }],
records: [{ id: 1 }],
plugins: [contextMenuPlugin]
});

contextMenuPlugin.run(
{
col: -1,
row: -1,
event: {
clientX: 10,
clientY: 20,
preventDefault
}
},
ListTable.EVENT_TYPE.CONTEXTMENU_CANVAS,
table
);

expect(preventDefault).toHaveBeenCalled();
expect(showMenu).toHaveBeenCalledWith(
[{ text: 'Canvas Item', menuKey: 'canvas_item' }],
10,
20,
{
rowIndex: -1,
colIndex: -1
},
table
);
});

test('ContextMenuPlugin init uses new options when added through updateOption', () => {
const container = createDiv();
const contextMenuPlugin = new ContextMenuPlugin({
contextMenuWorkOnlyCell: false
});

table = new ListTable({
container,
columns: [{ field: 'id', title: 'ID' }],
records: [{ id: 1 }]
});

table.updateOption({
container,
columns: [{ field: 'id', title: 'ID' }],
records: [{ id: 1 }],
plugins: [contextMenuPlugin]
});

expect(table.options.menu.contextMenuWorkOnlyCell).toBe(false);
});

test('PluginManager only releases plugins removed through updateOption', () => {
const container = createDiv();
const removedPlugin = {
id: 'removed-plugin',
name: 'removed-plugin',
runTime: [],
run: jest.fn(),
release: jest.fn()
};
const keptPlugin = {
id: 'kept-plugin',
name: 'kept-plugin',
runTime: [],
run: jest.fn(),
release: jest.fn()
};

table = new ListTable({
container,
columns: [{ field: 'id', title: 'ID' }],
records: [{ id: 1 }],
plugins: [removedPlugin, keptPlugin]
});

table.updateOption({
container,
columns: [{ field: 'id', title: 'ID' }],
records: [{ id: 1 }],
plugins: [keptPlugin]
});

expect(removedPlugin.release).toHaveBeenCalledTimes(1);
expect(keptPlugin.release).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as VTable from '@visactor/vtable';
import { ContextMenuPlugin } from '../../src/context-menu';

const CONTAINER_ID = 'vTable';

export function createTableInstance() {
const records = [
{ id: 1, name: 'alpha', value: 12 },
{ id: 2, name: 'beta', value: 34 }
];

const plugin = new ContextMenuPlugin({
contextMenuWorkOnlyCell: false,
bodyCellMenuItems: [
{ text: 'Copy from plugin', menuKey: 'copy' },
'---',
{ text: 'Body cell item', menuKey: 'body_cell_item' }
],
canvasMenuItems: [
{ text: 'Canvas blank area', menuKey: 'canvas_blank_area' },
{ text: 'Canvas menu item', menuKey: 'canvas_menu_item' }
],
beforeShowAdjustMenuItems: (menuItems, _table, col, row) => {
if (col === -1 && row === -1) {
return [
...menuItems,
{
text: `Blank canvas: col ${col}, row ${row}`,
menuKey: 'canvas_position'
}
];
}
return menuItems.filter(item => item !== '---');
}
});

const option: VTable.ListTableConstructorOptions = {
container: document.getElementById(CONTAINER_ID),
records,
columns: [
{ field: 'id', title: 'ID', width: 100 },
{ field: 'name', title: 'Name', width: 160 },
{ field: 'value', title: 'Value', width: 120 }
],
defaultRowHeight: 40,
defaultHeaderRowHeight: 40,
widthMode: 'standard',
heightMode: 'standard',
plugins: [plugin]
};

const tableInstance = new VTable.ListTable(option);
window.tableInstance = tableInstance;
return tableInstance;
}

export function createTable() {
const container = document.createElement('div');
container.id = CONTAINER_ID;
container.style.width = '100%';
container.style.height = '420px';
document.body.appendChild(container);

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 = `
<h3>Issue 5215: Canvas context menu</h3>
<p>Right-click the blank canvas area outside the table cells. The context menu should show canvas-specific items.</p>
<p>Right-click body cells to verify cell menu items are still independent from blank canvas menu items.</p>
`;

document.body.insertBefore(info, container);
return createTableInstance();
}
4 changes: 4 additions & 0 deletions packages/vtable-plugins/demo/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ export const menus = [
path: 'context-menu',
name: 'context-menu'
},
{
path: 'context-menu',
name: 'issue-5215-context-menu-canvas'
},
{
path: 'context-menu',
name: 'issue-5214-reverse-selected-row-delete'
Expand Down
33 changes: 32 additions & 1 deletion packages/vtable-plugins/src/context-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export interface ContextMenuOptions {
headerCellMenuItems?: MenuItemOrSeparator[];
/** 表体菜单项 */
bodyCellMenuItems?: MenuItemOrSeparator[];
/** 空白画布区域菜单项。仅 contextMenuWorkOnlyCell 为 false 时生效。 */
canvasMenuItems?: MenuItemOrSeparator[];
/** 右键菜单是否只工作在单元格上。默认 true;配置 false 时空白画布区域也弹出菜单。 */
contextMenuWorkOnlyCell?: boolean;
/** 自定义菜单样式 */
customMenuAttributions?: MenuAttributions;
/** 菜单点击回调。如果设置是函数,则忽略内部默认的菜单项处理逻辑。如果这里配置的是个对象(对象的key为menuKey),则有匹配的menuKey时忽略内部默认的菜单项处理逻辑,
Expand Down Expand Up @@ -56,7 +60,7 @@ export type MenuClickCallback = (args: MenuClickEventArgs, table: ListTable) =>
export class ContextMenuPlugin implements pluginsDefinition.IVTablePlugin {
id = `context-menu`;
name = 'Context Menu';
runTime = [TABLE_EVENT_TYPE.CONTEXTMENU_CELL, TABLE_EVENT_TYPE.PLUGIN_EVENT];
runTime = [TABLE_EVENT_TYPE.CONTEXTMENU_CELL, TABLE_EVENT_TYPE.CONTEXTMENU_CANVAS, TABLE_EVENT_TYPE.PLUGIN_EVENT];
pluginOptions: ContextMenuOptions;
table: ListTable;
/** 菜单管理器 */
Expand Down Expand Up @@ -156,6 +160,22 @@ export class ContextMenuPlugin implements pluginsDefinition.IVTablePlugin {
}
};

/**
* 处理空白画布右键菜单事件
*/
private handleContextMenuCanvas = (eventArgs: any, table: BaseTableAPI): void => {
let menuItems = this.pluginOptions.canvasMenuItems || [];
const { col = -1, row = -1 } = eventArgs;

if (this.pluginOptions.beforeShowAdjustMenuItems) {
menuItems = this.pluginOptions.beforeShowAdjustMenuItems(menuItems, table as ListTable, col, row);
}

if (menuItems.length > 0) {
this.showContextMenu(menuItems, eventArgs.event.clientX, eventArgs.event.clientY, col, row);
}
};

/**
* 处理插件事件
*/
Expand Down Expand Up @@ -191,6 +211,15 @@ export class ContextMenuPlugin implements pluginsDefinition.IVTablePlugin {
/**
* 运行插件
*/
init(_table: BaseTableAPI, options: BaseTableAPI['options']) {
if (this.pluginOptions.contextMenuWorkOnlyCell === false) {
options.menu = {
...options.menu,
contextMenuWorkOnlyCell: false
};
}
}

run(...args: any[]) {
const eventArgs = args[0];
const runTime = args[1];
Expand All @@ -203,6 +232,8 @@ export class ContextMenuPlugin implements pluginsDefinition.IVTablePlugin {
// 根据事件类型处理不同的右键菜单
if (runTime === TABLE_EVENT_TYPE.CONTEXTMENU_CELL) {
this.handleContextMenuCell(eventArgs, table);
} else if (runTime === TABLE_EVENT_TYPE.CONTEXTMENU_CANVAS) {
this.handleContextMenuCanvas(eventArgs, table);
} else if (runTime === TABLE_EVENT_TYPE.PLUGIN_EVENT) {
this.handlePluginEvent(eventArgs, table);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vtable/src/ListTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ export class ListTable extends BaseTable implements ListTableAPI {
) {
const internalProps = this.internalProps;

this.pluginManager.removeOrAddPlugins(options.plugins);
this.pluginManager.removeOrAddPlugins(options.plugins, options);
super.updateOption(options, updateConfig);
internalProps.frozenColDragHeaderMode =
options.dragOrder?.frozenColDragHeaderMode ?? options.frozenColDragHeaderMode;
Expand Down
6 changes: 4 additions & 2 deletions packages/vtable/src/plugins/interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { TableEvents } from '../core/TABLE_EVENT_TYPE';
import type { BaseTableAPI } from '../ts-types/base-table';
import type { BaseTableAPI, BaseTableConstructorOptions } from '../ts-types/base-table';

// 插件生命周期接口
export interface IVTablePlugin {
Expand All @@ -16,7 +16,9 @@ export interface IVTablePlugin {
runTime: TableEvents[keyof TableEvents][];
// // 插件依赖
// dependencies?: string[];
// 初始化方法,在VTable实例创建后、首次渲染前调用
// 初始化方法,在 VTable 实例创建后、首次渲染前调用
init?: (table: BaseTableAPI, options: BaseTableConstructorOptions) => void;
// 运行方法,在订阅的表格事件触发时调用
run: (...args: any[]) => void;
// 更新方法,当表格数据或配置更新时调用
update?: () => void;
Expand Down
9 changes: 6 additions & 3 deletions packages/vtable/src/plugins/plugin-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ export class PluginManager {

constructor(table: BaseTableAPI, options: BaseTableConstructorOptions) {
this.table = table;
options.plugins?.map(plugin => {
options.plugins?.forEach(plugin => {
this.register(plugin);
plugin.init?.(this.table, options);
this._bindTableEventForPlugin(plugin);
});
}
Expand Down Expand Up @@ -44,21 +45,23 @@ export class PluginManager {
}

// 移除或添加插件
removeOrAddPlugins(plugins?: IVTablePlugin[]): void {
removeOrAddPlugins(plugins?: IVTablePlugin[], options: BaseTableConstructorOptions = this.table.options): void {
// 先找到plugins中没有,但this.plugins中有,也就是已经被移除的插件
const removedPlugins = Array.from(this.plugins.values()).filter(plugin => !plugins?.some(p => p.id === plugin.id));
removedPlugins.forEach(plugin => {
this.pluginEventMap.get(plugin.id)?.forEach(id => {
this.table.off(id);
});
this.release();
this.pluginEventMap.delete(plugin.id);
plugin.release?.(this.table);
this.plugins.delete(plugin.id);
});

// 添加新插件
const addedPlugins = plugins?.filter(plugin => !this.plugins.has(plugin.id));
addedPlugins?.forEach(plugin => {
this.register(plugin);
plugin.init?.(this.table, options);
this._bindTableEventForPlugin(plugin);
});
}
Expand Down
Loading