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 { 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;

View File

@ -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 <Fragment />;
}
const instances = host.getComponentInstances(current);
console.info('current instances', instances)
if (!instances || instances.length < 1) {
return <Fragment />;
}

View File

@ -290,8 +290,6 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
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<SimulatorProps> {
this.sensing = true;
this.scroller.scrolling(e);
const dropTarget = this.getDropTarget(e);
console.info('aa', dropTarget);
if (!dropTarget) {
return null;
}

View File

@ -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 {

View File

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

View File

@ -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<string, Node>();
private nodes = new Set<Node>();
@obx.val private nodes = new Set<Node>();
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 {

View File

@ -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<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;
}
/*
// 用于数据重新灌入
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) {
this.import(value);
}
import(value: any) {
const type = typeof value;
if (value == null) {
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 Props from './props/props';
import DocumentModel from '../document-model';
@ -50,19 +50,19 @@ export default class Node {
* * Component /
*/
readonly componentName: string;
protected _props?: Props<Node>;
protected _directives?: Props<Node>;
protected _extras?: Props<Node>;
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<Node> | undefined {
get props(): Props | undefined {
return this._props;
}
get directives(): Props<Node> | undefined {
get directives(): Props | undefined {
return this._directives;
}
get extras(): Props<Node> | 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<Node>;
readonly directives: Props<Node>;
readonly extras: Props<Node>;
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;

View File

@ -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<Prop> = new Set();
@computed private get maps(): Map<string, Prop> {
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;

View File

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

View File

@ -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<O = any> implements IPropParent {
export default class Props implements IPropParent {
readonly id = uniqueId('props');
@obx.val private items: Prop[] = [];
@computed private get maps(): Map<string, Prop> {
@ -23,15 +23,14 @@ export default class Props<O = any> 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<O = any> 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<O = any> 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
*/

View File

@ -56,13 +56,13 @@ export default class RootNode extends Node implements NodeParent {
get children(): NodeChildren {
return this._children as NodeChildren;
}
get props(): Props<RootNode> {
get props(): Props {
return this._props as any;
}
get extras(): Props<RootNode> {
get extras(): Props {
return this._extras as any;
}
get directives(): Props<RootNode> {
get directives(): Props {
return this._directives as any;
}
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() {
const { designer } = this.props;
// TODO: support splitview
console.info(designer.project.documents);
return (
<div className="lc-project">
{designer.project.documents.map(doc => {

View File

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