in packages/engine/src/inner-plugins/builtin-hotkey.ts [226:546]
init() {
const { hotkey, project, logger, canvas } = ctx;
const { clipboard } = canvas;
// hotkey binding
hotkey.bind(['backspace', 'del'], (e: KeyboardEvent, action) => {
logger.info(`action ${action} is triggered`);
if (canvas.isInLiveEditing) {
return;
}
// TODO: use focus-tracker
const doc = project.currentDocument;
if (isFormEvent(e) || !doc) {
return;
}
e.preventDefault();
const sel = doc.selection;
const topItems = sel.getTopNodes();
// TODO: check can remove
topItems.forEach((node) => {
if (node?.canPerformAction('remove')) {
node && doc.removeNode(node);
}
});
sel.clear();
});
hotkey.bind('escape', (e: KeyboardEvent, action) => {
logger.info(`action ${action} is triggered`);
if (canvas.isInLiveEditing) {
return;
}
const sel = project.currentDocument?.selection;
if (isFormEvent(e) || !sel) {
return;
}
e.preventDefault();
sel.clear();
// currentFocus.esc();
});
// command + c copy command + x cut
hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => {
logger.info(`action ${action} is triggered`);
if (canvas.isInLiveEditing) {
return;
}
const doc = project.currentDocument;
if (isFormEvent(e) || !doc) {
return;
}
const anchorValue = document.getSelection()?.anchorNode?.nodeValue;
if (anchorValue && typeof anchorValue === 'string') {
return;
}
e.preventDefault();
let selected = doc.selection.getTopNodes(true);
selected = selected.filter((node) => {
return node?.canPerformAction('copy');
});
if (!selected || selected.length < 1) {
return;
}
const componentsMap = {};
const componentsTree = selected.map((item) => item?.exportSchema(IPublicEnumTransformStage.Clone));
// FIXME: clear node.id
const data = { type: 'nodeSchema', componentsMap, componentsTree };
clipboard.setData(data);
const cutMode = action && action.indexOf('x') > 0;
if (cutMode) {
selected.forEach((node) => {
const parentNode = node?.parent;
parentNode?.select();
node?.remove();
});
}
});
// command + v paste
hotkey.bind(['command+v', 'ctrl+v'], (e, action) => {
logger.info(`action ${action} is triggered`);
if (canvas.isInLiveEditing) {
return;
}
// TODO
const doc = project?.currentDocument;
if (isFormEvent(e) || !doc) {
return;
}
/* istanbul ignore next */
clipboard.waitPasteData(e, ({ componentsTree }) => {
if (componentsTree) {
const { target, index } = getSuitableInsertion(ctx, componentsTree) || {};
if (!target) {
return;
}
let canAddComponentsTree = componentsTree.filter((node: IPublicModelNode) => {
const dragNodeObject: IPublicTypeDragNodeObject = {
type: IPublicEnumDragObjectType.Node,
nodes: [node],
};
return doc.checkNesting(target, dragNodeObject);
});
if (canAddComponentsTree.length === 0) {
return;
}
const nodes = insertChildren(target, canAddComponentsTree, index);
if (nodes) {
doc.selection.selectAll(nodes.map((o) => o.id));
setTimeout(() => canvas.activeTracker?.track(nodes[0]), 10);
}
}
});
});
// command + z undo
hotkey.bind(['command+z', 'ctrl+z'], (e, action) => {
logger.info(`action ${action} is triggered`);
if (canvas.isInLiveEditing) {
return;
}
const history = project.currentDocument?.history;
if (isFormEvent(e) || !history) {
return;
}
e.preventDefault();
const selection = project.currentDocument?.selection;
const curSelected = selection?.selected && Array.from(selection?.selected);
history.back();
selection?.selectAll(curSelected);
});
// command + shift + z redo
hotkey.bind(['command+y', 'ctrl+y', 'command+shift+z'], (e, action) => {
logger.info(`action ${action} is triggered`);
if (canvas.isInLiveEditing) {
return;
}
const history = project.currentDocument?.history;
if (isFormEvent(e) || !history) {
return;
}
e.preventDefault();
const selection = project.currentDocument?.selection;
const curSelected = selection?.selected && Array.from(selection?.selected);
history.forward();
selection?.selectAll(curSelected);
});
// sibling selection
hotkey.bind(['left', 'right'], (e, action) => {
logger.info(`action ${action} is triggered`);
if (canvas.isInLiveEditing) {
return;
}
const doc = project.currentDocument;
if (isFormEvent(e) || !doc) {
return;
}
e.preventDefault();
const selected = doc.selection.getTopNodes(true);
if (!selected || selected.length < 1) {
return;
}
const firstNode = selected[0];
const silbing = action === 'left' ? firstNode?.prevSibling : firstNode?.nextSibling;
silbing?.select();
});
hotkey.bind(['up', 'down'], (e, action) => {
logger.info(`action ${action} is triggered`);
if (canvas.isInLiveEditing) {
return;
}
const doc = project.currentDocument;
if (isFormEvent(e) || !doc) {
return;
}
e.preventDefault();
const selected = doc.selection.getTopNodes(true);
if (!selected || selected.length < 1) {
return;
}
const firstNode = selected[0];
if (action === 'down') {
const next = getNextForSelect(firstNode, true, firstNode?.parent);
next?.select();
} else if (action === 'up') {
const prev = getPrevForSelect(firstNode, true, firstNode?.parent);
prev?.select();
}
});
hotkey.bind(['option+left', 'option+right'], (e, action) => {
logger.info(`action ${action} is triggered`);
if (canvas.isInLiveEditing) {
return;
}
const doc = project.currentDocument;
if (isFormEvent(e) || !doc) {
return;
}
e.preventDefault();
const selected = doc.selection.getTopNodes(true);
if (!selected || selected.length < 1) {
return;
}
// TODO: 此处需要增加判断当前节点是否可被操作移动,原ve里是用 node.canOperating()来判断
// TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断
const firstNode = selected[0];
const parent = firstNode?.parent;
if (!parent) return;
const isPrev = action && /(left)$/.test(action);
const silbing = isPrev ? firstNode.prevSibling : firstNode.nextSibling;
if (silbing) {
if (isPrev) {
parent.insertBefore(firstNode, silbing, true);
} else {
parent.insertAfter(firstNode, silbing, true);
}
firstNode?.select();
}
});
hotkey.bind(['option+up'], (e, action) => {
logger.info(`action ${action} is triggered`);
if (canvas.isInLiveEditing) {
return;
}
const doc = project.currentDocument;
if (isFormEvent(e) || !doc) {
return;
}
e.preventDefault();
const selected = doc.selection.getTopNodes(true);
if (!selected || selected.length < 1) {
return;
}
// TODO: 此处需要增加判断当前节点是否可被操作移动,原ve里是用 node.canOperating()来判断
// TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断
const firstNode = selected[0];
const parent = firstNode?.parent;
if (!parent) {
return;
}
const silbing = firstNode.prevSibling;
if (silbing) {
if (silbing.isContainerNode) {
const place = getSuitablePlaceForNode(silbing, firstNode, null);
silbing.insertAfter(firstNode, place.ref, true);
} else {
parent.insertBefore(firstNode, silbing, true);
}
firstNode?.select();
} else {
const place = getSuitablePlaceForNode(parent, firstNode, null); // upwards
if (place) {
const container = place.container.internalToShellNode();
container.insertBefore(firstNode, place.ref);
firstNode?.select();
}
}
});
hotkey.bind(['option+down'], (e, action) => {
logger.info(`action ${action} is triggered`);
if (canvas.isInLiveEditing) {
return;
}
const doc = project.getCurrentDocument();
if (isFormEvent(e) || !doc) {
return;
}
e.preventDefault();
const selected = doc.selection.getTopNodes(true);
if (!selected || selected.length < 1) {
return;
}
// TODO: 此处需要增加判断当前节点是否可被操作移动,原 ve 里是用 node.canOperating() 来判断
// TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断
const firstNode = selected[0];
const parent = firstNode?.parent;
if (!parent) {
return;
}
const silbing = firstNode.nextSibling;
if (silbing) {
if (silbing.isContainerNode) {
silbing.insertBefore(firstNode, undefined);
} else {
parent.insertAfter(firstNode, silbing, true);
}
firstNode?.select();
} else {
const place = getSuitablePlaceForNode(parent, firstNode, null); // upwards
if (place) {
const container = place.container.internalToShellNode();
container.insertAfter(firstNode, place.ref, true);
firstNode?.select();
}
}
});
},