feat: add getDOMNode api definition in node module

This commit is contained in:
liujuping 2023-02-28 17:41:19 +08:00 committed by 林熠
parent 9228a2baa7
commit 964833128b
21 changed files with 334 additions and 228 deletions

View File

@ -645,3 +645,14 @@ setConditionalVisible(): void;
```
**@since v1.1.0**
### getDOMNode
获取节点实例对应的 dom 节点
```typescript
/**
* 获取节点实例对应的 dom 节点
*/
getDOMNode(): HTMLElement;
```

View File

@ -15,6 +15,7 @@ const jestConfig = {
// testMatch: ['**/document-model.test.ts'],
// testMatch: ['**/prop.test.ts'],
// testMatch: ['(/tests?/.*(test))\\.[jt]s$'],
// testMatch: ['**/document/node/node.add.test.ts'],
transformIgnorePatterns: [
`/node_modules/(?!${esModules})/`,
],

View File

@ -71,6 +71,8 @@ export interface IDesigner {
get editor(): IPublicModelEditor;
get detecting(): Detecting;
createScroller(scrollable: IPublicTypeScrollable): IPublicModelScroller;
/**

View File

@ -17,6 +17,7 @@ function generateSessionId(nodes: INode[]) {
}
export interface ISettingTopEntry extends ISettingEntry {
purge(): void;
}
export class SettingTopEntry implements ISettingTopEntry {

View File

@ -87,6 +87,8 @@ export interface IDocumentModel extends Omit< IPublicModelDocumentModel<
get active(): boolean;
get nodesMap(): Map<string, INode>;
/**
* id
*/
@ -114,6 +116,12 @@ export interface IDocumentModel extends Omit< IPublicModelDocumentModel<
onChangeNodeVisible(fn: (node: INode, visible: boolean) => void): IPublicTypeDisposable;
addWillPurge(node: INode): void;
removeWillPurge(node: INode): void;
getComponentMeta(componentName: string): IComponentMeta;
insertNodes(parent: INode, thing: INode[] | IPublicTypeNodeData[], at?: number | null, copy?: boolean): INode[];
}
export class DocumentModel implements IDocumentModel {
@ -379,7 +387,7 @@ export class DocumentModel implements IDocumentModel {
* schema
*/
@action
createNode<T extends INode = INode, C = undefined>(data: GetDataType<C, T>, checkId: boolean = true): T {
createNode<T extends INode = INode, C = undefined>(data: GetDataType<C, T>): T {
let schema: any;
if (isDOMText(data) || isJSExpression(data)) {
schema = {
@ -410,7 +418,7 @@ export class DocumentModel implements IDocumentModel {
}
}
if (!node) {
node = new Node(this, schema, { checkId });
node = new Node(this, schema);
// will add
// todo: this.activeNodes?.push(node);
}
@ -429,7 +437,7 @@ export class DocumentModel implements IDocumentModel {
/**
*
*/
insertNode(parent: INode, thing: INode | IPublicTypeNodeData, at?: number | null, copy?: boolean): INode {
insertNode(parent: INode, thing: INode | IPublicTypeNodeData, at?: number | null, copy?: boolean): INode | null {
return insertChild(parent, thing, at, copy);
}
@ -445,7 +453,7 @@ export class DocumentModel implements IDocumentModel {
*/
removeNode(idOrNode: string | INode) {
let id: string;
let node: INode | null;
let node: INode | null = null;
if (typeof idOrNode === 'string') {
id = idOrNode;
node = this.getNode(id);
@ -859,7 +867,7 @@ export class DocumentModel implements IDocumentModel {
onReady(fn: Function) {
this.designer.editor.eventBus.on('document-open', fn);
return () => {
this.designer.editor.removeListener('document-open', fn);
this.designer.editor.eventBus.off('document-open', fn);
};
}

View File

@ -1,18 +1,30 @@
import { obx, computed, makeObservable } from '@alilc/lowcode-editor-core';
import { uniqueId } from '@alilc/lowcode-utils';
import { IPublicTypeTitleContent, IPublicModelExclusiveGroup } from '@alilc/lowcode-types';
import { Node } from './node';
import { INode } from './node';
import { intl } from '../../locale';
export interface IExclusiveGroup extends IPublicModelExclusiveGroup<INode> {
readonly name: string;
remove(node: INode): void;
add(node: INode): void;
isVisible(node: INode): boolean;
}
// modals assoc x-hide value, initial: check is Modal, yes will put it in modals, cross levels
// if-else-if assoc conditionGroup value, should be the same level,
// and siblings, need renderEngine support
export class ExclusiveGroup implements IPublicModelExclusiveGroup {
export class ExclusiveGroup implements IExclusiveGroup {
readonly isExclusiveGroup = true;
readonly id = uniqueId('exclusive');
@obx.shallow readonly children: Node[] = [];
readonly title: IPublicTypeTitleContent;
@obx.shallow readonly children: INode[] = [];
@obx private visibleIndex = 0;
@ -28,11 +40,11 @@ export class ExclusiveGroup implements IPublicModelExclusiveGroup {
return this.children.length;
}
@computed get visibleNode(): Node {
@computed get visibleNode(): INode {
return this.children[this.visibleIndex];
}
@computed get firstNode(): Node {
@computed get firstNode(): INode {
return this.children[0]!;
}
@ -40,8 +52,16 @@ export class ExclusiveGroup implements IPublicModelExclusiveGroup {
return this.firstNode.index;
}
add(node: Node) {
if (node.nextSibling && node.nextSibling.conditionGroup === this) {
constructor(readonly name: string, title?: IPublicTypeTitleContent) {
makeObservable(this);
this.title = title || {
type: 'i18n',
intl: intl('Condition Group'),
};
}
add(node: INode) {
if (node.nextSibling && node.nextSibling.conditionGroup?.id === this.id) {
const i = this.children.indexOf(node.nextSibling);
this.children.splice(i, 0, node);
} else {
@ -49,7 +69,7 @@ export class ExclusiveGroup implements IPublicModelExclusiveGroup {
}
}
remove(node: Node) {
remove(node: INode) {
const i = this.children.indexOf(node);
if (i > -1) {
this.children.splice(i, 1);
@ -61,27 +81,17 @@ export class ExclusiveGroup implements IPublicModelExclusiveGroup {
}
}
setVisible(node: Node) {
setVisible(node: INode) {
const i = this.children.indexOf(node);
if (i > -1) {
this.visibleIndex = i;
}
}
isVisible(node: Node) {
isVisible(node: INode) {
const i = this.children.indexOf(node);
return i === this.visibleIndex;
}
readonly title: IPublicTypeTitleContent;
constructor(readonly name: string, title?: IPublicTypeTitleContent) {
makeObservable(this);
this.title = title || {
type: 'i18n',
intl: intl('Condition Group'),
};
}
}
export function isExclusiveGroup(obj: any): obj is ExclusiveGroup {

View File

@ -1,6 +1,6 @@
import { obx, computed, globalContext, makeObservable, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
import { Node, INode } from './node';
import { IPublicTypeNodeData, IPublicModelNodeChildren, IPublicEnumTransformStage } from '@alilc/lowcode-types';
import { IPublicTypeNodeData, IPublicModelNodeChildren, IPublicEnumTransformStage, IPublicTypeDisposable } from '@alilc/lowcode-types';
import { shallowEqual, compatStage, isNodeSchema } from '@alilc/lowcode-utils';
import { foreachReverse } from '../../utils/tree';
import { NodeRemoveOptions } from '../../types';
@ -18,6 +18,8 @@ export interface INodeChildren extends Omit<IPublicModelNodeChildren<INode>,
> {
get owner(): INode;
get length(): number;
unlinkChild(node: INode): void;
/**
@ -58,6 +60,8 @@ export interface INodeChildren extends Omit<IPublicModelNodeChildren<INode>,
internalInitParent(): void;
onChange(fn: (info?: IOnChangeOptions) => void): IPublicTypeDisposable;
/** overriding methods end */
}
export class NodeChildren implements INodeChildren {
@ -478,7 +482,7 @@ export class NodeChildren implements INodeChildren {
}
}
onChange(fn: (info?: IOnChangeOptions) => void): () => void {
onChange(fn: (info?: IOnChangeOptions) => void): IPublicTypeDisposable {
this.emitter.on('change', fn);
return () => {
this.emitter.removeListener('change', fn);

View File

@ -18,14 +18,14 @@ import {
IPublicTypeDisposable,
IBaseModelNode,
} from '@alilc/lowcode-types';
import { compatStage, isDOMText, isJSExpression, isNode } from '@alilc/lowcode-utils';
import { ISettingTopEntry, SettingTopEntry } from '@alilc/lowcode-designer';
import { compatStage, isDOMText, isJSExpression, isNode, isNodeSchema } from '@alilc/lowcode-utils';
import { ISettingTopEntry } from '@alilc/lowcode-designer';
import { Props, getConvertedExtraKey, IProps } from './props/props';
import { DocumentModel, IDocumentModel } from '../document-model';
import { IDocumentModel } from '../document-model';
import { NodeChildren, INodeChildren } from './node-children';
import { IProp, Prop } from './props/prop';
import { ComponentMeta, IComponentMeta } from '../../component-meta';
import { ExclusiveGroup, isExclusiveGroup } from './exclusive-group';
import { IComponentMeta } from '../../component-meta';
import { ExclusiveGroup, IExclusiveGroup, isExclusiveGroup } from './exclusive-group';
import { includeSlot, removeSlot } from '../../utils/slot';
import { foreachReverse } from '../../utils/tree';
import { NodeRemoveOptions, EDITOR_EVENT } from '../../types';
@ -42,14 +42,11 @@ export interface INode extends Omit<IBaseModelNode<
INode,
INodeChildren,
IComponentMeta,
ISettingTopEntry
ISettingTopEntry,
IProps,
IProp,
IExclusiveGroup
>,
'slots' |
'slotFor' |
'props' |
'getProp' |
'getExtraProp' |
'replaceChild' |
'isRoot' |
'isPage' |
'isComponent' |
@ -64,29 +61,21 @@ export interface INode extends Omit<IBaseModelNode<
'exportSchema' |
'visible' |
'importSchema' |
'isEmptyNode' |
// 内外实现有差异
'isContainer' |
'isEmpty'
> {
get slots(): INode[];
/**
*
*/
get slotFor(): IProp | null;
get props(): IProps;
isNode: boolean;
get componentMeta(): IComponentMeta;
get settingEntry(): SettingTopEntry;
get settingEntry(): ISettingTopEntry;
get isPurged(): boolean;
setVisible(flag: boolean): void;
get index(): number | undefined;
getVisible(): boolean;
get isPurging(): boolean;
/**
* 使
@ -100,7 +89,7 @@ export interface INode extends Omit<IBaseModelNode<
internalPurgeStart(): void;
unlinkSlot(slotNode: Node): void;
unlinkSlot(slotNode: INode): void;
/**
* schema
@ -117,15 +106,9 @@ export interface INode extends Omit<IBaseModelNode<
onVisibleChange(func: (flag: boolean) => any): () => void;
getProp(path: string, createIfNone?: boolean): IProp | null;
getExtraProp(key: string, createIfNone?: boolean): IProp | null;
replaceChild(node: INode, data: any): INode;
getSuitablePlace(node: INode, ref: any): any;
onChildrenChange(fn: (param?: { type: string; node: INode }) => void): IPublicTypeDisposable;
onChildrenChange(fn: (param?: { type: string; node: INode }) => void): IPublicTypeDisposable | undefined;
onPropChange(func: (info: IPublicTypePropChangeOptions) => void): IPublicTypeDisposable;
@ -153,13 +136,17 @@ export interface INode extends Omit<IBaseModelNode<
options?: NodeRemoveOptions,
): void;
didDropIn(dragment: Node): void;
didDropIn(dragment: INode): void;
didDropOut(dragment: Node): void;
get isPurging(): boolean;
didDropOut(dragment: INode): void;
purge(): void;
removeSlot(slotNode: INode): boolean;
setVisible(flag: boolean): void;
getVisible(): boolean;
}
/**
@ -322,7 +309,11 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
return !!this._isRGLContainer;
}
private _slotFor?: IProp | null = null;
get isEmptyNode() {
return this.isEmpty();
}
private _slotFor?: IProp | null | undefined = null;
@obx.shallow _slots: INode[] = [];
@ -331,10 +322,10 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
}
/* istanbul ignore next */
@obx.ref private _conditionGroup: IPublicModelExclusiveGroup | null = null;
@obx.ref private _conditionGroup: IExclusiveGroup | null = null;
/* istanbul ignore next */
get conditionGroup(): IPublicModelExclusiveGroup | null {
get conditionGroup(): IExclusiveGroup | null {
return this._conditionGroup;
}
@ -518,7 +509,7 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
this.document.addWillPurge(this);
}
didDropIn(dragment: Node) {
didDropIn(dragment: INode) {
const { callbacks } = this.componentMeta.advanced;
if (callbacks?.onNodeAdd) {
const cbThis = this.internalToShellNode();
@ -529,7 +520,7 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
}
}
didDropOut(dragment: Node) {
didDropOut(dragment: INode) {
const { callbacks } = this.componentMeta.advanced;
if (callbacks?.onNodeRemove) {
const cbThis = this.internalToShellNode();
@ -590,7 +581,7 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
/**
*
*/
get slotFor(): IProp | null {
get slotFor(): IProp | null | undefined {
return this._slotFor;
}
@ -610,10 +601,10 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
});
}
if (this.isSlot()) {
this.parent.removeSlot(this, purge);
this.parent.children.internalDelete(this, purge, useMutator, { suppressRemoveEvent: true });
this.parent.removeSlot(this);
this.parent.children?.internalDelete(this, purge, useMutator, { suppressRemoveEvent: true });
} else {
this.parent.children.internalDelete(this, purge, useMutator, { suppressRemoveEvent: true });
this.parent.children?.internalDelete(this, purge, useMutator, { suppressRemoveEvent: true });
}
}
}
@ -653,7 +644,7 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
/**
*
*/
@computed get componentMeta(): ComponentMeta {
@computed get componentMeta(): IComponentMeta {
return this.document.getComponentMeta(this.componentName);
}
@ -670,6 +661,7 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
/* istanbul ignore next */
setConditionGroup(grp: IPublicModelExclusiveGroup | string | null) {
let _grp: IExclusiveGroup | null = null;
if (!grp) {
this.getExtraProp('conditionGroup', false)?.remove();
if (this._conditionGroup) {
@ -680,20 +672,20 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
}
if (!isExclusiveGroup(grp)) {
if (this.prevSibling?.conditionGroup?.name === grp) {
grp = this.prevSibling.conditionGroup;
_grp = this.prevSibling.conditionGroup;
} else if (this.nextSibling?.conditionGroup?.name === grp) {
grp = this.nextSibling.conditionGroup;
} else {
grp = new ExclusiveGroup(grp);
_grp = this.nextSibling.conditionGroup;
} else if (typeof grp === 'string') {
_grp = new ExclusiveGroup(grp);
}
}
if (this._conditionGroup !== grp) {
this.getExtraProp('conditionGroup', true)?.setValue(grp.name);
if (_grp && this._conditionGroup !== _grp) {
this.getExtraProp('conditionGroup', true)?.setValue(_grp.name);
if (this._conditionGroup) {
this._conditionGroup.remove(this);
}
this._conditionGroup = grp;
grp.add(this);
this._conditionGroup = _grp;
_grp?.add(this);
}
}
@ -749,13 +741,17 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
* @param {INode} node
* @param {object} data
*/
replaceChild(node: INode, data: any): INode {
replaceChild(node: INode, data: any): INode | null {
if (this.children?.has(node)) {
const selected = this.document.selection.has(node.id);
delete data.id;
const newNode = this.document.createNode(data);
if (!isNode(newNode)) {
return null;
}
this.insertBefore(newNode, node, false);
node.remove(false);
@ -838,39 +834,45 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
/**
*
*/
@computed get index(): number {
@computed get index(): number | undefined {
if (!this.parent) {
return -1;
}
return this.parent.children.indexOf(this);
return this.parent.children?.indexOf(this);
}
/**
*
*/
get nextSibling(): INode | null {
get nextSibling(): INode | null | undefined {
if (!this.parent) {
return null;
}
const { index } = this;
if (typeof index !== 'number') {
return null;
}
if (index < 0) {
return null;
}
return this.parent.children.get(index + 1);
return this.parent.children?.get(index + 1);
}
/**
*
*/
get prevSibling(): INode | null {
get prevSibling(): INode | null | undefined {
if (!this.parent) {
return null;
}
const { index } = this;
if (typeof index !== 'number') {
return null;
}
if (index < 1) {
return null;
}
return this.parent.children.get(index - 1);
return this.parent.children?.get(index - 1);
}
/**
@ -889,7 +891,7 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
if (this.isSlot()) {
foreachReverse(
this.children!,
(subNode: Node) => {
(subNode: INode) => {
subNode.remove(true, true);
},
(iterable, idx) => (iterable as NodeChildren).get(idx),
@ -954,7 +956,7 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
...this.document.designer.transformProps(_extras_, this, stage),
};
if (this.isParental() && this.children.size > 0 && !options.bypassChildren) {
if (this.isParental() && this.children && this.children.size > 0 && !options.bypassChildren) {
schema.children = this.children.export(stage);
}
@ -971,7 +973,7 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
/**
*
*/
getZLevelTop(zLevel: number): Node | null {
getZLevelTop(zLevel: number): INode | null {
return getZLevelTop(this, zLevel);
}
@ -983,11 +985,11 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
* 2 thisNode before or after otherNode
* 0 thisNode same as otherNode
*/
comparePosition(otherNode: Node): PositionNO {
comparePosition(otherNode: INode): PositionNO {
return comparePosition(this, otherNode);
}
unlinkSlot(slotNode: Node) {
unlinkSlot(slotNode: INode) {
const i = this._slots.indexOf(slotNode);
if (i < 0) {
return false;
@ -998,7 +1000,7 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
/**
* Slot节点
*/
removeSlot(slotNode: Node): boolean {
removeSlot(slotNode: INode): boolean {
// if (purge) {
// // should set parent null
// slotNode?.internalSetParent(null, false);
@ -1039,7 +1041,7 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
*
* @param node
*/
removeChild(node: Node) {
removeChild(node: INode) {
this.children?.delete(node);
}
@ -1090,7 +1092,7 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
return this.componentName;
}
insert(node: Node, ref?: INode, useMutator = true) {
insert(node: INode, ref?: INode, useMutator = true) {
this.insertAfter(node, ref, useMutator);
}
@ -1101,7 +1103,7 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
insertAfter(node: any, ref?: INode, useMutator = true) {
const nodeInstance = ensureNode(node, this.document);
this.children?.internalInsert(nodeInstance, ref ? ref.index + 1 : null, useMutator);
this.children?.internalInsert(nodeInstance, ref ? (ref.index || 0) + 1 : null, useMutator);
}
getParent() {
@ -1128,7 +1130,7 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
return this.props;
}
onChildrenChange(fn: (param?: { type: string; node: INode }) => void): IPublicTypeDisposable {
onChildrenChange(fn: (param?: { type: string; node: INode }) => void): IPublicTypeDisposable | undefined {
const wrappedFunc = wrapWithEventSwitch(fn);
return this.children?.onChange(wrappedFunc);
}
@ -1330,7 +1332,7 @@ export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema>
}
}
function ensureNode(node: any, document: DocumentModel): Node {
function ensureNode(node: any, document: IDocumentModel): INode {
let nodeInstance = node;
if (!isNode(node)) {
if (node.getComponentName) {
@ -1443,20 +1445,24 @@ export function insertChild(
thing: INode | IPublicTypeNodeData,
at?: number | null,
copy?: boolean,
): INode {
let node: INode;
if (isNode(thing) && (copy || thing.isSlot())) {
thing = thing.export(IPublicEnumTransformStage.Clone);
}
if (isNode(thing)) {
): INode | null {
let node: INode | null | RootNode | undefined;
let nodeSchema: IPublicTypeNodeSchema;
if (isNode<INode>(thing) && (copy || thing.isSlot())) {
nodeSchema = thing.export(IPublicEnumTransformStage.Clone);
node = container.document?.createNode(nodeSchema);
} else if (isNode<INode>(thing)) {
node = thing;
} else {
node = container.document.createNode(thing);
} else if (isNodeSchema(thing)) {
node = container.document?.createNode(thing);
}
container.children.insert(node, at);
if (isNode<INode>(node)) {
container.children?.insert(node, at);
return node;
}
return node;
return null;
}
export function insertChildren(

View File

@ -11,14 +11,14 @@ export const UNSET = Symbol.for('unset');
// eslint-disable-next-line no-redeclare
export type UNSET = typeof UNSET;
export interface IProp extends Omit<IPublicModelProp, 'exportSchema' | 'node' | 'slotNode' > {
export interface IProp extends Omit<IPublicModelProp<
INode
>, 'exportSchema' | 'node' > {
readonly props: IProps;
readonly owner: INode;
get slotNode(): INode | null;
delete(prop: Prop): void;
export(stage: IPublicEnumTransformStage): IPublicTypeCompositeValue;
@ -26,6 +26,10 @@ export interface IProp extends Omit<IPublicModelProp, 'exportSchema' | 'node' |
getNode(): INode;
getAsString(): string;
unset(): void;
get value(): IPublicTypeCompositeValue | UNSET;
}
export type ValueTypes = 'unset' | 'literal' | 'map' | 'list' | 'expression' | 'slot';

View File

@ -33,8 +33,6 @@ export interface IPropParent {
get path(): string[];
delete(prop: Prop): void;
query(path: string, createIfNone: boolean): Prop | null;
}
export interface IProps extends Omit<IBaseModelProps<IProp>, | 'getExtraProp' | 'getExtraPropValue' | 'setExtraPropValue' | 'node'> {
@ -52,6 +50,12 @@ export interface IProps extends Omit<IBaseModelProps<IProp>, | 'getExtraProp' |
};
merge(value: IPublicTypePropsMap, extras?: IPublicTypePropsMap): void;
purge(): void;
query(path: string, createIfNone: boolean): Prop | null;
import(value?: IPublicTypePropsMap | IPublicTypePropsList | null, extras?: ExtrasObject): void;
}
export class Props implements IProps, IPropParent {

View File

@ -23,7 +23,7 @@ describe('document-model 测试', () => {
it('empty schema', () => {
const doc = new DocumentModel(project);
expect(doc.rootNode.id).toBe('root');
expect(doc.rootNode?.id).toBe('root');
expect(doc.currentRoot).toBe(doc.rootNode);
expect(doc.root).toBe(doc.rootNode);
expect(doc.modalNode).toBeUndefined();

View File

@ -1,8 +1,8 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';
import '../../fixtures/window';
import { Project } from '../../../src/project/project';
import { Node } from '../../../src/document/node/node';
import { Project, IProject } from '../../../src/project/project';
import { Node, INode } from '../../../src/document/node/node';
import { Designer } from '../../../src/designer/designer';
import formSchema from '../../fixtures/schema/form';
import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils';
@ -37,7 +37,7 @@ beforeAll(() => {
describe('schema 生成节点模型测试', () => {
describe('block ❌ | component ❌ | slot ❌', () => {
let project: Project;
let project: IProject;
beforeEach(() => {
project = new Project(designer, {
componentsTree: [
@ -52,12 +52,12 @@ describe('schema 生成节点模型测试', () => {
it('基本的节点模型初始化,模型导出', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const nodesMap = currentDocument?.nodesMap;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
expect(nodesMap.size).toBe(expectedNodeCnt);
expect(nodesMap?.size).toBe(expectedNodeCnt);
ids.forEach(id => {
expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName);
expect(nodesMap?.get(id)?.componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName);
});
const pageNode = currentDocument?.getNode('page');
@ -76,18 +76,18 @@ describe('schema 生成节点模型测试', () => {
it('基本的节点模型初始化,节点深度', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const getNode = currentDocument.getNode.bind(currentDocument);
const getNode = currentDocument?.getNode.bind(currentDocument);
const pageNode = getNode('page');
const rootHeaderNode = getNode('node_k1ow3cba');
const rootContentNode = getNode('node_k1ow3cbb');
const rootFooterNode = getNode('node_k1ow3cbc');
const formNode = getNode('form');
const cardNode = getNode('node_k1ow3cbj');
const cardContentNode = getNode('node_k1ow3cbk');
const columnsLayoutNode = getNode('node_k1ow3cbw');
const columnNode = getNode('node_k1ow3cbx');
const textFieldNode = getNode('node_k1ow3cbz');
const pageNode = getNode?.('page');
const rootHeaderNode = getNode?.('node_k1ow3cba');
const rootContentNode = getNode?.('node_k1ow3cbb');
const rootFooterNode = getNode?.('node_k1ow3cbc');
const formNode = getNode?.('form');
const cardNode = getNode?.('node_k1ow3cbj');
const cardContentNode = getNode?.('node_k1ow3cbk');
const columnsLayoutNode = getNode?.('node_k1ow3cbw');
const columnNode = getNode?.('node_k1ow3cbx');
const textFieldNode = getNode?.('node_k1ow3cbz');
expect(pageNode?.zLevel).toBe(0);
expect(rootHeaderNode?.zLevel).toBe(1);
@ -131,7 +131,7 @@ describe('schema 生成节点模型测试', () => {
const textFieldNode = getNode('node_k1ow3cbz');
expect(pageNode?.index).toBe(-1);
expect(pageNode?.children.toString()).toBe('[object Array]');
expect(pageNode?.children?.toString()).toBe('[object Array]');
expect(pageNode?.children?.get(1)).toBe(rootContentNode);
expect(pageNode?.getChildren()?.get(1)).toBe(rootContentNode);
expect(pageNode?.getNode()).toBe(pageNode);
@ -162,20 +162,20 @@ describe('schema 生成节点模型测试', () => {
it('基本的节点模型初始化,节点新建、删除等事件', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const getNode = currentDocument.getNode.bind(currentDocument);
const createNode = currentDocument.createNode.bind(currentDocument);
const getNode = currentDocument?.getNode.bind(currentDocument);
const createNode = currentDocument?.createNode.bind(currentDocument);
const pageNode = getNode('page');
const pageNode = getNode?.('page');
const nodeCreateHandler = jest.fn();
const offCreate = currentDocument?.onNodeCreate(nodeCreateHandler);
const node = createNode({
const node = createNode?.({
componentName: 'TextInput',
props: {
propA: 'haha',
},
});
currentDocument?.insertNode(pageNode, node);
pageNode && node && currentDocument?.insertNode(pageNode, node);
expect(nodeCreateHandler).toHaveBeenCalledTimes(1);
expect(nodeCreateHandler.mock.calls[0][0]).toBe(node);
@ -184,7 +184,7 @@ describe('schema 生成节点模型测试', () => {
const nodeDestroyHandler = jest.fn();
const offDestroy = currentDocument?.onNodeDestroy(nodeDestroyHandler);
node.remove();
node?.remove();
expect(nodeDestroyHandler).toHaveBeenCalledTimes(1);
expect(nodeDestroyHandler.mock.calls[0][0]).toBe(node);
expect(nodeDestroyHandler.mock.calls[0][0].componentName).toBe('TextInput');
@ -290,9 +290,9 @@ describe('schema 生成节点模型测试', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form');
currentDocument?.insertNode(formNode, {
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form');
formNode && currentDocument?.insertNode(formNode, {
componentName: 'TextInput',
id: 'nodeschema-id1',
props: {
@ -300,11 +300,11 @@ describe('schema 生成节点模型测试', () => {
propB: 3,
},
}, 0);
expect(nodesMap.size).toBe(ids.length + 1);
expect(formNode.children.length).toBe(4);
const insertedNode = formNode.children.get(0);
expect(insertedNode.componentName).toBe('TextInput');
expect(insertedNode.propsData).toEqual({
expect(nodesMap?.size).toBe(ids.length + 1);
expect(formNode?.children?.length).toBe(4);
const insertedNode = formNode?.children?.get(0);
expect(insertedNode?.componentName).toBe('TextInput');
expect(insertedNode?.propsData).toEqual({
propA: 'haha',
propB: 3,
});
@ -316,9 +316,9 @@ describe('schema 生成节点模型测试', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form');
currentDocument?.insertNode(formNode, {
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form');
formNode && currentDocument?.insertNode(formNode, {
componentName: 'TextInput',
id: 'nodeschema-id1',
props: {
@ -326,11 +326,11 @@ describe('schema 生成节点模型测试', () => {
propB: 3,
},
}, 1);
expect(nodesMap.size).toBe(ids.length + 1);
expect(formNode.children.length).toBe(4);
const insertedNode = formNode.children.get(1);
expect(insertedNode.componentName).toBe('TextInput');
expect(insertedNode.propsData).toEqual({
expect(nodesMap?.size).toBe(ids.length + 1);
expect(formNode?.children?.length).toBe(4);
const insertedNode = formNode?.children?.get(1);
expect(insertedNode?.componentName).toBe('TextInput');
expect(insertedNode?.propsData).toEqual({
propA: 'haha',
propB: 3,
});
@ -342,8 +342,8 @@ describe('schema 生成节点模型测试', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form') as Node;
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form') as INode;
currentDocument?.insertNode(formNode, {
componentName: 'ParentNode',
props: {
@ -367,8 +367,8 @@ describe('schema 生成节点模型测试', () => {
},
],
});
expect(nodesMap.size).toBe(ids.length + 3);
expect(formNode.children.length).toBe(4);
expect(nodesMap?.size).toBe(ids.length + 3);
expect(formNode.children?.length).toBe(4);
expect(formNode.children?.get(3)?.componentName).toBe('ParentNode');
expect(formNode.children?.get(3)?.children?.get(0)?.componentName).toBe('SubNode');
expect(formNode.children?.get(3)?.children?.get(1)?.componentName).toBe('SubNode2');
@ -378,9 +378,9 @@ describe('schema 生成节点模型测试', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form');
currentDocument?.insertNode(formNode, {
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form');
formNode && currentDocument?.insertNode(formNode, {
componentName: 'TextInput',
id: 'nodeschema-id1',
props: {
@ -388,17 +388,17 @@ describe('schema 生成节点模型测试', () => {
propB: 3,
},
});
expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput');
expect(nodesMap.size).toBe(ids.length + 1);
expect(nodesMap?.get('nodeschema-id1')?.componentName).toBe('TextInput');
expect(nodesMap?.size).toBe(ids.length + 1);
});
it.skip('场景一:插入 NodeSchemaid 与现有 schema 里的 id 重复,但关闭了 id 检测器', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form');
currentDocument?.insertNode(formNode, {
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form');
formNode && currentDocument?.insertNode(formNode, {
componentName: 'TextInput',
id: 'nodeschema-id1',
props: {
@ -406,16 +406,16 @@ describe('schema 生成节点模型测试', () => {
propB: 3,
},
});
expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput');
expect(nodesMap.size).toBe(ids.length + 1);
expect(nodesMap?.get('nodeschema-id1')?.componentName).toBe('TextInput');
expect(nodesMap?.size).toBe(ids.length + 1);
});
it('场景二:插入 Node 实例', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form');
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form');
const inputNode = currentDocument?.createNode({
componentName: 'TextInput',
id: 'nodeschema-id2',
@ -424,22 +424,22 @@ describe('schema 生成节点模型测试', () => {
propB: 3,
},
});
currentDocument?.insertNode(formNode, inputNode);
expect(formNode.children?.get(3)?.componentName).toBe('TextInput');
expect(nodesMap.size).toBe(ids.length + 1);
formNode && currentDocument?.insertNode(formNode, inputNode);
expect(formNode?.children?.get(3)?.componentName).toBe('TextInput');
expect(nodesMap?.size).toBe(ids.length + 1);
});
it('场景三:插入 JSExpression', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form') as Node;
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form') as Node;
currentDocument?.insertNode(formNode, {
type: 'JSExpression',
value: 'just a expression',
});
expect(nodesMap.size).toBe(ids.length + 1);
expect(nodesMap?.size).toBe(ids.length + 1);
expect(formNode.children?.get(3)?.componentName).toBe('Leaf');
// expect(formNode.children?.get(3)?.children).toEqual({
// type: 'JSExpression',
@ -450,10 +450,10 @@ describe('schema 生成节点模型测试', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form') as Node;
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form') as Node;
currentDocument?.insertNode(formNode, 'just a string');
expect(nodesMap.size).toBe(ids.length + 1);
expect(nodesMap?.size).toBe(ids.length + 1);
expect(formNode.children?.get(3)?.componentName).toBe('Leaf');
// expect(formNode.children?.get(3)?.children).toBe('just a string');
});
@ -473,8 +473,8 @@ describe('schema 生成节点模型测试', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form') as Node;
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form') as Node;
const formNode2 = currentDocument?.getNode('form');
expect(formNode).toEqual(formNode2);
currentDocument?.insertNodes(formNode, [
@ -493,17 +493,17 @@ describe('schema 生成节点模型测试', () => {
},
},
], 1);
expect(nodesMap.size).toBe(ids.length + 2);
expect(nodesMap?.size).toBe(ids.length + 2);
expect(formNode.children?.length).toBe(5);
const insertedNode1 = formNode.children.get(1);
const insertedNode2 = formNode.children.get(2);
expect(insertedNode1.componentName).toBe('TextInput');
expect(insertedNode1.propsData).toEqual({
const insertedNode1 = formNode.children?.get(1);
const insertedNode2 = formNode.children?.get(2);
expect(insertedNode1?.componentName).toBe('TextInput');
expect(insertedNode1?.propsData).toEqual({
propA: 'haha2',
propB: 3,
});
expect(insertedNode2.componentName).toBe('TextInput2');
expect(insertedNode2.propsData).toEqual({
expect(insertedNode2?.componentName).toBe('TextInput2');
expect(insertedNode2?.propsData).toEqual({
propA: 'haha',
propB: 3,
});
@ -513,8 +513,8 @@ describe('schema 生成节点模型测试', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form') as Node;
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form') as INode;
const formNode2 = currentDocument?.getNode('form');
expect(formNode).toEqual(formNode2);
const createdNode1 = currentDocument?.createNode({
@ -532,17 +532,17 @@ describe('schema 生成节点模型测试', () => {
},
});
currentDocument?.insertNodes(formNode, [createdNode1, createdNode2], 1);
expect(nodesMap.size).toBe(ids.length + 2);
expect(nodesMap?.size).toBe(ids.length + 2);
expect(formNode.children?.length).toBe(5);
const insertedNode1 = formNode.children.get(1);
const insertedNode2 = formNode.children.get(2);
expect(insertedNode1.componentName).toBe('TextInput');
expect(insertedNode1.propsData).toEqual({
const insertedNode1 = formNode.children?.get(1);
const insertedNode2 = formNode.children?.get(2);
expect(insertedNode1?.componentName).toBe('TextInput');
expect(insertedNode1?.propsData).toEqual({
propA: 'haha2',
propB: 3,
});
expect(insertedNode2.componentName).toBe('TextInput2');
expect(insertedNode2.propsData).toEqual({
expect(insertedNode2?.componentName).toBe('TextInput2');
expect(insertedNode2?.propsData).toEqual({
propA: 'haha',
propB: 3,
});
@ -561,13 +561,13 @@ describe('schema 生成节点模型测试', () => {
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const nodesMap = currentDocument?.nodesMap;
const ids = getIdsFromSchema(formSchema);
// 目前每个 slot 会新增1 + children.length个节点
const expectedNodeCnt = ids.length + 2;
expect(nodesMap.size).toBe(expectedNodeCnt);
expect(nodesMap?.size).toBe(expectedNodeCnt);
// PageHeader
expect(nodesMap.get('node_k1ow3cbd').slots).toHaveLength(1);
expect(nodesMap?.get('node_k1ow3cbd')?.slots).toHaveLength(1);
});
});
});

View File

@ -0,0 +1,42 @@
import { IExclusiveGroup } from '@alilc/lowcode-designer';
import { IPublicModelExclusiveGroup, IPublicModelNode } from '@alilc/lowcode-types';
import { conditionGroupSymbol, nodeSymbol } from '../symbols';
import { Node } from './node';
export class ConditionGroup implements IPublicModelExclusiveGroup {
private [conditionGroupSymbol]: IExclusiveGroup | null;
constructor(conditionGroup: IExclusiveGroup | null) {
this[conditionGroupSymbol] = conditionGroup;
}
get id() {
return this[conditionGroupSymbol]?.id;
}
get title() {
return this[conditionGroupSymbol]?.title;
}
get firstNode() {
return Node.create(this[conditionGroupSymbol]?.firstNode);
}
setVisible(node: IPublicModelNode) {
this[conditionGroupSymbol]?.setVisible((node as any)[nodeSymbol] ? (node as any)[nodeSymbol] : node);
}
static create(conditionGroup: IExclusiveGroup | null) {
if (!conditionGroup) {
return null;
}
// @ts-ignore
if (conditionGroup[conditionGroupSymbol]) {
return (conditionGroup as any)[conditionGroupSymbol];
}
const shellConditionGroup = new ConditionGroup(conditionGroup);
// @ts-ignore
shellConditionGroup[conditionGroupSymbol] = shellConditionGroup;
return shellConditionGroup;
}
}

View File

@ -27,6 +27,7 @@ import { ComponentMeta as ShellComponentMeta } from './component-meta';
import { SettingTopEntry as ShellSettingTopEntry } from './setting-top-entry';
import { documentSymbol, nodeSymbol } from '../symbols';
import { ReactElement } from 'react';
import { ConditionGroup } from './condition-group';
const shellNodeSymbol = Symbol('shellNodeSymbol');
@ -289,7 +290,7 @@ export class Node implements IPublicModelNode {
/**
*
*/
get slotFor(): IPublicModelProp | null {
get slotFor(): IPublicModelProp | null | undefined {
return ShellProp.create(this[nodeSymbol].slotFor);
}
@ -349,7 +350,6 @@ export class Node implements IPublicModelNode {
/**
* dom
* @deprecated
*/
getDOMNode() {
return (this[nodeSymbol] as any).getDOMNode();
@ -362,9 +362,9 @@ export class Node implements IPublicModelNode {
* @param sorter
*/
mergeChildren(
remover: (node: Node, idx: number) => boolean,
adder: (children: Node[]) => any,
sorter: (firstNode: Node, secondNode: Node) => number,
remover: (node: IPublicModelNode, idx: number) => boolean,
adder: (children: IPublicModelNode[]) => any,
sorter: (firstNode: IPublicModelNode, secondNode: IPublicModelNode) => number,
): any {
return this.children?.mergeChildren(remover, adder, sorter);
}
@ -641,7 +641,7 @@ export class Node implements IPublicModelNode {
* @since v1.1.0
*/
get conditionGroup(): IPublicModelExclusiveGroup | null {
return this[nodeSymbol].conditionGroup;
return ConditionGroup.create(this[nodeSymbol].conditionGroup);
}
/**

View File

@ -33,3 +33,4 @@ export const resourceTypeSymbol = Symbol('resourceType');
export const resourceSymbol = Symbol('resource');
export const clipboardSymbol = Symbol('clipboard');
export const configSymbol = Symbol('configSymbol');
export const conditionGroupSymbol = Symbol('conditionGroup');

View File

@ -1,8 +1,10 @@
import { IPublicModelNode } from '..';
import { IPublicModelNode, IPublicTypeTitleContent } from '..';
export interface IPublicModelExclusiveGroup {
readonly id: string;
readonly title: string;
get firstNode(): IPublicModelNode;
export interface IPublicModelExclusiveGroup<
Node = IPublicModelNode,
> {
readonly id: string | undefined;
readonly title: IPublicTypeTitleContent | undefined;
get firstNode(): Node | null;
setVisible(node: Node): void;
}

View File

@ -8,7 +8,10 @@ export interface IBaseModelNode<
Node = IPublicModelNode,
NodeChildren = IPublicModelNodeChildren,
ComponentMeta = IPublicModelComponentMeta,
SettingTopEntry = IPublicModelSettingTopEntry
SettingTopEntry = IPublicModelSettingTopEntry,
Props = IPublicModelProps,
Prop = IPublicModelProp,
ExclusiveGroup = IPublicModelExclusiveGroup
> {
/**
@ -167,7 +170,7 @@ export interface IBaseModelNode<
*
* index
*/
get index(): number;
get index(): number | undefined;
/**
*
@ -203,13 +206,13 @@ export interface IBaseModelNode<
*
* get previous sibling of this node
*/
get prevSibling(): Node | null;
get prevSibling(): Node | null | undefined;
/**
*
* get next sibling of this node
*/
get nextSibling(): Node | null;
get nextSibling(): Node | null | undefined;
/**
*
@ -233,13 +236,13 @@ export interface IBaseModelNode<
*
* return coresponding prop when this node is a slot node
*/
get slotFor(): IPublicModelProp | null;
get slotFor(): Prop | null | undefined;
/**
*
* get props
*/
get props(): IPublicModelProps | null;
get props(): Props | null;
/**
*
@ -250,7 +253,7 @@ export interface IBaseModelNode<
/**
* get conditionGroup
*/
get conditionGroup(): IPublicModelExclusiveGroup | null;
get conditionGroup(): ExclusiveGroup | null;
/**
* - schema
@ -295,7 +298,7 @@ export interface IBaseModelNode<
* get prop by path
* @param path a / a.b / a.0
*/
getProp(path: string, createIfNone: boolean): IPublicModelProp | null;
getProp(path: string, createIfNone: boolean): Prop | null;
/**
* path
@ -313,7 +316,7 @@ export interface IBaseModelNode<
* @param path a / a.b / a.0
* @param createIfNone
*/
getExtraProp(path: string, createIfNone?: boolean): IPublicModelProp | null;
getExtraProp(path: string, createIfNone?: boolean): Prop | null;
/**
* path
@ -481,6 +484,11 @@ export interface IBaseModelNode<
* @since v1.1.0
*/
setConditionalVisible(): void;
/**
* dom
*/
getDOMNode(): HTMLElement;
}
export interface IPublicModelNode extends IBaseModelNode<IPublicModelDocumentModel, IPublicModelNode> {}

View File

@ -2,7 +2,9 @@ import { IPublicEnumTransformStage } from '../enum';
import { IPublicTypeCompositeValue } from '../type';
import { IPublicModelNode } from './';
export interface IPublicModelProp {
export interface IPublicModelProp<
Node = IPublicModelNode
> {
/**
* id
@ -25,14 +27,14 @@ export interface IPublicModelProp {
*
* get node instance, which this prop belongs to
*/
get node(): IPublicModelNode | null;
get node(): Node | null;
/**
* prop Slot slotNode
* return the slot node (only if the current prop represents a slot)
* @since v1.1.0
*/
get slotNode(): IPublicModelNode | undefined | null;
get slotNode(): Node | undefined | null;
/**
* Prop , true

View File

@ -195,8 +195,8 @@ export interface IPublicTypeCallbacks {
onChildMoveHook?: (childNode: IPublicModelNode, currentNode: IPublicModelNode) => boolean;
// events
onNodeRemove?: (removedNode: IPublicModelNode, currentNode: IPublicModelNode) => void;
onNodeAdd?: (addedNode: IPublicModelNode, currentNode: IPublicModelNode) => void;
onNodeRemove?: (removedNode: IPublicModelNode | null, currentNode: IPublicModelNode | null) => void;
onNodeAdd?: (addedNode: IPublicModelNode | null, currentNode: IPublicModelNode | null) => void;
onSubtreeModified?: (currentNode: IPublicModelNode, options: any) => void;
onResize?: (
e: MouseEvent & {

View File

@ -1,3 +1,3 @@
export function isDOMText(data: any): boolean {
export function isDOMText(data: any): data is string {
return typeof data === 'string';
}

View File

@ -1,5 +1,5 @@
import { IPublicTypeNodeSchema } from '@alilc/lowcode-types';
export function isNodeSchema(data: any): data is IPublicTypeNodeSchema {
return data && data.componentName;
return data && data.componentName && !data.isNode;
}