From 04ede3fe50e01b1e6e2ec25fcdaaeba5013138fd Mon Sep 17 00:00:00 2001 From: Wenzhao Hu Date: Tue, 27 Aug 2024 20:28:38 +0800 Subject: [PATCH] refactor(filter): refactor filter for demo (#3194) --- .vscode/redi.code-snippets | 6 +- examples/src/sheets/lazy.ts | 2 +- .../src/services/command/command.service.ts | 168 ++++++++++++------ .../sheets-filter-ui-desktop.controller.ts | 7 +- .../sheets-filter-ui-mobile.controller.ts | 22 ++- .../src/filter-ui-desktop.plugin.ts | 10 +- .../sheets-filter.render-controller.ts | 52 ++---- .../controllers/sheets-filter.controller.ts | 2 +- .../src/services/sheet-filter.service.ts | 14 +- .../src/controllers/__tests__/test.util.ts | 4 + .../create-selection-command-test-bed.ts | 2 + .../__tests__/set-selections.command.spec.ts | 16 +- .../commands/set-selection.command.ts | 41 ++--- .../sheet-permission-render.controller.ts | 22 +-- .../src/controllers/sheet-ui.controller.ts | 2 +- .../sheets-ui/src/mobile-sheets-ui-plugin.ts | 38 ++-- .../services/select-all/select-all.service.ts | 23 +++ .../base-selection-render.service.ts | 13 +- .../mobile-selection-render.service.ts | 15 +- packages/sheets-ui/src/sheets-ui-plugin.ts | 44 ++--- .../range-protection.ref-range.ts | 2 - .../range-protection.service.ts | 5 +- .../__tests__/create-core-test-bed.ts | 5 +- .../sheet-interceptor.service.spec.ts | 4 +- .../sheet-interceptor.service.ts | 12 +- packages/sheets/src/sheets-plugin.ts | 17 +- .../src/services/shortcut/shortcut.service.ts | 97 +++++++--- 27 files changed, 365 insertions(+), 280 deletions(-) rename packages/sheets-filter-ui/src/views/{widgets => }/render-modules/sheets-filter.render-controller.ts (81%) create mode 100644 packages/sheets-ui/src/services/select-all/select-all.service.ts diff --git a/.vscode/redi.code-snippets b/.vscode/redi.code-snippets index 22f45ff6f5..5bc6f73a51 100644 --- a/.vscode/redi.code-snippets +++ b/.vscode/redi.code-snippets @@ -18,11 +18,13 @@ "Redi Injection Identifier": { "prefix": ["@I"], "body": ["@I${1:identifier} private readonly _${2:name}: I${1:identifier},"], - "description": "Inject an identifier with Redi." + "description": "Inject an identifier with Redi.", + "scope": "typescript,typescriptreact" }, "Redi Injection": { "prefix": ["@In"], "body": ["@Inject(${1:identifier}) private readonly _${2:name}: ${1:identifier},"], - "description": "Inject a class item with Redi." + "description": "Inject a class item with Redi.", + "scope": "typescript,typescriptreact" } } diff --git a/examples/src/sheets/lazy.ts b/examples/src/sheets/lazy.ts index b10c232d50..61c1561edb 100644 --- a/examples/src/sheets/lazy.ts +++ b/examples/src/sheets/lazy.ts @@ -36,7 +36,7 @@ export default function getLazyPlugins(): Array<[PluginCtor] | [PluginCt // ], [UniverSheetsCrosshairHighlightPlugin], - [UniverSheetsFilterUIPlugin, { useRemoteFilterValuesGenerator: true }], + [UniverSheetsFilterUIPlugin, { useRemoteFilterValuesGenerator: false }], [UniverSheetsFindReplacePlugin], ]; } diff --git a/packages/core/src/services/command/command.service.ts b/packages/core/src/services/command/command.service.ts index 47d8fba70a..0c51ed31a8 100644 --- a/packages/core/src/services/command/command.service.ts +++ b/packages/core/src/services/command/command.service.ts @@ -25,93 +25,142 @@ import { IContextService } from '../context/context.service'; import { ILogService } from '../log/log.service'; import { CustomCommandExecutionError } from '../../common/error'; +/** + * The type of a command. + */ export enum CommandType { - /** Command could generate some operations or mutations. */ + /** + * Responsible for creating, orchestrating, and executing MUTATION or OPERATION according to specific business + * logic. For example, a delete row COMMAND will generate a delete row MUTATION, an insert row MUTATION for undo, + * and a set cell content MUTATION. + */ COMMAND = 0, - /** An operation that do not require conflict resolve. */ + /** + * MUTATION is the change made to the data saved to snapshot, such as inserting rows and columns, + * modifying cell content, modifying filter ranges, etc. If you want to add collaborative editing capabilities to + * Univer, it is the smallest unit of conflict resolution. + */ OPERATION = 1, - /** An operation that need to be resolved before applied on peer client. */ + /** + * OPERATION is the change made to data that is not saved to snapshot, without conflict resolution, + * such as modifying scroll position, modifying sidebar state, etc. + */ MUTATION = 2, } +/** + * In Univer, all data modifications need to be executed through commands. The command-based approach can better track + * changes in values, implement functions such as undo, redo, and collaborative editing, handle complex associated + * logic between functions, etc. + * + * All commands should implements this interface or related {@link IMutation} or {@link IOperation} interface, and + * should be registered in the {@link ICommandService}. + */ export interface ICommand

{ /** - * ${businessName}.${type}.${name} + * Identifier of the command. It should be unique in the application unless it is a {@link IMultiCommand}. + * Its pattern should be like `..`. + * + * @example { id: 'sheet.command.set-selection-frozen' } */ readonly id: string; - readonly type: CommandType; - - handler(accessor: IAccessor, params?: P, options?: IExecutionOptions): Promise | R; - /** - * When this command is unregistered, this function would be called. - * - * @deprecated + * The type of the command. */ - onDispose?: () => void; + readonly type: CommandType; + /** + * The handler of the command. + * @param accessor The accessor to the dependency injection container. + * @param params Params of the command. Params should be serializable. + * @param options Options of the command. + * @returns The result of the command. By default it should be a boolean value which indicates the command is + * executed successfully or not. + */ + handler(accessor: IAccessor, params?: P, options?: IExecutionOptions): Promise | R; } +/** + * A command that may have multiple implementations. Each implementation should have different `priority` + * and `preconditions` callback to determine which implementation should be executed. + */ export interface IMultiCommand

extends ICommand { + /** The name of the multi command. It should be unique in the application. */ name: string; + /** @ignore */ multi: true; + /** Priority of this implementation. Implementation with higher priority will be checked first. */ priority: number; - + /** + * A callback function that tells `ICommandService` if this implementation should be executed. + * @param contextService The context service. + * @returns If this implementation should be executed, return `true`, otherwise return `false`. + */ preconditions?: (contextService: IContextService) => boolean; } export interface IMutationCommonParams { + /** + * It is used to indicate which {@link CommandType.COMMAND} triggers the mutation. + */ trigger?: string; } /** - * Mutation would change the model of Univer applications. + * {@link CommandType.MUTATION} should implement this interface. */ export interface IMutation

extends ICommand { type: CommandType.MUTATION; - /** - * Mutations must be a sync process. - * @param accessor - * @param params Params of the mutation. A mutation must has params. + * The handler of the mutation. + * @param accessor The accessor to the dependency injection container. + * @param params Params of the mutation. Params should be serializable. + * @returns The result of the mutation. By default it should be a boolean value which indicates the mutation is + * executed successfully or not. */ handler(accessor: IAccessor, params: P): R; } /** - * Operation would change the state of Univer applications. State should only be in memory and does not - * require conflicting resolution. + * {@link CommandType.OPERATION} should implement this interface. */ export interface IOperation

extends ICommand { type: CommandType.OPERATION; - /** - * Operations must be a sync process. - * @param accessor - * @param params Params of the operation. A operation must has params. + * The handler of the operation. + * @param accessor The accessor to the dependency injection container. + * @param params Params of the operation. Params should be serializable. + * @returns The result of the operation. By default it should be a boolean value which indicates the operation is + * executed successfully or not. */ handler(accessor: IAccessor, params: P): R; } /** - * The command info, only a command id and responsible params + * This object represents an execution of a command. */ export interface ICommandInfo { - id: string; - - type?: CommandType; - /** - * Args should be serializable. + * Id of the command being executed. + */ + id: string; + /** + * Type of the command. + */ + type?: CommandType; + /** + * Parameters of this execution. */ params?: T; } +/** This object represents an execution of a {@link CommandType.MUTATION} */ export interface IMutationInfo { id: string; type?: CommandType.MUTATION; params: T; } +/** This object represents an execution of a {@link CommandType.OPERATION} */ export interface IOperationInfo { id: string; type?: CommandType.OPERATION; @@ -119,46 +168,66 @@ export interface IOperationInfo { } export interface IExecutionOptions { - /** This mutation should only be executed on the local machine, and not synced to replicas. */ + /** This mutation should only be executed on the local machine, and should not be synced to replicas. */ onlyLocal?: boolean; - /** This command is from collaboration peers. */ fromCollab?: boolean; - + /** @deprecated */ fromChangeset?: boolean; - [key: PropertyKey]: string | number | boolean | undefined; } export type CommandListener = (commandInfo: Readonly, options?: IExecutionOptions) => void; +/** + * The identifier of the command service. + */ +export const ICommandService = createIdentifier('anywhere.command-service'); +/** + * The service to register and execute commands. + */ export interface ICommandService { /** * Check if a command is already registered at the current command service. - * * @param commandId The id of the command. + * @returns If the command is registered, return `true`, otherwise return `false`. */ hasCommand(commandId: string): boolean; - + /** + * Register a command to the command service. + * @param command The command to register. + */ registerCommand(command: ICommand): IDisposable; - + /** + * Register a command as a multi command. + * @param command The command to register as a multi command. + */ registerMultipleCommand(command: ICommand): IDisposable; - + /** + * Execute a command with the given id and parameters. + * @param id Identifier of the command. + * @param params Parameters of this execution. + * @param options Options of this execution. + * @returns The result of the execution. It is a boolean value by default which indicates the command is executed. + */ executeCommand

( id: string, params?: P, options?: IExecutionOptions ): Promise; - - hasCommand(id: string): boolean; - + /** + * Execute a command with the given id and parameters synchronously. + * @param id Identifier of the command. + * @param params Parameters of this execution. + * @param options Options of this execution. + * @returns The result of the execution. It is a boolean value by default which indicates the command is executed. + */ syncExecuteCommand

(id: string, params?: P, options?: IExecutionOptions): R; - /** * Register a callback function that will be executed after a command is executed. + * @param listener */ onCommandExecuted(listener: CommandListener): IDisposable; - /** * Register a callback function that will be executed before a command is executed. * @param listener @@ -166,12 +235,7 @@ export interface ICommandService { beforeCommandExecuted(listener: CommandListener): IDisposable; } -export const ICommandService = createIdentifier('anywhere.command-service'); - -/** - * The registry of commands. - */ -export class CommandRegistry { +class CommandRegistry { private readonly _commands = new Map(); private readonly _commandTypes = new Map(); @@ -186,8 +250,6 @@ export class CommandRegistry { return toDisposable(() => { this._commands.delete(command.id); this._commandTypes.delete(command.id); - - command.onDispose?.(); }); } @@ -208,7 +270,7 @@ export class CommandRegistry { } } -interface ICommandExecutionStackItem extends ICommandInfo { } +interface ICommandExecutionStackItem extends ICommandInfo {} export const NilCommand: ICommand = { id: 'nil', @@ -476,8 +538,6 @@ class MultiCommand implements IMultiCommand { return toDisposable(() => { const index = this._implementations.indexOf(registry); this._implementations.splice(index, 1); - - implementation.onDispose?.(); }); } diff --git a/packages/sheets-filter-ui/src/controllers/sheets-filter-ui-desktop.controller.ts b/packages/sheets-filter-ui/src/controllers/sheets-filter-ui-desktop.controller.ts index ec1edbebae..6be9d62306 100644 --- a/packages/sheets-filter-ui/src/controllers/sheets-filter-ui-desktop.controller.ts +++ b/packages/sheets-filter-ui/src/controllers/sheets-filter-ui-desktop.controller.ts @@ -20,7 +20,7 @@ import { ComponentManager, IMenuService, IMessageService, IShortcutService } fro import type { IMenuItemFactory, MenuConfig } from '@univerjs/ui'; import { distinctUntilChanged } from 'rxjs'; -import { SheetCanvasPopManagerService } from '@univerjs/sheets-ui'; +import { SheetCanvasPopManagerService, SheetsRenderService } from '@univerjs/sheets-ui'; import { FilterSingle } from '@univerjs/icons'; import { IRenderManagerService } from '@univerjs/engine-render'; @@ -61,9 +61,10 @@ export class SheetsFilterUIDesktopController extends SheetsFilterUIMobileControl @IMenuService private readonly _menuService: IMenuService, @IContextService private readonly _contextService: IContextService, @IMessageService private readonly _messageService: IMessageService, - @IRenderManagerService _renderManagerService: IRenderManagerService + @Inject(SheetsRenderService) sheetsRenderService: SheetsRenderService, + @IRenderManagerService renderManagerService: IRenderManagerService ) { - super(_renderManagerService); + super(renderManagerService, sheetsRenderService); this._initCommands(); this._initShortcuts(); diff --git a/packages/sheets-filter-ui/src/controllers/sheets-filter-ui-mobile.controller.ts b/packages/sheets-filter-ui/src/controllers/sheets-filter-ui-mobile.controller.ts index 2de772bfc2..e865125381 100644 --- a/packages/sheets-filter-ui/src/controllers/sheets-filter-ui-mobile.controller.ts +++ b/packages/sheets-filter-ui/src/controllers/sheets-filter-ui-mobile.controller.ts @@ -14,21 +14,29 @@ * limitations under the License. */ -import { RxDisposable, UniverInstanceType } from '@univerjs/core'; +import { Inject, RxDisposable, UniverInstanceType } from '@univerjs/core'; import { IRenderManagerService } from '@univerjs/engine-render'; import type { Dependency } from '@univerjs/core'; -import { SheetsFilterRenderController } from '../views/widgets/render-modules/sheets-filter.render-controller'; +import { ReCalcSheetsFilterMutation, RemoveSheetsFilterMutation, SetSheetsFilterCriteriaMutation, SetSheetsFilterRangeMutation } from '@univerjs/sheets-filter'; +import { SheetsRenderService } from '@univerjs/sheets-ui'; +import { SheetsFilterRenderController } from '../views/render-modules/sheets-filter.render-controller'; export class SheetsFilterUIMobileController extends RxDisposable { constructor( - @IRenderManagerService private readonly _renderManagerService: IRenderManagerService + @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, + @Inject(SheetsRenderService) private _sheetsRenderService: SheetsRenderService ) { super(); - this._initRenderControllers(); - } + [ + SetSheetsFilterRangeMutation, + SetSheetsFilterCriteriaMutation, + RemoveSheetsFilterMutation, + ReCalcSheetsFilterMutation, + ].forEach((m) => this.disposeWithMe(this._sheetsRenderService.registerSkeletonChangingMutations(m.id))); - private _initRenderControllers(): void { - this.disposeWithMe(this._renderManagerService.registerRenderModule(UniverInstanceType.UNIVER_SHEET, [SheetsFilterRenderController] as Dependency)); + this.disposeWithMe(this._renderManagerService.registerRenderModule( + UniverInstanceType.UNIVER_SHEET, [SheetsFilterRenderController] as Dependency + )); } } diff --git a/packages/sheets-filter-ui/src/filter-ui-desktop.plugin.ts b/packages/sheets-filter-ui/src/filter-ui-desktop.plugin.ts index 8efe0785ae..2a92b50abf 100644 --- a/packages/sheets-filter-ui/src/filter-ui-desktop.plugin.ts +++ b/packages/sheets-filter-ui/src/filter-ui-desktop.plugin.ts @@ -49,15 +49,17 @@ export class UniverSheetsFilterUIPlugin extends Plugin { [SheetsFilterPanelService], [SheetsFilterPermissionController], [SheetsFilterUIDesktopController, { - useFactory: () => this._injector.createInstance(SheetsFilterUIDesktopController, this._config), + useFactory: (): SheetsFilterUIDesktopController => + this._injector.createInstance(SheetsFilterUIDesktopController, this._config), }], ] as Dependency[]).forEach((d) => this._injector.add(d)); if (this._config.useRemoteFilterValuesGenerator && this._rpcChannelService) { this._injector.add([ISheetsGenerateFilterValuesService, { - useFactory: () => toModule( - this._rpcChannelService!.requestChannel(SHEETS_GENERATE_FILTER_VALUES_SERVICE_NAME) - ), + useFactory: (): ISheetsGenerateFilterValuesService => + toModule( + this._rpcChannelService!.requestChannel(SHEETS_GENERATE_FILTER_VALUES_SERVICE_NAME) + ), }]); } } diff --git a/packages/sheets-filter-ui/src/views/widgets/render-modules/sheets-filter.render-controller.ts b/packages/sheets-filter-ui/src/views/render-modules/sheets-filter.render-controller.ts similarity index 81% rename from packages/sheets-filter-ui/src/views/widgets/render-modules/sheets-filter.render-controller.ts rename to packages/sheets-filter-ui/src/views/render-modules/sheets-filter.render-controller.ts index 17857dc40f..afd4e405a0 100644 --- a/packages/sheets-filter-ui/src/views/widgets/render-modules/sheets-filter.render-controller.ts +++ b/packages/sheets-filter-ui/src/views/render-modules/sheets-filter.render-controller.ts @@ -17,16 +17,15 @@ import type { IDisposable, IRange, Workbook } from '@univerjs/core'; import { CommandType, fromCallback, ICommandService, Inject, Injector, RxDisposable, ThemeService } from '@univerjs/core'; import type { IRenderContext, IRenderModule, SpreadsheetSkeleton } from '@univerjs/engine-render'; -import { IRenderManagerService } from '@univerjs/engine-render'; import type { ISelectionStyle, ISheetCommandSharedParams } from '@univerjs/sheets'; import { INTERCEPTOR_POINT, SheetInterceptorService } from '@univerjs/sheets'; import type { FilterModel } from '@univerjs/sheets-filter'; -import { FILTER_MUTATIONS, ReCalcSheetsFilterMutation, RemoveSheetsFilterMutation, SetSheetsFilterCriteriaMutation, SetSheetsFilterRangeMutation, SheetsFilterService } from '@univerjs/sheets-filter'; -import { getCoordByCell, ISheetSelectionRenderService, SelectionShape, SheetSkeletonManagerService, SheetsRenderService } from '@univerjs/sheets-ui'; +import { FILTER_MUTATIONS, SheetsFilterService } from '@univerjs/sheets-filter'; +import { getCoordByCell, ISheetSelectionRenderService, SelectionShape, SheetSkeletonManagerService } from '@univerjs/sheets-ui'; import { filter, map, of, startWith, switchMap, takeUntil, throttleTime } from 'rxjs'; -import type { ISheetsFilterButtonShapeProps } from '../filter-button.shape'; -import { FILTER_ICON_PADDING, FILTER_ICON_SIZE, SheetsFilterButtonShape } from '../filter-button.shape'; +import type { ISheetsFilterButtonShapeProps } from '../widgets/filter-button.shape'; +import { FILTER_ICON_PADDING, FILTER_ICON_SIZE, SheetsFilterButtonShape } from '../widgets/filter-button.shape'; const DEFAULT_Z_INDEX = 1000; @@ -52,20 +51,11 @@ export class SheetsFilterRenderController extends RxDisposable implements IRende @Inject(SheetsFilterService) private readonly _sheetsFilterService: SheetsFilterService, @Inject(ThemeService) private readonly _themeService: ThemeService, @Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService, - @Inject(SheetsRenderService) private _sheetsRenderService: SheetsRenderService, @ICommandService private readonly _commandService: ICommandService, - @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, @ISheetSelectionRenderService private readonly _selectionRenderService: ISheetSelectionRenderService ) { super(); - [ - SetSheetsFilterRangeMutation, - SetSheetsFilterCriteriaMutation, - RemoveSheetsFilterMutation, - ReCalcSheetsFilterMutation, - ].forEach((m) => this.disposeWithMe(this._sheetsRenderService.registerSkeletonChangingMutations(m.id))); - this._initRenderer(); } @@ -100,24 +90,17 @@ export class SheetsFilterRenderController extends RxDisposable implements IRende takeUntil(this.dispose$) ).subscribe((renderParams) => { this._disposeRendering(); - - // If there's no filter range, we don't need to render anything. if (!renderParams || !renderParams.range) { return; } - this._renderRange(renderParams.unitId, renderParams.range, renderParams.skeleton); + this._renderRange(renderParams.range, renderParams.skeleton); this._renderButtons(renderParams as Required); }); } - private _renderRange(unitId: string, range: IRange, skeleton: SpreadsheetSkeleton): void { - const renderer = this._renderManagerService.getRenderById(unitId); - if (!renderer) { - return; - } - - const { scene } = renderer; + private _renderRange(range: IRange, skeleton: SpreadsheetSkeleton): void { + const { scene } = this._context; const { rangeWithCoord, style } = this._selectionRenderService.attachSelectionWithCoord({ range, primary: null, @@ -138,15 +121,10 @@ export class SheetsFilterRenderController extends RxDisposable implements IRende private _renderButtons(params: Required): void { const { range, filterModel, unitId, skeleton, worksheetId } = params; - const currentRenderer = this._renderManagerService.getRenderById(unitId); - if (!currentRenderer) { - return; - } - - const { scene } = currentRenderer; + const { scene } = this._context; // Push cell contents to leave space for the filter buttons. - this._interceptCellContent(params.range); + this._interceptCellContent(unitId, worksheetId, params.range); // Create filter button shapes. const { startColumn, endColumn, startRow } = range; @@ -185,12 +163,18 @@ export class SheetsFilterRenderController extends RxDisposable implements IRende scene.makeDirty(); } - private _interceptCellContent(range: IRange): void { + private _interceptCellContent(workbookId: string, worksheetId: string, range: IRange): void { const { startRow, startColumn, endColumn } = range; this._buttonRenderDisposable = this._sheetInterceptorService.intercept(INTERCEPTOR_POINT.CELL_CONTENT, { handler: (cell, pos, next) => { - const { row, col } = pos; - if (row !== startRow || col < startColumn || col > endColumn) { + const { row, col, unitId, subUnitId } = pos; + if ( + unitId !== workbookId || + subUnitId !== worksheetId || + row !== startRow || + col < startColumn || + col > endColumn + ) { return next(cell); } diff --git a/packages/sheets-filter/src/controllers/sheets-filter.controller.ts b/packages/sheets-filter/src/controllers/sheets-filter.controller.ts index 1ebe0a221e..fd68828c5b 100644 --- a/packages/sheets-filter/src/controllers/sheets-filter.controller.ts +++ b/packages/sheets-filter/src/controllers/sheets-filter.controller.ts @@ -691,7 +691,7 @@ export class SheetsFilterController extends Disposable { }; } - private _commandExecutedListener() { + private _commandExecutedListener(): void { this.disposeWithMe(this._commandService.onCommandExecuted((command: ICommandInfo, options) => { const { unitId, subUnitId } = command.params as unknown as ISheetCommandSharedParams || {}; diff --git a/packages/sheets-filter/src/services/sheet-filter.service.ts b/packages/sheets-filter/src/services/sheet-filter.service.ts index 8b649edd68..49f1359070 100644 --- a/packages/sheets-filter/src/services/sheet-filter.service.ts +++ b/packages/sheets-filter/src/services/sheet-filter.service.ts @@ -118,11 +118,11 @@ export class SheetsFilterService extends Disposable { return false; } - setFilterErrorMsg(content: string) { + setFilterErrorMsg(content: string): void { this._errorMsg$.next(content); } - private _updateActiveFilterModel() { + private _updateActiveFilterModel(): void { let workbook: Nullable; try { workbook = this._univerInstanceService.getCurrentUnitForType(UniverInstanceType.UNIVER_SHEET); @@ -147,7 +147,7 @@ export class SheetsFilterService extends Disposable { this._activeFilterModel$.next(filterModel); } - private _initActiveFilterModel() { + private _initActiveFilterModel(): void { this.disposeWithMe( merge( // source1: executing filter related mutations @@ -174,7 +174,7 @@ export class SheetsFilterService extends Disposable { return JSON.stringify(json); } - private _deserializeAutoFiltersForUnit(unitId: string, json: ISheetsFilterResource) { + private _deserializeAutoFiltersForUnit(unitId: string, json: ISheetsFilterResource): void { const workbook = this._univerInstanceService.getUniverSheetInstance(unitId)!; Object.keys(json).forEach((worksheetId: WorksheetID) => { const autoFilter = json[worksheetId]!; @@ -183,10 +183,10 @@ export class SheetsFilterService extends Disposable { }); } - private _initModel() { + private _initModel(): void { this._resourcesManagerService.registerPluginResource({ pluginName: SHEET_FILTER_SNAPSHOT_ID, - businesses: [2], + businesses: [UniverInstanceType.UNIVER_SHEET], toJson: (id) => this._serializeAutoFiltersForUnit(id), parseJson: (json) => JSON.parse(json), onLoad: (unitId, value) => { @@ -204,7 +204,7 @@ export class SheetsFilterService extends Disposable { }); } - private _cacheFilterModel(unitId: string, subUnitId: string, filterModel: FilterModel) { + private _cacheFilterModel(unitId: string, subUnitId: string, filterModel: FilterModel): void { if (!this._filterModels.has(unitId)) { this._filterModels.set(unitId, new Map()); } diff --git a/packages/sheets-numfmt/src/controllers/__tests__/test.util.ts b/packages/sheets-numfmt/src/controllers/__tests__/test.util.ts index a012dfbd57..ff68f8097a 100644 --- a/packages/sheets-numfmt/src/controllers/__tests__/test.util.ts +++ b/packages/sheets-numfmt/src/controllers/__tests__/test.util.ts @@ -45,6 +45,7 @@ const TEST_WORKBOOK_DATA_DEMO: () => IWorkbookData = () => ({ sheetOrder: [], styles: {}, }); + export const createTestBed = (dependencies?: Dependency[]) => { const univer = new Univer(); const injector = univer.__getInjector(); @@ -67,7 +68,10 @@ export const createTestBed = (dependencies?: Dependency[]) => { injector.add([SheetInterceptorService]); injector.add([SheetSkeletonManagerService]); injector.add([LexerTreeBuilder]); + dependencies?.forEach((d) => injector.add(d)); + + injector.get(SheetInterceptorService); } } diff --git a/packages/sheets-ui/src/commands/commands/__tests__/create-selection-command-test-bed.ts b/packages/sheets-ui/src/commands/commands/__tests__/create-selection-command-test-bed.ts index b803844df3..d0fd87b465 100644 --- a/packages/sheets-ui/src/commands/commands/__tests__/create-selection-command-test-bed.ts +++ b/packages/sheets-ui/src/commands/commands/__tests__/create-selection-command-test-bed.ts @@ -29,11 +29,13 @@ import { } from '../set-frozen.command'; import { ExpandSelectionCommand, MoveSelectionCommand, SelectAllCommand } from '../set-selection.command'; import { SheetSkeletonManagerService } from '../../../services/sheet-skeleton-manager.service'; +import { SelectAllService } from '../../../services/select-all/select-all.service'; import { createCommandTestBed } from './create-command-test-bed'; export function createSelectionCommandTestBed(workbookData?: IWorkbookData) { const { univer, get, sheet } = createCommandTestBed(workbookData || SIMPLE_SELECTION_WORKBOOK_DATA, [ [ShortcutExperienceService], + [SelectAllService], ]); const commandService = get(ICommandService); diff --git a/packages/sheets-ui/src/commands/commands/__tests__/set-selections.command.spec.ts b/packages/sheets-ui/src/commands/commands/__tests__/set-selections.command.spec.ts index 5acfc05094..d65cd7364b 100644 --- a/packages/sheets-ui/src/commands/commands/__tests__/set-selections.command.spec.ts +++ b/packages/sheets-ui/src/commands/commands/__tests__/set-selections.command.spec.ts @@ -171,7 +171,7 @@ describe('Test commands used for change selections', () => { univer = null; } - function prepareTestBed(snapshot?: IWorkbookData) { + function prepareSelectionsTestBed(snapshot?: IWorkbookData) { const testBed = createSelectionCommandTestBed(snapshot); univer = testBed.univer; get = testBed.get; @@ -183,7 +183,7 @@ describe('Test commands used for change selections', () => { afterEach(disposeTestBed); describe('Simple movement to next cell', () => { - beforeEach(() => prepareTestBed()); + beforeEach(() => prepareSelectionsTestBed()); it('Should move selection with command', async () => { selectTopLeft(); @@ -258,7 +258,7 @@ describe('Test commands used for change selections', () => { }); describe('Move cell to/through merged cells', () => { - beforeEach(() => prepareTestBed(SELECTION_WITH_MERGED_CELLS_DATA)); + beforeEach(() => prepareSelectionsTestBed(SELECTION_WITH_MERGED_CELLS_DATA)); /** * A1 | B1 | C1 @@ -303,7 +303,7 @@ describe('Test commands used for change selections', () => { }); describe('Move to next cell that has value (skip cell)', () => { - beforeEach(() => prepareTestBed(SELECTION_WITH_EMPTY_CELLS_DATA)); + beforeEach(() => prepareSelectionsTestBed(SELECTION_WITH_EMPTY_CELLS_DATA)); it('Works on move', async () => { selectTopLeft(); @@ -362,7 +362,7 @@ describe('Test commands used for change selections', () => { }); describe('Expand to next selection or shrink to previous selection', () => { - beforeEach(() => prepareTestBed(SELECTION_WITH_EMPTY_CELLS_DATA)); + beforeEach(() => prepareSelectionsTestBed(SELECTION_WITH_EMPTY_CELLS_DATA)); it('Works on expand', async () => { selectTopLeft(); @@ -440,7 +440,7 @@ describe('Test commands used for change selections', () => { * When A1:C1 is selected and B2 is the primary cell, shrink should only shrink to one side. */ describe('Shrink edge case', () => { - beforeEach(() => prepareTestBed(SELECTION_WITH_MERGED_CELLS_DATA)); + beforeEach(() => prepareSelectionsTestBed(SELECTION_WITH_MERGED_CELLS_DATA)); it('Should shrink on side when primary is in the middle of selections', async () => { select(0, 0, 1, 2, 1, 1, true, false); @@ -458,7 +458,7 @@ describe('Test commands used for change selections', () => { }); describe('Expand to next gap position or shrink to previous gap', () => { - beforeEach(() => prepareTestBed(SELECTION_WITH_EMPTY_CELLS_DATA)); + beforeEach(() => prepareSelectionsTestBed(SELECTION_WITH_EMPTY_CELLS_DATA)); it('Works on gap expand', async () => { selectTopLeft(); @@ -506,7 +506,7 @@ describe('Test commands used for change selections', () => { }); describe('Select all', () => { - beforeEach(() => prepareTestBed()); + beforeEach(() => prepareSelectionsTestBed()); it('Should first select all neighbor cells, and then the whole sheet', async () => { selectTopLeft(); diff --git a/packages/sheets-ui/src/commands/commands/set-selection.command.ts b/packages/sheets-ui/src/commands/commands/set-selection.command.ts index 1182b80c86..044d5f227d 100644 --- a/packages/sheets-ui/src/commands/commands/set-selection.command.ts +++ b/packages/sheets-ui/src/commands/commands/set-selection.command.ts @@ -26,6 +26,7 @@ import { import { KeyCode } from '@univerjs/ui'; import { ShortcutExperienceService } from '../../services/shortcut-experience.service'; +import { SelectAllService } from '../../services/select-all/select-all.service'; import { checkIfShrink, expandToNextCell, @@ -330,35 +331,25 @@ export interface ISelectAllCommandParams { loop?: boolean; } -let RANGES_STACK: IRange[] = []; - -let SELECTED_RANGE_WORKSHEET = ''; - /** * This command expand selection to all neighbor ranges. If there are no neighbor ranges. Select the whole sheet. */ export const SelectAllCommand: ICommand = { id: 'sheet.command.select-all', type: CommandType.COMMAND, - onDispose() { - RANGES_STACK = []; - SELECTED_RANGE_WORKSHEET = ''; - }, handler: async (accessor, params = { expandToGapFirst: true, loop: false }) => { - const selection = getSelectionsService(accessor).getCurrentLastSelection(); const target = getSheetCommandTarget(accessor.get(IUniverInstanceService)); if (!target) return false; + const selection = getSelectionsService(accessor).getCurrentLastSelection(); + if (!selection) return false; + + const selectAllService = accessor.get(SelectAllService); const { worksheet, unitId, subUnitId } = target; - - if (!selection || !worksheet) { - return false; - } - const id = `${unitId}|${subUnitId}`; - if (id !== SELECTED_RANGE_WORKSHEET) { - RANGES_STACK = []; - SELECTED_RANGE_WORKSHEET = id; + if (id !== selectAllService.selectedRangeWorksheet) { + selectAllService.rangesStack = []; + selectAllService.selectedRangeWorksheet = id; } const maxRow = worksheet.getMaxRows(); @@ -371,9 +362,9 @@ export const SelectAllCommand: ICommand = { range.startRow === 0 && range.startColumn === 0; - if (!RANGES_STACK.some((s) => Rectangle.equals(s, range))) { - RANGES_STACK = []; - RANGES_STACK.push(range); + if (!selectAllService.rangesStack.some((s) => Rectangle.equals(s, range))) { + selectAllService.rangesStack = []; + selectAllService.rangesStack.push(range); } let destRange: IRange; @@ -381,12 +372,12 @@ export const SelectAllCommand: ICommand = { // determined what kind of adjustment it should get if (isWholeSheetSelected) { if (loop) { - const currentSelectionIndex = RANGES_STACK.findIndex((s) => Rectangle.equals(s, range)); - if (currentSelectionIndex !== RANGES_STACK.length - 1) { + const currentSelectionIndex = selectAllService.rangesStack.findIndex((s) => Rectangle.equals(s, range)); + if (currentSelectionIndex !== selectAllService.rangesStack.length - 1) { return false; } - destRange = RANGES_STACK[0]; + destRange = selectAllService.rangesStack[0]; } else { return false; } @@ -400,8 +391,8 @@ export const SelectAllCommand: ICommand = { destRange = expandToWholeSheet(worksheet); } - if (!RANGES_STACK.some((s) => Rectangle.equals(s, destRange))) { - RANGES_STACK.push(destRange); + if (!selectAllService.rangesStack.some((s) => Rectangle.equals(s, destRange))) { + selectAllService.rangesStack.push(destRange); } return accessor.get(ICommandService).executeCommand(SetSelectionsOperation.id, { diff --git a/packages/sheets-ui/src/controllers/permission/sheet-permission-render.controller.ts b/packages/sheets-ui/src/controllers/permission/sheet-permission-render.controller.ts index b72edf82f0..0e8cbd5a03 100644 --- a/packages/sheets-ui/src/controllers/permission/sheet-permission-render.controller.ts +++ b/packages/sheets-ui/src/controllers/permission/sheet-permission-render.controller.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Disposable, Inject, Injector, IPermissionService, LifecycleStages, OnLifecycle } from '@univerjs/core'; +import { Disposable, Inject, IPermissionService } from '@univerjs/core'; import type { MenuConfig } from '@univerjs/ui'; -import { ComponentManager, IMenuService } from '@univerjs/ui'; +import { ComponentManager } from '@univerjs/ui'; import { CheckMarkSingle, DeleteSingle, LockSingle, ProtectSingle, WriteSingle } from '@univerjs/icons'; import { RangeProtectionRuleModel } from '@univerjs/sheets'; import type { IRenderContext, IRenderModule, Spreadsheet } from '@univerjs/engine-render'; @@ -32,25 +32,17 @@ export interface IUniverSheetsPermissionMenuConfig { menu: MenuConfig; } -export const DefaultSheetPermissionMenuConfig = {}; - -@OnLifecycle(LifecycleStages.Rendered, SheetPermissionRenderManagerController) export class SheetPermissionRenderManagerController extends Disposable { - constructor( - private readonly _config: Partial, - @IMenuService private _menuService: IMenuService, - @Inject(ComponentManager) private _componentManager: ComponentManager, - @Inject(Injector) private readonly _injector: Injector - ) { + constructor(@Inject(ComponentManager) private _componentManager: ComponentManager) { super(); this._init(); } - private _init() { + private _init(): void { this._initComponents(); } - private _initComponents() { + private _initComponents(): void { ([ [permissionMenuIconKey, ProtectSingle], [permissionDeleteIconKey, DeleteSingle], @@ -96,7 +88,7 @@ export class SheetPermissionRenderController extends Disposable implements IRend }); } - private _initRender() { + private _initRender(): void { const spreadsheetRender = this._context.mainComponent as Spreadsheet; if (spreadsheetRender) { if (!spreadsheetRender.getExtensionByKey(RANGE_PROTECTION_CAN_VIEW_RENDER_EXTENSION_KEY)) { @@ -108,7 +100,7 @@ export class SheetPermissionRenderController extends Disposable implements IRend } } - private _initSkeleton() { + private _initSkeleton(): void { const markDirtySkeleton = () => { this._sheetSkeletonManagerService.reCalculate(); this._context.mainComponent?.makeDirty(); diff --git a/packages/sheets-ui/src/controllers/sheet-ui.controller.ts b/packages/sheets-ui/src/controllers/sheet-ui.controller.ts index 10cfa81ba1..31e43502a5 100644 --- a/packages/sheets-ui/src/controllers/sheet-ui.controller.ts +++ b/packages/sheets-ui/src/controllers/sheet-ui.controller.ts @@ -231,7 +231,7 @@ import { sheetPermissionAddProtectContextMenuFactory, sheetPermissionChangeSheet export interface IUniverSheetsUIConfig { menu: MenuConfig; - + disableAutoFocus?: true; override?: DependencyOverride; } diff --git a/packages/sheets-ui/src/mobile-sheets-ui-plugin.ts b/packages/sheets-ui/src/mobile-sheets-ui-plugin.ts index a3caa29fdc..b11aa1280f 100644 --- a/packages/sheets-ui/src/mobile-sheets-ui-plugin.ts +++ b/packages/sheets-ui/src/mobile-sheets-ui-plugin.ts @@ -69,6 +69,7 @@ import { SheetContextMenuMobileRenderController } from './controllers/render-con import { SheetRenderController } from './controllers/render-controllers/sheet.render-controller'; import { MobileSheetsSelectionRenderService } from './services/selection/mobile-selection-render.service'; import { ISheetSelectionRenderService } from './services/selection/base-selection-render.service'; +import { SelectAllService } from './services/select-all/select-all.service'; /** * @ignore @@ -92,16 +93,12 @@ export class UniverSheetsMobileUIPlugin extends Plugin { override onStarting(): void { ( [ - // services [ShortcutExperienceService], [ISheetClipboardService, { useClass: SheetClipboardService }], [ISheetBarService, { useClass: SheetBarService }], [IFormatPainterService, { useClass: FormatPainterService }], [IAutoFillService, { useClass: AutoFillService }], [SheetPrintInterceptorService], - - // This would be removed from global injector and moved into RenderUnit provider. - // [SheetSkeletonManagerService], [ISheetSelectionRenderService, { useClass: MobileSheetsSelectionRenderService }], [IStatusBarService, { useClass: StatusBarService }], [IMarkSelectionService, { useClass: MarkSelectionService }], @@ -109,21 +106,16 @@ export class UniverSheetsMobileUIPlugin extends Plugin { [DragManagerService], [SheetCanvasPopManagerService], [CellAlertManagerService], + [SelectAllService], - // controllers [ActiveWorksheetController], [AutoHeightController], [SheetClipboardController], [SheetsRenderService], - [ - SheetUIMobileController, - { - useFactory: () => this._injector.createInstance(SheetUIMobileController, this._config), - }, - ], + [SheetUIMobileController, { + useFactory: (): SheetUIMobileController => this._injector.createInstance(SheetUIMobileController, this._config), + }], [StatusBarController], - // [AutoFillController], - // [FormatPainterController], // permission [SheetPermissionPanelModel], @@ -132,18 +124,9 @@ export class UniverSheetsMobileUIPlugin extends Plugin { [SheetPermissionInterceptorClipboardController], [SheetPermissionInterceptorBaseController], [SheetPermissionInitController], + [SheetPermissionRenderManagerController], ] as Dependency[] ).forEach((d) => this._injector.add(d)); - - this._injector.add( - [ - SheetPermissionRenderManagerController, - { - useFactory: () => this._injector.createInstance(SheetPermissionRenderManagerController, this._config), - }, - ] - - ); } override onReady(): void { @@ -177,8 +160,7 @@ export class UniverSheetsMobileUIPlugin extends Plugin { [HeaderMoveRenderController], [HeaderFreezeRenderController], // Caution: ScrollRenderController should placed before ZoomRenderController - // because ZoomRenderController would change scrollInfo in currentSkeletonBefore$ - // currentSkeletonBefore$ --> ZoomRenderController ---> viewport.resize --> setScrollInfo, but ScrollRenderController needs scrollInfo + // because ZoomRenderController would change scrollInfo in currentSkeletonBefore$. [MobileSheetsScrollRenderController], [SheetsZoomRenderController], [FormatPainterRenderController], @@ -199,12 +181,12 @@ export class UniverSheetsMobileUIPlugin extends Plugin { }); } - private _markSheetAsFocused() { + private _markSheetAsFocused(): void { const univerInstanceService = this._univerInstanceService; - univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_SHEET) + this.disposeWithMe(univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_SHEET) .pipe(filter((v) => !!v)) .subscribe((workbook) => { univerInstanceService.focusUnit(workbook!.getUnitId()); - }); + })); } } diff --git a/packages/sheets-ui/src/services/select-all/select-all.service.ts b/packages/sheets-ui/src/services/select-all/select-all.service.ts new file mode 100644 index 0000000000..fa27b7f97e --- /dev/null +++ b/packages/sheets-ui/src/services/select-all/select-all.service.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IRange } from '@univerjs/core'; +import { Disposable } from '@univerjs/core'; + +export class SelectAllService extends Disposable { + rangesStack: IRange[] = []; + selectedRangeWorksheet: string = ''; +} diff --git a/packages/sheets-ui/src/services/selection/base-selection-render.service.ts b/packages/sheets-ui/src/services/selection/base-selection-render.service.ts index 91351770b5..8c857bfa90 100644 --- a/packages/sheets-ui/src/services/selection/base-selection-render.service.ts +++ b/packages/sheets-ui/src/services/selection/base-selection-render.service.ts @@ -15,6 +15,7 @@ */ import type { + IDisposable, IFreeze, IInterceptor, Injector, @@ -150,7 +151,6 @@ export class BaseSelectionRenderService extends Disposable implements ISheetSele readonly selectionMoveStart$ = this._selectionMoveStart$.asObservable(); private _selectionMoving = false; - get selectionMoving(): boolean { return this._selectionMoving; } @@ -159,6 +159,8 @@ export class BaseSelectionRenderService extends Disposable implements ISheetSele readonly interceptor = new InterceptorManager({ RANGE_MOVE_PERMISSION_CHECK, RANGE_FILL_PERMISSION_CHECK }); + private _escapeShortcutDisposable: Nullable = null; + constructor( protected readonly _injector: Injector, protected readonly _themeService: ThemeService, @@ -375,8 +377,10 @@ export class BaseSelectionRenderService extends Disposable implements ISheetSele endSelection(): void { this._clearUpdatingListeners(); this._selectionMoveEnd$.next(this.getSelectionDataWithStyle()); + // when selection mouse up, enable the short cut service - this._shortcutService.setDisable(false); + this._escapeShortcutDisposable?.dispose(); + this._escapeShortcutDisposable = null; } /** @@ -520,11 +524,12 @@ export class BaseSelectionRenderService extends Disposable implements ISheetSele this._setupPointerMoveListener(viewportMain, activeSelectionControl!, rangeType, scrollTimerType, viewportPosX, viewportPosY); - this._shortcutService.setDisable(true); + this._escapeShortcutDisposable = this._shortcutService.forceEscape(); this._scenePointerUpSub = scene.onPointerUp$.subscribeEvent(() => { this._clearUpdatingListeners(); this._selectionMoveEnd$.next(this.getSelectionDataWithStyle()); - this._shortcutService.setDisable(false); + this._escapeShortcutDisposable?.dispose(); + this._escapeShortcutDisposable = null; }); } diff --git a/packages/sheets-ui/src/services/selection/mobile-selection-render.service.ts b/packages/sheets-ui/src/services/selection/mobile-selection-render.service.ts index 0721a5633d..28c1bab5a1 100644 --- a/packages/sheets-ui/src/services/selection/mobile-selection-render.service.ts +++ b/packages/sheets-ui/src/services/selection/mobile-selection-render.service.ts @@ -505,12 +505,14 @@ export class MobileSheetsSelectionRenderService extends BaseSelectionRenderServi scene.getTransformer()?.clearSelectedObjects(); - //#region pointermove + // #region pointermove const relativeCoords = scene.getRelativeToViewportCoord(Vector2.FromArray([evt.offsetX, evt.offsetY])); this._setupPointerMoveListener(viewportMain, activeSelectionControl!, rangeType, scrollTimerType, relativeCoords.x, relativeCoords.y); - //#endregion + // #endregion - //#region pointerup + // #region pointerup + // when selection mouse down, disable the short cut service + const disposableShortcut = this._shortcutService.forceEscape(); this._scenePointerUpSub = scene.onPointerUp$.subscribeEvent((_evt: IPointerEvent | IMouseEvent) => { this.endSelection(); this._expandingSelection = false; @@ -518,12 +520,9 @@ export class MobileSheetsSelectionRenderService extends BaseSelectionRenderServi this._selectionMoveEnd$.next(this.getSelectionDataWithStyle()); // when selection mouse up, enable the short cut service - this._shortcutService.setDisable(false); + disposableShortcut.dispose(); }); - //#endregion - - // when selection mouse down, disable the short cut service - this._shortcutService.setDisable(true); + // #endregion } private _changeCurrCellWhenControlPointerDown(): ISelectionCellWithMergeInfo { diff --git a/packages/sheets-ui/src/sheets-ui-plugin.ts b/packages/sheets-ui/src/sheets-ui-plugin.ts index 1ae58ca682..b0f67f405e 100644 --- a/packages/sheets-ui/src/sheets-ui-plugin.ts +++ b/packages/sheets-ui/src/sheets-ui-plugin.ts @@ -82,12 +82,14 @@ import { SheetsDefinedNameController } from './controllers/defined-name/defined- import { MoveRangeRenderController } from './controllers/move-range.controller'; import { ISheetSelectionRenderService } from './services/selection/base-selection-render.service'; import { SheetScrollManagerService } from './services/scroll-manager.service'; +import { SelectAllService } from './services/select-all/select-all.service'; @DependentOn(UniverSheetsPlugin) export class UniverSheetsUIPlugin extends Plugin { static override pluginName = 'SHEET_UI_PLUGIN'; static override type = UniverInstanceType.UNIVER_SHEET; + /** @ignore */ constructor( private readonly _config: Partial = {}, @Inject(Injector) override readonly _injector: Injector, @@ -101,7 +103,6 @@ export class UniverSheetsUIPlugin extends Plugin { override onStarting(): void { mergeOverrideWithDependencies([ - // services [ShortcutExperienceService], [IEditorBridgeService, { useClass: EditorBridgeService }], [ISheetClipboardService, { useClass: SheetClipboardService }], @@ -111,15 +112,13 @@ export class UniverSheetsUIPlugin extends Plugin { [IFormulaEditorManagerService, { useClass: FormulaEditorManagerService }], [IAutoFillService, { useClass: AutoFillService }], [SheetPrintInterceptorService], - - // This would be removed from global injector and moved into RenderUnit provider. - // [SheetSkeletonManagerService], [IStatusBarService, { useClass: StatusBarService }], [IMarkSelectionService, { useClass: MarkSelectionService }], [HoverManagerService], [DragManagerService], [SheetCanvasPopManagerService], [CellAlertManagerService], + [SelectAllService], // controllers [ActiveWorksheetController], @@ -127,12 +126,9 @@ export class UniverSheetsUIPlugin extends Plugin { [FormulaEditorController], [SheetClipboardController], [SheetsRenderService], - [ - SheetUIController, - { - useFactory: () => this._injector.createInstance(SheetUIController, this._config), - }, - ], + [SheetUIController, { + useFactory: (): SheetUIController => this._injector.createInstance(SheetUIController, this._config), + }], [StatusBarController], [AutoFillController], [FormatPainterController], @@ -145,27 +141,22 @@ export class UniverSheetsUIPlugin extends Plugin { [SheetPermissionInterceptorClipboardController], [SheetPermissionInterceptorBaseController], [SheetPermissionInitController], - // [MoveRangeController], + [SheetPermissionRenderManagerController], ] as Dependency[], this._config.override).forEach((d) => this._injector.add(d)); - - this._injector.add( - [ - SheetPermissionRenderManagerController, - { - useFactory: () => this._injector.createInstance(SheetPermissionRenderManagerController, this._config), - }, - ] - - ); } override onReady(): void { - this._markSheetAsFocused(); + if (!this._config.disableAutoFocus) { + this._markSheetAsFocused(); + } + this._registerRenderBasics(); } override onRendered(): void { this._registerRenderModules(); + + this._injector.get(SheetPermissionRenderManagerController); } private _registerRenderBasics(): void { @@ -185,8 +176,7 @@ export class UniverSheetsUIPlugin extends Plugin { [HeaderMoveRenderController], [HeaderUnhideRenderController], [HeaderResizeRenderController], - // Caution: ScrollRenderController should placed before ZoomRenderController - // because ZoomRenderController ---> viewport.resize --> setScrollInfo, but ScrollRenderController needs scrollInfo + // Caution: ScrollRenderController should placed before ZoomRenderController. [SheetScrollManagerService], [SheetsScrollRenderController], [HeaderFreezeRenderController], @@ -217,10 +207,10 @@ export class UniverSheetsUIPlugin extends Plugin { }); } - private _markSheetAsFocused() { + private _markSheetAsFocused(): void { const univerInstanceService = this._univerInstanceService; - univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_SHEET) + this.disposeWithMe(univerInstanceService.getCurrentTypeOfUnit$(UniverInstanceType.UNIVER_SHEET) .pipe(filter((v) => !!v)) - .subscribe((workbook) => univerInstanceService.focusUnit(workbook!.getUnitId())); + .subscribe((workbook) => univerInstanceService.focusUnit(workbook!.getUnitId()))); } } diff --git a/packages/sheets/src/services/permission/range-permission/range-protection.ref-range.ts b/packages/sheets/src/services/permission/range-permission/range-protection.ref-range.ts index e50bf07fb5..e70a732383 100644 --- a/packages/sheets/src/services/permission/range-permission/range-protection.ref-range.ts +++ b/packages/sheets/src/services/permission/range-permission/range-protection.ref-range.ts @@ -21,7 +21,6 @@ import { RangeProtectionRuleModel } from '../../../model/range-protection-rule.m import { RangeProtectionRenderModel } from '../../../model/range-protection-render.model'; import type { ISetRangeProtectionMutationParams } from '../../../commands/mutations/set-range-protection.mutation'; import { SetRangeProtectionMutation } from '../../../commands/mutations/set-range-protection.mutation'; - import { InsertColMutation, InsertRowMutation } from '../../../commands/mutations/insert-row-col.mutation'; import { RemoveColMutation, RemoveRowMutation } from '../../../commands/mutations/remove-row-col.mutation'; import { type IMoveRowsMutationParams, MoveColsMutation, MoveRowsMutation } from '../../../commands/mutations/move-rows-cols.mutation'; @@ -32,7 +31,6 @@ import type { IInsertColCommandParams, IInsertRowCommandParams } from '../../../ import { InsertColCommand, InsertRowCommand } from '../../../commands/commands/insert-row-col.command'; import type { IRemoveRowColCommandParams } from '../../../commands/commands/remove-row-col.command'; import type { EffectRefRangeParams } from '../../../services/ref-range/type'; - import type { IMoveColsCommandParams, IMoveRowsCommandParams, diff --git a/packages/sheets/src/services/permission/range-permission/range-protection.service.ts b/packages/sheets/src/services/permission/range-permission/range-protection.service.ts index cd3422cdca..7082dbd6e7 100644 --- a/packages/sheets/src/services/permission/range-permission/range-protection.service.ts +++ b/packages/sheets/src/services/permission/range-permission/range-protection.service.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Disposable, Inject, IPermissionService, IResourceManagerService, LifecycleStages, OnLifecycle } from '@univerjs/core'; +import { Disposable, Inject, IPermissionService, IResourceManagerService } from '@univerjs/core'; import { UnitAction, UnitObject, UniverType } from '@univerjs/protocol'; import type { IObjectModel } from '../../../model/range-protection-rule.model'; @@ -24,7 +24,6 @@ import { getAllRangePermissionPoint } from './util'; const PLUGIN_NAME = 'SHEET_RANGE_PROTECTION_PLUGIN'; -@OnLifecycle(LifecycleStages.Starting, RangeProtectionService) export class RangeProtectionService extends Disposable { constructor( @Inject(RangeProtectionRuleModel) private _selectionProtectionRuleModel: RangeProtectionRuleModel, @@ -37,7 +36,7 @@ export class RangeProtectionService extends Disposable { this._initRuleChange(); } - private _initRuleChange() { + private _initRuleChange(): void { this.disposeWithMe( this._selectionProtectionRuleModel.ruleChange$.subscribe((info) => { switch (info.type) { diff --git a/packages/sheets/src/services/sheet-interceptor/__tests__/create-core-test-bed.ts b/packages/sheets/src/services/sheet-interceptor/__tests__/create-core-test-bed.ts index 4b3715d9a0..893097ddb3 100644 --- a/packages/sheets/src/services/sheet-interceptor/__tests__/create-core-test-bed.ts +++ b/packages/sheets/src/services/sheet-interceptor/__tests__/create-core-test-bed.ts @@ -16,6 +16,7 @@ import type { Dependency, IWorkbookData } from '@univerjs/core'; import { ILogService, Inject, Injector, IUniverInstanceService, LocaleType, LogLevel, Plugin, Univer, UniverInstanceType } from '@univerjs/core'; +import { SheetInterceptorService } from '../sheet-interceptor.service'; const TEST_WORKBOOK_DATA: IWorkbookData = { id: 'test', @@ -41,7 +42,7 @@ const TEST_WORKBOOK_DATA: IWorkbookData = { styles: {}, }; -export function createCoreTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]) { +export function createSheetTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]) { const univer = new Univer(); const injector = univer.__getInjector(); const get = injector.get.bind(injector); @@ -59,6 +60,8 @@ export function createCoreTestBed(workbookData?: IWorkbookData, dependencies?: D override onStarting(): void { dependencies?.forEach((d) => this._injector.add(d)); + + this._injector.get(SheetInterceptorService); } } diff --git a/packages/sheets/src/services/sheet-interceptor/__tests__/sheet-interceptor.service.spec.ts b/packages/sheets/src/services/sheet-interceptor/__tests__/sheet-interceptor.service.spec.ts index e245d43c60..6def963042 100644 --- a/packages/sheets/src/services/sheet-interceptor/__tests__/sheet-interceptor.service.spec.ts +++ b/packages/sheets/src/services/sheet-interceptor/__tests__/sheet-interceptor.service.spec.ts @@ -21,7 +21,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { INTERCEPTOR_POINT } from '../interceptor-const'; import { SheetInterceptorService } from '../sheet-interceptor.service'; import type { ISheetLocation } from '../utils/interceptor'; -import { createCoreTestBed } from './create-core-test-bed'; +import { createSheetTestBed } from './create-core-test-bed'; describe('Test SheetInterceptorService', () => { let univer: Univer; @@ -30,7 +30,7 @@ describe('Test SheetInterceptorService', () => { const numberIntercept = createInterceptorKey('numberIntercept'); beforeEach(() => { - const testBed = createCoreTestBed(undefined, [[SheetInterceptorService]]); + const testBed = createSheetTestBed(undefined, [[SheetInterceptorService]]); univer = testBed.univer; get = testBed.get; }); diff --git a/packages/sheets/src/services/sheet-interceptor/sheet-interceptor.service.ts b/packages/sheets/src/services/sheet-interceptor/sheet-interceptor.service.ts index 3c1f346c03..379a384ff2 100644 --- a/packages/sheets/src/services/sheet-interceptor/sheet-interceptor.service.ts +++ b/packages/sheets/src/services/sheet-interceptor/sheet-interceptor.service.ts @@ -30,8 +30,6 @@ import { Disposable, DisposableCollection, IUniverInstanceService, - LifecycleStages, - OnLifecycle, remove, toDisposable, UniverInstanceType, @@ -62,10 +60,7 @@ export interface IRangeInterceptors { /** * This class expose methods for sheet features to inject code to sheet underlying logic. - * - * It would inject Workbook & Worksheet. */ -@OnLifecycle(LifecycleStages.Starting, SheetInterceptorService) export class SheetInterceptorService extends Disposable { private _interceptorsByName: Map>> = new Map(); private _commandInterceptors: ICommandInterceptor[] = []; @@ -76,6 +71,7 @@ export class SheetInterceptorService extends Disposable { private readonly _workbookDisposables = new Map(); private readonly _worksheetDisposables = new Map(); + /** @ignore */ constructor(@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService) { super(); @@ -188,7 +184,7 @@ export class SheetInterceptorService extends Disposable { }; } - intercept>(name: T, interceptor: T) { + intercept>(name: T, interceptor: T): IDisposable { const key = name as unknown as string; if (!this._interceptorsByName.has(key)) { this._interceptorsByName.set(key, []); @@ -258,12 +254,12 @@ export class SheetInterceptorService extends Disposable { // We should intercept all instantiated worksheet and should subscribe to // worksheet creation event to intercept newly created worksheet. workbook.getSheets().forEach((worksheet) => interceptViewModel(worksheet)); - disposables.add(toDisposable(workbook.sheetCreated$.subscribe((worksheet) => interceptViewModel(worksheet)))); + disposables.add(workbook.sheetCreated$.subscribe((worksheet) => interceptViewModel(worksheet))); // Dispose all underlying interceptors when workbook is disposed. disposables.add(toDisposable(() => workbook.getSheets().forEach((worksheet) => this._disposeSheetInterceptor(unitId, worksheet)))); // Dispose interceptor when a worksheet is destroyed. - disposables.add(toDisposable(workbook.sheetDisposed$.subscribe((worksheet) => this._disposeSheetInterceptor(unitId, worksheet)))); + disposables.add(workbook.sheetDisposed$.subscribe((worksheet) => this._disposeSheetInterceptor(unitId, worksheet))); this._workbookDisposables.set(unitId, disposables); } diff --git a/packages/sheets/src/sheets-plugin.ts b/packages/sheets/src/sheets-plugin.ts index f2b6b6c494..237286748e 100644 --- a/packages/sheets/src/sheets-plugin.ts +++ b/packages/sheets/src/sheets-plugin.ts @@ -15,7 +15,7 @@ */ import type { Dependency, DependencyOverride } from '@univerjs/core'; -import { DependentOn, ICommandService, IConfigService, Inject, Injector, LocaleService, mergeOverrideWithDependencies, Plugin, UniverInstanceType } from '@univerjs/core'; +import { DependentOn, IConfigService, Inject, Injector, mergeOverrideWithDependencies, Plugin, UniverInstanceType } from '@univerjs/core'; import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'; import { BasicWorksheetController } from './controllers/basic-worksheet.controller'; @@ -52,9 +52,6 @@ export interface IUniverSheetsConfig { onlyRegisterFormulaRelatedMutations?: true; } -/** - * The main sheet base, construct the sheet container and layout, mount the rendering engine - */ @DependentOn(UniverFormulaEnginePlugin) export class UniverSheetsPlugin extends Plugin { static override pluginName = PLUGIN_NAME; @@ -62,9 +59,7 @@ export class UniverSheetsPlugin extends Plugin { constructor( private _config: IUniverSheetsConfig | undefined, - @ICommandService private readonly _commandService: ICommandService, @IConfigService private readonly _configService: IConfigService, - @Inject(LocaleService) private readonly _localeService: LocaleService, @Inject(Injector) override readonly _injector: Injector ) { super(); @@ -82,7 +77,7 @@ export class UniverSheetsPlugin extends Plugin { } } - private _initDependencies(sheetInjector: Injector) { + private _initDependencies(sheetInjector: Injector): void { const dependencies: Dependency[] = [ // services [BorderStyleManagerService], @@ -111,14 +106,14 @@ export class UniverSheetsPlugin extends Plugin { ]; if (!this._config?.notExecuteFormula) { - // Should execute formula. - dependencies.push( - [CalculateResultApplyController] - ); + dependencies.push([CalculateResultApplyController]); } mergeOverrideWithDependencies(dependencies, this._config?.override).forEach((d) => { sheetInjector.add(d); }); + + this._injector.get(SheetInterceptorService); + this._injector.get(RangeProtectionService); } } diff --git a/packages/ui/src/services/shortcut/shortcut.service.ts b/packages/ui/src/services/shortcut/shortcut.service.ts index 2df91d8edb..7825f68153 100644 --- a/packages/ui/src/services/shortcut/shortcut.service.ts +++ b/packages/ui/src/services/shortcut/shortcut.service.ts @@ -25,49 +25,110 @@ import { IPlatformService } from '../platform/platform.service'; import type { KeyCode } from './keycode'; import { KeyCodeToChar, MetaKeys } from './keycode'; +/** + * A shortcut item that could be registered to the {@link IShortcutService}. + */ export interface IShortcutItem

{ - /** This should reuse the corresponding command's id. */ + /** Id of the shortcut item. It should reuse the corresponding {@link ICommand}'s id. */ id: string; + /** Description of the shortcut. */ description?: string; /** If two shortcuts have the same binding, the one with higher priority would be check first. */ priority?: number; - /** A callback that will be triggered to examine if the shortcut should be invoked. */ + + /** + * A callback that will be triggered to examine if the shortcut should be invoked. The `{@link IContextService}` + * would be passed to the callback. + */ preconditions?: (contextService: IContextService) => boolean; - /** A command can be bound to several bindings, with different static parameters perhaps. */ + /** + * The binding of the shortcut. It should be a combination of {@link KeyCode} and {@link MetaKeys}. + * + * A command can be bound to several bindings, with different static parameters perhaps. + * + * @example { binding: KeyCode.ENTER | MetaKeys.ALT } + */ binding: KeyCode | number; + /** + * The binding of the shortcut for macOS. If the property is not specified, the default binding would be used. + */ mac?: number; + /** + * The binding of the shortcut for Windows. If the property is not specified, the default binding would be used. + */ win?: number; + /** + * The binding of the shortcut for Linux. If the property is not specified, the default binding would be used. + */ linux?: number; /** * The group of the menu item should belong to. The shortcut item would be rendered in the * panel if this is set. + * + * @example { group: '10_global-shortcut' } */ group?: string; - /** Static parameters of this shortcut. Would be send to `CommandService.executeCommand`. */ + /** + * Static parameters of this shortcut. Would be send to {@link ICommandService.executeCommand} as the second + * parameter when the corresponding command is executed. + * + * You can define multi shortcuts with the same command id but different static parameters. + */ staticParameters?: P; } +/** + * The dependency injection identifier of the {@link IShortcutService}. + */ +export const IShortcutService = createIdentifier('ui.shortcut.service'); +/** + * The interface of the shortcut service. + */ export interface IShortcutService { + /** + * An observable that emits when the shortcuts are changed. + */ shortcutChanged$: Observable; + /** + * Make the shortcut service ignore all keyboard events. + * @returns {IDisposable} a disposable that could be used to cancel the force escaping. + */ forceEscape(): IDisposable; - // registerCaptureSelector(selector: string): IDisposable; - // registerEscapeSelector(selector: string): IDisposable; + /** + * Register a shortcut item to the shortcut service. + * @param {IShortcutItem} shortcut - the shortcut item to be registered. + * @returns {IDisposable} a disposable that could be used to unregister the shortcut. + */ registerShortcut(shortcut: IShortcutItem): IDisposable; + /** + * Get the display string of the shortcut item. + * @param shortcut - the shortcut item to get the display string. + * @returns {string} the display string of the shortcut. For example `Ctrl+Enter`. + */ getShortcutDisplay(shortcut: IShortcutItem): string; + /** + * Get the display string of the shortcut of the command. + * @param id the id of the command to get the shortcut display. + * @returns {string | null} the display string of the shortcut. For example `Ctrl+Enter`. + */ getShortcutDisplayOfCommand(id: string): string | null; + /** + * Get all the shortcuts registered in the shortcut service. + * @returns {IShortcutItem[]} all the shortcuts registered in the shortcut service. + */ getAllShortcuts(): IShortcutItem[]; - setDisable(disable: boolean): void; } -export const IShortcutService = createIdentifier('univer.shortcut'); - +/** + * @ignore + */ export class ShortcutService extends Disposable implements IShortcutService { private readonly _shortCutMapping = new Map>(); private readonly _commandIDMapping = new Map>(); @@ -77,8 +138,6 @@ export class ShortcutService extends Disposable implements IShortcutService { private _forceEscaped = false; - private _disable = false; - constructor( @ICommandService private readonly _commandService: ICommandService, @IPlatformService private readonly _platformService: IPlatformService, @@ -97,10 +156,6 @@ export class ShortcutService extends Disposable implements IShortcutService { ); } - setDisable(disable: boolean): void { - this._disable = disable; - } - getAllShortcuts(): IShortcutItem[] { return Array.from(this._shortCutMapping.values()) .map((v) => Array.from(v.values())) @@ -179,14 +234,8 @@ export class ShortcutService extends Disposable implements IShortcutService { // Should get the container element of the Univer instance and see if // the event target is a descendant of the container element. // Also we should check through escape list and force catching list. - // if the target is not focused on the univer instance we should ingore the keyboard event - if (this._forceEscaped) { - return; - } - - if (this._disable) { - return; - } + // if the target is not focused on the univer instance we should ignore the keyboard event + if (this._forceEscaped) return; if ( this._layoutService && @@ -201,7 +250,7 @@ export class ShortcutService extends Disposable implements IShortcutService { } } - private _dispatch(e: KeyboardEvent) { + private _dispatch(e: KeyboardEvent): boolean { const binding = this._deriveBindingFromEvent(e); if (binding === null) { return false;