mirror of
				https://github.com/Tencent/tmagic-editor.git
				synced 2025-11-04 18:52:18 +08:00 
			
		
		
		
	feat(editor): 添加常用快捷键
This commit is contained in:
		
							parent
							
								
									94debf51c0
								
							
						
					
					
						commit
						51031fe8ab
					
				@ -64,13 +64,13 @@ export default defineComponent({
 | 
			
		||||
    const services = inject<Services>('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<number>('zoom') ?? 1);
 | 
			
		||||
    const showGuides = computed((): boolean => uiService?.get<boolean>('showGuides') ?? true);
 | 
			
		||||
    const showRule = computed((): boolean => uiService?.get<boolean>('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;
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<MNode> | 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<StageCore>('stage')?.select(nextNode.id);
 | 
			
		||||
 | 
			
		||||
    return nextNode;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async selectNextPage(): Promise<MNode> | never {
 | 
			
		||||
    const root = toRaw(this.get<MApp>('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<StageCore>('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);
 | 
			
		||||
 | 
			
		||||
@ -91,6 +91,11 @@ class Ui extends BaseService {
 | 
			
		||||
    return (state as any)[name];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public zoom(zoom: number) {
 | 
			
		||||
    this.set('zoom', (this.get<number>('zoom') * 100 + zoom * 100) / 100);
 | 
			
		||||
    if (this.get<number>('zoom') < 0.1) this.set('zoom', 0.1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private setColumnWidth({ left, center, right }: SetColumnWidth) {
 | 
			
		||||
    const columnWidth = {
 | 
			
		||||
      ...toRaw(this.get<GetColumnWidth>('columnWidth')),
 | 
			
		||||
 | 
			
		||||
@ -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}`);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user