mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-14 21:12:53 +00:00
feat: history log
This commit is contained in:
parent
9d2967f612
commit
fbb3577bd4
@ -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;
|
||||
|
||||
|
||||
@ -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 />;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* 删除一个节点
|
||||
*/
|
||||
|
||||
@ -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 = '';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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 路径查询属性,如果没有则临时生成一个
|
||||
*/
|
||||
|
||||
@ -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) {}
|
||||
|
||||
@ -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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
44
packages/designer/src/designer/helper/session.ts
Normal file
44
packages/designer/src/designer/helper/session.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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 => {
|
||||
|
||||
@ -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';
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user