feat: 合入 trunk-vision 代码

This commit is contained in:
力皓 2020-12-04 20:57:35 +08:00
commit ea6bc7aeb8
226 changed files with 18632 additions and 1384 deletions

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ava my-package",
"type": "node",
"request": "launch",
"runtimeExecutable": "${workspaceFolder}/packages/material-parser/node_modules/.bin/ava",
"runtimeArgs": ["debug", "--break", "${workspaceFolder}/packages/material-parser/test/antd.ts"]
}
]
}

1110
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

7
index.ts Normal file
View File

@ -0,0 +1,7 @@
import renderer from './packages/react-simulator-renderer/src/renderer';
if (typeof window !== 'undefined') {
(window as any).SimulatorRenderer = renderer;
}
export default renderer;

View File

@ -1,6 +1,6 @@
{
"lerna": "2.11.0",
"version": "independent",
"version": "0.13.1-29",
"npmClient": "tyarn",
"registry": "http://registry.npm.alibaba-inc.com",
"useWorkspaces": true,

View File

@ -14,7 +14,8 @@
"clean": "rm -rf ./packages/*/lib ./packages/*/es ./packages/*/dist ./packages/*/build",
"lint": "eslint --ext .ts,.tsx,.js,.jsx ./ --quiet",
"lint:fix": "eslint --ext .ts,.tsx,.js,.jsx ./ --quiet --fix",
"pub": "lerna publish --force-publish --cd-version patch",
"pub": "lerna publish --force-publish --cd-version prepatch",
"pubbeta": "lerna publish --force-publish --cd-version prerelease --npm-tag beta",
"setup": "./scripts/setup.sh",
"start": "./scripts/start.sh",
"start:server": "./scripts/start-server.sh",
@ -42,7 +43,7 @@
"xima": "^0.2.15"
},
"engines": {
"node": ">=10.0.0"
"node": ">=14.0.0"
},
"tnpm": {
"mode": "yarn",

View File

@ -1,8 +1,9 @@
module.exports = {
extends: '../../.eslintrc.js',
extends: 'eslint-config-ali/typescript/react',
ignorePatterns: [ 'tests/* '],
rules: {
'react/no-multi-comp': 0,
'no-unused-expressions': 1,
'no-unused-expressions': 0,
'implicit-arrow-linebreak': 1,
'no-nested-ternary': 1,
'no-mixed-operators': 1,
@ -12,5 +13,7 @@ module.exports = {
'no-prototype-builtins': 1,
'no-useless-constructor': 1,
'no-empty-function': 1,
'@typescript-eslint/member-ordering': 0,
'lines-between-class-members': 0
}
}

View File

@ -10,6 +10,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
### Bug Fixes
* 修复 slot 以及子节点不销毁 ([8ef62c8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/8ef62c8))
* 修复 setDevice 的时机,从 currentDocument -> simualtor ([0f14884](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/0f14884))
@ -21,6 +22,8 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
### Bug Fixes
* 修复数据源的接入问题 ([98ae1ed](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/98ae1ed))
* documentModel 里的 addon 相关函数跟原 vision 实现对齐 ([b0ea548](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/b0ea548))
* 原地编辑功能异常, 编辑时需要禁掉快捷键 ([3c000de](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/3c000de))
@ -32,6 +35,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
### Bug Fixes
* typo of onResizeEnd and remove ([8df5f05](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/8df5f05))
* 修复 registerAddon 函数 ([309920a](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/309920a))
@ -43,6 +47,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
### Bug Fixes
* 修改style-setter报错,loadAsyncLib 判断 ([7fe793a](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7fe793a))
* 优化选中页面根节点时, 直接点击组件面板插入位置 ([c1ca2c6](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c1ca2c6))
@ -110,6 +115,70 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
### Bug Fixes
* checkId 需要传递 ([bdff2b1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bdff2b1))
* path with / ([2470363](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2470363))
<a name="1.0.9-9"></a>
## [1.0.9-9](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.9-8...v1.0.9-9) (2020-09-22)
### Features
* 支持 node.children.onInsert ([f120df5](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f120df5))
<a name="1.0.9-8"></a>
## [1.0.9-8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.9-7...v1.0.9-8) (2020-09-22)
### Bug Fixes
* 修复修改 勾选框、富文本编辑器、下拉选择 等组件标题报错 ([8ba26ee](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/8ba26ee))
<a name="1.0.9-7"></a>
## [1.0.9-7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.9-5...v1.0.9-7) (2020-09-18)
### Bug Fixes
* 1. 小程序导航配置 pagePath -> path 2.OneAPIConfig -> oneAPIConfig ([2714285](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2714285))
* 修改 renderer 需等待 document 才开始渲染 ([e7cc9bc](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e7cc9bc))
<a name="1.0.9-6"></a>
## [1.0.9-6](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.9-5...v1.0.9-6) (2020-09-18)
### Bug Fixes
* 1. 小程序导航配置 pagePath -> path 2.OneAPIConfig -> oneAPIConfig ([2714285](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2714285))
* 修改 renderer 需等待 document 才开始渲染 ([e7cc9bc](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e7cc9bc))
<a name="1.0.9-5"></a>
## [1.0.9-5](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/v1.0.9-2...v1.0.9-5) (2020-09-17)
### Bug Fixes
* 低代码组件丢失代码找回 ([aac8126](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/aac8126))
* 1.修复 rax 路由问题 2.切换 designMode 重新 setupSelection 3.settingpane add state shouldIgnoreRoot ([890ec76](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/890ec76))
### Features
* 补充一些 vision API ([933cef1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/933cef1))
@ -136,7 +205,150 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
### Bug Fixes
* 支持 checkId 开关功能, 在 setSchema 时关闭, 避免 id 被不断重置 ([44bdda1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/44bdda1))
* props.getNode 防死循环 ([444e25c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/444e25c))
* **designer:** fix insertion style ([82c10d2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/82c10d2))
* fieldId 重复问题 ([e761b1a](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e761b1a))
* 🐛 add hotkey up/down/left/right ([9c8afe8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/9c8afe8))
* 🐛 add pollyfill for vision page.getHistory ([0b905d0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/0b905d0))
* 🐛 error when quick search ([801d954](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/801d954))
* 🐛 title缺少icon字段临时转接一下 ([2f9bb25](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2f9bb25))
* 🐛 增加 getAddonData api ([68b7e29](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/68b7e29))
* 🐛 增加剪切快捷键 ([a73a82e](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/a73a82e))
* 🐛 快捷键支持 ([73374dd](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/73374dd))
* 🐛 移动快捷键 ([7c8a27c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7c8a27c))
* 1. 修复dialog拖入不显示问题 2. dialog 只能在根节点下 3. 引入 modalNodeManager ([65977e7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/65977e7))
* add extraEnv ([9058ac8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/9058ac8))
* border action style ([6b91535](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/6b91535))
* cancel dragging on invalid position ([f961096](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f961096))
* canDropIn 为 boolean 时失效 ([7508fb6](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7508fb6))
* 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))
* documentModel toData 方法 ([1ea0d73](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/1ea0d73))
* export data ([41f7724](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/41f7724))
* fieldId 重复 ([5d64312](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/5d64312))
* force schema ([6d0a8ff](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/6d0a8ff))
* getSuitablePlace ([03e7c57](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/03e7c57))
* handling the undefined variable ([0efe8b4](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/0efe8b4))
* layout tabbar number ([3975571](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/3975571))
* lc-borders-actions ([56d9f5f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/56d9f5f))
* modal node locate ([9a72dd7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/9a72dd7))
* modify layout props ([9baba75](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/9baba75))
* nextId append the id of document ([80a5c93](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/80a5c93))
* nextId() 逻辑调整 ([488a5d8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/488a5d8))
* NodeChildren伪装为Array保证向前兼容 ([7950bf5](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7950bf5))
* panel visible time ([18ac1fa](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/18ac1fa))
* patch prototype ([f20bfaa](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f20bfaa))
* prop type=UNSET 时返回 undefined ([f437f30](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f437f30))
* quickSearch error ([a8009ef](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/a8009ef))
* revert ([dad21e2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/dad21e2))
* settingField items is empty when type is not 'group' ([582c41a](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/582c41a))
* settingfield添加props修复地区组件切换类型报错 ([88348f7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/88348f7))
* slot 兼容问题 + loop key bug fix ([bc64017](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bc64017))
* support dropObject is data ([809fda7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/809fda7))
* supports ([371b84c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/371b84c))
* try get settingfield ([56f242f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/56f242f))
* uniqueid ([8db52f0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/8db52f0))
* updateProps before init ([760e6a6](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/760e6a6))
* vc-filter bug fix ([31ea5d5](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/31ea5d5))
* VC-Filter组件的适配问题 ([1f581b8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/1f581b8))
* 不对外暴露 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))
* 优化simulator样式 ([25ba893](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/25ba893))
* 优化树子节点删除逻辑 ([47e814f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/47e814f))
* 优化画布中点击事件屏蔽,增加富文本组件的部分屏蔽 ([a5b6557](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/a5b6557))
* 优化画布中点击事件屏蔽,增加富文本组件的部分屏蔽 ([ec08c6c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ec08c6c))
* 低代码组件 props 显示 object 问题 ([116498e](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/116498e))
* 低代码组件修改之后渲染为空 ([ef71632](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ef71632))
* 修复 preset-vision 版本 lifeCycles 丢失以及 slot 初始化问题 ([7cf6d24](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7cf6d24))
* 修复 toolbar 弹出位置异常 ([b40b9a4](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/b40b9a4))
* 修复js面板引用计数问题 ([fcc1a6f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/fcc1a6f))
* 修复低代码组件设计器、区块设计器根节点为 Page 的问题,修复 topArea 样式 ([e85b542](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e85b542))
* 修复取不到值的情况 ([5e7e488](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/5e7e488))
* 修复在切换页面时没有销毁相应节点导致的一系列bug ([59fac25](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/59fac25))
* 修复导入的组件拖入画布报错 ([caf9915](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/caf9915))
* 修复获取 currentPage 的逻辑 ([d8221db](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d8221db))
* 修改移动端设备宽度 ([cd7b1e6](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/cd7b1e6))
* 兼容modal模式 ([1092ee9](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/1092ee9))
* 初始就create 所有documentInstance, 否则路由跳转有问题 ([fdd6978](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/fdd6978))
* 区块组件无法删除 ([d936d2b](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d936d2b))
* 卡片内容不可用拖动 ([6a85c43](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/6a85c43))
* 去掉根据 componentName 判断 isModal 的逻辑 ([28f0213](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/28f0213))
* 可以降级到历史的 JSBlock 格式 ([af1746b](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/af1746b))
* 合并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))
* 在Transducer中添加对MixedSetter的支持 ([7317f2f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7317f2f))
* 在设计器里,所有组件都需要展示,不管 condition 为何值 ([0e7e038](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/0e7e038))
* 增加 getNode 兼容接口 ([5b6792f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/5b6792f))
* 增加兼容 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))
* 处理 schema id 重复的问题 ([d2316be](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d2316be))
* 处理选区的 toolkit 位置不对的 bug ([bfc63db](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bfc63db))
* 复制之后 fieldId 重复 ([36621ea](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/36621ea))
* 实现 removeDocument ([c07b447](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c07b447))
* 快捷键增加判断 ([0f64829](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/0f64829))
* 快捷键增加判断 ([e18a231](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e18a231))
* 拖拽时要解除与原来节点的关系 ([7a6bf2c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7a6bf2c))
* 支持事件 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))
* 暂时使用 live 模式作为条件判断是否限制选中 Page 组件 ([0bab030](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/0bab030))
* 更改生成 id 的规则, 否则命中 recore 解析 id 的一个限制 ([5adff44](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/5adff44))
* 根据目标元素的canDropIn函数判断是否能放入其他元素 ([21d4f64](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/21d4f64))
* 灵犀vc组件中调用config, 补充进去 ([7171aa2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7171aa2))
* 画布BorderAction埋点数据 ([d813b50](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d813b50))
* 禁止组件拉到 Page 的直接子节点, 以及替换 tab 组件 ([d93a291](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d93a291))
* 简化 onPageReady 实现逻辑 ([a36e5f2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/a36e5f2))
* **designer/node.ts:** fix hasLoop logic ([99a7288](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/99a7288))
* 组件缺失占位 ([aff2f34](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/aff2f34))
* 补充documnet-model中addonData 相关方法 ([cbc70ea](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/cbc70ea))
* 解决 set('schema') 后 componentsTree 越来越多的 bug ([a171d3e](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/a171d3e))
* 调整 upgrade 和 init 的流程 ([09fc1a0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/09fc1a0))
* 调整visionNode修改未知 ([da59235](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/da59235))
* 页面加载之后就被标记位 isModified ([2840d27](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2840d27))
### Features
* 🎸 merge material-parser ([b40c286](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/b40c286))
* 🎸 polyfill exchange ([286e7d8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/286e7d8))
* 🎸 polyfill exchange ([7070557](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/7070557))
* 🎸 增加icon获取api ([f1a0823](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/f1a0823))
* 🎸 支持设置模拟器的 viewport 的宽高和缩放级别 ([3a54241](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/3a54241))
* add ? to component designer/src/designer/setting/utils.js ([0025e3c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/0025e3c))
* add alias for get index ([e853a4f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e853a4f))
* add resize box ([14a55ae](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/14a55ae))
* bord resizing ([361f4f6](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/361f4f6))
* border resizing ([c7fc28c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c7fc28c))
* change reducer stage ([c2e83c7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/c2e83c7))
* complete live-editing expr & i18n ([3ac08ba](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/3ac08ba))
* duplicate ([ec932aa](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ec932aa))
* Exchange ([ef5a72e](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ef5a72e))
* extend deviceClassName ([0e96074](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/0e96074))
* for box resizing ([cb2854d](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/cb2854d))
* for box resizing ([77e325f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/77e325f))
* get SettingField instead of SettingPropEntry for compatibility ([2787a12](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2787a12))
* history log ([fbb3577](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/fbb3577))
* import react-docgen to parse propTypes ([6e66168](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/6e66168))
* iphonex 样式 ([e3637b0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e3637b0))
* iphonex 样式 ([08d7875](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/08d7875))
* merge live mode ([92c3039](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/92c3039))
* rax simulator ([05b262d](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/05b262d))
* run vision polyfill ([33750b7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/33750b7))
* show value state ([bd49e50](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bd49e50))
* 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))
* 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))
* **designer:** add blank page logic ([aeeb9ba](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/aeeb9ba))
* **designer:** add builtin hotkeys ([2ec5883](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2ec5883))
* 修复状态切换失效 ([2e3f60d](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2e3f60d))
* 增加 node replaceWith 方法 ([d44f95b](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/d44f95b))
* 增加miniapp外壳 ([bccce8c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/bccce8c))
* 大纲树支持模态视图 ([3785e1c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/3785e1c))
* 导出的schema增加componentsMap ([dbc958c](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/dbc958c))
* 引擎层埋点 ([69de533](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/69de533))
* 新增simulatorurl可以设置cdn使用simulator ([1f45b05](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/1f45b05))
* 新增用于小程序跳过 variable 检测设置 hotvalue 的方法 ([ef799eb](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ef799eb))

View File

@ -1,7 +1,5 @@
{
"plugins": [
[
"build-plugin-component"
]
"build-plugin-component"
]
}

View File

@ -0,0 +1,6 @@
{
"plugins": [
"build-plugin-component",
"@ali/lowcode-test-mate/plugin/index.ts"
]
}

View File

@ -0,0 +1,24 @@
const esModules = ['@recore/obx-react'].join('|');
module.exports = {
// transform: {
// '^.+\\.[jt]sx?$': 'babel-jest',
// // '^.+\\.(ts|tsx)$': 'ts-jest',
// // '^.+\\.(js|jsx)$': 'babel-jest',
// },
// testMatch: ['**/project.test.ts'],
testMatch: ['(/tests?/.*(test))\\.[jt]s$'],
transformIgnorePatterns: [
`/node_modules/(?!${esModules})/`,
],
setupFiles: ['./tests/fixtures/unhandled-rejection.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
collectCoverage: false,
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/icons',
'!**/node_modules/**',
'!**/vendor/**',
],
};

View File

@ -10,8 +10,7 @@
],
"scripts": {
"build": "build-scripts build --skip-demo",
"test": "ava",
"test:snapshot": "ava --update-snapshots"
"test": "build-scripts test --config build.test.json"
},
"license": "MIT",
"dependencies": {
@ -24,23 +23,19 @@
"react-dom": "^16.7.0"
},
"devDependencies": {
"@alib/build-scripts": "^0.1.18",
"@ali/lowcode-test-mate": "^1.0.1",
"@alib/build-scripts": "^0.1.29",
"@types/classnames": "^2.2.7",
"@types/medium-editor": "^5.0.3",
"@types/node": "^13.7.1",
"@types/react": "^16",
"@types/react-dom": "^16",
"build-plugin-component": "^0.2.10"
},
"ava": {
"compileEnhancements": false,
"snapshotDir": "test/fixtures/__snapshots__",
"extensions": [
"ts"
],
"require": [
"ts-node/register"
]
"babel-jest": "^26.5.2",
"build-plugin-component": "^0.2.10",
"build-scripts-config": "^0.1.8",
"jest": "^26.5.2",
"lodash": "^4.17.20",
"typescript": "^4.0.3"
},
"publishConfig": {
"registry": "https://registry.npm.alibaba-inc.com"

View File

@ -57,9 +57,13 @@ export class BorderDetecting extends Component<{ host: BuiltinSimulatorHost }> {
@computed get current() {
const { host } = this.props;
const doc = host.document;
const doc = host.currentDocument;
if (!doc) {
return null;
}
const { selection } = doc;
const { current } = host.designer.detecting;
if (!current || current.document !== doc || selection.has(current.id)) {
return null;
}

View File

@ -1,10 +1,11 @@
import { Component, Fragment } from 'react';
import React, { Component, Fragment } from 'react';
import DragResizeEngine from './drag-resize-engine';
import { observer, computed, globalContext, Editor } from '@ali/lowcode-editor-core';
import classNames from 'classnames';
import { SimulatorContext } from '../context';
import { BuiltinSimulatorHost } from '../host';
import { OffsetObserver, Designer } from '../../designer';
import { Node } from '../../document';
@observer
export default class BoxResizing extends Component<{ host: BuiltinSimulatorHost }> {
@ -19,8 +20,8 @@ export default class BoxResizing extends Component<{ host: BuiltinSimulatorHost
}
@computed get selecting() {
const doc = this.host.document;
if (doc.suspensed) {
const doc = this.host.currentDocument;
if (!doc || doc.suspensed) {
return null;
}
const { selection } = doc;
@ -149,9 +150,9 @@ export class BoxResizingInstance extends Component<{
metaData.experimental.callbacks &&
typeof metaData.experimental.callbacks.onResize === 'function'
) {
e.trigger = direction;
e.deltaX = moveX;
e.deltaY = moveY;
(e as any).trigger = direction;
(e as any).deltaX = moveX;
(e as any).deltaY = moveY;
metaData.experimental.callbacks.onResize(e, node);
}
};
@ -164,7 +165,7 @@ export class BoxResizingInstance extends Component<{
metaData.experimental.callbacks &&
typeof metaData.experimental.callbacks.onResizeStart === 'function'
) {
e.trigger = direction;
(e as any).trigger = direction;
metaData.experimental.callbacks.onResizeStart(e, node);
}
};
@ -177,7 +178,7 @@ export class BoxResizingInstance extends Component<{
metaData.experimental.callbacks &&
typeof metaData.experimental.callbacks.onResizeEnd === 'function'
) {
e.trigger = direction;
(e as any).trigger = direction;
metaData.experimental.callbacks.onResizeEnd(e, node);
}

View File

@ -203,8 +203,8 @@ export class BorderSelecting extends Component<{ host: BuiltinSimulatorHost }> {
}
@computed get selecting() {
const doc = this.host.document;
if (doc.suspensed || this.host.liveEditing.editing) {
const doc = this.host.currentDocument;
if (!doc || doc.suspensed || this.host.liveEditing.editing) {
return null;
}
const { selection } = doc;

View File

@ -38,6 +38,8 @@ export default class DragResizeEngine {
private dragResizing = false;
private designer: Designer;
constructor(designer: Designer) {
this.designer = designer;
this.emitter = new EventEmitter();

View File

@ -1,4 +1,4 @@
import { Component } from 'react';
import React, { Component } from 'react';
import { observer } from '@ali/lowcode-editor-core';
import { BorderDetecting } from './border-detecting';
import { BuiltinSimulatorHost } from '../host';
@ -16,7 +16,11 @@ export class BemTools extends Component<{ host: BuiltinSimulatorHost }> {
render() {
const { host } = this.props;
const { designMode } = host;
const { scrollX, scrollY, scale } = host.viewport;
if (designMode === 'live') {
return null;
}
return (
<div className="lc-bem-tools" style={{ transform: `translate(${-scrollX * scale}px,${-scrollY * scale}px)` }}>
<BorderDetecting key="hovering" host={host} />

View File

@ -11,6 +11,7 @@ import {
import { ISimulatorHost } from '../../simulator';
import { ParentalNode } from '../../document';
import './insertion.less';
import { NodeData, NodeSchema } from '@ali/lowcode-types';
interface InsertionData {
edge?: DOMRect;
@ -18,6 +19,7 @@ interface InsertionData {
vertical?: boolean;
nearRect?: Rect;
coverRect?: DOMRect;
nearNode?: NodeData;
}
/**
@ -41,6 +43,7 @@ function processChildrenDetail(sim: ISimulatorHost, container: ParentalNode, det
if (detail.near) {
const { node, pos, rect, align } = detail.near;
ret.nearRect = rect || sim.computeRect(node);
ret.nearNode = node;
if (pos === 'replace') {
// FIXME: ret.nearRect mybe null
ret.coverRect = ret.nearRect;
@ -82,6 +85,7 @@ function processChildrenDetail(sim: ISimulatorHost, container: ParentalNode, det
ret.insertType = 'after';
}
ret.vertical = isVertical(ret.nearRect);
ret.nearNode = nearNode;
} else {
ret.insertType = 'cover';
ret.coverRect = edge;
@ -118,13 +122,17 @@ export class InsertionView extends Component<{ host: BuiltinSimulatorHost }> {
render() {
const { host } = this.props;
const loc = host.document.dropLocation;
const loc = host.currentDocument?.dropLocation;
if (!loc) {
return null;
}
// 如果是个绝对定位容器,不需要渲染插入标记
if (loc.target.componentMeta.getMetadata().experimental?.isAbsoluteLayoutContainer) {
return null;
}
const { scale, scrollX, scrollY } = host.viewport;
const { edge, insertType, coverRect, nearRect, vertical } = processDetail(loc);
const { edge, insertType, coverRect, nearRect, vertical, nearNode } = processDetail(loc);
if (!edge) {
return null;
@ -157,8 +165,12 @@ export class InsertionView extends Component<{ host: BuiltinSimulatorHost }> {
y = ((insertType === 'before' ? nearRect.top : nearRect.bottom) + scrollY) * scale;
style.width = nearRect.width * scale;
}
if (y === 0 && (nearNode as NodeSchema)?.componentMeta?.isTopFixed) {
return null;
}
}
style.transform = `translate3d(${x}px, ${y}px, 0)`;
style.transition = 'all 0.2s ease-in-out';
return <div className={className} style={style} />;
}

View File

@ -1,8 +1,8 @@
import { Component } from 'react';
import React, { Component } from 'react';
import { observer } from '@ali/lowcode-editor-core';
import { BuiltinSimulatorHost, BuiltinSimulatorProps } from './host';
import { DocumentModel } from '../document';
import { BemTools } from './bem-tools';
import { Project } from '../project';
import './host.less';
/*
@ -10,12 +10,11 @@ import './host.less';
Canvas(DeviceShell) CanvasViewport
CanvasViewport Canvas
Content(Shell) CanvasViewport margin
ContentFrame Content
Auxiliary Content 0,0 Canvas, Content
BemTools Content 0,0 Canvas, Content
*/
type SimulatorHostProps = BuiltinSimulatorProps & {
documentContext: DocumentModel;
project: Project;
onMount?: (host: BuiltinSimulatorHost) => void;
};
@ -24,8 +23,8 @@ export class BuiltinSimulatorHostView extends Component<SimulatorHostProps> {
constructor(props: any) {
super(props);
const { documentContext } = this.props;
this.host = (documentContext.simulator as BuiltinSimulatorHost) || new BuiltinSimulatorHost(documentContext);
const { project } = this.props;
this.host = (project.simulator as BuiltinSimulatorHost) || new BuiltinSimulatorHost(project);
this.host.setProps(this.props);
}

View File

@ -29,6 +29,27 @@
box-shadow: 0 2px 10px 0 rgba(31,56,88,.15);
}
&-device-iphonex { // 增加默认的小程序的壳
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 378px;
height: 812px;
max-height: calc(100vh - 50px);
background: url(https://img.alicdn.com/tfs/TB1b4DHilFR4u4jSZFPXXanzFXa-750-1574.png) no-repeat top;
background-size: 378px 812px;
border-radius: 44px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 36px 42px;
.@{scope}-canvas-viewport {
width: auto;
top: 50px;
left: 0px;
right: 0px;
margin-top: 40px;
max-height: 688px;
}
}
&-device-iphone6 {
left: 50%;
width: 378px;

View File

@ -1,8 +1,8 @@
import { obx, autorun, computed, getPublicPath, hotkey, focusTracker } from '@ali/lowcode-editor-core';
import { ISimulatorHost, Component, NodeInstance, ComponentInstance } from '../simulator';
import { ISimulatorHost, Component, NodeInstance, ComponentInstance, DropContainer } from '../simulator';
import Viewport from './viewport';
import { createSimulator } from './create-simulator';
import { Node, ParentalNode, DocumentModel, isNode, contains, isRootNode } from '../document';
import { Node, ParentalNode, isNode, contains, isRootNode } from '../document';
import ResourceConsumer from './resource-consumer';
import {
AssetLevel,
@ -13,6 +13,7 @@ import {
AssetType,
isElement,
isFormEvent,
hasOwnProperty,
} from '@ali/lowcode-utils';
import {
DragObjectType,
@ -29,12 +30,15 @@ import {
getRectTarget,
Rect,
CanvasPoint,
Designer,
} from '../designer';
import { parseMetadata } from './utils/parse-metadata';
import { ComponentMetadata, NodeSchema } from '@ali/lowcode-types';
import { ComponentMetadata, ComponentSchema } from '@ali/lowcode-types';
import { BuiltinSimulatorRenderer } from './renderer';
import clipboard from '../designer/clipboard';
import { LiveEditing } from './live-editing/live-editing';
import { Project } from '../project';
import { Scroller } from '../designer/scroller';
export interface LibraryItem {
package: string;
@ -113,9 +117,23 @@ const defaultRaxEnvironment = [
export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProps> {
readonly isSimulator = true;
constructor(readonly document: DocumentModel) {}
readonly project: Project;
readonly designer = this.document.designer;
readonly designer: Designer;
readonly viewport = new Viewport();
readonly scroller: Scroller;
constructor(project: Project) {
this.project = project;
this.designer = project?.designer;
this.scroller = this.designer.createScroller(this.viewport);
}
get currentDocument() {
return this.project.currentDocument;
}
@computed get renderEnv(): string {
return this.get('renderEnv') || 'default';
@ -182,6 +200,10 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
return autorun(fn as any, true);
}
autorun(fn: (context: { dispose: () => void; firstRun: boolean }) => void) {
return autorun(fn as any, true);
}
purge(): void {
// todo
}
@ -313,9 +335,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
}
setupDragAndClick() {
const documentModel = this.document;
const { selection } = documentModel;
const { designer } = documentModel;
const designer = this.designer;
const doc = this.contentDocument!;
// TODO: think of lock when edit a node
@ -325,7 +345,15 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
(downEvent: MouseEvent) => {
// fix for popups close logic
document.dispatchEvent(new Event('mousedown'));
if (this.liveEditing.editing) {
const documentModel = this.project.currentDocument;
if (this.liveEditing.editing || !documentModel) {
return;
}
const selection = documentModel.selection;
let isMulti = false;
if (this.designMode === 'design') {
isMulti = downEvent.metaKey || downEvent.ctrlKey;
} else if (!downEvent.metaKey) {
return;
}
// stop response document focus event
@ -336,23 +364,32 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
(downEvent.target as HTMLElement).removeAttribute('for');
const nodeInst = this.getNodeInstanceFromElement(downEvent.target as Element);
const node = nodeInst?.node || this.document.rootNode;
if (!node?.isValidComponent()) {
// 对于未注册组件直接返回
return;
}
const isMulti = downEvent.metaKey || downEvent.ctrlKey;
const node = nodeInst?.node || documentModel?.rootNode;
// if (!node?.isValidComponent()) {
// // 对于未注册组件直接返回
// return;
// }
const isLeftButton = downEvent.which === 1 || downEvent.button === 0;
const checkSelect = (e: MouseEvent) => {
doc.removeEventListener('mouseup', checkSelect, true);
// 鼠标是否移动
if (!isShaken(downEvent, e)) {
const { id } = node;
let id = node.id;
designer.activeTracker.track({ node, instance: nodeInst?.instance });
if (isMulti && !isRootNode(node) && selection.has(id)) {
selection.remove(id);
} else {
// TODO: 避免选中 Page 组件,默认选中第一个子节点;新增规则 或 判断 Live 模式
if (node.isPage() && node.getChildren()?.notEmpty() && this.designMode === 'live') {
const firstChildId = node
.getChildren()
?.get(0)
?.getId();
if (firstChildId) id = firstChildId;
}
selection.select(id);
// dirty code should refector
const editor = this.designer?.editor;
const npm = node?.componentMeta?.npm;
const selected =
@ -376,7 +413,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
selection.add(node.id);
ignoreUpSelected = true;
}
selection.remove(this.document.rootNode.id);
selection.remove(documentModel.rootNode.id);
// 获得顶层 nodes
nodes = selection.getTopNodes();
} else if (selection.containsNode(node, true)) {
@ -447,21 +484,22 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
private disableHovering?: () => void;
private disableDetecting?: () => void;
/**
*
*/
setupDetecting() {
const doc = this.contentDocument!;
const { detecting } = this.document.designer;
const detecting = this.designer.detecting;
const hover = (e: MouseEvent) => {
if (!detecting.enable) {
if (!detecting.enable || this.designMode !== 'design') {
return;
}
const nodeInst = this.getNodeInstanceFromElement(e.target as Element);
detecting.capture(nodeInst?.node || null);
e.stopPropagation();
};
const leave = () => detecting.leave(this.document);
const leave = () => detecting.leave(this.project.currentDocument);
doc.addEventListener('mouseover', hover, true);
doc.addEventListener('mouseleave', leave, false);
@ -475,12 +513,12 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
true,
);
this.disableHovering = () => {
detecting.leave(this.document);
doc.removeEventListener('mouseover', hover, true);
doc.removeEventListener('mouseleave', leave, false);
this.disableHovering = undefined;
};
// this.disableDetecting = () => {
// detecting.leave(this.project.currentDocument);
// doc.removeEventListener('mouseover', hover, true);
// doc.removeEventListener('mouseleave', leave, false);
// this.disableDetecting = undefined;
// };
}
readonly liveEditing = new LiveEditing();
@ -500,7 +538,10 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
if (!nodeInst) {
return;
}
const node = nodeInst.node || this.document.rootNode;
const node = nodeInst.node || this.project.currentDocument?.rootNode;
if (!node) {
return;
}
const rootElement = this.findDOMNodes(nodeInst.instance, node.componentMeta.rootSelector)?.find(
(item) =>
@ -525,15 +566,22 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
* @see ISimulator
*/
setSuspense(suspended: boolean) {
if (suspended) {
if (this.disableHovering) {
this.disableHovering();
}
// sleep some autorun reaction
} else if (!this.disableHovering) {
// weekup some autorun reaction
this.setupDetecting();
}
return false;
// if (suspended) {
// /*
// if (this.disableDetecting) {
// this.disableDetecting();
// }
// */
// // sleep some autorun reaction
// } else {
// // weekup some autorun reaction
// /*
// if (!this.disableDetecting) {
// this.setupDetecting();
// }
// */
// }
}
setupContextMenu() {
@ -544,10 +592,12 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
if (!nodeInst) {
return;
}
const node = nodeInst.node || this.document.rootNode;
const node = nodeInst.node || this.project.currentDocument?.rootNode;
if (!node) {
return;
}
// dirty code should refector
const editor = this.designer?.editor;
const npm = node?.componentMeta?.npm;
const selected =
@ -597,32 +647,40 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
return this.renderer?.getComponent(componentName) || null;
}
createComponent(schema: NodeSchema): Component | null {
return this.renderer?.createComponent(schema) || null;
createComponent(schema: ComponentSchema): Component | null {
return null;
// return this.renderer?.createComponent(schema) || null;
}
@obx.val private instancesMap = new Map<string, ComponentInstance[]>();
setInstance(id: string, instances: ComponentInstance[] | null) {
@obx private instancesMap: {
[docId: string]: Map<string, ComponentInstance[]>;
} = {};
setInstance(docId: string, id: string, instances: ComponentInstance[] | null) {
if (!hasOwnProperty(this.instancesMap, docId)) {
this.instancesMap[docId] = new Map();
}
if (instances == null) {
this.instancesMap.delete(id);
this.instancesMap[docId].delete(id);
} else {
this.instancesMap.set(id, instances.slice());
this.instancesMap[docId].set(id, instances.slice());
}
}
/**
* @see ISimulator
*/
getComponentInstances(node: Node): ComponentInstance[] | null {
return this.instancesMap.get(node.id) || null;
}
getComponentInstances(node: Node, context?: NodeInstance): ComponentInstance[] | null {
const docId = node.document.id;
/**
* @see ISimulator
*/
getComponentInstanceId(/* instance: ComponentInstance */) {
throw new Error('Method not implemented.');
let instances = this.instancesMap[docId]?.get(node.id) || null;
if (!instances || !context) {
return instances;
}
// filter with context
return instances.filter((instance) => {
return this.getClosestNodeInstance(instance, context.nodeId)?.instance === context.instance
});
}
/**
@ -747,7 +805,9 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
if (!nodeIntance) {
return null;
}
const node = this.document.getNode(nodeIntance.nodeId);
const { docId } = nodeIntance;
const doc = this.project.getDocument(docId)!;
const node = doc.getNode(nodeIntance.nodeId);
return {
...nodeIntance,
node,
@ -875,9 +935,6 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
e.target = this.contentDocument?.elementFromPoint(e.canvasX!, e.canvasY!);
}
// documentModel : 目标文档
e.documentModel = this.document;
// 事件已订正
e.fixed = true;
return e;
@ -923,6 +980,10 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
this.sensing = true;
this.scroller.scrolling(e);
const document = this.project.currentDocument;
if (!document) {
return null;
}
const dropContainer = this.getDropContainer(e);
const canDropIn = dropContainer?.container?.componentMeta?.prototype?.options?.canDropIn;
@ -956,27 +1017,22 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
};
const locationData = {
target: container,
target: container as ParentalNode,
detail,
source: `simulator${ this.document.id}`,
source: 'simulator' + document.id,
event: e,
};
// if (e.dragObject.type === 'node' && e.dragObject.nodes[0]?.getPrototype()?.isModal()) {
if (
e.dragObject &&
e.dragObject.nodes &&
e.dragObject.nodes.length &&
e.dragObject.nodes[0].getPrototype()?.isModal()
e.dragObject.nodes[0].componentMeta.isModal
) {
return this.designer.createLocation({
target: this.document.rootNode,
detail: {
type: LocationDetailType.Children,
index: 0,
valid: true,
},
source: `simulator${ this.document.id}`,
target: document.rootNode,
detail,
source: 'simulator' + document.id,
event: e,
});
}
@ -1077,10 +1133,11 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
/**
*
*/
getDropContainer(e: LocateEvent): DropContainer | LocationData | null {
getDropContainer(e: LocateEvent): DropContainer | null {
const { target, dragObject } = e;
const isAny = isDragAnyObject(dragObject);
const { modalNode, currentRoot } = this.document;
const document = this.project.currentDocument!;
const { currentRoot } = document;
let container: Node;
let nodeInstance: NodeInstance<ComponentInstance> | undefined;
@ -1104,11 +1161,6 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
container = container.parent || currentRoot;
}
// check container if in modalNode layer, if not, use modalNode
if (modalNode && !modalNode.contains(container)) {
container = modalNode;
}
// TODO: use spec container to accept specialData
if (isAny) {
// will return locationData
@ -1116,7 +1168,6 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
}
// get common parent, avoid drop container contains by dragObject
// TODO: renderengine support pointerEvents: none for acceleration
const drillDownExcludes = new Set<Node>();
if (isDragNodeObject(dragObject)) {
const { nodes } = dragObject;
@ -1128,63 +1179,70 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
}
}
if (p !== container) {
container = p || this.document.rootNode;
container = p || document.rootNode;
drillDownExcludes.add(container);
}
}
const ret: any = {
container,
};
let instance: any;
if (nodeInstance) {
if (nodeInstance.node === container) {
ret.instance = nodeInstance.instance;
instance = nodeInstance.instance;
} else {
ret.instance = this.getClosestNodeInstance(nodeInstance.instance as any, container.id)?.instance;
instance = this.getClosestNodeInstance(nodeInstance.instance as any, container.id)?.instance;
}
} else {
ret.instance = this.getComponentInstances(container)?.[0];
instance = this.getComponentInstances(container)?.[0];
}
let dropContainer: DropContainer = {
container: container as any,
instance
};
let res: any;
let upward: any;
// TODO: complete drill down logic
let upward: DropContainer | null = null;
while (container) {
if (ret.container !== container) {
ret.container = container;
ret.instance = this.getClosestNodeInstance(ret.instance, container.id)?.instance;
}
res = this.handleAccept(ret, e);
if (isLocationData(res)) {
return res;
}
res = this.handleAccept(dropContainer, e);
// if (isLocationData(res)) {
// return res;
// }
if (res === true) {
return ret;
return dropContainer;
}
if (!res) {
drillDownExcludes.add(container);
if (upward) {
container = upward;
dropContainer = upward;
container = dropContainer.container;
upward = null;
} else if (container.parent) {
container = container.parent;
instance = this.getClosestNodeInstance(dropContainer.instance, container.id)?.instance;
dropContainer = {
container: container as ParentalNode,
instance
};
} else {
return null;
}
} else if (isNode(res)) {
/* else if (res === DRILL_DOWN) {
}/* else if (res === DRILL_DOWN) {
if (!upward) {
upward = container.parent;
container = container.parent;
instance = this.getClosestNodeInstance(dropContainer.instance, container.id)?.instance;
upward = {
container,
instance
};
}
container = this.getNearByContainer(container, drillExcludes, e);
if (!container) {
container = upward;
dropContainer = this.getNearByContainer(dropContainer, drillDownExcludes, e);
if (!dropContainer) {
dropContainer = upward;
upward = null;
}
} */
container = res;
upward = null;
}
} else if (isNode(res)) {
// TODO:
}*/
}
return null;
}
@ -1204,10 +1262,11 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
/**
*
*/
handleAccept({ container/* , instance */ }: DropContainer, e: LocateEvent) {
handleAccept({ container, instance }: DropContainer, e: LocateEvent): boolean {
const { dragObject } = e;
const document = this.currentDocument!;
if (isRootNode(container)) {
return this.document.checkDropTarget(container, dragObject as any);
return document.checkDropTarget(container, dragObject as any);
}
const meta = (container as Node).componentMeta;
@ -1246,36 +1305,44 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
}
// check nesting
return this.document.checkNesting(container, dragObject as any);
return document.checkNesting(container, dragObject as any);
}
/**
*
*/
getNearByContainer(/* container: ParentalNode, e: LocateEvent */) {
/*
getNearByContainer({ container, instance }: DropContainer, drillDownExcludes: Set<Node>, e: LocateEvent) {
const children = container.children;
if (!children || children.length < 1) {
const document = this.project.currentDocument!;
if (!children || children.isEmpty()) {
return null;
}
let nearDistance: any = null;
let nearBy: any = null;
for (let i = 0, l = children.length; i < l; i++) {
let child: any = children[i];
if (!isElementNode(child)) {
for (let i = 0, l = children.size; i < l; i++) {
let child = children.get(i);
if (!child) {
continue;
}
if (hasConditionFlow(child)) {
const bn = child.conditionFlow;
if (child.conditionGroup) {
const bn = child.conditionGroup;
i = bn.index + bn.length - 1;
child = bn.visibleNode;
}
const rect = this.document.computeRect(child);
if (!child.isParental() || drillDownExcludes.has(child)) {
continue;
}
// TODO:
this.findDOMNodes(instance);
this.getComponentInstances(child)
const rect = this.computeRect(child);
if (!rect) {
continue;
}
/*
if (isPointInRect(e, rect)) {
return child;
}
@ -1284,11 +1351,10 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
if (nearDistance === null || distance < nearDistance) {
nearDistance = distance;
nearBy = child;
}
}*/
}
return nearBy;
*/
}
// #endregion
}
@ -1354,8 +1420,3 @@ function getMatched(elements: Array<Element | Text>, selector: string): Element
}
return firstQueried;
}
interface DropContainer {
container: ParentalNode;
instance: ComponentInstance;
}

View File

@ -22,7 +22,7 @@
fill: var(--color-icon-white, @title-bgcolor);
}
}
&-current {
.instance-node-selector-current {
background: var(--color-brand, @brand-color-1);
padding: 0 6px;
display: flex;
@ -37,14 +37,14 @@
color: var(--color-icon-white, @title-bgcolor);
}
}
&-list {
.instance-node-selector-list {
position: absolute;
left: 0;
right: 0;
opacity: 0;
visibility: hidden;
}
&-node {
.instance-node-selector-node {
margin: 2px 0;
&-content {
padding-left: 6px;
@ -68,15 +68,15 @@
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;
&:hover {
.instance-node-selector-current {
color: ar(--color-text-reverse, @white-alpha-2);
}
.instance-node-selector-popup {
visibility: visible;
opacity: 1;
transition: 0.2s all ease-in;
}
}
}

View File

@ -5,7 +5,6 @@ export interface BuiltinSimulatorRenderer {
readonly isSimulatorRenderer: true;
createComponent(schema: NodeSchema): Component | null;
getComponent(componentName: string): Component;
getComponentInstances(id: string): ComponentInstance[] | null;
getClosestNodeInstance(from: ComponentInstance, nodeId?: string): NodeInstance<ComponentInstance> | null;
findDOMNodes(instance: ComponentInstance): Array<Element | Text> | null;
getClientRects(element: Element | Text): DOMRect[];

View File

@ -54,7 +54,7 @@ const LowcodeTypes: any = {
(window as any).PropTypes = LowcodeTypes;
(window as any).React.PropTypes = LowcodeTypes;
// override primitive type chechers
// override primitive type checkers
primitiveTypes.forEach(type => {
const propType = (PropTypes as any)[type];
if (!propType) {

View File

@ -13,7 +13,7 @@ import {
FieldConfig,
} from '@ali/lowcode-types';
import { computed } from '@ali/lowcode-editor-core';
import { Node, ParentalNode } from './document';
import { isNode, Node, ParentalNode } from './document';
import { Designer } from './designer';
import { intlNode } from './locale';
import { IconContainer } from './icons/container';
@ -70,6 +70,10 @@ export class ComponentMeta {
return this._npm;
}
set npm(_npm) {
this._npm = _npm;
}
private _componentName?: string;
get componentName(): string {
@ -113,6 +117,12 @@ export class ComponentMeta {
return this._liveTextEditing;
}
private _isTopFixed?: boolean;
get isTopFixed() {
return this._isTopFixed;
}
private parentWhitelist?: NestingFilter | null;
private childWhitelist?: NestingFilter | null;
@ -195,6 +205,12 @@ export class ComponentMeta {
collectLiveTextEditing(this.configure);
this._liveTextEditing = liveTextEditing.length > 0 ? liveTextEditing : undefined;
const isTopFiexd = this._transformedMetadata.experimental?.isTopFixed;
if (isTopFiexd) {
this._isTopFixed = isTopFiexd;
}
const { configure = {} } = this._transformedMetadata;
this._acceptable = false;
@ -264,6 +280,9 @@ export class ComponentMeta {
checkNestingDown(my: Node, target: Node | NodeSchema) {
// 检查父子关系,直接约束型,在画布中拖拽直接掠过目标容器
if (this.childWhitelist) {
if (!isNode(target)) {
target = new Node(my.document, target);
}
return this.childWhitelist(target, my);
}
return true;

View File

@ -1,9 +1,16 @@
import { hotkey } from '@ali/lowcode-editor-core';
import { hotkey, Editor, globalContext } from '@ali/lowcode-editor-core';
import { isFormEvent } from '@ali/lowcode-utils';
import { focusing } from './focusing';
import { insertChildren, TransformStage } from '../document';
import clipboard from './clipboard';
function isInLiveEditing() {
if (globalContext.has(Editor)) {
return Boolean(globalContext.get(Editor).get('designer')?.project?.simulator?.liveEditing?.editing);
}
return false;
}
function getNextForSelect(next: any, head?: any, parent?: any): any {
if (next) {
if (!head) {
@ -66,6 +73,7 @@ function getPrevForSelect(prev: any, head?: any, parent?: any): any {
// hotkey binding
hotkey.bind(['backspace', 'del'], (e: KeyboardEvent) => {
if (isInLiveEditing()) return;
// TODO: use focus-tracker
const doc = focusing.focusDesigner?.currentDocument;
if (isFormEvent(e) || !doc) {
@ -86,6 +94,7 @@ hotkey.bind(['backspace', 'del'], (e: KeyboardEvent) => {
hotkey.bind('escape', (e: KeyboardEvent) => {
// const currentFocus = focusing.current;
if (isInLiveEditing()) return;
const sel = focusing.focusDesigner?.currentDocument?.selection;
if (isFormEvent(e) || !sel) {
return;
@ -98,6 +107,7 @@ hotkey.bind('escape', (e: KeyboardEvent) => {
// command + c copy command + x cut
hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => {
if (isInLiveEditing()) return;
const doc = focusing.focusDesigner?.currentDocument;
if (isFormEvent(e) || !doc) {
return;
@ -133,6 +143,7 @@ hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => {
// command + v paste
hotkey.bind(['command+v', 'ctrl+v'], (e) => {
if (isInLiveEditing()) return;
const designer = focusing.focusDesigner;
const doc = designer?.currentDocument;
if (isFormEvent(e) || !designer || !doc) {
@ -155,6 +166,7 @@ hotkey.bind(['command+v', 'ctrl+v'], (e) => {
// command + z undo
hotkey.bind(['command+z', 'ctrl+z'], (e) => {
if (isInLiveEditing()) return;
const his = focusing.focusDesigner?.currentHistory;
if (isFormEvent(e) || !his) {
return;
@ -166,6 +178,7 @@ hotkey.bind(['command+z', 'ctrl+z'], (e) => {
// command + shift + z redo
hotkey.bind(['command+y', 'ctrl+y', 'command+shift+z'], (e) => {
if (isInLiveEditing()) return;
const his = focusing.focusDesigner?.currentHistory;
if (isFormEvent(e) || !his) {
return;
@ -177,6 +190,7 @@ hotkey.bind(['command+y', 'ctrl+y', 'command+shift+z'], (e) => {
// sibling selection
hotkey.bind(['left', 'right'], (e, action) => {
if (isInLiveEditing()) return;
const designer = focusing.focusDesigner;
const doc = designer?.currentDocument;
if (isFormEvent(e) || !doc) {
@ -193,6 +207,7 @@ hotkey.bind(['left', 'right'], (e, action) => {
});
hotkey.bind(['up', 'down'], (e, action) => {
if (isInLiveEditing()) return;
const designer = focusing.focusDesigner;
const doc = designer?.currentDocument;
if (isFormEvent(e) || !doc) {
@ -215,6 +230,7 @@ hotkey.bind(['up', 'down'], (e, action) => {
});
hotkey.bind(['option+left', 'option+right'], (e, action) => {
if (isInLiveEditing()) return;
const designer = focusing.focusDesigner;
const doc = designer?.currentDocument;
if (isFormEvent(e) || !doc) {
@ -246,6 +262,7 @@ hotkey.bind(['option+left', 'option+right'], (e, action) => {
});
hotkey.bind(['option+up'], (e) => {
if (isInLiveEditing()) return;
const designer = focusing.focusDesigner;
const doc = designer?.currentDocument;
if (isFormEvent(e) || !doc) {
@ -284,6 +301,7 @@ hotkey.bind(['option+up'], (e) => {
});
hotkey.bind(['option+down'], (e) => {
if (isInLiveEditing()) return;
const designer = focusing.focusDesigner;
const doc = designer?.currentDocument;
if (isFormEvent(e) || !doc) {

View File

@ -9,6 +9,7 @@ import {
CompositeObject,
PropsList,
isNodeSchema,
NodeSchema,
} from '@ali/lowcode-types';
import { Project } from '../project';
import { Node, DocumentModel, insertChildren, isRootNode, ParentalNode, TransformStage } from '../document';
@ -171,20 +172,6 @@ export class Designer {
node.document.simulator?.scrollToNode(node, detail);
});
let selectionDispose: undefined | (() => void);
const setupSelection = () => {
if (selectionDispose) {
selectionDispose();
selectionDispose = undefined;
}
this.postEvent('selection.change', this.currentSelection);
if (this.currentSelection) {
const { currentSelection } = this;
selectionDispose = currentSelection.onSelectionChange(() => {
this.postEvent('selection.change', currentSelection);
});
}
};
let historyDispose: undefined | (() => void);
const setupHistory = () => {
if (historyDispose) {
@ -203,17 +190,39 @@ export class Designer {
this.postEvent('current-document.change', this.currentDocument);
this.postEvent('selection.change', this.currentSelection);
this.postEvent('history.change', this.currentHistory);
setupSelection();
this.setupSelection();
setupHistory();
});
this.postEvent('designer.init', this);
setupSelection();
this.setupSelection();
setupHistory();
// TODO: 先简单实现,后期通过焦点赋值
focusing.focusDesigner = this;
}
setupSelection = () => {
let selectionDispose: undefined | (() => void);
if (selectionDispose) {
selectionDispose();
selectionDispose = undefined;
}
const currentSelection = this.currentSelection;
// TODO: 避免选中 Page 组件,默认选中第一个子节点;新增规则 或 判断 Live 模式
if (currentSelection && currentSelection.selected.length === 0 && this.simulatorProps?.designMode === 'live') {
const rootNodeChildrens = this.currentDocument.getRoot().getChildren().children;
if (rootNodeChildrens.length > 0) {
currentSelection.select(rootNodeChildrens[0].id);
}
}
this.postEvent('selection.change', currentSelection);
if (currentSelection) {
selectionDispose = currentSelection.onSelectionChange(() => {
this.postEvent('selection.change', currentSelection);
});
}
};
postEvent(event: string, ...args: any[]) {
this.editor.emit(`designer.${event}`, ...args);
}
@ -286,7 +295,7 @@ export class Designer {
/**
*
*/
getSuitableInsertion(): { target: ParentalNode; index?: number } | null {
getSuitableInsertion(insertNode?: Node | NodeSchema): { target: ParentalNode; index?: number } | null {
const activedDoc = this.project.currentDocument;
if (!activedDoc) {
return null;
@ -298,7 +307,7 @@ export class Designer {
target = activedDoc.rootNode;
} else {
const node = nodes[0];
if (isRootNode(node)) {
if (isRootNode(node) || node.componentMeta.isContainer) {
target = node;
} else {
// FIXME!!, parent maybe null
@ -306,6 +315,11 @@ export class Designer {
index = node.index + 1;
}
}
if (target && insertNode && !target.componentMeta.checkNestingDown(target, insertNode)) {
return null;
}
return { target, index };
}
@ -322,6 +336,10 @@ export class Designer {
}
if (props.simulatorProps !== this.props.simulatorProps) {
this._simulatorProps = props.simulatorProps;
// 重新 setupSelection
if (props.simulatorProps?.designMode !== this.props.simulatorProps?.designMode) {
this.setupSelection();
}
}
if (props.suspensed !== this.props.suspensed && props.suspensed != null) {
this.suspensed = props.suspensed;
@ -362,7 +380,7 @@ export class Designer {
@obx.ref private _simulatorProps?: object | ((document: DocumentModel) => object);
@computed get simulatorProps(): object | ((document: DocumentModel) => object) {
@computed get simulatorProps(): object | ((project: Project) => object) {
return this._simulatorProps || {};
}
@ -447,12 +465,12 @@ export class Designer {
designer._componentMetasMap.forEach((config, key) => {
const metaData = config.getMetadata();
if (metaData.devMode === 'lowcode') {
maps[key] = this.currentDocument?.simulator?.createComponent(metaData.schema!);
maps[key] = metaData.schema;
} else {
const view = metaData.experimental?.view;
if (view) {
maps[key] = view;
} else if (config.npm) {
} else {
maps[key] = config.npm;
}
}

View File

@ -33,7 +33,7 @@ export class Detecting {
}
}
leave(document: DocumentModel) {
leave(document: DocumentModel | undefined) {
if (this.current && this.current.document === document) {
this._current = null;
}

View File

@ -2,6 +2,7 @@ import { Component } from 'react';
import { observer, obx, Title } from '@ali/lowcode-editor-core';
import { Designer } from '../designer';
import { DragObject, isDragNodeObject, isDragNodeDataObject } from '../dragon';
import { isSimulatorHost } from '../../simulator';
import './ghost.less';
type offBinding = () => any;
@ -16,6 +17,8 @@ export default class DragGhost extends Component<{ designer: Designer }> {
@obx.ref private y = 0;
@obx private isAbsoluteLayoutContainer = false;
private dragon = this.props.designer.dragon;
constructor(props: any) {
@ -32,6 +35,14 @@ export default class DragGhost extends Component<{ designer: Designer }> {
this.dragon.onDrag(e => {
this.x = e.globalX;
this.y = e.globalY;
if (isSimulatorHost(e.sensor)) {
const container = e.sensor.getDropContainer(e);
if (container.container.componentMeta.getMetadata().experimental?.isAbsoluteLayoutContainer) {
this.isAbsoluteLayoutContainer = true;
return;
}
}
this.isAbsoluteLayoutContainer = false;
}),
this.dragon.onDragend(() => {
this.dragObject = null;
@ -84,6 +95,10 @@ export default class DragGhost extends Component<{ designer: Designer }> {
return null;
}
if (this.isAbsoluteLayoutContainer) {
return null;
}
return (
<div
className="lc-ghost-group"

View File

@ -418,7 +418,8 @@ export class Dragon {
if (!sourceDocument || sourceDocument === document) {
evt.globalX = e.clientX;
evt.globalY = e.clientY;
} else { // event from simulator sandbox
} else {
// event from simulator sandbox
let srcSim: ISimulatorHost | undefined;
const lastSim = lastSensor && isSimulatorHost(lastSensor) ? lastSensor : null;
// check source simulator
@ -519,22 +520,30 @@ export class Dragon {
}
private getMasterSensors(): ISimulatorHost[] {
return this.designer.project.documents
.map((doc) => {
// TODO: not use actived,
if (doc.actived && doc.simulator?.sensorAvailable) {
return doc.simulator;
}
return null;
})
.filter(Boolean) as any;
return Array.from(
new Set(
this.designer.project.documents
.map((doc) => {
// TODO: not use actived,
if (doc.actived && doc.simulator?.sensorAvailable) {
return doc.simulator;
}
return null;
})
.filter(Boolean) as any,
),
);
}
private getSimulators() {
return new Set(this.designer.project.documents.map(doc => doc.simulator));
}
// #region ======== drag and drop helpers ============
private setNativeSelection(enableFlag: boolean) {
setNativeSelection(enableFlag);
this.designer.project.documents.forEach((doc) => {
doc.simulator?.setNativeSelection(enableFlag);
this.getSimulators().forEach((sim) => {
sim?.setNativeSelection(enableFlag);
});
}
@ -543,8 +552,8 @@ export class Dragon {
*/
private setDraggingState(state: boolean) {
cursor.setDragging(state);
this.designer.project.documents.forEach((doc) => {
doc.simulator?.setDraggingState(state);
this.getSimulators().forEach((sim) => {
sim?.setDraggingState(state);
});
}
@ -553,8 +562,8 @@ export class Dragon {
*/
private setCopyState(state: boolean) {
cursor.setCopy(state);
this.designer.project.documents.forEach((doc) => {
doc.simulator?.setCopyState(state);
this.getSimulators().forEach((sim) => {
sim?.setCopyState(state);
});
}
@ -563,8 +572,8 @@ export class Dragon {
*/
private clearState() {
cursor.release();
this.designer.project.documents.forEach((doc) => {
doc.simulator?.clearState();
this.getSimulators().forEach((sim) => {
sim?.clearState();
});
}
// #endregion

View File

@ -140,6 +140,18 @@ export class SettingField extends SettingPropEntry implements SettingEntry {
return this.transducer.toHot(v);
}
setMiniAppDataSourceValue(data: any, options?: any) {
this.hotValue = data;
const v = this.transducer.toNative(data);
this.setValue(v, false, false, options);
// dirty fix list setter
if (Array.isArray(data) && data[0] && data[0].__sid__) {
return;
}
this.valueChange();
}
setHotValue(data: any, options?: any) {
this.hotValue = data;
const v = this.transducer.toNative(data);
@ -154,6 +166,8 @@ export class SettingField extends SettingPropEntry implements SettingEntry {
this.setValue(v, false, false, options);
}
this.notifyValueChange();
// dirty fix list setter
if (Array.isArray(data) && data[0] && data[0].__sid__) {
return;

View File

@ -276,6 +276,10 @@ export class SettingPropEntry implements SettingEntry {
this.emitter.emit('valuechange');
}
notifyValueChange() {
this.editor.emit('node.prop.change', { node: this.getNode(), prop: this });
}
getDefaultValue() {
return this.extraProps.defaultValue;
}

View File

@ -68,7 +68,7 @@ export class SettingTopEntry implements SettingEntry {
readonly designer: Designer;
constructor(readonly editor: IEditor, readonly nodes: Node[]) {
if (nodes.length < 1) {
if (!Array.isArray(nodes) || nodes.length < 1) {
throw new ReferenceError('nodes should not be empty');
}
this.id = generateSessionId(nodes);

View File

@ -31,6 +31,10 @@ function combineTransducer(transducer, arr, context) {
}
export class Transducer {
setterTransducer: any;
context: any;
constructor(context, config) {
let { setter } = config;

View File

@ -4,12 +4,12 @@ import { EventEmitter } from 'events';
import { Project } from '../project';
import { ISimulatorHost } from '../simulator';
import { ComponentMeta } from '../component-meta';
import { isDragNodeDataObject, DragNodeObject, DragNodeDataObject, DropLocation } from '../designer';
import { isDragNodeDataObject, DragNodeObject, DragNodeDataObject, DropLocation, Designer } from '../designer';
import { Node, insertChildren, insertChild, isNode, RootNode, ParentalNode } from './node/node';
import { Selection } from './selection';
import { History } from './history';
import { TransformStage, ModalNodesManager } from './node';
import { uniqueId } from '@ali/lowcode-utils';
import { uniqueId, isPlainObject } from '@ali/lowcode-utils';
export type GetDataType<T, NodeType> = T extends undefined
? NodeType extends {
@ -56,6 +56,10 @@ export class DocumentModel {
private _nodesMap = new Map<string, Node>();
readonly project: Project;
readonly designer: Designer;
@obx.val private nodes = new Set<Node>();
private seqId = 0;
@ -69,13 +73,13 @@ export class DocumentModel {
/**
* @deprecated
*/
private _addons: { [key: string]: { exportData: () => any; isProp: boolean;} } = {};
private _addons: Array<{ name: string, exportData: any }> = [];
/**
*
*/
get simulator(): ISimulatorHost | null {
return this._simulator || null;
return this.project.simulator;
}
get nodesMap(): Map<string, Node> {
@ -83,28 +87,20 @@ export class DocumentModel {
}
get fileName(): string {
return this.rootNode.getExtraProp('fileName')?.getAsString() || this.id;
return this.rootNode?.getExtraProp('fileName')?.getAsString() || this.id;
}
set fileName(fileName: string) {
this.rootNode.getExtraProp('fileName', true)?.setValue(fileName);
this.rootNode?.getExtraProp('fileName', true)?.setValue(fileName);
}
private _modalNode?: ParentalNode;
private _blank?: boolean;
get modalNode() {
return this._modalNode;
}
get currentRoot() {
return this.modalNode || this.rootNode;
}
private inited = false;
constructor(readonly project: Project, schema?: RootSchema) {
constructor(project: Project, schema?: RootSchema) {
/*
// TODO
// use special purge process
@ -112,6 +108,8 @@ export class DocumentModel {
console.info(this.willPurgeSpace);
}, true);
*/
this.project = project;
this.designer = this.project?.designer;
this.emitter = new EventEmitter();
if (!schema) {
@ -140,6 +138,14 @@ export class DocumentModel {
@obx.val private willPurgeSpace: Node[] = [];
get modalNode() {
return this._modalNode;
}
get currentRoot() {
return this.modalNode || this.rootNode;
}
addWillPurge(node: Node) {
this.willPurgeSpace.push(node);
}
@ -155,16 +161,14 @@ export class DocumentModel {
return this._blank && !this.isModified();
}
readonly designer = this.project.designer;
/**
* id
*/
nextId() {
let id;
do {
id = `node_${ (this.id.slice(-10) + (++this.seqId).toString(36)).toLocaleLowerCase()}`;
} while (this.nodesMap.get(id));
nextId(possibleId: string | undefined) {
let id = possibleId;
while (!id || this.nodesMap.get(id)) {
id = `node_${(String(this.id).slice(-10) + (++this.seqId).toString(36)).toLocaleLowerCase()}`;
}
return id;
}
@ -186,10 +190,6 @@ export class DocumentModel {
@obx.val private activeNodes?: Node[];
private setupListenActiveNodes() {
// todo:
}
/**
* schema
*/
@ -205,7 +205,7 @@ export class DocumentModel {
}
let node: Node | null = null;
if (this.inited && checkId) {
if (this.hasNode(schema?.id)) {
schema.id = null;
}
if (schema.id) {
@ -274,7 +274,7 @@ export class DocumentModel {
if (!node) {
return;
}
this.internalRemoveAndPurgeNode(node);
this.internalRemoveAndPurgeNode(node, true);
}
/**
@ -350,7 +350,18 @@ export class DocumentModel {
}
export(stage: TransformStage = TransformStage.Serilize) {
return this.rootNode?.export(stage);
// 置顶只作用于 Page 的第一级子节点,目前还用不到里层的置顶;如果后面有需要可以考虑将这段写到 node-children 中的 export
const currentSchema = this.rootNode?.export(stage);
if (Array.isArray(currentSchema?.children) && currentSchema?.children.length > 0) {
const FixedTopNodeIndex = currentSchema.children
.filter(i => isPlainObject(i))
.findIndex((i => (i as NodeSchema).props?.__isTopFixed__));
if (FixedTopNodeIndex > 0) {
const FixedTopNode = currentSchema.children.splice(FixedTopNodeIndex, 1);
currentSchema.children.unshift(FixedTopNode[0]);
}
}
return currentSchema;
}
/**
@ -542,7 +553,7 @@ export class DocumentModel {
// add toData
toData(extraComps?: string[]) {
const node = this.project?.currentDocument?.export(TransformStage.Save);
const node = this.export(TransformStage.Save);
const data = {
componentsMap: this.getComponentsMap(extraComps),
componentsTree: [node],
@ -558,23 +569,30 @@ export class DocumentModel {
return this.rootNode;
}
onRendererReady(fn: (args: any) => void): () => void {
this.emitter.on('lowcode_engine_renderer_ready', fn);
return () => {
this.emitter.removeListener('lowcode_engine_renderer_ready', fn);
};
}
setRendererReady(renderer: any) {
this.emitter.emit('lowcode_engine_renderer_ready', renderer);
}
/**
* @deprecated
*/
getAddonData(name: string) {
const addon = this._addons[name];
return addon?.exportData();
const addon = this._addons.find((item) => item.name === name);
if (addon) {
return addon.exportData();
}
}
/**
* @deprecated
*/
exportAddonData() {
const addons = {};
this._addons.forEach((addon) => {
const data = addon.exportData();
if (data === null) {
delete addons[addon.name];
} else {
addons[addon.name] = data;
}
});
return addons;
}
/**
@ -584,17 +602,16 @@ export class DocumentModel {
if (['id', 'params', 'layout'].indexOf(name) > -1) {
throw new Error('addon name cannot be id, params, layout');
}
const i = this._addons?.findIndex((item) => item.name === name);
const i = this._addons.findIndex((item) => item.name === name);
if (i > -1) {
this._addons?.splice(i, 1);
this._addons.splice(i, 1);
}
this._addons?.push({
this._addons.push({
exportData,
name,
});
}
acceptRootNodeVisitor(
visitorName = 'default',
visitorFn: (node: RootNode) => any,
@ -675,6 +692,17 @@ export class DocumentModel {
onRefresh(/* func: () => void */) {
console.warn('onRefresh method is deprecated');
}
onReady(fn: Function) {
this.designer.editor.on('document-open', fn);
return () => {
this.designer.editor.removeListener('document-open', fn);
};
}
private setupListenActiveNodes() {
// todo:
}
}
export function isDocumentModel(obj: any): obj is DocumentModel {

View File

@ -9,7 +9,7 @@ export interface Serialization<T = any> {
unserialize(data: T): NodeSchema;
}
let currentSerializion: Serialization<any> = {
let currentSerialization: Serialization<any> = {
serialize(data: NodeSchema): string {
return JSON.stringify(data);
},
@ -18,8 +18,8 @@ let currentSerializion: Serialization<any> = {
},
};
export function setSerialization(serializion: Serialization) {
currentSerializion = serializion;
export function setSerialization(serialization: Serialization) {
currentSerialization = serialization;
}
export class History {
@ -46,7 +46,7 @@ export class History {
return;
}
untracked(() => {
const log = currentSerializion.serialize(data);
const log = currentSerialization.serialize(data);
if (this.session.cursor === 0 && this.session.isActive()) {
// first log
this.session.log(log);
@ -98,7 +98,7 @@ export class History {
this.obx.sleep();
try {
this.redoer(currentSerializion.unserialize(hotData));
this.redoer(currentSerialization.unserialize(hotData));
this.emitter.emit('cursor', hotData);
} catch (e) {
//
@ -222,7 +222,7 @@ class Session {
end() {
if (this.isActive()) {
this.clearTimer();
console.info('session end');
// console.info('session end');
}
}

View File

@ -1,4 +1,3 @@
export * from './document-view';
export * from './document-model';
export * from './node';
export * from './selection';

View File

@ -35,6 +35,10 @@ export class ExclusiveGroup {
return this.children[0]!;
}
get index() {
return this.firstNode.index;
}
add(node: Node) {
if (node.nextSibling && node.nextSibling.conditionGroup === this) {
const i = this.children.indexOf(node.nextSibling);

View File

@ -4,8 +4,7 @@ import { DocumentModel } from '../document-model';
function getModalNodes(node: Node) {
let nodes: any = [];
const prototype = node.getPrototype();
if (prototype && prototype.isModal()) {
if (node.componentMeta.isModal) {
nodes.push(node);
}
const children = node.getChildren();
@ -85,8 +84,7 @@ export class ModalNodesManager {
}
private addNode(node: Node) {
const prototype = node.getPrototype();
if (prototype && prototype.isModal()) {
if (node.componentMeta.isModal) {
this.hideModalNodes();
this.modalNodes.push(node);
this.addNodeEvent(node);
@ -96,8 +94,7 @@ export class ModalNodesManager {
}
private removeNode(node: Node) {
const prototype = node.getPrototype();
if (prototype && prototype.isModal()) {
if (node.componentMeta.isModal) {
const index = this.modalNodes.indexOf(node);
if (index >= 0) {
this.modalNodes.splice(index, 1);

View File

@ -183,6 +183,7 @@ export class NodeChildren {
}
this.emitter.emit('change');
this.emitter.emit('insert', node);
// this.reportModified(node, this.owner, { type: 'insert' });
// check condition group
@ -335,6 +336,13 @@ export class NodeChildren {
};
}
onInsert(fn: (node: Node) => void) {
this.emitter.on('insert', fn);
return () => {
this.emitter.removeListener('insert', fn);
};
}
get [Symbol.toStringTag]() {
// 保证向前兼容性
return 'Array';

View File

@ -158,24 +158,25 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
constructor(readonly document: DocumentModel, nodeSchema: Schema, options: any = {}) {
const { componentName, id, children, props, ...extras } = nodeSchema;
this.id = id || document.nextId();
this.id = document.nextId(id);
this.componentName = componentName;
if (this.componentName === 'Leaf') {
this.props = new Props(this, {
children: isDOMText(children) || isJSExpression(children) ? children : '',
});
this.settingEntry = this.document.designer.createSettingEntry([this]);
} else {
// 这里 props 被初始化两次,一次 new一次 importnew 的实例需要给 propsReducer 的钩子去使用,
// import 是为了使用钩子返回的值,并非完全幂等的操作,部分行为执行两次会有 bug
// 所以在 props 里会对 new / import 做一些区别化的解析
this.props = new Props(this, props, extras);
this._children = new NodeChildren(this as ParentalNode, this.initialChildren(children), options);
this.settingEntry = this.document.designer.createSettingEntry([this]);
this._children = new NodeChildren(this as ParentalNode, this.initialChildren(children));
this._children.internalInitParent();
this.props.import(this.upgradeProps(this.initProps(props || {})), this.upgradeProps(extras || {}));
this.setupAutoruns();
}
this.settingEntry = this.document.designer.createSettingEntry([this]);
this.emitter = new EventEmitter();
}
@ -264,6 +265,16 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
}
}
private didDropOut(dragment: Node) {
const callbacks = this.componentMeta.getMetadata().experimental?.callbacks;
if (callbacks?.onNodeRemove) {
callbacks?.onNodeRemove.call(this, dragment, this);
}
if (this._parent) {
this._parent.didDropOut(dragment);
}
}
/**
* 使
* @param useMutator
@ -281,9 +292,12 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
this._parent.children.unlinkChild(this);
}
}
// 建立新的父子关系
this._parent = parent;
if (useMutator) {
this._parent?.didDropOut(this);
}
if (parent) {
// 建立新的父子关系,尤其注意:对于 parent 为 null 的场景,不会赋值,因为 subtreeModified 等事件可能需要知道该 node 被删除前的父子关系
this._parent = parent;
this.document.removeWillPurge(this);
if (!this.conditionGroup) {
// initial conditionGroup
@ -609,6 +623,9 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
if (stage !== TransformStage.Clone) {
baseSchema.id = this.id;
}
if (stage === TransformStage.Render) {
baseSchema.docId = this.document.id;
}
if (this.isLeaf()) {
baseSchema.children = this.props.get('children')?.export(stage);
@ -893,7 +910,14 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
if (dropElement) {
return { container: dropElement, ref };
}
return { container: this, ref };
const rootCanDropIn = this.componentMeta?.prototype?.options?.canDropIn;
if (rootCanDropIn === undefined
|| rootCanDropIn === true
|| (typeof rootCanDropIn === 'function' && rootCanDropIn(node))) {
return { container: this, ref };
}
// 假如最后找不到合适位置,返回 undefined 阻止继续插入节点
return undefined;
}
const canDropIn = this.componentMeta?.prototype?.options?.canDropIn;

View File

@ -119,7 +119,7 @@ export class Prop implements IPropParent {
}
if (type === 'slot') {
const schema = this._slotNode!.export(stage);
const schema = this._slotNode?.export(stage) || {};
if (stage === TransformStage.Render) {
return {
type: 'JSSlot',
@ -233,7 +233,7 @@ export class Prop implements IPropParent {
} else if (Array.isArray(val)) {
this._type = 'list';
} else if (isPlainObject(val)) {
if (isJSSlot(val) && this.options.propsMode !== 'init') {
if (isJSSlot(val) && this.options.skipSetSlot !== true) {
this.setAsSlot(val);
return;
}

View File

@ -59,9 +59,9 @@ export class Props implements IPropParent {
constructor(readonly owner: Node, value?: PropsMap | PropsList | null, extras?: object) {
if (Array.isArray(value)) {
this.type = 'list';
this.items = value.map(item => new Prop(this, item.value, item.name, item.spread, { propsMode: 'init' }));
this.items = value.map(item => new Prop(this, item.value, item.name, item.spread, { skipSetSlot: true }));
} else if (value != null) {
this.items = Object.keys(value).map(key => new Prop(this, value[key], key, false, { propsMode: 'init' }));
this.items = Object.keys(value).map(key => new Prop(this, value[key], key, false, { skipSetSlot: true }));
}
if (extras) {
Object.keys(extras).forEach(key => {
@ -241,8 +241,8 @@ export class Props implements IPropParent {
/**
*
*/
add(value: CompositeValue | null, key?: string | number, spread = false): Prop {
const prop = new Prop(this, value, key, spread);
add(value: CompositeValue | null, key?: string | number, spread = false, options: any = {}): Prop {
const prop = new Prop(this, value, key, spread, options);
this.items.push(prop);
return prop;
}

View File

@ -1,23 +1,22 @@
import { Component } from 'react';
import { observer } from '@ali/lowcode-editor-core';
import { Designer } from '../designer';
import { DocumentView } from '../document';
import { intl } from '../locale';
import { BuiltinSimulatorHostView } from '../builtin-simulator';
import './project.less';
@observer
export class ProjectView extends Component<{ designer: Designer }> {
render() {
const { designer } = this.props;
// TODO: support splitview
const opens = designer.project.documents.filter((doc) => doc.opened);
const project = designer.project;
const simulatorProps = project.simulatorProps;
const Simulator = designer.simulatorComponent || BuiltinSimulatorHostView;
return (
<div className="lc-project">
{opens.length > 0 ? (
opens.map((doc) => <DocumentView key={doc.id} document={doc} />)
) : (
<div className="lc-project-empty">{intl('No opened document')}</div>
)}
<div className="lc-simulator-shell">
<Simulator {...simulatorProps} />
</div>
</div>
);
}

View File

@ -12,17 +12,9 @@
background: transparent url(//img.alicdn.com/tfs/TB1xLKQAbj1gK0jSZFuXXcrHpXa-90-90.png) center 30% no-repeat;
padding-top: 50%;
}
.lc-document {
.lc-simulator-shell {
width: 100%;
height: 100%;
&-hidden {
// todo:
display: none;
}
.lc-simulator-shell {
width: 100%;
height: 100%;
}
}
}

View File

@ -3,6 +3,7 @@ import { obx, computed } from '@ali/lowcode-editor-core';
import { Designer } from '../designer';
import { DocumentModel, isDocumentModel, isPageSchema } from '../document';
import { ProjectSchema, RootSchema } from '@ali/lowcode-types';
import { ISimulatorHost } from '../simulator';
export class Project {
private emitter = new EventEmitter();
@ -11,7 +12,14 @@ export class Project {
private data: ProjectSchema = { version: '1.0.0', componentsMap: [], componentsTree: [] };
@obx.ref canvasDisplayMode: 'exclusive' | 'overview' = 'exclusive';
private _simulator?: ISimulatorHost;
/**
*
*/
get simulator(): ISimulatorHost | null {
return this._simulator || null;
}
// TODO: 考虑项目级别 History
@ -23,6 +31,15 @@ export class Project {
return this.documents.find((doc) => doc.actived);
}
@obx private _config: any = {};
@computed get config(): any {
// TODO: parse layout Component
return this._config;
}
set config(value: any) {
this._config = value;
}
/**
* schema
*/
@ -40,7 +57,7 @@ export class Project {
*/
setSchema(schema?: ProjectSchema) {
const doc = this.documents.find((doc) => doc.actived);
doc && doc.import(schema?.componentsTree[0], false);
doc && doc.import(schema?.componentsTree[0]);
}
/**
@ -57,11 +74,19 @@ export class Project {
componentsTree: [],
...schema,
};
this.config = schema?.config;
if (autoOpen) {
if (autoOpen === true) {
// auto open first document or open a blank page
this.open(this.data.componentsTree[0]);
// this.open(this.data.componentsTree[0]);
const documentInstances = this.data.componentsTree.map((data) => this.createDocument(data));
// TODO: 暂时先读 config tabBar 里的值,后面看整个 layout 结构是否能作为引擎规范
if (this.config?.layout?.props?.tabBar?.items?.length > 0) {
documentInstances.find((i) => i.fileName === this.config.layout.props.tabBar.items[0].path?.slice(1))?.open();
} else {
documentInstances[0].open();
}
} else {
// auto open should be string of fileName
this.open(autoOpen);
@ -76,7 +101,9 @@ export class Project {
if (this.documents.length < 1) {
return;
}
this.documents.forEach((doc) => doc.remove());
for (let i = this.documents.length - 1; i >= 0; i--) {
this.documents[i].remove();
}
}
removeDocument(doc: DocumentModel) {
@ -85,6 +112,7 @@ export class Project {
return;
}
this.documents.splice(index, 1);
this.documentsMap.delete(doc.id);
}
/**
@ -105,6 +133,9 @@ export class Project {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
value: any,
): void {
if (key === 'config') {
this.config = value;
}
Object.assign(this.data, { [key]: value });
}
@ -114,27 +145,44 @@ export class Project {
get(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
key:
| 'version'
| 'componentsTree'
| 'componentsMap'
| 'utils'
| 'constants'
| 'i18n'
| 'css'
| 'dataSource'
| string,
| 'version'
| 'componentsTree'
| 'componentsMap'
| 'utils'
| 'constants'
| 'i18n'
| 'css'
| 'dataSource'
| 'config'
| string,
): any {
if (key === 'config') {
return this.config;
}
return Reflect.get(this.data, key);
}
open(doc?: string | DocumentModel | RootSchema): DocumentModel {
private documentsMap = new Map<string, DocumentModel>();
getDocument(id: string): DocumentModel | null {
// 此处不能使用 this.documentsMap.get(id),因为在乐高 rollback 场景document.id 会被改成其他值
return this.documents.find(doc => doc.id === id) || null;
}
createDocument(data?: RootSchema): DocumentModel {
const doc = new DocumentModel(this, data || this?.data?.componentsTree?.[0]);
this.documents.push(doc);
this.documentsMap.set(doc.id, doc);
return doc;
}
open(doc?: string | DocumentModel | RootSchema): DocumentModel | null {
if (!doc) {
const got = this.documents.find((item) => item.isBlank());
if (got) {
return got.open();
}
doc = new DocumentModel(this);
this.documents.push(doc);
doc = this.createDocument();
return doc.open();
}
if (typeof doc === 'string') {
@ -145,27 +193,24 @@ export class Project {
const data = this.data.componentsTree.find((data) => data.fileName === doc);
if (data) {
doc = new DocumentModel(this, data);
this.documents.push(doc);
doc = this.createDocument(data);
return doc.open();
}
return;
return null;
}
if (isDocumentModel(doc)) {
return doc.open();
} else if (isPageSchema(doc)) {
const foundDoc = this.documents.find(
(curDoc) => curDoc?.rootNode?.id && curDoc?.rootNode?.id === doc?.id,
);
if (foundDoc) {
foundDoc.remove();
}
// 暂时注释掉,影响了 diff 功能
// const foundDoc = this.documents.find(curDoc => curDoc?.rootNode?.id && curDoc?.rootNode?.id === doc?.id);
// if (foundDoc) {
// foundDoc.remove();
// }
}
doc = new DocumentModel(this, doc);
this.documents.push(doc);
doc = this.createDocument(doc);
return doc.open();
}
@ -186,13 +231,42 @@ export class Project {
});
}
/**
*
*/
@computed get simulatorProps(): object {
let simulatorProps = this.designer.simulatorProps;
if (typeof simulatorProps === 'function') {
simulatorProps = simulatorProps(this);
}
return {
...simulatorProps,
project: this,
onMount: this.mountSimulator.bind(this),
};
}
private mountSimulator(simulator: ISimulatorHost) {
// TODO: 多设备 simulator 支持
this._simulator = simulator;
this.designer.editor.set('simulator', simulator);
}
setRendererReady(renderer: any) {
this.emitter.emit('lowcode_engine_renderer_ready', renderer);
}
onRendererReady(fn: (args: any) => void): () => void {
this.emitter.on('lowcode_engine_renderer_ready', fn);
return () => {
this.emitter.removeListener('lowcode_engine_renderer_ready', fn);
};
}
onCurrentDocumentChange(fn: (doc: DocumentModel) => void): () => void {
this.emitter.on('current-document.change', fn);
return () => {
this.emitter.removeListener('current-document.change', fn);
};
}
// 通知标记删除,需要告知服务端
// 项目角度编辑不是全量打开所有文档,是按需加载,哪个更新就通知更新谁,
// 哪个删除就
}

View File

@ -1,7 +1,7 @@
import { Component as ReactComponent, ComponentType } from 'react';
import { ComponentMetadata, NodeSchema } from '@ali/lowcode-types';
import { ISensor, Point, ScrollTarget, IScrollable } from './designer';
import { Node } from './document';
import { ISensor, Point, ScrollTarget, IScrollable, LocateEvent, LocationData } from './designer';
import { Node, ParentalNode } from './document';
export type AutoFit = '100%';
export const AutoFit = '100%';
@ -60,6 +60,11 @@ export interface IViewport extends IScrollable {
toGlobalPoint(point: Point): Point;
}
export interface DropContainer {
container: ParentalNode;
instance: ComponentInstance;
}
/**
*
*/
@ -143,6 +148,8 @@ export interface ISimulatorHost<P = object> extends ISensor {
findDOMNodes(instance: ComponentInstance, selector?: string): Array<Element | Text> | null;
getDropContainer(e: LocateEvent): DropContainer | null;
/**
*
*/
@ -154,6 +161,7 @@ export function isSimulatorHost(obj: any): obj is ISimulatorHost {
}
export interface NodeInstance<T = ComponentInstance> {
docId: string;
nodeId: string;
instance: T;
node?: Node | null;

View File

@ -0,0 +1,10 @@
export class DocumentModel {
a = 1;
c = {};
constructor() {
console.log('xxxxxxxxxxxxxxxxxxxx');
const b = { x: { y: 2 } };
const c: number = 2;
this.a = b?.x?.y;
}
}

View File

@ -0,0 +1,9 @@
export class Node2 {
a = 1;
c = {};
constructor() {
const b = { x: { y: 2 } };
const c: number = 2;
this.a = b?.x?.y;
}
}

View File

@ -0,0 +1,55 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';
import '../fixtures/window';
import { Project } from '../../src/project/project';
// import { Node } from '../../../src/document/node/node';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
return {
getComponentMeta() {
return {
getMetadata() {
return { experimental: null };
},
};
},
transformProps(props) { return props; },
createSettingEntry: mockCreateSettingEntry,
postEvent() {},
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({});
});
it.todo('在同一个节点下,相同名称的 slot 只能有一个', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
expect(nodesMap.size).toBe(expectedNodeCnt);
ids.forEach(id => {
expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName);
});
const exportSchema = currentDocument?.export(1);
expect(getIdsFromSchema(exportSchema).length).toBe(expectedNodeCnt);
expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt);
});

View File

@ -0,0 +1,32 @@
import React from 'react';
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
import { Editor } from '@ali/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import TestRenderer from 'react-test-renderer';
import { configure, render, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
import { BuiltinSimulatorHostView } from '../../src/builtin-simulator/host-view';
configure({ adapter: new Adapter() });
const editor = new Editor();
describe('host-view 测试', () => {
let designer: Designer;
beforeEach(() => {
designer = new Designer({ editor });
});
afterEach(() => {
designer._componentMetasMap.clear();
designer = null;
});
it('host-view', () => {
const hostView = render(<BuiltinSimulatorHostView project={designer.project} />);
})
});

View File

@ -0,0 +1,145 @@
import React from 'react';
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
import { Editor } from '@ali/lowcode-editor-core';
import {
AssetLevel,
Asset,
AssetList,
assetBundle,
assetItem,
AssetType,
} from '@ali/lowcode-utils';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getMockDocument, getMockWindow, getMockEvent } from '../utils';
import { BuiltinSimulatorHost } from '../../src/builtin-simulator/host';
import { eq } from 'lodash';
const editor = new Editor();
describe('host 测试', () => {
let designer: Designer;
beforeEach(() => {
designer = new Designer({ editor });
});
afterEach(() => {
designer._componentMetasMap.clear();
designer = null;
});
it('基础方法测试', async () => {
const host = new BuiltinSimulatorHost(designer.project);
expect(host.currentDocument).toBe(designer.project.currentDocument);
expect(host.renderEnv).toBe('default');
expect(host.device).toBe('default');
expect(host.deviceClassName).toBeUndefined;
host.setProps({
renderEnv: 'rax',
device: 'mobile',
deviceClassName: 'mobile-rocks',
componentsAsset: [{
type: AssetType.JSText,
content: 'console.log(1)',
}, {
type: AssetType.JSUrl,
content: '//path/to/js',
}],
theme: {
type: AssetType.CSSText,
content: '.theme {font-size: 50px;}',
}
});
expect(host.renderEnv).toBe('rax');
expect(host.device).toBe('mobile');
expect(host.deviceClassName).toBe('mobile-rocks');
expect(host.componentsAsset).toEqual([{
type: AssetType.JSText,
content: 'console.log(1)',
}, {
type: AssetType.JSUrl,
content: '//path/to/js',
}]);
expect(host.theme).toEqual({
type: AssetType.CSSText,
content: '.theme {font-size: 50px;}',
});
expect(host.componentsMap).toBe(designer.componentsMap);
host.set('renderEnv', 'vue');
expect(host.renderEnv).toBe('vue');
expect(host.getComponentContext).toThrow('Method not implemented.');
});
it('事件测试', async () => {
const host = new BuiltinSimulatorHost(designer.project);
const mockDocument = getMockDocument();
const mockWindow = getMockWindow(mockDocument);
const mockIframe = {
contentWindow: mockWindow,
contentDocument: mockDocument,
dispatchEvent() {},
};
// 非法分支测试
host.mountContentFrame();
expect(host._iframe).toBeUndefined();
host.set('library', [{
package: '@ali/vc-deep',
library: 'lib',
urls: ['a.js', 'b.js']
}]);
host.componentsConsumer.consume(() => {});
host.injectionConsumer.consume(() => {});
await host.mountContentFrame(mockIframe);
expect(host.contentWindow).toBe(mockWindow);
mockDocument.triggerEventListener(
'mouseover',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'mouseleave',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'mousedown',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'mouseup',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'mousemove',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'click',
getMockEvent(document.createElement('input')),
host,
);
mockDocument.triggerEventListener(
'dblclick',
getMockEvent(mockDocument.createElement('div')),
host,
);
mockDocument.triggerEventListener(
'contextmenu',
getMockEvent(mockDocument.createElement('div')),
host,
);
})
});

View File

@ -0,0 +1,9 @@
import '../fixtures/window';
import { parseMetadata } from '../../src/builtin-simulator/utils/parse-metadata';
describe('parseMetadata', () => {
it('parseMetadata', async () => {
const md1 = parseMetadata('Div');
const md2 = parseMetadata({ componentName: 'Div' });
});
});

View File

@ -0,0 +1,78 @@
import {
generateComponentName,
getNormalizedImportPath,
isPackagePath,
toTitleCase,
makeRelativePath,
removeVersion,
resolveAbsoluatePath,
joinPath,
} from '../../src/builtin-simulator/utils/path';
describe('builtin-simulator/utils/path 测试', () => {
it('isPackagePath', () => {
expect(isPackagePath('a')).toBeTruthy;
expect(isPackagePath('@ali/a')).toBeTruthy;
expect(isPackagePath('@alife/a')).toBeTruthy;
expect(isPackagePath('a.b')).toBeTruthy;
expect(isPackagePath('./a')).toBeFalsy;
expect(isPackagePath('../a')).toBeFalsy;
expect(isPackagePath('/a')).toBeFalsy;
});
it('toTitleCase', () => {
expect(toTitleCase('a')).toBe('A');
expect(toTitleCase('a_b')).toBe('AB');
expect(toTitleCase('a b')).toBe('AB');
expect(toTitleCase('a-b')).toBe('AB');
expect(toTitleCase('a.b')).toBe('AB');
expect(toTitleCase('a.b.cx')).toBe('ABCx');
});
it('generateComponentName', () => {
expect(generateComponentName('a/index.js')).toBe('A');
expect(generateComponentName('a_b/index.js')).toBe('AB');
expect(generateComponentName('a_b/index.web.js')).toBe('AB');
expect(generateComponentName('a_b/index.xxx.js')).toBe('AB');
expect(generateComponentName('a_b')).toBe('AB');
expect(generateComponentName('')).toBe('Component');
});
it('getNormalizedImportPath', () => {
expect(getNormalizedImportPath('/a')).toBe('/a');
expect(getNormalizedImportPath('/a/')).toBe('/a/');
expect(getNormalizedImportPath('/a/index.js')).toBe('/a');
expect(getNormalizedImportPath('/a/index.ts')).toBe('/a');
expect(getNormalizedImportPath('/a/index.jsx')).toBe('/a');
expect(getNormalizedImportPath('/a/index.tsx')).toBe('/a');
expect(getNormalizedImportPath('/a/index.x')).toBe('/a/index.x');
});
it('makeRelativePath', () => {
expect(makeRelativePath('/a/b/c', '/a/b')).toBe('c');
expect(makeRelativePath('a/b/c', '/a/c')).toBe('a/b/c');
expect(makeRelativePath('/a/b/c', '/a/c')).toBe('./b/c');
expect(makeRelativePath('/a/b/c', '/a/c/d')).toBe('../b/c');
});
it('resolveAbsoluatePath', () => {
expect(resolveAbsoluatePath('/a/b/c', '/a')).toBe('/a/b/c');
expect(resolveAbsoluatePath('@ali/fe', '/a')).toBe('@ali/fe');
expect(resolveAbsoluatePath('./a/b', '/c')).toBe('/c/a/b');
expect(resolveAbsoluatePath('./a/b/d', '/c')).toBe('/c/a/b/d');
expect(resolveAbsoluatePath('../a/b', '/c')).toBe('/a/b');
expect(resolveAbsoluatePath('../a/b/d', '/c')).toBe('/a/b/d');
expect(resolveAbsoluatePath('../../a', 'c')).toBe('../a');
});
it('joinPath', () => {
expect(joinPath('/a', 'b', 'c')).toBe('/a/b/c');
expect(joinPath('a', 'b', 'c')).toBe('./a/b/c');
});
it('removeVersion', () => {
expect(removeVersion('@ali/fe')).toBe('@ali/fe');
expect(removeVersion('@ali/fe@1.0.0/index')).toBe('@ali/fe/index');
expect(removeVersion('haha')).toBe('haha');
});
});

View File

@ -0,0 +1,21 @@
import React from 'react';
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
import { Editor } from '@ali/lowcode-editor-core';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import TestRenderer from 'react-test-renderer';
import { configure, render, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getMockRenderer } from '../utils';
import { isSimulatorRenderer } from '../../src/builtin-simulator/renderer';
describe('renderer 测试', () => {
it('renderer', () => {
expect(isSimulatorRenderer(getMockRenderer())).toBeTruthy;
})
});

View File

@ -0,0 +1,22 @@
import '../fixtures/disable-raf';
import { throttle } from '../../src/builtin-simulator/utils/throttle';
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const cb = jest.fn();
describe('throttle', () => {
it('simple', async () => {
const fn = throttle(cb, 1000);
fn();
expect(cb).toBeCalledTimes(1);
await delay(200);
fn();
await delay(400);
fn();
expect(cb).toBeCalledTimes(1);
});
});

View File

@ -0,0 +1,202 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
import { Editor, globalContext } from '@ali/lowcode-editor-core';
import { Designer } from '../../src/designer/designer';
import { Project } from '../../src/project/project';
import formSchema from '../fixtures/schema/form';
import '../../src/designer/builtin-hotkey';
const editor = new Editor();
let designer: Designer;
beforeAll(() => {
globalContext.register(editor, Editor);
});
beforeEach(() => {
designer = new Designer({ editor });
designer.project.open(formSchema);
});
afterEach(() => {
designer = null;
});
// keyCode 对应表https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
// hotkey 模块底层用的 keyCode所以还不能用 key / code 测试
describe('快捷键测试', () => {
it('right', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbj')!;
firstCardNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 39 });
document.dispatchEvent(event);
expect(designer.currentSelection?.selected.includes('node_k1ow3cbl')).toBeTruthy;
});
it('left', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbl')!;
firstCardNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 37 });
document.dispatchEvent(event);
expect(designer.currentSelection?.selected.includes('node_k1ow3cbj')).toBeTruthy;
});
it('down', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbl')!;
firstCardNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 40 });
document.dispatchEvent(event);
expect(designer.currentSelection?.selected.includes('node_k1ow3cbm')).toBeTruthy;
});
it('up', () => {
const secondCardNode = designer.currentDocument?.getNode('node_k1ow3cbm')!;
secondCardNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 38 });
document.dispatchEvent(event);
expect(designer.currentSelection?.selected.includes('node_k1ow3cbl')).toBeTruthy;
});
// 跟右侧节点调换位置
it('option + right', () => {
const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!;
firstButtonNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 39, altKey: true });
document.dispatchEvent(event);
expect(firstButtonNode.prevSibling?.getId()).toBe('node_k1ow3cbp');
});
// 跟左侧节点调换位置
it('option + left', () => {
const secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
secondButtonNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 37, altKey: true });
document.dispatchEvent(event);
expect(secondButtonNode.nextSibling?.getId()).toBe('node_k1ow3cbn');
});
// 向父级移动该节点
it('option + up', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstCardNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 38, altKey: true });
document.dispatchEvent(event);
});
// 将节点移入到兄弟节点中
it('option + up', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstCardNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 40, altKey: true });
document.dispatchEvent(event);
});
// 撤销
it('command + z', async () => {
const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!;
let secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstButtonNode.remove();
expect(secondButtonNode.getParent()?.children.size).toBe(1);
await new Promise(resolve => setTimeout(resolve, 1000));
let event = new KeyboardEvent('keydown', { keyCode: 90, metaKey: true });
document.dispatchEvent(event);
// 重新获取一次节点,因为 documentModel.import 是全画布刷新
secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
expect(secondButtonNode.getParent()?.children.size).toBe(2);
});
// 重做
it('command + y', async () => {
const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!;
let secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstButtonNode.remove();
expect(secondButtonNode.getParent()?.children.size).toBe(1);
await new Promise(resolve => setTimeout(resolve, 1000));
let event = new KeyboardEvent('keydown', { keyCode: 90, metaKey: true });
document.dispatchEvent(event);
// 重新获取一次节点,因为 documentModel.import 是全画布刷新
secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
expect(secondButtonNode.getParent()?.children.size).toBe(2);
await new Promise(resolve => setTimeout(resolve, 1000));
event = new KeyboardEvent('keydown', { keyCode: 89, metaKey: true });
document.dispatchEvent(event);
// 重新获取一次节点,因为 documentModel.import 是全画布刷新
secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
expect(secondButtonNode.getParent()?.children.size).toBe(1);
});
it('command + c', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstCardNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 67, metaKey: true });
document.dispatchEvent(event);
});
it('command + v', async () => {
const secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
secondButtonNode.select();
let event = new KeyboardEvent('keydown', { keyCode: 67, metaKey: true });
document.dispatchEvent(event);
event = new KeyboardEvent('keydown', { keyCode: 86, metaKey: true });
document.dispatchEvent(event);
await new Promise(resolve => setTimeout(resolve, 1000));
// clipboard 异步,先注释
// expect(secondButtonNode.getParent()?.children.size).toBe(3);
});
// 撤销所有选中
it('escape', () => {
const firstCardNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstCardNode.select();
expect(designer.currentSelection!.selected.includes('node_k1ow3cbp')).toBeTruthy;
let event = new KeyboardEvent('keydown', { keyCode: 27 });
document.dispatchEvent(event);
expect(designer.currentSelection!.selected.length).toBe(0);
});
// 删除节点
it('delete', () => {
const firstButtonNode = designer.currentDocument?.getNode('node_k1ow3cbn')!;
const secondButtonNode = designer.currentDocument?.getNode('node_k1ow3cbp')!;
firstButtonNode.select();
expect(secondButtonNode.prevSibling.id).toBe('node_k1ow3cbn');
let event = new KeyboardEvent('keydown', { keyCode: 46 });
document.dispatchEvent(event);
expect(secondButtonNode.prevSibling).toBeNull;
});
});

View File

@ -0,0 +1,105 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../../fixtures/window';
import { Editor } from '@ali/lowcode-editor-core';
import { Project } from '../../../src/project/project';
import { Node } from '../../../src/document/node/node';
import { Designer } from '../../../src/designer/designer';
import formSchema from '../../../fixtures/schema/form';
import settingSchema from '../../fixtures/schema/setting';
import divMeta from '../../fixtures/prototype/div-meta';
import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils';
const editor = new Editor();
describe('setting-prop-entry 测试', () => {
let designer: Designer;
beforeEach(() => {
designer = new Designer({ editor });
});
afterEach(() => {
designer._componentMetasMap.clear();
designer = null;
});
describe('node 构造函数生成 settingEntry', () => {
it('常规方法测试', () => {
designer.createComponentMeta(divMeta);
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div');
const { settingEntry } = divNode!;
const behaviorProp = settingEntry.getProp('behavior');
expect(behaviorProp.getProps()).toBe(settingEntry);
expect(behaviorProp.props).toBe(settingEntry);
expect(behaviorProp.getName()).toBe('behavior');
expect(behaviorProp.getKey()).toBe('behavior');
expect(behaviorProp.isIgnore()).toBeFalsy;
behaviorProp.setKey('behavior2');
expect(behaviorProp.getKey()).toBe('behavior2');
behaviorProp.setKey('behavior');
expect(behaviorProp.getValue()).toBe('NORMAL');
expect(behaviorProp.getMockOrValue()).toBe('NORMAL');
behaviorProp.setValue('LARGE');
expect(behaviorProp.getValue()).toBe('LARGE');
// behaviorProp.setPropValue('behavior', 'SMALL');
// expect(behaviorProp.getValue()).toBe('SMALL');
behaviorProp.setValue('NORMAL');
expect(behaviorProp.getValue()).toBe('NORMAL');
behaviorProp.clearValue();
behaviorProp.clearPropValue();
expect(settingEntry.getProp('behavior').getValue()).toBeUndefined;
behaviorProp.setValue('LARGE');
expect(behaviorProp.getValue()).toBe('LARGE');
behaviorProp.remove();
expect(settingEntry.getProp('behavior').getValue()).toBeUndefined;
expect(behaviorProp.getNode()).toBe(divNode);
expect(behaviorProp.getId().startsWith('entry')).toBeTruthy;
expect(behaviorProp.designer).toBe(designer);
expect(behaviorProp.isSingle).toBeTruthy;
expect(behaviorProp.isMultiple).toBeFalsy;
expect(behaviorProp.isGroup).toBeFalsy;
expect(behaviorProp.isSameComponent).toBeTruthy;
expect(typeof settingEntry.getValue).toBe('function');
settingEntry.getValue();
behaviorProp.setExtraPropValue('extraPropA', 'heihei');
expect(behaviorProp.getExtraPropValue('extraPropA', 'heihei'));
});
it.skip('type: group 场景测试', () => {
});
it('JSExpression 类型的 prop', () => {
designer.createComponentMeta(divMeta);
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div');
const { settingEntry } = divNode!;
const customClassNameProp = settingEntry.getProp('customClassName');
expect(customClassNameProp.isUseVariable()).toBeTruthy;
expect(customClassNameProp.useVariable).toBeTruthy;
expect(customClassNameProp.getValue()).toEqual({
type: 'JSExpression',
value: 'getFromSomewhere()'
});
expect(customClassNameProp.getMockOrValue()).toBeUndefined;
expect(customClassNameProp.getVariableValue()).toBe('getFromSomewhere()');
customClassNameProp.setVariableValue('xxx');
expect(customClassNameProp.getVariableValue()).toBe('xxx');
const customClassName2Prop = settingEntry.getProp('customClassName2');
expect(customClassName2Prop.getMockOrValue()).toEqual({
hi: 'mock',
});
});
});
});

View File

@ -0,0 +1,174 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../../fixtures/window';
import { Editor } from '@ali/lowcode-editor-core';
import { Project } from '../../../src/project/project';
import { Node } from '../../../src/document/node/node';
import { Designer } from '../../../src/designer/designer';
import formSchema from '../../fixtures/schema/form';
import settingSchema from '../../fixtures/schema/setting';
import divMeta from '../../fixtures/prototype/div-meta';
import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils';
const editor = new Editor();
describe('setting-top-entry 测试', () => {
let designer: Designer;
beforeEach(() => {
designer = new Designer({ editor });
});
afterEach(() => {
designer._componentMetasMap.clear();
designer = null;
});
describe('node 构造函数生成 settingEntry', () => {
it('常规方法测试', () => {
designer.createComponentMeta(divMeta);
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div');
const { settingEntry } = divNode!;
expect(settingEntry.getPropValue('behavior')).toBe('NORMAL');
expect(settingEntry.getProp('behavior').getValue()).toBe('NORMAL');
settingEntry.setPropValue('behavior', 'LARGE');
expect(settingEntry.getPropValue('behavior')).toBe('LARGE');
expect(settingEntry.get('behavior').getValue()).toBe('LARGE');
settingEntry.getProp('behavior').setValue('SMALL');
expect(settingEntry.getPropValue('behavior')).toBe('SMALL');
settingEntry.clearPropValue('behavior');
expect(settingEntry.getPropValue('behavior')).toBeUndefined;
expect(settingEntry.getPropValue('fieldId')).toBe('div_k1ow3h1o');
settingEntry.setPropValue('fieldId', 'div_k1ow3h1o_new');
expect(settingEntry.getPropValue('fieldId')).toBe('div_k1ow3h1o_new');
expect(settingEntry.getExtraPropValue('extraPropA')).toBe('haha');
settingEntry.setExtraPropValue('extraPropA', 'haha2');
expect(settingEntry.getExtraPropValue('extraPropA')).toBe('haha2');
settingEntry.mergeProps({
newPropA: 'haha',
});
expect(settingEntry.getPropValue('newPropA')).toBe('haha');
settingEntry.setProps({
newPropB: 'haha',
});
expect(settingEntry.getPropValue('newPropB')).toBe('haha');
settingEntry.setValue({
newPropC: 'haha',
});
expect(settingEntry.getPropValue('newPropC')).toBe('haha');
expect(settingEntry.getPage()).toBe(currentDocument);
expect(settingEntry.getNode()).toBe(divNode);
expect(settingEntry.node).toBe(divNode);
expect(settingEntry.getId()).toBe('div');
expect(settingEntry.first).toBe(divNode);
expect(settingEntry.designer).toBe(designer);
expect(settingEntry.isSingle).toBeTruthy;
expect(settingEntry.isMultiple).toBeFalsy;
expect(settingEntry.isSameComponent).toBeTruthy;
expect(typeof settingEntry.getValue).toBe('function');
settingEntry.getValue();
});
it('清理方法测试', () => {
designer.createComponentMeta(divMeta);
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div');
const { settingEntry } = divNode!;
expect(settingEntry.items).toHaveLength(3);
settingEntry.purge();
expect(settingEntry.items).toHaveLength(0);
});
it('vision 兼容测试', () => {
designer.createComponentMeta(divMeta);
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div');
console.log(divNode?.getPropValue('behavior'));
const { settingEntry } = divNode!;
expect(typeof settingEntry.getChildren).toBe('function');
expect(typeof settingEntry.getDOMNode).toBe('function');
expect(typeof settingEntry.getStatus).toBe('function');
expect(typeof settingEntry.setStatus).toBe('function');
settingEntry.getStatus();
settingEntry.setStatus();
settingEntry.getChildren();
settingEntry.getDOMNode();
});
it('没有 node', () => {
const create1 = designer.createSettingEntry.bind(designer);
const create2 = designer.createSettingEntry.bind(designer, []);
expect(create1).toThrowError('nodes should not be empty');
expect(create2).toThrowError('nodes should not be empty');
});
});
describe('designer.createSettingEntry 生成 settingEntry多 node 场景)', () => {
it('相同类型的 node', () => {
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div');
const divNode2 = currentDocument?.getNode('div2');
const settingEntry = designer.createSettingEntry([divNode, divNode2]);
expect(settingEntry.isMultiple).toBeTruthy;
expect(settingEntry.isSameComponent).toBeTruthy;
expect(settingEntry.isSingle).toBeFalsy;
expect(settingEntry.getPropValue('behavior')).toBe('NORMAL');
expect(settingEntry.getProp('behavior').getValue()).toBe('NORMAL');
settingEntry.setPropValue('behavior', 'LARGE');
expect(settingEntry.getPropValue('behavior')).toBe('LARGE');
expect(settingEntry.get('behavior').getValue()).toBe('LARGE');
// 多个 node 都被成功设值
expect(divNode?.getPropValue('behavior')).toBe('LARGE');
expect(divNode2?.getPropValue('behavior')).toBe('LARGE');
settingEntry.getProp('behavior').setValue('SMALL');
expect(settingEntry.getPropValue('behavior')).toBe('SMALL');
// 多个 node 都被成功设值
expect(divNode?.getPropValue('behavior')).toBe('SMALL');
expect(divNode2?.getPropValue('behavior')).toBe('SMALL');
settingEntry.clearPropValue('behavior');
expect(settingEntry.getPropValue('behavior')).toBeUndefined;
// 多个 node 都被成功设值
expect(divNode?.getPropValue('behavior')).toBeUndefined;
expect(divNode2?.getPropValue('behavior')).toBeUndefined;
expect(settingEntry.getPropValue('fieldId')).toBe('div_k1ow3h1o');
settingEntry.setPropValue('fieldId', 'div_k1ow3h1o_new');
expect(settingEntry.getPropValue('fieldId')).toBe('div_k1ow3h1o_new');
expect(settingEntry.getExtraPropValue('extraPropA')).toBe('haha');
settingEntry.setExtraPropValue('extraPropA', 'haha2');
expect(settingEntry.getExtraPropValue('extraPropA')).toBe('haha2');
});
it('不同类型的 node', () => {
designer.project.open(settingSchema);
const { currentDocument } = designer.project;
const divNode = currentDocument?.getNode('div');
const testNode = currentDocument?.getNode('test');
const settingEntry = designer.createSettingEntry([divNode, testNode]);
expect(settingEntry.isMultiple).toBeTruthy;
expect(settingEntry.isSameComponent).toBeFalsy;
expect(settingEntry.isSingle).toBeFalsy;
// 不同类型的 node 场景下,理论上从页面上已没有修改属性的方法调用,所以此处不再断言各设值方法
// 思考:假如以后面向其他场景,比如用户用 API 强行调用,是否需要做健壮性保护?
});
});
});

View File

@ -0,0 +1,16 @@
import '../../fixtures/window';
window.matchMedia('width=600px');
import { DocumentModel } from '../../../src/document/document-model';
// const { DocumentModel } = require('../../../src/document/document-model');
// const { Node } = require('../__mocks__/node');
describe.skip('basic utility', () => {
test('delegateMethod - useOriginMethodName', () => {
const node = new DocumentModel({}, {
componentName: 'Component',
});
console.log(node);
expect(1).toBe(1);
});
});

View File

@ -0,0 +1,28 @@
import '../../fixtures/window';
import { DocumentModel } from '../../../src/document/document-model';
import { Node } from '../../../src/document/node/node';
// import { Node2 } from './__mocks__/node';
jest.mock('../../../src/document/document-model', () => {
return {
DocumentModel: jest.fn().mockImplementation(() => {
return {
project: {
designer: { createSettingEntry() {}, transformProps() {} },
getSchema() {},
},
nextId() {},
};
}),
};
});
describe.skip('basic utility', () => {
test('delegateMethod - useOriginMethodName', () => {
const dm = new DocumentModel({} as any, {} as any);
console.log(dm.nextId);
const node = new Node(dm, { componentName: 'Leaf' });
console.log(node);
expect(1).toBe(1);
});
});

View File

@ -0,0 +1,245 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';
import '../fixtures/window';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
return {
getComponentMeta() {
return {
getMetadata() {
return { experimental: null };
},
};
},
transformProps(props) { return props; },
createSettingEntry: mockCreateSettingEntry,
postEvent() {},
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({});
});
describe('选择区测试', () => {
it('常规方法', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
const selectionChangeHandler = jest.fn();
selection.onSelectionChange(selectionChangeHandler);
selection.select('form');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selection.selected).toEqual(['form']);
selectionChangeHandler.mockClear();
selection.select('form');
expect(selectionChangeHandler).toHaveBeenCalledTimes(0);
expect(selection.selected).toEqual(['form']);
selection.select('node_k1ow3cbj');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['node_k1ow3cbj']);
expect(selection.selected).toEqual(['node_k1ow3cbj']);
selectionChangeHandler.mockClear();
selection.selectAll(['node_k1ow3cbj', 'form']);
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['node_k1ow3cbj', 'form']);
expect(selection.selected).toEqual(['node_k1ow3cbj', 'form']);
selectionChangeHandler.mockClear();
selection.remove('node_k1ow3cbj');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form']);
expect(selection.selected).toEqual(['form']);
selectionChangeHandler.mockClear();
selection.clear();
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual([]);
expect(selection.selected).toEqual([]);
selectionChangeHandler.mockClear();
// 无选中时调用 clear不再触发事件
selection.clear();
expect(selectionChangeHandler).toHaveBeenCalledTimes(0);
expect(selection.selected).toEqual([]);
selectionChangeHandler.mockClear();
});
it('add 方法', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
const selectionChangeHandler = jest.fn();
selection.onSelectionChange(selectionChangeHandler);
selection.add('form');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form']);
expect(selection.selected).toEqual(['form']);
selectionChangeHandler.mockClear();
// 再加一次相同的节点,不触发事件
selection.add('form');
expect(selectionChangeHandler).toHaveBeenCalledTimes(0);
expect(selection.selected).toEqual(['form']);
selectionChangeHandler.mockClear();
selection.add('form2');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form', 'form2']);
expect(selection.selected).toEqual(['form', 'form2']);
selectionChangeHandler.mockClear();
});
it('dispose 方法', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
selection.selectAll(['form', 'node_k1ow3cbj', 'form2']);
const selectionChangeHandler = jest.fn();
selection.onSelectionChange(selectionChangeHandler);
selection.dispose();
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form', 'node_k1ow3cbj']);
expect(selection.selected).toEqual(['form', 'node_k1ow3cbj']);
selectionChangeHandler.mockClear();
});
it('dispose 方法', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
selection.selectAll(['form', 'node_k1ow3cbj', 'form2']);
const selectionChangeHandler = jest.fn();
selection.onSelectionChange(selectionChangeHandler);
selection.dispose();
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form', 'node_k1ow3cbj']);
expect(selection.selected).toEqual(['form', 'node_k1ow3cbj']);
selectionChangeHandler.mockClear();
});
it('containsNode 方法', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
const selectionChangeHandler = jest.fn();
selection.onSelectionChange(selectionChangeHandler);
selection.select('form');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form']);
expect(selection.selected).toEqual(['form']);
expect(selection.has('form')).toBe(true);
expect(selection.containsNode(currentDocument?.getNode('form'))).toBe(true);
expect(selection.containsNode(currentDocument?.getNode('node_k1ow3cbj'))).toBe(true);
expect(selection.containsNode(currentDocument?.getNode('node_k1ow3cb9'))).toBe(false);
expect(selection.getNodes()).toEqual([currentDocument?.getNode('form')]);
selectionChangeHandler.mockClear();
selection.add('node_k1ow3cbj');
expect(selection.selected).toEqual(['form', 'node_k1ow3cbj']);
expect(selection.getTopNodes()).toEqual([currentDocument?.getNode('form')]);
expect(selection.getTopNodes(true)).toEqual([currentDocument?.getNode('form')]);
});
it('containsNode 方法 - excludeRoot: true', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
const selectionChangeHandler = jest.fn();
selection.onSelectionChange(selectionChangeHandler);
selection.select('node_k1ow3cb9');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['node_k1ow3cb9']);
expect(selection.selected).toEqual(['node_k1ow3cb9']);
expect(selection.has('node_k1ow3cb9')).toBe(true);
expect(selection.containsNode(currentDocument?.getNode('form'))).toBe(true);
expect(selection.containsNode(currentDocument?.getNode('form'), true)).toBe(false);
selectionChangeHandler.mockClear();
});
it('containsNode 方法 - excludeRoot: true', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap, selection } = currentDocument!;
const selectionChangeHandler = jest.fn();
const dispose = selection.onSelectionChange(selectionChangeHandler);
selection.select('form');
expect(selectionChangeHandler).toHaveBeenCalledTimes(1);
expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form']);
selectionChangeHandler.mockClear();
// dispose 后selected 会被赋值,但是变更事件不会被触发
dispose();
selection.select('node_k1ow3cb9');
expect(selectionChangeHandler).toHaveBeenCalledTimes(0);
expect(selection.selected).toEqual(['node_k1ow3cb9']);
selectionChangeHandler.mockClear();
});
});

View File

@ -0,0 +1,275 @@
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: 'groupkgzzeo41',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
nestingRule: {
parentWhitelist: 'Div',
childWhitelist: 'Div',
},
},
supports: {},
},
experimental: {
callbacks: {},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
autoruns: [],
},
};

View File

@ -0,0 +1,3 @@
Object.defineProperty(window, 'requestAnimationFrame', {
value: null,
})

View File

@ -0,0 +1,259 @@
export default {
componentName: 'Div',
title: '容器',
docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md',
devMode: 'procode',
tags: ['布局'],
configure: {
props: [
{
type: 'field',
name: 'behavior',
title: '默认状态',
extraProps: {
display: 'inline',
defaultValue: 'NORMAL',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
options: [
{
title: '普通',
value: 'NORMAL',
},
{
title: '隐藏',
value: 'HIDDEN',
},
],
loose: false,
cancelable: false,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: '__style__',
title: {
label: '样式设置',
tip: '点击 ? 查看样式设置器用法指南',
docUrl: 'https://lark.alipay.com/legao/help/design-tool-style',
},
extraProps: {
display: 'accordion',
defaultValue: {},
},
setter: {
key: null,
ref: null,
props: {
advanced: true,
},
_owner: null,
},
},
{
type: 'group',
name: 'groupkh97h5kc',
title: '高级',
extraProps: {
display: 'accordion',
},
items: [
{
type: 'field',
name: 'fieldId',
title: {
label: '唯一标识',
},
extraProps: {
display: 'block',
},
setter: {
key: null,
ref: null,
props: {
placeholder: '请输入唯一标识',
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
},
{
type: 'field',
name: 'useFieldIdAsDomId',
title: {
label: '将唯一标识用作 DOM ID',
},
extraProps: {
display: 'block',
defaultValue: false,
},
setter: {
key: null,
ref: null,
props: {},
_owner: null,
},
},
{
type: 'field',
name: 'customClassName',
title: '自定义样式类',
extraProps: {
display: 'block',
defaultValue: '',
},
setter: {
componentName: 'MixedSetter',
props: {
setters: [
{
key: null,
ref: null,
props: {
placeholder: null,
multiline: false,
rows: 10,
required: false,
pattern: null,
maxLength: null,
},
_owner: null,
},
'VariableSetter',
],
},
},
},
{
type: 'field',
name: 'events',
title: {
label: '动作设置',
tip: '点击 ? 查看如何设置组件的事件响应动作',
docUrl: 'https://lark.alipay.com/legao/legao/events-call',
},
extraProps: {
display: 'accordion',
defaultValue: {
ignored: true,
},
},
setter: {
key: null,
ref: null,
props: {
events: [
{
name: 'onClick',
title: '当点击时',
initialValue:
"/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}",
},
{
name: 'onMouseEnter',
title: '当鼠标进入时',
initialValue:
"/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}",
},
{
name: 'onMouseLeave',
title: '当鼠标离开时',
initialValue:
"/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}",
},
],
},
_owner: null,
},
},
{
type: 'field',
name: 'onClick',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseEnter',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
{
type: 'field',
name: 'onMouseLeave',
extraProps: {
defaultValue: {
ignored: true,
},
},
setter: 'I18nSetter',
},
],
},
],
component: {
isContainer: true,
nestingRule: {},
},
supports: {},
},
experimental: {
callbacks: {},
initials: [
{
name: 'behavior',
},
{
name: '__style__',
},
{
name: 'fieldId',
},
{
name: 'useFieldIdAsDomId',
},
{
name: 'customClassName',
},
{
name: 'events',
},
{
name: 'onClick',
},
{
name: 'onMouseEnter',
},
{
name: 'onMouseLeave',
},
],
filters: [],
autoruns: [],
},
};

View File

@ -0,0 +1,983 @@
export default {
componentName: 'Page',
id: 'node_k1ow3cb9',
title: 'hey, i\' a page!',
props: {
extensions: {
启用页头: true,
},
pageStyle: {
backgroundColor: '#f2f3f5',
},
containerStyle: {},
className: 'page_kgaqfbm4',
templateVersion: '1.0.0',
},
lifeCycles: {
constructor: {
type: 'js',
compiled:
"function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
source:
"function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
},
},
condition: true,
css:
'body{background-color:#f2f3f5}.card_kgaqfbm5 {\n margin-bottom: 12px;\n}.card_kgaqfbm6 {\n margin-bottom: 12px;\n}.button_kgaqfbm7 {\n margin-right: 16px;\n width: 80px\n}.button_kgaqfbm8 {\n width: 80px;\n}.div_kgaqfbm9 {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
methods: {
__initMethods__: {
type: 'js',
source: 'function (exports, module) { /*set actions code here*/ }',
compiled: 'function (exports, module) { /*set actions code here*/ }',
},
},
dataSource: {
offline: [],
globalConfig: {
fit: {
compiled: '',
source: '',
type: 'js',
error: {},
},
},
online: [],
sync: true,
list: [],
},
children: [
{
componentName: 'RootHeader',
id: 'node_k1ow3cba',
props: {},
condition: true,
children: [
{
componentName: 'PageHeader',
id: 'node_k1ow3cbd',
props: {
extraContent: '',
__slot__extraContent: false,
__slot__action: false,
title: {
// type: 'JSSlot',
value: [
{
componentName: 'Text',
id: 'node_k1ow3cbf',
props: {
showTitle: false,
behavior: 'NORMAL',
content: {
use: 'zh_CN',
en_US: 'Title',
zh_CN: '个人信息',
type: 'i18n',
},
__style__: {},
fieldId: 'text_k1ow3h1j',
maxLine: 0,
},
condition: true,
},
],
},
content: '',
__slot__logo: false,
__slot__crumb: false,
crumb: '',
tab: '',
logo: '',
action: '',
__slot__tab: false,
__style__: {},
__slot__content: false,
fieldId: 'pageHeader_k1ow3h1i',
subTitle: false,
},
condition: true,
},
],
},
{
componentName: 'RootContent',
id: 'node_k1ow3cbb',
props: {
contentBgColor: 'transparent',
contentPadding: '0',
contentMargin: '20',
},
condition: true,
children: [
{
componentName: 'Form',
id: 'form',
extraPropA: 'extraPropA',
props: {
size: 'medium',
labelAlign: 'top',
autoValidate: true,
scrollToFirstError: true,
autoUnmount: true,
behavior: 'NORMAL',
dataSource: {
type: 'variable',
variable: 'state.formData',
},
obj: {
a: 1,
b: false,
c: 'string',
},
__style__: {},
fieldId: 'form',
fieldOptions: {},
slotA: '',
},
condition: true,
children: [
{
componentName: 'Card',
id: 'node_k1ow3cbj',
props: {
__slot__title: false,
subTitle: {
use: 'zh_CN',
en_US: '',
zh_CN: '',
type: 'i18n',
},
__slot__subTitle: false,
extra: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
className: 'card_kgaqfbm5',
title: {
use: 'zh_CN',
en_US: 'Title',
zh_CN: '基本信息',
type: 'i18n',
},
__slot__extra: false,
showHeadDivider: true,
__style__: ':root {\n margin-bottom: 12px;\n}',
showTitleBullet: true,
contentHeight: '',
fieldId: 'card_k1ow3h1l',
dividerNoInset: false,
},
condition: true,
children: [
{
componentName: 'CardContent',
id: 'node_k1ow3cbk',
props: {},
condition: true,
children: [
{
componentName: 'ColumnsLayout',
id: 'node_k1ow3cbw',
props: {
layout: '6:6',
columnGap: '20',
rowGap: 0,
__style__: {},
fieldId: 'columns_k1ow3h1v',
},
condition: true,
children: [
{
componentName: 'Column',
id: 'node_k1ow3cbx',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjm',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cbz',
props: {
fieldName: 'name',
hasClear: false,
autoFocus: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please input',
zh_CN: '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
addonBefore: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
validation: [
{
type: 'required',
},
],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h1w',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'TextField',
zh_CN: '姓名',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: null,
zh_CN: '',
},
},
condition: true,
},
{
componentName: 'TextField',
id: 'node_k1ow3cc1',
props: {
fieldName: 'englishName',
hasClear: false,
autoFocus: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please input',
zh_CN: '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
addonBefore: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h1y',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'TextField',
zh_CN: '英文名',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: null,
zh_CN: '',
},
},
condition: true,
},
{
componentName: 'TextField',
id: 'node_k1ow3cc3',
props: {
fieldName: 'jobTitle',
hasClear: false,
autoFocus: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please input',
zh_CN: '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
addonBefore: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h20',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'TextField',
zh_CN: '职位',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: null,
zh_CN: '',
},
},
condition: true,
},
],
},
{
componentName: 'Column',
id: 'node_k1ow3cby',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjn',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc2',
props: {
fieldName: 'nickName',
hasClear: false,
autoFocus: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please input',
zh_CN: '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
addonBefore: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h1z',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'TextField',
zh_CN: '花名',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: null,
zh_CN: '',
},
},
condition: true,
},
{
componentName: 'SelectField',
id: 'node_k1ow3cc0',
props: {
fieldName: 'gender',
hasClear: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
mode: 'single',
showSearch: false,
autoWidth: true,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please select',
zh_CN: '请选择',
type: 'i18n',
},
hasBorder: true,
behavior: 'NORMAL',
value: '',
validation: [
{
type: 'required',
},
],
__style__: {},
fieldId: 'select_k1ow3h1x',
notFoundContent: {
use: 'zh_CN',
type: 'i18n',
},
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'SelectField',
zh_CN: '性别',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
wrapperColOffset: 0,
hasSelectAll: false,
hasArrow: true,
size: 'medium',
labelAlign: 'top',
filterLocal: true,
dataSource: [
{
defaultChecked: false,
text: {
en_US: 'Option 1',
zh_CN: '男',
type: 'i18n',
__sid__: 'param_k1owc4tb',
},
__sid__: 'serial_k1owc4t1',
value: 'M',
sid: 'opt_k1owc4t2',
},
{
defaultChecked: false,
text: {
en_US: 'Option 2',
zh_CN: '女',
type: 'i18n',
__sid__: 'param_k1owc4tf',
},
__sid__: 'serial_k1owc4t2',
value: 'F',
sid: 'opt_k1owc4t3',
},
],
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: null,
zh_CN: '',
},
searchDelay: 300,
},
condition: true,
},
],
},
],
},
],
},
],
},
{
componentName: 'Card',
id: 'node_k1ow3cbl',
props: {
__slot__title: false,
subTitle: {
use: 'zh_CN',
en_US: '',
zh_CN: '',
type: 'i18n',
},
__slot__subTitle: false,
extra: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
className: 'card_kgaqfbm6',
title: {
use: 'zh_CN',
en_US: 'Title',
zh_CN: '部门信息',
type: 'i18n',
},
__slot__extra: false,
showHeadDivider: true,
__style__: ':root {\n margin-bottom: 12px;\n}',
showTitleBullet: true,
contentHeight: '',
fieldId: 'card_k1ow3h1m',
dividerNoInset: false,
},
condition: true,
children: [
{
componentName: 'CardContent',
id: 'node_k1ow3cbm',
props: {},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc4',
props: {
fieldName: 'department',
hasClear: false,
autoFocus: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please input',
zh_CN: '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
addonBefore: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h21',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'TextField',
zh_CN: '所属部门',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: null,
zh_CN: '',
},
},
condition: true,
},
{
componentName: 'ColumnsLayout',
id: 'node_k1ow3cc5',
props: {
layout: '6:6',
columnGap: '20',
rowGap: 0,
__style__: {},
fieldId: 'columns_k1ow3h22',
},
condition: true,
children: [
{
componentName: 'Column',
id: 'node_k1ow3cc6',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjo',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc8',
props: {
fieldName: 'leader',
hasClear: false,
autoFocus: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please input',
zh_CN: '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
addonBefore: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h23',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'TextField',
zh_CN: '主管',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: null,
zh_CN: '',
},
},
condition: true,
},
],
},
{
componentName: 'Column',
id: 'node_k1ow3cc7',
props: {
colSpan: '',
__style__: {},
fieldId: 'column_k1p1bnjp',
},
condition: true,
children: [
{
componentName: 'TextField',
id: 'node_k1ow3cc9',
props: {
fieldName: 'hrg',
hasClear: false,
autoFocus: false,
tips: {
en_US: '',
zh_CN: '',
type: 'i18n',
},
trim: false,
labelTextAlign: 'right',
placeholder: {
use: 'zh_CN',
en_US: 'please input',
zh_CN: '请输入',
type: 'i18n',
},
state: '',
behavior: 'NORMAL',
value: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
addonBefore: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
validation: [],
hasLimitHint: false,
cutString: false,
__style__: {},
fieldId: 'textField_k1ow3h24',
htmlType: 'input',
autoHeight: false,
labelColOffset: 0,
label: {
use: 'zh_CN',
en_US: 'TextField',
zh_CN: 'HRG',
type: 'i18n',
},
__category__: 'form',
labelColSpan: 4,
wrapperColSpan: 0,
rows: 4,
addonAfter: {
use: 'zh_CN',
zh_CN: '',
type: 'i18n',
},
wrapperColOffset: 0,
size: 'medium',
labelAlign: 'top',
__useMediator: 'value',
labelTipsTypes: 'none',
labelTipsIcon: '',
labelTipsText: {
type: 'i18n',
use: 'zh_CN',
en_US: null,
zh_CN: '',
},
},
condition: true,
},
],
},
],
},
],
},
],
},
{
componentName: 'Div',
id: 'node_k1ow3cbo',
props: {
className: 'div_kgaqfbm9',
behavior: 'NORMAL',
__style__:
':root {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
events: {},
fieldId: 'div_k1ow3h1o',
useFieldIdAsDomId: false,
customClassName: '',
},
condition: true,
children: [
{
componentName: 'Button',
id: 'node_k1ow3cbn',
props: {
triggerEventsWhenLoading: false,
onClick: {
rawType: 'events',
type: 'JSExpression',
value: 'this.utils.legaoBuiltin.execEventFlow.bind(this, [this.submit])',
events: [
{
name: 'submit',
id: 'submit',
params: {},
type: 'actionRef',
uuid: '1570966253282_0',
},
],
},
size: 'medium',
baseIcon: '',
otherIcon: '',
className: 'button_kgaqfbm7',
type: 'primary',
behavior: 'NORMAL',
loading: false,
content: {
use: 'zh_CN',
en_US: 'Button',
zh_CN: '提交',
type: 'i18n',
},
__style__: ':root {\n margin-right: 16px;\n width: 80px\n}',
fieldId: 'button_k1ow3h1n',
},
condition: true,
},
{
componentName: 'Button',
id: 'node_k1ow3cbp',
props: {
triggerEventsWhenLoading: false,
size: 'medium',
baseIcon: '',
otherIcon: '',
className: 'button_kgaqfbm8',
type: 'normal',
behavior: 'NORMAL',
loading: false,
content: {
use: 'zh_CN',
en_US: 'Button',
zh_CN: '取消',
type: 'i18n',
},
__style__: ':root {\n width: 80px;\n}',
fieldId: 'button_k1ow3h1p',
greeting: {
// type: 'JSSlot',
value: [{
componentName: 'Text',
props: {},
}]
}
},
condition: true,
},
],
},
],
},
],
},
{
componentName: 'RootFooter',
id: 'node_k1ow3cbc',
props: {},
condition: true,
},
],
};

View File

@ -0,0 +1,90 @@
export default {
componentName: 'Page',
id: 'node_k1ow3cb9',
title: 'hey, i\' a page!',
props: {
extensions: {
启用页头: true,
},
pageStyle: {
backgroundColor: '#f2f3f5',
},
containerStyle: {},
className: 'page_kgaqfbm4',
templateVersion: '1.0.0',
},
lifeCycles: {
constructor: {
type: 'js',
compiled:
"function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
source:
"function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
},
},
condition: true,
css:
'body{background-color:#f2f3f5}.card_kgaqfbm5 {\n margin-bottom: 12px;\n}.card_kgaqfbm6 {\n margin-bottom: 12px;\n}.button_kgaqfbm7 {\n margin-right: 16px;\n width: 80px\n}.button_kgaqfbm8 {\n width: 80px;\n}.div_kgaqfbm9 {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
methods: {
__initMethods__: {
type: 'js',
source: 'function (exports, module) { /*set actions code here*/ }',
compiled: 'function (exports, module) { /*set actions code here*/ }',
},
},
children: [
{
componentName: 'Div',
id: 'div',
props: {
className: 'div_kgaqfbm9',
behavior: 'NORMAL',
__style__:
':root {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
events: {},
fieldId: 'div_k1ow3h1o',
useFieldIdAsDomId: false,
customClassName: {
type: 'JSExpression',
value: 'getFromSomewhere()'
},
customClassName2: {
type: 'JSExpression',
mock: { hi: 'mock' },
value: 'getFromSomewhere()'
},
},
extraPropA: 'haha',
},
{
componentName: 'Div',
id: 'div2',
props: {
className: 'div_kgaqfbm9',
behavior: 'NORMAL',
__style__:
':root {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
events: {},
fieldId: 'div_k1ow3h1o',
useFieldIdAsDomId: false,
customClassName: '',
},
extraPropA: 'haha',
},
{
componentName: 'Test',
id: 'test',
props: {
className: 'div_kgaqfbm9',
behavior: 'NORMAL',
__style__:
':root {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n background: #fff;\n padding: 20px 0;\n}',
events: {},
fieldId: 'div_k1ow3h1o',
useFieldIdAsDomId: false,
customClassName: '',
},
extraPropA: 'haha',
}
],
};

View File

@ -0,0 +1,7 @@
if (!process.env.LISTENING_TO_UNHANDLED_REJECTION) {
process.on('unhandledRejection', reason => {
throw reason;
})
// Avoid memory leak by adding too many listeners
process.env.LISTENING_TO_UNHANDLED_REJECTION = true;
}

View File

@ -0,0 +1,18 @@
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
Object.defineProperty(window, 'React', {
writable: true,
value: {},
});

View File

@ -0,0 +1,71 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';
import '../fixtures/window';
import { Node } from '../../src/document/node/node';
import { Designer } from '../../src/designer/designer';
import divMeta from '../fixtures/component-metadata/div';
import { ComponentMeta, isComponentMeta, removeBuiltinComponentAction, addBuiltinComponentAction } from '../../src/component-meta';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
return {
getGlobalComponentActions: () => [],
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({});
});
describe('组件元数据处理', () => {
it('构造函数', () => {
const meta = new ComponentMeta(designer, divMeta);
expect(meta.isContainer).toBeTruthy;
expect(isComponentMeta(meta)).toBeTruthy;
expect(meta.acceptable).toBeTruthy;
expect(meta.isRootComponent()).toBeTruthy;
expect(meta.isModal).toBeFalsy;
expect(meta.rootSelector).toBeUndefined;
expect(meta.liveTextEditing).toBeUndefined;
expect(meta.descriptor).toBeUndefined;
expect(meta.icon).toBeUndefined;
expect(meta.getMetadata().title).toBe('容器');
expect(meta.title).toEqual({ type: 'i18n', 'en-US': 'Div', 'zh-CN': '容器' });
meta.setNpm({ package: '@ali/vc-div', componentName: 'Div' });
expect(meta.npm).toEqual({ package: '@ali/vc-div', componentName: 'Div' });
meta.setMetadata(divMeta);
});
it('availableActions', () => {
const meta = new ComponentMeta(designer, divMeta);
expect(meta.availableActions).toHaveLength(3);
expect(meta.availableActions[0].name).toBe('remove');
expect(meta.availableActions[1].name).toBe('hide');
expect(meta.availableActions[2].name).toBe('copy');
removeBuiltinComponentAction('remove');
// availableActions 有 computed 缓存
expect(meta.availableActions[0].name).toBe('remove');
expect(meta.availableActions[1].name).toBe('hide');
expect(meta.availableActions[2].name).toBe('copy');
addBuiltinComponentAction({
name: 'new',
content: {
action() {}
}
});
// availableActions 有 computed 缓存
expect(meta.availableActions).toHaveLength(3);
expect(meta.availableActions[0].name).toBe('remove');
expect(meta.availableActions[1].name).toBe('hide');
expect(meta.availableActions[2].name).toBe('copy');
});
});

View File

@ -0,0 +1,564 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';
import '../fixtures/window';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
import { EBADF } from 'constants';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
return {
getComponentMeta() {
return {
getMetadata() {
return { experimental: null };
},
};
},
transformProps(props) { return props; },
createSettingEntry: mockCreateSettingEntry,
postEvent() {},
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({});
});
describe('schema 生成节点模型测试', () => {
describe('block ❌ | component ❌ | slot ❌', () => {
let project: Project;
beforeEach(() => {
project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
});
afterEach(() => {
project.unload();
});
it('基本的节点模型初始化,模型导出', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
expect(nodesMap.size).toBe(expectedNodeCnt);
ids.forEach(id => {
expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName);
});
const pageNode = currentDocument?.getNode('node_k1ow3cb9');
expect(pageNode?.getComponentName()).toBe('Page');
expect(pageNode?.getIcon()).toBeUndefined;
const exportSchema = currentDocument?.export(1);
expect(getIdsFromSchema(exportSchema).length).toBe(expectedNodeCnt);
expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt);
});
it('基本的节点模型初始化,节点深度', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const getNode = currentDocument.getNode.bind(currentDocument);
const pageNode = getNode('node_k1ow3cb9');
const rootHeaderNode = getNode('node_k1ow3cba');
const rootContentNode = getNode('node_k1ow3cbb');
const rootFooterNode = getNode('node_k1ow3cbc');
const formNode = getNode('form');
const cardNode = getNode('node_k1ow3cbj');
const cardContentNode = getNode('node_k1ow3cbk');
const columnsLayoutNode = getNode('node_k1ow3cbw');
const columnNode = getNode('node_k1ow3cbx');
const textFieldNode = getNode('node_k1ow3cbz');
expect(pageNode?.zLevel).toBe(0);
expect(rootHeaderNode?.zLevel).toBe(1);
expect(rootContentNode?.zLevel).toBe(1);
expect(rootFooterNode?.zLevel).toBe(1);
expect(formNode?.zLevel).toBe(2);
expect(cardNode?.zLevel).toBe(3);
expect(cardContentNode?.zLevel).toBe(4);
expect(columnsLayoutNode?.zLevel).toBe(5);
expect(columnNode?.zLevel).toBe(6);
expect(textFieldNode?.zLevel).toBe(7);
expect(textFieldNode?.getZLevelTop(7)).toEqual(textFieldNode);
expect(textFieldNode?.getZLevelTop(6)).toEqual(columnNode);
expect(textFieldNode?.getZLevelTop(5)).toEqual(columnsLayoutNode);
expect(textFieldNode?.getZLevelTop(4)).toEqual(cardContentNode);
expect(textFieldNode?.getZLevelTop(3)).toEqual(cardNode);
expect(textFieldNode?.getZLevelTop(2)).toEqual(formNode);
expect(textFieldNode?.getZLevelTop(1)).toEqual(rootContentNode);
expect(textFieldNode?.getZLevelTop(0)).toEqual(pageNode);
// 异常情况
expect(textFieldNode?.getZLevelTop(8)).toBeNull;
expect(textFieldNode?.getZLevelTop(-1)).toBeNull;
});
it('基本的节点模型初始化,节点父子、兄弟相关方法', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const getNode = currentDocument.getNode.bind(currentDocument);
const pageNode = getNode('node_k1ow3cb9');
const rootHeaderNode = getNode('node_k1ow3cba');
const rootContentNode = getNode('node_k1ow3cbb');
const rootFooterNode = getNode('node_k1ow3cbc');
const formNode = getNode('form');
const cardNode = getNode('node_k1ow3cbj');
const cardContentNode = getNode('node_k1ow3cbk');
const columnsLayoutNode = getNode('node_k1ow3cbw');
const columnNode = getNode('node_k1ow3cbx');
const textFieldNode = getNode('node_k1ow3cbz');
expect(pageNode?.index).toBe(-1);
expect(pageNode?.children.toString()).toBe('[object Array]');
expect(pageNode?.children?.get(1)).toBe(rootContentNode);
expect(pageNode?.getChildren()?.get(1)).toBe(rootContentNode);
expect(pageNode?.getNode()).toBe(pageNode);
expect(rootFooterNode?.index).toBe(2);
expect(textFieldNode?.getParent()).toBe(columnNode);
expect(columnNode?.getParent()).toBe(columnsLayoutNode);
expect(columnsLayoutNode?.getParent()).toBe(cardContentNode);
expect(cardContentNode?.getParent()).toBe(cardNode);
expect(cardNode?.getParent()).toBe(formNode);
expect(formNode?.getParent()).toBe(rootContentNode);
expect(rootContentNode?.getParent()).toBe(pageNode);
expect(rootContentNode?.prevSibling).toBe(rootHeaderNode);
expect(rootContentNode?.nextSibling).toBe(rootFooterNode);
expect(pageNode?.isRoot()).toBe(true);
expect(pageNode?.contains(textFieldNode)).toBe(true);
expect(textFieldNode?.getRoot()).toBe(pageNode);
expect(columnNode?.getRoot()).toBe(pageNode);
expect(columnsLayoutNode?.getRoot()).toBe(pageNode);
expect(cardContentNode?.getRoot()).toBe(pageNode);
expect(cardNode?.getRoot()).toBe(pageNode);
expect(formNode?.getRoot()).toBe(pageNode);
expect(rootContentNode?.getRoot()).toBe(pageNode);
});
it('基本的节点模型初始化,节点新建、删除等事件', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const getNode = currentDocument.getNode.bind(currentDocument);
const createNode = currentDocument.createNode.bind(currentDocument);
const pageNode = getNode('node_k1ow3cb9');
const nodeCreateHandler = jest.fn();
currentDocument?.onNodeCreate(nodeCreateHandler);
const node = createNode({
componentName: 'TextInput',
props: {
propA: 'haha',
}
});
currentDocument?.insertNode(pageNode, node);
expect(nodeCreateHandler).toHaveBeenCalledTimes(1);
expect(nodeCreateHandler.mock.calls[0][0]).toBe(node);
expect(nodeCreateHandler.mock.calls[0][0].componentName).toBe('TextInput');
expect(nodeCreateHandler.mock.calls[0][0].getPropValue('propA')).toBe('haha');
const nodeDestroyHandler = jest.fn();
currentDocument?.onNodeDestroy(nodeDestroyHandler);
node.remove();
expect(nodeDestroyHandler).toHaveBeenCalledTimes(1);
expect(nodeDestroyHandler.mock.calls[0][0]).toBe(node);
expect(nodeDestroyHandler.mock.calls[0][0].componentName).toBe('TextInput');
expect(nodeDestroyHandler.mock.calls[0][0].getPropValue('propA')).toBe('haha');
});
it.skip('基本的节点模型初始化,节点插入等方法', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const getNode = currentDocument.getNode.bind(currentDocument);
const formNode = getNode('form');
const node1 = currentDocument.createNode({
componentName: 'TextInput',
props: {
propA: 'haha',
}
});
const node2 = currentDocument.createNode({
componentName: 'TextInput',
props: {
propA: 'heihei',
}
});
const node3 = currentDocument.createNode({
componentName: 'TextInput',
props: {
propA: 'heihei2',
}
});
const node4 = currentDocument.createNode({
componentName: 'TextInput',
props: {
propA: 'heihei3',
}
});
formNode?.insertBefore(node2);
// formNode?.insertBefore(node1, node2);
// formNode?.insertAfter(node3);
// formNode?.insertAfter(node4, node3);
expect(formNode?.children?.get(0)).toBe(node1);
expect(formNode?.children?.get(1)).toBe(node2);
// expect(formNode?.children?.get(5)).toBe(node3);
// expect(formNode?.children?.get(6)).toBe(node4);
});
it('基本的节点模型初始化,节点其他方法', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const getNode = currentDocument.getNode.bind(currentDocument);
const pageNode = getNode('node_k1ow3cb9');
expect(pageNode?.isPage()).toBe(true);
expect(pageNode?.isComponent()).toBe(false);
expect(pageNode?.isSlot()).toBe(false);
expect(pageNode?.title).toBe('hey, i\' a page!');
});
describe('节点新增insertNode', () => {
let project: Project;
beforeEach(() => {
project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
});
it('场景一:插入 NodeSchema不指定 index', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form') as Node;
const formNode2 = currentDocument?.getNode('form');
expect(formNode).toEqual(formNode2);
currentDocument?.insertNode(formNode, {
componentName: 'TextInput',
id: 'nodeschema-id1',
props: {
propA: 'haha',
propB: 3
}
});
expect(nodesMap.size).toBe(ids.length + 1);
expect(formNode.children?.length).toBe(4);
const insertedNode = formNode.children.get(formNode.children.length - 1);
expect(insertedNode.componentName).toBe('TextInput');
expect(insertedNode.propsData).toEqual({
propA: 'haha',
propB: 3
});
// TODO: 把 checkId 的 commit pick 过来
// expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput');
});
it('场景一:插入 NodeSchema指定 index: 0', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const nodesMap = currentDocument.nodesMap;
const formNode = nodesMap.get('form');
currentDocument?.insertNode(formNode, {
componentName: 'TextInput',
id: 'nodeschema-id1',
props: {
propA: 'haha',
propB: 3
}
}, 0);
expect(nodesMap.size).toBe(ids.length + 1);
expect(formNode.children.length).toBe(4);
const insertedNode = formNode.children.get(0);
expect(insertedNode.componentName).toBe('TextInput');
expect(insertedNode.propsData).toEqual({
propA: 'haha',
propB: 3
});
// TODO: 把 checkId 的 commit pick 过来
// expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput');
});
it('场景一:插入 NodeSchema指定 index: 1', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form');
currentDocument?.insertNode(formNode, {
componentName: 'TextInput',
id: 'nodeschema-id1',
props: {
propA: 'haha',
propB: 3
}
}, 1);
expect(nodesMap.size).toBe(ids.length + 1);
expect(formNode.children.length).toBe(4);
const insertedNode = formNode.children.get(1);
expect(insertedNode.componentName).toBe('TextInput');
expect(insertedNode.propsData).toEqual({
propA: 'haha',
propB: 3
});
// TODO: 把 checkId 的 commit pick 过来
// expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput');
});
it('场景一:插入 NodeSchema有 children', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form') as Node;
currentDocument?.insertNode(formNode, {
componentName: 'ParentNode',
props: {
propA: 'haha',
propB: 3
},
children: [
{
componentName: 'SubNode',
props: {
propA: 'haha',
propB: 3
}
},
{
componentName: 'SubNode2',
props: {
propA: 'haha',
propB: 3
}
}
]
});
expect(nodesMap.size).toBe(ids.length + 3);
expect(formNode.children.length).toBe(4);
expect(formNode.children?.get(3)?.componentName).toBe('ParentNode');
expect(formNode.children?.get(3)?.children?.get(0)?.componentName).toBe('SubNode');
expect(formNode.children?.get(3)?.children?.get(1)?.componentName).toBe('SubNode2');
});
it.skip('场景一:插入 NodeSchemaid 与现有 schema 里的 id 重复', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form');
currentDocument?.insertNode(formNode, {
componentName: 'TextInput',
id: 'nodeschema-id1',
props: {
propA: 'haha',
propB: 3
}
});
expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput');
expect(nodesMap.size).toBe(ids.length + 1);
});
it.skip('场景一:插入 NodeSchemaid 与现有 schema 里的 id 重复,但关闭了 id 检测器', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form');
currentDocument?.insertNode(formNode, {
componentName: 'TextInput',
id: 'nodeschema-id1',
props: {
propA: 'haha',
propB: 3
}
});
expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput');
expect(nodesMap.size).toBe(ids.length + 1);
});
it('场景二:插入 Node 实例', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form');
const inputNode = currentDocument?.createNode({
componentName: 'TextInput',
id: 'nodeschema-id2',
props: {
propA: 'haha',
propB: 3
}
});
currentDocument?.insertNode(formNode, inputNode);
expect(formNode.children?.get(3)?.componentName).toBe('TextInput');
expect(nodesMap.size).toBe(ids.length + 1);
});
it('场景三:插入 JSExpression', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form') as Node;
currentDocument?.insertNode(formNode, {
type: 'JSExpression',
value: 'just a expression'
});
expect(nodesMap.size).toBe(ids.length + 1);
expect(formNode.children?.get(3)?.componentName).toBe('Leaf');
// expect(formNode.children?.get(3)?.children).toEqual({
// type: 'JSExpression',
// value: 'just a expression'
// });
});
it('场景四:插入 string', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form') as Node;
currentDocument?.insertNode(formNode, 'just a string');
expect(nodesMap.size).toBe(ids.length + 1);
expect(formNode.children?.get(3)?.componentName).toBe('Leaf');
// expect(formNode.children?.get(3)?.children).toBe('just a string');
});
});
describe('节点新增insertNodes', () => {
let project: Project;
beforeEach(() => {
project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
});
it('场景一:插入 NodeSchema指定 index', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form') as Node;
const formNode2 = currentDocument?.getNode('form');
expect(formNode).toEqual(formNode2);
currentDocument?.insertNodes(formNode, [
{
componentName: 'TextInput',
props: {
propA: 'haha2',
propB: 3
}
},
{
componentName: 'TextInput2',
props: {
propA: 'haha',
propB: 3
}
}
], 1);
expect(nodesMap.size).toBe(ids.length + 2);
expect(formNode.children?.length).toBe(5);
const insertedNode1 = formNode.children.get(1);
const insertedNode2 = formNode.children.get(2);
expect(insertedNode1.componentName).toBe('TextInput');
expect(insertedNode1.propsData).toEqual({
propA: 'haha2',
propB: 3
});
expect(insertedNode2.componentName).toBe('TextInput2');
expect(insertedNode2.propsData).toEqual({
propA: 'haha',
propB: 3
});
});
it('场景二:插入 Node 实例,指定 index', () => {
expect(project).toBeTruthy();
const ids = getIdsFromSchema(formSchema);
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const formNode = nodesMap.get('form') as Node;
const formNode2 = currentDocument?.getNode('form');
expect(formNode).toEqual(formNode2);
const createdNode1 = currentDocument?.createNode({
componentName: 'TextInput',
props: {
propA: 'haha2',
propB: 3
}
});
const createdNode2 = currentDocument?.createNode({
componentName: 'TextInput2',
props: {
propA: 'haha',
propB: 3
}
});
currentDocument?.insertNodes(formNode, [ createdNode1, createdNode2 ], 1);
expect(nodesMap.size).toBe(ids.length + 2);
expect(formNode.children?.length).toBe(5);
const insertedNode1 = formNode.children.get(1);
const insertedNode2 = formNode.children.get(2);
expect(insertedNode1.componentName).toBe('TextInput');
expect(insertedNode1.propsData).toEqual({
propA: 'haha2',
propB: 3
});
expect(insertedNode2.componentName).toBe('TextInput2');
expect(insertedNode2.propsData).toEqual({
propA: 'haha',
propB: 3
});
});
});
})
describe('block ❌ | component ❌ | slot ✅', () => {
it('基本的 slot 创建', () => {
const formSchemaWithSlot = set(cloneDeep(formSchema), 'children[0].children[0].props.title.type', 'JSSlot');
const project = new Project(designer, {
componentsTree: [
formSchemaWithSlot,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
// 目前每个 slot 会新增1 + children.length个节点
const expectedNodeCnt = ids.length + 2;
expect(nodesMap.size).toBe(expectedNodeCnt);
// PageHeader
expect(nodesMap.get('node_k1ow3cbd').slots).toHaveLength(1);
});
});
});

View File

@ -0,0 +1,61 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';
import '../fixtures/window';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
return {
getComponentMeta() {
return {
getMetadata() {
return { experimental: null };
},
};
},
transformProps(props) { return props; },
createSettingEntry: mockCreateSettingEntry,
postEvent() {},
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({});
});
describe.skip('节点拖拽测试', () => {
describe('block ❌ | component ❌ | slot ❌', () => {
it('修改普通属性string | number', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
expect(nodesMap.size).toBe(expectedNodeCnt);
ids.forEach(id => {
expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName);
});
const exportSchema = currentDocument?.export(1);
expect(getIdsFromSchema(exportSchema).length).toBe(expectedNodeCnt);
expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt);
});
});
});

View File

@ -0,0 +1,456 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';
import '../fixtures/window';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
return {
getComponentMeta() {
return {
getMetadata() {
return { experimental: null };
},
};
},
transformProps(props) { return props; },
createSettingEntry: mockCreateSettingEntry,
postEvent() {},
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({});
});
describe('schema 生成节点模型测试', () => {
describe('block ❌ | component ❌ | slot ❌', () => {
let project: Project;
beforeEach(() => {
project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
});
it('读取普通属性string | number | object', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const formNode = currentDocument?.getNode('form');
/*
props: {
size: 'medium',
labelAlign: 'top',
autoValidate: true,
scrollToFirstError: true,
autoUnmount: true,
behavior: 'NORMAL',
dataSource: {
type: 'variable',
variable: 'state.formData',
},
obj: {
a: 1,
b: false,
c: 'string',
},
__style__: {},
fieldId: 'form',
fieldOptions: {},
},
id: 'form',
condition: true,
*/
const sizeProp = formNode?.getProp('size');
const sizeProp2 = formNode?.getProps().getProp('size');
expect(sizeProp).toBe(sizeProp2);
expect(sizeProp?.getAsString()).toBe('medium');
expect(sizeProp?.getValue()).toBe('medium');
const autoValidateProp = formNode?.getProp('autoValidate');
expect(autoValidateProp?.getValue()).toBe(true);
const objProp = formNode?.getProp('obj');
expect(objProp?.getValue()).toEqual({
a: 1,
b: false,
c: 'string',
});
const objAProp = formNode?.getProp('obj.a');
const objBProp = formNode?.getProp('obj.b');
const objCProp = formNode?.getProp('obj.c');
expect(objAProp?.getValue()).toBe(1);
expect(objBProp?.getValue()).toBe(false);
expect(objCProp?.getValue()).toBe('string');
const idProp = formNode?.getExtraProp('extraPropA');
expect(idProp?.getValue()).toBe('extraPropA');
});
it('修改普通属性string | number | object使用 Node 实例接口', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const formNode = currentDocument?.getNode('form');
/*
props: {
size: 'medium',
labelAlign: 'top',
autoValidate: true,
scrollToFirstError: true,
autoUnmount: true,
behavior: 'NORMAL',
dataSource: {
type: 'variable',
variable: 'state.formData',
},
obj: {
a: 1,
b: false,
c: 'string',
},
__style__: {},
fieldId: 'form',
fieldOptions: {},
},
id: 'form',
condition: true,
*/
formNode?.setPropValue('size', 'large');
const sizeProp = formNode?.getProp('size');
expect(sizeProp?.getAsString()).toBe('large');
expect(sizeProp?.getValue()).toBe('large');
formNode?.setPropValue('autoValidate', false);
const autoValidateProp = formNode?.getProp('autoValidate');
expect(autoValidateProp?.getValue()).toBe(false);
formNode?.setPropValue('obj', {
a: 2,
b: true,
c: 'another string'
});
const objProp = formNode?.getProp('obj');
expect(objProp?.getValue()).toEqual({
a: 2,
b: true,
c: 'another string',
});
formNode?.setPropValue('obj.a', 3);
formNode?.setPropValue('obj.b', false);
formNode?.setPropValue('obj.c', 'string');
const objAProp = formNode?.getProp('obj.a');
const objBProp = formNode?.getProp('obj.b');
const objCProp = formNode?.getProp('obj.c');
expect(objAProp?.getValue()).toBe(3);
expect(objBProp?.getValue()).toBe(false);
expect(objCProp?.getValue()).toBe('string');
expect(objProp?.getValue()).toEqual({
a: 3,
b: false,
c: 'string',
});
});
it('修改普通属性string | number | object使用 Props 实例接口', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const formNode = currentDocument?.getNode('form');
/*
props: {
size: 'medium',
labelAlign: 'top',
autoValidate: true,
scrollToFirstError: true,
autoUnmount: true,
behavior: 'NORMAL',
dataSource: {
type: 'variable',
variable: 'state.formData',
},
obj: {
a: 1,
b: false,
c: 'string',
},
__style__: {},
fieldId: 'form',
fieldOptions: {},
},
id: 'form',
condition: true,
*/
const props = formNode?.getProps();
props?.setPropValue('size', 'large');
const sizeProp = formNode?.getProp('size');
expect(sizeProp?.getAsString()).toBe('large');
expect(sizeProp?.getValue()).toBe('large');
props?.setPropValue('autoValidate', false);
const autoValidateProp = formNode?.getProp('autoValidate');
expect(autoValidateProp?.getValue()).toBe(false);
props?.setPropValue('obj', {
a: 2,
b: true,
c: 'another string'
});
const objProp = formNode?.getProp('obj');
expect(objProp?.getValue()).toEqual({
a: 2,
b: true,
c: 'another string',
});
props?.setPropValue('obj.a', 3);
props?.setPropValue('obj.b', false);
props?.setPropValue('obj.c', 'string');
const objAProp = formNode?.getProp('obj.a');
const objBProp = formNode?.getProp('obj.b');
const objCProp = formNode?.getProp('obj.c');
expect(objAProp?.getValue()).toBe(3);
expect(objBProp?.getValue()).toBe(false);
expect(objCProp?.getValue()).toBe('string');
expect(objProp?.getValue()).toEqual({
a: 3,
b: false,
c: 'string',
});
});
it('修改普通属性string | number | object使用 Prop 实例接口', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const formNode = currentDocument?.getNode('form');
/*
props: {
size: 'medium',
labelAlign: 'top',
autoValidate: true,
scrollToFirstError: true,
autoUnmount: true,
behavior: 'NORMAL',
dataSource: {
type: 'variable',
variable: 'state.formData',
},
obj: {
a: 1,
b: false,
c: 'string',
},
__style__: {},
fieldId: 'form',
fieldOptions: {},
},
id: 'form',
condition: true,
*/
const sizeProp = formNode?.getProp('size');
sizeProp?.setValue('large');
expect(sizeProp?.getAsString()).toBe('large');
expect(sizeProp?.getValue()).toBe('large');
const autoValidateProp = formNode?.getProp('autoValidate');
autoValidateProp?.setValue(false);
expect(autoValidateProp?.getValue()).toBe(false);
const objProp = formNode?.getProp('obj');
objProp?.setValue({
a: 2,
b: true,
c: 'another string'
});
expect(objProp?.getValue()).toEqual({
a: 2,
b: true,
c: 'another string',
});
const objAProp = formNode?.getProp('obj.a');
const objBProp = formNode?.getProp('obj.b');
const objCProp = formNode?.getProp('obj.c');
objAProp?.setValue(3);
objBProp?.setValue(false);
objCProp?.setValue('string');
expect(objAProp?.getValue()).toBe(3);
expect(objBProp?.getValue()).toBe(false);
expect(objCProp?.getValue()).toBe('string');
expect(objProp?.getValue()).toEqual({
a: 3,
b: false,
c: 'string',
});
});
});
describe('block ❌ | component ❌ | slot ✅', () => {
let project: Project;
beforeEach(() => {
project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
});
it('修改 slot 属性,初始存在 slot 属性名,正常生成节点模型', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const formNode = currentDocument?.getNode('form');
formNode?.setPropValue('slotA', {
type: 'JSSlot',
value: [{
componentName: 'TextInput1',
props: {
txt: 'haha',
num: 1,
bool: true
}
}, {
componentName: 'TextInput2',
props: {
txt: 'heihei',
num: 2,
bool: false
}
}]
});
expect(nodesMap.size).toBe(ids.length + 3);
expect(formNode?.slots).toHaveLength(1);
expect(formNode?.slots[0].children).toHaveLength(2);
const firstChildNode = formNode?.slots[0].children?.get(0);
const secondChildNode = formNode?.slots[0].children?.get(1);
expect(firstChildNode?.componentName).toBe('TextInput1');
expect(firstChildNode?.getPropValue('txt')).toBe('haha');
expect(firstChildNode?.getPropValue('num')).toBe(1);
expect(firstChildNode?.getPropValue('bool')).toBe(true);
expect(secondChildNode?.componentName).toBe('TextInput2');
expect(secondChildNode?.getPropValue('txt')).toBe('heihei');
expect(secondChildNode?.getPropValue('num')).toBe(2);
expect(secondChildNode?.getPropValue('bool')).toBe(false);
});
it('修改 slot 属性,初始存在 slot 属性名,关闭 slot', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const formNode = currentDocument?.getNode('form');
formNode?.setPropValue('slotA', {
type: 'JSSlot',
value: [{
componentName: 'TextInput1',
props: {
txt: 'haha',
num: 1,
bool: true
}
}, {
componentName: 'TextInput2',
props: {
txt: 'heihei',
num: 2,
bool: false
}
}]
});
expect(nodesMap.size).toBe(ids.length + 3);
expect(formNode?.slots).toHaveLength(1);
formNode?.setPropValue('slotA', '');
expect(nodesMap.size).toBe(ids.length);
expect(formNode?.slots).toHaveLength(0);
});
it('修改 slot 属性,初始存在 slot 属性名,同名覆盖 slot', () => {
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
const formNode = currentDocument?.getNode('form');
formNode?.setPropValue('slotA', {
type: 'JSSlot',
name: 'slotA',
value: [{
componentName: 'TextInput1',
props: {
txt: 'haha',
num: 1,
bool: true
}
}, {
componentName: 'TextInput2',
props: {
txt: 'heihei',
num: 2,
bool: false
}
}]
});
expect(nodesMap.size).toBe(ids.length + 3);
expect(formNode?.slots).toHaveLength(1);
expect(formNode?.slots[0].children).toHaveLength(2);
let firstChildNode = formNode?.slots[0].children?.get(0);
expect(firstChildNode?.componentName).toBe('TextInput1');
expect(firstChildNode?.getPropValue('txt')).toBe('haha');
expect(firstChildNode?.getPropValue('num')).toBe(1);
expect(firstChildNode?.getPropValue('bool')).toBe(true);
formNode?.setPropValue('slotA', {
type: 'JSSlot',
name: 'slotA',
value: [{
componentName: 'TextInput3',
props: {
txt: 'xixi',
num: 3,
bool: false
}
}]
});
expect(nodesMap.size).toBe(ids.length + 2);
expect(formNode?.slots).toHaveLength(1);
expect(formNode?.slots[0].children).toHaveLength(1);
firstChildNode = formNode?.slots[0].children?.get(0);
expect(firstChildNode?.componentName).toBe('TextInput3');
expect(firstChildNode?.getPropValue('txt')).toBe('xixi');
expect(firstChildNode?.getPropValue('num')).toBe(3);
expect(firstChildNode?.getPropValue('bool')).toBe(false);
});
});
});

View File

@ -0,0 +1,122 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep';
import '../fixtures/window';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
return {
getComponentMeta() {
return {
getMetadata() {
return { experimental: null };
},
};
},
transformProps(props) { return props; },
createSettingEntry: mockCreateSettingEntry,
postEvent() {},
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({});
});
describe('节点模型删除测试', () => {
it('删除叶子节点', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const originalNodeCnt = ids.length;
expect(nodesMap.size).toBe(originalNodeCnt);
currentDocument?.removeNode('node_k1ow3cbn');
// Button#1
expect(nodesMap.size).toBe(originalNodeCnt - 1);
currentDocument?.removeNode(nodesMap.get('node_k1ow3cbp'));
// Button#2
expect(nodesMap.size).toBe(originalNodeCnt - 2);
currentDocument?.removeNode('unexisting_node');
expect(nodesMap.size).toBe(originalNodeCnt - 2);
});
it('删除叶子节点,带有 slot', () => {
const formSchemaWithSlot = set(cloneDeep(formSchema),
'children[1].children[0].children[2].children[1].props.greeting.type', 'JSSlot');
const project = new Project(designer, {
componentsTree: [
formSchemaWithSlot,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const originalNodeCnt = ids.length + 2;
expect(nodesMap.size).toBe(originalNodeCnt);
currentDocument?.removeNode('node_k1ow3cbp');
// Button + Slot + Text
expect(nodesMap.size).toBe(originalNodeCnt - 3);
});
it('删除分支节点', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const originalNodeCnt = ids.length;
expect(nodesMap.size).toBe(originalNodeCnt);
currentDocument?.removeNode('node_k1ow3cbo');
// Div + 2 * Button
expect(nodesMap.size).toBe(originalNodeCnt - 3);
});
it('删除分支节点,带有 slot', () => {
const formSchemaWithSlot = set(cloneDeep(formSchema),
'children[1].children[0].children[2].children[1].props.greeting.type', 'JSSlot');
const project = new Project(designer, {
componentsTree: [
formSchemaWithSlot,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const originalNodeCnt = ids.length + 2;
expect(nodesMap.size).toBe(originalNodeCnt);
currentDocument?.removeNode('node_k1ow3cbo');
// Div + 2 * Button + Slot + Text
expect(nodesMap.size).toBe(originalNodeCnt - 5);
});
});

View File

@ -0,0 +1,134 @@
import set from 'lodash/set';
import cloneDeep from 'lodash/clonedeep';
import '../fixtures/window';
import { Project } from '../../src/project/project';
import { Node } from '../../src/document/node/node';
import { Designer } from '../../src/designer/designer';
import formSchema from '../fixtures/schema/form';
import { getIdsFromSchema, getNodeFromSchemaById } from '../utils';
const mockCreateSettingEntry = jest.fn();
jest.mock('../../src/designer/designer', () => {
return {
Designer: jest.fn().mockImplementation(() => {
return {
getComponentMeta() {
return {
getMetadata() {
return { experimental: null };
},
};
},
transformProps(props) { return props; },
createSettingEntry: mockCreateSettingEntry,
postEvent() {},
};
}),
};
});
let designer = null;
beforeAll(() => {
designer = new Designer({});
});
describe('schema 生成节点模型测试', () => {
describe('block ❌ | component ❌ | slot ❌', () => {
beforeEach(() => {
mockCreateSettingEntry.mockClear();
});
it('基本的节点模型初始化,模型导出,初始化传入 schema', () => {
const project = new Project(designer, {
componentsTree: [
formSchema,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
expect(nodesMap.size).toBe(expectedNodeCnt);
ids.forEach(id => {
expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName);
});
const exportSchema = currentDocument?.export(1);
expect(getIdsFromSchema(exportSchema).length).toBe(expectedNodeCnt);
expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt);
});
it('基本的节点模型初始化模型导出project.open 传入 schema', () => {
const project = new Project(designer);
project.open(formSchema);
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument;
const ids = getIdsFromSchema(formSchema);
const expectedNodeCnt = ids.length;
expect(nodesMap.size).toBe(expectedNodeCnt);
ids.forEach(id => {
expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName);
});
const exportSchema = currentDocument?.export(1);
expect(getIdsFromSchema(exportSchema).length).toBe(expectedNodeCnt);
expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt);
});
it('project 卸载所有 document - unload()', () => {
const project = new Project(designer);
project.open(formSchema);
expect(project).toBeTruthy();
const { currentDocument, documents } = project;
expect(documents).toHaveLength(1);
expect(currentDocument).toBe(documents[0]);
project.unload();
expect(documents).toHaveLength(0);
});
it('project 卸载指定 document - removeDocument()', () => {
const project = new Project(designer);
project.open(formSchema);
expect(project).toBeTruthy();
const { currentDocument, documents } = project;
expect(documents).toHaveLength(1);
expect(currentDocument).toBe(documents[0]);
project.removeDocument(currentDocument);
expect(documents).toHaveLength(0);
});
});
describe('block ❌ | component ❌ | slot ✅', () => {
it('基本的节点模型初始化,模型导出,初始化传入 schema', () => {
const formSchemaWithSlot = set(cloneDeep(formSchema), 'children[0].children[0].props.title.type', 'JSSlot');
const project = new Project(designer, {
componentsTree: [
formSchemaWithSlot,
],
});
project.open();
expect(project).toBeTruthy();
const { currentDocument } = project;
const { nodesMap } = currentDocument!;
const ids = getIdsFromSchema(formSchema);
// 目前每个 slot 会新增1 + children.length个节点
const expectedNodeCnt = ids.length + 2;
expect(nodesMap.size).toBe(expectedNodeCnt);
// PageHeader
expect(nodesMap.get('node_k1ow3cbd').slots).toHaveLength(1);
});
});
describe.skip('多 document 测试', () => {
});
});

View File

@ -0,0 +1,77 @@
import { getMockRenderer } from './renderer';
interface MockDocument extends Document {
// open(): any;
// write(): any;
// close(): any;
// addEventListener(): any;
// removeEventListener(): any;
triggerEventListener(): any;
// createElement(): any;
// appendChild(): any;
// removeChild(): any;
}
const eventsMap : Map<string, Set<Function>> = new Map<string, Set<Function>>();
const mockAddEventListener = jest.fn((eventName: string, cb) => {
if (!eventsMap.has(eventName)) {
eventsMap.set(eventName, new Set([cb]));
return;
}
eventsMap.get(eventName)!.add(cb);
});
const mockRemoveEventListener = jest.fn((eventName: string, cb) => {
if (!eventsMap.has(eventName)) return;
if (!cb) {
eventsMap.delete(eventName);
return;
}
eventsMap.get(eventName)?.delete(cb);
});
const mockTriggerEventListener = jest.fn((eventName: string, data: any, context: object = {}) => {
if (!eventsMap.has(eventName)) return;
for (const cb of eventsMap.get(eventName)) {
cb.call(context, data);
}
});
const mockCreateElement = jest.fn((tagName) => {
return {
style: {},
appendChild() {},
addEventListener: mockAddEventListener,
removeEventListener: mockRemoveEventListener,
triggerEventListener: mockTriggerEventListener,
}
})
export function getMockDocument(): MockDocument {
return {
open() {},
write() {},
close() {},
addEventListener: mockAddEventListener,
removeEventListener: mockRemoveEventListener,
triggerEventListener: mockTriggerEventListener,
createElement: mockCreateElement,
removeChild() {},
body: { appendChild() {}, removeChild() {} },
};
}
export function getMockWindow(doc?: MockDocument) {
return {
SimulatorRenderer: getMockRenderer(),
addEventListener: mockAddEventListener,
removeEventListener: mockRemoveEventListener,
triggerEventListener: mockTriggerEventListener,
document: doc || getMockDocument(),
};
}
export function clearEventsMap() {
eventsMap.clear();
}

View File

@ -0,0 +1,8 @@
export function getMockEvent(target, options) {
return {
target,
preventDefault() {},
stopPropagation() {},
...options,
};
}

View File

@ -0,0 +1,4 @@
export { getIdsFromSchema, getNodeFromSchemaById } from '@ali/lowcode-test-mate/es/utils';
export { getMockDocument, getMockWindow } from './bom';
export { getMockEvent } from './event';
export { getMockRenderer } from './renderer';

View File

@ -0,0 +1,8 @@
export function getMockRenderer() {
return {
isSimulatorRenderer: true,
run() {
console.log('renderer run');
}
}
}

View File

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

View File

@ -1,5 +1,5 @@
module.exports = {
extends: '../../.eslintrc.js',
extends: 'eslint-config-ali/typescript/react',
rules: {
'react/no-multi-comp': 1,
'no-unused-expressions': 1,

View File

@ -1,4 +1,4 @@
class Monitor {
export class Monitor {
fn = (params: any) => {
const { AES } = window as any;
if (typeof AES.log === 'function') {

View File

@ -1,8 +1,8 @@
module.exports = {
extends: '../../.eslintrc.js',
extends: 'eslint-config-ali/typescript/react',
rules: {
'react/no-multi-comp': 1,
'no-unused-expressions': 1,
'no-unused-expressions': 0,
'implicit-arrow-linebreak': 1,
'no-nested-ternary': 1,
'no-mixed-operators': 1,
@ -16,5 +16,6 @@ module.exports = {
'react/no-deprecated': 1,
'no-useless-escape': 1,
'brace-style': 1,
'@typescript-eslint/member-ordering': 0,
}
}

View File

@ -83,14 +83,27 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
**Note:** Version bump only for package @ali/lowcode-editor-preset-vision
<a name="1.0.12"></a>
## [1.0.12](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.11...@ali/lowcode-editor-preset-vision@1.0.12) (2020-10-20)
<a name="0.12.1-3"></a>
## [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))
<a name="0.12.1-2"></a>
## [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))
@ -119,14 +132,111 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
**Note:** Version bump only for package @ali/lowcode-editor-preset-vision
<a name="1.0.8"></a>
## [1.0.8](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-preset-vision@1.0.8-0...@ali/lowcode-editor-preset-vision@1.0.8) (2020-09-28)
<a name="1.0.9-1"></a>
## [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
<a name="1.0.9-0"></a>
## 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))
<a name="1.0.8-0"></a>
## [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)

View File

@ -10,10 +10,11 @@
"react": "var window.React",
"react-dom": "var window.ReactDOM",
"prop-types": "var window.PropTypes",
"rax": "var window.Rax",
"@ali/visualengine": "var window.VisualEngine",
"@ali/visualengine-utils": "var window.VisualEngineUtils",
"monaco-editor/esm/vs/editor/editor.api":"var window.monaco",
"monaco-editor/esm/vs/editor/editor.main.js":"var window.monaco"
"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"
}
}
],

View File

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

View File

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

View File

@ -10,8 +10,10 @@
"lib"
],
"scripts": {
"start": "build-scripts start",
"build": "build-scripts build --skip-demo",
"cloud-build": "build-scripts build --skip-demo"
"cloud-build": "build-scripts build --skip-demo",
"test": "build-scripts test --config build.test.json"
},
"license": "MIT",
"dependencies": {
@ -20,6 +22,7 @@
"@ali/lowcode-editor-skeleton": "^1.0.22",
"@ali/lowcode-plugin-designer": "^1.0.22",
"@ali/lowcode-plugin-outline-pane": "^1.0.21",
"@ali/lowcode-utils": "^1.0.21",
"@ali/ve-i18n-util": "^2.0.0",
"@ali/ve-icons": "^4.1.9",
"@ali/ve-less-variables": "2.0.3",
@ -37,6 +40,7 @@
"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",
@ -45,6 +49,7 @@
"build-plugin-fusion": "^0.1.0",
"build-plugin-moment-locales": "^0.1.0",
"build-plugin-react-app": "^1.1.2",
"prop-types": "^15.7.2",
"tsconfig-paths-webpack-plugin": "^3.2.0"
},
"publishConfig": {

View File

@ -16,6 +16,7 @@ import {
upgradePropConfig,
upgradeConfigure,
} from './upgrade-metadata';
import { accessLibrary } from '@ali/lowcode-utils';
import { designer } from '../editor';
@ -103,8 +104,8 @@ registerMetadataTransducer(
let top: FieldConfig[];
let bottom: FieldConfig[];
if (combined) {
top = combined?.[0].items || combined;
bottom = combined?.[combined.length - 1].items || combined;
top = combined?.[0]?.items || combined;
bottom = combined?.[combined.length - 1]?.items || combined;
} else if (props) {
top = props;
bottom = props;
@ -166,15 +167,6 @@ export interface OldGlobalPropConfig extends OldPropConfig {
const packageMaps: any = {};
function accessLibrary(library: string | object) {
if (typeof library !== 'string') {
return library;
}
// TODO: enhance logic
return (window as any)[library];
}
export function setPackages(packages: Array<{ package: string; library: object | string }>) {
packages.forEach((item) => {
let lib: any;
@ -236,6 +228,14 @@ class Prototype {
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;

View File

@ -30,7 +30,7 @@ export class Trunk {
}
getList(): any[] {
const list = this.trunk.reduceRight((prev, cur) => prev.concat(cur.getList()), []);
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)) {
@ -98,7 +98,6 @@ export class Trunk {
}
registerSetter(type: string, setter: CustomView | RegisteredSetter) {
// console.warn('Trunk.registerSetter is deprecated');
registerSetter(type, setter);
}

View File

@ -136,6 +136,7 @@ export interface OldPrototypeConfig {
};
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
@ -171,6 +172,7 @@ export interface OldPrototypeConfig {
onResizeEnd?: (e: MouseEvent, triggerDirection: string, dragment: Node) => void;
devMode?: string;
schema?: ProjectSchema;
isTopFixed?: boolean;
}
export interface ISetterConfig {
@ -592,6 +594,7 @@ export function upgradeMetadata(oldConfig: OldPrototypeConfig) {
configure,
transducers,
isContainer,
isAbsoluteLayoutContainer,
rectSelector,
isModal,
isFloating,
@ -620,6 +623,7 @@ export function upgradeMetadata(oldConfig: OldPrototypeConfig) {
onResizeEnd, // onResizeEnd
devMode,
schema,
isTopFixed,
} = oldConfig;
const meta: any = {
@ -678,7 +682,9 @@ export function upgradeMetadata(oldConfig: OldPrototypeConfig) {
component.nestingRule = nestingRule;
// 未考虑清楚的,放在实验性段落
const experimental: any = {};
const experimental: any = {
isAbsoluteLayoutContainer,
};
if (context) {
// for prototype.getContextInfo
experimental.context = context;
@ -724,6 +730,9 @@ export function upgradeMetadata(oldConfig: OldPrototypeConfig) {
if (view) {
experimental.view = view;
}
if (isTopFixed) {
experimental.isTopFixed = isTopFixed;
}
if (transducers) {
// Array<{ toStatic, toNative }>
// ? only twice

View File

@ -64,16 +64,20 @@ export class Bus {
const bus = new Bus();
editor.on('hotkey.callback.call', (data) => {
editor?.on('hotkey.callback.call', (data) => {
bus.emit('ve.hotkey.callback.call', data);
});
editor.on('history.back', (data) => {
editor?.on('history.back', (data) => {
bus.emit('ve.history.back', data);
});
editor.on('history.forward', (data) => {
editor?.on('history.forward', (data) => {
bus.emit('ve.history.forward', data);
});
editor?.on('node.prop.change', (data) => {
bus.emit('node.prop.change', data);
});
export default bus;

View File

@ -2,18 +2,27 @@ import Env from './env';
import { isJSSlot, isI18nData, isJSExpression } from '@ali/lowcode-types';
import { isPlainObject } from '@ali/lowcode-utils';
import i18nUtil from './i18n-util';
function isVariable(obj: any) {
return obj && obj.type === 'variable';
}
import { editor } from './editor';
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) {

View File

@ -1,10 +1,8 @@
import { isJSBlock, isJSExpression, isJSSlot, isI18nData } from '@ali/lowcode-types';
import { isPlainObject, hasOwnProperty } from '@ali/lowcode-utils';
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 } from '@ali/lowcode-designer';
import Outline, { OutlineBackupPane, getTreeMaster } from '@ali/lowcode-plugin-outline-pane';
import { toCss } from '@ali/vu-css-style';
import logger from '@ali/vu-logger';
import bus from './bus';
import { VE_EVENTS } from './base/const';
@ -13,6 +11,17 @@ import { Skeleton, SettingsPrimaryPane, registerDefaults } from '@ali/lowcode-ed
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';
export const editor = new Editor();
globalContext.register(editor, Editor);
@ -27,251 +36,39 @@ editor.set(Designer, designer);
editor.set('designer', designer);
designer.project.onCurrentDocumentChange((doc) => {
doc.onRendererReady(() => {
bus.emit(VE_EVENTS.VE_PAGE_PAGE_READY);
});
bus.emit(VE_EVENTS.VE_PAGE_PAGE_READY);
editor.set('currentDocument', doc);
});
interface Variable {
type: 'variable';
variable: string;
value: any;
}
function isVariable(obj: any): obj is Variable {
return obj && obj.type === 'variable';
}
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;
}
// 升级 Props
designer.addPropsReducer(upgradePropsReducer, TransformStage.Upgrade);
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;
}
// 节点 props 初始化
designer.addPropsReducer((props, node) => {
// run initials
const newProps: any = {
...props,
};
if (newProps.fieldId) {
const fieldIds = getCurrentFieldIds();
designer.addPropsReducer(initNodeReducer, TransformStage.Init);
// 全局的关闭 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;
}
} 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);
}
});
}
return newProps;
}, TransformStage.Init);
designer.addPropsReducer(liveLifecycleReducer, TransformStage.Render);
designer.addPropsReducer((props: any, node: Node) => {
if (node.isRoot() && props && props.lifeCycles) {
return {
...props,
lifeCycles: {},
};
}
return props;
}, TransformStage.Render);
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;
}
designer.addPropsReducer(filterReducer, TransformStage.Save);
designer.addPropsReducer(filterReducer, TransformStage.Render);
function compatiableReducer(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] = compatiableReducer(val);
});
return newProps;
}
// FIXME: Dirty fix, will remove this reducer
designer.addPropsReducer(compatiableReducer, TransformStage.Save);
designer.addPropsReducer(compatibleReducer, TransformStage.Save);
// 兼容历史版本的 Page 组件
designer.addPropsReducer((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;
}, TransformStage.Save);
designer.addPropsReducer(upgradePageLifeCyclesReducer, TransformStage.Save);
// 设计器组件样式处理
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}`)));
}
}
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);
skeleton.add({
area: 'mainArea',
name: 'designer',
@ -283,6 +80,9 @@ skeleton.add({
name: 'settingsPane',
type: 'Panel',
content: SettingsPrimaryPane,
props: {
ignoreRoot: true,
},
});
skeleton.add({
area: 'leftArea',

View File

@ -80,6 +80,11 @@ export class Env {
};
}
clear() {
this.envs = {};
this.featureMap = {};
}
getAliSchemaVersion() {
return ALI_SCHEMA_VERSION;
}

View File

@ -108,14 +108,14 @@ export class Flags {
return;
}
const doe = document.documentElement;
const doc = document.documentElement;
if (this.lastFlags) {
this.lastFlags.filter((flag: string) => this.flags.indexOf(flag) < 0).forEach((flag) => {
doe.classList.remove(`engine-${flag}`);
doc.classList.remove(`engine-${flag}`);
});
}
this.flags.forEach((flag) => {
doe.classList.add(`engine-${flag}`);
doc.classList.add(`engine-${flag}`);
});
this.lastFlags = this.flags.slice(0);

View File

@ -5,6 +5,7 @@ import logger from '@ali/vu-logger';
import { render } from 'react-dom';
import I18nUtil from './i18n-util';
import { hotkey as Hotkey, monitor } from '@ali/lowcode-editor-core';
import { registerMetadataTransducer } from '@ali/lowcode-designer';
import { createElement } from 'react';
import { VE_EVENTS as EVENTS, VE_HOOKS as HOOKS, VERSION as Version } from './base/const';
import Bus from './bus';
@ -14,6 +15,7 @@ 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';
@ -22,10 +24,11 @@ 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 '@ali/lowcode-editor-setters';
import './vision.less';
@ -60,6 +63,7 @@ const ui = {
const modules = {
VisualManager,
VisualDesigner,
I18nUtil,
Prop,
};
@ -107,6 +111,8 @@ const VisualEngine = {
Project,
logger,
Symbols,
registerMetadataTransducer,
// Flags,
};
(window as any).VisualEngine = VisualEngine;
@ -156,12 +162,18 @@ export {
Project,
logger,
Symbols,
registerMetadataTransducer,
};
const version = '6.0.0(LowcodeEngine 0.9.3)';
const version = '6.0.0 (LowcodeEngine 0.9.32)';
console.log(
`%c VisionEngine %c v${version} `,
<<<<<<< HEAD
'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;',
=======
'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;',
>>>>>>> origin/refactor/vision-code-split
);

View File

@ -32,27 +32,46 @@ const pages = Object.assign(project, {
if (!pages || !Array.isArray(pages) || pages.length === 0) {
throw new Error('pages schema 不合法');
}
let componentsTree: any;
if (isPageDataV1(pages[0])) {
componentsTree = [pages[0].layout];
// 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 {
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 = {};
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,
);
@ -105,6 +124,9 @@ Object.defineProperty(pages, 'currentPage', {
get() {
return project.currentDocument;
},
set(_currentPage) {
// do nothing
},
});
pages.onCurrentPageChange((page: DocumentModel) => {

View File

@ -47,6 +47,8 @@ export interface OldPaneConfig {
index?: number; // todo
isAction?: boolean; // as normal dock
fullScreen?: boolean; // todo
canSetFixed?: boolean; // 是否可以设置固定模式
defaultFixed?: boolean; // 是否默认固定
}
function upgradeConfig(config: OldPaneConfig): IWidgetBaseConfig & { area: string } {
@ -64,11 +66,24 @@ function upgradeConfig(config: OldPaneConfig): IWidgetBaseConfig & { area: strin
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 } = config;
const {
contents,
hideTitleBar,
tip,
width,
maxWidth,
height,
maxHeight,
menu,
isAction,
canSetFixed,
defaultFixed,
} = config;
if (menu) {
newConfig.props.title = menu;
}
@ -84,10 +99,12 @@ function upgradeConfig(config: OldPaneConfig): IWidgetBaseConfig & { area: strin
height,
maxHeight,
canSetFixed,
onInit: init,
onDestroy: destroy,
};
if (defaultFixed) {
newConfig.panelProps.area = 'leftFixedArea';
}
if (contents && Array.isArray(contents)) {
newConfig.content = contents.map(({ title, content, tip }, index) => {
return {
@ -103,23 +120,21 @@ function upgradeConfig(config: OldPaneConfig): IWidgetBaseConfig & { area: strin
});
}
}
} 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.props.onInit = init;
newConfig.props.onDestroy = destroy;
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.area = 'main';
newConfig.type = 'Widget';
}
newConfig.props.onInit = init;
newConfig.props.onDestroy = destroy;
return newConfig;
}

View File

@ -1,17 +1,19 @@
export class Project {
private schema: any;
import { designer } from './editor';
constructor() {
this.schema = {};
}
const { project } = designer;
getSchema() {
return this.schema;
}
Object.assign(project, {
getSchema(): any {
return this.schema || {};
},
setSchema(schema: any) {
this.schema = schema;
}
}
},
export default new Project();
setConfig(config: any) {
this.set('config', config);
},
});
export default project;

View File

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

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