diff --git a/examples/src/sheets/main.ts b/examples/src/sheets/main.ts index fc2e784115..191a77b6c3 100644 --- a/examples/src/sheets/main.ts +++ b/examples/src/sheets/main.ts @@ -53,8 +53,8 @@ import '@univerjs/sheets-filter/facade'; import '@univerjs/sheets-formula/facade'; import '@univerjs/sheets-numfmt/facade'; import '@univerjs/sheets-hyper-link-ui/facade'; -import '@univerjs/sheets-thread-comment/facade'; +import '@univerjs/sheets-thread-comment/facade'; import '@univerjs/sheets-conditional-formatting/facade'; import '../global.css'; diff --git a/examples/uniscripts/vim.js b/examples/uniscripts/vim.js new file mode 100644 index 0000000000..f1a5954945 --- /dev/null +++ b/examples/uniscripts/vim.js @@ -0,0 +1,232 @@ +/** + * 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. + */ + +export function onLoad(univerAPI) { + let mode = 'normal'; + + let label; + function setMode(m) { + mode = m; + if (!label) { + label = document.createElement('div'); + label.style.position = 'fixed'; + label.style.bottom = '36px'; + label.style.left = '0'; + label.style.borderTopRightRadius = '4px'; + label.style.backgroundColor = 'rgb(255, 255, 255)'; + label.style.zIndex = '1000000'; + label.style.paddingLeft = '8px'; + label.style.width = '120px'; + label.style.fontSize = '14px'; + label.style.fontFamily = 'monospace'; + + document.body.append(label); + } + + label.textContent = `Vim: ${mode}`; + switch (mode) { + case 'normal': + label.style.color = 'rgb(115, 141, 242)'; + break; + case 'insert': + label.style.color = 'rgb(223, 93, 0)'; + break; + case 'visual': + label.style.color = 'rgb(139, 187, 17)'; + break; + } + } + + setMode('normal'); + + univerAPI.getShortcut().disableShortcut(); + + univerAPI.onCommandExecuted((command) => { + if (command.id.startsWith('sheet.operation.set-cell-edit-visible')) { + if (command.params.visible) { + setMode('insert'); + } else { + setMode('normal'); + } + } else if (command.id === 'sheet.operation.set-selections') { + if (command.params.selections.length > 1) { + setMode('visual'); + return; + } + + const length = command.params.selections.length; + const range = command.params.selections[length - 1].range; + const primary = command.params.selections[length - 1].primary; + + if ( + range.startRow !== primary.startRow + || range.startColumn !== primary.startColumn + || range.endRow !== primary.endRow + || range.endColumn !== primary.endColumn + ) { + setMode('visual'); + } + } + }); + + window.addEventListener('keydown', (e) => { + if (mode === 'normal') { + if (e.key === 'i') { + setMode('insert'); + + univerAPI.getShortcut().enableShortcut(); + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (e.key === 'j') { + univerAPI.executeCommand('sheet.command.move-selection', { direction: 2 }); + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (e.key === 'k') { + univerAPI.executeCommand('sheet.command.move-selection', { direction: 0 }); + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (e.key === 'l') { + univerAPI.executeCommand('sheet.command.move-selection', { direction: 1 }); + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (e.key === 'h') { + univerAPI.executeCommand('sheet.command.move-selection', { direction: 3 }); + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (e.key === 'v') { + setMode('visual'); + + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (e.key === 'y') { + univerAPI.executeCommand('univer.command.copy'); + + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (e.key === 'd') { + univerAPI.executeCommand('univer.command.cut'); + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (e.key === 'p') { + univerAPI.executeCommand('univer.command.paste'); + setMode('normal'); + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (!univerAPI.getShortcut().dispatchShortcutEvent(e)) { + e.preventDefault(); + e.stopPropagation(); + } + } + + if (mode === 'insert' && e.key === 'Escape') { + setMode('normal'); + + univerAPI.getShortcut().disableShortcut(); + e.preventDefault(); + e.stopPropagation(); + } + + if (mode === 'visual') { + if (e.key === 'j') { + univerAPI.executeCommand('sheet.command.expand-selection', { direction: 2 }); + e.preventDefault(); + e.stopPropagation(); + } + + if (e.key === 'k') { + univerAPI.executeCommand('sheet.command.expand-selection', { direction: 0 }); + e.preventDefault(); + e.stopPropagation(); + } + + if (e.key === 'l') { + univerAPI.executeCommand('sheet.command.expand-selection', { direction: 1 }); + e.preventDefault(); + e.stopPropagation(); + } + + if (e.key === 'h') { + univerAPI.executeCommand('sheet.command.expand-selection', { direction: 3 }); + e.preventDefault(); + e.stopPropagation(); + } + + if (e.key === 'y') { + univerAPI.executeCommand('univer.command.copy'); + + setMode('normal'); + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (e.key === 'd') { + univerAPI.executeCommand('univer.command.cut'); + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (e.key === 'p') { + univerAPI.executeCommand('univer.command.paste'); + setMode('normal'); + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (e.key === 'Escape') { + setMode('normal'); + + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (!univerAPI.getShortcut().dispatchShortcutEvent(e)) { + e.preventDefault(); + e.stopPropagation(); + } + } + }); +} diff --git a/packages/sheets-ui/src/controllers/shortcuts/selection.shortcut.ts b/packages/sheets-ui/src/controllers/shortcuts/selection.shortcut.ts index 1253e7af6a..949b67c7f6 100644 --- a/packages/sheets-ui/src/controllers/shortcuts/selection.shortcut.ts +++ b/packages/sheets-ui/src/controllers/shortcuts/selection.shortcut.ts @@ -14,16 +14,15 @@ * limitations under the License. */ -import { Direction } from '@univerjs/core'; import type { IShortcutItem } from '@univerjs/ui'; -import { KeyCode, MetaKeys } from '@univerjs/ui'; - import type { IExpandSelectionCommandParams, IMoveSelectionCommandParams, IMoveSelectionEnterAndTabCommandParams, ISelectAllCommandParams, } from '../../commands/commands/set-selection.command'; +import { Direction } from '@univerjs/core'; +import { KeyCode, MetaKeys } from '@univerjs/ui'; import { ExpandSelectionCommand, JumpOver, diff --git a/packages/ui/src/common/lifecycle.ts b/packages/ui/src/common/lifecycle.ts index 04aa1395f0..2f1e17567b 100644 --- a/packages/ui/src/common/lifecycle.ts +++ b/packages/ui/src/common/lifecycle.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { toDisposable } from '@univerjs/core'; import type { IDisposable } from '@univerjs/core'; +import { toDisposable } from '@univerjs/core'; export function fromGlobalEvent( type: K, diff --git a/packages/ui/src/facade/f-shortcut.ts b/packages/ui/src/facade/f-shortcut.ts new file mode 100644 index 0000000000..927a3175be --- /dev/null +++ b/packages/ui/src/facade/f-shortcut.ts @@ -0,0 +1,46 @@ +/** + * 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 { IDisposable } from '@univerjs/core'; +import type { IShortcutItem } from '@univerjs/ui'; +import { FBase, Inject, Injector } from '@univerjs/core'; +import { IShortcutService } from '@univerjs/ui'; + +export class FShortcut extends FBase { + private _forceEscapeDisposable: IDisposable | null = null; + + constructor( + @Inject(Injector) protected readonly _injector: Injector, + @IShortcutService protected readonly _shortcutService: IShortcutService + ) { + super(); + } + + enableShortcut(): void { + this._forceEscapeDisposable?.dispose(); + this._forceEscapeDisposable = null; + } + + disableShortcut(): void { + if (!this._forceEscapeDisposable) { + this._forceEscapeDisposable = this._shortcutService.forceEscape(); + } + } + + dispatchShortcutEvent(e: KeyboardEvent): IShortcutItem | undefined { + return this._shortcutService.dispatch(e); + } +} diff --git a/packages/ui/src/facade/f-univer.ts b/packages/ui/src/facade/f-univer.ts index 3709f0dfa8..83484d1217 100644 --- a/packages/ui/src/facade/f-univer.ts +++ b/packages/ui/src/facade/f-univer.ts @@ -21,6 +21,7 @@ import type { IFacadeMenuItem, IFacadeSubmenuItem } from './f-menu-builder'; import { FUniver } from '@univerjs/core'; import { ComponentManager, CopyCommand, IDialogService, IMessageService, ISidebarService, PasteCommand } from '@univerjs/ui'; import { FMenu, FSubmenu } from './f-menu-builder'; +import { FShortcut } from './f-shortcut'; export interface IFUniverUIMixin { /** @@ -28,6 +29,10 @@ export interface IFUniverUIMixin { * @returns the [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object */ getURL(): URL; + /** + * Get the Shortcut handler to interact with Univer's shortcut functionalities. + */ + getShortcut(): FShortcut; /** * Copy the current selected content of the currently focused unit into your system clipboard. */ @@ -115,6 +120,10 @@ export class FUniverUIMixin extends FUniver implements IFUniverUIMixin { return new URL(window.location.href); } + override getShortcut(): FShortcut { + return this._injector.createInstance(FShortcut); + } + override copy(): Promise { return this._commandService.executeCommand(CopyCommand.id); } diff --git a/packages/ui/src/services/shortcut/shortcut.service.ts b/packages/ui/src/services/shortcut/shortcut.service.ts index 88a477aa8e..2d44c75ea7 100644 --- a/packages/ui/src/services/shortcut/shortcut.service.ts +++ b/packages/ui/src/services/shortcut/shortcut.service.ts @@ -100,6 +100,11 @@ export interface IShortcutService { */ forceEscape(): IDisposable; + /** + * Dispatch a keyboard event to the shortcut service and check if there is a shortcut that matches the event. + * @param e - the keyboard event to be dispatched. + */ + dispatch(e: KeyboardEvent): IShortcutItem | undefined; /** * Register a shortcut item to the shortcut service. * @param {IShortcutItem} shortcut - the shortcut item to be registered. @@ -235,6 +240,14 @@ export class ShortcutService extends Disposable implements IShortcutService { } private _resolveKeyboardEvent(e: KeyboardEvent): void { + const candidate = this.dispatch(e); + if (candidate) { + this._commandService.executeCommand(candidate.id, candidate.staticParameters); + e.preventDefault(); + } + } + + dispatch(e: KeyboardEvent): IShortcutItem | undefined { // 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. @@ -247,34 +260,21 @@ export class ShortcutService extends Disposable implements IShortcutService { ) { return; } - - const shouldPreventDefault = this._dispatch(e); - if (shouldPreventDefault) { - e.preventDefault(); - } - } - - private _dispatch(e: KeyboardEvent): boolean { const binding = this._deriveBindingFromEvent(e); if (binding === null) { - return false; + return undefined; } const shortcuts = this._shortCutMapping.get(binding); if (shortcuts === undefined) { - return false; + return undefined; } - const shouldTrigger = Array.from(shortcuts) + const candidateShortcut = Array.from(shortcuts) .sort((s1, s2) => (s2.priority ?? 0) - (s1.priority ?? 0)) .find((s) => s.preconditions?.(this._contextService) ?? true); - if (shouldTrigger) { - this._commandService.executeCommand(shouldTrigger.id, shouldTrigger.staticParameters); - return true; - } - - return false; + return candidateShortcut; } private _getBindingFromItem(item: IShortcutItem): number {