import { obx, computed } from '@ali/lowcode-editor-core'; import { isDOMText, isJSExpression, NodeSchema, PropsMap, PropsList, NodeData, TitleContent, I18nData, SlotSchema, PageSchema, ComponentSchema, NodeStatus, } from '@ali/lowcode-types'; import { Props, EXTRA_KEY_PREFIX } from './props/props'; import { DocumentModel } from '../document-model'; import { NodeChildren } from './node-children'; import { Prop } from './props/prop'; import { ComponentMeta } from '../../component-meta'; import { ExclusiveGroup, isExclusiveGroup } from './exclusive-group'; import { TransformStage } from './transform-stage'; import { ReactElement } from 'react'; /** * 基础节点 * * [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 * locked can not select/hover/ item on canvas but can control on 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 * ignore * locked * hidden */ export class Node { /** * 是节点实例 */ readonly isNode = true; /** * 节点 id */ readonly id: string; /** * 节点组件类型 * 特殊节点: * * Page 页面 * * Block 区块 * * Component 组件/元件 * * Fragment 碎片节点,无 props,有指令 * * Leaf 文字节点 | 表达式节点,无 props,无指令? * * Slot 插槽节点,无 props,正常 children,有 slotArgs,有指令 */ readonly componentName: string; /** * 属性抽象 */ readonly props: Props; protected _children?: NodeChildren; /** * @deprecated */ private _addons: { [key: string]: any } = {}; @obx.ref private _parent: ParentalNode | null = null; /** * 父级节点 */ get parent(): ParentalNode | null { return this._parent; } /** * 当前节点子集 */ get children(): NodeChildren | null { return this._children || null; } /** * 当前节点深度 */ @computed get zLevel(): number { if (this._parent) { return this._parent.zLevel + 1; } return 0; } @computed get title(): string | I18nData | ReactElement { let t = this.getExtraProp('title'); 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; } constructor(readonly document: DocumentModel, nodeSchema: Schema) { const { componentName, id, children, props, ...extras } = nodeSchema; this.id = id || `node$${document.nextId()}`; 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 ParentalNode, this.initialChildren(children)); this._children.interalInitParent(); this.props.import(this.transformProps(props || {}), extras); } } private transformProps(props: any): any { // FIXME! support PropsList const x = this.document.designer.transformProps(props, this, TransformStage.Init); return x; // TODO: run transducers in metadata.experimental } private initialChildren(children: any): NodeData[] { // FIXME! this is dirty code if (children == null) { const initialChildren = this.componentMeta.getMetadata().experimental?.initialChildren; if (initialChildren) { if (typeof initialChildren === 'function') { return initialChildren(this as any) || []; } return initialChildren; } } return children || []; } isContainer(): boolean { return this.isParental() && this.componentMeta.isContainer; } isRoot(): boolean { return this.document.rootNode == (this as any); } isPage(): boolean { return this.isRoot() && this.componentName === 'Page'; } isComponent(): boolean { return this.isRoot() && this.componentName === 'Component'; } isSlot(): boolean { return this._slotFor != null && this.componentName === 'Slot'; } /** * 是否一个父亲类节点 */ isParental(): this is ParentalNode { return !this.isLeaf(); } /** * 终端节点,内容一般为 文字 或者 表达式 */ isLeaf(): this is LeafNode { return this.componentName === 'Leaf'; } internalSetWillPurge() { this.internalSetParent(null); this.document.addWillPurge(this); } /** * 内部方法,请勿使用 */ internalSetParent(parent: ParentalNode | null) { if (this._parent === parent) { return; } if (this._parent) { if (this.isSlot()) { this._parent.removeSlot(this, false); } else { this._parent.children.delete(this); } } this._parent = parent; if (parent) { this.document.removeWillPurge(this); if (!this.conditionGroup) { // initial conditionGroup const grp = this.getExtraProp('conditionGroup', false)?.getAsString(); if (grp) { this.setConditionGroup(grp); } } } } private _slotFor?: Prop | null = null; internalSetSlotFor(slotFor: Prop | null | undefined) { this._slotFor = slotFor; } /** * 关联属性 */ get slotFor() { return this._slotFor; } /** * 移除当前节点 */ remove() { if (this.parent) { if (this.isSlot()) { this.parent.removeSlot(this, true); } else { this.parent.children.delete(this, true); } } } /** * 选择当前节点 */ 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(): ComponentMeta { return this.document.getComponentMeta(this.componentName); } @computed get propsData(): PropsMap | PropsList | null { if (!this.isParental() || this.componentName === 'Fragment') { return null; } return this.props.export(TransformStage.Serilize).props || null; } @obx.val _slots: Node[] = []; @computed hasSlots() { return this._slots.length > 0; } get slots() { return this._slots; } @obx.ref private _conditionGroup: ExclusiveGroup | null = null; get conditionGroup(): ExclusiveGroup | null { return this._conditionGroup; } setConditionGroup(grp: ExclusiveGroup | string | 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 { grp = new ExclusiveGroup(grp); } } if (this._conditionGroup !== grp) { this.getExtraProp('conditionGroup', true)?.setValue(grp.name); if (this._conditionGroup) { this._conditionGroup.remove(this); } this._conditionGroup = grp; grp.add(this); } } @computed isConditionalVisible(): boolean | undefined { return this._conditionGroup?.isVisible(this); } setConditionalVisible() { this._conditionGroup?.setVisible(this); } @computed hasCondition() { const v = this.getExtraProp('condition', false)?.getValue(); return v != null && v !== '' && v !== true; } @computed hasLoop() { const v = this.getExtraProp('loop', false)?.getValue(); return v != null && v !== ''; } wrapWith(schema: Schema) { // todo } replaceWith(schema: Schema, migrate = true) { // reuse the same id? or replaceSelection // } getProp(path: string, stash = true): Prop | null { return this.props.query(path, stash as any) || null; } getExtraProp(key: string, stash = true): Prop | null { return this.props.get(EXTRA_KEY_PREFIX + key, stash) || null; } /** * 获取单个属性值 */ 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: PropsMap) { this.props.merge(props); } /** * 设置多个属性值,替换原有值 */ setProps(props?: PropsMap | PropsList | null) { this.props.import(props); } /** * 获取节点在父容器中的索引 */ @computed get index(): number { if (!this.parent) { return -1; } return this.parent.children.indexOf(this); } /** * 获取下一个兄弟节点 */ get nextSibling(): Node | null { if (!this.parent) { return null; } const index = this.index; if (index < 0) { return null; } return this.parent.children.get(index + 1); } /** * 获取上一个兄弟节点 */ get prevSibling(): Node | null { if (!this.parent) { return null; } const index = this.index; if (index < 1) { return null; } return this.parent.children.get(index - 1); } /** * 获取符合搭建协议-节点 schema 结构 */ get schema(): Schema { return this.export(TransformStage.Save); } set schema(data: Schema) { this.import(data); } import(data: Schema, checkId = false) { const { componentName, id, children, props, ...extras } = data; if (this.isParental()) { this.props.import(props, extras); (this._children as NodeChildren).import(children, checkId); } else { this.props.get('children', true)!.setValue(isDOMText(children) || isJSExpression(children) ? children : ''); } } /** * 导出 schema */ export(stage: TransformStage = TransformStage.Save): Schema { // run transducers // run const baseSchema: any = { componentName: this.componentName, }; if (stage !== TransformStage.Save) { baseSchema.id = this.id; } if (this.isLeaf()) { baseSchema.children = this.props.get('children')?.export(stage); return baseSchema; } const { props = {}, extras } = this.props.export(stage) || {}; const _extras_: { [key: string]: any } = { ...extras, }; if (_extras_) { Object.keys(_extras_).forEach((key) => { const addon = this._addons[key]; if (addon) { _extras_[key] = addon(); } }); } const schema: any = { ...baseSchema, props: this.document.designer.transformProps(props, this, stage), ...this.document.designer.transformProps(_extras_, this, stage), }; if (this.isParental() && this.children.size > 0) { schema.children = this.children.export(stage); } return schema; } /** * 判断是否包含特定节点 */ contains(node: Node): boolean { return contains(this, node); } /** * 获取特定深度的父亲节点 */ getZLevelTop(zLevel: number): Node | 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: Node): PositionNO { return comparePosition(this, otherNode); } /** * 删除一个Slot节点 */ removeSlot(slotNode: Node, purge = false): boolean { const i = this._slots.indexOf(slotNode); if (i < 0) { return false; } const deleted = this._slots.splice(i, 1)[0]; if (purge) { // should set parent null deleted.internalSetParent(null); deleted.purge(); } return false; } addSlot(slotNode: Node) { slotNode.internalSetParent(this as ParentalNode); this._slots.push(slotNode); } private purged = false; /** * 是否已销毁 */ get isPurged() { return this.purged; } /** * 销毁 */ purge() { if (this.purged) { return; } if (this._parent) { // should remove thisNode before purge this.remove(); return; } this.purged = true; if (this.isParental()) { this.children.purge(); } this.props.purge(); this.document.internalRemoveAndPurgeNode(this); } // ======= compatible apis ==== isEmpty(): boolean { return this.children ? this.children.isEmpty() : true; } getChildren() { return this.children; } getComponentName() { return this.componentName; } insertBefore(node: Node, ref?: Node) { this.children?.insert(node, ref ? ref.index : null); } insertAfter(node: Node, ref?: Node) { this.children?.insert(node, ref ? ref.index + 1 : null); } 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: () => void) { return this.children?.onChange(fn); } mergeChildren(remover: () => any, adder: (children: Node[]) => NodeData[] | null, sorter: () => any) { this.children?.mergeChildren(remover, adder, sorter); } @obx.val status: NodeStatus = { inPlaceEditing: false, locking: false, pseudo: false, }; /** * @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; } /** * @deprecated */ getSuitablePlace(node: Node, ref: any): any { if (this.isRoot()) { return { container: this, ref }; } return { container: this.parent, ref: this }; } /** * @deprecated */ getAddonData(key: string) { const addon = this._addons[key]; if (addon) { return addon(); } return this.getExtraProp(key)?.value; } /** * @deprecated */ registerAddon(key: string, exportData: any) { if (this._addons[key]) { throw new Error(`node addon ${key} exist`); } this._addons[key] = exportData; } getRect(): DOMRect | null { if (this.isRoot()) { return this.document.simulator?.viewport.contentBounds || null; } return this.document.simulator?.computeRect(this) || null; } getPrototype() { return this; } getIcon() { return this.icon; } toString() { return this.id; } } export interface ParentalNode extends Node { readonly children: NodeChildren; } export interface LeafNode extends Node { readonly children: null; } export type SlotNode = ParentalNode; export type PageNode = ParentalNode; export type ComponentNode = ParentalNode; export type RootNode = PageNode | ComponentNode; export function isNode(node: any): node is Node { return node && node.isNode; } export function isRootNode(node: Node): node is RootNode { return node && node.isRoot(); } export function getZLevelTop(child: Node, zLevel: number): Node | 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; } export function contains(node1: Node, node2: Node): boolean { if (node1 === node2) { return true; } if (!node1.isParental || !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: Node, node2: Node): 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: ParentalNode, thing: Node | NodeData, at?: number | null, copy?: boolean): Node { let node: Node; if (isNode(thing) && (copy || thing.isSlot())) { thing = thing.export(TransformStage.Save); } if (isNode(thing)) { node = thing; } else { node = container.document.createNode(thing); } container.children.insert(node, at); return node; } export function insertChildren( container: ParentalNode, nodes: Node[] | NodeData[], at?: number | null, copy?: boolean, ): Node[] { let index = at; let node: any; const results: Node[] = []; // tslint:disable-next-line while ((node = nodes.pop())) { node = insertChild(container, node, index, copy); results.push(node); index = node.index; } return results; }