From fbb3577bd434c0ac77cc907abc36e3efe110fe8c Mon Sep 17 00:00:00 2001 From: kangwei Date: Wed, 26 Feb 2020 13:24:47 +0800 Subject: [PATCH] feat: history log --- .../src/builtins/drag-ghost/index.tsx | 4 +- .../host/auxilary/outline-hovering.tsx | 2 - .../src/builtins/simulator/host/host.ts | 3 - .../designer/src/designer/component-config.ts | 2 +- packages/designer/src/designer/designer.ts | 1 - .../src/designer/document/document-model.ts | 66 ++++++- .../designer/document/node/node-children.ts | 56 +++--- .../designer/document/node/node-content.ts | 4 + .../src/designer/document/node/node.ts | 113 ++++++----- .../props/{stash-space.ts => prop-stash.ts} | 16 +- .../src/designer/document/node/props/prop.ts | 101 +++++++--- .../src/designer/document/node/props/props.ts | 69 ++++--- .../src/designer/document/node/root-node.ts | 6 +- .../designer/src/designer/helper/history.ts | 175 +++++++++++++++++- .../designer/src/designer/helper/session.ts | 44 +++++ .../designer/src/designer/project-view.tsx | 1 - packages/designer/src/designer/schema.ts | 9 +- 17 files changed, 508 insertions(+), 164 deletions(-) rename packages/designer/src/designer/document/node/props/{stash-space.ts => prop-stash.ts} (78%) create mode 100644 packages/designer/src/designer/helper/session.ts diff --git a/packages/designer/src/builtins/drag-ghost/index.tsx b/packages/designer/src/builtins/drag-ghost/index.tsx index dffa6973e..c93204e2b 100644 --- a/packages/designer/src/builtins/drag-ghost/index.tsx +++ b/packages/designer/src/builtins/drag-ghost/index.tsx @@ -2,10 +2,8 @@ import { Component } from 'react'; import { obx } from '@recore/obx'; import { observer } from '@recore/core-obx'; 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 './ghost.less'; type offBinding = () => any; diff --git a/packages/designer/src/builtins/simulator/host/auxilary/outline-hovering.tsx b/packages/designer/src/builtins/simulator/host/auxilary/outline-hovering.tsx index 7d905d030..ffd527f1e 100644 --- a/packages/designer/src/builtins/simulator/host/auxilary/outline-hovering.tsx +++ b/packages/designer/src/builtins/simulator/host/auxilary/outline-hovering.tsx @@ -72,12 +72,10 @@ export class OutlineHovering extends Component { render() { const host = this.context as SimulatorHost; const current = this.current; - console.info('current', current) if (!current || host.viewport.scrolling) { return ; } const instances = host.getComponentInstances(current); - console.info('current instances', instances) if (!instances || instances.length < 1) { return ; } diff --git a/packages/designer/src/builtins/simulator/host/host.ts b/packages/designer/src/builtins/simulator/host/host.ts index 056456895..d540bc3f4 100644 --- a/packages/designer/src/builtins/simulator/host/host.ts +++ b/packages/designer/src/builtins/simulator/host/host.ts @@ -290,8 +290,6 @@ export class SimulatorHost implements ISimulator { return; } const nodeInst = this.getNodeInstanceFromElement(e.target as Element); - // TODO: enhance only hover one instance - console.info(nodeInst); hovering.hover(nodeInst?.node || null); e.stopPropagation(); }; @@ -633,7 +631,6 @@ export class SimulatorHost implements ISimulator { this.sensing = true; this.scroller.scrolling(e); const dropTarget = this.getDropTarget(e); - console.info('aa', dropTarget); if (!dropTarget) { return null; } diff --git a/packages/designer/src/designer/component-config.ts b/packages/designer/src/designer/component-config.ts index 64bb76fe0..db2802a85 100644 --- a/packages/designer/src/designer/component-config.ts +++ b/packages/designer/src/designer/component-config.ts @@ -280,7 +280,7 @@ export class ComponentConfig { } private _isContainer?: boolean; get isContainer(): boolean { - return this._isContainer! || this.isRootComponent(); + return true; // this._isContainer! || this.isRootComponent(); } private _isModal?: boolean; get isModal(): boolean { diff --git a/packages/designer/src/designer/designer.ts b/packages/designer/src/designer/designer.ts index 2ed1ecfc3..9ed147dd6 100644 --- a/packages/designer/src/designer/designer.ts +++ b/packages/designer/src/designer/designer.ts @@ -56,7 +56,6 @@ export default class Designer { }); this.dragon.onDrag(e => { - console.info('dropLocation', this._dropLocation); if (this.props?.onDrag) { this.props.onDrag(e); } diff --git a/packages/designer/src/designer/document/document-model.ts b/packages/designer/src/designer/document/document-model.ts index 793a289b0..16a561ded 100644 --- a/packages/designer/src/designer/document/document-model.ts +++ b/packages/designer/src/designer/document/document-model.ts @@ -4,9 +4,11 @@ import Node, { isNodeParent, insertChildren, insertChild, NodeParent } from './n import { Selection } from './selection'; import RootNode from './node/root-node'; import { ISimulator, Component } from '../simulator'; -import { computed, obx } from '@recore/obx'; +import { computed, obx, autorun } from '@recore/obx'; import Location from '../helper/location'; import { ComponentConfig } from '../component-config'; +import History from '../helper/history'; +import Prop from './node/props/prop'; export default class DocumentModel { /** @@ -24,11 +26,10 @@ export default class DocumentModel { /** * 操作记录控制 */ - // TODO - // readonly history: History = new History(this); + readonly history: History; private nodesMap = new Map(); - private nodes = new Set(); + @obx.val private nodes = new Set(); private seqId = 0; private _simulator?: ISimulator; @@ -48,8 +49,21 @@ export default class DocumentModel { } 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.history = new History( + () => this.schema, + (schema) => this.import(schema as RootSchema, true), + ); } readonly designer = this.project.designer; @@ -79,7 +93,7 @@ export default class DocumentModel { /** * 根据 schema 创建一个节点 */ - createNode(data: NodeData): Node { + createNode(data: NodeData, slotFor?: Prop): Node { let schema: any; if (isDOMText(data) || isJSExpression(data)) { schema = { @@ -89,7 +103,34 @@ export default class DocumentModel { } else { 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.nodes.add(node); return node; @@ -184,6 +225,11 @@ export default class DocumentModel { 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); } - - @obx.ref private _opened: boolean = true; @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 { diff --git a/packages/designer/src/designer/document/node/node-children.ts b/packages/designer/src/designer/document/node/node-children.ts index b29fa4b22..7ae7b9d7b 100644 --- a/packages/designer/src/designer/document/node/node-children.ts +++ b/packages/designer/src/designer/document/node/node-children.ts @@ -1,11 +1,11 @@ import Node, { NodeParent } from './node'; -import { NodeData } from '../../schema'; +import { NodeData, isNodeSchema } from '../../schema'; import { obx, computed } from '@recore/obx'; export default class NodeChildren { @obx.val private children: Node[]; - constructor(readonly owner: NodeParent, childrenData: NodeData | NodeData[]) { - this.children = (Array.isArray(childrenData) ? childrenData : [childrenData]).map(child => { + constructor(readonly owner: NodeParent, data: NodeData | NodeData[]) { + this.children = (Array.isArray(data) ? data : [data]).map(child => { const node = this.owner.document.createNode(child); node.internalSetParent(this.owner); return node; @@ -16,8 +16,33 @@ export default class NodeChildren { * 导出 schema * @param serialize 序列化,加 id 标识符,用于储存为操作记录 */ - exportSchema(serialize = false): NodeData[] { - return this.children.map(node => node.exportSchema(serialize)); + export(serialize = false): NodeData[] { + 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(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; } - /* - // 用于数据重新灌入 - 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()); - } - } - */ - /** * 删除一个节点 */ diff --git a/packages/designer/src/designer/document/node/node-content.ts b/packages/designer/src/designer/document/node/node-content.ts index 436681830..b7e5caeb5 100644 --- a/packages/designer/src/designer/document/node/node-content.ts +++ b/packages/designer/src/designer/document/node/node-content.ts @@ -57,6 +57,10 @@ export default class NodeContent { } constructor(value: any) { + this.import(value); + } + + import(value: any) { const type = typeof value; if (value == null) { this._value = ''; diff --git a/packages/designer/src/designer/document/node/node.ts b/packages/designer/src/designer/document/node/node.ts index 65f4f70fa..173e5139f 100644 --- a/packages/designer/src/designer/document/node/node.ts +++ b/packages/designer/src/designer/document/node/node.ts @@ -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 Props from './props/props'; import DocumentModel from '../document-model'; @@ -50,19 +50,19 @@ export default class Node { * * Component 组件/元件 */ readonly componentName: string; - protected _props?: Props; - protected _directives?: Props; - protected _extras?: Props; + protected _props?: Props; + protected _directives?: Props; + protected _extras?: Props; protected _children: NodeChildren | NodeContent; @obx.ref private _parent: NodeParent | null = null; @obx.ref private _zLevel = 0; - get props(): Props | undefined { + get props(): Props | undefined { return this._props; } - get directives(): Props | undefined { + get directives(): Props | undefined { return this._directives; } - get extras(): Props | undefined { + get extras(): Props | undefined { return this._extras; } /** @@ -80,8 +80,11 @@ export default class Node { /** * 当前节点深度 */ - get zLevel(): number { - return this._zLevel; + @computed get zLevel(): number { + if (this._parent) { + return this._parent.zLevel + 1; + } + return -1; } @computed get title(): string { @@ -98,11 +101,16 @@ export default class Node { 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; this.id = id || `node$${document.nextId()}`; this.componentName = componentName; - if (this.isNodeParent) { + this._slotFor = slotFor; + if (isNodeParent(this)) { this._props = new Props(this, props); this._directives = new Props(this, {}); Object.keys(extras).forEach(key => { @@ -127,30 +135,33 @@ export default class Node { /** * 内部方法,请勿使用 - * - * @ignore */ internalSetParent(parent: NodeParent | null) { if (this._parent === parent) { return; } - if (this._parent) { + + if (this._parent && !this.isSlotRoot) { this._parent.children.delete(this); } this._parent = parent; - if (parent) { - this._zLevel = parent.zLevel + 1; - } else { - this._zLevel = -1; - } + } + + private _slotFor?: Prop | null = null; + internalSetSlotFor(slotFor: Prop | null | undefined) { + this._slotFor = slotFor; + } + + get slotFor() { + return this._slotFor; } /** * 移除当前节点 */ remove() { - if (this.parent) { + if (this.parent && !this.isSlotRoot) { this.parent.children.delete(this, true); } } @@ -231,25 +242,10 @@ export default class Node { } 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 { return this.props?.query(path, useStash as any) || null; } @@ -300,28 +296,51 @@ export default class Node { * 获取符合搭建协议-节点 schema 结构 */ get schema(): NodeSchema { - // TODO: .. - return this.exportSchema(true); + return this.export(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 * @param serialize 序列化,加 id 标识符,用于储存为操作记录 */ - exportSchema(serialize = false): NodeSchema { + export(serialize = false): NodeSchema { // TODO... const schema: any = { componentName: this.componentName, - ...this.extras?.value, - props: this.props?.value || {}, - ...this.directives?.value, + ...this.extras?.export(serialize), + props: this.props?.export(serialize) || {}, + ...this.directives?.export(serialize), }; if (serialize) { schema.id = this.id; } if (isNodeParent(this)) { if (this.children.size > 0) { - schema.children = this.children.exportSchema(serialize); + schema.children = this.children.export(serialize); } } else { schema.children = (this.children as NodeContent).value; @@ -387,9 +406,9 @@ export default class Node { export interface NodeParent extends Node { readonly children: NodeChildren; - readonly props: Props; - readonly directives: Props; - readonly extras: Props; + readonly props: Props; + readonly directives: Props; + readonly extras: Props; } 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 { let node: Node; if (copy && isNode(thing)) { - thing = thing.exportSchema(false); + thing = thing.export(false); } if (isNode(thing)) { node = thing; diff --git a/packages/designer/src/designer/document/node/props/stash-space.ts b/packages/designer/src/designer/document/node/props/prop-stash.ts similarity index 78% rename from packages/designer/src/designer/document/node/props/stash-space.ts rename to packages/designer/src/designer/document/node/props/prop-stash.ts index e38d14015..836b2f45b 100644 --- a/packages/designer/src/designer/document/node/props/stash-space.ts +++ b/packages/designer/src/designer/document/node/props/prop-stash.ts @@ -1,8 +1,9 @@ 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 default class StashSpace implements IPropParent { +export default class PropStash implements IPropParent { @obx.val private space: Set = new Set(); @computed private get maps(): Map { const maps = new Map(); @@ -15,7 +16,7 @@ export default class StashSpace implements IPropParent { } private willPurge: () => void; - constructor(write: (item: Prop) => void, before: () => boolean) { + constructor(readonly props: Props, write: (item: Prop) => void) { this.willPurge = autorun(() => { if (this.space.size < 1) { return; @@ -28,11 +29,10 @@ export default class StashSpace implements IPropParent { } } if (pending.length > 0) { + debugger; untracked(() => { - if (before()) { - for (const item of pending) { - write(item); - } + for (const item of pending) { + write(item); } }); } @@ -42,7 +42,7 @@ export default class StashSpace implements IPropParent { get(key: string): Prop { let prop = this.maps.get(key); if (!prop) { - prop = new Prop(this, null, key); + prop = new Prop(this, UNSET, key); this.space.add(prop); } return prop; diff --git a/packages/designer/src/designer/document/node/props/prop.ts b/packages/designer/src/designer/document/node/props/prop.ts index 281d941d1..4ee046485 100644 --- a/packages/designer/src/designer/document/node/props/prop.ts +++ b/packages/designer/src/designer/document/node/props/prop.ts @@ -1,16 +1,19 @@ import { untracked, computed, obx } from '@recore/obx'; import { valueToSource } from '../../../../utils/value-to-source'; -import { CompositeValue, isJSExpression } from '../../../schema'; -import StashSpace from './stash-space'; +import { CompositeValue, isJSExpression, isJSSlot, NodeSchema, NodeData, isNodeSchema } from '../../../schema'; +import PropStash from './prop-stash'; import { uniqueId } from '../../../../utils/unique-id'; import { isPlainObject } from '../../../../utils/is-plain-object'; import { hasOwnProperty } from '../../../../utils/has-own-property'; +import Props from './props'; +import Node from '../node'; export const UNSET = Symbol.for('unset'); export type UNSET = typeof UNSET; export interface IPropParent { delete(prop: Prop): void; + readonly props: Props; } export default class Prop implements IPropParent { @@ -18,11 +21,11 @@ export default class Prop implements IPropParent { 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; } @@ -32,15 +35,27 @@ export default class Prop implements IPropParent { * 属性值 */ @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; } - const type = this._type; if (type === 'literal' || type === 'expression') { return this._value; } + if (type === 'slot') { + return { + type: 'JSSlot', + value: this._slotNode!.export(serialize), + }; + } + if (type === 'map') { if (!this._items) { return this._value; @@ -79,11 +94,14 @@ export default class Prop implements IPropParent { 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 (isJSSlot(val)) { + this.setAsSlot(val.value); + return; + } if (isJSExpression(val)) { this._type = 'expression'; } else { @@ -97,14 +115,42 @@ export default class Prop implements IPropParent { value: valueToSource(val), }; } - if (untracked(() => this._items)) { - this._items!.forEach(prop => prop.purge()); - this._items = null; + this.dispose(); + } + + private dispose() { + const items = untracked(() => this._items); + if (items) { + items.forEach(prop => prop.purge()); } + this._items = null; this._maps = null; if (this.stash) { 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 { } /** - * 值是否包含表达式 - * 包含 JSExpresion | JSSlot 等值 + * 值是否是带类型的 JS + * 比如 JSExpresion | JSSlot 等值 */ - @computed isContainJSExpression(): boolean { + @computed isTypedJS(): boolean { const type = this._type; - if (type === 'expression') { + if (type === 'expression' || type === 'slot') { return true; } if (type === 'literal' || type === 'unset') { return false; } if ((type === 'list' || type === 'map') && this.items) { - return this.items.some(item => item.isContainJSExpression()); + return this.items.some(item => item.isTypedJS()); } return false; } @@ -143,7 +189,7 @@ export default class Prop implements IPropParent { * 是否简单 JSON 数据 */ @computed isJSON() { - return !this.isContainJSExpression(); + return !this.isTypedJS(); } @obx.val private _items: Prop[] | null = null; @@ -189,7 +235,7 @@ export default class Prop implements IPropParent { return this._maps; } - private stash: StashSpace | undefined; + private stash: PropStash | undefined; /** * 键值 @@ -200,12 +246,15 @@ export default class Prop implements IPropParent { */ @obx spread: boolean; + readonly props: Props; + constructor( public parent: IPropParent, value: CompositeValue | UNSET = UNSET, key?: string | number, spread = false, ) { + this.props = parent.props; if (value !== UNSET) { this.value = value; } @@ -257,16 +306,11 @@ export default class Prop implements IPropParent { if (stash) { if (!this.stash) { - this.stash = new StashSpace( - item => { - // item take effect - this.set(String(item.key), item); - item.parent = this; - }, - () => { - return true; - }, - ); + this.stash = new PropStash(this.props, item => { + // item take effect + this.set(String(item.key), item); + item.parent = this; + }); } prop = this.stash.get(entry); if (nest) { @@ -401,6 +445,9 @@ export default class Prop implements IPropParent { this._items.forEach(item => item.purge()); } this._maps = null; + if (this._slotNode && this._slotNode.slotFor === this) { + this._slotNode.purge(); + } } /** diff --git a/packages/designer/src/designer/document/node/props/props.ts b/packages/designer/src/designer/document/node/props/props.ts index 091b094a0..fc87d3935 100644 --- a/packages/designer/src/designer/document/node/props/props.ts +++ b/packages/designer/src/designer/document/node/props/props.ts @@ -1,14 +1,14 @@ import { computed, obx } from '@recore/obx'; import { uniqueId } from '../../../../utils/unique-id'; import { CompositeValue, PropsList, PropsMap } from '../../../schema'; -import StashSpace from './stash-space'; +import PropStash from './prop-stash'; import Prop, { IPropParent } from './prop'; +import { NodeParent } from '../node'; export const UNSET = Symbol.for('unset'); export type UNSET = typeof UNSET; - -export default class Props implements IPropParent { +export default class Props implements IPropParent { readonly id = uniqueId('props'); @obx.val private items: Prop[] = []; @computed private get maps(): Map { @@ -23,15 +23,14 @@ export default class Props implements IPropParent { return maps; } - private stash = new StashSpace( - prop => { - this.items.push(prop); - prop.parent = this; - }, - () => { - return true; - }, - ); + get props(): Props { + return this; + } + + private stash = new PropStash(this, prop => { + this.items.push(prop); + prop.parent = this; + }); /** * 元素个数 @@ -41,6 +40,36 @@ export default class Props implements IPropParent { } @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) { return null; } @@ -48,30 +77,18 @@ export default class Props implements IPropParent { return this.items.map(item => ({ spread: item.spread, name: item.key as string, - value: item.value, + value: item.export(serialize), })); } const maps: any = {}; this.items.forEach(prop => { if (prop.key) { - maps[prop.key] = prop.value; + maps[prop.key] = prop.export(serialize); } }); 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 路径查询属性,如果没有则临时生成一个 */ diff --git a/packages/designer/src/designer/document/node/root-node.ts b/packages/designer/src/designer/document/node/root-node.ts index e1e9b5948..f0701b366 100644 --- a/packages/designer/src/designer/document/node/root-node.ts +++ b/packages/designer/src/designer/document/node/root-node.ts @@ -56,13 +56,13 @@ export default class RootNode extends Node implements NodeParent { get children(): NodeChildren { return this._children as NodeChildren; } - get props(): Props { + get props(): Props { return this._props as any; } - get extras(): Props { + get extras(): Props { return this._extras as any; } - get directives(): Props { + get directives(): Props { return this._directives as any; } internalSetParent(parent: null) {} diff --git a/packages/designer/src/designer/helper/history.ts b/packages/designer/src/designer/helper/history.ts index 65b3dba38..bfa1b89ec 100644 --- a/packages/designer/src/designer/helper/history.ts +++ b/packages/designer/src/designer/helper/history.ts @@ -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 { + serialize(data: NodeSchema): T; + unserialize(data: T): NodeSchema; +} + +let currentSerializion: Serialization = { + 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 = []; + } +} diff --git a/packages/designer/src/designer/helper/session.ts b/packages/designer/src/designer/helper/session.ts new file mode 100644 index 000000000..8095d525d --- /dev/null +++ b/packages/designer/src/designer/helper/session.ts @@ -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; + } +} diff --git a/packages/designer/src/designer/project-view.tsx b/packages/designer/src/designer/project-view.tsx index 1bb28b1f0..406563b8a 100644 --- a/packages/designer/src/designer/project-view.tsx +++ b/packages/designer/src/designer/project-view.tsx @@ -8,7 +8,6 @@ export default class ProjectView extends Component<{ designer: Designer }> { render() { const { designer } = this.props; // TODO: support splitview - console.info(designer.project.documents); return (
{designer.project.documents.map(doc => { diff --git a/packages/designer/src/designer/schema.ts b/packages/designer/src/designer/schema.ts index cb2d4eb91..db7e75418 100644 --- a/packages/designer/src/designer/schema.ts +++ b/packages/designer/src/designer/schema.ts @@ -90,15 +90,14 @@ export type PropsList = Array<{ export type NodeData = NodeSchema | JSExpression | DOMText; -export interface JSExpression { - type: 'JSExpression'; - value: string; -} - export function isJSExpression(data: any): data is 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 { return typeof data === 'string'; }