551 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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___';