= new Map();
constructor() {
this.dispose = [];
engineConfig.onGot('enableContextMenu', (enable) => {
if (this.enableContextMenu === enable) {
return;
}
this.enableContextMenu = enable;
this.dispose.forEach(d => d());
if (enable) {
this.initEvent();
}
});
}
handleContextMenu = (
event: MouseEvent,
) => {
event.stopPropagation();
event.preventDefault();
const actions: IPublicTypeContextMenuAction[] = [];
let contextMenu: ContextMenuActions = this.contextMenuActionsMap.values().next().value;
this.contextMenuActionsMap.forEach((contextMenu) => {
actions.push(...contextMenu.actions);
});
let destroyFn: Function | undefined;
const destroy = () => {
destroyFn?.();
};
const pluginContext: IPublicModelPluginContext = contextMenu.designer.editor.get('pluginContext') as IPublicModelPluginContext;
const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, {
nodes: [],
destroy,
event,
pluginContext,
});
if (!menus.length) {
return;
}
const layoutMenu = adjustMenuLayoutFn(menus);
const menuNode = parseContextMenuAsReactNode(layoutMenu, {
destroy,
nodes: [],
pluginContext,
});
const target = event.target;
const { top, left } = target?.getBoundingClientRect();
const menuInstance = Menu.create({
target: event.target,
offset: [event.clientX - left, event.clientY - top],
children: menuNode,
className: 'engine-context-menu',
});
destroyFn = (menuInstance as any).destroy;
};
initEvent() {
this.dispose.push(
(() => {
const handleContextMenu = (e: MouseEvent) => {
this.handleContextMenu(e);
};
document.addEventListener('contextmenu', handleContextMenu);
return () => {
document.removeEventListener('contextmenu', handleContextMenu);
};
})(),
);
}
registerContextMenuActions(contextMenu: ContextMenuActions) {
this.contextMenuActionsMap.set(contextMenu.id, contextMenu);
}
}
const globalContextMenuActions = new GlobalContextMenuActions();
export class ContextMenuActions implements IContextMenuActions {
actions: IPublicTypeContextMenuAction[] = [];
designer: IDesigner;
dispose: Function[];
enableContextMenu: boolean;
id: string = uniqueId('contextMenu');;
constructor(designer: IDesigner) {
this.designer = designer;
this.dispose = [];
engineConfig.onGot('enableContextMenu', (enable) => {
if (this.enableContextMenu === enable) {
return;
}
this.enableContextMenu = enable;
this.dispose.forEach(d => d());
if (enable) {
this.initEvent();
}
});
globalContextMenuActions.registerContextMenuActions(this);
}
handleContextMenu = (
nodes: INode[],
event: MouseEvent,
) => {
const designer = this.designer;
event.stopPropagation();
event.preventDefault();
const actions = designer.contextMenuActions.actions;
const { bounds } = designer.project.simulator?.viewport || { bounds: { left: 0, top: 0 } };
const { left: simulatorLeft, top: simulatorTop } = bounds;
let destroyFn: Function | undefined;
const destroy = () => {
destroyFn?.();
};
const pluginContext: IPublicModelPluginContext = this.designer.editor.get('pluginContext') as IPublicModelPluginContext;
const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, {
nodes: nodes.map(d => designer.shellModelFactory.createNode(d)!),
destroy,
event,
pluginContext,
});
if (!menus.length) {
return;
}
const layoutMenu = adjustMenuLayoutFn(menus);
const menuNode = parseContextMenuAsReactNode(layoutMenu, {
destroy,
nodes: nodes.map(d => designer.shellModelFactory.createNode(d)!),
pluginContext,
});
destroyFn = createContextMenu(menuNode, {
event,
offset: [simulatorLeft, simulatorTop],
});
};
initEvent() {
const designer = this.designer;
this.dispose.push(
designer.editor.eventBus.on('designer.builtinSimulator.contextmenu', ({
node,
originalEvent,
}: {
node: INode;
originalEvent: MouseEvent;
}) => {
originalEvent.stopPropagation();
originalEvent.preventDefault();
// 如果右键的节点不在 当前选中的节点中,选中该节点
if (!designer.currentSelection.has(node.id)) {
designer.currentSelection.select(node.id);
}
const nodes = designer.currentSelection.getNodes();
this.handleContextMenu(nodes, originalEvent);
}),
);
}
addMenuAction(action: IPublicTypeContextMenuAction) {
this.actions.push({
type: IPublicEnumContextMenuType.MENU_ITEM,
...action,
});
}
removeMenuAction(name: string) {
const i = this.actions.findIndex((action) => action.name === name);
if (i > -1) {
this.actions.splice(i, 1);
}
}
adjustMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]) {
adjustMenuLayoutFn = fn;
}
}
================================================
FILE: packages/designer/src/index.ts
================================================
export * from './component-meta';
export * from './simulator';
export * from './designer';
export * from './document';
export * from './project';
export * from './builtin-simulator';
export * from './plugin';
export * from './types';
export * from './context-menu-actions';
================================================
FILE: packages/designer/src/less-variables.less
================================================
/*
* 基础的 DPL 定义使用了 kuma base 的定义,参考:
* https://github.com/uxcore/kuma-base/tree/master/variables
*/
/**
* ===========================================================
* ==================== Font Family ==========================
* ===========================================================
*/
/*
* @font-family: "STHeiti", "Microsoft Yahei", "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, Verdana, sans-serif;
*/
@font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Helvetica, Arial, sans-serif;
@font-family-code: Monaco, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Helvetica, Arial,
sans-serif;
/**
* ===========================================================
* ===================== Color DPL ===========================
* ===========================================================
*/
@brand-color-1: rgba(0, 108, 255, 1);
@brand-color-2: rgba(25, 122, 255, 1);
@brand-color-3: rgba(0, 96, 229, 1);
@brand-color-1-3: rgba(0, 108, 255, 0.6);
@brand-color-1-4: rgba(0, 108, 255, 0.4);
@brand-color-1-5: rgba(0, 108, 255, 0.3);
@brand-color-1-6: rgba(0, 108, 255, 0.2);
@brand-color-1-7: rgba(0, 108, 255, 0.1);
@brand-color: @brand-color-1;
@white-alpha-1: rgb(255, 255, 255); // W-1
@white-alpha-2: rgba(255, 255, 255, 0.8); // W-2 A80
@white-alpha-3: rgba(255, 255, 255, 0.6); // W-3 A60
@white-alpha-4: rgba(255, 255, 255, 0.4); // W-4 A40
@white-alpha-5: rgba(255, 255, 255, 0.3); // W-5 A30
@white-alpha-6: rgba(255, 255, 255, 0.2); // W-6 A20
@white-alpha-7: rgba(255, 255, 255, 0.1); // W-7 A10
@white-alpha-8: rgba(255, 255, 255, 0.06); // W-8 A6
@dark-alpha-1: rgba(0, 0, 0, 1); // D-1 A100
@dark-alpha-2: rgba(0, 0, 0, 0.8); // D-2 A80
@dark-alpha-3: rgba(0, 0, 0, 0.6); // D-3 A60
@dark-alpha-4: rgba(0, 0, 0, 0.4); // D-4 A40
@dark-alpha-5: rgba(0, 0, 0, 0.3); // D-5 A30
@dark-alpha-6: rgba(0, 0, 0, 0.2); // D-6 A20
@dark-alpha-7: rgba(0, 0, 0, 0.1); // D-7 A10
@dark-alpha-8: rgba(0, 0, 0, 0.06); // D-8 A6
@dark-alpha-9: rgba(0, 0, 0, 0.04); // D-9 A4
@normal-alpha-1: rgba(31, 56, 88, 1); // N-1 A100
@normal-alpha-2: rgba(31, 56, 88, 0.8); // N-2 A80
@normal-alpha-3: rgba(31, 56, 88, 0.6); // N-3 A60
@normal-alpha-4: rgba(31, 56, 88, 0.4); // N-4 A40
@normal-alpha-5: rgba(31, 56, 88, 0.3); // N-5 A30
@normal-alpha-6: rgba(31, 56, 88, 0.2); // N-6 A20
@normal-alpha-7: rgba(31, 56, 88, 0.1); // N-7 A10
@normal-alpha-8: rgba(31, 56, 88, 0.06); // N-8 A6
@normal-alpha-9: rgba(31, 56, 88, 0.04); // N-9 A4
@normal-3: #77879c;
@normal-4: #a3aebd;
@normal-5: #bac3cc;
@normal-6: #d1d7de;
@gray-dark: #333; // N2_4
@gray: #666; // N2_3
@gray-light: #999; // N2_2
@gray-lighter: #ccc; // N2_1
@brand-secondary: #2c2f33; // B2_3
// 补色
@brand-complement: #00b3e8; // B3_1
// 复合
@brand-comosite: #00c587; // B3_2
// 浓度
@brand-deep: #73461d; // B3_3
// F1-1
@brand-danger: rgb(240, 70, 49);
// F1-2 (10% white)
@brand-danger-hover: rgba(240, 70, 49, 0.9);
// F1-3 (5% black)
@brand-danger-focus: rgba(240, 70, 49, 0.95);
// F2-1
@brand-warning: rgb(250, 189, 14);
// F3-1
@brand-success: rgb(102, 188, 92);
// F4-1
@brand-link: rgb(102, 188, 92);
// F4-2
@brand-link-hover: #2e76a6;
// F1-1-7 A10
@brand-danger-alpha-7: rgba(240, 70, 49, 0.1);
// F1-1-8 A6
@brand-danger-alpha-8: rgba(240, 70, 49, 0.8);
// F2-1-2 A80
@brand-warning-alpha-2: rgba(250, 189, 14, 0.8);
// F2-1-7 A10
@brand-warning-alpha-7: rgba(250, 189, 14, 0.1);
// F3-1-2 A80
@brand-success-alpha-2: rgba(102, 188, 92, 0.8);
// F3-1-7 A10
@brand-success-alpha-7: rgba(102, 188, 92, 0.1);
// F4-1-7 A10
@brand-link-alpha-7: rgba(102, 188, 92, 0.1);
// 文本色
@text-primary-color: @dark-alpha-3;
@text-secondary-color: @normal-alpha-3;
@text-thirdary-color: @dark-alpha-4;
@text-disabled-color: @normal-alpha-5;
@text-helper-color: @dark-alpha-4;
@text-danger-color: @brand-danger;
@text-ali-color: #ec6c00;
/**
* ===========================================================
* =================== Shadow Box ============================
* ===========================================================
*/
@box-shadow-1: 0 1px 4px 0 rgba(31, 56, 88, 0.15); // 1 级阴影,物体由原来存在于底面的物体展开,物体和底面关联紧密
@box-shadow-2: 0 2px 10px 0 rgba(31, 56, 88, 0.15); // 2 级阴影,hover状态,物体层级较高
@box-shadow-3: 0 4px 15px 0 rgba(31, 56, 88, 0.15); // 3 级阴影,当物体层级高于所有界面元素,弹窗用
/**
* ===========================================================
* ================= FontSize of Level =======================
* ===========================================================
*/
@fontSize-1: 26px;
@fontSize-2: 20px;
@fontSize-3: 16px;
@fontSize-4: 14px;
@fontSize-5: 12px;
@fontLineHeight-1: 38px;
@fontLineHeight-2: 30px;
@fontLineHeight-3: 26px;
@fontLineHeight-4: 24px;
@fontLineHeight-5: 20px;
/**
* ===========================================================
* ================= FontSize of Level =======================
* ===========================================================
*/
@global-border-radius: 3px;
@input-border-radius: 3px;
@popup-border-radius: 6px;
/**
* ===========================================================
* ===================== Transistion =========================
* ===========================================================
*/
@transition-duration: 0.3s;
@transition-ease: cubic-bezier(0.23, 1, 0.32, 1);
@transition-delay: 0s;
/**
* ===========================================================
* ================ Global Configruations ====================
* ===========================================================
*/
@topPaneHeight: 48px;
@actionpane-height: 48px;
@tabPaneWidth: 260px;
@input-standard-height: 32px;
@dockpane-width: 48px;
/**
* ===========================================================
* =================== Deprecated Items ======================
* ===========================================================
*/
@head-bgcolor: @white-alpha-1;
@pane-bgcolor: @white-alpha-1;
@pane-dark-bgcolor: @white-alpha-1;
@pane-bdcolor: @normal-4;
@blank-bgcolor: @normal-5;
@title-bgcolor: @white-alpha-1;
@title-bdcolor: transparent;
@section-bgcolor: transparent;
@section-bdcolor: @white-alpha-1;
@button-bgcolor: @white-alpha-1;
@button-bdcolor: transparent;
@button-blue-color: @brand-color;
@button-blue-hover-color: @brand-color;
@sub-title-bgcolor: @white-alpha-1;
@sub-title-bdcolor: transparent;
@text-color: @text-primary-color;
@icon-color: @gray;
@icon-color-active: @gray-light;
@ghost-bgcolor: @dark-alpha-3;
@input-bgcolor: transparent;
@input-bdcolor: @normal-alpha-5;
@hover-color: #5a99cc;
@active-color: #5a99cc;
@disabled-color: #666;
@setter-popup-bg: rgb(80, 86, 109);
================================================
FILE: packages/designer/src/simulator.ts
================================================
import { ComponentType } from 'react';
import { IPublicTypeComponentMetadata, IPublicTypeNodeSchema, IPublicTypeScrollable, IPublicTypeComponentInstance, IPublicModelSensor, IPublicTypeNodeInstance, IPublicTypePackage } from '@alilc/lowcode-types';
import { Point, ScrollTarget, ILocateEvent, IDesigner } from './designer';
import { BuiltinSimulatorRenderer } from './builtin-simulator/renderer';
import { INode } from './document';
import { IProject } from './project';
export type AutoFit = '100%';
// eslint-disable-next-line no-redeclare
export const AutoFit = '100%';
export interface IScrollable extends IPublicTypeScrollable {
}
export interface IViewport extends IScrollable {
/**
* 视口大小
*/
width: number;
height: number;
/**
* 内容大小
*/
contentWidth: number | AutoFit;
contentHeight: number | AutoFit;
/**
* 内容缩放
*/
scale: number;
/**
* 视口矩形维度
*/
readonly bounds: DOMRect;
/**
* 内容矩形维度
*/
readonly contentBounds: DOMRect;
/**
* 视口滚动对象
*/
readonly scrollTarget?: ScrollTarget;
/**
* 是否滚动中
*/
readonly scrolling: boolean;
/**
* 内容当前滚动 X
*/
readonly scrollX: number;
/**
* 内容当前滚动 Y
*/
readonly scrollY: number;
/**
* 全局坐标系转化为本地坐标系
*/
toLocalPoint(point: Point): Point;
/**
* 本地坐标系转化为全局坐标系
*/
toGlobalPoint(point: Point): Point;
}
export interface DropContainer {
container: INode;
instance: IPublicTypeComponentInstance;
}
/**
* 模拟器控制进程协议
*/
export interface ISimulatorHost extends IPublicModelSensor {
readonly isSimulator: true;
/**
* 获得边界维度等信息
*/
readonly viewport: IViewport;
readonly contentWindow?: Window;
readonly contentDocument?: Document;
readonly renderer?: BuiltinSimulatorRenderer;
readonly project: IProject;
readonly designer: IDesigner;
// dependsAsset // like react jQuery lodash
// themesAsset
// componentsAsset
// simulatorUrl //
// utils, dataSource, constants 模拟
//
// later:
// layout: ComponentName
// 获取区块代码,通过 components 传递,可异步获取
// 设置 simulator Props
setProps(props: P): void;
// 设置单个 Prop
set(key: string, value: any): void;
setSuspense(suspensed: boolean): void;
// #region ========= drag and drop helpers =============
/**
* 设置文字拖选
*/
setNativeSelection(enableFlag: boolean): void;
/**
* 设置拖拽态
*/
setDraggingState(state: boolean): void;
/**
* 设置拷贝态
*/
setCopyState(state: boolean): void;
/**
* 清除所有态:拖拽态、拷贝态
*/
clearState(): void;
// #endregion
/**
* 滚动视口到节点
*/
scrollToNode(node: INode, detail?: any): void;
/**
* 描述组件
*/
generateComponentMetadata(componentName: string): IPublicTypeComponentMetadata;
/**
* 根据组件信息获取组件类
*/
getComponent(componentName: string): Component | any;
/**
* 根据节点获取节点的组件实例
*/
getComponentInstances(node: INode): IPublicTypeComponentInstance[] | null;
/**
* 根据 schema 创建组件类
*/
createComponent(schema: IPublicTypeNodeSchema): Component | null;
/**
* 根据节点获取节点的组件运行上下文
*/
getComponentContext(node: INode): object | null;
getClosestNodeInstance(from: IPublicTypeComponentInstance, specId?: string): IPublicTypeNodeInstance | null;
computeRect(node: INode): DOMRect | null;
computeComponentInstanceRect(instance: IPublicTypeComponentInstance, selector?: string): DOMRect | null;
findDOMNodes(instance: IPublicTypeComponentInstance, selector?: string): Array | null;
getDropContainer(e: ILocateEvent): DropContainer | null;
postEvent(evtName: string, evtData: any): void;
rerender(): void;
/**
* 销毁
*/
purge(): void;
setupComponents(library: IPublicTypePackage[]): Promise;
}
export function isSimulatorHost(obj: any): obj is ISimulatorHost {
return obj && obj.isSimulator;
}
/**
* 组件类定义
*/
export type Component = ComponentType | object;
export interface INodeSelector {
node: INode;
instance?: IPublicTypeComponentInstance;
}
================================================
FILE: packages/designer/src/builtin-simulator/README.md
================================================
内置模拟器主进程
================================================
FILE: packages/designer/src/builtin-simulator/context.ts
================================================
import { createContext } from 'react';
import { BuiltinSimulatorHost } from './host';
export const SimulatorContext = createContext({} as any);
================================================
FILE: packages/designer/src/builtin-simulator/create-simulator.ts
================================================
// NOTE: 仅用作类型标注,切勿作为实体使用
import { BuiltinSimulatorHost } from './host';
import {
AssetLevel,
AssetLevels,
AssetList,
isAssetBundle,
isAssetItem,
AssetType,
assetItem,
isCSSUrl,
} from '@alilc/lowcode-utils';
import { BuiltinSimulatorRenderer } from './renderer';
export function createSimulator(
host: BuiltinSimulatorHost,
iframe: HTMLIFrameElement,
vendors: AssetList = [],
): Promise {
const win: any = iframe.contentWindow;
const doc = iframe.contentDocument!;
const innerPlugins = host.designer.editor.get('innerPlugins');
win.AliLowCodeEngine = innerPlugins._getLowCodePluginContext({});
win.LCSimulatorHost = host;
win._ = window._;
const styles: any = {};
const scripts: any = {};
AssetLevels.forEach((lv) => {
styles[lv] = [];
scripts[lv] = [];
});
function parseAssetList(assets: AssetList, level?: AssetLevel) {
for (let asset of assets) {
if (!asset) {
continue;
}
if (isAssetBundle(asset)) {
if (asset.assets) {
parseAssetList(
Array.isArray(asset.assets) ? asset.assets : [asset.assets],
asset.level || level,
);
}
continue;
}
if (Array.isArray(asset)) {
parseAssetList(asset, level);
continue;
}
if (!isAssetItem(asset)) {
asset = assetItem(isCSSUrl(asset) ? AssetType.CSSUrl : AssetType.JSUrl, asset, level)!;
}
const id = asset.id ? ` data-id="${asset.id}"` : '';
const lv = asset.level || level || AssetLevel.Environment;
const scriptType = asset.scriptType ? ` type="${asset.scriptType}"` : '';
if (asset.type === AssetType.JSUrl) {
scripts[lv].push(
``,
);
} else if (asset.type === AssetType.JSText) {
scripts[lv].push(``);
} else if (asset.type === AssetType.CSSUrl) {
styles[lv].push(
``,
);
} else if (asset.type === AssetType.CSSText) {
styles[lv].push(
``,
);
}
}
}
parseAssetList(vendors);
const styleFrags = Object.keys(styles)
.map((key) => {
return `${styles[key].join('\n')}`;
})
.join('');
const scriptFrags = Object.keys(scripts)
.map((key) => {
return scripts[key].join('\n');
})
.join('');
doc.open();
doc.write(`
${styleFrags}
${scriptFrags}
`);
doc.close();
return new Promise((resolve) => {
const renderer = win.SimulatorRenderer;
if (renderer) {
return resolve(renderer);
}
const loaded = () => {
resolve(win.SimulatorRenderer || host.renderer);
win.removeEventListener('load', loaded);
};
win.addEventListener('load', loaded);
});
}
================================================
FILE: packages/designer/src/builtin-simulator/host-view.tsx
================================================
import React, { Component } from 'react';
import { observer } from '@alilc/lowcode-editor-core';
import { BuiltinSimulatorHost, BuiltinSimulatorProps } from './host';
import { BemTools } from './bem-tools';
import { Project } from '../project';
import './host.less';
/*
Simulator 模拟器,可替换部件,有协议约束,包含画布的容器,使用场景:当 Canvas 大小变化时,用来居中处理 或 定位 Canvas
Canvas(DeviceShell) 设备壳层,通过背景图片来模拟,通过设备预设样式改变宽度、高度及定位 CanvasViewport
CanvasViewport 页面编排场景中宽高不可溢出 Canvas 区
Content(Shell) 内容外层,宽高紧贴 CanvasViewport,禁用边框,禁用 margin
BemTools 辅助显示层,初始相对 Content 位置 0,0,紧贴 Canvas, 根据 Content 滚动位置,改变相对位置
*/
type SimulatorHostProps = BuiltinSimulatorProps & {
project: Project;
onMount?: (host: BuiltinSimulatorHost) => void;
};
export class BuiltinSimulatorHostView extends Component {
readonly host: BuiltinSimulatorHost;
constructor(props: any) {
super(props);
const { project, onMount, designer } = this.props;
this.host = (project.simulator as BuiltinSimulatorHost) || new BuiltinSimulatorHost(project, designer);
this.host.setProps(this.props);
onMount?.(this.host);
}
shouldComponentUpdate(nextProps: BuiltinSimulatorProps) {
this.host.setProps(nextProps);
return false;
}
render() {
return (
{/* progressing.visible ?
: null */}
);
}
}
@observer
class Canvas extends Component<{ host: BuiltinSimulatorHost }> {
render() {
const sim = this.props.host;
let className = 'lc-simulator-canvas';
const { canvas = {}, viewport = {} } = sim.deviceStyle || {};
if (sim.deviceClassName) {
className += ` ${sim.deviceClassName}`;
} else if (sim.device) {
className += ` lc-simulator-device-${sim.device}`;
}
return (
sim.mountViewport(elmt)} className="lc-simulator-canvas-viewport" style={viewport}>
);
}
}
@observer
class Content extends Component<{ host: BuiltinSimulatorHost }> {
state = {
disabledEvents: false,
};
private dispose?: () => void;
componentDidMount() {
const editor = this.props.host.designer.editor;
const onEnableEvents = (type: boolean) => {
this.setState({
disabledEvents: type,
});
};
editor.eventBus.on('designer.builtinSimulator.disabledEvents', onEnableEvents);
this.dispose = () => {
editor.removeListener('designer.builtinSimulator.disabledEvents', onEnableEvents);
};
}
componentWillUnmount() {
this.dispose?.();
}
render() {
const sim = this.props.host;
const { disabledEvents } = this.state;
const { viewport, designer } = sim;
const frameStyle: any = {
transform: `scale(${viewport.scale})`,
height: viewport.contentHeight,
width: viewport.contentWidth,
};
if (disabledEvents) {
frameStyle.pointerEvents = 'none';
}
const { viewName } = designer;
return (
);
}
}
================================================
FILE: packages/designer/src/builtin-simulator/host.less
================================================
@scope: lc-simulator;
.@{scope} {
position: relative;
height: 100%;
width: 100%;
overflow: auto;
&-canvas {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
overflow: hidden;
&-viewport {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
}
}
&-device-mobile {
left: 50%;
width: 375px;
top: 16px;
bottom: 16px;
max-height: calc(100% - 32px);
max-width: calc(100% - 32px);
transform: translateX(-50%);
box-shadow: 0 2px 10px 0 var(--color-block-background-shallow, rgba(31,56,88,.15));
}
&-device-iphonex { // 增加默认的小程序的壳
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 375px;
height: 812px;
max-height: calc(100vh - 50px);
background: url(https://img.alicdn.com/tfs/TB1b4DHilFR4u4jSZFPXXanzFXa-750-1574.png) no-repeat top;
background-size: 375px 812px;
border-radius: 44px;
box-shadow: var(--color-block-background-shallow, rgba(0, 0, 0, 0.1)) 0 36px 42px;
.@{scope}-canvas-viewport {
width: auto;
top: 50px;
left: 0;
right: 0;
margin-top: 40px;
max-height: 688px;
}
}
&-device-iphone6 {
left: 50%;
width: 375px;
transform: translateX(-50%);
background: url(https://img.alicdn.com/tps/TB12GetLpXXXXXhXFXXXXXXXXXX-756-1544.png) no-repeat top;
background-size: 375px 772px;
top: 8px;
.@{scope}-canvas-viewport {
width: auto;
top: 114px;
left: 25px;
right: 25px;
max-height: 561px;
border-radius: 0 0 2px 2px;
}
}
&-device-default {
top: var(--simulator-top-distance, 16px);
right: var(--simulator-right-distance, 16px);
bottom: var(--simulator-bottom-distance, 16px);
left: var(--simulator-left-distance, 16px);
width: auto;
box-shadow: 0 1px 4px 0 var(--color-block-background-shallow, rgba(31, 50, 88, 0.125));
}
&-content {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
overflow: hidden;
&-frame {
border: none;
transform-origin: 0 0;
height: 100%;
width: 100%;
}
}
}
================================================
FILE: packages/designer/src/builtin-simulator/host.ts
================================================
import {
obx,
autorun,
reaction,
computed,
getPublicPath,
engineConfig,
globalLocale,
IReactionPublic,
IReactionOptions,
IReactionDisposer,
makeObservable,
createModuleEventBus,
IEventBus,
} from '@alilc/lowcode-editor-core';
import {
ISimulatorHost,
Component,
DropContainer,
} from '../simulator';
import Viewport from './viewport';
import { createSimulator } from './create-simulator';
import { Node, INode, contains, isRootNode, isLowCodeComponent } from '../document';
import ResourceConsumer from './resource-consumer';
import {
AssetLevel,
Asset,
AssetList,
assetBundle,
assetItem,
AssetType,
isElement,
isFormEvent,
hasOwnProperty,
UtilsMetadata,
getClosestNode,
transactionManager,
isDragAnyObject,
isDragNodeObject,
isLocationData,
Logger,
} from '@alilc/lowcode-utils';
import {
isShaken,
ILocateEvent,
isChildInline,
isRowContainer,
getRectTarget,
CanvasPoint,
Designer,
IDesigner,
} from '../designer';
import { parseMetadata } from './utils/parse-metadata';
import { getClosestClickableNode } from './utils/clickable';
import {
IPublicTypeComponentMetadata,
IPublicTypePackage,
IPublicEnumTransitionType,
IPublicEnumDragObjectType,
IPublicTypeNodeInstance,
IPublicTypeComponentInstance,
IPublicTypeLocationChildrenDetail,
IPublicTypeLocationDetailType,
IPublicTypeRect,
IPublicModelNode,
} from '@alilc/lowcode-types';
import { BuiltinSimulatorRenderer } from './renderer';
import { clipboard } from '../designer/clipboard';
import { LiveEditing } from './live-editing/live-editing';
import { IProject, Project } from '../project';
import { IScroller } from '../designer/scroller';
import { isElementNode, isDOMNodeVisible } from '../utils/misc';
import { debounce } from 'lodash';
const logger = new Logger({ level: 'warn', bizName: 'designer' });
export type LibraryItem = IPublicTypePackage & {
package: string;
library: string;
urls?: Asset;
editUrls?: Asset;
};
export interface DeviceStyleProps {
canvas?: object;
viewport?: object;
}
export interface BuiltinSimulatorProps {
// 从 documentModel 上获取
// suspended?: boolean;
designMode?: 'live' | 'design' | 'preview' | 'extend' | 'border';
device?: 'mobile' | 'iphone' | string;
deviceClassName?: string;
environment?: Asset;
// @TODO 补充类型
/** @property 请求处理器配置 */
requestHandlersMap?: any;
extraEnvironment?: Asset;
library?: LibraryItem[];
utilsMetadata?: UtilsMetadata;
simulatorUrl?: Asset;
theme?: Asset;
componentsAsset?: Asset;
// eslint-disable-next-line @typescript-eslint/member-ordering
[key: string]: any;
}
const defaultSimulatorUrl = (() => {
const publicPath = getPublicPath();
let urls;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, prefix = '', dev] = /^(.+?)(\/js)?\/?$/.exec(publicPath) || [];
if (dev) {
urls = [
`${prefix}/css/react-simulator-renderer.css`,
`${prefix}/js/react-simulator-renderer.js`,
];
} else if (process.env.NODE_ENV === 'production') {
urls = [`${prefix}/react-simulator-renderer.css`, `${prefix}/react-simulator-renderer.js`];
} else {
urls = [`${prefix}/react-simulator-renderer.css`, `${prefix}/react-simulator-renderer.js`];
}
return urls;
})();
const defaultEnvironment = [
// https://g.alicdn.com/mylib/??react/16.11.0/umd/react.production.min.js,react-dom/16.8.6/umd/react-dom.production.min.js,prop-types/15.7.2/prop-types.min.js
assetItem(
AssetType.JSText,
'window.React=parent.React;window.ReactDOM=parent.ReactDOM;window.__is_simulator_env__=true;',
undefined,
'react',
),
assetItem(
AssetType.JSText,
'window.PropTypes=parent.PropTypes;React.PropTypes=parent.PropTypes; window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;',
),
];
export class BuiltinSimulatorHost implements ISimulatorHost {
readonly isSimulator = true;
readonly project: IProject;
readonly designer: IDesigner;
readonly viewport = new Viewport();
readonly scroller: IScroller;
readonly emitter: IEventBus = createModuleEventBus('BuiltinSimulatorHost');
readonly componentsConsumer: ResourceConsumer;
readonly injectionConsumer: ResourceConsumer;
readonly i18nConsumer: ResourceConsumer;
/**
* 是否为画布自动渲染
*/
autoRender = true;
get currentDocument() {
return this.project.currentDocument;
}
@computed get renderEnv(): string {
return this.get('renderEnv') || 'default';
}
@computed get device(): string {
return this.get('device') || 'default';
}
@computed get locale(): string {
return this.get('locale') || globalLocale.getLocale();
}
@computed get deviceClassName(): string | undefined {
return this.get('deviceClassName');
}
@computed get designMode(): 'live' | 'design' | 'preview' {
// renderer 依赖
// TODO: 需要根据 design mode 不同切换鼠标响应情况
return this.get('designMode') || 'design';
}
@computed get requestHandlersMap(): any {
// renderer 依赖
// TODO: 需要根据 design mode 不同切换鼠标响应情况
return this.get('requestHandlersMap') || null;
}
get thisRequiredInJSE(): boolean {
return engineConfig.get('thisRequiredInJSE') ?? true;
}
get enableStrictNotFoundMode(): any {
return engineConfig.get('enableStrictNotFoundMode') ?? false;
}
get notFoundComponent(): any {
return engineConfig.get('notFoundComponent') ?? null;
}
get faultComponent(): any {
return engineConfig.get('faultComponent') ?? null;
}
get faultComponentMap(): any {
return engineConfig.get('faultComponentMap') ?? null;
}
@computed get componentsAsset(): Asset | undefined {
return this.get('componentsAsset');
}
@computed get theme(): Asset | undefined {
return this.get('theme');
}
@computed get componentsMap() {
// renderer 依赖
return this.designer.componentsMap;
}
@computed get deviceStyle(): DeviceStyleProps | undefined {
return this.get('deviceStyle');
}
@obx.ref _props: BuiltinSimulatorProps = {};
@obx.ref private _contentWindow?: Window;
get contentWindow() {
return this._contentWindow;
}
@obx.ref private _contentDocument?: Document;
@obx.ref private _appHelper?: any;
get contentDocument() {
return this._contentDocument;
}
private _renderer?: BuiltinSimulatorRenderer;
get renderer() {
return this._renderer;
}
readonly asyncLibraryMap: { [key: string]: {} } = {};
readonly libraryMap: { [key: string]: string } = {};
private _iframe?: HTMLIFrameElement;
private disableHovering?: () => void;
private disableDetecting?: () => void;
readonly liveEditing = new LiveEditing();
@obx private instancesMap: {
[docId: string]: Map;
} = {};
private tryScrollAgain: number | null = null;
private _sensorAvailable = true;
/**
* @see IPublicModelSensor
*/
get sensorAvailable(): boolean {
return this._sensorAvailable;
}
private sensing = false;
constructor(project: Project, designer: Designer) {
makeObservable(this);
this.project = project;
this.designer = designer;
this.scroller = this.designer.createScroller(this.viewport);
this.autoRender = !engineConfig.get('disableAutoRender', false);
this._appHelper = engineConfig.get('appHelper');
this.componentsConsumer = new ResourceConsumer(() => this.componentsAsset);
this.injectionConsumer = new ResourceConsumer(() => {
return {
appHelper: this._appHelper,
};
});
engineConfig.onGot('appHelper', (data) => {
// appHelper被config.set修改后触发injectionConsumer.consume回调
this._appHelper = data;
});
this.i18nConsumer = new ResourceConsumer(() => this.project.i18n);
transactionManager.onStartTransaction(() => {
this.stopAutoRepaintNode();
}, IPublicEnumTransitionType.REPAINT);
// 防止批量调用 transaction 时,执行多次 rerender
const rerender = debounce(this.rerender.bind(this), 28);
transactionManager.onEndTransaction(() => {
rerender();
this.enableAutoRepaintNode();
}, IPublicEnumTransitionType.REPAINT);
}
stopAutoRepaintNode() {
this.renderer?.stopAutoRepaintNode();
}
enableAutoRepaintNode() {
this.renderer?.enableAutoRepaintNode();
}
/**
* @see ISimulator
*/
setProps(props: BuiltinSimulatorProps) {
this._props = props;
}
set(key: string, value: any) {
this._props = {
...this._props,
[key]: value,
};
}
get(key: string): any {
if (key === 'device') {
return (
this.designer?.editor?.get('deviceMapper')?.transform?.(this._props.device) ||
this._props.device
);
}
return this._props[key];
}
/**
* 有 Renderer 进程连接进来,设置同步机制
*/
connect(
renderer: BuiltinSimulatorRenderer,
effect: (reaction: IReactionPublic) => void, options?: IReactionOptions,
) {
this._renderer = renderer;
return autorun(effect, options);
}
reaction(expression: (reaction: IReactionPublic) => unknown, effect: (value: unknown, prev: unknown, reaction: IReactionPublic) => void,
opts?: IReactionOptions | undefined): IReactionDisposer {
return reaction(expression, effect, opts);
}
autorun(effect: (reaction: IReactionPublic) => void, options?: IReactionOptions): IReactionDisposer {
return autorun(effect, options);
}
purge(): void {
// todo
}
mountViewport(viewport: HTMLElement | null) {
this.viewport.mount(viewport);
}
/**
* {
* "title":"BizCharts",
* "package":"bizcharts",
* "exportName":"bizcharts",
* "version":"4.0.14",
* "urls":[
* "https://g.alicdn.com/code/lib/bizcharts/4.0.14/BizCharts.js"
* ],
* "library":"BizCharts"
* }
* package:String 资源 npm 包名
* exportName:String umd 包导出名字,用于适配部分物料包 define name 不一致的问题,例如把 BizCharts 改成 bizcharts,用来兼容物料用 define 声明的 bizcharts
* version:String 版本号
* urls:Array 资源 cdn 地址,必须是 umd 类型,可以是.js 或者.css
* library:String umd 包直接导出的 name
*/
buildLibrary(library?: LibraryItem[]) {
const _library = library || (this.get('library') as LibraryItem[]);
const libraryAsset: AssetList = [];
const libraryExportList: string[] = [];
const functionCallLibraryExportList: string[] = [];
if (_library && _library.length) {
_library.forEach((item) => {
const { exportMode, exportSourceLibrary } = item;
this.libraryMap[item.package] = item.library;
if (item.async) {
this.asyncLibraryMap[item.package] = item;
}
if (item.exportName && item.library) {
libraryExportList.push(
`Object.defineProperty(window,'${item.exportName}',{get:()=>window.${item.library}});`,
);
}
if (exportMode === 'functionCall' && exportSourceLibrary) {
functionCallLibraryExportList.push(
`window["${item.library}"] = window["${exportSourceLibrary}"]("${item.library}", "${item.package}");`,
);
}
if (item.editUrls) {
libraryAsset.push(item.editUrls);
} else if (item.urls) {
libraryAsset.push(item.urls);
}
});
}
libraryAsset.unshift(assetItem(AssetType.JSText, libraryExportList.join('')));
libraryAsset.push(assetItem(AssetType.JSText, functionCallLibraryExportList.join('')));
return libraryAsset;
}
rerender() {
this.designer.refreshComponentMetasMap();
this.renderer?.rerender?.();
}
async mountContentFrame(iframe: HTMLIFrameElement | null): Promise {
if (!iframe || this._iframe === iframe) {
return;
}
this._iframe = iframe;
this._contentWindow = iframe.contentWindow!;
this._contentDocument = this._contentWindow.document;
const libraryAsset: AssetList = this.buildLibrary();
if (this.renderEnv === 'rax') {
logger.error('After LowcodeEngine v1.3.0, Rax is no longer supported.');
}
const vendors = [
// required & use once
assetBundle(
this.get('environment') ||
defaultEnvironment,
AssetLevel.Environment,
),
// required & use once
assetBundle(this.get('extraEnvironment'), AssetLevel.Environment),
// required & use once
assetBundle(libraryAsset, AssetLevel.Library),
// required & TODO: think of update
assetBundle(this.theme, AssetLevel.Theme),
// required & use once
assetBundle(
this.get('simulatorUrl') ||
defaultSimulatorUrl,
AssetLevel.Runtime,
),
];
// wait 准备 iframe 内容、依赖库注入
const renderer = await createSimulator(this, iframe, vendors);
// TODO: !!! thinkof reload onloa
// wait 业务组件被第一次消费,否则会渲染出错
await this.componentsConsumer.waitFirstConsume();
// wait 运行时上下文
await this.injectionConsumer.waitFirstConsume();
if (Object.keys(this.asyncLibraryMap).length > 0) {
// 加载异步 Library
await renderer.loadAsyncLibrary(this.asyncLibraryMap);
Object.keys(this.asyncLibraryMap).forEach((key) => {
delete this.asyncLibraryMap[key];
});
}
// step 5 ready & render
renderer.run();
// init events, overlays
this.viewport.setScrollTarget(this._contentWindow);
this.setupEvents();
// bind hotkey & clipboard
const hotkey = this.designer.editor.get('innerHotkey');
hotkey.mount(this._contentWindow);
const innerSkeleton = this.designer.editor.get('skeleton');
innerSkeleton.focusTracker.mount(this._contentWindow);
clipboard.injectCopyPaster(this._contentDocument);
// TODO: dispose the bindings
}
async setupComponents(library: LibraryItem[]) {
const libraryAsset: AssetList = this.buildLibrary(library);
await this.renderer?.load(libraryAsset);
if (Object.keys(this.asyncLibraryMap).length > 0) {
// 加载异步 Library
await this.renderer?.loadAsyncLibrary(this.asyncLibraryMap);
Object.keys(this.asyncLibraryMap).forEach((key) => {
delete this.asyncLibraryMap[key];
});
}
}
setupEvents() {
// TODO: Thinkof move events control to simulator renderer
// just listen special callback
// because iframe maybe reload
this.setupDragAndClick();
this.setupDetecting();
this.setupLiveEditing();
this.setupContextMenu();
}
postEvent(eventName: string, ...data: any[]) {
this.emitter.emit(eventName, ...data);
}
setupDragAndClick() {
const { designer } = this;
const doc = this.contentDocument!;
// TODO: think of lock when edit a node
// 事件路由
doc.addEventListener(
'mousedown',
(downEvent: MouseEvent) => {
// fix for popups close logic
document.dispatchEvent(new Event('mousedown'));
const documentModel = this.project.currentDocument;
if (this.liveEditing.editing || !documentModel) {
return;
}
const { selection } = documentModel;
let isMulti = false;
if (this.designMode === 'design') {
isMulti = downEvent.metaKey || downEvent.ctrlKey;
} else if (!downEvent.metaKey) {
return;
}
// FIXME: dirty fix remove label-for fro liveEditing
downEvent.target?.removeAttribute('for');
const nodeInst = this.getNodeInstanceFromElement(downEvent.target);
const { focusNode } = documentModel;
const node = getClosestClickableNode(nodeInst?.node || focusNode, downEvent);
// 如果找不到可点击的节点,直接返回
if (!node) {
return;
}
// 触发 onMouseDownHook 钩子
const onMouseDownHook = node.componentMeta.advanced.callbacks?.onMouseDownHook;
if (onMouseDownHook) {
onMouseDownHook(downEvent, node.internalToShellNode());
}
const rglNode = node?.getParent();
const isRGLNode = rglNode?.isRGLContainer;
if (isRGLNode) {
// 如果拖拽的是磁铁块的右下角 handle,则直接跳过
if (downEvent.target?.classList.contains('react-resizable-handle')) return;
// 禁止多选
isMulti = false;
designer.dragon.emitter.emit('rgl.switch', {
action: 'start',
rglNode,
});
} else {
// stop response document focus event
// 禁止原生拖拽
downEvent.stopPropagation();
downEvent.preventDefault();
}
// if (!node?.isValidComponent()) {
// // 对于未注册组件直接返回
// return;
// }
const isLeftButton = downEvent.which === 1 || downEvent.button === 0;
const checkSelect = (e: MouseEvent) => {
doc.removeEventListener('mouseup', checkSelect, true);
// 取消移动;
designer.dragon.emitter.emit('rgl.switch', {
action: 'end',
rglNode,
});
// 鼠标是否移动 ? - 鼠标抖动应该也需要支持选中事件,偶尔点击不能选中,磁帖块移除 shaken 检测
if (!isShaken(downEvent, e) || isRGLNode) {
let { id } = node;
designer.activeTracker.track({ node, instance: nodeInst?.instance });
if (isMulti && focusNode && !node.contains(focusNode) && selection.has(id)) {
selection.remove(id);
} else {
// TODO: 避免选中 Page 组件,默认选中第一个子节点;新增规则 或 判断 Live 模式
if (node.isPage() && node.getChildren()?.notEmpty() && this.designMode === 'live') {
const firstChildId = node.getChildren()?.get(0)?.getId();
if (firstChildId) id = firstChildId;
}
if (focusNode) {
selection.select(node.contains(focusNode) ? focusNode.id : id);
}
// dirty code should refector
const editor = this.designer?.editor;
const npm = node?.componentMeta?.npm;
const selected =
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||
node?.componentMeta?.componentName ||
'';
editor?.eventBus.emit('designer.builtinSimulator.select', {
selected,
});
}
}
};
if (isLeftButton && focusNode && !node.contains(focusNode)) {
let nodes: INode[] = [node];
let ignoreUpSelected = false;
if (isMulti) {
// multi select mode, directily add
if (!selection.has(node.id)) {
designer.activeTracker.track({ node, instance: nodeInst?.instance });
selection.add(node.id);
ignoreUpSelected = true;
}
focusNode?.id && selection.remove(focusNode.id);
// 获得顶层 nodes
nodes = selection.getTopNodes();
} else if (selection.containsNode(node, true)) {
nodes = selection.getTopNodes();
} else {
// will clear current selection & select dragment in dragstart
}
designer.dragon.boost(
{
type: IPublicEnumDragObjectType.Node,
nodes,
},
downEvent,
isRGLNode ? rglNode : undefined,
);
if (ignoreUpSelected) {
// multi select mode has add selected, should return
return;
}
}
doc.addEventListener('mouseup', checkSelect, true);
},
true,
);
doc.addEventListener(
'click',
(e) => {
// fix for popups close logic
const x = new Event('click');
x.initEvent('click', true);
this._iframe?.dispatchEvent(x);
const { target } = e;
const customizeIgnoreSelectors = engineConfig.get('customizeIgnoreSelectors');
// TODO: need more elegant solution to ignore click events of components in designer
const defaultIgnoreSelectors: string[] = [
'.next-input-group',
'.next-checkbox-group',
'.next-checkbox-wrapper',
'.next-date-picker',
'.next-input',
'.next-month-picker',
'.next-number-picker',
'.next-radio-group',
'.next-range',
'.next-range-picker',
'.next-rating',
'.next-select',
'.next-switch',
'.next-time-picker',
'.next-upload',
'.next-year-picker',
'.next-breadcrumb-item',
'.next-calendar-header',
'.next-calendar-table',
'.editor-container', // 富文本组件
];
const ignoreSelectors = customizeIgnoreSelectors?.(defaultIgnoreSelectors, e) || defaultIgnoreSelectors;
const ignoreSelectorsString = ignoreSelectors.join(',');
// 提供了 customizeIgnoreSelectors 的情况下,忽略 isFormEvent() 判断
if ((!customizeIgnoreSelectors && isFormEvent(e)) || target?.closest(ignoreSelectorsString)) {
e.preventDefault();
e.stopPropagation();
}
// stop response document click event
// todo: catch link redirect
},
true,
);
}
/**
* 设置悬停处理
*/
setupDetecting() {
const doc = this.contentDocument!;
const { detecting, dragon } = this.designer;
const hover = (e: MouseEvent) => {
if (!detecting.enable || this.designMode !== 'design') {
return;
}
const nodeInst = this.getNodeInstanceFromElement(e.target as Element);
if (nodeInst?.node) {
let { node } = nodeInst;
const focusNode = node.document?.focusNode;
if (focusNode && node.contains(focusNode)) {
node = focusNode;
}
detecting.capture(node);
} else {
detecting.capture(null);
}
if (!engineConfig.get('enableMouseEventPropagationInCanvas', false) || dragon.dragging) {
e.stopPropagation();
}
};
const leave = () => {
this.project.currentDocument && detecting.leave(this.project.currentDocument);
};
doc.addEventListener('mouseover', hover, true);
doc.addEventListener('mouseleave', leave, false);
// TODO: refactor this line, contains click, mousedown, mousemove
doc.addEventListener(
'mousemove',
(e: Event) => {
if (!engineConfig.get('enableMouseEventPropagationInCanvas', false) || dragon.dragging) {
e.stopPropagation();
}
},
true,
);
// this.disableDetecting = () => {
// detecting.leave(this.project.currentDocument);
// doc.removeEventListener('mouseover', hover, true);
// doc.removeEventListener('mouseleave', leave, false);
// this.disableDetecting = undefined;
// };
}
setupLiveEditing() {
const doc = this.contentDocument!;
// cause edit
doc.addEventListener(
'dblclick',
(e: MouseEvent) => {
// stop response document dblclick event
e.stopPropagation();
e.preventDefault();
const targetElement = e.target as HTMLElement;
const nodeInst = this.getNodeInstanceFromElement(targetElement);
if (!nodeInst) {
return;
}
const focusNode = this.project.currentDocument?.focusNode;
const node = nodeInst.node || focusNode;
if (!node || isLowCodeComponent(node)) {
return;
}
const rootElement = this.findDOMNodes(
nodeInst.instance,
node.componentMeta.rootSelector,
)?.find(
(item) =>
// 可能是 [null];
item && item.contains(targetElement),
) as HTMLElement;
if (!rootElement) {
return;
}
this.liveEditing.apply({
node,
rootElement,
event: e,
});
},
true,
);
}
/**
* @see ISimulator
*/
setSuspense(/** _suspended: boolean */) {
return false;
// if (suspended) {
// /*
// if (this.disableDetecting) {
// this.disableDetecting();
// }
// */
// // sleep some autorun reaction
// } else {
// // weekup some autorun reaction
// /*
// if (!this.disableDetecting) {
// this.setupDetecting();
// }
// */
// }
}
setupContextMenu() {
const doc = this.contentDocument!;
doc.addEventListener('contextmenu', (e: MouseEvent) => {
const targetElement = e.target as HTMLElement;
const nodeInst = this.getNodeInstanceFromElement(targetElement);
const editor = this.designer?.editor;
if (!nodeInst) {
editor?.eventBus.emit('designer.builtinSimulator.contextmenu', {
originalEvent: e,
});
return;
}
const node = nodeInst.node || this.project.currentDocument?.focusNode;
if (!node) {
editor?.eventBus.emit('designer.builtinSimulator.contextmenu', {
originalEvent: e,
});
return;
}
// dirty code should refector
const npm = node?.componentMeta?.npm;
const selected =
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||
node?.componentMeta?.componentName ||
'';
editor?.eventBus.emit('designer.builtinSimulator.contextmenu', {
selected,
...nodeInst,
instanceRect: this.computeComponentInstanceRect(nodeInst.instance),
originalEvent: e,
});
});
}
/**
* @see ISimulator
*/
generateComponentMetadata(componentName: string): IPublicTypeComponentMetadata {
// if html tags
if (isHTMLTag(componentName)) {
return {
componentName,
// TODO: read builtins html metadata
};
}
const component = this.getComponent(componentName);
if (!component) {
return {
componentName,
};
}
// TODO:
// 1. generate builtin div/p/h1/h2
// 2. read propTypes
return {
componentName,
...parseMetadata(component),
};
}
/**
* @see ISimulator
*/
getComponent(componentName: string): Component | null {
return this.renderer?.getComponent(componentName) || null;
}
createComponent(/** _schema: IPublicTypeComponentSchema */): Component | null {
return null;
// return this.renderer?.createComponent(schema) || null;
}
setInstance(docId: string, id: string, instances: IPublicTypeComponentInstance[] | null) {
if (!hasOwnProperty(this.instancesMap, docId)) {
this.instancesMap[docId] = new Map();
}
if (instances == null) {
this.instancesMap[docId].delete(id);
} else {
this.instancesMap[docId].set(id, instances.slice());
}
}
/**
* @see ISimulator
*/
getComponentInstances(node: INode, context?: IPublicTypeNodeInstance): IPublicTypeComponentInstance[] | null {
const docId = node.document?.id;
if (!docId) {
return null;
}
const instances = this.instancesMap[docId]?.get(node.id) || null;
if (!instances || !context) {
return instances;
}
// filter with context
return instances.filter((instance) => {
return this.getClosestNodeInstance(instance, context?.nodeId)?.instance === context.instance;
});
}
/**
* @see ISimulator
*/
getComponentContext(/* node: Node */): any {
throw new Error('Method not implemented.');
}
/**
* @see ISimulator
*/
getClosestNodeInstance(
from: IPublicTypeComponentInstance,
specId?: string,
): IPublicTypeNodeInstance | null {
return this.renderer?.getClosestNodeInstance(from, specId) || null;
}
/**
* @see ISimulator
*/
computeRect(node: INode): IPublicTypeRect | null {
const instances = this.getComponentInstances(node);
if (!instances) {
return null;
}
return this.computeComponentInstanceRect(instances[0], node.componentMeta.rootSelector);
}
/**
* @see ISimulator
*/
computeComponentInstanceRect(instance: IPublicTypeComponentInstance, selector?: string): IPublicTypeRect | null {
const renderer = this.renderer!;
const elements = this.findDOMNodes(instance, selector);
if (!elements) {
return null;
}
const elems = elements.slice();
let rects: DOMRect[] | undefined;
let last: { x: number; y: number; r: number; b: number } | undefined;
let _computed = false;
while (true) {
if (!rects || rects.length < 1) {
const elem = elems.pop();
if (!elem) {
break;
}
rects = renderer.getClientRects(elem);
}
const rect = rects.pop();
if (!rect) {
break;
}
if (rect.width === 0 && rect.height === 0) {
continue;
}
if (!last) {
last = {
x: rect.left,
y: rect.top,
r: rect.right,
b: rect.bottom,
};
continue;
}
if (rect.left < last.x) {
last.x = rect.left;
_computed = true;
}
if (rect.top < last.y) {
last.y = rect.top;
_computed = true;
}
if (rect.right > last.r) {
last.r = rect.right;
_computed = true;
}
if (rect.bottom > last.b) {
last.b = rect.bottom;
_computed = true;
}
}
if (last) {
const r: IPublicTypeRect = new DOMRect(last.x, last.y, last.r - last.x, last.b - last.y);
r.elements = elements;
r.computed = _computed;
return r;
}
return null;
}
/**
* @see ISimulator
*/
findDOMNodes(instance: IPublicTypeComponentInstance, selector?: string): Array | null {
const elements = this._renderer?.findDOMNodes(instance);
if (!elements) {
return null;
}
if (selector) {
const matched = getMatched(elements, selector);
if (!matched) {
return null;
}
return [matched];
}
return elements;
}
/**
* 通过 DOM 节点获取节点,依赖 simulator 的接口
*/
getNodeInstanceFromElement(target: Element | null): IPublicTypeNodeInstance | null {
if (!target) {
return null;
}
const nodeInstance = this.getClosestNodeInstance(target);
if (!nodeInstance) {
return null;
}
const { docId } = nodeInstance;
const doc = this.project.getDocument(docId)!;
const node = doc.getNode(nodeInstance.nodeId);
return {
...nodeInstance,
node,
};
}
/**
* @see ISimulator
*/
/* istanbul ignore next */
scrollToNode(node: Node, detail?: any /* , tryTimes = 0 */) {
this.tryScrollAgain = null;
if (this.sensing) {
// active sensor
return;
}
const opt: any = {};
let scroll = false;
const componentInstance = this.getComponentInstances(detail?.near?.node || node)?.[0];
if (!componentInstance) return;
const domNode = this.findDOMNodes(componentInstance)?.[0] as Element;
if (!domNode) return;
if (isElementNode(domNode) && !isDOMNodeVisible(domNode, this.viewport)) {
const { left, top } = domNode.getBoundingClientRect();
const { scrollTop = 0, scrollLeft = 0 } = this.contentDocument?.documentElement || {};
opt.left = left + scrollLeft;
opt.top = top + scrollTop;
scroll = true;
}
if (scroll && this.scroller) {
this.scroller.scrollTo(opt);
}
}
// #region ========= drag and drop helpers =============
/**
* @see ISimulator
*/
setNativeSelection(enableFlag: boolean) {
this.renderer?.setNativeSelection(enableFlag);
}
/**
* @see ISimulator
*/
setDraggingState(state: boolean) {
this.renderer?.setDraggingState(state);
}
/**
* @see ISimulator
*/
setCopyState(state: boolean) {
this.renderer?.setCopyState(state);
}
/**
* @see ISimulator
*/
clearState() {
this.renderer?.clearState();
}
/**
* @see IPublicModelSensor
*/
fixEvent(e: ILocateEvent): ILocateEvent {
if (e.fixed) {
return e;
}
const notMyEvent = e.originalEvent.view?.document !== this.contentDocument;
// fix canvasX canvasY : 当前激活文档画布坐标系
if (notMyEvent || !('canvasX' in e) || !('canvasY' in e)) {
const l = this.viewport.toLocalPoint({
clientX: e.globalX,
clientY: e.globalY,
});
e.canvasX = l.clientX;
e.canvasY = l.clientY;
}
// fix target : 浏览器事件响应目标
if (!e.target || notMyEvent) {
if (!isNaN(e.canvasX!) && !isNaN(e.canvasY!)) {
e.target = this.contentDocument?.elementFromPoint(e.canvasX!, e.canvasY!);
}
}
// 事件已订正
e.fixed = true;
return e;
}
/**
* @see IPublicModelSensor
*/
isEnter(e: ILocateEvent): boolean {
const rect = this.viewport.bounds;
return (
e.globalY >= rect.top &&
e.globalY <= rect.bottom &&
e.globalX >= rect.left &&
e.globalX <= rect.right
);
}
/**
* @see IPublicModelSensor
*/
deactiveSensor() {
this.sensing = false;
this.scroller.cancel();
}
// ========= drag location logic: helper for locate ==========
/**
* @see IPublicModelSensor
*/
locate(e: ILocateEvent): any {
const { dragObject } = e;
const nodes = dragObject?.nodes;
const operationalNodes = nodes?.filter((node) => {
const onMoveHook = node.componentMeta?.advanced.callbacks?.onMoveHook;
const canMove = onMoveHook && typeof onMoveHook === 'function' ? onMoveHook(node.internalToShellNode()) : true;
let parentContainerNode: INode | null = null;
let parentNode = node.parent;
while (parentNode) {
if (parentNode.isContainer()) {
parentContainerNode = parentNode;
break;
}
parentNode = parentNode.parent;
}
const onChildMoveHook = parentContainerNode?.componentMeta?.advanced.callbacks?.onChildMoveHook;
const childrenCanMove = onChildMoveHook && parentContainerNode && typeof onChildMoveHook === 'function' ? onChildMoveHook(node.internalToShellNode(), parentContainerNode.internalToShellNode()) : true;
return canMove && childrenCanMove;
});
if (nodes && (!operationalNodes || operationalNodes.length === 0)) {
return;
}
this.sensing = true;
this.scroller.scrolling(e);
const document = this.project.currentDocument;
if (!document) {
return null;
}
const dropContainer = this.getDropContainer(e);
const lockedNode = getClosestNode(dropContainer?.container, (node) => node.isLocked);
if (lockedNode) return null;
if (
!dropContainer
) {
return null;
}
if (isLocationData(dropContainer)) {
return this.designer.createLocation(dropContainer);
}
const { container, instance: containerInstance } = dropContainer;
const edge = this.computeComponentInstanceRect(
containerInstance,
container.componentMeta.rootSelector,
);
if (!edge) {
return null;
}
const { children } = container;
const detail: IPublicTypeLocationChildrenDetail = {
type: IPublicTypeLocationDetailType.Children,
index: 0,
edge,
};
const locationData = {
target: container,
detail,
source: `simulator${document.id}`,
event: e,
};
if (
e.dragObject &&
e.dragObject.nodes &&
e.dragObject.nodes.length &&
e.dragObject.nodes[0].componentMeta.isModal &&
document.focusNode
) {
return this.designer.createLocation({
target: document.focusNode,
detail,
source: `simulator${document.id}`,
event: e,
});
}
if (!children || children.size < 1 || !edge) {
return this.designer.createLocation(locationData);
}
let nearRect: IPublicTypeRect | null = null;
let nearIndex: number = 0;
let nearNode: INode | null = null;
let nearDistance: number | null = null;
let minTop: number | null = null;
let maxBottom: number | null = null;
for (let i = 0, l = children.size; i < l; i++) {
const node = children.get(i)!;
const index = i;
const instances = this.getComponentInstances(node);
const inst = instances
? instances.length > 1
? instances.find(
(_inst) => this.getClosestNodeInstance(_inst, container.id)?.instance === containerInstance,
)
: instances[0]
: null;
const rect = inst
? this.computeComponentInstanceRect(inst, node.componentMeta.rootSelector)
: null;
if (!rect) {
continue;
}
const distance = isPointInRect(e as any, rect) ? 0 : distanceToRect(e as any, rect);
if (distance === 0) {
nearDistance = distance;
nearNode = node;
nearIndex = index;
nearRect = rect;
break;
}
// 标记子节点最顶
if (minTop === null || rect.top < minTop) {
minTop = rect.top;
}
// 标记子节点最底
if (maxBottom === null || rect.bottom > maxBottom) {
maxBottom = rect.bottom;
}
if (nearDistance === null || distance < nearDistance) {
nearDistance = distance;
nearNode = node;
nearIndex = index;
nearRect = rect;
}
}
detail.index = nearIndex;
if (nearNode && nearRect) {
const el = getRectTarget(nearRect);
const inline = el ? isChildInline(el) : false;
const row = el ? isRowContainer(el.parentElement!) : false;
const vertical = inline || row;
// TODO: fix type
const near: {
node: IPublicModelNode;
pos: 'before' | 'after' | 'replace';
rect?: IPublicTypeRect;
align?: 'V' | 'H';
} = {
node: nearNode.internalToShellNode()!,
pos: 'before',
align: vertical ? 'V' : 'H',
};
detail.near = near;
if (isNearAfter(e as any, nearRect, vertical)) {
near.pos = 'after';
detail.index = nearIndex + 1;
}
if (!row && nearDistance !== 0) {
const edgeDistance = distanceToEdge(e as any, edge);
if (edgeDistance.distance < nearDistance!) {
const { nearAfter } = edgeDistance;
if (minTop == null) {
minTop = edge.top;
}
if (maxBottom == null) {
maxBottom = edge.bottom;
}
near.rect = new DOMRect(edge.left, minTop, edge.width, maxBottom - minTop);
near.align = 'H';
near.pos = nearAfter ? 'after' : 'before';
detail.index = nearAfter ? children.size : 0;
}
}
}
return this.designer.createLocation(locationData);
}
/**
* 查找合适的投放容器
*/
getDropContainer(e: ILocateEvent): DropContainer | null {
const { target, dragObject } = e;
const isAny = isDragAnyObject(dragObject);
const document = this.project.currentDocument!;
const { currentRoot } = document;
let container: INode | null;
let nodeInstance: IPublicTypeNodeInstance | undefined;
if (target) {
const ref = this.getNodeInstanceFromElement(target);
if (ref?.node) {
nodeInstance = ref;
container = ref.node;
} else if (isAny) {
return null;
} else {
container = currentRoot;
}
} else if (isAny) {
return null;
} else {
container = currentRoot;
}
if (!container?.isParental()) {
container = container?.parent || currentRoot;
}
// TODO: use spec container to accept specialData
if (isAny) {
// will return locationData
return null;
}
// get common parent, avoid drop container contains by dragObject
const drillDownExcludes = new Set();
if (isDragNodeObject(dragObject)) {
const { nodes } = dragObject;
let i = nodes.length;
let p: any = container;
while (i-- > 0) {
if (contains(nodes[i], p)) {
p = nodes[i].parent;
}
}
if (p !== container) {
container = p || document.focusNode;
container && drillDownExcludes.add(container);
}
}
let instance: any;
if (nodeInstance) {
if (nodeInstance.node === container) {
instance = nodeInstance.instance;
} else {
instance = this.getClosestNodeInstance(
nodeInstance.instance as any,
container?.id,
)?.instance;
}
} else {
instance = container && this.getComponentInstances(container)?.[0];
}
let dropContainer: DropContainer = {
container: container as any,
instance,
};
let res: any;
let upward: DropContainer | null = null;
while (container) {
res = this.handleAccept(dropContainer, e);
// if (isLocationData(res)) {
// return res;
// }
if (res === true) {
return dropContainer;
}
if (!res) {
drillDownExcludes.add(container);
if (upward) {
dropContainer = upward;
container = dropContainer.container;
upward = null;
} else if (container.parent) {
container = container.parent;
instance = this.getClosestNodeInstance(dropContainer.instance, container.id)?.instance;
dropContainer = {
container,
instance,
};
} else {
return null;
}
}
}
return null;
}
isAcceptable(): boolean {
return false;
}
/**
* 控制接受
*/
handleAccept({ container }: DropContainer, e: ILocateEvent): boolean {
const { dragObject } = e;
const document = this.currentDocument!;
const { focusNode } = document;
if (isRootNode(container) || container.contains(focusNode)) {
return document.checkNesting(focusNode!, dragObject as any);
}
const meta = (container as Node).componentMeta;
// FIXME: get containerInstance for accept logic use
const acceptable: boolean = this.isAcceptable(container);
if (!meta.isContainer && !acceptable) {
return false;
}
// check nesting
return document.checkNesting(container, dragObject as any);
}
/**
* 查找邻近容器
*/
getNearByContainer(
{ container, instance }: DropContainer,
drillDownExcludes: Set,
) {
const { children } = container;
if (!children || children.isEmpty()) {
return null;
}
const nearBy: any = null;
for (let i = 0, l = children.size; i < l; i++) {
let child = children.get(i);
if (!child) {
continue;
}
if (child.conditionGroup) {
const bn = child.conditionGroup;
i = (bn.index || 0) + bn.length - 1;
child = bn.visibleNode;
}
if (!child.isParental() || drillDownExcludes.has(child)) {
continue;
}
// TODO:
this.findDOMNodes(instance);
this.getComponentInstances(child);
const rect = this.computeRect(child);
if (!rect) {
continue;
}
}
return nearBy;
}
// #endregion
}
function isHTMLTag(name: string) {
return /^[a-z]\w*$/.test(name);
}
function isPointInRect(point: CanvasPoint, rect: IPublicTypeRect) {
return (
point.canvasY >= rect.top &&
point.canvasY <= rect.bottom &&
point.canvasX >= rect.left &&
point.canvasX <= rect.right
);
}
function distanceToRect(point: CanvasPoint, rect: IPublicTypeRect) {
let minX = Math.min(Math.abs(point.canvasX - rect.left), Math.abs(point.canvasX - rect.right));
let minY = Math.min(Math.abs(point.canvasY - rect.top), Math.abs(point.canvasY - rect.bottom));
if (point.canvasX >= rect.left && point.canvasX <= rect.right) {
minX = 0;
}
if (point.canvasY >= rect.top && point.canvasY <= rect.bottom) {
minY = 0;
}
return Math.sqrt(minX ** 2 + minY ** 2);
}
function distanceToEdge(point: CanvasPoint, rect: IPublicTypeRect) {
const distanceTop = Math.abs(point.canvasY - rect.top);
const distanceBottom = Math.abs(point.canvasY - rect.bottom);
return {
distance: Math.min(distanceTop, distanceBottom),
nearAfter: distanceBottom < distanceTop,
};
}
function isNearAfter(point: CanvasPoint, rect: IPublicTypeRect, inline: boolean) {
if (inline) {
return (
Math.abs(point.canvasX - rect.left) + Math.abs(point.canvasY - rect.top) >
Math.abs(point.canvasX - rect.right) + Math.abs(point.canvasY - rect.bottom)
);
}
return Math.abs(point.canvasY - rect.top) > Math.abs(point.canvasY - rect.bottom);
}
function getMatched(elements: Array, selector: string): Element | null {
let firstQueried: Element | null = null;
for (const elem of elements) {
if (isElement(elem)) {
if (elem.matches(selector)) {
return elem;
}
if (!firstQueried) {
firstQueried = elem.querySelector(selector);
}
}
}
return firstQueried;
}
================================================
FILE: packages/designer/src/builtin-simulator/index.ts
================================================
export * from './host';
export * from './host-view';
export * from './renderer';
export * from './live-editing/live-editing';
export { LowcodeTypes } from './utils/parse-metadata';
================================================
FILE: packages/designer/src/builtin-simulator/renderer.ts
================================================
import { Component } from '../simulator';
import { IPublicTypeComponentInstance, IPublicTypeSimulatorRenderer } from '@alilc/lowcode-types';
export type BuiltinSimulatorRenderer = IPublicTypeSimulatorRenderer;
export function isSimulatorRenderer(obj: any): obj is BuiltinSimulatorRenderer {
return obj && obj.isSimulatorRenderer;
}
================================================
FILE: packages/designer/src/builtin-simulator/resource-consumer.ts
================================================
import { autorun, makeObservable, obx, createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core';
import { BuiltinSimulatorHost } from './host';
import { BuiltinSimulatorRenderer, isSimulatorRenderer } from './renderer';
const UNSET = Symbol('unset');
export type MasterProvider = (master: BuiltinSimulatorHost) => any;
export type RendererConsumer = (renderer: BuiltinSimulatorRenderer, data: T) => Promise;
// master 进程
// 0. 初始化该对象,因为需要响应变更发生在 master 进程
// 1. 提供消费数据或数据提供器,比如 Asset 资源,如果不是数据提供器,会持续提供
// 2. 收到成功通知
// renderer 进程
// 1. 持续消费,并持续监听数据
// 2. 消费
// 这里涉及俩个自定义项
// 1. 被消费数据协议
// 2. 消费机制(渲染进程自定 + 传递进入)
export default class ResourceConsumer {
private emitter: IEventBus = createModuleEventBus('ResourceConsumer');
@obx.ref private _data: T | typeof UNSET = UNSET;
private _providing?: () => void;
private _consuming?: () => void;
private _firstConsumed = false;
private resolveFirst?: (resolve?: any) => void;
constructor(provider: () => T, private consumer?: RendererConsumer) {
makeObservable(this);
this._providing = autorun(() => {
this._data = provider();
});
}
consume(consumerOrRenderer: BuiltinSimulatorRenderer | ((data: T) => any)) {
if (this._consuming) {
return;
}
let consumer: (data: T) => any;
if (isSimulatorRenderer(consumerOrRenderer)) {
if (!this.consumer) {
// TODO: throw error
return;
}
const rendererConsumer = this.consumer!;
consumer = (data) => rendererConsumer(consumerOrRenderer, data);
} else {
consumer = consumerOrRenderer;
}
this._consuming = autorun(async () => {
if (this._data === UNSET) {
return;
}
await consumer(this._data);
// TODO: catch error and report
if (this.resolveFirst) {
this.resolveFirst();
} else {
this._firstConsumed = true;
}
});
}
dispose() {
if (this._providing) {
this._providing();
}
if (this._consuming) {
this._consuming();
}
this.emitter.removeAllListeners();
}
waitFirstConsume(): Promise {
if (this._firstConsumed) {
return Promise.resolve();
}
return new Promise((resolve) => {
this.resolveFirst = resolve;
});
}
}
================================================
FILE: packages/designer/src/builtin-simulator/viewport.ts
================================================
import { obx, computed, makeObservable } from '@alilc/lowcode-editor-core';
import { Point, ScrollTarget } from '../designer';
import { AutoFit, IViewport } from '../simulator';
export default class Viewport implements IViewport {
@obx.ref private rect?: DOMRect;
private _bounds?: DOMRect;
get bounds(): DOMRect {
if (this._bounds) {
return this._bounds;
}
this._bounds = this.viewportElement!.getBoundingClientRect();
requestAnimationFrame(() => {
this._bounds = undefined;
});
return this._bounds;
}
get contentBounds(): DOMRect {
const { bounds, scale } = this;
return new DOMRect(0, 0, bounds.width / scale, bounds.height / scale);
}
private viewportElement?: HTMLElement;
constructor() {
makeObservable(this);
}
mount(viewportElement: HTMLElement | null) {
if (!viewportElement || this.viewportElement === viewportElement) {
return;
}
this.viewportElement = viewportElement;
this.touch();
}
touch() {
if (this.viewportElement) {
this.rect = this.bounds;
}
}
@computed get height(): number {
if (!this.rect) {
return 600;
}
return this.rect.height;
}
set height(newHeight: number) {
this._contentHeight = newHeight / this.scale;
if (this.viewportElement) {
this.viewportElement.style.height = `${newHeight}px`;
this.touch();
}
}
@computed get width(): number {
if (!this.rect) {
return 1000;
}
return this.rect.width;
}
set width(newWidth: number) {
this._contentWidth = newWidth / this.scale;
if (this.viewportElement) {
this.viewportElement.style.width = `${newWidth}px`;
this.touch();
}
}
@obx.ref private _scale = 1;
/**
* 缩放比例
*/
@computed get scale(): number {
return this._scale;
}
set scale(newScale: number) {
if (isNaN(newScale) || newScale <= 0) {
throw new Error(`invalid new scale "${newScale}"`);
}
this._scale = newScale;
this._contentWidth = this.width / this.scale;
this._contentHeight = this.height / this.scale;
}
@obx.ref private _contentWidth: number | AutoFit = AutoFit;
@obx.ref private _contentHeight: number | AutoFit = AutoFit;
@computed get contentHeight(): number | AutoFit {
return this._contentHeight;
}
set contentHeight(newContentHeight: number | AutoFit) {
this._contentHeight = newContentHeight;
}
@computed get contentWidth(): number | AutoFit {
return this._contentWidth;
}
set contentWidth(val: number | AutoFit) {
this._contentWidth = val;
}
@obx.ref private _scrollX = 0;
@obx.ref private _scrollY = 0;
get scrollX() {
return this._scrollX;
}
get scrollY() {
return this._scrollY;
}
private _scrollTarget?: ScrollTarget;
/**
* 滚动对象
*/
get scrollTarget(): ScrollTarget | undefined {
return this._scrollTarget;
}
@obx private _scrolling = false;
get scrolling(): boolean {
return this._scrolling;
}
setScrollTarget(target: Window) {
const scrollTarget = new ScrollTarget(target);
this._scrollX = scrollTarget.left;
this._scrollY = scrollTarget.top;
let scrollTimer: any;
target.addEventListener('scroll', () => {
this._scrollX = scrollTarget.left;
this._scrollY = scrollTarget.top;
this._scrolling = true;
if (scrollTimer) {
clearTimeout(scrollTimer);
}
scrollTimer = setTimeout(() => {
this._scrolling = false;
}, 80);
});
target.addEventListener('resize', () => this.touch());
this._scrollTarget = scrollTarget;
}
toGlobalPoint(point: Point): Point {
if (!this.viewportElement) {
return point;
}
const rect = this.bounds;
return {
clientX: point.clientX * this.scale + rect.left,
clientY: point.clientY * this.scale + rect.top,
};
}
toLocalPoint(point: Point): Point {
if (!this.viewportElement) {
return point;
}
const rect = this.bounds;
return {
clientX: (point.clientX - rect.left) / this.scale,
clientY: (point.clientY - rect.top) / this.scale,
};
}
}
================================================
FILE: packages/designer/src/builtin-simulator/bem-tools/bem-tools.less
================================================
.lc-bem-tools {
pointer-events: none;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
overflow: visible;
z-index: 1;
}
================================================
FILE: packages/designer/src/builtin-simulator/bem-tools/border-container.tsx
================================================
import * as React from 'react';
import { Component, Fragment, ReactElement, PureComponent } from 'react';
import classNames from 'classnames';
import { computed, observer, Title, globalLocale } from '@alilc/lowcode-editor-core';
import { IPublicTypeI18nData, IPublicTypeTitleContent } from '@alilc/lowcode-types';
import { isI18nData } from '@alilc/lowcode-utils';
import { DropLocation } from '../../designer';
import { BuiltinSimulatorHost } from '../../builtin-simulator/host';
import { INode } from '../../document/node';
export class BorderContainerInstance extends PureComponent<{
title: IPublicTypeTitleContent;
rect: DOMRect | null;
scale: number;
scrollX: number;
scrollY: number;
}> {
render() {
const { title, rect, scale, scrollX, scrollY } = this.props;
if (!rect) {
return null;
}
const style = {
width: rect.width * scale,
height: rect.height * scale,
transform: `translate(${(scrollX + rect.left) * scale}px, ${(scrollY + rect.top) * scale}px)`,
};
const className = classNames('lc-borders lc-borders-detecting');
return (
);
}
}
function getTitle(title: string | IPublicTypeI18nData | ReactElement) {
if (typeof title === 'string') return title;
if (isI18nData(title)) {
const locale = globalLocale.getLocale() || 'zh-CN';
return `将放入到此${title[locale]}`;
}
return '';
}
@observer
export class BorderContainer extends Component<{
host: BuiltinSimulatorHost;
}, {
target?: INode;
}> {
state = {} as any;
@computed get scale() {
return this.props.host.viewport.scale;
}
@computed get scrollX() {
return this.props.host.viewport.scrollX;
}
@computed get scrollY() {
return this.props.host.viewport.scrollY;
}
componentDidMount() {
const { host } = this.props;
host.designer.editor.eventBus.on('designer.dropLocation.change', (loc: DropLocation) => {
let { target } = this.state;
if (target === loc?.target) return;
this.setState({
target: loc?.target,
});
});
}
render() {
const { host } = this.props;
const { target } = this.state;
if (target == undefined) {
return null;
}
const instances = host.getComponentInstances(target!);
if (!instances || instances.length < 1) {
return null;
}
if (instances.length === 1) {
return (
);
}
return (
{instances.map((inst, i) => (
))}
);
}
}
================================================
FILE: packages/designer/src/builtin-simulator/bem-tools/border-detecting.tsx
================================================
import { Component, Fragment, PureComponent } from 'react';
import classNames from 'classnames';
import { computed, observer, Title } from '@alilc/lowcode-editor-core';
import { IPublicTypeTitleContent } from '@alilc/lowcode-types';
import { getClosestNode } from '@alilc/lowcode-utils';
import { intl } from '../../locale';
import { BuiltinSimulatorHost } from '../host';
export class BorderDetectingInstance extends PureComponent<{
title: IPublicTypeTitleContent;
rect: DOMRect | null;
scale: number;
scrollX: number;
scrollY: number;
isLocked?: boolean;
}> {
render() {
const { title, rect, scale, scrollX, scrollY, isLocked } = this.props;
if (!rect) {
return null;
}
const style = {
width: rect.width * scale,
height: rect.height * scale,
transform: `translate(${(scrollX + rect.left) * scale}px, ${(scrollY + rect.top) * scale}px)`,
};
const className = classNames('lc-borders lc-borders-detecting');
// TODO:
// 1. thinkof icon
// 2. thinkof top|bottom|inner space
return (
{
isLocked ? () : null
}
);
}
}
@observer
export class BorderDetecting extends Component<{ host: BuiltinSimulatorHost }> {
@computed get scale() {
return this.props.host.viewport.scale;
}
@computed get scrollX() {
return this.props.host.viewport.scrollX;
}
@computed get scrollY() {
return this.props.host.viewport.scrollY;
}
@computed get current() {
const { host } = this.props;
const doc = host.currentDocument;
if (!doc) {
return null;
}
const { selection } = doc;
const { current } = host.designer.detecting;
if (!current || current.document !== doc || selection.has(current.id)) {
return null;
}
return current;
}
render() {
const { host } = this.props;
const { current } = this;
const canHoverHook = current?.componentMeta.advanced.callbacks?.onHoverHook;
const canHover = (canHoverHook && typeof canHoverHook === 'function') ? canHoverHook(current.internalToShellNode()) : true;
if (!canHover || !current || host.viewport.scrolling || host.liveEditing.editing) {
return null;
}
// rootNode, hover whole viewport
const focusNode = current.document.focusNode!;
if (!focusNode.contains(current)) {
return null;
}
if (current.contains(focusNode)) {
const bounds = host.viewport.bounds;
return (
);
}
const lockedNode = getClosestNode(current, (n) => {
// 假如当前节点就是 locked 状态,要从当前节点的父节点开始查找
return !!(current?.isLocked ? n.parent?.isLocked : n.isLocked);
});
if (lockedNode && lockedNode.getId() !== current.getId()) {
// 选中父节锁定的节点
return (
);
}
const instances = host.getComponentInstances(current);
if (!instances || instances.length < 1) {
return null;
}
if (instances.length === 1) {
return (
);
}
return (
{instances.map((inst, i) => (
))}
);
}
}
================================================
FILE: packages/designer/src/builtin-simulator/bem-tools/border-resizing.tsx
================================================
import React, { Component, Fragment } from 'react';
import DragResizeEngine from './drag-resize-engine';
import { observer, computed } from '@alilc/lowcode-editor-core';
import classNames from 'classnames';
import { SimulatorContext } from '../context';
import { BuiltinSimulatorHost } from '../host';
import { OffsetObserver, Designer, INode } from '../../designer';
import { Node } from '../../document';
import { normalizeTriggers } from '../../utils/misc';
@observer
export default class BoxResizing extends Component<{ host: BuiltinSimulatorHost }> {
static contextType = SimulatorContext;
get host(): BuiltinSimulatorHost {
return this.props.host;
}
get dragging(): boolean {
return this.host.designer.dragon.dragging;
}
@computed get selecting() {
const doc = this.host.currentDocument;
if (!doc || doc.suspensed) {
return null;
}
const { selection } = doc;
return this.dragging ? selection.getTopNodes() : selection.getNodes();
}
componentDidUpdate() {
// this.hoveringCapture.setBoundary(this.outline);
// this.willBind();
}
render() {
const { selecting } = this;
if (!selecting || selecting.length < 1) {
// DIRTY FIX, recore has a bug!
return ;
}
// const componentMeta = selecting[0].componentMeta;
// const metadata = componentMeta.getMetadata();
return (
{selecting.map((node) => (
))}
);
}
}
@observer
export class BoxResizingForNode extends Component<{ host: BuiltinSimulatorHost; node: Node }> {
static contextType = SimulatorContext;
get host(): BuiltinSimulatorHost {
return this.props.host;
}
get dragging(): boolean {
return this.host.designer.dragon.dragging;
}
@computed get instances() {
return this.host.getComponentInstances(this.props.node);
}
render() {
const { instances } = this;
const { node } = this.props;
const { designer } = this.host;
if (!instances || instances.length < 1 || this.dragging) {
return null;
}
return (
{instances.map((instance: any) => {
const observed = designer.createOffsetObserver({
node,
instance,
});
if (!observed) {
return null;
}
return (
);
})}
);
}
}
@observer
export class BoxResizingInstance extends Component<{
observed: OffsetObserver;
highlight?: boolean;
dragging?: boolean;
designer?: Designer;
}> {
// private outline: any;
private willUnbind: () => any;
// outline of eight direction
private outlineN: any;
private outlineE: any;
private outlineS: any;
private outlineW: any;
private outlineNE: any;
private outlineNW: any;
private outlineSE: any;
private outlineSW: any;
private dragEngine: DragResizeEngine;
constructor(props: any) {
super(props);
this.dragEngine = new DragResizeEngine(props.designer);
}
componentWillUnmount() {
if (this.willUnbind) {
this.willUnbind();
}
this.props.observed.purge();
}
componentDidMount() {
// this.hoveringCapture.setBoundary(this.outline);
this.willBind();
const resize = (e: MouseEvent, direction: string, node: INode, moveX: number, moveY: number) => {
const { advanced } = node.componentMeta;
if (
advanced.callbacks &&
typeof advanced.callbacks.onResize === 'function'
) {
(e as any).trigger = direction;
(e as any).deltaX = moveX;
(e as any).deltaY = moveY;
const cbNode = node?.isNode ? node.internalToShellNode() : node;
advanced.callbacks.onResize(e, cbNode);
}
};
const resizeStart = (e: MouseEvent, direction: string, node: INode) => {
const { advanced } = node.componentMeta;
if (
advanced.callbacks &&
typeof advanced.callbacks.onResizeStart === 'function'
) {
(e as any).trigger = direction;
const cbNode = node?.isNode ? node.internalToShellNode() : node;
advanced.callbacks.onResizeStart(e, cbNode);
}
};
const resizeEnd = (e: MouseEvent, direction: string, node: INode) => {
const { advanced } = node.componentMeta;
if (
advanced.callbacks &&
typeof advanced.callbacks.onResizeEnd === 'function'
) {
(e as any).trigger = direction;
const cbNode = node?.isNode ? node.internalToShellNode() : node;
advanced.callbacks.onResizeEnd(e, cbNode);
}
const editor = node.document?.designer.editor;
const npm = node?.componentMeta?.npm;
const selected =
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||
node?.componentMeta?.componentName ||
'';
editor?.eventBus.emit('designer.border.resize', {
selected,
layout: node?.parent?.getPropValue('layout') || '',
});
};
this.dragEngine.onResize(resize);
this.dragEngine.onResizeStart(resizeStart);
this.dragEngine.onResizeEnd(resizeEnd);
}
willBind() {
if (this.willUnbind) {
this.willUnbind();
}
if (
!this.outlineN &&
!this.outlineE &&
!this.outlineS &&
!this.outlineW &&
!this.outlineNE &&
!this.outlineNW &&
!this.outlineSE &&
!this.outlineSW
) {
return;
}
const unBind: any[] = [];
const { node } = this.props.observed;
unBind.push(
...[
this.dragEngine.from(this.outlineN, 'n', () => node),
this.dragEngine.from(this.outlineE, 'e', () => node),
this.dragEngine.from(this.outlineS, 's', () => node),
this.dragEngine.from(this.outlineW, 'w', () => node),
this.dragEngine.from(this.outlineNE, 'ne', () => node),
this.dragEngine.from(this.outlineNW, 'nw', () => node),
this.dragEngine.from(this.outlineSE, 'se', () => node),
this.dragEngine.from(this.outlineSW, 'sw', () => node),
],
);
this.willUnbind = () => {
if (unBind && unBind.length > 0) {
unBind.forEach((item) => {
item();
});
}
this.willUnbind = () => {};
};
}
render() {
const { observed } = this.props;
let triggerVisible: any = [];
let offsetWidth = 0;
let offsetHeight = 0;
let offsetTop = 0;
let offsetLeft = 0;
if (observed.hasOffset) {
offsetWidth = observed.offsetWidth;
offsetHeight = observed.offsetHeight;
offsetTop = observed.offsetTop;
offsetLeft = observed.offsetLeft;
const { node } = observed;
const metadata = node.componentMeta.getMetadata();
if (metadata.configure?.advanced?.getResizingHandlers) {
triggerVisible = metadata.configure.advanced.getResizingHandlers(node.internalToShellNode());
}
}
triggerVisible = normalizeTriggers(triggerVisible);
const baseSideClass = 'lc-borders lc-resize-side';
const baseCornerClass = 'lc-borders lc-resize-corner';
return (
{
this.outlineN = ref;
}}
className={classNames(baseSideClass, 'n')}
style={{
height: 20,
transform: `translate(${offsetLeft}px, ${offsetTop - 10}px)`,
width: offsetWidth,
display: triggerVisible.includes('N') ? 'flex' : 'none',
}}
/>
{
this.outlineNE = ref;
}}
className={classNames(baseCornerClass, 'ne')}
style={{
transform: `translate(${offsetLeft + offsetWidth - 5}px, ${offsetTop - 3}px)`,
cursor: 'nesw-resize',
display: triggerVisible.includes('NE') ? 'flex' : 'none',
}}
/>
{
this.outlineE = ref;
}}
style={{
height: offsetHeight,
transform: `translate(${offsetLeft + offsetWidth - 10}px, ${offsetTop}px)`,
width: 20,
display: triggerVisible.includes('E') ? 'flex' : 'none',
}}
/>
{
this.outlineSE = ref;
}}
className={classNames(baseCornerClass, 'se')}
style={{
transform: `translate(${offsetLeft + offsetWidth - 5}px, ${
offsetTop + offsetHeight - 5
}px)`,
cursor: 'nwse-resize',
display: triggerVisible.includes('SE') ? 'flex' : 'none',
}}
/>
{
this.outlineS = ref;
}}
className={classNames(baseSideClass, 's')}
style={{
height: 20,
transform: `translate(${offsetLeft}px, ${offsetTop + offsetHeight - 10}px)`,
width: offsetWidth,
display: triggerVisible.includes('S') ? 'flex' : 'none',
}}
/>
{
this.outlineSW = ref;
}}
className={classNames(baseCornerClass, 'sw')}
style={{
transform: `translate(${offsetLeft - 3}px, ${offsetTop + offsetHeight - 5}px)`,
cursor: 'nesw-resize',
display: triggerVisible.includes('SW') ? 'flex' : 'none',
}}
/>
{
this.outlineW = ref;
}}
className={classNames(baseSideClass, 'w')}
style={{
height: offsetHeight,
transform: `translate(${offsetLeft - 10}px, ${offsetTop}px)`,
width: 20,
display: triggerVisible.includes('W') ? 'flex' : 'none',
}}
/>
{
this.outlineNW = ref;
}}
className={classNames(baseCornerClass, 'nw')}
style={{
transform: `translate(${offsetLeft - 3}px, ${offsetTop - 3}px)`,
cursor: 'nwse-resize',
display: triggerVisible.includes('NW') ? 'flex' : 'none',
}}
/>
);
}
}
================================================
FILE: packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx
================================================
import {
Component,
Fragment,
ReactNodeArray,
isValidElement,
cloneElement,
createElement,
ReactNode,
ComponentType,
} from 'react';
import classNames from 'classnames';
import { observer, computed, Tip, engineConfig } from '@alilc/lowcode-editor-core';
import { createIcon, isReactComponent, isActionContentObject } from '@alilc/lowcode-utils';
import { IPublicTypeActionContentObject } from '@alilc/lowcode-types';
import { BuiltinSimulatorHost } from '../host';
import { INode, OffsetObserver } from '../../designer';
import NodeSelector from '../node-selector';
import { ISimulatorHost } from '../../simulator';
@observer
export class BorderSelectingInstance extends Component<{
observed: OffsetObserver;
highlight?: boolean;
dragging?: boolean;
}> {
componentWillUnmount() {
this.props.observed.purge();
}
render() {
const { observed, highlight, dragging } = this.props;
if (!observed.hasOffset) {
return null;
}
const { offsetWidth, offsetHeight, offsetTop, offsetLeft } = observed;
const style = {
width: offsetWidth,
height: offsetHeight,
transform: `translate3d(${offsetLeft}px, ${offsetTop}px, 0)`,
};
const className = classNames('lc-borders lc-borders-selecting', {
highlight,
dragging,
});
const { hideSelectTools } = observed.node.componentMeta.advanced;
const hideComponentAction = engineConfig.get('hideComponentAction');
if (hideSelectTools) {
return null;
}
return (
{(!dragging && !hideComponentAction) ? : null}
);
}
}
@observer
class Toolbar extends Component<{ observed: OffsetObserver }> {
render() {
const { observed } = this.props;
const { height, width } = observed.viewport;
const BAR_HEIGHT = 20;
const MARGIN = 1;
const BORDER = 2;
const SPACE_HEIGHT = BAR_HEIGHT + MARGIN + BORDER;
const SPACE_MINIMUM_WIDTH = 160; // magic number,大致是 toolbar 的宽度
let style: any;
// 计算 toolbar 的上/下位置
if (observed.top > SPACE_HEIGHT) {
style = {
top: -SPACE_HEIGHT,
height: BAR_HEIGHT,
};
} else if (observed.bottom + SPACE_HEIGHT < height) {
style = {
bottom: -SPACE_HEIGHT,
height: BAR_HEIGHT,
};
} else {
style = {
height: BAR_HEIGHT,
top: Math.max(MARGIN, MARGIN - observed.top),
};
}
// 计算 toolbar 的左/右位置
if (SPACE_MINIMUM_WIDTH > observed.left + observed.width) {
style.left = Math.max(-BORDER, observed.left - width - BORDER);
} else {
style.right = Math.max(-BORDER, observed.right - width - BORDER);
style.justifyContent = 'flex-start';
}
const { node } = observed;
const actions: ReactNodeArray = [];
node.componentMeta.availableActions.forEach((action) => {
const { important = true, condition, content, name } = action;
if (node.isSlot() && (name === 'copy' || name === 'remove')) {
// FIXME: need this?
return;
}
if (important && (typeof condition === 'function' ? condition(node) !== false : condition !== false)) {
actions.push(createAction(content, name, node));
}
});
return (
{actions}
);
}
}
function createAction(content: ReactNode | ComponentType
| IPublicTypeActionContentObject, key: string, node: INode) {
if (isValidElement<{ key: string; node: INode }>(content)) {
return cloneElement(content, { key, node });
}
if (isReactComponent(content)) {
return createElement(content, { key, node });
}
if (isActionContentObject(content)) {
const { action, title, icon } = content;
return (
{
action && action(node.internalToShellNode()!);
const editor = node.document?.designer.editor;
const npm = node?.componentMeta?.npm;
const selected =
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||
node?.componentMeta?.componentName ||
'';
editor?.eventBus.emit('designer.border.action', {
name: key,
selected,
});
}}
>
{icon && createIcon(icon, { key, node: node.internalToShellNode() })}
{title}
);
}
return null;
}
@observer
export class BorderSelectingForNode extends Component<{ host: ISimulatorHost; node: INode }> {
get host(): ISimulatorHost {
return this.props.host;
}
get dragging(): boolean {
return this.host.designer.dragon.dragging;
}
@computed get instances() {
return this.host.getComponentInstances(this.props.node);
}
render() {
const { instances } = this;
const { node } = this.props;
const { designer } = this.host;
if (!instances || instances.length < 1) {
return null;
}
return (
{instances.map((instance) => {
const observed = designer.createOffsetObserver({
node,
instance,
});
if (!observed) {
return null;
}
return ;
})}
);
}
}
@observer
export class BorderSelecting extends Component<{ host: BuiltinSimulatorHost }> {
get host(): BuiltinSimulatorHost {
return this.props.host;
}
get dragging(): boolean {
return this.host.designer.dragon.dragging;
}
@computed get selecting() {
const doc = this.host.currentDocument;
if (!doc || doc.suspensed || this.host.liveEditing.editing) {
return null;
}
const { selection } = doc;
return this.dragging ? selection.getTopNodes() : selection.getNodes();
}
render() {
const { selecting } = this;
if (!selecting || selecting.length < 1) {
return null;
}
return (
{selecting.map((node) => (
))}
);
}
}
================================================
FILE: packages/designer/src/builtin-simulator/bem-tools/borders.less
================================================
@scope: lc-borders;
.@{scope} {
box-sizing: border-box;
pointer-events: none;
position: absolute;
top: 0;
left: 0;
z-index: 1;
border: 1px solid var(--color-brand-light);
will-change: transform, width, height;
overflow: visible;
& > &-title {
color: var(--color-brand-light);
transform: translateY(-100%);
font-weight: lighter;
}
& > &-status {
margin-left: 5px;
color: var(--color-text, #3c3c3c);
transform: translateY(-100%);
font-weight: lighter;
}
& > &-actions {
position: absolute;
display: flex;
flex-direction: row-reverse;
align-items: stretch;
justify-content: flex-end;
pointer-events: all;
> * {
flex-shrink: 0;
}
}
&-action,
.ve-icon-button.ve-action-save {
box-sizing: border-box;
cursor: pointer;
height: 20px;
width: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--color-brand, #006cff);
opacity: 1;
max-height: 100%;
overflow: hidden;
color: var(--color-icon-reverse, white);
&:hover {
background: var(--color-brand-light, #006cff);
}
}
&.lc-resize-corner {
display: inline-block;
width: 8px;
height: 8px;
border: 1px solid var(--color-brand, #197aff);
background: var(--color-block-background-normal, #fff);
pointer-events: auto;
z-index: 2;
}
&.lc-resize-side {
border-width: 0;
z-index: 1;
pointer-events: auto;
align-items: center;
justify-content: center;
display: flex;
&:after {
content: "";
display: block;
border: 1px solid var(--color-brand, #197aff);
background: var(--color-block-background-normal, #fff);
border-radius: 2px;
}
&.e,
&.w {
cursor: ew-resize;
&:after {
width: 4px;
min-height: 50%;
}
}
&.n,
&.s {
cursor: ns-resize;
&:after {
min-width: 50%;
height: 4px;
}
}
}
&&-detecting {
z-index: 1;
border-style: dashed;
background: var(--color-canvas-detecting-background, rgba(0,121,242,.04));
}
&&-selecting {
z-index: 2;
border-width: 2px;
&.dragging {
background: var(--color-layer-mask-background, rgba(182, 178, 178, 0.8));
border: none;
}
}
}
================================================
FILE: packages/designer/src/builtin-simulator/bem-tools/drag-resize-engine.ts
================================================
import { ISimulatorHost } from '../../simulator';
import { Designer, Point } from '../../designer';
import { cursor } from '@alilc/lowcode-utils';
import { makeEventsHandler } from '../../utils/misc';
import { createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core';
// 拖动缩放
export default class DragResizeEngine {
private emitter: IEventBus;
private dragResizing = false;
private designer: Designer;
constructor(designer: Designer) {
this.designer = designer;
this.emitter = createModuleEventBus('DragResizeEngine');
}
isDragResizing() {
return this.dragResizing;
}
/**
* drag reszie from
* @param shell
* @param direction n/s/e/w
* @param boost (e: MouseEvent) => VE.Node
*/
from(shell: Element, direction: string, boost: (e: MouseEvent) => any) {
let node: any;
let startEvent: Point;
if (!shell) {
return () => {};
}
const move = (e: MouseEvent) => {
const x = createResizeEvent(e);
const moveX = x.clientX - startEvent.clientX;
const moveY = x.clientY - startEvent.clientY;
this.emitter.emit('resize', e, direction, node, moveX, moveY);
};
const masterSensors = this.getMasterSensors();
/* istanbul ignore next */
const createResizeEvent = (e: MouseEvent | DragEvent): Point => {
const sourceDocument = e.view?.document;
if (!sourceDocument || sourceDocument === document) {
return e;
}
const srcSim = masterSensors.find(sim => sim.contentDocument === sourceDocument);
if (srcSim) {
return srcSim.viewport.toGlobalPoint(e);
}
return e;
};
const over = (e: MouseEvent) => {
const handleEvents = makeEventsHandler(e, masterSensors);
handleEvents(doc => {
doc.removeEventListener('mousemove', move, true);
doc.removeEventListener('mouseup', over, true);
});
this.dragResizing = false;
this.designer.detecting.enable = true;
cursor.release();
this.emitter.emit('resizeEnd', e, direction, node);
};
const mousedown = (e: MouseEvent) => {
node = boost(e);
startEvent = createResizeEvent(e);
const handleEvents = makeEventsHandler(e, masterSensors);
handleEvents(doc => {
doc.addEventListener('mousemove', move, true);
doc.addEventListener('mouseup', over, true);
});
this.emitter.emit('resizeStart', e, direction, node);
this.dragResizing = true;
this.designer.detecting.enable = false;
cursor.addState('ew-resize');
};
shell.addEventListener('mousedown', mousedown);
return () => {
shell.removeEventListener('mousedown', mousedown);
};
}
onResizeStart(func: (e: MouseEvent, direction: string, node: any) => any) {
this.emitter.on('resizeStart', func);
return () => {
this.emitter.removeListener('resizeStart', func);
};
}
onResize(
func: (e: MouseEvent, direction: string, node: any, moveX: number, moveY: number) => any,
) {
this.emitter.on('resize', func);
return () => {
this.emitter.removeListener('resize', func);
};
}
onResizeEnd(func: (e: MouseEvent, direction: string, node: any) => any) {
this.emitter.on('resizeEnd', func);
return () => {
this.emitter.removeListener('resizeEnd', func);
};
}
private getMasterSensors(): ISimulatorHost[] {
return this.designer.project.documents
.map(doc => {
if (doc.active && doc.simulator?.sensorAvailable) {
return doc.simulator;
}
return null;
})
.filter(Boolean) as any;
}
}
// new DragResizeEngine();
================================================
FILE: packages/designer/src/builtin-simulator/bem-tools/index.tsx
================================================
import React, { Component } from 'react';
import { observer, engineConfig } from '@alilc/lowcode-editor-core';
import { BorderDetecting } from './border-detecting';
import { BorderContainer } from './border-container';
import { BuiltinSimulatorHost } from '../host';
import { BorderSelecting } from './border-selecting';
import BorderResizing from './border-resizing';
import { InsertionView } from './insertion';
import './bem-tools.less';
import './borders.less';
@observer
export class BemTools extends Component<{ host: BuiltinSimulatorHost }> {
render() {
const { host } = this.props;
const { designMode } = host;
const { scrollX, scrollY, scale } = host.viewport;
if (designMode === 'live') {
return null;
}
return (
{ !engineConfig.get('disableDetecting') && }
{ engineConfig.get('enableReactiveContainer') && }
{
host.designer.bemToolsManager.getAllBemTools().map(tools => {
const ToolsCls = tools.item;
return ;
})
}
);
}
}
================================================
FILE: packages/designer/src/builtin-simulator/bem-tools/insertion.less
================================================
.lc-insertion {
position: absolute;
top: -2px;
left: 0;
z-index: 12;
pointer-events: none !important;
background-color: var(--color-brand-light);
height: 4px;
&.cover {
top: 0;
height: auto;
width: auto;
border: none;
opacity: 0.3;
}
&.vertical {
top: 0;
left: -2px;
width: 4px;
height: auto;
}
&.invalid {
background-color: var(--color-error, var(--color-function-error, red));
}
}
================================================
FILE: packages/designer/src/builtin-simulator/bem-tools/insertion.tsx
================================================
import { Component } from 'react';
import { observer } from '@alilc/lowcode-editor-core';
import { BuiltinSimulatorHost } from '../host';
import {
DropLocation,
isVertical,
} from '../../designer';
import { ISimulatorHost } from '../../simulator';
import { INode } from '../../document';
import './insertion.less';
import { IPublicTypeNodeData, IPublicTypeNodeSchema, IPublicTypeLocationChildrenDetail, IPublicTypeRect } from '@alilc/lowcode-types';
import { isLocationChildrenDetail } from '@alilc/lowcode-utils';
interface InsertionData {
edge?: DOMRect;
insertType?: string;
vertical?: boolean;
nearRect?: IPublicTypeRect;
coverRect?: DOMRect;
nearNode?: IPublicTypeNodeData;
}
/**
* 处理拖拽子节点(INode)情况
*/
function processChildrenDetail(sim: ISimulatorHost, container: INode, detail: IPublicTypeLocationChildrenDetail): InsertionData {
let edge = detail.edge || null;
if (!edge) {
edge = sim.computeRect(container);
if (!edge) {
return {};
}
}
const ret: any = {
edge,
insertType: 'before',
};
if (detail.near) {
const { node, pos, rect, align } = detail.near;
ret.nearRect = rect || sim.computeRect(node);
ret.nearNode = node;
if (pos === 'replace') {
// FIXME: ret.nearRect mybe null
ret.coverRect = ret.nearRect;
ret.insertType = 'cover';
} else if (!ret.nearRect || (ret.nearRect.width === 0 && ret.nearRect.height === 0)) {
ret.nearRect = ret.edge;
ret.insertType = 'after';
ret.vertical = isVertical(ret.nearRect);
} else {
ret.insertType = pos;
ret.vertical = align ? align === 'V' : isVertical(ret.nearRect);
}
return ret;
}
// from outline-tree: has index, but no near
// TODO: think of shadowNode & ConditionFlow
const { index } = detail;
if (index == null) {
ret.coverRect = ret.edge;
ret.insertType = 'cover';
return ret;
}
let nearNode = container.children.get(index);
if (!nearNode) {
// index = 0, eg. nochild,
nearNode = container.children.get(index > 0 ? index - 1 : 0);
if (!nearNode) {
ret.insertType = 'cover';
ret.coverRect = edge;
return ret;
}
ret.insertType = 'after';
}
if (nearNode) {
ret.nearRect = sim.computeRect(nearNode);
if (!ret.nearRect || (ret.nearRect.width === 0 && ret.nearRect.height === 0)) {
ret.nearRect = ret.edge;
ret.insertType = 'after';
}
ret.vertical = isVertical(ret.nearRect);
ret.nearNode = nearNode;
} else {
ret.insertType = 'cover';
ret.coverRect = edge;
}
return ret;
}
/**
* 将 detail 信息转换为页面"坐标"信息
*/
function processDetail({ target, detail, document }: DropLocation): InsertionData {
const sim = document.simulator;
if (!sim) {
return {};
}
if (isLocationChildrenDetail(detail)) {
return processChildrenDetail(sim, target, detail);
} else {
// TODO: others...
const instances = sim.getComponentInstances(target);
if (!instances) {
return {};
}
const edge = sim.computeComponentInstanceRect(instances[0], target.componentMeta.rootSelector);
return edge ? { edge, insertType: 'cover', coverRect: edge } : {};
}
}
@observer
export class InsertionView extends Component<{ host: BuiltinSimulatorHost }> {
render() {
const { host } = this.props;
const loc = host.currentDocument?.dropLocation;
if (!loc) {
return null;
}
// 如果是个绝对定位容器,不需要渲染插入标记
if (loc.target?.componentMeta?.advanced.isAbsoluteLayoutContainer) {
return null;
}
const { scale, scrollX, scrollY } = host.viewport;
const { edge, insertType, coverRect, nearRect, vertical, nearNode } = processDetail(loc);
if (!edge) {
return null;
}
let className = 'lc-insertion';
if ((loc.detail as any)?.valid === false) {
className += ' invalid';
}
const style: any = {};
let x: number;
let y: number;
if (insertType === 'cover') {
className += ' cover';
x = (coverRect!.left + scrollX) * scale;
y = (coverRect!.top + scrollY) * scale;
style.width = coverRect!.width * scale;
style.height = coverRect!.height * scale;
} else {
if (!nearRect) {
return null;
}
if (vertical) {
className += ' vertical';
x = ((insertType === 'before' ? nearRect.left : nearRect.right) + scrollX) * scale;
y = (nearRect.top + scrollY) * scale;
style.height = nearRect!.height * scale;
} else {
x = (nearRect.left + scrollX) * scale;
y = ((insertType === 'before' ? nearRect.top : nearRect.bottom) + scrollY) * scale;
style.width = nearRect.width * scale;
}
if (y === 0 && (nearNode as IPublicTypeNodeSchema)?.componentMeta?.isTopFixed) {
return null;
}
}
style.transform = `translate3d(${x}px, ${y}px, 0)`;
// style.transition = 'all 0.2s ease-in-out';
return ;
}
}
================================================
FILE: packages/designer/src/builtin-simulator/bem-tools/manager.ts
================================================
import { ComponentType } from 'react';
import { Designer } from '../../designer';
import { invariant } from '../../utils';
import { BuiltinSimulatorHost } from '../../builtin-simulator/host';
export type BemToolsData = {
name: string;
item: ComponentType<{ host: BuiltinSimulatorHost }>;
};
export class BemToolsManager {
private designer: Designer;
private toolsContainer: BemToolsData[] = [];
constructor(designer: Designer) {
this.designer = designer;
}
addBemTools(toolsData: BemToolsData) {
const found = this.toolsContainer.find(item => item.name === toolsData.name);
invariant(!found, `${toolsData.name} already exists`);
this.toolsContainer.push(toolsData);
}
removeBemTools(name: string) {
const index = this.toolsContainer.findIndex(item => item.name === name);
if (index !== -1) {
this.toolsContainer.splice(index, 1);
}
}
getAllBemTools() {
return this.toolsContainer;
}
}
================================================
FILE: packages/designer/src/builtin-simulator/live-editing/live-editing.ts
================================================
import { obx } from '@alilc/lowcode-editor-core';
import { IPublicTypePluginConfig, IPublicTypeLiveTextEditingConfig } from '@alilc/lowcode-types';
import { INode, Prop } from '../../document';
const EDITOR_KEY = 'data-setter-prop';
function getSetterPropElement(ele: HTMLElement, root: HTMLElement): HTMLElement | null {
const box = ele.closest(`[${EDITOR_KEY}]`);
if (!box || !root.contains(box)) {
return null;
}
return box as HTMLElement;
}
function defaultSaveContent(content: string, prop: Prop) {
prop.setValue(content);
}
export interface EditingTarget {
node: INode;
rootElement: HTMLElement;
event: MouseEvent;
}
let saveHandlers: SaveHandler[] = [];
function addLiveEditingSaveHandler(handler: SaveHandler) {
saveHandlers.push(handler);
}
function clearLiveEditingSaveHandler() {
saveHandlers = [];
}
let specificRules: SpecificRule[] = [];
function addLiveEditingSpecificRule(rule: SpecificRule) {
specificRules.push(rule);
}
function clearLiveEditingSpecificRule() {
specificRules = [];
}
export class LiveEditing {
static addLiveEditingSpecificRule = addLiveEditingSpecificRule;
static clearLiveEditingSpecificRule = clearLiveEditingSpecificRule;
static addLiveEditingSaveHandler = addLiveEditingSaveHandler;
static clearLiveEditingSaveHandler = clearLiveEditingSaveHandler;
@obx.ref private _editing: Prop | null = null;
private _dispose?: () => void;
private _save?: () => void;
apply(target: EditingTarget) {
const { node, event, rootElement } = target;
const targetElement = event.target as HTMLElement;
const { liveTextEditing } = node.componentMeta;
const editor = node.document?.designer.editor;
const npm = node?.componentMeta?.npm;
const selected =
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') || node?.componentMeta?.componentName || '';
editor?.eventBus.emit('designer.builtinSimulator.liveEditing', {
selected,
});
let setterPropElement = getSetterPropElement(targetElement, rootElement);
let propTarget = setterPropElement?.dataset.setterProp;
let matched: (IPublicTypePluginConfig & { propElement?: HTMLElement }) | undefined | null;
if (liveTextEditing) {
if (propTarget) {
// 已埋点命中 data-setter-prop="proptarget", 从 liveTextEditing 读取配置(mode|onSaveContent)
matched = liveTextEditing.find(config => config.propTarget == propTarget);
} else {
// 执行 embedTextEditing selector 规则,获得第一个节点 是否 contains e.target,若匹配,读取配置
matched = liveTextEditing.find(config => {
if (!config.selector) {
return false;
}
setterPropElement = queryPropElement(rootElement, targetElement, config.selector);
return !!setterPropElement;
});
propTarget = matched?.propTarget;
}
} else {
specificRules.some((rule) => {
matched = rule(target);
return !!matched;
});
if (matched) {
propTarget = matched.propTarget;
setterPropElement = matched.propElement || queryPropElement(rootElement, targetElement, matched.selector);
}
}
// if (!propTarget) {
// // 自动纯文本编辑满足一下情况:
// // 1. children 内容都是 Leaf 且都是文本(一期)
// // 2. DOM 节点是单层容器,子集都是文本节点 (已满足)
// const isAllText = node.children?.every(item => {
// return item.isLeaf() && item.getProp('children')?.type === 'literal';
// });
// // TODO:
// }
if (propTarget && setterPropElement) {
const prop = node.getProp(propTarget, true)!;
if (this._editing === prop) {
return;
}
// 进入编辑
// 1. 设置 contentEditable="plaintext|..."
// 2. 添加类名
// 3. focus & cursor locate
// 4. 监听 blur 事件
// 5. 设置编辑锁定:disable hover | disable select | disable canvas drag
const onSaveContent = matched?.onSaveContent || saveHandlers.find(item => item.condition(prop))?.onSaveContent || defaultSaveContent;
setterPropElement.setAttribute('contenteditable', matched?.mode && matched.mode !== 'plaintext' ? 'true' : 'plaintext-only');
setterPropElement.classList.add('engine-live-editing');
// be sure
setterPropElement.focus();
setCaret(event);
this._save = () => {
onSaveContent(setterPropElement!.innerText, prop);
};
const keydown = (e: KeyboardEvent) => {
console.info(e.code);
switch (e.code) {
case 'Enter':
break;
// TODO: check is richtext?
case 'Escape':
break;
case 'Tab':
setterPropElement?.blur();
}
// esc
// enter
// tab
};
const focusout = (/* e: FocusEvent */) => {
this.saveAndDispose();
};
setterPropElement.addEventListener('focusout', focusout);
setterPropElement.addEventListener('keydown', keydown, true);
this._dispose = () => {
setterPropElement!.classList.remove('engine-live-editing');
setterPropElement!.removeAttribute('contenteditable');
setterPropElement!.removeEventListener('focusout', focusout);
setterPropElement!.removeEventListener('keydown', keydown, true);
};
this._editing = prop;
}
// TODO: process enter | esc events & joint the FocusTracker
// TODO: upward testing for b/i/a html elements
}
get editing() {
return this._editing;
}
saveAndDispose() {
if (this._save) {
this._save();
this._save = undefined;
}
this.dispose();
}
dispose() {
if (this._dispose) {
this._dispose();
this._dispose = undefined;
}
this._editing = null;
}
}
export type SpecificRule = (target: EditingTarget) => (IPublicTypeLiveTextEditingConfig & {
propElement?: HTMLElement;
}) | null;
export interface SaveHandler {
condition: (prop: Prop) => boolean;
onSaveContent: (content: string, prop: Prop) => void;
}
function setCaret(event: MouseEvent) {
const doc = event.view?.document;
if (!doc) return;
const range = doc.caretRangeFromPoint(event.clientX, event.clientY);
if (range) {
selectRange(doc, range);
setTimeout(() => selectRange(doc, range), 1);
}
}
function selectRange(doc: Document, range: Range) {
const selection = doc.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}
}
function queryPropElement(rootElement: HTMLElement, targetElement: HTMLElement, selector?: string) {
if (!selector) {
return null;
}
let propElement = selector === ':root' ? rootElement : rootElement.querySelector(selector);
if (!propElement) {
return null;
}
if (!propElement.contains(targetElement)) {
// try selectorAll
propElement = Array.from(rootElement.querySelectorAll(selector)).find(item => item.contains(targetElement)) as HTMLElement;
if (!propElement) {
return null;
}
}
return propElement as HTMLElement;
}
================================================
FILE: packages/designer/src/builtin-simulator/node-selector/index.less
================================================
@import '../../less-variables.less';
// 样式直接沿用之前的样式,优化了下命名
.instance-node-selector {
position: relative;
margin-right: 2px;
color: var(--color-icon-white, @title-bgcolor);
border-radius: @global-border-radius;
pointer-events: auto;
flex-grow: 0;
flex-shrink: 0;
svg {
width: 16px;
height: 16px;
margin-right: 5px;
flex-grow: 0;
flex-shrink: 0;
max-width: inherit;
path {
fill: var(--color-icon-white, @title-bgcolor);
}
}
.instance-node-selector-current {
background: var(--color-brand, @brand-color-1);
padding: 0 6px;
display: flex;
align-items: center;
height: 20px;
cursor: pointer;
color: var(--color-icon-white, @title-bgcolor);
border-radius: 3px;
&-title {
padding-right: 6px;
color: var(--color-icon-white, @title-bgcolor);
}
}
.instance-node-selector-list {
position: absolute;
left: 0;
right: 0;
opacity: 0;
visibility: hidden;
}
.instance-node-selector-node {
height: 20px;
margin-top: 2px;
&-content {
padding-left: 6px;
background: var(--color-layer-tooltip-background, #78869a);
display: inline-flex;
border-radius: 3px;
align-items: center;
height: 20px;
color: var(--color-icon-white, @title-bgcolor);
cursor: pointer;
overflow: visible;
}
&-title {
padding-right: 6px;
// margin-left: 5px;
color: var(--color-icon-white, @title-bgcolor);
cursor: pointer;
overflow: visible;
}
&:hover {
opacity: 0.8;
}
}
&:hover {
.instance-node-selector-current {
color: ar(--color-text-reverse, @white-alpha-2);
}
.instance-node-selector-popup {
visibility: visible;
opacity: 1;
transition: 0.2s all ease-in;
}
}
}
================================================
FILE: packages/designer/src/builtin-simulator/node-selector/index.tsx
================================================
import { Overlay } from '@alifd/next';
import React, { MouseEvent } from 'react';
import { Title, observer } from '@alilc/lowcode-editor-core';
import { canClickNode } from '@alilc/lowcode-utils';
import './index.less';
import { INode } from '@alilc/lowcode-designer';
const { Popup } = Overlay;
export interface IProps {
node: INode;
}
export interface IState {
parentNodes: INode[];
}
type UnionNode = INode | null;
@observer
export default class InstanceNodeSelector extends React.Component {
state: IState = {
parentNodes: [],
};
componentDidMount() {
const parentNodes = this.getParentNodes(this.props.node);
this.setState({
parentNodes: parentNodes ?? [],
});
}
// 获取节点的父级节点(最多获取 5 层)
getParentNodes = (node: INode) => {
const parentNodes: any[] = [];
const focusNode = node.document?.focusNode;
if (!focusNode) {
return null;
}
if (node.contains(focusNode) || !focusNode.contains(node)) {
return parentNodes;
}
let currentNode: UnionNode = node;
while (currentNode && parentNodes.length < 5) {
currentNode = currentNode.getParent();
if (currentNode) {
parentNodes.push(currentNode);
}
if (currentNode === focusNode) {
break;
}
}
return parentNodes;
};
onSelect = (node: INode) => (event: MouseEvent) => {
if (!node) {
return;
}
const canClick = canClickNode(node.internalToShellNode()!, event);
if (canClick && typeof node.select === 'function') {
node.select();
const editor = node.document?.designer.editor;
const npm = node?.componentMeta?.npm;
const selected =
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||
node?.componentMeta?.componentName ||
'';
editor?.eventBus.emit('designer.border.action', {
name: 'select',
selected,
});
}
};
onMouseOver = (node: INode) => (_: any, flag = true) => {
if (node && typeof node.hover === 'function') {
node.hover(flag);
}
};
onMouseOut = (node: INode) => (_: any, flag = false) => {
if (node && typeof node.hover === 'function') {
node.hover(flag);
}
};
renderNodes = () => {
const nodes = this.state.parentNodes;
if (!nodes || nodes.length < 1) {
return null;
}
const children = nodes.map((node, key) => {
return (
);
});
return children;
};
render() {
const { node } = this.props;
return (
}
triggerType="hover"
offset={[0, 0]}
>
{this.renderNodes()}
);
}
}
================================================
FILE: packages/designer/src/builtin-simulator/utils/clickable.ts
================================================
import { getClosestNode, canClickNode } from '@alilc/lowcode-utils';
import { INode } from '../../document';
/**
* 获取离当前节点最近的可点击节点
* @param currentNode
* @param event
*/
export const getClosestClickableNode = (
currentNode: INode | undefined | null,
event: MouseEvent,
) => {
let node = currentNode;
while (node) {
// 判断当前节点是否可点击
let canClick = canClickNode(node, event);
// eslint-disable-next-line no-loop-func
const lockedNode = getClosestNode(node!, (n) => {
// 假如当前节点就是 locked 状态,要从当前节点的父节点开始查找
return !!(node?.isLocked ? n.parent?.isLocked : n.isLocked);
});
if (lockedNode && lockedNode.getId() !== node.getId()) {
canClick = false;
}
if (canClick) {
break;
}
// 对于不可点击的节点,继续向上找
node = node.parent;
}
return node;
};
================================================
FILE: packages/designer/src/builtin-simulator/utils/parse-metadata.ts
================================================
import PropTypes from 'prop-types';
import { isValidElement } from 'react';
import { isElement } from '@alilc/lowcode-utils';
import { IPublicTypePropConfig } from '@alilc/lowcode-types';
export const primitiveTypes = [
'string',
'number',
'array',
'bool',
'func',
'object',
'node',
'element',
'symbol',
'any',
];
interface LowcodeCheckType {
// isRequired, props, propName, componentName, location, propFullName, secret
(props: any, propName: string, componentName: string, ...rest: any[]): Error | null;
// (...reset: any[]): Error | null;
isRequired?: LowcodeCheckType;
type?: string | object;
}
// eslint-disable-next-line @typescript-eslint/ban-types
function makeRequired(propType: any, lowcodeType: string | object): LowcodeCheckType {
function lowcodeCheckTypeIsRequired(...rest: any[]) {
return propType.isRequired(...rest);
}
if (typeof lowcodeType === 'string') {
lowcodeType = {
type: lowcodeType,
};
}
lowcodeCheckTypeIsRequired.lowcodeType = {
...lowcodeType,
isRequired: true,
};
return lowcodeCheckTypeIsRequired;
}
// eslint-disable-next-line @typescript-eslint/ban-types
function define(propType: any = PropTypes.any, lowcodeType: string | object = {}): LowcodeCheckType {
if (!propType._inner && propType.name !== 'lowcodeCheckType') {
propType.lowcodeType = lowcodeType;
}
function lowcodeCheckType(...rest: any[]) {
return propType(...rest);
}
lowcodeCheckType.lowcodeType = lowcodeType;
lowcodeCheckType.isRequired = makeRequired(propType, lowcodeType);
return lowcodeCheckType;
}
export const LowcodeTypes: any = {
...PropTypes,
define,
};
(window as any).PropTypes = LowcodeTypes;
if ((window as any).React) {
(window as any).React.PropTypes = LowcodeTypes;
}
// override primitive type checkers
primitiveTypes.forEach((type) => {
const propType = (PropTypes as any)[type];
if (!propType) {
return;
}
propType._inner = true;
LowcodeTypes[type] = define(propType, type);
});
// You can ensure that your prop is limited to specific values by treating
// it as an enum.
LowcodeTypes.oneOf = (list: any[]) => {
return define(PropTypes.oneOf(list), {
type: 'oneOf',
value: list,
});
};
// An array of a certain type
LowcodeTypes.arrayOf = (type: any) => {
return define(PropTypes.arrayOf(type), {
type: 'arrayOf',
value: type.lowcodeType || 'any',
});
};
// An object with property values of a certain type
LowcodeTypes.objectOf = (type: any) => {
return define(PropTypes.objectOf(type), {
type: 'objectOf',
value: type.lowcodeType || 'any',
});
};
// An object that could be one of many types
LowcodeTypes.oneOfType = (types: any[]) => {
const itemTypes = types.map((type) => type.lowcodeType || 'any');
return define(PropTypes.oneOfType(types), {
type: 'oneOfType',
value: itemTypes,
});
};
// An object with warnings on extra properties
LowcodeTypes.exact = (typesMap: any) => {
const configs = Object.keys(typesMap).map((key) => {
return {
name: key,
propType: typesMap[key]?.lowcodeType || 'any',
};
});
return define(PropTypes.exact(typesMap), {
type: 'exact',
value: configs,
});
};
// An object taking on a particular shape
LowcodeTypes.shape = (typesMap: any = {}) => {
const configs = Object.keys(typesMap).map((key) => {
return {
name: key,
propType: typesMap[key]?.lowcodeType || 'any',
};
});
return define(PropTypes.shape(typesMap), {
type: 'shape',
value: configs,
});
};
const BasicTypes = ['string', 'number', 'object'];
export function parseProps(component: any): IPublicTypePropConfig[] {
if (!component) {
return [];
}
const propTypes = component.propTypes || ({} as any);
const defaultProps = component.defaultProps || ({} as any);
const result: any = {};
if (!propTypes) return [];
Object.keys(propTypes).forEach((key) => {
const propTypeItem = propTypes[key];
const defaultValue = defaultProps[key];
const { lowcodeType } = propTypeItem;
if (lowcodeType) {
result[key] = {
name: key,
propType: lowcodeType,
};
if (defaultValue != null) {
result[key].defaultValue = defaultValue;
}
return;
}
let i = primitiveTypes.length;
while (i-- > 0) {
const k = primitiveTypes[i];
if ((LowcodeTypes as any)[k] === propTypeItem) {
result[key] = {
name: key,
propType: k,
};
if (defaultValue != null) {
result[key].defaultValue = defaultValue;
}
return;
}
}
result[key] = {
name: key,
propType: 'any',
};
if (defaultValue != null) {
result[key].defaultValue = defaultValue;
}
});
Object.keys(defaultProps).forEach((key) => {
if (result[key]) return;
const defaultValue = defaultProps[key];
let type: string = typeof defaultValue;
if (type === 'boolean') {
type = 'bool';
} else if (type === 'function') {
type = 'func';
} else if (type === 'object' && Array.isArray(defaultValue)) {
type = 'array';
} else if (defaultValue && isValidElement(defaultValue)) {
type = 'node';
} else if (defaultValue && isElement(defaultValue)) {
type = 'element';
} else if (!BasicTypes.includes(type)) {
type = 'any';
}
result[key] = {
name: key,
propType: type || 'any',
defaultValue,
};
});
return Object.keys(result).map((key) => result[key]);
}
export function parseMetadata(component: any): any {
return {
props: parseProps(component),
...component.componentMetadata,
};
}
================================================
FILE: packages/designer/src/builtin-simulator/utils/path.ts
================================================
/**
* Check whether a component is external package, e.g. @ali/uxcore
* @param path Component path
*/
export function isPackagePath(path: string): boolean {
return !path.startsWith('.') && !path.startsWith('/');
}
/**
* Title cased string
* @param s original string
*/
export function toTitleCase(s: string): string {
return s
.split(/[-_ .]+/)
.map((token) => token[0].toUpperCase() + token.substring(1))
.join('');
}
/**
* Make up an import name/tag for components
* @param path Original path name
*/
export function generateComponentName(path: string): string {
const parts = path.split('/');
let name = parts.pop();
if (name && /^index\./.test(name)) {
name = parts.pop();
}
return name ? toTitleCase(name) : 'Component';
}
/**
* normalizing import path for easier comparison
*/
export function getNormalizedImportPath(path: string): string {
const segments = path.split('/');
let basename = segments.pop();
if (!basename) {
return path;
}
const ignoredExtensions = ['.ts', '.js', '.tsx', '.jsx'];
const extIndex = basename.lastIndexOf('.');
if (extIndex > -1) {
const ext = basename.slice(extIndex);
if (ignoredExtensions.includes(ext)) {
basename = basename.slice(0, extIndex);
}
}
if (basename !== 'index') {
segments.push(basename);
}
return segments.join('/');
}
/**
* make a relative path
*
* @param toPath abolute path
* @param fromPath absolute path
*/
export function makeRelativePath(toPath: string, fromPath: string) {
// not a absolute path, eg. @ali/uxcore
if (!toPath.startsWith('/')) {
return toPath;
}
const toParts = toPath.split('/');
const fromParts = fromPath.split('/');
// find shared path header
const length = Math.min(fromParts.length, toParts.length);
let sharedUpTo = length;
for (let i = 0; i < length; i++) {
if (fromParts[i] !== toParts[i]) {
sharedUpTo = i;
break;
}
}
// find how many levels to go up from
// minus another 1 since we do not include the final
const numGoUp = fromParts.length - sharedUpTo - 1;
// generate final path
let outputParts = [];
if (numGoUp === 0) {
// in the same dir
outputParts.push('.');
} else {
// needs to go up
for (let i = 0; i < numGoUp; ++i) {
outputParts.push('..');
}
}
outputParts = outputParts.concat(toParts.slice(sharedUpTo));
return outputParts.join('/');
}
function normalizeArray(parts: string[], allowAboveRoot: boolean) {
const res = [];
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
// ignore empty parts
if (!p || p === '.') {
continue;
}
if (p === '..') {
if (res.length && res[res.length - 1] !== '..') {
res.pop();
} else if (allowAboveRoot) {
res.push('..');
}
} else {
res.push(p);
}
}
return res;
}
function normalize(path: string): string {
const isAbsolute = path[0] === '/';
const segments = normalizeArray(path.split('/'), !isAbsolute);
if (isAbsolute) {
segments.unshift('');
} else if (segments.length < 1 || segments[0] !== '..') {
segments.unshift('.');
}
return segments.join('/');
}
/**
* Resolve component with absolute path to relative path
* @param path absolute path of component from project
*/
export function resolveAbsoluatePath(path: string, base: string): string {
if (!path.startsWith('.')) {
// eg. /usr/path/to, @ali/button
return path;
}
path = path.replace(/\\/g, '/');
if (base.slice(-1) !== '/') {
base += '/';
}
return normalize(base + path);
}
export function joinPath(...segments: string[]) {
let path = '';
for (const seg of segments) {
if (seg) {
if (path === '') {
path += seg;
} else {
path += `/${ seg}`;
}
}
}
return normalize(path);
}
export function removeVersion(path: string): string {
if (path.lastIndexOf('@') > 0) {
path = path.replace(/(@?[^@]+)(@[\w.-]+)(.+)/, '$1$3');
}
return path;
}
================================================
FILE: packages/designer/src/builtin-simulator/utils/throttle.ts
================================================
const useRAF = typeof requestAnimationFrame === 'function';
// eslint-disable-next-line @typescript-eslint/ban-types
export function throttle(func: Function, delay: number) {
let lastArgs: any;
let lastThis: any;
let result: any;
let timerId: number | undefined;
let lastCalled: number | undefined;
let lastInvoked = 0;
function invoke(time: number) {
const args = lastArgs;
const thisArg = lastThis;
lastArgs = undefined;
lastThis = undefined;
lastInvoked = time;
result = func.apply(thisArg, args);
return result;
}
function startTimer(pendingFunc: any, wait: number): number {
if (useRAF) {
return requestAnimationFrame(pendingFunc);
}
return setTimeout(pendingFunc, wait) as any;
}
function leadingEdge(time: number) {
lastInvoked = time;
timerId = startTimer(timerExpired, delay);
return invoke(time);
}
function shouldInvoke(time: number) {
const timeSinceLastCalled = time - lastCalled!;
const timeSinceLastInvoked = time - lastInvoked;
return (
lastCalled === undefined ||
timeSinceLastCalled >= delay ||
timeSinceLastCalled < 0 ||
timeSinceLastInvoked >= delay
);
}
function remainingWait(time: number) {
const timeSinceLastCalled = time - lastCalled!;
const timeSinceLastInvoked = time - lastInvoked;
return Math.min(delay - timeSinceLastCalled, delay - timeSinceLastInvoked);
}
function timerExpired() {
const time = Date.now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
timerId = startTimer(timerExpired, remainingWait(time));
}
function trailingEdge(time: number) {
timerId = undefined;
if (lastArgs) {
return invoke(time);
}
lastArgs = undefined;
lastThis = undefined;
return result;
}
function debounced(this: any, ...args: any[]) {
const time = Date.now();
const isInvoking = shouldInvoke(time);
lastArgs = args;
lastThis = this;
lastCalled = time;
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCalled);
}
timerId = startTimer(timerExpired, delay);
return invoke(lastCalled);
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, delay);
}
return result;
}
return debounced;
}
================================================
FILE: packages/designer/src/designer/active-tracker.ts
================================================
import { INode } from '../document/node/node';
import { obx, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
import {
IPublicTypeActiveTarget,
IPublicModelActiveTracker,
} from '@alilc/lowcode-types';
import { isNode } from '@alilc/lowcode-utils';
export interface IActiveTracker extends Omit< IPublicModelActiveTracker, 'track' | 'onChange' > {
_target: ActiveTarget | INode;
track(originalTarget: ActiveTarget | INode): void;
onChange(fn: (target: ActiveTarget) => void): () => void;
}
export interface ActiveTarget extends Omit< IPublicTypeActiveTarget, 'node' > {
node: INode;
}
export class ActiveTracker implements IActiveTracker {
@obx.ref private _target?: ActiveTarget | INode;
private emitter: IEventBus = createModuleEventBus('ActiveTracker');
track(originalTarget: ActiveTarget | INode) {
let target = originalTarget;
if (isNode(originalTarget)) {
target = { node: originalTarget as INode };
}
this._target = target;
this.emitter.emit('change', target);
}
get currentNode() {
return (this._target as ActiveTarget)?.node;
}
get detail() {
return (this._target as ActiveTarget)?.detail;
}
/**
* @deprecated
*/
/* istanbul ignore next */
get intance() {
return this.instance;
}
get instance() {
return (this._target as ActiveTarget)?.instance;
}
onChange(fn: (target: ActiveTarget) => void): () => void {
this.emitter.addListener('change', fn);
return () => {
this.emitter.removeListener('change', fn);
};
}
}
================================================
FILE: packages/designer/src/designer/clipboard.ts
================================================
import { IPublicModelClipboard } from '@alilc/lowcode-types';
function getDataFromPasteEvent(event: ClipboardEvent) {
const { clipboardData } = event;
if (!clipboardData) {
return null;
}
try {
// { componentsMap, componentsTree, ... }
const data = JSON.parse(clipboardData.getData('text/plain'));
if (!data) {
return {};
}
if (data.componentsTree) {
return data;
} else if (data.componentName) {
return {
componentsTree: [data],
};
}
} catch (error) {
// TODO: open the parser implement
return { };
}
}
export interface IClipboard extends IPublicModelClipboard {
initCopyPaster(el: HTMLTextAreaElement): void;
injectCopyPaster(document: Document): void;
}
class Clipboard implements IClipboard {
private copyPasters: HTMLTextAreaElement[] = [];
private waitFn?: (data: any, e: ClipboardEvent) => void;
constructor() {
this.injectCopyPaster(document);
}
isCopyPasteEvent(e: Event) {
this.isCopyPaster(e.target);
}
private isCopyPaster(el: any) {
return this.copyPasters.includes(el);
}
initCopyPaster(el: HTMLTextAreaElement) {
this.copyPasters.push(el);
const onPaste = (e: ClipboardEvent) => {
if (this.waitFn) {
this.waitFn(getDataFromPasteEvent(e), e);
this.waitFn = undefined;
}
el.blur();
};
el.addEventListener('paste', onPaste, false);
return () => {
el.removeEventListener('paste', onPaste, false);
const i = this.copyPasters.indexOf(el);
if (i > -1) {
this.copyPasters.splice(i, 1);
}
};
}
injectCopyPaster(document: Document) {
if (this.copyPasters.find((x) => x.ownerDocument === document)) {
return;
}
const copyPaster = document.createElement<'textarea'>('textarea');
copyPaster.style.cssText = 'position: absolute;left: -9999px;top:-100px';
if (document.body) {
document.body.appendChild(copyPaster);
} else {
document.addEventListener('DOMContentLoaded', () => {
document.body.appendChild(copyPaster);
});
}
const dispose = this.initCopyPaster(copyPaster);
return () => {
dispose();
document.removeChild(copyPaster);
};
}
setData(data: any): void {
const copyPaster = this.copyPasters.find((x) => x.ownerDocument);
if (!copyPaster) {
return;
}
copyPaster.value = typeof data === 'string' ? data : JSON.stringify(data);
copyPaster.select();
copyPaster.ownerDocument!.execCommand('copy');
copyPaster.blur();
}
waitPasteData(keyboardEvent: KeyboardEvent, cb: (data: any, e: ClipboardEvent) => void) {
const win = keyboardEvent.view;
if (!win) {
return;
}
const copyPaster = this.copyPasters.find((cp) => cp.ownerDocument === win.document);
if (copyPaster) {
copyPaster.select();
this.waitFn = cb;
}
}
}
export const clipboard = new Clipboard();
================================================
FILE: packages/designer/src/designer/designer-view.tsx
================================================
import { Component } from 'react';
import classNames from 'classnames';
import BuiltinDragGhostComponent from './drag-ghost';
import { Designer, DesignerProps } from './designer';
import { ProjectView } from '../project';
import './designer.less';
type IProps = DesignerProps & {
designer?: Designer;
};
export class DesignerView extends Component
{
readonly designer: Designer;
readonly viewName: string | undefined;
constructor(props: IProps) {
super(props);
const { designer, ...designerProps } = props;
this.viewName = designer?.viewName;
if (designer) {
this.designer = designer;
designer.setProps(designerProps);
} else {
this.designer = new Designer(designerProps);
}
}
shouldComponentUpdate(nextProps: DesignerProps) {
this.designer.setProps(nextProps);
const { props } = this;
if (
nextProps.className !== props.className ||
nextProps.style !== props.style ||
nextProps.dragGhostComponent !== props.dragGhostComponent
) {
return true;
}
return false;
}
componentDidMount() {
const { onMount } = this.props;
if (onMount) {
onMount(this.designer);
}
this.designer.postEvent('mount', this.designer);
}
UNSAFE_componentWillMount() {
this.designer.purge();
}
render() {
const { className, style, dragGhostComponent } = this.props;
const DragGhost = dragGhostComponent || BuiltinDragGhostComponent;
return (
);
}
}
================================================
FILE: packages/designer/src/designer/designer.less
================================================
.lc-designer {
position: relative;
font-family: var(--font-family);
font-size: var(--font-size-text);
box-sizing: border-box;
* {
box-sizing: border-box;
}
}
================================================
FILE: packages/designer/src/designer/designer.ts
================================================
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,
IPublicTypeScrollable,
IPublicModelScroller,
IPublicTypeLocationData,
IPublicEnumTransformStage,
IPublicModelLocateEvent,
} from '@alilc/lowcode-types';
import { mergeAssets, IPublicTypeAssetsJson, isNodeSchema, isDragNodeObject, isDragNodeDataObject, isLocationChildrenDetail, Logger } from '@alilc/lowcode-utils';
import { IProject, Project } from '../project';
import { Node, DocumentModel, insertChildren, INode, ISelection } from '../document';
import { ComponentMeta, IComponentMeta } from '../component-meta';
import { INodeSelector, Component } from '../simulator';
import { Scroller } from './scroller';
import { Dragon, IDragon } from './dragon';
import { ActiveTracker, IActiveTracker } from './active-tracker';
import { Detecting } from './detecting';
import { DropLocation } from './location';
import { OffsetObserver, createOffsetObserver } from './offset-observer';
import { ISettingTopEntry, SettingTopEntry } from './setting';
import { BemToolsManager } from '../builtin-simulator/bem-tools/manager';
import { ComponentActions } from '../component-actions';
import { ContextMenuActions, IContextMenuActions } from '../context-menu-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?: Record | ((document: DocumentModel) => object);
simulatorComponent?: ComponentType;
dragGhostComponent?: ComponentType;
suspensed?: boolean;
componentMetadatas?: IPublicTypeComponentMetadata[];
globalComponentActions?: IPublicTypeComponentAction[];
onMount?: (designer: Designer) => void;
onDragstart?: (e: IPublicModelLocateEvent) => void;
onDrag?: (e: IPublicModelLocateEvent) => void;
onDragend?: (
e: { dragObject: IPublicModelDragObject; copy: boolean },
loc?: DropLocation,
) => void;
}
export interface IDesigner {
readonly shellModelFactory: IShellModelFactory;
viewName: string | undefined;
readonly project: IProject;
get dragon(): IDragon;
get activeTracker(): IActiveTracker;
get componentActions(): ComponentActions;
get contextMenuActions(): ContextMenuActions;
get editor(): IPublicModelEditor;
get detecting(): Detecting;
get simulatorComponent(): ComponentType | undefined;
get currentSelection(): ISelection;
createScroller(scrollable: IPublicTypeScrollable): IPublicModelScroller;
refreshComponentMetasMap(): void;
createOffsetObserver(nodeInstance: INodeSelector): OffsetObserver | null;
/**
* 创建插入位置,考虑放到 dragon 中
*/
createLocation(locationData: IPublicTypeLocationData): DropLocation;
get componentsMap(): { [key: string]: IPublicTypeNpmInfo | Component };
loadIncrementalAssets(incrementalAssets: IPublicTypeAssetsJson): Promise;
getComponentMeta(
componentName: string,
generateMetadata?: () => IPublicTypeComponentMetadata | null,
): IComponentMeta;
clearLocation(): void;
createComponentMeta(data: IPublicTypeComponentMetadata): IComponentMeta | null;
getComponentMetasMap(): Map;
addPropsReducer(reducer: IPublicTypePropsTransducer, stage: IPublicEnumTransformStage): void;
postEvent(event: string, ...args: any[]): void;
transformProps(props: IPublicTypeCompositeObject | IPublicTypePropsList, node: Node, stage: IPublicEnumTransformStage): IPublicTypeCompositeObject | IPublicTypePropsList;
createSettingEntry(nodes: INode[]): ISettingTopEntry;
autorun(effect: (reaction: IReactionPublic) => void, options?: IReactionOptions): IReactionDisposer;
}
export class Designer implements IDesigner {
dragon: IDragon;
viewName: string | undefined;
readonly componentActions = new ComponentActions();
readonly contextMenuActions: IContextMenuActions;
readonly activeTracker = new ActiveTracker();
readonly detecting = new Detecting();
readonly project: IProject;
readonly editor: IPublicModelEditor;
readonly bemToolsManager = new BemToolsManager(this);
readonly shellModelFactory: IShellModelFactory;
private _dropLocation?: DropLocation;
private propsReducers = new Map();
private _lostComponentMetasMap = new Map();
private props?: DesignerProps;
private oobxList: OffsetObserver[] = [];
private selectionDispose: undefined | (() => void);
@obx.ref private _componentMetasMap = new Map();
@obx.ref private _simulatorComponent?: ComponentType;
@obx.ref private _simulatorProps?: Record | ((project: IProject) => 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.contextMenuActions = new ContextMenuActions(this);
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: INode[] | 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 = () => {
if (this.selectionDispose) {
this.selectionDispose();
this.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 && rootNodeChildrens.length > 0) {
currentSelection.select(rootNodeChildrens[0].id);
}
}
this.postEvent('selection.change', currentSelection);
if (currentSelection) {
this.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 && this._dropLocation.document !== loc.document) {
this._dropLocation.document.dropLocation = null;
}
this._dropLocation = loc;
this.postEvent('dropLocation.change', loc);
if (loc.document) {
loc.document.dropLocation = loc;
}
this.activeTracker.track({ node: loc.target, detail: loc.detail });
return loc;
}
/**
* 清除插入位置
*/
clearLocation() {
if (this._dropLocation && this._dropLocation.document) {
this._dropLocation.document.dropLocation = null;
}
this.postEvent('dropLocation.change', undefined);
this._dropLocation = undefined;
}
createScroller(scrollable: IPublicTypeScrollable): 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: INode[]): ISettingTopEntry {
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 || 0) + 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 {
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 = mergeAssets(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 | undefined {
return this._simulatorComponent;
}
@computed get simulatorProps(): Record {
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:
}
}
================================================
FILE: packages/designer/src/designer/detecting.ts
================================================
import { makeObservable, obx, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
import { IPublicModelDetecting } from '@alilc/lowcode-types';
import type { IDocumentModel } from '../document/document-model';
import type { INode } from '../document/node/node';
const DETECTING_CHANGE_EVENT = 'detectingChange';
export interface IDetecting extends Omit,
'capture' |
'release' |
'leave'
> {
capture(node: INode | null): void;
release(node: INode | null): void;
leave(document: IDocumentModel | undefined): void;
get current(): INode | null;
}
export class Detecting implements IDetecting {
@obx.ref private _enable = true;
/**
* 控制大纲树 hover 时是否出现悬停效果
* TODO: 将该逻辑从设计器中抽离出来
*/
get enable() {
return this._enable;
}
set enable(flag: boolean) {
this._enable = flag;
if (!flag) {
this._current = null;
}
}
@obx.ref xRayMode = false;
@obx.ref private _current: INode | null = null;
private emitter: IEventBus = createModuleEventBus('Detecting');
constructor() {
makeObservable(this);
}
get current() {
return this._current;
}
capture(node: INode | null) {
if (this._current !== node) {
this._current = node;
this.emitter.emit(DETECTING_CHANGE_EVENT, this.current);
}
}
release(node: INode | null) {
if (this._current === node) {
this._current = null;
this.emitter.emit(DETECTING_CHANGE_EVENT, this.current);
}
}
leave(document: IDocumentModel | undefined) {
if (this.current && this.current.document === document) {
this._current = null;
}
}
onDetectingChange(fn: (node: INode) => void) {
this.emitter.on(DETECTING_CHANGE_EVENT, fn);
return () => {
this.emitter.off(DETECTING_CHANGE_EVENT, fn);
};
}
}
================================================
FILE: packages/designer/src/designer/dragon.ts
================================================
import { obx, makeObservable, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
import {
IPublicTypeDragNodeObject,
IPublicTypeDragAnyObject,
IPublicEnumDragObjectType,
IPublicTypeDragNodeDataObject,
IPublicModelDragObject,
IPublicModelNode,
IPublicModelDragon,
IPublicModelLocateEvent,
IPublicModelSensor,
} from '@alilc/lowcode-types';
import { setNativeSelection, cursor } from '@alilc/lowcode-utils';
import { INode, Node } from '../document';
import { ISimulatorHost, isSimulatorHost } from '../simulator';
import { IDesigner } from './designer';
import { makeEventsHandler } from '../utils/misc';
export interface ILocateEvent extends IPublicModelLocateEvent {
readonly type: 'LocateEvent';
/**
* 激活的感应器
*/
sensor?: IPublicModelSensor;
}
/**
* @deprecated use same function in @alilc/lowcode-utils
*/
export function isDragNodeObject(obj: any): obj is IPublicTypeDragNodeObject {
return obj && obj.type === IPublicEnumDragObjectType.Node;
}
/**
* @deprecated use same function in @alilc/lowcode-utils
*/
export function isDragNodeDataObject(obj: any): obj is IPublicTypeDragNodeDataObject {
return obj && obj.type === IPublicEnumDragObjectType.NodeData;
}
/**
* @deprecated use same function in @alilc/lowcode-utils
*/
export function isDragAnyObject(obj: any): obj is IPublicTypeDragAnyObject {
return obj && obj.type !== IPublicEnumDragObjectType.NodeData && obj.type !== IPublicEnumDragObjectType.Node;
}
export function isLocateEvent(e: any): e is ILocateEvent {
return e && e.type === 'LocateEvent';
}
const SHAKE_DISTANCE = 4;
/**
* mouse shake check
*/
export function isShaken(e1: MouseEvent | DragEvent, e2: MouseEvent | DragEvent): boolean {
if ((e1 as any).shaken) {
return true;
}
if (e1.target !== e2.target) {
return true;
}
return (
Math.pow(e1.clientY - e2.clientY, 2) + Math.pow(e1.clientX - e2.clientX, 2) > SHAKE_DISTANCE
);
}
export function isInvalidPoint(e: any, last: any): boolean {
return (
e.clientX === 0 &&
e.clientY === 0 &&
last &&
(Math.abs(last.clientX - e.clientX) > 5 || Math.abs(last.clientY - e.clientY) > 5)
);
}
export function isSameAs(e1: MouseEvent | DragEvent, e2: MouseEvent | DragEvent): boolean {
return e1.clientY === e2.clientY && e1.clientX === e2.clientX;
}
export function setShaken(e: any) {
e.shaken = true;
}
function getSourceSensor(dragObject: IPublicModelDragObject): ISimulatorHost | null {
if (!isDragNodeObject(dragObject)) {
return null;
}
return dragObject.nodes[0]?.document?.simulator || null;
}
function isDragEvent(e: any): e is DragEvent {
return e?.type?.startsWith('drag');
}
export interface IDragon extends IPublicModelDragon<
INode,
ILocateEvent
> {
emitter: IEventBus;
}
/**
* Drag-on 拖拽引擎
*/
export class Dragon implements IDragon {
private sensors: IPublicModelSensor[] = [];
private nodeInstPointerEvents: boolean;
key = Math.random();
/**
* current active sensor, 可用于感应区高亮
*/
@obx.ref private _activeSensor: IPublicModelSensor | undefined;
get activeSensor(): IPublicModelSensor | undefined {
return this._activeSensor;
}
@obx.ref private _dragging = false;
@obx.ref private _canDrop = false;
get dragging(): boolean {
return this._dragging;
}
viewName: string | undefined;
emitter: IEventBus = createModuleEventBus('Dragon');
constructor(readonly designer: IDesigner) {
makeObservable(this);
this.viewName = designer.viewName;
}
/**
* Quick listen a shell(container element) drag behavior
* @param shell container element
* @param boost boost got a drag object
*/
from(shell: Element, boost: (e: MouseEvent) => IPublicModelDragObject | null) {
const mousedown = (e: MouseEvent) => {
// ESC or RightClick
if (e.which === 3 || e.button === 2) {
return;
}
// Get a new node to be dragged
const dragObject = boost(e);
if (!dragObject) {
return;
}
this.boost(dragObject, e);
};
shell.addEventListener('mousedown', mousedown as any);
return () => {
shell.removeEventListener('mousedown', mousedown as any);
};
}
/**
* boost your dragObject for dragging(flying) 发射拖拽对象
*
* @param dragObject 拖拽对象
* @param boostEvent 拖拽初始时事件
*/
boost(dragObject: IPublicModelDragObject, boostEvent: MouseEvent | DragEvent, fromRglNode?: INode | IPublicModelNode) {
const { designer } = this;
const masterSensors = this.getMasterSensors();
const handleEvents = makeEventsHandler(boostEvent, masterSensors);
const newBie = !isDragNodeObject(dragObject);
const forceCopyState =
isDragNodeObject(dragObject) && dragObject.nodes.some((node: Node | IPublicModelNode) => (typeof node.isSlot === 'function' ? node.isSlot() : node.isSlot));
const isBoostFromDragAPI = isDragEvent(boostEvent);
let lastSensor: IPublicModelSensor | undefined;
this._dragging = false;
const getRGL = (e: MouseEvent | DragEvent) => {
const locateEvent = createLocateEvent(e);
const sensor = chooseSensor(locateEvent);
if (!sensor || !sensor.getNodeInstanceFromElement) return {};
const nodeInst = sensor.getNodeInstanceFromElement(e.target as Element);
return nodeInst?.node?.getRGL() || {};
};
const checkesc = (e: KeyboardEvent) => {
if (e.keyCode === 27) {
designer.clearLocation();
over();
}
};
let copy = false;
const checkcopy = (e: MouseEvent | DragEvent | KeyboardEvent) => {
/* istanbul ignore next */
if (isDragEvent(e) && e.dataTransfer) {
if (newBie || forceCopyState) {
e.dataTransfer.dropEffect = 'copy';
}
return;
}
if (newBie) {
return;
}
if (e.altKey || e.ctrlKey) {
copy = true;
this.setCopyState(true);
/* istanbul ignore next */
if (isDragEvent(e) && e.dataTransfer) {
e.dataTransfer.dropEffect = 'copy';
}
} else {
copy = false;
if (!forceCopyState) {
this.setCopyState(false);
/* istanbul ignore next */
if (isDragEvent(e) && e.dataTransfer) {
e.dataTransfer.dropEffect = 'move';
}
}
}
};
let lastArrive: any;
const drag = (e: MouseEvent | DragEvent) => {
// FIXME: donot setcopy when: newbie & no location
checkcopy(e);
if (isInvalidPoint(e, lastArrive)) return;
if (lastArrive && isSameAs(e, lastArrive)) {
lastArrive = e;
return;
}
lastArrive = e;
const { isRGL, rglNode } = getRGL(e);
const locateEvent = createLocateEvent(e);
const sensor = chooseSensor(locateEvent);
/* istanbul ignore next */
if (isRGL) {
// 禁止被拖拽元素的阻断
const nodeInst = dragObject.nodes[0].getDOMNode();
if (nodeInst && nodeInst.style) {
this.nodeInstPointerEvents = true;
nodeInst.style.pointerEvents = 'none';
}
// 原生拖拽
this.emitter.emit('rgl.sleeping', false);
if (fromRglNode && fromRglNode.id === rglNode.id) {
designer.clearLocation();
this.clearState();
this.emitter.emit('drag', locateEvent);
return;
}
this._canDrop = !!sensor?.locate(locateEvent);
if (this._canDrop) {
this.emitter.emit('rgl.add.placeholder', {
rglNode,
fromRglNode,
node: locateEvent.dragObject?.nodes[0],
event: e,
});
designer.clearLocation();
this.clearState();
this.emitter.emit('drag', locateEvent);
return;
}
} else {
this._canDrop = false;
this.emitter.emit('rgl.remove.placeholder');
this.emitter.emit('rgl.sleeping', true);
}
if (sensor) {
sensor.fixEvent(locateEvent);
sensor.locate(locateEvent);
} else {
designer.clearLocation();
}
this.emitter.emit('drag', locateEvent);
};
const dragstart = () => {
this._dragging = true;
setShaken(boostEvent);
const locateEvent = createLocateEvent(boostEvent);
if (newBie || forceCopyState) {
this.setCopyState(true);
} else {
chooseSensor(locateEvent);
}
this.setDraggingState(true);
// ESC cancel drag
if (!isBoostFromDragAPI) {
handleEvents((doc) => {
doc.addEventListener('keydown', checkesc, false);
});
}
this.emitter.emit('dragstart', locateEvent);
};
// route: drag-move
const move = (e: MouseEvent | DragEvent) => {
/* istanbul ignore next */
if (isBoostFromDragAPI) {
e.preventDefault();
}
if (this._dragging) {
// process dragging
drag(e);
return;
}
// first move check is shaken
if (isShaken(boostEvent, e)) {
// is shaken dragstart
dragstart();
drag(e);
}
};
let didDrop = true;
/* istanbul ignore next */
const drop = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
didDrop = true;
};
// end-tail drag process
const over = (e?: any) => {
// 禁止被拖拽元素的阻断
if (this.nodeInstPointerEvents) {
const nodeInst = dragObject.nodes[0].getDOMNode();
if (nodeInst && nodeInst.style) {
nodeInst.style.pointerEvents = '';
}
this.nodeInstPointerEvents = false;
}
// 发送drop事件
if (e) {
const { isRGL, rglNode } = getRGL(e);
/* istanbul ignore next */
if (isRGL && this._canDrop && this._dragging) {
const tarNode = dragObject.nodes[0];
if (rglNode.id !== tarNode.id) {
// 避免死循环
this.emitter.emit('rgl.drop', {
rglNode,
node: tarNode,
});
const selection = designer.project.currentDocument?.selection;
selection?.select(tarNode.id);
}
}
}
// 移除磁帖占位消息
this.emitter.emit('rgl.remove.placeholder');
/* istanbul ignore next */
if (e && isDragEvent(e)) {
e.preventDefault();
}
if (lastSensor) {
lastSensor.deactiveSensor();
}
/* istanbul ignore next */
if (isBoostFromDragAPI) {
if (!didDrop) {
designer.clearLocation();
}
} else {
this.setNativeSelection(true);
}
this.clearState();
let exception;
if (this._dragging) {
this._dragging = false;
try {
this.emitter.emit('dragend', { dragObject, copy });
} catch (ex) /* istanbul ignore next */ {
exception = ex;
}
}
designer.clearLocation();
handleEvents((doc) => {
/* istanbul ignore next */
if (isBoostFromDragAPI) {
doc.removeEventListener('dragover', move, true);
doc.removeEventListener('dragend', over, true);
doc.removeEventListener('drop', drop, true);
} else {
doc.removeEventListener('mousemove', move, true);
doc.removeEventListener('mouseup', over, true);
}
doc.removeEventListener('mousedown', over, true);
doc.removeEventListener('keydown', checkesc, false);
doc.removeEventListener('keydown', checkcopy, false);
doc.removeEventListener('keyup', checkcopy, false);
});
/* istanbul ignore next */
if (exception) {
throw exception;
}
};
// create drag locate event
const createLocateEvent = (e: MouseEvent | DragEvent): ILocateEvent => {
const evt: any = {
type: 'LocateEvent',
dragObject,
target: e.target,
originalEvent: e,
};
const sourceDocument = e.view?.document;
// event from current document
if (!sourceDocument || sourceDocument === document) {
evt.globalX = e.clientX;
evt.globalY = e.clientY;
} else /* istanbul ignore next */ {
// event from simulator sandbox
let srcSim: ISimulatorHost | undefined;
const lastSim = lastSensor && isSimulatorHost(lastSensor) ? lastSensor : null;
// check source simulator
if (lastSim && lastSim.contentDocument === sourceDocument) {
srcSim = lastSim;
} else {
srcSim = masterSensors.find((sim) => sim.contentDocument === sourceDocument);
if (!srcSim && lastSim) {
srcSim = lastSim;
}
}
if (srcSim) {
// transform point by simulator
const g = srcSim.viewport.toGlobalPoint(e);
evt.globalX = g.clientX;
evt.globalY = g.clientY;
evt.canvasX = e.clientX;
evt.canvasY = e.clientY;
evt.sensor = srcSim;
} else {
// this condition will not happen, just make sure ts ok
evt.globalX = e.clientX;
evt.globalY = e.clientY;
}
}
return evt;
};
const sourceSensor = getSourceSensor(dragObject);
/* istanbul ignore next */
const chooseSensor = (e: ILocateEvent) => {
// this.sensors will change on dragstart
const sensors: IPublicModelSensor[] = this.sensors.concat(masterSensors as IPublicModelSensor[]);
let sensor =
e.sensor && e.sensor.isEnter(e)
? e.sensor
: sensors.find((s) => s.sensorAvailable && s.isEnter(e));
if (!sensor) {
// TODO: enter some area like componentspanel cancel
if (lastSensor) {
sensor = lastSensor;
} else if (e.sensor) {
sensor = e.sensor;
} else if (sourceSensor) {
sensor = sourceSensor;
}
}
if (sensor !== lastSensor) {
if (lastSensor) {
lastSensor.deactiveSensor();
}
lastSensor = sensor;
}
if (sensor) {
e.sensor = sensor;
sensor.fixEvent(e);
}
this._activeSensor = sensor;
return sensor;
};
/* istanbul ignore next */
if (isDragEvent(boostEvent)) {
const { dataTransfer } = boostEvent;
if (dataTransfer) {
dataTransfer.effectAllowed = 'all';
try {
dataTransfer.setData('application/json', '{}');
} catch (ex) {
// ignore
}
}
dragstart();
} else {
this.setNativeSelection(false);
}
handleEvents((doc) => {
/* istanbul ignore next */
if (isBoostFromDragAPI) {
doc.addEventListener('dragover', move, true);
// dragexit
didDrop = false;
doc.addEventListener('drop', drop, true);
doc.addEventListener('dragend', over, true);
} else {
doc.addEventListener('mousemove', move, true);
doc.addEventListener('mouseup', over, true);
}
doc.addEventListener('mousedown', over, true);
});
// future think: drag things from browser-out or a iframe-pane
if (!newBie && !isBoostFromDragAPI) {
handleEvents((doc) => {
doc.addEventListener('keydown', checkcopy, false);
doc.addEventListener('keyup', checkcopy, false);
});
}
}
/* istanbul ignore next */
private getMasterSensors(): ISimulatorHost[] {
return Array.from(
new Set(
this.designer.project.documents
.map((doc) => {
if (doc.active && doc.simulator?.sensorAvailable) {
return doc.simulator;
}
return null;
})
.filter(Boolean) as any,
),
);
}
private getSimulators() {
return new Set(this.designer.project.documents.map((doc) => doc.simulator));
}
// #region ======== drag and drop helpers ============
private setNativeSelection(enableFlag: boolean) {
setNativeSelection(enableFlag);
this.getSimulators().forEach((sim) => {
sim?.setNativeSelection(enableFlag);
});
}
/**
* 设置拖拽态
*/
private setDraggingState(state: boolean) {
cursor.setDragging(state);
this.getSimulators().forEach((sim) => {
sim?.setDraggingState(state);
});
}
/**
* 设置拷贝态
*/
private setCopyState(state: boolean) {
cursor.setCopy(state);
this.getSimulators().forEach((sim) => {
sim?.setCopyState(state);
});
}
/**
* 清除所有态:拖拽态、拷贝态
*/
private clearState() {
cursor.release();
this.getSimulators().forEach((sim) => {
sim?.clearState();
});
}
// #endregion
/**
* 添加投放感应区
*/
addSensor(sensor: any) {
this.sensors.push(sensor);
}
/**
* 移除投放感应
*/
removeSensor(sensor: any) {
const i = this.sensors.indexOf(sensor);
if (i > -1) {
this.sensors.splice(i, 1);
}
}
onDragstart(func: (e: ILocateEvent) => any) {
this.emitter.on('dragstart', func);
return () => {
this.emitter.removeListener('dragstart', func);
};
}
onDrag(func: (e: ILocateEvent) => any) {
this.emitter.on('drag', func);
return () => {
this.emitter.removeListener('drag', func);
};
}
onDragend(func: (x: { dragObject: IPublicModelDragObject; copy: boolean }) => any) {
this.emitter.on('dragend', func);
return () => {
this.emitter.removeListener('dragend', func);
};
}
}
================================================
FILE: packages/designer/src/designer/index.ts
================================================
export * from './designer';
export * from './designer-view';
export * from './dragon';
export * from './detecting';
export * from './location';
export * from './offset-observer';
export * from './scroller';
export * from './setting';
export * from './active-tracker';
export * from '../document';
export * from './clipboard';
================================================
FILE: packages/designer/src/designer/location.ts
================================================
import type { IDocumentModel, INode } from '../document';
import { ILocateEvent } from './dragon';
import {
IPublicModelDropLocation,
IPublicTypeLocationDetailType,
IPublicTypeRect,
IPublicTypeLocationDetail,
IPublicTypeLocationData,
IPublicModelLocateEvent,
} from '@alilc/lowcode-types';
export interface Point {
clientX: number;
clientY: number;
}
export interface CanvasPoint {
canvasX: number;
canvasY: number;
}
export type Rects = DOMRect[] & {
elements: Array;
};
/**
* @deprecated use same function in @alilc/lowcode-utils
*/
export function isLocationData(obj: any): boolean {
return obj && obj.target && obj.detail;
}
/**
* @deprecated use same function in @alilc/lowcode-utils
*/
export function isLocationChildrenDetail(obj: any): boolean {
return obj && obj.type === IPublicTypeLocationDetailType.Children;
}
export function isRowContainer(container: Element | Text, win?: Window) {
if (isText(container)) {
return true;
}
const style = (win || getWindow(container)).getComputedStyle(container);
const display = style.getPropertyValue('display');
if (/flex$/.test(display)) {
const direction = style.getPropertyValue('flex-direction') || 'row';
if (direction === 'row' || direction === 'row-reverse') {
return true;
}
}
if (/grid$/.test(display)) {
return true;
}
return false;
}
export function isChildInline(child: Element | Text, win?: Window) {
if (isText(child)) {
return true;
}
const style = (win || getWindow(child)).getComputedStyle(child);
return /^inline/.test(style.getPropertyValue('display')) || /^(left|right)$/.test(style.getPropertyValue('float'));
}
export function getRectTarget(rect: IPublicTypeRect | null) {
if (!rect || rect.computed) {
return null;
}
const els = rect.elements;
return els && els.length > 0 ? els[0]! : null;
}
export function isVerticalContainer(rect: IPublicTypeRect | null) {
const el = getRectTarget(rect);
if (!el) {
return false;
}
return isRowContainer(el);
}
export function isVertical(rect: IPublicTypeRect | null) {
const el = getRectTarget(rect);
if (!el) {
return false;
}
return isChildInline(el) || (el.parentElement ? isRowContainer(el.parentElement) : false);
}
function isText(elem: any): elem is Text {
return elem.nodeType === Node.TEXT_NODE;
}
function isDocument(elem: any): elem is Document {
return elem.nodeType === Node.DOCUMENT_NODE;
}
export function getWindow(elem: Element | Document): Window {
return (isDocument(elem) ? elem : elem.ownerDocument!).defaultView!;
}
export interface IDropLocation extends Omit {
readonly source: string;
get target(): INode;
get document(): IDocumentModel | null;
clone(event: IPublicModelLocateEvent): IDropLocation;
}
export class DropLocation implements IDropLocation {
readonly target: INode;
readonly detail: IPublicTypeLocationDetail;
readonly event: ILocateEvent;
readonly source: string;
get document(): IDocumentModel | null {
return this.target.document;
}
constructor({ target, detail, source, event }: IPublicTypeLocationData) {
this.target = target;
this.detail = detail;
this.source = source;
this.event = event;
}
clone(event: ILocateEvent): IDropLocation {
return new DropLocation({
target: this.target,
detail: this.detail,
source: this.source,
event,
});
}
/**
* @deprecated
* 兼容 vision
*/
getContainer() {
return this.target;
}
/**
* @deprecated
* 兼容 vision
*/
getInsertion() {
if (!this.detail) {
return null;
}
if (this.detail.type === 'Children') {
if (this.detail.index <= 0) {
return null;
}
return this.target.children?.get(this.detail.index - 1);
}
return (this.detail as any)?.near?.node;
}
}
================================================
FILE: packages/designer/src/designer/offset-observer.ts
================================================
import requestIdleCallback, { cancelIdleCallback } from 'ric-shim';
import { obx, computed, makeObservable } from '@alilc/lowcode-editor-core';
import { uniqueId } from '@alilc/lowcode-utils';
import { INodeSelector, IViewport } from '../simulator';
import { INode } from '../document';
export class OffsetObserver {
readonly id = uniqueId('oobx');
private lastOffsetLeft?: number;
private lastOffsetTop?: number;
private lastOffsetHeight?: number;
private lastOffsetWidth?: number;
@obx private _height = 0;
@obx private _width = 0;
@obx private _left = 0;
@obx private _top = 0;
@obx private _right = 0;
@obx private _bottom = 0;
@computed get height() {
return this.isRoot ? this.viewport.height : this._height * this.scale;
}
@computed get width() {
return this.isRoot ? this.viewport.width : this._width * this.scale;
}
@computed get top() {
return this.isRoot ? 0 : this._top * this.scale;
}
@computed get left() {
return this.isRoot ? 0 : this._left * this.scale;
}
@computed get bottom() {
return this.isRoot ? this.viewport.height : this._bottom * this.scale;
}
@computed get right() {
return this.isRoot ? this.viewport.width : this._right * this.scale;
}
@obx hasOffset = false;
@computed get offsetLeft() {
if (this.isRoot) {
return this.viewport.scrollX * this.scale;
}
if (!this.viewport.scrolling || this.lastOffsetLeft == null) {
this.lastOffsetLeft = this.left + this.viewport.scrollX * this.scale;
}
return this.lastOffsetLeft;
}
@computed get offsetTop() {
if (this.isRoot) {
return this.viewport.scrollY * this.scale;
}
if (!this.viewport.scrolling || this.lastOffsetTop == null) {
this.lastOffsetTop = this.top + this.viewport.scrollY * this.scale;
}
return this.lastOffsetTop;
}
@computed get offsetHeight() {
if (!this.viewport.scrolling || this.lastOffsetHeight == null) {
this.lastOffsetHeight = this.isRoot ? this.viewport.height : this.height;
}
return this.lastOffsetHeight;
}
@computed get offsetWidth() {
if (!this.viewport.scrolling || this.lastOffsetWidth == null) {
this.lastOffsetWidth = this.isRoot ? this.viewport.width : this.width;
}
return this.lastOffsetWidth;
}
@computed get scale() {
return this.viewport.scale;
}
private pid: number | undefined;
readonly viewport: IViewport | undefined;
private isRoot: boolean;
readonly node: INode;
readonly compute: () => void;
constructor(readonly nodeInstance: INodeSelector) {
const { node, instance } = nodeInstance;
this.node = node;
const doc = node.document;
const host = doc?.simulator;
const focusNode = doc?.focusNode;
this.isRoot = node.contains(focusNode!);
this.viewport = host?.viewport;
makeObservable(this);
if (this.isRoot) {
this.hasOffset = true;
return;
}
if (!instance) {
return;
}
let pid: number | undefined;
const compute = () => {
if (pid !== this.pid) {
return;
}
const rect = host.computeComponentInstanceRect(instance!, node.componentMeta.rootSelector);
if (!rect) {
this.hasOffset = false;
} else if (!this.viewport.scrolling || !this.hasOffset) {
this._height = rect.height;
this._width = rect.width;
this._left = rect.left;
this._top = rect.top;
this._right = rect.right;
this._bottom = rect.bottom;
this.hasOffset = true;
}
this.pid = requestIdleCallback(compute);
pid = this.pid;
};
this.compute = compute;
// try first
compute();
// try second, ensure the dom mounted
this.pid = requestIdleCallback(compute);
pid = this.pid;
}
purge() {
if (this.pid) {
cancelIdleCallback(this.pid);
}
this.pid = undefined;
}
isPurged() {
return this.pid == null;
}
}
export function createOffsetObserver(nodeInstance: INodeSelector): OffsetObserver | null {
if (!nodeInstance.instance) {
return null;
}
return new OffsetObserver(nodeInstance);
}
================================================
FILE: packages/designer/src/designer/scroller.ts
================================================
import { isElement } from '@alilc/lowcode-utils';
import { IPublicModelScrollTarget, IPublicTypeScrollable, IPublicModelScroller } from '@alilc/lowcode-types';
export interface IScrollTarget extends IPublicModelScrollTarget {
}
export class ScrollTarget implements IScrollTarget {
get left() {
return 'scrollX' in this.target ? this.target.scrollX : this.target.scrollLeft;
}
get top() {
return 'scrollY' in this.target ? this.target.scrollY : this.target.scrollTop;
}
private doc?: HTMLElement;
constructor(private target: Window | Element) {
if (isWindow(target)) {
this.doc = target.document.documentElement;
}
}
scrollTo(options: { left?: number; top?: number }) {
this.target.scrollTo(options);
}
scrollToXY(x: number, y: number) {
this.target.scrollTo(x, y);
}
get scrollHeight(): number {
return ((this.doc || this.target) as any).scrollHeight;
}
get scrollWidth(): number {
return ((this.doc || this.target) as any).scrollWidth;
}
}
function isWindow(obj: any): obj is Window {
return obj && obj.document;
}
function easing(n: number) {
return Math.sin((n * Math.PI) / 2);
}
const SCROLL_ACCURACY = 30;
export interface IScroller extends IPublicModelScroller {
}
export class Scroller implements IScroller {
private pid: number | undefined;
scrollable: IPublicTypeScrollable;
constructor(scrollable: IPublicTypeScrollable) {
this.scrollable = scrollable;
}
get scrollTarget(): IScrollTarget | null {
let target = this.scrollable.scrollTarget;
if (!target) {
return null;
}
if (isElement(target)) {
target = new ScrollTarget(target);
this.scrollable.scrollTarget = target;
}
return target;
}
scrollTo(options: { left?: number; top?: number }) {
this.cancel();
const { scrollTarget } = this;
if (!scrollTarget) {
return;
}
let pid: number;
const { left } = scrollTarget;
const { top } = scrollTarget;
const end = () => {
this.cancel();
};
if ((left === options.left || options.left == null) && top === options.top) {
end();
return;
}
const duration = 200;
const start = +new Date();
const animate = () => {
if (pid !== this.pid) {
return;
}
const now = +new Date();
const time = Math.min(1, (now - start) / duration);
const eased = easing(time);
const opt: any = {};
if (options.left != null) {
opt.left = eased * (options.left - left) + left;
}
if (options.top != null) {
opt.top = eased * (options.top - top) + top;
}
scrollTarget.scrollTo(opt);
if (time < 1) {
this.pid = requestAnimationFrame(animate);
pid = this.pid;
} else {
end();
}
};
this.pid = requestAnimationFrame(animate);
pid = this.pid;
}
scrolling(point: { globalX: number; globalY: number }) {
this.cancel();
const { bounds, scale = 1 } = this.scrollable;
const { scrollTarget } = this;
if (!scrollTarget || !bounds) {
return;
}
const x = point.globalX;
const y = point.globalY;
const maxScrollHeight = scrollTarget.scrollHeight - bounds.height / scale;
const maxScrollWidth = scrollTarget.scrollWidth - bounds.width / scale;
let sx = scrollTarget.left;
let sy = scrollTarget.top;
let ax = 0;
let ay = 0;
if (y < bounds.top + SCROLL_ACCURACY) {
ay = -Math.min(Math.max(bounds.top + SCROLL_ACCURACY - y, 10), 50) / scale;
} else if (y > bounds.bottom - SCROLL_ACCURACY) {
ay = Math.min(Math.max(y + SCROLL_ACCURACY - bounds.bottom, 10), 50) / scale;
}
if (x < bounds.left + SCROLL_ACCURACY) {
ax = -Math.min(Math.max(bounds.top + SCROLL_ACCURACY - y, 10), 50) / scale;
} else if (x > bounds.right - SCROLL_ACCURACY) {
ax = Math.min(Math.max(x + SCROLL_ACCURACY - bounds.right, 10), 50) / scale;
}
if (!ax && !ay) {
return;
}
const animate = () => {
let scroll = false;
if ((ay > 0 && sy < maxScrollHeight) || (ay < 0 && sy > 0)) {
sy += ay;
sy = Math.min(Math.max(sy, 0), maxScrollHeight);
scroll = true;
}
if ((ax > 0 && sx < maxScrollWidth) || (ax < 0 && sx > 0)) {
sx += ax;
sx = Math.min(Math.max(sx, 0), maxScrollWidth);
scroll = true;
}
if (!scroll) {
return;
}
scrollTarget.scrollTo({ left: sx, top: sy });
this.pid = requestAnimationFrame(animate);
};
animate();
}
cancel() {
if (this.pid) {
cancelAnimationFrame(this.pid);
}
this.pid = undefined;
}
}
================================================
FILE: packages/designer/src/designer/drag-ghost/README.md
================================================
内置拖拽替身
================================================
FILE: packages/designer/src/designer/drag-ghost/ghost.less
================================================
.lc-ghost-group {
box-sizing: border-box;
position: fixed;
z-index: 99999;
width: 100px;
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
background-color: var(--color-block-background-deep-dark, rgba(0, 0, 0, 0.4));
box-shadow: 0 0 6px var(--color-block-background-shallow, grey);
transform: translate(-10%, -50%);
.lc-ghost {
.lc-ghost-title {
text-align: center;
font-size: var(--font-size-text);
text-overflow: ellipsis;
color: var(--color-text-light);
white-space: nowrap;
overflow: hidden;
}
}
}
================================================
FILE: packages/designer/src/designer/drag-ghost/index.tsx
================================================
import { Component, ReactElement } from 'react';
import { observer, obx, Title, makeObservable } from '@alilc/lowcode-editor-core';
import { Designer } from '../designer';
import { isDragNodeObject } from '../dragon';
import { isSimulatorHost } from '../../simulator';
import './ghost.less';
import { IPublicTypeI18nData, IPublicTypeNodeSchema, IPublicModelDragObject } from '@alilc/lowcode-types';
type offBinding = () => any;
@observer
export default class DragGhost extends Component<{ designer: Designer }> {
private dispose: offBinding[] = [];
@obx.ref private titles: (string | IPublicTypeI18nData | ReactElement)[] | null = null;
@obx.ref private x = 0;
@obx.ref private y = 0;
@obx private isAbsoluteLayoutContainer = false;
private dragon = this.props.designer.dragon;
constructor(props: any) {
super(props);
makeObservable(this);
this.dispose = [
this.dragon.onDragstart(e => {
if (e.originalEvent.type.slice(0, 4) === 'drag') {
return;
}
this.titles = this.getTitles(e.dragObject);
this.x = e.globalX;
this.y = e.globalY;
}),
this.dragon.onDrag(e => {
this.x = e.globalX;
this.y = e.globalY;
if (isSimulatorHost(e.sensor)) {
const container = e.sensor.getDropContainer(e);
if (container?.container.componentMeta.advanced.isAbsoluteLayoutContainer) {
this.isAbsoluteLayoutContainer = true;
return;
}
}
this.isAbsoluteLayoutContainer = false;
}),
this.dragon.onDragend(() => {
this.titles = null;
this.x = 0;
this.y = 0;
}),
];
}
getTitles(dragObject: IPublicModelDragObject) {
if (isDragNodeObject(dragObject)) {
return dragObject.nodes.map((node) => node.title);
}
const dataList = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data];
return dataList.map((item: IPublicTypeNodeSchema, i) => (this.props.designer.getComponentMeta(item.componentName).title));
}
componentWillUnmount() {
if (this.dispose) {
this.dispose.forEach(off => off());
}
}
renderGhostGroup() {
return this.titles?.map((title, i) => {
const ghost = (
);
return ghost;
});
}
render() {
if (!this.titles || !this.titles.length) {
return null;
}
if (this.isAbsoluteLayoutContainer) {
return null;
}
return (
{this.renderGhostGroup()}
);
}
}
================================================
FILE: packages/designer/src/designer/setting/index.ts
================================================
export * from './setting-field';
export * from './setting-top-entry';
export * from './setting-entry-type';
export * from './setting-prop-entry';
================================================
FILE: packages/designer/src/designer/setting/setting-entry-type.ts
================================================
import { IPublicApiSetters, IPublicModelEditor } from '@alilc/lowcode-types';
import { IDesigner } from '../designer';
import { INode } from '../../document';
import { ISettingField } from './setting-field';
export interface ISettingEntry {
readonly designer: IDesigner | undefined;
readonly id: string;
/**
* 同样类型的节点
*/
readonly isSameComponent: boolean;
/**
* 一个
*/
readonly isSingle: boolean;
/**
* 多个
*/
readonly isMultiple: boolean;
/**
* 编辑器引用
*/
readonly editor: IPublicModelEditor;
readonly setters: IPublicApiSetters;
/**
* 取得子项
*/
get: (propName: string | number) => ISettingField | null;
readonly nodes: INode[];
// @todo 补充 node 定义
/**
* 获取 node 中的第一项
*/
getNode: () => any;
}
================================================
FILE: packages/designer/src/designer/setting/setting-field.ts
================================================
import { ReactNode } from 'react';
import {
IPublicTypeTitleContent,
IPublicTypeSetterType,
IPublicTypeDynamicSetter,
IPublicTypeFieldExtraProps,
IPublicTypeFieldConfig,
IPublicTypeCustomView,
IPublicTypeDisposable,
IPublicModelSettingField,
IBaseModelSettingField,
} from '@alilc/lowcode-types';
import type {
IPublicTypeSetValueOptions,
} from '@alilc/lowcode-types';
import { Transducer } from './utils';
import { ISettingPropEntry, SettingPropEntry } from './setting-prop-entry';
import { computed, obx, makeObservable, action, untracked, intl } from '@alilc/lowcode-editor-core';
import { cloneDeep, isCustomView, isDynamicSetter, isJSExpression } from '@alilc/lowcode-utils';
import { ISettingTopEntry } from './setting-top-entry';
import { IComponentMeta, INode } from '@alilc/lowcode-designer';
function getSettingFieldCollectorKey(parent: ISettingTopEntry | ISettingField, config: IPublicTypeFieldConfig) {
let cur = parent;
const path = [config.name];
while (cur !== parent.top) {
if (cur instanceof SettingField && cur.type !== 'group') {
path.unshift(cur.name);
}
cur = cur.parent;
}
return path.join('.');
}
export interface ISettingField extends ISettingPropEntry, Omit, 'setValue' | 'key' | 'node'> {
readonly isSettingField: true;
readonly isRequired: boolean;
readonly isGroup: boolean;
extraProps: IPublicTypeFieldExtraProps;
get items(): Array;
get title(): string | ReactNode | undefined;
get setter(): IPublicTypeSetterType | null;
get expanded(): boolean;
get valueState(): number;
setExpanded(value: boolean): void;
purge(): void;
setValue(
val: any,
isHotValue?: boolean,
force?: boolean,
extraOptions?: IPublicTypeSetValueOptions,
): void;
clearValue(): void;
valueChange(options: IPublicTypeSetValueOptions): void;
createField(config: IPublicTypeFieldConfig): ISettingField;
onEffect(action: () => void): IPublicTypeDisposable;
internalToShellField(): IPublicModelSettingField;
}
export class SettingField extends SettingPropEntry implements ISettingField {
readonly isSettingField = true;
readonly isRequired: boolean;
readonly transducer: Transducer;
private _config: IPublicTypeFieldConfig;
private hotValue: any;
parent: ISettingTopEntry | ISettingField;
extraProps: IPublicTypeFieldExtraProps;
// ==== dynamic properties ====
private _title?: IPublicTypeTitleContent;
get title() {
return (
this._title || (typeof this.name === 'number' ? `${intl('Item')} ${this.name}` : this.name)
);
}
private _setter?: IPublicTypeSetterType | IPublicTypeDynamicSetter;
@obx.ref private _expanded = true;
private _items: Array = [];
constructor(
parent: ISettingTopEntry | ISettingField,
config: IPublicTypeFieldConfig,
private settingFieldCollector?: (name: string | number, field: ISettingField) => void,
) {
super(parent, config.name, config.type);
makeObservable(this);
const { title, items, setter, extraProps, ...rest } = config;
this.parent = parent;
this._config = config;
this._title = title;
this._setter = setter;
this.extraProps = {
...rest,
...extraProps,
};
this.isRequired = config.isRequired || (setter as any)?.isRequired;
this._expanded = !extraProps?.defaultCollapsed;
// initial items
if (items && items.length > 0) {
this.initItems(items, settingFieldCollector);
}
if (this.type !== 'group' && settingFieldCollector && config.name) {
settingFieldCollector(getSettingFieldCollectorKey(parent, config), this);
}
// compatiable old config
this.transducer = new Transducer(this, { setter });
}
@computed get setter(): IPublicTypeSetterType | null {
if (!this._setter) {
return null;
}
if (isDynamicSetter(this._setter)) {
return untracked(() => {
const shellThis = this.internalToShellField();
return (this._setter as IPublicTypeDynamicSetter)?.call(shellThis, shellThis!);
});
}
return this._setter;
}
get expanded(): boolean {
return this._expanded;
}
setExpanded(value: boolean) {
this._expanded = value;
}
get items(): Array {
return this._items;
}
get config(): IPublicTypeFieldConfig {
return this._config;
}
private initItems(
items: Array,
settingFieldCollector?: {
(name: string | number, field: ISettingField): void;
(name: string, field: ISettingField): void;
},
) {
this._items = items.map((item) => {
if (isCustomView(item)) {
return item;
}
return new SettingField(this, item, settingFieldCollector);
});
}
private disposeItems() {
this._items.forEach((item) => isSettingField(item) && item.purge());
this._items = [];
}
// 创建子配置项,通常用于 object/array 类型数据
createField(config: IPublicTypeFieldConfig): ISettingField {
this.settingFieldCollector?.(getSettingFieldCollectorKey(this.parent, config), this);
return new SettingField(this, config, this.settingFieldCollector);
}
purge() {
this.disposeItems();
}
// ======= compatibles for vision ======
getConfig(
configName?: K,
): IPublicTypeFieldConfig[K] | IPublicTypeFieldConfig {
if (configName) {
return this.config[configName];
}
return this._config;
}
getItems(
filter?: (item: ISettingField | IPublicTypeCustomView) => boolean,
): Array {
return this._items.filter((item) => {
if (filter) {
return filter(item);
}
return true;
});
}
@action
setValue(
val: any,
isHotValue?: boolean,
force?: boolean,
extraOptions?: IPublicTypeSetValueOptions,
) {
if (isHotValue) {
this.setHotValue(val, extraOptions);
return;
}
super.setValue(val, false, false, extraOptions);
}
getHotValue(): any {
if (this.hotValue) {
return this.hotValue;
}
// avoid View modify
let v = cloneDeep(this.getMockOrValue());
if (v == null) {
v = this.extraProps.defaultValue;
}
return this.transducer.toHot(v);
}
/* istanbul ignore next */
@action
setMiniAppDataSourceValue(data: any, options?: any) {
this.hotValue = data;
const v = this.transducer.toNative(data);
this.setValue(v, false, false, options);
// dirty fix list setter
if (Array.isArray(data) && data[0] && data[0].__sid__) {
return;
}
this.valueChange();
}
@action
setHotValue(data: any, options?: IPublicTypeSetValueOptions) {
this.hotValue = data;
const value = this.transducer.toNative(data);
if (options) {
options.fromSetHotValue = true;
} else {
options = { fromSetHotValue: true };
}
if (this.isUseVariable()) {
const oldValue = this.getValue();
if (isJSExpression(value)) {
this.setValue(
{
type: 'JSExpression',
value: value.value,
mock: oldValue.mock,
},
false,
false,
options,
);
} else {
this.setValue(
{
type: 'JSExpression',
value: oldValue.value,
mock: value,
},
false,
false,
options,
);
}
} else {
this.setValue(value, false, false, options);
}
// dirty fix list setter
if (Array.isArray(data) && data[0] && data[0].__sid__) {
return;
}
this.valueChange(options);
}
onEffect(action: () => void): IPublicTypeDisposable {
return this.designer!.autorun(action, true);
}
internalToShellField() {
return this.designer!.shellModelFactory.createSettingField(this);
}
}
/**
* @deprecated use same function from '@alilc/lowcode-utils' instead
*/
export function isSettingField(obj: any): obj is ISettingField {
return obj && obj.isSettingField;
}
================================================
FILE: packages/designer/src/designer/setting/setting-prop-entry.ts
================================================
import { obx, computed, makeObservable, runInAction, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
import { GlobalEvent, IPublicApiSetters, IPublicModelEditor, IPublicModelSettingField, IPublicTypeFieldExtraProps, IPublicTypeSetValueOptions } from '@alilc/lowcode-types';
import { uniqueId, isJSExpression } from '@alilc/lowcode-utils';
import { ISettingEntry } from './setting-entry-type';
import { INode } from '../../document';
import type { IComponentMeta } from '../../component-meta';
import { IDesigner } from '../designer';
import { ISettingTopEntry } from './setting-top-entry';
import { ISettingField, isSettingField } from './setting-field';
export interface ISettingPropEntry extends ISettingEntry {
readonly isGroup: boolean;
get props(): ISettingTopEntry;
get name(): string | number | undefined;
valueChange(options: IPublicTypeSetValueOptions): void;
getKey(): string | number | undefined;
setKey(key: string | number): void;
getDefaultValue(): any;
setUseVariable(flag: boolean): void;
getProps(): ISettingTopEntry;
isUseVariable(): boolean;
getMockOrValue(): any;
remove(): void;
setValue(val: any, isHotValue?: boolean, force?: boolean, extraOptions?: IPublicTypeSetValueOptions): void;
internalToShellField(): IPublicModelSettingField;
}
export class SettingPropEntry implements ISettingPropEntry {
// === static properties ===
readonly editor: IPublicModelEditor;
readonly isSameComponent: boolean;
readonly isMultiple: boolean;
readonly isSingle: boolean;
readonly setters: IPublicApiSetters;
readonly nodes: INode[];
readonly componentMeta: IComponentMeta | null;
readonly designer: IDesigner | undefined;
readonly top: ISettingTopEntry;
readonly isGroup: boolean;
readonly type: 'field' | 'group';
readonly id = uniqueId('entry');
readonly emitter: IEventBus = createModuleEventBus('SettingPropEntry');
// ==== dynamic properties ====
@obx.ref private _name: string | number | undefined;
get name() {
return this._name;
}
@computed get path() {
const path = this.parent.path.slice();
if (this.type === 'field' && this.name?.toString()) {
path.push(this.name);
}
return path;
}
extraProps: IPublicTypeFieldExtraProps = {};
constructor(readonly parent: ISettingTopEntry | ISettingField, name: string | number | undefined, type?: 'field' | 'group') {
makeObservable(this);
if (type == null) {
const c = typeof name === 'string' ? name.slice(0, 1) : '';
if (c === '#') {
this.type = 'group';
} else {
this.type = 'field';
}
} else {
this.type = type;
}
// initial self properties
this._name = name;
this.isGroup = this.type === 'group';
// copy parent static properties
this.editor = parent.editor;
this.nodes = parent.nodes;
this.setters = parent.setters;
this.componentMeta = parent.componentMeta;
this.isSameComponent = parent.isSameComponent;
this.isMultiple = parent.isMultiple;
this.isSingle = parent.isSingle;
this.designer = parent.designer;
this.top = parent.top;
}
getId() {
return this.id;
}
setKey(key: string | number) {
if (this.type !== 'field') {
return;
}
const propName = this.path.join('.');
let l = this.nodes.length;
while (l-- > 0) {
this.nodes[l].getProp(propName, true)!.key = key;
}
this._name = key;
}
getKey() {
return this._name;
}
remove() {
if (this.type !== 'field') {
return;
}
const propName = this.path.join('.');
let l = this.nodes.length;
while (l-- > 0) {
this.nodes[l].getProp(propName)?.remove();
}
}
// ====== 当前属性读写 =====
/**
* 判断当前属性值是否一致
* -1 多种值
* 0 无值
* 1 类似值,比如数组长度一样
* 2 单一植
*/
/* istanbul ignore next */
@computed get valueState(): number {
return runInAction(() => {
if (this.type !== 'field') {
const { getValue } = this.extraProps;
return getValue
? getValue(this.internalToShellField()!, undefined) === undefined
? 0
: 1
: 0;
}
if (this.nodes.length === 1) {
return 2;
}
const propName = this.path.join('.');
const first = this.nodes[0].getProp(propName)!;
let l = this.nodes.length;
let state = 2;
while (--l > 0) {
const next = this.nodes[l].getProp(propName, false);
const s = first.compare(next);
if (s > 1) {
return -1;
}
if (s === 1) {
state = 1;
}
}
if (state === 2 && first.isUnset()) {
return 0;
}
return state;
});
}
/**
* 获取当前属性值
*/
getValue(): any {
let val: any;
if (this.type === 'field' && this.name?.toString()) {
val = this.parent.getPropValue(this.name);
}
const { getValue } = this.extraProps;
try {
return getValue ? getValue(this.internalToShellField()!, val) : val;
} catch (e) {
console.warn(e);
return val;
}
}
/**
* 设置当前属性值
*/
setValue(val: any, isHotValue?: boolean, force?: boolean, extraOptions?: IPublicTypeSetValueOptions) {
const oldValue = this.getValue();
if (this.type === 'field') {
this.name?.toString() && this.parent.setPropValue(this.name, val);
}
const { setValue } = this.extraProps;
if (setValue && !extraOptions?.disableMutator) {
try {
setValue(this.internalToShellField()!, val);
} catch (e) {
/* istanbul ignore next */
console.warn(e);
}
}
this.notifyValueChange(oldValue, val);
// 如果 fromSetHotValue,那么在 setHotValue 中已经调用过 valueChange 了
if (!extraOptions?.fromSetHotValue) {
this.valueChange(extraOptions);
}
}
/**
* 清除已设置的值
*/
clearValue() {
if (this.type === 'field') {
this.name?.toString() && this.parent.clearPropValue(this.name);
}
const { setValue } = this.extraProps;
if (setValue) {
try {
setValue(this.internalToShellField()!, undefined);
} catch (e) {
/* istanbul ignore next */
console.warn(e);
}
}
}
/**
* 获取子项
*/
get(propName: string | number) {
const path = this.path.concat(propName).join('.');
return this.top.get(path);
}
/**
* 设置子级属性值
*/
setPropValue(propName: string | number, value: any) {
const path = this.path.concat(propName).join('.');
this.top.setPropValue(path, value);
}
/**
* 清除已设置值
*/
clearPropValue(propName: string | number) {
const path = this.path.concat(propName).join('.');
this.top.clearPropValue(path);
}
/**
* 获取子级属性值
*/
getPropValue(propName: string | number): any {
return this.top.getPropValue(this.path.concat(propName).join('.'));
}
/**
* 获取顶层附属属性值
*/
getExtraPropValue(propName: string) {
return this.top.getExtraPropValue(propName);
}
/**
* 设置顶层附属属性值
*/
setExtraPropValue(propName: string, value: any) {
this.top.setExtraPropValue(propName, value);
}
// ======= compatibles for vision ======
getNode() {
return this.nodes[0];
}
getName(): string {
return this.path.join('.');
}
getProps() {
return this.top;
}
// add settingfield props
get props() {
return this.top;
}
onValueChange(func: () => any) {
this.emitter.on('valuechange', func);
return () => {
this.emitter.removeListener('valuechange', func);
};
}
/**
* @deprecated
*/
valueChange(options: IPublicTypeSetValueOptions = {}) {
this.emitter.emit('valuechange', options);
if (this.parent && isSettingField(this.parent)) {
this.parent.valueChange(options);
}
}
notifyValueChange(oldValue: any, newValue: any) {
this.editor.eventBus.emit(GlobalEvent.Node.Prop.Change, {
node: this.getNode(),
prop: this,
oldValue,
newValue,
});
}
getDefaultValue() {
return this.extraProps.defaultValue;
}
isIgnore() {
return false;
}
getVariableValue() {
const v = this.getValue();
if (isJSExpression(v)) {
return v.value;
}
return '';
}
setVariableValue(value: string) {
const v = this.getValue();
this.setValue({
type: 'JSExpression',
value,
mock: isJSExpression(v) ? v.mock : v,
});
}
setUseVariable(flag: boolean) {
if (this.isUseVariable() === flag) {
return;
}
const v = this.getValue();
if (this.isUseVariable()) {
this.setValue(v.mock);
} else {
this.setValue({
type: 'JSExpression',
value: '',
mock: v,
});
}
}
isUseVariable() {
return isJSExpression(this.getValue());
}
get useVariable() {
return this.isUseVariable();
}
getMockOrValue() {
const v = this.getValue();
if (isJSExpression(v)) {
return v.mock;
}
return v;
}
internalToShellField(): IPublicModelSettingField {
return this.designer!.shellModelFactory.createSettingField(this);
}
}
================================================
FILE: packages/designer/src/designer/setting/setting-top-entry.ts
================================================
import { IPublicTypeCustomView, IPublicModelEditor, IPublicModelSettingTopEntry, IPublicApiSetters } from '@alilc/lowcode-types';
import { isCustomView } from '@alilc/lowcode-utils';
import { computed, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
import { ISettingEntry } from './setting-entry-type';
import { ISettingField, SettingField } from './setting-field';
import { INode } from '../../document';
import type { IComponentMeta } from '../../component-meta';
import { IDesigner } from '../designer';
function generateSessionId(nodes: INode[]) {
return nodes
.map((node) => node.id)
.sort()
.join(',');
}
export interface ISettingTopEntry extends ISettingEntry, IPublicModelSettingTopEntry<
INode,
ISettingField
> {
readonly top: ISettingTopEntry;
readonly parent: ISettingTopEntry;
readonly path: never[];
items: Array;
componentMeta: IComponentMeta | null;
purge(): void;
getExtraPropValue(propName: string): void;
setExtraPropValue(propName: string, value: any): void;
}
export class SettingTopEntry implements ISettingTopEntry {
private emitter: IEventBus = createModuleEventBus('SettingTopEntry');
private _items: Array = [];
private _componentMeta: IComponentMeta | null = null;
private _isSame = true;
private _settingFieldMap: { [prop: string]: ISettingField } = {};
readonly path = [];
readonly top = this;
readonly parent = this;
get componentMeta() {
return this._componentMeta;
}
get items() {
return this._items;
}
/**
* 同样的
*/
get isSameComponent(): boolean {
return this._isSame;
}
/**
* 一个
*/
get isSingle(): boolean {
return this.nodes.length === 1;
}
get isLocked(): boolean {
return this.first.isLocked;
}
/**
* 多个
*/
get isMultiple(): boolean {
return this.nodes.length > 1;
}
readonly id: string;
readonly first: INode;
readonly designer: IDesigner | undefined;
readonly setters: IPublicApiSetters;
disposeFunctions: any[] = [];
constructor(readonly editor: IPublicModelEditor, readonly nodes: INode[]) {
if (!Array.isArray(nodes) || nodes.length < 1) {
throw new ReferenceError('nodes should not be empty');
}
this.id = generateSessionId(nodes);
this.first = nodes[0];
this.designer = this.first.document?.designer;
this.setters = editor.get('setters') as IPublicApiSetters;
// setups
this.setupComponentMeta();
// clear fields
this.setupItems();
this.disposeFunctions.push(this.setupEvents());
}
private setupComponentMeta() {
// todo: enhance compile a temp configure.compiled
const { first } = this;
const meta = first.componentMeta;
const l = this.nodes.length;
let theSame = true;
for (let i = 1; i < l; i++) {
const other = this.nodes[i];
if (other.componentMeta !== meta) {
theSame = false;
break;
}
}
if (theSame) {
this._isSame = true;
this._componentMeta = meta;
} else {
this._isSame = false;
this._componentMeta = null;
}
}
private setupItems() {
if (this.componentMeta) {
const settingFieldMap: { [prop: string]: ISettingField } = {};
const settingFieldCollector = (name: string | number, field: ISettingField) => {
settingFieldMap[name] = field;
};
this._items = this.componentMeta.configure.map((item) => {
if (isCustomView(item)) {
return item;
}
return new SettingField(this, item as any, settingFieldCollector);
});
this._settingFieldMap = settingFieldMap;
}
}
private setupEvents() {
return this.componentMeta?.onMetadataChange(() => {
this.setupItems();
});
}
/**
* 获取当前属性值
*/
@computed getValue(): any {
return this.first?.propsData;
}
/**
* 设置当前属性值
*/
setValue(val: any) {
this.setProps(val);
// TODO: emit value change
}
/**
* 获取子项
*/
get(propName: string | number): ISettingField | null {
if (!propName) return null;
return this._settingFieldMap[propName] || (new SettingField(this, { name: propName }));
}
/**
* 设置子级属性值
*/
setPropValue(propName: string | number, value: any) {
this.nodes.forEach((node) => {
node.setPropValue(propName.toString(), value);
});
}
/**
* 清除已设置值
*/
clearPropValue(propName: string | number) {
this.nodes.forEach((node) => {
node.clearPropValue(propName.toString());
});
}
/**
* 获取子级属性值
*/
getPropValue(propName: string | number): any {
return this.first.getProp(propName.toString(), true)?.getValue();
}
/**
* 获取顶层附属属性值
*/
getExtraPropValue(propName: string) {
return this.first.getExtraProp(propName, false)?.getValue();
}
/**
* 设置顶层附属属性值
*/
setExtraPropValue(propName: string, value: any) {
this.nodes.forEach((node) => {
node.getExtraProp(propName, true)?.setValue(value);
});
}
// 设置多个属性值,替换原有值
setProps(data: object) {
this.nodes.forEach((node) => {
node.setProps(data as any);
});
}
// 设置多个属性值,和原有值合并
mergeProps(data: object) {
this.nodes.forEach((node) => {
node.mergeProps(data as any);
});
}
private disposeItems() {
this._items.forEach((item) => isPurgeable(item) && item.purge());
this._items = [];
}
purge() {
this.disposeItems();
this._settingFieldMap = {};
this.emitter.removeAllListeners();
this.disposeFunctions.forEach(f => f());
this.disposeFunctions = [];
}
getProp(propName: string | number) {
return this.get(propName);
}
// ==== copy some Node api =====
getStatus() {
}
setStatus() {
}
getChildren() {
// this.nodes.map()
}
getDOMNode() {
}
getId() {
return this.id;
}
getPage() {
return this.first.document;
}
/**
* @deprecated
*/
get node() {
return this.getNode();
}
getNode() {
return this.nodes[0];
}
}
interface Purgeable {
purge(): void;
}
function isPurgeable(obj: any): obj is Purgeable {
return obj && obj.purge;
}
================================================
FILE: packages/designer/src/designer/setting/utils.ts
================================================
// all this file for polyfill vision logic
import { isValidElement } from 'react';
import { IPublicTypeFieldConfig, IPublicTypeSetterConfig } from '@alilc/lowcode-types';
import { isSetterConfig, isDynamicSetter } from '@alilc/lowcode-utils';
import { ISettingField } from './setting-field';
function getHotterFromSetter(setter) {
return setter && (setter.Hotter || (setter.type && setter.type.Hotter)) || []; // eslint-disable-line
}
function getTransducerFromSetter(setter) {
return (
(setter &&
(setter.transducer ||
setter.Transducer ||
(setter.type && (setter.type.transducer || setter.type.Transducer)))) ||
null
); // eslint-disable-line
}
function combineTransducer(transducer, arr, context) {
if (!transducer && Array.isArray(arr)) {
const [toHot, toNative] = arr;
transducer = { toHot, toNative };
}
return {
toHot: (transducer && transducer.toHot || (x => x)).bind(context), // eslint-disable-line
toNative: (transducer && transducer.toNative || (x => x)).bind(context), // eslint-disable-line
};
}
export class Transducer {
setterTransducer: any;
context: any;
constructor(context: ISettingField, config: { setter: IPublicTypeFieldConfig['setter'] }) {
let { setter } = config;
// 1. validElement
// 2. IPublicTypeSetterConfig
// 3. IPublicTypeSetterConfig[]
if (Array.isArray(setter)) {
setter = setter[0];
} else if (isValidElement(setter) && setter.type.displayName === 'MixedSetter') {
setter = setter.props?.setters?.[0];
} else if (typeof setter === 'object' && setter.componentName === 'MixedSetter') {
setter = Array.isArray(setter?.props?.setters) && setter.props.setters[0];
}
/**
* 两种方式标识是 FC 而不是动态 setter
* 1. 物料描述里面 setter 的配置,显式设置为 false
* 2. registerSetter 注册 setter 时显式设置为 false
*/
let isDynamic = true;
if (isSetterConfig(setter)) {
const { componentName, isDynamic: dynamicFlag } = setter as IPublicTypeSetterConfig;
setter = componentName;
isDynamic = dynamicFlag !== false;
}
if (typeof setter === 'string') {
const { component, isDynamic: dynamicFlag } = context.setters.getSetter(setter) || {};
setter = component;
// 如果在物料配置中声明了,在 registerSetter 没有声明,取物料配置中的声明
isDynamic = dynamicFlag === undefined ? isDynamic : dynamicFlag !== false;
}
if (isDynamicSetter(setter) && isDynamic) {
try {
setter = setter.call(context.internalToShellField(), context.internalToShellField());
} catch (e) { console.error(e); }
}
this.setterTransducer = combineTransducer(getTransducerFromSetter(setter), getHotterFromSetter(setter), context);
this.context = context;
}
toHot(data) {
return this.setterTransducer.toHot(data);
}
toNative(data) {
return this.setterTransducer.toNative(data);
}
}
================================================
FILE: packages/designer/src/document/document-model.ts
================================================
import {
makeObservable,
obx,
engineConfig,
action,
runWithGlobalEventOff,
wrapWithEventSwitch,
createModuleEventBus,
IEventBus,
} from '@alilc/lowcode-editor-core';
import {
IPublicTypeNodeData,
IPublicTypeNodeSchema,
IPublicTypePageSchema,
IPublicTypeComponentsMap,
IPublicTypeDragNodeObject,
IPublicTypeDragNodeDataObject,
IPublicModelDocumentModel,
IPublicEnumTransformStage,
IPublicTypeOnChangeOptions,
IPublicTypeDisposable,
} from '@alilc/lowcode-types';
import type {
IPublicTypeRootSchema,
} from '@alilc/lowcode-types';
import type {
IDropLocation,
} from '@alilc/lowcode-designer';
import {
uniqueId,
isPlainObject,
compatStage,
isJSExpression,
isDOMText,
isNodeSchema,
isDragNodeObject,
isDragNodeDataObject,
isNode,
} from '@alilc/lowcode-utils';
import { IProject } from '../project';
import { ISimulatorHost } from '../simulator';
import type { IComponentMeta } from '../component-meta';
import { IDesigner, IHistory } from '../designer';
import { insertChildren, insertChild, IRootNode } from './node/node';
import type { INode } from './node/node';
import { Selection, ISelection } from './selection';
import { History } from './history';
import { IModalNodesManager, ModalNodesManager, Node } from './node';
import { EDITOR_EVENT } from '../types';
export type GetDataType = T extends undefined
? NodeType extends {
schema: infer R;
}
? R
: any
: T;
export interface IDocumentModel extends Omit,
'detecting' |
'checkNesting' |
'getNodeById' |
// 以下属性在内部的 document 中不存在
'exportSchema' |
'importSchema' |
'onAddNode' |
'onRemoveNode' |
'onChangeDetecting' |
'onChangeSelection' |
'onChangeNodeProp' |
'onImportSchema' |
'isDetectingNode' |
'onFocusNodeChanged' |
'onDropLocationChanged'
> {
readonly designer: IDesigner;
selection: ISelection;
get rootNode(): INode | null;
get simulator(): ISimulatorHost | null;
get active(): boolean;
get nodesMap(): Map;
/**
* 是否为非激活状态
*/
get suspensed(): boolean;
get fileName(): string;
get currentRoot(): INode | null;
isBlank(): boolean;
/**
* 根据 id 获取节点
*/
getNode(id: string): INode | null;
getRoot(): INode | null;
getHistory(): IHistory;
checkNesting(
dropTarget: INode,
dragObject: IPublicTypeDragNodeObject | IPublicTypeNodeSchema | INode | IPublicTypeDragNodeDataObject,
): boolean;
getNodeCount(): number;
nextId(possibleId: string | undefined): string;
import(schema: IPublicTypeRootSchema, checkId?: boolean): void;
export(stage: IPublicEnumTransformStage): IPublicTypeRootSchema | undefined;
onNodeCreate(func: (node: INode) => void): IPublicTypeDisposable;
onNodeDestroy(func: (node: INode) => void): IPublicTypeDisposable;
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[];
open(): IDocumentModel;
remove(): void;
suspense(): void;
close(): void;
unlinkNode(node: INode): void;
destroyNode(node: INode): void;
}
export class DocumentModel implements IDocumentModel {
/**
* 根节点 类型有:Page/Component/Block
*/
rootNode: IRootNode | null;
/**
* 文档编号
*/
id: string = uniqueId('doc');
/**
* 选区控制
*/
readonly selection: ISelection = new Selection(this);
/**
* 操作记录控制
*/
readonly history: IHistory;
/**
* 模态节点管理
*/
modalNodesManager: IModalNodesManager;
private _nodesMap = new Map();
readonly project: IProject;
readonly designer: IDesigner;
@obx.shallow private nodes = new Set();
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 {
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(): INode | null {
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: INode | null = null;
private _modalNode?: INode;
private _blank?: boolean;
private inited = false;
@obx.shallow private willPurgeSpace: INode[] = [];
get modalNode() {
return this._modalNode;
}
get currentRoot() {
return this.modalNode || this.focusNode;
}
@obx.shallow private activeNodes?: INode[];
@obx.ref private _dropLocation: IDropLocation | null = null;
set dropLocation(loc: IDropLocation | null) {
this._dropLocation = loc;
// pub event
this.designer.editor.eventBus.emit(
'document.dropLocation.changed',
{ document: this, location: loc },
);
}
/**
* 投放插入位置标记
*/
get dropLocation() {
return this._dropLocation;
}
/**
* 导出 schema 数据
*/
get schema(): IPublicTypeRootSchema {
return this.rootNode?.schema as any;
}
@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;
}
get root() {
return this.rootNode;
}
constructor(project: IProject, 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(
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,
);
this.setupListenActiveNodes();
this.modalNodesManager = new ModalNodesManager(this);
this.inited = true;
}
drillDown(node: INode | null) {
this._drillDownNode = node;
}
onChangeNodeVisible(fn: (node: INode, visible: boolean) => void): IPublicTypeDisposable {
this.designer.editor?.eventBus.on(EDITOR_EVENT.NODE_VISIBLE_CHANGE, fn);
return () => {
this.designer.editor?.eventBus.off(EDITOR_EVENT.NODE_VISIBLE_CHANGE, fn);
};
}
onChangeNodeChildren(fn: (info: IPublicTypeOnChangeOptions) => void): IPublicTypeDisposable {
this.designer.editor?.eventBus.on(EDITOR_EVENT.NODE_CHILDREN_CHANGE, fn);
return () => {
this.designer.editor?.eventBus.off(EDITOR_EVENT.NODE_CHILDREN_CHANGE, fn);
};
}
addWillPurge(node: INode) {
this.willPurgeSpace.push(node);
}
removeWillPurge(node: INode) {
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): string {
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): INode | 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;
}
onMountNode(fn: (payload: { node: INode }) => void) {
this.designer.editor.eventBus.on('node.add', fn as any);
return () => {
this.designer.editor.eventBus.off('node.add', fn as any);
};
}
/**
* 根据 schema 创建一个节点
*/
@action
createNode(data: GetDataType): T {
let schema: any;
if (isDOMText(data) || isJSExpression(data)) {
schema = {
componentName: 'Leaf',
children: data,
};
} else {
schema = data;
}
let node: INode | 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);
// 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: INode) {
this.emitter.emit('nodedestroy', node);
}
/**
* 插入一个节点
*/
insertNode(parent: INode, thing: INode | IPublicTypeNodeData, at?: number | null, copy?: boolean): INode | null {
return insertChild(parent, thing, at, copy);
}
/**
* 插入多个节点
*/
insertNodes(parent: INode, thing: INode[] | IPublicTypeNodeData[], at?: number | null, copy?: boolean) {
return insertChildren(parent, thing, at, copy);
}
/**
* 移除一个节点
*/
removeNode(idOrNode: string | INode) {
let id: string;
let node: INode | null = 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: INode, useMutator = false) {
if (!this.nodes.has(node)) {
return;
}
node.remove(useMutator);
}
unlinkNode(node: INode) {
this.nodes.delete(node);
this._nodesMap.delete(node.id);
}
/**
* 包裹当前选区中的节点
*/
wrapWith(schema: IPublicTypeNodeSchema): INode | 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;
}
@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): IPublicTypeRootSchema | undefined {
stage = compatStage(stage);
// 置顶只作用于 Page 的第一级子节点,目前还用不到里层的置顶;如果后面有需要可以考虑将这段写到 node-children 中的 export
const currentSchema = this.rootNode?.export(stage);
if (Array.isArray(currentSchema?.children) && currentSchema?.children?.length && 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(): boolean {
return this.history.isSavePoint();
}
// FIXME: does needed?
getComponent(componentName: string): any {
return this.simulator!.getComponent(componentName);
}
getComponentMeta(componentName: string): IComponentMeta {
return this.designer.getComponentMeta(
componentName,
() => this.simulator?.generateComponentMetadata(componentName) || null,
);
}
/**
* 切换激活,只有打开的才能激活
* 不激活,打开之后切换到另外一个时发生,比如 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 | INode | IPublicTypeDragNodeDataObject,
): boolean {
let items: Array;
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;
if (isDragNodeDataObject(dragObject)) {
items = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data];
} else if (isDragNodeObject(dragObject)) {
items = dragObject.nodes;
} else {
return false;
}
return items.every((item) => this.checkNestingUp(dropTarget, item));
}
/**
* 检查对象对父级的要求,涉及配置 parentWhitelist
*/
checkNestingUp(parent: INode, obj: IPublicTypeNodeSchema | INode): 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 | INode): 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(): IHistory {
return this.history;
}
/**
* @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: {
[key: string]: any;
} = {};
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: IRootNode) => any,
) {
let visitorResult = {};
if (!visitorName) {
/* eslint-disable-next-line no-console */
console.warn('Invalid or empty RootNodeVisitor name.');
}
try {
if (this.rootNode) {
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((componentName) => {
if (componentName && !exsitingMap[componentName]) {
const meta = this.getComponentMeta(componentName);
if (meta?.npm?.package) {
componentsMap.push({
...meta?.npm,
componentName,
});
} else {
componentsMap.push({
devMode: 'lowCode',
componentName,
});
}
}
});
}
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: INode) => void) {
const wrappedFunc = wrapWithEventSwitch(func);
this.emitter.on('nodecreate', wrappedFunc);
return () => {
this.emitter.removeListener('nodecreate', wrappedFunc);
};
}
onNodeDestroy(func: (node: INode) => 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: (...args: any[]) => void) {
this.designer.editor.eventBus.on('document-open', fn);
return () => {
this.designer.editor.eventBus.off('document-open', fn);
};
}
private setupListenActiveNodes() {
// todo:
}
}
export function isDocumentModel(obj: any): obj is IDocumentModel {
return obj && obj.rootNode;
}
export function isPageSchema(obj: any): obj is IPublicTypePageSchema {
return obj?.componentName === 'Page';
}
================================================
FILE: packages/designer/src/document/document-view.tsx
================================================
import { Component } from 'react';
import classNames from 'classnames';
import { observer } from '@alilc/lowcode-editor-core';
import { DocumentModel, IDocumentModel } from './document-model';
import { BuiltinSimulatorHostView } from '../builtin-simulator';
@observer
export class DocumentView extends Component<{ document: IDocumentModel }> {
render() {
const { document } = this.props;
const { simulatorProps } = document;
const Simulator = document.designer.simulatorComponent || BuiltinSimulatorHostView;
return (
);
}
}
class DocumentInfoView extends Component<{ document: IDocumentModel }> {
render() {
return null;
}
}
================================================
FILE: packages/designer/src/document/history.ts
================================================
import { reaction, untracked, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
import { IPublicTypeNodeSchema, IPublicModelHistory, IPublicTypeDisposable } from '@alilc/lowcode-types';
import { Logger } from '@alilc/lowcode-utils';
import { IDocumentModel } from '../designer';
const logger = new Logger({ level: 'warn', bizName: 'history' });
export interface Serialization {
serialize(data: K): T;
unserialize(data: T): K;
}
export interface IHistory extends IPublicModelHistory {
onStateChange(func: () => any): IPublicTypeDisposable;
}
export class History implements IHistory {
private session: Session;
private records: Session[];
private point = 0;
private emitter: IEventBus = createModuleEventBus('History');
private asleep = false;
private currentSerialization: Serialization = {
serialize(data: T): string {
return JSON.stringify(data);
},
unserialize(data: string) {
return JSON.parse(data);
},
};
get hotData() {
return this.session.data;
}
private timeGap: number = 1000;
constructor(
dataFn: () => T | null,
private redoer: (data: T) => void,
private document?: IDocumentModel,
) {
this.session = new Session(0, null, this.timeGap);
this.records = [this.session];
reaction((): any => {
return dataFn();
}, (data: T) => {
if (this.asleep) return;
untracked(() => {
const log = this.currentSerialization.serialize(data);
// do not record unchanged data
if (this.session.data === log) {
return;
}
if (this.session.isActive()) {
this.session.log(log);
} else {
this.session.end();
const lastState = this.getState();
const cursor = this.session.cursor + 1;
const session = new Session(cursor, log, this.timeGap);
this.session = session;
this.records.splice(cursor, this.records.length - cursor, session);
const currentState = this.getState();
if (currentState !== lastState) {
this.emitter.emit('statechange', currentState);
}
}
});
}, { fireImmediately: true });
}
setSerialization(serialization: Serialization) {
this.currentSerialization = serialization;
}
isSavePoint(): boolean {
return this.point !== this.session.cursor;
}
private sleep() {
this.asleep = true;
}
private wakeup() {
this.asleep = false;
}
go(originalCursor: number) {
this.session.end();
let cursor = originalCursor;
cursor = +cursor;
if (cursor < 0) {
cursor = 0;
} else if (cursor >= this.records.length) {
cursor = this.records.length - 1;
}
const currentCursor = this.session.cursor;
if (cursor === currentCursor) {
return;
}
const session = this.records[cursor];
const hotData = session.data;
this.sleep();
try {
this.redoer(this.currentSerialization.unserialize(hotData));
this.emitter.emit('cursor', hotData);
} catch (e) /* istanbul ignore next */ {
logger.error(e);
}
this.wakeup();
this.session = session;
this.emitter.emit('statechange', this.getState());
}
back() {
if (!this.session) {
return;
}
const cursor = this.session.cursor - 1;
this.go(cursor);
const editor = this.document?.designer.editor;
if (!editor) {
return;
}
editor.eventBus.emit('history.back', cursor);
}
forward() {
if (!this.session) {
return;
}
const cursor = this.session.cursor + 1;
this.go(cursor);
const editor = this.document?.designer.editor;
if (!editor) {
return;
}
editor.eventBus.emit('history.forward', cursor);
}
savePoint() {
if (!this.session) {
return;
}
this.session.end();
this.point = this.session.cursor;
this.emitter.emit('statechange', this.getState());
}
/**
* | 1 | 1 | 1 |
* | -------- | -------- | -------- |
* | modified | redoable | undoable |
*/
getState(): number {
const { cursor } = this.session;
let state = 7;
// undoable ?
if (cursor <= 0) {
state -= 1;
}
// redoable ?
if (cursor >= this.records.length - 1) {
state -= 2;
}
// modified ?
if (this.point === cursor) {
state -= 4;
}
return state;
}
/**
* 监听 state 变更事件
* @param func
* @returns
*/
onChangeState(func: () => any): IPublicTypeDisposable {
return this.onStateChange(func);
}
onStateChange(func: () => any): IPublicTypeDisposable {
this.emitter.on('statechange', func);
return () => {
this.emitter.removeListener('statechange', func);
};
}
/**
* 监听历史记录游标位置变更事件
* @param func
* @returns
*/
onChangeCursor(func: () => any): IPublicTypeDisposable {
return this.onCursor(func);
}
onCursor(func: () => any): () => void {
this.emitter.on('cursor', func);
return () => {
this.emitter.removeListener('cursor', func);
};
}
destroy() {
this.emitter.removeAllListeners();
this.records = [];
}
/**
*
* @deprecated
* @returns
* @memberof History
*/
isModified() {
return this.isSavePoint();
}
}
export class Session {
private _data: any;
private activeTimer: any;
get data() {
return this._data;
}
constructor(readonly cursor: number, data: any, private timeGap: number = 1000) {
this.setTimer();
this.log(data);
}
log(data: any) {
if (!this.isActive()) {
return;
}
this._data = data;
this.setTimer();
}
isActive() {
return this.activeTimer != null;
}
end() {
if (this.isActive()) {
this.clearTimer();
}
}
private setTimer() {
this.clearTimer();
this.activeTimer = setTimeout(() => this.end(), this.timeGap);
}
private clearTimer() {
if (this.activeTimer) {
clearTimeout(this.activeTimer);
}
this.activeTimer = null;
}
}
================================================
FILE: packages/designer/src/document/index.ts
================================================
export * from './document-model';
export * from './node';
export * from './selection';
export * from './history';
================================================
FILE: packages/designer/src/document/selection.ts
================================================
import { obx, makeObservable, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
import { INode, comparePosition, PositionNO } from './node/node';
import { DocumentModel } from './document-model';
import { IPublicModelSelection } from '@alilc/lowcode-types';
export interface ISelection extends Omit, 'node'> {
containsNode(node: INode, excludeRoot: boolean): boolean;
}
export class Selection implements ISelection {
private emitter: IEventBus = createModuleEventBus('Selection');
@obx.shallow private _selected: string[] = [];
constructor(readonly doc: DocumentModel) {
makeObservable(this);
}
/**
* 选中的节点 id
*/
get selected(): string[] {
return this._selected;
}
/**
* 选中
*/
select(id: string) {
if (this._selected.length === 1 && this._selected.indexOf(id) > -1) {
// avoid cause reaction
return;
}
const node = this.doc.getNode(id);
if (!node?.canSelect()) {
return;
}
this._selected = [id];
this.emitter.emit('selectionchange', this._selected);
}
/**
* 批量选中
*/
selectAll(ids: string[]) {
const selectIds: string[] = [];
ids.forEach(d => {
const node = this.doc.getNode(d);
if (node?.canSelect()) {
selectIds.push(d);
}
});
this._selected = selectIds;
this.emitter.emit('selectionchange', this._selected);
}
/**
* 清除选中
*/
clear() {
if (this._selected.length < 1) {
return;
}
this._selected = [];
this.emitter.emit('selectionchange', this._selected);
}
/**
* 整理选中
*/
dispose() {
const l = this._selected.length;
let i = l;
while (i-- > 0) {
const id = this._selected[i];
if (!this.doc.hasNode(id)) {
this._selected.splice(i, 1);
}
}
if (this._selected.length !== l) {
this.emitter.emit('selectionchange', this._selected);
}
}
/**
* 添加选中
*/
add(id: string) {
if (this._selected.indexOf(id) > -1) {
return;
}
this._selected.push(id);
this.emitter.emit('selectionchange', this._selected);
}
/**
* 是否选中
*/
has(id: string) {
return this._selected.indexOf(id) > -1;
}
/**
* 移除选中
*/
remove(id: string) {
const i = this._selected.indexOf(id);
if (i > -1) {
this._selected.splice(i, 1);
this.emitter.emit('selectionchange', this._selected);
}
}
/**
* 选区是否包含节点
*/
containsNode(node: INode, excludeRoot = false) {
for (const id of this._selected) {
const parent = this.doc.getNode(id);
if (excludeRoot && parent?.contains(this.doc.focusNode)) {
continue;
}
if (parent?.contains(node)) {
return true;
}
}
return false;
}
/**
* 获取选中的节点
*/
getNodes(): INode[] {
const nodes: INode[] = [];
for (const id of this._selected) {
const node = this.doc.getNode(id);
if (node) {
nodes.push(node);
}
}
return nodes;
}
/**
* 获取顶层选区节点,场景:拖拽时,建立蒙层,只蒙在最上层
*/
getTopNodes(includeRoot = false) {
const nodes = [];
for (const id of this._selected) {
const node = this.doc.getNode(id);
// 排除根节点
if (!node || (!includeRoot && node.contains(this.doc.focusNode))) {
continue;
}
let i = nodes.length;
let isTop = true;
while (i-- > 0) {
const n = comparePosition(nodes[i], node);
// nodes[i] contains node
if (n === PositionNO.Contains || n === PositionNO.TheSame) {
isTop = false;
break;
} else if (n === PositionNO.ContainedBy) {
// node contains nodes[i], delete nodes[i]
nodes.splice(i, 1);
}
}
// node is top item, push to nodes
if (isTop) {
nodes.push(node);
}
}
return nodes;
}
onSelectionChange(fn: (ids: string[]) => void): () => void {
this.emitter.on('selectionchange', fn);
return () => {
this.emitter.removeListener('selectionchange', fn);
};
}
}
================================================
FILE: packages/designer/src/document/node/exclusive-group.ts
================================================
import { obx, computed, makeObservable } from '@alilc/lowcode-editor-core';
import { uniqueId } from '@alilc/lowcode-utils';
import { IPublicTypeTitleContent, IPublicModelExclusiveGroup } from '@alilc/lowcode-types';
import type { INode } from './node';
import { intl } from '../../locale';
export interface IExclusiveGroup extends IPublicModelExclusiveGroup {
readonly name: string;
get index(): number | undefined;
remove(node: INode): void;
add(node: INode): void;
isVisible(node: INode): boolean;
get length(): number;
get visibleNode(): INode;
}
// 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 IExclusiveGroup {
readonly isExclusiveGroup = true;
readonly id = uniqueId('exclusive');
readonly title: IPublicTypeTitleContent;
@obx.shallow readonly children: INode[] = [];
@obx private visibleIndex = 0;
@computed get document() {
return this.visibleNode.document;
}
@computed get zLevel() {
return this.visibleNode.zLevel;
}
@computed get length() {
return this.children.length;
}
@computed get visibleNode(): INode {
return this.children[this.visibleIndex];
}
@computed get firstNode(): INode {
return this.children[0]!;
}
get index() {
return this.firstNode.index;
}
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 {
this.children.push(node);
}
}
remove(node: INode) {
const i = this.children.indexOf(node);
if (i > -1) {
this.children.splice(i, 1);
if (this.visibleIndex > i) {
this.visibleIndex -= 1;
} else if (this.visibleIndex >= this.children.length) {
this.visibleIndex = this.children.length - 1;
}
}
}
setVisible(node: INode) {
const i = this.children.indexOf(node);
if (i > -1) {
this.visibleIndex = i;
}
}
isVisible(node: INode) {
const i = this.children.indexOf(node);
return i === this.visibleIndex;
}
}
export function isExclusiveGroup(obj: any): obj is ExclusiveGroup {
return obj && obj.isExclusiveGroup;
}
================================================
FILE: packages/designer/src/document/node/index.ts
================================================
export * from './exclusive-group';
export * from './node';
export * from './node-children';
export * from './props/prop';
export * from './props/props';
export * from './transform-stage';
export * from './modal-nodes-manager';
================================================
FILE: packages/designer/src/document/node/modal-nodes-manager.ts
================================================
import { INode } from './node';
import { DocumentModel } from '../document-model';
import { IPublicModelModalNodesManager } from '@alilc/lowcode-types';
import { createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core';
export function getModalNodes(node: INode) {
if (!node) return [];
let nodes: any = [];
if (node.componentMeta.isModal) {
nodes.push(node);
}
const { children } = node;
if (children) {
children.forEach((child) => {
nodes = nodes.concat(getModalNodes(child));
});
}
return nodes;
}
export interface IModalNodesManager extends IPublicModelModalNodesManager {
}
export class ModalNodesManager implements IModalNodesManager {
willDestroy: any;
private page: DocumentModel;
private modalNodes: INode[];
private nodeRemoveEvents: any;
private emitter: IEventBus;
constructor(page: DocumentModel) {
this.page = page;
this.emitter = createModuleEventBus('ModalNodesManager');
this.nodeRemoveEvents = {};
this.setNodes();
this.hideModalNodes();
this.willDestroy = [
page.onNodeCreate((node) => this.addNode(node)),
page.onNodeDestroy((node) => this.removeNode(node)),
];
}
getModalNodes(): INode[] {
return this.modalNodes;
}
getVisibleModalNode(): INode | null {
const visibleNode = this.getModalNodes().find((node: INode) => node.getVisible());
return visibleNode || null;
}
hideModalNodes() {
this.modalNodes.forEach((node: INode) => {
node.setVisible(false);
});
}
setVisible(node: INode) {
this.hideModalNodes();
node.setVisible(true);
}
setInvisible(node: INode) {
node.setVisible(false);
}
onVisibleChange(func: () => any) {
this.emitter.on('visibleChange', func);
return () => {
this.emitter.removeListener('visibleChange', func);
};
}
onModalNodesChange(func: () => any) {
this.emitter.on('modalNodesChange', func);
return () => {
this.emitter.removeListener('modalNodesChange', func);
};
}
private addNode(node: INode) {
if (node?.componentMeta.isModal) {
this.hideModalNodes();
this.modalNodes.push(node);
this.addNodeEvent(node);
this.emitter.emit('modalNodesChange');
this.emitter.emit('visibleChange');
}
}
private removeNode(node: INode) {
if (node.componentMeta.isModal) {
const index = this.modalNodes.indexOf(node);
if (index >= 0) {
this.modalNodes.splice(index, 1);
}
this.removeNodeEvent(node);
this.emitter.emit('modalNodesChange');
if (node.getVisible()) {
this.emitter.emit('visibleChange');
}
}
}
private addNodeEvent(node: INode) {
this.nodeRemoveEvents[node.id] =
node.onVisibleChange(() => {
this.emitter.emit('visibleChange');
});
}
private removeNodeEvent(node: INode) {
if (this.nodeRemoveEvents[node.id]) {
this.nodeRemoveEvents[node.id]();
delete this.nodeRemoveEvents[node.id];
}
}
setNodes() {
const nodes = getModalNodes(this.page.rootNode!);
this.modalNodes = nodes;
this.modalNodes.forEach((node: INode) => {
this.addNodeEvent(node);
});
this.emitter.emit('modalNodesChange');
}
}
================================================
FILE: packages/designer/src/document/node/node-children.ts
================================================
import { obx, computed, makeObservable, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
import { Node, INode } from './node';
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';
export interface IOnChangeOptions {
type: string;
node: Node;
}
export interface INodeChildren extends Omit,
'importSchema' |
'exportSchema' |
'isEmpty' |
'notEmpty'
> {
children: INode[];
get owner(): INode;
get length(): number;
unlinkChild(node: INode): void;
/**
* 删除一个节点
*/
internalDelete(
node: INode,
purge: boolean,
useMutator: boolean,
options: NodeRemoveOptions
): boolean;
/**
* 插入一个节点,返回新长度
*/
internalInsert(node: INode, at?: number | null, useMutator?: boolean): void;
import(data?: IPublicTypeNodeData | IPublicTypeNodeData[], checkId?: boolean): void;
/**
* 导出 schema
*/
export(stage: IPublicEnumTransformStage): IPublicTypeNodeData[];
/** following methods are overriding super interface, using different param types */
/** overriding methods start */
forEach(fn: (item: INode, index: number) => void): void;
/**
* 根据索引获得节点
*/
get(index: number): INode | null;
isEmpty(): boolean;
notEmpty(): boolean;
internalInitParent(): void;
onChange(fn: (info?: IOnChangeOptions) => void): IPublicTypeDisposable;
/** overriding methods end */
}
export class NodeChildren implements INodeChildren {
@obx.shallow children: INode[];
private emitter: IEventBus = createModuleEventBus('NodeChildren');
/**
* 元素个数
*/
@computed get size(): number {
return this.children.length;
}
get isEmptyNode(): boolean {
return this.size < 1;
}
get notEmptyNode(): boolean {
return this.size > 0;
}
@computed get length(): number {
return this.children.length;
}
private purged = false;
get [Symbol.toStringTag]() {
// 保证向前兼容性
return 'Array';
}
constructor(
readonly owner: INode,
data: IPublicTypeNodeData | IPublicTypeNodeData[],
options: any = {},
) {
makeObservable(this);
this.children = (Array.isArray(data) ? data : [data]).filter(child => !!child).map((child) => {
return this.owner.document?.createNode(child, options.checkId);
});
}
internalInitParent() {
this.children.forEach((child) => child.internalSetParent(this.owner));
}
/**
* 导出 schema
*/
export(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save): IPublicTypeNodeData[] {
stage = compatStage(stage);
return this.children.map((node) => {
const data = node.export(stage);
if (node.isLeafNode && IPublicEnumTransformStage.Save === stage) {
// FIXME: filter empty
return data.children as IPublicTypeNodeData;
}
return data;
});
}
import(data?: IPublicTypeNodeData | IPublicTypeNodeData[], checkId = false) {
data = (data ? (Array.isArray(data) ? data : [data]) : []).filter(d => !!d);
const originChildren = this.children.slice();
this.children.forEach((child) => child.internalSetParent(null));
const children = new Array(data.length);
for (let i = 0, l = data.length; i < l; i++) {
const child = originChildren[i];
const item = data[i];
let node: INode | undefined | null;
if (isNodeSchema(item) && !checkId && child && child.componentName === item.componentName) {
node = child;
node.import(item);
} else {
node = this.owner.document?.createNode(item, checkId);
}
children[i] = node;
}
this.children = children;
this.internalInitParent();
if (!shallowEqual(children, originChildren)) {
this.emitter.emit('change');
}
}
/**
* @deprecated
* @param nodes
*/
concat(nodes: INode[]) {
return this.children.concat(nodes);
}
/**
*
*/
isEmpty() {
return this.isEmptyNode;
}
notEmpty() {
return this.notEmptyNode;
}
/**
* 回收销毁
*/
purge(useMutator = true) {
if (this.purged) {
return;
}
this.purged = true;
this.children.forEach((child) => {
child.purge(useMutator);
});
}
unlinkChild(node: INode) {
const i = this.children.map(d => d.id).indexOf(node.id);
if (i < 0) {
return false;
}
this.children.splice(i, 1);
this.emitter.emit('change', {
type: 'unlink',
node,
});
}
/**
* 删除一个节点
*/
delete(node: INode): boolean {
return this.internalDelete(node);
}
/**
* 删除一个节点
*/
internalDelete(node: INode, purge = false, useMutator = true, options: NodeRemoveOptions = {}): boolean {
node.internalPurgeStart();
if (node.isParentalNode) {
foreachReverse(
node.children,
(subNode: Node) => {
subNode.remove(useMutator, purge, options);
},
(iterable, idx) => (iterable as NodeChildren).get(idx),
);
foreachReverse(
node.slots,
(slotNode: Node) => {
slotNode.remove(useMutator, purge);
},
(iterable, idx) => (iterable as [])[idx],
);
}
// 需要在从 children 中删除 node 前记录下 index,internalSetParent 中会执行删除 (unlink) 操作
const i = this.children.map(d => d.id).indexOf(node.id);
if (purge) {
// should set parent null
node.internalSetParent(null, useMutator);
try {
node.purge();
} catch (err) {
console.error(err);
}
}
const { document } = node;
/* istanbul ignore next */
const editor = node.document?.designer.editor;
editor?.eventBus.emit('node.remove', { node, index: i });
document?.unlinkNode(node);
document?.selection.remove(node.id);
document?.destroyNode(node);
this.emitter.emit('change', {
type: 'delete',
node,
});
if (useMutator) {
this.reportModified(node, this.owner, {
type: 'remove',
propagated: false,
isSubDeleting: this.owner.isPurging,
removeIndex: i,
removeNode: node,
});
}
// purge 为 true 时,已在 internalSetParent 中删除了子节点
if (i > -1 && !purge) {
this.children.splice(i, 1);
}
return false;
}
insert(node: INode, at?: number | null): void {
this.internalInsert(node, at, true);
}
/**
* 插入一个节点,返回新长度
*/
internalInsert(node: INode, at?: number | null, useMutator = true): void {
const { children } = this;
let index = at == null || at === -1 ? children.length : at;
const i = children.map(d => d.id).indexOf(node.id);
if (node.parent) {
const editor = node.document?.designer.editor;
editor?.eventBus.emit('node.remove.topLevel', {
node,
index: node.index,
});
}
if (i < 0) {
if (index < children.length) {
children.splice(index, 0, node);
} else {
children.push(node);
}
node.internalSetParent(this.owner, useMutator);
} else {
if (index > i) {
index -= 1;
}
if (index === i) {
return;
}
children.splice(i, 1);
children.splice(index, 0, node);
}
this.emitter.emit('change', {
type: 'insert',
node,
});
this.emitter.emit('insert', node);
/* istanbul ignore next */
const editor = node.document?.designer.editor;
editor?.eventBus.emit('node.add', { node });
if (useMutator) {
this.reportModified(node, this.owner, { type: 'insert' });
}
// check condition group
if (node.conditionGroup) {
if (
!(
// just sort at condition group
(
(node.prevSibling && node.prevSibling.conditionGroup === node.conditionGroup) ||
(node.nextSibling && node.nextSibling.conditionGroup === node.conditionGroup)
)
)
) {
node.setConditionGroup(null);
}
}
if (node.prevSibling && node.nextSibling) {
const { conditionGroup } = node.prevSibling;
// insert at condition group
if (conditionGroup && conditionGroup === node.nextSibling.conditionGroup) {
node.setConditionGroup(conditionGroup);
}
}
}
/**
* 取得节点索引编号
*/
indexOf(node: INode): number {
return this.children.map(d => d.id).indexOf(node.id);
}
/**
*
*/
splice(start: number, deleteCount: number, node?: INode): INode[] {
if (node) {
return this.children.splice(start, deleteCount, node);
}
return this.children.splice(start, deleteCount);
}
/**
* 根据索引获得节点
*/
get(index: number): INode | null {
return this.children.length > index ? this.children[index] : null;
}
/**
* 是否存在节点
*/
has(node: INode) {
return this.indexOf(node) > -1;
}
/**
* 迭代器
*/
[Symbol.iterator](): { next(): { value: INode } } {
let index = 0;
const { children } = this;
const length = children.length || 0;
return {
next() {
if (index < length) {
return {
value: children[index++],
done: false,
};
}
return {
value: undefined as any,
done: true,
};
},
};
}
/**
* 遍历
*/
forEach(fn: (item: INode, index: number) => void): void {
this.children.forEach((child, index) => {
return fn(child, index);
});
}
/**
* 遍历
*/
map(fn: (item: INode, index: number) => T): T[] | null {
return this.children.map((child, index) => {
return fn(child, index);
});
}
every(fn: (item: INode, index: number) => any): boolean {
return this.children.every((child, index) => fn(child, index));
}
some(fn: (item: INode, index: number) => any): boolean {
return this.children.some((child, index) => fn(child, index));
}
filter(fn: (item: INode, index: number) => any): any {
return this.children.filter(fn);
}
find(fn: (item: INode, index: number) => boolean): INode | undefined {
return this.children.find(fn);
}
reduce(fn: (acc: any, cur: INode) => any, initialValue: any): void {
return this.children.reduce(fn, initialValue);
}
reverse() {
return this.children.reverse();
}
mergeChildren(
remover: (node: INode, idx: number) => boolean,
adder: (children: INode[]) => IPublicTypeNodeData[] | null,
sorter: (firstNode: INode, secondNode: INode) => number,
): any {
let changed = false;
if (remover) {
const willRemove = this.children.filter(remover);
if (willRemove.length > 0) {
willRemove.forEach((node) => {
const i = this.children.map(d => d.id).indexOf(node.id);
if (i > -1) {
this.children.splice(i, 1);
node.remove(false);
}
});
changed = true;
}
}
if (adder) {
const items = adder(this.children);
if (items && items.length > 0) {
items.forEach((child: IPublicTypeNodeData) => {
const node: INode = this.owner.document?.createNode(child);
this.children.push(node);
node.internalSetParent(this.owner);
/* istanbul ignore next */
const editor = node.document?.designer.editor;
editor?.eventBus.emit('node.add', { node });
});
changed = true;
}
}
if (sorter) {
this.children = this.children.sort(sorter);
changed = true;
}
if (changed) {
this.emitter.emit('change');
}
}
onChange(fn: (info?: IOnChangeOptions) => void): IPublicTypeDisposable {
this.emitter.on('change', fn);
return () => {
this.emitter.removeListener('change', fn);
};
}
onInsert(fn: (node: INode) => void) {
this.emitter.on('insert', fn);
return () => {
this.emitter.removeListener('insert', fn);
};
}
private reportModified(node: INode, owner: INode, options = {}) {
if (!node) {
return;
}
if (node.isRootNode) {
return;
}
const callbacks = owner.componentMeta?.advanced.callbacks;
if (callbacks?.onSubtreeModified) {
try {
callbacks?.onSubtreeModified.call(
node.internalToShellNode(),
owner.internalToShellNode(),
options,
);
} catch (e) {
console.error('error when execute advanced.callbacks.onSubtreeModified', e);
}
}
if (owner.parent && !owner.parent.isRootNode) {
this.reportModified(node, owner.parent, { ...options, propagated: true });
}
}
}
================================================
FILE: packages/designer/src/document/node/node.ts
================================================
import { ReactElement } from 'react';
import { obx, computed, autorun, makeObservable, runInAction, wrapWithEventSwitch, action, createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core';
import {
IPublicTypeNodeSchema,
IPublicTypePropsMap,
IPublicTypePropsList,
IPublicTypeNodeData,
IPublicTypeI18nData,
IPublicTypeSlotSchema,
IPublicTypePageSchema,
IPublicTypeComponentSchema,
IPublicTypeCompositeValue,
GlobalEvent,
IPublicTypeComponentAction,
IPublicModelNode,
IPublicModelExclusiveGroup,
IPublicEnumTransformStage,
IPublicTypeDisposable,
IBaseModelNode,
} from '@alilc/lowcode-types';
import { compatStage, isDOMText, isJSExpression, isNode, isNodeSchema } from '@alilc/lowcode-utils';
import { ISettingTopEntry } from '@alilc/lowcode-designer';
import { Props, getConvertedExtraKey, IProps } from './props/props';
import type { IDocumentModel } from '../document-model';
import { NodeChildren, INodeChildren } from './node-children';
import { IProp, Prop } from './props/prop';
import type { IComponentMeta } from '../../component-meta';
import { ExclusiveGroup, isExclusiveGroup } from './exclusive-group';
import type { IExclusiveGroup } from './exclusive-group';
import { includeSlot, removeSlot } from '../../utils/slot';
import { foreachReverse } from '../../utils/tree';
import { NodeRemoveOptions, EDITOR_EVENT } from '../../types';
export interface NodeStatus {
locking: boolean;
pseudo: boolean;
inPlaceEditing: boolean;
}
export interface IBaseNode extends Omit,
'isRoot' |
'isPage' |
'isComponent' |
'isModal' |
'isSlot' |
'isParental' |
'isLeaf' |
'settingEntry' |
// 在内部的 node 模型中不存在
'getExtraPropValue' |
'setExtraPropValue' |
'exportSchema' |
'visible' |
'importSchema' |
// 内外实现有差异
'isContainer' |
'isEmpty'
> {
isNode: boolean;
get componentMeta(): IComponentMeta;
get settingEntry(): ISettingTopEntry;
get isPurged(): boolean;
get index(): number | undefined;
get isPurging(): boolean;
getId(): string;
getParent(): INode | null;
/**
* 内部方法,请勿使用
* @param useMutator 是否触发联动逻辑
*/
internalSetParent(parent: INode | null, useMutator?: boolean): void;
setConditionGroup(grp: IPublicModelExclusiveGroup | string | null): void;
internalToShellNode(): IPublicModelNode | null;
internalPurgeStart(): void;
unlinkSlot(slotNode: INode): void;
/**
* 导出 schema
*/
export(stage: IPublicEnumTransformStage, options?: any): T;
emitPropChange(val: IPublicTypePropChangeOptions): void;
import(data: Schema, checkId?: boolean): void;
internalSetSlotFor(slotFor: Prop | null | undefined): void;
addSlot(slotNode: INode): void;
onVisibleChange(func: (flag: boolean) => any): () => void;
getSuitablePlace(node: INode, ref: any): any;
onChildrenChange(fn: (param?: { type: string; node: INode }) => void): IPublicTypeDisposable | undefined;
onPropChange(func: (info: IPublicTypePropChangeOptions) => void): IPublicTypeDisposable;
isModal(): boolean;
isRoot(): boolean;
isPage(): boolean;
isComponent(): boolean;
isSlot(): boolean;
isParental(): boolean;
isLeaf(): boolean;
isContainer(): boolean;
isEmpty(): boolean;
remove(
useMutator?: boolean,
purge?: boolean,
options?: NodeRemoveOptions,
): void;
didDropIn(dragment: INode): void;
didDropOut(dragment: INode): void;
purge(): void;
removeSlot(slotNode: INode): boolean;
setVisible(flag: boolean): void;
getVisible(): boolean;
getChildren(): INodeChildren | null;
clearPropValue(path: string | number): void;
setProps(props?: IPublicTypePropsMap | IPublicTypePropsList | Props | null): void;
mergeProps(props: IPublicTypePropsMap): void;
/** 是否可以选中 */
canSelect(): boolean;
}
/**
* 基础节点
*
* [Node Properties]
* componentName: Page/Block/Component
* props
* children
*
* [Directives]
* loop
* loopArgs
* condition
* ------- addition support -----
* conditionGroup use for condition, for exclusive
* title display on outline
* ignored ignore this node will not publish to render, but will store
* isLocked can not select/hover/ item on canvas and outline
* hidden not visible on canvas
* slotArgs like loopArgs, for slot node
*
* 根容器节点
*
* [Node Properties]
* componentName: Page/Block/Component
* props
* children
*
* [Root Container Extra Properties]
* fileName
* meta
* state
* defaultProps
* dataSource
* lifeCycles
* methods
* css
*
* [Directives **not used**]
* loop
* loopArgs
* condition
* ------- future support -----
* conditionGroup
* title
* ignored
* isLocked
* hidden
*/
export class Node implements IBaseNode {
private emitter: IEventBus;
/**
* 是节点实例
*/
readonly isNode = true;
/**
* 节点 id
*/
readonly id: string;
/**
* 节点组件类型
* 特殊节点:
* * Page 页面
* * Block 区块
* * Component 组件/元件
* * Fragment 碎片节点,无 props,有指令
* * Leaf 文字节点 | 表达式节点,无 props,无指令?
* * Slot 插槽节点,无 props,正常 children,有 slotArgs,有指令
*/
readonly componentName: string;
/**
* 属性抽象
*/
props: IProps;
protected _children?: INodeChildren;
/**
* @deprecated
*/
private _addons: { [key: string]: { exportData: () => any; isProp: boolean } } = {};
@obx.ref private _parent: INode | null = null;
/**
* 父级节点
*/
get parent(): INode | null {
return this._parent;
}
/**
* 当前节点子集
*/
get children(): INodeChildren | null {
return this._children || null;
}
/**
* 当前节点深度
*/
@computed get zLevel(): number {
if (this._parent) {
return this._parent.zLevel + 1;
}
return 0;
}
@computed get title(): string | IPublicTypeI18nData | ReactElement {
let t = this.getExtraProp('title');
// TODO: 暂时走不到这个分支
// if (!t && this.componentMeta.descriptor) {
// t = this.getProp(this.componentMeta.descriptor, false);
// }
if (t) {
const v = t.getAsString();
if (v) {
return v;
}
}
return this.componentMeta.title;
}
get icon() {
return this.componentMeta.icon;
}
isInited = false;
_settingEntry: ISettingTopEntry;
get settingEntry(): ISettingTopEntry {
if (this._settingEntry) return this._settingEntry;
this._settingEntry = this.document.designer.createSettingEntry([this]);
return this._settingEntry;
}
private autoruns?: Array<() => void>;
private _isRGLContainer = false;
set isRGLContainer(status: boolean) {
this._isRGLContainer = status;
}
get isRGLContainer(): boolean {
return !!this._isRGLContainer;
}
set isRGLContainerNode(status: boolean) {
this._isRGLContainer = status;
}
get isRGLContainerNode(): boolean {
return !!this._isRGLContainer;
}
get isEmptyNode() {
return this.isEmpty();
}
private _slotFor?: IProp | null | undefined = null;
@obx.shallow _slots: INode[] = [];
get slots(): INode[] {
return this._slots;
}
/* istanbul ignore next */
@obx.ref private _conditionGroup: IExclusiveGroup | null = null;
/* istanbul ignore next */
get conditionGroup(): IExclusiveGroup | null {
return this._conditionGroup;
}
private purged = false;
/**
* 是否已销毁
*/
get isPurged() {
return this.purged;
}
private purging: boolean = false;
/**
* 是否正在销毁
*/
get isPurging() {
return this.purging;
}
@obx.shallow status: NodeStatus = {
inPlaceEditing: false,
locking: false,
pseudo: false,
};
constructor(readonly document: IDocumentModel, nodeSchema: Schema) {
makeObservable(this);
const { componentName, id, children, props, ...extras } = nodeSchema;
this.id = document.nextId(id);
this.componentName = componentName;
if (this.componentName === 'Leaf') {
this.props = new Props(this, {
children: isDOMText(children) || isJSExpression(children) ? children : '',
});
} else {
this.props = new Props(this, props, extras);
this._children = new NodeChildren(this as INode, this.initialChildren(children));
this._children.internalInitParent();
this.props.merge(
this.upgradeProps(this.initProps(props || {})),
this.upgradeProps(extras),
);
this.setupAutoruns();
}
this.initBuiltinProps();
this.isInited = true;
this.emitter = createModuleEventBus('Node');
const { editor } = this.document.designer;
this.onVisibleChange((visible: boolean) => {
editor?.eventBus.emit(EDITOR_EVENT.NODE_VISIBLE_CHANGE, this, visible);
});
this.onChildrenChange((info?: { type: string; node: INode }) => {
editor?.eventBus.emit(EDITOR_EVENT.NODE_CHILDREN_CHANGE, {
type: info?.type,
node: this,
});
});
}
/**
* 节点初始化期间就把内置的一些 prop 初始化好,避免后续不断构造实例导致 reaction 执行多次
*/
@action
private initBuiltinProps() {
this.props.has(getConvertedExtraKey('hidden')) || this.props.add(false, getConvertedExtraKey('hidden'));
this.props.has(getConvertedExtraKey('title')) || this.props.add('', getConvertedExtraKey('title'));
this.props.has(getConvertedExtraKey('isLocked')) || this.props.add(false, getConvertedExtraKey('isLocked'));
this.props.has(getConvertedExtraKey('condition')) || this.props.add(true, getConvertedExtraKey('condition'));
this.props.has(getConvertedExtraKey('conditionGroup')) || this.props.add('', getConvertedExtraKey('conditionGroup'));
this.props.has(getConvertedExtraKey('loop')) || this.props.add(undefined, getConvertedExtraKey('loop'));
}
@action
private initProps(props: any): any {
return this.document.designer.transformProps(props, this, IPublicEnumTransformStage.Init);
}
@action
private upgradeProps(props: any): any {
return this.document.designer.transformProps(props, this, IPublicEnumTransformStage.Upgrade);
}
private setupAutoruns() {
const { autoruns } = this.componentMeta.advanced;
if (!autoruns || autoruns.length < 1) {
return;
}
this.autoruns = autoruns.map((item) => {
return autorun(() => {
item.autorun(this.props.getNode().settingEntry.get(item.name)?.internalToShellField());
});
});
}
private initialChildren(children: IPublicTypeNodeData | IPublicTypeNodeData[] | undefined): IPublicTypeNodeData[] {
const { initialChildren } = this.componentMeta.advanced;
if (children == null) {
if (initialChildren) {
if (typeof initialChildren === 'function') {
return initialChildren(this.internalToShellNode()!) || [];
}
return initialChildren;
}
return [];
}
if (Array.isArray(children)) {
return children;
}
return [children];
}
isContainer(): boolean {
return this.isContainerNode;
}
get isContainerNode(): boolean {
return this.isParentalNode && this.componentMeta.isContainer;
}
isModal(): boolean {
return this.isModalNode;
}
get isModalNode(): boolean {
return this.componentMeta.isModal;
}
isRoot(): boolean {
return this.isRootNode;
}
get isRootNode(): boolean {
return this.document.rootNode === (this as any);
}
isPage(): boolean {
return this.isPageNode;
}
get isPageNode(): boolean {
return this.isRootNode && this.componentName === 'Page';
}
isComponent(): boolean {
return this.isComponentNode;
}
get isComponentNode(): boolean {
return this.isRootNode && this.componentName === 'Component';
}
isSlot(): boolean {
return this.isSlotNode;
}
get isSlotNode(): boolean {
return this._slotFor != null && this.componentName === 'Slot';
}
/**
* 是否一个父亲类节点
*/
isParental(): boolean {
return this.isParentalNode;
}
get isParentalNode(): boolean {
return !this.isLeafNode;
}
/**
* 终端节点,内容一般为 文字 或者 表达式
*/
isLeaf(): boolean {
return this.isLeafNode;
}
get isLeafNode(): boolean {
return this.componentName === 'Leaf';
}
internalSetWillPurge() {
this.internalSetParent(null);
this.document.addWillPurge(this);
}
didDropIn(dragment: INode) {
const { callbacks } = this.componentMeta.advanced;
if (callbacks?.onNodeAdd) {
const cbThis = this.internalToShellNode();
callbacks?.onNodeAdd.call(cbThis, dragment.internalToShellNode(), cbThis);
}
if (this._parent) {
this._parent.didDropIn(dragment);
}
}
didDropOut(dragment: INode) {
const { callbacks } = this.componentMeta.advanced;
if (callbacks?.onNodeRemove) {
const cbThis = this.internalToShellNode();
callbacks?.onNodeRemove.call(cbThis, dragment.internalToShellNode(), cbThis);
}
if (this._parent) {
this._parent.didDropOut(dragment);
}
}
/**
* 内部方法,请勿使用
* @param useMutator 是否触发联动逻辑
*/
internalSetParent(parent: INode | null, useMutator = false) {
if (this._parent === parent) {
return;
}
// 解除老的父子关系,但不需要真的删除节点
if (this._parent) {
if (this.isSlot()) {
this._parent.unlinkSlot(this);
} else {
this._parent.children?.unlinkChild(this);
}
}
if (useMutator) {
this._parent?.didDropOut(this);
}
if (parent) {
// 建立新的父子关系,尤其注意:对于 parent 为 null 的场景,不会赋值,因为 subtreeModified 等事件可能需要知道该 node 被删除前的父子关系
this._parent = parent;
this.document.removeWillPurge(this);
/* istanbul ignore next */
if (!this.conditionGroup) {
// initial conditionGroup
const grp = this.getExtraProp('conditionGroup', false)?.getAsString();
if (grp) {
this.setConditionGroup(grp);
}
}
if (useMutator) {
parent.didDropIn(this);
}
}
}
internalSetSlotFor(slotFor: Prop | null | undefined) {
this._slotFor = slotFor;
}
internalToShellNode(): IPublicModelNode | null {
return this.document.designer.shellModelFactory.createNode(this);
}
/**
* 关联属性
*/
get slotFor(): IProp | null | undefined {
return this._slotFor;
}
/**
* 移除当前节点
*/
remove(
useMutator = true,
purge = true,
options: NodeRemoveOptions = { suppressRemoveEvent: false },
) {
if (this.parent) {
if (!options.suppressRemoveEvent) {
this.document.designer.editor?.eventBus.emit('node.remove.topLevel', {
node: this,
index: this.parent?.children?.indexOf(this),
});
}
if (this.isSlot()) {
this.parent.removeSlot(this);
this.parent.children?.internalDelete(this, purge, useMutator, { suppressRemoveEvent: true });
} else {
this.parent.children?.internalDelete(this, purge, useMutator, { suppressRemoveEvent: true });
}
}
}
/**
* 锁住当前节点
*/
lock(flag = true) {
this.setExtraProp('isLocked', flag);
}
/**
* 获取当前节点的锁定状态
*/
get isLocked(): boolean {
return !!this.getExtraProp('isLocked')?.getValue();
}
canSelect(): boolean {
const onSelectHook = this.componentMeta?.advanced?.callbacks?.onSelectHook;
const canSelect = typeof onSelectHook === 'function' ? onSelectHook(this.internalToShellNode()!) : true;
return canSelect;
}
/**
* 选择当前节点
*/
select() {
this.document.selection.select(this.id);
}
/**
* 悬停高亮
*/
hover(flag = true) {
if (flag) {
this.document.designer.detecting.capture(this);
} else {
this.document.designer.detecting.release(this);
}
}
/**
* 节点组件描述
*/
@computed get componentMeta(): IComponentMeta {
return this.document.getComponentMeta(this.componentName);
}
@computed get propsData(): IPublicTypePropsMap | IPublicTypePropsList | null {
if (!this.isParental() || this.componentName === 'Fragment') {
return null;
}
return this.props.export(IPublicEnumTransformStage.Serilize).props || null;
}
hasSlots() {
return this._slots.length > 0;
}
/* istanbul ignore next */
setConditionGroup(grp: IPublicModelExclusiveGroup | string | null) {
let _grp: IExclusiveGroup | null = null;
if (!grp) {
this.getExtraProp('conditionGroup', false)?.remove();
if (this._conditionGroup) {
this._conditionGroup.remove(this);
this._conditionGroup = null;
}
return;
}
if (!isExclusiveGroup(grp)) {
if (this.prevSibling?.conditionGroup?.name === grp) {
_grp = this.prevSibling.conditionGroup;
} else if (this.nextSibling?.conditionGroup?.name === grp) {
_grp = this.nextSibling.conditionGroup;
} else if (typeof grp === 'string') {
_grp = new ExclusiveGroup(grp);
}
}
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);
}
}
/* istanbul ignore next */
isConditionalVisible(): boolean | undefined {
return this._conditionGroup?.isVisible(this);
}
/* istanbul ignore next */
setConditionalVisible() {
this._conditionGroup?.setVisible(this);
}
hasCondition() {
const v = this.getExtraProp('condition', false)?.getValue();
return v != null && v !== '' && v !== true;
}
/**
* has loop when 1. loop is validArray with length > 1 ; OR 2. loop is variable object
* @return boolean, has loop config or not
*/
hasLoop() {
const value = this.getExtraProp('loop', false)?.getValue();
if (value === undefined || value === null) {
return false;
}
if (Array.isArray(value)) {
return true;
}
if (isJSExpression(value)) {
return true;
}
return false;
}
/* istanbul ignore next */
wrapWith(schema: Schema) {
const wrappedNode = this.replaceWith({ ...schema, children: [this.export()] });
return wrappedNode.children!.get(0);
}
replaceWith(schema: Schema, migrate = false): any {
// reuse the same id? or replaceSelection
schema = Object.assign({}, migrate ? this.export() : {}, schema);
return this.parent?.replaceChild(this, schema);
}
/**
* 替换子节点
*
* @param {INode} node
* @param {object} data
*/
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);
if (selected) {
this.document.selection.select(newNode.id);
}
return newNode;
}
return node;
}
setVisible(flag: boolean): void {
this.getExtraProp('hidden')?.setValue(!flag);
this.emitter.emit('visibleChange', flag);
}
getVisible(): boolean {
return !this.getExtraProp('hidden')?.getValue();
}
onVisibleChange(func: (flag: boolean) => any): () => void {
const wrappedFunc = wrapWithEventSwitch(func);
this.emitter.on('visibleChange', wrappedFunc);
return () => {
this.emitter.removeListener('visibleChange', wrappedFunc);
};
}
getProp(path: string, createIfNone = true): IProp | null {
return this.props.query(path, createIfNone) || null;
}
getExtraProp(key: string, createIfNone = true): IProp | null {
return this.props.get(getConvertedExtraKey(key), createIfNone) || null;
}
setExtraProp(key: string, value: IPublicTypeCompositeValue) {
this.getProp(getConvertedExtraKey(key), true)?.setValue(value);
}
/**
* 获取单个属性值
*/
getPropValue(path: string): any {
return this.getProp(path, false)?.value;
}
/**
* 设置单个属性值
*/
setPropValue(path: string, value: any) {
this.getProp(path, true)!.setValue(value);
}
/**
* 清除已设置的值
*/
clearPropValue(path: string): void {
this.getProp(path, false)?.unset();
}
/**
* 设置多个属性值,和原有值合并
*/
mergeProps(props: IPublicTypePropsMap) {
this.props.merge(props);
}
/**
* 设置多个属性值,替换原有值
*/
setProps(props?: IPublicTypePropsMap | IPublicTypePropsList | Props | null) {
if (props instanceof Props) {
this.props = props;
return;
}
this.props.import(props);
}
/**
* 获取节点在父容器中的索引
*/
@computed get index(): number | undefined {
if (!this.parent) {
return -1;
}
return this.parent.children?.indexOf(this);
}
/**
* 获取下一个兄弟节点
*/
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);
}
/**
* 获取上一个兄弟节点
*/
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);
}
/**
* 获取符合搭建协议-节点 schema 结构
*/
get schema(): Schema {
return this.export(IPublicEnumTransformStage.Save);
}
set schema(data: Schema) {
runInAction(() => this.import(data));
}
import(data: Schema, checkId = false) {
const { componentName, id, children, props, ...extras } = data;
if (this.isSlot()) {
foreachReverse(
this.children!,
(subNode: INode) => {
subNode.remove(true, true);
},
(iterable, idx) => (iterable as INodeChildren).get(idx),
);
}
if (this.isParental()) {
this.props.import(props, extras);
this._children?.import(children, checkId);
} else {
this.props
.get('children', true)!
.setValue(isDOMText(children) || isJSExpression(children) ? children : '');
}
}
toData() {
return this.export();
}
/**
* 导出 schema
*/
export(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save, options: any = {}): T {
stage = compatStage(stage);
const baseSchema: any = {
componentName: this.componentName,
};
if (stage !== IPublicEnumTransformStage.Clone) {
baseSchema.id = this.id;
}
if (stage === IPublicEnumTransformStage.Render) {
baseSchema.docId = this.document.id;
}
if (this.isLeaf()) {
if (!options.bypassChildren) {
baseSchema.children = this.props.get('children')?.export(stage);
}
return baseSchema;
}
const { props = {}, extras } = this.props.export(stage) || {};
const _extras_: { [key: string]: any } = {
...extras,
};
/* istanbul ignore next */
Object.keys(this._addons).forEach((key) => {
const addon = this._addons[key];
if (addon) {
if (addon.isProp) {
(props as any)[getConvertedExtraKey(key)] = addon.exportData();
} else {
_extras_[key] = addon.exportData();
}
}
});
const schema: any = {
...baseSchema,
props: this.document.designer.transformProps(props, this, stage),
...this.document.designer.transformProps(_extras_, this, stage),
};
if (this.isParental() && this.children && this.children.size > 0 && !options.bypassChildren) {
schema.children = this.children.export(stage);
}
return schema;
}
/**
* 判断是否包含特定节点
*/
contains(node: INode): boolean {
return contains(this, node);
}
/**
* 获取特定深度的父亲节点
*/
getZLevelTop(zLevel: number): INode | null {
return getZLevelTop(this, zLevel);
}
/**
* 判断与其它节点的位置关系
*
* 16 thisNode contains otherNode
* 8 thisNode contained_by otherNode
* 2 thisNode before or after otherNode
* 0 thisNode same as otherNode
*/
comparePosition(otherNode: INode): PositionNO {
return comparePosition(this, otherNode);
}
unlinkSlot(slotNode: INode) {
const i = this._slots.indexOf(slotNode);
if (i < 0) {
return false;
}
this._slots.splice(i, 1);
}
/**
* 删除一个Slot节点
*/
removeSlot(slotNode: INode): boolean {
// if (purge) {
// // should set parent null
// slotNode?.internalSetParent(null, false);
// slotNode?.purge();
// }
// this.document.unlinkNode(slotNode);
// this.document.selection.remove(slotNode.id);
const i = this._slots.indexOf(slotNode);
if (i < 0) {
return false;
}
this._slots.splice(i, 1);
return false;
}
addSlot(slotNode: INode) {
const slotName = slotNode?.getExtraProp('name')?.getAsString();
// 一个组件下的所有 slot,相同 slotName 的 slot 应该是唯一的
if (includeSlot(this, slotName)) {
removeSlot(this, slotName);
}
slotNode.internalSetParent(this as INode, true);
this._slots.push(slotNode);
}
/**
* 当前node对应组件是否已注册可用
*/
isValidComponent() {
const allComponents = this.document?.designer?.componentsMap;
if (allComponents && allComponents[this.componentName]) {
return true;
}
return false;
}
/**
* 删除一个节点
* @param node
*/
removeChild(node: INode) {
this.children?.delete(node);
}
/**
* 销毁
*/
purge() {
if (this.purged) {
return;
}
this.purged = true;
this.autoruns?.forEach((dispose) => dispose());
this.props.purge();
this.settingEntry?.purge();
// this.document.destroyNode(this);
}
internalPurgeStart() {
this.purging = true;
}
/**
* 是否可执行某 action
*/
canPerformAction(actionName: string): boolean {
const availableActions =
this.componentMeta?.availableActions?.filter((action: IPublicTypeComponentAction) => {
const { condition } = action;
return typeof condition === 'function' ?
condition(this) !== false :
condition !== false;
})
.map((action: IPublicTypeComponentAction) => action.name) || [];
return availableActions.indexOf(actionName) >= 0;
}
// ======= compatible apis ====
isEmpty(): boolean {
return this.children ? this.children.isEmpty() : true;
}
getChildren() {
return this.children;
}
getComponentName() {
return this.componentName;
}
insert(node: INode, ref?: INode, useMutator = true) {
this.insertAfter(node, ref, useMutator);
}
insertBefore(node: INode, ref?: INode, useMutator = true) {
const nodeInstance = ensureNode(node, this.document);
this.children?.internalInsert(nodeInstance, ref ? ref.index : null, useMutator);
}
insertAfter(node: any, ref?: INode, useMutator = true) {
const nodeInstance = ensureNode(node, this.document);
this.children?.internalInsert(nodeInstance, ref ? (ref.index || 0) + 1 : null, useMutator);
}
getParent() {
return this.parent;
}
getId() {
return this.id;
}
getIndex() {
return this.index;
}
getNode() {
return this;
}
getRoot() {
return this.document.rootNode;
}
getProps() {
return this.props;
}
onChildrenChange(fn: (param?: { type: string; node: INode }) => void): IPublicTypeDisposable | undefined {
const wrappedFunc = wrapWithEventSwitch(fn);
return this.children?.onChange(wrappedFunc);
}
mergeChildren(
remover: (node: INode, idx: number) => any,
adder: (children: INode[]) => IPublicTypeNodeData[] | null,
sorter: (firstNode: INode, secondNode: INode) => any,
) {
this.children?.mergeChildren(remover, adder, sorter);
}
/**
* @deprecated
*/
getStatus(field?: keyof NodeStatus) {
if (field && this.status[field] != null) {
return this.status[field];
}
return this.status;
}
/**
* @deprecated
*/
setStatus(field: keyof NodeStatus, flag: boolean) {
if (!this.status.hasOwnProperty(field)) {
return;
}
if (flag !== this.status[field]) {
this.status[field] = flag;
}
}
/**
* @deprecated
*/
getDOMNode(): any {
const instance = this.document.simulator?.getComponentInstances(this)?.[0];
if (!instance) {
return;
}
return this.document.simulator?.findDOMNodes(instance)?.[0];
}
/**
* @deprecated
*/
getPage() {
console.warn('getPage is deprecated, use document instead');
return this.document;
}
/**
* 获取磁贴相关信息
*/
getRGL(): {
isContainerNode: boolean;
isEmptyNode: boolean;
isRGLContainerNode: boolean;
isRGLNode: boolean;
isRGL: boolean;
rglNode: Node | null;
} {
const isContainerNode = this.isContainer();
const isEmptyNode = this.isEmpty();
const isRGLContainerNode = this.isRGLContainer;
const isRGLNode = (this.getParent()?.isRGLContainer) as boolean;
const isRGL = isRGLContainerNode || (isRGLNode && (!isContainerNode || !isEmptyNode));
let rglNode = isRGLContainerNode ? this : isRGL ? this?.getParent() : null;
return { isContainerNode, isEmptyNode, isRGLContainerNode, isRGLNode, isRGL, rglNode };
}
/**
* @deprecated no one is using this, will be removed in a future release
*/
getSuitablePlace(node: INode, ref: any): any {
const focusNode = this.document?.focusNode;
// 如果节点是模态框,插入到根节点下
if (node?.componentMeta?.isModal) {
return { container: focusNode, ref };
}
if (!ref && focusNode && this.contains(focusNode)) {
const rootCanDropIn = focusNode.componentMeta?.prototype?.options?.canDropIn;
if (
rootCanDropIn === undefined ||
rootCanDropIn === true ||
(typeof rootCanDropIn === 'function' && rootCanDropIn(node))
) {
return { container: focusNode };
}
return null;
}
if (this.isRoot() && this.children) {
const dropElement = this.children.filter((c) => {
if (!c.isContainerNode) {
return false;
}
const canDropIn = c.componentMeta?.prototype?.options?.canDropIn;
if (
canDropIn === undefined ||
canDropIn === true ||
(typeof canDropIn === 'function' && canDropIn(node))
) {
return true;
}
return false;
})[0];
if (dropElement) {
return { container: dropElement, ref };
}
const rootCanDropIn = this.componentMeta?.prototype?.options?.canDropIn;
if (
rootCanDropIn === undefined ||
rootCanDropIn === true ||
(typeof rootCanDropIn === 'function' && rootCanDropIn(node))
) {
return { container: this, ref };
}
return null;
}
const canDropIn = this.componentMeta?.prototype?.options?.canDropIn;
if (this.isContainer()) {
if (
canDropIn === undefined ||
(typeof canDropIn === 'boolean' && canDropIn) ||
(typeof canDropIn === 'function' && canDropIn(node))
) {
return { container: this, ref };
}
}
if (this.parent) {
return this.parent.getSuitablePlace(node, { index: this.index });
}
return null;
}
/**
* @deprecated
*/
getAddonData(key: string) {
const addon = this._addons[key];
if (addon) {
return addon.exportData();
}
return this.getExtraProp(key)?.getValue();
}
/**
* @deprecated
*/
registerAddon(key: string, exportData: () => any, isProp = false) {
this._addons[key] = { exportData, isProp };
}
getRect(): DOMRect | null {
if (this.isRoot()) {
return this.document.simulator?.viewport.contentBounds || null;
}
return this.document.simulator?.computeRect(this) || null;
}
/**
* @deprecated
*/
getPrototype() {
return this.componentMeta.prototype;
}
/**
* @deprecated
*/
setPrototype(proto: any) {
this.componentMeta.prototype = proto;
}
getIcon() {
return this.icon;
}
toString() {
return this.id;
}
emitPropChange(val: IPublicTypePropChangeOptions) {
this.emitter?.emit('propChange', val);
}
onPropChange(func: (info: IPublicTypePropChangeOptions) => void): IPublicTypeDisposable {
const wrappedFunc = wrapWithEventSwitch(func);
this.emitter.on('propChange', wrappedFunc);
return () => {
this.emitter.removeListener('propChange', wrappedFunc);
};
}
}
function ensureNode(node: any, document: IDocumentModel): INode {
let nodeInstance = node;
if (!isNode(node)) {
if (node.getComponentName) {
nodeInstance = document.createNode({
componentName: node.getComponentName(),
});
} else {
nodeInstance = document.createNode(node);
}
}
return nodeInstance;
}
export interface LeafNode extends Node {
readonly children: null;
}
export type IPublicTypePropChangeOptions = Omit;
export type ISlotNode = IBaseNode;
export type IPageNode = IBaseNode;
export type IComponentNode = IBaseNode;
export type IRootNode = IPageNode | IComponentNode;
export type INode = IPageNode | ISlotNode | IComponentNode | IRootNode;
export function isRootNode(node: INode): node is IRootNode {
return node && node.isRootNode;
}
export function isLowCodeComponent(node: INode): node is IComponentNode {
return node.componentMeta?.getMetadata().devMode === 'lowCode';
}
export function getZLevelTop(child: INode, zLevel: number): INode | null {
let l = child.zLevel;
if (l < zLevel || zLevel < 0) {
return null;
}
if (l === zLevel) {
return child;
}
let r: any = child;
while (r && l-- > zLevel) {
r = r.parent;
}
return r;
}
/**
* 测试两个节点是否为包含关系
* @param node1 测试的父节点
* @param node2 测试的被包含节点
* @returns 是否包含
*/
export function contains(node1: INode, node2: INode): boolean {
if (node1 === node2) {
return true;
}
if (!node1.isParentalNode || !node2.parent) {
return false;
}
const p = getZLevelTop(node2, node1.zLevel);
if (!p) {
return false;
}
return node1 === p;
}
// 16 node1 contains node2
// 8 node1 contained_by node2
// 2 node1 before or after node2
// 0 node1 same as node2
export enum PositionNO {
Contains = 16,
ContainedBy = 8,
BeforeOrAfter = 2,
TheSame = 0,
}
export function comparePosition(node1: INode, node2: INode): PositionNO {
if (node1 === node2) {
return PositionNO.TheSame;
}
const l1 = node1.zLevel;
const l2 = node2.zLevel;
if (l1 === l2) {
return PositionNO.BeforeOrAfter;
}
let p: any;
if (l1 < l2) {
p = getZLevelTop(node2, l1);
if (p && p === node1) {
return PositionNO.Contains;
}
return PositionNO.BeforeOrAfter;
}
p = getZLevelTop(node1, l2);
if (p && p === node2) {
return PositionNO.ContainedBy;
}
return PositionNO.BeforeOrAfter;
}
export function insertChild(
container: INode,
thing: INode | IPublicTypeNodeData,
at?: number | null,
copy?: boolean,
): INode | null {
let node: INode | null | IRootNode | undefined;
let nodeSchema: IPublicTypeNodeSchema;
if (isNode(thing) && (copy || thing.isSlot())) {
nodeSchema = thing.export(IPublicEnumTransformStage.Clone);
node = container.document?.createNode(nodeSchema);
} else if (isNode(thing)) {
node = thing;
} else if (isNodeSchema(thing)) {
node = container.document?.createNode(thing);
}
if (isNode(node)) {
container.children?.insert(node, at);
return node;
}
return null;
}
export function insertChildren(
container: INode,
nodes: INode[] | IPublicTypeNodeData[],
at?: number | null,
copy?: boolean,
): INode[] {
let index = at;
let node: any;
const results: INode[] = [];
// eslint-disable-next-line no-cond-assign
while ((node = nodes.pop())) {
node = insertChild(container, node, index, copy);
results.push(node);
index = node.index;
}
return results;
}
================================================
FILE: packages/designer/src/document/node/transform-stage.ts
================================================
export { TransformStage } from '@alilc/lowcode-types';
================================================
FILE: packages/designer/src/document/node/props/prop.ts
================================================
import { untracked, computed, obx, engineConfig, action, makeObservable, mobx, runInAction } from '@alilc/lowcode-editor-core';
import { GlobalEvent, IPublicEnumTransformStage } from '@alilc/lowcode-types';
import type { IPublicTypeCompositeValue, IPublicTypeJSSlot, IPublicTypeSlotSchema, IPublicModelProp } from '@alilc/lowcode-types';
import { uniqueId, isPlainObject, hasOwnProperty, compatStage, isJSExpression, isJSSlot, isNodeSchema } from '@alilc/lowcode-utils';
import { valueToSource } from './value-to-source';
import { IPropParent } from './props';
import type { IProps } from './props';
import { ISlotNode, INode } from '../node';
// import { TransformStage } from '../transform-stage';
const { set: mobxSet, isObservableArray } = mobx;
export const UNSET = Symbol.for('unset');
// eslint-disable-next-line no-redeclare
export type UNSET = typeof UNSET;
export interface IProp extends Omit, 'exportSchema' | 'node'>, IPropParent {
spread: boolean;
key: string | number | undefined;
readonly props: IProps;
readonly owner: INode;
delete(prop: IProp): void;
export(stage: IPublicEnumTransformStage): IPublicTypeCompositeValue;
getNode(): INode;
getAsString(): string;
unset(): void;
get value(): IPublicTypeCompositeValue | UNSET;
compare(other: IProp | null): number;
isUnset(): boolean;
purge(): void;
setupItems(): IProp[] | null;
isVirtual(): boolean;
get type(): ValueTypes;
get size(): number;
get code(): string;
}
export type ValueTypes = 'unset' | 'literal' | 'map' | 'list' | 'expression' | 'slot';
export class Prop implements IProp, IPropParent {
readonly isProp = true;
readonly owner: INode;
/**
* 键值
*/
@obx key: string | number | undefined;
/**
* 扩展值
*/
@obx spread: boolean;
readonly props: IProps;
readonly options: any;
readonly id = uniqueId('prop$');
@obx.ref private _type: ValueTypes = 'unset';
/**
* 属性类型
*/
get type(): ValueTypes {
return this._type;
}
@obx private _value: any = UNSET;
/**
* 属性值
*/
@computed get value(): IPublicTypeCompositeValue | UNSET {
return this.export(IPublicEnumTransformStage.Serilize);
}
private _code: string | null = null;
/**
* 获得表达式值
*/
@computed get code() {
if (isJSExpression(this.value)) {
return this.value.value;
}
// todo: JSFunction ...
if (this.type === 'slot') {
return JSON.stringify(this._slotNode!.export(IPublicEnumTransformStage.Save));
}
return this._code != null ? this._code : JSON.stringify(this.value);
}
/**
* 设置表达式值
*/
set code(code: string) {
if (isJSExpression(this._value)) {
this.setValue({
...this._value,
value: code,
});
this._code = code;
return;
}
try {
const v = JSON.parse(code);
this.setValue(v);
this._code = code;
return;
} catch (e) {
// ignore
}
this.setValue({
type: 'JSExpression',
value: code,
mock: this._value,
});
this._code = code;
}
private _slotNode?: INode | null;
get slotNode(): INode | null {
return this._slotNode || null;
}
@obx.shallow private _items: IProp[] | null = null;
/**
* 作为一层缓存机制,主要是复用部分已存在的 Prop,保持响应式关系,比如:
* 当前 Prop#_value 值为 { a: 1 },当调用 setValue({ a: 2 }) 时,所有原来的子 Prop 均被销毁,
* 导致假如外部有 mobx reaction(常见于 observer),此时响应式链路会被打断,
* 因为 reaction 监听的是原 Prop(a) 的 _value,而不是新 Prop(a) 的 _value。
*/
@obx.shallow private _maps: Map | null = null;
/**
* 构造 items 属性,同时构造 maps 属性
*/
private get items(): IProp[] | null {
if (this._items) return this._items;
return runInAction(() => {
let items: IProp[] | null = null;
if (this._type === 'list') {
const maps = new Map();
const data = this._value;
data.forEach((item: any, idx: number) => {
items = items || [];
let prop;
if (this._maps?.has(idx.toString())) {
prop = this._maps.get(idx.toString())!;
prop.setValue(item);
} else {
prop = new Prop(this, item, idx);
}
maps.set(idx.toString(), prop);
items.push(prop);
});
this._maps = maps;
} else if (this._type === 'map') {
const data = this._value;
const maps = new Map();
const keys = Object.keys(data);
for (const key of keys) {
let prop: IProp;
if (this._maps?.has(key)) {
prop = this._maps.get(key)!;
prop.setValue(data[key]);
} else {
prop = new Prop(this, data[key], key);
}
items = items || [];
items.push(prop);
maps.set(key, prop);
}
this._maps = maps;
} else {
items = null;
this._maps = null;
}
this._items = items;
return this._items;
});
}
@computed private get maps(): Map | null {
if (!this.items) {
return null;
}
return this._maps;
}
get path(): string[] {
return (this.parent.path || []).concat(this.key as string);
}
/**
* 元素个数
*/
get size(): number {
return this.items?.length || 0;
}
private purged = false;
constructor(
public parent: IPropParent,
value: IPublicTypeCompositeValue | UNSET = UNSET,
key?: string | number,
spread = false,
options = {},
) {
makeObservable(this);
this.owner = parent.owner;
this.props = parent.props;
this.key = key;
this.spread = spread;
this.options = options;
if (value !== UNSET) {
this.setValue(value);
}
this.setupItems();
}
// TODO: 先用调用方式触发子 prop 的初始化,后续须重构
@action
setupItems() {
return this.items;
}
/**
* @see SettingTarget
*/
@action
getPropValue(propName: string | number): any {
return this.get(propName)!.getValue();
}
/**
* @see SettingTarget
*/
@action
setPropValue(propName: string | number, value: any): void {
this.set(propName, value);
}
/**
* @see SettingTarget
*/
@action
clearPropValue(propName: string | number): void {
this.get(propName, false)?.unset();
}
export(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save): IPublicTypeCompositeValue {
stage = compatStage(stage);
const type = this._type;
if (stage === IPublicEnumTransformStage.Render && this.key === '___condition___') {
// 在设计器里,所有组件默认需要展示,除非开启了 enableCondition 配置
if (engineConfig?.get('enableCondition') !== true) {
return true;
}
return this._value;
}
if (type === 'unset') {
return undefined;
}
if (type === 'literal' || type === 'expression') {
return this._value;
}
if (type === 'slot') {
const schema = this._slotNode?.export(stage) || {} as any;
if (stage === IPublicEnumTransformStage.Render) {
return {
type: 'JSSlot',
params: schema.params,
value: schema,
id: schema.id,
};
}
return {
type: 'JSSlot',
params: schema.params,
value: schema.children,
title: schema.title,
name: schema.name,
id: schema.id,
};
}
if (type === 'map') {
if (!this._items) {
return this._value;
}
let maps: any;
this.items!.forEach((prop, key) => {
if (!prop.isUnset()) {
const v = prop.export(stage);
if (v != null) {
maps = maps || {};
maps[prop.key || key] = v;
}
}
});
return maps;
}
if (type === 'list') {
if (!this._items) {
return this._value;
}
return this.items!.map((prop) => {
return prop?.export(stage);
});
}
}
getAsString(): string {
if (this.type === 'literal') {
return this._value ? String(this._value) : '';
}
return '';
}
/**
* set value, val should be JSON Object
*/
@action
setValue(val: IPublicTypeCompositeValue) {
if (val === this._value) return;
const oldValue = this._value;
this._value = val;
this._code = null;
const t = typeof val;
if (val == null) {
// this._value = undefined;
this._type = 'literal';
} else if (t === 'string' || t === 'number' || t === 'boolean') {
this._type = 'literal';
} else if (Array.isArray(val)) {
this._type = 'list';
} else if (isPlainObject(val)) {
if (isJSSlot(val)) {
this.setAsSlot(val);
} else if (isJSExpression(val)) {
this._type = 'expression';
} else {
this._type = 'map';
}
} else /* istanbul ignore next */ {
this._type = 'expression';
this._value = {
type: 'JSExpression',
value: valueToSource(val),
};
}
this.dispose();
// setValue 的时候,如果不重新建立 items,items 的 setValue 没有触发,会导致子项的响应式逻辑不能被触发
this.setupItems();
if (oldValue !== this._value) {
this.emitChange({ oldValue });
}
}
emitChange = ({
oldValue,
}: {
oldValue: IPublicTypeCompositeValue | UNSET;
}) => {
const editor = this.owner.document?.designer.editor;
const propsInfo = {
key: this.key,
prop: this,
oldValue,
newValue: this.type === 'unset' ? undefined : this._value,
};
editor?.eventBus.emit(GlobalEvent.Node.Prop.InnerChange, {
node: this.owner as any,
...propsInfo,
});
this.owner?.emitPropChange?.(propsInfo);
};
getValue(): IPublicTypeCompositeValue {
return this.export(IPublicEnumTransformStage.Serilize);
}
@action
private dispose() {
const items = untracked(() => this._items);
if (items) {
items.forEach((prop) => prop.purge());
}
this._items = null;
if (this._type !== 'slot' && this._slotNode) {
this._slotNode.remove();
this._slotNode = undefined;
}
}
@action
setAsSlot(data: IPublicTypeJSSlot) {
this._type = 'slot';
let slotSchema: IPublicTypeSlotSchema;
// 当 data.value 的结构为 { componentName: 'Slot' } 时,复用部分 slotSchema 数据
if ((isPlainObject(data.value) && isNodeSchema(data.value) && data.value?.componentName === 'Slot')) {
const value = data.value as IPublicTypeSlotSchema;
slotSchema = {
componentName: 'Slot',
title: value.title || value.props?.slotTitle,
id: value.id,
name: value.name || value.props?.slotName,
params: value.params || value.props?.slotParams,
children: value.children,
} as IPublicTypeSlotSchema;
} else {
slotSchema = {
componentName: 'Slot',
title: data.title,
id: data.id,
name: data.name,
params: data.params,
children: data.value,
};
}
if (this._slotNode) {
this._slotNode.import(slotSchema);
} else {
const { owner } = this.props;
this._slotNode = owner.document?.createNode(slotSchema);
if (this._slotNode) {
owner.addSlot(this._slotNode);
this._slotNode.internalSetSlotFor(this);
}
}
}
/**
* 取消设置值
*/
@action
unset() {
if (this._type !== 'unset') {
this._type = 'unset';
this.emitChange({
oldValue: this._value,
});
}
}
/**
* 是否未设置值
*/
@action
isUnset() {
return this._type === 'unset';
}
isVirtual() {
return typeof this.key === 'string' && this.key.charAt(0) === '!';
}
/**
* @returns 0: the same 1: maybe & like 2: not the same
*/
compare(other: IProp | null): number {
if (!other || other.isUnset()) {
return this.isUnset() ? 0 : 2;
}
if (other.type !== this.type) {
return 2;
}
// list
if (this.type === 'list') {
return this.size === other.size ? 1 : 2;
}
if (this.type === 'map') {
return 1;
}
// 'literal' | 'map' | 'expression' | 'slot'
return this.code === other.code ? 0 : 2;
}
/**
* 获取某个属性
* @param createIfNone 当没有的时候,是否创建一个
*/
@action
get(path: string | number, createIfNone = true): IProp | null {
const type = this._type;
if (type !== 'map' && type !== 'list' && type !== 'unset' && !createIfNone) {
return null;
}
const maps = type === 'map' ? this.maps : null;
const items = type === 'list' ? this.items : null;
let entry = path;
let nest = '';
if (typeof path !== 'number') {
const i = path.indexOf('.');
if (i > 0) {
nest = path.slice(i + 1);
if (nest) {
entry = path.slice(0, i);
}
}
}
let prop: any;
if (type === 'list') {
if (isValidArrayIndex(entry, this.size)) {
prop = items![entry];
}
} else if (type === 'map') {
prop = maps?.get(entry);
}
if (prop) {
return nest ? prop.get(nest, createIfNone) : prop;
}
if (createIfNone) {
prop = new Prop(this, UNSET, entry);
this.set(entry, prop, true);
if (nest) {
return prop.get(nest, true);
}
return prop;
}
return null;
}
/**
* 从父级移除本身
*/
@action
remove() {
this.parent.delete(this);
this.unset();
}
/**
* 删除项
*/
@action
delete(prop: IProp): void {
/* istanbul ignore else */
if (this._items) {
const i = this._items.indexOf(prop);
if (i > -1) {
this._items.splice(i, 1);
prop.purge();
}
if (this._maps && prop.key) {
this._maps.delete(String(prop.key));
}
}
}
/**
* 删除 key
*/
@action
deleteKey(key: string): void {
/* istanbul ignore else */
if (this.maps) {
const prop = this.maps.get(key);
if (prop) {
this.delete(prop);
}
}
}
/**
* 添加值到列表
*
* @param force 强制
*/
@action
add(value: IPublicTypeCompositeValue, force = false): IProp | null {
const type = this._type;
if (type !== 'list' && type !== 'unset' && !force) {
return null;
}
if (type === 'unset' || (force && type !== 'list')) {
this.setValue([]);
}
const prop = new Prop(this, value);
this._items = this._items || [];
this._items.push(prop);
return prop;
}
/**
* 设置值到字典
*
* @param force 强制
*/
@action
set(key: string | number, value: IPublicTypeCompositeValue | Prop, force = false) {
const type = this._type;
if (type !== 'map' && type !== 'list' && type !== 'unset' && !force) {
return null;
}
if (type === 'unset' || (force && type !== 'map')) {
if (isValidArrayIndex(key)) {
if (type !== 'list') {
this.setValue([]);
}
} else {
this.setValue({});
}
}
const prop = isProp(value) ? value : new Prop(this, value, key);
let items = this._items! || [];
if (this.type === 'list') {
if (!isValidArrayIndex(key)) {
return null;
}
if (isObservableArray(items)) {
mobxSet(items, key, prop);
} else {
items[key] = prop;
}
this._items = items;
} else if (this.type === 'map') {
const maps = this._maps || new Map();
const orig = maps?.get(key);
if (orig) {
// replace
const i = items.indexOf(orig);
if (i > -1) {
items.splice(i, 1, prop)[0].purge();
}
maps?.set(key, prop);
} else {
// push
items.push(prop);
this._items = items;
maps?.set(key, prop);
}
this._maps = maps;
} /* istanbul ignore next */ else {
return null;
}
return prop;
}
/**
* 是否存在 key
*/
has(key: string): boolean {
if (this._type !== 'map') {
return false;
}
if (this._maps) {
return this._maps.has(key);
}
return hasOwnProperty(this._value, key);
}
/**
* 回收销毁
*/
@action
purge() {
if (this.purged) {
return;
}
this.purged = true;
if (this._items) {
this._items.forEach((item) => item.purge());
}
this._items = null;
this._maps = null;
if (this._slotNode && this._slotNode.slotFor === this) {
this._slotNode.remove();
this._slotNode = undefined;
}
}
/**
* 迭代器
*/
[Symbol.iterator](): { next(): { value: IProp } } {
let index = 0;
const { items } = this;
const length = items?.length || 0;
return {
next() {
if (index < length) {
return {
value: items![index++],
done: false,
};
}
return {
value: undefined as any,
done: true,
};
},
};
}
/**
* 遍历
*/
@action
forEach(fn: (item: IProp, key: number | string | undefined) => void): void {
const { items } = this;
if (!items) {
return;
}
const isMap = this._type === 'map';
items.forEach((item, index) => {
return isMap ? fn(item, item.key) : fn(item, index);
});
}
/**
* 遍历
*/
@action
map(fn: (item: IProp, key: number | string | undefined) => T): T[] | null {
const { items } = this;
if (!items) {
return null;
}
const isMap = this._type === 'map';
return items.map((item, index) => {
return isMap ? fn(item, item.key) : fn(item, index);
});
}
getProps() {
return this.props;
}
getNode() {
return this.owner;
}
}
export function isProp(obj: any): obj is Prop {
return obj && obj.isProp;
}
export function isValidArrayIndex(key: any, limit = -1): key is number {
const n = parseFloat(String(key));
return n >= 0 && Math.floor(n) === n && isFinite(n) && (limit < 0 || n < limit);
}
================================================
FILE: packages/designer/src/document/node/props/props.ts
================================================
import { computed, makeObservable, obx, action } from '@alilc/lowcode-editor-core';
import { IPublicTypePropsList, IPublicTypeCompositeValue, IPublicEnumTransformStage, IBaseModelProps } from '@alilc/lowcode-types';
import type { IPublicTypePropsMap } from '@alilc/lowcode-types';
import { uniqueId, compatStage } from '@alilc/lowcode-utils';
import { Prop, UNSET } from './prop';
import type { IProp } from './prop';
import { INode } from '../node';
// import { TransformStage } from '../transform-stage';
interface ExtrasObject {
[key: string]: any;
}
export const EXTRA_KEY_PREFIX = '___';
export function getConvertedExtraKey(key: string): string {
if (!key) {
return '';
}
let _key = key;
if (key.indexOf('.') > 0) {
_key = key.split('.')[0];
}
return EXTRA_KEY_PREFIX + _key + EXTRA_KEY_PREFIX + key.slice(_key.length);
}
export function getOriginalExtraKey(key: string): string {
return key.replace(new RegExp(`${EXTRA_KEY_PREFIX}`, 'g'), '');
}
export interface IPropParent {
readonly props: IProps;
readonly owner: INode;
get path(): string[];
delete(prop: IProp): void;
}
export interface IProps extends Omit, | 'getExtraProp' | 'getExtraPropValue' | 'setExtraPropValue' | 'node'>, IPropParent {
/**
* 获取 props 对应的 node
*/
getNode(): INode;
get(path: string, createIfNone?: boolean): IProp | null;
export(stage?: IPublicEnumTransformStage): {
props?: IPublicTypePropsMap | IPublicTypePropsList;
extras?: ExtrasObject;
};
merge(value: IPublicTypePropsMap, extras?: IPublicTypePropsMap): void;
purge(): void;
query(path: string, createIfNone: boolean): IProp | null;
import(value?: IPublicTypePropsMap | IPublicTypePropsList | null, extras?: ExtrasObject): void;
}
export class Props implements IProps, IPropParent {
readonly id = uniqueId('props');
@obx.shallow private items: IProp[] = [];
@computed private get maps(): Map {
const maps = new Map();
if (this.items.length > 0) {
this.items.forEach((prop) => {
if (prop.key) {
maps.set(prop.key, prop);
}
});
}
return maps;
}
readonly path = [];
get props(): IProps {
return this;
}
readonly owner: INode;
/**
* 元素个数
*/
@computed get size() {
return this.items.length;
}
@obx type: 'map' | 'list' = 'map';
private purged = false;
constructor(owner: INode, value?: IPublicTypePropsMap | IPublicTypePropsList | null, extras?: ExtrasObject) {
makeObservable(this);
this.owner = owner;
if (Array.isArray(value)) {
this.type = 'list';
this.items = value.map(
(item, idx) => new Prop(this, item.value, item.name || idx, item.spread),
);
} else if (value != null) {
this.items = Object.keys(value).map((key) => new Prop(this, value[key], key, false));
}
if (extras) {
Object.keys(extras).forEach((key) => {
this.items.push(new Prop(this, (extras as any)[key], getConvertedExtraKey(key)));
});
}
}
@action
import(value?: IPublicTypePropsMap | IPublicTypePropsList | null, extras?: ExtrasObject) {
const originItems = this.items;
if (Array.isArray(value)) {
this.type = 'list';
this.items = value.map(
(item, idx) => new Prop(this, item.value, item.name || idx, item.spread),
);
} else if (value != null) {
this.type = 'map';
this.items = Object.keys(value).map((key) => new Prop(this, value[key], key));
} else {
this.type = 'map';
this.items = [];
}
if (extras) {
Object.keys(extras).forEach((key) => {
this.items.push(new Prop(this, (extras as any)[key], getConvertedExtraKey(key)));
});
}
originItems.forEach((item) => item.purge());
}
@action
merge(value: IPublicTypePropsMap, extras?: IPublicTypePropsMap) {
Object.keys(value).forEach((key) => {
this.query(key, true)!.setValue(value[key]);
this.query(key, true)!.setupItems();
});
if (extras) {
Object.keys(extras).forEach((key) => {
this.query(getConvertedExtraKey(key), true)!.setValue(extras[key]);
this.query(getConvertedExtraKey(key), true)!.setupItems();
});
}
}
export(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save): {
props?: IPublicTypePropsMap | IPublicTypePropsList;
extras?: ExtrasObject;
} {
stage = compatStage(stage);
if (this.items.length < 1) {
return {};
}
let allProps = {} as any;
let props: any = {};
const extras: any = {};
if (this.type === 'list') {
props = [];
this.items.forEach((item) => {
let value = item.export(stage);
let name = item.key as string;
if (name && typeof name === 'string' && name.startsWith(EXTRA_KEY_PREFIX)) {
name = getOriginalExtraKey(name);
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 || item.isUnset() || item.isVirtual()) return;
let value = item.export(stage);
if (value != null) {
allProps[name] = value;
}
});
// compatible vision
const transformedProps = this.transformToStatic(allProps);
Object.keys(transformedProps).forEach((name) => {
const value = transformedProps[name];
if (typeof name === 'string' && name.startsWith(EXTRA_KEY_PREFIX)) {
name = getOriginalExtraKey(name);
extras[name] = value;
} else {
props[name] = value;
}
});
}
return { props, extras };
}
/**
* @deprecated
*/
/* istanbul ignore next */
private transformToStatic(props: any) {
let transducers = this.owner.componentMeta?.prototype?.options?.transducers;
if (!transducers) {
return props;
}
if (!Array.isArray(transducers)) {
transducers = [transducers];
}
props = transducers.reduce((xprops: any, transducer: any) => {
if (transducer && typeof transducer.toStatic === 'function') {
return transducer.toStatic(xprops);
}
return xprops;
}, props);
return props;
}
/**
* 根据 path 路径查询属性
*
* @param createIfNone 当没有的时候,是否创建一个
*/
@action
query(path: string, createIfNone = true): IProp | null {
return this.get(path, createIfNone);
}
/**
* 获取某个属性,如果不存在,临时获取一个待写入
* @param createIfNone 当没有的时候,是否创建一个
*/
@action
get(path: string, createIfNone = false): IProp | null {
let entry = path;
let nest = '';
const i = path.indexOf('.');
if (i > 0) {
nest = path.slice(i + 1);
if (nest) {
entry = path.slice(0, i);
}
}
let prop = this.maps.get(entry);
if (!prop && createIfNone) {
prop = new Prop(this, UNSET, entry);
this.items.push(prop);
}
if (prop) {
return nest ? prop.get(nest, createIfNone) : prop;
}
return null;
}
/**
* 删除项
*/
@action
delete(prop: IProp): void {
const i = this.items.indexOf(prop);
if (i > -1) {
this.items.splice(i, 1);
prop.purge();
}
}
/**
* 删除 key
*/
@action
deleteKey(key: string): void {
this.items = this.items.filter((item, i) => {
if (item.key === key) {
item.purge();
this.items.splice(i, 1);
return false;
}
return true;
});
}
/**
* 添加值
*/
@action
add(
value: IPublicTypeCompositeValue | null,
key?: string | number,
spread = false,
options: any = {},
): IProp {
const prop = new Prop(this, value, key, spread, options);
this.items.push(prop);
return prop;
}
/**
* 是否存在 key
*/
has(key: string): boolean {
return this.maps.has(key);
}
/**
* 迭代器
*/
[Symbol.iterator](): { next(): { value: IProp } } {
let index = 0;
const { items } = this;
const length = items.length || 0;
return {
next() {
if (index < length) {
return {
value: items[index++],
done: false,
};
}
return {
value: undefined as any,
done: true,
};
},
};
}
/**
* 遍历
*/
@action
forEach(fn: (item: IProp, key: number | string | undefined) => void): void {
this.items.forEach((item) => {
return fn(item, item.key);
});
}
/**
* 遍历
*/
@action
map(fn: (item: IProp, key: number | string | undefined) => T): T[] | null {
return this.items.map((item) => {
return fn(item, item.key);
});
}
@action
filter(fn: (item: IProp, key: number | string | undefined) => boolean) {
return this.items.filter((item) => {
return fn(item, item.key);
});
}
/**
* 回收销毁
*/
@action
purge() {
if (this.purged) {
return;
}
this.purged = true;
this.items.forEach((item) => item.purge());
}
/**
* 获取某个属性, 如果不存在,临时获取一个待写入
* @param createIfNone 当没有的时候,是否创建一个
*/
@action
getProp(path: string, createIfNone = true): IProp | null {
return this.query(path, createIfNone) || null;
}
/**
* 获取单个属性值
*/
@action
getPropValue(path: string): any {
return this.getProp(path, false)?.value;
}
/**
* 设置单个属性值
*/
@action
setPropValue(path: string, value: any) {
this.getProp(path, true)!.setValue(value);
}
/**
* 获取 props 对应的 node
*/
getNode() {
return this.owner;
}
/**
* @deprecated
* 获取 props 对应的 node
*/
@action
toData() {
return this.export()?.props;
}
}
================================================
FILE: packages/designer/src/document/node/props/value-to-source.ts
================================================
function propertyNameRequiresQuotes(propertyName: string) {
try {
const context = {
worksWithoutQuotes: false,
};
// eslint-disable-next-line no-new-func
new Function('ctx', `ctx.worksWithoutQuotes = {${propertyName}: true}['${propertyName}']`)();
return !context.worksWithoutQuotes;
} catch (ex) {
return true;
}
}
function quoteString(str: string, { doubleQuote }: any) {
return doubleQuote ? `"${str.replace(/"/gu, '\\"')}"` : `'${str.replace(/'/gu, "\\'")}'`;
}
export function valueToSource(
value: any,
{
circularReferenceToken = 'CIRCULAR_REFERENCE',
doubleQuote = true,
includeFunctions = true,
includeUndefinedProperties = false,
indentLevel = 0,
indentString = ' ',
lineEnding = '\n',
visitedObjects = new Set(),
}: any = {},
): any {
switch (typeof value) {
case 'boolean':
return value ? `${indentString.repeat(indentLevel)}true` : `${indentString.repeat(indentLevel)}false`;
case 'function':
if (includeFunctions) {
return `${indentString.repeat(indentLevel)}${value}`;
}
return null;
case 'number':
return `${indentString.repeat(indentLevel)}${value}`;
case 'object':
if (!value) {
return `${indentString.repeat(indentLevel)}null`;
}
if (visitedObjects.has(value)) {
return `${indentString.repeat(indentLevel)}${circularReferenceToken}`;
}
if (value instanceof Date) {
return `${indentString.repeat(indentLevel)}new Date(${quoteString(value.toISOString(), {
doubleQuote,
})})`;
}
if (value instanceof Map) {
return value.size
? `${indentString.repeat(indentLevel)}new Map(${valueToSource([...value], {
circularReferenceToken,
doubleQuote,
includeFunctions,
includeUndefinedProperties,
indentLevel,
indentString,
lineEnding,
visitedObjects: new Set([value, ...visitedObjects]),
}).slice(indentLevel * indentString.length)})`
: `${indentString.repeat(indentLevel)}new Map()`;
}
if (value instanceof RegExp) {
return `${indentString.repeat(indentLevel)}/${value.source}/${value.flags}`;
}
if (value instanceof Set) {
return value.size
? `${indentString.repeat(indentLevel)}new Set(${valueToSource([...value], {
circularReferenceToken,
doubleQuote,
includeFunctions,
includeUndefinedProperties,
indentLevel,
indentString,
lineEnding,
visitedObjects: new Set([value, ...visitedObjects]),
}).slice(indentLevel * indentString.length)})`
: `${indentString.repeat(indentLevel)}new Set()`;
}
if (Array.isArray(value)) {
if (!value.length) {
return `${indentString.repeat(indentLevel)}[]`;
}
const itemsStayOnTheSameLine = value.every(
item => typeof item === 'object' &&
item &&
!(item instanceof Date) &&
!(item instanceof Map) &&
!(item instanceof RegExp) &&
!(item instanceof Set) &&
(Object.keys(item).length || value.length === 1),
);
let previousIndex: number | null = null;
value = value.reduce((items, item, index) => {
if (previousIndex !== null) {
for (let i = index - previousIndex - 1; i > 0; i -= 1) {
items.push(indentString.repeat(indentLevel + 1));
}
}
previousIndex = index;
item = valueToSource(item, {
circularReferenceToken,
doubleQuote,
includeFunctions,
includeUndefinedProperties,
indentLevel: itemsStayOnTheSameLine ? indentLevel : indentLevel + 1,
indentString,
lineEnding,
visitedObjects: new Set([value, ...visitedObjects]),
});
if (item === null) {
items.push(indentString.repeat(indentLevel + 1));
} else if (itemsStayOnTheSameLine) {
items.push(item.slice(indentLevel * indentString.length));
} else {
items.push(item);
}
return items;
}, []);
return itemsStayOnTheSameLine
? `${indentString.repeat(indentLevel)}[${value.join(', ')}]`
: `${indentString.repeat(indentLevel)}[${lineEnding}${value.join(
`,${lineEnding}`,
)}${lineEnding}${indentString.repeat(indentLevel)}]`;
}
value = Object.keys(value).reduce((entries, propertyName) => {
const propertyValue = value[propertyName];
const propertyValueString =
typeof propertyValue !== 'undefined' || includeUndefinedProperties
? valueToSource(value[propertyName], {
circularReferenceToken,
doubleQuote,
includeFunctions,
includeUndefinedProperties,
indentLevel: indentLevel + 1,
indentString,
lineEnding,
visitedObjects: new Set([value, ...visitedObjects]),
})
: null;
if (propertyValueString) {
const quotedPropertyName = propertyNameRequiresQuotes(propertyName)
? quoteString(propertyName, {
doubleQuote,
})
: propertyName;
const trimmedPropertyValueString = propertyValueString.slice((indentLevel + 1) * indentString.length);
if (typeof propertyValue === 'function' && trimmedPropertyValueString.startsWith(`${propertyName}()`)) {
entries.push(
`${indentString.repeat(indentLevel + 1)}${quotedPropertyName} ${trimmedPropertyValueString.slice(
propertyName.length,
)}`,
);
} else {
entries.push(`${indentString.repeat(indentLevel + 1)}${quotedPropertyName}: ${trimmedPropertyValueString}`);
}
}
return entries;
}, []);
return value.length
? `${indentString.repeat(indentLevel)}{${lineEnding}${value.join(
`,${lineEnding}`,
)}${lineEnding}${indentString.repeat(indentLevel)}}`
: `${indentString.repeat(indentLevel)}{}`;
case 'string':
return `${indentString.repeat(indentLevel)}${quoteString(value, {
doubleQuote,
})}`;
case 'symbol': {
let key = Symbol.keyFor(value);
if (typeof key === 'string') {
return `${indentString.repeat(indentLevel)}Symbol.for(${quoteString(key, {
doubleQuote,
})})`;
}
key = value.toString().slice(7, -1);
if (key) {
return `${indentString.repeat(indentLevel)}Symbol(${quoteString(key, {
doubleQuote,
})})`;
}
return `${indentString.repeat(indentLevel)}Symbol()`;
}
case 'undefined':
return `${indentString.repeat(indentLevel)}undefined`;
default:
return `${indentString.repeat(indentLevel)}undefined`;
}
}
export function getSource(value: any): string {
if (value && value.__source) {
return value.__source;
}
let source = valueToSource(value);
if (source === 'undefined') {
source = '';
}
if (value) {
try {
value.__source = source;
} catch (ex) {
console.error(ex);
}
}
return source;
}
================================================
FILE: packages/designer/src/icons/clone.tsx
================================================
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
export function IconClone(props: IconProps) {
return (
);
}
IconClone.displayName = 'Clone';
================================================
FILE: packages/designer/src/icons/component.tsx
================================================
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
export function IconComponent(props: IconProps) {
return (
);
}
IconComponent.displayName = 'Component';
================================================
FILE: packages/designer/src/icons/container.tsx
================================================
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
export function IconContainer(props: IconProps) {
return (
);
}
IconContainer.displayName = 'Container';
================================================
FILE: packages/designer/src/icons/hidden.tsx
================================================
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
export function IconHidden(props: IconProps) {
return (
);
}
IconHidden.displayName = 'Hidden';
================================================
FILE: packages/designer/src/icons/index.ts
================================================
export * from './lock';
export * from './hidden';
export * from './remove';
export * from './setting';
export * from './component';
export * from './clone';
export * from './page';
export * from './container';
export * from './unlock';
================================================
FILE: packages/designer/src/icons/lock.tsx
================================================
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
export function IconLock(props: IconProps) {
return (
);
}
IconLock.displayName = 'IconLock';
================================================
FILE: packages/designer/src/icons/page.tsx
================================================
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
export function IconPage(props: IconProps) {
return (
);
}
IconPage.displayName = 'Page';
================================================
FILE: packages/designer/src/icons/remove.tsx
================================================
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
export function IconRemove(props: IconProps) {
return (
);
}
IconRemove.displayName = 'Remove';
================================================
FILE: packages/designer/src/icons/setting.tsx
================================================
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
export function IconSetting(props: IconProps) {
return (
);
}
IconSetting.displayName = 'Setting';
================================================
FILE: packages/designer/src/icons/unlock.tsx
================================================
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
export function IconUnlock(props: IconProps) {
return (
);
}
IconUnlock.displayName = 'IconUnlock';
================================================
FILE: packages/designer/src/locale/en-US.json
================================================
{
"copy": "Copy",
"remove": "Remove",
"hide": "Hide",
"lock": "Lock",
"unlock": "Unlock",
"Condition Group": "Condition Group",
"No opened document": "No opened document, open some document to editing",
"locked": "locked",
"Item": "Item"
}
================================================
FILE: packages/designer/src/locale/index.ts
================================================
import { createIntl } from '@alilc/lowcode-editor-core';
import enUS from './en-US.json';
import zhCN from './zh-CN.json';
const { intl, intlNode, getLocale, setLocale } = createIntl({
'en-US': enUS,
'zh-CN': zhCN,
});
export { intl, intlNode, getLocale, setLocale };
================================================
FILE: packages/designer/src/locale/zh-CN.json
================================================
{
"copy": "复制",
"remove": "删除",
"hide": "隐藏",
"lock": "锁定",
"unlock": "解锁",
"Condition Group": "条件组",
"No opened document": "没有打开的页面,请选择页面打开编辑",
"locked": "已锁定",
"Item": "项目"
}
================================================
FILE: packages/designer/src/plugin/index.ts
================================================
export * from './plugin-context';
export * from './plugin-manager';
export * from './plugin-types';
export * from './plugin';
================================================
FILE: packages/designer/src/plugin/plugin-context.ts
================================================
/* eslint-disable no-multi-assign */
import { engineConfig, createModuleEventBus } from '@alilc/lowcode-editor-core';
import {
IPublicApiHotkey,
IPublicApiProject,
IPublicApiSkeleton,
IPublicApiSetters,
IPublicApiMaterial,
IPublicApiEvent,
IPublicApiCommon,
IPublicModelPluginContext,
IPluginPreferenceMananger,
IPublicTypePreferenceValueType,
IPublicModelEngineConfig,
IPublicApiLogger,
IPublicApiPlugins,
IPublicTypePluginDeclaration,
IPublicApiCanvas,
IPublicApiWorkspace,
IPublicEnumPluginRegisterLevel,
IPublicModelWindow,
IPublicApiCommonUI,
} from '@alilc/lowcode-types';
import {
IPluginContextOptions,
ILowCodePluginContextApiAssembler,
ILowCodePluginContextPrivate,
} from './plugin-types';
import { isValidPreferenceKey } from './plugin-utils';
export default class PluginContext implements
IPublicModelPluginContext, ILowCodePluginContextPrivate {
hotkey: IPublicApiHotkey;
project: IPublicApiProject;
skeleton: IPublicApiSkeleton;
setters: IPublicApiSetters;
material: IPublicApiMaterial;
event: IPublicApiEvent;
config: IPublicModelEngineConfig;
common: IPublicApiCommon;
logger: IPublicApiLogger;
plugins: IPublicApiPlugins;
preference: IPluginPreferenceMananger;
pluginEvent: IPublicApiEvent;
canvas: IPublicApiCanvas;
workspace: IPublicApiWorkspace;
registerLevel: IPublicEnumPluginRegisterLevel;
editorWindow: IPublicModelWindow;
commonUI: IPublicApiCommonUI;
isPluginRegisteredInWorkspace: false;
constructor(
options: IPluginContextOptions,
contextApiAssembler: ILowCodePluginContextApiAssembler,
) {
const { pluginName = 'anonymous', meta = {} } = options;
contextApiAssembler.assembleApis(this, pluginName, meta);
this.pluginEvent = createModuleEventBus(pluginName, 200);
const enhancePluginContextHook = engineConfig.get('enhancePluginContextHook');
if (enhancePluginContextHook) {
enhancePluginContextHook(this);
}
}
setPreference(
pluginName: string,
preferenceDeclaration: IPublicTypePluginDeclaration,
): void {
const getPreferenceValue = (
key: string,
defaultValue?: IPublicTypePreferenceValueType,
): IPublicTypePreferenceValueType | undefined => {
if (!isValidPreferenceKey(key, preferenceDeclaration)) {
return undefined;
}
const pluginPreference = this.plugins.getPluginPreference(pluginName) || {};
if (pluginPreference[key] === undefined || pluginPreference[key] === null) {
return defaultValue;
}
return pluginPreference[key];
};
this.preference = {
getPreferenceValue,
};
}
}
================================================
FILE: packages/designer/src/plugin/plugin-manager.ts
================================================
import { engineConfig } from '@alilc/lowcode-editor-core';
import { getLogger } from '@alilc/lowcode-utils';
import {
ILowCodePluginRuntime,
ILowCodePluginManager,
IPluginContextOptions,
PluginPreference,
ILowCodePluginContextApiAssembler,
} from './plugin-types';
import { filterValidOptions, isLowCodeRegisterOptions } from './plugin-utils';
import { LowCodePluginRuntime } from './plugin';
// eslint-disable-next-line import/no-named-as-default
import LowCodePluginContext from './plugin-context';
import { invariant } from '../utils';
import sequencify from './sequencify';
import semverSatisfies from 'semver/functions/satisfies';
import {
IPublicTypePluginRegisterOptions,
IPublicTypePreferenceValueType,
IPublicTypePlugin,
} from '@alilc/lowcode-types';
const logger = getLogger({ level: 'warn', bizName: 'designer:pluginManager' });
// 保留的事件前缀
const RESERVED_EVENT_PREFIX = ['designer', 'editor', 'skeleton', 'renderer', 'render', 'utils', 'plugin', 'engine', 'editor-core', 'engine-core', 'plugins', 'event', 'events', 'log', 'logger', 'ctx', 'context'];
export class LowCodePluginManager implements ILowCodePluginManager {
private plugins: ILowCodePluginRuntime[] = [];
pluginsMap: Map = new Map();
pluginContextMap: Map = new Map();
private pluginPreference?: PluginPreference = new Map();
contextApiAssembler: ILowCodePluginContextApiAssembler;
constructor(contextApiAssembler: ILowCodePluginContextApiAssembler, readonly viewName = 'global') {
this.contextApiAssembler = contextApiAssembler;
}
_getLowCodePluginContext = (options: IPluginContextOptions) => {
const { pluginName } = options;
let context = this.pluginContextMap.get(pluginName);
if (!context) {
context = new LowCodePluginContext(options, this.contextApiAssembler);
this.pluginContextMap.set(pluginName, context);
}
return context;
};
isEngineVersionMatched(versionExp: string): boolean {
const engineVersion = engineConfig.get('ENGINE_VERSION');
// ref: https://github.com/npm/node-semver#functions
// 1.0.1-beta should match '^1.0.0'
return semverSatisfies(engineVersion, versionExp, { includePrerelease: true });
}
/**
* register a plugin
* @param pluginConfigCreator - a creator function which returns the plugin config
* @param options - the plugin options
* @param registerOptions - the plugin register options
*/
async register(
pluginModel: IPublicTypePlugin,
options?: any,
registerOptions?: IPublicTypePluginRegisterOptions,
): Promise {
// registerOptions maybe in the second place
if (isLowCodeRegisterOptions(options)) {
registerOptions = options;
options = {};
}
let { pluginName, meta = {} } = pluginModel;
const { preferenceDeclaration, engines } = meta;
// filter invalid eventPrefix
const { eventPrefix } = meta;
const isReservedPrefix = RESERVED_EVENT_PREFIX.find((item) => item === eventPrefix);
if (isReservedPrefix) {
meta.eventPrefix = undefined;
logger.warn(`plugin ${pluginName} is trying to use ${eventPrefix} as event prefix, which is a reserved event prefix, please use another one`);
}
const ctx = this._getLowCodePluginContext({ pluginName, meta });
const customFilterValidOptions = engineConfig.get('customPluginFilterOptions', filterValidOptions);
const pluginTransducer = engineConfig.get('customPluginTransducer', null);
const newPluginModel = pluginTransducer ? await pluginTransducer(pluginModel, ctx, options) : pluginModel;
const newOptions = customFilterValidOptions(options, newPluginModel.meta?.preferenceDeclaration);
const config = newPluginModel(ctx, newOptions);
// compat the legacy way to declare pluginName
// @ts-ignore
pluginName = pluginName || config.name;
invariant(
pluginName,
'pluginConfigCreator.pluginName required',
config,
);
ctx.setPreference(pluginName, preferenceDeclaration);
const allowOverride = registerOptions?.override === true;
if (this.pluginsMap.has(pluginName)) {
if (!allowOverride) {
throw new Error(`Plugin with name ${pluginName} exists`);
} else {
// clear existing plugin
const originalPlugin = this.pluginsMap.get(pluginName);
logger.log(
'plugin override, originalPlugin with name ',
pluginName,
' will be destroyed, config:',
originalPlugin?.config,
);
originalPlugin?.destroy();
this.pluginsMap.delete(pluginName);
}
}
const engineVersionExp = engines && engines.lowcodeEngine;
if (engineVersionExp && !this.isEngineVersionMatched(engineVersionExp)) {
throw new Error(`plugin ${pluginName} skipped, engine check failed, current engine version is ${engineConfig.get('ENGINE_VERSION')}, meta.engines.lowcodeEngine is ${engineVersionExp}`);
}
const plugin = new LowCodePluginRuntime(pluginName, this, config, meta);
// support initialization of those plugins which registered
// after normal initialization by plugin-manager
if (registerOptions?.autoInit) {
await plugin.init();
}
this.plugins.push(plugin);
this.pluginsMap.set(pluginName, plugin);
logger.log(`plugin registered with pluginName: ${pluginName}, config: `, config, 'meta:', meta);
}
get(pluginName: string): ILowCodePluginRuntime | undefined {
return this.pluginsMap.get(pluginName);
}
getAll(): ILowCodePluginRuntime[] {
return this.plugins;
}
has(pluginName: string): boolean {
return this.pluginsMap.has(pluginName);
}
async delete(pluginName: string): Promise {
const plugin = this.plugins.find(({ name }) => name === pluginName);
if (!plugin) return false;
await plugin.destroy();
const idx = this.plugins.indexOf(plugin);
this.plugins.splice(idx, 1);
return this.pluginsMap.delete(pluginName);
}
async init(pluginPreference?: PluginPreference) {
const pluginNames: string[] = [];
const pluginObj: { [name: string]: ILowCodePluginRuntime } = {};
this.pluginPreference = pluginPreference;
this.plugins.forEach((plugin) => {
pluginNames.push(plugin.name);
pluginObj[plugin.name] = plugin;
});
const { missingTasks, sequence } = sequencify(pluginObj, pluginNames);
invariant(!missingTasks.length, 'plugin dependency missing', missingTasks);
logger.log('load plugin sequence:', sequence);
for (const pluginName of sequence) {
try {
await this.pluginsMap.get(pluginName)!.init();
} catch (e) /* istanbul ignore next */ {
logger.error(
`Failed to init plugin:${pluginName}, it maybe affect those plugins which depend on this.`,
);
logger.error(e);
}
}
}
async destroy() {
for (const plugin of this.plugins) {
await plugin.destroy();
}
}
get size() {
return this.pluginsMap.size;
}
getPluginPreference(pluginName: string): Record | null | undefined {
if (!this.pluginPreference) {
return null;
}
return this.pluginPreference.get(pluginName);
}
toProxy() {
return new Proxy(this, {
get(target, prop, receiver) {
if (target.pluginsMap.has(prop as string)) {
// 禁用态的插件,直接返回 undefined
if (target.pluginsMap.get(prop as string)!.disabled) {
return undefined;
}
return target.pluginsMap.get(prop as string)?.toProxy();
}
return Reflect.get(target, prop, receiver);
},
});
}
/* istanbul ignore next */
setDisabled(pluginName: string, flag = true) {
logger.warn(`plugin:${pluginName} has been set disable:${flag}`);
this.pluginsMap.get(pluginName)?.setDisabled(flag);
}
async dispose() {
await this.destroy();
this.plugins = [];
this.pluginsMap.clear();
}
}
================================================
FILE: packages/designer/src/plugin/plugin-types.ts
================================================
import {
IPublicApiHotkey,
IPublicApiProject,
IPublicApiSkeleton,
IPublicApiSetters,
IPublicApiMaterial,
IPublicApiEvent,
IPublicApiCommon,
IPublicApiPlugins,
IPublicTypePluginConfig,
IPublicApiLogger,
IPublicTypePreferenceValueType,
IPublicModelEngineConfig,
IPublicTypePlugin,
IPublicApiCanvas,
IPublicApiWorkspace,
IPublicTypePluginMeta,
IPublicTypePluginRegisterOptions,
IPublicModelWindow,
IPublicEnumPluginRegisterLevel,
IPublicApiCommonUI,
IPublicApiCommand,
} from '@alilc/lowcode-types';
import PluginContext from './plugin-context';
export type PluginPreference = Map>;
export interface ILowCodePluginRuntimeCore {
name: string;
dep: string[];
disabled: boolean;
config: IPublicTypePluginConfig;
logger: IPublicApiLogger;
meta: IPublicTypePluginMeta;
init(forceInit?: boolean): void;
isInited(): boolean;
destroy(): void;
toProxy(): any;
setDisabled(flag: boolean): void;
}
interface ILowCodePluginRuntimeExportsAccessor {
[propName: string]: any;
}
// eslint-disable-next-line max-len
export type ILowCodePluginRuntime = ILowCodePluginRuntimeCore & ILowCodePluginRuntimeExportsAccessor;
export interface ILowCodePluginContextPrivate {
set hotkey(hotkey: IPublicApiHotkey);
set project(project: IPublicApiProject);
set skeleton(skeleton: IPublicApiSkeleton);
set setters(setters: IPublicApiSetters);
set material(material: IPublicApiMaterial);
set event(event: IPublicApiEvent);
set config(config: IPublicModelEngineConfig);
set common(common: IPublicApiCommon);
set plugins(plugins: IPublicApiPlugins);
set logger(plugins: IPublicApiLogger);
set pluginEvent(event: IPublicApiEvent);
set canvas(canvas: IPublicApiCanvas);
set workspace(workspace: IPublicApiWorkspace);
set editorWindow(window: IPublicModelWindow);
set registerLevel(level: IPublicEnumPluginRegisterLevel);
set isPluginRegisteredInWorkspace(flag: boolean);
set commonUI(commonUI: IPublicApiCommonUI);
set command(command: IPublicApiCommand);
}
export interface ILowCodePluginContextApiAssembler {
assembleApis(
context: ILowCodePluginContextPrivate,
pluginName: string,
meta: IPublicTypePluginMeta,
): void;
}
interface ILowCodePluginManagerPluginAccessor {
[pluginName: string]: ILowCodePluginRuntime | any;
}
export interface ILowCodePluginManagerCore {
register(
pluginModel: IPublicTypePlugin,
pluginOptions?: any,
options?: IPublicTypePluginRegisterOptions,
): Promise;
init(pluginPreference?: Map>): Promise;
get(pluginName: string): ILowCodePluginRuntime | undefined;
getAll(): ILowCodePluginRuntime[];
has(pluginName: string): boolean;
delete(pluginName: string): any;
setDisabled(pluginName: string, flag: boolean): void;
dispose(): void;
_getLowCodePluginContext (options: IPluginContextOptions): PluginContext;
}
export type ILowCodePluginManager = ILowCodePluginManagerCore & ILowCodePluginManagerPluginAccessor;
export interface IPluginContextOptions {
pluginName: string;
meta?: IPublicTypePluginMeta;
}
================================================
FILE: packages/designer/src/plugin/plugin-utils.ts
================================================
import { isPlainObject } from 'lodash';
import { IPublicTypePluginRegisterOptions, IPublicTypePluginDeclaration } from '@alilc/lowcode-types';
export function isValidPreferenceKey(
key: string,
preferenceDeclaration: IPublicTypePluginDeclaration,
): boolean {
if (!preferenceDeclaration || !Array.isArray(preferenceDeclaration.properties)) {
return false;
}
return preferenceDeclaration.properties.some((prop) => {
return prop.key === key;
});
}
export function isLowCodeRegisterOptions(opts: any): opts is IPublicTypePluginRegisterOptions {
return opts && ('autoInit' in opts || 'override' in opts);
}
export function filterValidOptions(
opts: any,
preferenceDeclaration: IPublicTypePluginDeclaration,
) {
if (!opts || !isPlainObject(opts)) return opts;
const filteredOpts = {} as any;
Object.keys(opts).forEach((key) => {
if (isValidPreferenceKey(key, preferenceDeclaration)) {
const v = opts[key];
if (v !== undefined && v !== null) {
filteredOpts[key] = v;
}
}
});
return filteredOpts;
}
================================================
FILE: packages/designer/src/plugin/plugin.ts
================================================
import { getLogger, Logger } from '@alilc/lowcode-utils';
import {
ILowCodePluginRuntime,
ILowCodePluginManager,
} from './plugin-types';
import {
IPublicTypePluginConfig,
IPublicTypePluginMeta,
} from '@alilc/lowcode-types';
import { invariant } from '../utils';
export class LowCodePluginRuntime implements ILowCodePluginRuntime {
config: IPublicTypePluginConfig;
logger: Logger;
private manager: ILowCodePluginManager;
private _inited: boolean;
private pluginName: string;
meta: IPublicTypePluginMeta;
/**
* 标识插件状态,是否被 disabled
*/
private _disabled: boolean;
constructor(
pluginName: string,
manager: ILowCodePluginManager,
config: IPublicTypePluginConfig,
meta: IPublicTypePluginMeta,
) {
this.manager = manager;
this.config = config;
this.pluginName = pluginName;
this.meta = meta;
this.logger = getLogger({ level: 'warn', bizName: `plugin:${pluginName}` });
}
get name() {
return this.pluginName;
}
get dep() {
if (typeof this.meta.dependencies === 'string') {
return [this.meta.dependencies];
}
// compat legacy way to declare dependencies
const legacyDepValue = (this.config as any).dep;
if (typeof legacyDepValue === 'string') {
return [legacyDepValue];
}
return this.meta.dependencies || legacyDepValue || [];
}
get disabled() {
return this._disabled;
}
isInited() {
return this._inited;
}
async init(forceInit?: boolean) {
if (this._inited && !forceInit) return;
this.logger.log('method init called');
await this.config.init?.call(undefined);
this._inited = true;
}
async destroy() {
if (!this._inited) return;
this.logger.log('method destroy called');
await this.config?.destroy?.call(undefined);
this._inited = false;
}
setDisabled(flag = true) {
this._disabled = flag;
}
toProxy() {
invariant(this._inited, 'Could not call toProxy before init');
const exports = this.config.exports?.();
return new Proxy(this, {
get(target, prop, receiver) {
if ({}.hasOwnProperty.call(exports, prop)) {
return exports?.[prop as string];
}
return Reflect.get(target, prop, receiver);
},
});
}
async dispose() {
await this.manager.delete(this.name);
}
}
================================================
FILE: packages/designer/src/plugin/sequencify.ts
================================================
interface ITaks {
[key: string]: {
name: string;
dep: string[];
};
}
export function sequence({
tasks,
names,
results,
missing,
recursive,
nest,
parentName,
}: {
tasks: ITaks;
names: string[];
results: string[];
missing: string[];
recursive: string[][];
nest: string[];
parentName: string;
}) {
names.forEach((name) => {
if (results.indexOf(name) !== -1) {
return; // de-dup results
}
const node = tasks[name];
if (!node) {
missing.push([parentName, name].filter((d => !!d)).join('.'));
} else if (nest.indexOf(name) > -1) {
nest.push(name);
recursive.push(nest.slice(0));
nest.pop();
} else if (node.dep.length) {
nest.push(name);
sequence({
tasks,
parentName: name,
names: node.dep,
results,
missing,
recursive,
nest,
}); // recurse
nest.pop();
}
results.push(name);
});
}
// tasks: object with keys as task names
// names: array of task names
export default function (tasks: ITaks, names: string[]) {
let results: string[] = []; // the final sequence
const missing: string[] = []; // missing tasks
const recursive: string[][] = []; // recursive task dependencies
sequence({
tasks,
names,
results,
missing,
recursive,
nest: [],
});
if (missing.length || recursive.length) {
results = []; // results are incomplete at best, completely wrong at worst, remove them to avoid confusion
}
return {
sequence: results,
missingTasks: missing,
recursiveDependencies: recursive,
};
}
================================================
FILE: packages/designer/src/project/index.ts
================================================
export * from './project';
export * from './project-view';
================================================
FILE: packages/designer/src/project/project-view.tsx
================================================
import { Component } from 'react';
import { observer, engineConfig } from '@alilc/lowcode-editor-core';
import { Designer } from '../designer';
import { BuiltinSimulatorHostView } from '../builtin-simulator';
import './project.less';
export class BuiltinLoading extends Component {
render() {
return (
);
}
}
@observer
export class ProjectView extends Component<{ designer: Designer }> {
componentDidMount() {
const { designer } = this.props;
const { project } = designer;
project.onRendererReady(() => {
this.forceUpdate();
});
}
render() {
const { designer } = this.props;
const { project, projectSimulatorProps: simulatorProps } = designer;
const Simulator = designer.simulatorComponent || BuiltinSimulatorHostView;
const Loading = engineConfig.get('loadingComponent', BuiltinLoading);
return (
{!project?.simulator?.renderer && }
);
}
}
================================================
FILE: packages/designer/src/project/project.less
================================================
.lc-project {
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
.lc-project-empty {
width: 100%;
height: 100%;
font-size: 16px;
text-align: center;
background: transparent url(//img.alicdn.com/tfs/TB1xLKQAbj1gK0jSZFuXXcrHpXa-90-90.png) center 30% no-repeat;
padding-top: 50%;
}
.lc-simulator {
background-color: var(--color-background, rgb(237, 239, 243));
}
.lc-simulator-shell {
width: 100%;
height: 100%;
}
}
#engine-loading-wrapper {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
================================================
FILE: packages/designer/src/project/project.ts
================================================
import { obx, computed, makeObservable, action, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
import { IDesigner } from '../designer';
import { DocumentModel, isDocumentModel } from '../document';
import type { IDocumentModel } from '../document';
import { IPublicEnumTransformStage } from '@alilc/lowcode-types';
import type {
IBaseApiProject,
IPublicTypeProjectSchema,
IPublicTypeRootSchema,
IPublicTypeComponentsMap,
IPublicTypeSimulatorRenderer,
} from '@alilc/lowcode-types';
import { isLowCodeComponentType, isProCodeComponentType } from '@alilc/lowcode-utils';
import { ISimulatorHost } from '../simulator';
export interface IProject extends Omit,
'simulatorHost' |
'importSchema' |
'exportSchema' |
'openDocument' |
'getDocumentById' |
'getCurrentDocument' |
'addPropsTransducer' |
'onRemoveDocument' |
'onChangeDocument' |
'onSimulatorHostReady' |
'onSimulatorRendererReady' |
'setI18n' |
'setConfig' |
'currentDocument' |
'selection' |
'documents' |
'createDocument' |
'getDocumentByFileName'
> {
get designer(): IDesigner;
get simulator(): ISimulatorHost | null;
get currentDocument(): IDocumentModel | null | undefined;
get documents(): IDocumentModel[];
get i18n(): {
[local: string]: {
[key: string]: any;
};
};
mountSimulator(simulator: ISimulatorHost): void;
open(doc?: string | IDocumentModel | IPublicTypeRootSchema): IDocumentModel | null;
getDocumentByFileName(fileName: string): IDocumentModel | null;
createDocument(data?: IPublicTypeRootSchema): IDocumentModel;
load(schema?: IPublicTypeProjectSchema, autoOpen?: boolean | string): void;
getSchema(
stage?: IPublicEnumTransformStage,
): IPublicTypeProjectSchema;
getDocument(id: string): IDocumentModel | null;
onCurrentDocumentChange(fn: (doc: IDocumentModel) => void): () => void;
onSimulatorReady(fn: (args: any) => void): () => void;
onRendererReady(fn: () => void): () => void;
/**
* 分字段设置储存数据,不记录操作记录
*/
set(key: T, value: IPublicTypeProjectSchema[T]): void;
set(key: string, value: unknown): void;
/**
* 分字段获取储存数据
*/
get(key: T): IPublicTypeProjectSchema[T];
get(key: string): T;
get(key: string): unknown;
checkExclusive(activeDoc: DocumentModel): void;
setRendererReady(renderer: IPublicTypeSimulatorRenderer): void;
}
export class Project implements IProject {
private emitter: IEventBus = createModuleEventBus('Project');
@obx.shallow readonly documents: IDocumentModel[] = [];
private data: IPublicTypeProjectSchema = {
version: '1.0.0',
componentsMap: [],
componentsTree: [],
i18n: {},
};
private _simulator?: ISimulatorHost;
private isRendererReady: boolean = false;
/**
* 模拟器
*/
get simulator(): ISimulatorHost | null {
return this._simulator || null;
}
@computed get currentDocument(): IDocumentModel | null | undefined {
return this.documents.find((doc) => doc.active);
}
@obx private _config: any = {};
@computed get config(): any {
// TODO: parse layout Component
return this._config;
}
set config(value: any) {
this._config = value;
}
@obx.ref private _i18n: any = {};
@computed get i18n(): any {
return this._i18n;
}
set i18n(value: any) {
this._i18n = value || {};
}
private documentsMap = new Map();
constructor(readonly designer: IDesigner, schema?: IPublicTypeProjectSchema, readonly viewName = 'global') {
makeObservable(this);
this.load(schema);
}
private getComponentsMap(): IPublicTypeComponentsMap {
return this.documents.reduce((
componentsMap: IPublicTypeComponentsMap,
curDoc: IDocumentModel,
): IPublicTypeComponentsMap => {
const curComponentsMap = curDoc.getComponentsMap();
if (Array.isArray(curComponentsMap)) {
curComponentsMap.forEach((item) => {
const found = componentsMap.find((eItem) => {
if (
isProCodeComponentType(eItem) &&
isProCodeComponentType(item) &&
eItem.package === item.package &&
eItem.componentName === item.componentName
) {
return true;
} else if (
isLowCodeComponentType(eItem) &&
eItem.componentName === item.componentName
) {
return true;
}
return false;
});
if (found) return;
componentsMap.push(item);
});
}
return componentsMap;
}, [] as IPublicTypeComponentsMap);
}
/**
* 获取项目整体 schema
*/
getSchema(
stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save,
): IPublicTypeProjectSchema {
return {
...this.data,
componentsMap: this.getComponentsMap(),
componentsTree: this.documents
.filter((doc) => !doc.isBlank())
.map((doc) => doc.export(stage) || {} as IPublicTypeRootSchema),
i18n: this.i18n,
};
}
/**
* 替换当前 document 的 schema,并触发渲染器的 render
* @param schema
*/
setSchema(schema?: IPublicTypeProjectSchema) {
// FIXME: 这里的行为和 getSchema 并不对等,感觉不太对
const doc = this.documents.find((doc) => doc.active);
doc && schema?.componentsTree[0] && doc.import(schema?.componentsTree[0]);
this.simulator?.rerender();
}
/**
* 整体设置项目 schema
*
* @param autoOpen true 自动打开文档 string 指定打开的文件
*/
@action
load(schema?: IPublicTypeProjectSchema, autoOpen?: boolean | string) {
this.unload();
// load new document
this.data = {
version: '1.0.0',
componentsMap: [],
componentsTree: [],
i18n: {},
...schema,
};
this.config = schema?.config || this.config;
this.i18n = schema?.i18n || this.i18n;
if (autoOpen) {
if (autoOpen === true) {
// auto open first document or open a blank page
// this.open(this.data.componentsTree[0]);
const documentInstances = this.data.componentsTree.map((data) => this.createDocument(data));
// TODO: 暂时先读 config tabBar 里的值,后面看整个 layout 结构是否能作为引擎规范
if (this.config?.layout?.props?.tabBar?.items?.length > 0) {
// slice(1) 这个贼不雅,默认任务 fileName 是类'/fileName'的形式
documentInstances
.find((i) => i.fileName === this.config.layout.props.tabBar.items[0].path?.slice(1))
?.open();
} else {
documentInstances[0].open();
}
} else {
// auto open should be string of fileName
this.open(autoOpen);
}
}
}
/**
* 卸载当前项目数据
*/
unload() {
if (this.documents.length < 1) {
return;
}
for (let i = this.documents.length - 1; i >= 0; i--) {
this.documents[i].remove();
}
}
removeDocument(doc: IDocumentModel) {
const index = this.documents.indexOf(doc);
if (index < 0) {
return;
}
this.documents.splice(index, 1);
this.documentsMap.delete(doc.id);
}
/**
* 分字段设置储存数据,不记录操作记录
*/
set(key: T, value: IPublicTypeProjectSchema[T]): void;
set(key: string, value: unknown): void;
set(key: string, value: unknown): void {
if (key === 'config') {
this.config = value;
}
if (key === 'i18n') {
this.i18n = value;
}
Object.assign(this.data, { [key]: value });
}
/**
* 分字段设置储存数据
*/
get(key: T): IPublicTypeRootSchema[T];
get(key: string): T;
get(key: string): unknown;
get(key: string): any {
if (key === 'config') {
return this.config;
}
if (key === 'i18n') {
return this.i18n;
}
return Reflect.get(this.data, key);
}
getDocument(id: string): IDocumentModel | null {
// 此处不能使用 this.documentsMap.get(id),因为在乐高 rollback 场景,document.id 会被改成其他值
return this.documents.find((doc) => doc.id === id) || null;
}
getDocumentByFileName(fileName: string): IDocumentModel | null {
return this.documents.find((doc) => doc.fileName === fileName) || null;
}
@action
createDocument(data?: IPublicTypeRootSchema): IDocumentModel {
const doc = new DocumentModel(this, data || this?.data?.componentsTree?.[0]);
this.documents.push(doc);
this.documentsMap.set(doc.id, doc);
return doc;
}
open(doc?: string | IDocumentModel | IPublicTypeRootSchema): IDocumentModel | null {
if (!doc) {
const got = this.documents.find((item) => item.isBlank());
if (got) {
return got.open();
}
doc = this.createDocument();
return doc.open();
}
if (typeof doc === 'string' || typeof doc === 'number') {
const got = this.documents.find((item) => item.fileName === String(doc) || String(item.id) === String(doc));
if (got) {
return got.open();
}
const data = this.data.componentsTree.find((data) => data.fileName === String(doc));
if (data) {
doc = this.createDocument(data);
return doc.open();
}
return null;
} else if (isDocumentModel(doc)) {
return doc.open();
}
// else if (isPageSchema(doc)) {
// 暂时注释掉,影响了 diff 功能
// const foundDoc = this.documents.find(curDoc => curDoc?.rootNode?.id && curDoc?.rootNode?.id === doc?.id);
// if (foundDoc) {
// foundDoc.remove();
// }
// }
doc = this.createDocument(doc);
return doc.open();
}
checkExclusive(activeDoc: DocumentModel) {
this.documents.forEach((doc) => {
if (doc !== activeDoc) {
doc.suspense();
}
});
this.emitter.emit('current-document.change', activeDoc);
}
closeOthers(opened: DocumentModel) {
this.documents.forEach((doc) => {
if (doc !== opened) {
doc.close();
}
});
}
mountSimulator(simulator: ISimulatorHost) {
// TODO: 多设备 simulator 支持
this._simulator = simulator;
this.emitter.emit('lowcode_engine_simulator_ready', simulator);
}
setRendererReady(renderer: any) {
this.isRendererReady = true;
this.emitter.emit('lowcode_engine_renderer_ready', renderer);
}
onSimulatorReady(fn: (args: any) => void): () => void {
if (this._simulator) {
fn(this._simulator);
return () => {};
}
this.emitter.on('lowcode_engine_simulator_ready', fn);
return () => {
this.emitter.removeListener('lowcode_engine_simulator_ready', fn);
};
}
onRendererReady(fn: () => void): () => void {
if (this.isRendererReady) {
fn();
}
this.emitter.on('lowcode_engine_renderer_ready', fn);
return () => {
this.emitter.removeListener('lowcode_engine_renderer_ready', fn);
};
}
onCurrentDocumentChange(fn: (doc: IDocumentModel) => void): () => void {
this.emitter.on('current-document.change', fn);
return () => {
this.emitter.removeListener('current-document.change', fn);
};
}
}
================================================
FILE: packages/designer/src/transducers/index.ts
================================================
import { IPublicTypeTransformedComponentMetadata as Metadata } from '@alilc/lowcode-types';
export function legacyIssues(metadata: Metadata): Metadata {
const { devMode } = metadata;
return {
...metadata,
devMode: devMode?.replace(/(low|pro)code/, '$1Code') as Metadata['devMode'],
};
}
export function componentDefaults(metadata: Metadata): Metadata {
const { configure, componentName } = metadata;
const { component = {} } = configure;
if (!component.nestingRule) {
let m;
// uri match xx.Group set subcontrolling: true, childWhiteList
// eslint-disable-next-line no-cond-assign
if ((m = /^(.+)\.Group$/.exec(componentName))) {
// component.subControlling = true;
component.nestingRule = {
childWhitelist: [`${m[1]}`],
};
// eslint-disable-next-line no-cond-assign
} else if ((m = /^(.+)\.Node$/.exec(componentName))) {
// uri match xx.Node set selfControlled: false, parentWhiteList
// component.selfControlled = false;
component.nestingRule = {
parentWhitelist: [`${m[1]}`, componentName],
};
// eslint-disable-next-line no-cond-assign
} else if ((m = /^(.+)\.(Item|Node|Option)$/.exec(componentName))) {
// uri match .Item .Node .Option set parentWhiteList
component.nestingRule = {
parentWhitelist: [`${m[1]}`],
};
}
}
// if (component.isModal == null && /Dialog/.test(componentName)) {
// component.isModal = true;
// }
return {
...metadata,
configure: {
...configure,
component,
},
};
}
================================================
FILE: packages/designer/src/types/index.ts
================================================
import { isFormEvent, compatibleLegaoSchema, getNodeSchemaById, isNodeSchema } from '@alilc/lowcode-utils';
export type NodeRemoveOptions = {
suppressRemoveEvent?: boolean;
};
export const utils = {
isNodeSchema,
isFormEvent,
compatibleLegaoSchema,
getNodeSchemaById,
};
export enum EDITOR_EVENT {
NODE_CHILDREN_CHANGE = 'node.children.change',
NODE_VISIBLE_CHANGE = 'node.visible.change',
}
export type Utils = typeof utils;
================================================
FILE: packages/designer/src/utils/index.ts
================================================
export * from './invariant';
export * from './slot';
export * from './tree';
================================================
FILE: packages/designer/src/utils/invariant.ts
================================================
export function invariant(check: any, message: string, thing?: any) {
if (!check) {
throw new Error(`[designer] Invariant failed: ${message}${thing ? ` in '${thing}'` : ''}`);
}
}
================================================
FILE: packages/designer/src/utils/misc.ts
================================================
import Viewport from '../builtin-simulator/viewport';
import { ISimulatorHost } from '../simulator';
export function isElementNode(domNode: Element) {
return domNode.nodeType === Node.ELEMENT_NODE;
}
/**
* 判断节点是否在 viewport 内,判断依据:只要节点有一部分在 viewport 内,都算 true,其余情况 false
* @param domNode 待检测的节点
* @param viewport 画布 viewport
* @returns 是否在 viewport 内
*/
export function isDOMNodeVisible(domNode: Element, viewport: Viewport) {
const domNodeRect = domNode.getBoundingClientRect();
const { width, height } = viewport.contentBounds;
const { left, right, top, bottom, width: nodeWidth, height: nodeHeight } = domNodeRect;
return (
left >= -nodeWidth &&
top >= -nodeHeight &&
bottom <= height + nodeHeight &&
right <= width + nodeWidth
);
}
/**
* normalize triggers
* @param triggers
*/
export function normalizeTriggers(triggers: string[]) {
return triggers.map((trigger: string) => trigger?.toUpperCase());
}
/**
* make a handler that listen all sensors:document, avoid frame lost
*/
export function makeEventsHandler(
boostEvent: MouseEvent | DragEvent,
sensors: ISimulatorHost[],
): (fn: (sdoc: Document) => void) => void {
const topDoc = window.document;
const sourceDoc = boostEvent.view?.document || topDoc;
const docs = new Set();
docs.add(topDoc);
docs.add(sourceDoc);
sensors.forEach((sim) => {
const sdoc = sim.contentDocument;
if (sdoc) {
docs.add(sdoc);
}
});
return (handle: (sdoc: Document) => void) => {
docs.forEach((doc) => handle(doc));
};
}
================================================
FILE: packages/designer/src/utils/slot.ts
================================================
import { Node } from '../document/node/node';
export function includeSlot(node: Node, slotName: string | undefined): boolean {
const { slots = [] } = node;
return slots.some((slot) => {
return slotName && slotName === slot?.getExtraProp('name')?.getAsString();
});
}
export function removeSlot(node: Node, slotName: string | undefined): boolean {
const { slots = [] } = node;
return slots.some((slot, idx) => {
if (slotName && slotName === slot?.getExtraProp('name')?.getAsString()) {
slot.remove();
slots.splice(idx, 1);
return true;
}
return false;
});
}
================================================
FILE: packages/designer/src/utils/tree.ts
================================================
import { NodeChildren } from '../document/node/node-children';
type IterableArray = NodeChildren | any[];
export function foreachReverse(
arr: IterableArray,
action: (item: any) => void,
getter: (arr: IterableArray, index: number) => any,
context: any = {},
) {
for (let i = arr.length - 1; i >= 0; i--) {
action.call(context, getter(arr, i));
}
}
================================================
FILE: packages/designer/tests/__mocks__/document-model.ts
================================================
export class DocumentModel {
a = 1;
c = {};
constructor() {
const b = { x: { y: 2 } };
const c: number = 2;
this.a = b?.x?.y;
}
}
================================================
FILE: packages/designer/tests/__mocks__/node.ts
================================================
export class Node2 {
a = 1;
c = {};
constructor() {
const b = { x: { y: 2 } };
const c: number = 2;
this.a = b?.x?.y;
}
}
================================================
FILE: packages/designer/tests/bugs/prop-variable-jse.test.ts
================================================
import { Editor } from '@alilc/lowcode-editor-core';
import { IPublicEnumTransformStage } from '@alilc/lowcode-types';
import { isPlainObject, isVariable, isJSBlock } from '@alilc/lowcode-utils';
import '../fixtures/window';
import { Designer } from '../../src/designer/designer';
import { DocumentModel } from '../../src/document/document-model';
import { Project } from '../../src/project/project';
import formSchema from '../fixtures/schema/form';
import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory';
/**
* bug 背景:
* Prop 在每次 setValue 时都会调用 dispose 方法用于重新计算子 Prop,我认为在 Node 未完成初始化之前的 dispose 都是
* 无意义的,所以增加了判断条件来调用 dispose,结果导致了 variable 结果没有正确转成 JSExpression 结构。
*
* 因为 propsReducer 的 Init / Upgrade 阶段依然可以更改 props,且此时的 Node 也未完成初始化,不调用 dispose 则导致新的 Prop 结构无法生效
*/
function upgradePropsReducer(props: any): any {
if (!props || !isPlainObject(props)) {
return props;
}
if (isJSBlock(props)) {
if (props.value.componentName === 'Slot') {
return {
type: 'JSSlot',
title: (props.value.props as any)?.slotTitle,
name: (props.value.props as any)?.slotName,
value: props.value.children,
};
} else {
return props.value;
}
}
if (isVariable(props)) {
return {
type: 'JSExpression',
value: props.variable,
mock: props.value,
};
}
const newProps: any = {};
Object.keys(props).forEach((key) => {
if (/^__slot__/.test(key) && props[key] === true) {
return;
}
newProps[key] = upgradePropsReducer(props[key]);
});
return newProps;
}
describe('Node 方法测试', () => {
let editor: Editor;
let designer: Designer;
let project: Project;
let doc: DocumentModel;
it('原始 prop 值是 variable 结构,通过一个 propsReducer 转成了 JSExpression 结构', () => {
editor = new Editor();
designer = new Designer({ editor, shellModelFactory });
designer.addPropsReducer(upgradePropsReducer, IPublicEnumTransformStage.Upgrade);
project = designer.project;
doc = new DocumentModel(project, formSchema);
const form = doc.getNode('form');
expect(form.getPropValue('dataSource')).toEqual({
type: 'JSExpression',
value: 'state.formData',
})
});
});
================================================
FILE: packages/designer/tests/bugs/why.md
================================================
背景:
在 UT 的基础上,希望借助一些 Bug 修复来完成场景测试,从而进一步增强稳定性。
至少在真正的 E2E 测试来临之前,我们保证不会重复犯两次相同的错误。
做法:
Bugs 文件夹每个文件记录一个 bug 修复的场景测试~
================================================
FILE: packages/designer/tests/builtin-simulator/host.test.ts
================================================
import { IPublicTypePluginMeta } from './../../../../lib/packages/types/src/shell/type/plugin-meta.d';
import '../fixtures/window';
import {
Editor,
globalContext,
Hotkey as InnerHotkey,
Setters as InnerSetters,
} from '@alilc/lowcode-editor-core';
import { Workspace as InnerWorkspace } from '@alilc/lowcode-workspace';
import {
AssetType,
} from '@alilc/lowcode-utils';
import {
IPublicEnumDragObjectType,
} from '@alilc/lowcode-types';
import { Project } from '../../src/project/project';
import pageMetadata from '../fixtures/component-metadata/page';
import { Designer } from '../../src/designer/designer';
import { DocumentModel } from '../../src/document/document-model';
import formSchema from '../fixtures/schema/form';
import { getMockDocument, getMockWindow, getMockEvent, delayObxTick } from '../utils';
import { BuiltinSimulatorHost } from '../../src/builtin-simulator/host';
import { fireEvent } from '@testing-library/react';
import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory';
import { Setters, Workspace } from '@alilc/lowcode-shell';
import { ILowCodePluginContextApiAssembler, ILowCodePluginContextPrivate, LowCodePluginManager } from '@alilc/lowcode-designer';
import {
Skeleton as InnerSkeleton,
} from '@alilc/lowcode-editor-skeleton';
describe('Host 测试', () => {
let editor: Editor;
let designer: Designer;
let project: Project;
let doc: DocumentModel;
let host: BuiltinSimulatorHost;
beforeAll(() => {
editor = new Editor();
const pluginContextApiAssembler: ILowCodePluginContextApiAssembler = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
assembleApis: (context: ILowCodePluginContextPrivate, pluginName: string, meta: IPublicTypePluginMeta) => {
context.project = project;
const eventPrefix = meta?.eventPrefix || 'common';
context.workspace = workspace;
},
};
const innerPlugins = new LowCodePluginManager(pluginContextApiAssembler);
const innerWorkspace = new InnerWorkspace(() => {}, {});
const workspace = new Workspace(innerWorkspace);
const innerSkeleton = new InnerSkeleton(editor);
editor.set('skeleton' as any, innerSkeleton);
editor.set('innerHotkey', new InnerHotkey())
editor.set('setters', new Setters(new InnerSetters()));
editor.set('innerPlugins' as any, innerPlugins);
!globalContext.has(Editor) && globalContext.register(editor, Editor);
!globalContext.has('workspace') && globalContext.register(innerWorkspace, 'workspace');
});
beforeEach(() => {
designer = new Designer({ editor, shellModelFactory });
project = designer.project;
designer.createComponentMeta(pageMetadata);
doc = project.createDocument(formSchema);
host = new BuiltinSimulatorHost(designer.project, designer);
});
afterEach(() => {
project.unload();
project.mountSimulator(undefined);
designer._componentMetasMap.clear();
designer.purge();
host.purge();
designer = null;
project = null;
host = null;
});
describe('基础方法测试', () => {
it('setProps / get / set', async () => {
expect(host.currentDocument).toBe(designer.project.currentDocument);
expect(host.renderEnv).toBe('default');
expect(host.device).toBe('default');
expect(host.deviceClassName).toBeUndefined();
expect(host.requestHandlersMap).toBeNull();
host.setProps({
renderEnv: 'rax',
device: 'mobile',
deviceClassName: 'mobile-rocks',
componentsAsset: [
{
type: AssetType.JSText,
content: 'console.log(1)',
},
{
type: AssetType.JSUrl,
content: '//path/to/js',
},
],
theme: {
type: AssetType.CSSText,
content: '.theme {font-size: 50px;}',
},
requestHandlersMap: {},
});
expect(host.renderEnv).toBe('rax');
expect(host.device).toBe('mobile');
expect(host.deviceClassName).toBe('mobile-rocks');
expect(host.componentsAsset).toEqual([
{
type: AssetType.JSText,
content: 'console.log(1)',
},
{
type: AssetType.JSUrl,
content: '//path/to/js',
},
]);
expect(host.theme).toEqual({
type: AssetType.CSSText,
content: '.theme {font-size: 50px;}',
});
expect(host.componentsMap).toEqual(designer.componentsMap);
expect(host.requestHandlersMap).toEqual({});
host.set('renderEnv', 'vue');
expect(host.renderEnv).toBe('vue');
expect(host.getComponentContext).toThrow('Method not implemented.');
});
it('connect', () => {
const mockFn = jest.fn();
const mockRenderer = { isSimulatorRenderer: true };
host.connect(mockRenderer, mockFn);
expect(host.renderer).toEqual(mockRenderer);
// await delayObxTick();
expect(mockFn).toHaveBeenCalled();
});
it('mountViewport', () => {
const mockBounds = {
top: 10,
bottom: 100,
left: 10,
right: 100,
};
host.mountViewport({
getBoundingClientRect() {
return mockBounds;
},
});
expect(host.viewport.bounds).toEqual(mockBounds);
});
it('autorun', () => {
const mockFn = jest.fn();
host.autorun(mockFn);
expect(mockFn).toHaveBeenCalled();
});
it('purge', () => {
host.purge();
});
it('isEnter', () => {
const mockBounds = {
top: 10,
bottom: 100,
left: 10,
right: 100,
};
host.mountViewport({
getBoundingClientRect() {
return mockBounds;
},
});
expect(
host.isEnter({
globalX: 5,
globalY: 50,
}),
).toBeFalsy();
expect(
host.isEnter({
globalX: 115,
globalY: 50,
}),
).toBeFalsy();
expect(
host.isEnter({
globalX: 50,
globalY: 50,
}),
).toBeTruthy();
expect(
host.isEnter({
globalX: 50,
globalY: 5,
}),
).toBeFalsy();
expect(
host.isEnter({
globalX: 50,
globalY: 150,
}),
).toBeFalsy();
expect(
host.isEnter({
globalX: 150,
globalY: 150,
}),
).toBeFalsy();
});
it('fixEvent', () => {
expect(host.fixEvent({ fixed: true, clientX: 1 })).toEqual({ fixed: true, clientX: 1 });
});
it('findDOMNodes', () => {
host.connect({
findDOMNodes: () => {
return null;
},
}, () => {});
expect(host.findDOMNodes()).toBeNull();
const mockElems = [document.createElement('div')];
host.connect({
findDOMNodes: () => {
return mockElems;
},
}, () => {});
expect(host.findDOMNodes({})).toBe(mockElems);
expect(host.findDOMNodes({}, 'xxx')).toBeNull();
expect(host.findDOMNodes({}, 'div')).toEqual(mockElems);
});
it('getClosestNodeInstance', () => {
const mockFn = jest.fn(() => {
return {
node: {},
nodeId: 'id',
docId: 'docId',
};
});
host.connect({
getClosestNodeInstance: mockFn,
}, () => {});
expect(host.getClosestNodeInstance()).toEqual({
node: {},
nodeId: 'id',
docId: 'docId',
});
});
it('getNodeInstanceFromElement', () => {
expect(host.getNodeInstanceFromElement()).toBeNull();
host.getClosestNodeInstance = () => {
return null;
};
expect(host.getNodeInstanceFromElement({})).toBeNull();
host.getClosestNodeInstance = () => {
return {
docId: project.currentDocument.id,
nodeId: 'xxx',
};
};
expect(host.getNodeInstanceFromElement({})).toBeTruthy();
});
it('getDropContainer', () => {
host.getNodeInstanceFromElement = () => {
return {
node: doc.rootNode,
};
};
host.getDropContainer({
target: {},
dragObject: {
type: IPublicEnumDragObjectType.Node,
nodes: [doc.getNode('page')],
},
});
});
it('getComponentInstances', () => {
const mockNode = {
document: { id: 'docId' },
};
host.instancesMap = {
docId: {
get() {
return [{ comp: true }, { comp2: true }];
},
},
};
expect(host.getComponentInstances(mockNode))
.toEqual([{ comp: true }, { comp2: true }]);
const mockInst = { inst: true };
host.getClosestNodeInstance = () => {
return {
instance: mockInst,
};
};
expect(host.getComponentInstances(mockNode, { instance: mockInst }))
.toEqual([{ comp: true }, { comp2: true }]);
});
it('setNativeSelection / setDraggingState / setCopyState / clearState', () => {
const mockFn1 = jest.fn();
const mockFn2 = jest.fn();
const mockFn3 = jest.fn();
const mockFn4 = jest.fn();
host.connect({
setNativeSelection: mockFn1,
setDraggingState: mockFn2,
setCopyState: mockFn3,
clearState: mockFn4,
}, () => {});
host.setNativeSelection(true);
expect(mockFn1).toHaveBeenCalledWith(true);
host.setDraggingState(false);
expect(mockFn2).toHaveBeenCalledWith(false);
host.setCopyState(true);
expect(mockFn3).toHaveBeenCalledWith(true);
host.clearState();
expect(mockFn4).toHaveBeenCalled();
});
it('sensorAvailable / deactiveSensor', () => {
expect(host.sensorAvailable).toBeTruthy();
host.deactiveSensor();
expect(host.sensing).toBeFalsy();
});
it('getComponent', () => {
host.connect({
getComponent: () => {
return {};
},
}, () => {});
expect(host.getComponent()).toEqual({});
expect(host.createComponent()).toBeNull();
expect(host.setSuspense()).toBeFalsy();
});
it('setInstance', () => {
host.instancesMap = {};
host.setInstance('docId1', 'id1', [{}]);
expect(host.instancesMap.docId1.get('id1')).toEqual([{}]);
host.setInstance('docId1', 'id1', null);
expect(host.instancesMap.docId1.get('id1')).toBeUndefined();
});
});
describe('locate 方法', () => {
beforeEach(() => {
const mockBounds = {
top: 10,
bottom: 100,
left: 10,
right: 100,
};
host.mountViewport({
getBoundingClientRect() {
return mockBounds;
},
});
});
it('locate,没有 nodes', () => {
expect(host.locate({
dragObject: {
type: IPublicEnumDragObjectType.Node,
nodes: [],
},
})).toBeUndefined();
});
it('locate,没有 document', () => {
project.removeDocument(doc);
expect(host.locate({
dragObject: {
type: IPublicEnumDragObjectType.Node,
nodes: [doc.getNode('page')],
},
})).toBeNull();
});
it('notFoundComponent', () => {
expect(host.locate({
dragObject: {
type: IPublicEnumDragObjectType.Node,
nodes: [doc.getNode('form')],
},
})).toBeUndefined();
})
it('locate', () => {
host.locate({
dragObject: {
type: IPublicEnumDragObjectType.Node,
nodes: [doc.getNode('page')],
},
});
});
});
describe('事件测试', () => {
it('setupDragAndClick', () => {});
it('setupContextMenu', async () => {
const mockDocument = getMockDocument();
const mockWindow = getMockWindow(mockDocument);
const mockIframe = {
contentWindow: mockWindow,
contentDocument: mockDocument,
dispatchEvent() {},
};
host.set('library', [
{
package: '@ali/vc-deep',
library: 'lib',
urls: ['a.js', 'b.js'],
},
]);
host.componentsConsumer.consume(() => {});
host.injectionConsumer.consume(() => {});
await host.mountContentFrame(mockIframe);
host.setupContextMenu();
host.getNodeInstanceFromElement = () => {
return {
node: { componentMeta: { componentName: 'Button', getMetadata() { return {} } }, contains() {} },
};
};
const mockFn = jest.fn();
host.designer.editor.on('designer.builtinSimulator.contextmenu', mockFn);
fireEvent.contextMenu(document, {});
// TODO:
// expect(mockFn).toHaveBeenCalledWith({ selected: 'Button' });
});
});
it('事件测试', async () => {
const mockDocument = getMockDocument();
const mockWindow = getMockWindow(mockDocument);
const mockIframe = {
contentWindow: mockWindow,
contentDocument: mockDocument,
dispatchEvent() {},
};
// 非法分支测试
host.mountContentFrame();
expect(host._iframe).toBeUndefined();
host.set('library', [
{
package: '@ali/vc-deep',
library: 'lib',
urls: ['a.js', 'b.js'],
},
]);
host.componentsConsumer.consume(() => {});
host.injectionConsumer.consume(() => {});
await host.mountContentFrame(mockIframe);
expect(host.contentWindow).toBe(mockWindow);
mockDocument.triggerEventListener(
'mouseover',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'mouseleave',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'mousedown',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'mouseup',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'mousemove',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener('click', getMockEvent(document.createElement('input')), host);
mockDocument.triggerEventListener(
'dblclick',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'contextmenu',
getMockEvent(mockDocument.createElement('div')),
host,
);
});
});
================================================
FILE: packages/designer/tests/builtin-simulator/renderer.test.tsx
================================================
import React from 'react';
import set from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';
import '../fixtures/window';
import { Editor } from '@alilc/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import TestRenderer from 'react-test-renderer';
import { configure, render, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getMockRenderer } from '../utils';
import { isSimulatorRenderer } from '../../src/builtin-simulator/renderer';
describe('renderer 测试', () => {
it('renderer', () => {
expect(isSimulatorRenderer(getMockRenderer())).toBeTruthy();
});
});
================================================
FILE: packages/designer/tests/builtin-simulator/resource-consumer.test.ts
================================================
import ResourceConsumer from '../../src/builtin-simulator/resource-consumer';
import { delayObxTick, delay } from '../utils';
it('ResourceConsumer 测试,先消费再监听', async () => {
const con = new ResourceConsumer(() => ({ a: 1, b: 2 }));
const mockFn = jest.fn();
con.consume((data) => {
mockFn(data);
});
await delay(1000);
expect(mockFn).toHaveBeenCalledWith({ a: 1, b: 2 });
con.consume(() => {});
await con.waitFirstConsume();
con.dispose();
});
it('ResourceConsumer 测试,先消费再监听,isSimulatorRenderer', async () => {
const mockFn = jest.fn();
const con = new ResourceConsumer(() => ({ a: 1, b: 2 }), () => {
const o = { a: 3, b: 4 };
mockFn(o);
return o;
});
con.consume({ isSimulatorRenderer: true });
await delay(1000);
expect(mockFn).toHaveBeenCalledWith({ a: 3, b: 4 });
con.consume(() => {});
await con.waitFirstConsume();
});
it('ResourceConsumer 测试,先消费再监听,isSimulatorRenderer,没有 consume', async () => {
const mockFn = jest.fn();
const con = new ResourceConsumer(() => ({ a: 1, b: 2 }));
con.consume({ isSimulatorRenderer: true });
});
it('ResourceConsumer 测试,先监听再消费', async () => {
const con = new ResourceConsumer(() => ({ a: 1, b: 2 }));
con.waitFirstConsume();
const mockFn = jest.fn();
con.consume((data) => {
mockFn(data);
});
await delay(1000);
expect(mockFn).toHaveBeenCalledWith({ a: 1, b: 2 });
});
================================================
FILE: packages/designer/tests/builtin-simulator/viewport.test.ts
================================================
import '../fixtures/window';
import { getMockWindow, getMockElement, delay } from '../utils';
import { Editor, globalContext } from '@alilc/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { DocumentModel } from '../../src/document/document-model';
import Viewport from '../../src/builtin-simulator/viewport';
import { Designer } from '../../src/designer/designer';
import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory';
describe('Viewport 测试', () => {
let editor: Editor;
let designer: Designer;
let project: Project;
let doc: DocumentModel;
let viewport: Viewport;
let viewportElem;
beforeAll(() => {
editor = new Editor();
!globalContext.has(Editor) && globalContext.register(editor, Editor);
window.DOMRect = class {
constructor(top, left, width, height) {
return { top, left, width, height };
}
};
});
beforeEach(() => {
designer = new Designer({ editor, shellModelFactory });
project = designer.project;
// doc = project.createDocument(formSchema);
});
afterEach(() => {
project.unload();
// project.mountSimulator(undefined);
designer.purge();
designer = null;
project = null;
viewport = null;
});
it('基本函数测试', async () => {
const rect = {
width: 500,
height: 500,
top: 100,
bottom: 500,
left: 100,
right: 500,
};
viewportElem = getMockElement('div', rect);
viewport = new Viewport();
viewport.mount();
expect(viewport.viewportElement).toBeUndefined();
expect(viewport.width).toBe(1000);
expect(viewport.height).toBe(600);
expect(viewport.toGlobalPoint({ left: 0, top: 0 })).toEqual({ left: 0, top: 0 });
expect(viewport.toLocalPoint({ left: 0, top: 0 })).toEqual({ left: 0, top: 0 });
viewport.mount(viewportElem);
expect(viewport.viewportElement).toBe(viewportElem);
expect(viewport.bounds).toEqual(rect);
expect(viewport.contentBounds).toEqual({ top: 0, left: 0, width: 500, height: 500 });
expect(viewport.rect).toEqual(rect);
expect(viewport.width).toBe(500);
expect(viewport.contentWidth).toBe('100%');
expect(viewport.height).toBe(500);
expect(viewport.contentHeight).toBe('100%');
await delay(100);
viewportElem.setWidth(300);
viewport.width = 300;
expect(viewport.width).toBe(300);
await delay(100);
viewportElem.setHeight(300);
viewport.height = 300;
expect(viewport.height).toBe(300);
viewport.contentWidth = 200;
expect(viewport.contentWidth).toBe(200);
viewport.contentHeight = 200;
expect(viewport.contentHeight).toBe(200);
});
it('scale', () => {
const rect = {
width: 500,
height: 500,
top: 100,
bottom: 500,
left: 100,
right: 500,
};
viewportElem = getMockElement('div', rect);
viewport = new Viewport();
viewport.mount(viewportElem);
expect(viewport.scale).toBe(1);
viewport.scale = 2;
expect(viewport.scale).toBe(2);
expect(viewport.contentWidth).toBe(500 / 2);
expect(viewport.contentHeight).toBe(500 / 2);
viewport.width = 300;
viewportElem.setWidth(300);
expect(viewport.contentWidth).toBe(300 / 2);
viewport.height = 300;
viewportElem.setHeight(300);
expect(viewport.contentHeight).toBe(300 / 2);
expect(() => { viewport.scale = NaN; }).toThrow();
expect(() => { viewport.scale = -1; }).toThrow();
});
it('setScrollTarget / scrollTarget / scrolling', async () => {
const rect = {
width: 500,
height: 500,
top: 100,
bottom: 500,
left: 100,
right: 500,
};
viewportElem = getMockElement('div', rect);
viewport = new Viewport();
viewport.mount(viewportElem);
const mockWindow = getMockWindow();
viewport.setScrollTarget(mockWindow);
// TODO: 待 mock
viewport.scrollTarget;
// expect(viewport.scrollTarget).toBe(mockWindow);
// mock scrollTarget
// viewport._scrollTarget = { left: 0, top: 0 };
// viewport._scrollTarget.left = 123;
// viewport._scrollTarget.top = 1234;
mockWindow.triggerEventListener('scroll');
expect(viewport.scrolling).toBeTruthy();
// TODO: 待 mock
viewport.scrollX;
viewport.scrollY;
// expect(viewport.scrollX).toBe(123);
// expect(viewport.scrollY).toBe(1234);
await delay(100);
expect(viewport.scrolling).toBeFalsy();
mockWindow.triggerEventListener('resize');
});
it('toGlobalPoint / toLocalPoint', () => {
const rect = {
width: 500,
height: 500,
top: 100,
bottom: 500,
left: 100,
right: 500,
};
viewportElem = getMockElement('div', rect);
viewport = new Viewport();
viewport.mount(viewportElem);
expect(viewport.toGlobalPoint({ clientX: 100, clientY: 100 })).toEqual({ clientX: 200, clientY: 200 });
expect(viewport.toLocalPoint({ clientX: 200, clientY: 200 })).toEqual({ clientX: 100, clientY: 100 });
viewport.scale = 2;
expect(viewport.toGlobalPoint({ clientX: 100, clientY: 100 })).toEqual({ clientX: 300, clientY: 300 });
expect(viewport.toLocalPoint({ clientX: 300, clientY: 300 })).toEqual({ clientX: 100, clientY: 100 });
});
});
================================================
FILE: packages/designer/tests/builtin-simulator/bem-tools/drag-resize-engine.test.ts
================================================
import '../../fixtures/window';
import { Editor, globalContext } from '@alilc/lowcode-editor-core';
import { Project } from '../../../src/project/project';
import { DocumentModel } from '../../../src/document/document-model';
import { Designer } from '../../../src/designer/designer';
import DragResizeEngine from '../../../src/builtin-simulator/bem-tools/drag-resize-engine';
import formSchema from '../../fixtures/schema/form';
import { fireEvent, createEvent } from '@testing-library/react';
import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory';
describe('DragResizeEngine 测试', () => {
let editor: Editor;
let designer: Designer;
let project: Project;
let doc: DocumentModel;
let resizeEngine: DragResizeEngine;
beforeAll(() => {
editor = new Editor();
!globalContext.has(Editor) && globalContext.register(editor, Editor);
});
beforeEach(() => {
designer = new Designer({ editor, shellModelFactory });
project = designer.project;
doc = project.createDocument(formSchema);
doc.open();
resizeEngine = new DragResizeEngine(designer);
});
afterEach(() => {
project.unload();
project.mountSimulator(undefined);
designer.purge();
resizeEngine = null;
designer = null;
project = null;
});
it('from', () => {
const resizeStartMockFn = jest.fn();
const resizeMockFn = jest.fn();
const resizeEndMockFn = jest.fn();
const offResizeStart = resizeEngine.onResizeStart(resizeStartMockFn);
const offResize = resizeEngine.onResize(resizeMockFn);
const offResizeEnd = resizeEngine.onResizeEnd(resizeEndMockFn);
const boostedNode = doc.getNode('node_k1ow3cbn');
const mockBoostFn = jest
.fn((e) => {
return boostedNode;
});
// do nothing
const noop = resizeEngine.from();
noop();
const offFrom = resizeEngine.from(document, 'e', mockBoostFn);
const mouseDownEvt = createEvent.mouseDown(document, { clientX: 100, clientY: 100 });
fireEvent(document, mouseDownEvt);
expect(resizeStartMockFn).toHaveBeenCalledTimes(1);
expect(resizeStartMockFn.mock.calls[0][0]).toBe(mouseDownEvt);
expect(resizeStartMockFn.mock.calls[0][1]).toBe('e');
expect(resizeStartMockFn.mock.calls[0][2]).toBe(boostedNode);
expect(resizeEngine.isDragResizing()).toBeTruthy();
const mouseMoveEvt1 = createEvent.mouseMove(document, { clientX: 108, clientY: 108 });
fireEvent(document, mouseMoveEvt1);
expect(resizeMockFn).toHaveBeenCalledTimes(1);
expect(resizeMockFn.mock.calls[0][0]).toBe(mouseMoveEvt1);
expect(resizeMockFn.mock.calls[0][1]).toBe('e');
expect(resizeMockFn.mock.calls[0][2]).toBe(boostedNode);
expect(resizeMockFn.mock.calls[0][3]).toBe(8);
expect(resizeMockFn.mock.calls[0][4]).toBe(8);
const mouseMoveEvt2 = createEvent.mouseMove(document, { clientX: 110, clientY: 110 }, 10, 10);
fireEvent(document, mouseMoveEvt2);
expect(resizeMockFn).toHaveBeenCalledTimes(2);
expect(resizeMockFn.mock.calls[1][0]).toBe(mouseMoveEvt2);
expect(resizeMockFn.mock.calls[1][1]).toBe('e');
expect(resizeMockFn.mock.calls[1][2]).toBe(boostedNode);
expect(resizeMockFn.mock.calls[1][3]).toBe(10);
expect(resizeMockFn.mock.calls[1][4]).toBe(10);
const mouseUpEvt = createEvent.mouseUp(document, { clientX: 118, clientY: 118 });
fireEvent(document, mouseUpEvt);
expect(resizeEndMockFn).toHaveBeenCalledTimes(1);
expect(resizeEndMockFn.mock.calls[0][0]).toBe(mouseUpEvt);
expect(resizeEndMockFn.mock.calls[0][1]).toBe('e');
expect(resizeEndMockFn.mock.calls[0][2]).toBe(boostedNode);
expect(resizeEngine.isDragResizing()).toBeFalsy();
offResizeStart();
offResize();
offResizeEnd();
resizeStartMockFn.mockClear();
resizeMockFn.mockClear();
fireEvent.mouseMove(document, { clientX: 100, clientY: 100 });
expect(resizeMockFn).not.toHaveBeenCalled();
offFrom();
fireEvent.mouseDown(document, { clientX: 100, clientY: 100 });
expect(resizeStartMockFn).not.toHaveBeenCalled();
});
it('has sensor', () => {
const mockDoc = document.createElement('iframe').contentWindow?.document;
project.mountSimulator({
sensorAvailable: true,
contentDocument: document,
});
const mockBoostFn = jest
.fn((e) => {
return doc.getNode('node_k1ow3cbn');
});
const offFrom = resizeEngine.from(document, 'e', mockBoostFn);
// TODO: 想办法 mock 一个 iframe.currentDocument
fireEvent.mouseDown(document, { clientX: 100, clientY: 100 });
});
});
================================================
FILE: packages/designer/tests/builtin-simulator/bem-tools/manager.test.tsx
================================================
import '../../fixtures/window';
import { Editor } from '@alilc/lowcode-editor-core';
import { Designer } from '../../../src/designer/designer';
import { BemToolsManager } from '../../../src/builtin-simulator/bem-tools/manager';
import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory';
describe('Node 方法测试', () => {
let editor: Editor;
let designer: Designer;
// let project: Project;
// let doc: DocumentModel;
let manager: BemToolsManager;
beforeEach(() => {
editor = new Editor();
designer = new Designer({ editor, shellModelFactory });
// project = designer.project;
// doc = new DocumentModel(project, formSchema);
manager = new BemToolsManager(designer);
});
afterEach(() => {
// project.unload();
designer.purge();
editor = null;
designer = null;
// project = null;
});
it('addBemTools / removeBemTools / getAllBemTools', () => {
manager.addBemTools({
name: 't1',
item: (props: any) => { return ; },
});
expect(manager.getAllBemTools().length).toBe(1);
expect(() => {
manager.addBemTools({
name: 't1',
item: (props: any) => { return ; },
});
}).toThrow(/already exists/);
manager.removeBemTools('t2');
expect(manager.getAllBemTools().length).toBe(1);
manager.removeBemTools('t1');
expect(manager.getAllBemTools().length).toBe(0);
});
});
================================================
FILE: packages/designer/tests/builtin-simulator/utils/parse-metadata.test.ts
================================================
import '../../fixtures/window';
import PropTypes from 'prop-types';
import { LowcodeTypes, parseMetadata, parseProps } from '../../../src/builtin-simulator/utils/parse-metadata';
import { default as ReactPropTypesSecret } from 'prop-types/lib/ReactPropTypesSecret';
describe('parseMetadata', () => {
it('parseMetadata', async () => {
const md1 = parseMetadata('Div');
const md2 = parseMetadata({ componentName: 'Div' });
});
it('LowcodeTypes.shape', async () => {
const result = (window as any).PropTypes.shape()
expect(result).toBeDefined();
});
});
describe('LowcodeTypes basic type validators', () => {
it('should validate string types', () => {
const stringValidator = LowcodeTypes.string;
// 对 stringValidator 进行测试
const props = { testProp: 'This is a string' };
const propName = 'testProp';
const componentName = 'TestComponent';
const result = stringValidator(props, propName, componentName, 'prop', null, ReactPropTypesSecret);
expect(result).toBeNull(); // No error for valid string
});
it('should fail with a non-string type', () => {
const stringValidator = LowcodeTypes.string;
const props = { testProp: 42 };
const propName = 'testProp';
const componentName = 'TestComponent';
const result = stringValidator(props, propName, componentName, 'prop', null, ReactPropTypesSecret);
expect(result).toBeInstanceOf(Error); // Error for non-string type
expect(result.message).toContain('Invalid prop `testProp` of type `number` supplied to `TestComponent`, expected `string`.');
});
it('should pass with a valid number', () => {
const numberValidator = LowcodeTypes.number;
const props = { testProp: 42 };
const propName = 'testProp';
const componentName = 'TestComponent';
const result = numberValidator(props, propName, componentName, 'prop', null, ReactPropTypesSecret);
expect(result).toBeNull(); // No error for valid number
});
it('should fail with a non-number type', () => {
const numberValidator = LowcodeTypes.number;
const props = { testProp: 'Not a number' };
const propName = 'testProp';
const componentName = 'TestComponent';
const result = numberValidator(props, propName, componentName, 'prop', null, ReactPropTypesSecret);
expect(result).toBeInstanceOf(Error); // Error for non-number type
expect(result.message).toContain('Invalid prop `testProp` of type `string` supplied to `TestComponent`, expected `number`.');
});
});
describe('Custom type constructors', () => {
it('should create a custom type validator using define', () => {
const customType = LowcodeTypes.define(PropTypes.string, 'customType');
const props = { testProp: 'This is a string' };
const propName = 'testProp';
const componentName = 'TestComponent';
// 测试有效值
const validResult = customType(props, propName, componentName, 'prop', null, ReactPropTypesSecret);
expect(validResult).toBeNull(); // No error for valid string
// 测试无效值
const invalidProps = { testProp: 42 };
const invalidResult = customType(invalidProps, propName, componentName, 'prop', null, ReactPropTypesSecret);
expect(invalidResult).toBeInstanceOf(Error); // Error for non-string type
// 验证 lowcodeType 属性
expect(customType.lowcodeType).toEqual('customType');
// 验证 isRequired 属性
const requiredResult = customType.isRequired(invalidProps, propName, componentName, 'prop', null, ReactPropTypesSecret);
expect(requiredResult).toBeInstanceOf(Error); // Error for non-string type
});
});
describe('Advanced type constructors', () => {
describe('oneOf Type Validator', () => {
const oneOfValidator = LowcodeTypes.oneOf(['red', 'green', 'blue']);
const propName = 'color';
const componentName = 'ColorPicker';
it('should pass with a valid value', () => {
const props = { color: 'red' };
const result = oneOfValidator(props, propName, componentName, 'prop', null, ReactPropTypesSecret);
expect(result).toBeNull(); // No error for valid value
});
it('should fail with an invalid value', () => {
const props = { color: 'yellow' };
const result = oneOfValidator(props, propName, componentName, 'prop', null, ReactPropTypesSecret);
expect(result).toBeInstanceOf(Error); // Error for invalid value
expect(result.message).toContain(`Invalid prop \`${propName}\` of value \`yellow\` supplied to \`${componentName}\`, expected one of ["red","green","blue"].`);
});
it('should fail with a non-existing value', () => {
const props = { color: 'others' };
const result = oneOfValidator(props, propName, componentName, 'prop', null, ReactPropTypesSecret);
expect(result).toBeInstanceOf(Error); // Error for non-existing value
expect(result.message).toContain(`Invalid prop \`${propName}\` of value \`others\` supplied to \`${componentName}\`, expected one of ["red","green","blue"].`);
});
});
});
describe('parseProps function', () => {
it('should correctly parse propTypes and defaultProps', () => {
const component = {
propTypes: {
name: LowcodeTypes.string,
age: LowcodeTypes.number,
},
defaultProps: {
name: 'John Doe',
age: 30,
},
};
const parsedProps = parseProps(component);
// 测试结果长度
expect(parsedProps.length).toBe(2);
// 测试 name 属性
const nameProp: any = parsedProps.find(prop => prop.name === 'name');
expect(nameProp).toBeDefined();
expect(nameProp.propType).toEqual('string');
expect(nameProp.defaultValue).toEqual('John Doe');
// 测试 age 属性
const ageProp: any = parsedProps.find(prop => prop.name === 'age');
expect(ageProp).toBeDefined();
expect(ageProp.propType).toEqual('number');
expect(ageProp.defaultValue).toEqual(30);
});
});
describe('parseProps function', () => {
it('should correctly parse propTypes and defaultProps', () => {
const component = {
propTypes: {
name: LowcodeTypes.string,
age: LowcodeTypes.number,
},
defaultProps: {
name: 'John Doe',
age: 30,
},
};
const parsedProps = parseProps(component);
// 测试结果长度
expect(parsedProps.length).toBe(2);
// 测试 name 属性
const nameProp: any = parsedProps.find(prop => prop.name === 'name');
expect(nameProp).toBeDefined();
expect(nameProp.propType).toEqual('string');
expect(nameProp.defaultValue).toEqual('John Doe');
// 测试 age 属性
const ageProp: any = parsedProps.find(prop => prop.name === 'age');
expect(ageProp).toBeDefined();
expect(ageProp.propType).toEqual('number');
expect(ageProp.defaultValue).toEqual(30);
});
});
================================================
FILE: packages/designer/tests/builtin-simulator/utils/path.test.ts
================================================
import {
generateComponentName,
getNormalizedImportPath,
isPackagePath,
toTitleCase,
makeRelativePath,
removeVersion,
resolveAbsoluatePath,
joinPath,
} from '../../../src/builtin-simulator/utils/path';
describe('builtin-simulator/utils/path 测试', () => {
it('isPackagePath', () => {
expect(isPackagePath('a')).toBeTruthy();
expect(isPackagePath('@ali/a')).toBeTruthy();
expect(isPackagePath('@alife/a')).toBeTruthy();
expect(isPackagePath('a.b')).toBeTruthy();
expect(isPackagePath('./a')).toBeFalsy();
expect(isPackagePath('../a')).toBeFalsy();
expect(isPackagePath('/a')).toBeFalsy();
});
it('toTitleCase', () => {
expect(toTitleCase('a')).toBe('A');
expect(toTitleCase('a_b')).toBe('AB');
expect(toTitleCase('a b')).toBe('AB');
expect(toTitleCase('a-b')).toBe('AB');
expect(toTitleCase('a.b')).toBe('AB');
expect(toTitleCase('a.b.cx')).toBe('ABCx');
});
it('generateComponentName', () => {
expect(generateComponentName('a/index.js')).toBe('A');
expect(generateComponentName('a_b/index.js')).toBe('AB');
expect(generateComponentName('a_b/index.web.js')).toBe('AB');
expect(generateComponentName('a_b/index.xxx.js')).toBe('AB');
expect(generateComponentName('a_b')).toBe('AB');
expect(generateComponentName('')).toBe('Component');
});
it('getNormalizedImportPath', () => {
expect(getNormalizedImportPath('/a')).toBe('/a');
expect(getNormalizedImportPath('/a/')).toBe('/a/');
expect(getNormalizedImportPath('/a/index.js')).toBe('/a');
expect(getNormalizedImportPath('/a/index.ts')).toBe('/a');
expect(getNormalizedImportPath('/a/index.jsx')).toBe('/a');
expect(getNormalizedImportPath('/a/index.tsx')).toBe('/a');
expect(getNormalizedImportPath('/a/index.x')).toBe('/a/index.x');
});
it('makeRelativePath', () => {
expect(makeRelativePath('/a/b/c', '/a/b')).toBe('c');
expect(makeRelativePath('a/b/c', '/a/c')).toBe('a/b/c');
expect(makeRelativePath('/a/b/c', '/a/c')).toBe('./b/c');
expect(makeRelativePath('/a/b/c', '/a/c/d')).toBe('../b/c');
});
it('resolveAbsoluatePath', () => {
expect(resolveAbsoluatePath('/a/b/c', '/a')).toBe('/a/b/c');
expect(resolveAbsoluatePath('@ali/fe', '/a')).toBe('@ali/fe');
expect(resolveAbsoluatePath('./a/b', '/c')).toBe('/c/a/b');
expect(resolveAbsoluatePath('./a/b/d', '/c')).toBe('/c/a/b/d');
expect(resolveAbsoluatePath('../a/b', '/c')).toBe('/a/b');
expect(resolveAbsoluatePath('../a/b/d', '/c')).toBe('/a/b/d');
expect(resolveAbsoluatePath('../../a', 'c')).toBe('../a');
});
it('joinPath', () => {
expect(joinPath('/a', 'b', 'c')).toBe('/a/b/c');
expect(joinPath('a', 'b', 'c')).toBe('./a/b/c');
});
it('removeVersion', () => {
expect(removeVersion('@ali/fe')).toBe('@ali/fe');
expect(removeVersion('@ali/fe@1.0.0/index')).toBe('@ali/fe/index');
expect(removeVersion('haha')).toBe('haha');
});
});
================================================
FILE: packages/designer/tests/builtin-simulator/utils/throttle.test.ts
================================================
import '../../fixtures/disable-raf';
import { throttle } from '../../../src/builtin-simulator/utils/throttle';
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const cb = jest.fn();
describe('throttle', () => {
it('simple', async () => {
const fn = throttle(cb, 1000);
fn();
expect(cb).toBeCalledTimes(1);
await delay(200);
fn();
await delay(400);
fn();
expect(cb).toBeCalledTimes(1);
});
});
================================================
FILE: packages/designer/tests/designer/active-tracker.test.ts
================================================
import '../fixtures/window';
import { ActiveTracker } from '../../src/designer/active-tracker';
it('ActiveTracker 测试,Node', () => {
const tracker = new ActiveTracker();
const mockFn = jest.fn();
const mockNode = { isNode: true };
const off = tracker.onChange(mockFn);
tracker.track(mockNode);
expect(mockFn).toHaveBeenCalledWith({ node: mockNode });
expect(tracker.currentNode).toBe(mockNode);
off();
mockFn.mockClear();
tracker.track(mockNode);
expect(mockFn).not.toHaveBeenCalled();
});
it('ActiveTracker 测试,ActiveTarget', () => {
const tracker = new ActiveTracker();
const mockFn = jest.fn();
const mockNode = { isNode: true };
const off = tracker.onChange(mockFn);
const mockTarget = { node: mockNode, detail: { isDetail: true }, instance: { isInstance: true } };
tracker.track(mockTarget);
expect(mockFn).toHaveBeenCalledWith(mockTarget);
expect(tracker.currentNode).toBe(mockNode);
expect(tracker.detail).toEqual({ isDetail: true });
expect(tracker.instance).toEqual({ isInstance: true });
off();
mockFn.mockClear();
tracker.track(mockNode);
expect(mockFn).not.toHaveBeenCalled();
});
================================================
FILE: packages/designer/tests/designer/builtin-hotkey.test.ts
================================================
import '../fixtures/window';
import {
Editor,
globalContext,
Hotkey as InnerHotkey,
} from '@alilc/lowcode-editor-core';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { fireEvent } from '@testing-library/react';
import { builtinHotkey } from '../../../engine/src/inner-plugins/builtin-hotkey';
import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory';
import { ILowCodePluginContextPrivate, LowCodePluginManager } from '@alilc/lowcode-designer';
import { IPublicApiPlugins } from '@alilc/lowcode-types';
import { Logger, Project, Canvas } from '@alilc/lowcode-shell';
import { Workspace } from '@alilc/lowcode-workspace';
const editor = new Editor();
const workspace = new Workspace();
let designer: Designer;
// keyCode 对应表:https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
// hotkey 模块底层用的 keyCode,所以还不能用 key / code 测试
describe('快捷键测试', () => {
let pluginManager: LowCodePluginManager;
let project: any = {};
beforeAll(() => {
return new Promise((resolve, reject) => {
const hotkey: any = new InnerHotkey();
const logger = new Logger({ level: 'warn', bizName: 'common' });
const contextApiAssembler = {
assembleApis(context: ILowCodePluginContextPrivate){
context.plugins = pluginManager as IPublicApiPlugins;
context.hotkey = hotkey;
context.logger = logger;
context.project = project;
context.canvas = new Canvas(editor);
}
};
pluginManager = new LowCodePluginManager(contextApiAssembler).toProxy();
pluginManager.register(builtinHotkey);
globalContext.register(editor, Editor);
globalContext.register(editor, 'editor');
globalContext.register(workspace, 'workspace');
pluginManager.init().then(() => {
resolve({});
});
})
});
afterAll(() => {
pluginManager.dispose();
});
beforeEach(() => {
designer = new Designer({ editor, shellModelFactory });
editor.set('designer', designer);
designer.project.open(formSchema);
project.__proto__ = new Project(designer.project);
});
afterEach(() => {
designer = null;
});
it('right', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbj')!;
firstCardNode.select();
fireEvent.keyDown(document, { keyCode: 39 });
expect(designer.currentSelection?.selected.includes('node_k1ow3cbl')).toBeTruthy();
});
it('left', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbl')!;
firstCardNode.select();
fireEvent.keyDown(document, { keyCode: 37 });
expect(designer.currentSelection?.selected.includes('node_k1ow3cbj')).toBeTruthy();
});
it('down', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbl')!;
firstCardNode.select();
fireEvent.keyDown(document, { keyCode: 40 });
expect(designer.currentSelection?.selected.includes('node_k1ow3cbo')).toBeTruthy();
});
it('up', () => {
const secondCardNode = designer.currentDocument?.getNode('node_k1ow3cbm')!;
secondCardNode.select();
fireEvent.keyDown(document, { keyCode: 38 });
expect(designer.currentSelection?.selected.includes('node_k1ow3cbl')).toBeTruthy();
});
// 跟右侧节点调换位置
it('option + right', () => {
const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!;
firstButtonNode.select();
fireEvent.keyDown(document, { keyCode: 39, altKey: true });
expect(firstButtonNode.prevSibling?.getId()).toBe('node_k1ow3cbp');
});
// 跟左侧节点调换位置
it('option + left', () => {
const secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
secondButtonNode.select();
fireEvent.keyDown(document, { keyCode: 37, altKey: true });
expect(secondButtonNode.nextSibling?.getId()).toBe('node_k1ow3cbn');
});
// 向父级移动该节点
it('option + up', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstCardNode.select();
fireEvent.keyDown(document, { keyCode: 38, altKey: true });
});
// 将节点移入到兄弟节点中
it('option + up', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstCardNode.select();
fireEvent.keyDown(document, { keyCode: 40, altKey: true });
});
// 撤销
it('command + z', async () => {
const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!;
let secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
// 等待第一个 session 结束
await new Promise(resolve => setTimeout(resolve, 1000));
firstButtonNode.remove();
expect(secondButtonNode.getParent()?.children.size).toBe(1);
await new Promise(resolve => setTimeout(resolve, 1000));
fireEvent.keyDown(document, { keyCode: 90, metaKey: true });
// 重新获取一次节点,因为 documentModel.import 是全画布刷新
secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
expect(secondButtonNode.getParent()?.children.size).toBe(2);
});
// 重做
it('command + y', async () => {
const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!;
let secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
// 等待第一个 session 结束
await new Promise(resolve => setTimeout(resolve, 1000));
firstButtonNode.remove();
expect(secondButtonNode.getParent()?.children.size).toBe(1);
await new Promise(resolve => setTimeout(resolve, 1000));
fireEvent.keyDown(document, { keyCode: 90, metaKey: true });
// 重新获取一次节点,因为 documentModel.import 是全画布刷新
secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
expect(secondButtonNode.getParent()?.children.size).toBe(2);
await new Promise(resolve => setTimeout(resolve, 1000));
fireEvent.keyDown(document, { keyCode: 89, metaKey: true });
// 重新获取一次节点,因为 documentModel.import 是全画布刷新
secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
expect(secondButtonNode.getParent()?.children.size).toBe(1);
});
it('command + c', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstCardNode.select();
fireEvent.keyDown(document, { keyCode: 67, metaKey: true });
});
it('command + v', async () => {
const secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
secondButtonNode.select();
fireEvent.keyDown(document, { keyCode: 67, metaKey: true });
fireEvent.keyDown(document, { keyCode: 86, metaKey: true });
await new Promise(resolve => setTimeout(resolve, 1000));
// clipboard 异步,先注释
// expect(secondButtonNode.getParent()?.children.size).toBe(3);
});
// 撤销所有选中
it('escape', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstCardNode.select();
expect(designer.currentSelection!.selected.includes('node_k1ow3cbp')).toBeTruthy();
fireEvent.keyDown(document, { keyCode: 27 });
expect(designer.currentSelection!.selected.length).toBe(0);
});
// 删除节点
it('delete', () => {
const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!;
const secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstButtonNode.select();
expect(secondButtonNode.prevSibling.id).toBe('node_k1ow3cbn');
fireEvent.keyDown(document, { keyCode: 46 });
expect(secondButtonNode.prevSibling).toBeNull();
});
describe('非正常分支', () => {
it('liveEditing mode', () => {
designer.project.mountSimulator({
liveEditing: {
editing: {},
},
});
editor.set('designer', designer);
designer.currentDocument?.selection.select('page');
// nothing happened
fireEvent.keyDown(document, { keyCode: 39 });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(document, { keyCode: 37 });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(document, { keyCode: 40 });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(document, { keyCode: 38 });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(document, { keyCode: 39, altKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(document, { keyCode: 37, altKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(document, { keyCode: 40, altKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(document, { keyCode: 38, altKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(document, { keyCode: 90, metaKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(document, { keyCode: 89, metaKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(document, { keyCode: 67, metaKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(document, { keyCode: 86, metaKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(document, { keyCode: 27 });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(document, { keyCode: 46 });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
});
it('isFormEvent: true', () => {
const inputDOMNode = document.createElement('INPUT');
document.body.appendChild(inputDOMNode);
designer.currentDocument?.selection.select('page');
// nothing happened
fireEvent.keyDown(inputDOMNode, { keyCode: 39 });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(inputDOMNode, { keyCode: 37 });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(inputDOMNode, { keyCode: 40 });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(inputDOMNode, { keyCode: 38 });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(inputDOMNode, { keyCode: 39, altKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(inputDOMNode, { keyCode: 37, altKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(inputDOMNode, { keyCode: 40, altKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(inputDOMNode, { keyCode: 38, altKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(inputDOMNode, { keyCode: 90, metaKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(inputDOMNode, { keyCode: 89, metaKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(inputDOMNode, { keyCode: 67, metaKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(inputDOMNode, { keyCode: 86, metaKey: true });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(inputDOMNode, { keyCode: 27 });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
fireEvent.keyDown(inputDOMNode, { keyCode: 46 });
expect(designer.currentDocument?.selection.selected[0]).toBe('page');
});
it('doc is null', () => {
designer.currentDocument?.selection.select('page');
designer.project.documents = [];
fireEvent.keyDown(document, { keyCode: 39 });
fireEvent.keyDown(document, { keyCode: 37 });
fireEvent.keyDown(document, { keyCode: 40 });
fireEvent.keyDown(document, { keyCode: 38 });
fireEvent.keyDown(document, { keyCode: 39, altKey: true });
fireEvent.keyDown(document, { keyCode: 37, altKey: true });
fireEvent.keyDown(document, { keyCode: 40, altKey: true });
fireEvent.keyDown(document, { keyCode: 38, altKey: true });
fireEvent.keyDown(document, { keyCode: 90, metaKey: true });
fireEvent.keyDown(document, { keyCode: 89, metaKey: true });
fireEvent.keyDown(document, { keyCode: 67, metaKey: true });
fireEvent.keyDown(document, { keyCode: 86, metaKey: true });
fireEvent.keyDown(document, { keyCode: 27 });
fireEvent.keyDown(document, { keyCode: 46 });
});
it('selected is []', () => {
fireEvent.keyDown(document, { keyCode: 39 });
fireEvent.keyDown(document, { keyCode: 37 });
fireEvent.keyDown(document, { keyCode: 40 });
fireEvent.keyDown(document, { keyCode: 38 });
fireEvent.keyDown(document, { keyCode: 39, altKey: true });
fireEvent.keyDown(document, { keyCode: 37, altKey: true });
fireEvent.keyDown(document, { keyCode: 40, altKey: true });
fireEvent.keyDown(document, { keyCode: 38, altKey: true });
fireEvent.keyDown(document, { keyCode: 90, metaKey: true });
fireEvent.keyDown(document, { keyCode: 89, metaKey: true });
fireEvent.keyDown(document, { keyCode: 67, metaKey: true });
fireEvent.keyDown(document, { keyCode: 86, metaKey: true });
fireEvent.keyDown(document, { keyCode: 27 });
fireEvent.keyDown(document, { keyCode: 46 });
});
});
});
================================================
FILE: packages/designer/tests/designer/designer.test.ts
================================================
import '../fixtures/window';
import { Editor, globalContext, Setters } from '@alilc/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { DocumentModel } from '../../src/document/document-model';
import { Designer } from '../../src/designer/designer';
import { Dragon } from '../../src/designer/dragon';
// import { TransformStage } from '../../src/document/node/transform-stage';
import formSchema from '../fixtures/schema/form';
import buttonMetadata from '../fixtures/component-metadata/button';
import pageMetadata from '../fixtures/component-metadata/page';
import divMetadata from '../fixtures/component-metadata/div';
import { delayObxTick } from '../utils';
import { fireEvent } from '@testing-library/react';
import { IPublicEnumDragObjectType, IPublicEnumTransformStage } from '@alilc/lowcode-types';
import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory';
const mockNode = {
internalToShellNode() {
return 'mockNode';
},
};
describe('Designer 测试', () => {
let editor: Editor;
let designer: Designer;
let project: Project;
let doc: DocumentModel;
let dragon: Dragon;
beforeAll(() => {
editor = new Editor();
const setters = new Setters();
editor.set('setters', setters);
!globalContext.has(Editor) && globalContext.register(editor, Editor);
});
beforeEach(() => {
designer = new Designer({ editor, shellModelFactory });
project = designer.project;
doc = project.createDocument(formSchema);
dragon = new Dragon(designer);
});
afterEach(() => {
project.unload();
project.mountSimulator(undefined);
designer.purge();
designer = null;
project = null;
dragon = null;
});
describe('onDragstart / onDrag / onDragend', () => {
it('IPublicEnumDragObjectType.Node', () => {
const dragStartMockFn = jest.fn();
const dragMockFn = jest.fn();
const dragEndMockFn = jest.fn();
const dragStartMockFn2 = jest.fn();
const dragMockFn2 = jest.fn();
const dragEndMockFn2 = jest.fn();
const designer = new Designer({
editor,
shellModelFactory,
onDragstart: dragStartMockFn,
onDrag: dragMockFn,
onDragend: dragEndMockFn,
});
editor.on('designer.dragstart', dragStartMockFn2);
editor.on('designer.drag', dragMockFn2);
editor.on('designer.dragend', dragEndMockFn2);
const { dragon } = designer;
dragon.boost(
{
type: IPublicEnumDragObjectType.Node,
nodes: [doc.getNode('node_k1ow3cbn')],
},
new MouseEvent('mousedown', { clientX: 100, clientY: 100 }),
);
fireEvent.mouseMove(document, { clientX: 108, clientY: 108 });
expect(dragStartMockFn).toHaveBeenCalledTimes(1);
expect(dragStartMockFn2).toHaveBeenCalledTimes(1);
expect(dragMockFn).toHaveBeenCalledTimes(1);
expect(dragMockFn2).toHaveBeenCalledTimes(1);
fireEvent.mouseMove(document, { clientX: 110, clientY: 110 });
expect(dragMockFn).toHaveBeenCalledTimes(2);
expect(dragMockFn2).toHaveBeenCalledTimes(2);
setMockDropLocation();
fireEvent.mouseUp(document, { clientX: 118, clientY: 118 });
expect(dragEndMockFn).toHaveBeenCalledTimes(1);
expect(dragEndMockFn2).toHaveBeenCalledTimes(1);
function setMockDropLocation() {
const mockTarget = {
document: doc,
children: {
get(x) {
return x;
},
insert() {},
internalInsert() {},
},
};
const mockDetail = { type: 'Children', index: 1, near: { node: { x: 1 } } };
const mockSource = {};
const mockEvent = { type: 'LocateEvent', nodes: [] };
return designer.createLocation({
target: mockTarget,
detail: mockDetail,
source: mockSource,
event: mockEvent,
});
}
});
it('IPublicEnumDragObjectType.NodeData', () => {
const dragStartMockFn = jest.fn();
const dragMockFn = jest.fn();
const dragEndMockFn = jest.fn();
const dragStartMockFn2 = jest.fn();
const dragMockFn2 = jest.fn();
const dragEndMockFn2 = jest.fn();
const designer = new Designer({
editor,
shellModelFactory,
onDragstart: dragStartMockFn,
onDrag: dragMockFn,
onDragend: dragEndMockFn,
});
editor.on('designer.dragstart', dragStartMockFn2);
editor.on('designer.drag', dragMockFn2);
editor.on('designer.dragend', dragEndMockFn2);
const { dragon } = designer;
dragon.boost(
{
type: IPublicEnumDragObjectType.NodeData,
data: [{
componentName: 'Button',
}],
},
new MouseEvent('mousedown', { clientX: 100, clientY: 100 }),
);
fireEvent.mouseMove(document, { clientX: 108, clientY: 108 });
expect(dragStartMockFn).toHaveBeenCalledTimes(1);
expect(dragStartMockFn2).toHaveBeenCalledTimes(1);
expect(dragMockFn).toHaveBeenCalledTimes(1);
expect(dragMockFn2).toHaveBeenCalledTimes(1);
fireEvent.mouseMove(document, { clientX: 110, clientY: 110 });
expect(dragMockFn).toHaveBeenCalledTimes(2);
expect(dragMockFn2).toHaveBeenCalledTimes(2);
setMockDropLocation();
fireEvent.mouseUp(document, { clientX: 118, clientY: 118 });
expect(dragEndMockFn).toHaveBeenCalledTimes(1);
expect(dragEndMockFn2).toHaveBeenCalledTimes(1);
function setMockDropLocation() {
const mockTarget = {
document: doc,
children: {
get(x) {
return x;
},
insert() {},
internalInsert() {},
},
};
const mockDetail = { type: 'Children', index: 1, near: { node: { x: 1 } } };
const mockSource = {};
const mockEvent = { type: 'LocateEvent', nodes: [] };
return designer.createLocation({
target: mockTarget,
detail: mockDetail,
source: mockSource,
event: mockEvent,
});
}
});
});
it('addPropsReducer / transformProps', () => {
// 没有相应的 reducer
expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Init)).toEqual({ num: 1 });
// props 是数组
expect(designer.transformProps([{ num: 1 }], mockNode, IPublicEnumTransformStage.Init)).toEqual([{ num: 1 }]);
designer.addPropsReducer((props, node) => {
props.num += 1;
return props;
}, IPublicEnumTransformStage.Init);
designer.addPropsReducer((props, node) => {
props.num += 1;
return props;
}, IPublicEnumTransformStage.Init);
designer.addPropsReducer((props, node) => {
props.num += 1;
return props;
}, IPublicEnumTransformStage.Clone);
designer.addPropsReducer((props, node) => {
props.num += 1;
return props;
}, IPublicEnumTransformStage.Serilize);
designer.addPropsReducer((props, node) => {
props.num += 1;
return props;
}, IPublicEnumTransformStage.Render);
designer.addPropsReducer((props, node) => {
props.num += 1;
return props;
}, IPublicEnumTransformStage.Save);
designer.addPropsReducer((props, node) => {
props.num += 1;
return props;
}, IPublicEnumTransformStage.Upgrade);
expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Init)).toEqual({ num: 3 });
expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Clone)).toEqual({ num: 2 });
expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Serilize)).toEqual({ num: 2 });
expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Render)).toEqual({ num: 2 });
expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Save)).toEqual({ num: 2 });
expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Upgrade)).toEqual({ num: 2 });
designer.addPropsReducer((props, node) => {
throw new Error('calculate error');
}, IPublicEnumTransformStage.Upgrade);
expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Upgrade)).toEqual({ num: 2 });
});
it('setProps', () => {
// 第一次设置 props
const initialProps = {
simulatorComponent: { isSimulatorComp: true },
simulatorProps: { designMode: 'design' },
suspensed: true,
componentMetadatas: [buttonMetadata, divMetadata],
};
designer = new Designer({
editor,
shellModelFactory,
...initialProps,
});
expect(designer.simulatorComponent).toEqual({ isSimulatorComp: true });
expect(designer.simulatorProps).toEqual({ designMode: 'design' });
expect(designer.suspensed).toBeTruthy();
expect((designer as any)._componentMetasMap.has('Div')).toBeTruthy();
expect((designer as any)._componentMetasMap.has('Button')).toBeTruthy();
const { editor: editorFromDesigner, shellModelFactory: shellModelFactoryFromDesigner, ...others } = (designer as any).props;
expect(others).toEqual(initialProps);
expect(designer.get('simulatorProps')).toEqual({ designMode: 'design' });
expect(designer.get('suspensed')).toBeTruthy();
expect(designer.get('xxx')).toBeUndefined();
// 第二次设置 props
const updatedProps = {
simulatorComponent: { isSimulatorComp2: true },
simulatorProps: { designMode: 'live' },
suspensed: false,
componentMetadatas: [buttonMetadata],
};
designer.setProps(updatedProps);
expect(designer.simulatorComponent).toEqual({ isSimulatorComp2: true });
expect(designer.simulatorProps).toEqual({ designMode: 'live' });
expect(designer.suspensed).toBeFalsy();
expect((designer as any)._componentMetasMap.has('Button')).toBeTruthy();
expect((designer as any)._componentMetasMap.has('Div')).toBeTruthy();
const { editor: editorFromDesigner2, shellModelFactory: shellModelFactoryFromDesigner2, ...others2 } = (designer as any).props;
expect(others2).toEqual(updatedProps);
// 第三次设置 props,跟第二次值一样,for 覆盖率测试
const updatedProps2 = updatedProps;
designer.setProps(updatedProps2);
expect(designer.simulatorComponent).toEqual({ isSimulatorComp2: true });
expect(designer.simulatorProps).toEqual({ designMode: 'live' });
expect(designer.suspensed).toBeFalsy();
expect((designer as any)._componentMetasMap.has('Button')).toBeTruthy();
expect((designer as any)._componentMetasMap.has('Div')).toBeTruthy();
const { editor: editorFromDesigner3, shellModelFactory: shellModelFactoryFromDesigner3, ...others3 } = (designer as any).props;
expect(others3).toEqual(updatedProps);
});
describe('getSuitableInsertion', () => {
it('没有 currentDocument', () => {
project.unload();
expect(designer.getSuitableInsertion({})).toBeNull();
});
it('有选中节点,isContainer && 允许放子节点', () => {
designer.createComponentMeta(divMetadata);
designer.createComponentMeta(buttonMetadata);
designer.currentSelection?.select('node_k1ow3cbo');
const { target, index } = designer.getSuitableInsertion(
doc.createNode({ componentName: 'Button' }),
);
expect(target).toBe(doc.getNode('node_k1ow3cbo'));
expect(index).toBeUndefined();
});
it('有选中节点,不是 isContainer', () => {
designer.createComponentMeta(divMetadata);
designer.createComponentMeta(buttonMetadata);
designer.currentSelection?.select('node_k1ow3cbn');
const { target, index } = designer.getSuitableInsertion(
doc.createNode({ componentName: 'Button' }),
);
expect(target).toBe(doc.getNode('node_k1ow3cbo'));
expect(index).toBe(1);
});
it('无选中节点', () => {
designer.createComponentMeta(pageMetadata);
const { target, index } = designer.getSuitableInsertion(
doc.createNode({ componentName: 'Button' }),
);
expect(target).toBe(doc.getNode('page'));
expect(index).toBeUndefined();
});
});
it('getComponentMetasMap', () => {
designer.createComponentMeta({
componentName: 'Div',
title: '容器',
docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
devMode: 'procode',
tags: ['布局'],
});
expect(designer.getComponentMetasMap().get('Div')).not.toBeUndefined();
});
it('refreshComponentMetasMap', () => {
designer.createComponentMeta({
componentName: 'Div',
title: '容器',
docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
devMode: 'procode',
tags: ['布局'],
});
const originalMetasMap = designer.getComponentMetasMap();
designer.refreshComponentMetasMap();
expect(originalMetasMap).not.toBe(designer.getComponentMetasMap());
});
describe('loadIncrementalAssets', () => {
it('components && packages', async () => {
editor.set('assets', { components: [], packages: [] });
const fn = jest.fn();
project.mountSimulator({
setupComponents: fn,
});
await designer.loadIncrementalAssets({
components: [{
componentName: 'Div2',
title: '容器',
docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
devMode: 'proCode',
tags: ['布局'],
}],
packages: [],
});
const comps = editor.get('assets').components;
expect(comps).toHaveLength(1);
expect(fn).toHaveBeenCalled();
});
it('no components && packages', async () => {
editor.set('assets', { components: [], packages: [] });
const fn = jest.fn();
project.mountSimulator({
setupComponents: fn,
});
await designer.loadIncrementalAssets({});
expect(fn).not.toHaveBeenCalled();
});
});
it('createLocation / clearLocation', () => {
const mockTarget = {
document: doc,
children: {
get(x) {
return x;
},
insert() {},
internalInsert() {},
},
};
const mockDetail = { type: 'Children', index: 1, near: { node: { x: 1 } } };
const mockSource = {};
const mockEvent = { type: 'LocateEvent', nodes: [] };
const loc = designer.createLocation({
target: mockTarget,
detail: mockDetail,
source: mockSource,
event: mockEvent,
});
expect(designer.dropLocation).toBe(loc);
const doc2 = project.createDocument({ componentName: 'Page' });
designer.createLocation({
target: {
document: doc2,
children: {
get(x) {
return x;
},
insert() {},
internalInsert() {},
},
},
detail: mockDetail,
source: mockSource,
event: mockEvent,
});
designer.clearLocation();
expect(designer.dropLocation).toBeUndefined();
});
it('autorun', async () => {
const mockFn = jest.fn();
designer.autorun(() => {
mockFn();
}, true);
await delayObxTick();
expect(mockFn).toHaveBeenCalled();
});
it('suspensed', () => {
designer.suspensed = true;
expect(designer.suspensed).toBeTruthy();
designer.suspensed = false;
expect(designer.suspensed).toBeFalsy();
});
it('schema', () => {
// TODO: matchSnapshot
designer.schema;
designer.setSchema({
componentsTree: [
{
componentName: 'Page',
props: {},
},
],
});
});
it('createOffsetObserver / clearOobxList / touchOffsetObserver', () => {
project.mountSimulator({
computeComponentInstanceRect() {},
});
designer.createOffsetObserver({ node: doc.getNode('page'), instance: {} });
expect(designer.oobxList).toHaveLength(1);
designer.createOffsetObserver({ node: doc.getNode('page'), instance: {} });
expect(designer.oobxList).toHaveLength(2);
designer.clearOobxList(true);
expect(designer.oobxList).toHaveLength(0);
const obx = designer.createOffsetObserver({ node: doc.getNode('page'), instance: {} });
obx.pid = 'xxx';
obx.compute = () => {};
expect(designer.oobxList).toHaveLength(1);
designer.touchOffsetObserver();
expect(designer.oobxList).toHaveLength(1);
});
});
================================================
FILE: packages/designer/tests/designer/detecting.test.ts
================================================
import { Detecting } from '../../src/designer/detecting';
it('Detecting 测试', () => {
const fn = jest.fn();
const detecting = new Detecting();
detecting.onDetectingChange(fn);
expect(detecting.enable).toBeTruthy();
const mockNode = { document };
detecting.capture(mockNode);
expect(fn).toHaveBeenCalledWith(detecting.current);
expect(detecting.current).toBe(mockNode);
detecting.release({});
detecting.release(mockNode);
expect(detecting.current).toBeNull();
detecting.capture(mockNode);
detecting.leave(document);
expect(detecting.current).toBeNull();
detecting.capture(mockNode);
detecting.enable = false;
expect(detecting.current).toBeNull();
});
================================================
FILE: packages/designer/tests/designer/dragon.test.ts
================================================
import '../fixtures/window';
import { Editor, globalContext } from '@alilc/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { DocumentModel } from '../../src/document/document-model';
import { Designer } from '../../src/designer/designer';
import {
Dragon,
isDragNodeObject,
isDragNodeDataObject,
isDragAnyObject,
isLocateEvent,
isShaken,
setShaken,
isInvalidPoint,
isSameAs,
} from '../../src/designer/dragon';
import { IPublicEnumDragObjectType } from '@alilc/lowcode-types';
import formSchema from '../fixtures/schema/form';
import { fireEvent } from '@testing-library/react';
import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory';
describe('Dragon 测试', () => {
let editor: Editor;
let designer: Designer;
let project: Project;
let doc: DocumentModel;
let dragon: Dragon;
beforeAll(() => {
editor = new Editor();
!globalContext.has(Editor) && globalContext.register(editor, Editor);
});
beforeEach(() => {
designer = new Designer({ editor, shellModelFactory });
project = designer.project;
doc = project.createDocument(formSchema);
dragon = new Dragon(designer);
});
afterEach(() => {
project.unload();
project.mountSimulator(undefined);
designer.purge();
designer = null;
project = null;
dragon = null;
});
it.skip('drag NodeData', () => {
const dragStartMockFn = jest.fn();
const dragMockFn = jest.fn();
const dragEndMockFn = jest.fn();
dragon.onDragstart((e) => {
console.log('start', e, e.originalEvent, e.originalEvent.clientX);
});
dragon.onDrag((e) => {
console.log('drag', e, e.originalEvent, e.originalEvent.clientX);
});
dragon.onDragend((e) => {
console.log('end', e, e.originalEvent);
});
dragon.boost(
{
type: IPublicEnumDragObjectType.NodeData,
data: [{ componentName: 'Button' }],
},
new Event('dragstart', { clientX: 100, clientY: 100 }),
);
fireEvent.dragOver(document, { clientX: 108, clientY: 108 });
fireEvent.dragEnd(document, { clientX: 118, clientY: 118 });
});
it.skip('drag Node', () => {
console.log(new MouseEvent('mousedown', { clientX: 1 }).clientX);
// console.log(new Event('mousedown', { clientX: 1 }).clientX);
// console.log(new Event('drag', { clientX: 1 }).clientX);
// console.log(new CustomEvent('drag', { clientX: 1 }).clientX);
console.log(document.createEvent('dragstart', { clientX: 1 }).clientX);
});
it('mouse NodeData', () => {
const dragStartMockFn = jest.fn();
const dragMockFn = jest.fn();
const dragEndMockFn = jest.fn();
const offDragStart = dragon.onDragstart(dragStartMockFn);
const offDrag = dragon.onDrag(dragMockFn);
const offDragEnd = dragon.onDragend(dragEndMockFn);
dragon.boost(
{
type: IPublicEnumDragObjectType.NodeData,
data: [{ componentName: 'Button' }],
},
new MouseEvent('mousedown', { clientX: 100, clientY: 100 }),
);
fireEvent.mouseMove(document, { clientX: 108, clientY: 108 });
fireEvent.mouseMove(document, { clientX: 110, clientY: 110 });
fireEvent.mouseUp(document, { clientX: 118, clientY: 118 });
expect(dragStartMockFn).toHaveBeenCalledTimes(1);
expect(dragMockFn).toHaveBeenCalledTimes(2);
expect(dragEndMockFn).toHaveBeenCalledTimes(1);
});
it('mouse Node', () => {
const dragStartMockFn = jest.fn();
const dragMockFn = jest.fn();
const dragEndMockFn = jest.fn();
const offDragStart = dragon.onDragstart(dragStartMockFn);
const offDrag = dragon.onDrag(dragMockFn);
const offDragEnd = dragon.onDragend(dragEndMockFn);
dragon.boost(
{
type: IPublicEnumDragObjectType.Node,
nodes: [doc.getNode('node_k1ow3cbn')],
},
new MouseEvent('mousedown', { clientX: 100, clientY: 100 }),
);
// mouseDown 模式正常不会触发 dragStart 事件,除非 shaken 型
expect(dragStartMockFn).not.toHaveBeenCalled();
fireEvent.mouseMove(document, { clientX: 108, clientY: 108 });
expect(dragStartMockFn).toHaveBeenCalledTimes(1);
expect(dragMockFn).toHaveBeenCalledTimes(1);
fireEvent.mouseMove(document, { clientX: 110, clientY: 110 });
expect(dragMockFn).toHaveBeenCalledTimes(2);
expect(dragon.dragging).toBeTruthy();
fireEvent.mouseUp(document, { clientX: 118, clientY: 118 });
expect(dragEndMockFn).toHaveBeenCalledTimes(1);
offDragStart();
offDrag();
offDragEnd();
dragMockFn.mockClear();
dragon.boost(
{
type: IPublicEnumDragObjectType.Node,
nodes: [doc.getNode('node_k1ow3cbn')],
},
new MouseEvent('mousedown', { clientX: 100, clientY: 100 }),
);
fireEvent.mouseMove(document, { clientX: 108, clientY: 108 });
expect(dragMockFn).not.toHaveBeenCalled();
});
it('mouse Node & esc', () => {
const dragStartMockFn = jest.fn();
const dragMockFn = jest.fn();
const dragEndMockFn = jest.fn();
const offDragStart = dragon.onDragstart(dragStartMockFn);
const offDrag = dragon.onDrag(dragMockFn);
const offDragEnd = dragon.onDragend(dragEndMockFn);
dragon.boost(
{
type: IPublicEnumDragObjectType.Node,
nodes: [doc.getNode('node_k1ow3cbn')],
},
new MouseEvent('mousedown', { clientX: 100, clientY: 100 }),
);
fireEvent.keyDown(document, { keyCode: 27 });
expect(dragon.designer.dropLocation).toBeUndefined();
});
it('mouse Node & copy', () => {
const dragStartMockFn = jest.fn();
const dragMockFn = jest.fn();
const dragEndMockFn = jest.fn();
const offDragStart = dragon.onDragstart(dragStartMockFn);
const offDrag = dragon.onDrag(dragMockFn);
const offDragEnd = dragon.onDragend(dragEndMockFn);
dragon.boost(
{
type: IPublicEnumDragObjectType.Node,
nodes: [doc.getNode('node_k1ow3cbn')],
},
new MouseEvent('mousedown', { clientX: 100, clientY: 100 }),
);
const mockFn1 = jest.fn();
project.mountSimulator({ setCopyState: mockFn1 });
expect(dragon.getSimulators().size).toBe(1);
fireEvent.keyDown(document, { ctrlKey: true });
expect(mockFn1).toHaveBeenCalled();
});
it('from', () => {
const dragStartMockFn = jest.fn();
const dragMockFn = jest.fn();
const dragEndMockFn = jest.fn();
const offDragStart = dragon.onDragstart(dragStartMockFn);
const offDrag = dragon.onDrag(dragMockFn);
const offDragEnd = dragon.onDragend(dragEndMockFn);
const mockBoostFn = jest
.fn((e) => {
return {
type: IPublicEnumDragObjectType.Node,
nodes: [doc.getNode('node_k1ow3cbn')],
};
})
.mockImplementationOnce(() => null);
const offFrom = dragon.from(document, mockBoostFn);
// 无用 mouseDown,无效的按钮
fireEvent.mouseDown(document, { button: 2 });
expect(dragStartMockFn).not.toHaveBeenCalled();
// 无用 mouseDown,无效的 dragObject
fireEvent.mouseDown(document, { clientX: 100, clientY: 100 });
expect(dragStartMockFn).not.toHaveBeenCalled();
fireEvent.mouseDown(document, { clientX: 100, clientY: 100 });
expect(dragStartMockFn).not.toHaveBeenCalled();
fireEvent.mouseMove(document, { clientX: 108, clientY: 108 });
expect(dragStartMockFn).toHaveBeenCalledTimes(1);
expect(dragMockFn).toHaveBeenCalledTimes(1);
fireEvent.mouseMove(document, { clientX: 110, clientY: 110 });
expect(dragMockFn).toHaveBeenCalledTimes(2);
expect(dragon.dragging).toBeTruthy();
fireEvent.mouseUp(document, { clientX: 118, clientY: 118 });
expect(dragEndMockFn).toHaveBeenCalledTimes(1);
offDragStart();
offDrag();
offDragEnd();
dragMockFn.mockClear();
fireEvent.mouseMove(document, { clientX: 100, clientY: 100 });
expect(dragMockFn).not.toHaveBeenCalled();
offFrom();
fireEvent.mouseMove(document, { clientX: 100, clientY: 100 });
expect(dragMockFn).not.toHaveBeenCalled();
});
it('addSensor / removeSensor', () => {
const sensor = {
locate: () => {},
sensorAvailable: true,
isEnter: () => true,
fixEvent: () => {},
deactiveSensor: () => {},
};
const sensor2 = {};
dragon.addSensor(sensor);
expect(dragon.sensors.length).toBe(1);
expect(dragon.activeSensor).toBeUndefined();
dragon.boost(
{
type: IPublicEnumDragObjectType.NodeData,
data: [{ componentName: 'Button' }],
},
new MouseEvent('mousedown', { clientX: 100, clientY: 100 }),
);
fireEvent.mouseMove(document, { clientX: 108, clientY: 108 });
fireEvent.mouseMove(document, { clientX: 110, clientY: 110 });
fireEvent.mouseUp(document, { clientX: 118, clientY: 118 });
expect(dragon.activeSensor).toBe(sensor);
// remove a non-existing sensor
dragon.removeSensor(sensor2);
expect(dragon.sensors.length).toBe(1);
dragon.removeSensor(sensor);
expect(dragon.sensors.length).toBe(0);
});
it('has sensor', () => {
const mockFn1 = jest.fn();
const mockDoc = document.createElement('iframe').contentWindow?.document;
dragon.addSensor({
fixEvent: () => {},
locate: () => {},
contentDocument: mockDoc,
});
project.mountSimulator({
setCopyState: mockFn1,
setNativeSelection: () => {},
clearState: () => {},
setDraggingState: () => {},
});
const mockBoostFn = jest
.fn((e) => {
return {
type: IPublicEnumDragObjectType.Node,
nodes: [doc.getNode('node_k1ow3cbn')],
};
})
.mockImplementationOnce(() => null);
const offFrom = dragon.from(document, mockBoostFn);
// TODO: 想办法 mock 一个 iframe.currentDocument
fireEvent.mouseDown(document, { clientX: 100, clientY: 100 });
});
});
describe('导出的其他函数', () => {
it('isDragNodeObject', () => {
expect(isDragNodeObject({ type: IPublicEnumDragObjectType.Node, nodes: [] })).toBeTruthy();
});
it('isDragNodeDataObject', () => {
expect(isDragNodeDataObject({ type: IPublicEnumDragObjectType.NodeData, data: [] })).toBeTruthy();
});
it('isDragAnyObject', () => {
expect(isDragAnyObject()).toBeFalsy();
expect(isDragAnyObject({ type: IPublicEnumDragObjectType.Node, nodes: [] })).toBeFalsy();
expect(isDragAnyObject({ type: IPublicEnumDragObjectType.NodeData, data: [] })).toBeFalsy();
expect(isDragAnyObject({ type: 'others', data: [] })).toBeTruthy();
});
it('isLocateEvent', () => {
expect(isLocateEvent({ type: 'LocateEvent' })).toBeTruthy();
});
it('isShaken', () => {
expect(
isShaken(
{ clientX: 1, clientY: 1, target: {} },
{ clientX: 1, clientY: 1, target: { other: 1 } },
),
).toBeTruthy();
expect(isShaken({ shaken: true })).toBeTruthy();
expect(isShaken({ clientX: 1, clientY: 1 }, { clientX: 2, clientY: 2 })).toBeFalsy();
expect(isShaken({ clientX: 1, clientY: 1 }, { clientX: 3, clientY: 5 })).toBeTruthy();
});
it('setShaken', () => {
const e = {};
setShaken(e);
expect(isShaken(e)).toBeTruthy();
});
it('isInvalidPoint', () => {
expect(isInvalidPoint({ clientX: 0, clientY: 0 }, { clientX: 6, clientY: 1 })).toBeTruthy();
expect(isInvalidPoint({ clientX: 0, clientY: 0 }, { clientX: 1, clientY: 6 })).toBeTruthy();
expect(isInvalidPoint({ clientX: 0, clientY: 0 }, { clientX: 6, clientY: 6 })).toBeTruthy();
expect(isInvalidPoint({ clientX: 1, clientY: 1 }, { clientX: 2, clientY: 1 })).toBeFalsy();
});
it('isSameAs', () => {
expect(isSameAs({ clientX: 1, clientY: 1 }, { clientX: 1, clientY: 1 })).toBeTruthy();
expect(isSameAs({ clientX: 1, clientY: 1 }, { clientX: 2, clientY: 1 })).toBeFalsy();
});
});
================================================
FILE: packages/designer/tests/designer/location.test.ts
================================================
import {
DropLocation,
isLocationData,
isLocationChildrenDetail,
isRowContainer,
isChildInline,
getRectTarget,
isVerticalContainer,
isVertical,
getWindow,
} from '../../src/designer/location';
import { getMockElement } from '../utils';
describe('DropLocation 测试', () => {
it('constructor', () => {
const mockTarget = { document };
const mockDetail = {};
const mockSource = {};
const mockEvent = { type: 'LocateEvent', nodes: [] };
const loc = new DropLocation({
target: mockTarget,
detail: mockDetail,
source: mockSource,
event: mockEvent,
});
expect(loc.getContainer()).toBe(mockTarget);
expect(loc.document).toBe(document);
expect(loc.target).toBe(mockTarget);
expect(loc.detail).toBe(mockDetail);
expect(loc.source).toBe(mockSource);
expect(loc.event).toBe(mockEvent);
const mockEvent2 = { type: 'LocateEvent', data: [] };
const loc2 = loc.clone(mockEvent2);
expect(loc2.target).toBe(mockTarget);
expect(loc2.detail).toBe(mockDetail);
expect(loc2.source).toBe(mockSource);
expect(loc2.event).toBe(mockEvent2);
});
it('constructor, detail: undefined', () => {
const mockTarget = { document };
const mockDetail = undefined;
const mockSource = {};
const mockEvent = { type: 'LocateEvent', nodes: [] };
const loc = new DropLocation({
target: mockTarget,
detail: mockDetail,
source: mockSource,
event: mockEvent,
});
expect(loc.getInsertion()).toBeNull();
});
it('constructor, detail.type: Children, detail.index <= 0', () => {
const mockTarget = { document };
const mockDetail = { type: 'Children', index: -1 };
const mockSource = {};
const mockEvent = { type: 'LocateEvent', nodes: [] };
const loc = new DropLocation({
target: mockTarget,
detail: mockDetail,
source: mockSource,
event: mockEvent,
});
expect(loc.getInsertion()).toBeNull();
});
it('constructor, detail.type: Children, detail.index > 0', () => {
const mockTarget = {
document,
children: {
get(x) {
return x;
},
},
};
const mockDetail = { type: 'Children', index: 1 };
const mockSource = {};
const mockEvent = { type: 'LocateEvent', nodes: [] };
const loc = new DropLocation({
target: mockTarget,
detail: mockDetail,
source: mockSource,
event: mockEvent,
});
expect(loc.getInsertion()).toBe(0);
});
it('constructor, detail.type: Prop', () => {
const mockTarget = {
document,
children: {
get(x) {
return x;
},
},
};
const mockDetail = { type: 'Prop', index: 1, near: { node: { x: 1 } } };
const mockSource = {};
const mockEvent = { type: 'LocateEvent', nodes: [] };
const loc = new DropLocation({
target: mockTarget,
detail: mockDetail,
source: mockSource,
event: mockEvent,
});
expect(loc.getInsertion()).toEqual({ x: 1 });
});
});
it('isLocationData', () => {
expect(isLocationData({ target: {}, detail: {} })).toBeTruthy();
});
it('isLocationChildrenDetail', () => {
expect(isLocationChildrenDetail({ type: 'Children' })).toBeTruthy();
});
it('isRowContainer', () => {
expect(isRowContainer({ nodeType: Node.TEXT_NODE })).toBeTruthy();
window.getComputedStyle = jest
.fn(() => {
return {
getPropertyValue: (pName) => {
return pName === 'display' ? 'flex' : '';
},
};
})
.mockImplementationOnce(() => {
return {
getPropertyValue: (pName) => {
return pName === 'display' ? 'flex' : 'column';
},
};
})
.mockImplementationOnce(() => {
return {
getPropertyValue: (pName) => {
return pName === 'display' ? 'grid' : 'column';
},
};
});
expect(isRowContainer(getMockElement('div'))).toBeFalsy();
expect(isRowContainer(getMockElement('div'))).toBeTruthy();
expect(isRowContainer(getMockElement('div'))).toBeTruthy();
});
it('isChildInline', () => {
window.getComputedStyle = jest
.fn(() => {
return {
getPropertyValue: (pName) => {
return pName === 'display' ? 'inline' : 'float';
},
};
});
expect(isChildInline({ nodeType: Node.TEXT_NODE })).toBeTruthy();
expect(isChildInline(getMockElement('div'))).toBeTruthy();
});
it('getRectTarget', () => {
expect(getRectTarget()).toBeNull();
expect(getRectTarget({ computed: false })).toBeNull();
expect(getRectTarget({ elements: [{}] })).toEqual({});
});
it('isVerticalContainer', () => {
window.getComputedStyle = jest
.fn(() => {
return {
getPropertyValue: (pName) => {
return pName === 'display' ? 'flex' : 'row';
},
};
});
expect(isVerticalContainer()).toBeFalsy();
expect(isVerticalContainer({ elements: [getMockElement('div')] })).toBeTruthy();
});
it('isVertical', () => {
expect(isVertical({ elements: [] })).toBeFalsy();
expect(isVertical({ elements: [getMockElement('div')] })).toBeFalsy();
const e1 = getMockElement('div');
const e2 = getMockElement('div');
e2.appendChild(e1);
expect(isVertical({ elements: [e1] })).toBeTruthy();
window.getComputedStyle = jest
.fn(() => {
return {
getPropertyValue: (pName) => {
return pName === 'display' ? 'inline' : 'float';
},
};
});
expect(isVertical({ elements: [getMockElement('div')] })).toBeTruthy();
});
it('getWindow', () => {
const mockElem = getMockElement('div');
expect(getWindow(mockElem)).toBe(window);
expect(getWindow(document)).toBe(window);
});
================================================
FILE: packages/designer/tests/designer/scroller.test.ts
================================================
import '../fixtures/window';
import { Editor, globalContext } from '@alilc/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { DocumentModel } from '../../src/document/document-model';
import { ScrollTarget, Scroller } from '../../src/designer/scroller';
import { Designer } from '../../src/designer/designer';
import {
Dragon,
} from '../../src/designer/dragon';
import formSchema from '../fixtures/schema/form';
import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory';
describe('Scroller 测试', () => {
let editor: Editor;
let designer: Designer;
let project: Project;
let doc: DocumentModel;
let dragon: Dragon;
beforeAll(() => {
editor = new Editor();
!globalContext.has(Editor) && globalContext.register(editor, Editor);
});
beforeEach(() => {
designer = new Designer({ editor, shellModelFactory });
project = designer.project;
doc = project.createDocument(formSchema);
dragon = new Dragon(designer);
});
afterEach(() => {
project.unload();
project.mountSimulator(undefined);
designer.purge();
designer = null;
project = null;
dragon = null;
});
function getMockWindow() {
let scrollX = 0;
let scrollY = 0;
const mockWindow = {
scrollTo(x, y) {
if (typeof x === 'number') {
scrollX = x;
scrollY = y;
} else {
scrollX = x.left;
scrollY = x.top;
}
},
get scrollX() { return scrollX; },
get scrollY() { return scrollY; },
scrollHeight: 1000,
scrollWidth: 500,
document: {},
nodeType: Node.ELEMENT_NODE,
};
return mockWindow;
}
describe('ScrollTarget 测试', () => {
it('constructor', () => {
const win = getMockWindow();
const target = new ScrollTarget(win);
expect(target.scrollWidth).toBe(500);
expect(target.scrollHeight).toBe(1000);
target.scrollToXY(50, 50);
expect(target.left).toBe(50);
expect(target.top).toBe(50);
target.scrollTo({ left: 100, top: 100 });
expect(target.left).toBe(100);
expect(target.top).toBe(100);
});
});
function mockRAF() {
let rafCount = 0;
window.requestAnimationFrame = (fn) => {
if (rafCount++ < 2) {
fn();
} else {
window.requestAnimationFrame = () => {};
}
};
}
describe('Scroller 测试', () => {
it('scrollTarget: ScrollTarget', () => {
const win = getMockWindow();
const scrollTarget = new ScrollTarget(win);
const scroller = new Scroller({ scrollTarget, bounds: { width: 50, height: 50, top: 50, bottom: 50, left: 50, right: 50 } });
mockRAF();
scroller.scrollTo({ left: 50, top: 50 });
mockRAF();
scroller.scrolling({ globalX: 100, globalY: 100 });
});
it('scrollTarget: ScrollTarget, same left / top', () => {
const win = getMockWindow();
const scrollTarget = new ScrollTarget(win);
const scroller = new Scroller({ scrollTarget, bounds: { width: 50, height: 50, top: 50, bottom: 50, left: 50, right: 50 } });
mockRAF();
scrollTarget.scrollTo({ left: 50, top: 50 });
scroller.scrollTo({ left: 50, top: 50 });
mockRAF();
scroller.scrolling({ globalX: 100, globalY: 100 });
});
it('scrollTarget: Element', () => {
const win = getMockWindow();
// const scrollTarget = new ScrollTarget(win);
const scroller = new Scroller({ scrollTarget: win, bounds: { width: 50, height: 50, top: 50, bottom: 50, left: 50, right: 50 } });
mockRAF();
scroller.scrollTo({ left: 50, top: 50 });
mockRAF();
scroller.scrolling({ globalX: 100, globalY: 100 });
});
it('scrollTarget: null', () => {
const win = getMockWindow();
// const scrollTarget = new ScrollTarget(win);
const scroller = new Scroller({ scrollTarget: null, bounds: { width: 50, height: 50, top: 50, bottom: 50, left: 50, right: 50 } });
mockRAF();
scroller.scrollTo({ left: 50, top: 50 });
mockRAF();
scroller.scrolling({ globalX: 100, globalY: 100 });
});
});
});
================================================
FILE: packages/designer/tests/designer/setting/setting-field.test.ts
================================================
// @ts-nocheck
import '../../fixtures/window';
import {
Editor,
Setters as InnerSetters,
} from '@alilc/lowcode-editor-core';
import {
Setters,
} from '@alilc/lowcode-shell';
import { SettingTopEntry } from '../../../src/designer/setting/setting-top-entry';
import { SettingField } from '../../../src/designer/setting/setting-field';
import { Node } from '../../../src/document/node/node';
import { Designer } from '../../../src/designer/designer';
import settingSchema from '../../fixtures/schema/setting';
import buttonMeta from '../../fixtures/component-metadata/button';
import { DocumentModel } from 'designer/src/document';
import { delayObxTick } from '../../utils';
import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory';
const editor = new Editor();
describe('setting-field 测试', () => {
let designer: Designer;
let doc: DocumentModel;
let setters: Setters;
beforeEach(() => {
setters = new InnerSetters();
editor.set('setters', setters);
designer = new Designer({ editor, shellModelFactory });
designer.createComponentMeta(buttonMeta);
doc = designer.project.open(settingSchema);
});
afterEach(() => {
designer._componentMetasMap.clear();
designer = null;
doc.purge();
doc = null;
});
describe('纯粹的 UnitTest', () => {
let mockNode: Node;
let mockTopEntry: SettingTopEntry;
beforeEach(() => {
mockNode = new Node(designer.currentDocument, {
componentName: 'Button',
props: {
// a: 'str',
// b: 222,
// obj: {
// x: 1,
// },
// jse: {
// type: 'JSExpression',
// value: 'state.a',
// mock: 111,
// }
},
});
// mockTopEntry = new SettingTopEntry(editor, [mockNode]);
});
afterEach(() => {
mockNode = null;
mockTopEntry = null;
});
it('常规方法', () => {
// 普通 field
const settingEntry = mockNode.settingEntry;
const field = settingEntry.get('behavior');
expect(field.title).toBe('默认状态');
expect(field.expanded).toBeTruthy();
field.setExpanded(false);
expect(field.expanded).toBeFalsy();
expect(field.config).toMatchSnapshot();
expect(field.getConfig()).toMatchSnapshot();
expect(field.getConfig('extraProps')).toEqual({
display: 'inline',
defaultValue: 'NORMAL',
});
expect(field.items).toHaveLength(0);
expect(field.getItems()).toHaveLength(0);
expect(field.getItems(x => x)).toHaveLength(0);
expect(field.setter.componentName).toBe('MixedSetter');
field.purge();
expect(field.items).toHaveLength(0);
const subField = field.createField({
name: 'sub',
title: 'sub',
});
subField.setValue({
type: 'JSExpression',
value: 'state.a',
mock: 'haha',
});
subField.setHotValue('heihei');
expect(subField.getHotValue('heihei'));
expect(subField.getValue().mock).toBe('heihei');
// 不存在的 field
const nonExistingField = mockNode.settingEntry.get('non-exsiting');
expect(nonExistingField.setter).toBeNull();
// group 类型的 field
const groupField = settingEntry.get('groupkgzzeo41');
expect(groupField.items).toEqual([]);
// 有子节点的 field
const objField = settingEntry.get('obj');
expect(objField.items).toHaveLength(3);
expect(objField.getItems()).toHaveLength(3);
expect(objField.getItems(x => x.name === 'a')).toHaveLength(1);
objField.purge();
expect(objField.items).toHaveLength(0);
const objAField = settingEntry.get('obj.a');
expect(objAField.setter).toBe('StringSetter');
});
it('setValue / getValue / setHotValue / getHotValue', () => {
// 获取已有的 prop
const settingEntry = mockNode.settingEntry as SettingTopEntry;
const field = settingEntry.get('behavior');
// 会读取 extraProps.defaultValue
expect(field.getHotValue()).toBe('NORMAL');
field.setValue('HIDDEN');
expect(field.getValue()).toBe('HIDDEN');
expect(field.getHotValue()).toBe('HIDDEN');
field.setHotValue('DISABLED');
expect(field.getHotValue()).toBe('DISABLED');
field.setHotValue('NORMAL', { fromSetHotValue: true });
expect(field.getHotValue()).toBe('NORMAL');
field.setValue('HIDDEN', true);
expect(field.getHotValue()).toBe('HIDDEN');
// dirty fix list setter
field.setHotValue([{ __sid__: 1 }]);
// 数组的 field
const arrField = settingEntry.get('arr');
const subArrField = arrField.createField({
name: 0,
title: 'sub',
});
const subArrField02 = arrField.createField({
name: 1,
title: 'sub',
});
const subArrField03 = arrField.createField({
name: '2',
title: 'sub',
});
subArrField.setValue({name: '1'});
expect(subArrField.path).toEqual(['arr', 0]);
expect(subArrField02.path).toEqual(['arr', 1]);
subArrField02.setValue({name: '2'});
expect(subArrField.getValue()).toEqual({name: '1'});
expect(arrField.getHotValue()).toEqual([{name: '1'}, {name: '2'}]);
subArrField.clearValue();
expect(subArrField.getValue()).toBeUndefined();
expect(arrField.getHotValue()).toEqual([undefined, {name: '2'}]);
subArrField03.setValue({name: '3'});
expect(arrField.getHotValue()).toEqual([undefined, {name: '2'}, {name: '3'}]);
});
it('js expression setValue / setHotValue', () => {
const settingEntry = mockNode.settingEntry;
const field = settingEntry.get('behavior');
const subField = field.createField({
name: 'sub',
title: 'sub',
});
subField.setValue({
type: 'JSExpression',
value: 'state.a',
mock: 'haha',
});
subField.setHotValue({
type: 'JSExpression',
value: 'state.b',
});
expect(subField.getValue()).toEqual({
type: 'JSExpression',
value: 'state.b',
mock: 'haha',
});
subField.setHotValue('mock02');
expect(subField.getValue()).toEqual({
type: 'JSExpression',
value: 'state.b',
mock: 'mock02',
});
});
it('onEffect', async () => {
const settingEntry = mockNode.settingEntry as SettingTopEntry;
const field = settingEntry.get('behavior');
const mockFn = jest.fn();
field.onEffect(mockFn);
field.setValue('DISABLED');
await delayObxTick();
expect(mockFn).toHaveBeenCalled();
});
it('autorun', async () => {
const settingEntry = mockNode.settingEntry as SettingTopEntry;
const arrField = settingEntry.get('columns');
const subArrField = arrField.createField({
name: 0,
title: 'sub',
});
const objSubField = subArrField.createField({
name: 'objSub',
title: 'objSub',
});
const mockFnArrField = jest.fn();
const mockFnSubArrField = jest.fn();
const mockFnObjSubField = jest.fn();
arrField.setValue([{ objSub: "subMock0.Index.0" }]);
// 这里需要 setValue 两遍,触发 prop 的 purge 方法,使 purged 为 true,之后的 purge 方法不会正常执行,prop 才能正常缓存,autorun 才能正常执行
// TODO: 该机制后续得研究一下,再确定是否要修改
arrField.setValue([{ objSub: "subMock0.Index.0" }]);
arrField.onEffect(() => {
mockFnArrField(arrField.getValue());
});
arrField.onEffect(() => {
mockFnSubArrField(subArrField.getValue());
});
arrField.onEffect(() => {
mockFnObjSubField(objSubField.getValue());
});
await delayObxTick();
expect(mockFnObjSubField).toHaveBeenCalledWith('subMock0.Index.0');
expect(mockFnSubArrField).toHaveBeenCalledWith({ objSub: "subMock0.Index.0" });
expect(mockFnArrField).toHaveBeenCalledWith([{ objSub: "subMock0.Index.0" }]);
arrField.setValue([{ objSub: "subMock0.Index.1" }]);
await delayObxTick();
expect(mockFnObjSubField).toHaveBeenCalledWith('subMock0.Index.1');
expect(mockFnSubArrField).toHaveBeenCalledWith({ objSub: "subMock0.Index.1" });
expect(mockFnArrField).toHaveBeenCalledWith([{ objSub: "subMock0.Index.1" }]);
subArrField.setValue({ objSub: "subMock0.Index.2" });
await delayObxTick();
expect(mockFnObjSubField).toHaveBeenCalledWith('subMock0.Index.2');
expect(mockFnSubArrField).toHaveBeenCalledWith({ objSub: "subMock0.Index.2" });
expect(mockFnArrField).toHaveBeenCalledWith([{ objSub: "subMock0.Index.2" }]);
objSubField.setValue('subMock0.Index.3');
await delayObxTick();
expect(mockFnObjSubField).toHaveBeenCalledWith('subMock0.Index.3');
expect(mockFnSubArrField).toHaveBeenCalledWith({ objSub: "subMock0.Index.3" });
expect(mockFnArrField).toHaveBeenCalledWith([{ objSub: "subMock0.Index.3" }]);
})
});
});
================================================
FILE: packages/designer/tests/designer/setting/setting-prop-entry.test.ts
================================================
import '../../fixtures/window';
import {
Editor,
Setters as InnerSetters,
} from '@alilc/lowcode-editor-core';
import { SettingTopEntry } from '../../../src/designer/setting/setting-top-entry';
import { SettingPropEntry } from '../../../src/designer/setting/setting-prop-entry';
import { Node } from '../../../src/document/node/node';
import { Designer } from '../../../src/designer/designer';
import settingSchema from '../../fixtures/schema/setting';
import divMeta from '../../fixtures/component-metadata/div';
import { DocumentModel } from 'designer/src/document';
import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory';
const editor = new Editor();
describe('setting-prop-entry 测试', () => {
let designer: Designer;
let doc: DocumentModel;
let setters: any;
beforeEach(() => {
setters = new InnerSetters();
editor.set('setters', setters);
designer = new Designer({ editor, shellModelFactory });
designer.createComponentMeta(divMeta);
doc = designer.project.open(settingSchema);
});
afterEach(() => {
designer._componentMetasMap.clear();
designer = null;
doc.purge();
doc = null;
});
describe('纯粹的 UnitTest', () => {
let mockNode: Node;
let mockTopEntry: SettingTopEntry;
beforeEach(() => {
mockNode = new Node(designer.currentDocument, {
componentName: 'Button',
props: {
a: 'str',
b: 222,
obj: {
x: 1,
},
jse: {
type: 'JSExpression',
value: 'state.a',
mock: 111,
}
},
});
mockTopEntry = new SettingTopEntry(editor, [mockNode]);
});
afterEach(() => {
mockNode = null;
mockTopEntry = null;
});
it('常规方法', () => {
// type: group 类型
const prop = new SettingPropEntry(mockTopEntry, 'xGroup', 'group');
expect(prop.setKey('xxx')).toBeUndefined();
expect(prop.remove()).toBeUndefined();
const prop2 = new SettingPropEntry(mockTopEntry, '#xGroup');
expect(prop2.setKey('xxx')).toBeUndefined();
expect(prop2.remove()).toBeUndefined();
expect(prop.getVariableValue()).toBe('');
});
it('setValue / getValue / onValueChange', () => {
// 获取已有的 prop
const prop1 = mockTopEntry.getProp('a');
prop1.extraProps = {
getValue: (prop, val) => `prefix ${val}`,
// prop 是 shell prop entry
setValue: (prop, val) => { prop.setValue(`modified ${val}`, { disableMutator: true }) },
defaultValue: 'default',
};
expect(prop1.getDefaultValue()).toBe('default');
expect(prop1.getValue()).toBe('prefix str');
// disableMutator: true
prop1.setValue('bbb', false, false, { disableMutator: true });
expect(prop1.getValue()).toBe('prefix bbb');
// disableMutator: false
prop1.setValue('bbb');
expect(prop1.getValue()).toBe('prefix modified bbb');
const mockFn3 = jest.fn();
const prop2 = mockTopEntry.getProp('obj');
const prop3 = prop2.get('x');
const offFn = prop3.onValueChange(mockFn3);
expect(prop3.getValue()).toBe(1);
prop3.setValue(2);
expect(mockFn3).toHaveBeenCalled();
offFn();
prop3.setValue(3);
mockFn3.mockClear();
expect(mockFn3).toHaveBeenCalledTimes(0);
const prop4 = mockTopEntry.getProp('b');
prop4.extraProps = {
getValue: () => { throw 'error'; },
};
expect(prop4.getValue()).toBe(222);
});
it('clearValue', () => {
const prop1 = mockTopEntry.getProp('a');
prop1.clearValue();
expect(prop1.getValue()).toBeUndefined();
const mockFn = jest.fn();
prop1.extraProps = {
setValue: mockFn,
};
prop1.clearValue();
expect(mockFn).toHaveBeenCalled();
});
it('getVariableValue/ setUseVariable / isUseVariable / getMockOrValue', () => {
const prop1 = mockTopEntry.getProp('jse');
expect(prop1.isUseVariable()).toBeTruthy();
expect(prop1.useVariable).toBeTruthy();
expect(prop1.getMockOrValue()).toEqual(111);
expect(prop1.getVariableValue()).toEqual('state.a');
prop1.setUseVariable(false);
expect(prop1.getValue()).toEqual(111);
prop1.setUseVariable(true);
expect(prop1.getValue()).toEqual({
type: 'JSExpression',
value: '',
mock: 111,
});
prop1.setUseVariable(true);
});
});
describe('node 构造函数生成 settingEntry', () => {
it('常规方法测试', () => {
const divNode = doc?.getNode('div');
const { settingEntry } = divNode!;
const behaviorProp = settingEntry.getProp('behavior');
expect(behaviorProp.getProps()).toBe(settingEntry);
expect(behaviorProp.props).toBe(settingEntry);
expect(behaviorProp.getName()).toBe('behavior');
expect(behaviorProp.getKey()).toBe('behavior');
expect(behaviorProp.isIgnore()).toBeFalsy();
behaviorProp.setKey('behavior2');
expect(behaviorProp.getKey()).toBe('behavior2');
behaviorProp.setKey('behavior');
expect(behaviorProp.getNode()).toBe(divNode);
expect(behaviorProp.getId().startsWith('entry')).toBeTruthy();
expect(behaviorProp.designer).toBe(designer);
expect(behaviorProp.isSingle).toBeTruthy();
expect(behaviorProp.isMultiple).toBeFalsy();
expect(behaviorProp.isGroup).toBeFalsy();
expect(behaviorProp.isSameComponent).toBeTruthy();
expect(typeof settingEntry.getValue).toBe('function');
settingEntry.getValue();
behaviorProp.setExtraPropValue('extraPropA', 'heihei');
expect(behaviorProp.getExtraPropValue('extraPropA', 'heihei'));
});
it('setValue / getValue', () => {
const divNode = doc?.getNode('div');
const { settingEntry } = divNode!;
const behaviorProp = settingEntry.getProp('behavior');
expect(behaviorProp.getValue()).toBe('NORMAL');
expect(behaviorProp.getMockOrValue()).toBe('NORMAL');
behaviorProp.setValue('LARGE');
expect(behaviorProp.getValue()).toBe('LARGE');
// behaviorProp.setPropValue('behavior', 'SMALL');
// expect(behaviorProp.getValue()).toBe('SMALL');
behaviorProp.setValue('NORMAL');
expect(behaviorProp.getValue()).toBe('NORMAL');
behaviorProp.clearValue();
behaviorProp.clearPropValue();
expect(behaviorProp.getValue()).toBeUndefined();
behaviorProp.setValue('LARGE');
expect(behaviorProp.getValue()).toBe('LARGE');
behaviorProp.remove();
expect(divNode?.getProp('behavior').getValue()).toBeUndefined();
});
it.skip('type: group 场景测试', () => {});
it('JSExpression 类型的 prop', () => {
designer.createComponentMeta(divMeta);
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div');
const { settingEntry } = divNode!;
const customClassNameProp = settingEntry.getProp('customClassName');
expect(customClassNameProp.isUseVariable()).toBeTruthy();
expect(customClassNameProp.useVariable).toBeTruthy();
expect(customClassNameProp.getValue()).toEqual({
type: 'JSExpression',
value: 'getFromSomewhere()',
});
expect(customClassNameProp.getMockOrValue()).toBeUndefined();
expect(customClassNameProp.getVariableValue()).toBe('getFromSomewhere()');
customClassNameProp.setVariableValue('xxx');
expect(customClassNameProp.getVariableValue()).toBe('xxx');
const customClassName2Prop = settingEntry.getProp('customClassName2');
expect(customClassName2Prop.getMockOrValue()).toEqual({
hi: 'mock',
});
});
});
});
================================================
FILE: packages/designer/tests/designer/setting/setting-top-entry.test.ts
================================================
import '../../fixtures/window';
import { Editor, Setters } from '@alilc/lowcode-editor-core';
import { Node } from '../../../src/document/node/node';
import { Designer } from '../../../src/designer/designer';
import settingSchema from '../../fixtures/schema/setting';
import divMeta from '../../fixtures/component-metadata/div';
import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory';
const editor = new Editor();
describe('setting-top-entry 测试', () => {
let designer: Designer;
beforeEach(() => {
editor.set('setters', new Setters())
designer = new Designer({ editor, shellModelFactory });
});
afterEach(() => {
designer._componentMetasMap.clear();
designer = null;
});
describe('node 构造函数生成 settingEntry', () => {
it('常规方法测试', () => {
designer.createComponentMeta(divMeta);
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div');
const { settingEntry } = divNode!;
expect(settingEntry.getPropValue('behavior')).toBe('NORMAL');
expect(settingEntry.getProp('behavior').getValue()).toBe('NORMAL');
settingEntry.setPropValue('behavior', 'LARGE');
expect(settingEntry.getPropValue('behavior')).toBe('LARGE');
expect(settingEntry.get('behavior').getValue()).toBe('LARGE');
settingEntry.getProp('behavior').setValue('SMALL');
expect(settingEntry.getPropValue('behavior')).toBe('SMALL');
settingEntry.clearPropValue('behavior');
expect(settingEntry.getPropValue('behavior')).toBeUndefined();
expect(settingEntry.getPropValue('fieldId')).toBe('div_k1ow3h1o');
settingEntry.setPropValue('fieldId', 'div_k1ow3h1o_new');
expect(settingEntry.getPropValue('fieldId')).toBe('div_k1ow3h1o_new');
expect(settingEntry.getExtraPropValue('extraPropA')).toBe('haha');
settingEntry.setExtraPropValue('extraPropA', 'haha2');
expect(settingEntry.getExtraPropValue('extraPropA')).toBe('haha2');
settingEntry.mergeProps({
newPropA: 'haha',
});
expect(settingEntry.getPropValue('newPropA')).toBe('haha');
settingEntry.setProps({
newPropB: 'haha',
});
expect(settingEntry.getPropValue('newPropB')).toBe('haha');
settingEntry.setValue({
newPropC: 'haha',
});
expect(settingEntry.getPropValue('newPropC')).toBe('haha');
expect(settingEntry.getPage()).toBe(currentDocument);
expect(settingEntry.getNode()).toBe(divNode);
expect(settingEntry.node).toBe(divNode);
expect(settingEntry.getId()).toBe('div');
expect(settingEntry.first).toBe(divNode);
expect(settingEntry.designer).toBe(designer);
expect(settingEntry.isSingle).toBeTruthy();
expect(settingEntry.isMultiple).toBeFalsy();
expect(settingEntry.isSameComponent).toBeTruthy();
expect(typeof settingEntry.getValue).toBe('function');
settingEntry.getValue();
});
it('onMetadataChange', () => {
designer.createComponentMeta(divMeta);
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div') as Node;
const { settingEntry } = divNode!;
const mockFn = jest.fn();
settingEntry.componentMeta.onMetadataChange(mockFn);
settingEntry.componentMeta.refreshMetadata();
expect(mockFn).toHaveBeenCalled();
});
it.skip('setupItems - customView', () => {
designer.createComponentMeta(divMeta);
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div') as Node;
const { settingEntry } = divNode;
// 模拟将第一个配置变成 react funcional component
settingEntry.componentMeta.getMetadata().combined[0].items[0] = props => props.xx;
settingEntry.setupItems();
});
it('清理方法测试', () => {
designer.createComponentMeta(divMeta);
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div');
const { settingEntry } = divNode!;
expect(settingEntry.items).toHaveLength(3);
settingEntry.purge();
expect(settingEntry.items).toHaveLength(0);
});
it('vision 兼容测试', () => {
designer.createComponentMeta(divMeta);
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div');
// console.log(divNode?.getPropValue('behavior'));
const { settingEntry } = divNode!;
expect(typeof settingEntry.getChildren).toBe('function');
expect(typeof settingEntry.getDOMNode).toBe('function');
expect(typeof settingEntry.getStatus).toBe('function');
expect(typeof settingEntry.setStatus).toBe('function');
settingEntry.getStatus();
settingEntry.setStatus();
settingEntry.getChildren();
settingEntry.getDOMNode();
});
it('没有 node', () => {
const create1 = designer.createSettingEntry.bind(designer);
const create2 = designer.createSettingEntry.bind(designer, []);
expect(create1).toThrowError('nodes should not be empty');
expect(create2).toThrowError('nodes should not be empty');
});
});
describe('designer.createSettingEntry 生成 settingEntry(多 node 场景)', () => {
it('相同类型的 node', () => {
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div');
const divNode2 = currentDocument?.getNode('div2');
const settingEntry = designer.createSettingEntry([divNode, divNode2]);
expect(settingEntry.isMultiple).toBeTruthy();
expect(settingEntry.isSameComponent).toBeTruthy();
expect(settingEntry.isSingle).toBeFalsy();
expect(settingEntry.getPropValue('behavior')).toBe('NORMAL');
expect(settingEntry.getProp('behavior').getValue()).toBe('NORMAL');
settingEntry.setPropValue('behavior', 'LARGE');
expect(settingEntry.getPropValue('behavior')).toBe('LARGE');
expect(settingEntry.get('behavior').getValue()).toBe('LARGE');
// 多个 node 都被成功设值
expect(divNode?.getPropValue('behavior')).toBe('LARGE');
expect(divNode2?.getPropValue('behavior')).toBe('LARGE');
settingEntry.getProp('behavior').setValue('SMALL');
expect(settingEntry.getPropValue('behavior')).toBe('SMALL');
// 多个 node 都被成功设值
expect(divNode?.getPropValue('behavior')).toBe('SMALL');
expect(divNode2?.getPropValue('behavior')).toBe('SMALL');
settingEntry.clearPropValue('behavior');
expect(settingEntry.getPropValue('behavior')).toBeUndefined();
// 多个 node 都被成功设值
expect(divNode?.getPropValue('behavior')).toBeUndefined();
expect(divNode2?.getPropValue('behavior')).toBeUndefined();
expect(settingEntry.getPropValue('fieldId')).toBe('div_k1ow3h1o');
settingEntry.setPropValue('fieldId', 'div_k1ow3h1o_new');
expect(settingEntry.getPropValue('fieldId')).toBe('div_k1ow3h1o_new');
expect(settingEntry.getExtraPropValue('extraPropA')).toBe('haha');
settingEntry.setExtraPropValue('extraPropA', 'haha2');
expect(settingEntry.getExtraPropValue('extraPropA')).toBe('haha2');
});
it('不同类型的 node', () => {
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div');
const testNode = currentDocument?.getNode('test');
const settingEntry = designer.createSettingEntry([divNode, testNode]);
expect(settingEntry.isMultiple).toBeTruthy();
expect(settingEntry.isSameComponent).toBeFalsy();
expect(settingEntry.isSingle).toBeFalsy();
// 不同类型的 node 场景下,理论上从页面上已没有修改属性的方法调用,所以此处不再断言各设值方法
// 思考:假如以后面向其他场景,比如用户用 API 强行调用,是否需要做健壮性保护?
});
});
});
================================================
FILE: packages/designer/tests/designer/setting/__snapshots__/setting-field.test.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`setting-field 测试 纯粹的 UnitTest 常规方法 1`] = `
Object {
"extraProps": Object {
"defaultValue": "NORMAL",
"display": "inline",
},
"name": "behavior",
"setter": Object {
"componentName": "MixedSetter",
"props": Object {
"setters": Array [
Object {
"_owner": null,
"key": null,
"props": Object {
"cancelable": false,
"loose": false,
"options": Array [
Object {
"title": "普通",
"value": "NORMAL",
},
Object {
"title": "隐藏",
"value": "HIDDEN",
},
],
},
"ref": null,
},
"VariableSetter",
],
},
},
"title": "默认状态",
"type": "field",
}
`;
exports[`setting-field 测试 纯粹的 UnitTest 常规方法 2`] = `
Object {
"extraProps": Object {
"defaultValue": "NORMAL",
"display": "inline",
},
"name": "behavior",
"setter": Object {
"componentName": "MixedSetter",
"props": Object {
"setters": Array [
Object {
"_owner": null,
"key": null,
"props": Object {
"cancelable": false,
"loose": false,
"options": Array [
Object {
"title": "普通",
"value": "NORMAL",
},
Object {
"title": "隐藏",
"value": "HIDDEN",
},
],
},
"ref": null,
},
"VariableSetter",
],
},
},
"title": "默认状态",
"type": "field",
}
`;
================================================
FILE: packages/designer/tests/document/selection.test.ts
================================================
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 { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
return {
getComponentMeta() {
return {
getMetadata() {
return { configure: { advanced: null } };
},
get advanced() {
return {};
},
};
},
transformProps(props) { return props; },
createSettingEntry: mockCreateSettingEntry,
postEvent() {},
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({});
});
describe('选择区测试', () => {
it('常规方法', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
const selectionChangeHandler = jest.fn();
selection.onSelectionChange(selectionChangeHandler);
selection.select('form');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selection.selected).toEqual(['form']);
selectionChangeHandler.mockClear();
selection.select('form');
expect(selectionChangeHandler).toHaveBeenCalledTimes(0);
expect(selection.selected).toEqual(['form']);
selection.select('node_k1ow3cbj');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['node_k1ow3cbj']);
expect(selection.selected).toEqual(['node_k1ow3cbj']);
selectionChangeHandler.mockClear();
selection.selectAll(['node_k1ow3cbj', 'form']);
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['node_k1ow3cbj', 'form']);
expect(selection.selected).toEqual(['node_k1ow3cbj', 'form']);
selectionChangeHandler.mockClear();
selection.remove('node_k1ow3cbj_fake');
selection.remove('node_k1ow3cbj');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form']);
expect(selection.selected).toEqual(['form']);
selectionChangeHandler.mockClear();
selection.clear();
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual([]);
expect(selection.selected).toEqual([]);
selectionChangeHandler.mockClear();
// 无选中时调用 clear,不再触发事件
selection.clear();
expect(selectionChangeHandler).toHaveBeenCalledTimes(0);
expect(selection.selected).toEqual([]);
selectionChangeHandler.mockClear();
});
it('add 方法', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
const selectionChangeHandler = jest.fn();
selection.onSelectionChange(selectionChangeHandler);
selection.add('form');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form']);
expect(selection.selected).toEqual(['form']);
selectionChangeHandler.mockClear();
// 再加一次相同的节点,不触发事件
selection.add('form');
expect(selectionChangeHandler).toHaveBeenCalledTimes(0);
expect(selection.selected).toEqual(['form']);
selectionChangeHandler.mockClear();
selection.add('form2');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form', 'form2']);
expect(selection.selected).toEqual(['form', 'form2']);
selectionChangeHandler.mockClear();
});
it('selectAll 包含不存在的 id', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
selection.selectAll(['form', 'node_k1ow3cbj', 'form2']);
expect(selection.selected).toEqual(['form', 'node_k1ow3cbj']);
});
it('dispose 方法 - 选中的节点没有被删除的', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
selection.selectAll(['form', 'node_k1ow3cbj']);
const selectionChangeHandler = jest.fn();
selection.onSelectionChange(selectionChangeHandler);
selection.dispose();
expect(selectionChangeHandler).not.toHaveBeenCalled();
});
it('containsNode 方法', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
const selectionChangeHandler = jest.fn();
selection.onSelectionChange(selectionChangeHandler);
selection.select('form');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form']);
expect(selection.selected).toEqual(['form']);
expect(selection.has('form')).toBe(true);
expect(selection.containsNode(currentDocument?.getNode('form'))).toBe(true);
expect(selection.containsNode(currentDocument?.getNode('node_k1ow3cbj'))).toBe(true);
expect(selection.containsNode(currentDocument?.getNode('page'))).toBe(false);
expect(selection.getNodes()).toEqual([currentDocument?.getNode('form')]);
selectionChangeHandler.mockClear();
selection.add('node_k1ow3cbj');
expect(selection.selected).toEqual(['form', 'node_k1ow3cbj']);
expect(selection.getTopNodes()).toEqual([currentDocument?.getNode('form')]);
expect(selection.getTopNodes(true)).toEqual([currentDocument?.getNode('form')]);
});
it('containsNode 方法 - excludeRoot: true', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
const selectionChangeHandler = jest.fn();
selection.onSelectionChange(selectionChangeHandler);
selection.select('page');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['page']);
expect(selection.selected).toEqual(['page']);
expect(selection.has('page')).toBe(true);
expect(selection.containsNode(currentDocument?.getNode('form'))).toBe(true);
expect(selection.containsNode(currentDocument?.getNode('form'), true)).toBe(false);
selectionChangeHandler.mockClear();
});
it('containsNode 方法 - excludeRoot: true', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
const selectionChangeHandler = jest.fn();
const dispose = selection.onSelectionChange(selectionChangeHandler);
selection.select('form');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form']);
selectionChangeHandler.mockClear();
// dispose 后,selected 会被赋值,但是变更事件不会被触发
dispose();
selection.select('page');
expect(selectionChangeHandler).toHaveBeenCalledTimes(0);
expect(selection.selected).toEqual(['page']);
selectionChangeHandler.mockClear();
});
it('getNodes', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
const { currentDocument } = project;
const { selection } = currentDocument!;
selection.selectAll(['form', 'node_k1ow3cbj', 'form2']);
// form2 is not a valid node
expect(selection.getNodes()).toHaveLength(2);
});
it('getTopNodes - BeforeOrAfter', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
const { currentDocument } = project;
const { selection } = currentDocument!;
selection.selectAll(['node_k1ow3cbj', 'node_k1ow3cbo']);
expect(selection.getTopNodes()).toHaveLength(2);
});
it('getTopNodes', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
const { currentDocument } = project;
const { selection } = currentDocument!;
selection.selectAll(['node_k1ow3cbj', 'node_k1ow3cbo', 'form', 'node_k1ow3cbl', 'form2']);
// form2 is not a valid node, and node_k1ow3cbj is a child node of form
expect(selection.getTopNodes()).toHaveLength(1);
});
});
================================================
FILE: packages/designer/tests/document/document-model/document-model.test.ts
================================================
import '../../fixtures/window';
import { DocumentModel, isDocumentModel, isPageSchema } from '../../../src/document/document-model';
import { Editor } from '@alilc/lowcode-editor-core';
import { Project } from '../../../src/project/project';
import { Designer } from '../../../src/designer/designer';
import formSchema from '../../fixtures/schema/form';
import divMeta from '../../fixtures/component-metadata/div';
import formMeta from '../../fixtures/component-metadata/form';
import otherMeta from '../../fixtures/component-metadata/other';
import pageMeta from '../../fixtures/component-metadata/page';
import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory';
describe('document-model 测试', () => {
let editor: Editor;
let designer: Designer;
let project: Project;
beforeEach(() => {
editor = new Editor();
designer = new Designer({ editor, shellModelFactory });
project = designer.project;
});
it('empty schema', () => {
const doc = new DocumentModel(project);
expect(doc.rootNode?.id).toBe('root');
expect(doc.currentRoot).toBe(doc.rootNode);
expect(doc.root).toBe(doc.rootNode);
expect(doc.modalNode).toBeUndefined();
expect(doc.isBlank()).toBeTruthy();
expect(doc.schema).toEqual({
componentName: 'Page',
condition: true,
conditionGroup: '',
hidden: false,
isLocked: false,
loop: undefined,
title: '',
id: 'root',
fileName: '',
props: {},
});
});
it('各种方法测试', () => {
const doc = new DocumentModel(project, formSchema);
const mockNode = { id: 1 };
doc.addWillPurge(mockNode);
expect(doc.willPurgeSpace).toHaveLength(1);
doc.removeWillPurge(mockNode);
expect(doc.willPurgeSpace).toHaveLength(0);
expect(doc.toData()).toMatchSnapshot();
// 测试插入已存在的 id,id 将会被重置
const formParentNode = doc.getNode('form').parent;
doc.insertNode(formParentNode, { id: 'form', componentName: 'Form' });
expect(formParentNode.children.get(formParentNode.children.size - 1).id).not.toBe('form');
doc.internalRemoveAndPurgeNode({ id: 'mockId' });
// internalSetDropLocation
doc.dropLocation = { a: 1 };
expect(doc.dropLocation).toEqual({ a: 1 });
// wrapWith
// none-selected
doc.wrapWith({ componentName: 'Wrap' });
doc.selection.select('form');
doc.wrapWith({ componentName: 'Wrap' });
expect(doc.getNode('form').parent.componentName).toBe('Wrap');
expect(doc.wrapWith({ componentName: 'Leaf' })).toBeNull();
// fileName
expect(doc.fileName).toBeTruthy();
doc.fileName = 'fileName';
expect(doc.fileName).toBe('fileName');
expect(doc.getNodeSchema(doc.getNode('form'))).toMatchSnapshot();
// TODO:
// expect(doc.simulatorProps).toMatchSnapshot();
const mockSimulator = {
isSimulator: true,
getComponent() {
return 'haha';
},
setSuspense() {},
};
doc.project.mountSimulator(mockSimulator);
expect(doc.simulator).toEqual(mockSimulator);
expect(doc.getComponent('Div')).toBe('haha');
expect(doc.opened).toBeFalsy();
expect(doc.isModified).toBeTruthy();
expect(doc.suspensed).toBeTruthy();
doc.open();
expect(doc.opened).toBeTruthy();
expect(doc.actived).toBeTruthy();
expect(doc.isModified).toBeTruthy();
expect(doc.suspensed).toBeFalsy();
doc.suspense();
doc.activate();
doc.close();
doc.remove();
const offReady = doc.onReady(() => {});
offReady();
expect(doc.history).toBe(doc.getHistory());
});
it('focusNode - using drillDown', () => {
const doc = new DocumentModel(project, formSchema);
expect(doc.focusNode.id).toBe('page');
doc.drillDown(doc.getNode('node_k1ow3cbb'));
expect(doc.focusNode.id).toBe('node_k1ow3cbb');
});
it('focusNode - using drillDown & import', () => {
const doc = new DocumentModel(project, formSchema);
expect(doc.focusNode.id).toBe('page');
doc.drillDown(doc.getNode('node_k1ow3cbb'));
doc.import(formSchema);
expect(doc.focusNode.id).toBe('node_k1ow3cbb');
});
it('focusNode - using focusNodeSelector', () => {
const doc = new DocumentModel(project, formSchema);
editor.set('focusNodeSelector', (rootNode) => {
return rootNode.children.get(1);
});
expect(doc.focusNode.id).toBe('node_k1ow3cbb');
});
it('getNodeCount', () => {
const doc = new DocumentModel(project);
// using default schema, only one node
expect(doc.getNodeCount()).toBe(1);
});
it('getNodeSchema', () => {
const doc = new DocumentModel(project, formSchema);
expect(doc.getNodeSchema('page').id).toBe('page');
});
it('export - with __isTopFixed__', () => {
formSchema.children[1].props.__isTopFixed__ = true;
const doc = new DocumentModel(project, formSchema);
const schema = doc.export();
expect(schema.children).toHaveLength(3);
expect(schema.children[0].componentName).toBe('RootContent');
expect(schema.children[1].componentName).toBe('RootHeader');
expect(schema.children[2].componentName).toBe('RootFooter');
});
describe('createNode', () => {
it('same id && componentName', () => {
const doc = new DocumentModel(project, formSchema);
const node = doc.createNode({
componentName: 'RootFooter',
id: 'node_k1ow3cbc',
props: {},
condition: true,
});
expect(node.parent).toBeNull();
});
it('same id && different componentName', () => {
const doc = new DocumentModel(project, formSchema);
const originalNode = doc.getNode('node_k1ow3cbc');
const node = doc.createNode({
componentName: 'RootFooter2',
id: 'node_k1ow3cbc',
props: {},
condition: true,
});
// expect(originalNode.parent).toBeNull();
expect(node.id).not.toBe('node_k1ow3cbc');
});
});
it('setSuspense', () => {
const doc = new DocumentModel(project, formSchema);
expect(doc.opened).toBeFalsy();
doc.setSuspense(false);
});
it('registerAddon / getAddonData / exportAddonData', () => {
const doc = new DocumentModel(project);
expect(doc.getAddonData('a')).toBeUndefined();
doc.registerAddon('a', () => 'addon a');
doc.registerAddon('a', () => 'modified addon a');
doc.registerAddon('b', () => 'addon b');
doc.registerAddon('c', () => null);
['id', 'layout', 'params'].forEach((name) => {
expect(() => doc.registerAddon(name, () => {})).toThrow();
});
expect(doc.getAddonData('a')).toBe('modified addon a');
expect(doc.getAddonData('b')).toBe('addon b');
expect(doc.exportAddonData()).toEqual({
a: 'modified addon a',
b: 'addon b',
});
});
it('checkNesting / checkDropTarget / checkNestingUp / checkNestingDown', () => {
designer.createComponentMeta(pageMeta);
designer.createComponentMeta(formMeta);
designer.createComponentMeta(otherMeta);
const doc = new DocumentModel(project, formSchema);
expect(
doc.checkDropTarget(doc.getNode('page'), { type: 'node', nodes: [doc.getNode('form')] }),
).toBeTruthy();
expect(
doc.checkDropTarget(doc.getNode('page'), {
type: 'nodedata',
data: { componentName: 'Form' },
}),
).toBeTruthy();
expect(
doc.checkNesting(doc.getNode('page'), { type: 'node', nodes: [doc.getNode('form')] }),
).toBeTruthy();
expect(
doc.checkNesting(doc.getNode('page'), {
type: 'nodedata',
data: { componentName: 'Form' },
}),
).toBeTruthy();
expect(
doc.checkNesting(doc.getNode('page'), doc.getNode('form'))
).toBeTruthy();
expect(
doc.checkNesting(doc.getNode('page'), null)
).toBeTruthy();
expect(
doc.checkNesting(doc.getNode('page'), {
type: 'nodedata',
data: { componentName: 'Other' },
})
).toBeFalsy();
expect(
doc.checkNestingUp(doc.getNode('page'), { componentName: 'Other' })
).toBeFalsy();
expect(
doc.checkNestingDown(doc.getNode('page'), { componentName: 'Other' })
).toBeTruthy();
expect(doc.checkNestingUp(doc.getNode('page'), null)).toBeTruthy();
});
it('getComponentsMap', () => {
designer.createComponentMeta(divMeta);
designer.createComponentMeta(otherMeta);
const doc = new DocumentModel(project, formSchema);
const comps = doc.getComponentsMap(['Other']);
expect(comps.find(comp => comp.componentName === 'Div')).toEqual(
{ componentName: 'Div', package: '@ali/vc-div' }
);
expect(comps.find(comp => comp.componentName === 'Other')).toEqual(
{ componentName: 'Other', package: '@ali/vc-other' }
);
expect(comps.find(comp => comp.componentName === 'Page')).toEqual(
{ componentName: 'Page', devMode: 'lowCode' }
);
const comps2 = doc.getComponentsMap(['Div']);
});
it('acceptRootNodeVisitor / getRootNodeVisitor', () => {
designer.createComponentMeta(divMeta);
designer.createComponentMeta(otherMeta);
const doc = new DocumentModel(project, formSchema);
const ret = doc.acceptRootNodeVisitor('getPageId', (root) => {
return 'page';
});
expect(ret).toBe('page');
expect(doc.getRootNodeVisitor('getPageId')).toBe('page');
// expect(doc.getComponentsMap(['Other'])).toEqual([
// { componentName: 'Div', package: '@ali/vc-div' },
// { componentName: 'Other', package: '@ali/vc-other' },
// ]);
});
it('deprecated methods', () => {
const doc = new DocumentModel(project, formSchema);
doc.refresh();
doc.onRefresh();
});
});
it('isDocumentModel', () => {
expect(isDocumentModel({ rootNode: {} })).toBeTruthy();
});
it('isPageSchema', () => {
expect(isPageSchema({ componentName: 'Page' })).toBeTruthy();
});
================================================
FILE: packages/designer/tests/document/document-model/__snapshots__/document-model.test.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`document-model 测试 各种方法测试 1`] = `
Object {
"componentsMap": Array [
Object {
"componentName": "PageHeader",
"devMode": "lowCode",
},
Object {
"componentName": "RootHeader",
"devMode": "lowCode",
},
Object {
"componentName": "TextField",
"devMode": "lowCode",
},
Object {
"componentName": "Column",
"devMode": "lowCode",
},
Object {
"componentName": "SelectField",
"devMode": "lowCode",
},
Object {
"componentName": "ColumnsLayout",
"devMode": "lowCode",
},
Object {
"componentName": "CardContent",
"devMode": "lowCode",
},
Object {
"componentName": "Card",
"devMode": "lowCode",
},
Object {
"componentName": "Button",
"devMode": "lowCode",
},
Object {
"componentName": "Div",
"devMode": "lowCode",
},
Object {
"componentName": "Form",
"devMode": "lowCode",
},
Object {
"componentName": "RootContent",
"devMode": "lowCode",
},
Object {
"componentName": "RootFooter",
"devMode": "lowCode",
},
Object {
"componentName": "Page",
"devMode": "lowCode",
},
],
"componentsTree": Array [
Object {
"children": Array [
Object {
"children": Array [
Object {
"componentName": "PageHeader",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cbd",
"isLocked": false,
"props": Object {
"__slot__action": false,
"__slot__content": false,
"__slot__crumb": false,
"__slot__extraContent": false,
"__slot__logo": false,
"__slot__tab": false,
"__style__": Object {},
"action": "",
"content": "",
"crumb": "",
"extraContent": "",
"fieldId": "pageHeader_k1ow3h1i",
"logo": "",
"subTitle": false,
"tab": "",
"title": Object {
"value": Array [
Object {
"componentName": "Text",
"condition": true,
"id": "node_k1ow3cbf",
"props": Object {
"__style__": Object {},
"behavior": "NORMAL",
"content": Object {
"en-US": "Title",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "个人信息",
},
"fieldId": "text_k1ow3h1j",
"maxLine": 0,
"showTitle": false,
},
},
],
},
},
"title": "",
},
],
"componentName": "RootHeader",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cba",
"isLocked": false,
"props": Object {},
"title": "",
},
Object {
"children": Array [
Object {
"children": Array [
Object {
"children": Array [
Object {
"children": Array [
Object {
"children": Array [
Object {
"children": Array [
Object {
"componentName": "TextField",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cbz",
"isLocked": false,
"props": Object {
"__category__": "form",
"__style__": Object {},
"__useMediator": "value",
"addonAfter": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"addonBefore": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"autoFocus": false,
"autoHeight": false,
"behavior": "NORMAL",
"cutString": false,
"fieldId": "textField_k1ow3h1w",
"fieldName": "name",
"hasClear": false,
"hasLimitHint": false,
"htmlType": "input",
"label": Object {
"en-US": "TextField",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "姓名",
},
"labelAlign": "top",
"labelColOffset": 0,
"labelColSpan": 4,
"labelTextAlign": "right",
"labelTipsIcon": "",
"labelTipsText": Object {
"en-US": "",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"labelTipsTypes": "none",
"placeholder": Object {
"en-US": "please input",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "请输入",
},
"rows": 4,
"size": "medium",
"state": "",
"tips": Object {
"en-US": "",
"type": "i18n",
"zh-CN": "",
},
"trim": false,
"validation": Array [
Object {
"type": "required",
},
],
"value": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"wrapperColOffset": 0,
"wrapperColSpan": 0,
},
"title": "",
},
Object {
"componentName": "TextField",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cc1",
"isLocked": false,
"props": Object {
"__category__": "form",
"__style__": Object {},
"__useMediator": "value",
"addonAfter": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"addonBefore": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"autoFocus": false,
"autoHeight": false,
"behavior": "NORMAL",
"cutString": false,
"fieldId": "textField_k1ow3h1y",
"fieldName": "englishName",
"hasClear": false,
"hasLimitHint": false,
"htmlType": "input",
"label": Object {
"en-US": "TextField",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "英文名",
},
"labelAlign": "top",
"labelColOffset": 0,
"labelColSpan": 4,
"labelTextAlign": "right",
"labelTipsIcon": "",
"labelTipsText": Object {
"en-US": "",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"labelTipsTypes": "none",
"placeholder": Object {
"en-US": "please input",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "请输入",
},
"rows": 4,
"size": "medium",
"state": "",
"tips": Object {
"en-US": "",
"type": "i18n",
"zh-CN": "",
},
"trim": false,
"validation": Array [],
"value": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"wrapperColOffset": 0,
"wrapperColSpan": 0,
},
"title": "",
},
Object {
"componentName": "TextField",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cc3",
"isLocked": false,
"props": Object {
"__category__": "form",
"__style__": Object {},
"__useMediator": "value",
"addonAfter": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"addonBefore": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"autoFocus": false,
"autoHeight": false,
"behavior": "NORMAL",
"cutString": false,
"fieldId": "textField_k1ow3h20",
"fieldName": "jobTitle",
"hasClear": false,
"hasLimitHint": false,
"htmlType": "input",
"label": Object {
"en-US": "TextField",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "职位",
},
"labelAlign": "top",
"labelColOffset": 0,
"labelColSpan": 4,
"labelTextAlign": "right",
"labelTipsIcon": "",
"labelTipsText": Object {
"en-US": "",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"labelTipsTypes": "none",
"placeholder": Object {
"en-US": "please input",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "请输入",
},
"rows": 4,
"size": "medium",
"state": "",
"tips": Object {
"en-US": "",
"type": "i18n",
"zh-CN": "",
},
"trim": false,
"validation": Array [],
"value": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"wrapperColOffset": 0,
"wrapperColSpan": 0,
},
"title": "",
},
],
"componentName": "Column",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cbx",
"isLocked": false,
"props": Object {
"__style__": Object {},
"colSpan": "",
"fieldId": "column_k1p1bnjm",
},
"title": "",
},
Object {
"children": Array [
Object {
"componentName": "TextField",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cc2",
"isLocked": false,
"props": Object {
"__category__": "form",
"__style__": Object {},
"__useMediator": "value",
"addonAfter": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"addonBefore": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"autoFocus": false,
"autoHeight": false,
"behavior": "NORMAL",
"cutString": false,
"fieldId": "textField_k1ow3h1z",
"fieldName": "nickName",
"hasClear": false,
"hasLimitHint": false,
"htmlType": "input",
"label": Object {
"en-US": "TextField",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "花名",
},
"labelAlign": "top",
"labelColOffset": 0,
"labelColSpan": 4,
"labelTextAlign": "right",
"labelTipsIcon": "",
"labelTipsText": Object {
"en-US": "",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"labelTipsTypes": "none",
"placeholder": Object {
"en-US": "please input",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "请输入",
},
"rows": 4,
"size": "medium",
"state": "",
"tips": Object {
"en-US": "",
"type": "i18n",
"zh-CN": "",
},
"trim": false,
"validation": Array [],
"value": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"wrapperColOffset": 0,
"wrapperColSpan": 0,
},
"title": "",
},
Object {
"componentName": "SelectField",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cc0",
"isLocked": false,
"props": Object {
"__category__": "form",
"__style__": Object {},
"__useMediator": "value",
"autoWidth": true,
"behavior": "NORMAL",
"dataSource": Array [
Object {
"__sid__": "serial_k1owc4t1",
"defaultChecked": false,
"sid": "opt_k1owc4t2",
"text": Object {
"__sid__": "param_k1owc4tb",
"en-US": "Option 1",
"type": "i18n",
"zh-CN": "男",
},
"value": "M",
},
Object {
"__sid__": "serial_k1owc4t2",
"defaultChecked": false,
"sid": "opt_k1owc4t3",
"text": Object {
"__sid__": "param_k1owc4tf",
"en-US": "Option 2",
"type": "i18n",
"zh-CN": "女",
},
"value": "F",
},
],
"fieldId": "select_k1ow3h1x",
"fieldName": "gender",
"filterLocal": true,
"hasArrow": true,
"hasBorder": true,
"hasClear": false,
"hasSelectAll": false,
"label": Object {
"en-US": "SelectField",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "性别",
},
"labelAlign": "top",
"labelColOffset": 0,
"labelColSpan": 4,
"labelTextAlign": "right",
"labelTipsIcon": "",
"labelTipsText": Object {
"en-US": "",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"labelTipsTypes": "none",
"mode": "single",
"notFoundContent": Object {
"type": "i18n",
"use": "zh-CN",
},
"placeholder": Object {
"en-US": "please select",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "请选择",
},
"searchDelay": 300,
"showSearch": false,
"size": "medium",
"tips": Object {
"en-US": "",
"type": "i18n",
"zh-CN": "",
},
"validation": Array [
Object {
"type": "required",
},
],
"value": "",
"wrapperColOffset": 0,
"wrapperColSpan": 0,
},
"title": "",
},
],
"componentName": "Column",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cby",
"isLocked": false,
"props": Object {
"__style__": Object {},
"colSpan": "",
"fieldId": "column_k1p1bnjn",
},
"title": "",
},
],
"componentName": "ColumnsLayout",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cbw",
"isLocked": false,
"props": Object {
"__style__": Object {},
"columnGap": "20",
"fieldId": "columns_k1ow3h1v",
"layout": "6:6",
"rowGap": 0,
},
"title": "",
},
],
"componentName": "CardContent",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cbk",
"isLocked": false,
"props": Object {},
"title": "",
},
],
"componentName": "Card",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cbj",
"isLocked": false,
"props": Object {
"__slot__extra": false,
"__slot__subTitle": false,
"__slot__title": false,
"__style__": ":root {
margin-bottom: 12px;
}",
"className": "card_kgaqfbm5",
"contentHeight": "",
"dividerNoInset": false,
"extra": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"fieldId": "card_k1ow3h1l",
"showHeadDivider": true,
"showTitleBullet": true,
"subTitle": Object {
"en-US": "",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"title": Object {
"en-US": "Title",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "基本信息",
},
},
"title": "",
},
Object {
"children": Array [
Object {
"children": Array [
Object {
"componentName": "TextField",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cc4",
"isLocked": false,
"props": Object {
"__category__": "form",
"__style__": Object {},
"__useMediator": "value",
"addonAfter": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"addonBefore": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"autoFocus": false,
"autoHeight": false,
"behavior": "NORMAL",
"cutString": false,
"fieldId": "textField_k1ow3h21",
"fieldName": "department",
"hasClear": false,
"hasLimitHint": false,
"htmlType": "input",
"label": Object {
"en-US": "TextField",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "所属部门",
},
"labelAlign": "top",
"labelColOffset": 0,
"labelColSpan": 4,
"labelTextAlign": "right",
"labelTipsIcon": "",
"labelTipsText": Object {
"en-US": "",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"labelTipsTypes": "none",
"placeholder": Object {
"en-US": "please input",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "请输入",
},
"rows": 4,
"size": "medium",
"state": "",
"tips": Object {
"en-US": "",
"type": "i18n",
"zh-CN": "",
},
"trim": false,
"validation": Array [],
"value": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"wrapperColOffset": 0,
"wrapperColSpan": 0,
},
"title": "",
},
Object {
"children": Array [
Object {
"children": Array [
Object {
"componentName": "TextField",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cc8",
"isLocked": false,
"props": Object {
"__category__": "form",
"__style__": Object {},
"__useMediator": "value",
"addonAfter": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"addonBefore": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"autoFocus": false,
"autoHeight": false,
"behavior": "NORMAL",
"cutString": false,
"fieldId": "textField_k1ow3h23",
"fieldName": "leader",
"hasClear": false,
"hasLimitHint": false,
"htmlType": "input",
"label": Object {
"en-US": "TextField",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "主管",
},
"labelAlign": "top",
"labelColOffset": 0,
"labelColSpan": 4,
"labelTextAlign": "right",
"labelTipsIcon": "",
"labelTipsText": Object {
"en-US": "",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"labelTipsTypes": "none",
"placeholder": Object {
"en-US": "please input",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "请输入",
},
"rows": 4,
"size": "medium",
"state": "",
"tips": Object {
"en-US": "",
"type": "i18n",
"zh-CN": "",
},
"trim": false,
"validation": Array [],
"value": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"wrapperColOffset": 0,
"wrapperColSpan": 0,
},
"title": "",
},
],
"componentName": "Column",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cc6",
"isLocked": false,
"props": Object {
"__style__": Object {},
"colSpan": "",
"fieldId": "column_k1p1bnjo",
},
"title": "",
},
Object {
"children": Array [
Object {
"componentName": "TextField",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cc9",
"isLocked": false,
"props": Object {
"__category__": "form",
"__style__": Object {},
"__useMediator": "value",
"addonAfter": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"addonBefore": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"autoFocus": false,
"autoHeight": false,
"behavior": "NORMAL",
"cutString": false,
"fieldId": "textField_k1ow3h24",
"fieldName": "hrg",
"hasClear": false,
"hasLimitHint": false,
"htmlType": "input",
"label": Object {
"en-US": "TextField",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "HRG",
},
"labelAlign": "top",
"labelColOffset": 0,
"labelColSpan": 4,
"labelTextAlign": "right",
"labelTipsIcon": "",
"labelTipsText": Object {
"en-US": "",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"labelTipsTypes": "none",
"placeholder": Object {
"en-US": "please input",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "请输入",
},
"rows": 4,
"size": "medium",
"state": "",
"tips": Object {
"en-US": "",
"type": "i18n",
"zh-CN": "",
},
"trim": false,
"validation": Array [],
"value": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"wrapperColOffset": 0,
"wrapperColSpan": 0,
},
"title": "",
},
],
"componentName": "Column",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cc7",
"isLocked": false,
"props": Object {
"__style__": Object {},
"colSpan": "",
"fieldId": "column_k1p1bnjp",
},
"title": "",
},
],
"componentName": "ColumnsLayout",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cc5",
"isLocked": false,
"props": Object {
"__style__": Object {},
"columnGap": "20",
"fieldId": "columns_k1ow3h22",
"layout": "6:6",
"rowGap": 0,
},
"title": "",
},
],
"componentName": "CardContent",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cbm",
"isLocked": false,
"props": Object {},
"title": "",
},
],
"componentName": "Card",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cbl",
"isLocked": false,
"props": Object {
"__slot__extra": false,
"__slot__subTitle": false,
"__slot__title": false,
"__style__": ":root {
margin-bottom: 12px;
}",
"className": "card_kgaqfbm6",
"contentHeight": "",
"dividerNoInset": false,
"extra": Object {
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"fieldId": "card_k1ow3h1m",
"showHeadDivider": true,
"showTitleBullet": true,
"subTitle": Object {
"en-US": "",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "",
},
"title": Object {
"en-US": "Title",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "部门信息",
},
},
"title": "",
},
Object {
"children": Array [
Object {
"componentName": "Button",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cbn",
"isLocked": false,
"props": Object {
"__style__": ":root {
margin-right: 16px;
width: 80px
}",
"baseIcon": "",
"behavior": "NORMAL",
"className": "button_kgaqfbm7",
"content": Object {
"en-US": "Button",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "提交",
},
"fieldId": "button_k1ow3h1n",
"loading": false,
"onClick": Object {
"events": Array [
Object {
"id": "submit",
"name": "submit",
"params": Object {},
"type": "actionRef",
"uuid": "1570966253282_0",
},
],
"rawType": "events",
"type": "JSExpression",
"value": "this.utils.legaoBuiltin.execEventFlow.bind(this, [this.submit])",
},
"otherIcon": "",
"size": "medium",
"triggerEventsWhenLoading": false,
"type": "primary",
},
"title": "",
},
Object {
"componentName": "Button",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cbp",
"isLocked": false,
"props": Object {
"__style__": ":root {
width: 80px;
}",
"baseIcon": "",
"behavior": "NORMAL",
"className": "button_kgaqfbm8",
"content": Object {
"en-US": "Button",
"type": "i18n",
"use": "zh-CN",
"zh-CN": "取消",
},
"fieldId": "button_k1ow3h1p",
"greeting": Object {
"value": Array [
Object {
"componentName": "Text",
"props": Object {},
},
],
},
"loading": false,
"otherIcon": "",
"size": "medium",
"triggerEventsWhenLoading": false,
"type": "normal",
},
"title": "",
},
],
"componentName": "Div",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cbo",
"isLocked": false,
"props": Object {
"__style__": ":root {
display: flex;
align-items: flex-start;
justify-content: center;
background: #fff;
padding: 20px 0;
}",
"behavior": "NORMAL",
"className": "div_kgaqfbm9",
"customClassName": "",
"events": Object {},
"fieldId": "div_k1ow3h1o",
"useFieldIdAsDomId": false,
},
"title": "",
},
],
"componentName": "Form",
"condition": true,
"conditionGroup": "",
"extraPropA": "extraPropA",
"hidden": false,
"id": "form",
"isLocked": false,
"props": Object {
"__style__": Object {},
"autoUnmount": true,
"autoValidate": true,
"behavior": "NORMAL",
"dataSource": Object {
"type": "variable",
"variable": "state.formData",
},
"fieldId": "form",
"fieldOptions": Object {},
"labelAlign": "top",
"obj": Object {
"a": 1,
"b": false,
"c": "string",
},
"scrollToFirstError": true,
"size": "medium",
"slotA": "",
},
"title": "",
},
],
"componentName": "RootContent",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cbb",
"isLocked": false,
"props": Object {
"contentBgColor": "transparent",
"contentMargin": "20",
"contentPadding": "0",
},
"title": "",
},
Object {
"componentName": "RootFooter",
"condition": true,
"conditionGroup": "",
"hidden": false,
"id": "node_k1ow3cbc",
"isLocked": false,
"props": Object {},
"title": "",
},
],
"componentName": "Page",
"condition": true,
"conditionGroup": "",
"css": "body{background-color:#f2f3f5}.card_kgaqfbm5 {
margin-bottom: 12px;
}.card_kgaqfbm6 {
margin-bottom: 12px;
}.button_kgaqfbm7 {
margin-right: 16px;
width: 80px
}.button_kgaqfbm8 {
width: 80px;
}.div_kgaqfbm9 {
display: flex;
align-items: flex-start;
justify-content: center;
background: #fff;
padding: 20px 0;
}",
"dataSource": Object {
"globalConfig": Object {
"fit": Object {
"compiled": "",
"error": Object {},
"source": "",
"type": "js",
},
},
"list": Array [],
"offline": Array [],
"online": Array [],
"sync": true,
},
"hidden": false,
"i18n": Object {
"en-US": Object {
"i18n-jwg27yo3": "China",
"i18n-jwg27yo4": "Hello",
},
"zh-CN": Object {
"i18n-jwg27yo3": "中国",
"i18n-jwg27yo4": "你好",
},
},
"id": "page",
"isLocked": false,
"lifeCycles": Object {
"constructor": Object {
"compiled": "function constructor() {
var module = { exports: {} };
var _this = this;
this.__initMethods__(module.exports, module);
Object.keys(module.exports).forEach(function(item) {
if(typeof module.exports[item] === 'function'){
_this[item] = module.exports[item];
}
});
}",
"source": "function constructor() {
var module = { exports: {} };
var _this = this;
this.__initMethods__(module.exports, module);
Object.keys(module.exports).forEach(function(item) {
if(typeof module.exports[item] === 'function'){
_this[item] = module.exports[item];
}
});
}",
"type": "js",
},
},
"methods": Object {
"__initMethods__": Object {
"compiled": "function (exports, module) { /*set actions code here*/ }",
"source": "function (exports, module) { /*set actions code here*/ }",
"type": "js",
},
},
"props": Object {
"className": "page_kgaqfbm4",
"containerStyle": Object {},
"extensions": Object {
"启用页头": true,
},
"pageStyle": Object {
"backgroundColor": "#f2f3f5",
},
"templateVersion": "1.0.0",
},
"title": "hey, i' a page!",
},
],
"utils": undefined,
}
`;
exports[`document-model 测试 各种方法测试 2`] = `null`;
================================================
FILE: packages/designer/tests/document/history/history.test.ts
================================================
import '../../fixtures/window';
import { mobx, makeAutoObservable, globalContext, Editor } from '@alilc/lowcode-editor-core';
import { History } from '../../../src/document/history';
import { delay } from '../../utils/misc';
import { Workspace } from '@alilc/lowcode-workspace';
class Node {
data: number;
children: Node[] = [];
constructor(data: number) {
makeAutoObservable(this);
this.data = data;
}
addNode(node: Node) {
this.children.push(node);
}
toObject() {
return {
data: this.data,
children: this.children.map((c) => c.toObject()),
};
}
}
let tree: Node = null;
beforeEach(() => {
tree = new Node(1);
tree.addNode(new Node(2));
});
afterEach(() => {
tree = null;
});
describe('History', () => {
beforeAll(() => {
const editor = new Editor();
globalContext.register(editor, Editor);
globalContext.register(editor, 'editor');
globalContext.register(new Workspace(), 'workspace');
});
it('data function & records', async () => {
const mockRedoFn = jest.fn();
const mockDataFn = jest.fn();
const history = new History(() => {
const data = tree.toObject();
mockDataFn(data);
return data;
}, mockRedoFn);
expect(mockDataFn).toHaveBeenCalledTimes(1);
expect(mockDataFn).toHaveBeenCalledWith({ data: 1, children: [{ data: 2, children: [] }] });
expect(history.hotData).toMatchSnapshot();
// @ts-ignore
expect(history.session.cursor).toBe(0);
// @ts-ignore
expect(history.records).toHaveLength(1);
tree.data = 3;
expect(mockDataFn).toHaveBeenCalledTimes(2);
expect(mockDataFn).toHaveBeenCalledWith({ data: 3, children: [{ data: 2, children: [] }] });
expect(history.hotData).toMatchSnapshot();
// @ts-ignore
expect(history.session.cursor).toBe(0);
// @ts-ignore
expect(history.records).toHaveLength(1);
// modify data after timeGap
await delay(1200);
tree.data = 5;
expect(mockDataFn).toHaveBeenCalledTimes(3);
expect(mockDataFn).toHaveBeenCalledWith({ data: 5, children: [{ data: 2, children: [] }] });
expect(history.hotData).toMatchSnapshot();
// @ts-ignore
expect(history.session.cursor).toBe(1);
// @ts-ignore
expect(history.records).toHaveLength(2);
history.setSerialization({
serialize(data: Node): string {
return JSON.stringify(data);
},
unserialize(data: string) {
return JSON.parse(data);
},
});
// modify data after timeGap
await delay(1200);
tree.data = 7;
expect(mockDataFn).toHaveBeenCalledTimes(4);
expect(mockDataFn).toHaveBeenCalledWith({ data: 7, children: [{ data: 2, children: [] }] });
expect(history.hotData).toMatchSnapshot();
});
it('isSavePoint & savePoint', async () => {
const history = new History(
() => {
const data = tree.toObject();
return data;
},
() => {},
);
expect(history.isSavePoint()).toBeFalsy();
expect(history.isModified()).toBeFalsy();
await delay(1200);
tree.data = 3;
expect(history.isSavePoint()).toBeTruthy();
history.savePoint();
expect(history.isSavePoint()).toBeFalsy();
});
it('go & forward & back & onCursor', async () => {
const mockRedoFn = jest.fn();
const mockCursorFn = jest.fn();
const mockStateFn = jest.fn();
const history = new History(
() => {
const data = tree.toObject();
return data;
},
(data) => {
mockRedoFn(data);
},
);
// undoable ❌ & redoable ❌ & modified ❌
expect(history.getState()).toBe(0);
await delay(1200);
tree.data = 3;
await delay(1200);
tree.data = 5;
await delay(1200);
tree.data = 7;
const dataCursor0 = { data: 1, children: [{ data: 2, children: [] }] };
const dataCursor1 = { data: 3, children: [{ data: 2, children: [] }] };
const dataCursor2 = { data: 5, children: [{ data: 2, children: [] }] };
const dataCursor3 = { data: 7, children: [{ data: 2, children: [] }] };
// redoable ❌
expect(history.getState()).toBe(7 - 2);
const off1 = history.onCursor(mockCursorFn);
const off2 = history.onStateChange(mockStateFn);
// @ts-ignore
expect(history.records).toHaveLength(4);
// @ts-ignore
expect(history.session.cursor).toBe(3);
// step 1
history.back();
expect(mockCursorFn).toHaveBeenNthCalledWith(
1,
JSON.stringify(dataCursor2),
);
expect(mockStateFn).toHaveBeenNthCalledWith(1, 7);
expect(mockRedoFn).toHaveBeenNthCalledWith(1, dataCursor2);
// step 2
history.back();
expect(mockCursorFn).toHaveBeenNthCalledWith(
2,
JSON.stringify(dataCursor1),
);
expect(mockStateFn).toHaveBeenNthCalledWith(2, 7);
expect(mockRedoFn).toHaveBeenNthCalledWith(2, dataCursor1);
// step 3
history.back();
expect(mockCursorFn).toHaveBeenNthCalledWith(
3,
JSON.stringify(dataCursor0),
);
expect(mockStateFn).toHaveBeenNthCalledWith(3, 7 - 4 - 1);
expect(mockRedoFn).toHaveBeenNthCalledWith(3, dataCursor0);
// step 4
history.forward();
expect(mockCursorFn).toHaveBeenNthCalledWith(
4,
JSON.stringify(dataCursor1),
);
expect(mockStateFn).toHaveBeenNthCalledWith(4, 7);
expect(mockRedoFn).toHaveBeenNthCalledWith(4, dataCursor1);
// step 5
history.forward();
expect(mockCursorFn).toHaveBeenNthCalledWith(
5,
JSON.stringify(dataCursor2),
);
expect(mockStateFn).toHaveBeenNthCalledWith(5, 7);
expect(mockRedoFn).toHaveBeenNthCalledWith(5, dataCursor2);
// step 6
history.go(3);
expect(mockCursorFn).toHaveBeenNthCalledWith(
6,
JSON.stringify(dataCursor3),
);
expect(mockStateFn).toHaveBeenNthCalledWith(6, 7 - 2);
expect(mockRedoFn).toHaveBeenNthCalledWith(6, dataCursor3);
// step 7
history.go(0);
expect(mockCursorFn).toHaveBeenNthCalledWith(
7,
JSON.stringify(dataCursor0),
);
expect(mockStateFn).toHaveBeenNthCalledWith(7, 7 - 4 - 1);
expect(mockRedoFn).toHaveBeenNthCalledWith(7, dataCursor0);
off1();
off2();
mockStateFn.mockClear();
mockCursorFn.mockClear();
history.go(1);
expect(mockStateFn).not.toHaveBeenCalled();
expect(mockCursorFn).not.toHaveBeenCalled();
});
it('go() - edge case of cursor', async () => {
const mockRedoFn = jest.fn();
const mockCursorFn = jest.fn();
const mockStateFn = jest.fn();
const history = new History(
() => {
const data = tree.toObject();
return data;
},
(data) => {
mockRedoFn(data);
},
);
await delay(1200);
tree.data = 3;
await delay(1200);
tree.data = 5;
history.go(-1);
// @ts-ignore
expect(history.session.cursor).toBe(0);
history.go(3);
// @ts-ignore
expect(history.session.cursor).toBe(2);
});
it('destroy()', async () => {
const history = new History(
() => {
const data = tree.toObject();
return data;
},
(data) => {
mockRedoFn(data);
},
);
history.destroy();
// @ts-ignore
expect(history.records).toHaveLength(0);
});
it('sleep & wakeup', async () => {
const mockRedoFn = jest.fn();
const history = new History(
() => {
const data = tree.toObject();
return data;
},
(data) => {
mockRedoFn(data);
},
);
// @ts-ignore
history.sleep();
await delay(1200);
tree.data = 3;
// no record has been pushed into records because of history is asleep.
// @ts-ignore
expect(history.records).toHaveLength(1);
// @ts-ignore
history.wakeup();
tree.data = 4;
// @ts-ignore
expect(history.records).toHaveLength(2);
});
});
describe('History - errors', () => {
beforeAll(() => {
globalContext.replace(Editor, null);
});
it('no editor', () => {
const history = new History(
() => {
const data = tree.toObject();
return data;
},
(data) => {
},
);
history.back();
history.forward();
});
it('no session', () => {
const history = new History(
() => {
const data = tree.toObject();
return data;
},
(data) => {
},
);
// @ts-ignore
history.session = undefined;
history.back();
history.forward();
history.savePoint();
});
});
================================================
FILE: packages/designer/tests/document/history/session.test.ts
================================================
import '../../fixtures/window';
import { Session } from '../../../src/document/history';
import { delay } from '../../utils/misc';
describe('Session', () => {
it('constructor', () => {
const session = new Session(1, { a: 1 });
expect(session.cursor).toBe(1);
expect(session.data).toEqual({ a: 1 });
// @ts-ignore
expect(session.timeGap).toBe(1000);
expect(session.isActive()).toBeTruthy();
});
it('log()', () => {
const session = new Session(1, { a: 1 });
session.log({ a: 2 });
session.log({ a: 3 });
expect(session.data).toEqual({ a: 3 });
});
it('end()', () => {
const session = new Session(1, { a: 1 });
session.end();
expect(session.isActive()).toBeFalsy();
session.log({ a: 2 });
// log is not possible if current session is inactive
expect(session.data).toEqual({ a: 1 });
});
it('timeGap', async () => {
const session = new Session(1, { a: 1 });
expect(session.isActive()).toBeTruthy();
await delay(1200);
expect(session.isActive()).toBeFalsy();
session.log({ a: 2 });
// log is not possible if current session is inactive
expect(session.data).toEqual({ a: 1 });
});
it('custom timeGap', async () => {
const session = new Session(1, { a: 1 }, 2000);
expect(session.isActive()).toBeTruthy();
await delay(1200);
expect(session.isActive()).toBeTruthy();
await delay(1000);
expect(session.isActive()).toBeFalsy();
session.log({ a: 2 });
// log is not possible if current session is inactive
expect(session.data).toEqual({ a: 1 });
});
});
================================================
FILE: packages/designer/tests/document/history/__snapshots__/history.test.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`History data function & records 1`] = `"{\\"data\\":1,\\"children\\":[{\\"data\\":2,\\"children\\":[]}]}"`;
exports[`History data function & records 2`] = `"{\\"data\\":3,\\"children\\":[{\\"data\\":2,\\"children\\":[]}]}"`;
exports[`History data function & records 3`] = `"{\\"data\\":5,\\"children\\":[{\\"data\\":2,\\"children\\":[]}]}"`;
exports[`History data function & records 4`] = `"{\\"data\\":7,\\"children\\":[{\\"data\\":2,\\"children\\":[]}]}"`;
================================================
FILE: packages/designer/tests/document/node/modal-nodes-manager.test.ts
================================================
import '../../fixtures/window';
import { Editor } from '@alilc/lowcode-editor-core';
import { Project } from '../../../src/project/project';
import { DocumentModel } from '../../../src/document/document-model';
import { Node } from '../../../src/document/node/node';
import { Designer } from '../../../src/designer/designer';
import formSchema from '../../fixtures/schema/form-with-modal';
import dlgMetadata from '../../fixtures/component-metadata/dialog';
import { getModalNodes } from '../../../src/document/node/modal-nodes-manager';
import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory';
let editor: Editor;
let designer: Designer;
let project: Project;
let doc: DocumentModel;
beforeEach(() => {
editor = new Editor();
designer = new Designer({ editor, shellModelFactory });
designer.createComponentMeta(dlgMetadata);
project = designer.project;
doc = new DocumentModel(project, formSchema);
});
afterEach(() => {
project.unload();
designer.purge();
editor = null;
designer = null;
project = null;
});
describe('ModalNodesManager 方法测试', () => {
it('getModalNodes / getVisibleModalNode', () => {
const mgr = doc.modalNodesManager;
const nodes = mgr.getModalNodes();
expect(nodes).toHaveLength(1);
expect(nodes[0].componentName).toBe('Dialog');
expect(mgr.getVisibleModalNode()).toBeFalsy();
});
it('setVisible / setInvisible / onVisibleChange', () => {
const mgr = doc.modalNodesManager;
const nodes = mgr.getModalNodes();
const mockFn = jest.fn();
const off = mgr.onVisibleChange(mockFn);
mgr.setVisible(nodes[0]);
expect(mockFn).toHaveBeenCalledTimes(2);
mgr.setInvisible(nodes[0]);
expect(mockFn).toHaveBeenCalledTimes(3);
off();
});
it('addNode / removeNode', () => {
const mgr = doc.modalNodesManager!;
const nodes = mgr.getModalNodes();
const nodesMockFn = jest.fn();
const visibleMockFn = jest.fn();
const off = mgr.onModalNodesChange(nodesMockFn);
const offVisible = mgr.onVisibleChange(visibleMockFn);
const newNode = new Node(doc, { componentName: 'Dialog' });
mgr.addNode(newNode);
expect(visibleMockFn).toHaveBeenCalledTimes(2);
expect(nodesMockFn).toHaveBeenCalledTimes(1);
mgr.setVisible(newNode);
mgr.removeNode(newNode);
expect(visibleMockFn).toHaveBeenCalledTimes(6);
expect(nodesMockFn).toHaveBeenCalledTimes(2);
off();
offVisible();
visibleMockFn.mockClear();
nodesMockFn.mockClear();
mgr.addNode(newNode);
expect(visibleMockFn).not.toHaveBeenCalled();
expect(nodesMockFn).not.toHaveBeenCalled();
const newNode2 = new Node(doc, { componentName: 'Dialog' });
mgr.addNode(newNode2);
mgr.setInvisible(newNode2);
mgr.removeNode(newNode2);
const newNode3 = new Node(doc, { componentName: 'Dialog' });
mgr.removeNode(newNode3);
const newNode4 = new Node(doc, { componentName: 'Non-Modal' });
mgr.removeNode(newNode4);
const newNode5 = doc.createNode({ componentName: 'Non-Modal' });
newNode5.remove(); // trigger node destroy
});
});
describe('其他方法', () => {
it('getModalNodes - null', () => {
expect(getModalNodes()).toEqual([]);
});
it('getModalNodes - no children', () => {
const node = doc.createNode({ componentName: 'Leaf', children: 'haha' });
expect(getModalNodes(node)).toEqual([]);
});
});
================================================
FILE: packages/designer/tests/document/node/node-children.test.ts
================================================
import '../../fixtures/window';
import { Editor } from '@alilc/lowcode-editor-core';
import { Project } from '../../../src/project/project';
import { DocumentModel } from '../../../src/document/document-model';
import {
Node,
} from '../../../src/document/node/node';
import { Designer } from '../../../src/designer/designer';
import formSchema from '../../fixtures/schema/form';
import divMetadata from '../../fixtures/component-metadata/div';
import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory';
describe('NodeChildren 方法测试', () => {
let editor: Editor;
let designer: Designer;
let project: Project;
let doc: DocumentModel;
beforeEach(() => {
editor = new Editor();
designer = new Designer({ editor, shellModelFactory });
project = designer.project;
doc = new DocumentModel(project, formSchema);
});
afterEach(() => {
project.unload();
designer.purge();
editor = null;
designer = null;
project = null;
});
it('isEmpty / notEmpty', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const secondBtn = doc.getNode('node_k1ow3cbp')!;
const { children } = firstBtn.parent!;
expect(children.isEmpty()).toBeFalsy();
expect(children.notEmpty()).toBeTruthy();
expect(firstBtn.children.notEmpty()).toBeFalsy();
expect(firstBtn.children.isEmpty()).toBeTruthy();
});
it('export', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
expect(children.export().length).toBe(2);
});
it('export - Leaf', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
firstBtn.parent!.insertAfter({ componentName: 'Leaf', children: 'haha' });
const { children } = firstBtn.parent!;
expect(children.export().length).toBe(3);
expect(children.export()[2]).toBe('haha');
});
it('import', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
children.import(children.export());
expect(children.export().length).toBe(2);
});
it('delete', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const leafNode = doc.createNode({ componentName: 'Leaf', children: 'haha' });
firstBtn.parent!.insertAfter(leafNode);
const { children } = firstBtn.parent!;
children.delete(leafNode);
expect(children.export().length).toBe(2);
});
it('delete - 插入已有的节点', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
firstBtn.parent!.insertBefore(firstBtn, firstBtn);
const { children } = firstBtn.parent!;
expect(children.export().length).toBe(2);
});
it('purge / for of', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
children.purge();
for (const child of children) {
expect(child.isPurged).toBeTruthy();
}
// purge when children is purged
children.purge();
});
it('splice', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
children.splice(0, 1);
expect(children.size).toBe(1);
expect(children.length).toBe(1);
children.splice(0, 0, doc.createNode({ componentName: 'Button' }));
expect(children.size).toBe(2);
expect(children.length).toBe(2);
});
it('map', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
const newMap = children.map((item) => item);
newMap?.forEach((item) => {
expect(item.componentName).toBe('Button');
});
});
it('forEach', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
children.forEach((item) => {
expect(item.componentName).toBe('Button');
});
});
it('some', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
children.some((item) => {
return expect(item.componentName).toBe('Button');
});
});
it('every', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
children.every((item) => {
return expect(item.componentName).toBe('Button');
});
});
it('filter', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
children
.filter((item) => item.componentName === 'Button')
.forEach((item) => {
expect(item.componentName).toBe('Button');
});
});
it('find', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
const found = children.find((item) => item.componentName === 'Button');
expect(found?.componentName).toBe('Button');
});
it('concat', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
const ret = children.concat([doc.createNode({ componentName: 'Button' })]);
expect(ret.length).toBe(3);
});
it('reduce', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
let ret = 0;
ret = children.reduce((count, node) => {
count = count + 1;
return count;
}, 0);
expect(ret).toBe(2);
});
it('mergeChildren', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
const changeMockFn = jest.fn();
const offChange = children.onChange(changeMockFn);
const rmMockFn = jest.fn((item) => {
if (item.index === 1) return true;
return false;
});
const addMockFn = jest.fn((children) => {
return [{ componentName: 'Button' }, { componentName: 'Button' }];
});
const sortMockFn = jest.fn((a, b) => {
return a > b ? 1 : -1;
});
children.mergeChildren(rmMockFn, addMockFn, sortMockFn);
expect(children.size).toBe(3);
expect(changeMockFn).toHaveBeenCalled();
offChange();
// no remover && adder && sorter
children.mergeChildren();
});
it('insert / onInsert', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
const mockFn = jest.fn();
const off = children.onInsert(mockFn);
children.insert(new Node(doc, { componentName: 'Button' }));
expect(mockFn).toHaveBeenCalledTimes(1);
off();
children.insert(new Node(doc, { componentName: 'Button' }));
expect(mockFn).toHaveBeenCalledTimes(1);
});
it('reportModified', () => {
const divMeta = designer.createComponentMeta(divMetadata);
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const { children } = firstBtn.parent!;
const modifiedMockFn = jest.fn();
divMeta.getMetadata = () => {
return { configure: { advanced: { callbacks: { onSubtreeModified: modifiedMockFn } } } };
};
children.reportModified(null);
children.reportModified(doc.rootNode);
children.reportModified(firstBtn, firstBtn.parent);
expect(modifiedMockFn).toHaveBeenCalled();
});
});
================================================
FILE: packages/designer/tests/document/node/node.add.test.ts
================================================
import set from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';
import '../../fixtures/window';
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';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
return {
getComponentMeta() {
return {
getMetadata() {
return { configure: { advanced: null } };
},
get advanced() {
return {};
},
};
},
transformProps(props) { return props; },
createSettingEntry: mockCreateSettingEntry,
postEvent() {},
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({});
});
describe('schema 生成节点模型测试', () => {
describe('block ❌ | component ❌ | slot ❌', () => {
let project: IProject;
beforeEach(() => {
project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
});
afterEach(() => {
project.unload();
});
it('基本的节点模型初始化,模型导出', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const nodesMap = currentDocument?.nodesMap;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
expect(nodesMap?.size).toBe(expectedNodeCnt);
ids.forEach(id => {
expect(nodesMap?.get(id)?.componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName);
});
const pageNode = currentDocument?.getNode('page');
expect(pageNode?.getComponentName()).toBe('Page');
expect(pageNode?.getIcon()).toBeUndefined();
const exportSchema = currentDocument?.export(1);
expect(getIdsFromSchema(exportSchema).length).toBe(expectedNodeCnt);
nodesMap.forEach(node => {
// 触发 getter
node.settingEntry;
});
expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt);
});
it('基本的节点模型初始化,节点深度', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
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');
expect(pageNode?.zLevel).toBe(0);
expect(rootHeaderNode?.zLevel).toBe(1);
expect(rootContentNode?.zLevel).toBe(1);
expect(rootFooterNode?.zLevel).toBe(1);
expect(formNode?.zLevel).toBe(2);
expect(cardNode?.zLevel).toBe(3);
expect(cardContentNode?.zLevel).toBe(4);
expect(columnsLayoutNode?.zLevel).toBe(5);
expect(columnNode?.zLevel).toBe(6);
expect(textFieldNode?.zLevel).toBe(7);
expect(textFieldNode?.getZLevelTop(7)).toEqual(textFieldNode);
expect(textFieldNode?.getZLevelTop(6)).toEqual(columnNode);
expect(textFieldNode?.getZLevelTop(5)).toEqual(columnsLayoutNode);
expect(textFieldNode?.getZLevelTop(4)).toEqual(cardContentNode);
expect(textFieldNode?.getZLevelTop(3)).toEqual(cardNode);
expect(textFieldNode?.getZLevelTop(2)).toEqual(formNode);
expect(textFieldNode?.getZLevelTop(1)).toEqual(rootContentNode);
expect(textFieldNode?.getZLevelTop(0)).toEqual(pageNode);
// 异常情况
expect(textFieldNode?.getZLevelTop(8)).toBeNull();
expect(textFieldNode?.getZLevelTop(-1)).toBeNull();
});
it('基本的节点模型初始化,节点父子、兄弟相关方法', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
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');
expect(pageNode?.index).toBe(-1);
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);
expect(rootFooterNode?.index).toBe(2);
expect(textFieldNode?.getParent()).toBe(columnNode);
expect(columnNode?.getParent()).toBe(columnsLayoutNode);
expect(columnsLayoutNode?.getParent()).toBe(cardContentNode);
expect(cardContentNode?.getParent()).toBe(cardNode);
expect(cardNode?.getParent()).toBe(formNode);
expect(formNode?.getParent()).toBe(rootContentNode);
expect(rootContentNode?.getParent()).toBe(pageNode);
expect(rootContentNode?.prevSibling).toBe(rootHeaderNode);
expect(rootContentNode?.nextSibling).toBe(rootFooterNode);
expect(pageNode?.isRoot()).toBe(true);
expect(pageNode?.contains(textFieldNode)).toBe(true);
expect(textFieldNode?.getRoot()).toBe(pageNode);
expect(columnNode?.getRoot()).toBe(pageNode);
expect(columnsLayoutNode?.getRoot()).toBe(pageNode);
expect(cardContentNode?.getRoot()).toBe(pageNode);
expect(cardNode?.getRoot()).toBe(pageNode);
expect(formNode?.getRoot()).toBe(pageNode);
expect(rootContentNode?.getRoot()).toBe(pageNode);
});
it('基本的节点模型初始化,节点新建、删除等事件', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const getNode = currentDocument?.getNode.bind(currentDocument);
const createNode = currentDocument?.createNode.bind(currentDocument);
const pageNode = getNode?.('page');
const nodeCreateHandler = jest.fn();
const offCreate = currentDocument?.onNodeCreate(nodeCreateHandler);
const node = createNode?.({
componentName: 'TextInput',
props: {
propA: 'haha',
},
});
pageNode && node && currentDocument?.insertNode(pageNode, node);
expect(nodeCreateHandler).toHaveBeenCalledTimes(1);
expect(nodeCreateHandler.mock.calls[0][0]).toBe(node);
expect(nodeCreateHandler.mock.calls[0][0].componentName).toBe('TextInput');
expect(nodeCreateHandler.mock.calls[0][0].getPropValue('propA')).toBe('haha');
const nodeDestroyHandler = jest.fn();
const offDestroy = currentDocument?.onNodeDestroy(nodeDestroyHandler);
node?.remove();
expect(nodeDestroyHandler).toHaveBeenCalledTimes(1);
expect(nodeDestroyHandler.mock.calls[0][0]).toBe(node);
expect(nodeDestroyHandler.mock.calls[0][0].componentName).toBe('TextInput');
expect(nodeDestroyHandler.mock.calls[0][0].getPropValue('propA')).toBe('haha');
offCreate();
offDestroy();
});
it.skip('基本的节点模型初始化,节点插入等方法', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const getNode = currentDocument.getNode.bind(currentDocument);
const formNode = getNode('form');
const node1 = currentDocument.createNode({
componentName: 'TextInput',
props: {
propA: 'haha',
},
});
const node2 = currentDocument.createNode({
componentName: 'TextInput',
props: {
propA: 'heihei',
},
});
const node3 = currentDocument.createNode({
componentName: 'TextInput',
props: {
propA: 'heihei2',
},
});
const node4 = currentDocument.createNode({
componentName: 'TextInput',
props: {
propA: 'heihei3',
},
});
formNode?.insertBefore(node2);
// formNode?.insertBefore(node1, node2);
// formNode?.insertAfter(node3);
// formNode?.insertAfter(node4, node3);
expect(formNode?.children?.get(0)).toBe(node1);
expect(formNode?.children?.get(1)).toBe(node2);
// expect(formNode?.children?.get(5)).toBe(node3);
// expect(formNode?.children?.get(6)).toBe(node4);
});
it('基本的节点模型初始化,节点其他方法', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const getNode = currentDocument.getNode.bind(currentDocument);
const pageNode = getNode('page');
expect(pageNode?.isPage()).toBe(true);
expect(pageNode?.isComponent()).toBe(false);
expect(pageNode?.isSlot()).toBe(false);
expect(pageNode?.title).toBe('hey, i\' a page!');
});
describe('节点新增(insertNode)', () => {
let project: Project;
beforeEach(() => {
project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
});
it('场景一:插入 NodeSchema,不指定 index', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form') as Node;
const formNode2 = currentDocument?.getNode('form');
expect(formNode).toEqual(formNode2);
currentDocument?.insertNode(formNode, {
componentName: 'TextInput',
id: 'nodeschema-id1',
props: {
propA: 'haha',
propB: 3,
},
});
expect(nodesMap.size).toBe(ids.length + 1);
expect(formNode.children?.length).toBe(4);
const insertedNode = formNode.children.get(formNode.children.length - 1);
expect(insertedNode.componentName).toBe('TextInput');
expect(insertedNode.propsData).toEqual({
propA: 'haha',
propB: 3,
});
// TODO: 把 checkId 的 commit pick 过来
// expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput');
});
it('场景一:插入 NodeSchema,指定 index: 0', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form');
formNode && currentDocument?.insertNode(formNode, {
componentName: 'TextInput',
id: 'nodeschema-id1',
props: {
propA: 'haha',
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({
propA: 'haha',
propB: 3,
});
// TODO: 把 checkId 的 commit pick 过来
// expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput');
});
it('场景一:插入 NodeSchema,指定 index: 1', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form');
formNode && currentDocument?.insertNode(formNode, {
componentName: 'TextInput',
id: 'nodeschema-id1',
props: {
propA: 'haha',
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({
propA: 'haha',
propB: 3,
});
// TODO: 把 checkId 的 commit pick 过来
// expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput');
});
it('场景一:插入 NodeSchema,有 children', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form') as INode;
currentDocument?.insertNode(formNode, {
componentName: 'ParentNode',
props: {
propA: 'haha',
propB: 3,
},
children: [
{
componentName: 'SubNode',
props: {
propA: 'haha',
propB: 3,
},
},
{
componentName: 'SubNode2',
props: {
propA: 'haha',
propB: 3,
},
},
],
});
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');
});
it.skip('场景一:插入 NodeSchema,id 与现有 schema 里的 id 重复', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form');
formNode && currentDocument?.insertNode(formNode, {
componentName: 'TextInput',
id: 'nodeschema-id1',
props: {
propA: 'haha',
propB: 3,
},
});
expect(nodesMap?.get('nodeschema-id1')?.componentName).toBe('TextInput');
expect(nodesMap?.size).toBe(ids.length + 1);
});
it.skip('场景一:插入 NodeSchema,id 与现有 schema 里的 id 重复,但关闭了 id 检测器', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form');
formNode && currentDocument?.insertNode(formNode, {
componentName: 'TextInput',
id: 'nodeschema-id1',
props: {
propA: 'haha',
propB: 3,
},
});
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?.nodesMap;
const formNode = nodesMap?.get('form');
const inputNode = currentDocument?.createNode({
componentName: 'TextInput',
id: 'nodeschema-id2',
props: {
propA: 'haha',
propB: 3,
},
});
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?.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(formNode.children?.get(3)?.componentName).toBe('Leaf');
// expect(formNode.children?.get(3)?.children).toEqual({
// type: 'JSExpression',
// value: 'just a expression'
// });
});
it('场景四:插入 string', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
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(formNode.children?.get(3)?.componentName).toBe('Leaf');
// expect(formNode.children?.get(3)?.children).toBe('just a string');
});
});
describe('节点新增(insertNodes)', () => {
let project: Project;
beforeEach(() => {
project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
});
it('场景一:插入 NodeSchema,指定 index', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form') as Node;
const formNode2 = currentDocument?.getNode('form');
expect(formNode).toEqual(formNode2);
currentDocument?.insertNodes(formNode, [
{
componentName: 'TextInput',
props: {
propA: 'haha2',
propB: 3,
},
},
{
componentName: 'TextInput2',
props: {
propA: 'haha',
propB: 3,
},
},
], 1);
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({
propA: 'haha2',
propB: 3,
});
expect(insertedNode2?.componentName).toBe('TextInput2');
expect(insertedNode2?.propsData).toEqual({
propA: 'haha',
propB: 3,
});
});
it.only('场景二:插入 Node 实例,指定 index', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const nodesMap = currentDocument?.nodesMap;
const formNode = nodesMap?.get('form') as INode;
const formNode2 = currentDocument?.getNode('form');
expect(formNode).toEqual(formNode2);
const createdNode1 = currentDocument?.createNode({
componentName: 'TextInput',
props: {
propA: 'haha2',
propB: 3,
},
});
const createdNode2 = currentDocument?.createNode({
componentName: 'TextInput2',
props: {
propA: 'haha',
propB: 3,
},
});
currentDocument?.insertNodes(formNode, [createdNode1, createdNode2], 1);
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({
propA: 'haha2',
propB: 3,
});
expect(insertedNode2?.componentName).toBe('TextInput2');
expect(insertedNode2?.propsData).toEqual({
propA: 'haha',
propB: 3,
});
});
});
});
describe('block ❌ | component ❌ | slot ✅', () => {
it('基本的 slot 创建', () => {
const formSchemaWithSlot = set(cloneDeep(formSchema), 'children[0].children[0].props.title.type', 'JSSlot');
const project = new Project(designer, {
componentsTree: [
formSchemaWithSlot,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const nodesMap = currentDocument?.nodesMap;
const ids = getIdsFromSchema(formSchema);
// 目前每个 slot 会新增(1 + children.length)个节点
const expectedNodeCnt = ids.length + 2;
expect(nodesMap?.size).toBe(expectedNodeCnt);
// PageHeader
expect(nodesMap?.get('node_k1ow3cbd')?.slots).toHaveLength(1);
});
});
});
================================================
FILE: packages/designer/tests/document/node/node.dragdrop.test.ts
================================================
import '../../fixtures/window';
import { Project } from '../../../src/project/project';
import { Designer } from '../../../src/designer/designer';
import formSchema from '../../fixtures/schema/form';
import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
return {
getComponentMeta() {
return {
getMetadata() {
return { configure: { advanced: null } };
},
};
},
transformProps(props) { return props; },
createSettingEntry: mockCreateSettingEntry,
postEvent() {},
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({});
});
describe.skip('节点拖拽测试', () => {
describe('block ❌ | component ❌ | slot ❌', () => {
it('修改普通属性,string | number', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
expect(nodesMap.size).toBe(expectedNodeCnt);
ids.forEach(id => {
expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName);
});
const exportSchema = currentDocument?.export(1);
expect(getIdsFromSchema(exportSchema).length).toBe(expectedNodeCnt);
expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt);
});
});
});
================================================
FILE: packages/designer/tests/document/node/node.modify.test.ts
================================================
import '../../fixtures/window';
import { Project } from '../../../src/project/project';
import { Designer } from '../../../src/designer/designer';
import formSchema from '../../fixtures/schema/form';
import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
return {
getComponentMeta() {
return {
getMetadata() {
return { configure: { advanced: null } };
},
get advanced() {
return {};
},
};
},
transformProps(props) { return props; },
createSettingEntry: mockCreateSettingEntry,
postEvent() {},
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({});
});
describe('schema 生成节点模型测试', () => {
describe('block ❌ | component ❌ | slot ❌', () => {
let project: Project;
beforeEach(() => {
project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
});
it('读取普通属性,string | number | object', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const formNode = currentDocument?.getNode('form');
/*
props: {
size: 'medium',
labelAlign: 'top',
autoValidate: true,
scrollToFirstError: true,
autoUnmount: true,
behavior: 'NORMAL',
dataSource: {
type: 'variable',
variable: 'state.formData',
},
obj: {
a: 1,
b: false,
c: 'string',
},
__style__: {},
fieldId: 'form',
fieldOptions: {},
},
id: 'form',
condition: true,
*/
const sizeProp = formNode?.getProp('size');
const sizeProp2 = formNode?.getProps().getProp('size');
expect(sizeProp).toBe(sizeProp2);
expect(sizeProp?.getAsString()).toBe('medium');
expect(sizeProp?.getValue()).toBe('medium');
const autoValidateProp = formNode?.getProp('autoValidate');
expect(autoValidateProp?.getValue()).toBe(true);
const objProp = formNode?.getProp('obj');
expect(objProp?.getValue()).toEqual({
a: 1,
b: false,
c: 'string',
});
const objAProp = formNode?.getProp('obj.a');
const objBProp = formNode?.getProp('obj.b');
const objCProp = formNode?.getProp('obj.c');
expect(objAProp?.getValue()).toBe(1);
expect(objBProp?.getValue()).toBe(false);
expect(objCProp?.getValue()).toBe('string');
const idProp = formNode?.getExtraProp('extraPropA');
expect(idProp?.getValue()).toBe('extraPropA');
});
it('修改普通属性,string | number | object,使用 Node 实例接口', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const formNode = currentDocument?.getNode('form');
/*
props: {
size: 'medium',
labelAlign: 'top',
autoValidate: true,
scrollToFirstError: true,
autoUnmount: true,
behavior: 'NORMAL',
dataSource: {
type: 'variable',
variable: 'state.formData',
},
obj: {
a: 1,
b: false,
c: 'string',
},
__style__: {},
fieldId: 'form',
fieldOptions: {},
},
id: 'form',
condition: true,
*/
formNode?.setPropValue('size', 'large');
const sizeProp = formNode?.getProp('size');
expect(sizeProp?.getAsString()).toBe('large');
expect(sizeProp?.getValue()).toBe('large');
formNode?.setPropValue('autoValidate', false);
const autoValidateProp = formNode?.getProp('autoValidate');
expect(autoValidateProp?.getValue()).toBe(false);
formNode?.setPropValue('obj', {
a: 2,
b: true,
c: 'another string',
});
const objProp = formNode?.getProp('obj');
expect(objProp?.getValue()).toEqual({
a: 2,
b: true,
c: 'another string',
});
formNode?.setPropValue('obj.a', 3);
formNode?.setPropValue('obj.b', false);
formNode?.setPropValue('obj.c', 'string');
const objAProp = formNode?.getProp('obj.a');
const objBProp = formNode?.getProp('obj.b');
const objCProp = formNode?.getProp('obj.c');
expect(objAProp?.getValue()).toBe(3);
expect(objBProp?.getValue()).toBe(false);
expect(objCProp?.getValue()).toBe('string');
expect(objProp?.getValue()).toEqual({
a: 3,
b: false,
c: 'string',
});
});
it('修改普通属性,string | number | object,使用 Props 实例接口', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const formNode = currentDocument?.getNode('form');
/*
props: {
size: 'medium',
labelAlign: 'top',
autoValidate: true,
scrollToFirstError: true,
autoUnmount: true,
behavior: 'NORMAL',
dataSource: {
type: 'variable',
variable: 'state.formData',
},
obj: {
a: 1,
b: false,
c: 'string',
},
__style__: {},
fieldId: 'form',
fieldOptions: {},
},
id: 'form',
condition: true,
*/
const props = formNode?.getProps();
props?.setPropValue('size', 'large');
const sizeProp = formNode?.getProp('size');
expect(sizeProp?.getAsString()).toBe('large');
expect(sizeProp?.getValue()).toBe('large');
props?.setPropValue('autoValidate', false);
const autoValidateProp = formNode?.getProp('autoValidate');
expect(autoValidateProp?.getValue()).toBe(false);
props?.setPropValue('obj', {
a: 2,
b: true,
c: 'another string',
});
const objProp = formNode?.getProp('obj');
expect(objProp?.getValue()).toEqual({
a: 2,
b: true,
c: 'another string',
});
props?.setPropValue('obj.a', 3);
props?.setPropValue('obj.b', false);
props?.setPropValue('obj.c', 'string');
const objAProp = formNode?.getProp('obj.a');
const objBProp = formNode?.getProp('obj.b');
const objCProp = formNode?.getProp('obj.c');
expect(objAProp?.getValue()).toBe(3);
expect(objBProp?.getValue()).toBe(false);
expect(objCProp?.getValue()).toBe('string');
expect(objProp?.getValue()).toEqual({
a: 3,
b: false,
c: 'string',
});
});
it('修改普通属性,string | number | object,使用 Prop 实例接口', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const formNode = currentDocument?.getNode('form');
/*
props: {
size: 'medium',
labelAlign: 'top',
autoValidate: true,
scrollToFirstError: true,
autoUnmount: true,
behavior: 'NORMAL',
dataSource: {
type: 'variable',
variable: 'state.formData',
},
obj: {
a: 1,
b: false,
c: 'string',
},
__style__: {},
fieldId: 'form',
fieldOptions: {},
},
id: 'form',
condition: true,
*/
const sizeProp = formNode?.getProp('size');
sizeProp?.setValue('large');
expect(sizeProp?.getAsString()).toBe('large');
expect(sizeProp?.getValue()).toBe('large');
const autoValidateProp = formNode?.getProp('autoValidate');
autoValidateProp?.setValue(false);
expect(autoValidateProp?.getValue()).toBe(false);
const objProp = formNode?.getProp('obj');
objProp?.setValue({
a: 2,
b: true,
c: 'another string',
});
expect(objProp?.getValue()).toEqual({
a: 2,
b: true,
c: 'another string',
});
const objAProp = formNode?.getProp('obj.a');
const objBProp = formNode?.getProp('obj.b');
const objCProp = formNode?.getProp('obj.c');
objAProp?.setValue(3);
objBProp?.setValue(false);
objCProp?.setValue('string');
expect(objAProp?.getValue()).toBe(3);
expect(objBProp?.getValue()).toBe(false);
expect(objCProp?.getValue()).toBe('string');
expect(objProp?.getValue()).toEqual({
a: 3,
b: false,
c: 'string',
});
});
});
describe('block ❌ | component ❌ | slot ✅', () => {
let project: Project;
beforeEach(() => {
project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
});
it('修改 slot 属性,初始存在 slot 属性名,正常生成节点模型', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const formNode = currentDocument?.getNode('form');
formNode?.setPropValue('slotA', {
type: 'JSSlot',
value: [{
componentName: 'TextInput1',
props: {
txt: 'haha',
num: 1,
bool: true,
},
}, {
componentName: 'TextInput2',
props: {
txt: 'heihei',
num: 2,
bool: false,
},
}],
});
expect(nodesMap.size).toBe(ids.length + 3);
expect(formNode?.slots).toHaveLength(1);
expect(formNode?.slots[0].children).toHaveLength(2);
const firstChildNode = formNode?.slots[0].children?.get(0);
const secondChildNode = formNode?.slots[0].children?.get(1);
expect(firstChildNode?.componentName).toBe('TextInput1');
expect(firstChildNode?.getPropValue('txt')).toBe('haha');
expect(firstChildNode?.getPropValue('num')).toBe(1);
expect(firstChildNode?.getPropValue('bool')).toBe(true);
expect(secondChildNode?.componentName).toBe('TextInput2');
expect(secondChildNode?.getPropValue('txt')).toBe('heihei');
expect(secondChildNode?.getPropValue('num')).toBe(2);
expect(secondChildNode?.getPropValue('bool')).toBe(false);
});
it('修改 slot 属性,初始存在 slot 属性名,关闭 slot', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const formNode = currentDocument?.getNode('form');
formNode?.setPropValue('slotA', {
type: 'JSSlot',
value: [{
componentName: 'TextInput1',
props: {
txt: 'haha',
num: 1,
bool: true,
},
}, {
componentName: 'TextInput2',
props: {
txt: 'heihei',
num: 2,
bool: false,
},
}],
});
expect(nodesMap.size).toBe(ids.length + 3);
expect(formNode?.slots).toHaveLength(1);
formNode?.setPropValue('slotA', '');
expect(nodesMap.size).toBe(ids.length);
expect(formNode?.slots).toHaveLength(0);
});
it('修改 slot 属性,初始存在 slot 属性名,同名覆盖 slot', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const formNode = currentDocument?.getNode('form');
formNode?.setPropValue('slotA', {
type: 'JSSlot',
name: 'slotA',
value: [{
componentName: 'TextInput1',
props: {
txt: 'haha',
num: 1,
bool: true,
},
}, {
componentName: 'TextInput2',
props: {
txt: 'heihei',
num: 2,
bool: false,
},
}],
});
expect(nodesMap.size).toBe(ids.length + 3);
expect(formNode?.slots).toHaveLength(1);
expect(formNode?.slots[0].children).toHaveLength(2);
let firstChildNode = formNode?.slots[0].children?.get(0);
expect(firstChildNode?.componentName).toBe('TextInput1');
expect(firstChildNode?.getPropValue('txt')).toBe('haha');
expect(firstChildNode?.getPropValue('num')).toBe(1);
expect(firstChildNode?.getPropValue('bool')).toBe(true);
formNode?.setPropValue('slotA', {
type: 'JSSlot',
name: 'slotA',
value: [{
componentName: 'TextInput3',
props: {
txt: 'xixi',
num: 3,
bool: false,
},
}],
});
expect(nodesMap.size).toBe(ids.length + 2);
expect(formNode?.slots).toHaveLength(1);
expect(formNode?.slots[0].children).toHaveLength(1);
firstChildNode = formNode?.slots[0].children?.get(0);
expect(firstChildNode?.componentName).toBe('TextInput3');
expect(firstChildNode?.getPropValue('txt')).toBe('xixi');
expect(firstChildNode?.getPropValue('num')).toBe(3);
expect(firstChildNode?.getPropValue('bool')).toBe(false);
});
});
});
================================================
FILE: packages/designer/tests/document/node/node.remove.test.ts
================================================
import set from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';
import '../../fixtures/window';
import { Project } from '../../../src/project/project';
import { Designer } from '../../../src/designer/designer';
import formSchema from '../../fixtures/schema/form';
import { getIdsFromSchema } from '../../utils';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
return {
getComponentMeta() {
return {
getMetadata() {
return { configure: { advanced: null } };
},
get advanced() {
return {};
},
};
},
transformProps(props) { return props; },
createSettingEntry: mockCreateSettingEntry,
postEvent() {},
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({});
});
describe('节点模型删除测试', () => {
it('删除叶子节点', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const originalNodeCnt = ids.length;
expect(nodesMap.size).toBe(originalNodeCnt);
currentDocument?.removeNode('node_k1ow3cbn');
// Button#1
expect(nodesMap.size).toBe(originalNodeCnt - 1);
currentDocument?.removeNode(nodesMap.get('node_k1ow3cbp'));
// Button#2
expect(nodesMap.size).toBe(originalNodeCnt - 2);
currentDocument?.removeNode('unexisting_node');
expect(nodesMap.size).toBe(originalNodeCnt - 2);
});
it('删除叶子节点,带有 slot', () => {
const formSchemaWithSlot = set(cloneDeep(formSchema),
'children[1].children[0].children[2].children[1].props.greeting.type', 'JSSlot');
const project = new Project(designer, {
componentsTree: [
formSchemaWithSlot,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const originalNodeCnt = ids.length + 2;
expect(nodesMap.size).toBe(originalNodeCnt);
currentDocument?.removeNode('node_k1ow3cbp');
// Button + Slot + Text
expect(nodesMap.size).toBe(originalNodeCnt - 3);
});
it('删除分支节点', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const originalNodeCnt = ids.length;
expect(nodesMap.size).toBe(originalNodeCnt);
currentDocument?.removeNode('node_k1ow3cbo');
// Div + 2 * Button
expect(nodesMap.size).toBe(originalNodeCnt - 3);
});
it('删除分支节点,带有 slot', () => {
const formSchemaWithSlot = set(cloneDeep(formSchema),
'children[1].children[0].children[2].children[1].props.greeting.type', 'JSSlot');
const project = new Project(designer, {
componentsTree: [
formSchemaWithSlot,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const originalNodeCnt = ids.length + 2;
expect(nodesMap.size).toBe(originalNodeCnt);
currentDocument?.removeNode('node_k1ow3cbo');
// Div + 2 * Button + Slot + Text
expect(nodesMap.size).toBe(originalNodeCnt - 5);
});
});
================================================
FILE: packages/designer/tests/document/node/node.test.ts
================================================
// @ts-nocheck
import '../../fixtures/window';
import { set } from '../../utils';
import {
Editor,
globalContext,
Setters as InnerSetters,
} from '@alilc/lowcode-editor-core';
import { Project } from '../../../src/project/project';
import { Workspace as InnerWorkspace } from '@alilc/lowcode-workspace';
import { DocumentModel } from '../../../src/document/document-model';
import {
isRootNode,
Node,
comparePosition,
contains,
PositionNO,
} from '../../../src/document/node/node';
import { Designer } from '../../../src/designer/designer';
import formSchema from '../../fixtures/schema/form';
import divMetadata from '../../fixtures/component-metadata/div';
import dialogMetadata from '../../fixtures/component-metadata/dialog';
import btnMetadata from '../../fixtures/component-metadata/button';
import formMetadata from '../../fixtures/component-metadata/form';
import pageMetadata from '../../fixtures/component-metadata/page';
import rootHeaderMetadata from '../../fixtures/component-metadata/root-header';
import rootContentMetadata from '../../fixtures/component-metadata/root-content';
import rootFooterMetadata from '../../fixtures/component-metadata/root-footer';
import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory';
import { isNode } from '@alilc/lowcode-utils';
import { Setters } from '@alilc/lowcode-shell';
describe('Node 方法测试', () => {
let editor: Editor;
let designer: Designer;
let project: Project;
let doc: DocumentModel;
beforeEach(() => {
editor = new Editor();
designer = new Designer({ editor, shellModelFactory });
project = designer.project;
doc = new DocumentModel(project, formSchema);
editor.set('setters', new Setters(new InnerSetters()));
!globalContext.has(Editor) && globalContext.register(editor, Editor);
!globalContext.has('workspace') && globalContext.register(new InnerWorkspace(), 'workspace');
});
afterEach(() => {
project.unload();
designer.purge();
editor = null;
designer = null;
project = null;
});
// Case 1: When children is null
test('initialChildren returns result of initialChildren function when children is null ', () => {
const node = new Node(doc, { componentName: 'Button', props: { a: 1 } });
const result = node.initialChildren(null);
// 预期结果是一个空数组
expect(result).toEqual([]);
});
// Case 2: When children is undefined
test('initialChildren returns result of initialChildren function when children is null ', () => {
const node = new Node(doc, { componentName: 'Button', props: { a: 1 } });
const result = node.initialChildren(undefined);
// 预期结果是一个空数组
expect(result).toEqual([]);
});
// Case 3: When children is array
test('initialChildren returns result of initialChildren function when children is null ', () => {
const node = new Node(doc, { componentName: 'Button', props: { a: 1 } });
const childrenArray = [{ id: 1, name: 'Child 1' }, { id: 2, name: 'Child 2' }];
const result = node.initialChildren(childrenArray);
// 预期结果是一个数组
expect(result).toEqual(childrenArray);
});
// Case 4: When children is not null and not an array
test('initialChildren returns result of initialChildren function when children is null ', () => {
const node = new Node(doc, { componentName: 'Button', props: { a: 1 } });
const childObject = { id: 1, name: 'Child 1' };
const result = node.initialChildren(childObject);
// 预期结果是一个数组
expect(result).toEqual([childObject]);
});
// Case 5: When children 0
test('initialChildren returns result of initialChildren function when children is null ', () => {
const node = new Node(doc, { componentName: 'Button', props: { a: 1 } });
const childObject = 0;
const result = node.initialChildren(childObject);
// 预期结果是一个数组
expect(result).toEqual([0]);
});
// Case 6: When children false
test('initialChildren returns result of initialChildren function when children is null ', () => {
const node = new Node(doc, { componentName: 'Button', props: { a: 1 } });
const childObject = false;
const result = node.initialChildren(childObject);
// 预期结果是一个数组
expect(result).toEqual([false]);
});
it('condition group', () => { });
it('getExtraProp / setExtraProp', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
expect(firstBtn.getExtraProp('non-existing', false)).toBeNull();
firstBtn.setExtraProp('xxx', '1111');
expect(firstBtn.getExtraProp('xxx', false).getValue()).toBe('1111');
});
it('import(leaf)', () => {
const form = doc.getNode('node_k1ow3cbo');
form.insert({ componentName: 'Leaf', children: '111' });
const leaf = form.getChildren().get(2);
expect(leaf.getPropValue('children')).toBe('111');
leaf.import({ componentName: 'Leaf', children: '222' });
expect(leaf.getPropValue('children')).toBe('222');
leaf.import({ componentName: 'Leaf', children: { type: 'JSExpression', value: 'state.x' } });
expect(leaf.getPropValue('children')).toEqual({ type: 'JSExpression', value: 'state.x' });
});
it('hasCondition', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
firstBtn.getExtraProp('condition')?.setValue(undefined);
expect(firstBtn.hasCondition()).toBeFalsy();
firstBtn.getExtraProp('condition')?.setValue(null);
expect(firstBtn.hasCondition()).toBeFalsy();
firstBtn.getExtraProp('condition')?.setValue(true);
expect(firstBtn.hasCondition()).toBeFalsy();
firstBtn.getExtraProp('condition')?.setValue('');
expect(firstBtn.hasCondition()).toBeFalsy();
firstBtn.getExtraProp('condition')?.setValue(1);
expect(firstBtn.hasCondition()).toBeTruthy();
firstBtn.getExtraProp('condition')?.setValue(false);
expect(firstBtn.hasCondition()).toBeTruthy();
});
it('hasLoop', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
expect(firstBtn.hasLoop()).toBeFalsy();
// 这里必须用 add,因为 hasLoop 实现的跳过了 stash
firstBtn.props.add([1, 2], '___loop___');
expect(firstBtn.hasLoop()).toBeTruthy();
firstBtn.getExtraProp('loop')?.setValue({ type: 'JSExpression', value: 'state.a' });
expect(firstBtn.hasLoop()).toBeTruthy();
firstBtn.getExtraProp('loop')?.setValue(1);
expect(firstBtn.hasLoop()).toBeFalsy();
});
describe('getSuitablePlace', () => {
it('root,子节点中有容器节点', () => {
designer.createComponentMeta(pageMetadata);
designer.createComponentMeta(rootHeaderMetadata);
designer.createComponentMeta(rootContentMetadata);
designer.createComponentMeta(rootFooterMetadata);
const rootHeaderMeta = designer.getComponentMeta('RootHeader');
set(rootHeaderMeta, 'prototype.options.canDropIn', true);
let o = doc.rootNode?.getSuitablePlace(doc.getNode('form'), 1);
expect(o).toEqual({
container: doc.getNode('node_k1ow3cba'),
ref: 1,
});
set(rootHeaderMeta, 'prototype.options.canDropIn', () => true);
o = doc.rootNode?.getSuitablePlace(doc.getNode('form'), 1);
expect(o).toEqual({
container: doc.getNode('node_k1ow3cba'),
ref: 1,
});
});
it('root,直接子节点中无容器节点,自身支持放入子节点', () => {
designer.createComponentMeta(pageMetadata);
let o = doc.rootNode?.getSuitablePlace(doc.getNode('form'), 1);
const pageMeta = designer.getComponentMeta('Page');
set(pageMeta, 'prototype.options.canDropIn', () => true);
expect(o).toEqual({
container: doc.rootNode,
ref: 1,
});
set(pageMeta, 'prototype.options.canDropIn', undefined);
o = doc.rootNode?.getSuitablePlace(doc.getNode('form'), 1);
expect(o).toEqual({
container: doc.rootNode,
ref: 1,
});
set(pageMeta, 'prototype.options.canDropIn', true);
o = doc.rootNode?.getSuitablePlace(doc.getNode('form'), 1);
expect(o).toEqual({
container: doc.rootNode,
ref: 1,
});
});
it('root,子节点中无容器节点,自己也不支持放入子节点', () => {
designer.createComponentMeta(pageMetadata);
let pageMeta = designer.getComponentMeta('Page');
pageMeta = set(pageMeta, 'prototype.options.canDropIn', () => false);
let o = doc.rootNode?.getSuitablePlace(doc.getNode('form'), 1);
expect(o).toBeNull();
set(pageMeta, 'prototype.options.canDropIn', false);
o = doc.rootNode?.getSuitablePlace(doc.getNode('form'), 1);
expect(o).toBeNull();
});
it('放入模态节点', () => {
designer.createComponentMeta(pageMetadata);
designer.createComponentMeta(dialogMetadata);
const dialog = doc.createNode({ componentName: 'Dialog' });
const o = doc.rootNode?.getSuitablePlace(dialog, 1);
expect(o.container).toBe(doc.rootNode);
expect(o.ref).toBe(1);
});
it('包含 focusNode', () => {
const o = doc.rootNode?.getSuitablePlace(doc.rootNode);
expect(o.container).toBe(doc.rootNode);
});
it.skip('非 root 节点,不能放入子节点', () => {
designer.createComponentMeta(formMetadata);
designer.createComponentMeta(pageMetadata);
// form 子节点以及自身都不能放入子节点
const formMeta = designer.getComponentMeta('Form');
set(formMeta, 'prototype.options.canDropIn', false);
const pageMeta = designer.getComponentMeta('Page');
set(pageMeta, 'prototype.options.canDropIn', () => true);
const o = doc.getNode('form')!.getSuitablePlace(doc.getNode('node_k1ow3cbj'), { index: 1 });
expect(o).toEqual({
container: doc.rootNode,
ref: { index: 1 },
});
});
it('非 root 节点,能放入子节点', () => {
designer.createComponentMeta(formMetadata);
designer.createComponentMeta(pageMetadata);
// form 子节点以及自身都不能放入子节点
const formMeta = designer.getComponentMeta('Form');
set(formMeta, 'prototype.options.canDropIn', true);
const o = doc.getNode('form')!.getSuitablePlace(doc.getNode('node_k1ow3cbj'), 1);
expect(o).toEqual({
container: doc.getNode('form'),
ref: 1,
});
});
it('null', () => {
expect(
doc.rootNode?.getSuitablePlace.call({
contains: () => false,
isContainer: () => false,
isRoot: () => false,
}),
).toBeNull();
});
});
it('removeChild / replaceWith / replaceChild', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const form = doc.getNode('node_k1ow3cbo');
// 不符合条件的节点直接返回
expect(firstBtn.replaceChild(form, { componentName: 'Button', props: { x: 1 } })).toBe(form);
firstBtn.select();
firstBtn.parent?.replaceChild(firstBtn, { componentName: 'Button', props: { x: 1 } });
expect(firstBtn.parent?.getChildren()?.size).toBe(2);
expect(firstBtn.parent?.getChildren()?.get(0)?.getPropValue('x')).toBe(1);
const secondBtn = doc.getNode('node_k1ow3cbp')!;
secondBtn.replaceWith({ componentName: 'Button', props: { y: 1 } });
expect(firstBtn.parent?.getChildren()?.size).toBe(2);
expect(firstBtn.parent?.getChildren()?.get(1)?.getPropValue('y')).toBe(1);
});
it('schema', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const schema = firstBtn.schema;
schema.props.size = 'large';
firstBtn.schema = schema;
expect(firstBtn.getPropValue('size')).toBe('large');
});
describe('插入相关方法', () => {
it('insertBefore / onChildrenChange', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const secondBtn = doc.getNode('node_k1ow3cbp')!;
const btnParent = firstBtn.parent!;
const mockFn = jest.fn();
const off = btnParent.onChildrenChange(mockFn);
// Node 实例
btnParent.insertBefore(new Node(doc, { componentName: 'Button', props: { a: 1 } }), firstBtn);
expect(btnParent.children.get(0)?.getProps().export().props).toEqual({ a: 1 });
expect(mockFn).toHaveBeenCalledTimes(1);
// TODO: 暂时不支持,后面补上
// // NodeSchema
// btnParent.insertBefore({ componentName: 'Button', props: { b: 1 } }, firstBtn);
// expect(btnParent.children.get(0)?.getProps().export().props).toEqual({ b: 1 });
// expect(mockFn).toHaveBeenCalledTimes(2);
// // getComponentName
// btnParent.insertBefore({ getComponentName: () => 'Button', props: { c: 1 } }, firstBtn);
// expect(btnParent.children.get(0)?.getProps().export().props).toEqual({ c: 1 });
// expect(mockFn).toHaveBeenCalledTimes(3);
});
it('insertAfter / onChildrenChange', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const secondBtn = doc.getNode('node_k1ow3cbp')!;
const btnParent = firstBtn.parent!;
const mockFn = jest.fn();
const off = btnParent.onChildrenChange(mockFn);
// Node 实例
btnParent.insertAfter(new Node(doc, { componentName: 'Button', props: { a: 1 } }), firstBtn);
expect(btnParent.children.get(1)?.getProps().export().props).toEqual({ a: 1 });
expect(mockFn).toHaveBeenCalledTimes(1);
// NodeSchema
btnParent.insertAfter({ componentName: 'Button', props: { b: 1 } }, firstBtn);
expect(btnParent.children.get(1)?.getProps().export().props).toEqual({ b: 1 });
expect(mockFn).toHaveBeenCalledTimes(2);
// getComponentName
btnParent.insertAfter({ getComponentName: () => 'Button' }, firstBtn);
expect(btnParent.children.get(1)?.getProps().export().props).toEqual({});
expect(mockFn).toHaveBeenCalledTimes(3);
});
});
it('setVisible / getVisible / onVisibleChange', () => {
const mockFn = jest.fn();
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const off = firstBtn.onVisibleChange(mockFn);
firstBtn.setVisible(true);
expect(firstBtn.getVisible()).toBeTruthy();
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith(true);
firstBtn.setVisible(false);
expect(firstBtn.getVisible()).toBeFalsy();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith(false);
off();
mockFn.mockClear();
firstBtn.setVisible(true);
expect(mockFn).not.toHaveBeenCalled();
});
it('RGL / getRGL', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
firstBtn.isRGLContainer = true;
expect(firstBtn.isRGLContainer).toBeTruthy();
const rgl = firstBtn.getRGL();
expect(rgl.isContainerNode).toBeFalsy();
expect(rgl.isEmptyNode).toBeTruthy();
expect(rgl.isRGLContainerNode).toBeTruthy();
expect(rgl.isRGLNode).toBeFalsy();
expect(rgl.isRGL).toBeTruthy();
});
it('onPropChange', () => {
const mockFn = jest.fn();
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const off = firstBtn.onPropChange(mockFn);
firstBtn.setPropValue('x', 1);
expect(mockFn).toHaveBeenCalledTimes(1);
firstBtn.setPropValue('x', 2);
expect(mockFn).toHaveBeenCalledTimes(2);
off();
mockFn.mockClear();
firstBtn.setPropValue('x', 3);
expect(mockFn).not.toHaveBeenCalled();
});
it('addSlot / unlinkSlot / removeSlot', () => { });
it('setProps', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const secondBtn = doc.getNode('node_k1ow3cbp')!;
firstBtn.setProps(secondBtn.getProps());
expect(firstBtn.getProps()).toBe(secondBtn.getProps());
});
it('advanced initials / autoruns', async () => {
designer.createComponentMeta(pageMetadata);
const pageMeta = designer.getComponentMeta('Page');
const autorunMockFn = jest.fn();
set(pageMeta, '_transformedMetadata.configure.advanced.autoruns', [
{ name: 'a', autorun: autorunMockFn },
]);
const initialChildrenMockFn = jest.fn();
set(pageMeta, '_transformedMetadata.configure.advanced.initialChildren', initialChildrenMockFn);
doc.createNode({ componentName: 'Page', props: { a: 1 } });
expect(autorunMockFn).toHaveBeenCalled();
expect(initialChildrenMockFn).toHaveBeenCalled();
set(pageMeta, '_transformedMetadata.configure.advanced.initialChildren', {});
doc.createNode({ componentName: 'Page', props: { a: 1 } });
expect(autorunMockFn).toHaveBeenCalledTimes(2);
});
it('isValidComponent', () => {
designer.createComponentMeta(divMetadata);
expect(doc.getNode('node_k1ow3cbo')?.isValidComponent()).toBeTruthy();
expect(doc.getNode('form')?.isValidComponent()).toBeFalsy();
});
it('title', () => {
designer.createComponentMeta(btnMetadata);
const btn = doc.getNode('node_k1ow3cbn');
// 从 componentMeta 中获取到 title 值
expect(btn.title).toEqual({ type: 'i18n', 'zh-CN': '按钮', 'en-US': 'Button' });
// 从 extraProp 中获取值
btn.setExtraProp('title', 'hello button');
expect(btn.title).toBe('hello button');
// btn.props.deleteKey('___title___');
// 从 componentMeta descriptor 指向的 key 获取 title
// btn.setPropValue('xTitle', 'title from descriptor')
// expect(btn.title).toBe('title from descriptor');
});
it('isEmpty / getIndex / getIcon', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
// expect(firstBtn.children).toBeNull();
expect(firstBtn.isEmpty()).toBeTruthy();
expect(firstBtn.index).toBe(0);
expect(firstBtn.getIndex()).toBe(0);
expect(typeof firstBtn.getIcon()).toBe('function');
expect(doc.getNode('page')!.index).toBe(-1);
});
it('schema / toData / export', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
expect(firstBtn.toData().componentName).toBe('Button');
});
it('internalSetParent / internalSetWillPurge', () => {
const firstChild = doc.rootNode?.getChildren()?.get(0);
firstChild?.internalSetParent(doc.rootNode);
const firstBtn = doc.getNode('node_k1ow3cbn')!;
firstBtn.internalSetWillPurge();
// expect(firstBtn.parent).();
expect(firstBtn.hasSlots()).toBeFalsy();
});
it('prevSibling / nextSibling', () => {
// no parent
const page = doc.getNode('page');
expect(page?.nextSibling).toBeNull();
expect(page?.prevSibling).toBeNull();
// normal
const firstBtn = doc.getNode('node_k1ow3cbn');
const secondBtn = doc.getNode('node_k1ow3cbp');
expect(firstBtn?.nextSibling).toBe(secondBtn);
expect(secondBtn?.prevSibling).toBe(firstBtn);
expect(secondBtn?.nextSibling).toBeNull();
// index < 0
firstBtn?.parent?.removeChild(firstBtn);
expect(firstBtn?.nextSibling).toBeNull();
expect(firstBtn?.prevSibling).toBeNull();
});
it('toString', () => {
expect(doc.rootNode.toString()).toBe('page');
});
it('lock', () => {
const form = doc.getNode('node_k1ow3cbo');
expect(form.isLocked).toBeFalsy();
form.lock(true);
expect(form.isLocked).toBeTruthy();
form.lock(false);
expect(form.isLocked).toBeFalsy();
form.lock();
expect(form.isLocked).toBeTruthy();
});
it('didDropIn / didDropOut', () => {
const form = doc.getNode('node_k1ow3cbo');
designer.createComponentMeta(divMetadata);
designer.createComponentMeta(formMetadata);
const callbacks = form.componentMeta.advanced.callbacks;
const fn1 = callbacks.onNodeAdd = jest.fn();
const fn2 = callbacks.onNodeRemove = jest.fn();
const textField = doc.getNode('node_k1ow3cc9');
form.didDropIn(textField);
expect(fn1).toHaveBeenCalledWith(textField.internalToShellNode(), form.internalToShellNode());
form.didDropOut(textField);
expect(fn2).toHaveBeenCalledWith(textField.internalToShellNode(), form.internalToShellNode());
});
it('hover', () => {
const firstBtn = doc.getNode('node_k1ow3cbn')!;
firstBtn.hover(true);
expect(doc.designer.detecting.current).toBe(firstBtn);
firstBtn.hover(false);
expect(doc.designer.detecting.current).toBeNull();
firstBtn.hover();
expect(doc.designer.detecting.current).toBe(firstBtn);
});
it('getRect', () => {
const root = doc.rootNode!;
const firstBtn = doc.getNode('node_k1ow3cbn')!;
expect(root.getRect()).toBeNull();
expect(firstBtn.getRect()).toBeNull();
doc.project.mountSimulator({
computeRect: () => ({ x: 2, y: 2 }),
viewport: {
contentBounds: { x: 1, y: 1 },
},
});
expect(root.getRect()).toEqual({ x: 1, y: 1 });
expect(firstBtn.getRect()).toEqual({ x: 2, y: 2 });
});
it('isRootNode / isRoot / isNode', () => {
expect(isRootNode(doc.rootNode)).toBeTruthy();
expect(isNode(doc.rootNode)).toBeTruthy();
});
it('contains / comparePosition', () => {
const page = doc.getNode('page')!;
const content = doc.getNode('node_k1ow3cbb')!;
const firstBtn = doc.getNode('node_k1ow3cbn')!;
const secondBtn = doc.getNode('node_k1ow3cbp')!;
const firstCard = doc.getNode('node_k1ow3cbj')!;
expect(contains(firstBtn, firstBtn)).toBeTruthy();
expect(contains(firstBtn, secondBtn)).toBeFalsy();
expect(contains(firstBtn, page)).toBeFalsy();
expect(contains(firstBtn, content)).toBeFalsy();
expect(contains(firstCard, firstBtn)).toBeFalsy();
expect(comparePosition(firstBtn, secondBtn)).toBe(PositionNO.BeforeOrAfter);
expect(firstBtn.comparePosition(firstBtn)).toBe(PositionNO.TheSame);
expect(comparePosition(firstBtn, firstBtn)).toBe(PositionNO.TheSame);
expect(comparePosition(firstBtn, firstBtn.parent)).toBe(PositionNO.ContainedBy);
expect(comparePosition(firstBtn.parent, firstBtn)).toBe(PositionNO.Contains);
expect(comparePosition(firstCard, firstBtn)).toBe(PositionNO.BeforeOrAfter);
expect(comparePosition(firstBtn, firstCard)).toBe(PositionNO.BeforeOrAfter);
});
it('getZLevelTop', () => { });
it('propsData', () => {
expect(new Node(doc, { componentName: 'Leaf' }).propsData).toBeNull();
expect(new Node(doc, { componentName: 'Fragment' }).propsData).toBeNull();
});
describe('deprecated methods', () => {
it('setStatus / getStatus', () => {
const root = doc.rootNode!;
root.setStatus('xxx', true);
root.setStatus('locking', true);
root.setStatus('pseudo', true);
root.setStatus('inPlaceEditing', true);
expect(root.getStatus('locking')).toBeTruthy();
expect(root.getStatus('pseudo')).toBeTruthy();
expect(root.getStatus('inPlaceEditing')).toBeTruthy();
expect(root.getStatus()).toEqual({
locking: true,
pseudo: true,
inPlaceEditing: true,
});
});
it('getPage', () => {
expect(doc.rootNode?.getPage()).toBe(doc);
});
it('getDOMNode', () => {
const root = doc.rootNode!;
const firstBtn = doc.getNode('node_k1ow3cbn')!;
doc.project.mountSimulator({
findDOMNodes: () => [{ x: 1, y: 1 }],
getComponentInstances: (node) => {
if (node.componentName === 'Page') {
return [];
}
return [{}];
},
});
expect(root.getDOMNode()).toBeUndefined();
expect(firstBtn.getDOMNode()).toEqual({ x: 1, y: 1 });
});
it('registerAddon / getAddonData', () => {
const page = doc.getNode('page')!;
page.registerAddon('a', () => 'prop a');
expect(page.getAddonData('a')).toBe('prop a');
expect(page.getAddonData('b')).toBeUndefined();
expect(page.export().a).toBe('prop a');
});
it('getPrototype / setPrototype', () => {
const page = doc.getNode('page')!;
page.setPrototype({ a: 1 });
expect(page.getPrototype()).toEqual({ a: 1 });
});
});
});
================================================
FILE: packages/designer/tests/document/node/props/prop.test.ts
================================================
import '../../../fixtures/window';
import { Editor, engineConfig } from '@alilc/lowcode-editor-core';
import { Designer } from '../../../../src/designer/designer';
import { DocumentModel } from '../../../../src/document/document-model';
import { Prop, isProp, isValidArrayIndex } from '../../../../src/document/node/props/prop';
import { GlobalEvent, IPublicEnumTransformStage } from '@alilc/lowcode-types';
import { shellModelFactory } from '../../../../../engine/src/modules/shell-model-factory';
const slotNodeImportMockFn = jest.fn();
const slotNodeRemoveMockFn = jest.fn();
const mockOwner = {
componentName: 'Div',
addSlot() {},
document: {
createNode(schema) {
return {
...schema,
addSlot() {},
internalSetSlotFor() {},
import: slotNodeImportMockFn,
export() {
return schema;
},
remove: slotNodeRemoveMockFn,
};
},
designer: {
editor: {
eventBus: {
emit: jest.fn(),
},
},
},
},
isInited: true,
emitPropChange: jest.fn(),
delete() {},
};
const mockPropsInst = {
owner: mockOwner,
delete() {},
};
mockPropsInst.props = mockPropsInst;
describe('Prop 类测试', () => {
describe('基础类型', () => {
let boolProp: Prop;
let strProp: Prop;
let numProp: Prop;
let nullProp: Prop;
let expProp: Prop;
let slotProp: Prop;
beforeEach(() => {
boolProp = new Prop(mockPropsInst, true, 'boolProp');
strProp = new Prop(mockPropsInst, 'haha', 'strProp');
numProp = new Prop(mockPropsInst, 1, 'numProp');
nullProp = new Prop(mockPropsInst, null, 'nullProp');
expProp = new Prop(mockPropsInst, { type: 'JSExpression', value: 'state.haha' }, 'expProp');
slotProp = new Prop(
mockPropsInst,
{
type: 'JSSlot',
title: '测试 slot',
name: 'testSlot',
params: { a: 1 },
value: [{ componentName: 'Button' }],
},
'slotProp',
);
slotNodeImportMockFn.mockClear();
slotNodeRemoveMockFn.mockClear();
});
afterEach(() => {
boolProp.purge();
strProp.purge();
numProp.purge();
nullProp.purge();
expProp.purge();
slotProp.purge();
});
it('consturctor / getProps / getNode', () => {
expect(boolProp.parent).toBe(mockPropsInst);
expect(boolProp.getProps()).toBe(mockPropsInst);
expect(boolProp.getNode()).toBe(mockOwner);
});
it('misc', () => {
expect(boolProp.get('x', false)).toBeNull();
expect(boolProp.maps).toBeNull();
expect(boolProp.add()).toBeNull();
strProp.unset();
strProp.add(2, true);
strProp.set(0);
expect(numProp.set()).toBeNull();
expect(numProp.has()).toBeFalsy();
expect(numProp.path).toEqual(['numProp']);
});
it('getValue / getAsString / setValue', () => {
expect(strProp.getValue()).toBe('haha');
strProp.setValue('heihei');
strProp.setValue('heihei');
expect(strProp.getValue()).toBe('heihei');
expect(strProp.getAsString()).toBe('heihei');
strProp.unset();
expect(strProp.getValue()).toBeUndefined();
});
it('code', () => {
expect(expProp.code).toBe('state.haha');
expect(boolProp.code).toBe('true');
expect(strProp.code).toBe('"haha"');
expProp.code = 'state.heihei';
expect(expProp.code).toBe('state.heihei');
expect(expProp.getValue()).toEqual({
type: 'JSExpression',
value: 'state.heihei',
});
boolProp.code = 'false';
expect(boolProp.code).toBe('false');
expect(boolProp.getValue()).toBe(false);
strProp.code = '"heihei"';
expect(strProp.code).toBe('"heihei"');
expect(strProp.getValue()).toBe('heihei');
// TODO: 不确定为什么会有这个分支
strProp.code = 'state.a';
expect(strProp.code).toBe('state.a');
expect(strProp.getValue()).toEqual({
type: 'JSExpression',
value: 'state.a',
mock: 'heihei',
});
});
it('export', () => {
expect(boolProp.export(IPublicEnumTransformStage.Save)).toBe(true);
expect(strProp.export(IPublicEnumTransformStage.Save)).toBe('haha');
expect(numProp.export(IPublicEnumTransformStage.Save)).toBe(1);
expect(nullProp.export(IPublicEnumTransformStage.Save)).toBe(null);
expect(nullProp.export(IPublicEnumTransformStage.Serilize)).toBe(null);
expect(expProp.export(IPublicEnumTransformStage.Save)).toEqual({
type: 'JSExpression',
value: 'state.haha',
});
strProp.unset();
expect(strProp.getValue()).toBeUndefined();
expect(strProp.isUnset()).toBeTruthy();
expect(strProp.export(IPublicEnumTransformStage.Save)).toBeUndefined();
expect(
new Prop(mockPropsInst, false, '___condition___').export(IPublicEnumTransformStage.Render),
).toBeTruthy();
engineConfig.set('enableCondition', true);
expect(
new Prop(mockPropsInst, false, '___condition___').export(IPublicEnumTransformStage.Render),
).toBeFalsy();
expect(slotProp.export(IPublicEnumTransformStage.Render)).toEqual({
type: 'JSSlot',
params: { a: 1 },
value: {
componentName: 'Slot',
title: '测试 slot',
name: 'testSlot',
params: { a: 1 },
children: [{ componentName: 'Button' }],
},
});
expect(slotProp.export(IPublicEnumTransformStage.Save)).toEqual({
type: 'JSSlot',
params: { a: 1 },
value: [{ componentName: 'Button' }],
title: '测试 slot',
name: 'testSlot',
});
});
it('compare', () => {
const newProp = new Prop(mockPropsInst, 'haha');
const newProp2 = new Prop(mockPropsInst, { a: 1 });
expect(strProp.compare(newProp)).toBe(0);
expect(strProp.compare(expProp)).toBe(2);
newProp.unset();
expect(strProp.compare(newProp)).toBe(2);
strProp.unset();
expect(strProp.compare(newProp)).toBe(0);
expect(strProp.compare(newProp2)).toBe(2);
});
it('isVirtual', () => {
expect(new Prop(mockPropsInst, 111, '!virtualProp')).toBeTruthy();
});
it('purge', () => {
boolProp.purge();
expect(boolProp.purged).toBeTruthy();
boolProp.purge();
});
it('slot', () => {
// 更新 slot
slotProp.setValue({
type: 'JSSlot',
value: [
{
componentName: 'Form',
},
],
});
expect(slotNodeImportMockFn).toBeCalled();
// 节点类型转换
slotProp.setValue(true);
expect(slotNodeRemoveMockFn).toBeCalled();
});
it('迭代器 / map / forEach', () => {
const mockFn = jest.fn();
for (const item of strProp) {
mockFn();
}
expect(mockFn).not.toHaveBeenCalled();
mockFn.mockClear();
strProp.forEach((item) => {
mockFn();
});
expect(mockFn).not.toHaveBeenCalled();
mockFn.mockClear();
strProp.map((item) => {
return mockFn();
});
expect(mockFn).not.toHaveBeenCalled();
mockFn.mockClear();
});
});
describe('复杂类型', () => {
describe('items(map 类型)', () => {
let prop: Prop;
beforeEach(() => {
prop = new Prop(mockPropsInst, {
a: 1,
b: 'str',
c: true,
d: {
type: 'JSExpression',
value: 'state.a',
},
emptyArr: [],
emptyObj: {},
z: {
z1: 1,
z2: 'str',
},
});
});
afterEach(() => {
prop.purge();
});
it('items / get', async () => {
expect(prop.size).toBe(7);
expect(prop.get('a').getValue()).toBe(1);
expect(prop.get('b').getValue()).toBe('str');
expect(prop.get('c').getValue()).toBe(true);
expect(prop.get('d').getValue()).toEqual({ type: 'JSExpression', value: 'state.a' });
expect(prop.get('z').getValue()).toEqual({
z1: 1,
z2: 'str',
});
expect(prop.getPropValue('a')).toBe(1);
prop.setPropValue('a', 2);
expect(prop.getPropValue('a')).toBe(2);
prop.clearPropValue('a');
expect(prop.get('a')?.isUnset()).toBeTruthy();
expect(prop.get('z.z1')?.getValue()).toBe(1);
expect(prop.get('z.z2')?.getValue()).toBe('str');
const newlyCreatedProp = prop.get('l', true);
const newlyCreatedNestedProp = prop.get('m.m1', true);
newlyCreatedProp.setValue('newlyCreatedProp');
newlyCreatedNestedProp?.setValue('newlyCreatedNestedProp');
expect(prop.get('l').getValue()).toBe('newlyCreatedProp');
expect(prop.get('m.m1').getValue()).toBe('newlyCreatedNestedProp');
const newlyCreatedNestedProp2 = prop.get('m.m2', true);
// .m2 的值为 undefined,导出时将会被移除
expect(prop.get('m').getValue()).toEqual({ m1: 'newlyCreatedNestedProp' });
// 对于空值的 list / map 类型,_items 应该为 null
expect(prop.get('emptyArr')._items).toBeNull();
expect(prop.get('emptyObj')._items).toBeNull();
});
it('export', () => {
expect(prop.export()).toEqual({
a: 1,
b: 'str',
c: true,
d: {
type: 'JSExpression',
value: 'state.a',
},
emptyArr: [],
emptyObj: {},
z: {
z1: 1,
z2: 'str',
},
});
});
it('compare', () => {
const prop1 = new Prop(mockPropsInst, { a: 1 });
const prop2 = new Prop(mockPropsInst, { b: 1 });
expect(prop1.compare(prop2)).toBe(1);
});
it('has / add / delete / deleteKey / remove', () => {
expect(prop.has('a')).toBeTruthy();
expect(prop.has('b')).toBeTruthy();
expect(prop.has('c')).toBeTruthy();
expect(prop.has('d')).toBeTruthy();
expect(prop.has('z')).toBeTruthy();
expect(prop.has('y')).toBeFalsy();
// 触发一下内部 maps 构造
prop.items;
expect(prop.has('z')).toBeTruthy();
expect(prop.add(1)).toBeNull();
prop.deleteKey('c');
expect(prop.get('c', false)).toBeNull();
prop.delete(prop.get('b'));
expect(prop.get('b', false)).toBeNull();
prop.get('d')?.remove();
expect(prop.get('d', false)).toBeNull();
});
it('set', () => {
prop.set('e', 1);
expect(prop.get('e', false)?.getValue()).toBe(1);
prop.set('a', 5);
expect(prop.get('a', false)?.getValue()).toBe(5);
});
it('迭代器 / map / forEach', () => {
const mockFn = jest.fn();
for (const item of prop) {
mockFn();
}
expect(mockFn).toHaveBeenCalledTimes(7);
mockFn.mockClear();
prop.forEach((item) => {
mockFn();
});
expect(mockFn).toHaveBeenCalledTimes(7);
mockFn.mockClear();
prop.map((item) => {
return mockFn();
});
expect(mockFn).toHaveBeenCalledTimes(7);
mockFn.mockClear();
});
it('dispose', () => {
prop.items;
prop.dispose();
expect(prop._items).toBeNull();
});
});
describe('items(list 类型)', () => {
let prop: Prop;
beforeEach(() => {
prop = new Prop(mockPropsInst, [1, true, 'haha']);
});
afterEach(() => {
prop.purge();
});
it('items / get', () => {
expect(prop.size).toBe(3);
expect(prop.get(0).getValue()).toBe(1);
expect(prop.get(1).getValue()).toBe(true);
expect(prop.get(2).getValue()).toBe('haha');
expect(prop.getAsString()).toBe('');
prop.unset();
prop.set(0, true);
expect(prop.set('x', 'invalid')).toBeNull();
expect(prop.get(0).getValue()).toBeTruthy();
// map / list 级联测试
prop.get('loopArgs.0', true).setValue('newItem');;
expect(prop.get('loopArgs.0').getValue()).toBe('newItem');
});
it('export', () => {
expect(prop.export()).toEqual([1, true, 'haha']);
// 触发构造
prop.items;
expect(prop.export()).toEqual([1, true, 'haha']);
});
it('compare', () => {
const prop1 = new Prop(mockPropsInst, [1]);
const prop2 = new Prop(mockPropsInst, [2]);
const prop3 = new Prop(mockPropsInst, [1, 2]);
expect(prop1.compare(prop2)).toBe(1);
expect(prop1.compare(prop3)).toBe(2);
});
it('set', () => {
prop.set(0, 1);
expect(prop.get(0, false)?.getValue()).toBe(1);
// illegal
// expect(prop.set(5, 1)).toBeNull();
});
it('should return undefined when all items are undefined', () => {
prop = new Prop(mockPropsInst, [undefined, undefined], '___loopArgs___');
expect(prop.getValue()).toEqual([undefined, undefined]);
});
it('迭代器 / map / forEach', () => {
const listProp = new Prop(mockPropsInst, [1, 2]);
const mockFn = jest.fn();
for (const item of listProp) {
mockFn();
}
expect(mockFn).toHaveBeenCalledTimes(2);
mockFn.mockClear();
listProp.forEach((item) => {
mockFn();
});
expect(mockFn).toHaveBeenCalledTimes(2);
mockFn.mockClear();
listProp.map((item) => {
return mockFn();
});
expect(mockFn).toHaveBeenCalledTimes(2);
mockFn.mockClear();
});
});
});
describe('slotNode / setAsSlot', () => {
const editor = new Editor();
const designer = new Designer({ editor, shellModelFactory });
const doc = new DocumentModel(designer.project, {
componentName: 'Page',
children: [
{
id: 'div',
componentName: 'Div',
},
],
});
const div = doc.getNode('div');
const slotProp = new Prop(div?.getProps(), {
type: 'JSSlot',
value: [
{
componentName: 'Button',
},
],
});
expect(slotProp.slotNode?.componentName).toBe('Slot');
// TODO: id 总是变,不好断言
expect(slotProp.code.includes('Button')).toBeTruthy();
slotProp.export();
expect(slotProp.export().value[0].componentName).toBe('Button');
expect(slotProp.export(IPublicEnumTransformStage.Serilize).value[0].componentName).toBe('Button');
slotProp.purge();
expect(slotProp.purged).toBeTruthy();
slotProp.dispose();
});
describe('slotNode-value / setAsSlot', () => {
const editor = new Editor();
const designer = new Designer({ editor, shellModelFactory });
const doc = new DocumentModel(designer.project, {
componentName: 'Page',
children: [
{
id: 'div',
componentName: 'Div',
},
],
});
const div = doc.getNode('div');
const slotProp = new Prop(div?.getProps(), {
type: 'JSSlot',
value: {
componentName: 'Slot',
id: 'node_oclei5rv2e2',
props: {
slotName: "content",
slotTitle: "主内容"
},
children: [
{
componentName: 'Button',
}
]
},
});
expect(slotProp.slotNode?.componentName).toBe('Slot');
expect(slotProp.slotNode?.title).toBe('主内容');
expect(slotProp.slotNode?.getExtraProp('name')?.getValue()).toBe('content');
expect(slotProp.slotNode?.export()?.id).toBe('node_oclei5rv2e2');
slotProp.export();
// Save
expect(slotProp.export()?.value[0].componentName).toBe('Button');
expect(slotProp.export()?.title).toBe('主内容');
expect(slotProp.export()?.name).toBe('content');
// Render
expect(slotProp.export(IPublicEnumTransformStage.Render)?.value.children[0].componentName).toBe('Button');
expect(slotProp.export(IPublicEnumTransformStage.Render)?.value.componentName).toBe('Slot');
slotProp.purge();
expect(slotProp.purged).toBeTruthy();
slotProp.dispose();
});
});
describe('其他导出函数', () => {
it('isProp', () => {
expect(isProp({ isProp: true })).toBeTruthy();
});
it('isValidArrayIndex', () => {
expect(isValidArrayIndex('1')).toBeTruthy();
expect(isValidArrayIndex('1', 2)).toBeTruthy();
expect(isValidArrayIndex('2', 1)).toBeFalsy();
});
});
describe('setValue with event', () => {
let propInstance;
let mockEmitChange;
let mockEventBusEmit;
let mockEmitPropChange;
beforeEach(() => {
// Initialize the instance of your class
propInstance = new Prop(mockPropsInst, true, 'stringProp');;
// Mock necessary methods and properties
mockEmitChange = jest.spyOn(propInstance, 'emitChange');
propInstance.owner = {
document: {
designer: {
editor: {
eventBus: {
emit: jest.fn(),
},
},
},
},
emitPropChange: jest.fn(),
delete() {},
};
mockEventBusEmit = jest.spyOn(propInstance.owner.document.designer.editor.eventBus, 'emit');
mockEmitPropChange = jest.spyOn(propInstance.owner, 'emitPropChange');
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should correctly handle string values and emit changes', () => {
const oldValue = propInstance._value;
const newValue = 'new string value';
propInstance.setValue(newValue);
const expectedPartialPropsInfo = expect.objectContaining({
key: propInstance.key,
newValue, // You can specifically test only certain keys
oldValue,
});
expect(propInstance.getValue()).toBe(newValue);
expect(propInstance.type).toBe('literal');
expect(mockEmitChange).toHaveBeenCalledWith({ oldValue });
expect(mockEventBusEmit).toHaveBeenCalledWith(GlobalEvent.Node.Prop.InnerChange, expectedPartialPropsInfo);
expect(mockEmitPropChange).toHaveBeenCalledWith(expectedPartialPropsInfo);
});
it('should handle object values and set type to map', () => {
const oldValue = propInstance._value;
const newValue = 234;
const expectedPartialPropsInfo = expect.objectContaining({
key: propInstance.key,
newValue, // You can specifically test only certain keys
oldValue,
});
propInstance.setValue(newValue);
expect(propInstance.getValue()).toEqual(newValue);
expect(propInstance.type).toBe('literal');
expect(mockEmitChange).toHaveBeenCalledWith({ oldValue });
expect(mockEventBusEmit).toHaveBeenCalledWith(GlobalEvent.Node.Prop.InnerChange, expectedPartialPropsInfo);
expect(mockEmitPropChange).toHaveBeenCalledWith(expectedPartialPropsInfo);
});
it('should has event when unset call', () => {
const oldValue = propInstance._value;
propInstance.unset();
const expectedPartialPropsInfo = expect.objectContaining({
key: propInstance.key,
newValue: undefined, // You can specifically test only certain keys
oldValue,
});
expect(propInstance.getValue()).toEqual(undefined);
expect(propInstance.type).toBe('unset');
expect(mockEmitChange).toHaveBeenCalledWith({
oldValue,
newValue: undefined,
});
expect(mockEventBusEmit).toHaveBeenCalledWith(GlobalEvent.Node.Prop.InnerChange, expectedPartialPropsInfo);
expect(mockEmitPropChange).toHaveBeenCalledWith(expectedPartialPropsInfo);
propInstance.unset();
expect(mockEmitChange).toHaveBeenCalledTimes(1);
});
// remove
it('should has event when remove call', () => {
const oldValue = propInstance._value;
propInstance.remove();
const expectedPartialPropsInfo = expect.objectContaining({
key: propInstance.key,
newValue: undefined, // You can specifically test only certain keys
oldValue,
});
expect(propInstance.getValue()).toEqual(undefined);
// expect(propInstance.type).toBe('unset');
expect(mockEmitChange).toHaveBeenCalledWith({
oldValue,
newValue: undefined,
});
expect(mockEventBusEmit).toHaveBeenCalledWith(GlobalEvent.Node.Prop.InnerChange, expectedPartialPropsInfo);
expect(mockEmitPropChange).toHaveBeenCalledWith(expectedPartialPropsInfo);
propInstance.remove();
expect(mockEmitChange).toHaveBeenCalledTimes(1);
});
});
================================================
FILE: packages/designer/tests/document/node/props/props.test.ts
================================================
// @ts-nocheck
import '../../../fixtures/window';
import { set, delayObxTick } from '../../../utils';
import { Editor } from '@alilc/lowcode-editor-core';
import {
Props,
getConvertedExtraKey,
getOriginalExtraKey,
Prop,
isProp,
isValidArrayIndex,
} from '../../../../src/document/node/props/props';
import { Designer } from '../../../../src/designer/designer';
import { Project } from '../../../../src/project/project';
import { DocumentModel } from '../../../../src/document/document-model';
import { TransformStage } from '@alilc/lowcode-types';
const mockOwner = { componentName: 'Page' };
describe('Props 类测试', () => {
let props: Props;
beforeEach(() => {
props = new Props(
mockOwner,
{
a: 1,
b: 'str',
c: true,
d: {
type: 'JSExpression',
value: 'state.a',
},
z: {
z1: 1,
z2: 'str',
},
},
{ condition: true },
);
});
afterEach(() => {
props.purge();
});
it('getNode', () => {
expect(props.getNode()).toBe(mockOwner);
});
it('items / get', async () => {
expect(props.size).toBe(6);
expect(props.get('a').getValue()).toBe(1);
expect(props.get('b').getValue()).toBe('str');
expect(props.get('c').getValue()).toBe(true);
expect(props.get('d').getValue()).toEqual({ type: 'JSExpression', value: 'state.a' });
expect(props.get('z').getValue()).toEqual({
z1: 1,
z2: 'str',
});
expect(props.getPropValue('a')).toBe(1);
props.setPropValue('a', 2);
expect(props.getPropValue('a')).toBe(2);
// props.clearPropValue('a');
// expect(props.get('a')?.isUnset()).toBeTruthy();
expect(props.get('z.z1')?.getValue()).toBe(1);
expect(props.get('z.z2')?.getValue()).toBe('str');
const notCreatedProp = props.get('i');
expect(notCreatedProp).toBeNull();
const newlyCreatedProp = props.get('l', true);
const newlyCreatedNestedProp = props.get('m.m1', true);
newlyCreatedProp.setValue('newlyCreatedProp');
newlyCreatedNestedProp?.setValue('newlyCreatedNestedProp');
expect(props.get('l').getValue()).toBe('newlyCreatedProp');
expect(props.get('m.m1').getValue()).toBe('newlyCreatedNestedProp');
// map / list 级联测试
props.get('loopArgs.0', true).setValue('newItem');
expect(props.get('loopArgs.0').getValue()).toBe('newItem');
});
it('export', () => {
expect(props.export()).toEqual({
props: {
a: 1,
b: 'str',
c: true,
d: {
type: 'JSExpression',
value: 'state.a',
},
z: {
z1: 1,
z2: 'str',
},
},
extras: {
condition: true,
},
});
expect(props.toData()).toEqual({
a: 1,
b: 'str',
c: true,
d: {
type: 'JSExpression',
value: 'state.a',
},
z: {
z1: 1,
z2: 'str',
},
});
props.get('a')?.unset();
expect(props.toData()).toEqual({
a: undefined,
b: 'str',
c: true,
d: {
type: 'JSExpression',
value: 'state.a',
},
z: {
z1: 1,
z2: 'str',
},
});
});
it('export - remove undefined items', () => {
props.import(
{
a: 1,
},
{ loop: false },
);
props.setPropValue('x', undefined);
expect(props.export()).toEqual({
props: {
a: 1,
},
extras: {
loop: false,
},
});
props.setPropValue('x', 2);
expect(props.export()).toEqual({
props: {
a: 1,
x: 2,
},
extras: {
loop: false,
},
});
props.setPropValue('y.z', undefined);
expect(props.export()).toEqual({
props: {
a: 1,
x: 2,
},
extras: {
loop: false,
},
});
props.setPropValue('y.z', 2);
expect(props.export()).toEqual({
props: {
a: 1,
x: 2,
y: { z: 2 },
},
extras: {
loop: false,
},
});
});
it('import', () => {
props.import(
{
x: 1,
y: true,
},
{ loop: false },
);
expect(props.export()).toEqual({
props: {
x: 1,
y: true,
},
extras: {
loop: false,
},
});
props.import();
});
it('merge', async () => {
props.merge({ x: 1 });
await delayObxTick();
expect(props.get('x')?.getValue()).toBe(1);
});
it('has / add / delete / deleteKey / remove', () => {
expect(props.has('a')).toBeTruthy();
expect(props.has('b')).toBeTruthy();
expect(props.has('c')).toBeTruthy();
expect(props.has('d')).toBeTruthy();
expect(props.has('z')).toBeTruthy();
expect(props.has('y')).toBeFalsy();
props.add(1, 'newAdded');
expect(props.has('newAdded')).toBeTruthy();
props.deleteKey('c');
expect(props.get('c', false)).toBeNull();
props.delete(props.get('b'));
expect(props.get('b', false)).toBeNull();
props.get('d')?.remove();
expect(props.get('d', false)).toBeNull();
});
it('迭代器 / map / forEach', () => {
const mockFn = jest.fn();
for (const item of props) {
mockFn();
}
expect(mockFn).toHaveBeenCalledTimes(6);
mockFn.mockClear();
props.forEach((item) => {
mockFn();
});
expect(mockFn).toHaveBeenCalledTimes(6);
mockFn.mockClear();
props.map((item) => {
return mockFn();
});
expect(mockFn).toHaveBeenCalledTimes(6);
mockFn.mockClear();
props.filter((item) => {
return mockFn();
});
expect(mockFn).toHaveBeenCalledTimes(6);
mockFn.mockClear();
});
it('purge', () => {
props.purge();
expect(props.purged).toBeTruthy();
});
it('empty items', () => {
expect(new Props(mockOwner).export()).toEqual({});
});
describe('list 类型', () => {
let props: Props;
beforeEach(() => {
props = new Props(mockOwner, [1, true, 'haha'], { condition: true });
});
it('constructor', () => {
props.purge();
});
it('export', () => {
expect(props.export().extras).toEqual({
condition: true,
});
});
it('import', () => {
props.import([1], { loop: true });
expect(props.export().extras).toEqual({
loop: true,
});
props.items[0]?.unset();
props.export();
});
});
});
describe('其他函数', () => {
it('getConvertedExtraKey', () => {
expect(getConvertedExtraKey()).toBe('');
expect(getConvertedExtraKey('a')).toBe('___a___');
expect(getConvertedExtraKey('a.b')).toBe('___a___.b');
expect(getConvertedExtraKey('a.0')).toBe('___a___.0');
});
it('getOriginalExtraKey', () => {
expect(getOriginalExtraKey('___a___')).toBe('a');
expect(getOriginalExtraKey('___a___.b')).toBe('a.b');
});
});
================================================
FILE: packages/designer/tests/document/node/props/value-to-source.test.ts
================================================
// @ts-nocheck
import '../../../fixtures/silent-console';
import { getSource, valueToSource } from '../../../../src/document/node/props/value-to-source';
it('valueToSource', () => {
expect(valueToSource(1)).toMatchSnapshot();
expect(valueToSource(true)).toMatchSnapshot();
expect(valueToSource([])).toMatchSnapshot();
expect(valueToSource([{ a: 1 }])).toMatchSnapshot();
expect(valueToSource({ a: 1 })).toMatchSnapshot();
expect(valueToSource(null)).toMatchSnapshot();
expect(valueToSource(() => {})).toMatchSnapshot();
expect(valueToSource(new Map())).toMatchSnapshot();
expect(valueToSource(new Set())).toMatchSnapshot();
expect(valueToSource(/haha/)).toMatchSnapshot();
expect(valueToSource('hahah')).toMatchSnapshot();
expect(valueToSource(Symbol('haha'))).toMatchSnapshot();
expect(valueToSource()).toMatchSnapshot();
expect(valueToSource(new Date(1607680998520))).toMatchSnapshot();
});
it('getSource', () => {
expect(getSource({ __source: { a: 1 } })).toEqual({ a: 1 });
expect(getSource()).toBe('');
const value = { abc: 1 };
getSource(value);
expect(value).toHaveProperty('__source');
expect(getSource(1)).toBe('1');
});
================================================
FILE: packages/designer/tests/document/node/props/__snapshots__/value-to-source.test.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`valueToSource 1`] = `"1"`;
exports[`valueToSource 2`] = `"true"`;
exports[`valueToSource 3`] = `"[]"`;
exports[`valueToSource 4`] = `
"[{
\\"a\\": 1
}]"
`;
exports[`valueToSource 5`] = `
"{
\\"a\\": 1
}"
`;
exports[`valueToSource 6`] = `"null"`;
exports[`valueToSource 7`] = `"() => {}"`;
exports[`valueToSource 8`] = `"new Map()"`;
exports[`valueToSource 9`] = `"new Set()"`;
exports[`valueToSource 10`] = `"/haha/"`;
exports[`valueToSource 11`] = `"\\"hahah\\""`;
exports[`valueToSource 12`] = `"Symbol(\\"haha\\")"`;
exports[`valueToSource 13`] = `"undefined"`;
exports[`valueToSource 14`] = `"new Date(\\"2020-12-11T10:03:18.520Z\\")"`;
================================================
FILE: packages/designer/tests/fixtures/disable-raf.ts
================================================
Object.defineProperty(window, 'requestAnimationFrame', {
value: null,
});
================================================
FILE: packages/designer/tests/fixtures/silent-console.ts
================================================
export const mockConsoleError = jest.fn();
export const mockConsoleWarn = jest.fn();
// const mockConsoleInfo = jest.fn();
console.error = mockConsoleError;
console.warn = mockConsoleWarn;
================================================
FILE: packages/designer/tests/fixtures/unhandled-rejection.ts
================================================
if (!process.env.LISTENING_TO_UNHANDLED_REJECTION) {
process.on('unhandledRejection', reason => {
throw reason;
});
// Avoid memory leak by adding too many listeners
process.env.LISTENING_TO_UNHANDLED_REJECTION = true;
}
================================================
FILE: packages/designer/tests/fixtures/window.ts
================================================
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
Object.defineProperty(window, 'React', {
writable: true,
value: {},
});
window.scrollTo = () => {};
window.console.warn = () => {};
const originalLog = window.console.log;
window.console.log = (...args) => {
// suppress boring warnings
if (args[0]?.includes && args[0].includes('@babel/plugin-proposal-private-property-in-object')) return;
originalLog.apply(window.console, args);
};
window.React = window.React || {};
================================================
FILE: packages/designer/tests/fixtures/component-metadata/abcgroup.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Abc.Group',
npm: {
package: '@ali/vc-div',
componentName: 'Div',
},
title: { label: '容器' },
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
},
supports: {},
advanced: {
isTopFixed: true,
callbacks: {
onNodeAdd: (dragment, self) => { console.log(dragment); },
onNodeRemove: (dragment, self) => { console.log(dragment); }
},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/abcitem.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Abc.Item',
npm: {
package: '@ali/vc-div',
componentName: 'Div',
},
title: { label: '容器' },
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
},
supports: {},
advanced: {
isTopFixed: true,
callbacks: {
onNodeAdd: (dragment, self) => { console.log(dragment); },
onNodeRemove: (dragment, self) => { console.log(dragment); }
},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/abcnode.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Abc.Node',
npm: {
package: '@ali/vc-div',
componentName: 'Div',
},
title: { label: '容器' },
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
},
supports: {},
advanced: {
isTopFixed: true,
callbacks: {
onNodeAdd: (dragment, self) => { console.log(dragment); },
onNodeRemove: (dragment, self) => { console.log(dragment); }
},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/abcoption.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Abc.Option',
npm: {
package: '@ali/vc-div',
componentName: 'Div',
},
title: { label: '容器' },
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
},
supports: {},
advanced: {
isTopFixed: true,
callbacks: {
onNodeAdd: (dragment, self) => { console.log(dragment); },
onNodeRemove: (dragment, self) => { console.log(dragment); }
},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/button.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Button',
npm: {
package: '@ali/vc-button',
componentName: 'Button',
},
title: '按钮',
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'field',
name: 'non-exsiting',
},
{
type: 'field',
name: 'obj',
items: [
{
name: 'a',
title: 'a',
setter: () => 'StringSetter',
},
{
name: 'b',
title: 'b',
setter: 'NumberSetter',
},
{
name: 'c',
title: 'c',
setter: {
componentName: 'ColorSetter'
},
},
],
},
() => 'haha', // IPublicTypeCustomView
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
nestingRule: {
// parentWhitelist: 'Div',
// childWhitelist: 'Div',
},
descriptor: 'xTitle'
},
supports: {},
},
experimental: {
callbacks: {},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/dialog.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Dialog',
npm: {
package: '@ali/vc-dialog',
componentName: 'Dialog',
},
title: '容器',
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
isModal: true,
},
supports: {},
},
experimental: {
callbacks: {},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/div.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Div',
npm: {
package: '@ali/vc-div',
componentName: 'Div',
},
title: '容器',
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
nestingRule: {
// parentWhitelist: 'Div',
// childWhitelist: 'Div',
},
},
supports: {},
advanced: {
callbacks: {
onNodeAdd: (dragment, self) => { console.log(dragment); },
onNodeRemove: (dragment, self) => { console.log(dragment); }
},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/div10.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Div',
title: '容器',
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
component: {
nestingRule: {
parentWhitelist: (parent, my) => {
if (parent.componentName === 'Form' && my.componentName === 'Div') return true;
return false;
},
childWhitelist: (child, my) => {
if (child.componentName === 'Image' && my.componentName === 'Div') return true;
return false;
},
},
},
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/div2.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Div',
npm: {
package: '@ali/vc-div',
componentName: 'Div',
},
title: { label: '容器' },
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
},
supports: {},
advanced: {
isTopFixed: true,
callbacks: {
onNodeAdd: (dragment, self) => { console.log(dragment); },
onNodeRemove: (dragment, self) => { console.log(dragment); }
},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/div3.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Div',
npm: {
package: '@ali/vc-div',
componentName: 'Div',
},
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
nestingRule: {
// parentWhitelist: 'Div',
// childWhitelist: 'Div',
}
},
supports: {},
advanced: {
callbacks: {
onNodeAdd: (dragment, self) => { console.log(dragment); },
onNodeRemove: (dragment, self) => { console.log(dragment); }
},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/div4.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Div',
npm: {
package: '@ali/vc-div',
componentName: 'Div',
},
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
experimental: {
callbacks: {
onNodeAdd: (dragment, self) => { console.log(dragment); },
onNodeRemove: (dragment, self) => { console.log(dragment); }
},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/div5.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Div',
npm: {
package: '@ali/vc-div',
componentName: 'Div',
},
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
nestingRule: {
// parentWhitelist: 'Div',
// childWhitelist: 'Div',
},
disableBehaviors: '*',
},
supports: {},
advanced: {
callbacks: {
onNodeAdd: (dragment, self) => { console.log(dragment); },
onNodeRemove: (dragment, self) => { console.log(dragment); }
},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/div6.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Div',
npm: {
package: '@ali/vc-div',
componentName: 'Div',
},
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
nestingRule: {
// parentWhitelist: 'Div',
// childWhitelist: 'Div',
},
disableBehaviors: '*',
},
supports: {},
},
experimental: {
callbacks: {
onNodeAdd: (dragment, self) => { console.log(dragment); },
onNodeRemove: (dragment, self) => { console.log(dragment); }
},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/div7.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Div',
npm: {
package: '@ali/vc-div',
componentName: 'Div',
},
title: '容器',
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
supports: {},
advanced: {
callbacks: {
onNodeAdd: (dragment, self) => { console.log(dragment); },
onNodeRemove: (dragment, self) => { console.log(dragment); }
},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/div8.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Div',
npm: {
package: '@ali/vc-div',
componentName: 'Div',
},
title: '容器',
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/div9.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Div',
title: '容器',
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/form.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Form',
npm: {
package: '@ali/vc-form',
},
title: '表单',
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
nestingRule: {
parentWhitelist: 'Div,Page',
// childWhitelist: 'Div',
},
},
supports: {},
},
experimental: {
callbacks: {},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/other.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Other',
npm: {
package: '@ali/vc-other',
},
title: '容器',
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
nestingRule: {
parentWhitelist: 'Div',
childWhitelist: 'Div',
},
},
supports: {},
},
experimental: {
callbacks: {},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/page.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Page',
npm: {
package: '@ali/vc-page',
},
title: '容器',
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
nestingRule: {
// parentWhitelist: 'Div',
// childWhitelist: 'Div',
},
},
supports: {},
},
experimental: {
callbacks: {},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/page2.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'Page',
npm: {
package: '@ali/vc-page',
},
title: '容器',
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
nestingRule: {
// parentWhitelist: 'Div',
// childWhitelist: 'Div',
},
},
supports: {},
},
experimental: {
callbacks: {},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/root-content.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'RootContent',
npm: {
package: '@ali/vc-page',
},
title: '容器',
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
nestingRule: {
// parentWhitelist: 'Div',
// childWhitelist: 'Div',
},
},
supports: {},
},
experimental: {
callbacks: {},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/root-footer.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'RootFooter',
npm: {
package: '@ali/vc-page',
},
title: '容器',
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
nestingRule: {
// parentWhitelist: 'Div',
// childWhitelist: 'Div',
},
},
supports: {},
},
experimental: {
callbacks: {},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/component-metadata/root-header.ts
================================================
import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types";
export default {
componentName: 'RootHeader',
npm: {
package: '@ali/vc-page',
},
title: '容器',
docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs',
devMode: 'proCode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
nestingRule: {
// parentWhitelist: 'Div',
// childWhitelist: 'Div',
},
},
supports: {},
},
experimental: {
callbacks: {},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
} as IPublicTypeComponentMetadata;
================================================
FILE: packages/designer/tests/fixtures/schema/form-with-modal.ts
================================================
export default {
componentName: 'Page',
id: 'page',
title: "hey, i' a page!",
props: {
extensions: {
启用页头: true,
},
pageStyle: {
backgroundColor: '#f2f3f5',
},
containerStyle: {},
className: 'page_kgaqfbm4',
templateVersion: '1.0.0',
},
lifeCycles: {
constructor: {
type: 'js',
compiled:
"function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
source:
"function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
},
},
condition: true,
css:
'body{background-color:#f2f3f5}.card_kgaqfbm5 {\n margin-bottom: 12px;\n}.card_kgaqfbm6 {\n margin-bottom: 12px;\n}.button_kgaqfbm7 {\n margin-right: 16px;\n width: 80px\n}.button_kgaqfbm8 {\n width: 80px;\n}.div_kgaqfbm9 {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
methods: {
__initMethods__: {
type: 'js',
source: 'function (exports, module) { /*set actions code here*/ }',
compiled: 'function (exports, module) { /*set actions code here*/ }',
},
},
dataSource: {
offline: [],
globalConfig: {
fit: {
compiled: '',
source: '',
type: 'js',
error: {},
},
},
online: [],
sync: true,
list: [],
},
children: [
{
componentName: 'Dialog',
id: 'modal',
props: {
title: {
type: 'i18n',
use: 'zh-CN',
'en-US': 'Dialog Title',
'zh-CN': 'Dialog标题',
},
visible: false,
hasMask: true,
closeable: 'esc',
autoFocus: true,
footer: true,
footerAlign: 'right',
footerActions: 'cancel,ok',
confirmText: {
type: 'i18n',
use: 'zh-CN',
'en-US': 'Confirm',
'zh-CN': '确定',
},
cancelText: {
type: 'i18n',
use: 'zh-CN',
'en-US': 'Cancel',
'zh-CN': '取消',
},
confirmStyle: 'primary',
confirmState: '确定',
__style__: {},
fieldId: 'dialog_kijq2hni',
popupOutDialog: true,
},
},
{
componentName: 'RootHeader',
id: 'node_k1ow3cba',
props: {},
condition: true,
children: [
{
componentName: 'PageHeader',
id: 'node_k1ow3cbd',
props: {
extraContent: '',
__slot__extraContent: false,
__slot__action: false,
title: {
// type: 'JSSlot',
value: [
{
componentName: 'Text',
id: 'node_k1ow3cbf',
props: {
showTitle: false,
behavior: 'NORMAL',
content: {
use: 'zh-CN',
'en-US': 'Title',
'zh-CN': '个人信息',
type: 'i18n',
},
__style__: {},
fieldId: 'text_k1ow3h1j',
maxLine: 0,
},
condition: true,
},
],
},
content: '',
__slot__logo: false,
__slot__crumb: false,
crumb: '',
tab: '',
logo: '',
action: '',
__slot__tab: false,
__style__: {},
__slot__content: false,
fieldId: 'pageHeader_k1ow3h1i',
subTitle: false,
},
condition: true,
},
],
},
{
componentName: 'RootContent',
id: 'node_k1ow3cbb',
props: {
contentBgColor: 'transparent',
contentPadding: '0',
contentMargin: '20',
},
condition: true,
children: [
{
componentName: 'Form',
id: 'form',
extraPropA: 'extraPropA',
props: {
size: 'medium',
labelAlign: 'top',
autoValidate: true,
scrollToFirstError: true,
autoUnmount: true,
behavior: 'NORMAL',
dataSource: {
type: 'variable',
variable: 'state.formData',
},
obj: {
a: 1,
b: false,
c: 'string',
},
__style__: {},
fieldId: 'form',
fieldOptions: {},
slotA: '',
},
condition: true,
children: [
{
componentName: 'Card',
id: 'node_k1ow3cbj',
props: {
__slot__title: false,
subTitle: {
use: 'zh-CN',
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
__slot__subTitle: false,
extra: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
className: 'card_kgaqfbm5',
title: {
use: 'zh-CN',
'en-US': 'Title',
'zh-CN': '基本信息',
type: 'i18n',
},
__slot__extra: false,
showHeadDivider: true,
__style__: ':root {\n margin-bottom: 12px;\n}',
showTitleBullet: true,
contentHeight: '',
fieldId: 'card_k1ow3h1l',
dividerNoInset: false,
},
condition: true,
children: [
{
componentName: 'CardContent',
id: 'node_k1ow3cbk',
props: {},
condition: true,
children: [
{
componentName: 'ColumnsLayout',
id: 'node_k1ow3cbw',
props: {
layout: '6:6',
columnGap: '20',
rowGap: 0,
__style__: {},
fieldId: 'columns_k1ow3h1v',
},
condition: true,
children: [
{
componentName: 'Column',
id: 'node_k1ow3cbx',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjm',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cbz',
props: {
fieldName: 'name',
hasClear: false,
autoFocus: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please input',
'zh-CN': '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
addonBefore: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
validation: [
{
type: 'required',
},
],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h1w',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'TextField',
'zh-CN': '姓名',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': null,
'zh-CN': '',
},
},
condition: true,
},
{
componentName: 'TextField',
id: 'node_k1ow3cc1',
props: {
fieldName: 'englishName',
hasClear: false,
autoFocus: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please input',
'zh-CN': '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
addonBefore: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h1y',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'TextField',
'zh-CN': '英文名',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': null,
'zh-CN': '',
},
},
condition: true,
},
{
componentName: 'TextField',
id: 'node_k1ow3cc3',
props: {
fieldName: 'jobTitle',
hasClear: false,
autoFocus: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please input',
'zh-CN': '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
addonBefore: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h20',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'TextField',
'zh-CN': '职位',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': null,
'zh-CN': '',
},
},
condition: true,
},
],
},
{
componentName: 'Column',
id: 'node_k1ow3cby',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjn',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc2',
props: {
fieldName: 'nickName',
hasClear: false,
autoFocus: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please input',
'zh-CN': '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
addonBefore: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h1z',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'TextField',
'zh-CN': '花名',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': null,
'zh-CN': '',
},
},
condition: true,
},
{
componentName: 'SelectField',
id: 'node_k1ow3cc0',
props: {
fieldName: 'gender',
hasClear: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
mode: 'single',
showSearch: false,
autoWidth: true,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please select',
'zh-CN': '请选择',
type: 'i18n',
},
hasBorder: true,
behavior: 'NORMAL',
value: '',
validation: [
{
type: 'required',
},
],
__style__: {},
fieldId: 'select_k1ow3h1x',
notFoundContent: {
use: 'zh-CN',
type: 'i18n',
},
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'SelectField',
'zh-CN': '性别',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
wrapperColOffset: 0,
hasSelectAll: false,
hasArrow: true,
size: 'medium',
labelAlign: 'top',
filterLocal: true,
dataSource: [
{
defaultChecked: false,
text: {
'en-US': 'Option 1',
'zh-CN': '男',
type: 'i18n',
__sid__: 'param_k1owc4tb',
},
__sid__: 'serial_k1owc4t1',
value: 'M',
sid: 'opt_k1owc4t2',
},
{
defaultChecked: false,
text: {
'en-US': 'Option 2',
'zh-CN': '女',
type: 'i18n',
__sid__: 'param_k1owc4tf',
},
__sid__: 'serial_k1owc4t2',
value: 'F',
sid: 'opt_k1owc4t3',
},
],
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': null,
'zh-CN': '',
},
searchDelay: 300,
},
condition: true,
},
],
},
],
},
],
},
],
},
{
componentName: 'Card',
id: 'node_k1ow3cbl',
props: {
__slot__title: false,
subTitle: {
use: 'zh-CN',
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
__slot__subTitle: false,
extra: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
className: 'card_kgaqfbm6',
title: {
use: 'zh-CN',
'en-US': 'Title',
'zh-CN': '部门信息',
type: 'i18n',
},
__slot__extra: false,
showHeadDivider: true,
__style__: ':root {\n margin-bottom: 12px;\n}',
showTitleBullet: true,
contentHeight: '',
fieldId: 'card_k1ow3h1m',
dividerNoInset: false,
},
condition: true,
children: [
{
componentName: 'CardContent',
id: 'node_k1ow3cbm',
props: {},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc4',
props: {
fieldName: 'department',
hasClear: false,
autoFocus: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please input',
'zh-CN': '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
addonBefore: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h21',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'TextField',
'zh-CN': '所属部门',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': null,
'zh-CN': '',
},
},
condition: true,
},
{
componentName: 'ColumnsLayout',
id: 'node_k1ow3cc5',
props: {
layout: '6:6',
columnGap: '20',
rowGap: 0,
__style__: {},
fieldId: 'columns_k1ow3h22',
},
condition: true,
children: [
{
componentName: 'Column',
id: 'node_k1ow3cc6',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjo',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc8',
props: {
fieldName: 'leader',
hasClear: false,
autoFocus: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please input',
'zh-CN': '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
addonBefore: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h23',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'TextField',
'zh-CN': '主管',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': null,
'zh-CN': '',
},
},
condition: true,
},
],
},
{
componentName: 'Column',
id: 'node_k1ow3cc7',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjp',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc9',
props: {
fieldName: 'hrg',
hasClear: false,
autoFocus: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please input',
'zh-CN': '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
addonBefore: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h24',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'TextField',
'zh-CN': 'HRG',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': null,
'zh-CN': '',
},
},
condition: true,
},
],
},
],
},
],
},
],
},
{
componentName: 'Div',
id: 'node_k1ow3cbo',
props: {
className: 'div_kgaqfbm9',
behavior: 'NORMAL',
__style__:
':root {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
events: {},
fieldId: 'div_k1ow3h1o',
useFieldIdAsDomId: false,
customClassName: '',
},
condition: true,
children: [
{
componentName: 'Button',
id: 'node_k1ow3cbn',
props: {
triggerEventsWhenLoading: false,
onClick: {
rawType: 'events',
type: 'JSExpression',
value: 'this.utils.legaoBuiltin.execEventFlow.bind(this, [this.submit])',
events: [
{
name: 'submit',
id: 'submit',
params: {},
type: 'actionRef',
uuid: '1570966253282_0',
},
],
},
size: 'medium',
baseIcon: '',
otherIcon: '',
className: 'button_kgaqfbm7',
type: 'primary',
behavior: 'NORMAL',
loading: false,
content: {
use: 'zh-CN',
'en-US': 'Button',
'zh-CN': '提交',
type: 'i18n',
},
__style__: ':root {\n margin-right: 16px;\n width: 80px\n}',
fieldId: 'button_k1ow3h1n',
},
condition: true,
},
{
componentName: 'Button',
id: 'node_k1ow3cbp',
props: {
triggerEventsWhenLoading: false,
size: 'medium',
baseIcon: '',
otherIcon: '',
className: 'button_kgaqfbm8',
type: 'normal',
behavior: 'NORMAL',
loading: false,
content: {
use: 'zh-CN',
'en-US': 'Button',
'zh-CN': '取消',
type: 'i18n',
},
__style__: ':root {\n width: 80px;\n}',
fieldId: 'button_k1ow3h1p',
greeting: {
// type: 'JSSlot',
value: [
{
componentName: 'Text',
props: {},
},
],
},
},
condition: true,
},
],
},
],
},
],
},
{
componentName: 'RootFooter',
id: 'node_k1ow3cbc',
props: {},
condition: true,
},
],
};
================================================
FILE: packages/designer/tests/fixtures/schema/form.ts
================================================
export default {
componentName: 'Page',
id: 'page',
title: 'hey, i\' a page!',
props: {
extensions: {
启用页头: true,
},
pageStyle: {
backgroundColor: '#f2f3f5',
},
containerStyle: {},
className: 'page_kgaqfbm4',
templateVersion: '1.0.0',
},
lifeCycles: {
constructor: {
type: 'js',
compiled:
"function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
source:
"function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
},
},
condition: true,
css:
'body{background-color:#f2f3f5}.card_kgaqfbm5 {\n margin-bottom: 12px;\n}.card_kgaqfbm6 {\n margin-bottom: 12px;\n}.button_kgaqfbm7 {\n margin-right: 16px;\n width: 80px\n}.button_kgaqfbm8 {\n width: 80px;\n}.div_kgaqfbm9 {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
methods: {
__initMethods__: {
type: 'js',
source: 'function (exports, module) { /*set actions code here*/ }',
compiled: 'function (exports, module) { /*set actions code here*/ }',
},
},
dataSource: {
offline: [],
globalConfig: {
fit: {
compiled: '',
source: '',
type: 'js',
error: {},
},
},
online: [],
sync: true,
list: [],
},
children: [
{
componentName: 'RootHeader',
id: 'node_k1ow3cba',
props: {},
condition: true,
children: [
{
componentName: 'PageHeader',
id: 'node_k1ow3cbd',
props: {
extraContent: '',
__slot__extraContent: false,
__slot__action: false,
title: {
// type: 'JSSlot',
value: [
{
componentName: 'Text',
id: 'node_k1ow3cbf',
props: {
showTitle: false,
behavior: 'NORMAL',
content: {
use: 'zh-CN',
'en-US': 'Title',
'zh-CN': '个人信息',
type: 'i18n',
},
__style__: {},
fieldId: 'text_k1ow3h1j',
maxLine: 0,
},
condition: true,
},
],
},
content: '',
__slot__logo: false,
__slot__crumb: false,
crumb: '',
tab: '',
logo: '',
action: '',
__slot__tab: false,
__style__: {},
__slot__content: false,
fieldId: 'pageHeader_k1ow3h1i',
subTitle: false,
},
condition: true,
},
],
},
{
componentName: 'RootContent',
id: 'node_k1ow3cbb',
props: {
contentBgColor: 'transparent',
contentPadding: '0',
contentMargin: '20',
},
condition: true,
children: [
{
componentName: 'Form',
id: 'form',
extraPropA: 'extraPropA',
props: {
size: 'medium',
labelAlign: 'top',
autoValidate: true,
scrollToFirstError: true,
autoUnmount: true,
behavior: 'NORMAL',
dataSource: {
type: 'variable',
variable: 'state.formData',
},
obj: {
a: 1,
b: false,
c: 'string',
},
__style__: {},
fieldId: 'form',
fieldOptions: {},
slotA: '',
},
condition: true,
children: [
{
componentName: 'Card',
id: 'node_k1ow3cbj',
props: {
__slot__title: false,
subTitle: {
use: 'zh-CN',
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
__slot__subTitle: false,
extra: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
className: 'card_kgaqfbm5',
title: {
use: 'zh-CN',
'en-US': 'Title',
'zh-CN': '基本信息',
type: 'i18n',
},
__slot__extra: false,
showHeadDivider: true,
__style__: ':root {\n margin-bottom: 12px;\n}',
showTitleBullet: true,
contentHeight: '',
fieldId: 'card_k1ow3h1l',
dividerNoInset: false,
},
condition: true,
children: [
{
componentName: 'CardContent',
id: 'node_k1ow3cbk',
props: {},
condition: true,
children: [
{
componentName: 'ColumnsLayout',
id: 'node_k1ow3cbw',
props: {
layout: '6:6',
columnGap: '20',
rowGap: 0,
__style__: {},
fieldId: 'columns_k1ow3h1v',
},
condition: true,
children: [
{
componentName: 'Column',
id: 'node_k1ow3cbx',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjm',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cbz',
props: {
fieldName: 'name',
hasClear: false,
autoFocus: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please input',
'zh-CN': '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
addonBefore: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
validation: [
{
type: 'required',
},
],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h1w',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'TextField',
'zh-CN': '姓名',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': '',
'zh-CN': '',
},
},
condition: true,
},
{
componentName: 'TextField',
id: 'node_k1ow3cc1',
props: {
fieldName: 'englishName',
hasClear: false,
autoFocus: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please input',
'zh-CN': '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
addonBefore: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h1y',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'TextField',
'zh-CN': '英文名',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': '',
'zh-CN': '',
},
},
condition: true,
},
{
componentName: 'TextField',
id: 'node_k1ow3cc3',
props: {
fieldName: 'jobTitle',
hasClear: false,
autoFocus: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please input',
'zh-CN': '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
addonBefore: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h20',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'TextField',
'zh-CN': '职位',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': '',
'zh-CN': '',
},
},
condition: true,
},
],
},
{
componentName: 'Column',
id: 'node_k1ow3cby',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjn',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc2',
props: {
fieldName: 'nickName',
hasClear: false,
autoFocus: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please input',
'zh-CN': '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
addonBefore: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h1z',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'TextField',
'zh-CN': '花名',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': '',
'zh-CN': '',
},
},
condition: true,
},
{
componentName: 'SelectField',
id: 'node_k1ow3cc0',
props: {
fieldName: 'gender',
hasClear: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
mode: 'single',
showSearch: false,
autoWidth: true,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please select',
'zh-CN': '请选择',
type: 'i18n',
},
hasBorder: true,
behavior: 'NORMAL',
value: '',
validation: [
{
type: 'required',
},
],
__style__: {},
fieldId: 'select_k1ow3h1x',
notFoundContent: {
use: 'zh-CN',
type: 'i18n',
},
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'SelectField',
'zh-CN': '性别',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
wrapperColOffset: 0,
hasSelectAll: false,
hasArrow: true,
size: 'medium',
labelAlign: 'top',
filterLocal: true,
dataSource: [
{
defaultChecked: false,
text: {
'en-US': 'Option 1',
'zh-CN': '男',
type: 'i18n',
__sid__: 'param_k1owc4tb',
},
__sid__: 'serial_k1owc4t1',
value: 'M',
sid: 'opt_k1owc4t2',
},
{
defaultChecked: false,
text: {
'en-US': 'Option 2',
'zh-CN': '女',
type: 'i18n',
__sid__: 'param_k1owc4tf',
},
__sid__: 'serial_k1owc4t2',
value: 'F',
sid: 'opt_k1owc4t3',
},
],
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': '',
'zh-CN': '',
},
searchDelay: 300,
},
condition: true,
},
],
},
],
},
],
},
],
},
{
componentName: 'Card',
id: 'node_k1ow3cbl',
props: {
__slot__title: false,
subTitle: {
use: 'zh-CN',
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
__slot__subTitle: false,
extra: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
className: 'card_kgaqfbm6',
title: {
use: 'zh-CN',
'en-US': 'Title',
'zh-CN': '部门信息',
type: 'i18n',
},
__slot__extra: false,
showHeadDivider: true,
__style__: ':root {\n margin-bottom: 12px;\n}',
showTitleBullet: true,
contentHeight: '',
fieldId: 'card_k1ow3h1m',
dividerNoInset: false,
},
condition: true,
children: [
{
componentName: 'CardContent',
id: 'node_k1ow3cbm',
props: {},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc4',
props: {
fieldName: 'department',
hasClear: false,
autoFocus: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please input',
'zh-CN': '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
addonBefore: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h21',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'TextField',
'zh-CN': '所属部门',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': '',
'zh-CN': '',
},
},
condition: true,
},
{
componentName: 'ColumnsLayout',
id: 'node_k1ow3cc5',
props: {
layout: '6:6',
columnGap: '20',
rowGap: 0,
__style__: {},
fieldId: 'columns_k1ow3h22',
},
condition: true,
children: [
{
componentName: 'Column',
id: 'node_k1ow3cc6',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjo',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc8',
props: {
fieldName: 'leader',
hasClear: false,
autoFocus: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please input',
'zh-CN': '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
addonBefore: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h23',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'TextField',
'zh-CN': '主管',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': '',
'zh-CN': '',
},
},
condition: true,
},
],
},
{
componentName: 'Column',
id: 'node_k1ow3cc7',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjp',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc9',
props: {
fieldName: 'hrg',
hasClear: false,
autoFocus: false,
tips: {
'en-US': '',
'zh-CN': '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh-CN',
'en-US': 'please input',
'zh-CN': '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
addonBefore: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h24',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh-CN',
'en-US': 'TextField',
'zh-CN': 'HRG',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh-CN',
'zh-CN': '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh-CN',
'en-US': '',
'zh-CN': '',
},
},
condition: true,
},
],
},
],
},
],
},
],
},
{
componentName: 'Div',
id: 'node_k1ow3cbo',
props: {
className: 'div_kgaqfbm9',
behavior: 'NORMAL',
__style__:
':root {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
events: {},
fieldId: 'div_k1ow3h1o',
useFieldIdAsDomId: false,
customClassName: '',
},
condition: true,
children: [
{
componentName: 'Button',
id: 'node_k1ow3cbn',
props: {
triggerEventsWhenLoading: false,
onClick: {
rawType: 'events',
type: 'JSExpression',
value: 'this.utils.legaoBuiltin.execEventFlow.bind(this, [this.submit])',
events: [
{
name: 'submit',
id: 'submit',
params: {},
type: 'actionRef',
uuid: '1570966253282_0',
},
],
},
size: 'medium',
baseIcon: '',
otherIcon: '',
className: 'button_kgaqfbm7',
type: 'primary',
behavior: 'NORMAL',
loading: false,
content: {
use: 'zh-CN',
'en-US': 'Button',
'zh-CN': '提交',
type: 'i18n',
},
__style__: ':root {\n margin-right: 16px;\n width: 80px\n}',
fieldId: 'button_k1ow3h1n',
},
condition: true,
},
{
componentName: 'Button',
id: 'node_k1ow3cbp',
props: {
triggerEventsWhenLoading: false,
size: 'medium',
baseIcon: '',
otherIcon: '',
className: 'button_kgaqfbm8',
type: 'normal',
behavior: 'NORMAL',
loading: false,
content: {
use: 'zh-CN',
'en-US': 'Button',
'zh-CN': '取消',
type: 'i18n',
},
__style__: ':root {\n width: 80px;\n}',
fieldId: 'button_k1ow3h1p',
greeting: {
// type: 'JSSlot',
value: [{
componentName: 'Text',
props: {},
}],
},
},
condition: true,
},
],
},
],
},
],
},
{
componentName: 'RootFooter',
id: 'node_k1ow3cbc',
props: {},
condition: true,
},
],
i18n: {
'zh-CN': {
'i18n-jwg27yo4': '你好',
'i18n-jwg27yo3': '中国',
},
'en-US': {
'i18n-jwg27yo4': 'Hello',
'i18n-jwg27yo3': 'China',
},
},
};
================================================
FILE: packages/designer/tests/fixtures/schema/setting.ts
================================================
export default {
componentName: 'Page',
id: 'page',
title: 'hey, i\' a page!',
props: {
extensions: {
启用页头: true,
},
pageStyle: {
backgroundColor: '#f2f3f5',
},
containerStyle: {},
className: 'page_kgaqfbm4',
templateVersion: '1.0.0',
},
lifeCycles: {
constructor: {
type: 'js',
compiled:
"function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
source:
"function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
},
},
condition: true,
css:
'body{background-color:#f2f3f5}.card_kgaqfbm5 {\n margin-bottom: 12px;\n}.card_kgaqfbm6 {\n margin-bottom: 12px;\n}.button_kgaqfbm7 {\n margin-right: 16px;\n width: 80px\n}.button_kgaqfbm8 {\n width: 80px;\n}.div_kgaqfbm9 {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
methods: {
__initMethods__: {
type: 'js',
source: 'function (exports, module) { /*set actions code here*/ }',
compiled: 'function (exports, module) { /*set actions code here*/ }',
},
},
children: [
{
componentName: 'Div',
id: 'div',
props: {
className: 'div_kgaqfbm9',
behavior: 'NORMAL',
__style__:
':root {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
events: {},
fieldId: 'div_k1ow3h1o',
useFieldIdAsDomId: false,
customClassName: {
type: 'JSExpression',
value: 'getFromSomewhere()',
},
customClassName2: {
type: 'JSExpression',
mock: { hi: 'mock' },
value: 'getFromSomewhere()',
},
},
extraPropA: 'haha',
},
{
componentName: 'Div',
id: 'div2',
props: {
className: 'div_kgaqfbm9',
behavior: 'NORMAL',
__style__:
':root {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
events: {},
fieldId: 'div_k1ow3h1o',
useFieldIdAsDomId: false,
customClassName: '',
},
extraPropA: 'haha',
},
{
componentName: 'Test',
id: 'test',
props: {
className: 'div_kgaqfbm9',
behavior: 'NORMAL',
__style__:
':root {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
events: {},
fieldId: 'div_k1ow3h1o',
useFieldIdAsDomId: false,
customClassName: '',
},
extraPropA: 'haha',
},
],
};
================================================
FILE: packages/designer/tests/main/simulator.test.ts
================================================
import '../fixtures/window';
import { isSimulatorHost } from '../../src/simulator';
it('isSimulatorHost', () => {
expect(isSimulatorHost({ isSimulator: true })).toBeTruthy();
expect(isSimulatorHost({ a: 1 })).toBeFalsy();
});
================================================
FILE: packages/designer/tests/main/meta/component-meta.test.ts
================================================
import '../../fixtures/window';
import { Designer } from '../../../src/designer/designer';
import divMeta from '../../fixtures/component-metadata/div';
import div2Meta from '../../fixtures/component-metadata/div2';
import div3Meta from '../../fixtures/component-metadata/div3';
import div4Meta from '../../fixtures/component-metadata/div4';
import div5Meta from '../../fixtures/component-metadata/div5';
import div6Meta from '../../fixtures/component-metadata/div6';
import div7Meta from '../../fixtures/component-metadata/div7';
import div8Meta from '../../fixtures/component-metadata/div8';
import div9Meta from '../../fixtures/component-metadata/div9';
import div10Meta from '../../fixtures/component-metadata/div10';
import abcgroup from '../../fixtures/component-metadata/abcgroup';
import abcitem from '../../fixtures/component-metadata/abcitem';
import abcnode from '../../fixtures/component-metadata/abcnode';
import abcoption from '../../fixtures/component-metadata/abcoption';
import page2Meta from '../../fixtures/component-metadata/page2';
import {
ComponentMeta,
isComponentMeta,
ensureAList,
buildFilter,
} from '../../../src/component-meta';
jest.mock('../../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
const { ComponentActions } = require('../../../src/component-actions');
return {
getGlobalComponentActions: () => [],
componentActions: new ComponentActions(),
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({} as any);
});
describe('组件元数据处理', () => {
it('构造函数', () => {
const meta = new ComponentMeta(designer, divMeta);
expect(meta.isContainer).toBeTruthy();
expect(isComponentMeta(meta)).toBeTruthy();
expect(meta.acceptable).toBeFalsy();
expect(meta.isRootComponent()).toBeFalsy();
expect(meta.isModal).toBeFalsy();
expect(meta.rootSelector).toBeUndefined();
expect(meta.liveTextEditing).toBeUndefined();
expect(meta.descriptor).toBeUndefined();
expect(typeof meta.icon).toBe('function');
expect(meta.getMetadata().title).toBe('容器');
expect(meta.title).toEqual({ type: 'i18n', 'en-US': 'Div', 'zh-CN': '容器' });
expect(meta.isMinimalRenderUnit).toBeFalsy();
expect(meta.isTopFixed).toBeFalsy();
meta.setNpm({ package: '@ali/vc-div', componentName: 'Div' });
expect(meta.npm).toEqual({ package: '@ali/vc-div', componentName: 'Div' });
meta.npm = { package: '@ali/vc-div', componentName: 'Div' };
expect(meta.npm).toEqual({ package: '@ali/vc-div', componentName: 'Div' });
const mockFn = jest.fn();
const offFn = meta.onMetadataChange(mockFn);
meta.setMetadata(divMeta);
expect(mockFn).toHaveBeenCalledTimes(1);
offFn();
meta.setMetadata(divMeta);
// 不会再触发函数
expect(mockFn).toHaveBeenCalledTimes(1);
});
it('构造函数 - 兼容场景(title 是个普通对象)', () => {
const meta = new ComponentMeta(designer, div2Meta);
expect(meta.title).toEqual('容器');
expect(meta.isTopFixed).toBeTruthy();
});
it('构造函数 - 兼容场景(title fallback 到 componentName)', () => {
const meta = new ComponentMeta(designer, div3Meta);
expect(meta.title).toEqual('Div');
});
it('构造函数 - 兼容场景(configure 是个数组)', () => {
const meta = new ComponentMeta(designer, div4Meta);
expect(meta.configure).toEqual(div4Meta.configure);
});
it('构造函数 - 兼容场景(使用 experimental)', () => {
const meta = new ComponentMeta(designer, div6Meta);
expect(meta.getMetadata().configure.advanced.initials).toHaveLength(9);
});
it('构造函数 - 兼容场景(没有 configure.component)', () => {
const meta = new ComponentMeta(designer, div7Meta);
expect(meta.isContainer).toBeFalsy();
expect(meta.isModal).toBeFalsy();
});
it('构造函数 - 兼容场景(没有 configure)', () => {
const meta = new ComponentMeta(designer, div8Meta);
expect(meta.configure).toEqual([]);
});
it('构造函数 - 兼容场景(没有 npm)', () => {
const meta = new ComponentMeta(designer, div9Meta);
expect(meta.npm).toBeUndefined();
meta.setNpm({ package: '@ali/vc-div', componentName: 'Div' });
expect(meta.npm).toEqual({ package: '@ali/vc-div', componentName: 'Div' });
});
it('availableActions', () => {
const meta = new ComponentMeta(designer, divMeta);
expect(meta.availableActions).toHaveLength(5);
expect(meta.availableActions[0].name).toBe('remove');
expect(meta.availableActions[1].name).toBe('hide');
expect(meta.availableActions[2].name).toBe('copy');
designer.componentActions.removeBuiltinComponentAction('remove');
expect(meta.availableActions).toHaveLength(4);
expect(meta.availableActions[0].name).toBe('hide');
expect(meta.availableActions[1].name).toBe('copy');
designer.componentActions.addBuiltinComponentAction({
name: 'new',
content: {
action() {},
},
});
expect(meta.availableActions).toHaveLength(5);
expect(meta.availableActions[0].name).toBe('hide');
expect(meta.availableActions[1].name).toBe('copy');
expect(meta.availableActions[4].name).toBe('new');
});
it('availableActions - disableBehaviors: *', () => {
const meta = new ComponentMeta(designer, div5Meta);
expect(meta.availableActions).toHaveLength(0);
});
it('availableActions - rootCompoment', () => {
const meta = new ComponentMeta(designer, page2Meta);
// (hide + new) left
expect(meta.availableActions).toHaveLength(2);
});
describe('checkNesting', () => {
const mockNode = (componentName) => {
return {
internalToShellNode() {
return {
componentName,
};
},
isNode: true,
};
};
const mockNodeForm = mockNode('Form');
const mockNodeImage = mockNode('Image');
const mockNodeDiv = mockNode('Div');
it('checkNestingUp', () => {
const meta1 = new ComponentMeta(designer, divMeta);
// 没有配置 parentWhitelist,判断默认为 true
expect(meta1.checkNestingUp(mockNodeDiv, mockNodeDiv)).toBeTruthy();
const meta2 = new ComponentMeta(designer, div10Meta);
expect(meta2.checkNestingUp(mockNodeDiv, mockNodeForm)).toBeTruthy();
expect(meta2.checkNestingUp(mockNodeDiv, mockNodeDiv)).toBeFalsy();
});
it('checkNestingDown', () => {
const meta1 = new ComponentMeta(designer, divMeta);
// 没有配置 childWhitelist,判断默认为 true
expect(meta1.checkNestingDown(mockNodeDiv, mockNodeDiv)).toBeTruthy();
const meta2 = new ComponentMeta(designer, div10Meta);
expect(meta2.checkNestingDown(mockNodeDiv, mockNodeForm)).toBeFalsy();
expect(meta2.checkNestingDown(mockNodeDiv, mockNodeImage)).toBeTruthy();
});
});
});
describe('组件元数据 transducers', () => {
it('legacyIssues', () => {
const legacyMeta: any = {
...divMeta,
devMode: 'procode',
};
const meta = new ComponentMeta(designer, legacyMeta);
const metadata = meta.getMetadata();
expect(metadata.devMode).toBe('proCode');
});
});
describe('帮助函数', () => {
it('ensureAList', () => {
expect(ensureAList()).toBeNull();
expect(ensureAList(1)).toBeNull();
expect(ensureAList([])).toBeNull();
expect(ensureAList('copy lock')).toEqual(['copy', 'lock']);
expect(ensureAList(['copy', 'lock'])).toEqual(['copy', 'lock']);
});
it('buildFilter', () => {
const mockFn = () => {};
expect(buildFilter()).toBeNull();
expect(buildFilter([])).toBeNull();
expect(buildFilter(mockFn)).toBe(mockFn);
const mockRE = /xxx/;
const filter = buildFilter(mockRE);
expect(filter({ componentName: 'xxx' })).toBeTruthy();
expect(filter({ componentName: 'yyy' })).toBeFalsy();
expect(buildFilter('xxx yyy')({ componentName: 'xxx' })).toBeTruthy();
expect(buildFilter('xxx yyy')({ componentName: 'zzz' })).toBeFalsy();
});
it('registerMetadataTransducer', () => {
expect(designer.componentActions.getRegisteredMetadataTransducers()).toHaveLength(2);
// 插入到 legacy-issues 和 component-defaults 的中间
designer.componentActions.registerMetadataTransducer((metadata) => metadata, 3, 'noop');
expect(designer.componentActions.getRegisteredMetadataTransducers()).toHaveLength(3);
designer.componentActions.registerMetadataTransducer((metadata) => metadata);
expect(designer.componentActions.getRegisteredMetadataTransducers()).toHaveLength(4);
});
it('modifyBuiltinComponentAction', () => {
designer.componentActions.modifyBuiltinComponentAction('copy', (action) => {
expect(action.name).toBe('copy');
});
});
});
describe('transducers', () => {
it('componentDefaults', () => {
const meta1 = new ComponentMeta(designer, abcgroup);
const meta2 = new ComponentMeta(designer, abcitem);
const meta3 = new ComponentMeta(designer, abcnode);
const meta4 = new ComponentMeta(designer, abcoption);
expect(meta1.getMetadata().configure.component.nestingRule.childWhitelist).toEqual(['Abc']);
expect(meta2.getMetadata().configure.component.nestingRule.parentWhitelist).toEqual(['Abc']);
expect(meta3.getMetadata().configure.component.nestingRule.parentWhitelist).toEqual(['Abc', 'Abc.Node']);
expect(meta4.getMetadata().configure.component.nestingRule.parentWhitelist).toEqual(['Abc']);
});
});
================================================
FILE: packages/designer/tests/plugin/plugin-manager.test.ts
================================================
import '../fixtures/window';
import { Editor, engineConfig } from '@alilc/lowcode-editor-core';
import { LowCodePluginManager } from '../../src/plugin/plugin-manager';
import { IPublicModelPluginContext, IPublicApiPlugins } from '@alilc/lowcode-types';
import { ILowCodePluginContextPrivate } from '../../src/plugin/plugin-types';
const editor = new Editor();
let contextApiAssembler;
describe('plugin 测试', () => {
let pluginManager: IPublicApiPlugins;
beforeEach(() => {
contextApiAssembler = {
assembleApis(context: ILowCodePluginContextPrivate){
context.plugins = pluginManager as IPublicApiPlugins;
// mock set apis
}
};
pluginManager = new LowCodePluginManager(contextApiAssembler).toProxy();
});
afterEach(() => {
pluginManager.dispose();
});
it('注册插件,插件参数生成函数能被调用,且能拿到正确的 ctx ', () => {
const mockFn = jest.fn();
const creator2 = (ctx: IPublicModelPluginContext) => {
mockFn(ctx);
return {
init: jest.fn(),
};
};
creator2.pluginName = 'demo1';
pluginManager.register(creator2);
const [expectedCtx] = mockFn.mock.calls[0];
expect(expectedCtx).toHaveProperty('project');
expect(expectedCtx).toHaveProperty('setters');
expect(expectedCtx).toHaveProperty('material');
expect(expectedCtx).toHaveProperty('hotkey');
expect(expectedCtx).toHaveProperty('plugins');
expect(expectedCtx).toHaveProperty('skeleton');
expect(expectedCtx).toHaveProperty('logger');
expect(expectedCtx).toHaveProperty('config');
expect(expectedCtx).toHaveProperty('event');
expect(expectedCtx).toHaveProperty('preference');
});
it('注册插件,调用插件 init 方法', async () => {
const mockFn = jest.fn();
const creator2 = (ctx: IPublicModelPluginContext) => {
return {
init: mockFn,
exports() {
return {
x: 1,
y: 2,
};
},
};
};
creator2.pluginName = 'demo1';
pluginManager.register(creator2);
await pluginManager.init();
expect(pluginManager.size).toBe(1);
expect(pluginManager.has('demo1')).toBeTruthy();
expect(pluginManager.get('demo1')!.isInited()).toBeTruthy();
expect(pluginManager.demo1).toBeTruthy();
expect(pluginManager.demo1.x).toBe(1);
expect(pluginManager.demo1.y).toBe(2);
expect(pluginManager.demo1.z).toBeUndefined();
expect(mockFn).toHaveBeenCalled();
});
it('注册插件,调用 setDisabled 方法', async () => {
const mockFn = jest.fn();
const creator2 = (ctx: IPublicModelPluginContext) => {
return {
init: mockFn,
};
};
creator2.pluginName = 'demo1';
pluginManager.register(creator2);
await pluginManager.init();
expect(pluginManager.demo1).toBeTruthy();
pluginManager.setDisabled('demo1', true);
expect(pluginManager.demo1).toBeUndefined();
});
it('注册插件,调用 plugin.setDisabled 方法', async () => {
const mockFn = jest.fn();
const creator2 = (ctx: IPublicModelPluginContext) => {
return {
init: mockFn,
};
};
creator2.pluginName = 'demo1';
pluginManager.register(creator2);
await pluginManager.init();
expect(pluginManager.demo1).toBeTruthy();
pluginManager.get('demo1').setDisabled();
expect(pluginManager.demo1).toBeUndefined();
});
it('删除插件,调用插件 destroy 方法', async () => {
const mockFn = jest.fn();
const creator2 = (ctx: IPublicModelPluginContext) => {
return {
init: jest.fn(),
destroy: mockFn,
};
};
creator2.pluginName = 'demo1';
pluginManager.register(creator2);
await pluginManager.init();
await pluginManager.delete('demo1');
expect(mockFn).toHaveBeenCalled();
await pluginManager.delete('non-existing');
});
describe('dependencies 依赖', () => {
it('dependencies 依赖', async () => {
const mockFn = jest.fn();
const creator21 = (ctx: IPublicModelPluginContext) => {
return {
init: () => mockFn('demo1'),
};
};
creator21.pluginName = 'demo1';
creator21.meta = {
dependencies: ['demo2'],
};
pluginManager.register(creator21);
const creator22 = (ctx: IPublicModelPluginContext) => {
return {
init: () => mockFn('demo2'),
};
};
creator22.pluginName = 'demo2';
pluginManager.register(creator22);
await pluginManager.init();
expect(mockFn).toHaveBeenNthCalledWith(1, 'demo2');
expect(mockFn).toHaveBeenNthCalledWith(2, 'demo1');
});
it('dependencies 依赖 - string', async () => {
const mockFn = jest.fn();
const creator21 = (ctx: IPublicModelPluginContext) => {
return {
init: () => mockFn('demo1'),
};
};
creator21.pluginName = 'demo1';
creator21.meta = {
dependencies: 'demo2',
};
pluginManager.register(creator21);
const creator22 = (ctx: IPublicModelPluginContext) => {
return {
init: () => mockFn('demo2'),
};
};
creator22.pluginName = 'demo2';
pluginManager.register(creator22);
await pluginManager.init();
expect(mockFn).toHaveBeenNthCalledWith(1, 'demo2');
expect(mockFn).toHaveBeenNthCalledWith(2, 'demo1');
});
it('dependencies 依赖 - 兼容 dep', async () => {
const mockFn = jest.fn();
const creator21 = (ctx: IPublicModelPluginContext) => {
return {
dep: ['demo4'],
init: () => mockFn('demo3'),
};
};
creator21.pluginName = 'demo3';
pluginManager.register(creator21);
const creator22 = (ctx: IPublicModelPluginContext) => {
return {
init: () => mockFn('demo4'),
};
};
creator22.pluginName = 'demo4';
pluginManager.register(creator22);
await pluginManager.init();
expect(mockFn).toHaveBeenNthCalledWith(1, 'demo4');
expect(mockFn).toHaveBeenNthCalledWith(2, 'demo3');
});
it('dependencies 依赖 - 兼容 dep & string', async () => {
const mockFn = jest.fn();
const creator21 = (ctx: IPublicModelPluginContext) => {
return {
dep: 'demo4',
init: () => mockFn('demo3'),
};
};
creator21.pluginName = 'demo3';
pluginManager.register(creator21);
const creator22 = (ctx: IPublicModelPluginContext) => {
return {
init: () => mockFn('demo4'),
};
};
creator22.pluginName = 'demo4';
pluginManager.register(creator22);
await pluginManager.init();
expect(mockFn).toHaveBeenNthCalledWith(1, 'demo4');
expect(mockFn).toHaveBeenNthCalledWith(2, 'demo3');
});
});
it('version 依赖', async () => {
const mockFn = jest.fn();
const creator21 = (ctx: IPublicModelPluginContext) => {
return {
init: () => mockFn('demo1'),
};
};
creator21.pluginName = 'demo1';
creator21.meta = {
engines: {
lowcodeEngine: '^1.1.0',
},
};
engineConfig.set('ENGINE_VERSION', '1.0.1');
console.log('version: ', engineConfig.get('ENGINE_VERSION'));
// not match should skip
pluginManager.register(creator21).catch((e) => {
expect(e).toEqual(
new Error(
'plugin demo1 skipped, engine check failed, current engine version is 1.0.1, meta.engines.lowcodeEngine is ^1.1.0',
),
);
});
expect(pluginManager.plugins.length).toBe(0);
const creator22 = (ctx: IPublicModelPluginContext) => {
return {
init: () => mockFn('demo2'),
};
};
creator22.pluginName = 'demo2';
creator22.meta = {
engines: {
lowcodeEngine: '^1.0.1',
},
};
engineConfig.set('ENGINE_VERSION', '1.0.3');
pluginManager.register(creator22);
expect(pluginManager.plugins.length).toBe(1);
const creator23 = (ctx: IPublicModelPluginContext) => {
return {
init: () => mockFn('demo3'),
};
};
creator23.pluginName = 'demo3';
creator23.meta = {
engines: {
lowcodeEngine: '1.x',
},
};
engineConfig.set('ENGINE_VERSION', '1.1.1');
pluginManager.register(creator23);
expect(pluginManager.plugins.length).toBe(2);
});
it('autoInit 功能', async () => {
const mockFn = jest.fn();
const creator2 = (ctx: IPublicModelPluginContext) => {
return {
init: mockFn,
};
};
creator2.pluginName = 'demo1';
await pluginManager.register(creator2, { autoInit: true });
expect(mockFn).toHaveBeenCalled();
});
it('插件不会重复 init,除非强制重新 init', async () => {
const mockFn = jest.fn();
const creator2 = (ctx: IPublicModelPluginContext) => {
return {
name: 'demo1',
init: mockFn,
};
};
creator2.pluginName = 'demo1';
pluginManager.register(creator2);
await pluginManager.init();
expect(mockFn).toHaveBeenCalledTimes(1);
pluginManager.get('demo1')!.init();
expect(mockFn).toHaveBeenCalledTimes(1);
pluginManager.get('demo1')!.init(true);
expect(mockFn).toHaveBeenCalledTimes(2);
});
it('默认情况不允许重复注册', async () => {
const mockFn = jest.fn();
const mockPlugin = (ctx: IPublicModelPluginContext) => {
return {
init: mockFn,
};
};
mockPlugin.pluginName = 'demoDuplicated';
pluginManager.register(mockPlugin);
pluginManager.register(mockPlugin).catch((e) => {
expect(e).toEqual(new Error('Plugin with name demoDuplicated exists'));
});
await pluginManager.init();
});
it('插件增加 override 参数时可以重复注册', async () => {
const mockFn = jest.fn();
const mockPlugin = (ctx: IPublicModelPluginContext) => {
return {
init: mockFn,
};
};
mockPlugin.pluginName = 'demoOverride';
pluginManager.register(mockPlugin);
pluginManager.register(mockPlugin, { override: true });
await pluginManager.init();
});
it('插件增加 override 参数时可以重复注册, 被覆盖的如果已初始化,会被销毁', async () => {
const mockInitFn = jest.fn();
const mockDestroyFn = jest.fn();
const mockPlugin = (ctx: IPublicModelPluginContext) => {
return {
init: mockInitFn,
destroy: mockDestroyFn,
};
};
mockPlugin.pluginName = 'demoOverride';
await pluginManager.register(mockPlugin, { autoInit: true });
expect(mockInitFn).toHaveBeenCalledTimes(1);
await pluginManager.register(mockPlugin, { override: true });
expect(mockDestroyFn).toHaveBeenCalledTimes(1);
await pluginManager.init();
});
it('dispose 方法', async () => {
const creator2 = (ctx: IPublicModelPluginContext) => {
return {};
};
creator2.pluginName = 'demo1';
pluginManager.register(creator2);
await pluginManager.init();
const plugin = pluginManager.get('demo1')!;
await plugin.dispose();
expect(pluginManager.has('demo1')).toBeFalsy();
});
it('getAll 方法', async () => {
const creator2 = (ctx: IPublicModelPluginContext) => {
return {};
};
creator2.pluginName = 'demo1';
pluginManager.register(creator2);
await pluginManager.init();
expect(pluginManager.getAll()).toHaveLength(1);
});
it('getPluginPreference 方法 - null', async () => {
const creator2 = (ctx: IPublicModelPluginContext) => {
return {};
};
creator2.pluginName = 'demo1';
pluginManager.register(creator2);
await pluginManager.init();
expect(pluginManager.getPluginPreference()).toBeNull();
});
it('getPluginPreference 方法', async () => {
const creator2 = (ctx: IPublicModelPluginContext) => {
return {};
};
const preference = new Map();
preference.set('demo1', { a: 1, b: 2 });
creator2.pluginName = 'demo1';
pluginManager.register(creator2);
await pluginManager.init(preference);
expect(pluginManager.getPluginPreference('demo1')).toEqual({ a: 1, b: 2 });
});
it('注册插件,调用插件 init 方法并传入 preference,可以成功获取', async () => {
const mockFn = jest.fn();
const mockFnForCtx = jest.fn();
const mockFnForCtx2 = jest.fn();
const mockPreference = new Map();
mockPreference.set('demo1', {
key1: 'value for key1',
key2: false,
key3: 123,
key5: 'value for key5, but declared, should not work',
});
const creator2 = (ctx: IPublicModelPluginContext) => {
mockFnForCtx(ctx);
return {
init: jest.fn(),
};
};
creator2.pluginName = 'demo1';
creator2.meta = {
preferenceDeclaration: {
title: 'demo1的的参数定义',
properties: [
{
key: 'key1',
type: 'string',
description: 'this is description for key1',
},
{
key: 'key2',
type: 'boolean',
description: 'this is description for key2',
},
{
key: 'key3',
type: 'number',
description: 'this is description for key3',
},
{
key: 'key4',
type: 'string',
description: 'this is description for key4',
},
],
},
};
const creator22 = (ctx: IPublicModelPluginContext) => {
mockFnForCtx2(ctx);
return {
init: jest.fn(),
};
};
creator22.pluginName = 'demo2';
creator22.meta = {
preferenceDeclaration: {
title: 'demo1的的参数定义',
properties: [
{
key: 'key1',
type: 'string',
description: 'this is description for key1',
},
],
},
};
pluginManager.register(creator2);
pluginManager.register(creator22);
expect(mockFnForCtx).toHaveBeenCalledTimes(1);
await pluginManager.init(mockPreference);
// creator2 only get excuted once
expect(mockFnForCtx).toHaveBeenCalledTimes(1);
const [expectedCtx, expectedOptions] = mockFnForCtx.mock.calls[0];
expect(expectedCtx).toHaveProperty('preference');
// test normal case
expect(expectedCtx.preference.getPreferenceValue('key1', 'default')).toBe('value for key1');
// test default value logic
expect(expectedCtx.preference.getPreferenceValue('key4', 'default for key4')).toBe(
'default for key4',
);
// test undeclared key
expect(expectedCtx.preference.getPreferenceValue('key5', 'default for key5')).toBeUndefined();
// no preference defined
const [expectedCtx2] = mockFnForCtx2.mock.calls[0];
expect(expectedCtx2.preference.getPreferenceValue('key1')).toBeUndefined();
});
it('注册插件,没有填写 pluginName,默认值为 anonymous', async () => {
const mockFn = jest.fn();
const creator2 = (ctx: IPublicModelPluginContext) => {
return {
name: 'xxx',
init: () => mockFn('anonymous'),
};
};
await pluginManager.register(creator2);
expect(pluginManager.get('anonymous')).toBeUndefined();
});
it('自定义/扩展 plugin context', async () => {
const mockFn = jest.fn();
const mockFn2 = jest.fn();
const creator2 = (ctx: IPublicModelPluginContext) => {
mockFn2(ctx);
return {
init: () => mockFn('anonymous'),
};
};
creator2.pluginName = 'yyy';
editor.set('enhancePluginContextHook', (originalContext) => {
originalContext.newProp = 1;
});
await pluginManager.register(creator2);
const [expectedCtx] = mockFn2.mock.calls[0];
expect(expectedCtx).toHaveProperty('newProp');
});
});
================================================
FILE: packages/designer/tests/plugin/plugin-utils.test.ts
================================================
import '../fixtures/window';
import { isValidPreferenceKey, filterValidOptions } from '../../src/plugin/plugin-utils';
describe('plugin utils 测试', () => {
it('isValidPreferenceKey', () => {
expect(isValidPreferenceKey('x')).toBeFalsy();
expect(isValidPreferenceKey('x', { properties: {} })).toBeFalsy();
expect(isValidPreferenceKey('x', { properties: 1 })).toBeFalsy();
expect(isValidPreferenceKey('x', { properties: 'str' })).toBeFalsy();
expect(isValidPreferenceKey('x', { properties: [] })).toBeFalsy();
expect(
isValidPreferenceKey('x', {
title: 'title',
properties: [
{
key: 'y',
type: 'string',
description: 'x desc',
},
],
}),
).toBeFalsy();
expect(
isValidPreferenceKey('x', {
title: 'title',
properties: [
{
key: 'x',
type: 'string',
description: 'x desc',
},
],
}),
).toBeTruthy();
});
it('filterValidOptions', () => {
const mockDeclaration = {
title: 'title',
properties: [
{
key: 'x',
type: 'string',
description: 'x desc',
},
{
key: 'y',
type: 'string',
description: 'y desc',
},
{
key: 'z',
type: 'string',
description: 'z desc',
},
],
};
expect(filterValidOptions()).toBeUndefined();
expect(filterValidOptions(1)).toBe(1);
expect(filterValidOptions({
x: 1,
y: 2,
}, mockDeclaration)).toEqual({
x: 1,
y: 2,
});
expect(filterValidOptions({
x: 1,
y: undefined,
}, mockDeclaration)).toEqual({
x: 1,
});
expect(filterValidOptions({
x: 1,
z: null,
}, mockDeclaration)).toEqual({
x: 1,
});
expect(filterValidOptions({
a: 1,
}, mockDeclaration)).toEqual({
});
});
});
================================================
FILE: packages/designer/tests/plugin/sequencify.test.ts
================================================
import sequencify, { sequence } from '../../src/plugin/sequencify';
describe('sequence', () => {
it('handles tasks with no dependencies', () => {
const tasks = {
task1: { name: 'Task 1', dep: [] },
task2: { name: 'Task 2', dep: [] }
};
const results = [];
const missing = [];
const recursive = [];
sequence({ tasks, names: ['task1', 'task2'], results, missing, recursive, nest: [] });
expect(results).toEqual(['task1', 'task2']);
expect(missing).toEqual([]);
expect(recursive).toEqual([]);
});
it('correctly orders tasks based on dependencies', () => {
const tasks = {
task1: { name: 'Task 1', dep: [] },
task2: { name: 'Task 2', dep: ['task1'] }
};
const results = [];
const missing = [];
const recursive = [];
sequence({ tasks, names: ['task2', 'task1'], results, missing, recursive, nest: [] });
expect(results).toEqual(['task1', 'task2']);
expect(missing).toEqual([]);
expect(recursive).toEqual([]);
});
it('identifies missing tasks', () => {
const tasks = {
task1: { name: 'Task 1', dep: [] }
};
const results = [];
const missing = [];
const recursive = [];
const nest = []
sequence({ tasks, names: ['task2'], results, missing, recursive, nest });
expect(results).toEqual(['task2']);
expect(missing).toEqual(['task2']);
expect(recursive).toEqual([]);
expect(nest).toEqual([]);
});
it('detects recursive dependencies', () => {
const tasks = {
task1: { name: 'Task 1', dep: ['task2'] },
task2: { name: 'Task 2', dep: ['task1'] }
};
const results = [];
const missing = [];
const recursive = [];
const nest = []
sequence({ tasks, names: ['task1', 'task2'], results, missing, recursive, nest });
expect(results).toEqual(['task1', 'task2', 'task1']);
expect(missing).toEqual([]);
expect(recursive).toEqual([['task1', 'task2', 'task1']]);
expect(nest).toEqual([]);
});
});
describe('sequence', () => {
it('should return tasks in sequence without dependencies', () => {
const tasks = {
task1: { name: 'Task 1', dep: [] },
task2: { name: 'Task 2', dep: [] },
task3: { name: 'Task 3', dep: [] }
};
const names = ['task1', 'task2', 'task3'];
const expected = {
sequence: ['task1', 'task2', 'task3'],
missingTasks: [],
recursiveDependencies: []
};
expect(sequencify(tasks, names)).toEqual(expected);
});
it('should handle tasks with dependencies', () => {
const tasks = {
task1: { name: 'Task 1', dep: [] },
task2: { name: 'Task 2', dep: ['task1'] },
task3: { name: 'Task 3', dep: ['task2'] }
};
const names = ['task3', 'task2', 'task1'];
const expected = {
sequence: ['task1', 'task2', 'task3'],
missingTasks: [],
recursiveDependencies: []
};
expect(sequencify(tasks, names)).toEqual(expected);
});
it('should identify missing tasks', () => {
const tasks = {
task1: { name: 'Task 1', dep: [] },
task2: { name: 'Task 2', dep: ['task3'] } // task3 is missing
};
const names = ['task1', 'task2'];
const expected = {
sequence: [],
missingTasks: ['task2.task3'],
recursiveDependencies: []
};
expect(sequencify(tasks, names)).toEqual(expected);
});
it('should detect recursive dependencies', () => {
const tasks = {
task1: { name: 'Task 1', dep: ['task2'] },
task2: { name: 'Task 2', dep: ['task1'] } // Recursive dependency
};
const names = ['task1', 'task2'];
const expected = {
sequence: [],
missingTasks: [],
recursiveDependencies: [['task1', 'task2', 'task1']]
};
expect(sequencify(tasks, names)).toEqual(expected);
});
});
================================================
FILE: packages/designer/tests/project/project-methods.test.ts
================================================
import '../fixtures/window';
import { Editor } from '@alilc/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { DocumentModel } from '../../src/document/document-model';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory';
describe.only('Project 方法测试', () => {
let editor: Editor;
let designer: Designer;
let project: Project;
let doc: DocumentModel;
beforeEach(() => {
editor = new Editor();
designer = new Designer({ editor, shellModelFactory });
project = designer.project;
doc = new DocumentModel(project, formSchema);
});
afterEach(() => {
project.unload();
designer.purge();
editor = null;
designer = null;
project = null;
});
it('simulator', () => {
const mockSimulator = { isSimulator: true, a: 1 };
project.mountSimulator(mockSimulator);
expect(project.simulator).toEqual(mockSimulator);
});
it('config / get / set', () => {
const mockConfig = { version: '1.0.0', componentsTree: [] };
project.config = mockConfig;
expect(project.config).toEqual(mockConfig);
const mockConfig2 = { version: '2.0.0', componentsTree: [] };
project.set('config', mockConfig2);
expect(project.get('config')).toEqual(mockConfig2);
project.set('version', '2.0.0');
expect(project.get('version')).toBe('2.0.0');
});
it('load', () => {
project.load({
componentsTree: [{
componentName: 'Page',
fileName: 'f1',
}],
}, 'f1');
expect(project.currentDocument?.fileName).toBe('f1');
});
it.skip('setSchema', () => {
project.load({
componentsTree: [{
componentName: 'Page',
fileName: 'f1',
}],
}, true);
project.setSchema({
componentsTree: [{
componentName: 'Page',
props: { a: 1 },
}],
});
expect(project.currentDocument?.rootNode?.propsData).toEqual({ a: 1 });
});
it('open / getDocument / checkExclusive', () => {
project.load({
componentsTree: [{
componentName: 'Page',
fileName: 'f1',
}],
});
const doc1 = project.createDocument({
componentName: 'Page',
fileName: 'f2',
});
const doc2 = project.createDocument({
componentName: 'Page',
fileName: 'f3',
});
project.open();
project.open('f2');
expect(project.currentDocument).toBe(doc1);
project.open('f3');
expect(project.currentDocument).toBe(doc2);
project.open('f1');
expect(project.currentDocument?.fileName).toBe('f1');
expect(project.open('not-existing')).toBeNull();
project.open(doc2);
expect(project.currentDocument).toBe(doc2);
const doc3 = project.open({
componentName: 'Page',
fileName: 'f4',
});
expect(project.currentDocument).toBe(doc3);
expect(project.documents.length).toBe(4);
expect(project.getDocument(project.currentDocument?.id)).toBe(doc3);
expect(project.getDocumentByFileName(project.currentDocument?.fileName)).toBe(doc3);
expect(project.getDocumentByFileName('unknown')).toBeNull();
expect(project.checkExclusive(project.currentDocument));
expect(project.documents[0].opened).toBeTruthy();
expect(project.documents[1].opened).toBeTruthy();
expect(project.documents[2].opened).toBeTruthy();
expect(project.documents[3].opened).toBeTruthy();
expect(project.documents[0].suspensed).toBeTruthy();
expect(project.documents[1].suspensed).toBeTruthy();
expect(project.documents[2].suspensed).toBeTruthy();
expect(project.documents[3].suspensed).toBeFalsy();
project.closeOthers(project.currentDocument);
expect(project.documents[0].opened).toBeFalsy();
expect(project.documents[1].opened).toBeFalsy();
expect(project.documents[2].opened).toBeFalsy();
expect(project.documents[3].opened).toBeTruthy();
expect(project.documents[0].suspensed).toBeTruthy();
expect(project.documents[1].suspensed).toBeTruthy();
expect(project.documents[2].suspensed).toBeTruthy();
expect(project.documents[3].suspensed).toBeFalsy();
});
it('removeDocument', () => {
const doc1 = project.createDocument({
componentName: 'Page',
fileName: 'f1',
});
project.removeDocument({});
expect(project.documents.length).toBe(1);
});
it('simulatorProps', () => {
designer._simulatorProps = { a: 1 };
expect(designer.simulatorProps.a).toBe(1);
designer._simulatorProps = () => ({ a: 1 });
expect(designer.simulatorProps.a).toBe(1);
});
it('onCurrentDocumentChange', () => {
const mockFn = jest.fn();
const off = project.onCurrentDocumentChange(mockFn);
project.open({
componentName: 'Page',
});
expect(mockFn).toHaveBeenCalled();
off();
mockFn.mockClear();
project.open({
componentName: 'Page',
});
expect(mockFn).not.toHaveBeenCalled();
});
it('setRendererReady / onRendererReady', () => {
const mockFn = jest.fn();
const off = project.onRendererReady(mockFn);
project.setRendererReady({ a: 1 });
expect(mockFn).toHaveBeenCalledWith({ a: 1 });
off();
mockFn.mockClear();
project.setRendererReady({ a: 1 });
expect(mockFn).not.toHaveBeenCalled();
});
});
================================================
FILE: packages/designer/tests/project/project.test.ts
================================================
import set from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';
import '../fixtures/window';
import { Editor } from '@alilc/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
return {
getComponentMeta() {
return {
getMetadata() {
return { configure: { advanced: null } };
},
get advanced() {
return {};
},
};
},
transformProps(props) { return props; },
createSettingEntry: mockCreateSettingEntry,
postEvent() {},
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({});
designer.editor = new Editor();
});
describe('schema 生成节点模型测试', () => {
describe('block ❌ | component ❌ | slot ❌', () => {
beforeEach(() => {
mockCreateSettingEntry.mockClear();
});
it('基本的节点模型初始化,模型导出,初始化传入 schema', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
expect(nodesMap.size).toBe(expectedNodeCnt);
ids.forEach(id => {
expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName);
});
const exportSchema = currentDocument?.export(1);
expect(getIdsFromSchema(exportSchema).length).toBe(expectedNodeCnt);
nodesMap.forEach(node => {
// 触发 getter
node.settingEntry;
});
expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt);
});
it('onSimulatorReady works', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const mockCallback = jest.fn();
const removeListener = project.onSimulatorReady(mockCallback);
project.mountSimulator(undefined);
expect(mockCallback).toBeCalled();
removeListener();
});
it('open doc when doc is blank', () => {
const project = new Project(designer);
project.open();
expect(project).toBeTruthy();
const blankDoc = project.documents[0];
expect(blankDoc).toBeTruthy();
// 触发保存
blankDoc.history.savePoint();
expect(blankDoc.isModified()).toBeFalsy();
expect(blankDoc.isBlank()).toBeTruthy();
//二次打开doc,会使用前面那个
const openedDoc = project.open();
expect(openedDoc).toBe(blankDoc);
});
it('load schema with autoOpen === true', () => {
const project = new Project(designer);
expect(project).toBeTruthy();
// trigger autoOpen case
project.load({
componentsTree: [
formSchema,
],
}, true);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
expect(nodesMap.size).toBe(expectedNodeCnt);
ids.forEach(id => {
expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName);
});
const exportSchema = currentDocument?.export(1);
expect(getIdsFromSchema(exportSchema).length).toBe(expectedNodeCnt);
nodesMap.forEach(node => {
// 触发 getter
node.settingEntry;
});
expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt);
});
it('load schema with autoOpen === true, and config contains layout.props.tabBar.item', () => {
const project = new Project(designer);
expect(project).toBeTruthy();
// trigger autoOpen case
project.load({
componentsTree: [
{
...formSchema,
fileName: 'demoFile1',
},
{
...formSchema,
fileName: 'demoFile2',
}
],
config: {
layout: {
props: {
tabBar: {
items: [
{
path: '/demoFile2',
}
],
}
}
}
}
}, true);
const { currentDocument } = project;
expect(currentDocument.fileName).toBe('demoFile2');
});
it('load schema with autoOpen === true', () => {
const project = new Project(designer);
expect(project).toBeTruthy();
// trigger autoOpen case
project.load({
componentsTree: [
{
...formSchema,
fileName: 'demoFile1',
},
{
...formSchema,
fileName: 'demoFile2',
}
],
}, 'demoFile2');
const { currentDocument } = project;
expect(currentDocument.fileName).toBe('demoFile2');
});
it('setSchema works', () => {
const project = new Project(designer);
project.open();
expect(project).toBeTruthy();
project.setSchema({
componentsTree: [
{
...formSchema,
fileName: 'demoFile1',
},
],
});
const { currentDocument } = project;
expect(currentDocument.fileName).toBe('demoFile1');
});
it('基本的节点模型初始化,模型导出,project.open 传入 schema', () => {
const project = new Project(designer);
project.open(formSchema);
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
expect(nodesMap.size).toBe(expectedNodeCnt);
ids.forEach(id => {
expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName);
});
const exportSchema = currentDocument?.export(1);
expect(getIdsFromSchema(exportSchema).length).toBe(expectedNodeCnt);
nodesMap.forEach(node => {
// 触发 getter
node.settingEntry;
});
expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt);
});
it('project 卸载所有 document - unload()', () => {
const project = new Project(designer);
project.open(formSchema);
expect(project).toBeTruthy();
const { currentDocument, documents } = project;
expect(documents).toHaveLength(1);
expect(currentDocument).toBe(documents[0]);
project.unload();
expect(documents).toHaveLength(0);
});
it('project 卸载指定 document - removeDocument()', () => {
const project = new Project(designer);
project.open(formSchema);
expect(project).toBeTruthy();
const { currentDocument, documents } = project;
expect(documents).toHaveLength(1);
expect(currentDocument).toBe(documents[0]);
project.removeDocument(currentDocument);
expect(documents).toHaveLength(0);
});
it('get unknown document', () => {
const project = new Project(designer);
project.open(formSchema);
expect(project).toBeTruthy();
expect(project.getDocument('unknownId')).toBeNull();
});
it('get set i18n works', () => {
const project = new Project(designer);
project.open(formSchema);
expect(project).toBeTruthy();
project.i18n = formSchema.i18n;
expect(project.i18n).toStrictEqual(formSchema.i18n);
project.i18n = null;
expect(project.i18n).toStrictEqual({});
project.set('i18n', formSchema.i18n);
expect(project.get('i18n')).toStrictEqual(formSchema.i18n);
project.set('i18n', null);
expect(project.get('i18n')).toStrictEqual({});
});
});
describe('block ❌ | component ❌ | slot ✅', () => {
it('基本的节点模型初始化,模型导出,初始化传入 schema', () => {
const formSchemaWithSlot = set(cloneDeep(formSchema), 'children[0].children[0].props.title.type', 'JSSlot');
const project = new Project(designer, {
componentsTree: [
formSchemaWithSlot,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument!;
const ids = getIdsFromSchema(formSchema);
// 目前每个 slot 会新增(1 + children.length)个节点
const expectedNodeCnt = ids.length + 2;
expect(nodesMap.size).toBe(expectedNodeCnt);
// PageHeader
expect(nodesMap.get('node_k1ow3cbd').slots).toHaveLength(1);
});
});
describe.skip('多 document 测试', () => {
});
});
================================================
FILE: packages/designer/tests/utils/bom.ts
================================================
import { getMockRenderer } from './renderer';
interface MockDocument extends Document {
// open(): any;
// write(): any;
// close(): any;
// addEventListener(): any;
// removeEventListener(): any;
triggerEventListener(): any;
// createElement(): any;
// appendChild(): any;
// removeChild(): any;
}
const eventsMap : Map> = new Map>();
const mockRemoveAttribute = jest.fn();
const mockAddEventListener = jest.fn((eventName: string, cb) => {
if (!eventsMap.has(eventName)) {
eventsMap.set(eventName, new Set([cb]));
return;
}
eventsMap.get(eventName)!.add(cb);
});
const mockRemoveEventListener = jest.fn((eventName: string, cb) => {
if (!eventsMap.has(eventName)) return;
if (!cb) {
eventsMap.delete(eventName);
return;
}
eventsMap.get(eventName)?.delete(cb);
});
const mockTriggerEventListener = jest.fn((eventName: string, data: any, context: object = {}) => {
if (!eventsMap.has(eventName)) return;
for (const cb of eventsMap.get(eventName)) {
cb.call(context, data);
}
});
const mockCreateElement = jest.fn((tagName) => {
return {
style: {},
appendChild() {},
addEventListener: mockAddEventListener,
removeEventListener: mockRemoveEventListener,
triggerEventListener: mockTriggerEventListener,
removeAttribute: mockRemoveAttribute,
};
});
export function getMockDocument(): MockDocument {
return {
open() {},
write() {},
close() {},
addEventListener: mockAddEventListener,
removeEventListener: mockRemoveEventListener,
triggerEventListener: mockTriggerEventListener,
createElement: mockCreateElement,
removeChild() {},
body: { appendChild() {}, removeChild() {} },
};
}
export function getMockWindow(doc?: MockDocument) {
return {
SimulatorRenderer: getMockRenderer(),
addEventListener: mockAddEventListener,
removeEventListener: mockRemoveEventListener,
triggerEventListener: mockTriggerEventListener,
document: doc || getMockDocument(),
};
}
export function clearEventsMap() {
eventsMap.clear();
}
export function getMockElement(tagName, options = {}) {
const elem = document.createElement(tagName);
let {
width = 0,
height = 0,
top = 0,
bottom = 0,
left = 0,
right = 0,
} = options;
elem.getBoundingClientRect = () => {
return {
width,
height,
top,
bottom,
left,
right,
};
};
elem.setWidth = (newWidth) => {
width = newWidth;
};
elem.setHeight = (newHeight) => {
height = newHeight;
};
// console.log(elem.ownerDocument);
// elem.ownerDocument = document;
// elem.ownerDocument.defaultView = window;
return elem;
}
================================================
FILE: packages/designer/tests/utils/event.ts
================================================
export function getMockEvent(target, options) {
return {
target,
preventDefault() {},
stopPropagation() {},
...options,
};
}
================================================
FILE: packages/designer/tests/utils/index.ts
================================================
export { getIdsFromSchema, getNodeFromSchemaById } from '@alilc/lowcode-test-mate/es/utils';
export * from './bom';
export * from './event';
export * from './renderer';
export * from './misc';
================================================
FILE: packages/designer/tests/utils/misc.ts
================================================
import lodashSet from 'lodash/set';
export function set(obj: any, path: any, val: any) {
if (typeof path === 'string' && path.startsWith('prototype')) {
const segs = path.split('.');
let acc = obj;
segs.forEach((seg, idx) => {
if (idx !== segs.length - 1) {
acc[seg] = acc[seg] || {};
acc = acc[seg];
} else {
acc[seg] = val;
}
});
}
return lodashSet(obj, path, val);
}
export function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function delayObxTick() {
return delay(100);
}
================================================
FILE: packages/designer/tests/utils/renderer.ts
================================================
export function getMockRenderer() {
return {
isSimulatorRenderer: true,
run() {
// console.log('renderer run');
},
};
}
================================================
FILE: packages/designer/tests/utils-ut/invariant.test.ts
================================================
// @ts-nocheck
import { invariant } from '../../src/utils/invariant';
it('invariant', () => {
expect(() => invariant(true)).not.toThrow();
expect(() => invariant(false, 'abc', 'xxx')).toThrow(/Invariant failed:/);
expect(() => invariant(false, 'abc')).toThrow(/Invariant failed:/);
});
================================================
FILE: packages/designer/tests/utils-ut/misc.test.ts
================================================
// @ts-nocheck
import { isElementNode, isDOMNodeVisible, normalizeTriggers, makeEventsHandler } from '../../src/utils/misc';
it('isElementNode', () => {
expect(isElementNode(document.createElement('div'))).toBeTruthy();
expect(isElementNode(1)).toBeFalsy();
});
/**
* const domNodeRect = domNode.getBoundingClientRect();
const { width, height } = viewport.contentBounds;
const { left, right, top, bottom, width: nodeWidth, height: nodeHeight } = domNodeRect;
return (
left >= -nodeWidth &&
top >= -nodeHeight &&
bottom <= height + nodeHeight &&
right <= width + nodeWidth
);
*/
const genMockNode = ({ left, right, top, bottom, width, height }) => {
return { getBoundingClientRect: () => {
if (width === undefined || height === undefined) throw new Error('width and height is required.');
const base = { width, height };
let coordinate = {};
if (left !== undefined) {
coordinate = top !== undefined ? {
left,
right: left + width,
top,
bottom: top + height,
} : {
left,
right: left + width,
bottom,
top: bottom - height,
}
} else if (right !== undefined) {
coordinate = top !== undefined ? {
left: right - width,
right,
top,
bottom: top + height,
} : {
left: right - width,
right,
bottom,
top: bottom - height,
}
}
return { ...base, ...coordinate };
} };
};
const mockViewport = {
contentBounds: {
width: 300,
height: 300,
},
};
describe('isDOMNodeVisible', () => {
it('isDOMNodeVisible', () => {
expect(
isDOMNodeVisible(
genMockNode({
width: 100,
height: 100,
left: 0,
top: 0,
}),
mockViewport,
),
).toBeTruthy();
expect(
isDOMNodeVisible(
genMockNode({
width: 100,
height: 100,
left: -100,
top: 0,
}),
mockViewport,
),
).toBeTruthy();
expect(
isDOMNodeVisible(
genMockNode({
width: 100,
height: 100,
left: 50,
top: 50,
}),
mockViewport,
),
).toBeTruthy();
// 左侧出界了
expect(
isDOMNodeVisible(
genMockNode({
width: 100,
height: 100,
left: -101,
top: 0,
}),
mockViewport,
),
).toBeFalsy();
// 右侧出界了
expect(
isDOMNodeVisible(
genMockNode({
width: 100,
height: 100,
right: 401,
top: 0,
}),
mockViewport,
),
).toBeFalsy();
// 上侧出界了
expect(
isDOMNodeVisible(
genMockNode({
width: 100,
height: 100,
left: 50,
top: -101,
}),
mockViewport,
),
).toBeFalsy();
// 下侧出界了
expect(
isDOMNodeVisible(
genMockNode({
width: 100,
height: 100,
left: 50,
bottom: 401,
}),
mockViewport,
),
).toBeFalsy();
});
});
it('normalizeTriggers', () => {
expect(normalizeTriggers(['n', 'w'])).toEqual(['N', 'W']);
});
it('makeEventsHandler', () => {
const sensor = { contentDocument: document };
// no contentDocument
const sensor2 = {};
const bind = makeEventsHandler({ view: { document } } as any, [sensor, sensor2]);
const fn = jest.fn();
bind((doc) => fn(doc));
expect(fn).toHaveBeenCalledTimes(1);
});
================================================
FILE: packages/designer/tests/utils-ut/slot.test.ts
================================================
// @ts-nocheck
import { includeSlot, removeSlot } from '../../src/utils/slot';
const genGetExtraProp = (val: string) => () => {
return {
getAsString() {
return val;
},
};
};
const remove = () => {};
const mockNode = {
slots: [{
getExtraProp: genGetExtraProp('haha'),
remove,
}, {
getExtraProp: genGetExtraProp('heihei'),
remove,
}]
};
// 没有 slots
const mockNode2 = {};
it('includeSlot', () => {
expect(includeSlot(mockNode, 'haha')).toBeTruthy();
expect(includeSlot(mockNode, 'heihei')).toBeTruthy();
expect(includeSlot(mockNode, 'xixi')).toBeFalsy();
expect(includeSlot(mockNode2, 'xixi')).toBeFalsy();
});
it('removeSlot', () => {
expect(removeSlot(mockNode, 'xixi')).toBeFalsy();
expect(mockNode.slots).toHaveLength(2);
expect(removeSlot(mockNode, 'haha')).toBeTruthy();
expect(mockNode.slots).toHaveLength(1);
expect(removeSlot(mockNode, 'heihei')).toBeTruthy();
expect(mockNode.slots).toHaveLength(0);
expect(removeSlot(mockNode2, 'xixi')).toBeFalsy();
});
================================================
FILE: packages/editor-core/build.json
================================================
{
"plugins": [
"@alilc/build-plugin-lce",
"build-plugin-fusion",
"./build.plugin.js"
]
}
================================================
FILE: packages/editor-core/build.plugin.js
================================================
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = ({ onGetWebpackConfig }) => {
onGetWebpackConfig((config) => {
config.resolve
.plugin('tsconfigpaths')
.use(TsconfigPathsPlugin, [{
configFile: './tsconfig.json',
}]);
});
};
================================================
FILE: packages/editor-core/build.test.json
================================================
{
"plugins": [
"@alilc/build-plugin-lce",
"@alilc/lowcode-test-mate/plugin/index.ts"
],
"babelPlugins": [
["@babel/plugin-proposal-private-property-in-object", { "loose": true }]
]
}
================================================
FILE: packages/editor-core/jest.config.js
================================================
const fs = require('fs');
const { join } = require('path');
const esModules = [].join('|');
const pkgNames = fs.readdirSync(join('..')).filter(pkgName => !pkgName.startsWith('.'));
const jestConfig = {
transformIgnorePatterns: [
`/node_modules/(?!${esModules})/`,
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
collectCoverage: false,
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/icons/**',
'!src/locale/**',
'!**/node_modules/**',
'!**/vendor/**',
],
};
// 只对本仓库内的 pkg 做 mapping
jestConfig.moduleNameMapper = {};
jestConfig.moduleNameMapper[`^@alilc/lowcode\\-(${pkgNames.join('|')})$`] = '/../$1/src';
module.exports = jestConfig;
================================================
FILE: packages/editor-core/package.json
================================================
{
"name": "@alilc/lowcode-editor-core",
"version": "1.3.2",
"description": "Core Api for Ali lowCode engine",
"license": "MIT",
"main": "lib/index.js",
"module": "es/index.js",
"files": [
"lib",
"es"
],
"scripts": {
"build": "build-scripts build",
"test": "build-scripts test --config build.test.json",
"test:cov": "build-scripts test --config build.test.json --jest-coverage"
},
"dependencies": {
"@alifd/next": "^1.19.16",
"@alilc/lowcode-types": "1.3.2",
"@alilc/lowcode-utils": "1.3.2",
"classnames": "^2.2.6",
"debug": "^4.1.1",
"intl-messageformat": "^9.3.1",
"lodash.get": "^4.4.2",
"mobx": "^6.3.0",
"mobx-react": "^7.2.0",
"power-di": "^2.2.4",
"react": "^16",
"react-dom": "^16.7.0",
"store": "^2.0.12"
},
"devDependencies": {
"@alib/build-scripts": "^0.1.18",
"@alilc/lowcode-datasource-types": "^1.0.1",
"@types/classnames": "^2.2.7",
"@types/lodash.get": "^4.4.6",
"@types/node": "^13.7.1",
"@types/react": "^16",
"@types/react-dom": "^16",
"@types/store": "^2.0.2",
"build-plugin-fusion": "^0.1.0",
"build-plugin-moment-locales": "^0.1.0"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "http",
"url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/editor-core"
},
"gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6",
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
"homepage": "https://github.com/alibaba/lowcode-engine/#readme"
}
================================================
FILE: packages/editor-core/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "lib"
},
"include": ["./src/"]
}
================================================
FILE: packages/editor-core/src/command.ts
================================================
import { IPublicApiCommand, IPublicEnumTransitionType, IPublicModelPluginContext, IPublicTypeCommand, IPublicTypeCommandHandlerArgs, IPublicTypeListCommand } from '@alilc/lowcode-types';
import { checkPropTypes } from '@alilc/lowcode-utils';
export interface ICommand extends Omit {
registerCommand(command: IPublicTypeCommand, options?: {
commandScope?: string;
}): void;
batchExecuteCommand(commands: { name: string; args: IPublicTypeCommandHandlerArgs }[], pluginContext?: IPublicModelPluginContext): void;
}
export interface ICommandOptions {
commandScope?: string;
}
export class Command implements ICommand {
private commands: Map = new Map();
private commandErrors: Function[] = [];
registerCommand(command: IPublicTypeCommand, options?: ICommandOptions): void {
if (!options?.commandScope) {
throw new Error('plugin meta.commandScope is required.');
}
const name = `${options.commandScope}:${command.name}`;
if (this.commands.has(name)) {
throw new Error(`Command '${command.name}' is already registered.`);
}
this.commands.set(name, {
...command,
name,
});
}
unregisterCommand(name: string): void {
if (!this.commands.has(name)) {
throw new Error(`Command '${name}' is not registered.`);
}
this.commands.delete(name);
}
executeCommand(name: string, args: IPublicTypeCommandHandlerArgs): void {
const command = this.commands.get(name);
if (!command) {
throw new Error(`Command '${name}' is not registered.`);
}
command.parameters?.forEach(d => {
if (!checkPropTypes(args[d.name], d.name, d.propType, 'command')) {
throw new Error(`Command '${name}' arguments ${d.name} is invalid.`);
}
});
try {
command.handler(args);
} catch (error) {
if (this.commandErrors && this.commandErrors.length) {
this.commandErrors.forEach(callback => callback(name, error));
} else {
throw error;
}
}
}
batchExecuteCommand(commands: { name: string; args: IPublicTypeCommandHandlerArgs }[], pluginContext: IPublicModelPluginContext): void {
if (!commands || !commands.length) {
return;
}
pluginContext.common.utils.executeTransaction(() => {
commands.forEach(command => this.executeCommand(command.name, command.args));
}, IPublicEnumTransitionType.REPAINT);
}
listCommands(): IPublicTypeListCommand[] {
return Array.from(this.commands.values()).map(d => {
const result: IPublicTypeListCommand = {
name: d.name,
};
if (d.description) {
result.description = d.description;
}
if (d.parameters) {
result.parameters = d.parameters;
}
return result;
});
}
onCommandError(callback: (name: string, error: Error) => void): void {
this.commandErrors.push(callback);
}
}
================================================
FILE: packages/editor-core/src/config.ts
================================================
import { get as lodashGet } from 'lodash';
import { isPlainObject } from '@alilc/lowcode-utils';
import {
IPublicTypeEngineOptions,
IPublicModelEngineConfig,
IPublicModelPreference,
} from '@alilc/lowcode-types';
import { getLogger } from './utils/logger';
import Preference from './utils/preference';
const logger = getLogger({ level: 'log', bizName: 'config' });
// this default behavior will be different later
const STRICT_PLUGIN_MODE_DEFAULT = true;
// used in strict mode, when only options in this VALID_ENGINE_OPTIONS can be accepted
// type and description are only used for developer`s assistance, won`t affect runtime
const VALID_ENGINE_OPTIONS = {
enableCondition: {
type: 'boolean',
description: '是否开启 condition 的能力,默认在设计器中不管 condition 是啥都正常展示',
},
designMode: {
type: 'string',
enum: ['design', 'live'],
default: 'design',
description: '设计模式,live 模式将会实时展示变量值',
},
device: {
type: 'string',
enum: ['default', 'mobile', 'any string value'],
default: 'default',
description: '设备类型',
},
deviceClassName: {
type: 'string',
default: undefined,
description: '指定初始化的 deviceClassName,挂载到画布的顶层节点上',
},
locale: {
type: 'string',
default: 'zh-CN',
description: '语言',
},
renderEnv: {
type: 'string',
enum: ['react', 'any string value'],
default: 'react',
description: '渲染器类型',
},
deviceMapper: {
type: 'object',
description: '设备类型映射器,处理设计器与渲染器中 device 的映射',
},
enableStrictPluginMode: {
type: 'boolean',
default: STRICT_PLUGIN_MODE_DEFAULT,
description: '开启严格插件模式,默认值:STRICT_PLUGIN_MODE_DEFAULT , 严格模式下,插件将无法通过 engineOptions 传递自定义配置项',
},
enableReactiveContainer: {
type: 'boolean',
default: false,
description: '开启拖拽组件时,即将被放入的容器是否有视觉反馈',
},
disableAutoRender: {
type: 'boolean',
default: false,
description: '关闭画布自动渲染,在资产包多重异步加载的场景有效',
},
disableDetecting: {
type: 'boolean',
default: false,
description: '关闭拖拽组件时的虚线响应,性能考虑',
},
customizeIgnoreSelectors: {
type: 'function',
default: undefined,
description: '定制画布中点击被忽略的 selectors, eg. (defaultIgnoreSelectors: string[], e: MouseEvent) => string[]',
},
disableDefaultSettingPanel: {
type: 'boolean',
default: false,
description: '禁止默认的设置面板',
},
disableDefaultSetters: {
type: 'boolean',
default: false,
description: '禁止默认的设置器',
},
enableCanvasLock: {
type: 'boolean',
default: false,
description: '打开画布的锁定操作',
},
enableLockedNodeSetting: {
type: 'boolean',
default: false,
description: '容器锁定后,容器本身是否可以设置属性,仅当画布锁定特性开启时生效',
},
stayOnTheSameSettingTab: {
type: 'boolean',
default: false,
description: '当选中节点切换时,是否停留在相同的设置 tab 上',
},
hideSettingsTabsWhenOnlyOneItem: {
type: 'boolean',
description: '是否在只有一个 item 的时候隐藏设置 tabs',
},
loadingComponent: {
type: 'ComponentType',
default: undefined,
description: '自定义 loading 组件',
},
supportVariableGlobally: {
type: 'boolean',
default: false,
description: '设置所有属性支持变量配置',
},
visionSettings: {
type: 'object',
description: 'Vision-polyfill settings',
},
simulatorUrl: {
type: 'array',
description: '自定义 simulatorUrl 的地址',
},
// 与 react-renderer 的 appHelper 一致,https://lowcode-engine.cn/site/docs/guide/expand/runtime/renderer#apphelper
appHelper: {
type: 'object',
description: '定义 utils 和 constants 等对象',
},
requestHandlersMap: {
type: 'object',
description: '数据源引擎的请求处理器映射',
},
thisRequiredInJSE: {
type: 'boolean',
description: 'JSExpression 是否只支持使用 this 来访问上下文变量',
},
enableStrictNotFoundMode: {
type: 'boolean',
description: '当开启组件未找到严格模式时,渲染模块不会默认给一个容器组件',
},
focusNodeSelector: {
type: 'function',
description: '配置指定节点为根组件',
},
enableAutoOpenFirstWindow: {
type: 'boolean',
description: '应用级设计模式下,自动打开第一个窗口',
default: true,
},
enableWorkspaceMode: {
type: 'boolean',
description: '是否开启应用级设计模式',
default: false,
},
workspaceEmptyComponent: {
type: 'function',
description: '应用级设计模式下,窗口为空时展示的占位组件',
},
enableContextMenu: {
type: 'boolean',
description: '是否开启右键菜单',
default: false,
},
hideComponentAction: {
type: 'boolean',
description: '是否隐藏设计器辅助层',
default: false,
},
};
const getStrictModeValue = (engineOptions: IPublicTypeEngineOptions, defaultValue: boolean): boolean => {
if (!engineOptions || !isPlainObject(engineOptions)) {
return defaultValue;
}
if (engineOptions.enableStrictPluginMode === undefined
|| engineOptions.enableStrictPluginMode === null) {
return defaultValue;
}
return engineOptions.enableStrictPluginMode;
};
export interface IEngineConfig extends IPublicModelEngineConfig {
/**
* if engineOptions.strictPluginMode === true, only accept propertied predefined in EngineOptions.
*
* @param {IPublicTypeEngineOptions} engineOptions
*/
setEngineOptions(engineOptions: IPublicTypeEngineOptions): void;
notifyGot(key: string): void;
setWait(key: string, resolve: (data: any) => void, once?: boolean): void;
delWait(key: string, fn: any): void;
}
export class EngineConfig implements IEngineConfig {
private config: { [key: string]: any } = {};
private waits = new Map<
string,
Array<{
once?: boolean;
resolve: (data: any) => void;
}>
>();
/**
* used to store preferences
*
*/
readonly preference: IPublicModelPreference;
constructor(config?: { [key: string]: any }) {
this.config = config || {};
this.preference = new Preference();
}
/**
* 判断指定 key 是否有值
* @param key
*/
has(key: string): boolean {
return this.config[key] !== undefined;
}
/**
* 获取指定 key 的值
* @param key
* @param defaultValue
*/
get(key: string, defaultValue?: any): any {
return lodashGet(this.config, key, defaultValue);
}
/**
* 设置指定 key 的值
* @param key
* @param value
*/
set(key: string, value: any) {
this.config[key] = value;
this.notifyGot(key);
}
/**
* 批量设值,set 的对象版本
* @param config
*/
setConfig(config: { [key: string]: any }) {
if (config) {
Object.keys(config).forEach((key) => {
this.set(key, config[key]);
});
}
}
/**
* if engineOptions.strictPluginMode === true, only accept propertied predefined in EngineOptions.
*
* @param {IPublicTypeEngineOptions} engineOptions
*/
setEngineOptions(engineOptions: IPublicTypeEngineOptions) {
if (!engineOptions || !isPlainObject(engineOptions)) {
return;
}
const strictMode = getStrictModeValue(engineOptions, STRICT_PLUGIN_MODE_DEFAULT) === true;
if (strictMode) {
const isValidKey = (key: string) => {
const result = (VALID_ENGINE_OPTIONS as any)[key];
return !(result === undefined || result === null);
};
Object.keys(engineOptions).forEach((key) => {
if (isValidKey(key)) {
this.set(key, (engineOptions as any)[key]);
} else {
logger.warn(`failed to config ${key} to engineConfig, only predefined options can be set under strict mode, predefined options: `, VALID_ENGINE_OPTIONS);
}
});
} else {
this.setConfig(engineOptions as any);
}
}
/**
* 获取指定 key 的值,若此时还未赋值,则等待,若已有值,则直接返回值
* 注:此函数返回 Promise 实例,只会执行(fullfill)一次
* @param key
* @returns
*/
onceGot(key: string): Promise {
const val = this.config[key];
if (val !== undefined) {
return Promise.resolve(val);
}
return new Promise((resolve) => {
this.setWait(key, resolve, true);
});
}
/**
* 获取指定 key 的值,函数回调模式,若多次被赋值,回调会被多次调用
* @param key
* @param fn
* @returns
*/
onGot(key: string, fn: (data: any) => void): () => void {
const val = this.config?.[key];
if (val !== undefined) {
fn(val);
}
this.setWait(key, fn);
return () => {
this.delWait(key, fn);
};
}
notifyGot(key: string): void {
let waits = this.waits.get(key);
if (!waits) {
return;
}
waits = waits.slice().reverse();
let i = waits.length;
while (i--) {
waits[i].resolve(this.get(key));
if (waits[i].once) {
waits.splice(i, 1);
}
}
if (waits.length > 0) {
this.waits.set(key, waits);
} else {
this.waits.delete(key);
}
}
setWait(key: string, resolve: (data: any) => void, once?: boolean) {
const waits = this.waits.get(key);
if (waits) {
waits.push({ resolve, once });
} else {
this.waits.set(key, [{ resolve, once }]);
}
}
delWait(key: string, fn: any) {
const waits = this.waits.get(key);
if (!waits) {
return;
}
let i = waits.length;
while (i--) {
if (waits[i].resolve === fn) {
waits.splice(i, 1);
}
}
if (waits.length < 1) {
this.waits.delete(key);
}
}
getPreference(): IPublicModelPreference {
return this.preference;
}
}
export const engineConfig = new EngineConfig();
================================================
FILE: packages/editor-core/src/editor.ts
================================================
/* eslint-disable no-console */
/* eslint-disable max-len */
import { StrictEventEmitter } from 'strict-event-emitter-types';
import { EventEmitter } from 'events';
import { EventBus, IEventBus } from './event-bus';
import {
IPublicModelEditor,
EditorConfig,
PluginClassSet,
IPublicTypeEditorValueKey,
IPublicTypeEditorGetResult,
HookConfig,
IPublicTypeComponentDescription,
IPublicTypeRemoteComponentDescription,
GlobalEvent,
} from '@alilc/lowcode-types';
import { engineConfig } from './config';
import { globalLocale } from './intl';
import { obx } from './utils';
import { IPublicTypeAssetsJson, AssetLoader } from '@alilc/lowcode-utils';
import { assetsTransform } from './utils/assets-transform';
EventEmitter.defaultMaxListeners = 100;
// inner instance keys which should not be stored in config
const keyBlacklist = [
'designer',
'skeleton',
'currentDocument',
'simulator',
'plugins',
'setters',
'material',
'innerHotkey',
'innerPlugins',
];
const AssetsCache: {
[key: string]: IPublicTypeRemoteComponentDescription;
} = {};
export declare interface Editor extends StrictEventEmitter {
addListener(event: string | symbol, listener: (...args: any[]) => void): this;
once(event: string | symbol, listener: (...args: any[]) => void): this;
removeListener(event: string | symbol, listener: (...args: any[]) => void): this;
off(event: string | symbol, listener: (...args: any[]) => void): this;
removeAllListeners(event?: string | symbol): this;
setMaxListeners(n: number): this;
getMaxListeners(): number;
listeners(event: string | symbol): Function[];
rawListeners(event: string | symbol): Function[];
listenerCount(type: string | symbol): number;
// Added in Node 6...
prependListener(event: string | symbol, listener: (...args: any[]) => void): this;
prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this;
eventNames(): Array;
}
export interface IEditor extends IPublicModelEditor {
config?: EditorConfig;
components?: PluginClassSet;
eventBus: IEventBus;
init(config?: EditorConfig, components?: PluginClassSet): Promise;
}
// eslint-disable-next-line no-redeclare
export class Editor extends EventEmitter implements IEditor {
/**
* Ioc Container
*/
@obx.shallow private context = new Map();
get locale() {
return globalLocale.getLocale();
}
config?: EditorConfig;
eventBus: EventBus;
components?: PluginClassSet;
// readonly utils = utils;
private hooks: HookConfig[] = [];
private waits = new Map<
IPublicTypeEditorValueKey,
Array<{
once?: boolean;
resolve: (data: any) => void;
}>
>();
constructor(readonly viewName: string = 'global', readonly workspaceMode: boolean = false) {
// eslint-disable-next-line constructor-super
super();
// set global emitter maxListeners
this.setMaxListeners(200);
this.eventBus = new EventBus(this);
}
get(
keyOrType: KeyOrType,
): IPublicTypeEditorGetResult | undefined {
return this.context.get(keyOrType as any);
}
has(keyOrType: IPublicTypeEditorValueKey): boolean {
return this.context.has(keyOrType);
}
set(key: IPublicTypeEditorValueKey, data: any): void | Promise {
if (key === 'assets') {
return this.setAssets(data);
}
// store the data to engineConfig while invoking editor.set()
if (!keyBlacklist.includes(key as string)) {
engineConfig.set(key as any, data);
}
this.context.set(key, data);
this.notifyGot(key);
}
async setAssets(assets: IPublicTypeAssetsJson) {
const { components } = assets;
if (components && components.length) {
const componentDescriptions: IPublicTypeComponentDescription[] = [];
const remoteComponentDescriptions: IPublicTypeRemoteComponentDescription[] = [];
components.forEach((component: any) => {
if (!component) {
return;
}
if (component.exportName && component.url) {
remoteComponentDescriptions.push(component);
} else {
componentDescriptions.push(component);
}
});
assets.components = componentDescriptions;
assets.componentList = assets.componentList || [];
// 如果有远程组件描述协议,则自动加载并补充到资产包中,同时出发 designer.incrementalAssetsReady 通知组件面板更新数据
if (remoteComponentDescriptions && remoteComponentDescriptions.length) {
await Promise.all(
remoteComponentDescriptions.map(async (component: IPublicTypeRemoteComponentDescription) => {
const { exportName, url, npm } = component;
if (!url || !exportName) {
return;
}
if (!AssetsCache[exportName] || !npm?.version || AssetsCache[exportName].npm?.version !== npm?.version) {
await (new AssetLoader()).load(url);
}
AssetsCache[exportName] = component;
function setAssetsComponent(component: any, extraNpmInfo: any = {}) {
const components = component.components;
assets.componentList = assets.componentList?.concat(component.componentList || []);
if (Array.isArray(components)) {
components.forEach(d => {
assets.components = assets.components.concat({
npm: {
...npm,
...extraNpmInfo,
},
...d,
} || []);
});
return;
}
if (component.components) {
assets.components = assets.components.concat({
npm: {
...npm,
...extraNpmInfo,
},
...component.components,
} || []);
}
}
function setArrayAssets(value: any[], preExportName: string = '', preSubName: string = '') {
value.forEach((d: any, i: number) => {
const exportName = [preExportName, i.toString()].filter(d => !!d).join('.');
const subName = [preSubName, i.toString()].filter(d => !!d).join('.');
Array.isArray(d) ? setArrayAssets(d, exportName, subName) : setAssetsComponent(d, {
exportName,
subName,
});
});
}
if ((window as any)[exportName]) {
if (Array.isArray((window as any)[exportName])) {
setArrayAssets((window as any)[exportName] as any);
} else {
setAssetsComponent((window as any)[exportName] as any);
}
}
return (window as any)[exportName];
}),
);
}
}
const innerAssets = assetsTransform(assets);
this.context.set('assets', innerAssets);
this.notifyGot('assets');
}
onceGot(keyOrType: KeyOrType): Promise> {
const x = this.context.get(keyOrType);
if (x !== undefined) {
return Promise.resolve(x);
}
return new Promise((resolve) => {
this.setWait(keyOrType, resolve, true);
});
}
onGot(
keyOrType: KeyOrType,
fn: (data: IPublicTypeEditorGetResult) => void,
): () => void {
const x = this.context.get(keyOrType);
if (x !== undefined) {
fn(x);
}
this.setWait(keyOrType, fn);
return () => {
this.delWait(keyOrType, fn);
};
}
onChange(
keyOrType: KeyOrType,
fn: (data: IPublicTypeEditorGetResult) => void,
): () => void {
this.setWait(keyOrType, fn);
return () => {
this.delWait(keyOrType, fn);
};
}
register(data: any, key?: IPublicTypeEditorValueKey): void {
this.context.set(key || data, data);
this.notifyGot(key || data);
}
async init(config?: EditorConfig, components?: PluginClassSet): Promise {
this.config = config || {};
this.components = components || {};
const { hooks = [], lifeCycles } = this.config;
this.emit('editor.beforeInit');
const init = (lifeCycles && lifeCycles.init) || ((): void => { });
try {
await init(this);
// 注册快捷键
// 注册 hooks
this.registerHooks(hooks);
this.emit('editor.afterInit');
return true;
} catch (err) {
console.error(err);
}
}
destroy(): void {
if (!this.config) {
return;
}
try {
const { lifeCycles = {} } = this.config;
this.unregisterHooks();
if (lifeCycles.destroy) {
lifeCycles.destroy(this);
}
} catch (err) {
console.warn(err);
}
}
initHooks = (hooks: HookConfig[]) => {
this.hooks = hooks.map((hook) => ({
...hook,
// 指定第一个参数为 editor
handler: hook.handler.bind(this, this),
}));
return this.hooks;
};
registerHooks = (hooks: HookConfig[]) => {
this.initHooks(hooks).forEach(({ message, type, handler }) => {
if (['on', 'once'].indexOf(type) !== -1) {
this[type]((message as any), handler);
}
});
};
unregisterHooks = () => {
this.hooks.forEach(({ message, handler }) => {
this.removeListener(message, handler);
});
};
private notifyGot(key: IPublicTypeEditorValueKey) {
let waits = this.waits.get(key);
if (!waits) {
return;
}
waits = waits.slice().reverse();
let i = waits.length;
while (i--) {
waits[i].resolve(this.get(key));
if (waits[i].once) {
waits.splice(i, 1);
}
}
if (waits.length > 0) {
this.waits.set(key, waits);
} else {
this.waits.delete(key);
}
}
private setWait(key: IPublicTypeEditorValueKey, resolve: (data: any) => void, once?: boolean) {
const waits = this.waits.get(key);
if (waits) {
waits.push({ resolve, once });
} else {
this.waits.set(key, [{ resolve, once }]);
}
}
private delWait(key: IPublicTypeEditorValueKey, fn: any) {
const waits = this.waits.get(key);
if (!waits) {
return;
}
let i = waits.length;
while (i--) {
if (waits[i].resolve === fn) {
waits.splice(i, 1);
}
}
if (waits.length < 1) {
this.waits.delete(key);
}
}
}
export const commonEvent = new EventBus(new EventEmitter());
================================================
FILE: packages/editor-core/src/event-bus.ts
================================================
import { IPublicApiEvent } from '@alilc/lowcode-types';
import { Logger } from '@alilc/lowcode-utils';
import EventEmitter from 'events';
const logger = new Logger({ level: 'warn', bizName: 'event-bus' });
const moduleLogger = new Logger({ level: 'warn', bizName: 'module-event-bus' });
export interface IEventBus extends IPublicApiEvent {
removeListener(event: string | symbol, listener: (...args: any[]) => void): any;
addListener(event: string | symbol, listener: (...args: any[]) => void): any;
setMaxListeners(n: number): any;
removeAllListeners(event?: string | symbol): any;
}
export class EventBus implements IEventBus {
private readonly eventEmitter: EventEmitter;
private readonly name?: string;
/**
* 内核触发的事件名
*/
readonly names = [];
constructor(emitter: EventEmitter, name?: string) {
this.eventEmitter = emitter;
this.name = name;
}
private getMsgPrefix(type: string): string {
if (this.name && this.name.length > 0) {
return `[${this.name}][event-${type}]`;
} else {
return `[*][event-${type}]`;
}
}
private getLogger(): Logger {
if (this.name && this.name.length > 0) {
return moduleLogger;
} else {
return logger;
}
}
/**
* 监听事件
* @param event 事件名称
* @param listener 事件回调
*/
on(event: string, listener: (...args: any[]) => void): () => void {
this.eventEmitter.on(event, listener);
this.getLogger().debug(`${this.getMsgPrefix('on')} ${event}`);
return () => {
this.off(event, listener);
};
}
prependListener(event: string, listener: (...args: any[]) => void): () => void {
this.eventEmitter.prependListener(event, listener);
this.getLogger().debug(`${this.getMsgPrefix('prependListener')} ${event}`);
return () => {
this.off(event, listener);
};
}
/**
* 取消监听事件
* @param event 事件名称
* @param listener 事件回调
*/
off(event: string, listener: (...args: any[]) => void) {
this.eventEmitter.off(event, listener);
this.getLogger().debug(`${this.getMsgPrefix('off')} ${event}`);
}
/**
* 触发事件
* @param event 事件名称
* @param args 事件参数
* @returns
*/
emit(event: string, ...args: any[]) {
this.eventEmitter.emit(event, ...args);
this.getLogger().debug(`${this.getMsgPrefix('emit')} name: ${event}, args: `, ...args);
}
removeListener(event: string | symbol, listener: (...args: any[]) => void): any {
return this.eventEmitter.removeListener(event, listener);
}
addListener(event: string | symbol, listener: (...args: any[]) => void): any {
return this.eventEmitter.addListener(event, listener);
}
setMaxListeners(n: number): any {
return this.eventEmitter.setMaxListeners(n);
}
removeAllListeners(event?: string | symbol): any {
return this.eventEmitter.removeAllListeners(event);
}
}
export const createModuleEventBus = (moduleName: string, maxListeners?: number): IEventBus => {
const emitter = new EventEmitter();
if (maxListeners) {
emitter.setMaxListeners(maxListeners);
}
return new EventBus(emitter, moduleName);
};
================================================
FILE: packages/editor-core/src/hotkey.ts
================================================
import { isEqual } from 'lodash';
import { globalContext } from './di';
import { IPublicTypeHotkeyCallback, IPublicTypeHotkeyCallbackConfig, IPublicTypeHotkeyCallbacks, IPublicApiHotkey } from '@alilc/lowcode-types';
interface KeyMap {
[key: number]: string;
}
interface CtrlKeyMap {
[key: string]: string;
}
interface ActionEvent {
type: string;
}
interface HotkeyDirectMap {
[key: string]: IPublicTypeHotkeyCallback;
}
interface KeyInfo {
key: string;
modifiers: string[];
action: string;
}
interface SequenceLevels {
[key: string]: number;
}
const MAP: KeyMap = {
8: 'backspace',
9: 'tab',
13: 'enter',
16: 'shift',
17: 'ctrl',
18: 'alt',
20: 'capslock',
27: 'esc',
32: 'space',
33: 'pageup',
34: 'pagedown',
35: 'end',
36: 'home',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
45: 'ins',
46: 'del',
91: 'meta',
93: 'meta',
224: 'meta',
};
const KEYCODE_MAP: KeyMap = {
106: '*',
107: '+',
109: '-',
110: '.',
111: '/',
186: ';',
187: '=',
188: ',',
189: '-',
190: '.',
191: '/',
192: '`',
219: '[',
220: '\\',
221: ']',
222: "'",
};
const SHIFT_MAP: CtrlKeyMap = {
'~': '`',
'!': '1',
'@': '2',
'#': '3',
$: '4',
'%': '5',
'^': '6',
'&': '7',
'*': '8',
'(': '9',
')': '0',
_: '-',
'+': '=',
':': ';',
'"': "'",
'<': ',',
'>': '.',
'?': '/',
'|': '\\',
};
const SPECIAL_ALIASES: CtrlKeyMap = {
option: 'alt',
command: 'meta',
return: 'enter',
escape: 'esc',
plus: '+',
mod: /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl',
};
let REVERSE_MAP: CtrlKeyMap;
/**
* loop through the f keys, f1 to f19 and add them to the map
* programatically
*/
for (let i = 1; i < 20; ++i) {
MAP[111 + i] = `f${ i}`;
}
/**
* loop through to map numbers on the numeric keypad
*/
for (let i = 0; i <= 9; ++i) {
MAP[i + 96] = String(i);
}
/**
* takes the event and returns the key character
*/
function characterFromEvent(e: KeyboardEvent): string {
const keyCode = e.keyCode || e.which;
// for keypress events we should return the character as is
if (e.type === 'keypress') {
let character = String.fromCharCode(keyCode);
// if the shift key is not pressed then it is safe to assume
// that we want the character to be lowercase. this means if
// you accidentally have caps lock on then your key bindings
// will continue to work
//
// the only side effect that might not be desired is if you
// bind something like 'A' cause you want to trigger an
// event when capital A is pressed caps lock will no longer
// trigger the event. shift+a will though.
if (!e.shiftKey) {
character = character.toLowerCase();
}
return character;
}
// for non keypress events the special maps are needed
if (MAP[keyCode]) {
return MAP[keyCode];
}
if (KEYCODE_MAP[keyCode]) {
return KEYCODE_MAP[keyCode];
}
// if it is not in the special map
// with keydown and keyup events the character seems to always
// come in as an uppercase character whether you are pressing shift
// or not. we should make sure it is always lowercase for comparisons
// tips: Q29weXJpZ2h0IChjKSAyMDIwLXByZXNlbnQgQWxpYmFiYSBJbmMuIFYy
return String.fromCharCode(keyCode).toLowerCase();
}
interface KeypressEvent extends KeyboardEvent {
type: 'keypress';
}
function isPressEvent(e: KeyboardEvent | ActionEvent): e is KeypressEvent {
return e.type === 'keypress';
}
/**
* checks if two arrays are equal
*/
function modifiersMatch(modifiers1: string[], modifiers2: string[]): boolean {
return modifiers1.sort().join(',') === modifiers2.sort().join(',');
}
/**
* takes a key event and figures out what the modifiers are
*/
function eventModifiers(e: KeyboardEvent): string[] {
const modifiers = [];
if (e.shiftKey) {
modifiers.push('shift');
}
if (e.altKey) {
modifiers.push('alt');
}
if (e.ctrlKey) {
modifiers.push('ctrl');
}
if (e.metaKey) {
modifiers.push('meta');
}
return modifiers;
}
/**
* determines if the keycode specified is a modifier key or not
*/
function isModifier(key: string): boolean {
return key === 'shift' || key === 'ctrl' || key === 'alt' || key === 'meta';
}
/**
* reverses the map lookup so that we can look for specific keys
* to see what can and can't use keypress
*
* @return {Object}
*/
function getReverseMap(): CtrlKeyMap {
if (!REVERSE_MAP) {
REVERSE_MAP = {};
for (const key in MAP) {
// pull out the numeric keypad from here cause keypress should
// be able to detect the keys from the character
if (Number(key) > 95 && Number(key) < 112) {
continue;
}
if (MAP.hasOwnProperty(key)) {
REVERSE_MAP[MAP[key]] = key;
}
}
}
return REVERSE_MAP;
}
/**
* picks the best action based on the key combination
*/
function pickBestAction(key: string, modifiers: string[], action?: string): string {
// if no action was picked in we should try to pick the one
// that we think would work best for this key
if (!action) {
action = getReverseMap()[key] ? 'keydown' : 'keypress';
}
// modifier keys don't work as expected with keypress,
// switch to keydown
if (action === 'keypress' && modifiers.length) {
action = 'keydown';
}
return action;
}
/**
* Converts from a string key combination to an array
*
* @param {string} combination like "command+shift+l"
* @return {Array}
*/
function keysFromString(combination: string): string[] {
if (combination === '+') {
return ['+'];
}
combination = combination.replace(/\+{2}/g, '+plus');
return combination.split('+');
}
/**
* Gets info for a specific key combination
*
* @param combination key combination ("command+s" or "a" or "*")
*/
function getKeyInfo(combination: string, action?: string): KeyInfo {
let keys: string[] = [];
let key = '';
let i: number;
const modifiers: string[] = [];
// take the keys from this pattern and figure out what the actual
// pattern is all about
keys = keysFromString(combination);
for (i = 0; i < keys.length; ++i) {
key = keys[i];
// normalize key names
if (SPECIAL_ALIASES[key]) {
key = SPECIAL_ALIASES[key];
}
// if this is not a keypress event then we should
// be smart about using shift keys
// this will only work for US keyboards however
if (action && action !== 'keypress' && SHIFT_MAP[key]) {
key = SHIFT_MAP[key];
modifiers.push('shift');
}
// if this key is a modifier then add it to the list of modifiers
if (isModifier(key)) {
modifiers.push(key);
}
}
// depending on what the key combination is
// we will try to pick the best event for it
action = pickBestAction(key, modifiers, action);
return {
key,
modifiers,
action,
};
}
/**
* actually calls the callback function
*
* if your callback function returns false this will use the jquery
* convention - prevent default and stop propogation on the event
*/
function fireCallback(callback: IPublicTypeHotkeyCallback, e: KeyboardEvent, combo?: string, sequence?: string): void {
try {
const workspace = globalContext.get('workspace');
const editor = workspace.isActive ? workspace.window?.editor : globalContext.get('editor');
const designer = editor?.get('designer');
const node = designer?.currentSelection?.getNodes()?.[0];
const npm = node?.componentMeta?.npm;
const selected =
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') || node?.componentMeta?.componentName || '';
if (callback(e, combo) === false) {
e.preventDefault();
e.stopPropagation();
}
editor?.eventBus.emit('hotkey.callback.call', {
callback,
e,
combo,
sequence,
selected,
});
} catch (err) {
console.error(err.message);
}
}
export interface IHotKey extends Omit {
activate(activate: boolean): void;
}
export class Hotkey implements IHotKey {
callBacks: IPublicTypeHotkeyCallbacks = {};
private directMap: HotkeyDirectMap = {};
private sequenceLevels: SequenceLevels = {};
private resetTimer = 0;
private ignoreNextKeyup: boolean | string = false;
private ignoreNextKeypress = false;
private nextExpectedAction: boolean | string = false;
private isActivate = true;
constructor(readonly viewName: string = 'global') {
this.mount(window);
}
activate(activate: boolean): void {
this.isActivate = activate;
}
mount(window: Window) {
const { document } = window;
const handleKeyEvent = this.handleKeyEvent.bind(this);
document.addEventListener('keypress', handleKeyEvent, false);
document.addEventListener('keydown', handleKeyEvent, false);
document.addEventListener('keyup', handleKeyEvent, false);
return () => {
document.removeEventListener('keypress', handleKeyEvent, false);
document.removeEventListener('keydown', handleKeyEvent, false);
document.removeEventListener('keyup', handleKeyEvent, false);
};
}
bind(combos: string[] | string, callback: IPublicTypeHotkeyCallback, action?: string): Hotkey {
this.bindMultiple(Array.isArray(combos) ? combos : [combos], callback, action);
return this;
}
unbind(combos: string[] | string, callback: IPublicTypeHotkeyCallback, action?: string) {
const combinations = Array.isArray(combos) ? combos : [combos];
combinations.forEach(combination => {
const info: KeyInfo = getKeyInfo(combination, action);
const { key, modifiers } = info;
const idx = this.callBacks[key].findIndex(info => {
return isEqual(info.modifiers, modifiers) && info.callback === callback;
});
if (idx !== -1) {
this.callBacks[key].splice(idx, 1);
}
});
}
/**
* resets all sequence counters except for the ones passed in
*/
private resetSequences(doNotReset?: SequenceLevels): void {
// doNotReset = doNotReset || {};
let activeSequences = false;
let key = '';
for (key in this.sequenceLevels) {
if (doNotReset && doNotReset[key]) {
activeSequences = true;
} else {
this.sequenceLevels[key] = 0;
}
}
if (!activeSequences) {
this.nextExpectedAction = false;
}
}
/**
* finds all callbacks that match based on the keycode, modifiers,
* and action
*/
private getMatches(
character: string,
modifiers: string[],
e: KeyboardEvent | ActionEvent,
sequenceName?: string,
combination?: string,
level?: number,
): IPublicTypeHotkeyCallbackConfig[] {
let i: number;
let callback: IPublicTypeHotkeyCallbackConfig;
const matches: IPublicTypeHotkeyCallbackConfig[] = [];
const action: string = e.type;
// if there are no events related to this keycode
if (!this.callBacks[character]) {
return [];
}
// if a modifier key is coming up on its own we should allow it
if (action === 'keyup' && isModifier(character)) {
modifiers = [character];
}
// loop through all callbacks for the key that was pressed
// and see if any of them match
for (i = 0; i < this.callBacks[character].length; ++i) {
callback = this.callBacks[character][i];
// if a sequence name is not specified, but this is a sequence at
// the wrong level then move onto the next match
if (!sequenceName && callback.seq && this.sequenceLevels[callback.seq] !== callback.level) {
continue;
}
// if the action we are looking for doesn't match the action we got
// then we should keep going
if (action !== callback.action) {
continue;
}
// if this is a keypress event and the meta key and control key
// are not pressed that means that we need to only look at the
// character, otherwise check the modifiers as well
//
// chrome will not fire a keypress if meta or control is down
// safari will fire a keypress if meta or meta+shift is down
// firefox will fire a keypress if meta or control is down
if ((isPressEvent(e) && !e.metaKey && !e.ctrlKey) || modifiersMatch(modifiers, callback.modifiers)) {
const deleteCombo = !sequenceName && callback.combo === combination;
const deleteSequence = sequenceName && callback.seq === sequenceName && callback.level === level;
if (deleteCombo || deleteSequence) {
this.callBacks[character].splice(i, 1);
}
matches.push(callback);
}
}
return matches;
}
private handleKey(character: string, modifiers: string[], e: KeyboardEvent): void {
const callbacks: IPublicTypeHotkeyCallbackConfig[] = this.getMatches(character, modifiers, e);
let i: number;
const doNotReset: SequenceLevels = {};
let maxLevel = 0;
let processedSequenceCallback = false;
// Calculate the maxLevel for sequences so we can only execute the longest callback sequence
for (i = 0; i < callbacks.length; ++i) {
if (callbacks[i].seq) {
maxLevel = Math.max(maxLevel, callbacks[i].level || 0);
}
}
// loop through matching callbacks for this key event
for (i = 0; i < callbacks.length; ++i) {
// fire for all sequence callbacks
// this is because if for example you have multiple sequences
// bound such as "g i" and "g t" they both need to fire the
// callback for matching g cause otherwise you can only ever
// match the first one
if (callbacks[i].seq) {
// only fire callbacks for the maxLevel to prevent
// subsequences from also firing
//
// for example 'a option b' should not cause 'option b' to fire
// even though 'option b' is part of the other sequence
//
// any sequences that do not match here will be discarded
// below by the resetSequences call
if (callbacks[i].level !== maxLevel) {
continue;
}
processedSequenceCallback = true;
// keep a list of which sequences were matches for later
doNotReset[callbacks[i].seq || ''] = 1;
fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
continue;
}
// if there were no sequence matches but we are still here
// that means this is a regular match so we should fire that
if (!processedSequenceCallback) {
fireCallback(callbacks[i].callback, e, callbacks[i].combo);
}
}
const ignoreThisKeypress = e.type === 'keypress' && this.ignoreNextKeypress;
if (e.type === this.nextExpectedAction && !isModifier(character) && !ignoreThisKeypress) {
this.resetSequences(doNotReset);
}
this.ignoreNextKeypress = processedSequenceCallback && e.type === 'keydown';
}
private handleKeyEvent(e: KeyboardEvent): void {
if (!this.isActivate) {
return;
}
const character = characterFromEvent(e);
// no character found then stop
if (!character) {
return;
}
// need to use === for the character check because the character can be 0
if (e.type === 'keyup' && this.ignoreNextKeyup === character) {
this.ignoreNextKeyup = false;
return;
}
this.handleKey(character, eventModifiers(e), e);
}
private resetSequenceTimer(): void {
if (this.resetTimer) {
clearTimeout(this.resetTimer);
}
this.resetTimer = window.setTimeout(this.resetSequences, 1000);
}
private bindSequence(combo: string, keys: string[], callback: IPublicTypeHotkeyCallback, action?: string): void {
// const self: any = this;
this.sequenceLevels[combo] = 0;
const increaseSequence = (nextAction: string) => {
return () => {
this.nextExpectedAction = nextAction;
++this.sequenceLevels[combo];
this.resetSequenceTimer();
};
};
const callbackAndReset = (e: KeyboardEvent): void => {
fireCallback(callback, e, combo);
if (action !== 'keyup') {
this.ignoreNextKeyup = characterFromEvent(e);
}
setTimeout(this.resetSequences, 10);
};
for (let i = 0; i < keys.length; ++i) {
const isFinal = i + 1 === keys.length;
const wrappedCallback = isFinal ? callbackAndReset : increaseSequence(action || getKeyInfo(keys[i + 1]).action);
this.bindSingle(keys[i], wrappedCallback, action, combo, i);
}
}
private bindSingle(
combination: string,
callback: IPublicTypeHotkeyCallback,
action?: string,
sequenceName?: string,
level?: number,
): void {
// store a direct mapped reference for use with HotKey.trigger
this.directMap[`${combination}:${action}`] = callback;
// make sure multiple spaces in a row become a single space
combination = combination.replace(/\s+/g, ' ');
const sequence: string[] = combination.split(' ');
// if this pattern is a sequence of keys then run through this method
// to reprocess each pattern one key at a time
if (sequence.length > 1) {
this.bindSequence(combination, sequence, callback, action);
return;
}
const info: KeyInfo = getKeyInfo(combination, action);
// make sure to initialize array if this is the first time
// a callback is added for this key
this.callBacks[info.key] = this.callBacks[info.key] || [];
// remove an existing match if there is one
this.getMatches(info.key, info.modifiers, { type: info.action }, sequenceName, combination, level);
// add this call back to the array
// if it is a sequence put it at the beginning
// if not put it at the end
//
// this is important because the way these are processed expects
// the sequence ones to come first
this.callBacks[info.key][sequenceName ? 'unshift' : 'push']({
callback,
modifiers: info.modifiers,
action: info.action,
seq: sequenceName,
level,
combo: combination,
});
}
private bindMultiple(combinations: string[], callback: IPublicTypeHotkeyCallback, action?: string) {
for (const item of combinations) {
this.bindSingle(item, callback, action);
}
}
}
================================================
FILE: packages/editor-core/src/index.ts
================================================
export * from './intl';
export * from './editor';
export * from './utils';
export * from './di';
export * from './hotkey';
export * from './widgets';
export * from './config';
export * from './event-bus';
export * from './command';
================================================
FILE: packages/editor-core/src/di/index.ts
================================================
export * from './setter';
export * from './ioc-context';
export * from '../widgets/tip/tip';
================================================
FILE: packages/editor-core/src/di/ioc-context.ts
================================================
import { IocContext } from 'power-di';
export * from 'power-di';
export const globalContext = IocContext.DefaultInstance;
================================================
FILE: packages/editor-core/src/di/setter.ts
================================================
import { ReactNode } from 'react';
import { IPublicApiSetters, IPublicModelSettingField, IPublicTypeCustomView, IPublicTypeRegisteredSetter } from '@alilc/lowcode-types';
import { createContent, isCustomView } from '@alilc/lowcode-utils';
const settersMap = new Map();
export function registerSetter(
typeOrMaps: string | { [key: string]: IPublicTypeCustomView | IPublicTypeRegisteredSetter },
setter?: IPublicTypeCustomView | IPublicTypeRegisteredSetter,
) {
if (typeof typeOrMaps === 'object') {
Object.keys(typeOrMaps).forEach(type => {
registerSetter(type, typeOrMaps[type]);
});
return;
}
if (!setter) {
return;
}
if (isCustomView(setter)) {
setter = {
component: setter,
// todo: intl
title: (setter as any).displayName || (setter as any).name || 'CustomSetter',
};
}
if (!setter.initialValue) {
const initial = getInitialFromSetter(setter.component);
if (initial) {
setter.initialValue = (field: IPublicModelSettingField) => {
return initial.call(field, field.getValue());
};
}
}
settersMap.set(typeOrMaps, { type: typeOrMaps, ...setter });
}
function getInitialFromSetter(setter: any) {
return setter && (
setter.initial || setter.Initial
|| (setter.type && (setter.type.initial || setter.type.Initial))
) || null; // eslint-disable-line
}
export interface ISetters extends IPublicApiSetters {
}
export class Setters implements ISetters {
settersMap = new Map();
constructor(readonly viewName: string = 'global') {}
getSetter = (type: string): IPublicTypeRegisteredSetter | null => {
return this.settersMap.get(type) || null;
};
registerSetter = (
typeOrMaps: string | { [key: string]: IPublicTypeCustomView | IPublicTypeRegisteredSetter },
setter?: IPublicTypeCustomView | IPublicTypeRegisteredSetter,
) => {
if (typeof typeOrMaps === 'object') {
Object.keys(typeOrMaps).forEach(type => {
this.registerSetter(type, typeOrMaps[type]);
});
return;
}
if (!setter) {
return;
}
if (isCustomView(setter)) {
setter = {
component: setter,
// todo: intl
title: (setter as any).displayName || (setter as any).name || 'CustomSetter',
};
}
if (!setter.initialValue) {
const initial = getInitialFromSetter(setter.component);
if (initial) {
setter.initialValue = (field: IPublicModelSettingField) => {
return initial.call(field, field.getValue());
};
}
}
this.settersMap.set(typeOrMaps, { type: typeOrMaps, ...setter });
};
getSettersMap = () => {
return this.settersMap;
};
createSetterContent = (setter: any, props: Record): ReactNode => {
if (typeof setter === 'string') {
setter = this.getSetter(setter);
if (!setter) {
return null;
}
if (setter.defaultProps) {
props = {
...setter.defaultProps,
...props,
};
}
setter = setter.component;
}
// Fusion 的表单组件都是通过 'value' in props 来判断是否使用 defaultValue
if ('value' in props && typeof props.value === 'undefined') {
delete props.value;
}
return createContent(setter, props);
};
}
================================================
FILE: packages/editor-core/src/intl/global-locale.ts
================================================
import { IEventBus, createModuleEventBus } from '../event-bus';
import { obx, computed } from '../utils/obx';
import { Logger } from '@alilc/lowcode-utils';
const logger = new Logger({ level: 'warn', bizName: 'globalLocale' });
const languageMap: { [key: string]: string } = {
en: 'en-US',
zh: 'zh-CN',
zt: 'zh-TW',
es: 'es-ES',
pt: 'pt-PT',
fr: 'fr-FR',
de: 'de-DE',
it: 'it-IT',
ru: 'ru-RU',
ja: 'ja-JP',
ko: 'ko-KR',
ar: 'ar-SA',
tr: 'tr-TR',
th: 'th-TH',
vi: 'vi-VN',
nl: 'nl-NL',
he: 'iw-IL',
id: 'in-ID',
pl: 'pl-PL',
hi: 'hi-IN',
uk: 'uk-UA',
ms: 'ms-MY',
tl: 'tl-PH',
};
const LowcodeConfigKey = 'ali-lowcode-config';
class GlobalLocale {
private emitter: IEventBus = createModuleEventBus('GlobalLocale');
@obx.ref private _locale?: string;
@computed get locale() {
if (this._locale != null) {
return this._locale;
}
// TODO: store 1 & store 2 abstract out as custom implements
// store 1: config from storage
let result = null;
if (hasLocalStorage(window)) {
const store = window.localStorage;
let config: any;
try {
config = JSON.parse(store.getItem(LowcodeConfigKey) || '');
} catch (e) {
// ignore;
}
if (config?.locale) {
result = (config.locale || '').replace('_', '-');
logger.debug(`getting locale from localStorage: ${result}`);
}
}
if (!result) {
// store 2: config from window
let localeFromConfig: string = getConfig('locale');
if (localeFromConfig) {
result = languageMap[localeFromConfig] || localeFromConfig.replace('_', '-');
logger.debug(`getting locale from config: ${result}`);
}
}
if (!result) {
// store 3: config from system
const { navigator } = window as any;
if (navigator.language) {
const lang = (navigator.language as string);
return languageMap[lang] || lang.replace('_', '-');
} else if (navigator.browserLanguage) {
const it = navigator.browserLanguage.split('-');
let localeFromSystem = it[0];
if (it[1]) {
localeFromSystem += `-${it[1].toUpperCase()}`;
}
result = localeFromSystem;
logger.debug(`getting locale from system: ${result}`);
}
}
if (!result) {
logger.warn('something when wrong when trying to get locale, use zh-CN as default, please check it out!');
result = 'zh-CN';
}
this._locale = result;
return result;
}
constructor() {
this.emitter.setMaxListeners(0);
}
setLocale(locale: string) {
logger.info(`setting locale to ${locale}`);
if (locale === this.locale) {
return;
}
this._locale = locale;
if (hasLocalStorage(window)) {
const store = window.localStorage;
let config: any;
try {
config = JSON.parse(store.getItem(LowcodeConfigKey) || '');
} catch (e) {
// ignore;
}
if (config && typeof config === 'object') {
config.locale = locale;
} else {
config = { locale };
}
store.setItem(LowcodeConfigKey, JSON.stringify(config));
}
this.emitter.emit('localechange', locale);
}
getLocale() {
return this.locale;
}
onChangeLocale(fn: (locale: string) => void): () => void {
this.emitter.on('localechange', fn);
return () => {
this.emitter.removeListener('localechange', fn);
};
}
}
function getConfig(name: string) {
const win: any = window;
return (
win[name]
|| (win.g_config || {})[name]
|| (win.pageConfig || {})[name]
);
}
function hasLocalStorage(obj: any): obj is WindowLocalStorage {
return obj.localStorage;
}
let globalLocale = new GlobalLocale();
export { globalLocale };
================================================
FILE: packages/editor-core/src/intl/index.ts
================================================
import { ReactNode, Component, createElement } from 'react';
import { IntlMessageFormat } from 'intl-messageformat';
import { globalLocale } from './global-locale';
import { isI18nData } from '@alilc/lowcode-utils';
import { observer } from '../utils';
import { IPublicTypeI18nData } from '@alilc/lowcode-types';
function generateTryLocales(locale: string) {
const tries = [locale, locale.replace('-', '_')];
if (locale === 'zh-TW' || locale === 'en-US') {
tries.push('zh-CN');
tries.push('zh_CN');
} else {
tries.push('en-US');
tries.push('en_US');
if (locale !== 'zh-CN') {
tries.push('zh-CN');
tries.push('zh_CN');
}
}
return tries;
}
function injectVars(msg: string, params: any, locale: string): string {
if (!msg || !params) {
return msg;
}
const formater = new IntlMessageFormat(msg, locale);
return formater.format(params as any) as string;
}
export function intl(data: IPublicTypeI18nData | string, params?: object): ReactNode {
if (!isI18nData(data)) {
return data;
}
if (data.intl) {
return data.intl;
}
const locale = globalLocale.getLocale();
const tries = generateTryLocales(locale);
let msg: string | undefined;
for (const lan of tries) {
msg = data[lan];
if (msg != null) {
break;
}
}
if (msg == null) {
return `##intl@${locale}##`;
}
return injectVars(msg, params, locale);
}
export function shallowIntl(data: any): any {
if (!data || typeof data !== 'object') {
return data;
}
const maps: any = {};
Object.keys(data).forEach(key => {
maps[key] = intl(data[key]);
});
return maps;
}
export function intlNode(data: any, params?: object): ReactNode {
if (isI18nData(data)) {
if (data.intlNode) {
return data.intlNode;
}
return createElement(IntlElement, { data, params });
}
return data;
}
@observer
class IntlElement extends Component<{ data: any; params?: object }> {
render() {
const { data, params } = this.props;
return intl(data, params);
}
}
export function createIntl(
instance: string | object,
): {
intlNode(id: string, params?: object): ReactNode;
intl(id: string, params?: object): string;
getLocale(): string;
setLocale(locale: string): void;
} {
// TODO: make reactive
const data = (() => {
const locale = globalLocale.getLocale();
if (typeof instance === 'string') {
if ((window as any)[instance]) {
return (window as any)[instance][locale] || {};
}
const key = `${instance}_${locale.toLocaleLowerCase()}`;
return (window as any)[key] || {};
}
if (instance && typeof instance === 'object') {
return (instance as any)[locale] || {};
}
return {};
})();
function intl(key: string, params?: object): string {
// TODO: tries lost language
const str = data[key];
if (str == null) {
return `##intl@${key}##`;
}
return injectVars(str, params, globalLocale.getLocale());
}
@observer
class IntlElement extends Component<{ id: string; params?: object }> {
render() {
const { id, params } = this.props;
return intl(id, params);
}
}
return {
intlNode(id: string, params?: object) {
return createElement(IntlElement, { id, params });
},
intl,
getLocale() {
return globalLocale.getLocale();
},
setLocale(locale: string) {
globalLocale.setLocale(locale);
},
};
}
export { globalLocale };
================================================
FILE: packages/editor-core/src/utils/app-preset.ts
================================================
import store from 'store';
declare global {
interface Window {
__isDebug?: boolean;
__newFunc?: (funcStr: string) => (...args: any[]) => any;
}
}
// 根据 url 参数设置 debug 选项
const debugRegRes = /_?debug=(.*?)(&|$)/.exec(location.search);
if (debugRegRes && debugRegRes[1]) {
// eslint-disable-next-line no-underscore-dangle
window.__isDebug = true;
// @ts-ignore
store.storage.write('debug', debugRegRes[1] === 'true' ? '*' : debugRegRes[1]);
} else {
// eslint-disable-next-line no-underscore-dangle
window.__isDebug = false;
store.remove('debug');
}
// 重要,用于矫正画布执行 new Function 的 window 对象上下文
// eslint-disable-next-line no-underscore-dangle
window.__newFunc = (funContext: string): ((...args: any[]) => any) => {
// eslint-disable-next-line no-new-func
return new Function(funContext) as (...args: any[]) => any;
};
// 关闭浏览器前提醒,只有产生过交互才会生效
window.onbeforeunload = function (e: Event): string {
const ev = e || window.event;
// 本地调试不生效
if (location.href.indexOf('localhost') > 0) {
return '';
}
const msg = '您确定要离开此页面吗?';
ev.cancelBubble = true;
ev.returnValue = true;
if (e.stopPropagation) {
e.stopPropagation();
e.preventDefault();
}
return msg;
};
================================================
FILE: packages/editor-core/src/utils/assets-transform.ts
================================================
/* eslint-disable no-param-reassign */
import { IPublicTypeAssetsJson, IPublicTypeComponentDescription, IPublicTypePackage, IPublicTypeRemoteComponentDescription } from '@alilc/lowcode-types';
// TODO: 该转换逻辑未来需要消化掉
export function assetsTransform(assets: IPublicTypeAssetsJson) {
const { components, packages } = assets;
const packageMaps = (packages || []).reduce((acc: Record, cur: IPublicTypePackage) => {
const key = cur.id || cur.package || '';
acc[key] = cur;
return acc;
}, {} as any);
components.forEach((componentDesc: IPublicTypeComponentDescription | IPublicTypeRemoteComponentDescription) => {
let { devMode, schema, reference } = componentDesc;
if ((devMode as string) === 'lowcode') {
devMode = 'lowCode';
} else if (devMode === 'proCode') {
devMode = 'proCode';
}
if (devMode) {
componentDesc.devMode = devMode;
}
if (devMode === 'lowCode' && !schema && reference) {
const referenceId = reference.id || '';
componentDesc.schema = packageMaps[referenceId].schema;
}
});
return assets;
}
================================================
FILE: packages/editor-core/src/utils/control.ts
================================================
let globalEventOn = true;
export function setGlobalEventFlag(flag: boolean) {
globalEventOn = flag;
}
export function switchGlobalEventOn() {
setGlobalEventFlag(true);
}
export function switchGlobalEventOff() {
setGlobalEventFlag(false);
}
export function isGlobalEventOn() {
return globalEventOn;
}
export function runWithGlobalEventOff(fn: Function) {
switchGlobalEventOff();
fn();
switchGlobalEventOn();
}
type ListenerFunc = (...args: any[]) => void;
export function wrapWithEventSwitch(fn: ListenerFunc): ListenerFunc {
return (...args: any[]) => {
if (isGlobalEventOn()) fn(...args);
};
}
================================================
FILE: packages/editor-core/src/utils/focus-tracker.ts
================================================
export class FocusTracker {
private actives: Focusable[] = [];
private modals: Array<{ checkDown: (e: MouseEvent) => boolean; checkOpen: () => boolean }> = [];
mount(win: Window) {
const checkDown = (e: MouseEvent) => {
if (this.checkModalDown(e)) {
return;
}
const { first } = this;
if (first && !first.internalCheckInRange(e)) {
this.internalSuspenseItem(first);
first.internalTriggerBlur();
}
};
win.document.addEventListener('click', checkDown, true);
return () => {
win.document.removeEventListener('click', checkDown, true);
};
}
get first() {
return this.actives[0];
}
addModal(checkDown: (e: MouseEvent) => boolean, checkOpen: () => boolean) {
this.modals.push({
checkDown,
checkOpen,
});
}
private checkModalOpen(): boolean {
return this.modals.some((item) => item.checkOpen());
}
private checkModalDown(e: MouseEvent): boolean {
return this.modals.some((item) => item.checkDown(e));
}
execSave() {
// has Modal return;
if (this.checkModalOpen()) {
return;
}
// catch
if (this.first) {
this.first.internalTriggerSave();
}
}
execEsc() {
const { first } = this;
if (first) {
this.internalSuspenseItem(first);
first.internalTriggerEsc();
}
}
create(config: FocusableConfig) {
return new Focusable(this, config);
}
internalActiveItem(item: Focusable) {
const first = this.actives[0];
if (first === item) {
return;
}
const i = this.actives.indexOf(item);
if (i > -1) {
this.actives.splice(i, 1);
}
this.actives.unshift(item);
if (!item.isModal && first) {
// trigger Blur
first.internalTriggerBlur();
}
// trigger onActive
item.internalTriggerActive();
}
internalSuspenseItem(item: Focusable) {
const i = this.actives.indexOf(item);
if (i > -1) {
this.actives.splice(i, 1);
this.first?.internalTriggerActive();
}
}
}
export interface FocusableConfig {
range: HTMLElement | ((e: MouseEvent) => boolean);
modal?: boolean; // 模态窗口级别
onEsc?: () => void;
onBlur?: () => void;
onSave?: () => void;
onActive?: () => void;
}
export class Focusable {
readonly isModal: boolean;
constructor(private tracker: FocusTracker, private config: FocusableConfig) {
this.isModal = config.modal == null ? false : config.modal;
}
active() {
this.tracker.internalActiveItem(this);
}
suspense() {
this.tracker.internalSuspenseItem(this);
}
purge() {
this.tracker.internalSuspenseItem(this);
}
internalCheckInRange(e: MouseEvent) {
const { range } = this.config;
if (!range) {
return false;
}
if (typeof range === 'function') {
return range(e);
}
return range.contains(e.target as HTMLElement);
}
internalTriggerBlur() {
if (this.config.onBlur) {
this.config.onBlur();
}
}
internalTriggerSave() {
if (this.config.onSave) {
this.config.onSave();
return true;
}
return false;
}
internalTriggerEsc() {
if (this.config.onEsc) {
this.config.onEsc();
}
}
internalTriggerActive() {
if (this.config.onActive) {
this.config.onActive();
}
}
}
================================================
FILE: packages/editor-core/src/utils/get-public-path.ts
================================================
const publicPath = (document.currentScript as HTMLScriptElement)?.src.replace(/^(.*\/)[^/]+$/, '$1');
export function getPublicPath(): string {
return publicPath || '';
}
================================================
FILE: packages/editor-core/src/utils/index.ts
================================================
export * from './get-public-path';
export * from './obx';
export * from './request';
export * from './focus-tracker';
export * from './control';
export * from './preference';
================================================
FILE: packages/editor-core/src/utils/logger.ts
================================================
import { Logger, Level } from '@alilc/lowcode-utils';
export { Logger };
export function getLogger(config: { level: Level; bizName: string }): Logger {
return new Logger(config);
}
================================================
FILE: packages/editor-core/src/utils/obx.ts
================================================
import { observer } from 'mobx-react';
import { configure } from 'mobx';
configure({ enforceActions: 'never' });
// 常用的直接导出,其他的以 mobx 命名空间导出
export {
observable as obx,
observable,
observe,
autorun,
makeObservable,
makeAutoObservable,
reaction,
computed,
action,
runInAction,
untracked,
} from 'mobx';
export type { IReactionDisposer, IReactionPublic, IReactionOptions } from 'mobx';
export * as mobx from 'mobx';
export { observer };
================================================
FILE: packages/editor-core/src/utils/preference.ts
================================================
import store from 'store';
import { getLogger } from './logger';
import { IPublicModelPreference } from '@alilc/lowcode-types';
const logger = getLogger({ level: 'warn', bizName: 'Preference' });
const STORAGE_KEY_PREFIX = 'ale';
/**
* used to store user preferences, such as pinned status of a pannel.
* save to local storage.
*/
export default class Preference implements IPublicModelPreference {
getStorageKey(key: string, module?: string): string {
const moduleKey = module || '__inner__';
return `${STORAGE_KEY_PREFIX}_${moduleKey}.${key}`;
}
set(key: string, value: any, module?: string): void {
if (!key || typeof key !== 'string' || key.length === 0) {
logger.error('Invalid key when setting preference', key);
return;
}
const storageKey = this.getStorageKey(key, module);
logger.debug('storageKey:', storageKey, 'set with value:', value);
store.set(storageKey, value);
}
get(key: string, module: string): any {
if (!key || typeof key !== 'string' || key.length === 0) {
logger.error('Invalid key when getting from preference', key);
return;
}
const storageKey = this.getStorageKey(key, module);
const result = store.get(storageKey);
logger.debug('storageKey:', storageKey, 'get with result:', result);
return result;
}
/**
* check if local storage contain certain key
*
* @param {string} key
* @param {string} module
*/
contains(key: string, module: string): boolean {
if (!key || typeof key !== 'string' || key.length === 0) {
logger.error('Invalid key when getting from preference', key);
return false;
}
const storageKey = this.getStorageKey(key, module);
const result = store.get(storageKey);
return !(result === undefined || result === null);
}
}
================================================
FILE: packages/editor-core/src/utils/request.ts
================================================
import Debug from 'debug';
const debug = Debug('request');
export function serialize(obj?: object): string {
if (!obj) {
return '';
}
const rst: string[] = [];
Object.entries(obj || {}).forEach(([key, val]): void => {
if (val === null || val === undefined || val === '') return;
if (typeof val === 'object') rst.push(`${key}=${encodeURIComponent(JSON.stringify(val))}`);
else rst.push(`${key}=${encodeURIComponent(val)}`);
});
return rst.join('&');
}
export function buildUrl(dataAPI: string, params?: object): string {
const paramStr = serialize(params);
if (paramStr) {
return dataAPI.indexOf('?') > 0 ? `${dataAPI}&${paramStr}` : `${dataAPI}?${paramStr}`;
}
return dataAPI;
}
export function get(dataAPI: string, params?: object, headers?: object, otherProps?: object): Promise {
const fetchHeaders = {
Accept: 'application/json',
...headers,
};
return request(buildUrl(dataAPI, params), 'GET', undefined, fetchHeaders, otherProps);
}
export function post(dataAPI: string, params?: object, headers?: object, otherProps?: object): Promise {
const fetchHeaders = {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
...headers,
};
return request(
dataAPI,
'POST',
fetchHeaders['Content-Type'].indexOf('application/json') > -1 || Array.isArray(params)
? JSON.stringify(params)
: serialize(params),
fetchHeaders,
otherProps,
);
}
export function request(
dataAPI: string,
method = 'GET',
data?: object | string,
headers?: object,
otherProps?: any,
): Promise {
return new Promise((resolve, reject): void => {
if (otherProps && otherProps.timeout) {
setTimeout((): void => {
reject(new Error('timeout'));
}, otherProps.timeout);
}
fetch(dataAPI, {
method,
credentials: 'include',
headers,
body: data,
...otherProps,
})
.then((response: Response): any => {
switch (response.status) {
case 200:
case 201:
case 202:
return response.json();
case 204:
if (method === 'DELETE') {
return {
success: true,
};
} else {
return {
__success: false,
code: response.status,
};
}
case 400:
case 401:
case 403:
case 404:
case 406:
case 410:
case 422:
case 500:
return response
.json()
.then((res: object): any => {
return {
__success: false,
code: response.status,
data: res,
};
})
.catch((): object => {
return {
__success: false,
code: response.status,
};
});
default:
return null;
}
})
.then((json: any): void => {
if (json && json.__success !== false) {
resolve(json);
} else {
delete json.__success;
reject(json);
}
})
.catch((err: Error): void => {
debug(err);
reject(err);
});
});
}
================================================
FILE: packages/editor-core/src/widgets/index.ts
================================================
// TODO move another place
export * from './tip';
export * from './title';
================================================
FILE: packages/editor-core/src/widgets/tip/help-tips.tsx
================================================
import { IPublicTypeHelpTipConfig, IPublicTypeTipConfig } from '@alilc/lowcode-types';
import { Tip } from './tip';
import { Icon } from '@alifd/next';
import { IconProps } from '@alifd/next/types/icon';
export function HelpTip({
help,
direction = 'top',
size = 'small',
}: {
help: IPublicTypeHelpTipConfig;
direction?: IPublicTypeTipConfig['direction'];
size?: IconProps['size'];
}) {
if (typeof help === 'string') {
return (
{help}
);
}
if (typeof help === 'object' && help.url) {
return (
);
}
return (
{help.content}
);
}
================================================
FILE: packages/editor-core/src/widgets/tip/index.ts
================================================
import './style.less';
export * from './tip';
export * from './tip-container';
export * from './help-tips';
================================================
FILE: packages/editor-core/src/widgets/tip/style.less
================================================
@keyframes shake {
from,
to {
margin: 0;
}
20%,
60% {
margin: 0 10px 0 -10px;
}
40%,
80% {
margin: 0 -10px 0 10px;
}
}
@keyframes drop {
from {
transform: translateY(-100%);
}
to {
transform: translateY(0);
}
}
@keyframes appear-left {
from {
transform: translateX(8px);
opacity: 0.8;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes appear-right {
from {
transform: translateX(-8px);
opacity: 0.8;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes appear-top {
from {
transform: translateY(8px);
opacity: 0.8;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes appear-bottom {
from {
transform: translateY(-8px);
opacity: 0.8;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes scale {
from {
transform: scale(0.9);
}
to {
transform: scale(1);
}
}
@keyframes spining {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
from,
to {
transform: scale(1);
opacity: 0.7;
}
50% {
transform: scale(1.02);
opacity: 1;
}
}
.lc-arrow {
position: absolute;
width: 36px;
height: 10px;
box-sizing: border-box;
overflow: hidden;
&:after {
content: '';
display: block;
width: 0;
height: 0;
margin: 0 auto;
border: 8px solid transparent;
border-top-color: var(--color-pane-background, rgb(255, 255, 255));
}
transform-origin: 0 0;
}
.lc-align-top > .lc-arrow {
bottom: 0;
left: 0;
transform: translateY(100%);
}
.lc-align-right > .lc-arrow {
left: 0;
top: 0;
transform: rotate(90deg);
}
.lc-align-left > .lc-arrow {
right: 0;
top: 0;
transform-origin: right top;
transform: rotate(-90deg);
}
.lc-align-bottom > .lc-arrow {
top: 0;
left: 0;
transform: scaleY(-1);
}
.lc-tip {
z-index: 2;
position: fixed;
box-sizing: border-box;
background: var(--color-layer-tooltip-background);
max-height: 400px;
color: var(--color-text-reverse, rgba(255, 255, 255, 0.8));
left: 0;
top: 0;
visibility: hidden;
opacity: 0;
border-radius: 3px;
padding: 6px 8px;
text-shadow: 0 -1px var(--color-field-label, rgba(0, 0, 0, 0.3));
font-size: var(--font-size-text);
line-height: 14px;
max-width: 200px;
pointer-events: none;
&.lc-align-top {
transform: translateY(8px);
}
&.lc-align-bottom {
transform: translateY(-8px);
}
&.lc-align-left {
transform: translateX(8px);
}
&.lc-align-right {
transform: translateX(-8px);
}
.lc-arrow {
width: 24px;
height: 8px;
&:after {
border: 6px solid transparent;
border-top-color: var(--color-layer-tooltip-background, rgba(0, 0, 0, 0.7));
}
}
&.lc-theme-black {
background: var(--color-icon-pane, rgba(0, 0, 0, 0.7));
.lc-arrow:after {
border-top-color: var(--color-layer-tooltip-background, rgba(0, 0, 0, 0.7));
}
}
&.lc-theme-green {
background: var(--color-success-dark, var(--color-function-success-dark, #57a672));
.lc-arrow:after {
border-top-color: var(--color-success-dark, var(--color-function-success-dark, #57a672));
}
}
&.lc-visible {
visibility: visible;
}
&.lc-visible-animate {
visibility: visible;
opacity: 1;
transition: transform ease-out 200ms, opacity ease-out 200ms;
}
will-change: transform, width, height, opacity, left, top;
}
.lc-tips-container {
pointer-events: none;
position: fixed;
top: 0;
left: 0;
overflow: visible;
z-index: 2000;
}
================================================
FILE: packages/editor-core/src/widgets/tip/tip-container.tsx
================================================
import { Component } from 'react';
import ReactDOM from 'react-dom';
import { TipItem } from './tip-item';
import { tipHandler } from './tip-handler';
export class TipContainer extends Component {
private dispose?: () => void;
shouldComponentUpdate() {
return false;
}
componentDidMount() {
const over = (e: MouseEvent) => tipHandler.setTarget(e.target as any);
const down = () => tipHandler.hideImmediately();
document.addEventListener('mouseover', over, false);
document.addEventListener('mousedown', down, true);
this.dispose = () => {
document.removeEventListener('mouseover', over, false);
document.removeEventListener('mousedown', down, true);
};
}
UNSAFE_componentWillMount() {
if (this.dispose) {
this.dispose();
}
}
render() {
return ReactDOM.createPortal(
,
document.querySelector('body')!,
);
}
}
================================================
FILE: packages/editor-core/src/widgets/tip/tip-handler.ts
================================================
import { IPublicTypeTipConfig } from '@alilc/lowcode-types';
import { IEventBus, createModuleEventBus } from '../../event-bus';
export interface TipOptions extends IPublicTypeTipConfig {
target: HTMLElement;
}
class TipHandler {
tip: TipOptions | null = null;
private showDelay: number | null = null;
private hideDelay: number | null = null;
private emitter: IEventBus = createModuleEventBus('TipHandler');
setTarget(target: HTMLElement) {
const tip = findTip(target);
if (tip) {
if (this.tip) {
// the some target should return
if ((this.tip as any).target === (tip as any).target) {
this.tip = tip;
return;
}
// not show already, reset show delay
if (this.showDelay) {
clearTimeout(this.showDelay);
this.showDelay = null;
this.tip = null;
} else {
if (this.hideDelay) {
clearTimeout(this.hideDelay);
this.hideDelay = null;
}
this.tip = tip;
this.emitter.emit('tipchange');
return;
}
}
this.tip = tip;
if (this.hideDelay) {
clearTimeout(this.hideDelay);
this.hideDelay = null;
this.emitter.emit('tipchange');
} else {
this.showDelay = setTimeout(() => {
this.showDelay = null;
this.emitter.emit('tipchange');
}, 350) as any;
}
} else {
if (this.showDelay) {
clearTimeout(this.showDelay);
this.showDelay = null;
} else {
this.hideDelay = setTimeout(() => {
this.hideDelay = null;
}, 100) as any;
}
this.tip = null;
this.emitter.emit('tipchange');
}
}
hideImmediately() {
if (this.hideDelay) {
clearTimeout(this.hideDelay);
this.hideDelay = null;
}
if (this.showDelay) {
clearTimeout(this.showDelay);
this.showDelay = null;
}
this.tip = null;
this.emitter.emit('tipchange');
}
onChange(func: () => void) {
this.emitter.on('tipchange', func);
return () => {
this.emitter.removeListener('tipchange', func);
};
}
}
export const tipHandler = new TipHandler();
function findTip(target: HTMLElement | null): TipOptions | null {
if (!target) {
return null;
}
// optimize deep finding on mouseover
let loopupLimit = 10;
while (target && loopupLimit-- > 0) {
// get tip from target node
if (target.dataset && target.dataset.tip) {
return {
children: target.dataset.tip,
direction: (target.dataset.direction || target.dataset.dir) as any,
theme: target.dataset.theme,
target,
};
}
// or get tip from child nodes
let child: HTMLElement | null = target.lastElementChild as HTMLElement;
while (child) {
if (child.dataset && child.dataset.role === 'tip') {
const { tipId } = child.dataset;
if (!tipId) {
return null;
}
const tipProps = tipsMap.get(tipId);
if (!tipProps) {
return null;
}
return {
...tipProps,
target,
};
}
child = child.previousElementSibling as HTMLElement;
}
target = target.parentNode as HTMLElement;
}
return null;
}
const tipsMap = new Map();
export function postTip(id: string, props: IPublicTypeTipConfig | null) {
if (props) {
tipsMap.set(id, props);
} else {
tipsMap.delete(id);
}
}
================================================
FILE: packages/editor-core/src/widgets/tip/tip-item.tsx
================================================
import { Component } from 'react';
import classNames from 'classnames';
import { IPublicTypeTipConfig } from '@alilc/lowcode-types';
import { intl } from '../../intl';
import { resolvePosition } from './utils';
import { tipHandler } from './tip-handler';
export class TipItem extends Component {
private dispose?: () => void;
constructor(props: any) {
super(props);
this.dispose = tipHandler.onChange(() => this.forceUpdate());
}
shouldComponentUpdate() {
return false;
}
componentDidMount() {
this.updateTip();
}
componentDidUpdate() {
this.updateTip();
}
componentWillUnmount() {
if (this.dispose) {
this.dispose();
}
this.clearTimer();
}
private timer: number | null = null;
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
private shell: HTMLDivElement | null = null;
private originClassName = '';
updateTip() {
if (!this.shell) {
return;
}
const { shell } = this;
const arrow = shell.querySelector('.lc-arrow') as HTMLElement;
// reset
shell.className = this.originClassName;
shell.style.cssText = '';
arrow.style.cssText = '';
this.clearTimer();
const { tip } = tipHandler;
if (!tip) {
return;
}
const { target, direction } = tip;
const targetRect = target.getBoundingClientRect();
if (targetRect.width === 0 || targetRect.height === 0) {
return;
}
const shellRect = shell.getBoundingClientRect();
const bounds = {
left: 1,
top: 1,
right: document.documentElement.clientWidth - 1,
bottom: document.documentElement.clientHeight - 1,
};
const arrowRect = arrow.getBoundingClientRect();
const { dir, left, top, arrowLeft, arrowTop } = resolvePosition(
shellRect,
targetRect,
arrowRect,
bounds,
direction,
);
shell.classList.add(`lc-align-${dir}`);
shell.style.top = `${top}px`;
shell.style.left = `${left}px`;
shell.style.width = `${shellRect.width}px`;
shell.style.height = `${shellRect.height}px`;
if (dir === 'top' || dir === 'bottom') {
arrow.style.left = `${arrowLeft}px`;
} else {
arrow.style.top = `${arrowTop}px`;
}
this.timer = window.setTimeout(() => {
shell.classList.add('lc-visible-animate');
shell.style.transform = 'none';
}, 10); /**/
}
render() {
const tip: IPublicTypeTipConfig = tipHandler.tip || ({} as any);
const className = classNames('lc-tip', tip.className, tip && tip.theme ? `lc-theme-${tip.theme}` : null);
this.originClassName = className;
return (
{
this.shell = ref;
}}
>
{intl(tip.children)}
);
}
}
================================================
FILE: packages/editor-core/src/widgets/tip/tip.tsx
================================================
import { Component } from 'react';
import { IPublicTypeTipConfig } from '@alilc/lowcode-types';
import { uniqueId } from '@alilc/lowcode-utils';
import { postTip } from './tip-handler';
export class Tip extends Component {
private id = uniqueId('tips$');
componentWillUnmount() {
postTip(this.id, null);
}
render() {
postTip(this.id, this.props);
return ;
}
}
================================================
FILE: packages/editor-core/src/widgets/tip/utils.ts
================================================
function resolveEdge(popup: any, target: any, arrow: any, bounds: any) {
const sx = arrow.width > target.width ? (arrow.width - target.width) / 2 : 0;
const sy = arrow.width > target.height ? (arrow.width - target.height) / 2 : 0;
const top = Math.max(target.top - popup.height + arrow.width - sy, bounds.top);
const right = Math.min(target.right + popup.width - arrow.width + sx, bounds.right);
const bottom = Math.min(target.bottom + popup.height - arrow.width + sy, bounds.bottom);
const left = Math.max(target.left - popup.width + arrow.width - sx, bounds.left);
return { top, right, bottom, left };
}
function resolveDirection(popup: any, target: any, edge: any, bounds: any, prefers: any) {
if (prefers.forceDirection) {
return prefers.dir;
}
const extendWidth = popup.width + popup.extraOffset;
const extendHeight = popup.height + popup.extraOffset;
const SY = popup.width * extendHeight;
const SX = popup.height * extendWidth;
const mw = Math.min(edge.right - edge.left, popup.width);
const mh = Math.min(edge.bottom - edge.top, popup.height);
const mat: any = {
top: () => {
const s = mw * Math.min(target.top - bounds.top, extendHeight);
return { s, enough: s >= SY };
},
bottom: () => {
const s = mw * Math.min(bounds.bottom - target.bottom, extendHeight);
return { s, enough: s >= SY };
},
left: () => {
const s = mh * Math.min(target.left - bounds.left, extendWidth);
return { s, enough: s >= SX };
},
right: () => {
const s = mh * Math.min(bounds.right - target.right, extendWidth);
return { s, enough: s >= SX };
},
};
const orders = ['top', 'right', 'bottom', 'left'];
if (prefers.dir) {
const i = orders.indexOf(prefers.dir);
if (i > -1) {
orders.splice(i, 1);
orders.unshift(prefers.dir);
}
}
let ms = 0;
let prefer = orders[0];
for (let i = 0, l = orders.length; i < l; i++) {
const dir = orders[i];
const { s, enough } = mat[dir]();
if (enough) {
return dir;
}
if (s > ms) {
ms = s;
prefer = dir;
}
}
return prefer;
}
function resolvePrefer(prefer: any, targetRect: any, bounds: any) {
if (!prefer) {
if (targetRect.left - bounds.left < 10) {
return { dir: 'right' };
} else if (targetRect.top - bounds.top < 10) {
return { dir: 'bottom' };
} else if (bounds.bottom - targetRect.bottom < 10) {
return { dir: 'top' };
} else if (bounds.right - targetRect.right < 10) {
return { dir: 'left' };
}
return {};
}
const force = prefer[0] === '!';
if (force) {
prefer = prefer.slice(1);
}
let [dir, offset] = prefer.split(/\s+/);
let forceDirection = false;
let forceOffset = false;
if (dir === 'center') {
dir = 'auto';
if (!offset) {
offset = 'center';
}
}
if (force) {
if (dir && dir !== 'auto') {
forceDirection = true;
}
if (offset && offset !== 'auto') {
forceOffset = true;
}
}
return { dir, offset, forceDirection, forceOffset };
}
export function resolvePosition(popup: any, target: any, arrow: any, bounds: any, prefer: any) {
popup = {
extraOffset: arrow.height,
top: popup.top,
right: popup.right,
left: popup.left,
bottom: popup.bottom,
height: popup.height,
width: popup.width,
};
const prefers = resolvePrefer(prefer, target, bounds);
const edge = resolveEdge(popup, target, arrow, bounds);
// 选择方向
const dir = resolveDirection(popup, target, edge, bounds, prefers);
let top;
let left;
let arrowTop;
let arrowLeft;
// 或得该方位上横向 或 纵向的 偏移
if (dir === 'top' || dir === 'bottom') {
if (dir === 'top') {
top = target.top - popup.extraOffset - popup.height;
} else {
top = target.bottom + popup.extraOffset;
}
// 解决横向偏移
const offset = arrow.width > target.width ? (arrow.width - target.width) / 2 : 0;
const minLeft = target.left + arrow.width - offset - popup.width;
const maxLeft = target.right - arrow.width + offset;
const centerLeft = target.left - (popup.width - target.width) / 2;
if (prefers.offset === 'left') {
left = minLeft;
} else if (prefers.offset === 'right') {
left = maxLeft;
} else {
left = centerLeft;
}
if (!prefers.forceOffset) {
left = Math.max(Math.min(edge.right - popup.width, left), minLeft);
left = Math.min(Math.max(edge.left, left), maxLeft);
}
arrowLeft = Math.min(popup.width - arrow.width, Math.max(target.left - (arrow.width - target.width) / 2 - left, 0));
} else {
if (dir === 'left') {
left = target.left - popup.extraOffset - popup.width;
} else {
left = target.right + popup.extraOffset;
}
// 解决纵向偏移
const offset = arrow.width > target.height ? (arrow.width - target.height) / 2 : 0;
const minTop = target.top + arrow.width - offset - popup.height;
const maxTop = target.bottom - arrow.width + offset;
const centerTop = target.top - (popup.height - target.height) / 2;
if (prefers.offset === 'top') {
top = minTop;
} else if (prefers.offset === 'bottom') {
top = maxTop;
} else {
top = centerTop;
}
if (!prefers.forceOffset) {
top = Math.max(Math.min(edge.bottom - popup.height, top), minTop);
top = Math.min(Math.max(edge.top, top), maxTop);
}
arrowTop = Math.min(popup.height - arrow.height, Math.max(target.top - (arrow.width - target.height) / 2 - top, 0));
}
return { dir, left, top, arrowLeft, arrowTop };
}
const percentPresets: any = {
right: 1,
left: 0,
top: 0,
bottom: 1,
center: 0.5,
};
function isPercent(val: any) {
return /^[\d.]+%$/.test(val);
}
function resolveRelativeValue(val: any, offset: any, total: any) {
if (!val) {
val = 0;
} else if (isPercent(val)) {
val = (parseFloat(val) / 100) * total;
} else if (percentPresets.hasOwnProperty(val)) {
val = percentPresets[val] * total;
} else {
val = parseFloat(val);
if (isNaN(val)) {
val = 0;
}
}
return `${val + offset}px`;
}
export function resolveRelativePosition(align: any, popup: any, bounds: any) {
if (!align) {
// return default position
return {
top: '38.2%',
left: 'calc(50% - 110px)',
};
}
let [xAlign, yAlign] = align.trim().split(/\s+/);
if (xAlign === 'top' || xAlign === 'bottom' || yAlign === 'left' || yAlign === 'right') {
const tmp = xAlign;
xAlign = yAlign;
yAlign = tmp;
}
if (xAlign === 'center' && !yAlign) {
yAlign = 'center';
}
return {
left: resolveRelativeValue(xAlign, 0, bounds.right - bounds.left - popup.width),
top: resolveRelativeValue(yAlign, 0, bounds.bottom - bounds.top - popup.height),
};
}
================================================
FILE: packages/editor-core/src/widgets/title/index.tsx
================================================
import { Component, isValidElement, ReactNode } from 'react';
import classNames from 'classnames';
import { createIcon, isI18nData, isTitleConfig } from '@alilc/lowcode-utils';
import { IPublicTypeI18nData, IPublicTypeTitleConfig, IPublicTypeTitleProps } from '@alilc/lowcode-types';
import { intl } from '../../intl';
import { Tip } from '../tip';
import './title.less';
/**
* 根据 keywords 将 label 分割成文字片段
* 示例:title = '自定义页面布局',keywords = '页面',返回结果为 ['自定义', '页面', '布局']
* @param label title
* @param keywords 关键字
* @returns 文字片段列表
*/
function splitLabelByKeywords(label: string, keywords: string): string[] {
const len = keywords.length;
const fragments = [];
let str = label;
while (str.length > 0) {
const index = str.indexOf(keywords);
if (index === 0) {
fragments.push(keywords);
str = str.slice(len);
} else if (index < 0) {
fragments.push(str);
str = '';
} else {
fragments.push(str.slice(0, index));
str = str.slice(index);
}
}
return fragments;
}
export class Title extends Component {
constructor(props: any) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e: React.MouseEvent) {
const { title, onClick } = this.props as any;
const url = title && (title.docUrl || title.url);
if (url) {
window.open(url);
// 防止触发行操作(如折叠面板)
e.stopPropagation();
}
// TODO: 操作交互冲突,目前 mixedSetter 仅有 2 个 setter 注册时用到了 onClick
onClick && onClick(e);
}
renderLabel = (label: string | IPublicTypeI18nData | ReactNode) => {
let { match, keywords } = this.props;
if (!label) {
return null;
}
const intlLabel = intl(label);
if (typeof intlLabel !== 'string') {
return {intlLabel};
}
let labelToRender: ReactNode = intlLabel;
if (match && keywords) {
const fragments = splitLabelByKeywords(intlLabel as string, keywords);
labelToRender = fragments.map(f => {f});
}
return (
{labelToRender}
);
};
render() {
// eslint-disable-next-line prefer-const
const { title, className } = this.props;
let _title: IPublicTypeTitleConfig;
if (title == null) {
return null;
}
if (isValidElement(title)) {
return title;
}
if (typeof title === 'string' || isI18nData(title)) {
_title = { label: title };
} else if (isTitleConfig(title)) {
_title = title;
} else {
_title = {
label: title,
};
}
const icon = _title.icon ? createIcon(_title.icon, { size: 20 }) : null;
let tip: any = null;
if (_title.tip) {
if (isValidElement(_title.tip) && _title.tip.type === Tip) {
tip = _title.tip;
} else {
const tipProps =
typeof _title.tip === 'object' && !(isValidElement(_title.tip) || isI18nData(_title.tip))
? _title.tip
: { children: _title.tip };
tip = ;
}
}
return (
{icon ? {icon} : null}
{this.renderLabel(_title.label)}
{tip}
);
}
}
================================================
FILE: packages/editor-core/src/widgets/title/title.less
================================================
.lc-title {
display: inline-flex;
align-items: center;
color: var(--color-text);
.lc-title-icon {
display: flex;
align-items: center;
margin-right: 4px;
img {
width: 16px;
height: 16px;
filter: brightness(0) invert(1);
}
}
&.only-icon {
.lc-title-icon {
margin-right: 0;
}
}
&.has-tip {
cursor: help;
text-decoration-line: underline;
text-decoration-style: dashed;
text-decoration-color: var(--color-text-light, rgba(31, 56, 88, .3));
}
line-height: initial !important;
word-break: break-all;
}
.actived .lc-title {
color: var(--color-actived);
}
================================================
FILE: packages/editor-core/test/command.test.ts
================================================
import { Command } from '../src/command';
describe('Command', () => {
let commandInstance;
let mockHandler;
beforeEach(() => {
commandInstance = new Command();
mockHandler = jest.fn();
});
describe('registerCommand', () => {
it('should register a command successfully', () => {
const command = {
name: 'testCommand',
handler: mockHandler,
};
commandInstance.registerCommand(command, { commandScope: 'testScope' });
const registeredCommand = commandInstance.listCommands().find(c => c.name === 'testScope:testCommand');
expect(registeredCommand).toBeDefined();
expect(registeredCommand.name).toBe('testScope:testCommand');
});
it('should throw an error if commandScope is not provided', () => {
const command = {
name: 'testCommand',
handler: mockHandler,
};
expect(() => {
commandInstance.registerCommand(command);
}).toThrow('plugin meta.commandScope is required.');
});
it('should throw an error if command is already registered', () => {
const command = {
name: 'testCommand',
handler: mockHandler,
};
commandInstance.registerCommand(command, { commandScope: 'testScope' });
expect(() => {
commandInstance.registerCommand(command, { commandScope: 'testScope' });
}).toThrow(`Command 'testCommand' is already registered.`);
});
});
afterEach(() => {
jest.clearAllMocks();
});
});
describe('unregisterCommand', () => {
let commandInstance;
let mockHandler;
beforeEach(() => {
commandInstance = new Command();
mockHandler = jest.fn();
// 先注册一个命令以便之后注销
const command = {
name: 'testCommand',
handler: mockHandler,
};
commandInstance.registerCommand(command, { commandScope: 'testScope' });
});
it('should unregister a command successfully', () => {
const commandName = 'testScope:testCommand';
expect(commandInstance.listCommands().find(c => c.name === commandName)).toBeDefined();
commandInstance.unregisterCommand(commandName);
expect(commandInstance.listCommands().find(c => c.name === commandName)).toBeUndefined();
});
it('should throw an error if the command is not registered', () => {
const nonExistingCommandName = 'testScope:nonExistingCommand';
expect(() => {
commandInstance.unregisterCommand(nonExistingCommandName);
}).toThrow(`Command '${nonExistingCommandName}' is not registered.`);
});
afterEach(() => {
jest.clearAllMocks();
});
});
describe('executeCommand', () => {
let commandInstance;
let mockHandler;
beforeEach(() => {
commandInstance = new Command();
mockHandler = jest.fn();
// 注册一个带参数校验的命令
const command = {
name: 'testCommand',
handler: mockHandler,
parameters: [
{ name: 'param1', propType: 'string' },
{ name: 'param2', propType: 'number' }
],
};
commandInstance.registerCommand(command, { commandScope: 'testScope' });
});
it('should execute a command successfully', () => {
const commandName = 'testScope:testCommand';
const args = { param1: 'test', param2: 42 };
commandInstance.executeCommand(commandName, args);
expect(mockHandler).toHaveBeenCalledWith(args);
});
it('should throw an error if the command is not registered', () => {
const nonExistingCommandName = 'testScope:nonExistingCommand';
expect(() => {
commandInstance.executeCommand(nonExistingCommandName, {});
}).toThrow(`Command '${nonExistingCommandName}' is not registered.`);
});
it('should throw an error if arguments are invalid', () => {
const commandName = 'testScope:testCommand';
const invalidArgs = { param1: 'test', param2: 'not-a-number' }; // param2 should be a number
expect(() => {
commandInstance.executeCommand(commandName, invalidArgs);
}).toThrow(`Command '${commandName}' arguments param2 is invalid.`);
});
it('should handle errors thrown by the command handler', () => {
const commandName = 'testScope:testCommand';
const args = { param1: 'test', param2: 42 };
const errorMessage = 'Command handler error';
mockHandler.mockImplementation(() => {
throw new Error(errorMessage);
});
expect(() => {
commandInstance.executeCommand(commandName, args);
}).toThrow(errorMessage);
});
afterEach(() => {
jest.clearAllMocks();
});
});
describe('batchExecuteCommand', () => {
let commandInstance;
let mockHandler;
let mockExecuteTransaction;
let mockPluginContext;
beforeEach(() => {
commandInstance = new Command();
mockHandler = jest.fn();
mockExecuteTransaction = jest.fn(callback => callback());
mockPluginContext = {
common: {
utils: {
executeTransaction: mockExecuteTransaction
}
}
};
// 注册几个命令
const command1 = {
name: 'testCommand1',
handler: mockHandler,
};
const command2 = {
name: 'testCommand2',
handler: mockHandler,
};
commandInstance.registerCommand(command1, { commandScope: 'testScope' });
commandInstance.registerCommand(command2, { commandScope: 'testScope' });
});
it('should execute a batch of commands', () => {
const commands = [
{ name: 'testScope:testCommand1', args: { param: 'value1' } },
{ name: 'testScope:testCommand2', args: { param: 'value2' } },
];
commandInstance.batchExecuteCommand(commands, mockPluginContext);
expect(mockExecuteTransaction).toHaveBeenCalledTimes(1);
expect(mockHandler).toHaveBeenCalledWith({ param: 'value1' });
expect(mockHandler).toHaveBeenCalledWith({ param: 'value2' });
});
it('should not execute anything if commands array is empty', () => {
commandInstance.batchExecuteCommand([], mockPluginContext);
expect(mockExecuteTransaction).not.toHaveBeenCalled();
expect(mockHandler).not.toHaveBeenCalled();
});
it('should handle errors thrown during command execution', () => {
const errorMessage = 'Command handler error';
mockHandler.mockImplementation(() => {
throw new Error(errorMessage);
});
const commands = [
{ name: 'testScope:testCommand1', args: { param: 'value1' } },
{ name: 'testScope:testCommand2', args: { param: 'value2' } },
];
expect(() => {
commandInstance.batchExecuteCommand(commands, mockPluginContext);
}).toThrow(errorMessage);
expect(mockExecuteTransaction).toHaveBeenCalledTimes(1); // Still called once
});
afterEach(() => {
jest.clearAllMocks();
});
});
describe('listCommands', () => {
let commandInstance;
let mockHandler;
beforeEach(() => {
commandInstance = new Command();
mockHandler = jest.fn();
});
it('should list all registered commands', () => {
// 注册几个命令
const command1 = {
name: 'testCommand1',
handler: mockHandler,
description: 'Test Command 1',
parameters: [{ name: 'param1', propType: 'string' }]
};
const command2 = {
name: 'testCommand2',
handler: mockHandler,
description: 'Test Command 2',
parameters: [{ name: 'param2', propType: 'number' }]
};
commandInstance.registerCommand(command1, { commandScope: 'testScope' });
commandInstance.registerCommand(command2, { commandScope: 'testScope' });
const listedCommands = commandInstance.listCommands();
expect(listedCommands.length).toBe(2);
expect(listedCommands).toEqual(expect.arrayContaining([
expect.objectContaining({
name: 'testScope:testCommand1',
description: 'Test Command 1',
parameters: [{ name: 'param1', propType: 'string' }]
}),
expect.objectContaining({
name: 'testScope:testCommand2',
description: 'Test Command 2',
parameters: [{ name: 'param2', propType: 'number' }]
})
]));
});
it('should return an empty array if no commands are registered', () => {
const listedCommands = commandInstance.listCommands();
expect(listedCommands).toEqual([]);
});
afterEach(() => {
jest.clearAllMocks();
});
});
describe('onCommandError', () => {
let commandInstance;
let mockHandler;
let mockErrorHandler1;
let mockErrorHandler2;
beforeEach(() => {
commandInstance = new Command();
mockHandler = jest.fn();
mockErrorHandler1 = jest.fn();
mockErrorHandler2 = jest.fn();
// 注册一个命令,该命令会抛出错误
const command = {
name: 'testCommand',
handler: () => {
throw new Error('Command execution failed');
},
};
commandInstance.registerCommand(command, { commandScope: 'testScope' });
});
it('should call all registered error handlers when a command throws an error', () => {
const commandName = 'testScope:testCommand';
commandInstance.onCommandError(mockErrorHandler1);
commandInstance.onCommandError(mockErrorHandler2);
expect(() => {
commandInstance.executeCommand(commandName, {});
}).not.toThrow();
// 确保所有错误处理函数都被调用,并且传递了正确的参数
expect(mockErrorHandler1).toHaveBeenCalledWith(commandName, expect.any(Error));
expect(mockErrorHandler2).toHaveBeenCalledWith(commandName, expect.any(Error));
});
it('should throw the error if no error handlers are registered', () => {
const commandName = 'testScope:testCommand';
expect(() => {
commandInstance.executeCommand(commandName, {});
}).toThrow('Command execution failed');
});
afterEach(() => {
jest.clearAllMocks();
});
});
================================================
FILE: packages/editor-skeleton/build.json
================================================
{
"plugins": [
"@alilc/build-plugin-lce",
"build-plugin-fusion",
["build-plugin-moment-locales", {
"locales": ["zh-cn"]
}]
]
}
================================================
FILE: packages/editor-skeleton/build.test.json
================================================
{
"plugins": [
"@alilc/build-plugin-lce",
"@alilc/lowcode-test-mate/plugin/index.ts"
],
"babelPlugins": [
["@babel/plugin-proposal-private-property-in-object", { "loose": true }]
]
}
================================================
FILE: packages/editor-skeleton/jest.config.js
================================================
const fs = require('fs');
const { join } = require('path');
const esModules = [].join('|');
const pkgNames = fs.readdirSync(join('..')).filter(pkgName => !pkgName.startsWith('.'));
const jestConfig = {
// transform: {
// '^.+\\.[jt]sx?$': 'babel-jest',
// // '^.+\\.(ts|tsx)$': 'ts-jest',
// // '^.+\\.(js|jsx)$': 'babel-jest',
// },
transformIgnorePatterns: [
`/node_modules/(?!${esModules})/`,
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
collectCoverage: false,
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!**/node_modules/**',
'!**/vendor/**',
],
};
// 只对本仓库内的 pkg 做 mapping
jestConfig.moduleNameMapper = {};
jestConfig.moduleNameMapper[`^@alilc/lowcode\\-(${pkgNames.join('|')})$`] = '/../$1/src';
module.exports = jestConfig;
================================================
FILE: packages/editor-skeleton/package.json
================================================
{
"name": "@alilc/lowcode-editor-skeleton",
"version": "1.3.2",
"description": "alibaba lowcode editor skeleton",
"main": "lib/index.js",
"module": "es/index.js",
"stylePath": "style.js",
"files": [
"lib",
"es"
],
"scripts": {
"test": "build-scripts test --config build.test.json",
"build": "build-scripts build"
},
"keywords": [
"lowcode",
"editor"
],
"dependencies": {
"@alifd/next": "^1.20.12",
"@alilc/lowcode-designer": "1.3.2",
"@alilc/lowcode-editor-core": "1.3.2",
"@alilc/lowcode-types": "1.3.2",
"@alilc/lowcode-utils": "1.3.2",
"classnames": "^2.2.6",
"react": "^16.8.1",
"react-dom": "^16.8.1"
},
"devDependencies": {
"@alib/build-scripts": "^0.1.3",
"@types/react": "^16.9.13",
"@types/react-dom": "^16.9.4",
"build-plugin-fusion": "^0.1.0",
"build-plugin-moment-locales": "^0.1.0"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "http",
"url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/editor-skeleton"
},
"gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6",
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
"homepage": "https://github.com/alibaba/lowcode-engine/#readme"
}
================================================
FILE: packages/editor-skeleton/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "lib"
},
"include": [
"./src/"
]
}
================================================
FILE: packages/editor-skeleton/src/area.ts
================================================
/* eslint-disable max-len */
import { obx, computed, makeObservable } from '@alilc/lowcode-editor-core';
import { Logger } from '@alilc/lowcode-utils';
import { IPublicTypeWidgetBaseConfig } from '@alilc/lowcode-types';
import { WidgetContainer } from './widget/widget-container';
import { ISkeleton } from './skeleton';
import { IWidget } from './widget/widget';
const logger = new Logger({ level: 'warn', bizName: 'skeleton:area' });
export interface IArea {
isEmpty(): boolean;
add(config: T | C): T;
remove(config: T | string): number;
setVisible(flag: boolean): void;
hide(): void;
show(): void;
}
export class Area implements IArea {
@obx private _visible = true;
@computed get visible() {
if (this.exclusive) {
return this.container.current != null;
}
return this._visible;
}
get current() {
if (this.exclusive) {
return this.container.current;
}
return null;
}
readonly container: WidgetContainer;
private lastCurrent: T | null = null;
constructor(readonly skeleton: ISkeleton, readonly name: string, handle: (item: T | C) => T, private exclusive?: boolean, defaultSetCurrent = false) {
makeObservable(this);
this.container = skeleton.createContainer(name, handle, exclusive, () => this.visible, defaultSetCurrent);
}
isEmpty(): boolean {
return this.container.items.length < 1;
}
add(config: T | C): T {
const item = this.container.get(config.name);
if (item) {
logger.warn(`The ${config.name} has already been added to skeleton.`);
return item;
}
return this.container.add(config);
}
remove(config: T | string): number {
return this.container.remove(config);
}
setVisible(flag: boolean) {
if (this.exclusive) {
const { current } = this.container;
if (flag && !current) {
this.container.active(this.lastCurrent || this.container.getAt(0));
} else if (current) {
this.lastCurrent = current;
this.container.unactive(current);
}
return;
}
this._visible = flag;
}
hide() {
this.setVisible(false);
}
show() {
this.setVisible(true);
}
// ========== compatible for vision ========
/**
* @deprecated
*/
removeAction(config: string): number {
return this.remove(config);
}
}
================================================
FILE: packages/editor-skeleton/src/context.ts
================================================
import { createContext } from 'react';
import { ISkeleton } from './skeleton';
export const SkeletonContext = createContext({} as any);
================================================
FILE: packages/editor-skeleton/src/index.ts
================================================
export * from './area';
export { Workbench } from './layouts/workbench';
export * from './skeleton';
export * from './types';
export * from './components/settings';
export * from './components/field';
export * from './components/popup';
export * from './context';
export * from './register-defaults';
export * from './widget';
export * from './layouts';
================================================
FILE: packages/editor-skeleton/src/less-variables.less
================================================
/*
* 基础的 DPL 定义使用了 kuma base 的定义,参考:
* https://github.com/uxcore/kuma-base/tree/master/variables
*/
/**
* ===========================================================
* ==================== Font Family ==========================
* ===========================================================
*/
/*
* @font-family: "STHeiti", "Microsoft Yahei", "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, Verdana, sans-serif;
*/
@font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Helvetica, Arial, sans-serif;
@font-family-code: Monaco, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Helvetica, Arial,
sans-serif;
/**
* ===========================================================
* ===================== Color DPL ===========================
* ===========================================================
*/
@brand-color-1: rgba(0, 108, 255, 1);
@brand-color-2: rgba(25, 122, 255, 1);
@brand-color-3: rgba(0, 96, 229, 1);
@brand-color-1-3: rgba(0, 108, 255, 0.6);
@brand-color-1-4: rgba(0, 108, 255, 0.4);
@brand-color-1-5: rgba(0, 108, 255, 0.3);
@brand-color-1-6: rgba(0, 108, 255, 0.2);
@brand-color-1-7: rgba(0, 108, 255, 0.1);
@brand-color: @brand-color-1;
@white-alpha-1: rgb(255, 255, 255); // W-1
@white-alpha-2: rgba(255, 255, 255, 0.8); // W-2 A80
@white-alpha-3: rgba(255, 255, 255, 0.6); // W-3 A60
@white-alpha-4: rgba(255, 255, 255, 0.4); // W-4 A40
@white-alpha-5: rgba(255, 255, 255, 0.3); // W-5 A30
@white-alpha-6: rgba(255, 255, 255, 0.2); // W-6 A20
@white-alpha-7: rgba(255, 255, 255, 0.1); // W-7 A10
@white-alpha-8: rgba(255, 255, 255, 0.06); // W-8 A6
@dark-alpha-1: rgba(0, 0, 0, 1); // D-1 A100
@dark-alpha-2: rgba(0, 0, 0, 0.8); // D-2 A80
@dark-alpha-3: rgba(0, 0, 0, 0.6); // D-3 A60
@dark-alpha-4: rgba(0, 0, 0, 0.4); // D-4 A40
@dark-alpha-5: rgba(0, 0, 0, 0.3); // D-5 A30
@dark-alpha-6: rgba(0, 0, 0, 0.2); // D-6 A20
@dark-alpha-7: rgba(0, 0, 0, 0.1); // D-7 A10
@dark-alpha-8: rgba(0, 0, 0, 0.06); // D-8 A6
@dark-alpha-9: rgba(0, 0, 0, 0.04); // D-9 A4
@normal-alpha-1: rgba(31, 56, 88, 1); // N-1 A100
@normal-alpha-2: rgba(31, 56, 88, 0.8); // N-2 A80
@normal-alpha-3: rgba(31, 56, 88, 0.6); // N-3 A60
@normal-alpha-4: rgba(31, 56, 88, 0.4); // N-4 A40
@normal-alpha-5: rgba(31, 56, 88, 0.3); // N-5 A30
@normal-alpha-6: rgba(31, 56, 88, 0.2); // N-6 A20
@normal-alpha-7: rgba(31, 56, 88, 0.1); // N-7 A10
@normal-alpha-8: rgba(31, 56, 88, 0.06); // N-8 A6
@normal-alpha-9: rgba(31, 56, 88, 0.04); // N-9 A4
@normal-3: #77879c;
@normal-4: #a3aebd;
@normal-5: #bac3cc;
@normal-6: #d1d7de;
@gray-dark: #333; // N2_4
@gray: #666; // N2_3
@gray-light: #999; // N2_2
@gray-lighter: #ccc; // N2_1
@brand-secondary: #2c2f33; // B2_3
// 补色
@brand-complement: #00b3e8; // B3_1
// 复合
@brand-comosite: #00c587; // B3_2
// 浓度
@brand-deep: #73461d; // B3_3
// F1-1
@brand-danger: rgb(240, 70, 49);
// F1-2 (10% white)
@brand-danger-hover: rgba(240, 70, 49, 0.9);
// F1-3 (5% black)
@brand-danger-focus: rgba(240, 70, 49, 0.95);
// F2-1
@brand-warning: rgb(250, 189, 14);
// F3-1
@brand-success: rgb(102, 188, 92);
// F4-1
@brand-link: rgb(102, 188, 92);
// F4-2
@brand-link-hover: #2e76a6;
// F1-1-7 A10
@brand-danger-alpha-7: rgba(240, 70, 49, 0.1);
// F1-1-8 A6
@brand-danger-alpha-8: rgba(240, 70, 49, 0.8);
// F2-1-2 A80
@brand-warning-alpha-2: rgba(250, 189, 14, 0.8);
// F2-1-7 A10
@brand-warning-alpha-7: rgba(250, 189, 14, 0.1);
// F3-1-2 A80
@brand-success-alpha-2: rgba(102, 188, 92, 0.8);
// F3-1-7 A10
@brand-success-alpha-7: rgba(102, 188, 92, 0.1);
// F4-1-7 A10
@brand-link-alpha-7: rgba(102, 188, 92, 0.1);
// 文本色
@text-primary-color: @dark-alpha-3;
@text-secondary-color: @normal-alpha-3;
@text-thirdary-color: @dark-alpha-4;
@text-disabled-color: @normal-alpha-5;
@text-helper-color: @dark-alpha-4;
@text-danger-color: @brand-danger;
@text-ali-color: #ec6c00;
/**
* ===========================================================
* =================== Shadow Box ============================
* ===========================================================
*/
@box-shadow-1: 0 1px 4px 0 rgba(31, 56, 88, 0.15); // 1 级阴影,物体由原来存在于底面的物体展开,物体和底面关联紧密
@box-shadow-2: 0 2px 10px 0 rgba(31, 56, 88, 0.15); // 2 级阴影,hover状态,物体层级较高
@box-shadow-3: 0 4px 15px 0 rgba(31, 56, 88, 0.15); // 3 级阴影,当物体层级高于所有界面元素,弹窗用
/**
* ===========================================================
* ================= FontSize of Level =======================
* ===========================================================
*/
@fontSize-1: 26px;
@fontSize-2: 20px;
@fontSize-3: 16px;
@fontSize-4: 14px;
@fontSize-5: 12px;
@fontLineHeight-1: 38px;
@fontLineHeight-2: 30px;
@fontLineHeight-3: 26px;
@fontLineHeight-4: 24px;
@fontLineHeight-5: 20px;
/**
* ===========================================================
* ================= FontSize of Level =======================
* ===========================================================
*/
@global-border-radius: 3px;
@input-border-radius: 3px;
@popup-border-radius: 6px;
/**
* ===========================================================
* ===================== Transistion =========================
* ===========================================================
*/
@transition-duration: 0.3s;
@transition-ease: cubic-bezier(0.23, 1, 0.32, 1);
@transition-delay: 0s;
/**
* ===========================================================
* ================ Global Configruations ====================
* ===========================================================
*/
@topPaneHeight: 48px;
@actionpane-height: 48px;
@tabPaneWidth: 260px;
@input-standard-height: 32px;
@dockpane-width: 48px;
/**
* ===========================================================
* =================== Deprecated Items ======================
* ===========================================================
*/
@head-bgcolor: @white-alpha-1;
@pane-bgcolor: @white-alpha-1;
@pane-dark-bgcolor: @white-alpha-1;
@pane-bdcolor: @normal-4;
@blank-bgcolor: @normal-5;
@title-bgcolor: @white-alpha-1;
@title-bdcolor: transparent;
@section-bgcolor: transparent;
@section-bdcolor: @white-alpha-1;
@button-bgcolor: @white-alpha-1;
@button-bdcolor: transparent;
@button-blue-color: @brand-color;
@button-blue-hover-color: @brand-color;
@sub-title-bgcolor: @white-alpha-1;
@sub-title-bdcolor: transparent;
@text-color: @text-primary-color;
@icon-color: @gray;
@icon-color-active: @gray-light;
@ghost-bgcolor: @dark-alpha-3;
@input-bgcolor: transparent;
@input-bdcolor: @normal-alpha-5;
@hover-color: #5a99cc;
@active-color: #5a99cc;
@disabled-color: #666;
@setter-popup-bg: rgb(80, 86, 109);
================================================
FILE: packages/editor-skeleton/src/register-defaults.ts
================================================
import parseJSFunc from './transducers/parse-func';
import parseProps from './transducers/parse-props';
import addonCombine from './transducers/addon-combine';
import { IPublicModelPluginContext } from '@alilc/lowcode-types';
export const registerDefaults = (ctx: IPublicModelPluginContext) => {
const { material } = ctx;
return {
init() {
// parseFunc
material.registerMetadataTransducer(parseJSFunc, 1, 'parse-func');
// parseProps
material.registerMetadataTransducer(parseProps, 5, 'parse-props');
// addon/platform custom
material.registerMetadataTransducer(addonCombine, 10, 'combine-props');
},
};
};
registerDefaults.pluginName = '___register_defaults___';
================================================
FILE: packages/editor-skeleton/src/skeleton.ts
================================================
import { action, makeObservable, obx, engineConfig, IEditor, FocusTracker } from '@alilc/lowcode-editor-core';
import {
DockConfig,
WidgetConfig,
PanelDockConfig,
DialogDockConfig,
isDockConfig,
isPanelDockConfig,
isPanelConfig,
DividerConfig,
isDividerConfig,
} from './types';
import { isPanel, Panel } from './widget/panel';
import { WidgetContainer } from './widget/widget-container';
import { Area } from './area';
import { isWidget, IWidget, Widget } from './widget/widget';
import { PanelDock } from './widget/panel-dock';
import { Dock } from './widget/dock';
import { Stage, StageConfig } from './widget/stage';
import { isValidElement } from 'react';
import { isPlainObject, uniqueId, Logger } from '@alilc/lowcode-utils';
import { Divider } from '@alifd/next';
import {
EditorConfig,
PluginClassSet,
IPublicTypeWidgetBaseConfig,
IPublicTypeWidgetConfigArea,
IPublicTypeSkeletonConfig,
IPublicApiSkeleton,
IPublicTypeConfigTransducer,
IPublicTypePanelConfig,
} from '@alilc/lowcode-types';
const logger = new Logger({ level: 'warn', bizName: 'skeleton' });
export enum SkeletonEvents {
PANEL_DOCK_ACTIVE = 'skeleton.panel-dock.active',
PANEL_DOCK_UNACTIVE = 'skeleton.panel-dock.unactive',
PANEL_SHOW = 'skeleton.panel.show',
PANEL_HIDE = 'skeleton.panel.hide',
WIDGET_SHOW = 'skeleton.widget.show',
WIDGET_HIDE = 'skeleton.widget.hide',
WIDGET_DISABLE = 'skeleton.widget.disable',
WIDGET_ENABLE = 'skeleton.widget.enable',
}
export interface ISkeleton extends Omit {
editor: IEditor;
readonly leftArea: Area;
readonly topArea: Area;
readonly subTopArea: Area;
readonly toolbar: Area;
readonly leftFixedArea: Area;
readonly leftFloatArea: Area;
readonly rightArea: Area;
readonly mainArea: Area;
readonly bottomArea: Area;
readonly stages: Area;
readonly widgets: IWidget[];
readonly focusTracker: FocusTracker;
getPanel(name: string): Panel | undefined;
getWidget(name: string): IWidget | undefined;
buildFromConfig(config?: EditorConfig, components?: PluginClassSet): void;
createStage(config: any): string | undefined;
getStage(name: string): Stage | null;
createContainer(
name: string,
handle: (item: any) => any,
exclusive?: boolean,
checkVisible?: () => boolean,
defaultSetCurrent?: boolean,
): WidgetContainer;
createPanel(config: IPublicTypePanelConfig): Panel;
add(config: IPublicTypeSkeletonConfig, extraConfig?: Record): IWidget | Widget | Panel | Stage | Dock | PanelDock | undefined;
}
export class Skeleton implements ISkeleton {
private panels = new Map();
private configTransducers: IPublicTypeConfigTransducer[] = [];
private containers = new Map>();
readonly leftArea: Area;
readonly topArea: Area;
readonly subTopArea: Area;
readonly toolbar: Area;
readonly leftFixedArea: Area;
readonly leftFloatArea: Area