mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-12 11:20:11 +00:00
529 lines
13 KiB
TypeScript
529 lines
13 KiB
TypeScript
import { obx, computed, globalContext, makeObservable, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
|
||
import { Node, INode } from './node';
|
||
import { IPublicTypeNodeData, IPublicModelNodeChildren, IPublicEnumTransformStage } from '@alilc/lowcode-types';
|
||
import { shallowEqual, compatStage, isNodeSchema } from '@alilc/lowcode-utils';
|
||
import { foreachReverse } from '../../utils/tree';
|
||
import { NodeRemoveOptions } from '../../types';
|
||
|
||
export interface IOnChangeOptions {
|
||
type: string;
|
||
node: Node;
|
||
}
|
||
|
||
export interface INodeChildren extends Omit<IPublicModelNodeChildren, 'forEach' | 'map' | 'every' | 'some' | 'filter' | 'find' | 'reduce' | 'mergeChildren' > {
|
||
get owner(): INode;
|
||
|
||
unlinkChild(node: INode): void;
|
||
|
||
/**
|
||
* 删除一个节点
|
||
*/
|
||
internalDelete(
|
||
node: INode,
|
||
purge: boolean,
|
||
useMutator: boolean,
|
||
options: NodeRemoveOptions
|
||
): boolean;
|
||
|
||
/**
|
||
* 插入一个节点,返回新长度
|
||
*/
|
||
internalInsert(node: INode, at?: number | null, useMutator?: boolean): void;
|
||
|
||
import(data?: IPublicTypeNodeData | IPublicTypeNodeData[], checkId?: boolean): void;
|
||
|
||
/**
|
||
* 导出 schema
|
||
*/
|
||
export(stage: IPublicEnumTransformStage): IPublicTypeNodeData[];
|
||
|
||
/** following methods are overriding super interface, using different param types */
|
||
/** overriding methods start */
|
||
|
||
forEach(fn: (item: INode, index: number) => void): void;
|
||
|
||
map<T>(fn: (item: INode, index: number) => T): T[] | null;
|
||
|
||
every(fn: (item: INode, index: number) => any): boolean;
|
||
|
||
some(fn: (item: INode, index: number) => any): boolean;
|
||
|
||
filter(fn: (item: INode, index: number) => any): any;
|
||
|
||
find(fn: (item: INode, index: number) => boolean): any;
|
||
|
||
reduce(fn: (acc: any, cur: INode) => any, initialValue: any): void;
|
||
|
||
reverse(): INode[];
|
||
|
||
mergeChildren(
|
||
remover: (node: INode, idx: number) => boolean,
|
||
adder: (children: INode[]) => IPublicTypeNodeData[] | null,
|
||
sorter: (firstNode: INode, secondNode: INode) => number,
|
||
): any;
|
||
|
||
/**
|
||
* 根据索引获得节点
|
||
*/
|
||
get(index: number): INode | null;
|
||
|
||
/** overriding methods end */
|
||
}
|
||
export class NodeChildren implements INodeChildren {
|
||
@obx.shallow private children: INode[];
|
||
|
||
private emitter: IEventBus = createModuleEventBus('NodeChildren');
|
||
|
||
/**
|
||
* 元素个数
|
||
*/
|
||
@computed get size(): number {
|
||
return this.children.length;
|
||
}
|
||
|
||
get isEmptyNode(): boolean {
|
||
return this.size < 1;
|
||
}
|
||
get notEmptyNode(): boolean {
|
||
return this.size > 0;
|
||
}
|
||
|
||
@computed get length(): number {
|
||
return this.children.length;
|
||
}
|
||
|
||
private purged = false;
|
||
|
||
get [Symbol.toStringTag]() {
|
||
// 保证向前兼容性
|
||
return 'Array';
|
||
}
|
||
|
||
constructor(
|
||
readonly owner: INode,
|
||
data: IPublicTypeNodeData | IPublicTypeNodeData[],
|
||
options: any = {},
|
||
) {
|
||
makeObservable(this);
|
||
this.children = (Array.isArray(data) ? data : [data]).map((child) => {
|
||
return this.owner.document?.createNode(child, options.checkId);
|
||
});
|
||
}
|
||
|
||
internalInitParent() {
|
||
this.children.forEach((child) => child.internalSetParent(this.owner));
|
||
}
|
||
|
||
/**
|
||
* 导出 schema
|
||
*/
|
||
export(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save): IPublicTypeNodeData[] {
|
||
stage = compatStage(stage);
|
||
return this.children.map((node) => {
|
||
const data = node.export(stage);
|
||
if (node.isLeafNode && IPublicEnumTransformStage.Save === stage) {
|
||
// FIXME: filter empty
|
||
return data.children as IPublicTypeNodeData;
|
||
}
|
||
return data;
|
||
});
|
||
}
|
||
|
||
import(data?: IPublicTypeNodeData | IPublicTypeNodeData[], checkId = false) {
|
||
data = data ? (Array.isArray(data) ? data : [data]) : [];
|
||
|
||
const originChildren = this.children.slice();
|
||
this.children.forEach((child) => child.internalSetParent(null));
|
||
|
||
const children = new Array<Node>(data.length);
|
||
for (let i = 0, l = data.length; i < l; i++) {
|
||
const child = originChildren[i];
|
||
const item = data[i];
|
||
|
||
let node: Node | undefined;
|
||
if (isNodeSchema(item) && !checkId && child && child.componentName === item.componentName) {
|
||
node = child;
|
||
node.import(item);
|
||
} else {
|
||
node = this.owner.document.createNode(item, checkId);
|
||
}
|
||
children[i] = node;
|
||
}
|
||
|
||
this.children = children;
|
||
this.internalInitParent();
|
||
if (!shallowEqual(children, originChildren)) {
|
||
this.emitter.emit('change');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @deprecated
|
||
* @param nodes
|
||
*/
|
||
concat(nodes: INode[]) {
|
||
return this.children.concat(nodes);
|
||
}
|
||
|
||
/**
|
||
*
|
||
*/
|
||
isEmpty() {
|
||
return this.isEmptyNode;
|
||
}
|
||
|
||
notEmpty() {
|
||
return this.notEmptyNode;
|
||
}
|
||
|
||
/**
|
||
* 回收销毁
|
||
*/
|
||
purge(useMutator = true) {
|
||
if (this.purged) {
|
||
return;
|
||
}
|
||
this.purged = true;
|
||
this.children.forEach((child) => {
|
||
child.purge(useMutator);
|
||
});
|
||
}
|
||
|
||
unlinkChild(node: INode) {
|
||
const i = this.children.map(d => d.id).indexOf(node.id);
|
||
if (i < 0) {
|
||
return false;
|
||
}
|
||
this.children.splice(i, 1);
|
||
this.emitter.emit('change', {
|
||
type: 'unlink',
|
||
node,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 删除一个节点
|
||
*/
|
||
delete(node: INode): boolean {
|
||
return this.internalDelete(node);
|
||
}
|
||
|
||
/**
|
||
* 删除一个节点
|
||
*/
|
||
internalDelete(node: INode, purge = false, useMutator = true, options: NodeRemoveOptions = {}): boolean {
|
||
node.internalPurgeStart();
|
||
if (node.isParentalNode) {
|
||
foreachReverse(
|
||
node.children,
|
||
(subNode: Node) => {
|
||
subNode.remove(useMutator, purge, options);
|
||
},
|
||
(iterable, idx) => (iterable as NodeChildren).get(idx),
|
||
);
|
||
foreachReverse(
|
||
node.slots,
|
||
(slotNode: Node) => {
|
||
slotNode.remove(useMutator, purge);
|
||
},
|
||
(iterable, idx) => (iterable as [])[idx],
|
||
);
|
||
}
|
||
// 需要在从 children 中删除 node 前记录下 index,internalSetParent 中会执行删除 (unlink) 操作
|
||
const i = this.children.map(d => d.id).indexOf(node.id);
|
||
if (purge) {
|
||
// should set parent null
|
||
node.internalSetParent(null, useMutator);
|
||
try {
|
||
node.purge();
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
}
|
||
const { document } = node;
|
||
/* istanbul ignore next */
|
||
if (globalContext.has('editor')) {
|
||
const workspace = globalContext.get('workspace');
|
||
const editor = workspace.isActive ? workspace.window.editor : globalContext.get('editor');
|
||
editor.eventBus.emit('node.remove', { node, index: i });
|
||
}
|
||
document?.unlinkNode(node);
|
||
document?.selection.remove(node.id);
|
||
document?.destroyNode(node);
|
||
this.emitter.emit('change', {
|
||
type: 'delete',
|
||
node,
|
||
});
|
||
if (useMutator) {
|
||
this.reportModified(node, this.owner, {
|
||
type: 'remove',
|
||
propagated: false,
|
||
isSubDeleting: this.owner.isPurging,
|
||
removeIndex: i,
|
||
removeNode: node,
|
||
});
|
||
}
|
||
// purge 为 true 时,已在 internalSetParent 中删除了子节点
|
||
if (i > -1 && !purge) {
|
||
this.children.splice(i, 1);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
insert(node: INode, at?: number | null): void {
|
||
this.internalInsert(node, at, true);
|
||
}
|
||
|
||
/**
|
||
* 插入一个节点,返回新长度
|
||
*/
|
||
internalInsert(node: INode, at?: number | null, useMutator = true): void {
|
||
const { children } = this;
|
||
let index = at == null || at === -1 ? children.length : at;
|
||
|
||
const i = children.map(d => d.id).indexOf(node.id);
|
||
|
||
if (node.parent) {
|
||
if (globalContext.has('editor')) {
|
||
const workspace = globalContext.get('workspace');
|
||
const editor = workspace.isActive ? workspace.window.editor : globalContext.get('editor');
|
||
editor.eventBus.emit('node.remove.topLevel', {
|
||
node,
|
||
index: node.index,
|
||
});
|
||
}
|
||
}
|
||
|
||
if (i < 0) {
|
||
if (index < children.length) {
|
||
children.splice(index, 0, node);
|
||
} else {
|
||
children.push(node);
|
||
}
|
||
node.internalSetParent(this.owner, useMutator);
|
||
} else {
|
||
if (index > i) {
|
||
index -= 1;
|
||
}
|
||
|
||
if (index === i) {
|
||
return;
|
||
}
|
||
|
||
children.splice(i, 1);
|
||
children.splice(index, 0, node);
|
||
}
|
||
|
||
this.emitter.emit('change', {
|
||
type: 'insert',
|
||
node,
|
||
});
|
||
this.emitter.emit('insert', node);
|
||
/* istanbul ignore next */
|
||
if (globalContext.has('editor')) {
|
||
const workspace = globalContext.get('workspace');
|
||
const editor = workspace.isActive ? workspace.window.editor : globalContext.get('editor');
|
||
editor.eventBus.emit('node.add', { node });
|
||
}
|
||
if (useMutator) {
|
||
this.reportModified(node, this.owner, { type: 'insert' });
|
||
}
|
||
|
||
// check condition group
|
||
if (node.conditionGroup) {
|
||
if (
|
||
!(
|
||
// just sort at condition group
|
||
(
|
||
(node.prevSibling && node.prevSibling.conditionGroup === node.conditionGroup) ||
|
||
(node.nextSibling && node.nextSibling.conditionGroup === node.conditionGroup)
|
||
)
|
||
)
|
||
) {
|
||
node.setConditionGroup(null);
|
||
}
|
||
}
|
||
if (node.prevSibling && node.nextSibling) {
|
||
const { conditionGroup } = node.prevSibling;
|
||
// insert at condition group
|
||
if (conditionGroup && conditionGroup === node.nextSibling.conditionGroup) {
|
||
node.setConditionGroup(conditionGroup);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取得节点索引编号
|
||
*/
|
||
indexOf(node: INode): number {
|
||
return this.children.map(d => d.id).indexOf(node.id);
|
||
}
|
||
|
||
/**
|
||
*
|
||
*/
|
||
splice(start: number, deleteCount: number, node?: INode): INode[] {
|
||
if (node) {
|
||
return this.children.splice(start, deleteCount, node);
|
||
}
|
||
return this.children.splice(start, deleteCount);
|
||
}
|
||
|
||
/**
|
||
* 根据索引获得节点
|
||
*/
|
||
get(index: number): INode | null {
|
||
return this.children.length > index ? this.children[index] : null;
|
||
}
|
||
|
||
/**
|
||
* 是否存在节点
|
||
*/
|
||
has(node: INode) {
|
||
return this.indexOf(node) > -1;
|
||
}
|
||
|
||
/**
|
||
* 迭代器
|
||
*/
|
||
[Symbol.iterator](): { next(): { value: INode } } {
|
||
let index = 0;
|
||
const { children } = this;
|
||
const length = children.length || 0;
|
||
return {
|
||
next() {
|
||
if (index < length) {
|
||
return {
|
||
value: children[index++],
|
||
done: false,
|
||
};
|
||
}
|
||
return {
|
||
value: undefined as any,
|
||
done: true,
|
||
};
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 遍历
|
||
*/
|
||
forEach(fn: (item: INode, index: number) => void): void {
|
||
this.children.forEach((child, index) => {
|
||
return fn(child, index);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 遍历
|
||
*/
|
||
map<T>(fn: (item: INode, index: number) => T): T[] | null {
|
||
return this.children.map((child, index) => {
|
||
return fn(child, index);
|
||
});
|
||
}
|
||
|
||
every(fn: (item: INode, index: number) => any): boolean {
|
||
return this.children.every((child, index) => fn(child, index));
|
||
}
|
||
|
||
some(fn: (item: INode, index: number) => any): boolean {
|
||
return this.children.some((child, index) => fn(child, index));
|
||
}
|
||
|
||
filter(fn: (item: INode, index: number) => any): any {
|
||
return this.children.filter(fn);
|
||
}
|
||
|
||
find(fn: (item: INode, index: number) => boolean) {
|
||
return this.children.find(fn);
|
||
}
|
||
|
||
reduce(fn: (acc: any, cur: INode) => any, initialValue: any): void {
|
||
return this.children.reduce(fn, initialValue);
|
||
}
|
||
|
||
reverse() {
|
||
return this.children.reverse();
|
||
}
|
||
|
||
mergeChildren(
|
||
remover: (node: INode, idx: number) => boolean,
|
||
adder: (children: INode[]) => IPublicTypeNodeData[] | null,
|
||
sorter: (firstNode: INode, secondNode: INode) => number,
|
||
): any {
|
||
let changed = false;
|
||
if (remover) {
|
||
const willRemove = this.children.filter(remover);
|
||
if (willRemove.length > 0) {
|
||
willRemove.forEach((node) => {
|
||
const i = this.children.map(d => d.id).indexOf(node.id);
|
||
if (i > -1) {
|
||
this.children.splice(i, 1);
|
||
node.remove(false);
|
||
}
|
||
});
|
||
changed = true;
|
||
}
|
||
}
|
||
if (adder) {
|
||
const items = adder(this.children);
|
||
if (items && items.length > 0) {
|
||
items.forEach((child: IPublicTypeNodeData) => {
|
||
const node = this.owner.document?.createNode(child);
|
||
this.children.push(node);
|
||
node.internalSetParent(this.owner);
|
||
});
|
||
changed = true;
|
||
}
|
||
}
|
||
if (sorter) {
|
||
this.children = this.children.sort(sorter);
|
||
changed = true;
|
||
}
|
||
if (changed) {
|
||
this.emitter.emit('change');
|
||
}
|
||
}
|
||
|
||
onChange(fn: (info?: IOnChangeOptions) => void): () => void {
|
||
this.emitter.on('change', fn);
|
||
return () => {
|
||
this.emitter.removeListener('change', fn);
|
||
};
|
||
}
|
||
|
||
onInsert(fn: (node: INode) => void) {
|
||
this.emitter.on('insert', fn);
|
||
return () => {
|
||
this.emitter.removeListener('insert', fn);
|
||
};
|
||
}
|
||
|
||
private reportModified(node: INode, owner: INode, options = {}) {
|
||
if (!node) {
|
||
return;
|
||
}
|
||
if (node.isRootNode) {
|
||
return;
|
||
}
|
||
const callbacks = owner.componentMeta?.advanced.callbacks;
|
||
if (callbacks?.onSubtreeModified) {
|
||
try {
|
||
callbacks?.onSubtreeModified.call(
|
||
node.internalToShellNode(),
|
||
owner.internalToShellNode(),
|
||
options,
|
||
);
|
||
} catch (e) {
|
||
console.error('error when execute advanced.callbacks.onSubtreeModified', e);
|
||
}
|
||
}
|
||
|
||
if (owner.parent && !owner.parent.isRootNode) {
|
||
this.reportModified(node, owner.parent, { ...options, propagated: true });
|
||
}
|
||
}
|
||
}
|