2020-05-05 18:40:58 +08:00

821 lines
18 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.

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<Schema extends NodeSchema = NodeSchema> {
/**
* 是节点实例
*/
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
return this.document.designer.transformProps(props, this, TransformStage.Init);
// 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(): this is RootNode {
return this.document.rootNode == (this as any);
}
isPage(): this is PageNode {
return this.isRoot() && this.componentName === 'Page';
}
isComponent(): this is ComponentNode {
return this.isRoot() && this.componentName === 'Component';
}
isSlot(): this is SlotNode {
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.isSlot() && this._parent) {
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.isSlot() && this.parent) {
this.parent.children.delete(this, true);
}
}
/**
* 选择当前节点
*/
select() {
this.document.selection.select(this.id);
}
/**
* 悬停高亮
*/
hover(flag = true) {
if (flag) {
this.document.designer.hovering.hover(this);
} else {
this.document.designer.hovering.unhover(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;
}
@computed hasSlots() {
for (const item of this.props) {
if (item.type === 'slot') {
return true;
}
}
return false;
}
@computed get slots() {
// TODO: optimize recore/obx, array maked every time, donot as changed
const slots: Node[] = [];
this.props.forEach((item) => {
if (item.type === 'slot') {
slots.push(item.slotNode!);
}
});
return 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);
}
/**
* 设置多个属性值,和原有值合并
*/
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),
..._extras_,
};
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);
}
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;
}
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;
}
toString() {
return this.id;
}
}
export interface ParentalNode<T extends NodeSchema = NodeSchema> extends Node<T> {
readonly children: NodeChildren;
}
export interface LeafNode extends Node {
readonly children: null;
}
export type SlotNode = ParentalNode<SlotSchema>;
export type PageNode = ParentalNode<PageSchema>;
export type ComponentNode = ParentalNode<ComponentSchema>;
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;
}