diff --git a/packages/demo/src/app.ts b/packages/demo/src/app.ts index 9c6f9eea7..8f55a5be0 100644 --- a/packages/demo/src/app.ts +++ b/packages/demo/src/app.ts @@ -31,22 +31,12 @@ export default class App extends ViewController { SettingsPane }; - editor = { - on(type: string, fn: any) { - emitter.on(type, fn); - } - } + editor = emitter; $didMount() { const designer = this.$refs.d.designer; const pane = this.$refs.pane; (window as any).LCDesigner = designer; - if (designer.project.activedDocument) { - emitter.emit('designer.actived-document-change', designer.project.activedDocument); - } - designer.project.onActivedDocumentChange((doc: any) => { - emitter.emit('designer.actived-document-change', doc); - }); (this.editor as any).designer = designer; designer.dragon.from(pane, () => { return { diff --git a/packages/demo/src/app.vx b/packages/demo/src/app.vx index a7ac12b57..ce1f1d754 100644 --- a/packages/demo/src/app.vx +++ b/packages/demo/src/app.vx @@ -1,5 +1,6 @@
; dragGhostComponent?: ReactComponentType; suspensed?: boolean; - componentDescriptionSpecs?: ComponentDescription[]; + componentsDescription?: ComponentDescription[]; + eventPipe?: EventEmitter; onMount?: (designer: Designer) => void; onDragstart?: (e: LocateEvent) => void; onDrag?: (e: LocateEvent) => void; onDragend?: (e: { dragObject: DragObject; copy: boolean }, loc?: Location) => void; - // TODO: ...add other events support [key: string]: any; } @@ -40,7 +41,21 @@ export default class Designer { readonly hovering = new Hovering(); readonly project: Project; + get currentDocument() { + return this.project.currentDocument; + } + + get currentHistory() { + return this.currentDocument?.history; + } + + get currentSelection() { + return this.currentDocument?.selection; + } + constructor(props: DesignerProps) { + this.setProps(props); + this.project = new Project(this, props.defaultSchema); this.dragon.onDragstart(e => { @@ -53,12 +68,14 @@ export default class Designer { if (this.props?.onDragstart) { this.props.onDragstart(e); } + this.postEvent('dragstart', e); }); this.dragon.onDrag(e => { if (this.props?.onDrag) { this.props.onDrag(e); } + this.postEvent('drag', e); }); this.dragon.onDragend(e => { @@ -84,6 +101,7 @@ export default class Designer { if (this.props?.onDragend) { this.props.onDragend(e, loc); } + this.postEvent('dragend', e, loc); this.hovering.enable = true; }); @@ -91,7 +109,30 @@ export default class Designer { node.document.simulator?.scrollToNode(node, detail); }); - this.setProps(props); + let selectionDispose: undefined | (() => void); + const setupSelection = () => { + if (selectionDispose) { + selectionDispose(); + selectionDispose = undefined; + } + if (this.currentSelection) { + const currentSelection = this.currentSelection; + selectionDispose = currentSelection.onSelectionChange(() => { + this.postEvent('current-selection-change', currentSelection); + }); + } + } + this.project.onCurrentDocumentChange(() => { + this.postEvent('current-document-change', this.currentDocument); + this.postEvent('current-selection-change', this.currentSelection); + this.postEvent('current-history-change', this.currentHistory); + setupSelection(); + }); + setupSelection(); + } + + postEvent(event: string, ...args: any[]) { + this.props?.eventPipe?.emit(`designer.${event}`, ...args); } private _dropLocation?: Location; @@ -128,7 +169,7 @@ export default class Designer { * 获得合适的插入位置 */ getSuitableInsertion() { - const activedDoc = this.project.activedDocument; + const activedDoc = this.project.currentDocument; if (!activedDoc) { return null; } @@ -165,8 +206,8 @@ export default class Designer { if (props.suspensed !== this.props.suspensed && props.suspensed != null) { this.suspensed = props.suspensed; } - if (props.componentDescriptionSpecs !== this.props.componentDescriptionSpecs && props.componentDescriptionSpecs != null) { - this.buildComponentTypesMap(props.componentDescriptionSpecs); + if (props.componentsDescription !== this.props.componentsDescription && props.componentsDescription != null) { + this.buildComponentTypesMap(props.componentsDescription); } } else { // init hotkeys @@ -182,8 +223,8 @@ export default class Designer { if (props.suspensed != null) { this.suspensed = props.suspensed; } - if (props.componentDescriptionSpecs != null) { - this.buildComponentTypesMap(props.componentDescriptionSpecs); + if (props.componentsDescription != null) { + this.buildComponentTypesMap(props.componentsDescription); } } this.props = props; diff --git a/packages/designer/src/designer/document/document-model.ts b/packages/designer/src/designer/document/document-model.ts index 720c4a1b4..d33cc0f17 100644 --- a/packages/designer/src/designer/document/document-model.ts +++ b/packages/designer/src/designer/document/document-model.ts @@ -41,11 +41,11 @@ export default class DocumentModel { } get fileName(): string { - return (this.rootNode.extras.get('fileName')?.value as string) || this.id; + return (this.rootNode.getExtraProp('fileName')?.getAsString()) || this.id; } set fileName(fileName: string) { - this.rootNode.extras.get('fileName', true).value = fileName; + this.rootNode.getExtraProp('fileName', true)?.setValue(fileName); } constructor(readonly project: Project, schema: RootSchema) { @@ -290,7 +290,7 @@ export default class DocumentModel { return this.designer.getComponentType(componentName); } - @obx.ref private _opened: boolean = true; + @obx.ref private _opened: boolean = false; @obx.ref private _suspensed: boolean = false; /** @@ -341,7 +341,11 @@ export default class DocumentModel { * 打开,已载入,默认建立时就打开状态,除非手动关闭 */ open(): void { + const originState = this._opened; this._opened = true; + if (originState === false) { + this.designer.postEvent('document-open', this); + } if (this._suspensed) { this.setSuspense(false); } else { diff --git a/packages/designer/src/designer/document/node/node.ts b/packages/designer/src/designer/document/node/node.ts index 43e0e0c1e..09799c8e4 100644 --- a/packages/designer/src/designer/document/node/node.ts +++ b/packages/designer/src/designer/document/node/node.ts @@ -1,6 +1,6 @@ import { obx, computed, untracked } from '@recore/obx'; import { NodeSchema, NodeData, PropsMap, PropsList } from '../../schema'; -import Props from './props/props'; +import Props, { EXTRA_KEY_PREFIX } from './props/props'; import DocumentModel from '../document-model'; import NodeChildren from './node-children'; import Prop from './props/prop'; @@ -8,8 +8,6 @@ import NodeContent from './node-content'; import { Component } from '../../simulator'; import { ComponentType } from '../../component-type'; -const DIRECTIVES = ['condition', 'conditionGroup', 'loop', 'loopArgs', 'title', 'ignore', 'hidden', 'locked']; - /** * 基础节点 * @@ -51,20 +49,11 @@ export default class Node { */ readonly componentName: string; 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 { return this._props; } - get directives(): Props | undefined { - return this._directives; - } - get extras(): Props | undefined { - return this._extras; - } /** * 父级节点 */ @@ -88,7 +77,7 @@ export default class Node { } @computed get title(): string { - let t = this.getDirective('x-title'); + let t = this.getExtraProp('title'); if (!t && this.componentType.descriptor) { t = this.getProp(this.componentType.descriptor, false); } @@ -111,15 +100,7 @@ export default class Node { this.componentName = componentName; this._slotFor = slotFor; if (isNodeParent(this)) { - this._props = new Props(this, props); - this._directives = new Props(this, {}); - Object.keys(extras).forEach(key => { - if (DIRECTIVES.indexOf(key) > -1) { - this._directives!.add((extras as any)[key], key); - delete (extras as any)[key]; - } - }); - this._extras = new Props(this, extras as any); + this._props = new Props(this, props, extras); this._children = new NodeChildren(this as NodeParent, children || []); } else { this._children = new NodeContent(children); @@ -197,22 +178,15 @@ export default class Node { /** * 节点组件描述 */ - @obx.ref get componentType(): ComponentType { + @computed get componentType(): ComponentType { return this.document.getComponentType(this.componentName, this.component); } - @obx.ref get propsData(): PropsMap | PropsList | null { + @computed get propsData(): PropsMap | PropsList | null { if (!this.isNodeParent || this.componentName === 'Fragment') { return null; } - return this.props?.value || null; - } - - get directivesData(): PropsMap | null { - if (!this.isNodeParent) { - return null; - } - return this.directives?.value as PropsMap || null; + return this.props?.export(true).props || null; } private _conditionGroup: string | null = null; @@ -261,6 +235,10 @@ export default class Node { return this.props?.query(path, useStash as any) || null; } + getExtraProp(key: string, useStash: boolean = true): Prop | null { + return this.props?.get(EXTRA_KEY_PREFIX + key, useStash) || null; + } + /** * 获取单个属性值 */ @@ -289,10 +267,6 @@ export default class Node { this.props?.import(props); } - getDirective(name: string, useStash: boolean = true): Prop | null { - return this.directives?.get(name, useStash as any) || null; - } - /** * 获取节点在父容器中的索引 */ @@ -346,16 +320,7 @@ export default class Node { 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._props!.import(props, extras); this._children.import(children, checkId); } else { this._children.import(children); @@ -367,12 +332,11 @@ export default class Node { * @param serialize 序列化,加 id 标识符,用于储存为操作记录 */ export(serialize = false): NodeSchema { - // TODO... + const { props, extras } = this.props?.export(serialize) || {}; const schema: any = { componentName: this.componentName, - ...this.extras?.export(serialize), - props: this.props?.export(serialize) || {}, - ...this.directives?.export(serialize), + props, + ...extras, }; if (serialize) { schema.id = this.id; @@ -437,8 +401,6 @@ export default class Node { this.children.purge(); } this.props?.purge(); - this.directives?.purge(); - this.extras?.purge(); this.document.internalRemoveAndPurgeNode(this); } } @@ -446,8 +408,6 @@ export default class Node { export interface NodeParent extends Node { readonly children: NodeChildren; readonly props: Props; - readonly directives: Props; - readonly extras: Props; } export function isNode(node: any): node is Node { diff --git a/packages/designer/src/designer/document/node/props/prop.ts b/packages/designer/src/designer/document/node/props/prop.ts index 6ed5da245..3cd55d304 100644 --- a/packages/designer/src/designer/document/node/props/prop.ts +++ b/packages/designer/src/designer/document/node/props/prop.ts @@ -140,6 +140,10 @@ export default class Prop implements IPropParent { this.dispose(); } + getValue(serialize = true) { + // todo: + } + private dispose() { const items = untracked(() => this._items); if (items) { diff --git a/packages/designer/src/designer/document/node/props/props.ts b/packages/designer/src/designer/document/node/props/props.ts index 8581ac515..c604ef88c 100644 --- a/packages/designer/src/designer/document/node/props/props.ts +++ b/packages/designer/src/designer/document/node/props/props.ts @@ -1,10 +1,12 @@ import { computed, obx } from '@recore/obx'; import { uniqueId } from '../../../../../../utils/unique-id'; -import { CompositeValue, PropsList, PropsMap } from '../../../schema'; +import { CompositeValue, PropsList, PropsMap, CompositeObject } from '../../../schema'; import PropStash from './prop-stash'; import Prop, { IPropParent, UNSET } from './prop'; import { NodeParent } from '../node'; +export const EXTRA_KEY_PREFIX = '__'; + export default class Props implements IPropParent { readonly id = uniqueId('props'); @obx.val private items: Prop[] = []; @@ -36,22 +38,23 @@ export default class Props implements IPropParent { return this.items.length; } - @computed get value(): PropsMap | PropsList | null { - return this.export(true); - } - @obx type: 'map' | 'list' = 'map'; - constructor(readonly owner: NodeParent, value?: PropsMap | PropsList | null) { + constructor(readonly owner: NodeParent, value?: PropsMap | PropsList | null, extras?: object) { 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)); } + if (extras) { + Object.keys(extras).forEach(key => { + this.items.push(new Prop(this, (extras as any)[key], EXTRA_KEY_PREFIX + key)); + }); + } } - import(value?: PropsMap | PropsList | null) { + import(value?: PropsMap | PropsList | null, extras?: object) { this.stash.clear(); const originItems = this.items; if (Array.isArray(value)) { @@ -64,6 +67,11 @@ export default class Props implements IPropParent { this.type = 'map'; this.items = []; } + if (extras) { + Object.keys(extras).forEach(key => { + this.items.push(new Prop(this, (extras as any)[key], EXTRA_KEY_PREFIX + key)); + }); + } originItems.forEach(item => item.purge()); } @@ -73,27 +81,52 @@ export default class Props implements IPropParent { }); } - export(serialize = false): PropsMap | PropsList | null { + export(serialize = false): { props?: PropsMap | PropsList; extras?: object} { if (this.items.length < 1) { - return null; + return {}; } + let props: any = {}; + const extras: any = {}; if (this.type === 'list') { - return this.items.map(item => { - const v = item.export(serialize); - return { - spread: item.spread, - name: item.key as string, - value: v === UNSET ? null : v, - }; + props = []; + this.items.forEach(item => { + let value = item.export(serialize); + if (value === UNSET) { + value = null; + } + let name = item.key as string; + if (name && typeof name === 'string' && name.startsWith(EXTRA_KEY_PREFIX)) { + name = name.substr(EXTRA_KEY_PREFIX.length); + extras[name] = value; + } else { + props.push({ + spread: item.spread, + name, + value, + }); + } + }); + } else { + this.items.forEach(item => { + let name = item.key as string; + if (name == null) { + // todo ...spread + return; + } + let value = item.export(serialize); + if (value === UNSET) { + value = null; + } + if (typeof name === 'string' && name.startsWith(EXTRA_KEY_PREFIX)) { + name = name.substr(EXTRA_KEY_PREFIX.length); + extras[name] = value; + } else { + props[name] = value; + } }); } - const maps: any = {}; - this.items.forEach(prop => { - if (prop.key) { - maps[prop.key] = prop.export(serialize); - } - }); - return maps; + + return { props, extras }; } /** diff --git a/packages/designer/src/designer/document/node/root-node.ts b/packages/designer/src/designer/document/node/root-node.ts index f0701b366..46bf1d51d 100644 --- a/packages/designer/src/designer/document/node/root-node.ts +++ b/packages/designer/src/designer/document/node/root-node.ts @@ -59,12 +59,6 @@ export default class RootNode extends Node implements NodeParent { get props(): Props { return this._props as any; } - get extras(): Props { - return this._extras as any; - } - get directives(): Props { - return this._directives as any; - } internalSetParent(parent: null) {} constructor(readonly document: DocumentModel, rootSchema: RootSchema) { diff --git a/packages/designer/src/designer/document/selection.ts b/packages/designer/src/designer/document/selection.ts index dc167d0fd..7f648e258 100644 --- a/packages/designer/src/designer/document/selection.ts +++ b/packages/designer/src/designer/document/selection.ts @@ -4,57 +4,63 @@ import DocumentModel from './document-model'; import { EventEmitter } from 'events'; export class Selection { - @obx.val private selected: string[] = []; private emitter = new EventEmitter(); + @obx.val private _selected: string[] = []; + /** + * 选中的节点 id + */ + get selected(): string[] { + return this._selected; + } - constructor(private doc: DocumentModel) {} + constructor(readonly doc: DocumentModel) {} /** * 选中 */ select(id: string) { - if (this.selected.length === 1 && this.selected.indexOf(id) > -1) { + if (this._selected.length === 1 && this._selected.indexOf(id) > -1) { // avoid cause reaction return; } - this.selected = [id]; - this.emitter.emit('selectionchange'); + this._selected = [id]; + this.emitter.emit('selectionchange', this._selected); } /** * 批量选中 */ selectAll(ids: string[]) { - this.selected = ids; - this.emitter.emit('selectionchange'); + this._selected = ids; + this.emitter.emit('selectionchange', this._selected); } /** * 清除选中 */ clear() { - if (this.selected.length < 1) { + if (this._selected.length < 1) { return; } - this.selected = []; - this.emitter.emit('selectionchange'); + this._selected = []; + this.emitter.emit('selectionchange', this._selected); } /** * 整理选中 */ dispose() { - const l = this.selected.length; + const l = this._selected.length; let i = l; while (i-- > 0) { - const id = this.selected[i]; + const id = this._selected[i]; if (!this.doc.hasNode(id)) { - this.selected.splice(i, 1); + this._selected.splice(i, 1); } } - if (this.selected.length !== l) { - this.emitter.emit('selectionchange'); + if (this._selected.length !== l) { + this.emitter.emit('selectionchange', this._selected); } } @@ -62,29 +68,29 @@ export class Selection { * 添加选中 */ add(id: string) { - if (this.selected.indexOf(id) > -1) { + if (this._selected.indexOf(id) > -1) { return; } - this.selected.push(id); - this.emitter.emit('selectionchange'); + this._selected.push(id); + this.emitter.emit('selectionchange', this._selected); } /** * 是否选中 */ has(id: string) { - return this.selected.indexOf(id) > -1; + return this._selected.indexOf(id) > -1; } /** * 移除选中 */ remove(id: string) { - let i = this.selected.indexOf(id); + let i = this._selected.indexOf(id); if (i > -1) { - this.selected.splice(i, 1); - this.emitter.emit('selectionchange'); + this._selected.splice(i, 1); + this.emitter.emit('selectionchange', this._selected); } } @@ -92,7 +98,7 @@ export class Selection { * 选区是否包含节点 */ containsNode(node: Node) { - for (const id of this.selected) { + for (const id of this._selected) { const parent = this.doc.getNode(id); if (parent?.contains(node)) { return true; @@ -106,7 +112,7 @@ export class Selection { */ getNodes() { const nodes = []; - for (const id of this.selected) { + for (const id of this._selected) { const node = this.doc.getNode(id); if (node) { nodes.push(node); @@ -120,7 +126,7 @@ export class Selection { */ getTopNodes() { const nodes = []; - for (const id of this.selected) { + for (const id of this._selected) { const node = this.doc.getNode(id); if (!node) { continue; diff --git a/packages/designer/src/designer/project.ts b/packages/designer/src/designer/project.ts index a53af5dff..5b5ecde9e 100644 --- a/packages/designer/src/designer/project.ts +++ b/packages/designer/src/designer/project.ts @@ -27,7 +27,7 @@ export default class Project { }); } - @computed get activedDocument() { + @computed get currentDocument() { return this.documents.find(doc => doc.actived); } @@ -111,7 +111,7 @@ export default class Project { doc.suspense(); } }); - this.emitter.emit('actived-document-change', actived); + this.emitter.emit('current-document-change', actived); } closeOthers(opened: DocumentModel) { @@ -122,13 +122,14 @@ export default class Project { }); } + onCurrentDocumentChange(fn: (doc: DocumentModel) => void): () => void { + this.emitter.on('current-document-change', fn); + return () => { + this.emitter.removeListener('current-document-change', fn); + }; + } // 通知标记删除,需要告知服务端 // 项目角度编辑不是全量打开所有文档,是按需加载,哪个更新就通知更新谁, // 哪个删除就 - onActivedDocumentChange(fn: (doc: DocumentModel) => void): () => void { - this.emitter.on('actived-document-change', fn); - return () => { - this.emitter.removeListener('actived-document-change', fn); - }; - } + } diff --git a/packages/plugin-settings-pane/src/builtin-setters/object-setter/object-setter.tsx b/packages/plugin-settings-pane/src/builtin-setters/object-setter/object-setter.tsx index d1fbb7d8a..39ca6b11e 100644 --- a/packages/plugin-settings-pane/src/builtin-setters/object-setter/object-setter.tsx +++ b/packages/plugin-settings-pane/src/builtin-setters/object-setter/object-setter.tsx @@ -1,5 +1,5 @@ import { Component } from "react"; -import { FieldConfig } from '../../main'; +import { FieldConfig, SettingField } from '../../main'; class ObjectSetter extends Component<{ mode?: 'popup' | 'row' | 'form'; @@ -29,6 +29,7 @@ interface ObjectSetterConfig { // for table|list row class RowSetter extends Component<{ + decriptor?: string | ((rowField: SettingField) => string); config: ObjectSetterConfig; columnsLimit?: number; }> { diff --git a/packages/plugin-settings-pane/src/main.ts b/packages/plugin-settings-pane/src/main.ts index 1b84b02bf..1d9f58787 100644 --- a/packages/plugin-settings-pane/src/main.ts +++ b/packages/plugin-settings-pane/src/main.ts @@ -4,9 +4,9 @@ import { ComponentType } from '../../designer/src/designer/component-type'; import Node from '../../designer/src/designer/document/node/node'; import { TitleContent } from './title'; import { ReactElement, ComponentType as ReactComponentType, isValidElement } from 'react'; -import DocumentModel from '../../designer/src/designer/document/document-model'; import { isReactComponent } from '../../utils/is-react'; import Designer from '../../designer/src/designer/designer'; +import { Selection } from '../../designer/src/designer/document/selection'; export interface SettingTarget { // 所设置的节点集,至少一个 @@ -347,7 +347,6 @@ export function isSettingField(obj: any): obj is SettingField { return obj && obj.isSettingField; } - export class SettingsMain implements SettingTarget { private emitter = new EventEmitter(); @@ -402,38 +401,26 @@ export class SettingsMain implements SettingTarget { private _designer?: Designer; get designer() { - return this._designer; + return this._designer || this.editor.designer; } constructor(readonly editor: any) { - let selectionChangeDispose: any = null; - const setupDoc = (doc: DocumentModel) => { - if (selectionChangeDispose) { - selectionChangeDispose(); - selectionChangeDispose = null; - } - if (doc) { + const setupSelection = (selection?: Selection) => { + if (selection) { if (!this._designer) { - this._designer = doc.designer; + this._designer = selection.doc.designer; } - const selection = doc.selection; this.setup(selection.getNodes()); - selectionChangeDispose = doc.selection.onSelectionChange(() => { - this.setup(selection.getNodes()); - }); } else { this.setup([]); } }; - const activedDispose = editor.on('designer.actived-document-change', setupDoc); + editor.on('designer.current-selection-change', setupSelection); if (editor.designer) { - setupDoc(editor.designer.project.activedDocument); + setupSelection(editor.designer.currentSelection); } this.disposeListener = () => { - if (selectionChangeDispose) { - selectionChangeDispose(); - } - activedDispose(); + editor.removeListener('designer.current-selection-change', setupSelection); }; }