mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-06-19 04:01:33 +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