feat: history log

This commit is contained in:
kangwei 2020-02-26 13:24:47 +08:00
parent 9d2967f612
commit fbb3577bd4
17 changed files with 508 additions and 164 deletions

View File

@ -2,10 +2,8 @@ import { Component } from 'react';
import { obx } from '@recore/obx'; import { obx } from '@recore/obx';
import { observer } from '@recore/core-obx'; import { observer } from '@recore/core-obx';
import Designer from '../../designer/designer'; import Designer from '../../designer/designer';
import './ghost.less';
import { NodeSchema } from '../../designer/schema';
import Node from '../../designer/document/node/node';
import { isDragNodeObject, DragObject, isDragNodeDataObject } from '../../designer/helper/dragon'; import { isDragNodeObject, DragObject, isDragNodeDataObject } from '../../designer/helper/dragon';
import './ghost.less';
type offBinding = () => any; type offBinding = () => any;

View File

@ -72,12 +72,10 @@ export class OutlineHovering extends Component {
render() { render() {
const host = this.context as SimulatorHost; const host = this.context as SimulatorHost;
const current = this.current; const current = this.current;
console.info('current', current)
if (!current || host.viewport.scrolling) { if (!current || host.viewport.scrolling) {
return <Fragment />; return <Fragment />;
} }
const instances = host.getComponentInstances(current); const instances = host.getComponentInstances(current);
console.info('current instances', instances)
if (!instances || instances.length < 1) { if (!instances || instances.length < 1) {
return <Fragment />; return <Fragment />;
} }

View File

@ -290,8 +290,6 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
return; return;
} }
const nodeInst = this.getNodeInstanceFromElement(e.target as Element); const nodeInst = this.getNodeInstanceFromElement(e.target as Element);
// TODO: enhance only hover one instance
console.info(nodeInst);
hovering.hover(nodeInst?.node || null); hovering.hover(nodeInst?.node || null);
e.stopPropagation(); e.stopPropagation();
}; };
@ -633,7 +631,6 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
this.sensing = true; this.sensing = true;
this.scroller.scrolling(e); this.scroller.scrolling(e);
const dropTarget = this.getDropTarget(e); const dropTarget = this.getDropTarget(e);
console.info('aa', dropTarget);
if (!dropTarget) { if (!dropTarget) {
return null; return null;
} }

View File

@ -280,7 +280,7 @@ export class ComponentConfig {
} }
private _isContainer?: boolean; private _isContainer?: boolean;
get isContainer(): boolean { get isContainer(): boolean {
return this._isContainer! || this.isRootComponent(); return true; // this._isContainer! || this.isRootComponent();
} }
private _isModal?: boolean; private _isModal?: boolean;
get isModal(): boolean { get isModal(): boolean {

View File

@ -56,7 +56,6 @@ export default class Designer {
}); });
this.dragon.onDrag(e => { this.dragon.onDrag(e => {
console.info('dropLocation', this._dropLocation);
if (this.props?.onDrag) { if (this.props?.onDrag) {
this.props.onDrag(e); this.props.onDrag(e);
} }

View File

@ -4,9 +4,11 @@ import Node, { isNodeParent, insertChildren, insertChild, NodeParent } from './n
import { Selection } from './selection'; import { Selection } from './selection';
import RootNode from './node/root-node'; import RootNode from './node/root-node';
import { ISimulator, Component } from '../simulator'; import { ISimulator, Component } from '../simulator';
import { computed, obx } from '@recore/obx'; import { computed, obx, autorun } from '@recore/obx';
import Location from '../helper/location'; import Location from '../helper/location';
import { ComponentConfig } from '../component-config'; import { ComponentConfig } from '../component-config';
import History from '../helper/history';
import Prop from './node/props/prop';
export default class DocumentModel { export default class DocumentModel {
/** /**
@ -24,11 +26,10 @@ export default class DocumentModel {
/** /**
* *
*/ */
// TODO readonly history: History;
// readonly history: History = new History(this);
private nodesMap = new Map<string, Node>(); private nodesMap = new Map<string, Node>();
private nodes = new Set<Node>(); @obx.val private nodes = new Set<Node>();
private seqId = 0; private seqId = 0;
private _simulator?: ISimulator; private _simulator?: ISimulator;
@ -48,8 +49,21 @@ export default class DocumentModel {
} }
constructor(readonly project: Project, schema: RootSchema) { constructor(readonly project: Project, schema: RootSchema) {
this.rootNode = this.createNode(schema) as RootNode; // todo: purge this autorun
/*
autorun(() => {
this.nodes.forEach(item => {
if (item.parent == null && item !== this.rootNode) {
// item.remove();
}
});
}, true);*/
this.rootNode = this.createRootNode(schema);
this.id = this.rootNode.id; this.id = this.rootNode.id;
this.history = new History(
() => this.schema,
(schema) => this.import(schema as RootSchema, true),
);
} }
readonly designer = this.project.designer; readonly designer = this.project.designer;
@ -79,7 +93,7 @@ export default class DocumentModel {
/** /**
* schema * schema
*/ */
createNode(data: NodeData): Node { createNode(data: NodeData, slotFor?: Prop): Node {
let schema: any; let schema: any;
if (isDOMText(data) || isJSExpression(data)) { if (isDOMText(data) || isJSExpression(data)) {
schema = { schema = {
@ -89,7 +103,34 @@ export default class DocumentModel {
} else { } else {
schema = data; schema = data;
} }
const node = new Node(this, schema);
let node: Node | null = null;
if (schema.id) {
node = this.getNode(schema.id);
if (node && node.componentName === schema.componentName) {
node.internalSetParent(null);
node.internalSetSlotFor(slotFor);
node.import(schema, true);
} else if (node) {
node = null;
}
}
if (!node) {
node = new Node(this, schema, slotFor);
}
if (this.nodesMap.has(node.id)) {
node.purge();
}
this.nodesMap.set(node.id, node);
this.nodes.add(node);
return node;
}
private createRootNode(schema: RootSchema) {
const node = new RootNode(this, schema);
this.nodesMap.set(node.id, node); this.nodesMap.set(node.id, node);
this.nodes.add(node); this.nodes.add(node);
return node; return node;
@ -184,6 +225,11 @@ export default class DocumentModel {
return this.rootNode.schema as any; return this.rootNode.schema as any;
} }
import(schema: RootSchema, checkId: boolean = false) {
this.rootNode.import(schema, checkId);
// todo: purge something
}
/** /**
* *
*/ */
@ -231,8 +277,6 @@ export default class DocumentModel {
return this.designer.getComponentConfig(componentName); return this.designer.getComponentConfig(componentName);
} }
@obx.ref private _opened: boolean = true; @obx.ref private _opened: boolean = true;
@obx.ref private _suspensed: boolean = false; @obx.ref private _suspensed: boolean = false;
@ -303,7 +347,9 @@ export default class DocumentModel {
/** /**
* *
*/ */
remove() {} remove() {
// todo:
}
} }
export function isDocumentModel(obj: any): obj is DocumentModel { export function isDocumentModel(obj: any): obj is DocumentModel {

View File

@ -1,11 +1,11 @@
import Node, { NodeParent } from './node'; import Node, { NodeParent } from './node';
import { NodeData } from '../../schema'; import { NodeData, isNodeSchema } from '../../schema';
import { obx, computed } from '@recore/obx'; import { obx, computed } from '@recore/obx';
export default class NodeChildren { export default class NodeChildren {
@obx.val private children: Node[]; @obx.val private children: Node[];
constructor(readonly owner: NodeParent, childrenData: NodeData | NodeData[]) { constructor(readonly owner: NodeParent, data: NodeData | NodeData[]) {
this.children = (Array.isArray(childrenData) ? childrenData : [childrenData]).map(child => { this.children = (Array.isArray(data) ? data : [data]).map(child => {
const node = this.owner.document.createNode(child); const node = this.owner.document.createNode(child);
node.internalSetParent(this.owner); node.internalSetParent(this.owner);
return node; return node;
@ -16,8 +16,33 @@ export default class NodeChildren {
* schema * schema
* @param serialize id * @param serialize id
*/ */
exportSchema(serialize = false): NodeData[] { export(serialize = false): NodeData[] {
return this.children.map(node => node.exportSchema(serialize)); return this.children.map(node => node.export(serialize));
}
import(data?: NodeData | NodeData[], checkId: boolean = false) {
data = data ? (Array.isArray(data) ? data : [data]) : [];
const originChildren = this.children.slice();
this.children.forEach(child => child.internalSetParent(null));
const children = new Array<Node>(data.length);
for (let i = 0, l = data.length; i < l; i++) {
const child = originChildren[i];
const item = data[i];
let node: Node | undefined;
if (isNodeSchema(item) && !checkId && child && child.componentName === item.componentName) {
node = child;
node.import(item);
} else {
node = this.owner.document.createNode(item);
}
node.internalSetParent(this.owner);
children[i] = node;
}
this.children = children;
} }
/** /**
@ -34,27 +59,6 @@ export default class NodeChildren {
return this.size < 1; return this.size < 1;
} }
/*
// 用于数据重新灌入
merge() {
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());
}
}
*/
/** /**
* *
*/ */

View File

@ -57,6 +57,10 @@ export default class NodeContent {
} }
constructor(value: any) { constructor(value: any) {
this.import(value);
}
import(value: any) {
const type = typeof value; const type = typeof value;
if (value == null) { if (value == null) {
this._value = ''; this._value = '';

View File

@ -1,4 +1,4 @@
import { obx, computed } from '@recore/obx'; import { obx, computed, untracked } from '@recore/obx';
import { NodeSchema, NodeData, PropsMap, PropsList } from '../../schema'; import { NodeSchema, NodeData, PropsMap, PropsList } from '../../schema';
import Props from './props/props'; import Props from './props/props';
import DocumentModel from '../document-model'; import DocumentModel from '../document-model';
@ -50,19 +50,19 @@ export default class Node {
* * Component / * * Component /
*/ */
readonly componentName: string; readonly componentName: string;
protected _props?: Props<Node>; protected _props?: Props;
protected _directives?: Props<Node>; protected _directives?: Props;
protected _extras?: Props<Node>; protected _extras?: Props;
protected _children: NodeChildren | NodeContent; protected _children: NodeChildren | NodeContent;
@obx.ref private _parent: NodeParent | null = null; @obx.ref private _parent: NodeParent | null = null;
@obx.ref private _zLevel = 0; @obx.ref private _zLevel = 0;
get props(): Props<Node> | undefined { get props(): Props | undefined {
return this._props; return this._props;
} }
get directives(): Props<Node> | undefined { get directives(): Props | undefined {
return this._directives; return this._directives;
} }
get extras(): Props<Node> | undefined { get extras(): Props | undefined {
return this._extras; return this._extras;
} }
/** /**
@ -80,8 +80,11 @@ export default class Node {
/** /**
* *
*/ */
get zLevel(): number { @computed get zLevel(): number {
return this._zLevel; if (this._parent) {
return this._parent.zLevel + 1;
}
return -1;
} }
@computed get title(): string { @computed get title(): string {
@ -98,11 +101,16 @@ export default class Node {
return this.componentName; return this.componentName;
} }
constructor(readonly document: DocumentModel, nodeSchema: NodeSchema) { get isSlotRoot(): boolean {
return this._slotFor != null;
}
constructor(readonly document: DocumentModel, nodeSchema: NodeSchema, slotFor?: Prop) {
const { componentName, id, children, props, ...extras } = nodeSchema; const { componentName, id, children, props, ...extras } = nodeSchema;
this.id = id || `node$${document.nextId()}`; this.id = id || `node$${document.nextId()}`;
this.componentName = componentName; this.componentName = componentName;
if (this.isNodeParent) { this._slotFor = slotFor;
if (isNodeParent(this)) {
this._props = new Props(this, props); this._props = new Props(this, props);
this._directives = new Props(this, {}); this._directives = new Props(this, {});
Object.keys(extras).forEach(key => { Object.keys(extras).forEach(key => {
@ -127,30 +135,33 @@ export default class Node {
/** /**
* 使 * 使
*
* @ignore
*/ */
internalSetParent(parent: NodeParent | null) { internalSetParent(parent: NodeParent | null) {
if (this._parent === parent) { if (this._parent === parent) {
return; return;
} }
if (this._parent) {
if (this._parent && !this.isSlotRoot) {
this._parent.children.delete(this); this._parent.children.delete(this);
} }
this._parent = parent; this._parent = parent;
if (parent) { }
this._zLevel = parent.zLevel + 1;
} else { private _slotFor?: Prop | null = null;
this._zLevel = -1; internalSetSlotFor(slotFor: Prop | null | undefined) {
} this._slotFor = slotFor;
}
get slotFor() {
return this._slotFor;
} }
/** /**
* *
*/ */
remove() { remove() {
if (this.parent) { if (this.parent && !this.isSlotRoot) {
this.parent.children.delete(this, true); this.parent.children.delete(this, true);
} }
} }
@ -231,25 +242,10 @@ export default class Node {
} }
replaceWith(schema: NodeSchema, migrate: boolean = true) { replaceWith(schema: NodeSchema, migrate: boolean = true) {
// reuse the same id? or replaceSelection
// //
} }
/*
// TODO
// 外部修改merge 进来,产生一次可恢复的历史数据
merge(data: ElementData) {
this.elementData = data;
const { leadingComments } = data;
this.leadingComments = leadingComments ? leadingComments.slice() : [];
this.parse();
this.mergeChildren(data.children || []);
}
// TODO: 再利用历史数据,不产生历史数据
reuse(timelineData: NodeSchema) {}
*/
getProp(path: string, useStash: boolean = true): Prop | null { getProp(path: string, useStash: boolean = true): Prop | null {
return this.props?.query(path, useStash as any) || null; return this.props?.query(path, useStash as any) || null;
} }
@ -300,28 +296,51 @@ export default class Node {
* - schema * - schema
*/ */
get schema(): NodeSchema { get schema(): NodeSchema {
// TODO: .. return this.export(true);
return this.exportSchema(true); }
set schema(data: NodeSchema) {
this.import(data);
}
import(data: NodeSchema, checkId: boolean = false) {
const { componentName, id, children, props, ...extras } = data;
if (isNodeParent(this)) {
const directives: any = {};
Object.keys(extras).forEach(key => {
if (DIRECTIVES.indexOf(key) > -1) {
directives[key] = (extras as any)[key];
delete (extras as any)[key];
}
});
this._props!.import(data.props);
this._directives!.import(directives);
this._extras!.import(extras as any);
this._children.import(children, checkId);
} else {
this._children.import(children);
}
} }
/** /**
* schema * schema
* @param serialize id * @param serialize id
*/ */
exportSchema(serialize = false): NodeSchema { export(serialize = false): NodeSchema {
// TODO... // TODO...
const schema: any = { const schema: any = {
componentName: this.componentName, componentName: this.componentName,
...this.extras?.value, ...this.extras?.export(serialize),
props: this.props?.value || {}, props: this.props?.export(serialize) || {},
...this.directives?.value, ...this.directives?.export(serialize),
}; };
if (serialize) { if (serialize) {
schema.id = this.id; schema.id = this.id;
} }
if (isNodeParent(this)) { if (isNodeParent(this)) {
if (this.children.size > 0) { if (this.children.size > 0) {
schema.children = this.children.exportSchema(serialize); schema.children = this.children.export(serialize);
} }
} else { } else {
schema.children = (this.children as NodeContent).value; schema.children = (this.children as NodeContent).value;
@ -387,9 +406,9 @@ export default class Node {
export interface NodeParent extends Node { export interface NodeParent extends Node {
readonly children: NodeChildren; readonly children: NodeChildren;
readonly props: Props<Node>; readonly props: Props;
readonly directives: Props<Node>; readonly directives: Props;
readonly extras: Props<Node>; readonly extras: Props;
} }
export function isNode(node: any): node is Node { export function isNode(node: any): node is Node {
@ -466,7 +485,7 @@ export function comparePosition(node1: Node, node2: Node): number {
export function insertChild(container: NodeParent, thing: Node | NodeData, at?: number | null, copy?: boolean): Node { export function insertChild(container: NodeParent, thing: Node | NodeData, at?: number | null, copy?: boolean): Node {
let node: Node; let node: Node;
if (copy && isNode(thing)) { if (copy && isNode(thing)) {
thing = thing.exportSchema(false); thing = thing.export(false);
} }
if (isNode(thing)) { if (isNode(thing)) {
node = thing; node = thing;

View File

@ -1,8 +1,9 @@
import { obx, autorun, untracked, computed } from '@recore/obx'; import { obx, autorun, untracked, computed } from '@recore/obx';
import Prop, { IPropParent } from './prop'; import Prop, { IPropParent, UNSET } from './prop';
import Props from './props';
export type PendingItem = Prop[]; export type PendingItem = Prop[];
export default class StashSpace implements IPropParent { export default class PropStash implements IPropParent {
@obx.val private space: Set<Prop> = new Set(); @obx.val private space: Set<Prop> = new Set();
@computed private get maps(): Map<string, Prop> { @computed private get maps(): Map<string, Prop> {
const maps = new Map(); const maps = new Map();
@ -15,7 +16,7 @@ export default class StashSpace implements IPropParent {
} }
private willPurge: () => void; private willPurge: () => void;
constructor(write: (item: Prop) => void, before: () => boolean) { constructor(readonly props: Props, write: (item: Prop) => void) {
this.willPurge = autorun(() => { this.willPurge = autorun(() => {
if (this.space.size < 1) { if (this.space.size < 1) {
return; return;
@ -28,11 +29,10 @@ export default class StashSpace implements IPropParent {
} }
} }
if (pending.length > 0) { if (pending.length > 0) {
debugger;
untracked(() => { untracked(() => {
if (before()) { for (const item of pending) {
for (const item of pending) { write(item);
write(item);
}
} }
}); });
} }
@ -42,7 +42,7 @@ export default class StashSpace implements IPropParent {
get(key: string): Prop { get(key: string): Prop {
let prop = this.maps.get(key); let prop = this.maps.get(key);
if (!prop) { if (!prop) {
prop = new Prop(this, null, key); prop = new Prop(this, UNSET, key);
this.space.add(prop); this.space.add(prop);
} }
return prop; return prop;

View File

@ -1,16 +1,19 @@
import { untracked, computed, obx } from '@recore/obx'; import { untracked, computed, obx } from '@recore/obx';
import { valueToSource } from '../../../../utils/value-to-source'; import { valueToSource } from '../../../../utils/value-to-source';
import { CompositeValue, isJSExpression } from '../../../schema'; import { CompositeValue, isJSExpression, isJSSlot, NodeSchema, NodeData, isNodeSchema } from '../../../schema';
import StashSpace from './stash-space'; import PropStash from './prop-stash';
import { uniqueId } from '../../../../utils/unique-id'; import { uniqueId } from '../../../../utils/unique-id';
import { isPlainObject } from '../../../../utils/is-plain-object'; import { isPlainObject } from '../../../../utils/is-plain-object';
import { hasOwnProperty } from '../../../../utils/has-own-property'; import { hasOwnProperty } from '../../../../utils/has-own-property';
import Props from './props';
import Node from '../node';
export const UNSET = Symbol.for('unset'); export const UNSET = Symbol.for('unset');
export type UNSET = typeof UNSET; export type UNSET = typeof UNSET;
export interface IPropParent { export interface IPropParent {
delete(prop: Prop): void; delete(prop: Prop): void;
readonly props: Props;
} }
export default class Prop implements IPropParent { export default class Prop implements IPropParent {
@ -18,11 +21,11 @@ export default class Prop implements IPropParent {
readonly id = uniqueId('prop$'); readonly id = uniqueId('prop$');
private _type: 'unset' | 'literal' | 'map' | 'list' | 'expression' = 'unset'; @obx.ref private _type: 'unset' | 'literal' | 'map' | 'list' | 'expression' | 'slot' = 'unset';
/** /**
* *
*/ */
get type(): 'unset' | 'literal' | 'map' | 'list' | 'expression' { get type(): 'unset' | 'literal' | 'map' | 'list' | 'expression' | 'slot' {
return this._type; return this._type;
} }
@ -32,15 +35,27 @@ export default class Prop implements IPropParent {
* *
*/ */
@computed get value(): CompositeValue { @computed get value(): CompositeValue {
if (this._type === 'unset') { return this.export(true);
}
export(serialize = false): CompositeValue {
const type = this._type;
if (type === 'unset') {
return null; return null;
} }
const type = this._type;
if (type === 'literal' || type === 'expression') { if (type === 'literal' || type === 'expression') {
return this._value; return this._value;
} }
if (type === 'slot') {
return {
type: 'JSSlot',
value: this._slotNode!.export(serialize),
};
}
if (type === 'map') { if (type === 'map') {
if (!this._items) { if (!this._items) {
return this._value; return this._value;
@ -79,11 +94,14 @@ export default class Prop implements IPropParent {
this._value = null; this._value = null;
this._type = 'literal'; this._type = 'literal';
} else if (t === 'string' || t === 'number' || t === 'boolean') { } else if (t === 'string' || t === 'number' || t === 'boolean') {
this._value = val;
this._type = 'literal'; this._type = 'literal';
} else if (Array.isArray(val)) { } else if (Array.isArray(val)) {
this._type = 'list'; this._type = 'list';
} else if (isPlainObject(val)) { } else if (isPlainObject(val)) {
if (isJSSlot(val)) {
this.setAsSlot(val.value);
return;
}
if (isJSExpression(val)) { if (isJSExpression(val)) {
this._type = 'expression'; this._type = 'expression';
} else { } else {
@ -97,14 +115,42 @@ export default class Prop implements IPropParent {
value: valueToSource(val), value: valueToSource(val),
}; };
} }
if (untracked(() => this._items)) { this.dispose();
this._items!.forEach(prop => prop.purge()); }
this._items = null;
private dispose() {
const items = untracked(() => this._items);
if (items) {
items.forEach(prop => prop.purge());
} }
this._items = null;
this._maps = null; this._maps = null;
if (this.stash) { if (this.stash) {
this.stash.clear(); this.stash.clear();
} }
if (this._type !== 'slot' && this._slotNode) {
this._slotNode.purge();
this._slotNode = undefined;
}
}
private _slotNode?: Node;
setAsSlot(data: NodeData) {
this._type = 'slot';
if (
this._slotNode &&
isNodeSchema(data) &&
(!data.id || this._slotNode.id === data.id) &&
this._slotNode.componentName === data.componentName
) {
this._slotNode.import(data);
} else {
this._slotNode?.internalSetParent(null);
const owner = this.props.owner;
this._slotNode = owner.document.createNode(data, this);
this._slotNode.internalSetParent(owner);
}
this.dispose();
} }
/** /**
@ -122,19 +168,19 @@ export default class Prop implements IPropParent {
} }
/** /**
* * JS
* JSExpresion | JSSlot * JSExpresion | JSSlot
*/ */
@computed isContainJSExpression(): boolean { @computed isTypedJS(): boolean {
const type = this._type; const type = this._type;
if (type === 'expression') { if (type === 'expression' || type === 'slot') {
return true; return true;
} }
if (type === 'literal' || type === 'unset') { if (type === 'literal' || type === 'unset') {
return false; return false;
} }
if ((type === 'list' || type === 'map') && this.items) { if ((type === 'list' || type === 'map') && this.items) {
return this.items.some(item => item.isContainJSExpression()); return this.items.some(item => item.isTypedJS());
} }
return false; return false;
} }
@ -143,7 +189,7 @@ export default class Prop implements IPropParent {
* JSON * JSON
*/ */
@computed isJSON() { @computed isJSON() {
return !this.isContainJSExpression(); return !this.isTypedJS();
} }
@obx.val private _items: Prop[] | null = null; @obx.val private _items: Prop[] | null = null;
@ -189,7 +235,7 @@ export default class Prop implements IPropParent {
return this._maps; return this._maps;
} }
private stash: StashSpace | undefined; private stash: PropStash | undefined;
/** /**
* *
@ -200,12 +246,15 @@ export default class Prop implements IPropParent {
*/ */
@obx spread: boolean; @obx spread: boolean;
readonly props: Props;
constructor( constructor(
public parent: IPropParent, public parent: IPropParent,
value: CompositeValue | UNSET = UNSET, value: CompositeValue | UNSET = UNSET,
key?: string | number, key?: string | number,
spread = false, spread = false,
) { ) {
this.props = parent.props;
if (value !== UNSET) { if (value !== UNSET) {
this.value = value; this.value = value;
} }
@ -257,16 +306,11 @@ export default class Prop implements IPropParent {
if (stash) { if (stash) {
if (!this.stash) { if (!this.stash) {
this.stash = new StashSpace( this.stash = new PropStash(this.props, item => {
item => { // item take effect
// item take effect this.set(String(item.key), item);
this.set(String(item.key), item); item.parent = this;
item.parent = this; });
},
() => {
return true;
},
);
} }
prop = this.stash.get(entry); prop = this.stash.get(entry);
if (nest) { if (nest) {
@ -401,6 +445,9 @@ export default class Prop implements IPropParent {
this._items.forEach(item => item.purge()); this._items.forEach(item => item.purge());
} }
this._maps = null; this._maps = null;
if (this._slotNode && this._slotNode.slotFor === this) {
this._slotNode.purge();
}
} }
/** /**

View File

@ -1,14 +1,14 @@
import { computed, obx } from '@recore/obx'; import { computed, obx } from '@recore/obx';
import { uniqueId } from '../../../../utils/unique-id'; import { uniqueId } from '../../../../utils/unique-id';
import { CompositeValue, PropsList, PropsMap } from '../../../schema'; import { CompositeValue, PropsList, PropsMap } from '../../../schema';
import StashSpace from './stash-space'; import PropStash from './prop-stash';
import Prop, { IPropParent } from './prop'; import Prop, { IPropParent } from './prop';
import { NodeParent } from '../node';
export const UNSET = Symbol.for('unset'); export const UNSET = Symbol.for('unset');
export type UNSET = typeof UNSET; export type UNSET = typeof UNSET;
export default class Props implements IPropParent {
export default class Props<O = any> implements IPropParent {
readonly id = uniqueId('props'); readonly id = uniqueId('props');
@obx.val private items: Prop[] = []; @obx.val private items: Prop[] = [];
@computed private get maps(): Map<string, Prop> { @computed private get maps(): Map<string, Prop> {
@ -23,15 +23,14 @@ export default class Props<O = any> implements IPropParent {
return maps; return maps;
} }
private stash = new StashSpace( get props(): Props {
prop => { return this;
this.items.push(prop); }
prop.parent = this;
}, private stash = new PropStash(this, prop => {
() => { this.items.push(prop);
return true; prop.parent = this;
}, });
);
/** /**
* *
@ -41,6 +40,36 @@ export default class Props<O = any> implements IPropParent {
} }
@computed get value(): PropsMap | PropsList | null { @computed get value(): PropsMap | PropsList | null {
return this.export(true);
}
@obx type: 'map' | 'list' = 'map';
constructor(readonly owner: NodeParent, value?: PropsMap | PropsList | null) {
if (Array.isArray(value)) {
this.type = 'list';
this.items = value.map(item => new Prop(this, item.value, item.name, item.spread));
} else if (value != null) {
this.items = Object.keys(value).map(key => new Prop(this, value[key], key));
}
}
import(value?: PropsMap | PropsList | null) {
this.stash.clear();
if (Array.isArray(value)) {
this.type = 'list';
this.items = value.map(item => new Prop(this, item.value, item.name, item.spread));
} else if (value != null) {
this.type = 'map';
this.items = Object.keys(value).map(key => new Prop(this, value[key], key));
} else {
this.type = 'map';
this.items = [];
}
this.items.forEach(item => item.purge());
}
export(serialize = false): PropsMap | PropsList | null {
if (this.items.length < 1) { if (this.items.length < 1) {
return null; return null;
} }
@ -48,30 +77,18 @@ export default class Props<O = any> implements IPropParent {
return this.items.map(item => ({ return this.items.map(item => ({
spread: item.spread, spread: item.spread,
name: item.key as string, name: item.key as string,
value: item.value, value: item.export(serialize),
})); }));
} }
const maps: any = {}; const maps: any = {};
this.items.forEach(prop => { this.items.forEach(prop => {
if (prop.key) { if (prop.key) {
maps[prop.key] = prop.value; maps[prop.key] = prop.export(serialize);
} }
}); });
return maps; return maps;
} }
@obx type: 'map' | 'list' = 'map';
constructor(readonly owner: O, value?: PropsMap | PropsList | null) {
if (Array.isArray(value)) {
this.type = 'list';
this.items = value.map(item => new Prop(this, item.value, item.name, item.spread));
} else if (value != null) {
this.type = 'map';
this.items = Object.keys(value).map(key => new Prop(this, value[key], key));
}
}
/** /**
* path * path
*/ */

View File

@ -56,13 +56,13 @@ export default class RootNode extends Node implements NodeParent {
get children(): NodeChildren { get children(): NodeChildren {
return this._children as NodeChildren; return this._children as NodeChildren;
} }
get props(): Props<RootNode> { get props(): Props {
return this._props as any; return this._props as any;
} }
get extras(): Props<RootNode> { get extras(): Props {
return this._extras as any; return this._extras as any;
} }
get directives(): Props<RootNode> { get directives(): Props {
return this._directives as any; return this._directives as any;
} }
internalSetParent(parent: null) {} internalSetParent(parent: null) {}

View File

@ -1 +1,174 @@
// todo import { EventEmitter } from 'events';
import Session from './session';
import { autorun, Reaction, untracked } from '@recore/obx';
import { NodeSchema } from '../schema';
export interface Serialization<T = any> {
serialize(data: NodeSchema): T;
unserialize(data: T): NodeSchema;
}
let currentSerializion: Serialization<any> = {
serialize(data: NodeSchema): string {
return JSON.stringify(data);
},
unserialize(data: string) {
return JSON.parse(data);
},
};
export function setSerialization(serializion: Serialization) {
currentSerializion = serializion;
}
export default class History {
private session: Session;
private records: Session[];
private point: number = 0;
private emitter = new EventEmitter();
private obx: Reaction;
private justWokeup: boolean = false;
constructor(
logger: () => any,
private redoer: (data: NodeSchema) => void,
private timeGap: number = 1000,
) {
this.session = new Session(0, null, this.timeGap);
this.records = [this.session];
this.obx = autorun(() => {
const data = logger();
console.info('log');
if (this.justWokeup) {
this.justWokeup = false;
return;
}
untracked(() => {
const log = currentSerializion.serialize(data);
if (this.session.cursor === 0 && this.session.isActive()) {
this.session.log(log);
this.session.end();
} else if (this.session) {
if (this.session.isActive()) {
this.session.log(log);
} else {
this.session.end();
const cursor = this.session.cursor + 1;
const session = new Session(cursor, log, this.timeGap);
this.session = session;
this.records.splice(cursor, this.records.length - cursor, session);
}
}
});
}, true).$obx;
}
get hotData() {
return this.session.data;
}
isSavePoint(): boolean {
return this.point !== this.session.cursor;
}
go(cursor: number) {
this.session.end();
const currentCursor = this.session.cursor;
cursor = +cursor;
if (cursor < 0) {
cursor = 0;
} else if (cursor >= this.records.length) {
cursor = this.records.length - 1;
}
if (cursor === currentCursor) {
return;
}
const session = this.records[cursor];
const hotData = session.data;
this.obx.sleep();
try {
this.redoer(hotData);
this.emitter.emit('cursor', hotData);
} catch (e) {
//
}
this.justWokeup = true;
this.obx.wakeup();
this.session = session;
this.emitter.emit('statechange', this.getState());
}
back() {
if (!this.session) {
return;
}
const cursor = this.session.cursor - 1;
this.go(cursor);
}
forward() {
if (!this.session) {
return;
}
const cursor = this.session.cursor + 1;
this.go(cursor);
}
savePoint() {
if (!this.session) {
return;
}
this.session.end();
this.point = this.session.cursor;
this.emitter.emit('statechange', this.getState());
}
/**
* | 1 | 1 | 1 |
* | -------- | -------- | -------- |
* | modified | redoable | undoable |
*/
getState(): number {
const cursor = this.session.cursor;
let state = 7;
// undoable ?
if (cursor <= 0) {
state -= 1;
}
// redoable ?
if (cursor >= this.records.length - 1) {
state -= 2;
}
// modified ?
if (this.point === cursor) {
state -= 4;
}
return state;
}
onStateChange(func: () => any) {
this.emitter.on('statechange', func);
return () => {
this.emitter.removeListener('statechange', func);
};
}
onCursor(func: () => any) {
this.emitter.on('cursor', func);
return () => {
this.emitter.removeListener('cursor', func);
};
}
destroy() {
this.emitter.removeAllListeners();
this.records = [];
}
}

View File

@ -0,0 +1,44 @@
export default class Session {
private _data: any;
private activedTimer: any;
get data() {
return this._data;
}
constructor(readonly cursor: number, data: any, private timeGap: number = 1000) {
this.setTimer();
this.log(data);
}
log(data: any) {
if (!this.isActive()) {
return;
}
this._data = data;
this.setTimer();
}
isActive() {
return this.activedTimer != null;
}
end() {
if (this.isActive()) {
this.clearTimer();
console.info('session end');
}
}
private setTimer() {
this.clearTimer();
this.activedTimer = setTimeout(() => this.end(), this.timeGap);
}
private clearTimer() {
if (this.activedTimer) {
clearTimeout(this.activedTimer);
}
this.activedTimer = null;
}
}

View File

@ -8,7 +8,6 @@ export default class ProjectView extends Component<{ designer: Designer }> {
render() { render() {
const { designer } = this.props; const { designer } = this.props;
// TODO: support splitview // TODO: support splitview
console.info(designer.project.documents);
return ( return (
<div className="lc-project"> <div className="lc-project">
{designer.project.documents.map(doc => { {designer.project.documents.map(doc => {

View File

@ -90,15 +90,14 @@ export type PropsList = Array<{
export type NodeData = NodeSchema | JSExpression | DOMText; export type NodeData = NodeSchema | JSExpression | DOMText;
export interface JSExpression {
type: 'JSExpression';
value: string;
}
export function isJSExpression(data: any): data is JSExpression { export function isJSExpression(data: any): data is JSExpression {
return data && data.type === 'JSExpression'; return data && data.type === 'JSExpression';
} }
export function isJSSlot(data: any): data is JSSlot {
return data && data.type === 'JSSlot';
}
export function isDOMText(data: any): data is DOMText { export function isDOMText(data: any): data is DOMText {
return typeof data === 'string'; return typeof data === 'string';
} }