mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-06-23 02:15:10 +08:00
feat(editor): 优化快捷键操作
This commit is contained in:
parent
567b054b32
commit
06d289aff3
@ -96,6 +96,7 @@ import depService from './services/dep';
|
||||
import editorService from './services/editor';
|
||||
import eventsService from './services/events';
|
||||
import historyService from './services/history';
|
||||
import keybindingService from './services/keybinding';
|
||||
import propsService from './services/props';
|
||||
import storageService from './services/storage';
|
||||
import uiService from './services/ui';
|
||||
@ -130,6 +131,7 @@ export default defineComponent({
|
||||
codeBlockService,
|
||||
depService,
|
||||
dataSourceService,
|
||||
keybindingService,
|
||||
};
|
||||
|
||||
initServiceEvents(props, emit, services);
|
||||
|
@ -10,6 +10,7 @@
|
||||
ref="tree"
|
||||
node-key="id"
|
||||
empty-text="页面空荡荡的"
|
||||
tabindex="-1"
|
||||
draggable
|
||||
:default-expanded-keys="expandedKeys"
|
||||
:default-checked-keys="checkedKeys"
|
||||
@ -75,6 +76,7 @@ defineProps<{
|
||||
const throttleTime = 150;
|
||||
const services = inject<Services>('services');
|
||||
const editorService = services?.editorService;
|
||||
const keybindingService = services?.keybindingService;
|
||||
|
||||
const tree = ref<InstanceType<typeof TMagicTree>>();
|
||||
const menu = ref<InstanceType<typeof LayerMenu>>();
|
||||
@ -246,27 +248,40 @@ const windowBlurHandler = () => {
|
||||
isCtrlKeyDown.value = false;
|
||||
};
|
||||
|
||||
const globalKeyupHandler = () => {
|
||||
if (document.activeElement !== tree.value?.$el) {
|
||||
isCtrlKeyDown.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
let keycon: KeyController;
|
||||
|
||||
onMounted(() => {
|
||||
editorService?.on('remove', editorServiceRemoveHandler);
|
||||
|
||||
keycon = new KeyController();
|
||||
const isMac = /mac os x/.test(navigator.userAgent.toLowerCase());
|
||||
const ctrl = isMac ? 'meta' : 'ctrl';
|
||||
if (tree.value?.$el) {
|
||||
// 如果是在树上按下ctrl,那么在树外松开ctrl时,也要触发松开事件
|
||||
keybindingService?.on('keyup', globalKeyupHandler);
|
||||
|
||||
keycon
|
||||
.keydown((e) => {
|
||||
if (e.key !== ctrl) {
|
||||
keycon = new KeyController(tree.value.$el);
|
||||
const isMac = /mac os x/.test(navigator.userAgent.toLowerCase());
|
||||
const ctrl = isMac ? 'meta' : 'ctrl';
|
||||
|
||||
keycon
|
||||
.keydown((e) => {
|
||||
if (e.key !== ctrl) {
|
||||
isCtrlKeyDown.value = false;
|
||||
}
|
||||
})
|
||||
.keydown(ctrl, () => {
|
||||
isCtrlKeyDown.value = true;
|
||||
})
|
||||
.keyup(ctrl, () => {
|
||||
isCtrlKeyDown.value = false;
|
||||
}
|
||||
})
|
||||
.keydown(ctrl, () => {
|
||||
isCtrlKeyDown.value = true;
|
||||
})
|
||||
.keyup(ctrl, () => {
|
||||
isCtrlKeyDown.value = false;
|
||||
});
|
||||
});
|
||||
|
||||
tree.value.$el.addEventListener('blur', windowBlurHandler);
|
||||
}
|
||||
|
||||
globalThis.addEventListener('blur', windowBlurHandler);
|
||||
});
|
||||
@ -275,7 +290,9 @@ onUnmounted(() => {
|
||||
keycon.destroy();
|
||||
|
||||
editorService?.off('remove', editorServiceRemoveHandler);
|
||||
keybindingService?.off('keyup', globalKeyupHandler);
|
||||
globalThis.removeEventListener('blur', windowBlurHandler);
|
||||
tree.value?.$el.removeEventListener('blur', windowBlurHandler);
|
||||
});
|
||||
|
||||
// 鼠标在组件树移动触发高亮
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="m-editor-workspace" tabindex="-1" ref="workspace">
|
||||
<div class="m-editor-workspace" tabindex="-1">
|
||||
<Breadcrumb></Breadcrumb>
|
||||
|
||||
<slot name="stage">
|
||||
@ -16,10 +16,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, onUnmounted, ref } from 'vue';
|
||||
import KeyController from 'keycon';
|
||||
|
||||
import { isPage } from '@tmagic/utils';
|
||||
import { computed, inject } from 'vue';
|
||||
|
||||
import type { MenuButton, MenuComponent, Services } from '@editor/type';
|
||||
|
||||
@ -36,132 +33,6 @@ defineProps<{
|
||||
}>();
|
||||
|
||||
const services = inject<Services>('services');
|
||||
const workspace = ref<HTMLDivElement>();
|
||||
const nodes = computed(() => services?.editorService.get('nodes'));
|
||||
|
||||
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>
|
||||
|
240
packages/editor/src/services/keybinding.ts
Normal file
240
packages/editor/src/services/keybinding.ts
Normal 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();
|
@ -30,11 +30,12 @@ import type {
|
||||
|
||||
import type { CodeBlockService } from './services/codeBlock';
|
||||
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 { EditorService } from './services/editor';
|
||||
import type { EventsService } from './services/events';
|
||||
import type { HistoryService } from './services/history';
|
||||
import type { KeybindingService } from './services/keybinding';
|
||||
import type { PropsService } from './services/props';
|
||||
import type { StorageService } from './services/storage';
|
||||
import type { UiService } from './services/ui';
|
||||
@ -58,6 +59,7 @@ export interface Services {
|
||||
codeBlockService: CodeBlockService;
|
||||
depService: DepService;
|
||||
dataSourceService: DataSourceService;
|
||||
keybindingService: KeybindingService;
|
||||
}
|
||||
|
||||
export interface StageOptions {
|
||||
|
Loading…
x
Reference in New Issue
Block a user