mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-15 05:36:39 +00:00
node 90%
This commit is contained in:
parent
ffeebdfad3
commit
b566524466
@ -1,6 +1,6 @@
|
||||
import Project from '../project';
|
||||
import { RootSchema, NodeData, isDOMText, isJSExpression } from '../schema';
|
||||
import Node from './node';
|
||||
import Node from './node/node';
|
||||
|
||||
export default class DocumentContext {
|
||||
/**
|
||||
@ -85,6 +85,7 @@ export default class DocumentContext {
|
||||
if (!node || !node.parent) {
|
||||
return;
|
||||
}
|
||||
// TODO: 考虑留着缓存
|
||||
this.nodesMap.delete(id);
|
||||
this.nodes.delete(node);
|
||||
node.parent.removeChild(node);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { INode, INodeParent } from './node';
|
||||
import { INode, INodeParent } from './node/node';
|
||||
import DocumentContext from './document-context';
|
||||
|
||||
export interface LocationData {
|
||||
|
||||
@ -23,7 +23,7 @@ import {
|
||||
contains,
|
||||
isRootNode,
|
||||
isConfettiNode,
|
||||
} from './node';
|
||||
} from './node/node';
|
||||
import {
|
||||
Point,
|
||||
Rect,
|
||||
|
||||
@ -1,490 +0,0 @@
|
||||
import { obx } from '@recore/obx';
|
||||
import { NodeSchema, NodeData, PropsMap, PropsList } from '../schema';
|
||||
import Props, { Prop } from './props';
|
||||
import DocumentContext from './document-context';
|
||||
|
||||
/**
|
||||
* nodeSchema are:
|
||||
* [basic]
|
||||
* .componentName
|
||||
* .props
|
||||
* .children
|
||||
* [directive]
|
||||
* .condition
|
||||
* .loop
|
||||
* .loopArgs
|
||||
* [addon]
|
||||
* .conditionGroup = 'abc' // 当移动时值会改
|
||||
* .title
|
||||
* .ignore
|
||||
* .hidden
|
||||
* .locked
|
||||
*/
|
||||
const DIRECTIVES = ['condition', 'conditionGroup', 'loop', 'loopArgs', 'title', 'ignore', 'hidden', 'locked'];
|
||||
export default class Node {
|
||||
/**
|
||||
* 是节点实例
|
||||
*/
|
||||
readonly isNode = true;
|
||||
|
||||
/**
|
||||
* 节点 id
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* 节点组件类型
|
||||
* 特殊节点:
|
||||
* * #text 文字节点
|
||||
* * #expression 表达式节点
|
||||
* * Page 页面
|
||||
* * Block/Fragment 区块
|
||||
* * Component 组件/元件
|
||||
*/
|
||||
readonly componentName: string;
|
||||
|
||||
readonly props?: Props<Node>;
|
||||
readonly directives?: Props<Node>;
|
||||
readonly extras?: Props<Node>;
|
||||
|
||||
constructor(readonly document: DocumentContext, private nodeSchema: NodeSchema) {
|
||||
const { componentName, id, children, props, ...extras } = nodeSchema;
|
||||
this.id = id || `node$${document.nextId()}`;
|
||||
this.componentName = componentName;
|
||||
if (this.isNodeParent()) {
|
||||
this.props = new Props(this, props);
|
||||
this.directives = new Props(this, {});
|
||||
Object.keys(extras).forEach(key => {
|
||||
this.directives!.add((extras as any)[key], key);
|
||||
delete (extras as any)[key];
|
||||
});
|
||||
this.extras = new Props(this, extras as any);
|
||||
if (children) {
|
||||
this._children = (Array.isArray(children) ? children : [children]).map(child => {
|
||||
const node = this.document.createNode(child);
|
||||
node.internalSetParent(this as INodeParent);
|
||||
return node;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否一个父亲类节点
|
||||
*/
|
||||
isNodeParent(): boolean {
|
||||
return this.componentName.charAt(0) !== '#';
|
||||
}
|
||||
|
||||
private _parent: INodeParent | null = null;
|
||||
|
||||
/**
|
||||
* 父级节点
|
||||
*/
|
||||
get parent(): INodeParent | null {
|
||||
return this._parent;
|
||||
}
|
||||
/**
|
||||
* 内部方法,请勿使用
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
internalSetParent(parent: INodeParent | null) {
|
||||
if (this._parent === parent) {
|
||||
return;
|
||||
}
|
||||
if (this._parent) {
|
||||
removeChild(this._parent, this);
|
||||
}
|
||||
|
||||
this._parent = parent;
|
||||
if (parent) {
|
||||
this._zLevel = parent.zLevel + 1;
|
||||
} else {
|
||||
this._zLevel = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private _zLevel = 0;
|
||||
/**
|
||||
* 当前节点深度
|
||||
*/
|
||||
get zLevel(): number {
|
||||
return this._zLevel;
|
||||
}
|
||||
|
||||
private _children: Node[] | null = null;
|
||||
/**
|
||||
* 当前节点子集
|
||||
*/
|
||||
get children(): Node[] | null {
|
||||
if (this.purged) {
|
||||
return null;
|
||||
}
|
||||
return this._children;
|
||||
}
|
||||
|
||||
/*
|
||||
@obx.ref get component(): ReactType {
|
||||
return this.document.getComponent(this.tagName);
|
||||
}
|
||||
@obx.ref get prototype(): Prototype {
|
||||
return this.document.getPrototype(this.component, this.tagName);
|
||||
}
|
||||
*/
|
||||
|
||||
@obx.ref get propsData(): PropsMap | PropsList | null {
|
||||
if (!this.isNodeParent() || this.componentName === 'Fragment') {
|
||||
return null;
|
||||
}
|
||||
return this.props?.value || null;
|
||||
}
|
||||
|
||||
get directivesData(): PropsMap | null {
|
||||
if (!this.isNodeParent()) {
|
||||
return null;
|
||||
}
|
||||
return this.directives?.value as PropsMap || null;
|
||||
}
|
||||
|
||||
private _conditionGroup: string | null = null;
|
||||
/**
|
||||
* 条件组
|
||||
*/
|
||||
get conditionGroup(): string | null {
|
||||
if (this._conditionGroup) {
|
||||
return this._conditionGroup;
|
||||
}
|
||||
// 如果 condition 有值,且没有 group
|
||||
if (this._condition) {
|
||||
return this.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
set conditionGroup(val) {
|
||||
this._conditionGroup = val;
|
||||
}
|
||||
|
||||
private _condition: any;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
get condition() {
|
||||
if (this._condition == null) {
|
||||
if (this._conditionGroup) {
|
||||
// FIXME: should be expression
|
||||
return true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return this._condition;
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO
|
||||
// 外部修改,merge 进来,产生一次可恢复的历史数据
|
||||
merge(data: ElementData) {
|
||||
this.elementData = data;
|
||||
const { leadingComments } = data;
|
||||
this.leadingComments = leadingComments ? leadingComments.slice() : [];
|
||||
this.parse();
|
||||
this.mergeChildren(data.children || []);
|
||||
}
|
||||
|
||||
private mergeChildren(data: NodeData[]) {
|
||||
for (let i = 0, l = data.length; i < l; i++) {
|
||||
const item = this.children[i];
|
||||
if (item && isMergeable(item) && item.tagName === data[i].tagName) {
|
||||
item.merge(data[i]);
|
||||
} else {
|
||||
if (item) {
|
||||
item.purge();
|
||||
}
|
||||
this.children[i] = this.document.createNode(data[i]);
|
||||
this.children[i].internalSetParent(this);
|
||||
}
|
||||
}
|
||||
if (this.children.length > data.length) {
|
||||
this.children.splice(data.length).forEach(child => child.purge());
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
getProp(path: string, useStash: boolean = true): Prop | null {
|
||||
return this.props?.query(path, useStash as any) || null;
|
||||
}
|
||||
|
||||
getDirective(name: string, useStash: boolean = true): Prop | null {
|
||||
return this.directives?.get(name, useStash as any) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点在父容器中的索引
|
||||
*/
|
||||
get index(): number {
|
||||
if (!this.parent) {
|
||||
return -1;
|
||||
}
|
||||
return indexOf(this.parent, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个兄弟节点
|
||||
*/
|
||||
get nextSibling(): Node | null {
|
||||
if (!this.parent) {
|
||||
return null;
|
||||
}
|
||||
const index = this.index;
|
||||
if (index < 0) {
|
||||
return null;
|
||||
}
|
||||
return getChildAt(this.parent, index + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上一个兄弟节点
|
||||
*/
|
||||
get prevSibling(): Node | null {
|
||||
if (!this.parent) {
|
||||
return null;
|
||||
}
|
||||
const index = this.index;
|
||||
if (index < 1) {
|
||||
return null;
|
||||
}
|
||||
return getChildAt(this.parent, index - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取符合搭建协议-节点 schema 结构
|
||||
*/
|
||||
get schema(): NodeSchema {
|
||||
return this.exportSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 schema
|
||||
* @param serialize 序列化,加 id 标识符,用于储存为操作记录
|
||||
*/
|
||||
exportSchema(serialize = false): NodeSchema {
|
||||
const schema: any = {
|
||||
componentName: this.componentName,
|
||||
props: this.props,
|
||||
condition: this.condition,
|
||||
conditionGroup: this.conditionGroup,
|
||||
...this.directives,
|
||||
};
|
||||
if (serialize) {
|
||||
schema.id = this.id;
|
||||
}
|
||||
const children = this.children;
|
||||
if (children && children.length > 0) {
|
||||
schema.children = children.map(node => node.exportSchema(serialize));
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
// TODO: 再利用历史数据,不产生历史数据
|
||||
reuse(timelineData: NodeSchema) {}
|
||||
|
||||
/**
|
||||
* 判断是否包含特定节点
|
||||
*/
|
||||
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): number {
|
||||
return comparePosition(this, otherNode);
|
||||
}
|
||||
|
||||
private purged = false;
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
purge() {
|
||||
if (this.purged) {
|
||||
return;
|
||||
}
|
||||
this.purged = true;
|
||||
if (this._children) {
|
||||
this._children.forEach(child => child.purge());
|
||||
}
|
||||
// TODO: others dispose...
|
||||
}
|
||||
}
|
||||
|
||||
export interface INodeParent extends Node {
|
||||
readonly children: Node[];
|
||||
}
|
||||
|
||||
export function isNode(node: any): node is Node {
|
||||
return node && node.isNode;
|
||||
}
|
||||
|
||||
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.isNodeParent() || !node1.children || !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 function comparePosition(node1: Node, node2: Node): number {
|
||||
if (node1 === node2) {
|
||||
return 0;
|
||||
}
|
||||
const l1 = node1.zLevel;
|
||||
const l2 = node2.zLevel;
|
||||
if (l1 === l2) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
let p: any;
|
||||
if (l1 > l2) {
|
||||
p = getZLevelTop(node2, l1);
|
||||
if (p && p === node1) {
|
||||
return 16;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
p = getZLevelTop(node1, l2);
|
||||
if (p && p === node2) {
|
||||
return 8;
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
export function insertChild(container: INodeParent, thing: Node | NodeData, at?: number | null, copy?: boolean): Node {
|
||||
let node: Node;
|
||||
if (copy && isNode(thing)) {
|
||||
thing = thing.schema;
|
||||
}
|
||||
if (isNode(thing)) {
|
||||
node = thing;
|
||||
} else {
|
||||
node = container.document.createNode(thing);
|
||||
}
|
||||
const children = container.children;
|
||||
let index = at == null ? children.length : at;
|
||||
|
||||
const i = children.indexOf(node);
|
||||
|
||||
if (i < 0) {
|
||||
if (index < children.length) {
|
||||
children.splice(index, 0, node);
|
||||
} else {
|
||||
children.push(node);
|
||||
}
|
||||
node.internalSetParent(container);
|
||||
} else {
|
||||
if (index > i) {
|
||||
index -= 1;
|
||||
}
|
||||
|
||||
if (index === i) {
|
||||
return node;
|
||||
}
|
||||
|
||||
children.splice(i, 1);
|
||||
children.splice(index, 0, node);
|
||||
}
|
||||
|
||||
// check condition group
|
||||
node.conditionGroup = null;
|
||||
if (node.prevSibling && node.nextSibling) {
|
||||
const conditionGroup = node.prevSibling.conditionGroup;
|
||||
if (conditionGroup && conditionGroup === node.nextSibling.conditionGroup) {
|
||||
node.conditionGroup = conditionGroup;
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
export function insertChildren(
|
||||
container: INodeParent,
|
||||
nodes: Node[] | NodeSchema[],
|
||||
at?: number | null,
|
||||
copy?: boolean,
|
||||
): Node[] {
|
||||
let index = at;
|
||||
let node: any;
|
||||
const results: Node[] = [];
|
||||
// tslint:disable-next-line
|
||||
while ((node = nodes.pop())) {
|
||||
results.push(insertChild(container, node, index, copy));
|
||||
index = node.index;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export function getChildAt(parent: INodeParent, index: number): Node | null {
|
||||
if (!parent.children) {
|
||||
return null;
|
||||
}
|
||||
return parent.children[index];
|
||||
}
|
||||
|
||||
export function indexOf(parent: INodeParent, child: Node): number {
|
||||
if (!parent.children) {
|
||||
return -1;
|
||||
}
|
||||
return parent.children.indexOf(child);
|
||||
}
|
||||
|
||||
export function removeChild(parent: INodeParent, child: Node) {
|
||||
if (!parent.children) {
|
||||
return;
|
||||
}
|
||||
const i = parent.children.indexOf(child);
|
||||
if (i > -1) {
|
||||
parent.children.splice(i, 1);
|
||||
}
|
||||
}
|
||||
@ -1,683 +0,0 @@
|
||||
import { untracked, computed, obx } from '@recore/obx';
|
||||
import { uniqueId, isPlainObject, hasOwnProperty } from '../../utils';
|
||||
import { valueToSource } from '../../utils/value-to-source';
|
||||
import { CompositeValue, isJSExpression, PropsList, PropsMap } from '../schema';
|
||||
import StashSpace from './stash-space';
|
||||
|
||||
export const UNSET = Symbol.for('unset');
|
||||
export type UNSET = typeof UNSET;
|
||||
|
||||
export interface IPropParent {
|
||||
delete(prop: Prop): void;
|
||||
}
|
||||
|
||||
export class Prop implements IPropParent {
|
||||
readonly isProp = true;
|
||||
|
||||
readonly id = uniqueId('prop$');
|
||||
|
||||
private _type: 'unset' | 'literal' | 'map' | 'list' | 'expression' = 'unset';
|
||||
/**
|
||||
* 属性类型
|
||||
*/
|
||||
get type(): 'unset' | 'literal' | 'map' | 'list' | 'expression' {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
@obx.ref private _value: any = UNSET;
|
||||
|
||||
/**
|
||||
* 属性值
|
||||
*/
|
||||
@computed get value(): CompositeValue {
|
||||
if (this._type === 'unset') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const type = this._type;
|
||||
if (type === 'literal' || type === 'expression') {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
if (type === 'map') {
|
||||
if (!this._items) {
|
||||
return this._value;
|
||||
}
|
||||
const maps: any = {};
|
||||
this.items!.forEach((prop, key) => {
|
||||
maps[key] = prop.value;
|
||||
});
|
||||
return maps;
|
||||
}
|
||||
|
||||
if (type === 'list') {
|
||||
if (!this._items) {
|
||||
return this._items;
|
||||
}
|
||||
return this.items!.map(prop => prop.value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* set value, val should be JSON Object
|
||||
*/
|
||||
set value(val: CompositeValue) {
|
||||
this._value = val;
|
||||
const t = typeof val;
|
||||
if (val == null) {
|
||||
this._value = null;
|
||||
this._type = 'literal';
|
||||
} else if (t === 'string' || t === 'number' || t === 'boolean') {
|
||||
this._value = val;
|
||||
this._type = 'literal';
|
||||
} else if (Array.isArray(val)) {
|
||||
this._type = 'list';
|
||||
} else if (isPlainObject(val)) {
|
||||
if (isJSExpression(val)) {
|
||||
this._type = 'expression';
|
||||
} else {
|
||||
this._type = 'map';
|
||||
}
|
||||
this._type = 'map';
|
||||
} else {
|
||||
this._type = 'expression';
|
||||
this._value = {
|
||||
type: 'JSExpression',
|
||||
value: valueToSource(val),
|
||||
};
|
||||
}
|
||||
if (untracked(() => this._items)) {
|
||||
this._items!.forEach(prop => prop.purge());
|
||||
this._items = null;
|
||||
}
|
||||
this._maps = null;
|
||||
if (this.stash) {
|
||||
this.stash.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消设置值
|
||||
*/
|
||||
unset() {
|
||||
this._type = 'unset';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否未设置值
|
||||
*/
|
||||
isUnset() {
|
||||
return this._type === 'unset';
|
||||
}
|
||||
|
||||
/**
|
||||
* 值是否包含表达式
|
||||
* 包含 JSExpresion | JSSlot 等值
|
||||
*/
|
||||
isContainJSExpression(): boolean {
|
||||
const type = this._type;
|
||||
if (type === 'expression') {
|
||||
return true;
|
||||
}
|
||||
if (type === 'literal' || type === 'unset') {
|
||||
return false;
|
||||
}
|
||||
if ((type === 'list' || type === 'map') && this.items) {
|
||||
return this.items.some(item => item.isContainJSExpression());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否简单 JSON 数据
|
||||
*/
|
||||
isJSON() {
|
||||
return !this.isContainJSExpression();
|
||||
}
|
||||
|
||||
private _items: Prop[] | null = null;
|
||||
private _maps: Map<string, Prop> | null = null;
|
||||
@computed private get items(): Prop[] | null {
|
||||
let _items: any;
|
||||
untracked(() => {
|
||||
_items = this._items;
|
||||
});
|
||||
if (!_items) {
|
||||
if (this._type === 'list') {
|
||||
const data = this._value;
|
||||
const items = [];
|
||||
for (const item of data) {
|
||||
items.push(new Prop(this, item));
|
||||
}
|
||||
_items = items;
|
||||
this._maps = null;
|
||||
} else if (this._type === 'map') {
|
||||
const data = this._value;
|
||||
const items = [];
|
||||
const maps = new Map<string, Prop>();
|
||||
const keys = Object.keys(data);
|
||||
for (const key of keys) {
|
||||
const prop = new Prop(this, data[key], key);
|
||||
items.push(prop);
|
||||
maps.set(key, prop);
|
||||
}
|
||||
_items = items;
|
||||
this._maps = maps;
|
||||
} else {
|
||||
_items = null;
|
||||
this._maps = null;
|
||||
}
|
||||
this._items = _items;
|
||||
}
|
||||
return _items;
|
||||
}
|
||||
@computed private get maps(): Map<string, Prop> | null {
|
||||
if (!this.items || this.items.length < 1) {
|
||||
return null;
|
||||
}
|
||||
return this._maps;
|
||||
}
|
||||
|
||||
private stash: StashSpace | undefined;
|
||||
|
||||
/**
|
||||
* 键值
|
||||
*/
|
||||
@obx key: string | number | undefined;
|
||||
/**
|
||||
* 扩展值
|
||||
*/
|
||||
@obx spread: boolean;
|
||||
|
||||
constructor(
|
||||
public parent: IPropParent,
|
||||
value: CompositeValue | UNSET = UNSET,
|
||||
key?: string | number,
|
||||
spread = false,
|
||||
) {
|
||||
if (value !== UNSET) {
|
||||
this.value = value;
|
||||
}
|
||||
this.key = key;
|
||||
this.spread = spread;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某个属性
|
||||
* @param stash 强制
|
||||
*/
|
||||
get(path: string, stash: false): Prop | null;
|
||||
/**
|
||||
* 获取某个属性, 如果不存在,临时获取一个待写入
|
||||
* @param stash 强制
|
||||
*/
|
||||
get(path: string, stash: true): Prop;
|
||||
/**
|
||||
* 获取某个属性, 如果不存在,临时获取一个待写入
|
||||
*/
|
||||
get(path: string): Prop;
|
||||
get(path: string, stash = true) {
|
||||
const type = this._type;
|
||||
if (type !== 'map' && type !== 'unset' && !stash) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const maps = type === 'map' ? this.maps : null;
|
||||
|
||||
let prop: any = maps ? maps.get(path) : null;
|
||||
|
||||
if (prop) {
|
||||
return prop;
|
||||
}
|
||||
|
||||
const i = path.indexOf('.');
|
||||
let entry = path;
|
||||
let nest = '';
|
||||
if (i > 0) {
|
||||
nest = path.slice(i + 1);
|
||||
if (nest) {
|
||||
entry = path.slice(0, i);
|
||||
prop = maps ? maps.get(entry) : null;
|
||||
if (prop) {
|
||||
return prop.get(nest, stash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stash) {
|
||||
if (!this.stash) {
|
||||
this.stash = new StashSpace(
|
||||
item => {
|
||||
// item take effect
|
||||
this.set(String(item.key), item);
|
||||
item.parent = this;
|
||||
},
|
||||
() => {
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
prop = this.stash.get(entry);
|
||||
if (nest) {
|
||||
return prop.get(nest, true);
|
||||
}
|
||||
|
||||
return prop;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从父级移除本身
|
||||
*/
|
||||
remove() {
|
||||
this.parent.delete(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除项
|
||||
*/
|
||||
delete(prop: Prop): void {
|
||||
if (this.items) {
|
||||
const i = this.items.indexOf(prop);
|
||||
if (i > -1) {
|
||||
this.items.slice(i, 1);
|
||||
prop.purge();
|
||||
}
|
||||
if (this._maps && prop.key) {
|
||||
this._maps.delete(String(prop.key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 key
|
||||
*/
|
||||
deleteKey(key: string): void {
|
||||
if (this.maps) {
|
||||
const prop = this.maps.get(key);
|
||||
if (prop) {
|
||||
this.delete(prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 元素个数
|
||||
*/
|
||||
size(): number {
|
||||
return this.items?.length || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加值到列表
|
||||
*
|
||||
* @param force 强制
|
||||
*/
|
||||
add(value: CompositeValue, force = false): Prop | null {
|
||||
const type = this._type;
|
||||
if (type !== 'list' && type !== 'unset' && !force) {
|
||||
return null;
|
||||
}
|
||||
if (type === 'unset' || (force && type !== 'list')) {
|
||||
this.value = [];
|
||||
}
|
||||
const prop = new Prop(this, value);
|
||||
this.items!.push(prop);
|
||||
return prop;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置值到字典
|
||||
*
|
||||
* @param force 强制
|
||||
*/
|
||||
set(key: string, value: CompositeValue | Prop, force = false) {
|
||||
const type = this._type;
|
||||
if (type !== 'map' && type !== 'unset' && !force) {
|
||||
return null;
|
||||
}
|
||||
if (type === 'unset' || (force && type !== 'map')) {
|
||||
this.value = {};
|
||||
}
|
||||
const prop = isProp(value) ? value : new Prop(this, value, key);
|
||||
const items = this.items!;
|
||||
const maps = this.maps!;
|
||||
const orig = maps.get(key);
|
||||
if (orig) {
|
||||
// replace
|
||||
const i = items.indexOf(orig);
|
||||
if (i > -1) {
|
||||
items.splice(i, 1, prop)[0].purge();
|
||||
}
|
||||
maps.set(key, prop);
|
||||
} else {
|
||||
// push
|
||||
items.push(prop);
|
||||
maps.set(key, prop);
|
||||
}
|
||||
|
||||
return prop;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存在 key
|
||||
*/
|
||||
has(key: string): boolean {
|
||||
if (this._type !== 'map') {
|
||||
return false;
|
||||
}
|
||||
if (this._maps) {
|
||||
return this._maps.has(key);
|
||||
}
|
||||
return hasOwnProperty(this._value, key);
|
||||
}
|
||||
|
||||
private purged = false;
|
||||
/**
|
||||
* 回收销毁
|
||||
*/
|
||||
purge() {
|
||||
if (this.purged) {
|
||||
return;
|
||||
}
|
||||
this.purged = true;
|
||||
if (this.stash) {
|
||||
this.stash.purge();
|
||||
}
|
||||
if (this._items) {
|
||||
this._items.forEach(item => item.purge());
|
||||
}
|
||||
this._maps = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 迭代器
|
||||
*/
|
||||
[Symbol.iterator](): { next(): { value: Prop } } {
|
||||
let index = 0;
|
||||
const items = this.items;
|
||||
const length = items?.length || 0;
|
||||
return {
|
||||
next() {
|
||||
if (index < length) {
|
||||
return {
|
||||
value: items![index++],
|
||||
done: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
value: undefined as any,
|
||||
done: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历
|
||||
*/
|
||||
forEach(fn: (item: Prop, key: number | string | undefined) => void): void {
|
||||
const items = this.items;
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
const isMap = this._type === 'map';
|
||||
items.forEach((item, index) => {
|
||||
return isMap ? fn(item, item.key) : fn(item, index);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历
|
||||
*/
|
||||
map<T>(fn: (item: Prop, key: number | string | undefined) => T): T[] | null {
|
||||
const items = this.items;
|
||||
if (!items) {
|
||||
return null;
|
||||
}
|
||||
const isMap = this._type === 'map';
|
||||
return items.map((item, index) => {
|
||||
return isMap ? fn(item, item.key) : fn(item, index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function isProp(obj: any): obj is Prop {
|
||||
return obj && obj.isProp;
|
||||
}
|
||||
|
||||
export default class Props<O = any> implements IPropParent {
|
||||
@obx.val private items: Prop[] = [];
|
||||
@obx.ref private get maps(): Map<string, Prop> {
|
||||
const maps = new Map();
|
||||
if (this.items.length > 0) {
|
||||
this.items.forEach(prop => {
|
||||
if (prop.key) {
|
||||
maps.set(prop.key, prop);
|
||||
}
|
||||
});
|
||||
}
|
||||
return maps;
|
||||
}
|
||||
|
||||
private stash = new StashSpace(
|
||||
prop => {
|
||||
this.items.push(prop);
|
||||
prop.parent = this;
|
||||
},
|
||||
() => {
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 元素个数
|
||||
*/
|
||||
get size() {
|
||||
return this.items.length;
|
||||
}
|
||||
|
||||
@computed get value(): PropsMap | PropsList | null {
|
||||
if (this.items.length < 1) {
|
||||
return null;
|
||||
}
|
||||
if (this.type === 'list') {
|
||||
return this.items.map(item => ({
|
||||
spread: item.spread,
|
||||
name: item.key as string,
|
||||
value: item.value,
|
||||
}));
|
||||
}
|
||||
const maps: any = {};
|
||||
this.items.forEach(prop => {
|
||||
if (prop.key) {
|
||||
maps[prop.key] = prop.value;
|
||||
}
|
||||
});
|
||||
return maps;
|
||||
}
|
||||
|
||||
@obx type: 'map' | 'list' = 'map';
|
||||
|
||||
constructor(readonly owner: O, value?: PropsMap | PropsList | null) {
|
||||
if (Array.isArray(value)) {
|
||||
this.type = 'list';
|
||||
value.forEach(item => {});
|
||||
} else if (value != null) {
|
||||
this.type = 'map';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 path 路径查询属性,如果没有则临时生成一个
|
||||
*/
|
||||
query(path: string): Prop;
|
||||
/**
|
||||
* 根据 path 路径查询属性
|
||||
*
|
||||
* @useStash 如果没有则临时生成一个
|
||||
*/
|
||||
query(path: string, useStash: true): Prop;
|
||||
/**
|
||||
* 根据 path 路径查询属性
|
||||
*/
|
||||
query(path: string, useStash: false): Prop | null;
|
||||
/**
|
||||
* 根据 path 路径查询属性
|
||||
*
|
||||
* @useStash 如果没有则临时生成一个
|
||||
*/
|
||||
query(path: string, useStash: boolean = true) {
|
||||
let matchedLength = 0;
|
||||
let firstMatched = null;
|
||||
if (this.items) {
|
||||
// target: a.b.c
|
||||
// trys: a.b.c, a.b, a
|
||||
let i = this.items.length;
|
||||
while (i-- > 0) {
|
||||
const expr = this.items[i];
|
||||
if (!expr.key) {
|
||||
continue;
|
||||
}
|
||||
const name = String(expr.key);
|
||||
if (name === path) {
|
||||
// completely match
|
||||
return expr;
|
||||
}
|
||||
|
||||
// fisrt match
|
||||
const l = name.length;
|
||||
if (path.slice(0, l + 1) === `${name}.`) {
|
||||
matchedLength = l;
|
||||
firstMatched = expr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ret = null;
|
||||
if (firstMatched) {
|
||||
ret = firstMatched.get(path.slice(matchedLength + 1), true);
|
||||
}
|
||||
if (!ret && useStash) {
|
||||
return this.stash.get(path);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某个属性, 如果不存在,临时获取一个待写入
|
||||
* @param useStash 强制
|
||||
*/
|
||||
get(path: string, useStash: true): Prop;
|
||||
/**
|
||||
* 获取某个属性
|
||||
* @param useStash 强制
|
||||
*/
|
||||
get(path: string, useStash: false): Prop | null;
|
||||
/**
|
||||
* 获取某个属性
|
||||
*/
|
||||
get(path: string): Prop | null;
|
||||
get(name: string, useStash = false) {
|
||||
return this.maps.get(name) || (useStash && this.stash.get(name)) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除项
|
||||
*/
|
||||
delete(prop: Prop): void {
|
||||
const i = this.items.indexOf(prop);
|
||||
if (i > -1) {
|
||||
this.items.splice(i, 1);
|
||||
prop.purge();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 key
|
||||
*/
|
||||
deleteKey(key: string): void {
|
||||
this.items = this.items.filter(item => {
|
||||
if (item.key === key) {
|
||||
item.purge();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加值
|
||||
*/
|
||||
add(value: CompositeValue | null, key?: string | number, spread = false): Prop {
|
||||
const prop = new Prop(this, value, key, spread);
|
||||
this.items.push(prop);
|
||||
return prop;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存在 key
|
||||
*/
|
||||
has(key: string): boolean {
|
||||
return this.maps.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 迭代器
|
||||
*/
|
||||
[Symbol.iterator](): { next(): { value: Prop } } {
|
||||
let index = 0;
|
||||
const items = this.items;
|
||||
const length = items.length || 0;
|
||||
return {
|
||||
next() {
|
||||
if (index < length) {
|
||||
return {
|
||||
value: items[index++],
|
||||
done: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
value: undefined as any,
|
||||
done: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历
|
||||
*/
|
||||
forEach(fn: (item: Prop, key: number | string | undefined) => void): void {
|
||||
this.items.forEach(item => {
|
||||
return fn(item, item.key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历
|
||||
*/
|
||||
map<T>(fn: (item: Prop, key: number | string | undefined) => T): T[] | null {
|
||||
return this.items.map(item => {
|
||||
return fn(item, item.key);
|
||||
});
|
||||
}
|
||||
|
||||
private purged = false;
|
||||
/**
|
||||
* 回收销毁
|
||||
*/
|
||||
purge() {
|
||||
if (this.purged) {
|
||||
return;
|
||||
}
|
||||
this.purged = true;
|
||||
this.stash.purge();
|
||||
this.items.forEach(item => item.purge());
|
||||
}
|
||||
}
|
||||
@ -1,133 +0,0 @@
|
||||
import Node from './node';
|
||||
|
||||
/**
|
||||
* state
|
||||
* lifeCycles
|
||||
* fileName
|
||||
* meta
|
||||
* methods
|
||||
* dataSource
|
||||
* css
|
||||
* defaultProps
|
||||
*/
|
||||
export default class RootNode extends Node {
|
||||
readonly isRootNode = true;
|
||||
readonly index = 0;
|
||||
readonly props: object = {};
|
||||
readonly nextSibling = null;
|
||||
readonly prevSibling = null;
|
||||
readonly zLevel = 0;
|
||||
readonly parent = null;
|
||||
internalSetParent(parent: null) {}
|
||||
|
||||
get viewData(): ViewData {
|
||||
return {
|
||||
file: this.file,
|
||||
children: this.nodeData,
|
||||
};
|
||||
}
|
||||
|
||||
readonly fileName: string;
|
||||
readonly viewType: string;
|
||||
readonly viewVersion: string;
|
||||
|
||||
get ready() {
|
||||
return this.document.ready;
|
||||
}
|
||||
|
||||
get nodeData(): NodeData[] {
|
||||
if (!this.ready) {
|
||||
// TODO: add mocks data
|
||||
return this.childrenData;
|
||||
}
|
||||
const children = this.children;
|
||||
if (!children || children.length < 1) {
|
||||
return [];
|
||||
}
|
||||
return children.map(node => node.nodeData as NodeData);
|
||||
}
|
||||
|
||||
private childrenData: NodeData[];
|
||||
private _children: INode[] | null = null;
|
||||
@obx.val get children(): INode[] {
|
||||
if (this._children) {
|
||||
return this._children;
|
||||
}
|
||||
if (!this.ready || this.purged) {
|
||||
return [];
|
||||
}
|
||||
const children = this.childrenData;
|
||||
/* eslint-disable */
|
||||
this._children = children
|
||||
? untracked(() =>
|
||||
children.map(child => {
|
||||
const node = this.document.createNode(child);
|
||||
node.internalSetParent(this);
|
||||
return node;
|
||||
}),
|
||||
)
|
||||
: [];
|
||||
/* eslint-enable */
|
||||
return this._children;
|
||||
}
|
||||
|
||||
get scope() {
|
||||
return this.mocks.scope;
|
||||
}
|
||||
|
||||
constructor(readonly document: DocumentContext, { children, file, viewType, viewVersion }: ViewData) {
|
||||
this.file = file;
|
||||
this.viewType = viewType || '';
|
||||
this.viewVersion = viewVersion || '';
|
||||
|
||||
const expr = getMockExpr(children);
|
||||
if (expr) {
|
||||
this.childrenData = children.slice(0, -1);
|
||||
this.mocksExpr = expr;
|
||||
} else {
|
||||
this.childrenData = children.slice();
|
||||
}
|
||||
}
|
||||
|
||||
merge(schema: DocumentSchema) {
|
||||
for (let i = 0, l = data.length; i < l; i++) {
|
||||
const item = this.children[i];
|
||||
if (item && isMergeable(item) && item.tagName === data[i].tagName) {
|
||||
item.merge(data[i]);
|
||||
} else {
|
||||
if (item) {
|
||||
item.purge();
|
||||
}
|
||||
this.children[i] = this.document.createNode(data[i]);
|
||||
this.children[i].internalSetParent(this);
|
||||
}
|
||||
}
|
||||
if (this.children.length > data.length) {
|
||||
this.children.splice(data.length).forEach(child => child.purge());
|
||||
}
|
||||
}
|
||||
|
||||
// todo:
|
||||
reuse() {}
|
||||
|
||||
private purged = false;
|
||||
|
||||
purge() {
|
||||
if (this.purged) {
|
||||
return;
|
||||
}
|
||||
this.purged = true;
|
||||
if (this._children) {
|
||||
this._children.forEach(child => child.purge());
|
||||
}
|
||||
}
|
||||
|
||||
receiveViewData({ children }: ViewData) {
|
||||
this.merge(children);
|
||||
// this.selection.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export function isRootNode(node: any): node is RootNode {
|
||||
return node && node.isRootNode;
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { INode, contains, isNode, comparePosition } from './node';
|
||||
import { INode, contains, isNode, comparePosition } from './node/node';
|
||||
import { obx } from '@ali/recore';
|
||||
import DocumentContext from './document-context';
|
||||
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
import { obx, autorun, untracked } from '@recore/obx';
|
||||
import { Prop, IPropParent } from './props';
|
||||
|
||||
export type PendingItem = Prop[];
|
||||
export default class StashSpace implements IPropParent {
|
||||
@obx.val private space: Set<Prop> = new Set();
|
||||
@obx.ref private get maps(): Map<string, Prop> {
|
||||
const maps = new Map();
|
||||
if (this.space.size > 0) {
|
||||
this.space.forEach(prop => {
|
||||
maps.set(prop.key, prop);
|
||||
});
|
||||
}
|
||||
return maps;
|
||||
}
|
||||
private willPurge: () => void;
|
||||
|
||||
constructor(write: (item: Prop) => void, before: () => boolean) {
|
||||
this.willPurge = autorun(() => {
|
||||
if (this.space.size < 1) {
|
||||
return;
|
||||
}
|
||||
const pending: Prop[] = [];
|
||||
for (const prop of this.space) {
|
||||
if (!prop.isUnset()) {
|
||||
this.space.delete(prop);
|
||||
pending.push(prop);
|
||||
}
|
||||
}
|
||||
if (pending.length > 0) {
|
||||
untracked(() => {
|
||||
if (before()) {
|
||||
for (const item of pending) {
|
||||
write(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get(key: string): Prop {
|
||||
let prop = this.maps.get(key);
|
||||
if (!prop) {
|
||||
prop = new Prop(this, null, key);
|
||||
this.space.add(prop);
|
||||
}
|
||||
return prop;
|
||||
}
|
||||
|
||||
delete(prop: Prop) {
|
||||
this.space.delete(prop);
|
||||
prop.purge();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.space.forEach(item => item.purge());
|
||||
this.space.clear();
|
||||
}
|
||||
|
||||
purge() {
|
||||
this.willPurge();
|
||||
this.space.clear();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user