packages/designer/src/document/node/node.ts (1,025 lines of code) (raw):
import { ReactElement } from 'react';
import { obx, computed, autorun, makeObservable, runInAction, wrapWithEventSwitch, action, createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core';
import {
IPublicTypeNodeSchema,
IPublicTypePropsMap,
IPublicTypePropsList,
IPublicTypeNodeData,
IPublicTypeI18nData,
IPublicTypeSlotSchema,
IPublicTypePageSchema,
IPublicTypeComponentSchema,
IPublicTypeCompositeValue,
GlobalEvent,
IPublicTypeComponentAction,
IPublicModelNode,
IPublicModelExclusiveGroup,
IPublicEnumTransformStage,
IPublicTypeDisposable,
IBaseModelNode,
} from '@alilc/lowcode-types';
import { compatStage, isDOMText, isJSExpression, isNode, isNodeSchema } from '@alilc/lowcode-utils';
import { ISettingTopEntry } from '@alilc/lowcode-designer';
import { Props, getConvertedExtraKey, IProps } from './props/props';
import type { IDocumentModel } from '../document-model';
import { NodeChildren, INodeChildren } from './node-children';
import { IProp, Prop } from './props/prop';
import type { IComponentMeta } from '../../component-meta';
import { ExclusiveGroup, isExclusiveGroup } from './exclusive-group';
import type { IExclusiveGroup } from './exclusive-group';
import { includeSlot, removeSlot } from '../../utils/slot';
import { foreachReverse } from '../../utils/tree';
import { NodeRemoveOptions, EDITOR_EVENT } from '../../types';
export interface NodeStatus {
locking: boolean;
pseudo: boolean;
inPlaceEditing: boolean;
}
export interface IBaseNode<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema> extends Omit<IBaseModelNode<
IDocumentModel,
IBaseNode,
INodeChildren,
IComponentMeta,
ISettingTopEntry,
IProps,
IProp,
IExclusiveGroup
>,
'isRoot' |
'isPage' |
'isComponent' |
'isModal' |
'isSlot' |
'isParental' |
'isLeaf' |
'settingEntry' |
// 在内部的 node 模型中不存在
'getExtraPropValue' |
'setExtraPropValue' |
'exportSchema' |
'visible' |
'importSchema' |
// 内外实现有差异
'isContainer' |
'isEmpty'
> {
isNode: boolean;
get componentMeta(): IComponentMeta;
get settingEntry(): ISettingTopEntry;
get isPurged(): boolean;
get index(): number | undefined;
get isPurging(): boolean;
getId(): string;
getParent(): INode | null;
/**
* 内部方法,请勿使用
* @param useMutator 是否触发联动逻辑
*/
internalSetParent(parent: INode | null, useMutator?: boolean): void;
setConditionGroup(grp: IPublicModelExclusiveGroup | string | null): void;
internalToShellNode(): IPublicModelNode | null;
internalPurgeStart(): void;
unlinkSlot(slotNode: INode): void;
/**
* 导出 schema
*/
export<T = Schema>(stage: IPublicEnumTransformStage, options?: any): T;
emitPropChange(val: IPublicTypePropChangeOptions): void;
import(data: Schema, checkId?: boolean): void;
internalSetSlotFor(slotFor: Prop | null | undefined): void;
addSlot(slotNode: INode): void;
onVisibleChange(func: (flag: boolean) => any): () => void;
getSuitablePlace(node: INode, ref: any): any;
onChildrenChange(fn: (param?: { type: string; node: INode }) => void): IPublicTypeDisposable | undefined;
onPropChange(func: (info: IPublicTypePropChangeOptions) => void): IPublicTypeDisposable;
isModal(): boolean;
isRoot(): boolean;
isPage(): boolean;
isComponent(): boolean;
isSlot(): boolean;
isParental(): boolean;
isLeaf(): boolean;
isContainer(): boolean;
isEmpty(): boolean;
remove(
useMutator?: boolean,
purge?: boolean,
options?: NodeRemoveOptions,
): void;
didDropIn(dragment: INode): void;
didDropOut(dragment: INode): void;
purge(): void;
removeSlot(slotNode: INode): boolean;
setVisible(flag: boolean): void;
getVisible(): boolean;
getChildren(): INodeChildren | null;
clearPropValue(path: string | number): void;
setProps(props?: IPublicTypePropsMap | IPublicTypePropsList | Props | null): void;
mergeProps(props: IPublicTypePropsMap): void;
/** 是否可以选中 */
canSelect(): boolean;
}
/**
* 基础节点
*
* [Node Properties]
* componentName: Page/Block/Component
* props
* children
*
* [Directives]
* loop
* loopArgs
* condition
* ------- addition support -----
* conditionGroup use for condition, for exclusive
* title display on outline
* ignored ignore this node will not publish to render, but will store
* isLocked can not select/hover/ item on canvas and outline
* hidden not visible on canvas
* slotArgs like loopArgs, for slot node
*
* 根容器节点
*
* [Node Properties]
* componentName: Page/Block/Component
* props
* children
*
* [Root Container Extra Properties]
* fileName
* meta
* state
* defaultProps
* dataSource
* lifeCycles
* methods
* css
*
* [Directives **not used**]
* loop
* loopArgs
* condition
* ------- future support -----
* conditionGroup
* title
* ignored
* isLocked
* hidden
*/
export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema> implements IBaseNode {
private emitter: IEventBus;
/**
* 是节点实例
*/
readonly isNode = true;
/**
* 节点 id
*/
readonly id: string;
/**
* 节点组件类型
* 特殊节点:
* * Page 页面
* * Block 区块
* * Component 组件/元件
* * Fragment 碎片节点,无 props,有指令
* * Leaf 文字节点 | 表达式节点,无 props,无指令?
* * Slot 插槽节点,无 props,正常 children,有 slotArgs,有指令
*/
readonly componentName: string;
/**
* 属性抽象
*/
props: IProps;
protected _children?: INodeChildren;
/**
* @deprecated
*/
private _addons: { [key: string]: { exportData: () => any; isProp: boolean } } = {};
@obx.ref private _parent: INode | null = null;
/**
* 父级节点
*/
get parent(): INode | null {
return this._parent;
}
/**
* 当前节点子集
*/
get children(): INodeChildren | null {
return this._children || null;
}
/**
* 当前节点深度
*/
@computed get zLevel(): number {
if (this._parent) {
return this._parent.zLevel + 1;
}
return 0;
}
@computed get title(): string | IPublicTypeI18nData | ReactElement {
let t = this.getExtraProp('title');
// TODO: 暂时走不到这个分支
// if (!t && this.componentMeta.descriptor) {
// t = this.getProp(this.componentMeta.descriptor, false);
// }
if (t) {
const v = t.getAsString();
if (v) {
return v;
}
}
return this.componentMeta.title;
}
get icon() {
return this.componentMeta.icon;
}
isInited = false;
_settingEntry: ISettingTopEntry;
get settingEntry(): ISettingTopEntry {
if (this._settingEntry) return this._settingEntry;
this._settingEntry = this.document.designer.createSettingEntry([this]);
return this._settingEntry;
}
private autoruns?: Array<() => void>;
private _isRGLContainer = false;
set isRGLContainer(status: boolean) {
this._isRGLContainer = status;
}
get isRGLContainer(): boolean {
return !!this._isRGLContainer;
}
set isRGLContainerNode(status: boolean) {
this._isRGLContainer = status;
}
get isRGLContainerNode(): boolean {
return !!this._isRGLContainer;
}
get isEmptyNode() {
return this.isEmpty();
}
private _slotFor?: IProp | null | undefined = null;
@obx.shallow _slots: INode[] = [];
get slots(): INode[] {
return this._slots;
}
/* istanbul ignore next */
@obx.ref private _conditionGroup: IExclusiveGroup | null = null;
/* istanbul ignore next */
get conditionGroup(): IExclusiveGroup | null {
return this._conditionGroup;
}
private purged = false;
/**
* 是否已销毁
*/
get isPurged() {
return this.purged;
}
private purging: boolean = false;
/**
* 是否正在销毁
*/
get isPurging() {
return this.purging;
}
@obx.shallow status: NodeStatus = {
inPlaceEditing: false,
locking: false,
pseudo: false,
};
constructor(readonly document: IDocumentModel, nodeSchema: Schema) {
makeObservable(this);
const { componentName, id, children, props, ...extras } = nodeSchema;
this.id = document.nextId(id);
this.componentName = componentName;
if (this.componentName === 'Leaf') {
this.props = new Props(this, {
children: isDOMText(children) || isJSExpression(children) ? children : '',
});
} else {
this.props = new Props(this, props, extras);
this._children = new NodeChildren(this as INode, this.initialChildren(children));
this._children.internalInitParent();
this.props.merge(
this.upgradeProps(this.initProps(props || {})),
this.upgradeProps(extras),
);
this.setupAutoruns();
}
this.initBuiltinProps();
this.isInited = true;
this.emitter = createModuleEventBus('Node');
const { editor } = this.document.designer;
this.onVisibleChange((visible: boolean) => {
editor?.eventBus.emit(EDITOR_EVENT.NODE_VISIBLE_CHANGE, this, visible);
});
this.onChildrenChange((info?: { type: string; node: INode }) => {
editor?.eventBus.emit(EDITOR_EVENT.NODE_CHILDREN_CHANGE, {
type: info?.type,
node: this,
});
});
}
/**
* 节点初始化期间就把内置的一些 prop 初始化好,避免后续不断构造实例导致 reaction 执行多次
*/
@action
private initBuiltinProps() {
this.props.has(getConvertedExtraKey('hidden')) || this.props.add(false, getConvertedExtraKey('hidden'));
this.props.has(getConvertedExtraKey('title')) || this.props.add('', getConvertedExtraKey('title'));
this.props.has(getConvertedExtraKey('isLocked')) || this.props.add(false, getConvertedExtraKey('isLocked'));
this.props.has(getConvertedExtraKey('condition')) || this.props.add(true, getConvertedExtraKey('condition'));
this.props.has(getConvertedExtraKey('conditionGroup')) || this.props.add('', getConvertedExtraKey('conditionGroup'));
this.props.has(getConvertedExtraKey('loop')) || this.props.add(undefined, getConvertedExtraKey('loop'));
}
@action
private initProps(props: any): any {
return this.document.designer.transformProps(props, this, IPublicEnumTransformStage.Init);
}
@action
private upgradeProps(props: any): any {
return this.document.designer.transformProps(props, this, IPublicEnumTransformStage.Upgrade);
}
private setupAutoruns() {
const { autoruns } = this.componentMeta.advanced;
if (!autoruns || autoruns.length < 1) {
return;
}
this.autoruns = autoruns.map((item) => {
return autorun(() => {
item.autorun(this.props.getNode().settingEntry.get(item.name)?.internalToShellField());
});
});
}
private initialChildren(children: IPublicTypeNodeData | IPublicTypeNodeData[] | undefined): IPublicTypeNodeData[] {
const { initialChildren } = this.componentMeta.advanced;
if (children == null) {
if (initialChildren) {
if (typeof initialChildren === 'function') {
return initialChildren(this.internalToShellNode()!) || [];
}
return initialChildren;
}
return [];
}
if (Array.isArray(children)) {
return children;
}
return [children];
}
isContainer(): boolean {
return this.isContainerNode;
}
get isContainerNode(): boolean {
return this.isParentalNode && this.componentMeta.isContainer;
}
isModal(): boolean {
return this.isModalNode;
}
get isModalNode(): boolean {
return this.componentMeta.isModal;
}
isRoot(): boolean {
return this.isRootNode;
}
get isRootNode(): boolean {
return this.document.rootNode === (this as any);
}
isPage(): boolean {
return this.isPageNode;
}
get isPageNode(): boolean {
return this.isRootNode && this.componentName === 'Page';
}
isComponent(): boolean {
return this.isComponentNode;
}
get isComponentNode(): boolean {
return this.isRootNode && this.componentName === 'Component';
}
isSlot(): boolean {
return this.isSlotNode;
}
get isSlotNode(): boolean {
return this._slotFor != null && this.componentName === 'Slot';
}
/**
* 是否一个父亲类节点
*/
isParental(): boolean {
return this.isParentalNode;
}
get isParentalNode(): boolean {
return !this.isLeafNode;
}
/**
* 终端节点,内容一般为 文字 或者 表达式
*/
isLeaf(): boolean {
return this.isLeafNode;
}
get isLeafNode(): boolean {
return this.componentName === 'Leaf';
}
internalSetWillPurge() {
this.internalSetParent(null);
this.document.addWillPurge(this);
}
didDropIn(dragment: INode) {
const { callbacks } = this.componentMeta.advanced;
if (callbacks?.onNodeAdd) {
const cbThis = this.internalToShellNode();
callbacks?.onNodeAdd.call(cbThis, dragment.internalToShellNode(), cbThis);
}
if (this._parent) {
this._parent.didDropIn(dragment);
}
}
didDropOut(dragment: INode) {
const { callbacks } = this.componentMeta.advanced;
if (callbacks?.onNodeRemove) {
const cbThis = this.internalToShellNode();
callbacks?.onNodeRemove.call(cbThis, dragment.internalToShellNode(), cbThis);
}
if (this._parent) {
this._parent.didDropOut(dragment);
}
}
/**
* 内部方法,请勿使用
* @param useMutator 是否触发联动逻辑
*/
internalSetParent(parent: INode | null, useMutator = false) {
if (this._parent === parent) {
return;
}
// 解除老的父子关系,但不需要真的删除节点
if (this._parent) {
if (this.isSlot()) {
this._parent.unlinkSlot(this);
} else {
this._parent.children?.unlinkChild(this);
}
}
if (useMutator) {
this._parent?.didDropOut(this);
}
if (parent) {
// 建立新的父子关系,尤其注意:对于 parent 为 null 的场景,不会赋值,因为 subtreeModified 等事件可能需要知道该 node 被删除前的父子关系
this._parent = parent;
this.document.removeWillPurge(this);
/* istanbul ignore next */
if (!this.conditionGroup) {
// initial conditionGroup
const grp = this.getExtraProp('conditionGroup', false)?.getAsString();
if (grp) {
this.setConditionGroup(grp);
}
}
if (useMutator) {
parent.didDropIn(this);
}
}
}
internalSetSlotFor(slotFor: Prop | null | undefined) {
this._slotFor = slotFor;
}
internalToShellNode(): IPublicModelNode | null {
return this.document.designer.shellModelFactory.createNode(this);
}
/**
* 关联属性
*/
get slotFor(): IProp | null | undefined {
return this._slotFor;
}
/**
* 移除当前节点
*/
remove(
useMutator = true,
purge = true,
options: NodeRemoveOptions = { suppressRemoveEvent: false },
) {
if (this.parent) {
if (!options.suppressRemoveEvent) {
this.document.designer.editor?.eventBus.emit('node.remove.topLevel', {
node: this,
index: this.parent?.children?.indexOf(this),
});
}
if (this.isSlot()) {
this.parent.removeSlot(this);
this.parent.children?.internalDelete(this, purge, useMutator, { suppressRemoveEvent: true });
} else {
this.parent.children?.internalDelete(this, purge, useMutator, { suppressRemoveEvent: true });
}
}
}
/**
* 锁住当前节点
*/
lock(flag = true) {
this.setExtraProp('isLocked', flag);
}
/**
* 获取当前节点的锁定状态
*/
get isLocked(): boolean {
return !!this.getExtraProp('isLocked')?.getValue();
}
canSelect(): boolean {
const onSelectHook = this.componentMeta?.advanced?.callbacks?.onSelectHook;
const canSelect = typeof onSelectHook === 'function' ? onSelectHook(this.internalToShellNode()!) : true;
return canSelect;
}
/**
* 选择当前节点
*/
select() {
this.document.selection.select(this.id);
}
/**
* 悬停高亮
*/
hover(flag = true) {
if (flag) {
this.document.designer.detecting.capture(this);
} else {
this.document.designer.detecting.release(this);
}
}
/**
* 节点组件描述
*/
@computed get componentMeta(): IComponentMeta {
return this.document.getComponentMeta(this.componentName);
}
@computed get propsData(): IPublicTypePropsMap | IPublicTypePropsList | null {
if (!this.isParental() || this.componentName === 'Fragment') {
return null;
}
return this.props.export(IPublicEnumTransformStage.Serilize).props || null;
}
hasSlots() {
return this._slots.length > 0;
}
/* istanbul ignore next */
setConditionGroup(grp: IPublicModelExclusiveGroup | string | null) {
let _grp: IExclusiveGroup | null = null;
if (!grp) {
this.getExtraProp('conditionGroup', false)?.remove();
if (this._conditionGroup) {
this._conditionGroup.remove(this);
this._conditionGroup = null;
}
return;
}
if (!isExclusiveGroup(grp)) {
if (this.prevSibling?.conditionGroup?.name === grp) {
_grp = this.prevSibling.conditionGroup;
} else if (this.nextSibling?.conditionGroup?.name === grp) {
_grp = this.nextSibling.conditionGroup;
} else if (typeof grp === 'string') {
_grp = new ExclusiveGroup(grp);
}
}
if (_grp && this._conditionGroup !== _grp) {
this.getExtraProp('conditionGroup', true)?.setValue(_grp.name);
if (this._conditionGroup) {
this._conditionGroup.remove(this);
}
this._conditionGroup = _grp;
_grp?.add(this);
}
}
/* istanbul ignore next */
isConditionalVisible(): boolean | undefined {
return this._conditionGroup?.isVisible(this);
}
/* istanbul ignore next */
setConditionalVisible() {
this._conditionGroup?.setVisible(this);
}
hasCondition() {
const v = this.getExtraProp('condition', false)?.getValue();
return v != null && v !== '' && v !== true;
}
/**
* has loop when 1. loop is validArray with length > 1 ; OR 2. loop is variable object
* @return boolean, has loop config or not
*/
hasLoop() {
const value = this.getExtraProp('loop', false)?.getValue();
if (value === undefined || value === null) {
return false;
}
if (Array.isArray(value)) {
return true;
}
if (isJSExpression(value)) {
return true;
}
return false;
}
/* istanbul ignore next */
wrapWith(schema: Schema) {
const wrappedNode = this.replaceWith({ ...schema, children: [this.export()] });
return wrappedNode.children!.get(0);
}
replaceWith(schema: Schema, migrate = false): any {
// reuse the same id? or replaceSelection
schema = Object.assign({}, migrate ? this.export() : {}, schema);
return this.parent?.replaceChild(this, schema);
}
/**
* 替换子节点
*
* @param {INode} node
* @param {object} data
*/
replaceChild(node: INode, data: any): INode | null {
if (this.children?.has(node)) {
const selected = this.document.selection.has(node.id);
delete data.id;
const newNode = this.document.createNode(data);
if (!isNode(newNode)) {
return null;
}
this.insertBefore(newNode, node, false);
node.remove(false);
if (selected) {
this.document.selection.select(newNode.id);
}
return newNode;
}
return node;
}
setVisible(flag: boolean): void {
this.getExtraProp('hidden')?.setValue(!flag);
this.emitter.emit('visibleChange', flag);
}
getVisible(): boolean {
return !this.getExtraProp('hidden')?.getValue();
}
onVisibleChange(func: (flag: boolean) => any): () => void {
const wrappedFunc = wrapWithEventSwitch(func);
this.emitter.on('visibleChange', wrappedFunc);
return () => {
this.emitter.removeListener('visibleChange', wrappedFunc);
};
}
getProp(path: string, createIfNone = true): IProp | null {
return this.props.query(path, createIfNone) || null;
}
getExtraProp(key: string, createIfNone = true): IProp | null {
return this.props.get(getConvertedExtraKey(key), createIfNone) || null;
}
setExtraProp(key: string, value: IPublicTypeCompositeValue) {
this.getProp(getConvertedExtraKey(key), true)?.setValue(value);
}
/**
* 获取单个属性值
*/
getPropValue(path: string): any {
return this.getProp(path, false)?.value;
}
/**
* 设置单个属性值
*/
setPropValue(path: string, value: any) {
this.getProp(path, true)!.setValue(value);
}
/**
* 清除已设置的值
*/
clearPropValue(path: string): void {
this.getProp(path, false)?.unset();
}
/**
* 设置多个属性值,和原有值合并
*/
mergeProps(props: IPublicTypePropsMap) {
this.props.merge(props);
}
/**
* 设置多个属性值,替换原有值
*/
setProps(props?: IPublicTypePropsMap | IPublicTypePropsList | Props | null) {
if (props instanceof Props) {
this.props = props;
return;
}
this.props.import(props);
}
/**
* 获取节点在父容器中的索引
*/
@computed get index(): number | undefined {
if (!this.parent) {
return -1;
}
return this.parent.children?.indexOf(this);
}
/**
* 获取下一个兄弟节点
*/
get nextSibling(): INode | null | undefined {
if (!this.parent) {
return null;
}
const { index } = this;
if (typeof index !== 'number') {
return null;
}
if (index < 0) {
return null;
}
return this.parent.children?.get(index + 1);
}
/**
* 获取上一个兄弟节点
*/
get prevSibling(): INode | null | undefined {
if (!this.parent) {
return null;
}
const { index } = this;
if (typeof index !== 'number') {
return null;
}
if (index < 1) {
return null;
}
return this.parent.children?.get(index - 1);
}
/**
* 获取符合搭建协议-节点 schema 结构
*/
get schema(): Schema {
return this.export(IPublicEnumTransformStage.Save);
}
set schema(data: Schema) {
runInAction(() => this.import(data));
}
import(data: Schema, checkId = false) {
const { componentName, id, children, props, ...extras } = data;
if (this.isSlot()) {
foreachReverse(
this.children!,
(subNode: INode) => {
subNode.remove(true, true);
},
(iterable, idx) => (iterable as INodeChildren).get(idx),
);
}
if (this.isParental()) {
this.props.import(props, extras);
this._children?.import(children, checkId);
} else {
this.props
.get('children', true)!
.setValue(isDOMText(children) || isJSExpression(children) ? children : '');
}
}
toData() {
return this.export();
}
/**
* 导出 schema
*/
export<T = IPublicTypeNodeSchema>(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save, options: any = {}): T {
stage = compatStage(stage);
const baseSchema: any = {
componentName: this.componentName,
};
if (stage !== IPublicEnumTransformStage.Clone) {
baseSchema.id = this.id;
}
if (stage === IPublicEnumTransformStage.Render) {
baseSchema.docId = this.document.id;
}
if (this.isLeaf()) {
if (!options.bypassChildren) {
baseSchema.children = this.props.get('children')?.export(stage);
}
return baseSchema;
}
const { props = {}, extras } = this.props.export(stage) || {};
const _extras_: { [key: string]: any } = {
...extras,
};
/* istanbul ignore next */
Object.keys(this._addons).forEach((key) => {
const addon = this._addons[key];
if (addon) {
if (addon.isProp) {
(props as any)[getConvertedExtraKey(key)] = addon.exportData();
} else {
_extras_[key] = addon.exportData();
}
}
});
const schema: any = {
...baseSchema,
props: this.document.designer.transformProps(props, this, stage),
...this.document.designer.transformProps(_extras_, this, stage),
};
if (this.isParental() && this.children && this.children.size > 0 && !options.bypassChildren) {
schema.children = this.children.export(stage);
}
return schema;
}
/**
* 判断是否包含特定节点
*/
contains(node: INode): boolean {
return contains(this, node);
}
/**
* 获取特定深度的父亲节点
*/
getZLevelTop(zLevel: number): INode | null {
return getZLevelTop(this, zLevel);
}
/**
* 判断与其它节点的位置关系
*
* 16 thisNode contains otherNode
* 8 thisNode contained_by otherNode
* 2 thisNode before or after otherNode
* 0 thisNode same as otherNode
*/
comparePosition(otherNode: INode): PositionNO {
return comparePosition(this, otherNode);
}
unlinkSlot(slotNode: INode) {
const i = this._slots.indexOf(slotNode);
if (i < 0) {
return false;
}
this._slots.splice(i, 1);
}
/**
* 删除一个Slot节点
*/
removeSlot(slotNode: INode): boolean {
// if (purge) {
// // should set parent null
// slotNode?.internalSetParent(null, false);
// slotNode?.purge();
// }
// this.document.unlinkNode(slotNode);
// this.document.selection.remove(slotNode.id);
const i = this._slots.indexOf(slotNode);
if (i < 0) {
return false;
}
this._slots.splice(i, 1);
return false;
}
addSlot(slotNode: INode) {
const slotName = slotNode?.getExtraProp('name')?.getAsString();
// 一个组件下的所有 slot,相同 slotName 的 slot 应该是唯一的
if (includeSlot(this, slotName)) {
removeSlot(this, slotName);
}
slotNode.internalSetParent(this as INode, true);
this._slots.push(slotNode);
}
/**
* 当前node对应组件是否已注册可用
*/
isValidComponent() {
const allComponents = this.document?.designer?.componentsMap;
if (allComponents && allComponents[this.componentName]) {
return true;
}
return false;
}
/**
* 删除一个节点
* @param node
*/
removeChild(node: INode) {
this.children?.delete(node);
}
/**
* 销毁
*/
purge() {
if (this.purged) {
return;
}
this.purged = true;
this.autoruns?.forEach((dispose) => dispose());
this.props.purge();
this.settingEntry?.purge();
// this.document.destroyNode(this);
}
internalPurgeStart() {
this.purging = true;
}
/**
* 是否可执行某 action
*/
canPerformAction(actionName: string): boolean {
const availableActions =
this.componentMeta?.availableActions?.filter((action: IPublicTypeComponentAction) => {
const { condition } = action;
return typeof condition === 'function' ?
condition(this) !== false :
condition !== false;
})
.map((action: IPublicTypeComponentAction) => action.name) || [];
return availableActions.indexOf(actionName) >= 0;
}
// ======= compatible apis ====
isEmpty(): boolean {
return this.children ? this.children.isEmpty() : true;
}
getChildren() {
return this.children;
}
getComponentName() {
return this.componentName;
}
insert(node: INode, ref?: INode, useMutator = true) {
this.insertAfter(node, ref, useMutator);
}
insertBefore(node: INode, ref?: INode, useMutator = true) {
const nodeInstance = ensureNode(node, this.document);
this.children?.internalInsert(nodeInstance, ref ? ref.index : null, useMutator);
}
insertAfter(node: any, ref?: INode, useMutator = true) {
const nodeInstance = ensureNode(node, this.document);
this.children?.internalInsert(nodeInstance, ref ? (ref.index || 0) + 1 : null, useMutator);
}
getParent() {
return this.parent;
}
getId() {
return this.id;
}
getIndex() {
return this.index;
}
getNode() {
return this;
}
getRoot() {
return this.document.rootNode;
}
getProps() {
return this.props;
}
onChildrenChange(fn: (param?: { type: string; node: INode }) => void): IPublicTypeDisposable | undefined {
const wrappedFunc = wrapWithEventSwitch(fn);
return this.children?.onChange(wrappedFunc);
}
mergeChildren(
remover: (node: INode, idx: number) => any,
adder: (children: INode[]) => IPublicTypeNodeData[] | null,
sorter: (firstNode: INode, secondNode: INode) => any,
) {
this.children?.mergeChildren(remover, adder, sorter);
}
/**
* @deprecated
*/
getStatus(field?: keyof NodeStatus) {
if (field && this.status[field] != null) {
return this.status[field];
}
return this.status;
}
/**
* @deprecated
*/
setStatus(field: keyof NodeStatus, flag: boolean) {
if (!this.status.hasOwnProperty(field)) {
return;
}
if (flag !== this.status[field]) {
this.status[field] = flag;
}
}
/**
* @deprecated
*/
getDOMNode(): any {
const instance = this.document.simulator?.getComponentInstances(this)?.[0];
if (!instance) {
return;
}
return this.document.simulator?.findDOMNodes(instance)?.[0];
}
/**
* @deprecated
*/
getPage() {
console.warn('getPage is deprecated, use document instead');
return this.document;
}
/**
* 获取磁贴相关信息
*/
getRGL(): {
isContainerNode: boolean;
isEmptyNode: boolean;
isRGLContainerNode: boolean;
isRGLNode: boolean;
isRGL: boolean;
rglNode: Node | null;
} {
const isContainerNode = this.isContainer();
const isEmptyNode = this.isEmpty();
const isRGLContainerNode = this.isRGLContainer;
const isRGLNode = (this.getParent()?.isRGLContainer) as boolean;
const isRGL = isRGLContainerNode || (isRGLNode && (!isContainerNode || !isEmptyNode));
let rglNode = isRGLContainerNode ? this : isRGL ? this?.getParent() : null;
return { isContainerNode, isEmptyNode, isRGLContainerNode, isRGLNode, isRGL, rglNode };
}
/**
* @deprecated no one is using this, will be removed in a future release
*/
getSuitablePlace(node: INode, ref: any): any {
const focusNode = this.document?.focusNode;
// 如果节点是模态框,插入到根节点下
if (node?.componentMeta?.isModal) {
return { container: focusNode, ref };
}
if (!ref && focusNode && this.contains(focusNode)) {
const rootCanDropIn = focusNode.componentMeta?.prototype?.options?.canDropIn;
if (
rootCanDropIn === undefined ||
rootCanDropIn === true ||
(typeof rootCanDropIn === 'function' && rootCanDropIn(node))
) {
return { container: focusNode };
}
return null;
}
if (this.isRoot() && this.children) {
const dropElement = this.children.filter((c) => {
if (!c.isContainerNode) {
return false;
}
const canDropIn = c.componentMeta?.prototype?.options?.canDropIn;
if (
canDropIn === undefined ||
canDropIn === true ||
(typeof canDropIn === 'function' && canDropIn(node))
) {
return true;
}
return false;
})[0];
if (dropElement) {
return { container: dropElement, ref };
}
const rootCanDropIn = this.componentMeta?.prototype?.options?.canDropIn;
if (
rootCanDropIn === undefined ||
rootCanDropIn === true ||
(typeof rootCanDropIn === 'function' && rootCanDropIn(node))
) {
return { container: this, ref };
}
return null;
}
const canDropIn = this.componentMeta?.prototype?.options?.canDropIn;
if (this.isContainer()) {
if (
canDropIn === undefined ||
(typeof canDropIn === 'boolean' && canDropIn) ||
(typeof canDropIn === 'function' && canDropIn(node))
) {
return { container: this, ref };
}
}
if (this.parent) {
return this.parent.getSuitablePlace(node, { index: this.index });
}
return null;
}
/**
* @deprecated
*/
getAddonData(key: string) {
const addon = this._addons[key];
if (addon) {
return addon.exportData();
}
return this.getExtraProp(key)?.getValue();
}
/**
* @deprecated
*/
registerAddon(key: string, exportData: () => any, isProp = false) {
this._addons[key] = { exportData, isProp };
}
getRect(): DOMRect | null {
if (this.isRoot()) {
return this.document.simulator?.viewport.contentBounds || null;
}
return this.document.simulator?.computeRect(this) || null;
}
/**
* @deprecated
*/
getPrototype() {
return this.componentMeta.prototype;
}
/**
* @deprecated
*/
setPrototype(proto: any) {
this.componentMeta.prototype = proto;
}
getIcon() {
return this.icon;
}
toString() {
return this.id;
}
emitPropChange(val: IPublicTypePropChangeOptions) {
this.emitter?.emit('propChange', val);
}
onPropChange(func: (info: IPublicTypePropChangeOptions) => void): IPublicTypeDisposable {
const wrappedFunc = wrapWithEventSwitch(func);
this.emitter.on('propChange', wrappedFunc);
return () => {
this.emitter.removeListener('propChange', wrappedFunc);
};
}
}
function ensureNode(node: any, document: IDocumentModel): INode {
let nodeInstance = node;
if (!isNode(node)) {
if (node.getComponentName) {
nodeInstance = document.createNode({
componentName: node.getComponentName(),
});
} else {
nodeInstance = document.createNode(node);
}
}
return nodeInstance;
}
export interface LeafNode extends Node {
readonly children: null;
}
export type IPublicTypePropChangeOptions = Omit<GlobalEvent.Node.Prop.ChangeOptions, 'node'>;
export type ISlotNode = IBaseNode<IPublicTypeSlotSchema>;
export type IPageNode = IBaseNode<IPublicTypePageSchema>;
export type IComponentNode = IBaseNode<IPublicTypeComponentSchema>;
export type IRootNode = IPageNode | IComponentNode;
export type INode = IPageNode | ISlotNode | IComponentNode | IRootNode;
export function isRootNode(node: INode): node is IRootNode {
return node && node.isRootNode;
}
export function isLowCodeComponent(node: INode): node is IComponentNode {
return node.componentMeta?.getMetadata().devMode === 'lowCode';
}
export function getZLevelTop(child: INode, zLevel: number): INode | null {
let l = child.zLevel;
if (l < zLevel || zLevel < 0) {
return null;
}
if (l === zLevel) {
return child;
}
let r: any = child;
while (r && l-- > zLevel) {
r = r.parent;
}
return r;
}
/**
* 测试两个节点是否为包含关系
* @param node1 测试的父节点
* @param node2 测试的被包含节点
* @returns 是否包含
*/
export function contains(node1: INode, node2: INode): boolean {
if (node1 === node2) {
return true;
}
if (!node1.isParentalNode || !node2.parent) {
return false;
}
const p = getZLevelTop(node2, node1.zLevel);
if (!p) {
return false;
}
return node1 === p;
}
// 16 node1 contains node2
// 8 node1 contained_by node2
// 2 node1 before or after node2
// 0 node1 same as node2
export enum PositionNO {
Contains = 16,
ContainedBy = 8,
BeforeOrAfter = 2,
TheSame = 0,
}
export function comparePosition(node1: INode, node2: INode): PositionNO {
if (node1 === node2) {
return PositionNO.TheSame;
}
const l1 = node1.zLevel;
const l2 = node2.zLevel;
if (l1 === l2) {
return PositionNO.BeforeOrAfter;
}
let p: any;
if (l1 < l2) {
p = getZLevelTop(node2, l1);
if (p && p === node1) {
return PositionNO.Contains;
}
return PositionNO.BeforeOrAfter;
}
p = getZLevelTop(node1, l2);
if (p && p === node2) {
return PositionNO.ContainedBy;
}
return PositionNO.BeforeOrAfter;
}
export function insertChild(
container: INode,
thing: INode | IPublicTypeNodeData,
at?: number | null,
copy?: boolean,
): INode | null {
let node: INode | null | IRootNode | undefined;
let nodeSchema: IPublicTypeNodeSchema;
if (isNode<INode>(thing) && (copy || thing.isSlot())) {
nodeSchema = thing.export(IPublicEnumTransformStage.Clone);
node = container.document?.createNode(nodeSchema);
} else if (isNode<INode>(thing)) {
node = thing;
} else if (isNodeSchema(thing)) {
node = container.document?.createNode(thing);
}
if (isNode<INode>(node)) {
container.children?.insert(node, at);
return node;
}
return null;
}
export function insertChildren(
container: INode,
nodes: INode[] | IPublicTypeNodeData[],
at?: number | null,
copy?: boolean,
): INode[] {
let index = at;
let node: any;
const results: INode[] = [];
// eslint-disable-next-line no-cond-assign
while ((node = nodes.pop())) {
node = insertChild(container, node, index, copy);
results.push(node);
index = node.index;
}
return results;
}