mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-18 16:02:52 +00:00
551 lines
16 KiB
TypeScript
551 lines
16 KiB
TypeScript
/* eslint-disable max-len */
|
||
import { isFormEvent, isNodeSchema, isNode } from '@alilc/lowcode-utils';
|
||
import {
|
||
IPublicModelPluginContext,
|
||
IPublicEnumTransformStage,
|
||
IPublicModelNode,
|
||
IPublicTypeNodeSchema,
|
||
IPublicTypeNodeData,
|
||
IPublicEnumDragObjectType,
|
||
IPublicTypeDragNodeObject,
|
||
} from '@alilc/lowcode-types';
|
||
|
||
function insertChild(
|
||
container: IPublicModelNode,
|
||
originalChild: IPublicModelNode | IPublicTypeNodeData,
|
||
at?: number | null,
|
||
): IPublicModelNode | null {
|
||
let child = originalChild;
|
||
if (isNode(child) && (child as IPublicModelNode).isSlotNode) {
|
||
child = (child as IPublicModelNode).exportSchema(IPublicEnumTransformStage.Clone);
|
||
}
|
||
let node = null;
|
||
if (isNode(child)) {
|
||
node = (child as IPublicModelNode);
|
||
container.children?.insert(node, at);
|
||
} else {
|
||
node = container.document?.createNode(child) || null;
|
||
if (node) {
|
||
container.children?.insert(node, at);
|
||
}
|
||
}
|
||
|
||
return (node as IPublicModelNode) || null;
|
||
}
|
||
|
||
function insertChildren(
|
||
container: IPublicModelNode,
|
||
nodes: IPublicModelNode[] | IPublicTypeNodeData[],
|
||
at?: number | null,
|
||
): IPublicModelNode[] {
|
||
let index = at;
|
||
let node: any;
|
||
const results: IPublicModelNode[] = [];
|
||
// eslint-disable-next-line no-cond-assign
|
||
while ((node = nodes.pop())) {
|
||
node = insertChild(container, node, index);
|
||
results.push(node);
|
||
index = node.index;
|
||
}
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* 获得合适的插入位置
|
||
*/
|
||
function getSuitableInsertion(
|
||
pluginContext: IPublicModelPluginContext,
|
||
insertNode?: IPublicModelNode | IPublicTypeNodeSchema | IPublicTypeNodeSchema[],
|
||
): { target: IPublicModelNode; index?: number } | null {
|
||
const { project, material } = pluginContext;
|
||
const activeDoc = project.currentDocument;
|
||
if (!activeDoc) {
|
||
return null;
|
||
}
|
||
if (
|
||
Array.isArray(insertNode) &&
|
||
isNodeSchema(insertNode[0]) &&
|
||
material.getComponentMeta(insertNode[0].componentName)?.isModal
|
||
) {
|
||
if (!activeDoc.root) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
target: activeDoc.root,
|
||
};
|
||
}
|
||
|
||
const focusNode = activeDoc.focusNode!;
|
||
const nodes = activeDoc.selection.getNodes();
|
||
const refNode = nodes.find((item) => focusNode.contains(item));
|
||
let target;
|
||
let index: number | undefined;
|
||
if (!refNode || refNode === focusNode) {
|
||
target = focusNode;
|
||
} else if (refNode.componentMeta?.isContainer) {
|
||
target = refNode;
|
||
} else {
|
||
// FIXME!!, parent maybe null
|
||
target = refNode.parent!;
|
||
index = refNode.index + 1;
|
||
}
|
||
|
||
if (target && insertNode && !target.componentMeta?.checkNestingDown(target, insertNode)) {
|
||
return null;
|
||
}
|
||
|
||
return { target, index };
|
||
}
|
||
|
||
/* istanbul ignore next */
|
||
function getNextForSelect(next: IPublicModelNode | null, head?: any, parent?: IPublicModelNode | null): any {
|
||
if (next) {
|
||
if (!head) {
|
||
return next;
|
||
}
|
||
|
||
let ret;
|
||
if (next.isContainerNode) {
|
||
const { children } = next;
|
||
if (children && !children.isEmptyNode) {
|
||
ret = getNextForSelect(children.get(0));
|
||
if (ret) {
|
||
return ret;
|
||
}
|
||
}
|
||
}
|
||
|
||
ret = getNextForSelect(next.nextSibling);
|
||
if (ret) {
|
||
return ret;
|
||
}
|
||
}
|
||
|
||
if (parent) {
|
||
return getNextForSelect(parent.nextSibling, false, parent?.parent);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/* istanbul ignore next */
|
||
function getPrevForSelect(prev: IPublicModelNode | null, head?: any, parent?: IPublicModelNode | null): any {
|
||
if (prev) {
|
||
let ret;
|
||
if (!head && prev.isContainerNode) {
|
||
const { children } = prev;
|
||
const lastChild = children && !children.isEmptyNode ? children.get(children.size - 1) : null;
|
||
|
||
ret = getPrevForSelect(lastChild);
|
||
if (ret) {
|
||
return ret;
|
||
}
|
||
}
|
||
|
||
if (!head) {
|
||
return prev;
|
||
}
|
||
|
||
ret = getPrevForSelect(prev.prevSibling);
|
||
if (ret) {
|
||
return ret;
|
||
}
|
||
}
|
||
|
||
if (parent) {
|
||
return parent;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function getSuitablePlaceForNode(targetNode: IPublicModelNode, node: IPublicModelNode, ref: any): any {
|
||
const { document } = targetNode;
|
||
if (!document) {
|
||
return null;
|
||
}
|
||
|
||
const dragNodeObject: IPublicTypeDragNodeObject = {
|
||
type: IPublicEnumDragObjectType.Node,
|
||
nodes: [node],
|
||
};
|
||
|
||
const focusNode = document?.focusNode;
|
||
// 如果节点是模态框,插入到根节点下
|
||
if (node?.componentMeta?.isModal) {
|
||
return { container: focusNode, ref };
|
||
}
|
||
|
||
if (!ref && focusNode && targetNode.contains(focusNode)) {
|
||
if (document.checkNesting(focusNode, dragNodeObject)) {
|
||
return { container: focusNode };
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
if (targetNode.isRootNode && targetNode.children) {
|
||
const dropElement = targetNode.children.filter((c) => {
|
||
if (!c.isContainerNode) {
|
||
return false;
|
||
}
|
||
if (document.checkNesting(c, dragNodeObject)) {
|
||
return true;
|
||
}
|
||
return false;
|
||
})[0];
|
||
|
||
if (dropElement) {
|
||
return { container: dropElement, ref };
|
||
}
|
||
|
||
if (document.checkNesting(targetNode, dragNodeObject)) {
|
||
return { container: targetNode, ref };
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
if (targetNode.isContainerNode) {
|
||
if (document.checkNesting(targetNode, dragNodeObject)) {
|
||
return { container: targetNode, ref };
|
||
}
|
||
}
|
||
|
||
if (targetNode.parent) {
|
||
return getSuitablePlaceForNode(targetNode.parent, node, { index: targetNode.index });
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// 注册默认的 setters
|
||
export const builtinHotkey = (ctx: IPublicModelPluginContext) => {
|
||
return {
|
||
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();
|
||
}
|
||
}
|
||
});
|
||
},
|
||
};
|
||
};
|
||
|
||
builtinHotkey.pluginName = '___builtin_hotkey___';
|