Adding Custom Menu Item

GitHubEdit on GitHub

In Univer, both the top toolbar menu (Ribbon) and the context menu can be extended by writing plugins. This section will introduce how to register menu items using the IMenuManagerService in the dependency injection system.

1. Plugin Environment

Make sure you have a basic understanding of the plugin mechanism.

First, construct a controller class to register menu item commands, menu item icons, and menu item configurations.

src/plugin/controllers/custom-menu.controller.ts
import { Disposable, ICommandService, Inject, Injector } from '@univerjs/core'
import { ComponentManager, IMenuManagerService } 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 { }
}

Register this controller in the plugin.

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. Menu Item Commands

Before registering the menu, you need to construct a Command, which will be executed when the menu is clicked.

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) => {
    console.log('Single button operation')
    return true
  },
}

Register this Command with ICommandService.

src/plugin/controllers/custom-menu.controller.ts
import { SingleButtonOperation } from '../commands/operations/single-button.operation'

export class CustomMenuController extends Plugin {
  private _initCommands(): void {
    [
      SingleButtonOperation,
    ].forEach((c) => {
      this.disposeWithMe(this._commandService.registerCommand(c))
    })
  }
}

3. Menu Item Icons

If your menu item needs an icon, you also need to register the icon in advance.

First, construct an icon tsx component.

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>
  )
};

Register this icon with ComponentManager.

src/plugin/controllers/custom-menu.controller.ts
import { ButtonIcon } from '../components/button-icon/ButtonIcon'

export class CustomMenuController extends Plugin {
  private _registerComponents(): void {
    this.disposeWithMe(this._componentManager.register('ButtonIcon', ButtonIcon))
  }
}

4. Menu Item Internationalization

If your menu item needs internationalization, you need to add internationalization resources in advance.

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',
  },
}

Register this internationalized resource to 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. Menu Item Configuration

Define a menu item configuration factory function that returns a menu item configuration object.

src/plugin/controllers/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 {
    // Bind the command id, clicking the button will trigger this command
    id: SingleButtonOperation.id,
    // The type of the menu item, in this case, it is a button
    type: MenuItemType.BUTTON,
    // The icon of the button, which needs to be registered in ComponentManager
    icon: 'ButtonIcon',
    // The tooltip of the button. Prioritize matching internationalization. If no match is found, the original string will be displayed
    tooltip: 'customMenu.singleButton',
    // The title of the button. Prioritize matching internationalization. If no match is found, the original string will be displayed
    title: 'customMenu.button',
  }
}

Construct these menu items into a Schema and merge them into the menu through IMenuManagerService.

src/plugin/controllers/custom-menu.controller.ts
import { ContextMenuGroup, ContextMenuPosition, RibbonStartGroup } from '@univerjs/ui'
import { SingleButtonOperation } from '../commands/operations/single-button.operation'
import { CustomMenuItemSingleButtonFactory } from './menu'

export class CustomMenuController extends Disposable {
  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. Dropdown List

In addition to adding a single button menu item, you can also add a dropdown menu item. The specific implementation is similar, except for the difference in constructing the menu item configuration:

  • Replace menu item configuration return type IMenuButtonItem<string> with IMenuSelectorItem<string>
  • Replace menu item type MenuItemType.BUTTON with MenuItemType.SUBITEMS
  • The main button of the dropdown list needs to customize an id, which is used as the unique identifier of the dropdown list and is used to associate the sub-menu items of the dropdown list.
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'

const CUSTOM_MENU_DROPDOWN_LIST_OPERATION_ID = 'custom-menu.operation.dropdown-list'

export function CustomMenuItemDropdownListMainButtonFactory(): IMenuSelectorItem<string> {
  return {
    // When type is MenuItemType.SUBITEMS, this factory serves as a container for the drop-down list, and you can set any unique id
    id: CUSTOM_MENU_DROPDOWN_LIST_OPERATION_ID,
    // The type of the menu item, in this case, it is a subitems
    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',
  }
}

Then construct these menu items into a Schema and merge them into the menu through IMenuManagerService.

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'

export class CustomMenuController extends Disposable {
  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,
            },
          },
        },
      },
    })
  }
}

Export the plugin and register it with Univer instance.

src/plugin/index.ts
export { UniverSheetsCustomMenuPlugin } from './plugin'
src/index.ts
import { UniverSheetsCustomMenuPlugin } from './plugin'

univer.registerPlugin(UniverSheetsCustomMenuPlugin)

Now you have successfully added a custom menu item to Univer. You can see the menu item in the top toolbar and context menu.

You can find the menu item ID through the data-u-command attribute on the menu item DOM element.

Toolbar Menu Items

Menu Item IDMenu Item Name
univer.command.undoUndo
univer.command.redoRedo
sheet.command.set-once-format-painterPaint format
sheet.command.set-range-boldBold
sheet.command.set-range-italicItalic
sheet.command.set-range-underlineUnderline
sheet.command.set-range-strokeStrikethrough
sheet.command.set-range-font-familyFont
sheet.command.set-range-fontsizeFont size
sheet.command.set-range-text-colorText color
sheet.command.set-background-colorFill color
sheet.command.set-border-basicBorder
sheet.command.set-horizontal-text-alignHorizontal align
sheet.command.set-vertical-text-alignVertical align
sheet.command.set-text-wrapText wrap
sheet.command.set-text-rotationText rotate
sheet.command.add-worksheet-mergeMerge cells
sheet.command.add-worksheet-merge-allMerge cells - Merge all
sheet.command.add-worksheet-merge-verticalMerge cells - Vertical merge
sheet.command.add-worksheet-merge-horizontalMerge cells - Horizontal merge
sheet.command.remove-worksheet-mergeMerge cells - Cancel merge
sheet.operation.open.conditional.formatting.panelConditional Formatting
formula-ui.operation.insert-functionFunctions
formula-ui.operation.more-functionsFunctions - More Functions
sheet.menu.sheets-sortSort
sheet.command.sort-range-ascSort - Ascending
sheet.command.sort-range-asc-extSort - Expand Ascending
sheet.command.sort-range-descSort - Descending
sheet.command.sort-range-desc-extSort - Expand Descending
sheet.command.sort-range-customSort - Custom Sort
sheet.menu.imageImage
sheet.command.insert-float-imageImage - Float Image
sheet.command.insert-cell-imageImage - Cell Image
sheet.command.numfmt.set.currencyCurrency
sheet.command.numfmt.add.decimal.commandIncrease decimal places
sheet.command.numfmt.subtract.decimal.commandDecreasing decimal places
sheet.command.numfmt.set.percentPercentage
sheet.operation.open.numfmt.panelNumber format
sheet.menu.data-validationData validation
data-validation.operation.open-validation-panelData validation - Data validation management
data-validation.command.addRuleAndOpenData validation - Add Rule
sheet.command.smart-toggle-filterToggle Filter
sheet.command.clear-filter-criteriaToggle Filter - Clear Filter Conditions
sheet.command.re-calc-filterToggle Filter - Re-calc Filter Conditions
sheet.operation.open-pivot-table-range-selector-panelPivot Table
sheet.operation.print-openPrint
data-connector.operation.sidebarData Connector
sheets-exchange-client.operation.exchangeFile
exchange-client.operation.import-xlsxFile - Open(File)
exchange-client.operation.export-xlsxFile - Save As
sheet.command.menu-insert-chartInsert chart
sheet.command.add-range-protection-from-toolbarProtection
univer.operation.toggle-edit-historyEdit History
sheet.operation.open-sparkline-selectorSparkline
thread-comment-ui.operation.toggle-panelComment Management
sheet.operation.insert-hyper-link-toolbarInsert Link
ui.operation.open-find-dialogFind & Replace
base-ui.operation.toggle-shortcut-panelToggle Shortcut Panel

Context Menu Items

Menu Item IDMenu Item Name
sheet.command.copyCopy
sheet.command.pastePaste
sheet.menu.paste-specialPaste Special
sheet.command.paste-valuesPaste Special - Paste Value
sheet.command.paste-formatPaste Special - Paste Format
sheet.command.paste-col-widthPaste Special - Paste Column Width
sheet.command.paste-besides-borderPaste Special - Paste Besides Border Styles
sheet.command.paste-formulaPaste Special - Paste Formula
sheet.menu.clear-selectionClear
sheet.command.clear-selection-contentClear - Clear Contents
sheet.command.clear-selection-formatClear - Clear Formats
sheet.command.clear-selection-allClear - Clear All
sheet.menu.cell-insertInsert
sheet.command.insert-row-beforeInsert - Insert Row Before
sheet.command.insert-col-beforeInsert - Insert Column Before
sheet.command.insert-range-move-right-confirmInsert - Move Right
sheet.command.insert-range-move-down-confirmInsert - Move Down
sheet.menu.deleteDelete
sheet.command.remove-row-confirmDelete Selected Row
sheet.command.remove-col-confirmDelete Selected Column
sheet.command.delete-range-move-left-confirmDelete - Move Left
sheet.command.delete-range-move-up-confirmDelete - Move Up
sheet.menu.sheet-frozenFreeze
sheet.header-menu.sheet-frozenFreeze (Row & Column Header Right Click Menu Item)
sheet.command.set-selection-frozenFreeze - Freeze
sheet.command.set-row-frozenFreeze - Freeze to this row
sheet.command.set-col-frozenFreeze - Freeze to this column
sheet.command.cancel-frozenFreeze - Cancel freeze
sheet.contextMenu.permissionProtect Rows And Columns
sheet.command.add-range-protection-from-context-menuProtect Rows And Columns - Add Protection Range
sheet.command.set-range-protection-from-context-menuProtect Rows And Columns - Set Protection Range
sheet.command.delete-range-protection-from-context-menuProtect Rows And Columns - Remove Protection Range
sheet.command.view-sheet-permission-from-context-menuProtect Rows And Columns - View All Protection Ranges
sheet.menu.sheets-sort-ctxSort
sheet.command.sort-range-asc-ctxSort - Ascending
sheet.command.sort-range-asc-ext-ctxSort - Expand Ascending
sheet.command.sort-range-desc-ctxSort - Descending
sheet.command.sort-range-desc-ext-ctxSort - Expand Descending
sheet.command.sort-range-custom-ctxSort - Custom Sort
sheets.operation.show-comment-modalAdd Comment
sheet.operation.insert-hyper-linkInsert Link
zen-editor.command.open-zen-editorFull Screen Editor
sheet.operation.screenshotCopy as picture

Context Menu Items - Row Header

Menu Item IDMenu Item Name
sheet.command.insert-multi-rows-aboveInsert Row Before
sheet.command.insert-multi-rows-afterInsert Row After
sheet.command.hide-row-confirmHide Selected Row
sheet.command.set-selected-rows-visibleShow Hide Row
sheet.command.set-row-heightRow Height
sheet.command.set-row-is-auto-heightFit for data

Context Menu Items - Column Header

Menu Item IDMenu Item Name
sheet.command.insert-multi-cols-beforeInsert Column Before
sheet.command.insert-multi-cols-rightInsert Column After
sheet.command.hide-col-confirmHide Selected Column
sheet.command.set-selected-cols-visibleShow Hide Column
sheet.command.set-worksheet-col-widthColumn Width
sheet.command.set-col-auto-widthFit for data