自定义 UI
新增菜单项
在 Univer 中,无论是顶部的工具栏菜单还是右键菜单,都可以通过编写插件来实现扩展。下面将介绍如何使用依赖注入系统中的 IMenuManagerService
来注册菜单项。
1. 插件环境
确保你对插件机制有所了解。
首先构造一个 controller
类,用于注册菜单项命令、菜单项图标、菜单项配置。
// src/plugin/controllers/custom-menu.controller.ts
import { Disposable, ICommandService, Inject, Injector } from '@univerjs/core';
import { IMenuManagerService, ComponentManager } from '@univerjs/ui';
export class CustomMenuController extends Disposable {
constructor(
@Inject(Injector) private readonly _injector: Injector,
@ICommandService private readonly _commandService: ICommandService,
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService,
@Inject(ComponentManager) private readonly _componentManager: ComponentManager,
) {
super();
this._initCommands();
this._registerComponents();
this._initMenus();
}
/**
* register commands
*/
private _initCommands(): void { }
/**
* register icon components
*/
private _registerComponents(): void { }
/**
* register menu items
*/
private _initMenus(): void { }
}
将这个 controller
注册到插件中
// src/plugin/plugin.ts
import type { Dependency } from '@univerjs/core';
import { Inject, Injector, Plugin, touchDependencies, UniverInstanceType } from '@univerjs/core';
import { CustomMenuController } from './controllers/custom-menu.controller';
const SHEET_CUSTOM_MENU_PLUGIN = 'SHEET_CUSTOM_MENU_PLUGIN';
export class UniverSheetsCustomMenuPlugin extends Plugin {
static override type = UniverInstanceType.UNIVER_SHEET;
static override pluginName = SHEET_CUSTOM_MENU_PLUGIN;
constructor(
@Inject(Injector) protected readonly _injector: Injector
) {
super();
}
override onStarting(): void {
([
[CustomMenuController],
] as Dependency[]).forEach(d => this._injector.add(d))
}
override onRendered(): void {
touchDependencies(this._injector, [
[CustomMenuController],
])
}
}
2. 菜单项命令
注册菜单之前,需要构造一个 Command
,这个 Command
会在菜单被点击时执行。
// src/plugin/commands/operations/single-button.operation.ts
import type { IAccessor, ICommand } from '@univerjs/core';
import { CommandType } from '@univerjs/core';
export const SingleButtonOperation: ICommand = {
id: 'custom-menu.operation.single-button',
type: CommandType.OPERATION,
handler: async (accessor: IAccessor) => {
alert('Single button operation');
return true;
},
};
将这个 Command
注册到 ICommandService
// src/plugin/controllers/custom-menu.controller.ts
import { SingleButtonOperation } from '../commands/operations/single-button.operation';
private _initCommands(): void {
[
SingleButtonOperation
].forEach((c) => {
this.disposeWithMe(this._commandService.registerCommand(c));
});
}
3. 菜单项图标
如果你的菜单项需要图标,也需要提前注册图标。
先构造一个图标 tsx 组件
// src/plugin/components/button-icon/ButtonIcon.tsx
export function ButtonIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m.16 14a6.981 6.981 0 0 0-5.147 2.256A7.966 7.966 0 0 0 12 20a7.97 7.97 0 0 0 5.167-1.892A6.979 6.979 0 0 0 12.16 16M12 4a8 8 0 0 0-6.384 12.821A8.975 8.975 0 0 1 12.16 14a8.972 8.972 0 0 1 6.362 2.634A8 8 0 0 0 12 4m0 1a4 4 0 1 1 0 8a4 4 0 0 1 0-8m0 2a2 2 0 1 0 0 4a2 2 0 0 0 0-4" />
</svg>
);
};
将这个图标注册到 ComponentManager
// src/plugin/controllers/custom-menu.controller.ts
import { ButtonIcon } from '../components/button-icon/ButtonIcon';
private _registerComponents(): void {
this.disposeWithMe(this._componentManager.register("ButtonIcon", ButtonIcon));
}
4. 菜单项国际化
如果你的菜单项需要国际化,需要提前添加好国际化资源。
// src/plugin/locale/zh-CN.ts
export default {
customMenu: {
button: '按钮',
singleButton: '单个按钮',
},
};
// src/plugin/locale/en-US.ts
export default {
customMenu: {
button: 'Button',
singleButton: 'Single button',
},
};
将这个国际化资源注册到 ILocaleService
// src/plugin/plugin.ts
+ import { LocaleService } from '@univerjs/core';
import enUS from './locale/en-US';
import zhCN from './locale/zh-CN';
export class UniverSheetsCustomMenuPlugin extends Plugin {
static override type = UniverInstanceType.UNIVER_SHEET;
static override pluginName = SHEET_CUSTOM_MENU_PLUGIN;
constructor(
@Inject(Injector) protected readonly _injector: Injector,
@Inject(LocaleService) private readonly _localeService: LocaleService
) {
super();
this._localeService.load({
enUS,
zhCN,
});
}
}
5. 菜单项配置
定义一个菜单项配置工厂函数,返回一个菜单项配置对象。
// src/plugin/controllers/menu/single-button.menu.ts
import type { IMenuButtonItem } from '@univerjs/ui';
import { MenuItemType } from '@univerjs/ui';
import { SingleButtonOperation } from '../../commands/operations/single-button.operation';
export function CustomMenuItemSingleButtonFactory(): IMenuButtonItem<string> {
return {
// 绑定 Command id,单击该按钮将触发该命令
id: SingleButtonOperation.id,
// 菜单项的类型,在本例中,它是一个按钮
type: MenuItemType.BUTTON,
// 按钮的图标,需要在 ComponentManager 中注册
icon: 'ButtonIcon',
// 按钮的提示,优先匹配国际化,如果没有匹配到,将显示原始字符串
tooltip: 'customMenu.singleButton',
// 按钮的标题,优先匹配国际化,如果没有匹配到,将显示原始字符串
title: 'customMenu.button',
};
}
将这些菜单项构造成成 Schema 并通过 IMenuManagerService
合并到菜单中。
// src/plugin/controllers/custom-menu.controller.ts
+ import { ContextMenuGroup, ContextMenuPosition, RibbonStartGroup } from '@univerjs/ui';
import { CustomMenuItemSingleButtonFactory } from './menu/single-button.menu';
import { SingleButtonOperation } from '../commands/operations/single-button.operation';
private _initMenus(): void {
this._menuManagerService.mergeMenu({
[RibbonStartGroup.OTHERS]: {
[SingleButtonOperation.id]: {
order: 10,
menuItemFactory: CustomMenuItemSingleButtonFactory,
},
},
[ContextMenuPosition.MAIN_AREA]: {
[ContextMenuGroup.OTHERS]: {
[SingleButtonOperation.id]: {
order: 12,
menuItemFactory: CustomMenuItemSingleButtonFactory,
},
},
},
})
}
6. 下拉列表
除了添加单个按钮的菜单项,还可以添加一个下拉菜单项,具体实现方式类似,只在构造菜单 Schema 时有所区别:
- 将菜单项配置返回类型
IMenuButtonItem<string>
替换为IMenuSelectorItem<string>
- 将菜单项类型
MenuItemType.BUTTON
替换为MenuItemType.SUBITEMS
- 下拉列表主按钮需要自定义一个 id,作为下拉列表的唯一标识,用于关联下拉列表的子菜单项
// src/plugin/controllers/menu/dropdown-list.menu.ts
import type { IMenuButtonItem, IMenuSelectorItem } from '@univerjs/ui';
import { MenuItemType } from '@univerjs/ui';
import { DropdownListFirstItemOperation, DropdownListSecondItemOperation } from '../../commands/operations/dropdown-list.operation';
export const CUSTOM_MENU_DROPDOWN_LIST_OPERATION_ID = 'custom-menu.operation.dropdown-list';
export function CustomMenuItemDropdownListMainButtonFactory(): IMenuSelectorItem<string> {
return {
// 当 type 为 MenuItemType.SUBITEMS 时,工厂函数作为下拉列表的容器,可以设置任意唯一的id
id: CUSTOM_MENU_DROPDOWN_LIST_OPERATION_ID,
// 菜单项的类型
type: MenuItemType.SUBITEMS,
icon: 'MainButtonIcon',
tooltip: 'customMenu.dropdownList',
title: 'customMenu.dropdown',
}
}
export function CustomMenuItemDropdownListFirstItemFactory(): IMenuButtonItem<string> {
return {
id: DropdownListFirstItemOperation.id,
type: MenuItemType.BUTTON,
title: 'customMenu.itemOne',
icon: 'ItemIcon',
}
}
export function CustomMenuItemDropdownListSecondItemFactory(): IMenuButtonItem<string> {
return {
id: DropdownListSecondItemOperation.id,
type: MenuItemType.BUTTON,
title: 'customMenu.itemTwo',
icon: 'ItemIcon',
}
}
构造菜单 Schema:
// src/plugin/controllers/custom-menu.controller.ts
import { DropdownListFirstItemOperation, DropdownListSecondItemOperation } from '../commands/operations/dropdown-list.operation';
import { CUSTOM_MENU_DROPDOWN_LIST_OPERATION_ID, CustomMenuItemDropdownListFirstItemFactory, CustomMenuItemDropdownListMainButtonFactory, CustomMenuItemDropdownListSecondItemFactory } from './menu/dropdown-list.menu';
private _initMenus(): void {
this._menuManagerService.mergeMenu({
[RibbonStartGroup.OTHERS]: {
[CUSTOM_MENU_DROPDOWN_LIST_OPERATION_ID]: {
order: 11,
menuItemFactory: CustomMenuItemDropdownListMainButtonFactory,
[DropdownListFirstItemOperation.id]: {
order: 0,
menuItemFactory: CustomMenuItemDropdownListFirstItemFactory,
},
[DropdownListSecondItemOperation.id]: {
order: 1,
menuItemFactory: CustomMenuItemDropdownListSecondItemFactory,
},
},
},
[ContextMenuPosition.MAIN_AREA]: {
[ContextMenuGroup.OTHERS]: {
[CUSTOM_MENU_DROPDOWN_LIST_OPERATION_ID]: {
order: 9,
menuItemFactory: CustomMenuItemDropdownListMainButtonFactory,
[DropdownListFirstItemOperation.id]: {
order: 0,
menuItemFactory: CustomMenuItemDropdownListFirstItemFactory,
},
[DropdownListSecondItemOperation.id]: {
order: 1,
menuItemFactory: CustomMenuItemDropdownListSecondItemFactory,
},
},
},
},
})
}
导出这个插件,并在 Univer 实例中注册。
// src/plugin/index.ts
export { UniverSheetsCustomMenuPlugin } from './plugin'
// src/index.ts
import { UniverSheetsCustomMenuPlugin } from './plugin';
univer.registerPlugin(UniverSheetsCustomMenuPlugin);
现在你已经成功添加了一个新的菜单项,可以在 Univer 菜单栏和右键菜单中看到它。
Demo
部分源码可以参考 这里
覆盖内置业务组件
Univer 中部分业务组件是可以被覆盖的,例如右侧的菜单面板、弹窗、图标等。这些组件在 Univer 的内部经由 ComponentManager
进行注册,你可以通过注册同名的组件来覆盖默认的组件。
export class CustomMenuController extends Disposable {
constructor(
+ @Inject(ComponentManager) private readonly _componentManager: ComponentManager,
) {
super();
// 替换默认的 BoldSingle 图标
+ this._componentManager.register('BoldSingle', YourBoldIconComponent);
}
}
定制菜单项(隐藏菜单项)
在 Univer 中定制或隐藏菜单项是一种常见需求,我们为所有包含了菜单项的插件提供了配置项。只要知道菜单项对应的命令 ID,你就可以非常轻松地通过配置项来实现这一需求。
例如,隐藏加粗的菜单项:
import { UniverSheetsUIPlugin, SetRangeBoldCommand } from '@univerjs/sheets-ui';
univer.registerPlugin(UniverSheetsUIPlugin, {
menu: {
[SetRangeBoldCommand.id]: {
hidden: true,
},
},
});
关于如何获取命令 ID,可以参考教程如何查找命令 ID。