refactor(filter): refactor filter for demo (#3194)

This commit is contained in:
Wenzhao Hu
2024-08-27 20:28:38 +08:00
committed by GitHub
parent 9a57c5d56d
commit 04ede3fe50
27 changed files with 365 additions and 280 deletions
+4 -2
View File
@@ -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"
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ export default function getLazyPlugins(): Array<[PluginCtor<Plugin>] | [PluginCt
// ],
[UniverSheetsCrosshairHighlightPlugin],
[UniverSheetsFilterUIPlugin, { useRemoteFilterValuesGenerator: true }],
[UniverSheetsFilterUIPlugin, { useRemoteFilterValuesGenerator: false }],
[UniverSheetsFindReplacePlugin],
];
}
@@ -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<P extends object = object, R = boolean> {
/**
* ${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 `<namespace>.<type>.<command-name>`.
*
* @example { id: 'sheet.command.set-selection-frozen' }
*/
readonly id: string;
readonly type: CommandType;
handler(accessor: IAccessor, params?: P, options?: IExecutionOptions): Promise<R> | 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> | 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<P extends object = object, R = boolean> extends ICommand<P, R> {
/** 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<P extends object, R = boolean> extends ICommand<P, R> {
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<P extends object = object, R = boolean> extends ICommand<P, R> {
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<T extends object = object> {
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<T extends object = object> {
id: string;
type?: CommandType.MUTATION;
params: T;
}
/** This object represents an execution of a {@link CommandType.OPERATION} */
export interface IOperationInfo<T extends object = object> {
id: string;
type?: CommandType.OPERATION;
@@ -119,46 +168,66 @@ export interface IOperationInfo<T extends object = object> {
}
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<ICommandInfo>, options?: IExecutionOptions) => void;
/**
* The identifier of the command service.
*/
export const ICommandService = createIdentifier<ICommandService>('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<object, unknown>): IDisposable;
/**
* Register a command as a multi command.
* @param command The command to register as a multi command.
*/
registerMultipleCommand(command: ICommand<object, unknown>): 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<P extends object = object, R = boolean>(
id: string,
params?: P,
options?: IExecutionOptions
): Promise<R>;
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<P extends object = object, R = boolean>(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<ICommandService>('anywhere.command-service');
/**
* The registry of commands.
*/
export class CommandRegistry {
class CommandRegistry {
private readonly _commands = new Map<string, ICommand>();
private readonly _commandTypes = new Map<string, CommandType>();
@@ -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?.();
});
}
@@ -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();
@@ -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
));
}
}
@@ -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<ISheetsGenerateFilterValuesService>(
this._rpcChannelService!.requestChannel(SHEETS_GENERATE_FILTER_VALUES_SERVICE_NAME)
),
useFactory: (): ISheetsGenerateFilterValuesService =>
toModule<ISheetsGenerateFilterValuesService>(
this._rpcChannelService!.requestChannel(SHEETS_GENERATE_FILTER_VALUES_SERVICE_NAME)
),
}]);
}
}
@@ -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<ISheetsFilterRenderParams>);
});
}
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<ISheetsFilterRenderParams>): 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);
}
@@ -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 || {};
@@ -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<Workbook>;
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<ISheetsFilterResource>({
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());
}
@@ -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);
}
}
@@ -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);
@@ -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();
@@ -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<ISelectAllCommandParams> = {
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<ISelectAllCommandParams> = {
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<ISelectAllCommandParams> = {
// 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<ISelectAllCommandParams> = {
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, {
@@ -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<IUniverSheetsPermissionMenuConfig>,
@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();
@@ -231,7 +231,7 @@ import { sheetPermissionAddProtectContextMenuFactory, sheetPermissionChangeSheet
export interface IUniverSheetsUIConfig {
menu: MenuConfig;
disableAutoFocus?: true;
override?: DependencyOverride;
}
@@ -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$<Workbook>(UniverInstanceType.UNIVER_SHEET)
this.disposeWithMe(univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET)
.pipe(filter((v) => !!v))
.subscribe((workbook) => {
univerInstanceService.focusUnit(workbook!.getUnitId());
});
}));
}
}
@@ -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 = '';
}
@@ -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<IDisposable> = 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;
});
}
@@ -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 {
+17 -27
View File
@@ -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<IUniverSheetsUIConfig> = {},
@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$<Workbook>(UniverInstanceType.UNIVER_SHEET)
this.disposeWithMe(univerInstanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET)
.pipe(filter((v) => !!v))
.subscribe((workbook) => univerInstanceService.focusUnit(workbook!.getUnitId()));
.subscribe((workbook) => univerInstanceService.focusUnit(workbook!.getUnitId())));
}
}
@@ -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,
@@ -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) {
@@ -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);
}
}
@@ -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<number, { step: number }>('numberIntercept');
beforeEach(() => {
const testBed = createCoreTestBed(undefined, [[SheetInterceptorService]]);
const testBed = createSheetTestBed(undefined, [[SheetInterceptorService]]);
univer = testBed.univer;
get = testBed.get;
});
@@ -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<string, Array<IInterceptor<unknown, unknown>>> = new Map();
private _commandInterceptors: ICommandInterceptor[] = [];
@@ -76,6 +71,7 @@ export class SheetInterceptorService extends Disposable {
private readonly _workbookDisposables = new Map<string, IDisposable>();
private readonly _worksheetDisposables = new Map<string, IDisposable>();
/** @ignore */
constructor(@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService) {
super();
@@ -188,7 +184,7 @@ export class SheetInterceptorService extends Disposable {
};
}
intercept<T extends IInterceptor<any, any>>(name: T, interceptor: T) {
intercept<T extends IInterceptor<any, any>>(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);
}
+6 -11
View File
@@ -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);
}
}
@@ -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<P extends object = object> {
/** 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<IShortcutService>('ui.shortcut.service');
/**
* The interface of the shortcut service.
*/
export interface IShortcutService {
/**
* An observable that emits when the shortcuts are changed.
*/
shortcutChanged$: Observable<void>;
/**
* 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<IShortcutService>('univer.shortcut');
/**
* @ignore
*/
export class ShortcutService extends Disposable implements IShortcutService {
private readonly _shortCutMapping = new Map<number, Set<IShortcutItem>>();
private readonly _commandIDMapping = new Map<string, Set<IShortcutItem>>();
@@ -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;