mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-03-05 01:37:17 +00:00
624 lines
18 KiB
TypeScript
624 lines
18 KiB
TypeScript
import { ComponentType } from 'react';
|
||
import { obx, computed, autorun, makeObservable, IReactionPublic, IReactionOptions, IReactionDisposer } from '@alilc/lowcode-editor-core';
|
||
import {
|
||
IPublicTypeProjectSchema,
|
||
IPublicTypeComponentMetadata,
|
||
IPublicTypeComponentAction,
|
||
IPublicTypeNpmInfo,
|
||
IPublicModelEditor,
|
||
IPublicTypeCompositeObject,
|
||
IPublicTypePropsList,
|
||
IPublicTypeNodeSchema,
|
||
IPublicTypePropsTransducer,
|
||
IShellModelFactory,
|
||
IPublicModelDragObject,
|
||
IPublicModelScrollable,
|
||
IPublicModelScroller,
|
||
IPublicTypeLocationData,
|
||
IPublicEnumTransformStage,
|
||
IPublicModelDragon,
|
||
IPublicModelDropLocation,
|
||
} from '@alilc/lowcode-types';
|
||
import { megreAssets, IPublicTypeAssetsJson, isNodeSchema, isDragNodeObject, isDragNodeDataObject, isLocationChildrenDetail, Logger } from '@alilc/lowcode-utils';
|
||
import { Project } from '../project';
|
||
import { Node, DocumentModel, insertChildren, INode } from '../document';
|
||
import { ComponentMeta, IComponentMeta } from '../component-meta';
|
||
import { INodeSelector, Component } from '../simulator';
|
||
import { Scroller } from './scroller';
|
||
import { Dragon, IDragon, ILocateEvent } from './dragon';
|
||
import { ActiveTracker, IActiveTracker } from './active-tracker';
|
||
import { Detecting } from './detecting';
|
||
import { DropLocation } from './location';
|
||
import { OffsetObserver, createOffsetObserver } from './offset-observer';
|
||
import { SettingTopEntry } from './setting';
|
||
import { BemToolsManager } from '../builtin-simulator/bem-tools/manager';
|
||
import { ComponentActions } from '../component-actions';
|
||
|
||
const logger = new Logger({ level: 'warn', bizName: 'designer' });
|
||
|
||
export interface DesignerProps {
|
||
[key: string]: any;
|
||
editor: IPublicModelEditor;
|
||
shellModelFactory: IShellModelFactory;
|
||
className?: string;
|
||
style?: object;
|
||
defaultSchema?: IPublicTypeProjectSchema;
|
||
hotkeys?: object;
|
||
viewName?: string;
|
||
simulatorProps?: object | ((document: DocumentModel) => object);
|
||
simulatorComponent?: ComponentType<any>;
|
||
dragGhostComponent?: ComponentType<any>;
|
||
suspensed?: boolean;
|
||
componentMetadatas?: IPublicTypeComponentMetadata[];
|
||
globalComponentActions?: IPublicTypeComponentAction[];
|
||
onMount?: (designer: Designer) => void;
|
||
onDragstart?: (e: ILocateEvent) => void;
|
||
onDrag?: (e: ILocateEvent) => void;
|
||
onDragend?: (
|
||
e: { dragObject: IPublicModelDragObject; copy: boolean },
|
||
loc?: DropLocation,
|
||
) => void;
|
||
}
|
||
|
||
export interface IDesigner {
|
||
|
||
get dragon(): IPublicModelDragon;
|
||
|
||
get activeTracker(): IActiveTracker;
|
||
|
||
get componentActions(): ComponentActions;
|
||
|
||
get editor(): IPublicModelEditor;
|
||
|
||
createScroller(scrollable: IPublicModelScrollable): IPublicModelScroller;
|
||
|
||
/**
|
||
* 创建插入位置,考虑放到 dragon 中
|
||
*/
|
||
createLocation(locationData: IPublicTypeLocationData): IPublicModelDropLocation;
|
||
|
||
get componentsMap(): { [key: string]: IPublicTypeNpmInfo | Component };
|
||
|
||
loadIncrementalAssets(incrementalAssets: IPublicTypeAssetsJson): Promise<void>;
|
||
|
||
getComponentMeta(
|
||
componentName: string,
|
||
generateMetadata?: () => IPublicTypeComponentMetadata | null,
|
||
): IComponentMeta;
|
||
|
||
createComponentMeta(data: IPublicTypeComponentMetadata): IComponentMeta | null;
|
||
|
||
getComponentMetasMap(): Map<string, IComponentMeta>;
|
||
|
||
addPropsReducer(reducer: IPublicTypePropsTransducer, stage: IPublicEnumTransformStage): void;
|
||
}
|
||
|
||
export class Designer implements IDesigner {
|
||
dragon: IDragon;
|
||
|
||
viewName: string | undefined;
|
||
|
||
readonly componentActions = new ComponentActions();
|
||
|
||
readonly activeTracker = new ActiveTracker();
|
||
|
||
readonly detecting = new Detecting();
|
||
|
||
readonly project: Project;
|
||
|
||
readonly editor: IPublicModelEditor;
|
||
|
||
readonly bemToolsManager = new BemToolsManager(this);
|
||
|
||
readonly shellModelFactory: IShellModelFactory;
|
||
|
||
private _dropLocation?: DropLocation;
|
||
|
||
private propsReducers = new Map<IPublicEnumTransformStage, IPublicTypePropsTransducer[]>();
|
||
|
||
private _lostComponentMetasMap = new Map<string, ComponentMeta>();
|
||
|
||
private props?: DesignerProps;
|
||
|
||
private oobxList: OffsetObserver[] = [];
|
||
|
||
@obx.ref private _componentMetasMap = new Map<string, IComponentMeta>();
|
||
|
||
@obx.ref private _simulatorComponent?: ComponentType<any>;
|
||
|
||
@obx.ref private _simulatorProps?: object | ((project: Project) => object);
|
||
|
||
@obx.ref private _suspensed = false;
|
||
|
||
get currentDocument() {
|
||
return this.project.currentDocument;
|
||
}
|
||
|
||
get currentHistory() {
|
||
return this.currentDocument?.history;
|
||
}
|
||
|
||
get currentSelection() {
|
||
return this.currentDocument?.selection;
|
||
}
|
||
|
||
constructor(props: DesignerProps) {
|
||
makeObservable(this);
|
||
const { editor, viewName, shellModelFactory } = props;
|
||
this.editor = editor;
|
||
this.viewName = viewName;
|
||
this.shellModelFactory = shellModelFactory;
|
||
this.setProps(props);
|
||
|
||
this.project = new Project(this, props.defaultSchema, viewName);
|
||
|
||
this.dragon = new Dragon(this);
|
||
this.dragon.onDragstart((e) => {
|
||
this.detecting.enable = false;
|
||
const { dragObject } = e;
|
||
if (isDragNodeObject(dragObject)) {
|
||
if (dragObject.nodes.length === 1) {
|
||
if (dragObject.nodes[0].parent) {
|
||
// ensure current selecting
|
||
dragObject.nodes[0].select();
|
||
} else {
|
||
this.currentSelection?.clear();
|
||
}
|
||
}
|
||
} else {
|
||
this.currentSelection?.clear();
|
||
}
|
||
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) => {
|
||
const { dragObject, copy } = e;
|
||
logger.debug('onDragend: dragObject ', dragObject, ' copy ', copy);
|
||
const loc = this._dropLocation;
|
||
if (loc) {
|
||
if (isLocationChildrenDetail(loc.detail) && loc.detail.valid !== false) {
|
||
let nodes: Node[] | undefined;
|
||
if (isDragNodeObject(dragObject)) {
|
||
nodes = insertChildren(loc.target, [...dragObject.nodes], loc.detail.index, copy);
|
||
} else if (isDragNodeDataObject(dragObject)) {
|
||
// process nodeData
|
||
const nodeData = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data];
|
||
const isNotNodeSchema = nodeData.find((item) => !isNodeSchema(item));
|
||
if (isNotNodeSchema) {
|
||
return;
|
||
}
|
||
nodes = insertChildren(loc.target, nodeData, loc.detail.index);
|
||
}
|
||
if (nodes) {
|
||
loc.document.selection.selectAll(nodes.map((o) => o.id));
|
||
setTimeout(() => this.activeTracker.track(nodes![0]), 10);
|
||
}
|
||
}
|
||
}
|
||
if (this.props?.onDragend) {
|
||
this.props.onDragend(e, loc);
|
||
}
|
||
this.postEvent('dragend', e, loc);
|
||
this.detecting.enable = true;
|
||
});
|
||
|
||
this.activeTracker.onChange(({ node, detail }) => {
|
||
node.document.simulator?.scrollToNode(node, detail);
|
||
});
|
||
|
||
let historyDispose: undefined | (() => void);
|
||
const setupHistory = () => {
|
||
if (historyDispose) {
|
||
historyDispose();
|
||
historyDispose = undefined;
|
||
}
|
||
this.postEvent('history.change', this.currentHistory);
|
||
if (this.currentHistory) {
|
||
const { currentHistory } = this;
|
||
historyDispose = currentHistory.onStateChange(() => {
|
||
this.postEvent('history.change', currentHistory);
|
||
});
|
||
}
|
||
};
|
||
this.project.onCurrentDocumentChange(() => {
|
||
this.postEvent('current-document.change', this.currentDocument);
|
||
this.postEvent('selection.change', this.currentSelection);
|
||
this.postEvent('history.change', this.currentHistory);
|
||
this.setupSelection();
|
||
setupHistory();
|
||
});
|
||
this.postEvent('init', this);
|
||
this.setupSelection();
|
||
setupHistory();
|
||
}
|
||
|
||
setupSelection = () => {
|
||
let selectionDispose: undefined | (() => void);
|
||
if (selectionDispose) {
|
||
selectionDispose();
|
||
selectionDispose = undefined;
|
||
}
|
||
const { currentSelection } = this;
|
||
// TODO: 避免选中 Page 组件,默认选中第一个子节点;新增规则 或 判断 Live 模式
|
||
if (
|
||
currentSelection &&
|
||
currentSelection.selected.length === 0 &&
|
||
this.simulatorProps?.designMode === 'live'
|
||
) {
|
||
const rootNodeChildrens = this.currentDocument.getRoot().getChildren().children;
|
||
if (rootNodeChildrens.length > 0) {
|
||
currentSelection.select(rootNodeChildrens[0].id);
|
||
}
|
||
}
|
||
this.postEvent('selection.change', currentSelection);
|
||
if (currentSelection) {
|
||
selectionDispose = currentSelection.onSelectionChange(() => {
|
||
this.postEvent('selection.change', currentSelection);
|
||
});
|
||
}
|
||
};
|
||
|
||
postEvent(event: string, ...args: any[]) {
|
||
this.editor.eventBus.emit(`designer.${event}`, ...args);
|
||
}
|
||
|
||
get dropLocation() {
|
||
return this._dropLocation;
|
||
}
|
||
|
||
/**
|
||
* 创建插入位置,考虑放到 dragon 中
|
||
*/
|
||
createLocation(locationData: IPublicTypeLocationData): DropLocation {
|
||
const loc = new DropLocation(locationData);
|
||
if (this._dropLocation && this._dropLocation.document !== loc.document) {
|
||
this._dropLocation.document.dropLocation = null;
|
||
}
|
||
this._dropLocation = loc;
|
||
this.postEvent('dropLocation.change', loc);
|
||
loc.document.dropLocation = loc;
|
||
this.activeTracker.track({ node: loc.target, detail: loc.detail });
|
||
return loc;
|
||
}
|
||
|
||
/**
|
||
* 清除插入位置
|
||
*/
|
||
clearLocation() {
|
||
if (this._dropLocation) {
|
||
this._dropLocation.document.dropLocation = null;
|
||
}
|
||
this.postEvent('dropLocation.change', undefined);
|
||
this._dropLocation = undefined;
|
||
}
|
||
|
||
createScroller(scrollable: IPublicModelScrollable): IPublicModelScroller {
|
||
return new Scroller(scrollable);
|
||
}
|
||
|
||
createOffsetObserver(nodeInstance: INodeSelector): OffsetObserver | null {
|
||
const oobx = createOffsetObserver(nodeInstance);
|
||
this.clearOobxList();
|
||
if (oobx) {
|
||
this.oobxList.push(oobx);
|
||
}
|
||
return oobx;
|
||
}
|
||
|
||
private clearOobxList(force?: boolean) {
|
||
let l = this.oobxList.length;
|
||
if (l > 20 || force) {
|
||
while (l-- > 0) {
|
||
if (this.oobxList[l].isPurged()) {
|
||
this.oobxList.splice(l, 1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
touchOffsetObserver() {
|
||
this.clearOobxList(true);
|
||
this.oobxList.forEach((item) => item.compute());
|
||
}
|
||
|
||
createSettingEntry(nodes: Node[]) {
|
||
return new SettingTopEntry(this.editor, nodes);
|
||
}
|
||
|
||
/**
|
||
* 获得合适的插入位置
|
||
* @deprecated
|
||
*/
|
||
getSuitableInsertion(
|
||
insertNode?: INode | IPublicTypeNodeSchema | IPublicTypeNodeSchema[],
|
||
): { target: INode; index?: number } | null {
|
||
const activeDoc = this.project.currentDocument;
|
||
if (!activeDoc) {
|
||
return null;
|
||
}
|
||
if (
|
||
Array.isArray(insertNode) &&
|
||
isNodeSchema(insertNode[0]) &&
|
||
this.getComponentMeta(insertNode[0].componentName).isModal
|
||
) {
|
||
return {
|
||
target: activeDoc.rootNode as INode,
|
||
};
|
||
}
|
||
const focusNode = activeDoc.focusNode!;
|
||
const nodes = activeDoc.selection.getNodes();
|
||
const refNode = nodes.find((item) => focusNode.contains(item));
|
||
let target;
|
||
let index: number | undefined;
|
||
if (!refNode || refNode === focusNode) {
|
||
target = focusNode;
|
||
} else if (refNode.componentMeta.isContainer) {
|
||
target = refNode;
|
||
} else {
|
||
// FIXME!!, parent maybe null
|
||
target = refNode.parent!;
|
||
index = refNode.index + 1;
|
||
}
|
||
|
||
if (target && insertNode && !target.componentMeta.checkNestingDown(target, insertNode)) {
|
||
return null;
|
||
}
|
||
|
||
return { target, index };
|
||
}
|
||
|
||
setProps(nextProps: DesignerProps) {
|
||
const props = this.props ? { ...this.props, ...nextProps } : nextProps;
|
||
if (this.props) {
|
||
// check hotkeys
|
||
// TODO:
|
||
// check simulatorConfig
|
||
if (props.simulatorComponent !== this.props.simulatorComponent) {
|
||
this._simulatorComponent = props.simulatorComponent;
|
||
}
|
||
if (props.simulatorProps !== this.props.simulatorProps) {
|
||
this._simulatorProps = props.simulatorProps;
|
||
// 重新 setupSelection
|
||
if (props.simulatorProps?.designMode !== this.props.simulatorProps?.designMode) {
|
||
this.setupSelection();
|
||
}
|
||
}
|
||
if (props.suspensed !== this.props.suspensed && props.suspensed != null) {
|
||
this.suspensed = props.suspensed;
|
||
}
|
||
if (
|
||
props.componentMetadatas !== this.props.componentMetadatas &&
|
||
props.componentMetadatas != null
|
||
) {
|
||
this.buildComponentMetasMap(props.componentMetadatas);
|
||
}
|
||
} else {
|
||
// init hotkeys
|
||
// todo:
|
||
// init simulatorConfig
|
||
if (props.simulatorComponent) {
|
||
this._simulatorComponent = props.simulatorComponent;
|
||
}
|
||
if (props.simulatorProps) {
|
||
this._simulatorProps = props.simulatorProps;
|
||
}
|
||
// init suspensed
|
||
if (props.suspensed != null) {
|
||
this.suspensed = props.suspensed;
|
||
}
|
||
if (props.componentMetadatas != null) {
|
||
this.buildComponentMetasMap(props.componentMetadatas);
|
||
}
|
||
}
|
||
this.props = props;
|
||
}
|
||
|
||
async loadIncrementalAssets(incrementalAssets: IPublicTypeAssetsJson): Promise<void> {
|
||
const { components, packages } = incrementalAssets;
|
||
components && this.buildComponentMetasMap(components);
|
||
if (packages) {
|
||
await this.project.simulator?.setupComponents(packages);
|
||
}
|
||
|
||
if (components) {
|
||
// 合并 assets
|
||
let assets = this.editor.get('assets') || {};
|
||
let newAssets = megreAssets(assets, incrementalAssets);
|
||
// 对于 assets 存在需要二次网络下载的过程,必须 await 等待结束之后,再进行事件触发
|
||
await this.editor.set('assets', newAssets);
|
||
}
|
||
// TODO: 因为涉及修改 prototype.view,之后在 renderer 里修改了 vc 的 view 获取逻辑后,可删除
|
||
this.refreshComponentMetasMap();
|
||
// 完成加载增量资源后发送事件,方便插件监听并处理相关逻辑
|
||
this.editor.eventBus.emit('designer.incrementalAssetsReady');
|
||
}
|
||
|
||
/**
|
||
* 刷新 componentMetasMap,可间接触发模拟器里的 buildComponents
|
||
*/
|
||
refreshComponentMetasMap() {
|
||
this._componentMetasMap = new Map(this._componentMetasMap);
|
||
}
|
||
|
||
get(key: string): any {
|
||
return this.props?.[key];
|
||
}
|
||
|
||
@computed get simulatorComponent(): ComponentType<any> | undefined {
|
||
return this._simulatorComponent;
|
||
}
|
||
|
||
@computed get simulatorProps(): object {
|
||
if (typeof this._simulatorProps === 'function') {
|
||
return this._simulatorProps(this.project);
|
||
}
|
||
return this._simulatorProps || {};
|
||
}
|
||
|
||
/**
|
||
* 提供给模拟器的参数
|
||
*/
|
||
@computed get projectSimulatorProps(): any {
|
||
return {
|
||
...this.simulatorProps,
|
||
project: this.project,
|
||
designer: this,
|
||
onMount: (simulator: any) => {
|
||
this.project.mountSimulator(simulator);
|
||
this.editor.set('simulator', simulator);
|
||
},
|
||
};
|
||
}
|
||
|
||
get suspensed(): boolean {
|
||
return this._suspensed;
|
||
}
|
||
|
||
set suspensed(flag: boolean) {
|
||
this._suspensed = flag;
|
||
// Todo afterwards...
|
||
if (flag) {
|
||
// this.project.suspensed = true?
|
||
}
|
||
}
|
||
|
||
get schema(): IPublicTypeProjectSchema {
|
||
return this.project.getSchema();
|
||
}
|
||
|
||
setSchema(schema?: IPublicTypeProjectSchema) {
|
||
this.project.load(schema);
|
||
}
|
||
|
||
buildComponentMetasMap(metas: IPublicTypeComponentMetadata[]) {
|
||
metas.forEach((data) => this.createComponentMeta(data));
|
||
}
|
||
|
||
createComponentMeta(data: IPublicTypeComponentMetadata): IComponentMeta | null {
|
||
const key = data.componentName;
|
||
if (!key) {
|
||
return null;
|
||
}
|
||
let meta = this._componentMetasMap.get(key);
|
||
if (meta) {
|
||
meta.setMetadata(data);
|
||
|
||
this._componentMetasMap.set(key, meta);
|
||
} else {
|
||
meta = this._lostComponentMetasMap.get(key);
|
||
|
||
if (meta) {
|
||
meta.setMetadata(data);
|
||
this._lostComponentMetasMap.delete(key);
|
||
} else {
|
||
meta = new ComponentMeta(this, data);
|
||
}
|
||
|
||
this._componentMetasMap.set(key, meta);
|
||
}
|
||
return meta;
|
||
}
|
||
|
||
getGlobalComponentActions(): IPublicTypeComponentAction[] | null {
|
||
return this.props?.globalComponentActions || null;
|
||
}
|
||
|
||
getComponentMeta(
|
||
componentName: string,
|
||
generateMetadata?: () => IPublicTypeComponentMetadata | null,
|
||
): IComponentMeta {
|
||
if (this._componentMetasMap.has(componentName)) {
|
||
return this._componentMetasMap.get(componentName)!;
|
||
}
|
||
|
||
if (this._lostComponentMetasMap.has(componentName)) {
|
||
return this._lostComponentMetasMap.get(componentName)!;
|
||
}
|
||
|
||
const meta = new ComponentMeta(this, {
|
||
componentName,
|
||
...(generateMetadata ? generateMetadata() : null),
|
||
});
|
||
|
||
this._lostComponentMetasMap.set(componentName, meta);
|
||
|
||
return meta;
|
||
}
|
||
|
||
getComponentMetasMap() {
|
||
return this._componentMetasMap;
|
||
}
|
||
|
||
@computed get componentsMap(): { [key: string]: IPublicTypeNpmInfo | Component } {
|
||
const maps: any = {};
|
||
const designer = this;
|
||
designer._componentMetasMap.forEach((config, key) => {
|
||
const metaData = config.getMetadata();
|
||
if (metaData.devMode === 'lowCode') {
|
||
maps[key] = metaData.schema;
|
||
} else {
|
||
const { view } = config.advanced;
|
||
if (view) {
|
||
maps[key] = view;
|
||
} else {
|
||
maps[key] = config.npm;
|
||
}
|
||
}
|
||
});
|
||
return maps;
|
||
}
|
||
|
||
transformProps(props: IPublicTypeCompositeObject | IPublicTypePropsList, node: Node, stage: IPublicEnumTransformStage) {
|
||
if (Array.isArray(props)) {
|
||
// current not support, make this future
|
||
return props;
|
||
}
|
||
|
||
const reducers = this.propsReducers.get(stage);
|
||
if (!reducers) {
|
||
return props;
|
||
}
|
||
|
||
return reducers.reduce((xprops, reducer) => {
|
||
try {
|
||
return reducer(xprops, node.internalToShellNode() as any, { stage });
|
||
} catch (e) {
|
||
// todo: add log
|
||
console.warn(e);
|
||
return xprops;
|
||
}
|
||
}, props);
|
||
}
|
||
|
||
addPropsReducer(reducer: IPublicTypePropsTransducer, stage: IPublicEnumTransformStage) {
|
||
if (!reducer) {
|
||
logger.error('reducer is not available');
|
||
return;
|
||
}
|
||
const reducers = this.propsReducers.get(stage);
|
||
if (reducers) {
|
||
reducers.push(reducer);
|
||
} else {
|
||
this.propsReducers.set(stage, [reducer]);
|
||
}
|
||
}
|
||
|
||
autorun(effect: (reaction: IReactionPublic) => void, options?: IReactionOptions): IReactionDisposer {
|
||
return autorun(effect, options);
|
||
}
|
||
|
||
purge() {
|
||
// TODO:
|
||
}
|
||
}
|