feat(ui): export shortcut facade API (#4324)

This commit is contained in:
Wenzhao Hu
2024-12-18 17:38:40 +08:00
committed by GitHub
parent a446c82e1f
commit ceff4eb305
7 changed files with 308 additions and 22 deletions
+1 -1
View File
@@ -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';
+232
View File
@@ -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();
}
}
});
}
@@ -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,
+1 -1
View File
@@ -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<K extends keyof WindowEventMap>(
type: K,
+46
View File
@@ -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<object> | undefined {
return this._shortcutService.dispatch(e);
}
}
+9
View File
@@ -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<boolean> {
return this._commandService.executeCommand(CopyCommand.id);
}
@@ -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<object> | 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<object> | 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 {