Extending Commands
It is recommended to familiarize oneself with the Univer command system before reading this section.
Create a New Command
Creating a new command is the most common way to extend Univer. By creating a new command, you can implement various business logic. The following are the steps to create a new command:
Create an Object Implementing the ICommand
Interface
import { CommandType, ICommand } from '@univerjs/core'
export interface IYourCommandInterface {
// Your command's param's interface.
}
export const YourCommand: ICommand = {
name: 'your-command',
type: CommandType.COMMAND,
handler: async (accessor: IAccessor, params?: IYourCommandInterface) => {
// Implement your business logic here.
}
}
Commands require the declaration of the following attributes:
name
: The command’s name, which must be unique. It is suggested to name commands in the format ofdomain:type:meaning
, such assheet.command.copy
andsheet.command.paste
.type
: The command’s type.handler
: The command’s execution logic, which accepts a singleIAccessor
object and the command parameters. TheIAccessor
object allows access to Univer’s dependency injection system.
Commands may receive parameters, which must be grouped into an object. The interface of the parameter is determined by your business logic. Of course, a command can also not receive any parameters, in which case the second parameter of handler is undefined.
Register the Command with the Command Service
import { Disposable, ICommandService } from '@univerjs/core'
export class YourController extends Disposable {
constructor(
@ICommandService private readonly _commandService: ICommandService
) {
this.disposeWithMe(this._commandService.registerCommand(YourCommand))
}
}
After declaring the required attributes, the command can be executed using the ICommandService
. In real-world scenarios, it’s common to trigger commands via the user interface (UI).
Undo / Redo
Univer offers undo-redo functionality for commands that require it. To utilize this feature, call the appropriate methods of the IUndoRedoService
within the handler
callback function of your command:
import { IUndoRedoService } from '@univerjs/core'
export const YourCommand: ICommand = {
name: 'your-command',
type: CommandType.COMMAND,
handler: async (accessor: IAccessor, params?: IYourCommandInterface) => {
const undoRedoService = accessor.get(IUndoRedoService)
undoRedoService.pushUndoRedo({
unitID: 'your-documents-id',
undoMutations: [/** mutations for undo */],
redoMutations: [/** mutations for do and redo */],
})
}
}
Extend an Existing Command
Beyond creating new commands, Univer also supports extending existing ones, a feature especially important for expanding Univer’s built-in capabilities. Here, three representative scenarios are introduced.
Add Mutations at the Specific Command Execution Time
Extend the Copy-paste Functionality
In Univer, copy&paste operations are all added via hooks, which means that you can:
- Modify the default handling process of copying/pasting;
- Add some process in addition to the default.
To create a custom hook object, implement the ISheetClipboardHook
interface and add it to the SheetClipboardService
. The default copy-paste behavior in Univer is implemented in this way.
The hook object contains multiple optional hook functions for copying or pasting. You can learn about the definitions and execution times of all hooks in a later introduction.
During the copy-paste process, Univer will call the hook functions of ISheetClipboardHook
and execute them in a specific order and rule.
Create and Add a Hook
import { Disposable } from '@univerjs/core'
import { ISheetClipboardService, ISheetClipboardHook } from '@univerjs/sheets-ui';
export class YourController extends Disposable {
constructor(
@ISheetClipboardService private readonly _sheetClipboardService: ISheetClipboardService
) {
super()
const yourHook: ISheetClipboardHook = {
id: 'your-hook-id',
onBeforeCopy: () => {
alert('Hello!') // In this method, your code will be executed before copying.
}
// all hook methods are optional, you can learn it from interface definition.
}
// register your hook
this.disposeWithMe(this._sheetClipboardService.addClipboardHook(yourHook))
}
}
Handling the Copy Process with Hooks
Copy and paste behaviors are not always consistent, and the source of the copy and the destination of the paste can be both within Univer and external to it. Therefore, in a hook object, copy and paste can be processed independently, and you can choose to implement both or only one of them. In Univer, copy-paste behavior is mainly handled through the clipboard and relies on the clipboard.write
API.
Copy and cut operations can be triggered in Univer through keyboard shortcuts and menus. Once triggered, Univer will generate HTML and PLAIN text and write them to the clipboard.
Copy and cut share Hook Functions, which will be distinguished only during paste.
The following methods are exposed to implement in hook to handle the HTML generation process:
- onBeforeCopy: This Hook Function will be executed before the copy, and you can do some preliminary work here.
- onCopyCellContent: This Hook Function will handle the content of the copied cell, and it will process the string content of the
<td />
inside the<table />
in the generated HTML. - onCopyRow: This Hook Function will handle the row properties of the copy, and it will process the attributes of the
<tr />
inside the<table />
in the generated HTML. - onCopyColumn: This Hook Function will handle the column properties of the copy, and it will process the string content of the
<colgroup />
inside the<table />
in the generated HTML. - onAfterCopy: This Hook Function will be executed after the copy.
Handling the Paste Process with Hooks
In Univer, the paste operation can be triggered through keyboard shortcuts and menus. Unlike copy, paste flow involves mutation to modify data, and therefore, the Hook Functions related to paste mostly need to return a Mutations array, which should specify Undo and Redo. The parameters of the Hook Function can be used to determine whether the paste is from copy or cut.
The following methods are exposed in the hook to handle the paste process:
-
onBeforePaste: This Hook Function will be executed before the paste, and you can do some preliminary work here.
-
onPasteCells: This Hook Function will handle pasting the cells, and it should return the Undo Mutations & Redo Mutations for handling the cell content.
-
onPasteRows: This Hook Function will handle pasting the row properties, and it should return the Undo Mutations & Redo Mutations for handling the row properties.
-
onPasteColumns: This Hook Function will handle pasting the column properties, and it should return the Undo Mutations & Redo Mutations for handling the column properties.
-
onAfterPaste: This Hook Function will be executed after the paste.
Example: Number Format Copy-Paste in Univer
In Univer tables, the number format is a top-level module, and its information is independent of cell information. In the case of internal copy-paste, it requires actively saving format information when copying and performing corresponding operations to add number format when pasting. Therefore, only the onBeforeCopy and onPasteCells hooks need to be implemented. In the onPasteCells implementation, it is necessary to distinguish whether it is cut or copy.
export class NumfmtCopyPasteController extends Disposable {
constructor(
@Inject(Injector) private _injector: Injector,
@Inject(ISheetClipboardService) private _sheetClipboardService: ISheetClipboardService,
) {
super()
this._initClipboardHook()
}
// register hook
private _initClipboardHook() {
this.disposeWithMe(
this._sheetClipboardService.addClipboardHook({
hookName: 'numfmt',
onBeforeCopy: (unitId, subUnitId, range) => this._collectNumfmt(unitId, subUnitId, range),
onPasteCells: (pastedRange, _m, _p, _copyInfo) =>
this._generateNumfmtMutations(pastedRange, { ..._copyInfo, pasteType: _p }),
})
)
}
// collect number format info
private _collectNumfmt(unitId: string, subUnitId: string, range: IRange) {
// save number format info to private variable
}
// generate number format mutations
private _generateNumfmtMutations(
pastedRange: IRange,
copyInfo: {
copyType: COPY_TYPE
copyRange?: IRange
pasteType: string
}
) {
if (copyInfo.copyType === COPY_TYPE.CUT) {
// remove number format info
}
if (copyInfo.pasteType === PASTE_TYPE.COPY) {
// add number format info
return {
redos: [
{ id: RemoveNumfmtMutation.id, params: removeRedos },
{ id: SetNumfmtMutation.id, params: setRedos },
],
undos: [
...factorySetNumfmtUndoMutation(this._injector, setRedos),
...factoryRemoveNumfmtUndoMutation(this._injector, removeRedos),
],
}
}
}
}
Here is the detailed definition of the ISheetClipboardHook
interface:
export interface ISheetClipboardHook {
hookName: string
specialPasteInfo?: ISpecialPasteInfo
priority?: number
onBeforeCopy?: (unitId: string, subUnitId: string, range: IRange) => void
onCopyCellContent?: (row: number, col: number) => string
onCopyCellStyle?: (row: number, col: number, rowSpan?: number, colSpan?: number) => IClipboardPropertyItem | null
onCopyRow?: (row: number) => IClipboardPropertyItem | null
onCopyColumn?: (col: number) => IClipboardPropertyItem | null
onAfterCopy?: () => void
onBeforePaste?: (unitId: string, subUnitId: string, range: IRange) => boolean
onPasteCells?: (
pastedRange: IRange,
matrix: ObjectMatrix<ICellDataWithSpanInfo>,
pasteType: string,
copyInfo: {
copyType: COPY_TYPE
copyRange?: IRange
subUnitId?: string
unitId?: string
}
) => {
undos: IMutationInfo[]
redos: IMutationInfo[]
}
onPasteRows?: (
range: IRange,
rowProperties: IClipboardPropertyItem[],
pasteType: string
) => {
undos: IMutationInfo[]
redos: IMutationInfo[]
}
onPasteColumns?: (
range: IRange,
colProperties: IClipboardPropertyItem[],
pasteType: string
) => {
undos: IMutationInfo[]
redos: IMutationInfo[]
}
onAfterPaste?: (success: boolean) => void
getFilteredOutRows?: () => number[]
}
Extend the Drop-down Filling
In Univer, the drop-down filling is also implemented through hooks, similar to copy-paste. This means that you can implement the ISheetAutoFillHook
interface to add a hook object to modify and extend the behavior of drop-down filling.
Create and Add a Hook
import { Disposable } from '@univerjs/core'
import { IAutoFillService, ISheetAutoFillHook } from '@univerjs/sheets-ui'
export class YourController extends Disposable {
constructor(
@IAutoFillService private readonly _autoFillService: IAutoFillService,
) {
super()
const yourHook: ISheetAutoFillHook = {
id: 'your-hook-id',
priority: 1,
type: AutoFillHookType.Append, // This hook will be executed after the default one
onBeforeFillData: (location, direction) => {
// In this method, you can cache the date in source range in case of refilling
console.log(`AutoFill will apply from Range-${location.source} to Range-${location.target}`)
},
onFillData: (location, direction, applyType) => {
console.log(`apply type is ${applyType}`)
// In this method, you can provide the mutations in redos which are supposed to be executed.
// Undos is also necessary.
return {
undos: [],
redos: [],
}
},
onAfterFillData: (location, direction, applyType) => {
console.log('AutoFill is completed.')
}
// all hook methods are optional, you can learn it from interface definition.
}
// register your hook
this.disposeWithMe(this._autoFillService.addHook(yourHook))
}
}
Modifying the Default Drop-Down Filling
In Univer, the default drop-down filling is implemented using hooks, with the exception of changes to the selection. Its Hook Function handles the sequence content and style.
This default hook is similar to other hooks, except for its type is AutoFillHookType.Default
. Only one such hook can be effective, and it will be the first to execute. Therefore, you can write your own default hook, as long as its Priority is greater than the default value of 0.
const yourHook: ISheetAutoFillHook = {
id: 'your-hook-id',
priority: 1,
type: AutoFillHookType.Default,
onBeforeFillData: (location, direction) => {
console.log(`AutoFill will apply from Range-${location.source} to Range-${location.target}`)
},
onFillData: (location, direction, applyType) => {
return {
undos: [],
redos: [],
}
},
onAfterFillData: (location, direction, applyType) => {
console.log('AutoFill is completed.')
}
}
Add Mutations to the Drop-Down Filling
If you want to execute some additional mutations during drop-down filling, such as having third-party values also fill down with the selection, you can add a hook object which type is AutoFillHookType.Append
, and write your codes in the corresponding Hook Function. This type of hook will be executed after the default hook. it and can also be disabled using the disable method.
const yourHook: ISheetAutoFillHook = {
id: 'your-hook-id',
priority: 0,
type: AutoFillHookType.Append,
disable: (location, direction, applyType) => true,
onBeforeFillData: (location, direction) => {
console.log(`AutoFill will apply from Range-${location.source} to Range-${location.target}`)
},
onFillData: (location, direction, applyType) => {
return {
undos: [],
redos: [],
}
},
onAfterFillData: (location, direction, applyType) => {
console.log('AutoFill is completed.')
}
}