2022-12-26 14:08:12 +08:00

777 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { makeObservable, obx, engineConfig, action, runWithGlobalEventOff, wrapWithEventSwitch, createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core';
import {
IPublicTypeNodeData,
IPublicTypeNodeSchema,
IPublicTypeRootSchema,
IPublicTypePageSchema,
IPublicTypeComponentsMap,
IPublicTypeDragNodeObject,
IPublicTypeDragNodeDataObject,
IPublicModelDocumentModel,
IPublicModelSelection,
IPublicModelHistory,
IPublicModelModalNodesManager,
IPublicModelNode,
IPublicApiProject,
IPublicModelDropLocation,
IPublicEnumTransformStage,
} from '@alilc/lowcode-types';
import { Project } from '../project';
import { ISimulatorHost } from '../simulator';
import { ComponentMeta } from '../component-meta';
import { IDropLocation, Designer } from '../designer';
import { Node, insertChildren, insertChild, isNode, RootNode, INode } from './node/node';
import { Selection } from './selection';
import { History } from './history';
import { ModalNodesManager } from './node';
import { uniqueId, isPlainObject, compatStage, isJSExpression, isDOMText, isNodeSchema, isDragNodeObject, isDragNodeDataObject } from '@alilc/lowcode-utils';
export type GetDataType<T, NodeType> = T extends undefined
? NodeType extends {
schema: infer R;
}
? R
: any
: T;
export interface IDocumentModel extends IPublicModelDocumentModel {
}
export class DocumentModel implements IDocumentModel {
/**
* 根节点 类型有Page/Component/Block
*/
rootNode: RootNode | null;
/**
* 文档编号
*/
id: string = uniqueId('doc');
/**
* 选区控制
*/
readonly selection: IPublicModelSelection = new Selection(this);
/**
* 操作记录控制
*/
readonly history: IPublicModelHistory;
/**
* 模态节点管理
*/
readonly modalNodesManager: IPublicModelModalNodesManager;
private _nodesMap = new Map<string, IPublicModelNode>();
readonly project: IPublicApiProject;
readonly designer: Designer;
@obx.shallow private nodes = new Set<IPublicModelNode>();
private seqId = 0;
private emitter: IEventBus;
private rootNodeVisitorMap: { [visitorName: string]: any } = {};
/**
* @deprecated
*/
private _addons: Array<{ name: string; exportData: any }> = [];
/**
* 模拟器
*/
get simulator(): ISimulatorHost | null {
return this.project.simulator;
}
get nodesMap(): Map<string, Node> {
return this._nodesMap;
}
get fileName(): string {
return this.rootNode?.getExtraProp('fileName', false)?.getAsString() || this.id;
}
set fileName(fileName: string) {
this.rootNode?.getExtraProp('fileName', true)?.setValue(fileName);
}
get focusNode() {
if (this._drillDownNode) {
return this._drillDownNode;
}
const selector = engineConfig.get('focusNodeSelector');
if (selector && typeof selector === 'function') {
return selector(this.rootNode!);
}
return this.rootNode;
}
@obx.ref private _drillDownNode: Node | null = null;
drillDown(node: Node | null) {
this._drillDownNode = node;
}
private _modalNode?: INode;
private _blank?: boolean;
private inited = false;
constructor(project: Project, schema?: IPublicTypeRootSchema) {
makeObservable(this);
this.project = project;
this.designer = this.project?.designer;
this.emitter = createModuleEventBus('DocumentModel');
if (!schema) {
this._blank = true;
}
// 兼容 vision
this.id = project.getSchema()?.id || this.id;
this.rootNode = this.createNode<RootNode>(
schema || {
componentName: 'Page',
id: 'root',
fileName: '',
},
);
this.history = new History(
() => this.export(IPublicEnumTransformStage.Serilize),
(schema) => {
this.import(schema as IPublicTypeRootSchema, true);
this.simulator?.rerender();
},
);
this.setupListenActiveNodes();
this.modalNodesManager = new ModalNodesManager(this);
this.inited = true;
}
@obx.shallow private willPurgeSpace: Node[] = [];
get modalNode() {
return this._modalNode;
}
get currentRoot() {
return this.modalNode || this.focusNode;
}
addWillPurge(node: Node) {
this.willPurgeSpace.push(node);
}
removeWillPurge(node: Node) {
const i = this.willPurgeSpace.indexOf(node);
if (i > -1) {
this.willPurgeSpace.splice(i, 1);
}
}
isBlank() {
return this._blank && !this.isModified();
}
/**
* 生成唯一id
*/
nextId(possibleId: string | undefined) {
let id = possibleId;
while (!id || this.nodesMap.get(id)) {
id = `node_${(String(this.id).slice(-10) + (++this.seqId).toString(36)).toLocaleLowerCase()}`;
}
return id;
}
/**
* 根据 id 获取节点
*/
getNode(id: string): Node | null {
return this._nodesMap.get(id) || null;
}
/**
* 根据 id 获取节点
*/
getNodeCount(): number {
return this._nodesMap?.size;
}
/**
* 是否存在节点
*/
hasNode(id: string): boolean {
const node = this.getNode(id);
return node ? !node.isPurged : false;
}
@obx.shallow private activeNodes?: Node[];
/**
* 根据 schema 创建一个节点
*/
@action
createNode<T extends Node = Node, C = undefined>(data: GetDataType<C, T>, checkId: boolean = true): T {
let schema: any;
if (isDOMText(data) || isJSExpression(data)) {
schema = {
componentName: 'Leaf',
children: data,
};
} else {
schema = data;
}
let node: Node | null = null;
if (this.hasNode(schema?.id)) {
schema.id = null;
}
/* istanbul ignore next */
if (schema.id) {
node = this.getNode(schema.id);
// TODO: 底下这几段代码似乎永远都进不去
if (node && node.componentName === schema.componentName) {
if (node.parent) {
node.internalSetParent(null, false);
// will move to another position
// todo: this.activeNodes?.push(node);
}
node.import(schema, true);
} else if (node) {
node = null;
}
}
if (!node) {
node = new Node(this, schema, { checkId });
// will add
// todo: this.activeNodes?.push(node);
}
this._nodesMap.set(node.id, node);
this.nodes.add(node);
this.emitter.emit('nodecreate', node);
return node as any;
}
public destroyNode(node: Node) {
this.emitter.emit('nodedestroy', node);
}
/**
* 插入一个节点
*/
insertNode(parent: INode, thing: Node | IPublicTypeNodeData, at?: number | null, copy?: boolean): Node {
return insertChild(parent, thing, at, copy);
}
/**
* 插入多个节点
*/
insertNodes(parent: INode, thing: Node[] | IPublicTypeNodeData[], at?: number | null, copy?: boolean) {
return insertChildren(parent, thing, at, copy);
}
/**
* 移除一个节点
*/
removeNode(idOrNode: string | Node) {
let id: string;
let node: Node | null;
if (typeof idOrNode === 'string') {
id = idOrNode;
node = this.getNode(id);
} else if (idOrNode.id) {
id = idOrNode.id;
node = this.getNode(id);
}
if (!node) {
return;
}
this.internalRemoveAndPurgeNode(node, true);
}
/**
* 内部方法,请勿调用
*/
internalRemoveAndPurgeNode(node: Node, useMutator = false) {
if (!this.nodes.has(node)) {
return;
}
node.remove(useMutator);
}
unlinkNode(node: Node) {
this.nodes.delete(node);
this._nodesMap.delete(node.id);
}
@obx.ref private _dropLocation: IDropLocation | null = null;
set dropLocation(loc: IPublicModelDropLocation | null) {
this._dropLocation = loc;
// pub event
this.designer.editor.eventBus.emit(
'document.dropLocation.changed',
{ document: this, location: loc },
);
}
/**
* 投放插入位置标记
*/
get dropLocation() {
return this._dropLocation;
}
/**
* 包裹当前选区中的节点
*/
wrapWith(schema: IPublicTypeNodeSchema): Node | null {
const nodes = this.selection.getTopNodes();
if (nodes.length < 1) {
return null;
}
const wrapper = this.createNode(schema);
if (wrapper.isParental()) {
const first = nodes[0];
// TODO: check nesting rules x 2
insertChild(first.parent!, wrapper, first.index);
insertChildren(wrapper, nodes);
this.selection.select(wrapper.id);
return wrapper;
}
this.removeNode(wrapper);
return null;
}
/**
* 导出 schema 数据
*/
get schema(): IPublicTypeRootSchema {
return this.rootNode?.schema as any;
}
@action
import(schema: IPublicTypeRootSchema, checkId = false) {
const drillDownNodeId = this._drillDownNode?.id;
runWithGlobalEventOff(() => {
// TODO: 暂时用饱和式删除,原因是 Slot 节点并不是树节点,无法正常递归删除
this.nodes.forEach(node => {
if (node.isRoot()) return;
this.internalRemoveAndPurgeNode(node, true);
});
this.rootNode?.import(schema as any, checkId);
this.modalNodesManager = new ModalNodesManager(this);
// todo: select added and active track added
if (drillDownNodeId) {
this.drillDown(this.getNode(drillDownNodeId));
}
});
}
export(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Serilize) {
stage = compatStage(stage);
// 置顶只作用于 Page 的第一级子节点,目前还用不到里层的置顶;如果后面有需要可以考虑将这段写到 node-children 中的 export
const currentSchema = this.rootNode?.export(stage);
if (Array.isArray(currentSchema?.children) && currentSchema?.children.length > 0) {
const FixedTopNodeIndex = currentSchema.children
.filter(i => isPlainObject(i))
.findIndex((i => (i as IPublicTypeNodeSchema).props?.__isTopFixed__));
if (FixedTopNodeIndex > 0) {
const FixedTopNode = currentSchema.children.splice(FixedTopNodeIndex, 1);
currentSchema.children.unshift(FixedTopNode[0]);
}
}
return currentSchema;
}
/**
* 导出节点数据
*/
getNodeSchema(id: string): IPublicTypeNodeData | null {
const node = this.getNode(id);
if (node) {
return node.schema;
}
return null;
}
/**
* 是否已修改
*/
isModified() {
return this.history.isSavePoint();
}
// FIXME: does needed?
getComponent(componentName: string): any {
return this.simulator!.getComponent(componentName);
}
getComponentMeta(componentName: string): ComponentMeta {
return this.designer.getComponentMeta(
componentName,
() => this.simulator?.generateComponentMetadata(componentName) || null,
);
}
@obx.ref private _opened = false;
@obx.ref private _suspensed = false;
/**
* 是否为非激活状态
*/
get suspensed(): boolean {
return this._suspensed || !this._opened;
}
/**
* 与 suspensed 相反,是否为激活状态,这个函数可能用的更多一点
*/
get active(): boolean {
return !this._suspensed;
}
/**
* @deprecated 兼容
*/
get actived(): boolean {
return this.active;
}
/**
* 是否打开
*/
get opened() {
return this._opened;
}
/**
* 切换激活,只有打开的才能激活
* 不激活,打开之后切换到另外一个时发生,比如 tab 视图,切换到另外一个标签页
*/
private setSuspense(flag: boolean) {
if (!this._opened && !flag) {
return;
}
this._suspensed = flag;
this.simulator?.setSuspense(flag);
if (!flag) {
this.project.checkExclusive(this);
}
}
suspense() {
this.setSuspense(true);
}
activate() {
this.setSuspense(false);
}
/**
* 打开,已载入,默认建立时就打开状态,除非手动关闭
*/
open(): DocumentModel {
const originState = this._opened;
this._opened = true;
if (originState === false) {
this.designer.postEvent('document-open', this);
}
if (this._suspensed) {
this.setSuspense(false);
} else {
this.project.checkExclusive(this);
}
return this;
}
/**
* 关闭,相当于 sleep仍然缓存停止一切响应如果有发生的变更没被保存仍然需要去取数据保存
*/
close(): void {
this.setSuspense(true);
this._opened = false;
}
/**
* 从项目中移除
*/
remove() {
this.designer.postEvent('document.remove', { id: this.id });
this.purge();
this.project.removeDocument(this);
}
purge() {
this.rootNode?.purge();
this.nodes.clear();
this._nodesMap.clear();
this.rootNode = null;
}
checkNesting(dropTarget: INode, dragObject: IPublicTypeDragNodeObject | IPublicTypeNodeSchema | Node | IPublicTypeDragNodeDataObject): boolean {
let items: Array<Node | IPublicTypeNodeSchema>;
if (isDragNodeDataObject(dragObject)) {
items = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data];
} else if (isDragNodeObject(dragObject)) {
items = dragObject.nodes;
} else if (isNode(dragObject) || isNodeSchema(dragObject)) {
items = [dragObject];
} else {
console.warn('the dragObject is not in the correct type, dragObject:', dragObject);
return true;
}
return items.every((item) => this.checkNestingDown(dropTarget, item) && this.checkNestingUp(dropTarget, item));
}
/**
* @deprecated since version 1.0.16.
* Will be deleted in version 2.0.0.
* Use checkNesting method instead.
*/
checkDropTarget(dropTarget: INode, dragObject: IPublicTypeDragNodeObject | IPublicTypeDragNodeDataObject): boolean {
let items: Array<Node | IPublicTypeNodeSchema>;
if (isDragNodeDataObject(dragObject)) {
items = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data];
} else {
items = dragObject.nodes;
}
return items.every((item) => this.checkNestingUp(dropTarget, item));
}
/**
* 检查对象对父级的要求,涉及配置 parentWhitelist
*/
checkNestingUp(parent: INode, obj: IPublicTypeNodeSchema | Node): boolean {
if (isNode(obj) || isNodeSchema(obj)) {
const config = isNode(obj) ? obj.componentMeta : this.getComponentMeta(obj.componentName);
if (config) {
return config.checkNestingUp(obj, parent);
}
}
return true;
}
/**
* 检查投放位置对子级的要求,涉及配置 childWhitelist
*/
checkNestingDown(parent: INode, obj: IPublicTypeNodeSchema | Node): boolean {
const config = parent.componentMeta;
return config.checkNestingDown(parent, obj);
}
// ======= compatibles for vision
getRoot() {
return this.rootNode;
}
// add toData
toData(extraComps?: string[]) {
const node = this.export(IPublicEnumTransformStage.Save);
const data = {
componentsMap: this.getComponentsMap(extraComps),
utils: this.getUtilsMap(),
componentsTree: [node],
};
return data;
}
getHistory(): History {
return this.history;
}
get root() {
return this.rootNode;
}
/**
* @deprecated
*/
/* istanbul ignore next */
getAddonData(name: string) {
const addon = this._addons.find((item) => item.name === name);
if (addon) {
return addon.exportData();
}
}
/**
* @deprecated
*/
/* istanbul ignore next */
exportAddonData() {
const addons = {};
this._addons.forEach((addon) => {
const data = addon.exportData();
if (data === null) {
delete addons[addon.name];
} else {
addons[addon.name] = data;
}
});
return addons;
}
/**
* @deprecated
*/
/* istanbul ignore next */
registerAddon(name: string, exportData: any) {
if (['id', 'params', 'layout'].indexOf(name) > -1) {
throw new Error('addon name cannot be id, params, layout');
}
const i = this._addons.findIndex((item) => item.name === name);
if (i > -1) {
this._addons.splice(i, 1);
}
this._addons.push({
exportData,
name,
});
}
/* istanbul ignore next */
acceptRootNodeVisitor(
visitorName = 'default',
visitorFn: (node: RootNode) => any,
) {
let visitorResult = {};
if (!visitorName) {
/* eslint-disable-next-line no-console */
console.warn('Invalid or empty RootNodeVisitor name.');
}
try {
visitorResult = visitorFn.call(this, this.rootNode);
this.rootNodeVisitorMap[visitorName] = visitorResult;
} catch (e) {
console.error('RootNodeVisitor is not valid.');
console.error(e);
}
return visitorResult;
}
/* istanbul ignore next */
getRootNodeVisitor(name: string) {
return this.rootNodeVisitorMap[name];
}
getComponentsMap(extraComps?: string[]) {
const componentsMap: IPublicTypeComponentsMap = [];
// 组件去重
const exsitingMap: { [componentName: string]: boolean } = {};
for (const node of this._nodesMap.values()) {
const { componentName } = node || {};
if (componentName === 'Slot') continue;
if (!exsitingMap[componentName]) {
exsitingMap[componentName] = true;
if (node.componentMeta?.npm?.package) {
componentsMap.push({
...node.componentMeta.npm,
componentName,
});
} else {
componentsMap.push({
devMode: 'lowCode',
componentName,
});
}
}
}
// 合并外界传入的自定义渲染的组件
if (Array.isArray(extraComps)) {
extraComps.forEach(c => {
if (c && !exsitingMap[c]) {
const m = this.getComponentMeta(c);
if (m && m.npm?.package) {
componentsMap.push({
...m?.npm,
componentName: c,
});
}
}
});
}
return componentsMap;
}
/**
* 获取 schema 中的 utils 节点,当前版本不判断页面中使用了哪些 utils直接返回资产包中所有的 utils
* @returns
*/
getUtilsMap() {
return this.designer?.editor?.get('assets')?.utils?.map((item: any) => ({
name: item.name,
type: item.type || 'npm',
// TODO 当前只有 npm 类型content 直接设置为 item.npm有 function 类型之后需要处理
content: item.npm,
}));
}
onNodeCreate(func: (node: Node) => void) {
const wrappedFunc = wrapWithEventSwitch(func);
this.emitter.on('nodecreate', wrappedFunc);
return () => {
this.emitter.removeListener('nodecreate', wrappedFunc);
};
}
onNodeDestroy(func: (node: Node) => void) {
const wrappedFunc = wrapWithEventSwitch(func);
this.emitter.on('nodedestroy', wrappedFunc);
return () => {
this.emitter.removeListener('nodedestroy', wrappedFunc);
};
}
/**
* @deprecated
*/
refresh() {
console.warn('refresh method is deprecated');
}
/**
* @deprecated
*/
onRefresh(/* func: () => void */) {
console.warn('onRefresh method is deprecated');
}
onReady(fn: Function) {
this.designer.editor.eventBus.on('document-open', fn);
return () => {
this.designer.editor.removeListener('document-open', fn);
};
}
private setupListenActiveNodes() {
// todo:
}
}
export function isDocumentModel(obj: any): obj is DocumentModel {
return obj && obj.rootNode;
}
export function isPageSchema(obj: any): obj is IPublicTypePageSchema {
return obj?.componentName === 'Page';
}