This commit is contained in:
kangwei 2020-02-14 17:10:18 +08:00
parent ffeebdfad3
commit b566524466
8 changed files with 5 additions and 1375 deletions

View File

@ -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);

View File

@ -1,4 +1,4 @@
import { INode, INodeParent } from './node';
import { INode, INodeParent } from './node/node';
import DocumentContext from './document-context';
export interface LocationData {

View File

@ -23,7 +23,7 @@ import {
contains,
isRootNode,
isConfettiNode,
} from './node';
} from './node/node';
import {
Point,
Rect,

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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;
}

View File

@ -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';

View File

@ -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();
}
}