diff --git a/packages/engine/.eslintrc.js b/packages/engine/.eslintrc.js new file mode 100644 index 000000000..4c845e2de --- /dev/null +++ b/packages/engine/.eslintrc.js @@ -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, + } +} \ No newline at end of file diff --git a/packages/engine/README.md b/packages/engine/README.md new file mode 100644 index 000000000..688968e8b --- /dev/null +++ b/packages/engine/README.md @@ -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..xxx +通配 view.xxx + +universal + +规则 2 +urls: "view.js,view2 , view3 ", +urls: [ + "view.js", + "view.js *", + "view1.js mobile|pc", + "view2.js " +] + +环境通用资源 + +"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 信息都可以丢掉 diff --git a/packages/engine/build.json b/packages/engine/build.json new file mode 100644 index 000000000..e7b2baf07 --- /dev/null +++ b/packages/engine/build.json @@ -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" + ] +} diff --git a/packages/engine/build.plugin.js b/packages/engine/build.plugin.js new file mode 100644 index 000000000..ba3096f14 --- /dev/null +++ b/packages/engine/build.plugin.js @@ -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); + }); +}; diff --git a/packages/engine/build.test.json b/packages/engine/build.test.json new file mode 100644 index 000000000..4bde6acd1 --- /dev/null +++ b/packages/engine/build.test.json @@ -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" + ] +} diff --git a/packages/engine/jest.config.js b/packages/engine/jest.config.js new file mode 100644 index 000000000..31a6eab6e --- /dev/null +++ b/packages/engine/jest.config.js @@ -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/**', + ], +}; diff --git a/packages/engine/package.json b/packages/engine/package.json new file mode 100644 index 000000000..3bc316b34 --- /dev/null +++ b/packages/engine/package.json @@ -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" + } +} diff --git a/packages/engine/scripts/version.js b/packages/engine/scripts/version.js new file mode 100644 index 000000000..57723de1d --- /dev/null +++ b/packages/engine/scripts/version.js @@ -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)); diff --git a/packages/engine/src/editor.ts b/packages/engine/src/editor.ts new file mode 100644 index 000000000..685aa16b0 --- /dev/null +++ b/packages/engine/src/editor.ts @@ -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, +}); \ No newline at end of file diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts new file mode 100644 index 000000000..2c0b8e427 --- /dev/null +++ b/packages/engine/src/index.ts @@ -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;', +); + diff --git a/packages/engine/tsconfig.json b/packages/engine/tsconfig.json new file mode 100644 index 000000000..c37b76ecc --- /dev/null +++ b/packages/engine/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "lib" + }, + "include": [ + "./src/" + ] +} diff --git a/packages/ignitor/build.json b/packages/ignitor/build.json index d8349e4d4..3a098e387 100644 --- a/packages/ignitor/build.json +++ b/packages/ignitor/build.json @@ -1,6 +1,8 @@ { "entry": { "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", "rax-simulator-renderer": "../rax-simulator-renderer/src/index.ts" }, diff --git a/packages/vision-polyfill/.eslintrc.js b/packages/vision-polyfill/.eslintrc.js new file mode 100644 index 000000000..4c845e2de --- /dev/null +++ b/packages/vision-polyfill/.eslintrc.js @@ -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, + } +} \ No newline at end of file diff --git a/packages/vision-polyfill/CHANGELOG.md b/packages/vision-polyfill/CHANGELOG.md new file mode 100644 index 000000000..111393025 --- /dev/null +++ b/packages/vision-polyfill/CHANGELOG.md @@ -0,0 +1,1146 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + + +## [1.0.29](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.28-beta.2...v1.0.29) (2021-01-05) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.28-beta.2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.28-beta.1...v1.0.28-beta.2) (2021-01-04) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.28-beta.1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.27...v1.0.28-beta.1) (2021-01-04) + + +### Features + +* 支持新版的 plugin 机制 ([1e8fc63](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/1e8fc63)) + + + + + +## [1.0.28-beta.0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.27...v1.0.28-beta.0) (2021-01-04) + + +### Features + +* 支持新版的 plugin 机制 ([1e8fc63](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/1e8fc63)) + + + + + +## [1.0.27](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.27-beta.2...v1.0.27) (2020-12-24) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.27-beta.2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.27-beta.1...v1.0.27-beta.2) (2020-12-23) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.27-beta.1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.27-beta.0...v1.0.27-beta.1) (2020-12-23) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.27-beta.0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.26...v1.0.27-beta.0) (2020-12-23) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.26](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.26-beta.1...v1.0.26) (2020-12-22) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.26-beta.1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.26-beta.0...v1.0.26-beta.1) (2020-12-22) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.26-beta.0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.25-beta.1...v1.0.26-beta.0) (2020-12-22) + + +### Bug Fixes + +* 修复 overridePropsConfigure 参数为数组时的逻辑 ([4e58e09](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/4e58e09)) + + +### Features + +* 支持 build sourceMap, 方便用户调试 ([6bf75cd](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/6bf75cd)) +* 支持用户修改 builtinComponentActions ([bc183d1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bc183d1)) + + + + + +## [1.0.25-beta.1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.24-beta.4...v1.0.25-beta.1) (2020-12-15) + + +### Bug Fixes + +* 修复大纲树和组件面板来回点击异常 ([8b9a6ec](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/8b9a6ec)) + + + + + +## [1.0.24-beta.4](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.24-beta.3...v1.0.24-beta.4) (2020-12-14) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.24-beta.3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.24-beta.2...v1.0.24-beta.3) (2020-12-11) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.24-beta.2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.24-beta.1...v1.0.24-beta.2) (2020-12-10) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.24-beta.1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.24-beta.0...v1.0.24-beta.1) (2020-12-09) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.24-beta.0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.23...v1.0.24-beta.0) (2020-12-09) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.23](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.23-beta.2...v1.0.23) (2020-12-08) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.23-beta.5](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.23-beta.4...v1.0.23-beta.5) (2020-12-08) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.23-beta.4](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.23-beta.3...v1.0.23-beta.4) (2020-12-08) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.23-beta.3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.23-beta.2...v1.0.23-beta.3) (2020-12-08) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.23-beta.2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.23-beta.1...v1.0.23-beta.2) (2020-12-08) + + +### Bug Fixes + +* editor-core 统一版本 ([edd4129](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/edd4129)) +* 补充 [@ali](https://gitlab.alibaba-inc.com/ali)/lowcode-editor-setters 依赖 ([c25c014](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c25c014)) + + + + + +## [1.0.23-beta.1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v0.13.1-29...v1.0.23-beta.1) (2020-12-07) + + +### Bug Fixes + +* fix bug ([113e409](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/113e409)) + + +### Features + +* 合入 trunk-vision 代码 ([ea6bc7a](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ea6bc7a)) + + + + + +## [1.0.23-beta.0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v0.13.1-29...v1.0.23-beta.0) (2020-12-07) + + +### Bug Fixes + +* fix bug ([113e409](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/113e409)) + + +### Features + +* 合入 trunk-vision 代码 ([ea6bc7a](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ea6bc7a)) + + + + + +## [1.0.22](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.21...@ali/lowcode-editor-preset-vision@1.0.22) (2020-11-16) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.21](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.20...@ali/lowcode-editor-preset-vision@1.0.21) (2020-11-10) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.20](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.19...@ali/lowcode-editor-preset-vision@1.0.20) (2020-11-10) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.19](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.18...@ali/lowcode-editor-preset-vision@1.0.19) (2020-11-05) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.18](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.17...@ali/lowcode-editor-preset-vision@1.0.18) (2020-11-05) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.17](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.16...@ali/lowcode-editor-preset-vision@1.0.17) (2020-11-05) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.16](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.15...@ali/lowcode-editor-preset-vision@1.0.16) (2020-11-04) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.15](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.13...@ali/lowcode-editor-preset-vision@1.0.15) (2020-11-04) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.14](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.13...@ali/lowcode-editor-preset-vision@1.0.14) (2020-11-04) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.13](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.12...@ali/lowcode-editor-preset-vision@1.0.13) (2020-11-02) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [0.12.1-3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v0.12.1-2...v0.12.1-3) (2020-10-12) + + +### Bug Fixes + +* 去掉 flags ([75fc3c6](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/75fc3c6)) +* 处理 JSExpreesion 的 i18n 场景 ([9b87407](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/9b87407)) + + + + + +## [0.12.1-2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v0.12.1-1...v0.12.1-2) (2020-09-23) + + +### Bug Fixes + +* 合并分支 ([add2f23](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/add2f23)) +* fix bug ([113e409](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/113e409)) +* i18n 绑定变量后消失 ([0aafafe](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/0aafafe)) + + + + + +## [1.0.11](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.10...@ali/lowcode-editor-preset-vision@1.0.11) (2020-10-19) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.10](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.9...@ali/lowcode-editor-preset-vision@1.0.10) (2020-09-29) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.9](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.8...@ali/lowcode-editor-preset-vision@1.0.9) (2020-09-28) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.9-1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.9-0...v1.0.9-1) (2020-09-14) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## 1.0.9-0 (2020-09-14) + + +### Bug Fixes + +* fieldId 重复问题 ([e761b1a](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e761b1a)) +* 🐛 eslint ([14803dd](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/14803dd)) +* 🐛 eslint ([e3ca0bd](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e3ca0bd)) +* 🐛 use intl ([a22e66a](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/a22e66a)) +* 🐛 用 isI18nData 判断 meta title ([732bccf](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/732bccf)) +* 🐛 解决点击组件时无法聚焦到点中的组件上的问题 ([852d882](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/852d882)) +* 🐛 逻辑简化 ([710f3ba](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/710f3ba)) +* add extraEnv ([9058ac8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/9058ac8)) +* compatiable bug ([45574db](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/45574db)) +* compatiable old VE api ([45af1c5](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/45af1c5)) +* compatiableReducer 递归 ([e905928](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e905928)) +* createComponent 支持所有 schema ([7f946f5](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7f946f5)) +* currentPage.id 返回 formUuid ([775725d](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/775725d)) +* fieldId 重复 ([5d64312](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/5d64312)) +* fieldId 重置bug ([31215da](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/31215da)) +* formUuid 可能不在 url 中 ([8657ab8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/8657ab8)) +* i18n parser & setting ([dbdd9e4](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/dbdd9e4)) +* modify layout props ([9baba75](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/9baba75)) +* patch prototype ([f20bfaa](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f20bfaa)) +* render children ([487f257](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/487f257)) +* settingField items is empty when type is not 'group' ([582c41a](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/582c41a)) +* slot 兼容问题 + loop key bug fix ([bc64017](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bc64017)) +* style ([4694331](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/4694331)) +* Trunk add getSetter ([b6d64c3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/b6d64c3)) +* Trunk.getSetter return ReactElement ([34bf71d](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/34bf71d)) +* upgradePropsReducer ([e68977f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e68977f)) +* variable init bug ([6d55bd3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/6d55bd3)) +* vc-filter bug fix ([31ea5d5](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/31ea5d5)) +* vision API 兼容 DockPane.getDocks() ([f72fb66](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f72fb66)) +* vision prop 初始化时有依赖已初始化的 prop,需要实时添加 ([1feb46f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/1feb46f)) +* vision 大包 window 指向问题 ([aa1b526](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/aa1b526)) +* 不对外暴露 Node ([05957ce](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/05957ce)) +* 不应该限定 parent 才做解绑操作 ([2e616e3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2e616e3)) +* 使用深拷贝赋值并修改 dataSource.list 避免影响 legao 现有逻辑 ([82c5d2e](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/82c5d2e)) +* 保存区块按钮渲染异常 ([33a7227](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/33a7227)) +* 修复 initial 重复、type = 'composite' 时 items 为空 ([bf79e63](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bf79e63)) +* 修复 preset-vision 版本 lifeCycles 丢失以及 slot 初始化问题 ([7cf6d24](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7cf6d24)) +* 修复 slot 获取初始值异常的 bug ([63b19f1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/63b19f1)) +* 修复低代码组件设计器、区块设计器根节点为 Page 的问题,修复 topArea 样式 ([e85b542](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e85b542)) +* 修复组件面板详情加载不了的 bug ([cca3309](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/cca3309)) +* 修复获取 currentPage 的逻辑 ([d8221db](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d8221db)) +* 兼容 listSetter 内部变量,修复回退 fieldId 重置问题 ([c95e618](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c95e618)) +* 兼容 rpx ([5050af7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/5050af7)) +* 兼容 variable 历史数据格式 ([d666317](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d666317)) +* 兼容事件绑定 ([f4c07af](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f4c07af)) +* 兼容原来 prototype 的 componentName/view ([d542a40](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d542a40)) +* 区块组件无法删除 ([d936d2b](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d936d2b)) +* 可以降级到历史的 JSBlock 格式 ([af1746b](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/af1746b)) +* 右侧配置面板样式修复 ([05f62da](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/05f62da)) +* 合并master分支 ([bd2c6ad](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bd2c6ad)) +* 在 renderer 层面做 function component 包装,避免影响 rax 等其他场景 ([1f920dd](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/1f920dd)) +* 增加兼容 API ([2960446](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2960446)) +* 处理 function component 无法选中的问题,本质上是没有 ref ([fa94aab](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/fa94aab)) +* 支持 AC 组件 ([c287bad](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c287bad)) +* 支持事件 VE_EVENTS.VE_PAGE_PAGE_READY ([935ffad](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/935ffad)) +* **editor-skeleton:** add canSetFixed prop to panel config ([1b57d5c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/1b57d5c)) +* 支持页面回滚 ([5d7dc2f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/5d7dc2f)) +* 框架样式调整 ([58790c5](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/58790c5)) +* 用户在动态修改 prototype 时也需要重新计算 meta ([66c21c0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/66c21c0)) +* 移除 isInSimulator 函数 ([6370889](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/6370889)) +* 简化 onPageReady 实现逻辑 ([a36e5f2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/a36e5f2)) +* 补全 packageName, 否则在组件面板会被隐藏 ([88e5008](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/88e5008)) +* 调整 upgrade 和 init 的流程 ([09fc1a0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/09fc1a0)) +* 调整保存成功弹出框位置 ([5198dae](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/5198dae)) +* 页面加载之后就被标记位 isModified ([2840d27](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2840d27)) + + +### Features + +* 🎸 prototype getTitle 支持 i18n ([18807ab](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/18807ab)) +* complete live-editing expr & i18n ([3ac08ba](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/3ac08ba)) +* export Monitor ([51025f0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/51025f0)) +* get layout config from legao-design ([b9103a2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/b9103a2)) +* JSexpression props ([26f4fb1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/26f4fb1)) +* live mode lifeCycles ([66f0c79](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/66f0c79)) +* live 模式取消 mock 兼容 ([ab66fd4](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ab66fd4)) +* merge live mode ([92c3039](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/92c3039)) +* register-defaults 改为可选项 ([2195797](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2195797)) +* support prop.autorun ([c0a5235](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c0a5235)) +* support subtreeModified ([7eeb51c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7eeb51c)) +* ve事件埋点 ([700e5b0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/700e5b0)) +* 在 editor-preset-vision 中对 legao schema 进行向前兼容 ([7867917](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7867917)) +* 增加 defaultFixed,面板可默认固定 ([eb51b5e](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/eb51b5e)) +* 支持 entry 模式 ([fe1f6f1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/fe1f6f1)) +* 支持多 pages 的 schema 结构 ([d9b5adb](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d9b5adb)) +* 适配 webtable ([91f1702](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/91f1702)) +* 适配乐高 OneApi 数据源,将 options.params 从 Array 改为 Object ([aa135c0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/aa135c0)) + + + + + +## [1.0.8-0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.57...@ali/lowcode-editor-preset-vision@1.0.8-0) (2020-09-09) + + +### Bug Fixes + +* 不应该限定 parent 才做解绑操作 ([2e616e3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2e616e3)) +* 合并master分支 ([bd2c6ad](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bd2c6ad)) + + + + + +## [1.0.7-0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.6-0...@ali/lowcode-editor-preset-vision@1.0.7-0) (2020-09-02) + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.6-0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.54...@ali/lowcode-editor-preset-vision@1.0.6-0) (2020-09-02) + + +## [0.8.57](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.56...@ali/lowcode-editor-preset-vision@0.8.57) (2020-09-08) + + +### Bug Fixes + +* 移除 isInSimulator 函数 ([6370889](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/6370889)) +* 补全 packageName, 否则在组件面板会被隐藏 ([88e5008](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/88e5008)) + + + +## [0.8.56](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.55...@ali/lowcode-editor-preset-vision@0.8.56) (2020-09-03) + + +### Bug Fixes + +* 合并master分支 ([bd2c6ad](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bd2c6ad)) +* 用户在动态修改 prototype 时也需要重新计算 meta ([66c21c0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/66c21c0)) + + + + + +## [0.8.55](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.54...@ali/lowcode-editor-preset-vision@0.8.55) (2020-09-03) + +## [1.0.5-0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.4-0...@ali/lowcode-editor-preset-vision@1.0.5-0) (2020-08-20) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.4-0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.3-0...@ali/lowcode-editor-preset-vision@1.0.4-0) (2020-08-20) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.3-0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.2-0...@ali/lowcode-editor-preset-vision@1.0.3-0) (2020-08-20) + +## [0.8.54](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.53...@ali/lowcode-editor-preset-vision@0.8.54) (2020-08-27) + + +### Bug Fixes + +* 在 renderer 层面做 function component 包装,避免影响 rax 等其他场景 ([1f920dd](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/1f920dd)) +* **editor-skeleton:** add canSetFixed prop to panel config ([1b57d5c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/1b57d5c)) + + + + + +## [0.8.53](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.52...@ali/lowcode-editor-preset-vision@0.8.53) (2020-08-26) + + +### Bug Fixes + +* 处理 function component 无法选中的问题,本质上是没有 ref ([fa94aab](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/fa94aab)) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.2-0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.1-0...@ali/lowcode-editor-preset-vision@1.0.2-0) (2020-08-20) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [1.0.1-0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.49...@ali/lowcode-editor-preset-vision@1.0.1-0) (2020-08-20) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +# [1.0.0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.13.0...@ali/lowcode-editor-preset-vision@1.0.0) (2020-08-17) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +# [0.13.0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.12.0...@ali/lowcode-editor-preset-vision@0.13.0) (2020-08-17) + +## [0.8.50](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.49...@ali/lowcode-editor-preset-vision@0.8.50) (2020-08-20) + + +### Bug Fixes + +* 兼容原来 prototype 的 componentName/view ([d542a40](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d542a40)) + + + + + +## [0.8.49](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.48...@ali/lowcode-editor-preset-vision@0.8.49) (2020-08-19) + + +### Bug Fixes + +* 修复获取 currentPage 的逻辑 ([d8221db](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d8221db)) + + +### Features + +* register-defaults 改为可选项 ([2195797](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2195797)) + + + + + +## [0.8.48](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.47...@ali/lowcode-editor-preset-vision@0.8.48) (2020-08-19) + + +### Bug Fixes + +* compatiableReducer 递归 ([e905928](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e905928)) + + + + + +## [0.8.47](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.46...@ali/lowcode-editor-preset-vision@0.8.47) (2020-08-19) + + +### Bug Fixes + +* currentPage.id 返回 formUuid ([775725d](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/775725d)) +* formUuid 可能不在 url 中 ([8657ab8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/8657ab8)) +* 区块组件无法删除 ([d936d2b](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d936d2b)) + + + + + +## [0.8.46](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.45...@ali/lowcode-editor-preset-vision@0.8.46) (2020-08-17) + + +### Bug Fixes + +* fieldId 重复 ([5d64312](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/5d64312)) +* 页面加载之后就被标记位 isModified ([2840d27](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2840d27)) + + + + + +## [0.8.45](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.44...@ali/lowcode-editor-preset-vision@0.8.45) (2020-08-14) + + +### Bug Fixes + +* 兼容 rpx ([5050af7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/5050af7)) + + + + + +## [0.8.44](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.43...@ali/lowcode-editor-preset-vision@0.8.44) (2020-08-14) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +# [0.12.0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.10.0...@ali/lowcode-editor-preset-vision@0.12.0) (2020-08-17) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +# [0.11.0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.10.0...@ali/lowcode-editor-preset-vision@0.11.0) (2020-08-17) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +# [0.10.0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.9.0...@ali/lowcode-editor-preset-vision@0.10.0) (2020-08-16) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +# [0.9.0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.38...@ali/lowcode-editor-preset-vision@0.9.0) (2020-08-14) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [0.8.38](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.37...@ali/lowcode-editor-preset-vision@0.8.38) (2020-08-04) + + +### Bug Fixes + +* 🐛 解决点击组件时无法聚焦到点中的组件上的问题 ([852d882](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/852d882)) + + + + + +## [0.8.37](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.35...@ali/lowcode-editor-preset-vision@0.8.37) (2020-08-04) + + +### Bug Fixes + +* 修复 slot 获取初始值异常的 bug ([63b19f1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/63b19f1)) +* 兼容 listSetter 内部变量,修复回退 fieldId 重置问题 ([c95e618](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c95e618)) + + + + + +## [0.8.36](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.35...@ali/lowcode-editor-preset-vision@0.8.36) (2020-08-04) + + +### Bug Fixes + +* 修复 slot 获取初始值异常的 bug ([63b19f1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/63b19f1)) +* 兼容 listSetter 内部变量,修复回退 fieldId 重置问题 ([c95e618](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c95e618)) + + + + + +## [0.8.35](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.34...@ali/lowcode-editor-preset-vision@0.8.35) (2020-07-29) + + +### Bug Fixes + +* slot 兼容问题 + loop key bug fix ([bc64017](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bc64017)) + + + + + +## [0.8.34](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.33...@ali/lowcode-editor-preset-vision@0.8.34) (2020-07-28) + + +### Bug Fixes + +* vc-filter bug fix ([31ea5d5](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/31ea5d5)) + + +### Features + +* 支持 entry 模式 ([fe1f6f1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/fe1f6f1)) + + + + + +## [0.8.33](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.32...@ali/lowcode-editor-preset-vision@0.8.33) (2020-07-23) + + +### Bug Fixes + +* vision prop 初始化时有依赖已初始化的 prop,需要实时添加 ([1feb46f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/1feb46f)) + + + + + +## [0.8.32](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.31...@ali/lowcode-editor-preset-vision@0.8.32) (2020-07-22) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [0.8.31](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.30...@ali/lowcode-editor-preset-vision@0.8.31) (2020-07-21) + + +### Bug Fixes + +* settingField items is empty when type is not 'group' ([582c41a](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/582c41a)) +* 修复 initial 重复、type = 'composite' 时 items 为空 ([bf79e63](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bf79e63)) +* 修复组件面板详情加载不了的 bug ([cca3309](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/cca3309)) +* 兼容 variable 历史数据格式 ([d666317](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d666317)) +* 兼容事件绑定 ([f4c07af](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f4c07af)) +* 调整 upgrade 和 init 的流程 ([09fc1a0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/09fc1a0)) + + +### Features + +* support subtreeModified ([7eeb51c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7eeb51c)) + + + + + +## [0.8.30](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.29...@ali/lowcode-editor-preset-vision@0.8.30) (2020-07-21) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [0.8.29](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.28...@ali/lowcode-editor-preset-vision@0.8.29) (2020-07-21) + + +### Bug Fixes + +* 🐛 eslint ([e3ca0bd](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e3ca0bd)) +* 🐛 eslint ([14803dd](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/14803dd)) +* 🐛 use intl ([a22e66a](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/a22e66a)) +* 🐛 用 isI18nData 判断 meta title ([732bccf](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/732bccf)) +* 🐛 逻辑简化 ([710f3ba](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/710f3ba)) +* 可以降级到历史的 JSBlock 格式 ([af1746b](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/af1746b)) + + +### Features + +* 🎸 prototype getTitle 支持 i18n ([18807ab](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/18807ab)) + + + + + +## [0.8.28](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.27...@ali/lowcode-editor-preset-vision@0.8.28) (2020-07-14) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [0.8.27](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.26...@ali/lowcode-editor-preset-vision@0.8.27) (2020-07-13) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [0.8.26](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.25...@ali/lowcode-editor-preset-vision@0.8.26) (2020-07-12) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [0.8.25](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.24...@ali/lowcode-editor-preset-vision@0.8.25) (2020-07-12) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [0.8.24](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.23...@ali/lowcode-editor-preset-vision@0.8.24) (2020-06-24) + + +### Bug Fixes + +* variable init bug ([6d55bd3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/6d55bd3)) + + + + + +## [0.8.23](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.22...@ali/lowcode-editor-preset-vision@0.8.23) (2020-06-23) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [0.8.22](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.21...@ali/lowcode-editor-preset-vision@0.8.22) (2020-06-23) + + +### Bug Fixes + +* add extraEnv ([9058ac8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/9058ac8)) +* compatiable bug ([45574db](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/45574db)) +* Trunk add getSetter ([b6d64c3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/b6d64c3)) +* Trunk.getSetter return ReactElement ([34bf71d](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/34bf71d)) +* 修复低代码组件设计器、区块设计器根节点为 Page 的问题,修复 topArea 样式 ([e85b542](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e85b542)) +* 右侧配置面板样式修复 ([05f62da](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/05f62da)) +* 支持事件 VE_EVENTS.VE_PAGE_PAGE_READY ([935ffad](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/935ffad)) +* 支持页面回滚 ([5d7dc2f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/5d7dc2f)) +* 简化 onPageReady 实现逻辑 ([a36e5f2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/a36e5f2)) + + +### Features + +* export Monitor ([51025f0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/51025f0)) + + + + + +## [0.8.21](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.20...@ali/lowcode-editor-preset-vision@0.8.21) (2020-06-16) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [0.8.20](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.19...@ali/lowcode-editor-preset-vision@0.8.20) (2020-06-15) + + +### Bug Fixes + +* compatiable old VE api ([45af1c5](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/45af1c5)) +* i18n parser & setting ([dbdd9e4](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/dbdd9e4)) +* patch prototype ([f20bfaa](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f20bfaa)) +* render children ([487f257](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/487f257)) +* style ([4694331](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/4694331)) +* 调整保存成功弹出框位置 ([5198dae](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/5198dae)) + + +### Features + +* complete live-editing expr & i18n ([3ac08ba](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/3ac08ba)) +* support prop.autorun ([c0a5235](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c0a5235)) +* ve事件埋点 ([700e5b0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/700e5b0)) + + + + + +## [0.8.19](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.18...@ali/lowcode-editor-preset-vision@0.8.19) (2020-05-20) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [0.8.18](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.17...@ali/lowcode-editor-preset-vision@0.8.18) (2020-05-19) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [0.8.17](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.16...@ali/lowcode-editor-preset-vision@0.8.17) (2020-05-18) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [0.8.16](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@0.8.15...@ali/lowcode-editor-preset-vision@0.8.16) (2020-05-18) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## 0.8.15 (2020-05-18) + + + + +**Note:** Version bump only for package @ali/lowcode-editor-preset-vision + + +## [0.8.14](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-vision-preset@0.8.13...@ali/lowcode-vision-preset@0.8.14) (2020-05-16) + + + + +**Note:** Version bump only for package @ali/lowcode-vision-preset + + +## [0.8.13](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-vision-preset@0.8.12...@ali/lowcode-vision-preset@0.8.13) (2020-05-16) + + + + +**Note:** Version bump only for package @ali/lowcode-vision-preset + + +## [0.8.12](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-vision-preset@0.8.11...@ali/lowcode-vision-preset@0.8.12) (2020-05-16) + + + + +**Note:** Version bump only for package @ali/lowcode-vision-preset + + +## [0.8.11](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-vision-preset@0.8.10...@ali/lowcode-vision-preset@0.8.11) (2020-05-15) + + +### Features + +* add filter reducer ([17c6ed3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/17c6ed3)) +* change reducer stage ([c2e83c7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c2e83c7)) + + + + + +## [0.8.10](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-vision-preset@0.8.9...@ali/lowcode-vision-preset@0.8.10) (2020-05-15) + + + + +**Note:** Version bump only for package @ali/lowcode-vision-preset + + +## [0.8.9](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-vision-preset@0.8.8...@ali/lowcode-vision-preset@0.8.9) (2020-05-15) + + +### Bug Fixes + +* add pages.toData method ([95d3cb3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/95d3cb3)) +* lc-borders-actions ([56d9f5f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/56d9f5f)) + + + + + +## [0.8.8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-vision-preset@0.8.7...@ali/lowcode-vision-preset@0.8.8) (2020-05-13) + + + + +**Note:** Version bump only for package @ali/lowcode-vision-preset + + +## [0.8.7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-vision-preset@0.8.6...@ali/lowcode-vision-preset@0.8.7) (2020-05-13) + + +### Bug Fixes + +* supports ([371b84c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/371b84c)) + + +### Features + +* add label for i18n setter in slots ([b298c18](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/b298c18)) +* support global inline editing ([4f7179b](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/4f7179b)) +* support plaintext liveediting ([ea62f12](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ea62f12)) +* 支持body和背景样式 ([661d98d](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/661d98d)) + + + + + +## [0.8.6](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-vision-preset@0.8.5...@ali/lowcode-vision-preset@0.8.6) (2020-05-08) + + + + +**Note:** Version bump only for package @ali/lowcode-vision-preset + + +## [0.8.5](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-vision-preset@0.8.4...@ali/lowcode-vision-preset@0.8.5) (2020-05-08) + + + + +**Note:** Version bump only for package @ali/lowcode-vision-preset + + +## [0.8.4](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-vision-preset@0.8.3...@ali/lowcode-vision-preset@0.8.4) (2020-05-07) + + +### Bug Fixes + +* 🐛 i18n面板不生效 ([27cd916](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/27cd916)) +* 🐛 修复区块面板命名冲突的问题 ([de50ebf](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/de50ebf)) +* 🐛 修复富文本高级内容弹层样式问题 ([edb480d](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/edb480d)) +* 🐛 绑定动作无法打开代码面板 ([160d6f7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/160d6f7)) + + +### Features + +* 🎸 增加一个hover事件效果 ([da5dd1a](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/da5dd1a)) +* 🎸 增加节点选择组件调用入口 ([e945d79](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e945d79)) +* 🎸 旧的组件无法继续沿用,增加了一个节点选择组件 ([f042041](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f042041)) + + + + + +## [0.8.3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-vision-preset@0.8.2...@ali/lowcode-vision-preset@0.8.3) (2020-04-27) + + + + +**Note:** Version bump only for package @ali/lowcode-vision-preset + + +## [0.8.2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-vision-preset@0.8.1...@ali/lowcode-vision-preset@0.8.2) (2020-04-27) + + + + +**Note:** Version bump only for package @ali/lowcode-vision-preset + + +## 0.8.1 (2020-04-27) + + + + +**Note:** Version bump only for package @ali/lowcode-vision-preset + + +## [0.8.2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-vision-polyfill@0.8.1...@ali/lowcode-vision-polyfill@0.8.2) (2020-04-16) + + + + +**Note:** Version bump only for package @ali/lowcode-vision-polyfill + + +## 0.8.1 (2020-04-15) + + +### Bug Fixes + +* editor ([ccd9162](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ccd9162)) +* plugin-designer ([c0c8760](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c0c8760)) +* plugin-designer ([2dfbcd4](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2dfbcd4)) + + +### Features + +* 🎸 ployfill for vision ([41a0647](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/41a0647)) +* **vision-polyfill:** add context ([f724487](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f724487)) +* **vision-polyfill:** add context as portal ([bd12730](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bd12730)) +* **vision-polyfill:** support polyfill of vision package ([204fdfe](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/204fdfe)) +* 🎸 polyfill exchange ([7070557](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7070557)) +* 🎸 polyfill exchange ([286e7d8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/286e7d8)) +* Exchange ([ef5a72e](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ef5a72e)) +* run vision polyfill ([33750b7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/33750b7)) diff --git a/packages/vision-polyfill/README.md b/packages/vision-polyfill/README.md new file mode 100644 index 000000000..688968e8b --- /dev/null +++ b/packages/vision-polyfill/README.md @@ -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..xxx +通配 view.xxx + +universal + +规则 2 +urls: "view.js,view2 , view3 ", +urls: [ + "view.js", + "view.js *", + "view1.js mobile|pc", + "view2.js " +] + +环境通用资源 + +"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 信息都可以丢掉 diff --git a/packages/vision-polyfill/build.json b/packages/vision-polyfill/build.json new file mode 100644 index 000000000..a1ab0f015 --- /dev/null +++ b/packages/vision-polyfill/build.json @@ -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" + ] +} diff --git a/packages/vision-polyfill/build.plugin.js b/packages/vision-polyfill/build.plugin.js new file mode 100644 index 000000000..ba3096f14 --- /dev/null +++ b/packages/vision-polyfill/build.plugin.js @@ -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); + }); +}; diff --git a/packages/vision-polyfill/build.test.json b/packages/vision-polyfill/build.test.json new file mode 100644 index 000000000..4bde6acd1 --- /dev/null +++ b/packages/vision-polyfill/build.test.json @@ -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" + ] +} diff --git a/packages/vision-polyfill/jest.config.js b/packages/vision-polyfill/jest.config.js new file mode 100644 index 000000000..31a6eab6e --- /dev/null +++ b/packages/vision-polyfill/jest.config.js @@ -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/**', + ], +}; diff --git a/packages/vision-polyfill/package.json b/packages/vision-polyfill/package.json new file mode 100644 index 000000000..7c6bab0c9 --- /dev/null +++ b/packages/vision-polyfill/package.json @@ -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" + } +} diff --git a/packages/vision-polyfill/scripts/version.js b/packages/vision-polyfill/scripts/version.js new file mode 100644 index 000000000..57723de1d --- /dev/null +++ b/packages/vision-polyfill/scripts/version.js @@ -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)); diff --git a/packages/vision-polyfill/src/base/base.ts b/packages/vision-polyfill/src/base/base.ts new file mode 100644 index 000000000..3c3616a91 --- /dev/null +++ b/packages/vision-polyfill/src/base/base.ts @@ -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); + } +} diff --git a/packages/vision-polyfill/src/base/const.ts b/packages/vision-polyfill/src/base/const.ts new file mode 100644 index 000000000..b0eb8cd12 --- /dev/null +++ b/packages/vision-polyfill/src/base/const.ts @@ -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', +}; diff --git a/packages/vision-polyfill/src/base/schemaManager.ts b/packages/vision-polyfill/src/base/schemaManager.ts new file mode 100644 index 000000000..7a0a90867 --- /dev/null +++ b/packages/vision-polyfill/src/base/schemaManager.ts @@ -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; + } +} diff --git a/packages/vision-polyfill/src/base/visualDesigner.ts b/packages/vision-polyfill/src/base/visualDesigner.ts new file mode 100644 index 000000000..a210b6737 --- /dev/null +++ b/packages/vision-polyfill/src/base/visualDesigner.ts @@ -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); + } +} diff --git a/packages/vision-polyfill/src/base/visualManager.ts b/packages/vision-polyfill/src/base/visualManager.ts new file mode 100644 index 000000000..737120f20 --- /dev/null +++ b/packages/vision-polyfill/src/base/visualManager.ts @@ -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); + } +} diff --git a/packages/vision-polyfill/src/base/visualRender.ts b/packages/vision-polyfill/src/base/visualRender.ts new file mode 100644 index 000000000..513b978f0 --- /dev/null +++ b/packages/vision-polyfill/src/base/visualRender.ts @@ -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 ''; + } +} diff --git a/packages/vision-polyfill/src/bundle/bundle.ts b/packages/vision-polyfill/src/bundle/bundle.ts new file mode 100644 index 000000000..7f994ebc5 --- /dev/null +++ b/packages/vision-polyfill/src/bundle/bundle.ts @@ -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; +} + +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; + } +} diff --git a/packages/vision-polyfill/src/bundle/prototype.ts b/packages/vision-polyfill/src/bundle/prototype.ts new file mode 100644 index 000000000..65e40dc17 --- /dev/null +++ b/packages/vision-polyfill/src/bundle/prototype.ts @@ -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) { + 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; diff --git a/packages/vision-polyfill/src/bundle/trunk.ts b/packages/vision-polyfill/src/bundle/trunk.ts new file mode 100644 index 000000000..156e6c22e --- /dev/null +++ b/packages/vision-polyfill/src/bundle/trunk.ts @@ -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(); diff --git a/packages/vision-polyfill/src/bundle/upgrade-metadata.ts b/packages/vision-polyfill/src/bundle/upgrade-metadata.ts new file mode 100644 index 000000000..ffc2158a1 --- /dev/null +++ b/packages/vision-polyfill/src/bundle/upgrade-metadata.ts @@ -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 | ReactElement> | (() => ReactElement); // => configure.component.actions + title?: string; // => + icon?: ComponentType | 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 | 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; +} diff --git a/packages/vision-polyfill/src/bus.ts b/packages/vision-polyfill/src/bus.ts new file mode 100644 index 000000000..37432e7f0 --- /dev/null +++ b/packages/vision-polyfill/src/bus.ts @@ -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; diff --git a/packages/vision-polyfill/src/components/index.less b/packages/vision-polyfill/src/components/index.less new file mode 100644 index 000000000..c630e0914 --- /dev/null +++ b/packages/vision-polyfill/src/components/index.less @@ -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; + } +} diff --git a/packages/vision-polyfill/src/components/index.tsx b/packages/vision-polyfill/src/components/index.tsx new file mode 100644 index 000000000..c005f2f9a --- /dev/null +++ b/packages/vision-polyfill/src/components/index.tsx @@ -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 { + 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> + </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> + ); + } +} diff --git a/packages/vision-polyfill/src/context.ts b/packages/vision-polyfill/src/context.ts new file mode 100644 index 000000000..ead94af2b --- /dev/null +++ b/packages/vision-polyfill/src/context.ts @@ -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(); diff --git a/packages/vision-polyfill/src/deep-value-parser.ts b/packages/vision-polyfill/src/deep-value-parser.ts new file mode 100644 index 000000000..728eb102c --- /dev/null +++ b/packages/vision-polyfill/src/deep-value-parser.ts @@ -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; +} diff --git a/packages/vision-polyfill/src/drag-engine.ts b/packages/vision-polyfill/src/drag-engine.ts new file mode 100644 index 000000000..e1cca3ba4 --- /dev/null +++ b/packages/vision-polyfill/src/drag-engine.ts @@ -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; diff --git a/packages/vision-polyfill/src/env.ts b/packages/vision-polyfill/src/env.ts new file mode 100644 index 000000000..7853167b3 --- /dev/null +++ b/packages/vision-polyfill/src/env.ts @@ -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(); diff --git a/packages/vision-polyfill/src/exchange.ts b/packages/vision-polyfill/src/exchange.ts new file mode 100644 index 000000000..866504a70 --- /dev/null +++ b/packages/vision-polyfill/src/exchange.ts @@ -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); + }; + }, +}; diff --git a/packages/vision-polyfill/src/fields/field.tsx b/packages/vision-polyfill/src/fields/field.tsx new file mode 100644 index 000000000..ede347595 --- /dev/null +++ b/packages/vision-polyfill/src/fields/field.tsx @@ -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> + ); + } +} diff --git a/packages/vision-polyfill/src/fields/fields.less b/packages/vision-polyfill/src/fields/fields.less new file mode 100644 index 000000000..c02417425 --- /dev/null +++ b/packages/vision-polyfill/src/fields/fields.less @@ -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; +} diff --git a/packages/vision-polyfill/src/fields/fields.tsx b/packages/vision-polyfill/src/fields/fields.tsx new file mode 100644 index 000000000..8f9d645c9 --- /dev/null +++ b/packages/vision-polyfill/src/fields/fields.tsx @@ -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> + ); + } +} diff --git a/packages/vision-polyfill/src/fields/index.ts b/packages/vision-polyfill/src/fields/index.ts new file mode 100644 index 000000000..682cba49d --- /dev/null +++ b/packages/vision-polyfill/src/fields/index.ts @@ -0,0 +1,2 @@ +export * from './settingField'; +export * from './fields'; diff --git a/packages/vision-polyfill/src/fields/inlinetip.tsx b/packages/vision-polyfill/src/fields/inlinetip.tsx new file mode 100644 index 000000000..6ba0ec658 --- /dev/null +++ b/packages/vision-polyfill/src/fields/inlinetip.tsx @@ -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> + ); + } +} diff --git a/packages/vision-polyfill/src/fields/settingField.tsx b/packages/vision-polyfill/src/fields/settingField.tsx new file mode 100644 index 000000000..ad8f8b34a --- /dev/null +++ b/packages/vision-polyfill/src/fields/settingField.tsx @@ -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; + } + } +} diff --git a/packages/vision-polyfill/src/fields/variableSetter.less b/packages/vision-polyfill/src/fields/variableSetter.less new file mode 100644 index 000000000..6ea5653d3 --- /dev/null +++ b/packages/vision-polyfill/src/fields/variableSetter.less @@ -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); + } + } +} diff --git a/packages/vision-polyfill/src/fields/variableSetter.tsx b/packages/vision-polyfill/src/fields/variableSetter.tsx new file mode 100644 index 000000000..87842967e --- /dev/null +++ b/packages/vision-polyfill/src/fields/variableSetter.tsx @@ -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)} + /> + ); + } +} diff --git a/packages/vision-polyfill/src/fields/variableSwitcher.less b/packages/vision-polyfill/src/fields/variableSwitcher.less new file mode 100644 index 000000000..0991b0ed6 --- /dev/null +++ b/packages/vision-polyfill/src/fields/variableSwitcher.less @@ -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; + } + } +} diff --git a/packages/vision-polyfill/src/fields/variableSwitcher.tsx b/packages/vision-polyfill/src/fields/variableSwitcher.tsx new file mode 100644 index 000000000..49085f885 --- /dev/null +++ b/packages/vision-polyfill/src/fields/variableSwitcher.tsx @@ -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> + ); + } +} diff --git a/packages/vision-polyfill/src/flags.ts b/packages/vision-polyfill/src/flags.ts new file mode 100644 index 000000000..2a68db857 --- /dev/null +++ b/packages/vision-polyfill/src/flags.ts @@ -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(); diff --git a/packages/vision-polyfill/src/i18n-util/index.d.ts b/packages/vision-polyfill/src/i18n-util/index.d.ts new file mode 100644 index 000000000..d258fe9d2 --- /dev/null +++ b/packages/vision-polyfill/src/i18n-util/index.d.ts @@ -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; diff --git a/packages/vision-polyfill/src/i18n-util/index.js b/packages/vision-polyfill/src/i18n-util/index.js new file mode 100644 index 000000000..86f0bb3b7 --- /dev/null +++ b/packages/vision-polyfill/src/i18n-util/index.js @@ -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(); diff --git a/packages/vision-polyfill/src/index.ts b/packages/vision-polyfill/src/index.ts new file mode 100644 index 000000000..669bef56c --- /dev/null +++ b/packages/vision-polyfill/src/index.ts @@ -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, +}; diff --git a/packages/vision-polyfill/src/module.d.ts b/packages/vision-polyfill/src/module.d.ts new file mode 100644 index 000000000..5392ba0b6 --- /dev/null +++ b/packages/vision-polyfill/src/module.d.ts @@ -0,0 +1 @@ +declare module '@ali/vu-css-style'; diff --git a/packages/vision-polyfill/src/pages.ts b/packages/vision-polyfill/src/pages.ts new file mode 100644 index 000000000..03adf54a4 --- /dev/null +++ b/packages/vision-polyfill/src/pages.ts @@ -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; diff --git a/packages/vision-polyfill/src/panes.ts b/packages/vision-polyfill/src/panes.ts new file mode 100644 index 000000000..0a61d860e --- /dev/null +++ b/packages/vision-polyfill/src/panes.ts @@ -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, +}; diff --git a/packages/vision-polyfill/src/project.ts b/packages/vision-polyfill/src/project.ts new file mode 100644 index 000000000..7e1d9c7d6 --- /dev/null +++ b/packages/vision-polyfill/src/project.ts @@ -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; diff --git a/packages/vision-polyfill/src/prop.ts b/packages/vision-polyfill/src/prop.ts new file mode 100644 index 000000000..3e7f77d84 --- /dev/null +++ b/packages/vision-polyfill/src/prop.ts @@ -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); + }; + } +} diff --git a/packages/vision-polyfill/src/props-reducers/downgrade-schema-reducer.ts b/packages/vision-polyfill/src/props-reducers/downgrade-schema-reducer.ts new file mode 100644 index 000000000..65186ea81 --- /dev/null +++ b/packages/vision-polyfill/src/props-reducers/downgrade-schema-reducer.ts @@ -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; +} diff --git a/packages/vision-polyfill/src/props-reducers/filter-reducer.ts b/packages/vision-polyfill/src/props-reducers/filter-reducer.ts new file mode 100644 index 000000000..928e8dd82 --- /dev/null +++ b/packages/vision-polyfill/src/props-reducers/filter-reducer.ts @@ -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; +} \ No newline at end of file diff --git a/packages/vision-polyfill/src/props-reducers/index.ts b/packages/vision-polyfill/src/props-reducers/index.ts new file mode 100644 index 000000000..148d115b7 --- /dev/null +++ b/packages/vision-polyfill/src/props-reducers/index.ts @@ -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'; diff --git a/packages/vision-polyfill/src/props-reducers/init-node-reducer.ts b/packages/vision-polyfill/src/props-reducers/init-node-reducer.ts new file mode 100644 index 000000000..031482f46 --- /dev/null +++ b/packages/vision-polyfill/src/props-reducers/init-node-reducer.ts @@ -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; +} diff --git a/packages/vision-polyfill/src/props-reducers/live-lifecycle-reducer.ts b/packages/vision-polyfill/src/props-reducers/live-lifecycle-reducer.ts new file mode 100644 index 000000000..825964127 --- /dev/null +++ b/packages/vision-polyfill/src/props-reducers/live-lifecycle-reducer.ts @@ -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; +} \ No newline at end of file diff --git a/packages/vision-polyfill/src/props-reducers/node-top-fixed-reducer.ts b/packages/vision-polyfill/src/props-reducers/node-top-fixed-reducer.ts new file mode 100644 index 000000000..194496a9c --- /dev/null +++ b/packages/vision-polyfill/src/props-reducers/node-top-fixed-reducer.ts @@ -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; +} \ No newline at end of file diff --git a/packages/vision-polyfill/src/props-reducers/remove-empty-prop-reducer.ts b/packages/vision-polyfill/src/props-reducers/remove-empty-prop-reducer.ts new file mode 100644 index 000000000..305f0bdb7 --- /dev/null +++ b/packages/vision-polyfill/src/props-reducers/remove-empty-prop-reducer.ts @@ -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; +} diff --git a/packages/vision-polyfill/src/props-reducers/style-reducer.ts b/packages/vision-polyfill/src/props-reducers/style-reducer.ts new file mode 100644 index 000000000..c1a1425fa --- /dev/null +++ b/packages/vision-polyfill/src/props-reducers/style-reducer.ts @@ -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}`))); + } +} \ No newline at end of file diff --git a/packages/vision-polyfill/src/props-reducers/upgrade-reducer.ts b/packages/vision-polyfill/src/props-reducers/upgrade-reducer.ts new file mode 100644 index 000000000..acb7a4883 --- /dev/null +++ b/packages/vision-polyfill/src/props-reducers/upgrade-reducer.ts @@ -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; +} diff --git a/packages/vision-polyfill/src/reducers.ts b/packages/vision-polyfill/src/reducers.ts new file mode 100644 index 000000000..fa07d7058 --- /dev/null +++ b/packages/vision-polyfill/src/reducers.ts @@ -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); diff --git a/packages/vision-polyfill/src/rootNodeVisitor.ts b/packages/vision-polyfill/src/rootNodeVisitor.ts new file mode 100644 index 000000000..3efd4d017 --- /dev/null +++ b/packages/vision-polyfill/src/rootNodeVisitor.ts @@ -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)); + } + } +} diff --git a/packages/vision-polyfill/src/symbols.ts b/packages/vision-polyfill/src/symbols.ts new file mode 100644 index 000000000..81091e371 --- /dev/null +++ b/packages/vision-polyfill/src/symbols.ts @@ -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(); diff --git a/packages/vision-polyfill/src/utils/index.ts b/packages/vision-polyfill/src/utils/index.ts new file mode 100644 index 000000000..67a2ca579 --- /dev/null +++ b/packages/vision-polyfill/src/utils/index.ts @@ -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}'` : '')); + } +} + diff --git a/packages/vision-polyfill/src/vc-live-editing.ts b/packages/vision-polyfill/src/vc-live-editing.ts new file mode 100644 index 000000000..59cceb655 --- /dev/null +++ b/packages/vision-polyfill/src/vc-live-editing.ts @@ -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 或 弹出绑定变量 diff --git a/packages/vision-polyfill/src/viewport.ts b/packages/vision-polyfill/src/viewport.ts new file mode 100644 index 000000000..65feed571 --- /dev/null +++ b/packages/vision-polyfill/src/viewport.ts @@ -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(); diff --git a/packages/vision-polyfill/src/vision.less b/packages/vision-polyfill/src/vision.less new file mode 100644 index 000000000..fdf1c737b --- /dev/null +++ b/packages/vision-polyfill/src/vision.less @@ -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 +} diff --git a/packages/vision-polyfill/tests/bundle/bundle.test.ts b/packages/vision-polyfill/tests/bundle/bundle.test.ts new file mode 100644 index 000000000..31174d481 --- /dev/null +++ b/packages/vision-polyfill/tests/bundle/bundle.test.ts @@ -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(); + }); +}); diff --git a/packages/vision-polyfill/tests/bundle/prototype.test.ts b/packages/vision-polyfill/tests/bundle/prototype.test.ts new file mode 100644 index 000000000..525a8a01e --- /dev/null +++ b/packages/vision-polyfill/tests/bundle/prototype.test.ts @@ -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'); + }); + }); +}); diff --git a/packages/vision-polyfill/tests/bundle/trunk.test.ts b/packages/vision-polyfill/tests/bundle/trunk.test.ts new file mode 100644 index 000000000..d4bca021f --- /dev/null +++ b/packages/vision-polyfill/tests/bundle/trunk.test.ts @@ -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; + }); +}); diff --git a/packages/vision-polyfill/tests/fixtures/prototype/div-meta.ts b/packages/vision-polyfill/tests/fixtures/prototype/div-meta.ts new file mode 100644 index 000000000..a2b410494 --- /dev/null +++ b/packages/vision-polyfill/tests/fixtures/prototype/div-meta.ts @@ -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: [], + }, +}; diff --git a/packages/vision-polyfill/tests/fixtures/prototype/div-vision-full.ts b/packages/vision-polyfill/tests/fixtures/prototype/div-vision-full.ts new file mode 100644 index 000000000..756c37649 --- /dev/null +++ b/packages/vision-polyfill/tests/fixtures/prototype/div-vision-full.ts @@ -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, + }, + }, + ], + }, + ], +}; diff --git a/packages/vision-polyfill/tests/fixtures/prototype/div-vision.ts b/packages/vision-polyfill/tests/fixtures/prototype/div-vision.ts new file mode 100644 index 000000000..c4ae4374f --- /dev/null +++ b/packages/vision-polyfill/tests/fixtures/prototype/div-vision.ts @@ -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, + }, + }, + ], + }, + ], +}; diff --git a/packages/vision-polyfill/tests/fixtures/schema/form.ts b/packages/vision-polyfill/tests/fixtures/schema/form.ts new file mode 100644 index 000000000..5492a9ffb --- /dev/null +++ b/packages/vision-polyfill/tests/fixtures/schema/form.ts @@ -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, + }, + ], +}; diff --git a/packages/vision-polyfill/tests/fixtures/window.ts b/packages/vision-polyfill/tests/fixtures/window.ts new file mode 100644 index 000000000..b5886ad6d --- /dev/null +++ b/packages/vision-polyfill/tests/fixtures/window.ts @@ -0,0 +1,8 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +React.PropTypes = PropTypes; +window.React = React; + +document.documentElement.requestFullscreen = () => {}; +document.exitFullscreen = () => {}; \ No newline at end of file diff --git a/packages/vision-polyfill/tests/master/__snapshots__/deep-value-parser.test.ts.snap b/packages/vision-polyfill/tests/master/__snapshots__/deep-value-parser.test.ts.snap new file mode 100644 index 000000000..0dd46763f --- /dev/null +++ b/packages/vision-polyfill/tests/master/__snapshots__/deep-value-parser.test.ts.snap @@ -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": "中文", +} +`; diff --git a/packages/vision-polyfill/tests/master/bus.test.ts b/packages/vision-polyfill/tests/master/bus.test.ts new file mode 100644 index 000000000..c705a5b92 --- /dev/null +++ b/packages/vision-polyfill/tests/master/bus.test.ts @@ -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); + }); + }); + }); +}); diff --git a/packages/vision-polyfill/tests/master/context.test.ts b/packages/vision-polyfill/tests/master/context.test.ts new file mode 100644 index 000000000..362e4103d --- /dev/null +++ b/packages/vision-polyfill/tests/master/context.test.ts @@ -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(); + }); +}); diff --git a/packages/vision-polyfill/tests/master/deep-value-parser.test.ts b/packages/vision-polyfill/tests/master/deep-value-parser.test.ts new file mode 100644 index 000000000..a55f6f68a --- /dev/null +++ b/packages/vision-polyfill/tests/master/deep-value-parser.test.ts @@ -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(); + }); +}); diff --git a/packages/vision-polyfill/tests/master/drag-engine.test.ts b/packages/vision-polyfill/tests/master/drag-engine.test.ts new file mode 100644 index 000000000..cd3d8c5c1 --- /dev/null +++ b/packages/vision-polyfill/tests/master/drag-engine.test.ts @@ -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(); + }); +}); diff --git a/packages/vision-polyfill/tests/master/env.test.ts b/packages/vision-polyfill/tests/master/env.test.ts new file mode 100644 index 000000000..6f280295c --- /dev/null +++ b/packages/vision-polyfill/tests/master/env.test.ts @@ -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 } }); + }); + }); +}); diff --git a/packages/vision-polyfill/tests/master/flags.test.ts b/packages/vision-polyfill/tests/master/flags.test.ts new file mode 100644 index 000000000..c5c3301e6 --- /dev/null +++ b/packages/vision-polyfill/tests/master/flags.test.ts @@ -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']); + }); +}); diff --git a/packages/vision-polyfill/tests/master/panes.test.ts b/packages/vision-polyfill/tests/master/panes.test.ts new file mode 100644 index 000000000..1a36eb271 --- /dev/null +++ b/packages/vision-polyfill/tests/master/panes.test.ts @@ -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(); + }); +}); diff --git a/packages/vision-polyfill/tests/master/symbols.test.ts b/packages/vision-polyfill/tests/master/symbols.test.ts new file mode 100644 index 000000000..35463ad8a --- /dev/null +++ b/packages/vision-polyfill/tests/master/symbols.test.ts @@ -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'); + }); +}); diff --git a/packages/vision-polyfill/tests/master/viewport.test.ts b/packages/vision-polyfill/tests/master/viewport.test.ts new file mode 100644 index 000000000..1fd82bc03 --- /dev/null +++ b/packages/vision-polyfill/tests/master/viewport.test.ts @@ -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;}', + }]); + }); +}); diff --git a/packages/vision-polyfill/tests/props-reducers/downgrade-schema.test.ts b/packages/vision-polyfill/tests/props-reducers/downgrade-schema.test.ts new file mode 100644 index 000000000..ac7f3f45e --- /dev/null +++ b/packages/vision-polyfill/tests/props-reducers/downgrade-schema.test.ts @@ -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); + }); +}); diff --git a/packages/vision-polyfill/tests/props-reducers/filter.test.ts b/packages/vision-polyfill/tests/props-reducers/filter.test.ts new file mode 100644 index 000000000..691cfc086 --- /dev/null +++ b/packages/vision-polyfill/tests/props-reducers/filter.test.ts @@ -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, + }); + }); +}); diff --git a/packages/vision-polyfill/tests/props-reducers/init-node.test.ts b/packages/vision-polyfill/tests/props-reducers/init-node.test.ts new file mode 100644 index 000000000..874576a70 --- /dev/null +++ b/packages/vision-polyfill/tests/props-reducers/init-node.test.ts @@ -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', + }); + }); + }); +}); diff --git a/packages/vision-polyfill/tests/props-reducers/live-lifecycle.test.ts b/packages/vision-polyfill/tests/props-reducers/live-lifecycle.test.ts new file mode 100644 index 000000000..cd396da5e --- /dev/null +++ b/packages/vision-polyfill/tests/props-reducers/live-lifecycle.test.ts @@ -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; +}); diff --git a/packages/vision-polyfill/tests/props-reducers/node-top-fixed.test.ts b/packages/vision-polyfill/tests/props-reducers/node-top-fixed.test.ts new file mode 100644 index 000000000..b1becca60 --- /dev/null +++ b/packages/vision-polyfill/tests/props-reducers/node-top-fixed.test.ts @@ -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', + }); +}); diff --git a/packages/vision-polyfill/tests/props-reducers/remove-empty-prop.test.ts b/packages/vision-polyfill/tests/props-reducers/remove-empty-prop.test.ts new file mode 100644 index 000000000..2e5fb8627 --- /dev/null +++ b/packages/vision-polyfill/tests/props-reducers/remove-empty-prop.test.ts @@ -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', + }, + }, + }, + ], + }, + }); +}); diff --git a/packages/vision-polyfill/tests/props-reducers/style-props.test.ts b/packages/vision-polyfill/tests/props-reducers/style-props.test.ts new file mode 100644 index 000000000..d79fbac75 --- /dev/null +++ b/packages/vision-polyfill/tests/props-reducers/style-props.test.ts @@ -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; }'; + }); +}); diff --git a/packages/vision-polyfill/tests/props-reducers/upgrade-schema.test copy.ts b/packages/vision-polyfill/tests/props-reducers/upgrade-schema.test copy.ts new file mode 100644 index 000000000..b16c9b127 --- /dev/null +++ b/packages/vision-polyfill/tests/props-reducers/upgrade-schema.test copy.ts @@ -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(); +}); diff --git a/packages/vision-polyfill/tests/utils/index.ts b/packages/vision-polyfill/tests/utils/index.ts new file mode 100644 index 000000000..70fce0af2 --- /dev/null +++ b/packages/vision-polyfill/tests/utils/index.ts @@ -0,0 +1 @@ +export { getIdsFromSchema, getNodeFromSchemaById } from '@ali/lowcode-test-mate/es/utils'; diff --git a/packages/vision-polyfill/tests/vision-api/api-export.test.ts b/packages/vision-polyfill/tests/vision-api/api-export.test.ts new file mode 100644 index 000000000..8da131447 --- /dev/null +++ b/packages/vision-polyfill/tests/vision-api/api-export.test.ts @@ -0,0 +1,87 @@ +import set from 'lodash/set'; +import cloneDeep from 'lodash/clonedeep'; +import '../fixtures/window'; +// import { Project } from '../../src/project/project'; +import formSchema from '../fixtures/schema/form'; +import VisualEngine, { + designer, + 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, +} from '../../src'; +import { Editor } from '@ali/lowcode-editor-core'; + +describe('API 多种导出场景测试', () => { + it('window.VisualEngine 和 npm 导出 API 测试', () => { + expect(VisualEngine).toBe(window.VisualEngine); + }); + + it('npm 导出 API 对比测试', () => { + expect(VisualEngine.designer).toBe(designer); + expect(VisualEngine.editor).toBe(editor); + expect(VisualEngine.skeleton).toBe(skeleton); + expect(VisualEngine.Popup).toBe(Popup); + expect(VisualEngine.utils).toBe(utils); + expect(VisualEngine.I18nUtil).toBe(I18nUtil); + expect(VisualEngine.Hotkey).toBe(Hotkey); + expect(VisualEngine.Env).toBe(Env); + expect(VisualEngine.monitor).toBe(monitor); + expect(VisualEngine.Bus).toBe(Bus); + expect(VisualEngine.EVENTS).toBe(EVENTS); + expect(VisualEngine.HOOKS).toBe(HOOKS); + expect(VisualEngine.Exchange).toBe(Exchange); + expect(VisualEngine.context).toBe(context); + expect(VisualEngine.init).toBe(init); + expect(VisualEngine.ui).toBe(ui); + expect(VisualEngine.Panes).toBe(Panes); + expect(VisualEngine.modules).toBe(modules); + expect(VisualEngine.Trunk).toBe(Trunk); + expect(VisualEngine.Prototype).toBe(Prototype); + expect(VisualEngine.Bundle).toBe(Bundle); + expect(VisualEngine.DragEngine).toBe(DragEngine); + expect(VisualEngine.Pages).toBe(Pages); + expect(VisualEngine.Viewport).toBe(Viewport); + expect(VisualEngine.Version).toBe(Version); + expect(VisualEngine.Project).toBe(Project); + expect(VisualEngine.logger).toBe(logger); + expect(VisualEngine.Symbols).toBe(Symbols); + }); +}); \ No newline at end of file diff --git a/packages/vision-polyfill/tests/vision-api/exchange.test.ts b/packages/vision-polyfill/tests/vision-api/exchange.test.ts new file mode 100644 index 000000000..16c93b313 --- /dev/null +++ b/packages/vision-polyfill/tests/vision-api/exchange.test.ts @@ -0,0 +1,23 @@ +import set from 'lodash/set'; +import cloneDeep from 'lodash/clonedeep'; +import '../fixtures/window'; +import formSchema from '../fixtures/schema/form'; +import VisualEngine from '../../src'; + +describe('VisualEngine.Exchange 相关 API 测试', () => { + it('select / getSelected', () => { + const doc = VisualEngine.Pages.addPage(formSchema); + VisualEngine.Exchange.select(doc?.getNode('form')); + expect(VisualEngine.Exchange.getSelected()?.componentName).toBe('Form'); + expect(VisualEngine.Exchange.getSelected()?.id).toBe('form'); + + // clear selection + VisualEngine.Exchange.select(); + expect(VisualEngine.Exchange.getSelected()).toBeUndefined; + }); + + it('onIntoView', () => { + expect(typeof VisualEngine.Exchange.onIntoView).toBe('function'); + VisualEngine.Exchange.onIntoView(); + }); +}); diff --git a/packages/vision-polyfill/tests/vision-api/pages.test.ts b/packages/vision-polyfill/tests/vision-api/pages.test.ts new file mode 100644 index 000000000..87b04db0a --- /dev/null +++ b/packages/vision-polyfill/tests/vision-api/pages.test.ts @@ -0,0 +1,170 @@ +import set from 'lodash/set'; +import cloneDeep from 'lodash/clonedeep'; +import '../fixtures/window'; +import formSchema from '../fixtures/schema/form'; +import VisualEngine, { Prototype } from '../../src'; +import { Editor } from '@ali/lowcode-editor-core'; +import { getIdsFromSchema, getNodeFromSchemaById } from '../utils'; +import divPrototypeConfig from '../fixtures/prototype/div-vision'; + +const pageSchema = { componentsTree: [formSchema] }; + +describe('VisualEngine.Pages 相关 API 测试', () => { + afterEach(() => { + VisualEngine.Pages.unload(); + }); + describe('addPage 系列', () => { + it('基本的节点模型初始化,初始化传入 schema', () => { + const doc = VisualEngine.Pages.addPage(pageSchema)!; + expect(doc).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + expect(doc.nodesMap.size).toBe(expectedNodeCnt); + }); + it('基本的节点模型初始化,初始化传入 schema,带有 slot', () => { + const formSchemaWithSlot = set(cloneDeep(formSchema), 'children[0].children[0].props.title', { + type: 'JSBlock', + value: { + componentName: 'Slot', + children: [ + { + componentName: 'Text', + id: 'node_k1ow3cbf', + props: { + showTitle: false, + behavior: 'NORMAL', + content: { + type: 'variable', + value: { + use: 'zh_CN', + en_US: 'Title', + zh_CN: '个人信息', + type: 'i18n', + }, + variable: 'state.title', + }, + __style__: {}, + fieldId: 'text_k1ow3h1j', + maxLine: 0, + }, + condition: true, + }, + ], + props: { + slotTitle: '标题区域', + slotName: 'title', + }, + }, + }); + const doc = VisualEngine.Pages.addPage({ componentsTree: [formSchemaWithSlot] })!; + expect(doc).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + // slot 会多出(1 + N)个节点 + expect(doc.nodesMap.size).toBe(expectedNodeCnt + 2); + }); + it('基本的节点模型初始化,初始化传入 schema,构造 prototype', () => { + const proto = new Prototype(divPrototypeConfig); + const doc = VisualEngine.Pages.addPage(pageSchema)!; + expect(doc).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + expect(doc.nodesMap.size).toBe(expectedNodeCnt); + }); + it('导出 schema', () => { + const doc = VisualEngine.Pages.addPage(pageSchema)!; + expect(doc).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + const exportedData = doc.toData(); + expect(exportedData).toHaveProperty('componentsMap'); + expect(exportedData).toHaveProperty('componentsTree'); + expect(exportedData.componentsTree).toHaveLength(1); + const exportedSchema = exportedData.componentsTree[0]; + expect(getIdsFromSchema(exportedSchema).length).toBe(expectedNodeCnt); + }); + }); + describe('removePage 系列', () => { + it('removePage', () => { + const doc = VisualEngine.Pages.addPage(pageSchema)!; + expect(doc).toBeTruthy(); + expect(VisualEngine.Pages.documents).toHaveLength(1); + VisualEngine.Pages.removePage(doc); + expect(VisualEngine.Pages.documents).toHaveLength(0); + }); + }); + describe('getPage 系列', () => { + it('getPage', () => { + const doc = VisualEngine.Pages.addPage(pageSchema); + const anotherFormSchema = set(cloneDeep(formSchema), 'id', 'page'); + const doc2 = VisualEngine.Pages.addPage({ componentsTree: [anotherFormSchema] }); + expect(VisualEngine.Pages.getPage(0)).toBe(doc); + expect(VisualEngine.Pages.getPage((_doc) => _doc.rootNode.id === 'page')).toBe(doc2); + }); + }); + describe('setPages 系列', () => { + it('setPages componentsTree 只有一个元素', () => { + VisualEngine.Pages.setPages([pageSchema]); + const { currentDocument } = VisualEngine.Pages; + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + const exportedData = currentDocument.toData(); + expect(exportedData).toHaveProperty('componentsMap'); + expect(exportedData).toHaveProperty('componentsTree'); + expect(exportedData.componentsTree).toHaveLength(1); + const exportedSchema = exportedData.componentsTree[0]; + expect(getIdsFromSchema(exportedSchema).length).toBe(expectedNodeCnt); + }); + }); + describe('setCurrentPage / getCurrentPage / currentPage / currentDocument 系列', () => { + it('getCurrentPage', () => { + const doc = VisualEngine.Pages.addPage(pageSchema)!; + expect(doc).toBeTruthy(); + expect(doc).toBe(VisualEngine.Pages.getCurrentPage()); + expect(doc).toBe(VisualEngine.Pages.currentDocument); + expect(doc).toBe(VisualEngine.Pages.currentPage); + }); + it('setCurrentPage', () => { + const doc = VisualEngine.Pages.addPage(pageSchema); + expect(doc).toBe(VisualEngine.Pages.currentDocument); + const anotherFormSchema = set(cloneDeep(formSchema), 'id', 'page'); + const doc2 = VisualEngine.Pages.addPage({ componentsTree: [anotherFormSchema] }); + expect(doc2).toBe(VisualEngine.Pages.currentDocument); + VisualEngine.Pages.setCurrentPage(doc); + expect(doc).toBe(VisualEngine.Pages.currentDocument); + }); + }); + describe('onCurrentPageChange 系列', () => { + it('多次切换', () => { + const doc = VisualEngine.Pages.addPage(pageSchema); + const anotherFormSchema = set(cloneDeep(formSchema), 'id', 'page'); + const doc2 = VisualEngine.Pages.addPage({ componentsTree: [anotherFormSchema] }); + const docChangeHandler = jest.fn(); + VisualEngine.Pages.onCurrentDocumentChange(docChangeHandler); + VisualEngine.Pages.setCurrentPage(doc); + expect(docChangeHandler).toHaveBeenCalledTimes(1); + expect(docChangeHandler).toHaveBeenLastCalledWith(doc); + + VisualEngine.Pages.setCurrentPage(doc2); + expect(docChangeHandler).toHaveBeenCalledTimes(2); + expect(docChangeHandler).toHaveBeenLastCalledWith(doc2); + }); + }); + describe('toData 系列', () => { + it('基本的节点模型初始化,模型导出,初始化传入 schema', () => { + const doc = VisualEngine.Pages.addPage(pageSchema); + const anotherFormSchema = set(cloneDeep(formSchema), 'id', 'page'); + const doc2 = VisualEngine.Pages.addPage({ componentsTree: [anotherFormSchema] }); + const dataList = VisualEngine.Pages.toData(); + expect(dataList.length).toBe(2); + expect(dataList[0]).toHaveProperty('componentsMap'); + expect(dataList[0]).toHaveProperty('componentsTree'); + expect(dataList[0].componentsTree).toHaveLength(1); + expect(dataList[0].componentsTree[0].id).toBe('node_k1ow3cb9'); + expect(dataList[1]).toHaveProperty('componentsMap'); + expect(dataList[1]).toHaveProperty('componentsTree'); + expect(dataList[1].componentsTree).toHaveLength(1); + expect(dataList[1].componentsTree[0].id).toBe('page'); + }); + }); +}); diff --git a/packages/vision-polyfill/tests/vision-api/project.test.ts b/packages/vision-polyfill/tests/vision-api/project.test.ts new file mode 100644 index 000000000..bec071d51 --- /dev/null +++ b/packages/vision-polyfill/tests/vision-api/project.test.ts @@ -0,0 +1,26 @@ +import set from 'lodash/set'; +import cloneDeep from 'lodash/clonedeep'; +import '../fixtures/window'; +import formSchema from '../fixtures/schema/form'; +import { Project } from '../../src'; + +describe('VisualEngine.Project 相关 API 测试', () => { + it('getSchema / setSchema 系列', () => { + Project.setSchema({ + componentsMap: {}, + componentsTree: [formSchema], + }); + expect(Project.getSchema()).toEqual({ + componentsMap: {}, + componentsTree: [formSchema], + }); + + }); + + it('setConfig', () => { + Project.setConfig({ haha: 1 }); + expect(Project.get('config')).toEqual({ + haha: 1, + }); + }); +}); diff --git a/packages/vision-polyfill/tsconfig.json b/packages/vision-polyfill/tsconfig.json new file mode 100644 index 000000000..c37b76ecc --- /dev/null +++ b/packages/vision-polyfill/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "lib" + }, + "include": [ + "./src/" + ] +}