From 51031fe8ab4396eb6785738b4f208f96a97627e0 Mon Sep 17 00:00:00 2001 From: roymondchen Date: Fri, 22 Apr 2022 12:14:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(editor):=20=E6=B7=BB=E5=8A=A0=E5=B8=B8?= =?UTF-8?q?=E7=94=A8=E5=BF=AB=E6=8D=B7=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/editor/src/components/ToolButton.vue | 6 +- .../src/layouts/workspace/Workspace.vue | 86 +++++++++++++++---- packages/editor/src/services/editor.ts | 55 +++++++++++- packages/editor/src/services/ui.ts | 5 ++ packages/editor/src/utils/editor.ts | 2 +- .../tests/unit/components/ToolButton.spec.ts | 14 ++- 6 files changed, 139 insertions(+), 29 deletions(-) diff --git a/packages/editor/src/components/ToolButton.vue b/packages/editor/src/components/ToolButton.vue index 121d1e86..5dda1352 100644 --- a/packages/editor/src/components/ToolButton.vue +++ b/packages/editor/src/components/ToolButton.vue @@ -64,13 +64,13 @@ export default defineComponent({ const services = inject('services'); const uiService = services?.uiService; - const zoomInHandler = () => uiService?.set('zoom', zoom.value + 0.1); - const zoomOutHandler = () => uiService?.set('zoom', zoom.value - 0.1); - const zoom = computed((): number => uiService?.get('zoom') ?? 1); const showGuides = computed((): boolean => uiService?.get('showGuides') ?? true); const showRule = computed((): boolean => uiService?.get('showRule') ?? true); + const zoomInHandler = () => uiService?.zoom(0.1); + const zoomOutHandler = () => uiService?.zoom(-0.1); + const item = computed((): MenuButton | MenuComponent => { if (typeof props.data !== 'string') { return props.data; diff --git a/packages/editor/src/layouts/workspace/Workspace.vue b/packages/editor/src/layouts/workspace/Workspace.vue index 6d2e913c..a343dcb9 100644 --- a/packages/editor/src/layouts/workspace/Workspace.vue +++ b/packages/editor/src/layouts/workspace/Workspace.vue @@ -65,39 +65,95 @@ export default defineComponent({ workspace.value?.focus(); }; - const mouseleaveHandler = () => { - workspace.value?.blur(); - }; - 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', () => { + .keyup('delete', (e) => { + e.inputEvent.preventDefault(); if (!node.value || isPage(node.value)) return; services?.editorService.remove(node.value); }) - .keyup(['ctrl', 'c'], () => { + .keydown([ctrl, 'c'], (e) => { + e.inputEvent.preventDefault(); node.value && services?.editorService.copy(node.value); }) - .keyup(['ctrl', 'v'], () => { + .keyup([ctrl, 'v'], (e) => { + e.inputEvent.preventDefault(); node.value && services?.editorService.paste(); }) - .keyup(['ctrl', 'x'], () => { - if (node.value && services) { - services.editorService.copy(node.value); - services.editorService.remove(node.value); - } + .keyup([ctrl, 'x'], (e) => { + e.inputEvent.preventDefault(); + if (!node.value || isPage(node.value)) return; + services?.editorService.copy(node.value); + services?.editorService.remove(node.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, '-'], (e) => { + e.inputEvent.preventDefault(); + services?.uiService.zoom(-0.1); }); }); onUnmounted(() => { workspace.value?.removeEventListener('mouseenter', mouseenterHandler); - workspace.value?.removeEventListener('mouseleave', mouseleaveHandler); keycon.destroy(); }); diff --git a/packages/editor/src/services/editor.ts b/packages/editor/src/services/editor.ts index d4328294..f5eae804 100644 --- a/packages/editor/src/services/editor.ts +++ b/packages/editor/src/services/editor.ts @@ -23,7 +23,7 @@ import serialize from 'serialize-javascript'; import type { Id, MApp, MComponent, MContainer, MNode, MPage } from '@tmagic/schema'; import { NodeType } from '@tmagic/schema'; import StageCore from '@tmagic/stage'; -import { getNodePath, isPop } from '@tmagic/utils'; +import { getNodePath, isNumber, isPage, isPop } from '@tmagic/utils'; import historyService, { StepValue } from '@editor/services/history'; import propsService from '@editor/services/props'; @@ -193,6 +193,39 @@ class Editor extends BaseService { return node!; } + public async selectNextNode(): Promise | never { + const node = toRaw(this.get('node')); + + if (!node || isPage(node) || node.type === NodeType.ROOT) return node; + + const parent = toRaw(this.getParentById(node.id)); + + if (!parent) return node; + + const index = getNodeIndex(node, parent); + + const nextNode = parent.items[index + 1] || parent.items[0]; + + await this.select(nextNode); + this.get('stage')?.select(nextNode.id); + + return nextNode; + } + + public async selectNextPage(): Promise | never { + const root = toRaw(this.get('root')); + const page = toRaw(this.get('page')); + + const index = getNodeIndex(page, root); + + const nextPage = root.items[index + 1] || root.items[0]; + + await this.select(nextPage); + this.get('stage')?.select(nextPage.id); + + return nextPage; + } + /** * 高亮指定节点 * @param config 指定节点配置或者ID @@ -493,6 +526,26 @@ class Editor extends BaseService { return value; } + public async move(left: number, top: number) { + const node = toRaw(this.get('node')); + if (!node || isPage(node)) return; + + const { style, id } = node; + if (!style || style.position !== 'absolute') return; + + if (top && !isNumber(style.top)) return; + if (left && !isNumber(style.left)) return; + + this.update({ + id, + style: { + ...style, + left: Number(style.left) + left, + top: Number(style.top) + top, + }, + }); + } + public destroy() { this.removeAllListeners(); this.set('root', null); diff --git a/packages/editor/src/services/ui.ts b/packages/editor/src/services/ui.ts index 0bf6766c..f0bc96c4 100644 --- a/packages/editor/src/services/ui.ts +++ b/packages/editor/src/services/ui.ts @@ -91,6 +91,11 @@ class Ui extends BaseService { return (state as any)[name]; } + public zoom(zoom: number) { + this.set('zoom', (this.get('zoom') * 100 + zoom * 100) / 100); + if (this.get('zoom') < 0.1) this.set('zoom', 0.1); + } + private setColumnWidth({ left, center, right }: SetColumnWidth) { const columnWidth = { ...toRaw(this.get('columnWidth')), diff --git a/packages/editor/src/utils/editor.ts b/packages/editor/src/utils/editor.ts index cb44f33b..ab143cf5 100644 --- a/packages/editor/src/utils/editor.ts +++ b/packages/editor/src/utils/editor.ts @@ -125,7 +125,7 @@ export const setNewItemId = (config: MNode, parent?: MPage) => { */ export const isFixed = (node: MNode): boolean => node.style?.position === 'fixed'; -export const getNodeIndex = (node: MNode, parent: MContainer): number => { +export const getNodeIndex = (node: MNode, parent: MContainer | MApp): number => { const items = parent?.items || []; return items.findIndex((item: MNode) => `${item.id}` === `${node.id}`); }; diff --git a/packages/editor/tests/unit/components/ToolButton.spec.ts b/packages/editor/tests/unit/components/ToolButton.spec.ts index ce777f5f..765c1aab 100644 --- a/packages/editor/tests/unit/components/ToolButton.spec.ts +++ b/packages/editor/tests/unit/components/ToolButton.spec.ts @@ -20,6 +20,7 @@ import { mount } from '@vue/test-utils'; import ElementPlus, { ElDropdown } from 'element-plus'; import ToolButton from '@editor/components/ToolButton.vue'; +import uiService from '@editor/services/ui'; // ResizeObserver mock globalThis.ResizeObserver = @@ -50,12 +51,6 @@ const historyService = { }, }; -// mock -const uiService = { - set: jest.fn(), - get: jest.fn(() => 0.5), -}; - const getWrapper = ( props: any = { data: 'delete', @@ -110,24 +105,25 @@ describe('ToolButton', () => { }); it('放大', (done) => { + uiService.set('zoom', 1); const wrapper = getWrapper({ data: 'zoom-in' }); setTimeout(async () => { const icon = wrapper.find('.el-button'); await icon.trigger('click'); - expect(uiService.get).toBeCalled(); - expect(uiService.set.mock.calls[0]).toEqual(['zoom', 0.6]); + expect(uiService.get('zoom')).toBe(1.1); done(); }, 0); }); it('缩小', (done) => { + uiService.set('zoom', 1); const wrapper = getWrapper({ data: 'zoom-out' }); setTimeout(async () => { const icon = wrapper.find('.el-button'); await icon.trigger('click'); - expect(uiService.set.mock.calls[1]).toEqual(['zoom', 0.4]); + expect(uiService.get('zoom')).toBe(0.9); done(); }, 0); });