feat(editor): 优化快捷键操作

This commit is contained in:
roymondchen 2023-06-12 17:28:56 +08:00
parent 567b054b32
commit 06d289aff3
5 changed files with 279 additions and 147 deletions

View File

@ -96,6 +96,7 @@ import depService from './services/dep';
import editorService from './services/editor'; import editorService from './services/editor';
import eventsService from './services/events'; import eventsService from './services/events';
import historyService from './services/history'; import historyService from './services/history';
import keybindingService from './services/keybinding';
import propsService from './services/props'; import propsService from './services/props';
import storageService from './services/storage'; import storageService from './services/storage';
import uiService from './services/ui'; import uiService from './services/ui';
@ -130,6 +131,7 @@ export default defineComponent({
codeBlockService, codeBlockService,
depService, depService,
dataSourceService, dataSourceService,
keybindingService,
}; };
initServiceEvents(props, emit, services); initServiceEvents(props, emit, services);

View File

@ -10,6 +10,7 @@
ref="tree" ref="tree"
node-key="id" node-key="id"
empty-text="页面空荡荡的" empty-text="页面空荡荡的"
tabindex="-1"
draggable draggable
:default-expanded-keys="expandedKeys" :default-expanded-keys="expandedKeys"
:default-checked-keys="checkedKeys" :default-checked-keys="checkedKeys"
@ -75,6 +76,7 @@ defineProps<{
const throttleTime = 150; const throttleTime = 150;
const services = inject<Services>('services'); const services = inject<Services>('services');
const editorService = services?.editorService; const editorService = services?.editorService;
const keybindingService = services?.keybindingService;
const tree = ref<InstanceType<typeof TMagicTree>>(); const tree = ref<InstanceType<typeof TMagicTree>>();
const menu = ref<InstanceType<typeof LayerMenu>>(); const menu = ref<InstanceType<typeof LayerMenu>>();
@ -246,12 +248,22 @@ const windowBlurHandler = () => {
isCtrlKeyDown.value = false; isCtrlKeyDown.value = false;
}; };
const globalKeyupHandler = () => {
if (document.activeElement !== tree.value?.$el) {
isCtrlKeyDown.value = false;
}
};
let keycon: KeyController; let keycon: KeyController;
onMounted(() => { onMounted(() => {
editorService?.on('remove', editorServiceRemoveHandler); editorService?.on('remove', editorServiceRemoveHandler);
keycon = new KeyController(); if (tree.value?.$el) {
// ctrlctrl
keybindingService?.on('keyup', globalKeyupHandler);
keycon = new KeyController(tree.value.$el);
const isMac = /mac os x/.test(navigator.userAgent.toLowerCase()); const isMac = /mac os x/.test(navigator.userAgent.toLowerCase());
const ctrl = isMac ? 'meta' : 'ctrl'; const ctrl = isMac ? 'meta' : 'ctrl';
@ -268,6 +280,9 @@ onMounted(() => {
isCtrlKeyDown.value = false; isCtrlKeyDown.value = false;
}); });
tree.value.$el.addEventListener('blur', windowBlurHandler);
}
globalThis.addEventListener('blur', windowBlurHandler); globalThis.addEventListener('blur', windowBlurHandler);
}); });
@ -275,7 +290,9 @@ onUnmounted(() => {
keycon.destroy(); keycon.destroy();
editorService?.off('remove', editorServiceRemoveHandler); editorService?.off('remove', editorServiceRemoveHandler);
keybindingService?.off('keyup', globalKeyupHandler);
globalThis.removeEventListener('blur', windowBlurHandler); globalThis.removeEventListener('blur', windowBlurHandler);
tree.value?.$el.removeEventListener('blur', windowBlurHandler);
}); });
// //

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="m-editor-workspace" tabindex="-1" ref="workspace"> <div class="m-editor-workspace" tabindex="-1">
<Breadcrumb></Breadcrumb> <Breadcrumb></Breadcrumb>
<slot name="stage"> <slot name="stage">
@ -16,10 +16,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, onUnmounted, ref } from 'vue'; import { computed, inject } from 'vue';
import KeyController from 'keycon';
import { isPage } from '@tmagic/utils';
import type { MenuButton, MenuComponent, Services } from '@editor/type'; import type { MenuButton, MenuComponent, Services } from '@editor/type';
@ -36,132 +33,6 @@ defineProps<{
}>(); }>();
const services = inject<Services>('services'); const services = inject<Services>('services');
const workspace = ref<HTMLDivElement>();
const nodes = computed(() => services?.editorService.get('nodes'));
const page = computed(() => services?.editorService.get('page')); const page = computed(() => services?.editorService.get('page'));
const mouseenterHandler = () => {
workspace.value?.focus();
};
const mouseleaveHandler = () => {
workspace.value?.blur();
};
let keycon: KeyController;
onMounted(() => {
workspace.value?.addEventListener('mouseenter', mouseenterHandler);
workspace.value?.addEventListener('mouseleave', mouseleaveHandler);
keycon = new KeyController(workspace.value);
const isMac = /mac os x/.test(navigator.userAgent.toLowerCase());
const ctrl = isMac ? 'meta' : 'ctrl';
keycon
.keyup('delete', (e) => {
e.inputEvent.preventDefault();
if (!nodes.value || isPage(nodes.value[0])) return;
services?.editorService.remove(nodes.value);
})
.keyup('backspace', (e) => {
e.inputEvent.preventDefault();
if (!nodes.value || isPage(nodes.value[0])) return;
services?.editorService.remove(nodes.value);
})
.keydown([ctrl, 'c'], (e) => {
e.inputEvent.preventDefault();
nodes.value && services?.editorService.copy(nodes.value);
})
.keydown([ctrl, 'v'], (e) => {
e.inputEvent.preventDefault();
nodes.value && services?.editorService.paste({ offsetX: 10, offsetY: 10 });
})
.keydown([ctrl, 'x'], (e) => {
e.inputEvent.preventDefault();
if (!nodes.value || isPage(nodes.value[0])) return;
services?.editorService.copy(nodes.value);
services?.editorService.remove(nodes.value);
})
.keydown([ctrl, 'z'], (e) => {
e.inputEvent.preventDefault();
services?.editorService.undo();
})
.keydown([ctrl, 'shift', 'z'], (e) => {
e.inputEvent.preventDefault();
services?.editorService.redo();
})
.keydown('up', (e) => {
e.inputEvent.preventDefault();
services?.editorService.move(0, -1);
})
.keydown('down', (e) => {
e.inputEvent.preventDefault();
services?.editorService.move(0, 1);
})
.keydown('left', (e) => {
e.inputEvent.preventDefault();
services?.editorService.move(-1, 0);
})
.keydown('right', (e) => {
e.inputEvent.preventDefault();
services?.editorService.move(1, 0);
})
.keydown([ctrl, 'up'], (e) => {
e.inputEvent.preventDefault();
services?.editorService.move(0, -10);
})
.keydown([ctrl, 'down'], (e) => {
e.inputEvent.preventDefault();
services?.editorService.move(0, 10);
})
.keydown([ctrl, 'left'], (e) => {
e.inputEvent.preventDefault();
services?.editorService.move(-10, 0);
})
.keydown([ctrl, 'right'], (e) => {
e.inputEvent.preventDefault();
services?.editorService.move(10, 0);
})
.keydown('tab', (e) => {
e.inputEvent.preventDefault();
services?.editorService.selectNextNode();
})
.keydown([ctrl, 'tab'], (e) => {
e.inputEvent.preventDefault();
services?.editorService.selectNextPage();
})
.keydown([ctrl, '='], (e) => {
e.inputEvent.preventDefault();
services?.uiService.zoom(0.1);
})
.keydown([ctrl, 'numpadplus'], (e) => {
e.inputEvent.preventDefault();
services?.uiService.zoom(0.1);
})
.keydown([ctrl, '-'], (e) => {
e.inputEvent.preventDefault();
services?.uiService.zoom(-0.1);
})
.keydown([ctrl, 'numpad-'], (e) => {
e.inputEvent.preventDefault();
services?.uiService.zoom(-0.1);
})
.keydown([ctrl, '0'], async (e) => {
e.inputEvent.preventDefault();
services?.uiService.set('zoom', await services.uiService.calcZoom());
})
.keydown([ctrl, '1'], (e) => {
e.inputEvent.preventDefault();
services?.uiService.set('zoom', 1);
});
});
onUnmounted(() => {
workspace.value?.removeEventListener('mouseenter', mouseenterHandler);
workspace.value?.removeEventListener('mouseleave', mouseleaveHandler);
keycon.destroy();
});
</script> </script>

View File

@ -0,0 +1,240 @@
import KeyController from 'keycon';
import { isPage } from '@tmagic/utils';
import BaseService from './BaseService';
import editorService from './editor';
import uiService from './ui';
class Keybinding extends BaseService {
private keycon = new KeyController();
private ctrlKey = /mac os x/.test(navigator.userAgent.toLowerCase()) ? 'meta' : 'ctrl';
constructor() {
super();
this.keycon
.keyup((e) => {
this.emit('keyup', e.inputEvent);
})
.keyup('delete', (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
this.removeNode();
})
.keyup('backspace', (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
this.removeNode();
})
.keydown([this.ctrlKey, 'c'], (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
const nodes = editorService.get('nodes');
nodes && editorService.copy(nodes);
})
.keydown([this.ctrlKey, 'v'], (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
const nodes = editorService.get('nodes');
nodes && editorService.paste({ offsetX: 10, offsetY: 10 });
})
.keydown([this.ctrlKey, 'x'], (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
const nodes = editorService.get('nodes');
if (!nodes || isPage(nodes[0])) return;
editorService.copy(nodes);
editorService.remove(nodes);
})
.keydown([this.ctrlKey, 'z'], (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
editorService.undo();
})
.keydown([this.ctrlKey, 'shift', 'z'], (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
editorService.redo();
})
.keydown('up', (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
editorService.move(0, -1);
})
.keydown('down', (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
editorService.move(0, 1);
})
.keydown('left', (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
editorService.move(-1, 0);
})
.keydown('right', (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
editorService.move(1, 0);
})
.keydown([this.ctrlKey, 'up'], (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
editorService.move(0, -10);
})
.keydown([this.ctrlKey, 'down'], (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
editorService.move(0, 10);
})
.keydown([this.ctrlKey, 'left'], (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
editorService.move(-10, 0);
})
.keydown([this.ctrlKey, 'right'], (e) => {
e.inputEvent.preventDefault();
editorService.move(10, 0);
})
.keydown('tab', (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
editorService.selectNextNode();
})
.keydown([this.ctrlKey, 'tab'], (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
editorService.selectNextPage();
})
.keydown([this.ctrlKey, '='], (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
uiService.zoom(0.1);
})
.keydown([this.ctrlKey, 'numpadplus'], (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
uiService.zoom(0.1);
})
.keydown([this.ctrlKey, '-'], (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
uiService.zoom(-0.1);
})
.keydown([this.ctrlKey, 'numpad-'], (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
uiService.zoom(-0.1);
})
.keydown([this.ctrlKey, '0'], async (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
uiService.set('zoom', await uiService.calcZoom());
})
.keydown([this.ctrlKey, '1'], (e) => {
if (this.isDisabledKeyEvent(e.inputEvent.target)) {
return;
}
e.inputEvent.preventDefault();
uiService.set('zoom', 1);
});
}
public destroy() {
this.keycon.destroy();
}
private isDisabledKeyEvent(node: EventTarget | null) {
const el = node as HTMLElement | null;
if (!el) return false;
// 当前是在输入框中,禁止响应画布快捷键
return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable;
}
private removeNode() {
const nodes = editorService.get('nodes');
if (!nodes || isPage(nodes[0])) return;
editorService.remove(nodes);
}
}
export type KeybindingService = Keybinding;
export default new Keybinding();

View File

@ -30,11 +30,12 @@ import type {
import type { CodeBlockService } from './services/codeBlock'; import type { CodeBlockService } from './services/codeBlock';
import type { ComponentListService } from './services/componentList'; import type { ComponentListService } from './services/componentList';
import { DataSourceService } from './services/dataSource'; import type { DataSourceService } from './services/dataSource';
import type { DepService } from './services/dep'; import type { DepService } from './services/dep';
import type { EditorService } from './services/editor'; import type { EditorService } from './services/editor';
import type { EventsService } from './services/events'; import type { EventsService } from './services/events';
import type { HistoryService } from './services/history'; import type { HistoryService } from './services/history';
import type { KeybindingService } from './services/keybinding';
import type { PropsService } from './services/props'; import type { PropsService } from './services/props';
import type { StorageService } from './services/storage'; import type { StorageService } from './services/storage';
import type { UiService } from './services/ui'; import type { UiService } from './services/ui';
@ -58,6 +59,7 @@ export interface Services {
codeBlockService: CodeBlockService; codeBlockService: CodeBlockService;
depService: DepService; depService: DepService;
dataSourceService: DataSourceService; dataSourceService: DataSourceService;
keybindingService: KeybindingService;
} }
export interface StageOptions { export interface StageOptions {