创建新命令
创建新命令是 Univer 中最常见的扩展方式,通过创建新的命令,你可以实现各种业务逻辑。下面是创建新命令的步骤:
name:命令的名称,必须唯一;我们建议以业务域:类型:含义的方式来命名,例如sheet.command.copy、sheet.command.paste等type:命令的类型handler:命令的执行逻辑,接收一个IAccessor对象以及命令参数,通过IAccessor对象可以访问 Univer 的依赖注入系统
创建一个实现 ICommand 接口的对象:
import type { IAccessor, ICommand } from '@univerjs/core'import { CommandType } 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. },}命令需要声明以下属性:
命令可接收参数,参数需要是一个对象,接口由你的业务逻辑决定。当然一个命令也可以不接收参数,此时 handler 的第二个参数为 undefined。
将 Command 注册到命令服务上:
import { Disposable, ICommandService } from '@univerjs/core'export class YourController extends Disposable { constructor(@ICommandService private readonly _commandService: ICommandService) { this.disposeWithMe(this._commandService.registerCommand(YourCommand)) }}之后就可以通过 ICommandService 来执行命令了。实践中常用的方式是通过 UI 触发。
Undo / Redo
Univer 提供了 undo redo 的能力,如果你的命令需要接入,需要在命令的 handler 回调函数中调用 IUndoRedoService 的对应方法:
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 */], }) },}扩展已有命令
除了创建新命令,Univer 还支持扩展已有的命令,这在扩展 Univer 内置能力时尤其必要。以下介绍三个比较典型的场景。
在其他 COMMAND 执行的时机补充 MUTATIONS
扩展复制粘贴
Univer 中的复制粘贴操作都是通过插件化的方式添加的,这意味着你可以:
- 修改复制/粘贴的默认处理过程;
- 在默认行为之外再追加一些逻辑。
你可以通过实现 ISheetClipboardHook 接口来构造一个你自己的 Hook 对象,并把它添加到 SheetClipboardService 上,Univer 默认的复制粘贴行为也是通过这一方式实现的。
Hook 对象包含多个可选的复制或粘贴钩子函数(Hook Function),你可以在后面的介绍了解所有钩子的定义和执行时机。
在复制粘贴的过程中,Univer 会调用 ISheetClipboardHook 的钩子函数,并按照特定规则及顺序执行。
创建和添加 Hook
import type { ISheetClipboardHook } from '@univerjs/sheets-ui'import { Disposable } from '@univerjs/core'import { ISheetClipboardService } from '@univerjs/sheets-ui'export class YourController extends Disposable { constructor(@ISheetClipboardService private readonly _sheetClipboardService: ISheetClipboardService) { super() const yourHook: ISheetClipboardHook = { id: 'your-hook-id', onBeforeCopy: () => { console.log('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)) }}使用 Hook 处理复制流程
复制和粘贴行为并不具有连贯性,复制的来源和粘贴去向不仅会在 Univer 内部,也可能在外部。因此,在 Hook 对象中,复制和粘贴可以进行独立的处理,你可以选择在 你的 Hook 中同时处理复制和粘贴,或只处理一个。Univer 复制粘贴的内容主要通过剪切板来存取,依赖clipboard.writeAPI。
在 Univer 中,可以通过快捷键和菜单触发复制操作。触发后,Univer 会将选区内容生成 HTML 和 PLAIN 写入剪切板。
复制和剪切共用 Hook Functions,它们只会在粘贴时进行区分。
Hook 中暴露如下方法来处理 HTML 的生成过程:
- onBeforeCopy: 这个 Hook Function 会在复制前执行,你可以在这里做一些前置工作。
- onCopyCellContent: 这个 Hook Function 会处理复制单元格的内容,它会决定生成的 HTML 中
<table />中的<td />里的字符串内容。 - onCopyRow: 这个 Hook Function 会处理复制行属性,它会决定生成的 HTML 中
<table />中的<tr />里的属性。 - onCopyColumn: 这个 Hook Function 会处理复制列属性,它会决定生成的 HTML 中
<table />中的<colgroup />里的字符串内容。 - onAfterCopy: 这个 Hook Function 会在复制后执行。
使用 Hook 处理粘贴流程
在 Univer 中,可以通过快捷键和菜单触发粘贴操作。不同于复制,粘贴流程会触发 Mutation 来修改数据,因此关于粘贴的 Hook Function 基本都需要返回 Mutations 数组,需指定 Undo 和 Redo。在 Hook Function 的参数中,可以判断粘贴的来源是复制还是剪切。
Hook 中暴露如下方法来处理粘贴过程:
-
onBeforePaste: 这个 Hook Function 会在粘贴前执行,你可以在这里做一些前置工作。
-
onPasteCells: 这个 Hook Function 会处理粘贴单元格,它需要返回处理单元格内容的 Undo Mutations & Redo Mutations。
-
onPasteRows: 这个 Hook Function 会处理粘贴行属性,它需要返回处理行属性的 Undo Mutations & Redo Mutations。
-
onPasteColumns: 这个 Hook Function 会处理粘贴列属性,它需要返回处理列属性的 Undo Mutations & Redo Mutations。
-
onAfterPaste: 这个 Hook Function 会在粘贴后执行。
例子:在 Univer 中复制粘贴数据格式
在 Univer 表格中,数字格式是一个上层模块,其信息独立于单元格信息之外。在只考虑内部复制粘贴的情况下,它需要在复制时主动去保存格式信息,并在粘贴时执行对应的添加数字格式的操作,因此只需实现 Hook 中的 onBeforeCopy 和 onPasteCells。在 onPasteCells 的实现中,需要区分是剪切还是复制。
import type { IRange } from '@univerjs/core'import { Disposable, Inject, Injector } from '@univerjs/core'import { factoryRemoveNumfmtUndoMutation, factorySetNumfmtUndoMutation, IRemoveNumfmtMutationParams, ISetNumfmtMutationParams, RemoveNumfmtMutation, SetNumfmtMutation,} from '@univerjs/sheets'import { COPY_TYPE, ISheetClipboardService } from '@univerjs/sheets-ui'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({ id: '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 // const setRedos: ISetNumfmtMutationParams; // const removeRedos: IRemoveNumfmtMutationParams; return { redos: [ { id: RemoveNumfmtMutation.id, params: removeRedos }, { id: SetNumfmtMutation.id, params: setRedos }, ], undos: [ ...factorySetNumfmtUndoMutation(this._injector, setRedos), ...factoryRemoveNumfmtUndoMutation(this._injector, removeRedos), ], } } }}关于ISheetClipboardHook接口的详细定义如下
/** * `ClipboardHook` 可以: * 1. 在复制/剪切/粘贴之前,决定是否执行命令,并在必要时准备缓存。 * 2. 在复制时,决定哪些内容可以写入剪贴板。 * 3. 在粘贴时,获取剪贴板内容并将变更追加到粘贴命令。 */export interface ISheetClipboardHook { /** * 钩子的唯一 ID。 */ id: string /** * 是否为默认钩子。 */ isDefaultHook?: boolean /** * 只有特殊粘贴信息时,才会提供,该信息会替换默认钩子。 */ specialPasteInfo?: ISpecialPasteInfo /** * 钩子的优先级。优先级越高,钩子调用越早。 */ priority?: number /** * 在剪贴板服务决定需要复制的区域后调用的回调。 * 功能可以使用此钩子构建复制缓存或执行其他预复制任务。 * @param unitId * @param subUnitId * @param range */ onBeforeCopy?: (unitId: string, subUnitId: string, range: IRange) => void /** * 需要附加到 td 元素的属性。 * * @param row * @param col * @return content */ onCopyCellContent?: (row: number, col: number) => string /** * 需要附加到 td 元素的属性。 * * @deprecated 应该将其与 `onCopyCellContent` 合并到 `onCopyCell` 中 * @param row 复制的单元格的行 * @param col 复制的单元格的列 * @param rowSpan 复制的单元格的行跨度 * @param colSpan 复制的单元格的列跨度 * @return content */ onCopyCellStyle?: (row: number, col: number, rowSpan?: number, colSpan?: number) => IClipboardPropertyItem | null /** * 需要附加到 tr 元素的属性。 * @param row 复制区域的每一行 * @return content */ onCopyRow?: (row: number) => IClipboardPropertyItem | null /** * 需要附加到 col 元素的属性。 * @param col 复制区域的每一列 * @return content */ onCopyColumn?: (col: number) => IClipboardPropertyItem | null /** * 在复制内容写入剪贴板后调用。 * 功能可以在这里做一些清理工作。 */ onAfterCopy?: () => void // 不需要剪切钩子。我们可以使用复制钩子完成相同的操作, // 并告诉粘贴钩子源自剪切命令。 // 与复制钩子不同,粘贴钩子**每次仅调用一次**,因为功能会执行批量变更。 /** * 在剪贴板服务决定需要粘贴的区域后调用的回调。 * 功能可以使用此钩子构建粘贴缓存或执行其他预粘贴任务。 * * @param pasteTo * @returns 如果阻止复制,应该返回 false */ onBeforePaste?: (pasteTo: ISheetDiscreteRangeLocation) => boolean /** * 处理粘贴单元格,需要返回处理单元格内容的撤销和重做变更。 * * @param pasteFrom * @param pasteTo * @param data * @param payload * @returns 撤销和重做变更 */ onPasteCells?: ( pasteFrom: ISheetDiscreteRangeLocation | null, pasteTo: ISheetDiscreteRangeLocation, data: ObjectMatrix<ICellDataWithSpanInfo>, payload: ICopyPastePayload, ) => { undos: IMutationInfo[] redos: IMutationInfo[] } /** * 处理粘贴的行属性,需要返回处理行属性的撤销和重做变更。 * * @param pasteTo * @param rowProperties * @param payload * @returns 撤销和重做变更 */ onPasteRows?: ( pasteTo: ISheetDiscreteRangeLocation, rowProperties: IClipboardPropertyItem[], payload: ICopyPastePayload, ) => { undos: IMutationInfo[] redos: IMutationInfo[] } /** * 处理粘贴的列属性,需要返回处理列属性的撤销和重做变更。 * * @param pasteTo * @param colProperties * @param payload * @returns 撤销和重做变更 */ onPasteColumns?: ( pasteTo: ISheetDiscreteRangeLocation, colProperties: IClipboardPropertyItem[], payload: ICopyPastePayload, ) => { undos: IMutationInfo[] redos: IMutationInfo[] } /** * 处理粘贴的纯文本,需要返回处理纯文本的撤销和重做变更。 * * @param pasteTo * @param text * @param payload * @returns 撤销和重做变更 */ onPastePlainText?: ( pasteTo: ISheetDiscreteRangeLocation, text: string, payload: ICopyPastePayload, ) => { undos: IMutationInfo[] redos: IMutationInfo[] } /** * 在粘贴内容写入 Univer 后调用。 * 功能可以在这里做一些清理工作。 * * @param success 是否粘贴操作成功 */ onAfterPaste?: (success: boolean) => void /** * 在剪贴板服务决定需要复制或粘贴的区域之前调用的回调。 * 它会跳过这些被过滤的行,在复制或粘贴时忽略它们。 */ getFilteredOutRows?: (range: IRange) => number[]}更多关于复制粘贴 Hook 的案例
扩展下拉填充
Univer 中的下拉填充也是通过插件化的方式添加的,类似复制粘贴,这意味着你可以通过实现接口ISheetAutoFillHook来添加一个 Hook 对象,从而修改和拓展下拉填充的行为。
创建和添加 Hook
import type { ISheetAutoFillHook } from '@univerjs/sheets-ui'import { Disposable } from '@univerjs/core'import { IAutoFillService } 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)) }}修改默认下拉填充
除了选区变更以外,Univer 的默认下拉填充行为都是以 Hook 的形式添加的,在它的 Hook Function 中处理了扩充序列内容、样式等逻辑。
这个 Hook 与其他的 Hook 没什么不同,只是type: AutoFillHookType.Default而已,这类 Hook 只会有一个生效,并会第一个执行。因此,你完全可以自己写一个type: AutoFillHookType.Default的 Hook,只要它的 Priority 比默认的 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.') },}拓展下拉填充
如果在下拉填充时,希望执行一些额外的逻辑,比如让第三方的值也会跟随着下拉一起填充,你可以添加一个type: AutoFillHookType.Append的 Hook 对象,并在对应的 Hook Function 处理你的逻辑。这类 Hook 会在 AutoFillHookType.Default 后跟随执行,也可以通过 disable 方法去让它禁用。
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.') },}更多关于下拉填充 Hook 的案例