Merge branch 'feat/universal-api' into release/1.0.30

This commit is contained in:
力皓 2021-01-11 19:52:08 +08:00
commit 373f8c3b2e
106 changed files with 13331 additions and 0 deletions

View File

@ -0,0 +1,21 @@
module.exports = {
extends: 'eslint-config-ali/typescript/react',
rules: {
'react/no-multi-comp': 1,
'no-unused-expressions': 0,
'implicit-arrow-linebreak': 1,
'no-nested-ternary': 1,
'no-mixed-operators': 1,
'@typescript-eslint/no-parameter-properties': 1,
'@typescript-eslint/ban-types': 1,
'no-shadow': 1,
'no-prototype-builtins': 1,
'@typescript-eslint/no-unused-vars': 1,
'no-multi-assign': 1,
'no-dupe-class-members': 1,
'react/no-deprecated': 1,
'no-useless-escape': 1,
'brace-style': 1,
'@typescript-eslint/member-ordering': 0,
}
}

82
packages/engine/README.md Normal file
View File

@ -0,0 +1,82 @@
子视图
prototype.view
view.Preview
view.Mobile
实时切
设备
device
创建多个 simulator
不同simulator 加载不同视图
这样有利于 环境隔离,比如 rax 和 react
适配规则
规则 1
mobile view.mobile.xxx
rax view.rax.xxx
miniapp view.miniapp.xxx
view.<device>.xxx
通配 view.xxx
universal
规则 2
urls: "view.js,view2 <device selector>, view3 <device selector>",
urls: [
"view.js",
"view.js *",
"view1.js mobile|pc",
"view2.js <device selector>"
]
环境通用资源
"react": {
"urls": [
"//g.alicdn.com/platform/c/react/16.5.2/react.min.js"
],
"library": "React",
"package": "react",
"version": "16.5.2",
"devices-for": "*" | ["mobile", "web"] | "rax|mobile"
}
load legao assets
load all x-prototype-urls
load assets
build componentMeta
if has x-prototype-urls ,
load x-prototype-urls
call Bundle.createPrototype() or something register
got prototypeView
load schema
open schema
load simulator resources
simulator 中加载资源,根据 componentsMap 构建组件查询字典,
获取 view 相关的样式、脚本
获取 proto 相关的样式
在 simulator 中也加载一次
1. meta 信息构造
2. components 字典构造, proto.getView 或者 通过 npm 信息查询
3.
componentMeta 段描述的信息,如果包含 x-prototype-urls ,那么这个 meta 信息都可以丢掉

View File

@ -0,0 +1,28 @@
{
"sourceMap": true,
"plugins": [
[
"build-plugin-component",
{
"filename": "engine",
"library": "AliLowCodeEngine",
"libraryTarget": "umd",
"externals": {
"react": "var window.React",
"react-dom": "var window.ReactDOM",
"prop-types": "var window.PropTypes",
"@ali/visualengine": "var window.VisualEngine",
"@ali/visualengine-utils": "var window.VisualEngineUtils",
"rax": "var window.Rax",
"monaco-editor/esm/vs/editor/editor.api": "var window.monaco",
"monaco-editor/esm/vs/editor/editor.main.js": "var window.monaco"
}
}
],
"build-plugin-fusion",
["build-plugin-moment-locales", {
"locales": ["zh-cn"]
}],
"./build.plugin.js"
]
}

View File

@ -0,0 +1,23 @@
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = ({ onGetWebpackConfig }) => {
onGetWebpackConfig((config) => {
config.resolve
.plugin('tsconfigpaths')
.use(TsconfigPathsPlugin, [{
configFile: './tsconfig.json',
}]);
/*
config
// 定义插件名称
.plugin('MonacoWebpackPlugin')
// 第一项为具体插件,第二项为插件参数
.use(new MonacoWebpackPlugin({
languages:["javascript","css","json"]
}), []);
*/
config.plugins.delete('hot');
config.devServer.hot(false);
});
};

View File

@ -0,0 +1,19 @@
{
"plugins": [
[
"build-plugin-component",
{
"filename": "editor-preset-vision",
"library": "LowcodeEditor",
"libraryTarget": "umd",
"externals": {
"react": "var window.React",
"react-dom": "var window.ReactDOM",
"prop-types": "var window.PropTypes",
"rax": "var window.Rax"
}
}
],
"@ali/lowcode-test-mate/plugin/index.ts"
]
}

View File

@ -0,0 +1,27 @@
const esModules = [
'@recore/obx-react',
// '@ali/lowcode-editor-core',
].join('|');
module.exports = {
// transform: {
// '^.+\\.[jt]sx?$': 'babel-jest',
// // '^.+\\.(ts|tsx)$': 'ts-jest',
// // '^.+\\.(js|jsx)$': 'babel-jest',
// },
// testMatch: ['(/tests?/.*(test))\\.[jt]s$'],
transformIgnorePatterns: [
`/node_modules/(?!${esModules})/`,
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
collectCoverage: false,
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/base/**',
'!src/fields/**',
'!src/prop.ts',
'!**/node_modules/**',
'!**/vendor/**',
],
};

View File

@ -0,0 +1,60 @@
{
"name": "@ali/lowcode-engine",
"version": "1.0.29",
"description": "Universal API for AliLowCode engine",
"main": "lib/index.js",
"private": true,
"files": [
"dist",
"es",
"lib"
],
"scripts": {
"start": "build-scripts start",
"version:update": "node ./scripts/version.js",
"build": "tnpm run version:update && build-scripts build --skip-demo",
"cloud-build": "build-scripts build --skip-demo",
"test": "build-scripts test --config build.test.json"
},
"license": "MIT",
"dependencies": {
"@ali/lowcode-designer": "^1.0.29",
"@ali/lowcode-editor-core": "^1.0.29",
"@ali/lowcode-editor-skeleton": "^1.0.29",
"@ali/lowcode-plugin-designer": "^1.0.29",
"@ali/lowcode-plugin-outline-pane": "^1.0.29",
"@ali/lowcode-utils": "^1.0.29",
"@ali/ve-i18n-util": "^2.0.0",
"@ali/ve-icons": "^4.1.9",
"@ali/ve-less-variables": "2.0.3",
"@ali/ve-popups": "^4.2.5",
"@ali/ve-utils": "^1.1.0",
"@ali/vu-css-style": "^1.1.3",
"@ali/vu-logger": "^1.0.7",
"@ali/vu-style-sheet": "^2.4.0",
"@alifd/next": "^1.19.12",
"@alife/theme-lowcode-dark": "^0.1.0",
"@alife/theme-lowcode-light": "^0.1.0",
"domready": "^1.0.8",
"immutable": "^3.8.1",
"react": "^16.8.1",
"react-dom": "^16.8.1"
},
"devDependencies": {
"@ali/lowcode-test-mate": "^1.0.1",
"@alib/build-scripts": "^0.1.18",
"@types/domready": "^1.0.0",
"@types/events": "^3.0.0",
"@types/react": "^16.8.3",
"@types/react-dom": "^16.8.2",
"build-plugin-fusion": "^0.1.0",
"build-plugin-moment-locales": "^0.1.0",
"build-plugin-react-app": "^1.1.2",
"fs-extra": "^9.0.1",
"prop-types": "^15.7.2",
"tsconfig-paths-webpack-plugin": "^3.2.0"
},
"publishConfig": {
"registry": "https://registry.npm.alibaba-inc.com"
}
}

View File

@ -0,0 +1,22 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-require-imports */
const { execSync } = require('child_process');
const { join } = require('path');
const fse = require('fs-extra');
const gitBranchName = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' });
const reBranchVersion = /^(?:[a-z]+\/)(\d+\.\d+\.\d+)$/im;
const match = reBranchVersion.exec(gitBranchName);
if (!match) {
console.warn(`[checkversion] gitBranchName: ${gitBranchName}`);
return;
}
const releaseVersion = match[1];
const indexFile = join(__dirname, '../src/index.ts');
const indexContent = fse.readFileSync(indexFile, 'utf-8');
fse.writeFileSync(indexFile, indexContent.replace('{VERSION}', releaseVersion));

View File

@ -0,0 +1,60 @@
import { isJSBlock, isJSExpression, isJSSlot } from '@ali/lowcode-types';
import { isPlainObject, hasOwnProperty, cloneDeep, isI18NObject, isUseI18NSetter, convertToI18NObject, isString } from '@ali/lowcode-utils';
import { globalContext, Editor } from '@ali/lowcode-editor-core';
import { Designer, LiveEditing, TransformStage, Node, getConvertedExtraKey, LowCodePluginManager } from '@ali/lowcode-designer';
import Outline, { OutlineBackupPane, getTreeMaster } from '@ali/lowcode-plugin-outline-pane';
import DesignerPlugin from '@ali/lowcode-plugin-designer';
import { Skeleton, SettingsPrimaryPane, registerDefaults } from '@ali/lowcode-editor-skeleton';
export const editor = new Editor();
globalContext.register(editor, Editor);
export const skeleton = new Skeleton(editor);
editor.set(Skeleton, skeleton);
editor.set('skeleton', skeleton);
registerDefaults();
export const designer = new Designer({ editor });
editor.set(Designer, designer);
editor.set('designer', designer);
export const plugins = (new LowCodePluginManager(editor)).toProxy();
editor.set('plugins', plugins);
skeleton.add({
area: 'mainArea',
name: 'designer',
type: 'Widget',
content: DesignerPlugin,
});
skeleton.add({
area: 'rightArea',
name: 'settingsPane',
type: 'Panel',
content: SettingsPrimaryPane,
props: {
ignoreRoot: true,
},
});
skeleton.add({
area: 'leftArea',
name: 'outlinePane',
type: 'PanelDock',
content: Outline,
panelProps: {
area: 'leftFixedArea',
},
});
skeleton.add({
area: 'rightArea',
name: 'backupOutline',
type: 'Panel',
props: {
condition: () => {
return designer.dragon.dragging && !getTreeMaster(designer).hasVisibleTreeBoard();
},
},
content: OutlineBackupPane,
});

View File

@ -0,0 +1,90 @@
import { createElement } from 'react';
import { render } from 'react-dom';
import * as editorHelper from '@ali/lowcode-editor-core';
import * as designerHelper from '@ali/lowcode-designer';
// import { Node } from '@ali/lowcode-designer';
import { skeleton, designer, editor, plugins } from './editor';
import * as skeletonHelper from '@ali/lowcode-editor-skeleton';
const { project, currentSelection: selection } = designer;
const { hotkey, monitor, getSetter, registerSetter } = editorHelper;
const { Workbench } = skeletonHelper;
const setters = {
getSetter,
registerSetter,
};
export {
editor,
editorHelper,
skeleton,
skeletonHelper,
designer,
designerHelper,
plugins,
setters,
project,
selection,
/**
*
*/
// hooks,
/**
*
*/
// store,
hotkey,
monitor,
};
// TODO: build-plugin-component 的 umd 开发态没有导出 AliLowCodeEngine这里先简单绕过
(window as any).AliLowCodeEngine = {
editor,
editorHelper,
skeleton,
skeletonHelper,
designer,
designerHelper,
plugins,
setters,
project,
selection,
/**
*
*/
// hooks,
/**
*
*/
// store,
hotkey,
monitor,
}
export async function init(container?: Element) {
let engineContainer = container;
if (!engineContainer) {
engineContainer = document.createElement('div');
document.body.appendChild(engineContainer);
}
engineContainer.id = 'engine';
await plugins.init();
render(
createElement(Workbench, {
skeleton,
className: 'engine-main',
topAreaItemClassName: 'engine-actionitem',
}),
engineContainer,
);
}
const version = '{VERSION}';
console.log(
`%c AliLowCodeEngine %c v${version} `,
'padding: 2px 1px; border-radius: 3px 0 0 3px; color: #fff; background: #606060; font-weight: bold;',
'padding: 2px 1px; border-radius: 0 3px 3px 0; color: #fff; background: #42c02e; font-weight: bold;',
);

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "lib"
},
"include": [
"./src/"
]
}

View File

@ -1,6 +1,8 @@
{ {
"entry": { "entry": {
"editor-preset-vision": "../editor-preset-vision/src/index.ts", "editor-preset-vision": "../editor-preset-vision/src/index.ts",
"engine": "../engine/src/index.ts",
"vision-polyfill": "../vision-polyfill/src/index.ts",
"react-simulator-renderer": "../react-simulator-renderer/src/index.ts", "react-simulator-renderer": "../react-simulator-renderer/src/index.ts",
"rax-simulator-renderer": "../rax-simulator-renderer/src/index.ts" "rax-simulator-renderer": "../rax-simulator-renderer/src/index.ts"
}, },

View File

@ -0,0 +1,21 @@
module.exports = {
extends: 'eslint-config-ali/typescript/react',
rules: {
'react/no-multi-comp': 1,
'no-unused-expressions': 0,
'implicit-arrow-linebreak': 1,
'no-nested-ternary': 1,
'no-mixed-operators': 1,
'@typescript-eslint/no-parameter-properties': 1,
'@typescript-eslint/ban-types': 1,
'no-shadow': 1,
'no-prototype-builtins': 1,
'@typescript-eslint/no-unused-vars': 1,
'no-multi-assign': 1,
'no-dupe-class-members': 1,
'react/no-deprecated': 1,
'no-useless-escape': 1,
'brace-style': 1,
'@typescript-eslint/member-ordering': 0,
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,82 @@
子视图
prototype.view
view.Preview
view.Mobile
实时切
设备
device
创建多个 simulator
不同simulator 加载不同视图
这样有利于 环境隔离,比如 rax 和 react
适配规则
规则 1
mobile view.mobile.xxx
rax view.rax.xxx
miniapp view.miniapp.xxx
view.<device>.xxx
通配 view.xxx
universal
规则 2
urls: "view.js,view2 <device selector>, view3 <device selector>",
urls: [
"view.js",
"view.js *",
"view1.js mobile|pc",
"view2.js <device selector>"
]
环境通用资源
"react": {
"urls": [
"//g.alicdn.com/platform/c/react/16.5.2/react.min.js"
],
"library": "React",
"package": "react",
"version": "16.5.2",
"devices-for": "*" | ["mobile", "web"] | "rax|mobile"
}
load legao assets
load all x-prototype-urls
load assets
build componentMeta
if has x-prototype-urls ,
load x-prototype-urls
call Bundle.createPrototype() or something register
got prototypeView
load schema
open schema
load simulator resources
simulator 中加载资源,根据 componentsMap 构建组件查询字典,
获取 view 相关的样式、脚本
获取 proto 相关的样式
在 simulator 中也加载一次
1. meta 信息构造
2. components 字典构造, proto.getView 或者 通过 npm 信息查询
3.
componentMeta 段描述的信息,如果包含 x-prototype-urls ,那么这个 meta 信息都可以丢掉

View File

@ -0,0 +1,29 @@
{
"sourceMap": true,
"plugins": [
[
"build-plugin-component",
{
"filename": "vision-polyfill",
"library": "VisualEngine",
"libraryTarget": "umd",
"externals": {
"react": "var window.React",
"react-dom": "var window.ReactDOM",
"prop-types": "var window.PropTypes",
"@ali/visualengine": "var window.VisualEngine",
"@ali/lowcode-engine": "var window.AliLowCodeEngine",
"@ali/visualengine-utils": "var window.VisualEngineUtils",
"rax": "var window.Rax",
"monaco-editor/esm/vs/editor/editor.api": "var window.monaco",
"monaco-editor/esm/vs/editor/editor.main.js": "var window.monaco"
}
}
],
"build-plugin-fusion",
["build-plugin-moment-locales", {
"locales": ["zh-cn"]
}],
"./build.plugin.js"
]
}

View File

@ -0,0 +1,23 @@
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = ({ onGetWebpackConfig }) => {
onGetWebpackConfig((config) => {
config.resolve
.plugin('tsconfigpaths')
.use(TsconfigPathsPlugin, [{
configFile: './tsconfig.json',
}]);
/*
config
// 定义插件名称
.plugin('MonacoWebpackPlugin')
// 第一项为具体插件,第二项为插件参数
.use(new MonacoWebpackPlugin({
languages:["javascript","css","json"]
}), []);
*/
config.plugins.delete('hot');
config.devServer.hot(false);
});
};

View File

@ -0,0 +1,19 @@
{
"plugins": [
[
"build-plugin-component",
{
"filename": "editor-preset-vision",
"library": "LowcodeEditor",
"libraryTarget": "umd",
"externals": {
"react": "var window.React",
"react-dom": "var window.ReactDOM",
"prop-types": "var window.PropTypes",
"rax": "var window.Rax"
}
}
],
"@ali/lowcode-test-mate/plugin/index.ts"
]
}

View File

@ -0,0 +1,27 @@
const esModules = [
'@recore/obx-react',
// '@ali/lowcode-editor-core',
].join('|');
module.exports = {
// transform: {
// '^.+\\.[jt]sx?$': 'babel-jest',
// // '^.+\\.(ts|tsx)$': 'ts-jest',
// // '^.+\\.(js|jsx)$': 'babel-jest',
// },
// testMatch: ['(/tests?/.*(test))\\.[jt]s$'],
transformIgnorePatterns: [
`/node_modules/(?!${esModules})/`,
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
collectCoverage: false,
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/base/**',
'!src/fields/**',
'!src/prop.ts',
'!**/node_modules/**',
'!**/vendor/**',
],
};

View File

@ -0,0 +1,61 @@
{
"name": "@ali/lowcode-editor-preset-vision",
"version": "1.0.29",
"description": "Vision Polyfill for Ali lowCode engine",
"main": "lib/index.js",
"private": true,
"files": [
"dist",
"es",
"lib"
],
"scripts": {
"start": "build-scripts start",
"version:update": "node ./scripts/version.js",
"build": "tnpm run version:update && build-scripts build --skip-demo",
"cloud-build": "build-scripts build --skip-demo",
"test": "build-scripts test --config build.test.json"
},
"license": "MIT",
"dependencies": {
"@ali/lowcode-designer": "^1.0.29",
"@ali/lowcode-editor-core": "^1.0.29",
"@ali/lowcode-editor-setters": "^1.0.22",
"@ali/lowcode-editor-skeleton": "^1.0.29",
"@ali/lowcode-plugin-designer": "^1.0.29",
"@ali/lowcode-plugin-outline-pane": "^1.0.29",
"@ali/lowcode-utils": "^1.0.29",
"@ali/ve-i18n-util": "^2.0.0",
"@ali/ve-icons": "^4.1.9",
"@ali/ve-less-variables": "2.0.3",
"@ali/ve-popups": "^4.2.5",
"@ali/ve-utils": "^1.1.0",
"@ali/vu-css-style": "^1.1.3",
"@ali/vu-logger": "^1.0.7",
"@ali/vu-style-sheet": "^2.4.0",
"@alifd/next": "^1.19.12",
"@alife/theme-lowcode-dark": "^0.1.0",
"@alife/theme-lowcode-light": "^0.1.0",
"domready": "^1.0.8",
"immutable": "^3.8.1",
"react": "^16.8.1",
"react-dom": "^16.8.1"
},
"devDependencies": {
"@ali/lowcode-test-mate": "^1.0.1",
"@alib/build-scripts": "^0.1.18",
"@types/domready": "^1.0.0",
"@types/events": "^3.0.0",
"@types/react": "^16.8.3",
"@types/react-dom": "^16.8.2",
"build-plugin-fusion": "^0.1.0",
"build-plugin-moment-locales": "^0.1.0",
"build-plugin-react-app": "^1.1.2",
"fs-extra": "^9.0.1",
"prop-types": "^15.7.2",
"tsconfig-paths-webpack-plugin": "^3.2.0"
},
"publishConfig": {
"registry": "https://registry.npm.alibaba-inc.com"
}
}

View File

@ -0,0 +1,22 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-require-imports */
const { execSync } = require('child_process');
const { join } = require('path');
const fse = require('fs-extra');
const gitBranchName = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' });
const reBranchVersion = /^(?:[a-z]+\/)(\d+\.\d+\.\d+)$/im;
const match = reBranchVersion.exec(gitBranchName);
if (!match) {
console.warn(`[checkversion] gitBranchName: ${gitBranchName}`);
return;
}
const releaseVersion = match[1];
const indexFile = join(__dirname, '../src/index.ts');
const indexContent = fse.readFileSync(indexFile, 'utf-8');
fse.writeFileSync(indexFile, indexContent.replace('{VERSION}', releaseVersion));

View File

@ -0,0 +1,137 @@
import bus from '../bus';
import SchemaManager from './schemaManager';
import VisualDesigner from './visualDesigner';
import VisualManager from './visualManager';
import { findIndex, get, unionBy, uniqueId } from 'lodash';
export type removeEventListener = () => void;
export interface IEventNameMap {
[eventName: string]: string | symbol;
}
export interface ISchemaController {
getSchemaManager(): SchemaManager;
getSchemaManagerById(id?: string): SchemaManager;
getSchemaManagerByName(name?: string): SchemaManager[];
getSchemaManagerList(): SchemaManager[];
connectSchemaManager(manager: SchemaManager): this;
connectSchemaManagerList(managerList: SchemaManager[]): this;
notifyAllSchemaManagers(eventName: string | symbol, eventData: any): boolean;
}
export interface IManagerController {
getManager(): VisualManager;
getManagerById(id?: string): VisualManager;
getManagerByName(name?: string): VisualManager[];
getManagerList(name?: string): VisualManager[];
connectManager(manager: VisualManager): this;
connectManagerList(managerList: VisualManager[]): this;
notifyAllManagers(eventName: string | symbol, eventData: any): boolean;
}
export interface IDesignerController {
getDesigner(): VisualDesigner;
getDesignerById(id?: string): VisualDesigner;
getDesignerByName(name?: string): VisualDesigner[];
getDesignerList(): VisualDesigner[];
connectDesigner(designer: VisualDesigner): this;
connectDesignerList(designerList: VisualDesigner[]): this;
notifyAllDesigners(eventName: string | symbol, eventData: any): boolean;
}
export interface INameable {
getName(): string;
getId(): string;
setName(name?: string): this;
}
export interface IObservable {
getEventMap(): IEventNameMap;
on(eventName: string | symbol, callback: () => any): removeEventListener;
emit(eventName: string | symbol, eventData?: any[]): boolean;
}
export interface IManagerConfigs {
name?: string;
disableEvents?: boolean;
emitter?: IEmitter;
}
export interface IEmitter {
on(eventName: string | symbol, callback: () => any): removeEventListener;
emit(eventName: string | symbol, eventData?: any): boolean;
removeListener(eventName: string | symbol, callback: () => any): any;
}
export function connectGeneralManager(manager: any, managerList: any[]) {
const index = findIndex(managerList, (m) => m.getId() === manager.getId());
if (index > -1) {
managerList.push(manager);
} else {
managerList.splice(index, 1, manager);
}
return managerList;
}
export function connectGeneralManagerList(managerList: any[], sourceManagerList: any[]): any {
return unionBy(sourceManagerList, managerList, (manager) => manager.getId());
}
export class BaseManager implements INameable, IObservable {
static EVENTS: IEventNameMap = {};
static NAME = 'BaseManager';
private name: string;
private id: string;
private emitter: any;
constructor(managerConfigs: IManagerConfigs = {}) {
this.name = managerConfigs.name || get(this, 'constructor', 'NAME');
this.id = uniqueId(this.name);
if (!managerConfigs.disableEvents) {
if (managerConfigs.emitter) {
// 使用自定义的满足 EventEmitter 接口要求的自定义事件对象
this.emitter = managerConfigs.emitter;
} else {
// Bus 为单例模式
this.emitter = bus;
}
}
}
getId(): string {
return this.id;
}
setName(name: string): this {
this.name = name;
return this;
}
getName(): string {
return this.name;
}
getEventMap() {
/**
* Hack for get current constructor
* because if we write this.constructor.EVENTS
* ts compiler will show compiled error
*/
return get(this, 'constructor', BaseManager.EVENTS);
}
on(eventName: string | symbol, callback: () => any): removeEventListener {
this.emitter.on(eventName, callback);
return () => this.emitter.removeListener(eventName, callback);
}
emit(eventName: string | symbol, ...eventData: any[]): boolean {
return this.emitter.emit.call(this.emitter, eventName, ...eventData);
}
}

View File

@ -0,0 +1,44 @@
/**
* Storage the const variables
*/
/**
* Global
*/
export const VERSION = '5.3.0';
/**
* schema version defined in alibaba
*/
export const ALI_SCHEMA_VERSION = '1.0.0';
export const VE_EVENTS = {
/**
* node props to be dynamically replaced
* @event props the new props object been replaced
*/
VE_NODE_CREATED: 've.node.created',
VE_NODE_DESTROY: 've.node.destroyed',
VE_NODE_PROPS_REPLACE: 've.node.props.replaced',
// copy / clone node
VE_OVERLAY_ACTION_CLONE_NODE: 've.overlay.cloneElement',
// remove / delete node
VE_OVERLAY_ACTION_REMOVE_NODE: 've.overlay.removeElement',
// one page successfully mount on the DOM
VE_PAGE_PAGE_READY: 've.page.pageReady',
};
export const VE_HOOKS = {
// a decorator function
VE_NODE_PROPS_DECORATOR: 've.leaf.props.decorator',
// a remove callback function
VE_NODE_REMOVE_HELPER: 've.outline.actions.removeHelper',
/**
* provide customization field
*/
VE_SETTING_FIELD_PROVIDER: 've.settingField.provider',
/**
* VariableSetter for variable mode of a specified prop
*/
VE_SETTING_FIELD_VARIABLE_SETTER: 've.settingField.variableSetter',
};

View File

@ -0,0 +1,104 @@
import { cloneDeep, find } from 'lodash';
import {
BaseManager,
connectGeneralManager,
connectGeneralManagerList,
IManagerController,
ISchemaController,
} from './base';
import VisualManager from './visualManager';
export default class SchemaManager extends BaseManager implements IManagerController, ISchemaController {
private schemaData: object = {};
private visualManagerList: VisualManager[] = [];
private schemaManagerList: SchemaManager[] = [];
getManager(): VisualManager {
return this.visualManagerList[0];
}
getManagerByName(name?: string): VisualManager[] {
return this.visualManagerList.filter((m) => m.getName() === name);
}
getManagerById(id?: string): VisualManager {
return find(this.visualManagerList, (m) => m.getId() === id) as VisualManager;
}
getManagerList(): VisualManager[] {
return this.visualManagerList;
}
getSchemaManager(): SchemaManager {
return this.schemaManagerList[0];
}
getSchemaManagerById(id?: string): SchemaManager {
return find(this.schemaManagerList, (m) => m.getId() === id) as SchemaManager;
}
getSchemaManagerByName(name?: string): SchemaManager[] {
return this.schemaManagerList.filter((m) => m.getName() === name);
}
getSchemaManagerList() {
return this.schemaManagerList;
}
connectManager(manager: any) {
connectGeneralManager.call(this, manager, this.visualManagerList as any);
return this;
}
connectSchemaManager(manager: SchemaManager): this {
connectGeneralManager.call(this, manager, this.schemaManagerList);
return this;
}
connectManagerList(managerList: VisualManager[]): this {
this.visualManagerList = connectGeneralManagerList.call(this, managerList as any, this.visualManagerList as any);
return this;
}
connectSchemaManagerList(managerList: SchemaManager[]): this {
this.schemaManagerList = connectGeneralManagerList.call(this, managerList, this.schemaManagerList);
return this;
}
notifyAllManagers(eventName: string | symbol, ...eventData: any[]): boolean {
return this.visualManagerList.map((m) => m.emit(eventName, eventData)).every((r) => r);
}
notifyAllSchemaManagers(eventName: string | symbol, ...eventData: any[]): boolean {
return this.schemaManagerList.map((m) => m.emit(eventName, eventData)).every((r) => r);
}
exportSchema(): string {
try {
return JSON.stringify(this.schemaData);
} catch (e) {
throw new Error(e.message);
}
}
exportSchemaObject(): object {
return cloneDeep(this.schemaData);
}
importSchema(schemaString: string): this {
try {
this.schemaData = JSON.parse(schemaString);
return this;
} catch (e) {
throw new Error(e.message);
}
}
importSchemaObject(schema: object): this {
this.schemaData = schema;
return this;
}
}

View File

@ -0,0 +1,116 @@
import { assign, find, get } from 'lodash';
import { Component } from 'react';
import bus from '../bus';
import {
BaseManager,
connectGeneralManager,
connectGeneralManagerList,
IEmitter,
IEventNameMap,
IManagerController,
INameable,
IObservable,
} from './base';
import VisualManager from './visualManager';
interface IDesignerProps {
name?: string;
visualManagers?: VisualManager[];
emitter?: IEmitter;
}
export default class VisualDesigner extends Component implements IManagerController, IObservable, INameable {
static NAME = 'VisualDesigner';
static EVENTS: IEventNameMap = {};
props: IDesignerProps = {};
defaultProps: IDesignerProps = {
name: 'defaultDesigner',
visualManagers: [],
};
private visualManagerList: VisualManager[] = [];
private name = '';
private id = '';
private emitter: IEmitter;
constructor(props: IDesignerProps) {
super(props);
this.setName(props.name || get(this, 'constructor', 'NAME'));
this.connectManagerList(this.props.visualManagers as any);
if (props.emitter) {
// 使用自定义的满足 EventEmitter 接口要求的自定义事件对象
this.emitter = props.emitter;
} else {
this.emitter = bus;
}
}
getId(): string {
return this.id;
}
setName(name: string): this {
this.name = name;
return this;
}
getName() {
return this.name;
}
getManager(): VisualManager {
return this.visualManagerList[0];
}
getManagerByName(name?: string): VisualManager[] {
return this.visualManagerList.filter((m) => m.getName() === name);
}
getManagerById(id: string): VisualManager {
return find(this.visualManagerList, (m) => m.getId() === id) as VisualManager;
}
getManagerList(): VisualManager[] {
return this.visualManagerList;
}
connectManager(manager: VisualManager) {
connectGeneralManager.call(this, manager, this.visualManagerList);
return this;
}
connectManagerList(managerList: VisualManager[]): this {
this.visualManagerList = connectGeneralManagerList.call(this, managerList, this.visualManagerList);
return this;
}
getEventMap() {
/**
* Hack for get current constructor
* because if we write this.constructor.EVENTS
* ts compiler will show compiled error
*/
return get(this, 'constructor', BaseManager.EVENTS);
}
notifyAllManagers(eventName: string | symbol, ...eventData: any[]): boolean {
return this.visualManagerList.map((m) => m.emit(eventName, eventData)).every((r) => r);
}
on(eventName: string | symbol, callback: () => any) {
this.emitter.on(eventName, callback);
return () => this.emitter.removeListener(eventName, callback);
}
emit(eventName: string | symbol, ...eventData: any[]): boolean {
return this.emitter.emit.call(this.emitter, eventName, ...eventData);
}
}

View File

@ -0,0 +1,80 @@
import { find } from 'lodash';
import {
BaseManager,
connectGeneralManager,
connectGeneralManagerList,
IDesignerController,
IManagerController,
} from './base';
import VisualDesigner from './visualDesigner';
export default class VisualManager extends BaseManager implements IManagerController, IDesignerController {
private visualManagerList: VisualManager[] = [];
private visualDesignerList: VisualDesigner[] = [];
getManager(): VisualManager {
return this.visualManagerList[0];
}
getManagerByName(name?: string): VisualManager[] {
return this.visualManagerList.filter((m) => m.getName() === name);
}
getManagerById(id?: string): VisualManager {
return find(this.visualManagerList, (m) => m.getId() === id) as VisualManager;
}
getManagerList(): VisualManager[] {
return this.visualManagerList;
}
getDesigner(): VisualDesigner {
return this.visualDesignerList[0];
}
getDesignerByName(name?: string): VisualDesigner[] {
return this.visualDesignerList.filter((m) => m.getName() === name);
}
getDesignerById(id?: string): VisualDesigner {
return find(this.visualDesignerList, (m) => m.getId() === id) as VisualDesigner;
}
getDesignerList() {
return this.visualDesignerList;
}
connectManager(manager: VisualManager) {
connectGeneralManager.call(this, manager, this.visualManagerList);
return this;
}
connectDesigner(manager: VisualDesigner): this {
connectGeneralManager.call(this, manager, this.visualDesignerList);
return this;
}
connectManagerList(managerList: VisualManager[]): this {
this.visualManagerList = connectGeneralManagerList.call(this, managerList, this.visualManagerList);
return this;
}
connectDesignerList(managerList: VisualDesigner[]): this {
this.visualDesignerList = connectGeneralManagerList.call(this, managerList, this.visualDesignerList);
return this;
}
notifyAllManagers(eventName: string | symbol, ...eventData: any[]): boolean {
return this.getManagerList()
.map((m) => m.emit(eventName, eventData))
.every((r) => r);
}
notifyAllDesigners(eventName: string | symbol, ...eventData: any[]): boolean {
return this.getDesignerList()
.map((m) => m.emit(eventName, eventData))
.every((r) => r);
}
}

View File

@ -0,0 +1,48 @@
import { find } from 'lodash';
import { BaseManager, connectGeneralManager, connectGeneralManagerList, IManagerController } from './base';
import VisualManager from './visualManager';
export default class VisualRender extends BaseManager implements IManagerController {
private visualManagerList: VisualManager[] = [];
getManager(): VisualManager {
return this.visualManagerList[0];
}
getManagerByName(name?: string): VisualManager[] {
return this.visualManagerList.filter((m) => m.getName() === name);
}
getManagerById(id?: string): VisualManager {
return find(this.visualManagerList, (m) => m.getId() === id) as VisualManager;
}
getManagerList(): VisualManager[] {
return this.visualManagerList;
}
connectManager(manager: VisualManager) {
connectGeneralManager.call(this, manager, this.visualManagerList);
return this;
}
connectManagerList(managerList: VisualManager[]): this {
this.visualManagerList = connectGeneralManagerList.call(this, managerList, this.visualManagerList);
return this;
}
notifyAllManagers(eventName: string | symbol, ...eventData: any[]): boolean {
return this.visualManagerList.map((m) => m.emit(eventName, eventData)).every((r) => r);
}
/**
* Render function
* @override
*
* @memberof VisualRender
*/
render(): any {
return '';
}
}

View File

@ -0,0 +1,208 @@
import lg from '@ali/vu-logger';
import { ComponentClass, ComponentType } from 'react';
import Prototype, { isPrototype } from './prototype';
import { designer } from '@ali/lowcode-engine';
import { upgradeMetadata } from './upgrade-metadata';
import trunk from './trunk';
function basename(name: string) {
return name ? (/[^\/]+$/.exec(name) || [''])[0] : '';
}
function getCamelName(name: string) {
const words = basename(name)
.replace(/^((vc)-)?(.+)/, '$3')
.split('-');
return words.reduce((s, word) => s + word[0].toUpperCase() + word.substring(1), '');
}
export interface ComponentProtoBundle {
// @ali/vc-xxx
name: string;
componentName?: string;
category?: string;
module: Prototype | Prototype[];
}
export interface ComponentViewBundle {
// @ali/vc-xxx
name: string;
// alias to property name
componentName?: string;
category?: string;
module: ComponentType<any>;
}
export default class Bundle {
static createPrototype = Prototype.create;
static addGlobalPropsReducer = Prototype.addGlobalPropsReducer;
static addGlobalPropsConfigure = Prototype.addGlobalPropsConfigure;
static addGlobalExtraActions = Prototype.addGlobalExtraActions;
static removeGlobalPropsConfigure = Prototype.removeGlobalPropsConfigure;
static overridePropsConfigure = Prototype.overridePropsConfigure;
static create(protos: ComponentProtoBundle[], views?: ComponentViewBundle[]) {
return new Bundle(protos, views);
}
private viewsMap: { [componentName: string]: ComponentType } = {};
private registry: { [componentName: string]: Prototype } = {};
private prototypeList: Prototype[] = [];
constructor(protos?: ComponentProtoBundle[], views?: ComponentViewBundle[]) {
// 注册 prototypeView 视图
if (views && Array.isArray(views)) {
this.recursivelyRegisterViews(views);
}
protos?.forEach((item) => {
const prototype = item.module;
if (prototype instanceof Prototype) {
this.revisePrototype(item, prototype);
const componentName = item.componentName || prototype.getComponentName()!;
const matchedView = this.viewsMap[componentName] || null;
if (matchedView) {
prototype.setView(matchedView);
}
this.registerPrototype(prototype);
} else if (Array.isArray(prototype)) {
this.recursivelyRegisterPrototypes(prototype, item);
}
});
// invoke prototype mocker while the prototype does not exist
Object.keys(this.viewsMap).forEach((viewName) => {
if (!this.prototypeList.find((proto) => proto.getComponentName() === viewName)) {
const mockedPrototype = trunk.mockComponentPrototype(this.viewsMap[viewName]);
if (mockedPrototype) {
mockedPrototype.setView(this.viewsMap[viewName]);
this.registerPrototype(mockedPrototype);
if (!mockedPrototype.getPackageName()) {
mockedPrototype.setPackageName((this.viewsMap[viewName] as any)._packageName_);
}
}
}
});
}
getFromMeta(componentName: string): Prototype {
if (this.registry[componentName]) {
return this.registry[componentName];
}
const meta = designer.getComponentMeta(componentName);
const prototype = Prototype.create(meta);
this.prototypeList.push(prototype);
this.registry[componentName] = prototype;
return prototype;
}
removeComponentBundle(componentName: string) {
const cIndex = this.prototypeList.findIndex((proto) => proto.getComponentName() === componentName);
delete this.registry[componentName];
this.prototypeList.splice(cIndex, 1);
}
getList() {
return this.prototypeList;
}
get(componentName: string) {
return this.registry[componentName];
}
replacePrototype(componentName: string, cp: Prototype) {
const view: any = this.get(componentName).getView();
this.removeComponentBundle(componentName);
this.registry[cp.getComponentName()!] = cp;
this.prototypeList.push(cp);
cp.setView(view);
}
/**
* TODO dirty fix
*/
addComponentBundle(bundles: any) {
/**
* Normal Component bundle: [ Prototype, PrototypeView ]
* Component without Prototype.js: [ View ]
*/
if (bundles.length >= 2) {
const prototype = bundles[0];
const metadata = upgradeMetadata({ ...prototype.options, packageName: prototype.packageName });
prototype.meta = designer.createComponentMeta(metadata);
const prototypeView = bundles[1];
prototype.setView(prototypeView);
this.registerPrototype(prototype);
}
}
private recursivelyRegisterViews(list: any[], viewName?: string): void {
list.forEach((item: any) => {
if (Array.isArray(item.module)) {
return this.recursivelyRegisterViews(item.module, item.name);
} else if (Array.isArray(item)) {
return this.recursivelyRegisterViews(item, viewName);
}
let viewDetail: ComponentClass;
if (item.module && typeof item.module === 'function') {
viewDetail = item.module;
} else {
viewDetail = item;
}
if (!viewDetail.displayName) {
lg.log('ERROR_NO_PROTOTYPE_VIEW');
lg.error('WARNING: the package without displayName is', item);
viewDetail.displayName = getCamelName(viewName || item.name);
}
(viewDetail as any)._packageName_ = viewName || item.name;
this.viewsMap[viewDetail.displayName] = viewDetail;
});
}
private recursivelyRegisterPrototypes(list: any[], cp: ComponentProtoBundle) {
const propList: ComponentProtoBundle[] = list;
propList.forEach((proto: any, index: number) => {
if (Array.isArray(proto)) {
this.recursivelyRegisterPrototypes(proto, cp);
return;
}
if (isPrototype(proto)) {
const componentName = proto.getComponentName()!;
if (this.viewsMap[componentName]) {
proto.setView(this.viewsMap[componentName]);
}
if (cp.name && !proto.getPackageName()) {
proto.setPackageName(cp.name);
}
this.registerPrototype(proto);
}
});
}
private revisePrototype(item: ComponentProtoBundle, prototype: Prototype) {
if (item.category) {
prototype.setCategory(item.category);
}
if (item.name && !prototype.getPackageName()) {
prototype.setPackageName(item.name);
}
}
private registerPrototype(prototype: Prototype) {
const componentName = prototype.getComponentName()!;
if (this.registry[componentName]) {
lg.warn('WARN: override prototype', prototype, componentName);
const idx = this.prototypeList.findIndex((proto) => componentName === proto.getComponentName());
this.prototypeList[idx] = prototype;
} else {
this.prototypeList.push(prototype);
}
this.registry[componentName] = prototype;
}
}

View File

@ -0,0 +1,371 @@
import { ComponentType, ReactElement, Component, FunctionComponent } from 'react';
import { ComponentMetadata, FieldConfig, InitialItem, FilterItem, AutorunItem } from '@ali/lowcode-types';
import {
ComponentMeta,
} from '@ali/lowcode-designer';
import {
OldPropConfig,
OldPrototypeConfig,
upgradeMetadata,
upgradeActions,
upgradePropConfig,
upgradeConfigure,
} from './upgrade-metadata';
import { accessLibrary } from '@ali/lowcode-utils';
import { designer, designerHelper, editorHelper } from '@ali/lowcode-engine';
const {
addBuiltinComponentAction,
isComponentMeta,
registerMetadataTransducer,
TransformStage,
} = designerHelper;
const { intl } = editorHelper;
const GlobalPropsConfigure: Array<{
position: string;
initials?: InitialItem[];
filters?: FilterItem[];
autoruns?: AutorunItem[];
config: FieldConfig;
}> = [];
const Overrides: {
[componentName: string]: {
initials?: InitialItem[];
filters?: FilterItem[];
autoruns?: AutorunItem[];
override: any;
};
} = {};
function addGlobalPropsConfigure(config: OldGlobalPropConfig) {
const initials: InitialItem[] = [];
const filters: FilterItem[] = [];
const autoruns: AutorunItem[] = [];
GlobalPropsConfigure.push({
position: config.position || 'bottom',
initials,
filters,
autoruns,
config: upgradePropConfig(config, {
addInitial: (item) => {
initials.push(item);
},
addFilter: (item) => {
filters.push(item);
},
addAutorun: (item) => {
autoruns.push(item);
},
}),
});
}
function removeGlobalPropsConfigure(name: string) {
let l = GlobalPropsConfigure.length;
while (l-- > 0) {
if (GlobalPropsConfigure[l].config.name === name) {
GlobalPropsConfigure.splice(l, 1);
}
}
}
function overridePropsConfigure(componentName: string, config: { [name: string]: OldPropConfig } | OldPropConfig[]) {
const initials: InitialItem[] = [];
const filters: FilterItem[] = [];
const autoruns: AutorunItem[] = [];
const addInitial = (item: InitialItem) => {
initials.push(item);
};
const addFilter = (item: FilterItem) => {
filters.push(item);
};
const addAutorun = (item: AutorunItem) => {
autoruns.push(item);
};
let override: any;
if (Array.isArray(config)) {
override = upgradeConfigure(config, { addInitial, addFilter, addAutorun });
} else {
override = {};
Object.keys(config).forEach((key) => {
override[key] = upgradePropConfig(config[key], { addInitial, addFilter, addAutorun });
});
}
Overrides[componentName] = {
initials,
filters,
autoruns,
override,
};
}
registerMetadataTransducer(
(metadata) => {
const {
configure: { combined, props },
componentName,
} = metadata;
let top: FieldConfig[];
let bottom: FieldConfig[];
if (combined) {
top = combined?.[0]?.items || combined;
bottom = combined?.[combined.length - 1]?.items || combined;
} else if (props) {
top = props;
bottom = props;
} else {
metadata.configure.props = top = bottom = [];
}
GlobalPropsConfigure.forEach((item) => {
const position = item.position || 'bottom';
if (position === 'top') {
top.unshift(item.config);
} else if (position === 'bottom') {
bottom.push(item.config);
}
// TODO: replace autoruns,initials,filters
});
const override = Overrides[componentName]?.override;
if (override) {
if (Array.isArray(override)) {
// 替换 #props其他暂时忽略
const idx = metadata.configure.combined?.findIndex(item => item.name === '#props');
if (idx > -1) {
metadata.configure.combined[idx].items = override;
}
} else {
let l = top.length;
let item;
while (l-- > 0) {
item = top[l];
if (item.name in override) {
if (override[item.name]) {
top.splice(l, 1, override[item.name]);
} else {
top.splice(l, 1);
}
}
}
}
// TODO: replace autoruns,initials,filters
}
return metadata;
},
100,
'vision-polyfill',
);
function addGlobalExtraActions(action: () => ReactElement) {
upgradeActions(action)?.forEach(addBuiltinComponentAction);
}
// const GlobalPropsReducers: any[] = [];
function addGlobalPropsReducer(reducer: () => any) {
// GlobalPropsReducers.push(reducer);
designer.addPropsReducer(reducer, TransformStage.Render);
}
export interface OldGlobalPropConfig extends OldPropConfig {
position?: 'top' | 'bottom';
}
const packageMaps: any = {};
export function setPackages(packages: Array<{ package: string; library: object | string }>) {
packages.forEach((item) => {
let lib: any;
if (packageMaps.hasOwnProperty(item.package)) {
return;
}
Object.defineProperty(packageMaps, item.package, {
get() {
if (lib === undefined) {
lib = accessLibrary(item.library);
}
return lib;
},
});
});
}
export function getPackage(name: string): object | null {
if (packageMaps.hasOwnProperty(name)) {
return packageMaps[name];
}
return null;
}
function isNewSpec(options: any): options is ComponentMetadata {
return (
options &&
(options.npm || options.props || (options.configure && (options.configure.props || options.configure.component)))
);
}
class Prototype {
static addGlobalPropsReducer = addGlobalPropsReducer;
static addGlobalPropsConfigure = addGlobalPropsConfigure;
static addGlobalExtraActions = addGlobalExtraActions;
static removeGlobalPropsConfigure = removeGlobalPropsConfigure;
static overridePropsConfigure = overridePropsConfigure;
static create(config: OldPrototypeConfig | ComponentMetadata | ComponentMeta, extraConfigs: any = null, lookup = false) {
return new Prototype(config, extraConfigs, lookup);
}
readonly isPrototype = true;
readonly meta: ComponentMeta;
readonly options: OldPrototypeConfig | ComponentMetadata;
get componentName() {
return this.getId();
}
get packageName() {
return this.meta.npm?.package;
}
set packageName(pkgName) {
if (this.meta.npm) {
this.meta.npm.package = pkgName;
} else {
this.meta.npm = { package: pkgName };
}
}
// 兼容原 vision 用法
view: ComponentType | undefined;
constructor(input: OldPrototypeConfig | ComponentMetadata | ComponentMeta, extraConfigs: any = null, lookup = false) {
if (lookup) {
this.meta = designer.getComponentMeta(input.componentName);
this.options = this.meta.getMetadata();
return this.meta.prototype || this;
} else {
if (isComponentMeta(input)) {
this.meta = input;
this.options = input.getMetadata();
} else {
this.options = input;
const metadata = isNewSpec(input) ? input : upgradeMetadata(input);
this.meta = designer.createComponentMeta(metadata);
}
(this.meta as any).prototype = this;
}
}
getId() {
return this.getComponentName();
}
getConfig(configName?: keyof (OldPrototypeConfig | ComponentMetadata)) {
if (configName) {
return this.options[configName];
}
return this.options;
}
getPackageName() {
return this.packageName;
}
getContextInfo(name: string): any {
return this.meta.getMetadata().experimental?.context?.[name];
}
getTitle() {
return intl(this.meta.title);
}
getComponentName() {
return this.meta.componentName;
}
getDocUrl() {
return this.meta.getMetadata().docUrl;
}
getPropConfigs() {
return this.options;
}
private category?: string;
getCategory() {
if (this.options.category != null) {
return this.options.category;
}
return this.meta.getMetadata().tags?.[0] || '*';
}
setCategory(category: string) {
this.options.category = category;
}
getIcon() {
return this.meta.icon;
}
getConfigure() {
return this.meta.configure;
}
getRectSelector() {
return this.meta.rootSelector;
}
isContainer() {
return this.meta.isContainer;
}
isModal() {
return this.meta.isModal;
}
isAutoGenerated() {
return false;
}
setPackageName(name: string) {
this.meta.setNpm({
package: name,
componentName: this.getComponentName(),
});
}
setView(view: ComponentType<any>) {
this.view = view;
const metadata = this.meta.getMetadata();
if (!metadata.experimental) {
metadata.experimental = {
view,
};
} else {
metadata.experimental.view = view;
}
}
getView() {
return (
this.view ||
this.meta.getMetadata().experimental?.view ||
designer.currentDocument?.simulator?.getComponent(this.getComponentName())
);
}
}
export function isPrototype(obj: any): obj is Prototype {
return obj && obj.isPrototype;
}
export default Prototype;

View File

@ -0,0 +1,144 @@
import { ReactElement, ComponentType } from 'react';
import { EventEmitter } from 'events';
import { setters } from '@ali/lowcode-engine';
import { RegisteredSetter } from '@ali/lowcode-editor-core';
import lg from '@ali/vu-logger';
import { CustomView } from '@ali/lowcode-types';
import Bundle from './bundle';
import Prototype from './prototype';
const { registerSetter, getSetter } = setters;
export class Trunk {
private trunk: any[] = [];
private emitter: EventEmitter = new EventEmitter();
private metaBundle = new Bundle();
private componentPrototypeMocker: any;
isReady() {
return this.getList().length > 0;
}
addBundle(bundle: Bundle) {
this.trunk.push(bundle);
this.emitter.emit('trunkchange');
}
getBundle(): Bundle {
console.warn('Trunk.getBundle is deprecated');
return this.trunk[0];
}
getList(): any[] {
const list = this.trunk.filter(o => o).reduceRight((prev, cur) => prev.concat(cur.getList()), []);
const result: Prototype[] = [];
list.forEach((item: Prototype) => {
if (!result.find(r => r.options.componentName === item.options.componentName)) {
result.push(item);
}
});
return result;
}
getPrototype(name: string) {
let i = this.trunk.length;
let bundle;
let prototype;
while (i-- > 0) {
bundle = this.trunk[i];
prototype = bundle.get(name);
if (prototype) {
return (prototype.meta as any).prototype;
}
}
return this.metaBundle.getFromMeta(name);
}
getPrototypeById(id: string) {
return this.getPrototype(id);
}
listByCategory() {
const categories: any[] = [];
const categoryMap: any = {};
const categoryItems: any[] = [];
const defaultCategory = {
items: categoryItems,
name: '*',
};
categories.push(defaultCategory);
categoryMap['*'] = defaultCategory;
this.getList().forEach((prototype) => {
const cat = prototype.getCategory();
if (!cat) {
return;
}
if (!categoryMap.hasOwnProperty(cat)) {
const categoryMapItems: any[] = [];
categoryMap[cat] = {
items: categoryMapItems,
name: cat,
};
categories.push(categoryMap[cat]);
}
categoryMap[cat].items.push(prototype);
});
return categories;
}
getPrototypeView(componentName: string) {
return this.getPrototype(componentName)?.getView();
}
onTrunkChange(func: () => any) {
this.emitter.on('trunkchange', func);
return () => {
this.emitter.removeListener('trunkchange', func);
};
}
registerSetter(type: string, setter: CustomView | RegisteredSetter) {
registerSetter(type, setter);
}
beforeLoadBundle() {
console.warn('Trunk.beforeLoadBundle is deprecated');
}
afterLoadBundle() {
console.warn('Trunk.afterLoadBundle is deprecated');
}
registerComponentPrototypeMocker(mocker: any) {
this.componentPrototypeMocker = mocker;
}
mockComponentPrototype(bundle: any) {
if (!this.componentPrototypeMocker) {
lg.error('ERROR: no component prototypeMocker is set');
}
return this.componentPrototypeMocker
&& this.componentPrototypeMocker.mockPrototype(bundle);
}
setPackages() {
console.warn('Trunk.setPackages is deprecated');
}
getSetter(type: string): any {
const setter = getSetter(type);
if (setter?.component) {
return setter.component;
}
return setter;
}
getRecents(limit: number) {
return this.getList().filter((prototype) => prototype.getCategory()).slice(0, limit);
}
}
export default new Trunk();

View File

@ -0,0 +1,823 @@
import { ComponentType, ReactElement, isValidElement, ComponentClass } from 'react';
import { isPlainObject, uniqueId } from '@ali/lowcode-utils';
import { isI18nData, SettingTarget, InitialItem, FilterItem, isJSSlot, ProjectSchema, AutorunItem, isJSBlock } from '@ali/lowcode-types';
import { editorHelper, designerHelper } from '@ali/lowcode-engine';
const { SettingField } = designerHelper;
const { untracked } = editorHelper;
type Field = SettingTarget;
export enum DISPLAY_TYPE {
NONE = 'none', // => condition'plain'
PLAIN = 'plain',
INLINE = 'inline',
BLOCK = 'block',
ACCORDION = 'accordion',
TAB = 'tab', // => 'accordion'
ENTRY = 'entry',
}
// from vision 5.4
export interface OldPropConfig {
/**
* composite share the namespace
* group just be tie up together
*/
type?: 'composite' | 'group'; // => composite as objectSetter
/**
* when type is composite or group
*/
items?: OldPropConfig[]; // => items
/**
* property name: the field key in props of schema
*/
name: string; // =>
title?: string; // =>
tip?: {
content?: string;
url?: string;
};
defaultValue?: any; // => extraProps.defaultValue
initialValue?: any | ((value: any, defaultValue: any) => any); // => initials.initialValue
initial?: (value: any, defaultValue: any) => any; // => initials.initialValue
display?: DISPLAY_TYPE; // => fieldExtraProps
fieldStyle?: DISPLAY_TYPE; // => fieldExtraProps
setter?: ComponentClass | ISetterConfig[] | string | SetterGetter; // =>
supportVariable?: boolean; // => use MixedSetter
/**
* the prop should be collapsed while display value is accordion
*/
collapse?: boolean; // => extraProps.defaultCollapsed
/**
* alias to collapse
*/
collapsed?: boolean; // => extraProps.defaultCollapsed
fieldCollapsed?: boolean; // => extraProps.defaultCollapsed
/**
* if a prop is declared as disabled, it will not be saved into
* schema
*/
disabled?: boolean | ReturnBooleanFunction; // => hide & virtual ? thinkof global transform
/**
* will not export data to schema
*/
ignore?: boolean | ReturnBooleanFunction; // => ?virtualProp ? thinkof global transform
hidden?: boolean | ReturnBooleanFunction; // => condition
/**
* when use getValue(), accessor shall be called as initializer
*/
accessor?(this: Field, value: any): any; // => getValue
/**
* when current prop value mutate, the mutator function shall be called
*/
mutator?( // => setValue
this: Field,
value: any,
hotValue: any,
): /*
preValue: any, // => x
preHotValue: any, // => x
*/
void;
/**
* other values' change will trigger sync function here
*/
sync?(this: Field, value: any): void; // => autorun
/**
* user click var to change current field to
* variable setting field
*/
useVariableChange?(this: Field, data: { isUseVariable: boolean }): any; // => as MixinSetter param
slotName?: string;
slotTitle?: string;
initialChildren?: any; // schema
allowTextInput: boolean;
liveTextEditing?: any;
}
// from vision 5.4
export interface OldPrototypeConfig {
packageName: string; // => npm.package
/**
* category display in the component pane
* component will be hidden while the value is: null
*/
category: string; // => tags
componentName: string; // =>
docUrl?: string; // =>
defaultProps?: any; // => ?
/**
* extra actions on the outline of current selected node
* by default we have: remove / clone
*/
extraActions?: Array<ComponentType<any> | ReactElement> | (() => ReactElement); // => configure.component.actions
title?: string; // =>
icon?: ComponentType<any> | ReactElement; // =>
view: ComponentType; // => ?
initialChildren?: (props: any) => any[]; // => snippets
/**
* Props configurations of node
*/
configure: OldPropConfig[]; // => configure.props
snippets?: any[]; // => snippets
transducers?: any; // => ?
/**
* Selector expression rectangle of a node, it is usually a querySelector string
* @example '.classname > div'
*/
rectSelector?: string; // => configure.component.rectSelector
context?: {
// => ?
[contextInfoName: string]: any;
};
isContainer?: boolean; // => configure.component.isContainer
isAbsoluteLayoutContainer?: boolean; // => meta.experimental.isAbsoluteLayoutContainer 是否是绝对定位容器
isModal?: boolean; // => configure.component.isModal
isFloating?: boolean; // => configure.component.isFloating
descriptor?: string; // => configure.component.descriptor
// alias to canDragging
canDraging?: boolean; // => onDrag
canDragging?: boolean; // => ?
canOperating?: boolean; // => disabledActions
canUseCondition?: boolean;
canLoop?: boolean;
canContain?: (dragment: Node) => boolean; // => nestingRule
canDropTo?: ((container: Node) => boolean) | boolean | string | string[]; // => nestingRule
canDropto?: ((container: Node) => boolean) | boolean | string | string[]; // => nestingRule
canDropIn?: ((dragment: Node) => boolean) | boolean | string | string[]; // => nestingRule
canDroping?: ((dragment: Node) => boolean) | boolean | string | string[]; // => nestingRule
didDropOut?: (dragment: any, container: any) => void; // => hooks
didDropIn?: (dragment: any, container: any) => void; // => hooks
/**
* when sub-node of the current node changed
* including: sub-node insert / remove
*/
subtreeModified?(this: Node): any; // => ? hooks
// => ?
canResizing?: ((dragment: any, triggerDirection: string) => boolean) | boolean;
onResizeStart?: (e: MouseEvent, triggerDirection: string, dragment: Node) => void;
onResize?: (e: MouseEvent, triggerDirection: string, dragment: Node, moveX: number, moveY: number) => void;
onResizeEnd?: (e: MouseEvent, triggerDirection: string, dragment: Node) => void;
devMode?: string;
schema?: ProjectSchema;
isTopFixed?: boolean;
}
export interface ISetterConfig {
setter?: ComponentClass;
// use value to decide whether this setter is available
condition?: (value: any) => boolean;
}
type SetterGetter = (this: Field, value: any) => ComponentClass;
type ReturnBooleanFunction = (this: Field, value: any) => boolean;
export function upgradePropConfig(config: OldPropConfig, collector: ConfigCollector) {
const {
type,
name,
title,
tip,
slotName,
slotTitle,
initialChildren,
allowTextInput,
initialValue,
defaultValue,
display,
fieldStyle,
collapse,
collapsed,
fieldCollapsed,
hidden,
disabled,
items,
ignore,
initial,
sync,
accessor,
mutator,
setter,
useVariableChange,
supportVariable,
liveTextEditing,
} = config;
const extraProps: any = {
display: DISPLAY_TYPE.BLOCK,
};
const newConfig: any = {
type: type === 'group' ? 'group' : 'field',
name: type === 'group' && !name ? uniqueId('group') : name,
title,
extraProps,
};
if (tip) {
if (typeof title !== 'object' || isI18nData(title) || isValidElement(title)) {
newConfig.title = {
label: title,
tip: tip.content,
docUrl: tip.url,
};
} else {
newConfig.title = {
...(title as any),
tip: tip.content,
docUrl: tip.url,
};
}
}
if (display || fieldStyle) {
extraProps.display = display || fieldStyle;
if (extraProps.display === DISPLAY_TYPE.TAB) {
extraProps.display = DISPLAY_TYPE.ACCORDION;
}
}
if (collapse || collapsed || fieldCollapsed || extraProps.display === DISPLAY_TYPE.ENTRY) {
extraProps.defaultCollapsed = true;
}
function isDisabled(field: Field) {
if (typeof disabled === 'function') {
return disabled.call(field, field.getValue()) === true;
}
return disabled === true;
}
function isHidden(field: Field) {
if (typeof hidden === 'function') {
return hidden.call(field, field.getValue()) === true;
}
return hidden === true;
}
if (extraProps.display === DISPLAY_TYPE.NONE) {
extraProps.display = undefined;
extraProps.condition = () => false;
} else if (hidden != null || disabled != null) {
extraProps.condition = (field: Field) => !(isHidden(field) || isDisabled(field));
}
if (type === 'group') {
newConfig.items = items ? upgradeConfigure(items, collector) : [];
return newConfig;
}
if (defaultValue !== undefined) {
extraProps.defaultValue = defaultValue;
} else if (typeof initialValue !== 'function') {
extraProps.defaultValue = initialValue;
}
let initialFn = (slotName ? null : initial) || initialValue;
if (slotName && initialValue === true) {
initialFn = (value: any, defaultValue: any) => {
if (isJSBlock(value)) {
return value;
}
return {
type: 'JSSlot',
title: slotTitle || title,
name: slotName,
value: initialChildren,
};
};
}
if (!slotName) {
if (accessor) {
extraProps.getValue = (field: Field, fieldValue: any) => {
return accessor.call(field, fieldValue);
};
}
if (sync || accessor) {
collector.addAutorun({
name,
autorun: (field: Field) => {
let fieldValue = untracked(() => field.getValue());
if (accessor) {
fieldValue = accessor.call(field, fieldValue);
}
if (sync) {
fieldValue = sync.call(field, fieldValue);
if (fieldValue !== undefined) {
field.setValue(fieldValue);
}
} else {
field.setValue(fieldValue);
}
},
});
}
if (mutator) {
extraProps.setValue = (field: Field, value: any) => {
// TODO: 兼容代码,不触发查询组件的 Mutator
if (field instanceof SettingField && field.componentMeta?.componentName === 'Filter') {
return;
}
mutator.call(field, value, value);
};
}
}
const setterInitial = getInitialFromSetter(setter);
if (type !== 'composite') {
collector.addInitial({
// FIXME! name could be "xxx.xxx"
name: slotName || name,
initial: (field: Field, currentValue: any) => {
// FIXME! read from prototype.defaultProps
const defaults = extraProps.defaultValue;
if (typeof initialFn !== 'function') {
initialFn = defaultInitial;
}
const v = initialFn.call(field, currentValue, defaults);
if (setterInitial) {
return setterInitial.call(field, v, defaults);
}
return v;
},
});
}
if (ignore != null || disabled != null) {
collector.addFilter({
// FIXME! name should be "xxx.xxx"
name: slotName || name,
filter: (field: Field, currentValue: any) => {
let disabledValue: boolean;
if (typeof disabled === 'function') {
disabledValue = disabled.call(field, currentValue) === true;
} else {
disabledValue = disabled === true;
}
if (disabledValue) {
return false;
}
if (typeof ignore === 'function') {
return ignore.call(field, currentValue) !== true;
}
return ignore !== true;
},
});
}
if (slotName) {
newConfig.name = slotName;
if (!newConfig.title && slotTitle) {
newConfig.title = slotTitle;
}
const setters: any[] = [
{
componentName: 'SlotSetter',
initialValue: (field: any, value: any) => {
if (isJSSlot(value)) {
return {
title: slotTitle || title,
...value,
};
}
return {
type: 'JSSlot',
title: slotTitle || title,
name: slotName,
value: value == null ? initialChildren : value,
};
},
},
];
if (allowTextInput) {
setters.unshift('I18nSetter');
}
if (supportVariable) {
setters.push('VariableSetter');
}
newConfig.setter = setters.length > 1 ? setters : setters[0];
return newConfig;
}
let primarySetter: any;
if (type === 'composite') {
const initials: InitialItem[] = [];
const objItems = items
? upgradeConfigure(items,
{
addInitial: (item) => {
initials.push(item);
},
addFilter: (item) => {
collector.addFilter({
name: `${name}.${item.name}`,
filter: item.filter,
});
},
addAutorun: (item) => {
collector.addAutorun({
name: `${name}.${item.name}`,
autorun: item.autorun,
});
},
})
: [];
newConfig.items = objItems;
const initial = (target: SettingTarget, value?: any) => {
// TODO:
const defaults = extraProps.defaultValue;
const data: any = {};
initials.forEach((item) => {
// FIXME! Target may be a wrong
data[item.name] = item.initial(target, isPlainObject(value) ? value[item.name] : null);
});
return data;
};
collector.addInitial({
name,
initial,
});
primarySetter = {
componentName: 'ObjectSetter',
props: {
config: {
items: objItems,
},
},
initialValue: (field: Field) => {
return initial(field, field.getValue());
},
};
} else if (setter) {
if (Array.isArray(setter)) {
// FIXME! read initial from setter
primarySetter = setter.map(({ setter, condition }) => {
return {
componentName: setter,
condition: condition
? (field: Field) => {
return condition.call(field, field.getValue());
}
: null,
};
});
} else {
primarySetter = setter;
}
}
if (!primarySetter) {
primarySetter = 'I18nSetter';
}
if (supportVariable) {
const setters = Array.isArray(primarySetter)
? primarySetter.concat('VariableSetter')
: [primarySetter, 'VariableSetter'];
primarySetter = {
componentName: 'MixedSetter',
props: {
setters,
onSetterChange: (field: Field, name: string) => {
if (useVariableChange) {
useVariableChange.call(field, { isUseVariable: name === 'VariableSetter' });
}
},
},
};
}
newConfig.setter = primarySetter;
if (liveTextEditing) {
extraProps.liveTextEditing = liveTextEditing;
}
return newConfig;
}
type AddInitial = (initialItem: InitialItem) => void;
type AddFilter = (filterItem: FilterItem) => void;
type AddAutorun = (autorunItem: AutorunItem) => void;
type ConfigCollector = {
addInitial: AddInitial;
addFilter: AddFilter;
addAutorun: AddAutorun;
};
function getInitialFromSetter(setter: any) {
return setter && (
setter.initial || setter.Initial
|| (setter.type && (setter.type.initial || setter.type.Initial))
) || null; // eslint-disable-line
}
function defaultInitial(value: any, defaultValue: any) {
return value == null ? defaultValue : value;
}
export function upgradeConfigure(items: OldPropConfig[], collector: ConfigCollector) {
const configure: any[] = [];
let ignoreSlotName: any = null;
items.forEach((config) => {
if (config.slotName) {
ignoreSlotName = config.slotName;
} else if (ignoreSlotName) {
if (config.name === ignoreSlotName) {
ignoreSlotName = null;
return;
}
ignoreSlotName = null;
}
configure.push(upgradePropConfig(config, collector));
});
return configure;
}
export function upgradeActions(actions?: Array<ComponentType<any> | ReactElement> | (() => ReactElement)) {
if (!actions) {
return null;
}
if (!Array.isArray(actions)) {
actions = [actions];
}
return actions.map((content) => {
const type: any = isValidElement(content) ? content.type : content;
if (typeof content === 'function') {
const fn = content as () => ReactElement;
content = (({ node }: any) => {
return fn.call(node);
}) as any;
}
return {
name: type.displayName || type.name || 'anonymous',
content,
important: true,
};
});
}
/**
*
*/
export function upgradeMetadata(oldConfig: OldPrototypeConfig) {
const {
componentName,
docUrl,
title,
icon,
packageName,
category,
extraActions,
defaultProps,
initialChildren,
snippets,
view,
configure,
transducers,
isContainer,
isAbsoluteLayoutContainer,
rectSelector,
isModal,
isFloating,
descriptor,
context,
canOperating,
canContain,
canDropTo,
canDropto,
canDropIn,
canDroping,
canUseCondition,
canLoop,
// hooks
canDraging,
canDragging, // handleDragging
// events
didDropOut, // onNodeRemove
didDropIn, // onNodeAdd
subtreeModified, // onSubtreeModified
canResizing, // resizing
onResizeStart, // onResizeStart
onResize, // onResize
onResizeEnd, // onResizeEnd
devMode,
schema,
isTopFixed,
} = oldConfig;
const meta: any = {
componentName,
title,
icon,
docUrl,
devMode: devMode || 'procode',
schema: schema?.componentsTree[0],
};
if (category) {
meta.tags = [category];
}
if (packageName) {
meta.npm = {
componentName,
package: packageName,
};
}
const component: any = {
isContainer,
rootSelector: rectSelector,
isModal,
isFloating,
descriptor,
};
if (canOperating === false) {
component.disableBehaviors = '*';
}
if (extraActions) {
component.actions = upgradeActions(extraActions);
}
const nestingRule: any = {};
if (canContain) {
nestingRule.descendantWhitelist = canContain;
}
if (canDropTo != null || canDropto != null) {
if (canDropTo === false || canDropto === false) {
nestingRule.parentWhitelist = () => false;
}
if (canDropTo !== true && canDropto !== true) {
nestingRule.parentWhitelist = canDropTo || canDropto;
}
}
if (canDropIn != null || canDroping != null) {
if (canDropIn === false || canDroping === false) {
nestingRule.childWhitelist = () => false;
}
if (canDropIn !== true && canDroping !== true) {
nestingRule.childWhitelist = canDropIn || canDroping;
}
}
component.nestingRule = nestingRule;
// 未考虑清楚的,放在实验性段落
const experimental: any = {
isAbsoluteLayoutContainer,
};
if (context) {
// for prototype.getContextInfo
experimental.context = context;
}
if (snippets) {
experimental.snippets = snippets.map((data) => {
const { schema = {} } = data;
if (!schema.children && initialChildren && typeof initialChildren !== 'function') {
schema.children = initialChildren;
}
return {
...data,
schema,
};
});
}
// FIXME! defaultProps for initial input
// initialChildren maybe a function
else if (defaultProps || initialChildren) {
const snippet = {
screenshot: icon,
label: title,
schema: {
componentName,
props: defaultProps,
children: initialChildren,
},
};
if (experimental.snippets) {
experimental.snippets.push(snippet);
} else {
experimental.snippets = [snippet];
}
}
if (initialChildren) {
experimental.initialChildren =
typeof initialChildren === 'function'
? (node: any) => {
return initialChildren.call(node, node.settingEntry);
}
: initialChildren;
}
if (view) {
experimental.view = view;
}
if (isTopFixed) {
experimental.isTopFixed = isTopFixed;
}
if (transducers) {
// Array<{ toStatic, toNative }>
// ? only twice
experimental.transducers = transducers;
}
if (canResizing) {
// TODO: enhance
experimental.getResizingHandlers = (currentNode: any) => {
const directs = ['n', 'w', 's', 'e'];
if (canResizing === true) {
return directs;
}
return directs.filter((d) => canResizing(currentNode, d));
};
}
const callbacks: any = {};
if (canDragging != null || canDraging != null) {
let v = true;
if (canDragging === false || canDraging === false) {
v = false;
}
callbacks.onMoveHook = () => v;
}
if (didDropIn) {
callbacks.onNodeAdd = didDropIn;
}
if (didDropOut) {
callbacks.onNodeRemove = didDropOut;
}
if (subtreeModified) {
callbacks.onSubtreeModified = subtreeModified;
}
if (onResize) {
callbacks.onResize = (e: any, currentNode: any) => {
// todo: what is trigger?
const { trigger, deltaX, deltaY } = e;
onResize(e, trigger, currentNode, deltaX, deltaY);
};
}
if (onResizeStart) {
callbacks.onResizeStart = (e: any, currentNode: any) => {
// todo: what is trigger?
const { trigger } = e;
onResizeStart(e, trigger, currentNode);
};
}
if (onResizeEnd) {
callbacks.onResizeEnd = (e: any, currentNode: any) => {
// todo: what is trigger?
const { trigger } = e;
onResizeEnd(e, trigger, currentNode);
};
}
experimental.callbacks = callbacks;
const initials: InitialItem[] = [];
const filters: FilterItem[] = [];
const autoruns: AutorunItem[] = [];
const props = upgradeConfigure(configure || [],
{
addInitial: (item) => {
initials.push(item);
},
addFilter: (item) => {
filters.push(item);
},
addAutorun: (item) => {
autoruns.push(item);
},
});
experimental.initials = initials;
experimental.filters = filters;
experimental.autoruns = autoruns;
const supports: any = {};
if (canUseCondition != null) {
supports.condition = canUseCondition;
}
if (canLoop != null) {
supports.loop = canLoop;
}
meta.configure = { props, component, supports };
meta.experimental = experimental;
return meta;
}

View File

@ -0,0 +1,83 @@
import logger from '@ali/vu-logger';
import { EventEmitter } from 'events';
import { editor } from '@ali/lowcode-engine';
/**
* Bus class as an EventEmitter
*/
export class Bus {
private emitter = new EventEmitter();
getEmitter() {
return this.emitter;
}
// alias to sub
on(event: string | symbol, func: (...args: any[]) => any): any {
return this.sub(event, func);
}
// alias to unsub
off(event: string, func: (...args: any[]) => any) {
this.unsub(event, func);
}
// alias to pub
emit(event: string, ...args: any[]): boolean {
return this.pub(event, ...args);
}
sub(event: string | symbol, func: (...args: any[]) => any) {
this.emitter.on(event, func);
return () => {
this.emitter.removeListener(event, func);
};
}
once(event: string, func: (...args: any[]) => any) {
this.emitter.once(event, func);
return () => {
this.emitter.removeListener(event, func);
};
}
unsub(event: string, func: (...args: any[]) => any) {
if (func) {
this.emitter.removeListener(event, func);
} else {
this.emitter.removeAllListeners(event);
}
}
/**
* Release & Publish Events
*/
pub(event: string, ...args: any[]): boolean {
logger.info('INFO:', 'eventData:', event, ...args);
return this.emitter.emit(event, ...args);
}
removeListener(eventName: string | symbol, callback: () => any) {
return this.emitter.removeListener(eventName, callback);
}
}
const bus = new Bus();
editor?.on('hotkey.callback.call', (data) => {
bus.emit('ve.hotkey.callback.call', data);
});
editor?.on('history.back', (data) => {
bus.emit('ve.history.back', data);
});
editor?.on('history.forward', (data) => {
bus.emit('ve.history.forward', data);
});
editor?.on('node.prop.change', (data) => {
bus.emit('node.prop.change', data);
});
export default bus;

View File

@ -0,0 +1,82 @@
@import '~@ali/ve-less-variables/index.less';
// 样式直接沿用之前的样式,优化了下命名
.instance-node-selector {
position: relative;
margin-right: 2px;
color: var(--color-icon-white, @title-bgcolor);
border-radius: @global-border-radius;
margin-right: 2px;
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);
}
}
&-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);
}
}
&-list {
position: absolute;
left: 0;
right: 0;
opacity: 0;
visibility: hidden;
}
&-node {
margin: 2px 0;
&-content {
padding-left: 6px;
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;
}
}

View File

@ -0,0 +1,113 @@
import { Overlay } from '@alifd/next';
import React from 'react';
import { Node, ParentalNode } from '@ali/lowcode-designer';
import { editorHelper } from '@ali/lowcode-engine';
import './index.less';
const { Popup } = Overlay;
const { Title } = editorHelper;
export interface IProps {
node: Node;
}
export interface IState {
parentNodes: Node[];
}
type UnionNode = Node | ParentalNode | null;
export class InstanceNodeSelector extends React.Component<IProps, IState> {
state: IState = {
parentNodes: [],
};
componentDidMount() {
const parentNodes = this.getParentNodes(this.props.node);
this.setState({
parentNodes,
});
}
// 获取节点的父级节点最多获取5层
getParentNodes = (node: Node) => {
const parentNodes = [];
let currentNode: UnionNode = node;
while (currentNode && parentNodes.length < 5) {
currentNode = currentNode.getParent();
if (currentNode) {
parentNodes.push(currentNode);
}
}
return parentNodes;
};
onSelect = (node: Node) => () => {
if (node && typeof node.select === 'function') {
node.select();
}
};
onMouseOver = (node: Node) => (_: any, flag = true) => {
if (node && typeof node.hover === 'function') {
node.hover(flag);
}
};
onMouseOut = (node: Node) => (_: any, flag = false) => {
if (node && typeof node.hover === 'function') {
node.hover(flag);
}
};
renderNodes = (node: Node) => {
const nodes = this.state.parentNodes || [];
const children = nodes.map((node, key) => {
return (
<div
key={key}
onClick={this.onSelect(node)}
onMouseEnter={this.onMouseOver(node)}
onMouseLeave={this.onMouseOut(node)}
className="instance-node-selector-node"
>
<div className="instance-node-selector-node-content">
<Title
className="instance-node-selector-node-title"
title={{
label: node.title,
icon: node.icon,
}}
/>
</div>
</div>
);
});
return children;
};
render() {
const { node } = this.props;
return (
<div className="instance-node-selector">
<Popup
trigger={
<div className="instance-node-selector-current">
<Title
className="instance-node-selector-node-title"
title={{
label: node.title,
icon: node.icon,
}}
/>
</div>
}
triggerType="hover"
>
<div className="instance-node-selector">{this.renderNodes(node)}</div>
</Popup>
</div>
);
}
}

View File

@ -0,0 +1,113 @@
import { assign } from 'lodash';
import { Component, ReactElement } from 'react';
import VisualManager from './base/visualManager';
import Prototype from './bundle/prototype';
import { VE_HOOKS } from './base/const';
import { setters } from '@ali/lowcode-engine';
// TODO: Env 本地引入后需要兼容方法 getDesignerLocale
// import Env from './env';
const { registerSetter } = setters;
// prop is Prop object in Designer
export type SetterProvider = (prop: any, componentPrototype: Prototype) => Component | ReactElement<any>;
export class VisualEngineContext {
private managerMap: { [name: string]: VisualManager } = {};
private moduleMap: { [name: string]: any } = {};
private pluginsMap: { [name: string]: any } = {};
use(pluginName: string, plugin: any) {
this.pluginsMap[pluginName || 'unknown'] = plugin;
if (pluginName === VE_HOOKS.VE_SETTING_FIELD_VARIABLE_SETTER) {
registerSetter('VariableSetter', {
component: plugin,
title: { type: 'i18n', 'zh-CN': '变量绑定', 'en-US': 'Variable Binding' },
// TODO: add logic below
// initialValue?: any | ((field: any) => any);
});
}
}
getPlugin(name: string) {
if (!name) {
name = 'default';
}
if (this.pluginsMap[name]) {
return this.pluginsMap[name];
} else if (this.moduleMap[name]) {
return this.moduleMap[name];
}
return this.getManager(name);
}
registerManager(managerMap?: { [name: string]: VisualManager }): this;
registerManager(name: string, manager: VisualManager): this;
registerManager(name?: any, manager?: VisualManager): this {
if (name && typeof name === 'object') {
this.managerMap = assign(this.managerMap, name);
} else {
this.managerMap[name] = manager as VisualManager;
}
return this;
}
registerModule(moduleMap: { [name: string]: any }): this;
registerModule(name: string, module: any): this;
registerModule(name?: any, module?: any): this {
if (typeof name === 'object') {
this.moduleMap = Object.assign({}, this.moduleMap, name);
} else {
this.moduleMap[name] = module;
}
return this;
}
getManager(name: string): VisualManager {
return this.managerMap[name];
}
getModule(name: string): any {
return this.moduleMap[name];
}
// getDesignerLocale(): string {
// return Env.getLocale();
// }
/**
* Builtin APIs
*/
/**
* support dynamic setter replacement
*/
registerDynamicSetterProvider(setterProvider: SetterProvider) {
if (!setterProvider) {
console.error('ERROR: ', 'please set provider function.');
return;
}
this.use('ve.plugin.setterProvider', setterProvider);
}
/**
* support add treePane on the setting pane
* @param treePane see @ali/ve-tree-pane
* @param treeCore see @ali/ve-tree-pane
*/
registerTreePane(TreePane: Component, TreeCore: Component) {
if (TreePane && TreeCore) {
this.registerModule('TreePane', TreePane);
this.registerModule('TreeCore', TreeCore);
}
}
}
export default new VisualEngineContext();

View File

@ -0,0 +1,58 @@
import Env from './env';
import { isJSSlot, isI18nData, isJSExpression } from '@ali/lowcode-types';
import { isPlainObject } from '@ali/lowcode-utils';
import i18nUtil from './i18n-util';
import { editor } from '@ali/lowcode-engine';
import { isVariable } from './utils';
// FIXME: 表达式使用 mock 值未来live 模式直接使用原始值
// TODO: designType
export function deepValueParser(obj?: any): any {
if (isJSExpression(obj)) {
if (editor.get('designMode') === 'live') {
return obj;
}
obj = obj.mock;
}
// 兼容 ListSetter 中的变量结构
if (isVariable(obj)) {
if (editor.get('designMode') === 'live') {
return {
type: 'JSExpression',
value: obj.variable,
mock: obj.value,
};
}
obj = obj.value;
}
if (!obj) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => deepValueParser(item));
}
if (isPlainObject(obj)) {
if (isI18nData(obj)) {
// FIXME! use editor.get
let locale = Env.getLocale();
if (obj.key) {
// FIXME: 此处需要升级I18nUtil改成响应式
return i18nUtil.get(obj.key, locale);
}
if (locale !== 'zh_CN' && locale !== 'zh_TW' && !obj[locale]) {
locale = 'en_US';
}
return obj[obj.use || locale] || obj.zh_CN;
}
if (isJSSlot(obj)) {
return obj;
}
const out: any = {};
Object.keys(obj).forEach((key) => {
out[key] = deepValueParser(obj[key]);
});
return out;
}
return obj;
}

View File

@ -0,0 +1,59 @@
import { designer, designerHelper } from '@ali/lowcode-engine';
import { isPrototype } from './bundle/prototype';
const { DragObjectType, isNode, isDragNodeDataObject } = designerHelper;
const { dragon } = designer;
const DragEngine = {
from(shell: Element, boost: (e: MouseEvent) => any): any {
return dragon.from(shell, (e) => {
const r = boost(e);
if (!r) {
return null;
}
if (isPrototype(r)) {
return {
type: DragObjectType.NodeData,
data: {
componentName: r.getComponentName(),
},
};
} else if (isNode(r)) {
return {
type: DragObjectType.Node,
nodes: [r],
};
} else {
return {
type: DragObjectType.NodeData,
data: r,
};
}
});
},
onDragstart(func: (e: any, dragment: any) => any) {
return dragon.onDragstart((evt) => {
func(evt.originalEvent, evt.dragObject.nodes[0]);
});
},
onDrag(func: (e: any, dragment: any, location: any) => any) {
return dragon.onDrag((evt) => {
const loc = designer.currentDocument?.dropLocation;
func(evt.originalEvent, evt.dragObject.nodes[0], loc);
});
},
onDragend(func: (dragment: any, location: any, copy: any) => any) {
return dragon.onDragend(({ dragObject, copy }) => {
const loc = designer.currentDocument?.dropLocation;
if (isDragNodeDataObject(dragObject)) {
func(dragObject.data, loc, copy);
} else {
func(dragObject.nodes[0], loc, copy);
}
});
},
inDragging() {
return dragon.dragging;
},
};
export default DragEngine;

View File

@ -0,0 +1,95 @@
import { EventEmitter } from 'events';
import { ALI_SCHEMA_VERSION } from './base/const';
import { editorHelper } from '@ali/lowcode-engine';
const { obx } = editorHelper;
interface ILiteralObject {
[key: string]: any;
}
export class Env {
@obx.val envs: ILiteralObject = {};
private emitter: EventEmitter;
private featureMap: ILiteralObject;
constructor() {
this.emitter = new EventEmitter();
this.emitter.setMaxListeners(0);
this.featureMap = {};
}
get(name: string): any {
return this.getEnv(name);
}
getEnv(name: string): any {
return this.envs[name];
}
set(name: string, value: any) {
return this.setEnv(name, value);
}
setEnv(name: string, value: any) {
const orig = this.envs[name];
if (JSON.stringify(orig) === JSON.stringify(value)) {
return;
}
this.envs[name] = value;
this.emitter.emit('envchange', this.envs, name, value);
}
setEnvMap(envs: ILiteralObject): void {
this.envs = Object.assign(this.envs, envs);
this.emitter.emit('envchange', this.envs);
}
getLocale(): string {
return this.getEnv('locale') || 'zh_CN';
}
setLocale(locale: string) {
this.setEnv('locale', locale);
}
setExpertMode(flag: string) {
this.setEnv('expertMode', !!flag);
}
isExpertMode() {
return !!this.getEnv('expertMode');
}
getSupportFeatures() {
return Object.assign({}, this.featureMap);
}
setSupportFeatures(features: ILiteralObject) {
this.featureMap = Object.assign({}, this.featureMap, features);
}
supports(name = 'supports') {
return !!this.featureMap[name];
}
onEnvChange(func: (envs: ILiteralObject, name: string, value: any) => any) {
this.emitter.on('envchange', func);
return () => {
this.emitter.removeListener('envchange', func);
};
}
clear() {
this.envs = {};
this.featureMap = {};
}
getAliSchemaVersion() {
return ALI_SCHEMA_VERSION;
}
}
export default new Env();

View File

@ -0,0 +1,24 @@
import { Node } from '@ali/lowcode-designer';
import { designer } from '@ali/lowcode-engine';
export default {
select: (node: Node) => {
if (!node) {
return designer.currentSelection?.clear();
}
designer.currentSelection?.select(node.id);
},
getSelected: () => {
const nodes = designer.currentSelection?.getNodes();
return nodes?.[0];
},
/**
* TODO dirty fix
*/
onIntoView(func: (node: any, insertion: any) => any) {
// this.emitter.on('intoview', func);
return () => {
// this.emitter.removeListener('intoview', func);
};
},
};

View File

@ -0,0 +1,151 @@
import classnames from 'classnames';
import * as React from 'react';
import { Component } from 'react';
import InlineTip from './inlinetip';
import { isPlainObject } from '@ali/lowcode-utils';
interface IHelpTip {
url?: string;
content?: string;
}
function splitWord(title: string): JSX.Element[] {
return (title || '').split('').map((w, i) => <b key={`word${i}`} className="engine-word">{w}</b>);
}
function getFieldTitle(title: string, tip: IHelpTip, compact?: boolean, propName?: string): JSX.Element {
const className = classnames('engine-field-title', { 've-compact': compact });
let titleContent = null;
if (!compact && typeof title === 'string') {
titleContent = splitWord(title);
}
let tipUrl = null;
let tipContent = null;
tipContent = (
<div>
<div>{propName}</div>
</div>
);
if (isPlainObject(tip)) {
tipUrl = tip.url;
tipContent = (
<div>
<div>{propName}</div>
<div>{tip.content}</div>
</div>
);
} else if (tip) {
tipContent = (
<div>
<div>{propName}</div>
<div>{tip}</div>
</div>
);
}
return (
<a
className={className}
target="_blank"
rel="noopener noreferrer"
href={tipUrl!}
>
{titleContent || (typeof title === 'object' ? '' : title)}
<InlineTip position="top">{tipContent}</InlineTip>
</a>
);
}
export interface IVEFieldProps {
prop: any;
children: JSX.Element | string;
title?: string;
tip?: any;
propName?: string;
className?: string;
compact?: boolean;
stageName?: string;
/**
* render the top-header by jsx
*/
headDIY?: boolean;
isSupportVariable?: boolean;
isSupportMultiSetter?: boolean;
isUseVariable?: boolean;
isGroup?: boolean;
isExpand?: boolean;
toggleExpand?: () => any;
onExpandChange?: (fn: () => any) => any;
}
interface IVEFieldState {
hasError?: boolean;
}
export default class VEField extends Component<IVEFieldProps, IVEFieldState> {
public static displayName = 'VEField';
public readonly props: IVEFieldProps;
public classNames: string[] = [];
public state: IVEFieldState = {
hasError: false,
};
public componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error(error);
console.warn(info.componentStack);
}
public renderHead(): JSX.Element | JSX.Element[] | null {
const { title, tip, compact, propName } = this.props;
return getFieldTitle(title!, tip, compact, propName);
}
public renderBody(): JSX.Element | string {
return this.props.children;
}
public renderFoot(): any {
return null;
}
public render(): JSX.Element {
const { stageName, headDIY } = this.props;
const classNameList = classnames(...this.classNames, this.props.className);
const fieldProps: any = {};
if (stageName) {
// 为 stage 切换奠定基础
fieldProps['data-stage-target'] = this.props.stageName;
}
if (this.state.hasError) {
return (
<div>Field render error, please open console to find out.</div>
);
}
const headContent = headDIY ? this.renderHead()
: <div className="engine-field-head">{this.renderHead()}</div>;
return (
<div className={classNameList} {...fieldProps}>
{headContent}
<div className="engine-field-body">
{this.renderBody()}
</div>
<div className="engine-field-foot">
{this.renderFoot()}
</div>
</div>
);
}
}

View File

@ -0,0 +1,272 @@
@import '~@ali/ve-less-variables/index.less';
.engine-setting-field {
white-space: nowrap;
position: relative;
&:after, &:before {
content: " ";
display: table;
}
&:after {
clear: both;
}
.engine-field-title {
font-size: 12px;
font-family: @font-family;
line-height: 1em;
user-select: none;
color: var(--color-text, @dark-alpha-3);
width: fit-content;
white-space: initial;
word-break: break-word;
&::first-letter {
text-transform: capitalize;
}
.engine-word {
flex: 1;
text-align: center;
font-weight: normal;
&:first-child {
text-align: left;
}
&:last-of-type {
text-align: right;
}
&:only-of-type {
text-align: center;
}
overflow: hidden;
}
}
a.engine-field-title {
border-bottom: 1px dashed var(--color-line-normal, @normal-alpha-7);
text-decoration: none;
padding-bottom: 2px;
&:hover {
cursor: help;
}
}
.engine-field-variable-wrapper {
margin-left: 5px;
}
.engine-field-variable {
cursor: pointer;
opacity: 0.6;
&.engine-active {
opacity: 1;
color: var(--color-brand, @brand-color-1);
}
}
.engine-field-head {
padding-left: 10px;
height: 32px;
background: var(--color-block-background-shallow, @normal-alpha-8);
display: flex;
align-items: center;
font-weight: 500;
border-top: 1px solid var(--color-line-normal, @normal-alpha-7);
border-bottom: 1px solid var(--color-line-normal, @normal-alpha-7);
color: var(--color-title, @dark-alpha-2);
>.engine-icontip {
margin-left: 2px;
}
}
.engine-field-body {
min-height: 20px;
margin: 6px 0;
&:after, &:before {
content: " ";
display: table;
}
&:after {
clear: both;
}
.engine-field-head {
height: 28px;
border: none;
font-weight: 400;
}
}
&.engine-plain-field {
>.engine-field-variable {
position: absolute;
right: 5px;
top: 8px;
}
&:hover {
>.engine-field-variable {
opacity: 1;
}
}
}
&.engine-entry-field {
cursor: pointer;
display: flex;
align-items: center;
height: 32px;
padding-left: 10px;
font-weight: 500;
border-top: 1px solid var(--color-line-normal, @normal-alpha-7);
border-bottom: 1px solid var(--color-line-normal, @normal-alpha-7);
background: var(--color-block-background-shallow, @normal-alpha-8);
margin-bottom: 6px;
>.engine-field-title {
letter-spacing: 1px;
}
>.engine-icontip {
margin-left: 2px;
}
>.engine-field-arrow {
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%) rotate(-90deg);
opacity: 0.4;
}
&:hover {
>.engine-field-arrow {
opacity: 1;
}
}
}
&.engine-popup-field {
cursor: pointer;
display: flex;
align-items: center;
height: 32px;
padding-left: 10px;
background: var(--color-block-background-shallow, @normal-alpha-8);
margin-bottom: 1px;
>.engine-field-title {
letter-spacing: 1px;
}
>.engine-icontip {
margin-left: 2px;
}
>.engine-field-icon {
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
opacity: 0.6;
}
&:hover {
>.engine-field-icon {
opacity: 1;
}
}
}
&.engine-block-field {
>.engine-field-head{
> .engine-field-title {
letter-spacing: 1px;
}
>.engine-field-variable {
margin-left: 2px;
}
}
>.engine-field-body {
margin: 6px;
}
}
&.engine-inline-field {
display: flex;
align-items: center;
margin: 10px;
>.engine-field-head {
display: inline-flex;
background: none;
padding: 0;
border: none;
>.engine-field-title {
display: inline-flex;
width: 50px;
margin-right: 5px;
}
}
>.engine-field-body {
width: 100%;
display: inline-flex;
align-items: flex-start;
padding: 0;
margin: 0;
flex: 1;
position: relative;
}
>.engine-field-variable {
margin-left: 2px;
}
&:hover {
>.engine-field-variable {
opacity: 1;
}
}
}
&.engine-accordion-field {
>.engine-field-head {
position: relative;
cursor: pointer;
>.engine-field-title {
letter-spacing: 1px;
}
>.engine-field-arrow {
transform: rotate(180deg);
position: absolute;
right: 7px;
top: 7px;
transition: transform 0.1s ease;
opacity: 0.6;
}
>.engine-field-variable {
margin-left: 2px;
}
}
&.engine-collapsed {
>.engine-field-head {
margin-bottom: 6px;
}
>.engine-field-head > .engine-field-arrow {
transform: rotate(0);
}
>.engine-field-body {
display: none;
}
}
>.engine-field-body {
margin: 6px;
}
}
}
.engine-block-field,.engine-accordion-field,.engine-entry-field {
.engine-input-control {
margin: 10px;
}
}
.engine-field-tip-icon {
margin-left: 2px;
}

View File

@ -0,0 +1,379 @@
import Icons from '@ali/ve-icons';
import classNames from 'classnames';
import { Component } from 'react';
import { testType } from '@ali/ve-utils';
import VEField, { IVEFieldProps } from './field';
import { SettingField } from './settingField';
import VariableSwitcher from './variableSwitcher';
import popups from '@ali/ve-popups';
import './fields.less';
interface IHelpTip {
url?: string;
content?: string | JSX.Element;
}
function renderTip(tip: IHelpTip, prop?: { propName?: string }) {
const propName = prop && prop.propName;
if (!tip) {
return (
<Icons.Tip position="top" key="icon" className="engine-field-tip-icon">
<div>
<div>{propName}</div>
</div>
</Icons.Tip>
);
}
if (testType(tip) === 'object') {
return (
<Icons.Tip position="top" url={tip.url} key="icon-tip" className="engine-field-tip-icon">
<div>
<div>{propName}</div>
<div>{tip.content}</div>
</div>
</Icons.Tip>
);
}
return (
<Icons.Tip position="top" key="icon" className="engine-field-tip-icon">
<div>
<div>{propName}</div>
<div>{tip}</div>
</div>
</Icons.Tip>
);
}
export class PlainField extends VEField {
public static defaultProps = {
headDIY: true,
};
public static displayName = 'PlainField';
public renderHead(): null {
return null;
}
}
export class InlineField extends VEField {
public static displayName = 'InlineField';
constructor(props: any) {
super(props);
this.classNames = ['engine-setting-field', 'engine-inline-field'];
}
public renderFoot() {
return (
<div className="engine-field-variable-wrapper">
<VariableSwitcher {...this.props} />
</div>
);
}
}
export class BlockField extends VEField {
public static displayName = 'BlockField';
constructor(props: IVEFieldProps) {
super(props);
this.classNames = ['engine-setting-field', 'engine-block-field', props.isGroup ? 'engine-group-field' : ''];
}
public renderHead() {
const { title, tip, propName } = this.props;
return [
<span className="engine-field-title" key={title}>
{title}
</span>,
renderTip(tip, { propName }),
<VariableSwitcher {...this.props} />,
];
}
}
export class AccordionField extends VEField {
public readonly props: IVEFieldProps;
private willDetach?: () => any;
constructor(props: IVEFieldProps) {
super(props);
this._generateClassNames(props);
if (this.props.onExpandChange) {
this.willDetach = this.props.onExpandChange(() => this.forceUpdate());
}
}
public componentWillReceiveProps(nextProps: IVEFieldProps) {
this.classNames = this._generateClassNames(nextProps);
}
public componentWillUnmount() {
if (this.willDetach) {
this.willDetach();
}
}
public renderHead() {
const { title, tip, toggleExpand, propName } = this.props;
return (
<div className="engine-field-head" onClick={() => toggleExpand && toggleExpand()}>
<Icons name="arrow" className="engine-field-arrow" size="12px" />
<span className="engine-field-title">{title}</span>
{renderTip(tip, { propName })}
{<VariableSwitcher {...this.props} />}
</div>
);
}
private _generateClassNames(props: IVEFieldProps) {
this.classNames = [
'engine-setting-field',
'engine-accordion-field',
props.isGroup ? 'engine-group-field' : '',
!props.isExpand ? 'engine-collapsed' : '',
];
return this.classNames;
}
}
export class EntryField extends VEField {
constructor(props: any) {
super(props);
this.classNames = ['engine-setting-field', 'engine-entry-field'];
}
public render() {
const { propName, stageName, tip, title } = this.props;
const classNameList = classNames(...this.classNames, this.props.className);
const fieldProps: any = {};
if (stageName) {
// 为 stage 切换奠定基础
fieldProps['data-stage-target'] = this.props.stageName;
}
const innerElements = [
<span className="engine-field-title" key="field-title">
{title}
</span>,
renderTip(tip, { propName }),
<Icons name="arrow" className="engine-field-arrow" size="12px" key="engine-field-arrow-icon" />,
];
return (
<div className={classNameList} {...fieldProps}>
{innerElements}
</div>
);
}
}
export class PopupField extends VEField {
constructor(props: any) {
super(props);
this.classNames = ['engine-setting-field', 'engine-popup-field'];
}
public renderBody() {
return '';
}
public render() {
const { propName, stageName, tip, title } = this.props;
const classNameList = classNames(...this.classNames, this.props.className);
const fieldProps: any = {};
if (stageName) {
// 为 stage 切换奠定基础
fieldProps['data-stage-target'] = this.props.stageName;
}
return (
<div
className={classNameList}
onClick={(e) => popups.popup({
cancelOnBlur: true,
content: this.props.children,
position: 'left bottom',
showClose: true,
sizeFixed: true,
target: e.currentTarget,
})
}
>
<span className="engine-field-title">{title}</span>
{renderTip(tip, { propName })}
<VariableSwitcher {...this.props} />
<Icons name="popup" className="engine-field-icon" size="medium" />
</div>
);
}
}
export class CaptionField extends VEField {
constructor(props: IVEFieldProps) {
super(props);
this.classNames = ['engine-setting-field', 'engine-caption-field'];
}
public renderHead() {
const { title, tip, propName } = this.props;
return (
<div>
<span className="engine-field-title">{title}</span>
{renderTip(tip, { propName })}
</div>
);
}
}
export class Stage extends Component {
public readonly props: {
key: any;
stage: any;
current?: boolean;
direction?: any;
};
public stage: any;
public additionClassName: string;
public shell: Element | null = null;
private willDetach: () => any;
public componentWillMount() {
this.stage = this.props.stage;
if (this.stage.onCurrentTabChange) {
this.willDetach = this.stage.onCurrentTabChange(() => this.forceUpdate());
}
}
public componentDidMount() {
this.doSkate();
}
public componentWillReceiveProps(props: any) {
if (props.stage !== this.stage) {
this.stage = props.stage;
if (this.willDetach) {
this.willDetach();
}
if (this.stage.onCurrentTabChange) {
this.willDetach = this.stage.onCurrentTabChange(() => this.forceUpdate());
}
}
}
public componentDidUpdate() {
this.doSkate();
}
public componentWillUnmount() {
if (this.willDetach) {
this.willDetach();
}
}
public doSkate() {
if (this.additionClassName) {
setTimeout(() => {
const elem = this.shell;
if (elem && elem.classList) {
if (this.props.current) {
elem.classList.remove(this.additionClassName);
} else {
elem.classList.add(this.additionClassName);
}
this.additionClassName = '';
}
}, 10);
}
}
public render() {
const { stage } = this;
let content = null;
let tabs = null;
let className = 'engine-settings-stage';
if (stage.getTabs) {
const selected = stage.getNode();
// stat for cache
stage.stat();
const currentTab = stage.getCurrentTab();
if (stage.hasTabs()) {
className += ' engine-has-tabs';
tabs = (
<div className="engine-settings-tabs">
{stage.getTabs().map((tab: any) => (
<div
key={tab.getId()}
className={`engine-settings-tab${tab === currentTab ? ' engine-active' : ''}`}
onClick={() => stage.setCurrentTab(tab)}
>
{tab.getTitle()}
{renderTip(tab.getTip())}
</div>
))}
</div>
);
}
if (currentTab) {
if (currentTab.getVisibleItems) {
content = currentTab
.getVisibleItems()
.map((item: any) => <SettingField key={item.getId()} selected={selected} prop={item} />);
} else if (currentTab.getSetter) {
content = (
<SettingField key={currentTab.getId()} selected={selected} prop={currentTab} forceDisplay="plain" />
);
}
}
} else {
content = stage.getContent();
}
if (this.props.current) {
if (this.props.direction) {
this.additionClassName = `engine-stagein-${this.props.direction}`;
className += ` ${this.additionClassName}`;
}
} else if (this.props.direction) {
this.additionClassName = `engine-stageout-${this.props.direction}`;
}
let stageBacker = null;
if (stage.hasBack()) {
className += ' engine-has-backer';
stageBacker = (
<div className="engine-settings-stagebacker" data-stage-target="stageback">
<Icons name="arrow" className="engine-field-arrow" size="12px" />
<span className="engine-field-title">{stage.getTitle()}</span>
{renderTip(stage.getTip())}
</div>
);
}
return (
<div
ref={(ref) => {
this.shell = ref;
}}
className={className}
>
{stageBacker}
{tabs}
<div className="engine-stage-content">{content}</div>
</div>
);
}
}

View File

@ -0,0 +1,2 @@
export * from './settingField';
export * from './fields';

View File

@ -0,0 +1,30 @@
import { Component } from 'react';
export interface InlineTipProps {
position: string;
theme?: 'green' | 'black';
children: React.ReactNode;
}
export default class InlineTip extends Component<InlineTipProps> {
public static displayName = 'InlineTip';
public static defaultProps = {
position: 'auto',
theme: 'black',
};
public render(): React.ReactNode {
const { position, theme, children } = this.props;
return (
<div
style={{ display: 'none' }}
data-role="tip"
data-position={position}
data-theme={theme}
>
{children}
</div>
);
}
}

View File

@ -0,0 +1,189 @@
import VariableSetter from './variableSetter';
import context from '../context';
import { VE_HOOKS } from '../base/const';
import {
AccordionField,
BlockField,
EntryField,
InlineField,
PlainField,
PopupField,
} from './fields';
import { ComponentClass, Component, isValidElement, createElement } from 'react';
import { editorHelper, setters } from '@ali/lowcode-engine';
const { getSetter } = setters;
const { createSetterContent } = editorHelper;
function isReactClass(obj: any): obj is ComponentClass<any> {
return (
obj &&
obj.prototype &&
(obj.prototype.isReactComponent || obj.prototype instanceof Component)
);
}
interface IExtraProps {
stageName?: string;
isGroup?: boolean;
isExpand?: boolean;
propName?: string;
toggleExpand?: () => any;
onExpandChange?: () => any;
}
const FIELD_TYPE_MAP: any = {
accordion: AccordionField,
block: BlockField,
entry: EntryField,
inline: InlineField,
plain: PlainField,
popup: PopupField,
tab: AccordionField,
};
export class SettingField extends Component {
public readonly props: {
prop: any;
selected?: boolean;
forceDisplay?: string;
className?: string;
children?: JSX.Element | string;
compact?: boolean;
key?: string;
addonProps?: object;
};
/**
* VariableSetter placeholder
*/
public variableSetter: any;
constructor(props: any) {
super(props);
this.variableSetter = getSetter('VariableSetter')?.component || VariableSetter;
}
public render() {
const { prop, selected, addonProps } = this.props;
const display = this.props.forceDisplay || prop.getDisplay();
if (display === 'none') {
return null;
}
// 标准的属性,即每一个 Field 在 VE 下都拥有的属性
const standardProps = {
className: this.props.className,
compact: this.props.compact,
isSupportMultiSetter: this.supportMultiSetter(),
isSupportVariable: prop.isSupportVariable(),
isUseVariable: prop.isUseVariable(),
prop,
setUseVariable: () => prop.setUseVariable(!prop.isUseVariable()),
tip: prop.getTip(),
title: prop.getTitle(),
};
// 部分 Field 所需要的额外 fieldProps
const extraProps = {};
const ctx = context;
const plugin = ctx.getPlugin(VE_HOOKS.VE_SETTING_FIELD_PROVIDER);
let Field;
if (typeof plugin === 'function') {
Field = plugin(display, FIELD_TYPE_MAP, prop);
}
if (!Field) {
Field = FIELD_TYPE_MAP[display] || PlainField;
}
this._prepareProps(display, extraProps);
if (display === 'entry') {
return <Field {...{ ...standardProps, ...extraProps }} />;
}
let setter;
const props: any = {
prop,
selected,
};
const fieldProps = { ...standardProps, ...extraProps };
if (prop.isUseVariable() && !this.variableSetter.isPopup) {
props.placeholder = '请输入表达式: ${var}';
props.key = `${prop.getId()}-variable`;
setter = createElement(this.variableSetter, props);
return <Field {...fieldProps}>{setter}</Field>;
}
// for composited prop
if (prop.getVisibleItems) {
setter = prop
.getVisibleItems()
.map((item: any) => (
<SettingField {...{ key: item.getId(), prop: item, selected }} />
));
return <Field {...fieldProps}>{setter}</Field>;
}
setter = prop.getSetter();
if (
typeof setter === 'object' &&
'componentName' in setter &&
!(isValidElement(setter) || isReactClass(setter))
) {
const { componentName: setterType, props: setterProps } = setter as any;
setter = createSetterContent(setterType, {
...addonProps,
...setterProps,
...props,
});
} else {
setter = createSetterContent(setter, {
...addonProps,
...props,
});
}
return <Field {...fieldProps}>{setter}</Field>;
}
private supportMultiSetter() {
const { prop } = this.props;
const setter = prop && prop.getConfig && prop.getConfig('setter');
return prop.isSupportVariable() || Array.isArray(setter);
}
private _prepareProps(displayType: string, extraProps: IExtraProps): void {
const { prop } = this.props;
extraProps.propName = prop.isGroup()
? '组合属性,无属性名称'
: prop.getName();
switch (displayType) {
case 'title':
break;
case 'block':
Object.assign(extraProps, { isGroup: prop.isGroup() });
break;
case 'accordion':
Object.assign(extraProps, {
headDIY: true,
isExpand: prop.isExpand(),
isGroup: prop.isGroup(),
onExpandChange: () => prop.onExpandChange(() => this.forceUpdate()),
toggleExpand: () => {
prop.toggleExpand();
},
});
break;
case 'entry':
Object.assign(extraProps, { stageName: prop.getName() });
break;
default:
break;
}
}
}

View File

@ -0,0 +1,43 @@
@import '~@ali/ve-less-variables/index.less';
.engine-input-control {
box-sizing: border-box;
font-size: 12px;
font-family: Consolas, "Courier New", Courier, FreeMono, monospace;
color: var(--color-text, @dark-alpha-3);
background: var(--color-field-background, @white-alpha-1);
border: 1px solid var(--color-field-border, @normal-alpha-5);
flex: 1;
border-radius: @global-border-radius;
max-height: 200px;
&:hover {
border-color: var(--color-field-border-hover, @normal-alpha-4);
}
&.engine-focused {
border-color: var(--color-field-border-active, @normal-alpha-3);
}
textarea {
resize: none;
}
>.engine-input {
box-sizing: border-box;
padding: 6px;
display: block;
font-size: 12px;
line-height: 16px;
color: var(--color-text, @dark-alpha-3);
width: 100%;
border: 0;
margin: 0;
background: transparent;
outline: none;
&::-webkit-input-placeholder {
color: var(--color-field-placeholder, @normal-alpha-5);
}
}
}

View File

@ -0,0 +1,86 @@
import './variableSetter.less';
import { Component } from 'react';
class Input extends Component {
public props: {
value: string;
placeholder: string;
onChange: (val: any) => any;
};
public state: { focused: boolean };
constructor(props: object) {
super(props);
this.state = {
focused: false,
};
}
public componentDidMount() {
this.adjustTextAreaHeight();
}
private domRef: HTMLTextAreaElement | null = null;
public adjustTextAreaHeight() {
if (!this.domRef) {
return;
}
this.domRef.style.height = '1px';
const calculatedHeight = this.domRef.scrollHeight;
this.domRef.style.height = calculatedHeight >= 200 ? '200px' : `${calculatedHeight }px`;
}
public render() {
const { value, placeholder, onChange } = this.props;
return (
<div
className={`engine-variable-setter-input engine-input-control${this.state.focused ? ' engine-focused' : ''}`}
>
<textarea
ref={(r) => {
this.domRef = r;
}}
className="engine-input"
value={value || ''}
placeholder={placeholder || ''}
onChange={(e) => {
onChange(e.target.value || '');
}}
onBlur={() => this.setState({ focused: false })}
onFocus={() => this.setState({ focused: true })}
onKeyUp={this.adjustTextAreaHeight.bind(this)}
/>
</div>
);
}
}
export default class VariableSetter extends Component<{
prop: any;
placeholder: string;
}> {
public willDetach: () => any;
public componentWillMount() {
this.willDetach = this.props.prop.onValueChange(() => this.forceUpdate());
}
public componentWillUnmount() {
if (this.willDetach) {
this.willDetach();
}
}
public render() {
const { prop } = this.props;
return (
<Input
value={prop.getVariableValue()}
placeholder={this.props.placeholder}
onChange={(val: string) => prop.setVariableValue(val)}
/>
);
}
}

View File

@ -0,0 +1,20 @@
@import '~@ali/ve-less-variables/index.less';
.engine-field-variable-switcher {
cursor: pointer;
opacity: 0.6;
margin-left: 2px;
&.engine-active {
opacity: 1;
background: var(--color-brand, @brand-color-1);
color: #fff !important;
border-radius: 3px;
margin-left: 4px;
svg {
height: 22px !important;
width: 22px !important;
}
}
}

View File

@ -0,0 +1,61 @@
import VariableSetter from './variableSetter';
import Icons from '@ali/ve-icons';
import { IVEFieldProps } from './field';
import './variableSwitcher.less';
import { Component } from 'react';
import { setters } from '@ali/lowcode-engine';
const { getSetter } = setters;
interface IState {
visible: boolean;
}
export default class VariableSwitcher extends Component<IVEFieldProps, IState> {
private ref: HTMLElement | null = null;
private VariableSetter: any;
constructor(props: IVEFieldProps) {
super(props);
this.VariableSetter = getSetter('VariableSetter')?.component || VariableSetter;
this.state = {
visible: false,
};
}
public render() {
const { isUseVariable, prop } = this.props;
const { visible } = this.state;
const isSupportVariable = prop.isSupportVariable();
const tip = !isUseVariable ? '绑定变量' : prop.getVariableValue();
if (!isSupportVariable) {
return null;
}
return (
<div>
<Icons.Tip
name="var"
size="24px"
position="bottom center"
className={`engine-field-variable-switcher ${isUseVariable ? 'engine-active' : ''}`}
data-tip={tip}
onClick={(e: Event) => {
e.stopPropagation();
if (this.VariableSetter.isPopup) {
this.VariableSetter.show({
prop,
});
} else {
prop.setUseVariable(!isUseVariable);
}
}}
>
</Icons.Tip>
</div>
);
}
}

View File

@ -0,0 +1,152 @@
import domReady from 'domready';
import { EventEmitter } from 'events';
const Shells = ['iphone6'];
export class Flags {
public emitter: EventEmitter;
public flags: string[];
public ready: boolean;
public lastFlags: string[];
public lastShell: string;
private lastSimulatorDevice: string;
constructor() {
this.emitter = new EventEmitter();
this.flags = [];
domReady(() => {
this.ready = true;
this.applyFlags();
});
}
public setDragMode(flag: boolean) {
if (flag) {
this.add('drag-mode');
} else {
this.remove('drag-mode');
}
}
public setPreviewMode(flag: boolean) {
if (flag) {
this.add('preview-mode');
this.remove('design-mode');
} else {
this.add('design-mode');
this.remove('preview-mode');
}
}
public setWithShell(shell: string) {
if (shell === this.lastShell) {
return;
}
if (this.lastShell) {
this.remove(`with-${this.lastShell}shell`);
}
if (shell) {
if (Shells.indexOf(shell) < 0) {
shell = Shells[0];
}
this.add(`with-${shell}shell`);
this.lastShell = shell;
}
}
public setSimulator(device: string) {
if (this.lastSimulatorDevice) {
this.remove(`simulator-${this.lastSimulatorDevice}`);
}
if (device !== '' && device !== 'pc') {
this.add(`simulator-${device}`);
}
this.lastSimulatorDevice = device;
}
public setHideSlate(flag: boolean) {
if (this.has('slate-fixed')) {
return;
}
if (flag) {
this.add('hide-slate');
} else {
this.remove('hide-slate');
}
}
public setSlateFixedMode(flag: boolean) {
if (flag) {
this.remove('hide-slate');
this.add('slate-fixed');
} else {
this.remove('slate-fixed');
}
}
public setSlateFullMode(flag: boolean) {
if (flag) {
this.add('slate-full-screen');
} else {
this.remove('slate-full-screen');
}
}
public getFlags() {
return this.flags;
}
public applyFlags(modifiedFlag?: string) {
if (!this.ready) {
return;
}
const doc = document.documentElement;
if (this.lastFlags) {
this.lastFlags.filter((flag: string) => this.flags.indexOf(flag) < 0).forEach((flag) => {
doc.classList.remove(`engine-${flag}`);
});
}
this.flags.forEach((flag) => {
doc.classList.add(`engine-${flag}`);
});
this.lastFlags = this.flags.slice(0);
this.emitter.emit('flagschange', this.flags, modifiedFlag);
}
public has(flag: string) {
return this.flags.indexOf(flag) > -1;
}
public add(flag: string) {
if (!this.has(flag)) {
this.flags.push(flag);
this.applyFlags(flag);
}
}
public remove(flag: string) {
const i = this.flags.indexOf(flag);
if (i > -1) {
this.flags.splice(i, 1);
this.applyFlags(flag);
}
}
public onFlagsChange(func: () => any) {
this.emitter.on('flagschange', func);
return () => {
this.emitter.removeListener('flagschange', func);
};
}
}
export default new Flags();

View File

@ -0,0 +1,79 @@
declare enum LANGUAGES {
zh_CN = 'zh_CN',
en_US = 'en_US'
}
export interface I18nRecord {
type?: 'i18n';
[key: string]: string;
/**
* i18n unique key
*/
key?: string;
}
export interface I18nRecordData {
gmtCreate: Date;
gmtModified: Date;
i18nKey: string;
i18nText: I18nRecord;
id: number;
}
export interface II18nUtilConfigs {
items?: {};
/**
*
*/
disableInstantLoad?: boolean;
/**
*
*/
disableFullLoad?: boolean;
loader?: (configs: ILoaderConfigs) => Promise<I18nRecordData[]>;
remover?: (key: string, dic: I18nRecord) => Promise<void>;
saver?: (key: string, dic: I18nRecord) => Promise<void>;
}
export interface ILoaderConfigs {
/**
* search keywords
*/
keyword?: string;
/**
* should load all i18n items
*/
isFull?: boolean;
/**
* search i18n item based on uniqueKey
*/
key?: string;
}
export interface II18nUtil {
init(config: II18nUtilConfigs): void;
isInitialized(): boolean;
isReady(): boolean;
attach(prop: object, value: I18nRecord, updator: () => any);
search(keyword: string, silent?: boolean);
load(configs: ILoaderConfigs): Promise<I18nRecord[]>;
/**
* Get local i18n Record
* @param key
* @param lang
*/
get(key: string, lang: string): string | I18nRecord;
getFromRemote(key: string): Promise<I18nRecord>;
getItem(key: string, forceData?: boolean): any;
getItems(): I18nRecord[];
update(key: string, doc: I18nRecord, lang: LANGUAGES);
create(doc: I18nRecord, lang: LANGUAGES): string;
remove(key: string): Promise<void>;
onReady(func: () => any);
onRowsChange(func: () => any);
onChange(func: (dic: I18nRecord) => any);
}
declare const i18nUtil: II18nUtil;
export default i18nUtil;

View File

@ -0,0 +1,312 @@
import { EventEmitter } from 'events';
import { editorHelper } from '@ali/lowcode-engine';
const { obx } = editorHelper;
let keybase = Date.now();
function keygen(maps) {
let key;
do {
key = `i18n-${(keybase).toString(36)}`;
keybase += 1;
} while (key in maps);
return key;
}
class DocItem {
constructor(parent, doc, unInitial) {
this.parent = parent;
const { use, ...strings } = doc;
this.doc = obx.val({
type: 'i18n',
...strings,
});
this.emitter = new EventEmitter();
this.inited = unInitial !== true;
}
getKey() {
return this.doc.key;
}
getDoc(lang) {
if (lang) {
return this.doc[lang];
}
return this.doc;
}
setDoc(doc, lang, initial) {
if (lang) {
this.doc[lang] = doc;
} else {
const { use, strings } = doc || {};
Object.assign(this.doc, strings);
}
this.emitter.emit('change', this.doc);
if (initial) {
this.inited = true;
} else if (this.inited) {
this.parent._saveChange(this.doc.key, this.doc);
}
}
remove() {
if (!this.inited) return Promise.reject('not initialized');
const { key, ...doc } = this.doc; // eslint-disable-line
this.emitter.emit('change', doc);
return this.parent.remove(this.getKey());
}
onChange(func) {
this.emitter.on('change', func);
return () => {
this.emitter.removeListener('change', func);
};
}
}
class I18nUtil {
constructor() {
this.emitter = new EventEmitter();
// original data source from remote
this.i18nData = {};
// current i18n records on the left pane
this.items = [];
this.maps = {};
// full list of i18n records for synchronized call
this.fullList = [];
this.fullMap = {};
this.config = {};
this.ready = false;
this.isInited = false;
}
_prepareItems(items, isFull = false, isSilent = false) {
this[isFull ? 'fullList' : 'items'] = items.map((dict) => {
let item = this[isFull ? 'fullMap' : 'maps'][dict.key];
if (item) {
item.setDoc(dict, null, true);
} else {
item = new DocItem(this, dict);
this[isFull ? 'fullMap' : 'maps'][dict.key] = item;
}
return item;
});
if (this.ready && !isSilent) {
this.emitter.emit('rowschange');
this.emitter.emit('change');
} else {
this.ready = true;
this.emitter.emit('ready');
}
}
_load(configs = {}, silent) {
if (!this.config.loader) {
console.error(new Error('Please load loader while init I18nUtil.'));
return Promise.reject();
}
return this.config.loader(configs).then((data) => {
if (configs.i18nKey) {
return Promise.resolve(data.i18nText);
}
this._prepareItems(data.data, configs.isFull, silent);
// set pagination data to i18nData
this.i18nData = data;
if (!silent) {
this.emitter.emit('rowschange');
this.emitter.emit('change');
}
return Promise.resolve(this.items.map(i => i.getDoc()));
});
}
_saveToItems(key, dict) {
let item = null;
item = this.items.find(doc => doc.getKey() === key);
if (!item) {
item = this.fullList.find(doc => doc.getKey() === key);
}
if (item) {
item.setDoc(dict);
} else {
item = new DocItem(this, {
key,
...dict,
});
this.items.unshift(item);
this.fullList.unshift(item);
this.maps[key] = item;
this.fullMap[key] = item;
this._saveChange(key, dict, true);
}
}
_saveChange(key, dict, rowschange) {
if (rowschange) {
this.emitter.emit('rowschange');
}
this.emitter.emit('change');
if (dict === null) {
delete this.maps[key];
delete this.fullMap[key];
}
return this._save(key, dict);
}
_save(key, dict) {
const saver = dict === null ? this.config.remover : this.config.saver;
if (!saver) return Promise.reject('Saver function is not set');
return saver(key, dict);
}
init(config) {
if (this.isInited) return;
this.config = config || {};
if (this.config.items) {
// inject to current page
this._prepareItems(this.config.items);
}
if (!this.config.disableInstantLoad) {
this._load({ isFull: !this.config.disableFullLoad });
}
this.isInited = true;
}
isInitialized() {
return this.isInited;
}
isReady() {
return this.ready;
}
// add events updater when i18n record change
// we should notify engine's view to change
attach(prop, value, updator) {
const isI18nValue = value && value.type === 'i18n' && value.key;
const key = isI18nValue ? value.key : null;
if (prop.i18nLink) {
if (isI18nValue && (key === prop.i18nLink.key)) {
return prop.i18nLink;
}
prop.i18nLink.detach();
}
if (isI18nValue) {
return {
key,
detach: this.getItem(key, value).onChange(updator),
};
}
return null;
}
/**
* 搜索 i18n 词条
*
* @param {any} keyword 搜索关键字
* @param {boolean} [silent=false] 是否刷新左侧的 i18n 数据
* @returns
*
* @memberof I18nUtil
*/
search(keyword, silent = false) {
return this._load({ keyword }, silent);
}
load(configs = {}) {
return this._load(configs);
}
get(key, lang) {
const item = this.getItem(key);
if (item) {
return item.getDoc(lang);
}
return null;
}
getFromRemote(key) {
return this._load({ i18nKey: key });
}
getItem(key, forceData) {
if (forceData && !this.maps[key] && !this.fullList[key]) {
const item = new DocItem(this, {
key,
...forceData,
}, true);
this.maps[key] = item;
this.fullMap[key] = item;
this.fullList.push(item);
this.items.push(item);
}
return this.maps[key] || this.fullMap[key];
}
getItems() {
return this.items;
}
update(key, doc, lang) {
let dict = this.get(key) || {};
if (!lang) {
dict = doc;
} else {
dict[lang] = doc;
}
this._saveToItems(key, dict);
}
create(doc, lang) {
const dict = lang ? { [lang]: doc } : doc;
const key = keygen(this.fullMap);
this._saveToItems(key, dict);
return key;
}
remove(key) {
const index = this.items.findIndex(item => item.getKey() === key);
const indexG = this.fullList.findIndex(item => item.getKey() === key);
if (index > -1) {
this.items.splice(index, 1);
}
if (indexG > -1) {
this.fullList.splice(index, 1);
}
return this._saveChange(key, null, true);
}
onReady(func) {
this.emitter.on('ready', func);
return () => {
this.emitter.removeListener('ready', func);
};
}
onRowsChange(func) {
this.emitter.on('rowschange', func);
return () => {
this.emitter.removeListener('rowschange', func);
};
}
onChange(func) {
this.emitter.on('change', func);
return () => {
this.emitter.removeListener('change', func);
};
}
}
export default new I18nUtil();

View File

@ -0,0 +1,180 @@
import { createElement } from 'react';
import { render } from 'react-dom';
import * as utils from '@ali/ve-utils';
import Popup from '@ali/ve-popups';
import Icons from '@ali/ve-icons';
import logger from '@ali/vu-logger';
import I18nUtil from './i18n-util';
import { VE_EVENTS as EVENTS, VE_HOOKS as HOOKS, VERSION as Version } from './base/const';
import Bus from './bus';
import { skeleton, designer, editor, plugins, init, hotkey as Hotkey, monitor, designerHelper } from '@ali/lowcode-engine';
import Panes from './panes';
import Exchange from './exchange';
import context from './context';
import VisualManager from './base/visualManager';
import VisualDesigner from './base/visualDesigner';
import Trunk from './bundle/trunk';
import Prototype from './bundle/prototype';
import Bundle from './bundle/bundle';
import Pages from './pages';
import * as Field from './fields';
import Prop from './prop';
import Env from './env';
import DragEngine from './drag-engine';
// import Flags from './base/flags';
import Viewport from './viewport';
import Project from './project';
import Symbols from './symbols';
import { invariant } from './utils';
import '@ali/lowcode-editor-setters';
import './reducers';
import './vision.less';
invariant((window as any).AliLowCodeEngine, 'AliLowCodeEngine is required, since vision polyfill is totally based on AliLowCodeEngine');
// async function init(container?: Element) {
// if (!container) {
// container = document.createElement('div');
// document.body.appendChild(container);
// }
// container.id = 'engine';
// await plugins.init();
// render(
// createElement(Workbench, {
// skeleton,
// className: 'engine-main',
// topAreaItemClassName: 'engine-actionitem',
// }),
// container,
// );
// }
/**
* VE.ui.xxx
*
* Core UI Components
*/
const ui = {
Field,
Icon: Icons,
Icons,
Popup,
};
const modules = {
VisualManager,
VisualDesigner,
I18nUtil,
Prop,
};
// const designerHelper = {
// registerMetadataTransducer,
// addBuiltinComponentAction,
// removeBuiltinComponentAction,
// // modifyBuiltinComponentAction,
// };
const { registerMetadataTransducer } = designerHelper;
const VisualEngine = {
designer,
designerHelper,
editor,
skeleton,
/**
* VE.Popup
*/
Popup,
/**
* VE Utils
*/
utils,
I18nUtil,
Hotkey,
Env,
monitor,
/* pub/sub 集线器 */
Bus,
/* 事件 */
EVENTS,
/* 修饰方法 */
HOOKS,
Exchange,
context,
/**
* VE.init
*
* Initialized the whole VisualEngine UI
*/
init,
ui,
Panes,
modules,
Trunk,
Prototype,
Bundle,
Pages,
DragEngine,
Viewport,
Version,
Project,
logger,
Symbols,
registerMetadataTransducer,
plugins,
// Flags,
};
(window as any).VisualEngine = VisualEngine;
export default VisualEngine;
export {
designer,
designerHelper,
editor,
skeleton,
/**
* VE.Popup
*/
Popup,
/**
* VE Utils
*/
utils,
I18nUtil,
Hotkey,
Env,
monitor,
/* pub/sub 集线器 */
Bus,
/* 事件 */
EVENTS,
/* 修饰方法 */
HOOKS,
Exchange,
context,
/**
* VE.init
*
* Initialized the whole VisualEngine UI
*/
init,
ui,
Panes,
modules,
Trunk,
Prototype,
Bundle,
Pages,
DragEngine,
Viewport,
Version,
Project,
logger,
Symbols,
registerMetadataTransducer,
plugins,
};

View File

@ -0,0 +1 @@
declare module '@ali/vu-css-style';

View File

@ -0,0 +1,145 @@
import { RootSchema } from '@ali/lowcode-types';
import { DocumentModel } from '@ali/lowcode-designer';
import { designer } from '@ali/lowcode-engine';
import NodeCacheVisitor from './rootNodeVisitor';
const { project } = designer;
export interface PageDataV1 {
id: string;
componentsTree: RootSchema[];
layout: RootSchema;
[dataAddon: string]: any;
}
export interface PageDataV2 {
id: string;
componentsTree: RootSchema[];
[dataAddon: string]: any;
}
function isPageDataV1(obj: any): obj is PageDataV1 {
return obj && obj.layout;
}
function isPageDataV2(obj: any): obj is PageDataV2 {
return obj && obj.componentsTree && Array.isArray(obj.componentsTree);
}
type OldPageData = PageDataV1 | PageDataV2;
const pages = Object.assign(project, {
setPages(pages: OldPageData[]) {
if (!pages || !Array.isArray(pages) || pages.length === 0) {
throw new Error('pages schema 不合法');
}
// todo: miniapp
let componentsTree: any = [];
if (window.pageConfig?.isNoCodeMiniApp) {
// 小程序多页面
pages.forEach((item: any) => {
if (isPageDataV1(item)) {
componentsTree.push(item.layout);
} else {
componentsTree.push(item.componentsTree[0]);
}
});
} else {
if (isPageDataV1(pages[0])) {
componentsTree = [pages[0].layout];
} else {
// if (!pages[0].componentsTree) return;
componentsTree = pages[0].componentsTree;
if (componentsTree[0]) {
componentsTree[0].componentName = componentsTree[0].componentName || 'Page';
// FIXME
if (componentsTree[0].componentName === 'Page' || componentsTree[0].componentName === 'Component') {
componentsTree[0].methods = {};
}
}
}
}
componentsTree.forEach((item: any) => {
item.componentName = item.componentName || 'Page';
if (item.componentName === 'Page' || item.componentName === 'Component') {
item.methods = {};
}
});
project.load(
{
version: '1.0.0',
componentsMap: [],
componentsTree,
id: pages[0].id,
config: project.config,
},
true,
);
// FIXME: 根本原因应该是 propStash 导致的,这样可以避免页面加载之后就被标记为 isModified
setTimeout(() => {
project.currentDocument?.history.savePoint();
}, 0);
},
addPage(data: OldPageData | RootSchema) {
if (isPageDataV1(data)) {
data = data.layout;
} else if (isPageDataV2(data)) {
data = data.componentsTree[0];
}
return project.open(data);
},
getPage(fnOrIndex: ((page: DocumentModel) => boolean) | number) {
if (typeof fnOrIndex === 'number') {
return project.documents[fnOrIndex];
} else if (typeof fnOrIndex === 'function') {
return project.documents.find(fnOrIndex);
}
return null;
},
removePage(page: DocumentModel) {
page.remove();
},
getPages() {
return project.documents;
},
setCurrentPage(page: DocumentModel) {
page.active();
},
getCurrentPage() {
return project.currentDocument;
},
onPagesChange() {
// noop
},
onCurrentPageChange(fn: (page: DocumentModel) => void) {
return project.onCurrentDocumentChange(fn);
},
toData() {
return project.documents.map((doc) => doc.toData());
},
});
Object.defineProperty(pages, 'currentPage', {
get() {
return project.currentDocument;
},
set(_currentPage) {
// do nothing
},
});
pages.onCurrentPageChange((page: DocumentModel) => {
if (!page) {
return;
}
page.acceptRootNodeVisitor('NodeCache', (rootNode) => {
const visitor: NodeCacheVisitor = page.getRootNodeVisitor('NodeCache');
if (visitor) {
visitor.destroy();
}
return new NodeCacheVisitor(page, rootNode);
});
});
export default pages;

View File

@ -0,0 +1,278 @@
import { skeleton, editor } from '@ali/lowcode-engine';
import { ReactElement } from 'react';
import { IWidgetBaseConfig } from '@ali/lowcode-editor-skeleton';
import { uniqueId } from '@ali/lowcode-utils';
import bus from './bus';
export interface IContentItemConfig {
title: string;
content: JSX.Element;
tip?: {
content: string;
url?: string;
};
}
export interface OldPaneConfig {
// 'dock' | 'action' | 'tab' | 'widget' | 'stage'
type?: string; // where
id?: string;
name: string;
title?: string;
content?: any;
place?: string; // align: left|right|top|center|bottom
description?: string; // tip?
tip?:
| string
| {
// as help tip
url?: string;
content?: string | JSX.Element;
}; // help
init?: () => any;
destroy?: () => any;
props?: any;
contents?: IContentItemConfig[];
hideTitleBar?: boolean;
width?: number;
maxWidth?: number;
height?: number;
maxHeight?: number;
position?: string | string[]; // todo
menu?: JSX.Element; // as title
index?: number; // todo
isAction?: boolean; // as normal dock
fullScreen?: boolean; // todo
canSetFixed?: boolean; // 是否可以设置固定模式
defaultFixed?: boolean; // 是否默认固定
}
function upgradeConfig(config: OldPaneConfig): IWidgetBaseConfig & { area: string } {
const { type, id, name, title, content, place, description, init, destroy, props, index } = config;
const newConfig: any = {
id,
name,
content,
props: {
title,
description,
align: place,
},
contentProps: props,
index: index || props?.index,
};
if (type === 'dock') {
newConfig.type = 'PanelDock';
newConfig.area = 'left';
newConfig.props.description = description || title;
const {
contents,
hideTitleBar,
tip,
width,
maxWidth,
height,
maxHeight,
menu,
isAction,
canSetFixed,
defaultFixed,
} = config;
if (menu) {
newConfig.props.title = menu;
}
if (isAction) {
newConfig.type = 'Dock';
} else {
newConfig.panelProps = {
title,
hideTitleBar,
help: tip,
width,
maxWidth,
height,
maxHeight,
canSetFixed,
};
if (defaultFixed) {
newConfig.panelProps.area = 'leftFixedArea';
}
if (contents && Array.isArray(contents)) {
newConfig.content = contents.map(({ title, content, tip }, index) => {
return {
type: 'Panel',
name: typeof title === 'string' ? title : `${name}:${index}`,
content,
contentProps: props,
props: {
title,
help: tip,
},
};
});
}
}
} else if (type === 'action') {
newConfig.area = 'top';
newConfig.type = 'Dock';
} else if (type === 'tab') {
newConfig.area = 'right';
newConfig.type = 'Panel';
} else if (type === 'stage') {
newConfig.area = 'stages';
newConfig.type = 'Widget';
} else {
newConfig.area = 'main';
newConfig.type = 'Widget';
}
newConfig.props.onInit = init;
newConfig.props.onDestroy = destroy;
return newConfig;
}
function add(config: (() => OldPaneConfig) | OldPaneConfig, extraConfig?: any) {
if (typeof config === 'function') {
config = config.call(null);
}
if (!config || !config.type) {
return null;
}
if (extraConfig) {
config = { ...config, ...extraConfig };
}
const upgraded = upgradeConfig(config);
if (upgraded.area === 'stages') {
if (upgraded.id) {
upgraded.name = upgraded.id;
} else if (!upgraded.name) {
upgraded.name = uniqueId('stage');
}
const stage = skeleton.add(upgraded);
return stage?.getName();
} else {
return skeleton.add(upgraded);
}
}
const actionPane = Object.assign(skeleton.topArea, {
/**
* compatible *VE.actionPane.getActions*
*/
getActions(): any {
return skeleton.topArea.container.items;
},
/**
* compatible *VE.actionPane.activeDock*
*/
setActions() {
// empty
},
get actions() {
return skeleton.topArea.container.items;
},
});
const dockPane = Object.assign(skeleton.leftArea, {
/**
* compatible *VE.dockPane.activeDock*
*/
activeDock(item: any) {
if (!item) {
skeleton.leftFloatArea?.current?.hide();
return;
}
const name = item.name || item;
const pane = skeleton.getPanel(name);
if (!pane) {
console.warn(`Could not find pane with name ${name}`);
}
pane?.active();
bus.emit('ve.dock_pane.active_doc', pane);
},
/**
* compatible *VE.dockPane.onDockShow*
*/
onDockShow(fn: (dock: any) => void): () => void {
const f = (_: any, dock: any) => {
fn(dock);
};
editor.on('skeleton.panel-dock.active', f);
return () => {
editor.removeListener('skeleton.panel-dock.active', f);
};
},
/**
* compatible *VE.dockPane.onDockHide*
*/
onDockHide(fn: (dock: any) => void): () => void {
const f = (_: any, dock: any) => {
fn(dock);
};
editor.on('skeleton.panel-dock.unactive', f);
return () => {
editor.removeListener('skeleton.panel-dock.unactive', f);
};
},
/**
* compatible *VE.dockPane.setFixed*
*/
setFixed(flag: boolean) {
// todo:
},
getDocks() {
return skeleton.leftFloatArea?.container.items;
},
});
const tabPane = Object.assign(skeleton.rightArea, {
setFloat(flag: boolean) {
// todo:
},
});
const toolbar = Object.assign(skeleton.toolbar, {
setContents(contents: ReactElement) {
// todo:
},
});
const widgets = skeleton.mainArea;
const stages = Object.assign(skeleton.stages, {
getStage(name: string) {
return skeleton.stages.container.get(name);
},
createStage(config: any) {
config = upgradeConfig(config);
if (config.id) {
config.name = config.id;
} else if (!config.name) {
config.name = uniqueId('stage');
}
const stage = skeleton.stages.add(config);
return stage.getName();
},
});
export default {
ActionPane: actionPane, // topArea
actionPane, //
DockPane: dockPane, // leftArea
dockPane,
TabPane: tabPane, // rightArea
tabPane,
add,
toolbar, // toolbar
Stages: stages,
Widgets: widgets, // centerArea
widgets,
};

View File

@ -0,0 +1,19 @@
import { designer } from '@ali/lowcode-engine';
const { project } = designer;
Object.assign(project, {
getSchema(): any {
return this.schema || {};
},
setSchema(schema: any) {
this.schema = schema;
},
setConfig(config: any) {
this.set('config', config);
},
});
export default project;

View File

@ -0,0 +1,630 @@
import { Component } from 'react';
import { EventEmitter } from 'events';
import { fromJS, Iterable, Map as IMMap } from 'immutable';
import logger from '@ali/vu-logger';
import { cloneDeep, isDataEqual, combineInitial, Transducer } from '@ali/ve-utils';
import I18nUtil from '@ali/ve-i18n-util';
import { editor, setters } from '@ali/lowcode-engine';
import { OldPropConfig, DISPLAY_TYPE } from './bundle/upgrade-metadata';
import { uniqueId } from '@ali/lowcode-utils';
const { getSetter } = setters;
type IPropConfig = OldPropConfig;
// 1: chain -1: start 0: discard
const CHAIN_START = -1;
const CHAIN_HAS_REACH = 0;
export enum PROP_VALUE_CHANGED_TYPE {
/**
* normal set value
*/
SET_VALUE = 'SET_VALUE',
/**
* value changed caused by sub-prop value change
*/
SUB_VALUE_CHANGE = 'SUB_VALUE_CHANGE',
}
/**
* Dynamic setter will use 've.plugin.setterProvider' to
* calculate setter type in runtime
*/
let dynamicSetterProvider: any;
export interface IHotDataMap extends IMMap<string, any> {
value: any;
hotValue: any;
}
export interface ISetValueOptions {
disableMutator?: boolean;
type?: PROP_VALUE_CHANGED_TYPE;
}
export interface IVariableSettable {
useVariable?: boolean;
variableValue: string;
isUseVariable: () => boolean;
isSupportVariable: () => boolean;
setVariableValue: (value: string) => void;
setUseVariable: (flag?: boolean) => void;
getVariableValue: () => string;
onUseVariableChange: (func: (data: { isUseVariable: boolean }) => any) => void;
}
export default class Prop implements IVariableSettable {
/**
* Setters predefined as default options
* can by selected by user for every prop
*
* @static
* @memberof Prop
*/
public static INSET_SETTER = {};
public id: string;
public emitter: EventEmitter;
public inited: boolean;
public i18nLink: any;
public loopLock: boolean;
public props: any;
public parent: any;
public config: IPropConfig;
public initial: any;
public initialData: any;
public expanded: boolean;
public useVariable?: boolean;
/**
* value to be saved in schema it is usually JSON serialized
* prototype.js can config Transducer.toNative to generate value
*/
public value: any;
/**
* value to be used in VisualDesigner more flexible
* prototype.js can config Transducer.toHot to generate hotValue
*/
public hotValue: any;
/**
*
*/
public variableValue: string;
public hotData: IMMap<string, IHotDataMap>;
public defaultValue: any;
public transducer: any;
public inGroup: boolean;
constructor(parent: any, config: IPropConfig, data?: any) {
if (parent.isProps) {
this.props = parent;
this.parent = null;
} else {
this.props = parent.getProps();
this.parent = parent;
}
this.id = uniqueId('prop');
if (typeof config.setter === 'string') {
config.setter = getSetter(config.setter)?.component as any;
}
this.config = config;
this.emitter = new EventEmitter();
this.emitter.setMaxListeners(100);
this.initialData = data;
this.useVariable = false;
dynamicSetterProvider = editor.get('ve.plugin.setterProvider');
this.beforeInit();
}
public getId() {
return this.id;
}
public isTab() {
return this.getDisplay() === 'tab';
}
public isGroup() {
return false;
}
public beforeInit() {
if (IMMap.isMap(this.initialData)) {
this.value = this.initialData.get('value');
if (this.value && typeof this.value.toJS === 'function') {
this.value = this.value.toJS();
}
this.hotData = this.initialData;
} else {
this.value = this.initialData;
}
this.resolveValue();
let defaultValue = null;
if (this.config.defaultValue !== undefined) {
defaultValue = this.config.defaultValue;
} else if (typeof this.config.initialValue !== 'function') {
defaultValue = this.config.initialValue;
}
this.defaultValue = defaultValue;
this.transducer = new Transducer(this, this.config);
this.initial = combineInitial(this, this.config);
}
public resolveValue() {
if (this.value && this.value.type === 'variable') {
const { value, variable } = this.value;
this.value = value;
this.variableValue = variable;
this.useVariable = this.isSupportVariable();
} else {
this.useVariable = false;
}
}
public init(defaultValue?: any) {
if (this.inited) { return; }
this.value = this.initial(this.value,
this.defaultValue != null ? this.defaultValue : defaultValue);
if (this.hotData) {
const tempVal = this.hotData.get('value');
// if we create a prop from runtime data, we don't need initial() or set with defaultValue process
// but if we got an empty value, we fill with the initial() process and default value
if (Iterable.isIterable(tempVal)) {
this.value = tempVal.toJS() || this.value;
} else {
this.value = tempVal || this.value;
}
this.resolveValue();
}
this.i18nLink = I18nUtil.attach(this, this.value,
((val: any) => { this.setValue(val, false, true); }) as any);
// call config.accessor
const value = this.getValue();
if (this.hotData) {
this.hotValue = this.hotData.get('hotValue');
if (this.hotValue && Iterable.isIterable(this.hotValue)) {
this.hotValue = this.hotValue.toJS();
}
} else {
try {
this.hotValue = this.transducer.toHot(value);
} catch (e) {
logger.log('ERROR_PROP_VALUE');
logger.warn('属性初始化错误:', this);
}
this.hotData = fromJS({
hotValue: this.hotValue,
value: this.getMixValue(value),
});
}
this.inited = true;
}
public isInited() {
return this.inited;
}
public getHotData() {
return this.hotData;
}
public getProps() {
return this.props;
}
public getNode(): any {
return this.getProps().getNode();
}
/**
*
*
* @returns {string}
*/
public getName(): string {
const ns = this.parent ? `${this.parent.getName()}.` : '';
return ns + this.config.name;
}
public getKey() {
return this.config.name;
}
/**
*
*
* @returns {string}
*/
public getTitle() {
return this.config.title || this.getName();
}
public getTip() {
return this.config.tip || null;
}
public getValue(disableCache?: boolean, options?: {
disableAccessor?: boolean;
}) {
const { accessor } = this.config;
if (accessor && (!options || !options.disableAccessor)) {
const value = accessor.call(this as any, this.value);
if (!disableCache) {
this.value = value;
}
return value;
}
return this.value;
}
public getMixValue(value?: any) {
if (value == null) {
value = this.getValue();
}
if (this.isUseVariable()) {
value = {
type: 'variable',
value,
variable: this.getVariableValue(),
};
}
return value;
}
public toData() {
return cloneDeep(this.getMixValue());
}
public getDefaultValue() {
return this.defaultValue;
}
public getHotValue() {
return this.hotValue;
}
public getConfig<K extends keyof IPropConfig>(configName?: K): IPropConfig[K] | IPropConfig {
if (configName) {
return this.config[configName];
}
return this.config;
}
public sync() {
if (this.props.hasReach(this)) {
return;
}
const { sync } = this.config;
if (sync) {
const value = sync.call(this as any, this.getValue(true));
if (value !== undefined) {
this.setValue(value);
}
} else {
// sync 的时候不再需要调用经过 accessor 处理之后的值了
// 这里之所以需要 setValue 是为了过 getValue() 中的 accessor 修饰函数
this.setValue(this.getValue(true), false, false, {
disableMutator: true,
});
}
}
public isUseVariable() {
return this.useVariable || false;
}
public isSupportVariable() {
return this.config.supportVariable || false;
}
public setVariableValue(value: string) {
if (!this.isUseVariable()) { return; }
const state = this.props.chainReach(this);
if (state === CHAIN_HAS_REACH) {
return;
}
this.variableValue = value;
if (this.modify()) {
this.valueChange();
this.props.syncPass(this);
}
if (state === CHAIN_START) {
this.props.endChain();
}
}
public setUseVariable(flag = false) {
if (this.useVariable === flag) { return; }
const state = this.props.chainReach(this);
if (state === CHAIN_HAS_REACH) {
return;
}
this.useVariable = flag;
this.expanded = true;
if (this.modify()) {
this.valueChange();
this.props.syncPass(this);
}
if (state === CHAIN_START) {
this.props.endChain();
}
this.emitter.emit('ve.prop.useVariableChange', { isUseVariable: flag });
if (this.config.useVariableChange) {
this.config.useVariableChange.call(this as any, { isUseVariable: flag });
}
}
public getVariableValue() {
return this.variableValue;
}
/**
* @param value
* @param isHotValue
* @param force
*/
public setValue(value: any, isHotValue?: boolean, force?: boolean, extraOptions?: ISetValueOptions) {
const state = this.props.chainReach(this);
if (state === CHAIN_HAS_REACH) {
return;
}
const preValue = this.value;
const preHotValue = this.hotValue;
if (isHotValue) {
this.hotValue = value;
this.value = this.transducer.toNative(this.hotValue);
} else {
if (!isDataEqual(value, this.value)) {
this.hotValue = this.transducer.toHot(value);
}
this.value = value;
}
this.i18nLink = I18nUtil.attach(this, this.value, ((val: any) => this.setValue(val, false, true)) as any);
const { mutator } = this.config;
if (!extraOptions) {
extraOptions = {};
}
if (mutator && !extraOptions.disableMutator) {
mutator.call(this as any, this.value);
}
if (this.modify(force)) {
this.valueChange(extraOptions);
this.props.syncPass(this);
}
if (state === CHAIN_START) {
this.props.endChain();
}
}
public setHotValue(hotValue: any, options?: ISetValueOptions) {
try {
this.setValue(hotValue, true, false, options);
} catch (e) {
logger.log('ERROR_PROP_VALUE');
logger.warn('属性值设置错误:', e, hotValue);
}
}
/**
*
* @param force
*/
public modify(force?: boolean) {
const hotData = this.hotData.merge(fromJS({
hotValue: this.getHotValue(),
value: this.getMixValue(),
}));
if (!force && hotData.equals(this.hotData)) {
return false;
}
this.hotData = hotData;
(this.parent || this.props).modify(this.getName());
return true;
}
public setHotData(hotData: IMMap<string, IHotDataMap>, options?: ISetValueOptions) {
if (!IMMap.isMap(hotData)) {
return;
}
this.hotData = hotData;
let value = hotData.get('value');
if (value && typeof value.toJS === 'function') {
value = value.toJS();
}
let hotValue = hotData.get('hotValue');
if (hotValue && typeof hotValue.toJS === 'function') {
hotValue = hotValue.toJS();
}
const preValue = value;
const preHotValue = hotValue;
this.value = value;
this.hotValue = hotValue;
this.resolveValue();
if (!options || !options.disableMutator) {
const { mutator } = this.config;
if (mutator) {
mutator.call(this as any, value);
}
}
this.valueChange();
}
public valueChange(options?: ISetValueOptions) {
if (this.loopLock) { return; }
this.emitter.emit('valuechange', options);
if (this.parent) {
this.parent.valueChange(options);
}
}
public getDisplay() {
return this.config.display || this.config.fieldStyle || 'block';
}
public isHidden() {
if (!this.isInited() || this.getDisplay() === DISPLAY_TYPE.NONE || this.isDisabled()) {
return true;
}
let { hidden } = this.config;
if (typeof hidden === 'function') {
hidden = hidden.call(this as any, this.getValue());
}
return hidden === true;
}
public isDisabled() {
let { disabled } = this.config;
if (typeof disabled === 'function') {
disabled = disabled.call(this as any, this.getValue());
}
return disabled === true;
}
public isIgnore() {
if (this.isDisabled()) { return true; }
let { ignore } = this.config;
if (typeof ignore === 'function') {
ignore = ignore.call(this as any, this.getValue());
}
return ignore === true;
}
public isExpand() {
if (this.expanded == null) {
this.expanded = !(this.config.collapsed || this.config.fieldCollapsed);
}
return this.expanded;
}
public toggleExpand() {
if (this.expanded) {
this.expanded = false;
} else {
this.expanded = true;
}
this.emitter.emit('expandchange', this.expanded);
}
public getSetter() {
if (dynamicSetterProvider) {
const setter = dynamicSetterProvider.call(this, this, this.getNode().getPrototype());
if (setter) {
return setter;
}
}
const setterConfig = this.config.setter;
if (typeof setterConfig === 'function' && !(setterConfig.prototype instanceof Component)) {
return (setterConfig as any).call(this, this.getValue());
}
if (Array.isArray(setterConfig)) {
let item;
for (item of setterConfig) {
if (item.condition?.call(this, this.getValue())) {
return item.setter;
}
}
return setterConfig[0].setter;
}
return setterConfig;
}
public getSetterData(): any {
if (Array.isArray(this.config.setter)) {
let item;
for (item of this.config.setter) {
if (item.condition?.call(this, this.getValue())) {
return item;
}
}
return this.config.setter[0];
}
return { };
}
public destroy() {
if (this.i18nLink) {
this.i18nLink.detach();
}
}
public onValueChange(func: () => any) {
this.emitter.on('valuechange', func);
return () => {
this.emitter.removeListener('valuechange', func);
};
}
public onExpandChange(func: () => any) {
this.emitter.on('expandchange', func);
return () => {
this.emitter.removeListener('expandchange', func);
};
}
public onUseVariableChange(func: (data: { isUseVariable: boolean }) => any) {
this.emitter.on('ve.prop.useVariableChange', func);
return () => {
this.emitter.removeListener('ve.prop.useVariableChange', func);
};
}
}

View File

@ -0,0 +1,36 @@
import {
isPlainObject,
} from '@ali/lowcode-utils';
import { isJSExpression, isJSSlot } from '@ali/lowcode-types';
export function compatibleReducer(props: any) {
if (!props || !isPlainObject(props)) {
return props;
}
// 为了能降级到老版本,建议在后期版本去掉以下代码
if (isJSSlot(props)) {
return {
type: 'JSBlock',
value: {
componentName: 'Slot',
children: props.value,
props: {
slotTitle: props.title,
slotName: props.name,
},
},
};
}
if (isJSExpression(props) && !props.events) {
return {
type: 'variable',
value: props.mock,
variable: props.value,
};
}
const newProps: any = {};
Object.entries<any>(props).forEach(([key, val]) => {
newProps[key] = compatibleReducer(val);
});
return newProps;
}

View File

@ -0,0 +1,25 @@
import logger from '@ali/vu-logger';
import { hasOwnProperty } from '@ali/lowcode-utils';
export function filterReducer(props: any, node: Node): any {
const filters = node.componentMeta.getMetadata().experimental?.filters;
if (filters && filters.length) {
const newProps = { ...props };
filters.forEach((item) => {
// FIXME! item.name could be 'xxx.xxx'
if (!hasOwnProperty(newProps, item.name)) {
return;
}
try {
if (item.filter(node.settingEntry.getProp(item.name), props[item.name]) === false) {
delete newProps[item.name];
}
} catch (e) {
console.warn(e);
logger.trace(e);
}
});
return newProps;
}
return props;
}

View File

@ -0,0 +1,8 @@
export * from './downgrade-schema-reducer';
export * from './filter-reducer';
export * from './init-node-reducer';
export * from './live-lifecycle-reducer';
export * from './remove-empty-prop-reducer';
export * from './style-reducer';
export * from './upgrade-reducer';
export * from './node-top-fixed-reducer';

View File

@ -0,0 +1,68 @@
import {
hasOwnProperty,
isI18NObject,
isUseI18NSetter,
convertToI18NObject,
isString,
} from '@ali/lowcode-utils';
import { isJSExpression, isJSBlock, isJSSlot } from '@ali/lowcode-types';
import { isVariable, getCurrentFieldIds } from '../utils';
export function initNodeReducer(props, node) {
// run initials
const newProps: any = {
...props,
};
if (newProps.fieldId) {
const fieldIds = getCurrentFieldIds();
// 全局的关闭 uniqueIdChecker 信号,在 ve-utils 中实现
if (fieldIds.indexOf(props.fieldId) >= 0 && !(window as any).__disable_unique_id_checker__) {
newProps.fieldId = undefined;
}
}
const initials = node.componentMeta.getMetadata().experimental?.initials;
if (initials) {
const getRealValue = (propValue: any) => {
if (isVariable(propValue)) {
return propValue.value;
}
if (isJSExpression(propValue)) {
return propValue.mock;
}
return propValue;
};
initials.forEach(item => {
// FIXME! this implements SettingTarget
try {
// FIXME! item.name could be 'xxx.xxx'
const ov = newProps[item.name];
const v = item.initial(node as any, getRealValue(ov));
if (ov === undefined && v !== undefined) {
newProps[item.name] = v;
}
// 兼容 props 中的属性为 i18n 类型,但是仅提供了一个字符串值,非变量绑定
if (
isUseI18NSetter(node.componentMeta.prototype, item.name) &&
!isI18NObject(ov) &&
!isJSExpression(ov) &&
!isJSBlock(ov) &&
!isJSSlot(ov) &&
!isVariable(ov) &&
(isString(v) || isI18NObject(v))
) {
newProps[item.name] = convertToI18NObject(v);
}
} catch (e) {
if (hasOwnProperty(props, item.name)) {
newProps[item.name] = props[item.name];
}
}
if (newProps[item.name] && !node.props.has(item.name)) {
node.props.add(newProps[item.name], item.name, false, { skipSetSlot: true });
}
});
}
return newProps;
}

View File

@ -0,0 +1,26 @@
import { editor } from '@ali/lowcode-engine';
import { Node } from '@ali/lowcode-designer';
export function liveLifecycleReducer(props: any, node: Node) {
// live 模式下解析 lifeCycles
if (node.isRoot() && props && props.lifeCycles) {
if (editor.get('designMode') === 'live') {
const lifeCycleMap = {
didMount: 'componentDidMount',
willUnmount: 'componentWillUnMount',
};
const lifeCycles = props.lifeCycles;
Object.keys(lifeCycleMap).forEach(key => {
if (lifeCycles[key]) {
lifeCycles[lifeCycleMap[key]] = lifeCycles[key];
}
});
return props;
}
return {
...props,
lifeCycles: {},
};
}
return props;
}

View File

@ -0,0 +1,12 @@
import { Node } from '@ali/lowcode-designer';
export function nodeTopFixedReducer(props: any, node: Node) {
if (node.componentMeta.isTopFixed) {
return {
...props,
// experimental prop value
__isTopFixed__: true,
};
}
return props;
}

View File

@ -0,0 +1,23 @@
import {
cloneDeep,
} from '@ali/lowcode-utils';
// 清除空的 props value
export function removeEmptyPropsReducer(props: any, node: Node) {
if (node.isRoot() && props.dataSource && Array.isArray(props.dataSource.online)) {
const online = cloneDeep(props.dataSource.online);
online.forEach((item: any) => {
const newParam: any = {};
if (Array.isArray(item?.options?.params)) {
item.options.params.forEach((element: any) => {
if (element.name) {
newParam[element.name] = element.value;
}
});
item.options.params = newParam;
}
});
props.dataSource.list = online;
}
return props;
}

View File

@ -0,0 +1,48 @@
import { editor, designer } from '@ali/lowcode-engine';
import { toCss } from '@ali/vu-css-style';
export function stylePropsReducer(props: any, node: any) {
if (props && typeof props === 'object' && props.__style__) {
const cssId = `_style_pesudo_${ node.id.replace(/\$/g, '_')}`;
const cssClass = `_css_pesudo_${ node.id.replace(/\$/g, '_')}`;
const styleProp = props.__style__;
appendStyleNode(props, styleProp, cssClass, cssId);
}
if (props && typeof props === 'object' && props.pageStyle) {
const cssId = '_style_pesudo_engine-document';
const cssClass = 'engine-document';
const styleProp = props.pageStyle;
appendStyleNode(props, styleProp, cssClass, cssId);
}
if (props && typeof props === 'object' && props.containerStyle) {
const cssId = `_style_pesudo_${ node.id}`;
const cssClass = `_css_pesudo_${ node.id.replace(/\$/g, '_')}`;
const styleProp = props.containerStyle;
appendStyleNode(props, styleProp, cssClass, cssId);
}
return props;
}
function appendStyleNode(props: any, styleProp: any, cssClass: string, cssId: string) {
const doc = designer.currentDocument?.simulator?.contentDocument;
if (!doc) {
return;
}
const dom = doc.getElementById(cssId);
if (dom) {
dom.parentNode?.removeChild(dom);
}
if (typeof styleProp === 'object') {
styleProp = toCss(styleProp);
}
if (typeof styleProp === 'string') {
const s = doc.createElement('style');
props.className = cssClass;
s.setAttribute('type', 'text/css');
s.setAttribute('id', cssId);
doc.getElementsByTagName('head')[0].appendChild(s);
s.appendChild(doc.createTextNode(styleProp.replace(/(\d+)rpx/g, (a, b) => {
return `${b / 2}px`;
}).replace(/:root/g, `.${cssClass}`)));
}
}

View File

@ -0,0 +1,55 @@
import {
isPlainObject,
} from '@ali/lowcode-utils';
import { isJSBlock } from '@ali/lowcode-types';
import { isVariable } from '../utils';
import { designerHelper } from '@ali/lowcode-engine';
const { getConvertedExtraKey } = designerHelper;
export function upgradePropsReducer(props: 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;
}
export function upgradePageLifeCyclesReducer(props: any, node: Node) {
const lifeCycleNames = ['didMount', 'willUnmount'];
if (node.isRoot()) {
lifeCycleNames.forEach(key => {
if (props[key]) {
const lifeCycles = node.props.getPropValue(getConvertedExtraKey('lifeCycles')) || {};
lifeCycles[key] = props[key];
node.props.setPropValue(getConvertedExtraKey('lifeCycles'), lifeCycles);
}
});
}
return props;
}

View File

@ -0,0 +1,58 @@
import { isJSBlock, isJSExpression, isJSSlot } from '@ali/lowcode-types';
import { isPlainObject, hasOwnProperty, cloneDeep, isI18NObject, isUseI18NSetter, convertToI18NObject, isString } from '@ali/lowcode-utils';
import { editor, designer, designerHelper } from '@ali/lowcode-engine';
import bus from './bus';
import { VE_EVENTS } from './base/const';
import { deepValueParser } from './deep-value-parser';
import { liveEditingRule, liveEditingSaveHander } from './vc-live-editing';
import {
compatibleReducer,
upgradePageLifeCyclesReducer,
stylePropsReducer,
upgradePropsReducer,
filterReducer,
removeEmptyPropsReducer,
initNodeReducer,
liveLifecycleReducer,
nodeTopFixedReducer,
} from './props-reducers';
const { LiveEditing, TransformStage } = designerHelper;
LiveEditing.addLiveEditingSpecificRule(liveEditingRule);
LiveEditing.addLiveEditingSaveHandler(liveEditingSaveHander);
designer.project.onCurrentDocumentChange((doc) => {
bus.emit(VE_EVENTS.VE_PAGE_PAGE_READY);
editor.set('currentDocument', doc);
});
// 升级 Props
designer.addPropsReducer(upgradePropsReducer, TransformStage.Upgrade);
// 节点 props 初始化
designer.addPropsReducer(initNodeReducer, TransformStage.Init);
designer.addPropsReducer(liveLifecycleReducer, TransformStage.Render);
designer.addPropsReducer(filterReducer, TransformStage.Save);
designer.addPropsReducer(filterReducer, TransformStage.Render);
// FIXME: Dirty fix, will remove this reducer
designer.addPropsReducer(compatibleReducer, TransformStage.Save);
// 兼容历史版本的 Page 组件
designer.addPropsReducer(upgradePageLifeCyclesReducer, TransformStage.Save);
// 设计器组件样式处理
designer.addPropsReducer(stylePropsReducer, TransformStage.Render);
// 国际化 & Expression 渲染时处理
designer.addPropsReducer(deepValueParser, TransformStage.Render);
// Init 的时候没有拿到 dataSource, 只能在 Render 和 Save 的时候都调用一次,理论上执行时机在 Init
// Render 和 Save 都要各调用一次,感觉也是有问题的,是不是应该在 Render 执行一次就行了?见上 filterReducer 也是一样的处理方式。
designer.addPropsReducer(removeEmptyPropsReducer, TransformStage.Render);
designer.addPropsReducer(removeEmptyPropsReducer, TransformStage.Save);
designer.addPropsReducer(nodeTopFixedReducer, TransformStage.Render);
designer.addPropsReducer(nodeTopFixedReducer, TransformStage.Save);

View File

@ -0,0 +1,95 @@
import { findIndex } from 'lodash';
import { DocumentModel, Node, RootNode } from '@ali/lowcode-designer';
/**
* RootNodeVisitor for VisualEngine Page
*
* - store / cache node
* - quickly find / search or do operations on Node
*/
export default class RootNodeVisitor {
public nodeIdMap: {[id: string]: Node} = {};
public nodeFieldIdMap: {[fieldId: string]: Node} = {};
public nodeList: Node[] = [];
private page: DocumentModel;
private root: RootNode;
private cancelers: Function[] = [];
constructor(page: DocumentModel, rootNode: RootNode) {
this.page = page;
this.root = rootNode;
this._findNode(this.root);
this._init();
}
public getNodeList() {
return this.nodeList;
}
public getNodeIdMap() {
return this.nodeIdMap;
}
public getNodeFieldIdMap() {
return this.nodeFieldIdMap;
}
public getNodeById(id?: string) {
if (!id) { return this.nodeIdMap; }
return this.nodeIdMap[id];
}
public getNodeByFieldId(fieldId?: string) {
if (!fieldId) { return this.nodeFieldIdMap; }
return this.nodeFieldIdMap[fieldId];
}
public destroy() {
this.cancelers.forEach((canceler) => canceler());
}
private _init() {
this.cancelers.push(
this.page.onNodeCreate((node) => {
this.nodeList.push(node);
this.nodeIdMap[node.id] = node;
if (node.getPropValue('fieldId')) {
this.nodeFieldIdMap[node.getPropValue('fieldId')] = node;
}
}),
);
this.cancelers.push(
this.page.onNodeDestroy((node) => {
const idx = findIndex(this.nodeList, (n) => node.id === n.id);
this.nodeList.splice(idx, 1);
delete this.nodeIdMap[node.id];
if (node.getPropValue('fieldId')) {
delete this.nodeFieldIdMap[node.getPropValue('fieldId')];
}
}),
);
}
private _findNode(node: Node) {
const props = node.getProps();
const fieldId = props && props.getPropValue('fieldId');
this.nodeIdMap[node.getId()] = node;
this.nodeList.push(node);
if (fieldId) {
this.nodeFieldIdMap[fieldId] = node;
}
const children = node.getChildren();
if (children) {
children.forEach((child) => this._findNode(child));
}
}
}

View File

@ -0,0 +1,17 @@
export class SymbolManager {
private symbolMap: { [symbolName: string]: symbol } = {};
public create(name: string): symbol {
if (this.symbolMap[name]) {
return this.symbolMap[name];
}
this.symbolMap[name] = Symbol(name);
return this.symbolMap[name];
}
public get(name: string) {
return this.symbolMap[name];
}
}
export default new SymbolManager();

View File

@ -0,0 +1,30 @@
import { designer } from '@ali/lowcode-engine';
interface Variable {
type: 'variable';
variable: string;
value: any;
}
export function isVariable(obj: any): obj is Variable {
return obj && obj.type === 'variable';
}
export function getCurrentFieldIds() {
const fieldIds: any = [];
const nodesMap = designer?.currentDocument?.nodesMap || new Map();
nodesMap.forEach((curNode: any) => {
const fieldId = nodesMap?.get(curNode.id)?.getPropValue('fieldId');
if (fieldId) {
fieldIds.push(fieldId);
}
});
return fieldIds;
}
export function invariant(check: any, message: string, thing?: any) {
if (!check) {
throw new Error('Invariant failed: ' + message + (thing ? ` in '${thing}'` : ''));
}
}

View File

@ -0,0 +1,116 @@
import { EditingTarget, Node as DocNode, SaveHandler } from '@ali/lowcode-designer';
import Env from './env';
import { isJSExpression, isI18nData } from '@ali/lowcode-types';
import i18nUtil from './i18n-util';
interface I18nObject {
type?: string;
use?: string;
key?: string;
[lang: string]: string | undefined;
}
function getI18nText(obj: I18nObject) {
let locale = Env.getLocale();
if (obj.key) {
return i18nUtil.get(obj.key, locale);
}
if (locale !== 'zh_CN' && locale !== 'zh_TW' && !obj[locale]) {
locale = 'en_US';
}
return obj[obj.use || locale] || obj.zh_CN;
}
function getText(node: DocNode, prop: string) {
const p = node.getProp(prop, false);
if (!p || p.isUnset()) {
return null;
}
let v = p.getValue();
if (isJSExpression(v)) {
v = v.mock;
}
if (v == null) {
return null;
}
if (p.type === 'literal') {
return v;
}
if ((v as any).type === 'i18n') {
return getI18nText(v as any);
}
return Symbol.for('not-literal');
}
export function liveEditingRule(target: EditingTarget) {
// for vision components specific
const { node, rootElement, event } = target;
const targetElement = event.target as HTMLElement;
if (!Array.from(targetElement.childNodes).every(item => item.nodeType === Node.TEXT_NODE)) {
return null;
}
const { innerText } = targetElement;
const propTarget = ['title', 'label', 'text', 'content'].find(prop => {
return equalText(getText(node, prop), innerText);
});
if (propTarget) {
return {
propElement: targetElement,
propTarget,
};
}
return null;
}
function equalText(v: any, innerText: string) {
// TODO: enhance compare text logic
if (typeof v !== 'string') {
return false;
}
return v.trim() === innerText;
}
export const liveEditingSaveHander: SaveHandler = {
condition: (prop) => {
const v = prop.getValue();
return prop.type === 'expression' || isI18nData(v);
},
onSaveContent: (content, prop) => {
const v = prop.getValue();
const locale = Env.getLocale();
let data = v;
if (isJSExpression(v)) {
data = v.mock;
}
if (isI18nData(data)) {
const i18n = data.key ? i18nUtil.getItem(data.key) : null;
if (i18n) {
i18n.setDoc(content, locale);
return;
}
data = {
...(data as any),
[locale]: content,
};
} else {
data = content;
}
if (isJSExpression(v)) {
prop.setValue({
type: 'JSExpression',
value: v.value,
mock: data,
});
} else {
prop.setValue(data);
}
},
};
// TODO:
// 非文本编辑
// 国际化数据,改变当前
// JSExpression, 改变 mock 或 弹出绑定变量

View File

@ -0,0 +1,289 @@
import { EventEmitter } from 'events';
import Flags from './flags';
import { editor } from '@ali/lowcode-engine';
const domReady = require('domready');
function enterFullscreen() {
const elem = document.documentElement;
if (elem.requestFullscreen) {
elem.requestFullscreen();
}
}
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
function isFullscreen() {
return document.fullscreen || false;
}
interface IStyleResourceConfig {
media?: string;
type?: string;
content?: string;
}
class StyleResource {
config: IStyleResourceConfig;
styleElement: HTMLStyleElement;
mounted: boolean;
inited: boolean;
constructor(config: IStyleResourceConfig) {
this.config = config || {};
}
matchDevice(device: string) {
const { media } = this.config;
if (!media || media === 'ALL' || media === '*') {
return true;
}
return media.toUpperCase() === device.toUpperCase();
}
init() {
if (this.inited) {
return;
}
this.inited = true;
const { type, content } = this.config;
let styleElement: any;
if (type === 'URL') {
styleElement = document.createElement('link');
styleElement.href = content || '';
styleElement.rel = 'stylesheet';
} else {
styleElement = document.createElement('style');
styleElement.setAttribute('type', 'text/css');
if (styleElement.styleSheet) {
styleElement.styleSheet.cssText = content;
} else {
styleElement.appendChild(document.createTextNode(content || ''));
}
}
this.styleElement = styleElement;
}
apply() {
if (this.mounted) {
return;
}
this.init();
document.head.appendChild(this.styleElement);
this.mounted = true;
}
unmount() {
if (!this.mounted) {
return;
}
document.head.removeChild(this.styleElement);
this.mounted = false;
}
}
export class Viewport {
preview: boolean;
focused: boolean;
slateFixed: boolean;
emitter: EventEmitter;
device: string;
focusTarget: any;
cssResourceSet: StyleResource[];
constructor() {
this.preview = false;
this.emitter = new EventEmitter();
document.addEventListener('webkitfullscreenchange', () => {
this.emitter.emit('fullscreenchange', this.isFullscreen());
});
domReady(() => this.applyMediaCSS());
}
setFullscreen(flag: boolean) {
const fullscreen = this.isFullscreen();
if (fullscreen && !flag) {
exitFullscreen();
} else if (!fullscreen && flag) {
enterFullscreen();
}
}
toggleFullscreen() {
if (this.isFullscreen()) {
exitFullscreen();
} else {
enterFullscreen();
}
}
isFullscreen() {
return isFullscreen();
}
setFocus(flag: boolean) {
if (this.focused && !flag) {
this.focused = false;
Flags.remove('view-focused');
this.emitter.emit('focuschange', false);
} else if (!this.focused && flag) {
this.focused = true;
Flags.add('view-focused');
this.emitter.emit('focuschange', true);
}
}
setFocusTarget(focusTarget: any) {
this.focusTarget = focusTarget;
}
returnFocus() {
if (this.focusTarget) {
this.focusTarget.focus();
}
}
isFocus() {
return this.focused;
}
setPreview(flag: boolean) {
if (this.preview && !flag) {
this.preview = false;
Flags.setPreviewMode(false);
this.emitter.emit('preview', false);
this.changeViewport();
} else if (!this.preview && flag) {
this.preview = true;
Flags.setPreviewMode(true);
this.emitter.emit('preview', true);
this.changeViewport();
}
}
togglePreview() {
if (this.isPreview()) {
this.setPreview(false);
} else {
this.setPreview(true);
}
}
isPreview() {
return this.preview;
}
async setDevice(device = 'pc') {
if (this.getDevice() !== device) {
this.device = device;
const simulator = await editor.onceGot('simulator');
simulator?.set('device', device === 'mobile' ? 'mobile' : 'default');
// Flags.setSimulator(device);
// this.applyMediaCSS();
this.emitter.emit('devicechange', device);
this.changeViewport();
}
}
getDevice() {
return this.device || 'pc';
}
changeViewport() {
this.emitter.emit('viewportchange', this.getViewport());
}
getViewport() {
return `${this.isPreview() ? 'preview' : 'design'}-${this.getDevice()}`;
}
applyMediaCSS() {
if (!document.head || !this.cssResourceSet) {
return;
}
const device = this.getDevice();
this.cssResourceSet.forEach((item) => {
if (item.matchDevice(device)) {
item.apply();
} else {
item.unmount();
}
});
}
setGlobalCSS(resourceSet: IStyleResourceConfig[]) {
if (this.cssResourceSet) {
this.cssResourceSet.forEach((item) => {
item.unmount();
});
}
this.cssResourceSet = resourceSet.map((item: IStyleResourceConfig) => new StyleResource(item)).reverse();
this.applyMediaCSS();
}
setWithShell(shell: string) {
// Flags.setWithShell(shell);
}
onFullscreenChange(func: () => any) {
this.emitter.on('fullscreenchange', func);
return () => {
this.emitter.removeListener('fullscreenchange', func);
};
}
onPreview(func: () => any) {
this.emitter.on('preview', func);
return () => {
this.emitter.removeListener('preview', func);
};
}
onDeviceChange(func: () => any) {
this.emitter.on('devicechange', func);
return () => {
this.emitter.removeListener('devicechange', func);
};
}
onSlateFixedChange(func: (flag: boolean) => any) {
this.emitter.on('slatefixed', func);
return () => {
this.emitter.removeListener('slatefixed', func);
};
}
onViewportChange(func: () => any) {
this.emitter.on('viewportchange', func);
return () => {
this.emitter.removeListener('viewportchange', func);
};
}
onFocusChange(func: (flag: boolean) => any) {
this.emitter.on('focuschange', func);
return () => {
this.emitter.removeListener('focuschange', func);
};
}
}
export default new Viewport();

View File

@ -0,0 +1,128 @@
html.engine-cursor-move, html.engine-cursor-move * {
cursor: grabbing !important
}
html.engine-cursor-copy, html.engine-cursor-copy * {
cursor: copy !important
}
html.engine-cursor-ew-resize, html.engine-cursor-ew-resize * {
cursor: ew-resize !important
}
body, #engine {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
box-sizing: border-box;
padding: 0;
margin: 0;
overflow: hidden;
text-rendering: optimizeLegibility;
-webkit-user-select: none;
-webkit-user-drag: none;
-webkit-text-size-adjust: none;
-webkit-touch-callout: none;
-webkit-font-smoothing: antialiased;
}
html {
min-width: 1024px;
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.3);
border-radius: 5px;
}
html.engine-blur #engine {
filter: blur(4px);
}
.engine-main {
width: 100%;
height: 100%;
position: fixed;
.ve-icon-button {
> .ve-icon-contents {
color: var(--color-text, rgba(51,51,51,.6));
}
&:hover, &.active {
> .ve-icon-contents {
color: var(--color-text-light, rgba(51,51,51,.8));
}
}
}
}
.engine-empty {
background: #f2f3f5;
color: #a7b1bd;
outline: 1px dashed rgba(31, 56, 88, 0.2);
outline-offset: -1px !important;
height: 66px;
max-height: 100%;
min-width: 140px;
text-align: center;
overflow: hidden;
display: flex;
align-items: center;
}
.engine-empty:before {
content: '\62D6\62FD\7EC4\4EF6\6216\6A21\677F\5230\8FD9\91CC';
font-size: 14px;
z-index: 1;
width: 100%;
white-space: nowrap;
}
// dirty fix override vision reset
.engine-design-mode {
.next-input-group,
.next-checkbox-group,.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 {
pointer-events: auto !important;
}
}
.vs-icon .vs-icon-del, .vs-icon .vs-icon-entry {
width: 16px!important;
height: 16px!important;
}
// .lc-left-float-pane {
// font-size: 14px;
// }
html.engine-preview-mode {
.lc-left-area, .lc-right-area {
display: none !important;
}
}
.ve-popups .ve-message {
right: calc(var(--right-area-width, 300px) + 10px);
.ve-message-content {
display: flex;
align-items: center;
line-height: 22px;
}
}
.lc-setter-mixed {
flex: 1
}

View File

@ -0,0 +1,116 @@
import { Component } from 'react';
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
import divPrototypeConfig from '../fixtures/prototype/div-vision';
import trunk from '../../src/bundle/trunk';
import Prototype from '../../src/bundle/prototype';
import Bundle from '../../src/bundle/bundle';
import { Editor } from '@ali/lowcode-editor-core';
jest.mock('../../src/bundle/trunk', () => {
// mockComponentPrototype = jest.fn();
// return {
// mockComponentPrototype: jest.fn().mockImplementation(() => {
// return proto;
// }),
// }
// return jest.fn().mockImplementation(() => {
// return {playSoundFile: fakePlaySoundFile};
// });
// return jest.fn().mockImplementation(() => {
// return { mockComponentPrototype };
// });
return {
__esModule: true,
default: {
mockComponentPrototype: jest.fn(),
},
};
});
function wrap(name, thing) {
return {
name,
componentName: name,
category: '布局',
module: thing,
};
}
const proto1 = new Prototype(divPrototypeConfig);
const protoConfig2 = cloneDeep(divPrototypeConfig);
set(protoConfig2, 'componentName', 'Div2');
const proto2 = new Prototype(protoConfig2);
const protoConfig3 = cloneDeep(divPrototypeConfig);
set(protoConfig3, 'componentName', 'Div3');
const proto3 = new Prototype(protoConfig3);
const protoConfig4 = cloneDeep(divPrototypeConfig);
set(protoConfig4, 'componentName', 'Div4');
const proto4 = new Prototype(protoConfig4);
const protoConfig5 = cloneDeep(divPrototypeConfig);
set(protoConfig5, 'componentName', 'Div5');
const proto5 = new Prototype(protoConfig5);
function getComponentProtos() {
return [
wrap('Div', proto1),
// wrap('Div2', proto2),
// wrap('Div3', proto3),
wrap('DivPortal', [proto2, proto3]),
];
}
class Div extends Component {}
Div.displayName = 'Div';
class Div2 extends Component {}
Div2.displayName = 'Div2';
class Div3 extends Component {}
Div3.displayName = 'Div3';
class Div4 extends Component {}
Div4.displayName = 'Div4';
class Div5 extends Component {}
Div5.displayName = 'Div5';
function getComponentViews() {
return [
wrap('Div', Div),
// wrap('Div2', Div2),
// wrap('Div3', Div3),
wrap('DivPortal', [Div2, Div3]),
];
}
describe('Bundle', () => {
it('构造函数', () => {
const protos = getComponentProtos();
const views = getComponentViews();
const bundle = new Bundle(protos, views);
expect(bundle.getList()).toHaveLength(3);
expect(bundle.get('Div')).toBe(proto1);
expect(bundle.get('Div2')).toBe(proto2);
expect(bundle.get('Div3')).toBe(proto3);
bundle.addComponentBundle([proto4, Div4]);
expect(bundle.getList()).toHaveLength(4);
expect(bundle.get('Div4')).toBe(proto4);
bundle.replacePrototype('Div4', proto3);
expect(proto3.getView()).toBe(Div4);
bundle.removeComponentBundle('Div2');
expect(bundle.getList()).toHaveLength(3);
expect(bundle.get('Div2')).toBeUndefined;
expect(bundle.getFromMeta('Div')).toBe(proto1);
bundle.getFromMeta('Div5');
expect(bundle.getList()).toHaveLength(4);
});
it('静态方法 create', () => {
const protos = getComponentProtos();
const views = getComponentViews();
const bundle = Bundle.create(protos, views);
expect(bundle).toBeTruthy();
});
});

View File

@ -0,0 +1,219 @@
import { Component } from 'react';
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 divPrototypeConfig from '../fixtures/prototype/div-vision';
import divFullPrototypeConfig from '../fixtures/prototype/div-vision-full';
import divPrototypeMeta from '../fixtures/prototype/div-meta';
// import VisualEngine from '../../src';
import { designer } from '../../src/reducers';
import Prototype, { isPrototype } from '../../src/bundle/prototype';
import { Editor } from '@ali/lowcode-editor-core';
// import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
describe('Prototype', () => {
it('构造函数 - OldPrototypeConfig', () => {
const proto = new Prototype(divPrototypeConfig);
expect(isPrototype(proto)).toBeTruthy;
expect(proto.getComponentName()).toBe('Div');
expect(proto.getId()).toBe('Div');
expect(proto.getCategory()).toBe('布局');
expect(proto.getDocUrl()).toBe(
'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
);
expect(proto.getIcon()).toBeUndefined;
expect(proto.getTitle()).toBe('Div');
expect(proto.isPrototype).toBeTruthy;
expect(proto.isContainer()).toBeTruthy;
expect(proto.isModal()).toBeFalsy;
});
it('构造函数 - 全量 OldPrototypeConfig', () => {
const proto = new Prototype(divFullPrototypeConfig);
expect(isPrototype(proto)).toBeTruthy;
expect(proto.getComponentName()).toBe('Div');
expect(proto.getId()).toBe('Div');
expect(proto.getCategory()).toBe('布局');
expect(proto.getDocUrl()).toBe(
'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
);
expect(proto.getIcon()).toBeUndefined;
expect(proto.getTitle()).toBe('Div');
expect(proto.isPrototype).toBeTruthy;
expect(proto.isContainer()).toBeTruthy;
expect(proto.isModal()).toBeFalsy;
});
it('构造函数 - ComponentMetadata', () => {
const proto = new Prototype(divPrototypeMeta);
expect(proto.getComponentName()).toBe('Div');
expect(proto.getId()).toBe('Div');
expect(proto.getCategory()).toBe('布局');
expect(proto.getDocUrl()).toBe(
'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
);
expect(proto.getIcon()).toBeUndefined;
expect(proto.getTitle()).toBe('Div');
expect(proto.isPrototype).toBeTruthy;
expect(proto.isContainer()).toBeTruthy;
expect(proto.isModal()).toBeFalsy;
});
it('构造函数 - ComponentMeta', () => {
const meta = designer.createComponentMeta(divPrototypeMeta);
const proto = new Prototype(meta);
expect(proto.getComponentName()).toBe('Div');
expect(proto.getId()).toBe('Div');
expect(proto.getCategory()).toBe('布局');
expect(proto.getDocUrl()).toBe(
'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
);
expect(proto.getIcon()).toBeUndefined;
expect(proto.getTitle()).toBe('Div');
expect(proto.isPrototype).toBeTruthy;
expect(proto.isContainer()).toBeTruthy;
expect(proto.isModal()).toBeFalsy;
});
it('构造函数 - 静态函数 create', () => {
const proto = Prototype.create(divPrototypeConfig);
expect(proto.getComponentName()).toBe('Div');
expect(proto.getId()).toBe('Div');
expect(proto.getCategory()).toBe('布局');
expect(proto.getDocUrl()).toBe(
'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
);
expect(proto.getIcon()).toBeUndefined;
expect(proto.getTitle()).toBe('Div');
expect(proto.isPrototype).toBeTruthy;
expect(proto.isContainer()).toBeTruthy;
expect(proto.isModal()).toBeFalsy;
});
it('构造函数 - lookup: true', () => {
const proto = Prototype.create(divPrototypeConfig);
const proto2 = Prototype.create(divPrototypeConfig, {}, true);
expect(proto).toBe(proto2);
});
describe('类成员函数', () => {
let proto: Prototype = null;
beforeEach(() => {
proto = new Prototype(divPrototypeConfig);
});
afterEach(() => {
proto = null;
});
it('各种函数', () => {
expect(proto.componentName).toBe('Div');
expect(proto.getComponentName()).toBe('Div');
expect(proto.getId()).toBe('Div');
expect(proto.getContextInfo('anything')).toBeUndefined;
expect(proto.getPropConfigs()).toBe(divPrototypeConfig);
expect(proto.getConfig()).toBe(divPrototypeConfig);
expect(proto.getConfig('componentName')).toBe('Div');
expect(proto.getConfig('configure')).toBe(divPrototypeConfig.configure);
expect(proto.getConfig('docUrl')).toBe(
'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
);
expect(proto.getConfig('title')).toBe('容器');
expect(proto.getConfig('isContainer')).toBeTruthy;
class MockView extends Component {}
expect(proto.getView()).toBeUndefined;
proto.setView(MockView);
expect(proto.getView()).toBe(MockView);
expect(proto.meta.getMetadata().experimental?.view).toBe(MockView);
expect(proto.getPackageName()).toBeUndefined;
proto.setPackageName('@ali/vc-div');
expect(proto.getPackageName()).toBe('@ali/vc-div');
expect(proto.getConfig('category')).toBe('布局');
proto.setCategory('布局 new');
expect(proto.getConfig('category')).toBe('布局 new');
expect(proto.getConfigure()).toHaveLength(3);
expect(proto.getConfigure()[0].name).toBe('#props');
expect(proto.getConfigure()[1].name).toBe('#styles');
expect(proto.getConfigure()[2].name).toBe('#advanced');
expect(proto.getRectSelector()).toBeUndefined;
expect(proto.isAutoGenerated()).toBeFalsy;
});
});
describe('类成员函数', () => {
it('addGlobalPropsConfigure', () => {
Prototype.addGlobalPropsConfigure({
name: 'globalInsertProp1',
});
const proto1 = new Prototype(divPrototypeConfig);
expect(proto1.getConfigure()[2].items).toHaveLength(4);
expect(proto1.getConfigure()[2].items[3].name).toBe('globalInsertProp1');
Prototype.addGlobalPropsConfigure({
name: 'globalInsertProp2',
});
const proto2 = new Prototype(divPrototypeConfig);
expect(proto2.getConfigure()[2].items).toHaveLength(5);
expect(proto1.getConfigure()[2].items[4].name).toBe('globalInsertProp2');
Prototype.addGlobalPropsConfigure({
name: 'globalInsertProp3',
position: 'top',
});
const proto3 = new Prototype(divPrototypeConfig);
expect(proto3.getConfigure()[0].items).toHaveLength(3);
expect(proto1.getConfigure()[0].items[0].name).toBe('globalInsertProp3');
});
it('removeGlobalPropsConfigure', () => {
Prototype.removeGlobalPropsConfigure('globalInsertProp1');
Prototype.removeGlobalPropsConfigure('globalInsertProp2');
Prototype.removeGlobalPropsConfigure('globalInsertProp3');
const proto2 = new Prototype(divPrototypeConfig);
expect(proto2.getConfigure()[0].items).toHaveLength(2);
expect(proto2.getConfigure()[2].items).toHaveLength(3);
});
it('overridePropsConfigure', () => {
Prototype.addGlobalPropsConfigure({
name: 'globalInsertProp1',
title: 'globalInsertPropTitle',
position: 'top',
});
const proto1 = new Prototype(divPrototypeConfig);
expect(proto1.getConfigure()[0].items).toHaveLength(3);
expect(proto1.getConfigure()[0].items[0].name).toBe('globalInsertProp1');
expect(proto1.getConfigure()[0].items[0].title).toBe('globalInsertPropTitle');
Prototype.overridePropsConfigure('Div', [
{
name: 'globalInsertProp1',
title: 'globalInsertPropTitleChanged',
},
]);
const proto2 = new Prototype(divPrototypeConfig);
expect(proto2.getConfigure()[0].name).toBe('globalInsertProp1');
expect(proto2.getConfigure()[0].title).toBe('globalInsertPropTitleChanged');
Prototype.overridePropsConfigure('Div', {
globalInsertProp1: {
name: 'globalInsertProp1',
title: 'globalInsertPropTitleChanged new',
position: 'top',
},
});
const proto3 = new Prototype(divPrototypeConfig);
expect(proto3.getConfigure()[0].items[0].name).toBe('globalInsertProp1');
expect(proto3.getConfigure()[0].items[0].title).toBe('globalInsertPropTitleChanged new');
});
it('addGlobalExtraActions', () => {
function haha() { return 'heihei'; }
Prototype.addGlobalExtraActions(haha);
const proto1 = new Prototype(divPrototypeConfig);
expect(proto1.meta.availableActions).toHaveLength(4);
expect(proto1.meta.availableActions[3].name).toBe('haha');
});
});
});

View File

@ -0,0 +1,111 @@
import { Component } from 'react';
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
import divPrototypeConfig from '../fixtures/prototype/div-vision';
import Prototype from '../../src/bundle/prototype';
import Bundle from '../../src/bundle/bundle';
import trunk from '../../src/bundle/trunk';
import lg from '@ali/vu-logger';
const proto1 = new Prototype(divPrototypeConfig);
const protoConfig2 = cloneDeep(divPrototypeConfig);
set(protoConfig2, 'componentName', 'Div2');
const proto2 = new Prototype(protoConfig2);
const protoConfig3 = cloneDeep(divPrototypeConfig);
set(protoConfig3, 'componentName', 'Div3');
const proto3 = new Prototype(protoConfig3);
const mockComponentPrototype = jest.fn();
jest.mock('../../src/bundle/bundle', () => {
// return {
// mockComponentPrototype: jest.fn().mockImplementation(() => {
// return proto;
// }),
// }
// return jest.fn().mockImplementation(() => {
// return {playSoundFile: fakePlaySoundFile};
// });
return jest.fn().mockImplementation(() => {
return {
get: () => {},
getList: () => { return []; },
getFromMeta: () => {},
};
});
});
const mockError = jest.fn();
jest.mock('@ali/vu-logger');
lg.error = mockError;
function wrap(name, thing) {
return {
name,
componentName: name,
category: '布局',
module: thing,
};
}
function getComponentProtos() {
return [
wrap('Div', proto1),
// wrap('Div2', proto2),
// wrap('Div3', proto3),
wrap('DivPortal', [proto2, proto3]),
];
}
class Div extends Component {}
Div.displayName = 'Div';
class Div2 extends Component {}
Div2.displayName = 'Div2';
class Div3 extends Component {}
Div3.displayName = 'Div3';
function getComponentViews() {
return [
wrap('Div', Div),
// wrap('Div2', Div2),
// wrap('Div3', Div3),
wrap('DivPortal', [Div2, Div3]),
];
}
describe('Trunk', () => {
it('构造函数', () => {
const warn = console.warn = jest.fn();
const trunkChangeHandler = jest.fn();
const off = trunk.onTrunkChange(trunkChangeHandler);
trunk.addBundle(new Bundle([proto1], [Div]));
trunk.addBundle(new Bundle([proto2], [Div2]));
expect(trunkChangeHandler).toHaveBeenCalledTimes(2);
off();
trunk.addBundle(new Bundle([proto3], [Div3]));
expect(trunkChangeHandler).toHaveBeenCalledTimes(2);
trunk.getList();
trunk.getPrototype('Div');
trunk.getPrototypeById('Div');
trunk.getPrototypeView('Div');
trunk.listByCategory();
expect(trunk.mockComponentPrototype(new Bundle([proto3], [Div3]))).toBeUndefined;
expect(mockError).toHaveBeenCalled();
trunk.registerComponentPrototypeMocker({ mockPrototype: jest.fn().mockImplementation(() => { return proto3; }) });
expect(trunk.mockComponentPrototype(new Bundle([proto3], [Div3]))).toBe(proto3);
const hahaSetter = () => 'haha';
trunk.registerSetter('haha', hahaSetter);
expect(trunk.getSetter('haha')).toBe(hahaSetter);
trunk.getRecents(5);
trunk.setPackages();
expect(warn).toHaveBeenCalledTimes(1);
trunk.beforeLoadBundle();
expect(warn).toHaveBeenCalledTimes(2);
trunk.afterLoadBundle();
expect(warn).toHaveBeenCalledTimes(3);
trunk.getBundle();
expect(warn).toHaveBeenCalledTimes(4);
expect(trunk.isReady()).toBeTruthy;
});
});

View File

@ -0,0 +1,259 @@
export default {
componentName: 'Div',
title: '容器',
docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
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: 'groupkh97h5kc',
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: {},
},
supports: {},
},
experimental: {
callbacks: {},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [],
autoruns: [],
},
};

View File

@ -0,0 +1,293 @@
export default {
title: '容器',
componentName: 'Div',
docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
category: '布局',
isContainer: true,
canOperating: false,
extraActions: [],
canContain: 'Form',
canDropTo: 'Div',
canDropIn: 'Div',
canResizing: true,
canDraging: false,
context: {},
initialChildren() {},
didDropIn() {},
didDropOut() {},
subtreeModified() {},
onResize() {},
onResizeStart() {},
onResizeEnd() {},
canUseCondition: true,
canLoop: true,
snippets: [
{
screenshot: 'https://img.alicdn.com/tfs/TB1CHN3u4z1gK0jSZSgXXavwpXa-112-64.png',
label: '普通型',
schema: {
componentName: 'Div',
props: {},
},
},
],
configure: [
{
name: 'myName',
title: '我的名字',
display: 'tab',
initialValue: 'NORMAL',
defaultValue: 'NORMAL',
collapsed: true,
supportVariable: true,
accessor(field, val) {},
mutator(field, val) {},
disabled() {
return true;
},
useVariableChange() {},
allowTextInput: true,
liveTextEditing: true,
setter: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
],
},
{
name: 'mySlotName',
slotName: 'mySlotName',
slotTitle: '我的 Slot 名字',
display: 'tab',
initialValue: 'NORMAL',
defaultValue: 'NORMAL',
collapsed: true,
supportVariable: true,
accessor(field, val) {},
mutator(field, val) {},
disabled() {
return true;
},
setter: {
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
},
{
name: 'behavior',
title: '默认状态',
display: 'inline',
initialValue: 'NORMAL',
supportVariable: true,
setter: {
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
},
{
name: '__style__',
title: '样式设置',
display: 'accordion',
collapsed: false,
initialValue: {},
tip: {
url: 'https://lark.alipay.com/legao/help/design-tool-style',
content: '点击 ? 查看样式设置器用法指南',
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
title: '高级',
display: 'accordion',
items: [
{
name: 'fieldId',
title: '唯一标识',
display: 'block',
tip:
'组件的唯一标识符,不能够与其它组件重名,不能够为空,且只能够使用以字母开头的,下划线以及数字的组合。',
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
name: 'useFieldIdAsDomId',
title: '将唯一标识用作 DOM ID',
display: 'block',
tip:
'开启这个配置项后,会在当前组件的 HTML 元素上加入 id="当前组件的 fieldId",一般用于做 utils 的绑定,不常用',
initialValue: false,
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
name: 'customClassName',
title: '自定义样式类',
display: 'block',
supportVariable: true,
initialValue: '',
setter: {
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
name: 'events',
title: '动作设置',
tip: {
url: 'https://lark.alipay.com/legao/legao/events-call',
content: '点击 ? 查看如何设置组件的事件响应动作',
},
display: 'accordion',
initialValue: {
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,
},
},
{
name: 'onClick',
display: 'none',
initialValue: {
ignored: true,
},
},
{
name: 'onMouseEnter',
display: 'none',
initialValue: {
ignored: true,
},
},
{
name: 'onMouseLeave',
display: 'none',
initialValue: {
ignored: true,
},
},
],
},
],
};

View File

@ -0,0 +1,175 @@
export default {
title: '容器',
componentName: 'Div',
docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
category: '布局',
isContainer: true,
configure: [
{
name: 'behavior',
title: '默认状态',
display: 'inline',
initialValue: 'NORMAL',
supportVariable: true,
setter: {
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
},
{
name: '__style__',
title: '样式设置',
display: 'accordion',
collapsed: false,
initialValue: {},
tip: {
url: 'https://lark.alipay.com/legao/help/design-tool-style',
content: '点击 ? 查看样式设置器用法指南',
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
title: '高级',
display: 'accordion',
items: [
{
name: 'fieldId',
title: '唯一标识',
display: 'block',
tip:
'组件的唯一标识符,不能够与其它组件重名,不能够为空,且只能够使用以字母开头的,下划线以及数字的组合。',
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
name: 'useFieldIdAsDomId',
title: '将唯一标识用作 DOM ID',
display: 'block',
tip:
'开启这个配置项后,会在当前组件的 HTML 元素上加入 id="当前组件的 fieldId",一般用于做 utils 的绑定,不常用',
initialValue: false,
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
name: 'customClassName',
title: '自定义样式类',
display: 'block',
supportVariable: true,
initialValue: '',
setter: {
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
name: 'events',
title: '动作设置',
tip: {
url: 'https://lark.alipay.com/legao/legao/events-call',
content: '点击 ? 查看如何设置组件的事件响应动作',
},
display: 'accordion',
initialValue: {
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,
},
},
{
name: 'onClick',
display: 'none',
initialValue: {
ignored: true,
},
},
{
name: 'onMouseEnter',
display: 'none',
initialValue: {
ignored: true,
},
},
{
name: 'onMouseLeave',
display: 'none',
initialValue: {
ignored: true,
},
},
],
},
],
};

View File

@ -0,0 +1,955 @@
export default {
componentName: 'Page',
id: 'node_k1ow3cb9',
props: {
extensions: {
启用页头: true,
},
pageStyle: {
backgroundColor: '#f2f3f5',
},
containerStyle: {},
className: 'page_kh05zf9c',
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_kh05zf9d {\n margin-bottom: 12px;\n}.card_kh05zf9e {\n margin-bottom: 12px;\n}.button_kh05zf9f {\n margin-right: 16px;\n width: 80px\n}.button_kh05zf9g {\n width: 80px;\n}.div_kh05zf9h {\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: '',
content: '',
__slot__logo: false,
__slot__crumb: false,
crumb: '',
tab: '',
logo: '',
action: '',
__slot__tab: false,
__style__: {},
__slot__content: false,
fieldId: 'pageHeader_k1ow3h1i',
subTitle: '',
},
condition: true,
},
],
},
{
componentName: 'RootContent',
id: 'node_k1ow3cbb',
props: {
contentBgColor: 'transparent',
contentPadding: '0',
contentMargin: '20',
},
condition: true,
children: [
{
componentName: 'Form',
id: 'form',
props: {
size: 'medium',
labelAlign: 'top',
autoValidate: true,
scrollToFirstError: true,
autoUnmount: true,
behavior: 'NORMAL',
dataSource: {
type: 'variable',
variable: 'state.formData',
},
__style__: {},
fieldId: 'form',
fieldOptions: {},
},
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_kh05zf9d',
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: '4:8',
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: '',
},
maxLength: 200,
},
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: '',
},
maxLength: 200,
},
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: '',
},
maxLength: 200,
},
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: '',
},
maxLength: 200,
},
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: '',
},
useDetailValue: false,
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_kh05zf9e',
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: '',
},
maxLength: 200,
},
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: '',
},
maxLength: 200,
},
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: '',
},
maxLength: 200,
},
condition: true,
},
],
},
],
},
],
},
],
},
{
componentName: 'Div',
id: 'node_k1ow3cbo',
props: {
className: 'div_kh05zf9h',
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_kh05zf9f',
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_kh05zf9g',
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',
},
condition: true,
},
],
},
],
},
],
},
{
componentName: 'RootFooter',
id: 'node_k1ow3cbc',
props: {},
condition: true,
},
],
};

View File

@ -0,0 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
React.PropTypes = PropTypes;
window.React = React;
document.documentElement.requestFullscreen = () => {};
document.exitFullscreen = () => {};

View File

@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`deepValueParser 测试 designMode: design 1`] = `
Object {
"a": "111",
"arr": Array [
"111",
"111",
],
"b": "222",
"c": "中文",
"slot": Object {
"type": "JSSlot",
"value": Array [
Object {
"componentName": "Div",
"props": Object {},
},
],
},
}
`;
exports[`deepValueParser 测试 designMode: live 1`] = `
Object {
"a": Object {
"mock": "111",
"type": "JSExpression",
"value": "state.a",
},
"arr": Array [
Object {
"mock": "111",
"type": "JSExpression",
"value": "state.a",
},
Object {
"mock": "111",
"type": "JSExpression",
"value": "state.b",
},
],
"b": Object {
"mock": "222",
"type": "JSExpression",
"value": "state.b",
},
"c": "中文",
}
`;

View File

@ -0,0 +1,240 @@
import '../fixtures/window';
import bus from '../../src/bus';
import { editor } from '../../src/reducers';
describe('bus 测试', () => {
afterEach(() => {
bus.unsub('evt1');
});
it('sub / pub 测试', () => {
const mockFn1 = jest.fn();
const off1 = bus.sub('evt1', mockFn1);
const evtData = { a: 1 };
bus.pub('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
off1();
bus.pub('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
});
it('on / emit 测试', () => {
const mockFn1 = jest.fn();
const off1 = bus.on('evt1', mockFn1);
const evtData = { a: 1 };
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
off1();
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
});
it('once / emit 测试', () => {
const mockFn1 = jest.fn();
bus.once('evt1', mockFn1);
const evtData = { a: 1 };
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
});
it('once / emit 测试,调用解绑函数', () => {
const mockFn1 = jest.fn();
const off1 = bus.once('evt1', mockFn1);
off1();
const evtData = { a: 1 };
bus.emit('evt1', evtData);
expect(mockFn1).not.toHaveBeenCalled();
});
it('removeListener 测试', () => {
const mockFn1 = jest.fn();
bus.on('evt1', mockFn1);
const evtData = { a: 1 };
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
bus.removeListener('evt1', mockFn1);
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
});
it('unsub 测试 - 只有一个 handler', () => {
const mockFn1 = jest.fn();
bus.on('evt1', mockFn1);
const evtData = { a: 1 };
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
bus.unsub('evt1', mockFn1);
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
});
it('unsub 测试 - 只 unsub 一个 handler', () => {
const mockFn1 = jest.fn();
const mockFn2 = jest.fn();
bus.on('evt1', mockFn1);
bus.on('evt1', mockFn2);
const evtData = { a: 1 };
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
expect(mockFn2).toHaveBeenCalledTimes(1);
expect(mockFn2).toHaveBeenCalledWith(evtData);
bus.unsub('evt1', mockFn1);
const evtData2 = { a: 2 };
bus.emit('evt1', evtData2);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
expect(mockFn2).toHaveBeenCalledTimes(2);
expect(mockFn2).toHaveBeenLastCalledWith(evtData2);
});
it('unsub 测试 - 多个 handler', () => {
const mockFn1 = jest.fn();
const mockFn2 = jest.fn();
bus.on('evt1', mockFn1);
bus.on('evt1', mockFn2);
const evtData = { a: 1 };
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
expect(mockFn2).toHaveBeenCalledTimes(1);
expect(mockFn2).toHaveBeenCalledWith(evtData);
bus.unsub('evt1');
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
expect(mockFn2).toHaveBeenCalledTimes(1);
expect(mockFn2).toHaveBeenCalledWith(evtData);
});
it('off 测试 - 只有一个 handler', () => {
const mockFn1 = jest.fn();
bus.on('evt1', mockFn1);
const evtData = { a: 1 };
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
bus.off('evt1', mockFn1);
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
});
it('off 测试 - 只 off 一个 handler', () => {
const mockFn1 = jest.fn();
const mockFn2 = jest.fn();
bus.on('evt1', mockFn1);
bus.on('evt1', mockFn2);
const evtData = { a: 1 };
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
expect(mockFn2).toHaveBeenCalledTimes(1);
expect(mockFn2).toHaveBeenCalledWith(evtData);
bus.off('evt1', mockFn1);
const evtData2 = { a: 2 };
bus.emit('evt1', evtData2);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
expect(mockFn2).toHaveBeenCalledTimes(2);
expect(mockFn2).toHaveBeenLastCalledWith(evtData2);
});
it('off 测试 - 多个 handler', () => {
const mockFn1 = jest.fn();
const mockFn2 = jest.fn();
bus.on('evt1', mockFn1);
bus.on('evt1', mockFn2);
const evtData = { a: 1 };
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
expect(mockFn2).toHaveBeenCalledTimes(1);
expect(mockFn2).toHaveBeenCalledWith(evtData);
bus.off('evt1');
bus.emit('evt1', evtData);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData);
expect(mockFn2).toHaveBeenCalledTimes(1);
expect(mockFn2).toHaveBeenCalledWith(evtData);
});
it('简单测试dummy', () => {
bus.getEmitter();
});
describe('editor 事件转发', () => {
const fwdEvtMap = {
've.hotkey.callback.call': 'hotkey.callback.call',
've.history.back': 'history.back',
've.history.forward': 'history.forward',
'node.prop.change': 'node.prop.change',
};
Object.keys(fwdEvtMap).forEach(veEventName => {
it(`${veEventName} 测试`, () => {
const mockFn1 = jest.fn();
const evtData1 = { a: 1 };
bus.on(veEventName, mockFn1);
editor.emit(fwdEvtMap[veEventName], evtData1);
expect(mockFn1).toHaveBeenCalledTimes(1);
expect(mockFn1).toHaveBeenCalledWith(evtData1);
});
});
});
});

View File

@ -0,0 +1,62 @@
import '../fixtures/window';
// import { Project } from '../../src/project/project';
// import { Node } from '../../src/document/node/node';
// import { Designer } from '../../src/designer/designer';
import { VisualEngineContext } from '../../src/context';
import { autorun } from '@ali/lowcode-editor-core';
describe('VisualEngineContext 测试', () => {
it('registerManager | getManager', () => {
const ctx = new VisualEngineContext();
ctx.registerManager({
mgr1: {},
});
ctx.registerManager('mgr2', {});
expect(ctx.getManager('mgr1')).toEqual({});
});
it('registerModule | getModule', () => {
const ctx = new VisualEngineContext();
ctx.registerModule({
mod1: {},
});
ctx.registerModule('mod2', {});
expect(ctx.getModule('mod1')).toEqual({});
});
it('use | getPlugin', () => {
const ctx = new VisualEngineContext();
ctx.use('plugin1', { plugin: 1 });
ctx.registerManager({
mgr1: { manager: 1 },
});
ctx.registerModule({
mod1: { mod: 1 },
});
expect(ctx.getPlugin('plugin1')).toEqual({ plugin: 1 });
expect(ctx.getPlugin('mgr1')).toEqual({ manager: 1 });
expect(ctx.getPlugin('mod1')).toEqual({ mod: 1 });
expect(ctx.getPlugin()).toBeUndefined;
ctx.use('ve.settingField.variableSetter', {});
});
it('registerTreePane | getModule', () => {
const ctx = new VisualEngineContext();
ctx.registerTreePane({ pane: 1 }, { core: 2 });
expect(ctx.getModule('TreePane')).toEqual({ pane: 1 });
expect(ctx.getModule('TreeCore')).toEqual({ core: 2 });
});
it('registerDynamicSetterProvider', () => {
const ctx = new VisualEngineContext();
ctx.registerDynamicSetterProvider({});
expect(ctx.getPlugin('ve.plugin.setterProvider')).toEqual({});
ctx.registerDynamicSetterProvider();
});
});

View File

@ -0,0 +1,84 @@
import '../fixtures/window';
import { deepValueParser } from '../../src/deep-value-parser';
import { editor } from '../../src/reducers';
describe('deepValueParser 测试', () => {
it('null & undefined', () => {
expect(deepValueParser()).toBeNull;
expect(deepValueParser()).toBeUndefined;
});
it('designMode: design', () => {
expect(deepValueParser({
a: {
type: 'variable',
variable: 'state.a',
value: '111',
},
b: {
type: 'JSExpression',
value: 'state.b',
mock: '222',
},
c: {
type: 'i18n',
use: 'zh_CN',
zh_CN: '中文',
en_US: 'eng',
},
slot: {
type: 'JSSlot',
value: [{
componentName: 'Div',
props: {},
}],
},
arr: [
{
type: 'variable',
variable: 'state.a',
value: '111',
},
{
type: 'variable',
variable: 'state.b',
value: '111',
},
],
})).toMatchSnapshot();
});
it('designMode: live', () => {
editor.set('designMode', 'live');
expect(deepValueParser({
a: {
type: 'variable',
variable: 'state.a',
value: '111',
},
b: {
type: 'JSExpression',
value: 'state.b',
mock: '222',
},
c: {
type: 'i18n',
use: 'zh_CN',
zh_CN: '中文',
en_US: 'eng',
},
arr: [
{
type: 'variable',
variable: 'state.a',
value: '111',
},
{
type: 'variable',
variable: 'state.b',
value: '111',
},
],
})).toMatchSnapshot();
});
});

View File

@ -0,0 +1,182 @@
import '../fixtures/window';
// import { Project } from '../../src/project/project';
// import { Node } from '../../src/document/node/node';
// import { Editor } from '@ali/lowcode-editor-core';
// import { Designer } from '@ali/lowcode-designer';
import { designer } from '../../src/reducers';
import DragEngine from '../../src/drag-engine';
import formSchema from '../fixtures/schema/form';
// const editor = new Editor();
// const designer = new Designer({ editor });
designer.project.open(formSchema);
const mockBoostPrototype = jest.fn((e: MouseEvent) => {
return {
isPrototype: true,
getComponentName() {
return 'Div';
},
};
});
const mockBoostNode = jest.fn((e: MouseEvent) => {
return designer.currentDocument?.getNode('node_k1ow3cbo');
});
const mockBoostNodeData = jest.fn((e: MouseEvent) => {
return {
type: 'NodeData',
componentName: 'Div',
};
});
const mockBoostNull = jest.fn((e: MouseEvent) => {
return null;
});
const mockDragstart = jest.fn();
const mockDrag = jest.fn();
const mockDragend = jest.fn();
describe('drag-engine 测试', () => {
it('prototype', async () => {
DragEngine.from(document, mockBoostPrototype);
DragEngine.onDragstart(mockDragstart);
DragEngine.onDrag(mockDrag);
DragEngine.onDragend(mockDragend);
const mousedownEvt = new MouseEvent('mousedown');
document.dispatchEvent(mousedownEvt);
designer.dragon.emitter.emit('dragstart', {
dragObject: {
nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')],
},
originalEvent: mousedownEvt,
});
// await new Promise(resolve => resolve(setTimeout, 500));
expect(mockDragstart).toHaveBeenCalled();
designer.dragon.emitter.emit('drag', {
dragObject: {
nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')]
},
originalEvent: mousedownEvt,
});
expect(mockDrag).toHaveBeenCalled();
expect(DragEngine.inDragging()).toBeTruthy;
designer.dragon.emitter.emit('dragend', {
dragObject: {
nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')]
},
originalEvent: mousedownEvt,
});
expect(mockDragend).toHaveBeenCalled();
});
it('Node', async () => {
DragEngine.from(document, mockBoostNode);
DragEngine.onDragstart(mockDragstart);
DragEngine.onDrag(mockDrag);
DragEngine.onDragend(mockDragend);
const mousedownEvt = new MouseEvent('mousedown');
document.dispatchEvent(mousedownEvt);
designer.dragon.emitter.emit('dragstart', {
dragObject: {
nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')],
},
originalEvent: mousedownEvt,
});
// await new Promise(resolve => resolve(setTimeout, 500));
expect(mockDragstart).toHaveBeenCalled();
designer.dragon.emitter.emit('drag', {
dragObject: {
nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')]
},
originalEvent: mousedownEvt,
});
expect(mockDrag).toHaveBeenCalled();
designer.dragon.emitter.emit('dragend', {
dragObject: {
nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')]
},
originalEvent: mousedownEvt,
});
expect(mockDragend).toHaveBeenCalled();
});
it('NodeData', async () => {
DragEngine.from(document, mockBoostNodeData);
DragEngine.onDragstart(mockDragstart);
DragEngine.onDrag(mockDrag);
DragEngine.onDragend(mockDragend);
const mousedownEvt = new MouseEvent('mousedown');
document.dispatchEvent(mousedownEvt);
designer.dragon.emitter.emit('dragstart', {
dragObject: {
nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')],
},
originalEvent: mousedownEvt,
});
// await new Promise(resolve => resolve(setTimeout, 500));
expect(mockDragstart).toHaveBeenCalled();
designer.dragon.emitter.emit('drag', {
dragObject: {
nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')]
},
originalEvent: mousedownEvt,
});
expect(mockDrag).toHaveBeenCalled();
designer.dragon.emitter.emit('dragend', {
dragObject: {
type: 'nodedata',
data: {
componentName: 'Div',
},
},
originalEvent: mousedownEvt,
});
expect(mockDragend).toHaveBeenCalled();
});
it('null', async () => {
DragEngine.from(document, mockBoostNull);
DragEngine.onDragstart(mockDragstart);
DragEngine.onDrag(mockDrag);
DragEngine.onDragend(mockDragend);
const mousedownEvt = new MouseEvent('mousedown');
document.dispatchEvent(mousedownEvt);
designer.dragon.emitter.emit('dragstart', {
dragObject: {
nodes: [designer.currentDocument?.getNode('node_k1ow3cbo')],
},
originalEvent: mousedownEvt,
});
expect(mockDragstart).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,111 @@
import '../fixtures/window';
// import { Project } from '../../src/project/project';
// import { Node } from '../../src/document/node/node';
// import { Designer } from '../../src/designer/designer';
import env from '../../src/env';
import { autorun } from '@ali/lowcode-editor-core';
describe('env 测试', () => {
describe('常规 API 测试', () => {
it('setEnv / getEnv / setEnvMap / set / get', () => {
expect(env.getEnv('xxx')).toBeUndefined;
const mockFn1 = jest.fn();
const off1 = env.onEnvChange(mockFn1);
const envData = { a: 1 };
env.setEnv('xxx', envData);
expect(env.getEnv('xxx')).toEqual(envData);
expect(env.get('xxx')).toEqual(envData);
expect(mockFn1).toHaveBeenCalled();
expect(mockFn1).toHaveBeenCalledWith(env.envs, 'xxx', envData);
mockFn1.mockClear();
// 设置相同的值
env.setEnv('xxx', envData);
expect(env.getEnv('xxx')).toEqual(envData);
expect(env.get('xxx')).toEqual(envData);
expect(mockFn1).not.toHaveBeenCalled();
mockFn1.mockClear();
// 设置另一个 envName
const envData2 = { b: 1 };
env.set('yyy', envData2);
expect(env.getEnv('yyy')).toEqual(envData2);
expect(env.get('yyy')).toEqual(envData2);
expect(mockFn1).toHaveBeenCalled();
expect(mockFn1).toHaveBeenCalledWith(env.envs, 'yyy', envData2);
mockFn1.mockClear();
env.setEnvMap({
zzz: { a: 1, b: 1 },
});
expect(env.getEnv('xxx')).toBeUndefined;
expect(env.getEnv('yyy')).toBeUndefined;
expect(env.getEnv('zzz')).toEqual({ a: 1, b: 1 });
expect(mockFn1).toHaveBeenCalled();
expect(mockFn1).toHaveBeenCalledWith(env.envs);
mockFn1.mockClear();
// 解绑事件
off1();
env.setEnvMap({
zzz: { a: 1, b: 1 },
});
expect(mockFn1).not.toHaveBeenCalled();
mockFn1.mockClear();
});
it('setLocale / getLocale', () => {
expect(env.getLocale()).toBe('zh_CN');
env.setLocale('en_US');
expect(env.getLocale()).toBe('en_US');
});
it('setExpertMode / isExpertMode', () => {
expect(env.isExpertMode()).toBeFalsy;
env.setExpertMode('truthy value');
expect(env.isExpertMode()).toBeTruthy;
});
it('getSupportFeatures / setSupportFeatures / supports', () => {
expect(env.getSupportFeatures()).toEqual({});
env.setSupportFeatures({
mobile: true,
pc: true,
});
expect(env.getSupportFeatures()).toEqual({
mobile: true,
pc: true,
});
expect(env.supports('mobile')).toBeTruthy;
expect(env.supports('pc')).toBeTruthy;
expect(env.supports('iot')).toBeFalsy;
});
it('getAliSchemaVersion', () => {
expect(env.getAliSchemaVersion()).toBe('1.0.0');
});
it('envs obx 测试', async () => {
const mockFn = jest.fn();
env.clear();
autorun(() => {
mockFn(env.envs);
env.envs;
});
await new Promise(resolve => setTimeout(resolve, 16));
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenLastCalledWith({});
env.setEnv('abc', { a: 1 });
await new Promise(resolve => setTimeout(resolve, 16));
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenLastCalledWith({ abc: { a: 1 } });
});
});
});

View File

@ -0,0 +1,71 @@
import '../fixtures/window';
import flagsCtrl from '../../src/flags';
import domready from 'domready';
jest.mock('domready', () => {
return (fn) => fn();
});
// domready.mockImplementation((fn) => fn());
describe('flags 测试', () => {
it('flags', () => {
const mockFlagsChange = jest.fn();
flagsCtrl.flags = [];
const off = flagsCtrl.onFlagsChange(mockFlagsChange);
flagsCtrl.add('a');
expect(mockFlagsChange).toHaveBeenCalledTimes(1);
off();
flagsCtrl.add('b');
expect(mockFlagsChange).toHaveBeenCalledTimes(1);
expect(flagsCtrl.getFlags()).toEqual(['a', 'b']);
flagsCtrl.flags = [];
flagsCtrl.setDragMode(true);
expect(flagsCtrl.getFlags()).toEqual(['drag-mode']);
flagsCtrl.setDragMode(false);
expect(flagsCtrl.getFlags()).toEqual([]);
flagsCtrl.setPreviewMode(true);
expect(flagsCtrl.getFlags()).toEqual(['preview-mode']);
flagsCtrl.setPreviewMode(false);
expect(flagsCtrl.getFlags()).toEqual(['design-mode']);
flagsCtrl.flags = [];
flagsCtrl.setHideSlate(true);
expect(flagsCtrl.getFlags()).toEqual(['hide-slate']);
flagsCtrl.setHideSlate(false);
expect(flagsCtrl.getFlags()).toEqual([]);
flagsCtrl.flags = [];
flagsCtrl.setSlateFixedMode(true);
expect(flagsCtrl.getFlags()).toEqual(['slate-fixed']);
flagsCtrl.setHideSlate(true);
expect(flagsCtrl.getFlags()).toEqual(['slate-fixed']);
flagsCtrl.setSlateFixedMode(false);
expect(flagsCtrl.getFlags()).toEqual([]);
flagsCtrl.flags = [];
flagsCtrl.setSlateFullMode(true);
expect(flagsCtrl.getFlags()).toEqual(['slate-full-screen']);
flagsCtrl.setSlateFullMode(false);
expect(flagsCtrl.getFlags()).toEqual([]);
expect([].slice.apply(document.documentElement.classList)).toEqual(flagsCtrl.getFlags());
flagsCtrl.flags = [];
// setWithShell
flagsCtrl.setWithShell('shellA');
expect(flagsCtrl.getFlags()).toEqual(['with-iphone6shell']);
flagsCtrl.setWithShell('iPhone6');
expect(flagsCtrl.getFlags()).toEqual(['with-iphone6shell']);
flagsCtrl.flags = [];
// setSimulator
flagsCtrl.setSimulator('simA');
expect(flagsCtrl.getFlags()).toEqual(['simulator-simA']);
flagsCtrl.setSimulator('simB');
expect(flagsCtrl.getFlags()).toEqual(['simulator-simB']);
});
});

View File

@ -0,0 +1,176 @@
import '../fixtures/window';
// import { Project } from '../../src/project/project';
// import { Node } from '../../src/document/node/node';
// import { Designer } from '../../src/designer/designer';
import panes from '../../src/panes';
import { autorun } from '@ali/lowcode-editor-core';
describe('panes 测试', () => {
it('add: type dock | PanelDock', () => {
const mockDockShow = jest.fn();
const mockDockHide = jest.fn();
const { DockPane } = panes;
const offDockShow = DockPane.onDockShow(mockDockShow);
const offDockHide = DockPane.onDockHide(mockDockHide);
const pane1 = panes.add({
name: 'trunk',
type: 'dock',
width: 300,
description: '组件库',
contents: [
{
title: '普通组件',
tip: '普通组件',
content: () => 'haha',
},
],
menu: '组件库',
defaultFixed: true,
});
const pane2 = panes.add({
name: 'trunk2',
type: 'dock',
width: 300,
description: '组件库',
contents: [
{
title: '普通组件',
tip: '普通组件',
content: () => 'haha',
},
],
menu: '组件库',
defaultFixed: true,
});
const pane3 = panes.add({
name: 'trunk3',
type: 'dock',
isAction: true,
});
// DockPane.container.items.map(item => console.log(item.name))
// 2 trunks + 1 outline-pane
expect(DockPane.container.items.length).toBe(4);
DockPane.activeDock(pane1);
// expect(mockDockShow).toHaveBeenCalledTimes(1);
// expect(mockDockShow).toHaveBeenLastCalledWith(pane1);
expect(DockPane.container.items[1].visible).toBeTruthy;
DockPane.activeDock(pane2);
expect(DockPane.container.items[2].visible).toBeTruthy;
// expect(mockDockShow).toHaveBeenCalledTimes(2);
// expect(mockDockShow).toHaveBeenLastCalledWith(pane2);
// expect(mockDockHide).toHaveBeenCalledTimes(1);
// expect(mockDockHide).toHaveBeenLastCalledWith(pane1);
DockPane.activeDock();
DockPane.activeDock({ name: 'unexisting' });
offDockShow();
offDockHide();
// DockPane.activeDock(pane1);
// expect(mockDockShow).toHaveBeenCalledTimes(2);
// expect(mockDockHide).toHaveBeenCalledTimes(1);
expect(typeof DockPane.getDocks).toBe('function');
DockPane.getDocks();
expect(typeof DockPane.setFixed).toBe('function');
DockPane.setFixed();
});
it('add: type action', () => {
panes.add({
name: 'trunk',
type: 'action',
init() {},
destroy() {},
});
const { ActionPane } = panes;
expect(typeof ActionPane.getActions).toBe('function');
ActionPane.getActions();
expect(typeof ActionPane.setActions).toBe('function');
ActionPane.setActions();
expect(ActionPane.getActions()).toBe(ActionPane.actions);
});
it('add: type action - extraConfig', () => {
panes.add({
name: 'trunk',
type: 'action',
init() {},
destroy() {},
}, {});
});
it('add: type action - function', () => {
panes.add(() => ({
name: 'trunk',
type: 'action',
init() {},
destroy() {},
}));
});
it('add: type tab', () => {
panes.add({
name: 'trunk',
type: 'tab',
});
const { TabPane } = panes;
expect(typeof TabPane.setFloat).toBe('function');
TabPane.setFloat();
});
it('add: type stage', () => {
panes.add({
id: 'stage1',
type: 'stage',
});
panes.add({
type: 'stage',
});
const { Stages } = panes;
expect(typeof Stages.getStage).toBe('function');
Stages.getStage();
expect(typeof Stages.createStage).toBe('function');
Stages.createStage({
id: 'stage1',
type: 'stage',
});
Stages.createStage({
type: 'stage',
});
});
it('add: type stage - id', () => {
panes.add({
id: 'trunk',
name: 'trunk',
type: 'stage',
});
});
it('add: type widget', () => {
panes.add({
name: 'trunk',
type: 'widget',
});
});
it('add: type null', () => {
panes.add({
name: 'trunk',
});
const { toolbar } = panes;
expect(typeof toolbar.setContents).toBe('function');
toolbar.setContents();
});
});

View File

@ -0,0 +1,15 @@
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 symbols from '../../src/symbols';
describe('symbols 测试', () => {
it('API', () => {
symbols.create('abc');
symbols.create('abc');
symbols.get('abc');
});
});

View File

@ -0,0 +1,189 @@
import '../fixtures/window';
import { Editor, globalContext } from '@ali/lowcode-editor-core';
import { editor } from '../../src/reducers';
import { Viewport } from '../../src/viewport';
import domready from 'domready';
// const editor = globalContext.get(Editor);
jest.mock('domready', () => {
return (fn) => fn();
});
// 貌似 jsdom 没有响应 fullscreen 变更事件,先这么 mock 吧
const mockSetFullscreen = flag => { document.fullscreen = flag; };
describe('viewport 测试', () => {
mockSetFullscreen(true);
it('getDevice / setDevice / getViewport / onDeviceChange / onViewportChange', async () => {
const viewport = new Viewport();
const mockDeviceChange = jest.fn();
const mockViewportChange = jest.fn();
const offDevice = viewport.onDeviceChange(mockDeviceChange);
const offViewport = viewport.onViewportChange(mockViewportChange);
expect(viewport.getDevice()).toBe('pc');
expect(viewport.getViewport()).toBe('design-pc');
editor.set('currentDocument', { simulator: { set() {} } });
await viewport.setDevice('mobile');
expect(viewport.getDevice()).toBe('mobile');
expect(viewport.getViewport()).toBe('design-mobile');
expect(mockDeviceChange).toHaveBeenCalledTimes(1);
expect(mockViewportChange).toHaveBeenCalledTimes(1);
offDevice();
offViewport();
await viewport.setDevice('pc');
expect(mockDeviceChange).toHaveBeenCalledTimes(1);
expect(mockViewportChange).toHaveBeenCalledTimes(1);
});
it('setPreview / isPreview / togglePreivew / getViewport / onViewportChange', () => {
const viewport = new Viewport();
const mockViewportChange = jest.fn();
const mockPreivewChange = jest.fn();
const off = viewport.onViewportChange(mockViewportChange);
const offPreview = viewport.onPreview(mockPreivewChange);
viewport.setPreview(true);
expect(viewport.isPreview).toBeTruthy;
expect(viewport.getViewport()).toBe('preview-pc');
expect(mockViewportChange).toHaveBeenCalledTimes(1);
expect(mockPreivewChange).toHaveBeenCalledTimes(1);
viewport.setPreview(false);
expect(viewport.isPreview).toBeFalsy;
expect(viewport.getViewport()).toBe('design-pc');
expect(mockViewportChange).toHaveBeenCalledTimes(2);
expect(mockPreivewChange).toHaveBeenCalledTimes(2);
viewport.togglePreview();
expect(viewport.getViewport()).toBe('preview-pc');
expect(mockViewportChange).toHaveBeenCalledTimes(3);
expect(mockPreivewChange).toHaveBeenCalledTimes(3);
viewport.togglePreview();
expect(viewport.getViewport()).toBe('design-pc');
expect(mockViewportChange).toHaveBeenCalledTimes(4);
expect(mockPreivewChange).toHaveBeenCalledTimes(4);
off();
offPreview();
viewport.togglePreview();
expect(mockViewportChange).toHaveBeenCalledTimes(4);
expect(mockPreivewChange).toHaveBeenCalledTimes(4);
});
it('setFocusTarget / returnFocus / setFocus / isFocus / onFocusChange', () => {
const viewport = new Viewport();
const mockFocusChange = jest.fn();
const off = viewport.onFocusChange(mockFocusChange);
viewport.setFocusTarget(document.createElement('div'));
viewport.returnFocus();
viewport.setFocus(true);
expect(viewport.isFocus()).toBeTruthy();
expect(mockFocusChange).toHaveBeenCalledTimes(1);
expect(mockFocusChange).toHaveBeenLastCalledWith(true);
viewport.setFocus(false);
expect(viewport.isFocus()).toBeFalsy();
expect(mockFocusChange).toHaveBeenCalledTimes(2);
expect(mockFocusChange).toHaveBeenLastCalledWith(false);
off();
viewport.setFocus(false);
expect(mockFocusChange).toHaveBeenCalledTimes(2);
});
it('isFullscreen / toggleFullscreen / setFullscreen / onFullscreenChange', () => {
const viewport = new Viewport();
const mockFullscreenChange = jest.fn();
const off = viewport.onFullscreenChange(mockFullscreenChange);
mockSetFullscreen(false);
viewport.setFullscreen(true);
mockSetFullscreen(true);
expect(viewport.isFullscreen()).toBeTruthy;
// expect(mockFullscreenChange).toHaveBeenCalledTimes(1);
viewport.setFullscreen(true);
// expect(mockFullscreenChange).toHaveBeenCalledTimes(1);
mockSetFullscreen(true);
viewport.setFullscreen(false);
mockSetFullscreen(false);
expect(viewport.isFullscreen()).toBeFalsy;
// expect(mockFullscreenChange).toHaveBeenCalledTimes(2);
viewport.setFullscreen(false);
// expect(mockFullscreenChange).toHaveBeenCalledTimes(2);
mockSetFullscreen(true);
viewport.toggleFullscreen();
mockSetFullscreen(false);
// expect(mockFullscreenChange).toHaveBeenCalledTimes(3);
viewport.toggleFullscreen();
// expect(mockFullscreenChange).toHaveBeenCalledTimes(4);
off();
viewport.toggleFullscreen();
// expect(mockFullscreenChange).toHaveBeenCalledTimes(4);
});
it('setWithShell', () => {
const viewport = new Viewport();
viewport.setWithShell();
});
it('onSlateFixedChange', () => {
const viewport = new Viewport();
const mockSlateFixedChange = jest.fn();
const off = viewport.onSlateFixedChange(mockSlateFixedChange);
viewport.emitter.emit('slatefixed');
expect(mockSlateFixedChange).toHaveBeenCalledTimes(1);
off();
viewport.emitter.emit('slatefixed');
expect(mockSlateFixedChange).toHaveBeenCalledTimes(1);
});
it('setGlobalCSS', () => {
const viewport = new Viewport();
viewport.setGlobalCSS([{
media: '*',
type: 'URL',
content: '//path/to.css',
}, {
media: 'ALL',
type: 'text',
content: 'body {font-size: 50px;}',
}, {
media: '',
type: 'text',
content: 'body {font-size: 50px;}',
}, {
media: 'mobile',
type: 'text',
content: 'body {font-size: 50px;}',
}]);
viewport.cssResourceSet[0].apply();
viewport.cssResourceSet[0].init();
viewport.cssResourceSet[1].apply();
viewport.cssResourceSet[1].apply();
viewport.cssResourceSet[1].unmount();
viewport.setGlobalCSS([{
media: '*',
type: 'URL',
content: '//path/to.css',
}, {
media: 'ALL',
type: 'text',
content: 'body {font-size: 50px;}',
}, {
media: '',
type: 'text',
content: 'body {font-size: 50px;}',
}, {
media: 'mobile',
type: 'text',
content: 'body {font-size: 50px;}',
}]);
});
});

View File

@ -0,0 +1,96 @@
import '../fixtures/window';
import { Node, Designer, getConvertedExtraKey } from '@ali/lowcode-designer';
import { Editor } from '@ali/lowcode-editor-core';
import {
compatibleReducer,
} from '../../src/props-reducers/downgrade-schema-reducer';
import formSchema from '../fixtures/schema/form';
describe('compatibleReducer 测试', () => {
it('compatibleReducer 测试', () => {
const downgradedProps = {
a: {
type: 'JSBlock',
value: {
componentName: 'Slot',
props: {
slotTitle: '标题',
slotName: 'title',
},
children: [],
},
},
c: {
c1: {
type: 'JSBlock',
value: {
componentName: 'Slot',
props: {
slotTitle: '标题',
slotName: 'title',
},
},
},
},
d: {
type: 'variable',
variable: 'state.a',
value: '111',
},
e: {
e1: {
type: 'variable',
variable: 'state.b',
value: '222',
},
e2: {
type: 'JSExpression',
value: 'state.b',
mock: '222',
events: {},
},
},
};
expect(compatibleReducer({
a: {
type: 'JSSlot',
title: '标题',
name: 'title',
value: [],
},
c: {
c1: {
type: 'JSSlot',
title: '标题',
name: 'title',
value: undefined,
},
},
d: {
type: 'JSExpression',
value: 'state.a',
mock: '111',
},
e: {
e1: {
type: 'JSExpression',
value: 'state.b',
mock: '222',
},
e2: {
type: 'JSExpression',
value: 'state.b',
mock: '222',
events: {},
},
},
})).toEqual(downgradedProps);
});
it('空值', () => {
expect(compatibleReducer(null)).toBeNull;
expect(compatibleReducer(undefined)).toBeUndefined;
expect(compatibleReducer(111)).toBe(111);
});
});

View File

@ -0,0 +1,81 @@
import '../fixtures/window';
import { Node, Designer, getConvertedExtraKey } from '@ali/lowcode-designer';
import { Editor } from '@ali/lowcode-editor-core';
import { filterReducer } from '../../src/props-reducers/filter-reducer';
import formSchema from '../fixtures/schema/form';
describe('filterReducer 测试', () => {
it('filterReducer 测试 - 有 filters', () => {
const mockNode = {
componentMeta: {
getMetadata() {
return {
experimental: {
filters: [
{
name: 'shouldBeFitlered',
filter: () => false,
},
{
name: 'keeped',
filter: () => true,
},
{
name: 'throwErr',
filter: () => { throw new Error('xxx'); },
},
{
name: 'zzz',
filter: () => true,
},
],
},
};
},
},
settingEntry: {
getProp(propName) {
return { name: propName };
},
},
};
expect(filterReducer({
shouldBeFitlered: 111,
keeped: 222,
noCorresponingFilter: 222,
throwErr: 111,
}, mockNode)).toEqual({
keeped: 222,
noCorresponingFilter: 222,
throwErr: 111,
});
});
it('filterReducer 测试 - 无 filters', () => {
const mockNode = {
componentMeta: {
getMetadata() {
return {
experimental: {
filters: [],
},
};
},
},
settingEntry: {
getProp(propName) {
return { name: propName };
},
},
};
expect(filterReducer({
shouldBeFitlered: 111,
keeped: 222,
noCorresponingFilter: 222,
}, mockNode)).toEqual({
shouldBeFitlered: 111,
keeped: 222,
noCorresponingFilter: 222,
});
});
});

View File

@ -0,0 +1,488 @@
import '../fixtures/window';
import { Node, Designer, getConvertedExtraKey } from '@ali/lowcode-designer';
import { Editor, globalContext } from '@ali/lowcode-editor-core';
import { initNodeReducer } from '../../src/props-reducers/init-node-reducer';
import formSchema from '../fixtures/schema/form';
describe('initNodeReducer 测试', () => {
it('initNodeReducer 测试 - 有 initials', () => {
const mockNode = {
componentMeta: {
getMetadata() {
return {
experimental: {
initials: [
{
name: 'propA',
initial: () => '111',
},
{
name: 'propB',
initial: () => '111',
},
{
name: 'propC',
initial: () => {
throw new Error('111');
},
},
{
name: 'propD',
initial: () => '111',
},
{
name: 'propE',
initial: () => '111',
},
{
name: 'propF',
initial: () => '111',
},
],
},
};
},
prototype: {
options: {
configure: [
{
name: 'propF',
setter: {
type: {
displayName: 'I18nSetter',
},
},
},
],
},
},
},
settingEntry: {
getProp(propName) {
return { name: propName };
},
},
props: {
has() {
return false;
},
add() {},
},
};
expect(
initNodeReducer(
{
propA: '111',
propC: '222',
propD: {
type: 'JSExpression',
mock: '111',
},
propE: {
type: 'variable',
value: '111',
},
},
mockNode,
),
).toEqual({
propA: '111',
propB: '111',
propC: '222',
propD: {
type: 'JSExpression',
mock: '111',
},
propE: {
type: 'variable',
value: '111',
},
propF: {
type: 'i18n',
use: 'zh_CN',
zh_CN: '111',
},
});
});
it('filterReducer 测试 - 无 initials', () => {
const mockNode = {
componentMeta: {
getMetadata() {
return {
experimental: {},
};
},
},
settingEntry: {
getProp(propName) {
return { name: propName };
},
},
};
expect(
initNodeReducer(
{
propA: 111,
},
mockNode,
),
).toEqual({
propA: 111,
});
});
describe('i18n', () => {
const mockNode = {
componentMeta: {
getMetadata() {
return {
experimental: {
initials: [
{
name: 'propF',
initial: () => 111,
},
],
},
};
},
},
prototype: {
options: {
configure: [
{
name: 'propF',
setter: {
type: {
displayName: 'I18nSetter',
},
},
},
],
},
},
props: {
has() {
return false;
},
add() {},
},
};
it('isI18NObject(ov): true', () => {
expect(
initNodeReducer(
{
propF: {
type: 'i18n',
zh_CN: '222',
},
},
mockNode,
),
).toEqual({
propF: {
type: 'i18n',
zh_CN: '222',
},
});
});
it('isJSExpression(ov): true', () => {
expect(
initNodeReducer(
{
propF: {
type: 'JSExpression',
value: 'state.a',
},
},
mockNode,
),
).toEqual({
propF: {
type: 'JSExpression',
value: 'state.a',
},
});
});
it('isJSBlock(ov): true', () => {
expect(
initNodeReducer(
{
propF: {
type: 'JSBlock',
value: 'state.a',
},
},
mockNode,
),
).toEqual({
propF: {
type: 'JSBlock',
value: 'state.a',
},
});
});
it('isJSSlot(ov): true', () => {
expect(
initNodeReducer(
{
propF: {
type: 'JSSlot',
value: 'state.a',
},
},
mockNode,
),
).toEqual({
propF: {
type: 'JSSlot',
value: 'state.a',
},
});
});
it('isVariable(ov): true', () => {
expect(
initNodeReducer(
{
propF: {
type: 'variable',
value: 'state.a',
},
},
mockNode,
),
).toEqual({
propF: {
type: 'variable',
value: 'state.a',
},
});
});
it('isI18NObject(v): false', () => {
const mockNode = {
componentMeta: {
getMetadata() {
return {
experimental: {
initials: [
{
name: 'propF',
initial: () => 111,
},
],
},
};
},
prototype: {
options: {
configure: [
{
name: 'propF',
setter: {
type: {
displayName: 'I18nSetter',
},
},
},
],
},
},
},
props: {
has() {
return false;
},
add() {},
},
};
expect(
initNodeReducer(
{
propF: {
type: 'variable',
value: 'state.a',
},
},
mockNode,
),
).toEqual({
propF: {
type: 'variable',
value: 'state.a',
},
});
});
it('isI18NObject(v): false', () => {
const mockNode = {
componentMeta: {
getMetadata() {
return {
experimental: {
initials: [{
name: 'propF',
initial: () => 111,
}],
},
};
},
},
prototype: {
options: {
configure: [
{
name: 'propF',
setter: {
type: {
displayName: 'I18nSetter',
},
},
},
],
},
},
props: {
has() {
return false;
},
add() {},
},
};
expect(
initNodeReducer(
{
propF: {
type: 'variable',
value: 'state.a',
},
},
mockNode,
),
).toEqual({
propF: {
type: 'variable',
value: 'state.a',
},
});
});
});
it('成功使用兼容后的 i18n 对象', () => {
const mockNode = {
componentMeta: {
getMetadata() {
return {
experimental: {
initials: [{
name: 'propF',
initial: () => {
return {
type: 'i18n',
use: 'zh_CN',
zh_CN: '111',
};
},
}],
},
};
},
prototype: {
options: {
configure: [
{
name: 'propF',
setter: {
type: {
displayName: 'I18nSetter',
},
},
},
],
},
},
},
props: {
has() {
return false;
},
add() {},
},
};
expect(
initNodeReducer(
{
propF: '111',
},
mockNode,
),
).toEqual({
propF: {
type: 'i18n',
use: 'zh_CN',
zh_CN: '111',
},
});
});
describe('fieldId', () => {
const mockNode = {
componentMeta: {
getMetadata() {
return {
experimental: {
initials: [
{
name: 'propA',
initial: () => '111',
},
],
},
};
},
},
settingEntry: {
getProp(propName) {
return { name: propName };
},
},
props: {
has() {
return false;
},
add() {},
},
};
const editor = new Editor();
globalContext.register(editor, Editor);
const designer = new Designer({ editor });
editor.set('designer', designer);
designer.project.open(formSchema);
it('fieldId - 已存在', () => {
expect(initNodeReducer({
propA: '111',
fieldId: 'form',
}, mockNode)).toEqual({
propA: '111',
fieldId: undefined,
});
});
it('fieldId - 已存在,但有全局关闭标识', () => {
window.__disable_unique_id_checker__ = true;
expect(initNodeReducer({
propA: '111',
fieldId: 'form',
}, mockNode)).toEqual({
propA: '111',
fieldId: 'form',
});
});
});
});

View File

@ -0,0 +1,78 @@
import '../fixtures/window';
import { Node, Designer, getConvertedExtraKey } from '@ali/lowcode-designer';
import { Editor, globalContext } from '@ali/lowcode-editor-core';
import { liveLifecycleReducer } from '../../src/props-reducers/live-lifecycle-reducer';
import formSchema from '../fixtures/schema/form';
const editor = new Editor();
globalContext.register(editor, Editor);
it('liveLifecycleReducer 测试 - live', () => {
const mockDidMount = jest.fn();
const mockWillUnmount = jest.fn();
editor.set('designMode', 'live');
const newProps = liveLifecycleReducer(
{
lifeCycles: {
didMount: mockDidMount,
willUnmount: mockWillUnmount,
},
},
{
isRoot() {
return true;
},
},
);
const { lifeCycles } = newProps;
expect(typeof lifeCycles.componentDidMount).toBe('function');
expect(typeof lifeCycles.componentWillUnMount).toBe('function');
lifeCycles.didMount();
lifeCycles.willUnmount();
expect(mockDidMount).toHaveBeenCalled();
expect(mockWillUnmount).toHaveBeenCalled();
});
it('liveLifecycleReducer 测试 - design', () => {
const mockDidMount = jest.fn();
const mockWillUnmount = jest.fn();
editor.set('designMode', 'design');
const newProps = liveLifecycleReducer(
{
lifeCycles: {
didMount: mockDidMount,
willUnmount: mockWillUnmount,
},
},
{
isRoot() {
return true;
},
},
);
const { lifeCycles } = newProps;
expect(lifeCycles).toEqual({});
});
it('liveLifecycleReducer 测试', () => {
const mockDidMount = jest.fn();
const mockWillUnmount = jest.fn();
editor.set('designMode', 'design');
const newProps = liveLifecycleReducer(
{
propA: '111',
},
{
isRoot() {
return true;
},
},
);
const { lifeCycles } = newProps;
expect(lifeCycles).toBeUndefined;
});

View File

@ -0,0 +1,28 @@
import '../fixtures/window';
import { nodeTopFixedReducer } from '../../src/props-reducers/node-top-fixed-reducer';
import formSchema from '../fixtures/schema/form';
it('nodeTopFixedReducer 测试', () => {
expect(
nodeTopFixedReducer(
{
propA: '111',
},
{ componentMeta: { isTopFixed: true } },
),
).toEqual({
propA: '111',
__isTopFixed__: true,
});
expect(
nodeTopFixedReducer(
{
propA: '111',
},
{ componentMeta: { } },
),
).toEqual({
propA: '111',
});
});

View File

@ -0,0 +1,62 @@
import '../fixtures/window';
import { Node, Designer, getConvertedExtraKey } from '@ali/lowcode-designer';
import { Editor, globalContext } from '@ali/lowcode-editor-core';
import { removeEmptyPropsReducer } from '../../src/props-reducers/remove-empty-prop-reducer';
import formSchema from '../fixtures/schema/form';
it('removeEmptyPropsReducer 测试', () => {
const newProps = removeEmptyPropsReducer(
{
propA: '111',
dataSource: {
online: [
{
options: {
params: [
{
name: 'propA',
value: '111',
},
{
value: '111',
},
],
},
},
],
},
},
{
isRoot() {
return true;
},
},
);
expect(newProps).toEqual({
propA: '111',
dataSource: {
online: [
{
options: {
params: [{
name: 'propA',
value: '111',
}, {
value: '111',
}],
},
},
],
list: [
{
options: {
params: {
propA: '111',
},
},
},
],
},
});
});

View File

@ -0,0 +1,121 @@
import '../fixtures/window';
import { Node, Designer, getConvertedExtraKey } from '@ali/lowcode-designer';
import { Editor, globalContext } from '@ali/lowcode-editor-core';
import { stylePropsReducer } from '../../src/props-reducers/style-reducer';
import formSchema from '../fixtures/schema/form';
const editor: Editor = new Editor();
globalContext.register(editor, Editor);
beforeEach(() => {
// const designer = new Designer({ editor });
editor.set('designer', {
currentDocument: {
simulator: {
contentDocument: document,
},
},
});
});
// designer.project.open(formSchema);
describe('stylePropsReducer 测试', () => {
it('无 style 相关属性', () => {
expect(stylePropsReducer({ propA: 1 })).toEqual({ propA: 1 });
});
it('__style__', () => {
const props = {
__style__: {
'font-size': '50px',
},
};
const mockNode = { id: 'id1' };
expect(stylePropsReducer(props, mockNode)).toEqual({
className: '_css_pesudo_id1',
__style__: {
'font-size': '50px',
},
});
expect(document.querySelector('#_style_pesudo_id1')).textContent =
'._css_pesudo_id1 { font-size: 50px; }';
});
it('__style__ - 无 contentDocument', () => {
editor.set('designer', {
currentDocument: {
simulator: {
contentDocument: undefined,
},
},
});
const props = {
__style__: {
'font-size': '50px',
},
};
const mockNode = { id: 'id11' };
expect(stylePropsReducer(props, mockNode)).toEqual({
__style__: {
'font-size': '50px',
},
});
expect(document.querySelector('#_style_pesudo_id11')).toBeNull;
});
it('__style__ - css id 已存在', () => {
const s = document.createElement('style');
s.setAttribute('type', 'text/css');
s.setAttribute('id', '_style_pesudo_id2');
document.getElementsByTagName('head')[0].appendChild(s);
s.appendChild(document.createTextNode('body {}'));
const props = {
__style__: {
'font-size': '50px',
},
};
const mockNode = { id: 'id2' };
expect(stylePropsReducer(props, mockNode)).toEqual({
className: '_css_pesudo_id2',
__style__: {
'font-size': '50px',
},
});
expect(document.querySelector('#_style_pesudo_id2')).textContent =
'._css_pesudo_id2 { font-size: 50px; }';
});
it('containerStyle', () => {
const props = {
containerStyle: {
'font-size': '50px',
},
};
const mockNode = { id: 'id3' };
expect(stylePropsReducer(props, mockNode)).toEqual({
className: '_css_pesudo_id3',
containerStyle: {
'font-size': '50px',
},
});
expect(document.querySelector('#_style_pesudo_id3')).textContent =
'._css_pesudo_id3 { font-size: 50px; }';
});
it('pageStyle', () => {
const props = {
pageStyle: {
'font-size': '50rpx',
},
};
const mockNode = { id: 'id4' };
expect(stylePropsReducer(props, mockNode)).toEqual({
className: 'engine-document',
pageStyle: {
'font-size': '50rpx',
},
});
expect(document.querySelector('#_style_pesudo_id4')).textContent =
'._css_pesudo_id4 { font-size: 50px; }';
});
});

View File

@ -0,0 +1,107 @@
import '../fixtures/window';
import { Node, Designer, getConvertedExtraKey } from '@ali/lowcode-designer';
import { Editor } from '@ali/lowcode-editor-core';
import {
upgradePropsReducer,
upgradePageLifeCyclesReducer,
} from '../../src/props-reducers/upgrade-reducer';
import formSchema from '../fixtures/schema/form';
describe('upgradePropsReducer 测试', () => {
it('upgradePropsReducer 测试', () => {
const props = {
a: {
type: 'JSBlock',
value: {
componentName: 'Slot',
props: {
slotTitle: '标题',
slotName: 'title',
},
children: [],
},
},
b: {
type: 'JSBlock',
value: {
componentName: 'Div',
props: {},
},
},
c: {
c1: {
type: 'JSBlock',
value: {
componentName: 'Slot',
props: {
slotTitle: '标题',
slotName: 'title',
},
},
},
},
d: {
type: 'variable',
variable: 'state.a',
value: '111',
},
__slot__haha: true,
};
expect(upgradePropsReducer(props)).toEqual({
a: {
type: 'JSSlot',
title: '标题',
name: 'title',
value: [],
},
b: {
componentName: 'Div',
props: {},
},
c: {
c1: {
type: 'JSSlot',
title: '标题',
name: 'title',
value: undefined,
},
},
d: {
type: 'JSExpression',
value: 'state.a',
mock: '111',
},
});
});
it('空值', () => {
expect(upgradePropsReducer(null)).toBeNull;
expect(upgradePropsReducer(undefined)).toBeUndefined;
});
});
const editor = new Editor();
const designer = new Designer({ editor });
designer.project.open(formSchema);
it('upgradePageLifeCyclesReducer 测试', () => {
const rootNode = designer.currentDocument?.rootNode;
const mockDidMount = jest.fn();
const mockWillUnmount = jest.fn();
upgradePageLifeCyclesReducer({
didMount: mockDidMount,
willUnmount: mockWillUnmount,
}, rootNode);
const lifeCycles = rootNode?.getPropValue(getConvertedExtraKey('lifeCycles'));
expect(typeof lifeCycles.didMount).toBe('function');
expect(typeof lifeCycles.willUnmount).toBe('function');
lifeCycles.didMount();
lifeCycles.willUnmount();
expect(mockDidMount).toHaveBeenCalled();
expect(mockWillUnmount).toHaveBeenCalled();
});

Some files were not shown because too many files have changed in this diff Show More