feat(sheets-ui): add ctrl enter shortcut (#4956)

Co-authored-by: GitHub Actions <actions@github.com>
This commit is contained in:
Wenzhao Hu
2025-04-09 10:50:49 +08:00
committed by GitHub
co-authored by GitHub Actions
parent 205389258a
commit 741547a9f4
13 changed files with 182 additions and 52 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 78 KiB

@@ -85,7 +85,6 @@ export class DebuggerController extends Disposable {
private _initCustomComponents(): void {
const componentManager = this._componentManager;
// this.disposeWithMe(componentManager.register(TEST_EDITOR_CONTAINER_COMPONENT, TestEditorContainer));
this.disposeWithMe(componentManager.register('VueI18nIcon', VueI18nIcon, {
framework: 'vue3',
}));
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { type MenuSchemaType, RibbonOthersGroup } from '@univerjs/ui';
import type { MenuSchemaType } from '@univerjs/ui';
import { RibbonOthersGroup } from '@univerjs/ui';
import { CreateFloatDomCommand } from '../commands/commands/float-dom.command';
import { CreateEmptySheetCommand, DisposeCurrentUnitCommand, DisposeUniverCommand, LoadSheetSnapshotCommand } from '../commands/commands/unit.command';
import { ShowCellContentOperation } from '../commands/operations/cell.operation';
@@ -37,7 +37,7 @@ import { SidebarOperation } from '../commands/operations/sidebar.operation';
import { ThemeOperation } from '../commands/operations/theme.operation';
import { UNIVER_WATERMARK_MENU } from './watermark.menu.controller';
export function LocaleMenuItemFactory(accessor: IAccessor): IMenuSelectorItem {
export function LocaleMenuItemFactory(): IMenuSelectorItem {
return {
id: LocaleOperation.id,
icon: 'VueI18nIcon',
@@ -72,7 +72,7 @@ export function LocaleMenuItemFactory(accessor: IAccessor): IMenuSelectorItem {
};
}
export function DarkModeMenuItemFactory(accessor: IAccessor): IMenuSelectorItem {
export function DarkModeMenuItemFactory(): IMenuSelectorItem {
return {
id: DarkModeOperation.id,
title: '🌓',
@@ -95,7 +95,7 @@ export function DarkModeMenuItemFactory(accessor: IAccessor): IMenuSelectorItem
};
}
export function ThemeMenuItemFactory(accessor: IAccessor): IMenuSelectorItem {
export function ThemeMenuItemFactory(): IMenuSelectorItem {
return {
id: ThemeOperation.id,
title: 'Theme',
@@ -114,7 +114,7 @@ export function ThemeMenuItemFactory(accessor: IAccessor): IMenuSelectorItem {
};
}
export function NotificationMenuItemFactory(accessor: IAccessor): IMenuSelectorItem {
export function NotificationMenuItemFactory(): IMenuSelectorItem {
return {
id: NotificationOperation.id,
title: 'Notification',
@@ -141,7 +141,7 @@ export function NotificationMenuItemFactory(accessor: IAccessor): IMenuSelectorI
};
}
export function DialogMenuItemFactory(accessor: IAccessor): IMenuSelectorItem {
export function DialogMenuItemFactory(): IMenuSelectorItem {
return {
id: DialogOperation.id,
title: 'Dialog',
@@ -160,7 +160,7 @@ export function DialogMenuItemFactory(accessor: IAccessor): IMenuSelectorItem {
};
}
export function ConfirmMenuItemFactory(accessor: IAccessor): IMenuSelectorItem {
export function ConfirmMenuItemFactory(): IMenuSelectorItem {
return {
id: ConfirmOperation.id,
title: 'Confirm',
@@ -322,7 +322,7 @@ export function CreateFloatDOMMenuItemFactory(): IMenuButtonItem {
};
}
export function ShowCellContentMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
export function ShowCellContentMenuItemFactory(): IMenuButtonItem {
return {
id: ShowCellContentOperation.id,
type: MenuItemType.BUTTON,
@@ -354,7 +354,7 @@ export function ChangeUserMenuItemFactory(): IMenuSelectorItem {
};
}
export function WatermarkMenuItemFactory(accessor: IAccessor): IMenuButtonItem {
export function WatermarkMenuItemFactory(): IMenuButtonItem {
return {
id: OpenWatermarkPanelOperation.id,
title: 'univer-watermark.title',
@@ -0,0 +1,43 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { Disposable, ICommandService } from '@univerjs/core';
import { IMenuManagerService } from '@univerjs/ui';
import { menuSchema, SwitchUnitOperation } from './multi-units.menu';
/**
* This controller helps to debug multi units inside a single Univer
* instance.
*/
export class MultiUnitsController extends Disposable {
constructor(
@ICommandService private readonly _commandService: ICommandService,
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService
) {
super();
this._initializeCommand();
this._initializeMenu();
}
private _initializeCommand(): void {
this.disposeWithMe(this._commandService.registerCommand(SwitchUnitOperation));
}
private _initializeMenu(): void {
this._menuManagerService.mergeMenu(menuSchema);
}
}
@@ -0,0 +1,62 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 { IAccessor, IOperation, Workbook } from '@univerjs/core';
import type { IMenuSelectorItem, IValueOption, MenuSchemaType } from '@univerjs/ui';
import { CommandType, IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
import { MenuItemType, RibbonStartGroup } from '@univerjs/ui';
import { merge, Observable, shareReplay } from 'rxjs';
export const SwitchUnitOperation: IOperation<{ value: string }> = {
id: 'debugger.operation.switch-unit',
type: CommandType.OPERATION,
handler: (accessor, params) => {
const univerInstanceService = accessor.get(IUniverInstanceService);
if (!univerInstanceService.getUnit(params.value)) return false;
univerInstanceService.setCurrentUnitForType(params.value);
return true;
},
};
function SwitchUnitMenuFactory(accessor: IAccessor): IMenuSelectorItem {
const instanceService = accessor.get(IUniverInstanceService);
const selections$ = new Observable<Array<IValueOption>>((observer) => {
function emit() {
const sheets = instanceService.getAllUnitsForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
const options = sheets.map((sheet) => ({ label: sheet.getName() || sheet.getUnitId(), value: sheet.getUnitId() }));
observer.next(options);
}
merge(instanceService.unitAdded$, instanceService.unitDisposed$).subscribe(() => emit());
emit();
}).pipe(shareReplay(1));
return {
id: SwitchUnitOperation.id,
title: 'Switch Unit', // TODO: i18n,
type: MenuItemType.SELECTOR,
selections: selections$,
};
}
export const menuSchema: MenuSchemaType = {
[RibbonStartGroup.FORMAT]: {
[SwitchUnitOperation.id]: {
order: -1,
menuItemFactory: SwitchUnitMenuFactory,
},
},
};
+17 -9
View File
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import type { Dependency } from '@univerjs/core';
import type { IUniverDebuggerConfig } from './controllers/config.schema';
import { IConfigService, Inject, Injector, merge, Plugin } from '@univerjs/core';
import { IConfigService, Inject, Injector, merge, Plugin, registerDependencies, touchDependencies } from '@univerjs/core';
import { DEBUGGER_PLUGIN_CONFIG_KEY, defaultPluginConfig } from './controllers/config.schema';
import { DarkModeController } from './controllers/dark-mode.controller';
import { DebuggerController } from './controllers/debugger.controller';
import { E2EController } from './controllers/e2e/e2e.controller';
import { MultiUnitsController } from './controllers/multi-units/multi-units.controller';
import { PerformanceMonitorController } from './controllers/performance-monitor.controller';
import { UniverWatermarkMenuController } from './controllers/watermark.menu.controller';
@@ -49,25 +49,33 @@ export class UniverDebuggerPlugin extends Plugin {
}
override onStarting(): void {
([
registerDependencies(this._injector, [
[PerformanceMonitorController],
[DarkModeController],
[DebuggerController],
[MultiUnitsController],
[E2EController],
[UniverWatermarkMenuController],
] as Dependency[]).forEach((d) => this._injector.add(d));
]);
this._injector.get(E2EController);
touchDependencies(this._injector, [
[E2EController],
]);
}
override onReady(): void {
this._injector.get(DebuggerController);
touchDependencies(this._injector, [
[DebuggerController],
[MultiUnitsController],
]);
}
override onRendered(): void {
this._injector.get(DarkModeController);
this._injector.get(PerformanceMonitorController);
this._injector.get(UniverWatermarkMenuController);
touchDependencies(this._injector, [
[DarkModeController],
[PerformanceMonitorController],
[UniverWatermarkMenuController],
]);
}
getDebuggerController() {
@@ -19,7 +19,7 @@
import type { DocumentDataModel, ICellData, ICommandInfo, IDisposable, IDocumentBody, IDocumentData, IDocumentStyle, IMutationInfo, IStyleData, Nullable, Styles, Workbook } from '@univerjs/core';
import type { IRichTextEditingMutationParams } from '@univerjs/docs';
import type { IRenderContext, IRenderModule } from '@univerjs/engine-render';
import type { MutationsAffectRange, WorkbookSelectionModel } from '@univerjs/sheets';
import type { ISetRangeValuesCommandParams, MutationsAffectRange, WorkbookSelectionModel } from '@univerjs/sheets';
import type { IEditorBridgeServiceVisibleParam } from '../../services/editor-bridge.service';
import {
@@ -64,7 +64,7 @@ import {
} from '@univerjs/engine-render';
import { adjustRangeOnMutation, COMMAND_LISTENER_SKELETON_CHANGE, InsertColMutation, InsertRowMutation, MoveColsMutation, MoveRowsMutation, REF_SELECTIONS_ENABLED, RemoveColMutation, RemoveRowMutation, SetRangeValuesCommand, SetSelectionsOperation, SetWorksheetActivateCommand, SetWorksheetActiveOperation, SheetInterceptorService, SheetsSelectionsService } from '@univerjs/sheets';
import { KeyCode } from '@univerjs/ui';
import { KeyCode, MetaKeys } from '@univerjs/ui';
import { distinctUntilChanged, filter } from 'rxjs';
import { getEditorObject } from '../../basics/editor/get-editor-object';
import { MoveSelectionCommand, MoveSelectionEnterAndTabCommand } from '../../commands/commands/set-selection.command';
@@ -114,7 +114,7 @@ export class EditingRenderController extends Disposable implements IRenderModule
@ICommandService private readonly _commandService: ICommandService,
@Inject(LocaleService) protected readonly _localService: LocaleService,
@IEditorService private readonly _editorService: IEditorService,
@Inject(SheetCellEditorResizeService) private readonly _sheetCellEditorResizeService: SheetCellEditorResizeService,
@Inject(SheetCellEditorResizeService) private readonly _sheetCellEditorResizeService: SheetCellEditorResizeService, // RenderModule
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@Inject(SheetInterceptorService) private readonly _sheetInterceptorService: SheetInterceptorService
) {
@@ -183,12 +183,12 @@ export class EditingRenderController extends Disposable implements IRenderModule
this.disposeWithMe(
this._editorBridgeService.visible$
.pipe(distinctUntilChanged((prev, curr) => prev.visible === curr.visible))
.subscribe((param) => {
if ((param.unitId === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY || param.unitId === this._context.unitId) && param.visible) {
.subscribe((params) => {
if ((params.unitId === DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY || params.unitId === this._context.unitId) && params.visible) {
this._isUnitEditing = true;
this._handleEditorVisible(param);
this._handleEditorVisible(params);
} else if (this._isUnitEditing) {
this._handleEditorInvisible(param);
this._handleEditorInvisible(params);
this._isUnitEditing = false;
}
})
@@ -558,13 +558,13 @@ export class EditingRenderController extends Disposable implements IRenderModule
}
const isEmpty = snapshot?.body?.dataStream.length === 2;
const isCellImage = editCellState.documentLayoutObject.documentModel ? this._isCellImageData(editCellState.documentLayoutObject.documentModel.getSnapshot()) : false;
const isCellImage = editCellState.documentLayoutObject.documentModel
? this._isCellImageData(editCellState.documentLayoutObject.documentModel.getSnapshot())
: false;
if (snapshot && !(isEmpty && isCellImage)) {
const res = await this._submitCellData(snapshot);
// if the submit was rejected, don't move selection
if (res === false) {
return;
}
const res = await this._submitEdit(snapshot, keycode === (MetaKeys.CTRL_COMMAND | KeyCode.ENTER));
if (res === false) return; // if the submit was rejected, don't move selection
}
// moveSelection need to put behind of SetRangeValuesCommand, fix https://github.com/dream-num/univer/issues/1155
@@ -581,10 +581,10 @@ export class EditingRenderController extends Disposable implements IRenderModule
}
submitCellData(documentDataModel: DocumentDataModel) {
return this._submitCellData(documentDataModel.getSnapshot());
return this._submitEdit(documentDataModel.getSnapshot());
}
private async _submitCellData(snapshot: IDocumentData) {
private async _submitEdit(snapshot: IDocumentData, wholeSelection = false) {
const editCellState = this._editorBridgeService.getEditCellState();
if (editCellState == null) {
return true;
@@ -618,21 +618,25 @@ export class EditingRenderController extends Disposable implements IRenderModule
return true;
}
const finalCell = this._sheetInterceptorService.onWriteCell(workbook, worksheet, row, column, cellData);
const finalCell = this._sheetInterceptorService.onWriteCell(workbook, worksheet, row, column, cellData) as ICellData;
if (finalCell === worksheet.getCellRaw(row, column)) {
return true;
}
const redoUndoId = generateRandomId(6);
const res = this._commandService.syncExecuteCommand(SetRangeValuesCommand.id, {
subUnitId: sheetId,
unitId,
range: {
const range = wholeSelection
? this._workbookSelections.getCurrentLastSelection()?.range
: {
startRow: row,
startColumn: column,
endRow: row,
endColumn: column,
},
};
const redoUndoId = generateRandomId(6);
const res = this._commandService.syncExecuteCommand<ISetRangeValuesCommandParams>(SetRangeValuesCommand.id, {
subUnitId: sheetId,
unitId,
range,
value: finalCell,
redoUndoId,
});
@@ -112,6 +112,7 @@ import { SHEETS_UI_PLUGIN_CONFIG_KEY } from './config.schema';
import { menuSchema } from './menu.schema';
import {
EditorBreakLineShortcut,
EditorCursorCtrlEnterShortcut,
EditorCursorEnterShortcut,
EditorCursorEscShortcut,
EditorCursorTabShortcut,
@@ -338,6 +339,7 @@ export class SheetUIController extends Disposable {
EditorDeleteLeftShortcut,
EditorDeleteLeftShortcutInActive,
EditorCursorEscShortcut,
EditorCursorCtrlEnterShortcut,
// operation shortcuts
SetRowHiddenShortcutItem,
@@ -123,6 +123,17 @@ export const EditorCursorEscShortcut: IShortcutItem = {
},
};
export const EditorCursorCtrlEnterShortcut: IShortcutItem = {
id: SetCellEditVisibleOperation.id,
binding: KeyCode.ENTER | MetaKeys.CTRL_COMMAND,
preconditions: (contextService) => whenEditorDidNotInputFormulaActivated(contextService),
staticParameters: {
visible: false,
eventType: DeviceInputEventType.Keyboard,
keycode: KeyCode.ENTER | MetaKeys.CTRL_COMMAND,
},
};
export const EditorBreakLineShortcut: IShortcutItem = {
id: BreakLineCommand.id,
description: 'shortcut.sheet.break-line',
@@ -113,7 +113,7 @@ export class EditorBridgeService extends Disposable implements IEditorBridgeServ
private _editorIsDirty: boolean = false;
private _isDisabled: boolean = false;
private _visible: IEditorBridgeServiceVisibleParam = {
private _visibleParams: IEditorBridgeServiceVisibleParam = {
visible: false,
eventType: DeviceInputEventType.Dblclick,
unitId: '',
@@ -134,11 +134,11 @@ export class EditorBridgeService extends Disposable implements IEditorBridgeServ
switchMap((editCellState) => this._currentEditCellLayout$.pipe(map((layout) => (editCellState && layout ? { ...editCellState, ...layout } : null))))
);
private readonly _visible$ = new BehaviorSubject<IEditorBridgeServiceVisibleParam>(this._visible);
readonly visible$ = this._visible$.asObservable();
private readonly _visibleParams$ = new BehaviorSubject<IEditorBridgeServiceVisibleParam>(this._visibleParams);
readonly visible$ = this._visibleParams$.asObservable();
private readonly _afterVisible$ = new BehaviorSubject<IEditorBridgeServiceVisibleParam>(this._visible);
readonly afterVisible$ = this._afterVisible$.asObservable();
private readonly _afterVisibleParams$ = new BehaviorSubject<IEditorBridgeServiceVisibleParam>(this._visibleParams);
readonly afterVisible$ = this._afterVisibleParams$.asObservable();
private readonly _forceKeepVisible$ = new BehaviorSubject(false);
readonly forceKeepVisible$ = this._forceKeepVisible$.asObservable();
@@ -427,20 +427,20 @@ export class EditorBridgeService extends Disposable implements IEditorBridgeServ
return this._editorUnitId;
}
changeVisible(param: IEditorBridgeServiceVisibleParam) {
this._visible = param;
changeVisible(params: IEditorBridgeServiceVisibleParam) {
this._visibleParams = params;
// Reset the dirty status when the editor is visible.
if (param.visible) {
if (params.visible) {
this._editorIsDirty = false;
}
this._visible$.next(this._visible);
this._afterVisible$.next(this._visible);
this._visibleParams$.next(this._visibleParams);
this._afterVisibleParams$.next(this._visibleParams);
}
isVisible() {
return this._visible;
return this._visibleParams;
}
enableForceKeepVisible(): void {