mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-04-20 04:18:05 +00:00
refactor: remove useless codes
This commit is contained in:
parent
1f1da44199
commit
8510f998fe
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
@ -621,7 +621,7 @@ component
|
|||||||
| docUrl | 组件文档链接 | String | 否 |
|
| docUrl | 组件文档链接 | String | 否 |
|
||||||
| screenshot | 组件快照 | String | 否 |
|
| screenshot | 组件快照 | String | 否 |
|
||||||
| icon | 组件的小图标 | String (URL) | 是 |
|
| icon | 组件的小图标 | String (URL) | 是 |
|
||||||
| tags | 组件标签 | String | 是 |
|
| tags | 组件标签 | String[] | 是 |
|
||||||
| keywords | 组件关键词,用于搜索联想 | String | 是 |
|
| keywords | 组件关键词,用于搜索联想 | String | 是 |
|
||||||
| devMode | 组件研发模式 | String (proCode,lowCode) | 是 |
|
| devMode | 组件研发模式 | String (proCode,lowCode) | 是 |
|
||||||
| npm | npm 源引入完整描述对象 | Object | 否 |
|
| npm | npm 源引入完整描述对象 | Object | 否 |
|
||||||
@ -634,7 +634,7 @@ component
|
|||||||
| snippets | 内容为组件不同状态下的低代码 schema (可以有多个),用户从组件面板拖入组件到设计器时会向页面 schema 中插入 snippets 中定义的组件低代码 schema | Object[] | 否 |
|
| snippets | 内容为组件不同状态下的低代码 schema (可以有多个),用户从组件面板拖入组件到设计器时会向页面 schema 中插入 snippets 中定义的组件低代码 schema | Object[] | 否 |
|
||||||
| group | 用于描述当前组件位于组件面板的哪个 tab | String | 否 |
|
| group | 用于描述当前组件位于组件面板的哪个 tab | String | 否 |
|
||||||
| category | 用于描述组件位于组件面板同一 tab 的哪个区域 | String | 否 |
|
| category | 用于描述组件位于组件面板同一 tab 的哪个区域 | String | 否 |
|
||||||
| priority | 用于描述组件在同一 category 中的排序 | String | 否 |
|
| priority | 用于描述组件在同一 category 中的排序 | number | 否 |
|
||||||
|
|
||||||
##### 2.2.2.3 组件属性信息 props (A)
|
##### 2.2.2.3 组件属性信息 props (A)
|
||||||
|
|
||||||
@ -1177,132 +1177,6 @@ export interface ComponentDescription { // 组件描述协议,通过 npm 中
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2.2.3 资产包协议
|
|
||||||
|
|
||||||
##### 2.2.3.1 协议结构
|
|
||||||
|
|
||||||
协议最顶层结构如下,包含 5 方面的描述内容:
|
|
||||||
|
|
||||||
- version { String } 当前协议版本号
|
|
||||||
- packages{ Array } 低代码编辑器中加载的资源列表
|
|
||||||
- components { Array } 所有组件的描述协议列表
|
|
||||||
- sort { Object } 用于描述组件面板中的 tab 和 category
|
|
||||||
|
|
||||||
##### 2.2.3.2 version (A)
|
|
||||||
|
|
||||||
定义当前协议 schema 的版本号;
|
|
||||||
|
|
||||||
| 根属性名称 | 类型 | 说明 | 变量支持 | 默认值 |
|
|
||||||
| ---------- | ------ | ---------- | -------- | ------ |
|
|
||||||
| version | String | 协议版本号 | - | 1.0.0 |
|
|
||||||
|
|
||||||
##### 2.2.3.3 packages (A)
|
|
||||||
|
|
||||||
定义低代码编辑器中加载的资源列表,包含公共库和组件 (库) cdn 资源等;
|
|
||||||
|
|
||||||
| 字段 | 字段描述 | 字段类型 | 备注 |
|
|
||||||
| ----------------------- | --------------------------------------------------- | --------------- | -------------------------------- |
|
|
||||||
| packages[].title? (A) | 资源标题 | String | 资源标题 |
|
|
||||||
| packages[].package (A) | npm 包名 | String | 组件资源唯一标识 |
|
|
||||||
| packages[].version(A) | npm 包版本号 | String | 组件资源版本号 |
|
|
||||||
| packages[].library(A) | 作为全局变量引用时的名称,用来定义全局变量名 | String | 低代码引擎通过该字段获取组件实例 |
|
|
||||||
| packages[].editUrls (A) | 组件编辑态视图打包后的 CDN url 列表,包含 js 和 css | Array\<String\> | 低代码引擎编辑器会加载这些 url |
|
|
||||||
| packages[].urls (AA) | 组件渲染态视图打包后的 CDN url 列表,包含 js 和 css | Array\<String\> | 低代码引擎渲染模块会加载这些 url |
|
|
||||||
|
|
||||||
描述举例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"packages": [
|
|
||||||
{
|
|
||||||
"package": "moment",
|
|
||||||
"version": "2.24.0",
|
|
||||||
"urls": ["https://g.alicdn.com/mylib/moment/2.24.0/min/moment.min.js"],
|
|
||||||
"library": "moment"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "lodash",
|
|
||||||
"library": "_",
|
|
||||||
"urls": ["https://g.alicdn.com/platform/c/lodash/4.6.1/lodash.min.js"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "fusion 组件库",
|
|
||||||
"package": "@alifd/next",
|
|
||||||
"version": "1.24.18",
|
|
||||||
"urls": [
|
|
||||||
"https://g.alicdn.com/code/lib/alifd__next/1.24.18/next.min.css",
|
|
||||||
"https://g.alicdn.com/code/lib/alifd__next/1.24.18/next-with-locales.min.js"
|
|
||||||
],
|
|
||||||
"library": "Next"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "@alilc/lowcode-materials",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"library": "AlilcLowcodeMaterials",
|
|
||||||
"urls": [
|
|
||||||
"https://alifd.alicdn.com/npm/@alilc/lowcode-materials@1.0.0/dist/AlilcLowcodeMaterials.js",
|
|
||||||
"https://alifd.alicdn.com/npm/@alilc/lowcode-materials@1.0.0/dist/AlilcLowcodeMaterials.css"
|
|
||||||
],
|
|
||||||
"editUrls": [
|
|
||||||
"https://alifd.alicdn.com/npm/@alilc/lowcode-materials@1.0.0/build/lowcode/view.js",
|
|
||||||
"https://alifd.alicdn.com/npm/@alilc/lowcode-materials@1.0.0/build/lowcode/view.css"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "@alifd/fusion-ui",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"library": "AlifdFusionUi",
|
|
||||||
"urls": [
|
|
||||||
"https://alifd.alicdn.com/npm/@alifd/fusion-ui@1.0.0/build/lowcode/view.js",
|
|
||||||
"https://alifd.alicdn.com/npm/@alifd/fusion-ui@1.0.0/build/lowcode/view.css"
|
|
||||||
],
|
|
||||||
"editUrls": [
|
|
||||||
"https://alifd.alicdn.com/npm/@alifd/fusion-ui@1.0.0/build/lowcode/view.js",
|
|
||||||
"https://alifd.alicdn.com/npm/@alifd/fusion-ui@1.0.0/build/lowcode/view.css"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### 2.2.3.4 components (A)
|
|
||||||
|
|
||||||
定义所有组件的描述协议列表,组件描述协议遵循本规范章节 2.2.2 的定义;
|
|
||||||
|
|
||||||
##### 2.2.3.5 sort (A)
|
|
||||||
|
|
||||||
定义组件列表分组
|
|
||||||
|
|
||||||
| 根属性名称 | 类型 | 说明 | 变量支持 | 默认值 |
|
|
||||||
| ----------------- | -------- | -------------------------------------------------------------------------------------------- | -------- | ---------------------------------------- |
|
|
||||||
| sort.groupList | String[] | 组件分组,用于组件面板 tab 展示 | - | ['精选组件', '原子组件'] |
|
|
||||||
| sort.categoryList | String[] | 组件面板中同一个 tab 下的不同区间用 category 区分,category 的排序依照 categoryList 顺序排列 | - | ['通用', '数据展示', '表格类', '表单类'] |
|
|
||||||
|
|
||||||
##### 2.2.3.6 TypeScript 定义
|
|
||||||
|
|
||||||
```TypeScript
|
|
||||||
export interface ComponentSort {
|
|
||||||
groupList?: String[]; // 用于描述组件面板的 tab 项及其排序,例如:["精选组件", "原子组件"]
|
|
||||||
categoryList?: String[]; // 组件面板中同一个 tab 下的不同区间用 category 区分,category 的排序依照 categoryList 顺序排列;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Assets {
|
|
||||||
version: string; // 资产包协议版本号
|
|
||||||
packages?: Array<Package>; // 大包列表,external与package的概念相似,融合在一起
|
|
||||||
components: Array<ComponentDescription> | Array<RemoteComponentDescription>; // 所有组件的描述协议列表
|
|
||||||
componentList?: ComponentCategory[]; // 【待废弃】组件分类列表,用来描述物料面板
|
|
||||||
sort: ComponentSort; // 新增字段,用于描述组件面板中的 tab 和 category
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RemoteComponentDescription {
|
|
||||||
exportName: string; // 组件描述导出名字,可以通过 window[exportName] 获取到组件描述的 Object 内容;
|
|
||||||
url: string; // 组件描述的资源链接;
|
|
||||||
package: { // 组件(库)的 npm 信息;
|
|
||||||
npm: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3 物料规范 - 区块规范
|
## 3 物料规范 - 区块规范
|
||||||
|
|
||||||
### 3.1 源码规范
|
### 3.1 源码规范
|
||||||
@ -1313,10 +1187,11 @@ export interface RemoteComponentDescription {
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
block/ ├── build │ ├── index.css // 【编译生成】 │ ├── index.html //
|
block/ ├── build │ ├── index.css // 【编译生成】 │ ├── index.html //
|
||||||
【编译生成】【必选】可直接预览文件 │ ├── index.js // 【编译生成】 │ └── views // 【3A
|
【编译生成】【必选】可直接预览文件 │ ├── index.js // 【编译生成】 │ └──
|
||||||
编译生成】html2sketch │ ├── block_view1.html // 【3A 编译生成】给 sketch 用的 html │ └──
|
views // 【3A 编译生成】html2sketch │ ├── block_view1.html // 【3A
|
||||||
block_view1.png // 【3A 编译生成】截图 ├── src // 【必选】区块源码 │ ├── index.jsx // 【必选】入口 │
|
编译生成】给 sketch 用的 html │ └── block_view1.png // 【3A 编译生成】截图 ├──
|
||||||
└── index.module.scss // 【可选】如有样式请使用 CSS Modules 避免冲突 ├── README.md //
|
src // 【必选】区块源码 │ ├── index.jsx // 【必选】入口 │ └── index.module.scss
|
||||||
|
// 【可选】如有样式请使用 CSS Modules 避免冲突 ├── README.md //
|
||||||
【可选】无格式要求 └── package.json // 【必选】
|
【可选】无格式要求 └── package.json // 【必选】
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -1471,23 +1346,28 @@ block_view1.png // 【3A 编译生成】截图 ├── src // 【必选】区
|
|||||||
与标准源码 build-scripts 对齐
|
与标准源码 build-scripts 对齐
|
||||||
|
|
||||||
```html
|
```html
|
||||||
├── META/ # 低代码元数据信息,用于多分支冲突解决、数据回滚等功能 ├── build │ ├── index.css #
|
├── META/ # 低代码元数据信息,用于多分支冲突解决、数据回滚等功能 ├── build │
|
||||||
【编译生成】 │ ├── index.html # 【编译生成】【必选】可直接预览文件 │ ├── index.js # 【编译生成】
|
├── index.css # 【编译生成】 │ ├── index.html #
|
||||||
│ └── views # 【3A 编译生成】html2sketch │ ├── page1.html # 【3A 编译生成】给 sketch 用的 html
|
【编译生成】【必选】可直接预览文件 │ ├── index.js # 【编译生成】 │ └── views
|
||||||
│ └── page1.png # 【3A 编译生成】截图 ├── public/ # 静态文件,构建时会 copy 到 build/ 目录 │ ├──
|
# 【3A 编译生成】html2sketch │ ├── page1.html # 【3A 编译生成】给 sketch 用的
|
||||||
index.html # 应用入口 HTML │ └── favicon.png # Favicon ├── src/ │ ├── components/ #
|
html │ └── page1.png # 【3A 编译生成】截图 ├── public/ # 静态文件,构建时会
|
||||||
应用内的低代码业务组件 │ │ └── GuideComponent/ │ │ ├── index.js # 组件入口 │ │ ├── components.js #
|
copy 到 build/ 目录 │ ├── index.html # 应用入口 HTML │ └── favicon.png # Favicon
|
||||||
组件依赖的其他组件 │ │ ├── schema.js # schema 描述 │ │ └── index.scss # css 样式 │ ├── pages/ # 页面
|
├── src/ │ ├── components/ # 应用内的低代码业务组件 │ │ └── GuideComponent/ │ │
|
||||||
│ │ └── HomePage/ # Home 页面 │ │ ├── index.js # 页面入口 │ │ └── index.scss # css 样式 │ ├──
|
├── index.js # 组件入口 │ │ ├── components.js # 组件依赖的其他组件 │ │ ├──
|
||||||
layouts/ │ │ └── BasicLayout/ # layout 组件名称 │ │ ├── index.js # layout 入口 │ │ ├── components.js
|
schema.js # schema 描述 │ │ └── index.scss # css 样式 │ ├── pages/ # 页面 │ │
|
||||||
# layout 组件依赖的其他组件 │ │ ├── schema.js # layout schema 描述 │ │ └── index.scss # layout css
|
└── HomePage/ # Home 页面 │ │ ├── index.js # 页面入口 │ │ └── index.scss # css
|
||||||
样式 │ ├── config/ # 配置信息 │ │ ├── components.js # 应用上下文所有组件 │ │ ├── routes.js #
|
样式 │ ├── layouts/ │ │ └── BasicLayout/ # layout 组件名称 │ │ ├── index.js #
|
||||||
页面路由列表 │ │ └── constants.js # 全局常量定义 │ │ └── app.js # 应用配置文件 │ ├── utils/ # 工具库
|
layout 入口 │ │ ├── components.js # layout 组件依赖的其他组件 │ │ ├── schema.js
|
||||||
│ │ └── index.js # 应用第三方扩展函数 │ ├── stores/ # [可选] 全局状态管理 │ │ └── user.js │ ├──
|
# layout schema 描述 │ │ └── index.scss # layout css 样式 │ ├── config/ #
|
||||||
locales/ # [可选] 国际化资源 │ │ ├── en-US │ │ └── zh-CN │ ├── global.scss # 全局样式 │ └──
|
配置信息 │ │ ├── components.js # 应用上下文所有组件 │ │ ├── routes.js #
|
||||||
index.jsx # 应用入口脚本,依赖 config/routes.js 的路由配置动态生成路由; ├── webpack.config.js #
|
页面路由列表 │ │ └── constants.js # 全局常量定义 │ │ └── app.js # 应用配置文件 │
|
||||||
项目工程配置,包含插件配置及自定义 `webpack` 配置等 ├── README.md ├── package.json ├── .editorconfig
|
├── utils/ # 工具库 │ │ └── index.js # 应用第三方扩展函数 │ ├── stores/ # [可选]
|
||||||
├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .stylelintignore └── .stylelintrc.js
|
全局状态管理 │ │ └── user.js │ ├── locales/ # [可选] 国际化资源 │ │ ├── en-US │
|
||||||
|
│ └── zh-CN │ ├── global.scss # 全局样式 │ └── index.jsx # 应用入口脚本,依赖
|
||||||
|
config/routes.js 的路由配置动态生成路由; ├── webpack.config.js #
|
||||||
|
项目工程配置,包含插件配置及自定义 `webpack` 配置等 ├── README.md ├──
|
||||||
|
package.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore
|
||||||
|
├── .stylelintignore └── .stylelintrc.js
|
||||||
```
|
```
|
||||||
|
|
||||||
##### 入口文件
|
##### 入口文件
|
||||||
|
|||||||
@ -27,14 +27,19 @@ export default tseslint.config({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'@stylistic/indent': ['error', 2],
|
|
||||||
'@stylistic/indent-binary-ops': ['error', 2],
|
|
||||||
'@stylistic/max-len': [
|
'@stylistic/max-len': [
|
||||||
'error',
|
'error',
|
||||||
{ code: 100, tabWidth: 2, ignoreStrings: true, ignoreComments: true, ignoreTemplateLiterals: true }
|
{
|
||||||
|
code: 100,
|
||||||
|
tabWidth: 2,
|
||||||
|
ignoreStrings: true,
|
||||||
|
ignoreComments: true,
|
||||||
|
ignoreTemplateLiterals: true,
|
||||||
|
ignoreRegExpLiterals: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
'@stylistic/no-tabs': 'error',
|
'@stylistic/no-tabs': 'error',
|
||||||
'@stylistic/quotes': ['error', 'single'],
|
'@stylistic/quotes': ['error', 'single', { allowTemplateLiterals: true }],
|
||||||
'@stylistic/quote-props': ['error', 'as-needed'],
|
'@stylistic/quote-props': ['error', 'as-needed'],
|
||||||
'@stylistic/jsx-pascal-case': [2],
|
'@stylistic/jsx-pascal-case': [2],
|
||||||
'@stylistic/jsx-indent': [2, 2, { checkAttributes: true, indentLogicalExpressions: true }],
|
'@stylistic/jsx-indent': [2, 2, { checkAttributes: true, indentLogicalExpressions: true }],
|
||||||
@ -42,7 +47,7 @@ export default tseslint.config({
|
|||||||
'@stylistic/eol-last': ['error', 'always'],
|
'@stylistic/eol-last': ['error', 'always'],
|
||||||
'@stylistic/jsx-quotes': ['error', 'prefer-double'],
|
'@stylistic/jsx-quotes': ['error', 'prefer-double'],
|
||||||
|
|
||||||
'@typescript-eslint/ban-ts-comment': ["error", { 'ts-expect-error': 'allow-with-description' }],
|
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-expect-error': 'allow-with-description' }],
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
|
||||||
'react/jsx-no-undef': 'error',
|
'react/jsx-no-undef': 'error',
|
||||||
@ -53,6 +58,8 @@ export default tseslint.config({
|
|||||||
'react/no-children-prop': 'warn',
|
'react/no-children-prop': 'warn',
|
||||||
|
|
||||||
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
|
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
|
||||||
'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies
|
'react-hooks/exhaustive-deps': 'off', // Checks effect dependencies
|
||||||
|
|
||||||
|
'no-inner-declarations': 'off',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -31,6 +31,7 @@
|
|||||||
"@stylistic/eslint-plugin": "^1.7.0",
|
"@stylistic/eslint-plugin": "^1.7.0",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
"@types/react-router": "5.1.18",
|
"@types/react-router": "5.1.18",
|
||||||
|
"@vanilla-extract/vite-plugin": "^4.0.7",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-react": "^7.34.1",
|
"eslint-plugin-react": "^7.34.1",
|
||||||
@ -39,12 +40,12 @@
|
|||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"less": "^4.2.0",
|
"less": "^4.2.0",
|
||||||
"lint-staged": "^15.2.2",
|
"lint-staged": "^15.2.2",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
"rimraf": "^5.0.2",
|
"rimraf": "^5.0.2",
|
||||||
"rollup": "^4.13.0",
|
|
||||||
"typescript": "^5.4.2",
|
"typescript": "^5.4.2",
|
||||||
"typescript-eslint": "^7.5.0",
|
"typescript-eslint": "^7.5.0",
|
||||||
"vite": "^5.1.6",
|
"vite": "^5.2.9",
|
||||||
"vitest": "^1.3.1"
|
"vitest": "^1.5.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0",
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0",
|
||||||
|
|||||||
36
packages/core/__tests__/instantiation.spec.ts
Normal file
36
packages/core/__tests__/instantiation.spec.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { it, describe, expect } from 'vitest';
|
||||||
|
import { lazyInject, provide, initInstantiation } from '../src/instantiation';
|
||||||
|
|
||||||
|
interface Warrior {
|
||||||
|
fight(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Weapon {
|
||||||
|
hit(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@provide(Katana)
|
||||||
|
class Katana implements Weapon {
|
||||||
|
public hit() {
|
||||||
|
return 'cut!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@provide(Ninja)
|
||||||
|
class Ninja implements Warrior {
|
||||||
|
@lazyInject(Katana)
|
||||||
|
private _katana: Weapon;
|
||||||
|
|
||||||
|
public fight() {
|
||||||
|
return this._katana.hit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initInstantiation();
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
it('works', () => {
|
||||||
|
const n = new Ninja();
|
||||||
|
expect(n.fight()).toBe('cut!');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@alilc/lowcode-editor-core",
|
"name": "@alilc/lowcode-core",
|
||||||
"version": "2.0.0-beta.0",
|
"version": "2.0.0-beta.0",
|
||||||
"description": "Core Api for Ali lowCode engine",
|
"description": "Core Api for Ali lowCode engine",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -33,31 +33,27 @@
|
|||||||
"test:cov": ""
|
"test:cov": ""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alifd/next": "^1.27.8",
|
"@abraham/reflection": "^0.12.0",
|
||||||
"@alilc/lowcode-shared": "workspace:*",
|
"@alilc/lowcode-shared": "workspace:*",
|
||||||
"classnames": "^2.5.1",
|
"@alilc/lowcode-types": "workspace:*",
|
||||||
"intl-messageformat": "^10.5.1",
|
"@alilc/lowcode-utils": "workspace:*",
|
||||||
|
"@formatjs/intl": "^2.10.1",
|
||||||
|
"inversify": "^6.0.2",
|
||||||
|
"inversify-binding-decorators": "^4.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mobx": "^6.12.0",
|
|
||||||
"mobx-react": "^9.1.0",
|
|
||||||
"power-di": "^2.4.4",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"semver": "^7.6.0",
|
|
||||||
"store": "^2.0.12",
|
|
||||||
"events": "^3.3.0"
|
"events": "^3.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
"@types/react-dom": "^18.2.0",
|
"@types/react-dom": "^18.2.0"
|
||||||
"@types/store": "^2.0.2",
|
|
||||||
"@types/semver": "^7.5.8",
|
|
||||||
"less": "^4.2.0"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@alifd/next": "^1.27.8",
|
|
||||||
"@alilc/lowcode-shared": "workspace:*",
|
"@alilc/lowcode-shared": "workspace:*",
|
||||||
|
"@alilc/lowcode-types": "workspace:*",
|
||||||
|
"@alilc/lowcode-utils": "workspace:*",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
50
packages/core/src/command.ts
Normal file
50
packages/core/src/command.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
export interface Command {
|
||||||
|
/**
|
||||||
|
* 命令名称
|
||||||
|
* 命名规则:commandName
|
||||||
|
* 使用规则:commandScope:commandName (commandScope 在插件 meta 中定义,用于区分不同插件的命令)
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 命令描述
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 命令处理函数
|
||||||
|
*/
|
||||||
|
handler: (...args: any[]) => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Commands {
|
||||||
|
/**
|
||||||
|
* 注册一个新命令及其处理函数
|
||||||
|
*/
|
||||||
|
registerCommand(command: Command): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注销一个已存在的命令
|
||||||
|
*/
|
||||||
|
unregisterCommand(name: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过名称和给定参数执行一个命令,会校验参数是否符合命令定义
|
||||||
|
*/
|
||||||
|
executeCommand(name: string, args?: IPublicTypeCommandHandlerArgs): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量执行命令,执行完所有命令后再进行一次重绘,历史记录中只会记录一次
|
||||||
|
*/
|
||||||
|
batchExecuteCommand(commands: { name: string; args?: IPublicTypeCommandHandlerArgs }[]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出所有已注册的命令
|
||||||
|
*/
|
||||||
|
listCommands(): IPublicTypeListCommand[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册错误处理回调函数
|
||||||
|
*/
|
||||||
|
onCommandError(callback: (name: string, error: Error) => void): void;
|
||||||
|
}
|
||||||
174
packages/core/src/configuration/config.ts
Normal file
174
packages/core/src/configuration/config.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import { get as lodashGet, isPlainObject } from 'lodash-es';
|
||||||
|
import { createLogger, type PlainObject, invariant } from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
|
const logger = createLogger({ level: 'log', bizName: 'config' });
|
||||||
|
|
||||||
|
// this default behavior will be different later
|
||||||
|
const STRICT_PLUGIN_MODE_DEFAULT = true;
|
||||||
|
|
||||||
|
interface ConfigurationOptions<Config extends PlainObject, K extends keyof Config = keyof Config> {
|
||||||
|
strictMode?: boolean;
|
||||||
|
setterValidator?: (key: K, value: Config[K]) => boolean | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Configuration<Config extends PlainObject, K extends keyof Config = keyof Config> {
|
||||||
|
#strictMode = STRICT_PLUGIN_MODE_DEFAULT;
|
||||||
|
#setterValidator: (key: K, value: Config[K]) => boolean | string = () => true;
|
||||||
|
|
||||||
|
#config: Config = {} as Config;
|
||||||
|
|
||||||
|
#waits = new Map<
|
||||||
|
K,
|
||||||
|
{
|
||||||
|
once?: boolean;
|
||||||
|
resolve: (data: any) => void;
|
||||||
|
}[]
|
||||||
|
>();
|
||||||
|
|
||||||
|
constructor(config: Config, options?: ConfigurationOptions<Config>) {
|
||||||
|
invariant(config, 'config must exist', 'Configuration');
|
||||||
|
|
||||||
|
this.#config = config;
|
||||||
|
|
||||||
|
const { strictMode, setterValidator } = options ?? {};
|
||||||
|
|
||||||
|
if (strictMode === false) {
|
||||||
|
this.#strictMode = false;
|
||||||
|
}
|
||||||
|
if (setterValidator) {
|
||||||
|
invariant(
|
||||||
|
typeof setterValidator === 'function',
|
||||||
|
'setterValidator must be a function',
|
||||||
|
'Configuration',
|
||||||
|
);
|
||||||
|
this.#setterValidator = setterValidator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断指定 key 是否有值
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
has(key: K): boolean {
|
||||||
|
return this.#config[key] !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定 key 的值
|
||||||
|
* @param key
|
||||||
|
* @param defaultValue
|
||||||
|
*/
|
||||||
|
get(key: K, defaultValue?: any): any {
|
||||||
|
return lodashGet(this.#config, key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置指定 key 的值
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
set(key: K, value: any) {
|
||||||
|
if (this.#strictMode) {
|
||||||
|
const valid = this.#setterValidator(key, value);
|
||||||
|
if (valid === false || typeof valid === 'string') {
|
||||||
|
return logger.warn(
|
||||||
|
`failed to config ${key.toString()}, only predefined options can be set under strict mode, predefined options: `,
|
||||||
|
valid ? valid : '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#config[key] = value;
|
||||||
|
this.notifyGot(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设值,set 的对象版本
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
setConfig(config: Partial<Config>) {
|
||||||
|
if (isPlainObject(config)) {
|
||||||
|
Object.keys(config).forEach((key) => {
|
||||||
|
this.set(key as K, config[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定 key 的值,若此时还未赋值,则等待,若已有值,则直接返回值
|
||||||
|
* 注:此函数返回 Promise 实例,只会执行(fullfill)一次
|
||||||
|
* @param key
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
onceGot(key: K) {
|
||||||
|
const val = this.#config[key];
|
||||||
|
if (val !== undefined) {
|
||||||
|
return Promise.resolve(val);
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.setWait(key, resolve, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定 key 的值,函数回调模式,若多次被赋值,回调会被多次调用
|
||||||
|
* @param key
|
||||||
|
* @param fn
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
onGot(key: K, fn: (data: Config[K]) => void): () => void {
|
||||||
|
const val = this.#config[key];
|
||||||
|
if (val !== undefined) {
|
||||||
|
fn(val);
|
||||||
|
}
|
||||||
|
this.setWait(key, fn);
|
||||||
|
return () => {
|
||||||
|
this.delWait(key, fn);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyGot(key: K): void {
|
||||||
|
let waits = this.#waits.get(key);
|
||||||
|
if (!waits) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
waits = waits.slice().reverse();
|
||||||
|
let i = waits.length;
|
||||||
|
while (i--) {
|
||||||
|
waits[i].resolve(this.get(key));
|
||||||
|
if (waits[i].once) {
|
||||||
|
waits.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (waits.length > 0) {
|
||||||
|
this.#waits.set(key, waits);
|
||||||
|
} else {
|
||||||
|
this.#waits.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setWait(key: K, resolve: (data: any) => void, once?: boolean) {
|
||||||
|
const waits = this.#waits.get(key);
|
||||||
|
if (waits) {
|
||||||
|
waits.push({ resolve, once });
|
||||||
|
} else {
|
||||||
|
this.#waits.set(key, [{ resolve, once }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delWait(key: K, fn: any) {
|
||||||
|
const waits = this.#waits.get(key);
|
||||||
|
if (!waits) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let i = waits.length;
|
||||||
|
while (i--) {
|
||||||
|
if (waits[i].resolve === fn) {
|
||||||
|
waits.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (waits.length < 1) {
|
||||||
|
this.#waits.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
packages/core/src/configuration/index.ts
Normal file
2
packages/core/src/configuration/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './config';
|
||||||
|
export { Preference, userPreference } from './preference';
|
||||||
@ -1,14 +1,17 @@
|
|||||||
import store from 'store';
|
import store from 'store';
|
||||||
import { createLogger } from '@alilc/lowcode-utils';
|
import { createLogger } from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
const logger = createLogger({ level: 'warn', bizName: 'Preference' });
|
const logger = createLogger({ level: 'warn', bizName: 'Preference' });
|
||||||
|
|
||||||
const STORAGE_KEY_PREFIX = 'ale';
|
const STORAGE_KEY_PREFIX = 'ale';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* used to store user preferences, such as pinned status of a pannel.
|
* used to store user preferences, such as pinned status of a pannel.
|
||||||
* save to local storage.
|
* save to local storage.
|
||||||
*/
|
*/
|
||||||
export default class Preference {
|
export class Preference {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
private getStorageKey(key: string, module?: string): string {
|
private getStorageKey(key: string, module?: string): string {
|
||||||
const moduleKey = module || '__inner__';
|
const moduleKey = module || '__inner__';
|
||||||
return `${STORAGE_KEY_PREFIX}_${moduleKey}.${key}`;
|
return `${STORAGE_KEY_PREFIX}_${moduleKey}.${key}`;
|
||||||
@ -58,3 +61,5 @@ export default class Preference {
|
|||||||
return !(result === undefined || result === null);
|
return !(result === undefined || result === null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const userPreference = new Preference();
|
||||||
@ -1,88 +1,41 @@
|
|||||||
/**
|
/**
|
||||||
* key event helper:https://www.toptal.com/developers/keycode
|
* key event helper:https://www.toptal.com/developers/keycode
|
||||||
|
* key code table: https://www.toptal.com/developers/keycode/table
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { globalContext } from './di';
|
import { type EventDisposable, Platform } from '@alilc/lowcode-shared';
|
||||||
import {
|
|
||||||
IPublicTypeHotkeyCallback,
|
|
||||||
IPublicTypeHotkeyCallbackConfig,
|
|
||||||
IPublicTypeHotkeyCallbacks,
|
|
||||||
IPublicApiHotkey,
|
|
||||||
IPublicTypeDisposable,
|
|
||||||
} from '@alilc/lowcode-types';
|
|
||||||
|
|
||||||
interface KeyMap {
|
type KeyboardEventKeyMapping = Record<string, string>;
|
||||||
[key: number]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CtrlKeyMap {
|
interface KeyboardEventLike {
|
||||||
[key: string]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActionEvent {
|
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HotkeyDirectMap {
|
type KeyAction = 'keypress' | 'keydown' | 'keyup';
|
||||||
[key: string]: IPublicTypeHotkeyCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface KeyInfo {
|
interface KeyInfo {
|
||||||
key: string;
|
key: string;
|
||||||
modifiers: string[];
|
modifiers: string[];
|
||||||
action: string;
|
action: KeyAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SequenceLevels {
|
type SequenceLevels = Record<string, number>;
|
||||||
[key: string]: number;
|
|
||||||
|
export type HotkeyCallback = (e: KeyboardEvent, combo?: string) => void | false | any;
|
||||||
|
|
||||||
|
export interface HotkeyCallbackConfig {
|
||||||
|
callback: HotkeyCallback;
|
||||||
|
modifiers: string[];
|
||||||
|
action: KeyAction;
|
||||||
|
seq?: string;
|
||||||
|
level?: number;
|
||||||
|
combo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAP: KeyMap = {
|
export type HotkeyCallbackConfigRecord = Record<string, HotkeyCallbackConfig[]>;
|
||||||
8: 'backspace',
|
|
||||||
9: 'tab',
|
|
||||||
13: 'enter',
|
|
||||||
16: 'shift',
|
|
||||||
17: 'ctrl',
|
|
||||||
18: 'alt',
|
|
||||||
20: 'capslock',
|
|
||||||
27: 'esc',
|
|
||||||
32: 'space',
|
|
||||||
33: 'pageup',
|
|
||||||
34: 'pagedown',
|
|
||||||
35: 'end',
|
|
||||||
36: 'home',
|
|
||||||
37: 'left',
|
|
||||||
38: 'up',
|
|
||||||
39: 'right',
|
|
||||||
40: 'down',
|
|
||||||
45: 'ins',
|
|
||||||
46: 'del',
|
|
||||||
91: 'meta',
|
|
||||||
93: 'meta',
|
|
||||||
224: 'meta',
|
|
||||||
};
|
|
||||||
|
|
||||||
const KEYCODE_MAP: KeyMap = {
|
const SHIFT_ALTERNATE_KEYS_MAP: KeyboardEventKeyMapping = {
|
||||||
106: '*',
|
|
||||||
107: '+',
|
|
||||||
109: '-',
|
|
||||||
110: '.',
|
|
||||||
111: '/',
|
|
||||||
186: ';',
|
|
||||||
187: '=',
|
|
||||||
188: ',',
|
|
||||||
189: '-',
|
|
||||||
190: '.',
|
|
||||||
191: '/',
|
|
||||||
192: '`',
|
|
||||||
219: '[',
|
|
||||||
220: '\\',
|
|
||||||
221: ']',
|
|
||||||
222: '\'',
|
|
||||||
};
|
|
||||||
|
|
||||||
const SHIFT_MAP: CtrlKeyMap = {
|
|
||||||
'~': '`',
|
'~': '`',
|
||||||
'!': '1',
|
'!': '1',
|
||||||
'@': '2',
|
'@': '2',
|
||||||
@ -97,91 +50,27 @@ const SHIFT_MAP: CtrlKeyMap = {
|
|||||||
_: '-',
|
_: '-',
|
||||||
'+': '=',
|
'+': '=',
|
||||||
':': ';',
|
':': ';',
|
||||||
'"': '\'',
|
// eslint-disable-next-line @stylistic/quotes
|
||||||
|
'"': "'",
|
||||||
'<': ',',
|
'<': ',',
|
||||||
'>': '.',
|
'>': '.',
|
||||||
'?': '/',
|
'?': '/',
|
||||||
'|': '\\',
|
'|': '\\',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SPECIAL_ALIASES: CtrlKeyMap = {
|
const SPECIAL_DEFINED_ALIASES: KeyboardEventKeyMapping = {
|
||||||
option: 'alt',
|
option: 'alt',
|
||||||
command: 'meta',
|
command: 'meta',
|
||||||
return: 'enter',
|
return: 'enter',
|
||||||
escape: 'esc',
|
escape: 'esc',
|
||||||
plus: '+',
|
plus: '+',
|
||||||
mod: /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl',
|
mod: Platform.isIOS ? 'meta' : 'ctrl',
|
||||||
};
|
};
|
||||||
|
|
||||||
let REVERSE_MAP: CtrlKeyMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* loop through the f keys, f1 to f19 and add them to the map
|
|
||||||
* programatically
|
|
||||||
*/
|
|
||||||
for (let i = 1; i < 20; ++i) {
|
|
||||||
MAP[111 + i] = `f${i}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* loop through to map numbers on the numeric keypad
|
|
||||||
*/
|
|
||||||
for (let i = 0; i <= 9; ++i) {
|
|
||||||
MAP[i + 96] = String(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* takes the event and returns the key character
|
|
||||||
*/
|
|
||||||
function characterFromEvent(e: KeyboardEvent): string {
|
|
||||||
const keyCode = e.keyCode || e.which;
|
|
||||||
// for keypress events we should return the character as is
|
|
||||||
if (e.type === 'keypress') {
|
|
||||||
let character = String.fromCharCode(keyCode);
|
|
||||||
// if the shift key is not pressed then it is safe to assume
|
|
||||||
// that we want the character to be lowercase. this means if
|
|
||||||
// you accidentally have caps lock on then your key bindings
|
|
||||||
// will continue to work
|
|
||||||
//
|
|
||||||
// the only side effect that might not be desired is if you
|
|
||||||
// bind something like 'A' cause you want to trigger an
|
|
||||||
// event when capital A is pressed caps lock will no longer
|
|
||||||
// trigger the event. shift+a will though.
|
|
||||||
if (!e.shiftKey) {
|
|
||||||
character = character.toLowerCase();
|
|
||||||
}
|
|
||||||
return character;
|
|
||||||
}
|
|
||||||
// for non keypress events the special maps are needed
|
|
||||||
if (MAP[keyCode]) {
|
|
||||||
return MAP[keyCode];
|
|
||||||
}
|
|
||||||
if (KEYCODE_MAP[keyCode]) {
|
|
||||||
return KEYCODE_MAP[keyCode];
|
|
||||||
}
|
|
||||||
// if it is not in the special map
|
|
||||||
// with keydown and keyup events the character seems to always
|
|
||||||
// come in as an uppercase character whether you are pressing shift
|
|
||||||
// or not. we should make sure it is always lowercase for comparisons
|
|
||||||
// tips: Q29weXJpZ2h0IChjKSAyMDIwLXByZXNlbnQgQWxpYmFiYSBJbmMuIFYy
|
|
||||||
return String.fromCharCode(keyCode).toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
interface KeypressEvent extends KeyboardEvent {
|
interface KeypressEvent extends KeyboardEvent {
|
||||||
type: 'keypress';
|
type: 'keypress';
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPressEvent(e: KeyboardEvent | ActionEvent): e is KeypressEvent {
|
|
||||||
return e.type === 'keypress';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* checks if two arrays are equal
|
|
||||||
*/
|
|
||||||
function modifiersMatch(modifiers1: string[], modifiers2: string[]): boolean {
|
|
||||||
return modifiers1.sort().join(',') === modifiers2.sort().join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* takes a key event and figures out what the modifiers are
|
* takes a key event and figures out what the modifiers are
|
||||||
*/
|
*/
|
||||||
@ -191,15 +80,12 @@ function eventModifiers(e: KeyboardEvent): string[] {
|
|||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
modifiers.push('shift');
|
modifiers.push('shift');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.altKey) {
|
if (e.altKey) {
|
||||||
modifiers.push('alt');
|
modifiers.push('alt');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
modifiers.push('ctrl');
|
modifiers.push('ctrl');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.metaKey) {
|
if (e.metaKey) {
|
||||||
modifiers.push('meta');
|
modifiers.push('meta');
|
||||||
}
|
}
|
||||||
@ -207,188 +93,55 @@ function eventModifiers(e: KeyboardEvent): string[] {
|
|||||||
return modifiers;
|
return modifiers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* determines if the keycode specified is a modifier key or not
|
|
||||||
*/
|
|
||||||
function isModifier(key: string): boolean {
|
|
||||||
return key === 'shift' || key === 'ctrl' || key === 'alt' || key === 'meta';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* reverses the map lookup so that we can look for specific keys
|
|
||||||
* to see what can and can't use keypress
|
|
||||||
*
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
function getReverseMap(): CtrlKeyMap {
|
|
||||||
if (!REVERSE_MAP) {
|
|
||||||
REVERSE_MAP = {};
|
|
||||||
for (const key in MAP) {
|
|
||||||
// pull out the numeric keypad from here cause keypress should
|
|
||||||
// be able to detect the keys from the character
|
|
||||||
if (Number(key) > 95 && Number(key) < 112) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(MAP, key)) {
|
|
||||||
REVERSE_MAP[MAP[key]] = key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return REVERSE_MAP;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* picks the best action based on the key combination
|
|
||||||
*/
|
|
||||||
function pickBestAction(key: string, modifiers: string[], action?: string): string {
|
|
||||||
// if no action was picked in we should try to pick the one
|
|
||||||
// that we think would work best for this key
|
|
||||||
if (!action) {
|
|
||||||
action = getReverseMap()[key] ? 'keydown' : 'keypress';
|
|
||||||
}
|
|
||||||
// modifier keys don't work as expected with keypress,
|
|
||||||
// switch to keydown
|
|
||||||
if (action === 'keypress' && modifiers.length) {
|
|
||||||
action = 'keydown';
|
|
||||||
}
|
|
||||||
return action;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts from a string key combination to an array
|
|
||||||
*
|
|
||||||
* @param {string} combination like "command+shift+l"
|
|
||||||
* @return {Array}
|
|
||||||
*/
|
|
||||||
function keysFromString(combination: string): string[] {
|
|
||||||
if (combination === '+') {
|
|
||||||
return ['+'];
|
|
||||||
}
|
|
||||||
|
|
||||||
combination = combination.replace(/\+{2}/g, '+plus');
|
|
||||||
return combination.split('+');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets info for a specific key combination
|
|
||||||
*
|
|
||||||
* @param combination key combination ("command+s" or "a" or "*")
|
|
||||||
*/
|
|
||||||
function getKeyInfo(combination: string, action?: string): KeyInfo {
|
|
||||||
let keys: string[] = [];
|
|
||||||
let key = '';
|
|
||||||
let i: number;
|
|
||||||
const modifiers: string[] = [];
|
|
||||||
|
|
||||||
// take the keys from this pattern and figure out what the actual
|
|
||||||
// pattern is all about
|
|
||||||
keys = keysFromString(combination);
|
|
||||||
|
|
||||||
for (i = 0; i < keys.length; ++i) {
|
|
||||||
key = keys[i];
|
|
||||||
|
|
||||||
// normalize key names
|
|
||||||
if (SPECIAL_ALIASES[key]) {
|
|
||||||
key = SPECIAL_ALIASES[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
// if this is not a keypress event then we should
|
|
||||||
// be smart about using shift keys
|
|
||||||
// this will only work for US keyboards however
|
|
||||||
if (action && action !== 'keypress' && SHIFT_MAP[key]) {
|
|
||||||
key = SHIFT_MAP[key];
|
|
||||||
modifiers.push('shift');
|
|
||||||
}
|
|
||||||
|
|
||||||
// if this key is a modifier then add it to the list of modifiers
|
|
||||||
if (isModifier(key)) {
|
|
||||||
modifiers.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// depending on what the key combination is
|
|
||||||
// we will try to pick the best event for it
|
|
||||||
action = pickBestAction(key, modifiers, action);
|
|
||||||
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
modifiers,
|
|
||||||
action,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* actually calls the callback function
|
* actually calls the callback function
|
||||||
*
|
*
|
||||||
* if your callback function returns false this will use the jquery
|
* if your callback function returns false this will use the jquery
|
||||||
* convention - prevent default and stop propogation on the event
|
* convention - prevent default and stop propogation on the event
|
||||||
*/
|
*/
|
||||||
function fireCallback(
|
function fireCallback(callback: HotkeyCallback, e: KeyboardEvent, combo?: string): void {
|
||||||
callback: IPublicTypeHotkeyCallback,
|
|
||||||
e: KeyboardEvent,
|
|
||||||
combo?: string,
|
|
||||||
sequence?: string,
|
|
||||||
): void {
|
|
||||||
try {
|
try {
|
||||||
const workspace = globalContext.get('workspace');
|
|
||||||
const editor = workspace.isActive ? workspace.window?.editor : globalContext.get('editor');
|
|
||||||
const designer = editor?.get('designer');
|
|
||||||
const node = designer?.currentSelection?.getNodes()?.[0];
|
|
||||||
const npm = node?.componentMeta?.npm;
|
|
||||||
const selected =
|
|
||||||
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||
|
|
||||||
node?.componentMeta?.componentName ||
|
|
||||||
'';
|
|
||||||
if (callback(e, combo) === false) {
|
if (callback(e, combo) === false) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
editor?.eventBus.emit('hotkey.callback.call', {
|
|
||||||
callback,
|
|
||||||
e,
|
|
||||||
combo,
|
|
||||||
sequence,
|
|
||||||
selected,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error((err as Error).message);
|
console.error((err as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IHotKey extends Hotkey {}
|
export class Hotkey {
|
||||||
|
#resetTimer = 0;
|
||||||
|
#ignoreNextKeyup: boolean | string = false;
|
||||||
|
#ignoreNextKeypress = false;
|
||||||
|
#nextExpectedAction: boolean | string = false;
|
||||||
|
#isActivate = true;
|
||||||
|
|
||||||
export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
|
#sequenceLevels: SequenceLevels = {};
|
||||||
callBacks: IPublicTypeHotkeyCallbacks = {};
|
/**
|
||||||
|
* 当前快捷键配置
|
||||||
|
*/
|
||||||
|
#callBackConfigRecord: HotkeyCallbackConfigRecord = {};
|
||||||
|
|
||||||
private directMap: HotkeyDirectMap = {};
|
active(): void {
|
||||||
|
this.#isActivate = true;
|
||||||
private sequenceLevels: SequenceLevels = {};
|
|
||||||
|
|
||||||
private resetTimer = 0;
|
|
||||||
|
|
||||||
private ignoreNextKeyup: boolean | string = false;
|
|
||||||
|
|
||||||
private ignoreNextKeypress = false;
|
|
||||||
|
|
||||||
private nextExpectedAction: boolean | string = false;
|
|
||||||
|
|
||||||
private isActivate = true;
|
|
||||||
|
|
||||||
constructor(readonly viewName: string = 'global') {
|
|
||||||
this.mount(window);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
activate(activate: boolean): void {
|
inactive() {
|
||||||
this.isActivate = activate;
|
this.#isActivate = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
mount(window: Window): IPublicTypeDisposable {
|
/**
|
||||||
|
* 给指定窗口绑定快捷键
|
||||||
|
* @param window 窗口的 window 对象
|
||||||
|
*/
|
||||||
|
mount(window: Window): EventDisposable {
|
||||||
const { document } = window;
|
const { document } = window;
|
||||||
const handleKeyEvent = this.handleKeyEvent.bind(this);
|
const handleKeyEvent = this.handleKeyEvent.bind(this);
|
||||||
document.addEventListener('keypress', handleKeyEvent, false);
|
document.addEventListener('keypress', handleKeyEvent, false);
|
||||||
document.addEventListener('keydown', handleKeyEvent, false);
|
document.addEventListener('keydown', handleKeyEvent, false);
|
||||||
document.addEventListener('keyup', handleKeyEvent, false);
|
document.addEventListener('keyup', handleKeyEvent, false);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keypress', handleKeyEvent, false);
|
document.removeEventListener('keypress', handleKeyEvent, false);
|
||||||
document.removeEventListener('keydown', handleKeyEvent, false);
|
document.removeEventListener('keydown', handleKeyEvent, false);
|
||||||
@ -396,42 +149,94 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
bind(combos: string[] | string, callback: IPublicTypeHotkeyCallback, action?: string): Hotkey {
|
/**
|
||||||
|
* 绑定快捷键
|
||||||
|
* bind hotkey/hotkeys,
|
||||||
|
* @param combos 快捷键,格式如:['command + s'] 、['ctrl + shift + s'] 等
|
||||||
|
* @param callback 回调函数
|
||||||
|
* @param action
|
||||||
|
*/
|
||||||
|
bind(combos: string[] | string, callback: HotkeyCallback, action?: KeyAction): Hotkey {
|
||||||
this.bindMultiple(Array.isArray(combos) ? combos : [combos], callback, action);
|
this.bindMultiple(Array.isArray(combos) ? combos : [combos], callback, action);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
unbind(combos: string[] | string, callback: IPublicTypeHotkeyCallback, action?: string) {
|
/**
|
||||||
|
* 取消绑定快捷键
|
||||||
|
* bind hotkey/hotkeys,
|
||||||
|
* @param combos 快捷键,格式如:['command + s'] 、['ctrl + shift + s'] 等
|
||||||
|
* @param callback 回调函数
|
||||||
|
* @param action
|
||||||
|
*/
|
||||||
|
unbind(combos: string[] | string, callback: HotkeyCallback, action?: KeyAction) {
|
||||||
const combinations = Array.isArray(combos) ? combos : [combos];
|
const combinations = Array.isArray(combos) ? combos : [combos];
|
||||||
|
|
||||||
combinations.forEach((combination) => {
|
combinations.forEach((combination) => {
|
||||||
const info: KeyInfo = getKeyInfo(combination, action);
|
const info: KeyInfo = getKeyInfo(combination, action);
|
||||||
const { key, modifiers } = info;
|
const { key, modifiers } = info;
|
||||||
const idx = this.callBacks[key].findIndex((info) => {
|
const idx = this.#callBackConfigRecord[key].findIndex((info) => {
|
||||||
return isEqual(info.modifiers, modifiers) && info.callback === callback;
|
return isEqual(info.modifiers, modifiers) && info.callback === callback;
|
||||||
});
|
});
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
this.callBacks[key].splice(idx, 1);
|
this.#callBackConfigRecord[key].splice(idx, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private bindSingle(
|
||||||
* resets all sequence counters except for the ones passed in
|
combination: string,
|
||||||
*/
|
callback: HotkeyCallback,
|
||||||
private resetSequences(doNotReset?: SequenceLevels): void {
|
action?: KeyAction,
|
||||||
// doNotReset = doNotReset || {};
|
sequenceName?: string,
|
||||||
let activeSequences = false;
|
level?: number,
|
||||||
let key = '';
|
): void {
|
||||||
for (key in this.sequenceLevels) {
|
// make sure multiple spaces in a row become a single space
|
||||||
if (doNotReset && doNotReset[key]) {
|
combination = combination.replace(/\s+/g, ' ');
|
||||||
activeSequences = true;
|
|
||||||
} else {
|
const sequence: string[] = combination.split(' ');
|
||||||
this.sequenceLevels[key] = 0;
|
|
||||||
|
// if this pattern is a sequence of keys then run through this method
|
||||||
|
// to reprocess each pattern one key at a time
|
||||||
|
if (sequence.length > 1) {
|
||||||
|
this.bindSequence(combination, sequence, callback, action);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const info: KeyInfo = getKeyInfo(combination, action);
|
||||||
|
|
||||||
|
// make sure to initialize array if this is the first time
|
||||||
|
// a callback is added for this key
|
||||||
|
this.#callBackConfigRecord[info.key] ??= [];
|
||||||
|
|
||||||
|
// remove an existing match if there is one
|
||||||
|
this.getMatches(
|
||||||
|
info.key,
|
||||||
|
info.modifiers,
|
||||||
|
{ type: info.action },
|
||||||
|
sequenceName,
|
||||||
|
combination,
|
||||||
|
level,
|
||||||
|
);
|
||||||
|
|
||||||
|
// add this call back to the array
|
||||||
|
// if it is a sequence put it at the beginning
|
||||||
|
// if not put it at the end
|
||||||
|
//
|
||||||
|
// this is important because the way these are processed expects
|
||||||
|
// the sequence ones to come first
|
||||||
|
this.#callBackConfigRecord[info.key][sequenceName ? 'unshift' : 'push']({
|
||||||
|
callback,
|
||||||
|
modifiers: info.modifiers,
|
||||||
|
action: info.action,
|
||||||
|
seq: sequenceName,
|
||||||
|
level,
|
||||||
|
combo: combination,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!activeSequences) {
|
|
||||||
this.nextExpectedAction = false;
|
private bindMultiple(combinations: string[], callback: HotkeyCallback, action?: KeyAction) {
|
||||||
|
for (const item of combinations) {
|
||||||
|
this.bindSingle(item, callback, action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -442,18 +247,18 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
|
|||||||
private getMatches(
|
private getMatches(
|
||||||
character: string,
|
character: string,
|
||||||
modifiers: string[],
|
modifiers: string[],
|
||||||
e: KeyboardEvent | ActionEvent,
|
e: KeyboardEvent | KeyboardEventLike,
|
||||||
sequenceName?: string,
|
sequenceName?: string,
|
||||||
combination?: string,
|
combination?: string,
|
||||||
level?: number,
|
level?: number,
|
||||||
): IPublicTypeHotkeyCallbackConfig[] {
|
): HotkeyCallbackConfig[] {
|
||||||
let i: number;
|
let i: number;
|
||||||
let callback: IPublicTypeHotkeyCallbackConfig;
|
let callback: HotkeyCallbackConfig;
|
||||||
const matches: IPublicTypeHotkeyCallbackConfig[] = [];
|
const matches: HotkeyCallbackConfig[] = [];
|
||||||
const action: string = e.type;
|
const action = e.type as KeyAction;
|
||||||
|
|
||||||
// if there are no events related to this keycode
|
// if there are no events related to this keycode
|
||||||
if (!this.callBacks[character]) {
|
if (!this.#callBackConfigRecord[character]) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -464,12 +269,12 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
|
|||||||
|
|
||||||
// loop through all callbacks for the key that was pressed
|
// loop through all callbacks for the key that was pressed
|
||||||
// and see if any of them match
|
// and see if any of them match
|
||||||
for (i = 0; i < this.callBacks[character].length; ++i) {
|
for (i = 0; i < this.#callBackConfigRecord[character].length; ++i) {
|
||||||
callback = this.callBacks[character][i];
|
callback = this.#callBackConfigRecord[character][i];
|
||||||
|
|
||||||
// if a sequence name is not specified, but this is a sequence at
|
// if a sequence name is not specified, but this is a sequence at
|
||||||
// the wrong level then move onto the next match
|
// the wrong level then move onto the next match
|
||||||
if (!sequenceName && callback.seq && this.sequenceLevels[callback.seq] !== callback.level) {
|
if (!sequenceName && callback.seq && this.#sequenceLevels[callback.seq] !== callback.level) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -494,21 +299,35 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
|
|||||||
const deleteSequence =
|
const deleteSequence =
|
||||||
sequenceName && callback.seq === sequenceName && callback.level === level;
|
sequenceName && callback.seq === sequenceName && callback.level === level;
|
||||||
if (deleteCombo || deleteSequence) {
|
if (deleteCombo || deleteSequence) {
|
||||||
this.callBacks[character].splice(i, 1);
|
this.#callBackConfigRecord[character].splice(i, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
matches.push(callback);
|
matches.push(callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleKeyEvent(e: KeyboardEvent): void {
|
||||||
|
if (!this.#isActivate) return;
|
||||||
|
|
||||||
|
const character = e.key.toLowerCase();
|
||||||
|
|
||||||
|
// need to use === for the character check because the character can be 0
|
||||||
|
if (e.type === 'keyup' && this.#ignoreNextKeyup === character) {
|
||||||
|
this.#ignoreNextKeyup = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleKey(character, eventModifiers(e), e);
|
||||||
|
}
|
||||||
|
|
||||||
private handleKey(character: string, modifiers: string[], e: KeyboardEvent): void {
|
private handleKey(character: string, modifiers: string[], e: KeyboardEvent): void {
|
||||||
const callbacks: IPublicTypeHotkeyCallbackConfig[] = this.getMatches(character, modifiers, e);
|
const callbacks: HotkeyCallbackConfig[] = this.getMatches(character, modifiers, e);
|
||||||
|
|
||||||
let i: number;
|
let i: number;
|
||||||
const doNotReset: SequenceLevels = {};
|
|
||||||
let maxLevel = 0;
|
let maxLevel = 0;
|
||||||
let processedSequenceCallback = false;
|
|
||||||
|
|
||||||
// Calculate the maxLevel for sequences so we can only execute the longest callback sequence
|
// Calculate the maxLevel for sequences so we can only execute the longest callback sequence
|
||||||
for (i = 0; i < callbacks.length; ++i) {
|
for (i = 0; i < callbacks.length; ++i) {
|
||||||
@ -517,6 +336,9 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let processedSequenceCallback = false;
|
||||||
|
const doNotReset: SequenceLevels = {};
|
||||||
|
|
||||||
// loop through matching callbacks for this key event
|
// loop through matching callbacks for this key event
|
||||||
for (i = 0; i < callbacks.length; ++i) {
|
for (i = 0; i < callbacks.length; ++i) {
|
||||||
// fire for all sequence callbacks
|
// fire for all sequence callbacks
|
||||||
@ -541,7 +363,7 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
|
|||||||
|
|
||||||
// keep a list of which sequences were matches for later
|
// keep a list of which sequences were matches for later
|
||||||
doNotReset[callbacks[i].seq || ''] = 1;
|
doNotReset[callbacks[i].seq || ''] = 1;
|
||||||
fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
|
fireCallback(callbacks[i].callback, e, callbacks[i].combo);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -552,55 +374,33 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ignoreThisKeypress = e.type === 'keypress' && this.ignoreNextKeypress;
|
const ignoreThisKeypress = e.type === 'keypress' && this.#ignoreNextKeypress;
|
||||||
if (e.type === this.nextExpectedAction && !isModifier(character) && !ignoreThisKeypress) {
|
if (e.type === this.#nextExpectedAction && !isModifier(character) && !ignoreThisKeypress) {
|
||||||
this.resetSequences(doNotReset);
|
this.resetSequences(doNotReset);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ignoreNextKeypress = processedSequenceCallback && e.type === 'keydown';
|
this.#ignoreNextKeypress = processedSequenceCallback && e.type === 'keydown';
|
||||||
}
|
|
||||||
|
|
||||||
private handleKeyEvent(e: KeyboardEvent): void {
|
|
||||||
console.log(e);
|
|
||||||
// debugger;
|
|
||||||
if (!this.isActivate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const character = characterFromEvent(e);
|
|
||||||
|
|
||||||
// no character found then stop
|
|
||||||
if (!character) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// need to use === for the character check because the character can be 0
|
|
||||||
if (e.type === 'keyup' && this.ignoreNextKeyup === character) {
|
|
||||||
this.ignoreNextKeyup = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handleKey(character, eventModifiers(e), e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetSequenceTimer(): void {
|
private resetSequenceTimer(): void {
|
||||||
if (this.resetTimer) {
|
if (this.#resetTimer) {
|
||||||
clearTimeout(this.resetTimer);
|
clearTimeout(this.#resetTimer);
|
||||||
}
|
}
|
||||||
this.resetTimer = window.setTimeout(this.resetSequences, 1000);
|
this.#resetTimer = window.setTimeout(this.resetSequences, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bindSequence(
|
private bindSequence(
|
||||||
combo: string,
|
combo: string,
|
||||||
keys: string[],
|
keys: string[],
|
||||||
callback: IPublicTypeHotkeyCallback,
|
callback: HotkeyCallback,
|
||||||
action?: string,
|
action?: KeyAction,
|
||||||
): void {
|
): void {
|
||||||
// const self: any = this;
|
this.#sequenceLevels[combo] = 0;
|
||||||
this.sequenceLevels[combo] = 0;
|
|
||||||
const increaseSequence = (nextAction: string) => {
|
const increaseSequence = (nextAction: string) => {
|
||||||
return () => {
|
return () => {
|
||||||
this.nextExpectedAction = nextAction;
|
this.#nextExpectedAction = nextAction;
|
||||||
++this.sequenceLevels[combo];
|
++this.#sequenceLevels[combo];
|
||||||
this.resetSequenceTimer();
|
this.resetSequenceTimer();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -608,81 +408,137 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
|
|||||||
fireCallback(callback, e, combo);
|
fireCallback(callback, e, combo);
|
||||||
|
|
||||||
if (action !== 'keyup') {
|
if (action !== 'keyup') {
|
||||||
this.ignoreNextKeyup = characterFromEvent(e);
|
this.#ignoreNextKeyup = e.key.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(this.resetSequences, 10);
|
setTimeout(this.resetSequences, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < keys.length; ++i) {
|
for (let i = 0; i < keys.length; ++i) {
|
||||||
const isFinal = i + 1 === keys.length;
|
const isFinal = i + 1 === keys.length;
|
||||||
const wrappedCallback = isFinal
|
const wrappedCallback = isFinal
|
||||||
? callbackAndReset
|
? callbackAndReset
|
||||||
: increaseSequence(action || getKeyInfo(keys[i + 1]).action);
|
: increaseSequence(action || getKeyInfo(keys[i + 1]).action);
|
||||||
|
|
||||||
this.bindSingle(keys[i], wrappedCallback, action, combo, i);
|
this.bindSingle(keys[i], wrappedCallback, action, combo, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bindSingle(
|
/**
|
||||||
combination: string,
|
* resets all sequence counters except for the ones passed in
|
||||||
callback: IPublicTypeHotkeyCallback,
|
*/
|
||||||
action?: string,
|
private resetSequences(doNotReset?: SequenceLevels): void {
|
||||||
sequenceName?: string,
|
// doNotReset = doNotReset || {};
|
||||||
level?: number,
|
let activeSequences = false;
|
||||||
): void {
|
let key = '';
|
||||||
// store a direct mapped reference for use with HotKey.trigger
|
for (key in this.#sequenceLevels) {
|
||||||
this.directMap[`${combination}:${action}`] = callback;
|
if (doNotReset && doNotReset[key]) {
|
||||||
|
activeSequences = true;
|
||||||
// make sure multiple spaces in a row become a single space
|
} else {
|
||||||
combination = combination.replace(/\s+/g, ' ');
|
this.#sequenceLevels[key] = 0;
|
||||||
|
|
||||||
const sequence: string[] = combination.split(' ');
|
|
||||||
|
|
||||||
// if this pattern is a sequence of keys then run through this method
|
|
||||||
// to reprocess each pattern one key at a time
|
|
||||||
if (sequence.length > 1) {
|
|
||||||
this.bindSequence(combination, sequence, callback, action);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const info: KeyInfo = getKeyInfo(combination, action);
|
|
||||||
|
|
||||||
// make sure to initialize array if this is the first time
|
|
||||||
// a callback is added for this key
|
|
||||||
this.callBacks[info.key] = this.callBacks[info.key] || [];
|
|
||||||
|
|
||||||
// remove an existing match if there is one
|
|
||||||
this.getMatches(
|
|
||||||
info.key,
|
|
||||||
info.modifiers,
|
|
||||||
{ type: info.action },
|
|
||||||
sequenceName,
|
|
||||||
combination,
|
|
||||||
level,
|
|
||||||
);
|
|
||||||
|
|
||||||
// add this call back to the array
|
|
||||||
// if it is a sequence put it at the beginning
|
|
||||||
// if not put it at the end
|
|
||||||
//
|
|
||||||
// this is important because the way these are processed expects
|
|
||||||
// the sequence ones to come first
|
|
||||||
this.callBacks[info.key][sequenceName ? 'unshift' : 'push']({
|
|
||||||
callback,
|
|
||||||
modifiers: info.modifiers,
|
|
||||||
action: info.action,
|
|
||||||
seq: sequenceName,
|
|
||||||
level,
|
|
||||||
combo: combination,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
if (!activeSequences) {
|
||||||
private bindMultiple(
|
this.#nextExpectedAction = false;
|
||||||
combinations: string[],
|
|
||||||
callback: IPublicTypeHotkeyCallback,
|
|
||||||
action?: string,
|
|
||||||
) {
|
|
||||||
for (const item of combinations) {
|
|
||||||
this.bindSingle(item, callback, action);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets info for a specific key combination
|
||||||
|
*
|
||||||
|
* @param combination key combination ("command+s" or "a" or "*")
|
||||||
|
* @param action optional, keyboard event type, eg: keypress key down
|
||||||
|
*/
|
||||||
|
function getKeyInfo(combination: string, action?: KeyAction): KeyInfo {
|
||||||
|
let keys: string[] = [];
|
||||||
|
let key = '';
|
||||||
|
let i: number;
|
||||||
|
const modifiers: string[] = [];
|
||||||
|
|
||||||
|
// take the keys from this pattern and figure out what the actual
|
||||||
|
// pattern is all about
|
||||||
|
keys = keysFromString(combination);
|
||||||
|
|
||||||
|
for (i = 0; i < keys.length; ++i) {
|
||||||
|
key = keys[i];
|
||||||
|
|
||||||
|
// normalize key names
|
||||||
|
if (SPECIAL_DEFINED_ALIASES[key]) {
|
||||||
|
key = SPECIAL_DEFINED_ALIASES[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this is not a keypress event then we should
|
||||||
|
// be smart about using shift keys
|
||||||
|
// this will only work for US keyboards however
|
||||||
|
if (action && action !== 'keypress' && SHIFT_ALTERNATE_KEYS_MAP[key]) {
|
||||||
|
key = SHIFT_ALTERNATE_KEYS_MAP[key];
|
||||||
|
modifiers.push('shift');
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this key is a modifier then add it to the list of modifiers
|
||||||
|
if (isModifier(key)) {
|
||||||
|
modifiers.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// depending on what the key combination is
|
||||||
|
// we will try to pick the best event for it
|
||||||
|
action = pickBestAction(key, modifiers, action);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
modifiers,
|
||||||
|
action,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts from a string key combination to an array
|
||||||
|
*
|
||||||
|
* @param {string} combination like "command+shift+l"
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
|
function keysFromString(combination: string): string[] {
|
||||||
|
if (combination === '+') {
|
||||||
|
return ['+'];
|
||||||
|
}
|
||||||
|
|
||||||
|
combination = combination.toLowerCase().replace(/\+{2}/g, '+plus');
|
||||||
|
return combination.split('+');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* determines if the keycode specified is a modifier key or not
|
||||||
|
*/
|
||||||
|
function isModifier(key: string): boolean {
|
||||||
|
return key === 'shift' || key === 'ctrl' || key === 'alt' || key === 'meta';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* picks the best action based on the key combination
|
||||||
|
*/
|
||||||
|
function pickBestAction(key: string, modifiers: string[], action?: KeyAction): KeyAction {
|
||||||
|
// if no action was picked in we should try to pick the one
|
||||||
|
// that we think would work best for this key
|
||||||
|
if (!action) {
|
||||||
|
action = 'keydown';
|
||||||
|
}
|
||||||
|
// modifier keys don't work as expected with keypress,
|
||||||
|
// switch to keydown
|
||||||
|
if (action === 'keypress' && modifiers.length) {
|
||||||
|
action = 'keydown';
|
||||||
|
}
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPressEvent(e: KeyboardEvent | KeyboardEventLike): e is KeypressEvent {
|
||||||
|
return e.type === 'keypress';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if two arrays are equal
|
||||||
|
*/
|
||||||
|
function modifiersMatch(modifiers1: string[], modifiers2: string[]): boolean {
|
||||||
|
return modifiers1.sort().join(',') === modifiers2.sort().join(',');
|
||||||
|
}
|
||||||
4
packages/core/src/index.ts
Normal file
4
packages/core/src/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './configuration';
|
||||||
|
export * from './hotkey';
|
||||||
|
export * from './intl';
|
||||||
|
export * from './instantiation';
|
||||||
43
packages/core/src/instantiation/index.ts
Normal file
43
packages/core/src/instantiation/index.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import '@abraham/reflection';
|
||||||
|
import { Container, inject } from 'inversify';
|
||||||
|
import { fluentProvide, buildProviderModule } from 'inversify-binding-decorators';
|
||||||
|
|
||||||
|
export const iocContainer = new Container();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifies a service of type `T`.
|
||||||
|
*/
|
||||||
|
export interface ServiceIdentifier<T> {
|
||||||
|
(...args: any[]): void;
|
||||||
|
type: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Constructor<T = any> = new (...args: any[]) => T;
|
||||||
|
|
||||||
|
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
|
||||||
|
const id = <any>(
|
||||||
|
function (target: Constructor, targetKey: string, indexOrPropertyDescriptor: any): any {
|
||||||
|
return inject(serviceId)(target, targetKey, indexOrPropertyDescriptor);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
id.toString = () => serviceId;
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Provide(serviceId: string, isSingleTon?: boolean) {
|
||||||
|
const ret = fluentProvide(serviceId.toString());
|
||||||
|
|
||||||
|
if (isSingleTon) {
|
||||||
|
return ret.inSingletonScope().done();
|
||||||
|
}
|
||||||
|
return ret.done();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createInstance<T extends Constructor>(App: T) {
|
||||||
|
return iocContainer.resolve<InstanceType<T>>(App);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bootstrapModules() {
|
||||||
|
iocContainer.load(buildProviderModule());
|
||||||
|
}
|
||||||
170
packages/core/src/intl.ts
Normal file
170
packages/core/src/intl.ts
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import {
|
||||||
|
signal,
|
||||||
|
computed,
|
||||||
|
effect,
|
||||||
|
createLogger,
|
||||||
|
type Signal,
|
||||||
|
type I18nMap,
|
||||||
|
type ComputedSignal,
|
||||||
|
type PlainObject,
|
||||||
|
} from '@alilc/lowcode-shared';
|
||||||
|
import { createIntl, createIntlCache, type IntlShape as IntlFormatter } from '@formatjs/intl';
|
||||||
|
import { mapKeys } from 'lodash-es';
|
||||||
|
|
||||||
|
export { IntlFormatter };
|
||||||
|
|
||||||
|
const logger = createLogger({ level: 'warn', bizName: 'globalLocale' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* todo: key 需要被统一
|
||||||
|
*/
|
||||||
|
const STORED_LOCALE_KEY = 'ali-lowcode-config';
|
||||||
|
|
||||||
|
export type Locale = string;
|
||||||
|
export type IntlMessage = I18nMap[Locale];
|
||||||
|
export type IntlMessageRecord = I18nMap;
|
||||||
|
|
||||||
|
export class Intl {
|
||||||
|
#locale: Signal<Locale>;
|
||||||
|
#messageStore: Signal<IntlMessageRecord>;
|
||||||
|
#currentMessage: ComputedSignal<IntlMessage>;
|
||||||
|
#intlShape: IntlFormatter;
|
||||||
|
|
||||||
|
constructor(defaultLocale?: string, messages: IntlMessageRecord = {}) {
|
||||||
|
if (defaultLocale) {
|
||||||
|
defaultLocale = nomarlizeLocale(defaultLocale);
|
||||||
|
} else {
|
||||||
|
defaultLocale = initializeLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageStore = mapKeys(messages, (_, key) => {
|
||||||
|
return nomarlizeLocale(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#locale = signal(defaultLocale);
|
||||||
|
this.#messageStore = signal(messageStore);
|
||||||
|
this.#currentMessage = computed(() => {
|
||||||
|
return this.#messageStore.value[this.#locale.value] ?? {};
|
||||||
|
});
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
const cache = createIntlCache();
|
||||||
|
this.#intlShape = createIntl(
|
||||||
|
{
|
||||||
|
locale: this.#locale.value,
|
||||||
|
messages: this.#currentMessage.value,
|
||||||
|
},
|
||||||
|
cache,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocale() {
|
||||||
|
return this.#locale.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocale(locale: Locale) {
|
||||||
|
const nomarlizedLocale = nomarlizeLocale(locale);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// store storage
|
||||||
|
let config = JSON.parse(localStorage.getItem(STORED_LOCALE_KEY) || '');
|
||||||
|
|
||||||
|
if (config && typeof config === 'object') {
|
||||||
|
config.locale = locale;
|
||||||
|
} else {
|
||||||
|
config = { locale };
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(STORED_LOCALE_KEY, JSON.stringify(config));
|
||||||
|
} catch {
|
||||||
|
// ignore;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#locale.value = nomarlizedLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessages(locale: Locale, messages: IntlMessage) {
|
||||||
|
locale = nomarlizeLocale(locale);
|
||||||
|
const original = this.#messageStore.value[locale];
|
||||||
|
|
||||||
|
this.#messageStore.value[locale] = Object.assign(original, messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormatter(): IntlFormatter {
|
||||||
|
return this.#intlShape;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeLocale() {
|
||||||
|
let result: Locale | undefined;
|
||||||
|
|
||||||
|
let config: PlainObject = {};
|
||||||
|
try {
|
||||||
|
// store 1: config from storage
|
||||||
|
config = JSON.parse(localStorage.getItem(STORED_LOCALE_KEY) || '');
|
||||||
|
} catch {
|
||||||
|
// ignore;
|
||||||
|
}
|
||||||
|
if (config?.locale) {
|
||||||
|
result = (config.locale || '').replace('_', '-');
|
||||||
|
logger.debug(`getting locale from localStorage: ${result}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result && navigator.language) {
|
||||||
|
// store 2: config from system
|
||||||
|
result = nomarlizeLocale(navigator.language);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
logger.warn(
|
||||||
|
'something when wrong when trying to get locale, use zh-CN as default, please check it out!',
|
||||||
|
);
|
||||||
|
result = 'zh-CN';
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigatorLanguageMapping: Record<string, string> = {
|
||||||
|
en: 'en-US',
|
||||||
|
zh: 'zh-CN',
|
||||||
|
zt: 'zh-TW',
|
||||||
|
es: 'es-ES',
|
||||||
|
pt: 'pt-PT',
|
||||||
|
fr: 'fr-FR',
|
||||||
|
de: 'de-DE',
|
||||||
|
it: 'it-IT',
|
||||||
|
ru: 'ru-RU',
|
||||||
|
ja: 'ja-JP',
|
||||||
|
ko: 'ko-KR',
|
||||||
|
ar: 'ar-SA',
|
||||||
|
tr: 'tr-TR',
|
||||||
|
th: 'th-TH',
|
||||||
|
vi: 'vi-VN',
|
||||||
|
nl: 'nl-NL',
|
||||||
|
he: 'iw-IL',
|
||||||
|
id: 'in-ID',
|
||||||
|
pl: 'pl-PL',
|
||||||
|
hi: 'hi-IN',
|
||||||
|
uk: 'uk-UA',
|
||||||
|
ms: 'ms-MY',
|
||||||
|
tl: 'tl-PH',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* nomarlize navigator.language or user input's locale
|
||||||
|
* eg: zh -> zh-CN, zh_CN -> zh-CN, zh-cn -> zh-CN
|
||||||
|
* @param target
|
||||||
|
*/
|
||||||
|
function nomarlizeLocale(target: Locale) {
|
||||||
|
if (navigatorLanguageMapping[target]) {
|
||||||
|
return navigatorLanguageMapping[target];
|
||||||
|
}
|
||||||
|
|
||||||
|
const replaced = target.replace('_', '-');
|
||||||
|
const splited = replaced.split('-').slice(0, 2);
|
||||||
|
splited[1] = splited[1].toUpperCase();
|
||||||
|
|
||||||
|
return splited.join('-');
|
||||||
|
}
|
||||||
@ -3,5 +3,5 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src", "__tests__"]
|
||||||
}
|
}
|
||||||
@ -30,7 +30,7 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alilc/lowcode-editor-core": "workspace:*",
|
"@alilc/lowcode-core": "workspace:*",
|
||||||
"@alilc/lowcode-shared": "workspace:*",
|
"@alilc/lowcode-shared": "workspace:*",
|
||||||
"@alilc/lowcode-types": "workspace:*",
|
"@alilc/lowcode-types": "workspace:*",
|
||||||
"@alilc/lowcode-utils": "workspace:*",
|
"@alilc/lowcode-utils": "workspace:*",
|
||||||
@ -55,7 +55,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@alifd/next": "^1.27.8",
|
"@alifd/next": "^1.27.8",
|
||||||
"@alilc/lowcode-editor-core": "workspace:*",
|
"@alilc/lowcode-core": "workspace:*",
|
||||||
"@alilc/lowcode-shared": "workspace:*",
|
"@alilc/lowcode-shared": "workspace:*",
|
||||||
"@alilc/lowcode-types": "workspace:*",
|
"@alilc/lowcode-types": "workspace:*",
|
||||||
"@alilc/lowcode-utils": "workspace:*",
|
"@alilc/lowcode-utils": "workspace:*",
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
内置模拟器主进程
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
.lc-bem-tools {
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
overflow: visible;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
@ -1,120 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { Component, Fragment, ReactElement, PureComponent } from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { computed, observer, globalLocale } from '@alilc/lowcode-editor-core';
|
|
||||||
import { IPublicTypeI18nData, IPublicTypeTitleContent } from '@alilc/lowcode-types';
|
|
||||||
import { isI18nData } from '@alilc/lowcode-utils';
|
|
||||||
import { DropLocation } from '../../designer';
|
|
||||||
import { BuiltinSimulatorHost } from '../../builtin-simulator/host';
|
|
||||||
import { INode } from '../../document/node';
|
|
||||||
import { Title } from '../../widgets';
|
|
||||||
|
|
||||||
export class BorderContainerInstance extends PureComponent<{
|
|
||||||
title: IPublicTypeTitleContent;
|
|
||||||
rect: DOMRect | null;
|
|
||||||
scale: number;
|
|
||||||
scrollX: number;
|
|
||||||
scrollY: number;
|
|
||||||
}> {
|
|
||||||
render() {
|
|
||||||
const { title, rect, scale, scrollX, scrollY } = this.props;
|
|
||||||
if (!rect) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
width: rect.width * scale,
|
|
||||||
height: rect.height * scale,
|
|
||||||
transform: `translate(${(scrollX + rect.left) * scale}px, ${(scrollY + rect.top) * scale}px)`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const className = classNames('lc-borders lc-borders-detecting');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className} style={style}>
|
|
||||||
<Title title={title} className="lc-borders-title" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTitle(title: string | IPublicTypeI18nData | ReactElement) {
|
|
||||||
if (typeof title === 'string') return title;
|
|
||||||
if (isI18nData(title)) {
|
|
||||||
const locale = globalLocale.getLocale() || 'zh-CN';
|
|
||||||
return `将放入到此${title[locale]}`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class BorderContainer extends Component<{
|
|
||||||
host: BuiltinSimulatorHost;
|
|
||||||
}, {
|
|
||||||
target?: INode;
|
|
||||||
}> {
|
|
||||||
state = {} as any;
|
|
||||||
|
|
||||||
@computed get scale() {
|
|
||||||
return this.props.host.viewport.scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get scrollX() {
|
|
||||||
return this.props.host.viewport.scrollX;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get scrollY() {
|
|
||||||
return this.props.host.viewport.scrollY;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { host } = this.props;
|
|
||||||
|
|
||||||
host.designer.editor.eventBus.on('designer.dropLocation.change', (loc: DropLocation) => {
|
|
||||||
const { target } = this.state;
|
|
||||||
if (target === loc?.target) return;
|
|
||||||
this.setState({
|
|
||||||
target: loc?.target,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { host } = this.props;
|
|
||||||
const { target } = this.state;
|
|
||||||
if (target == undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const instances = host.getComponentInstances(target!);
|
|
||||||
if (!instances || instances.length < 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instances.length === 1) {
|
|
||||||
return (
|
|
||||||
<BorderContainerInstance
|
|
||||||
key="line-h"
|
|
||||||
title={getTitle(target.componentMeta.title)}
|
|
||||||
scale={this.scale}
|
|
||||||
scrollX={this.scrollX}
|
|
||||||
scrollY={this.scrollY}
|
|
||||||
rect={host.computeComponentInstanceRect(instances[0], target.componentMeta.rootSelector)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
{instances.map((inst, i) => (
|
|
||||||
<BorderContainerInstance
|
|
||||||
key={`line-h-${i}`}
|
|
||||||
title={getTitle(target.componentMeta.title)}
|
|
||||||
scale={this.scale}
|
|
||||||
scrollX={this.scrollX}
|
|
||||||
scrollY={this.scrollY}
|
|
||||||
rect={host.computeComponentInstanceRect(inst, target.componentMeta.rootSelector)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,164 +0,0 @@
|
|||||||
import { Component, Fragment, PureComponent } from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { computed, observer } from '@alilc/lowcode-editor-core';
|
|
||||||
import { IPublicTypeTitleContent } from '@alilc/lowcode-types';
|
|
||||||
import { getClosestNode } from '@alilc/lowcode-utils';
|
|
||||||
import { intl } from '../../locale';
|
|
||||||
import { BuiltinSimulatorHost } from '../host';
|
|
||||||
import { Title } from '../../widgets';
|
|
||||||
|
|
||||||
export class BorderDetectingInstance extends PureComponent<{
|
|
||||||
title: IPublicTypeTitleContent;
|
|
||||||
rect: DOMRect | null;
|
|
||||||
scale: number;
|
|
||||||
scrollX: number;
|
|
||||||
scrollY: number;
|
|
||||||
isLocked?: boolean;
|
|
||||||
}> {
|
|
||||||
render() {
|
|
||||||
const { title, rect, scale, scrollX, scrollY, isLocked } = this.props;
|
|
||||||
if (!rect) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
width: rect.width * scale,
|
|
||||||
height: rect.height * scale,
|
|
||||||
transform: `translate(${(scrollX + rect.left) * scale}px, ${(scrollY + rect.top) * scale}px)`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const className = classNames('lc-borders lc-borders-detecting');
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// 1. thinkof icon
|
|
||||||
// 2. thinkof top|bottom|inner space
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className} style={style}>
|
|
||||||
<Title title={title} className="lc-borders-title" />
|
|
||||||
{isLocked ? <Title title={intl('locked')} className="lc-borders-status" /> : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class BorderDetecting extends Component<{ host: BuiltinSimulatorHost }> {
|
|
||||||
@computed get scale() {
|
|
||||||
return this.props.host.viewport.scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get scrollX() {
|
|
||||||
return this.props.host.viewport.scrollX;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get scrollY() {
|
|
||||||
return this.props.host.viewport.scrollY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get current() {
|
|
||||||
const { host } = this.props;
|
|
||||||
const doc = host.currentDocument;
|
|
||||||
if (!doc) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { selection } = doc;
|
|
||||||
const { current } = host.designer.detecting;
|
|
||||||
|
|
||||||
if (!current || current.document !== doc || selection.has(current.id)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { host } = this.props;
|
|
||||||
const { current } = this;
|
|
||||||
|
|
||||||
const canHoverHook = current?.componentMeta.advanced.callbacks?.onHoverHook;
|
|
||||||
const canHover =
|
|
||||||
canHoverHook && typeof canHoverHook === 'function'
|
|
||||||
? canHoverHook(current.internalToShellNode()!)
|
|
||||||
: true;
|
|
||||||
|
|
||||||
if (!canHover || !current || host.viewport.scrolling || host.liveEditing.editing) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// rootNode, hover whole viewport
|
|
||||||
const focusNode = current.document.focusNode!;
|
|
||||||
|
|
||||||
if (!focusNode.contains(current)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current.contains(focusNode)) {
|
|
||||||
const bounds = host.viewport.bounds;
|
|
||||||
return (
|
|
||||||
<BorderDetectingInstance
|
|
||||||
key="line-root"
|
|
||||||
title={current.title}
|
|
||||||
scale={this.scale}
|
|
||||||
scrollX={host.viewport.scrollX}
|
|
||||||
scrollY={host.viewport.scrollY}
|
|
||||||
rect={new DOMRect(0, 0, bounds.width, bounds.height)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lockedNode = getClosestNode(current as any, (n) => {
|
|
||||||
// 假如当前节点就是 locked 状态,要从当前节点的父节点开始查找
|
|
||||||
return !!(current?.isLocked ? n.parent?.isLocked : n.isLocked);
|
|
||||||
});
|
|
||||||
if (lockedNode && lockedNode.getId() !== current.getId()) {
|
|
||||||
// 选中父节锁定的节点
|
|
||||||
return (
|
|
||||||
<BorderDetectingInstance
|
|
||||||
key="line-h"
|
|
||||||
title={current.title}
|
|
||||||
scale={this.scale}
|
|
||||||
scrollX={this.scrollX}
|
|
||||||
scrollY={this.scrollY}
|
|
||||||
// @ts-ignore
|
|
||||||
rect={host.computeComponentInstanceRect(
|
|
||||||
host.getComponentInstances(lockedNode)![0],
|
|
||||||
lockedNode.componentMeta.rootSelector,
|
|
||||||
)}
|
|
||||||
isLocked={lockedNode?.getId() !== current.getId()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const instances = host.getComponentInstances(current);
|
|
||||||
if (!instances || instances.length < 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instances.length === 1) {
|
|
||||||
return (
|
|
||||||
<BorderDetectingInstance
|
|
||||||
key="line-h"
|
|
||||||
title={current.title}
|
|
||||||
scale={this.scale}
|
|
||||||
scrollX={this.scrollX}
|
|
||||||
scrollY={this.scrollY}
|
|
||||||
rect={host.computeComponentInstanceRect(instances[0], current.componentMeta.rootSelector)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
{instances.map((inst, i) => (
|
|
||||||
<BorderDetectingInstance
|
|
||||||
key={`line-h-${i}`}
|
|
||||||
title={current.title}
|
|
||||||
scale={this.scale}
|
|
||||||
scrollX={this.scrollX}
|
|
||||||
scrollY={this.scrollY}
|
|
||||||
rect={host.computeComponentInstanceRect(inst, current.componentMeta.rootSelector)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,362 +0,0 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
|
||||||
import DragResizeEngine from './drag-resize-engine';
|
|
||||||
import { observer, computed } from '@alilc/lowcode-editor-core';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { SimulatorContext } from '../context';
|
|
||||||
import { BuiltinSimulatorHost } from '../host';
|
|
||||||
import { OffsetObserver, Designer, INode } from '../../designer';
|
|
||||||
import { Node } from '../../document';
|
|
||||||
import { normalizeTriggers } from '../../utils/misc';
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export default class BoxResizing extends Component<{ host: BuiltinSimulatorHost }> {
|
|
||||||
static contextType = SimulatorContext;
|
|
||||||
|
|
||||||
get host(): BuiltinSimulatorHost {
|
|
||||||
return this.props.host;
|
|
||||||
}
|
|
||||||
|
|
||||||
get dragging(): boolean {
|
|
||||||
return this.host.designer.dragon.dragging;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get selecting() {
|
|
||||||
const doc = this.host.currentDocument;
|
|
||||||
if (!doc || doc.suspensed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { selection } = doc;
|
|
||||||
return this.dragging ? selection.getTopNodes() : selection.getNodes();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
// this.hoveringCapture.setBoundary(this.outline);
|
|
||||||
// this.willBind();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { selecting } = this;
|
|
||||||
if (!selecting || selecting.length < 1) {
|
|
||||||
// DIRTY FIX, recore has a bug!
|
|
||||||
return <Fragment />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// const componentMeta = selecting[0].componentMeta;
|
|
||||||
// const metadata = componentMeta.getMetadata();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
{selecting.map((node) => (
|
|
||||||
<BoxResizingForNode key={node.id} node={node} host={this.props.host} />
|
|
||||||
))}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class BoxResizingForNode extends Component<{ host: BuiltinSimulatorHost; node: Node }> {
|
|
||||||
static contextType = SimulatorContext;
|
|
||||||
|
|
||||||
get host(): BuiltinSimulatorHost {
|
|
||||||
return this.props.host;
|
|
||||||
}
|
|
||||||
|
|
||||||
get dragging(): boolean {
|
|
||||||
return this.host.designer.dragon.dragging;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get instances() {
|
|
||||||
return this.host.getComponentInstances(this.props.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { instances } = this;
|
|
||||||
const { node } = this.props;
|
|
||||||
const { designer } = this.host;
|
|
||||||
|
|
||||||
if (!instances || instances.length < 1 || this.dragging) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Fragment key={node.id}>
|
|
||||||
{instances.map((instance: any) => {
|
|
||||||
const observed = designer.createOffsetObserver({
|
|
||||||
node,
|
|
||||||
instance,
|
|
||||||
});
|
|
||||||
if (!observed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<BoxResizingInstance
|
|
||||||
key={observed.id}
|
|
||||||
dragging={this.dragging}
|
|
||||||
designer={designer}
|
|
||||||
observed={observed}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class BoxResizingInstance extends Component<{
|
|
||||||
observed: OffsetObserver;
|
|
||||||
highlight?: boolean;
|
|
||||||
dragging?: boolean;
|
|
||||||
designer?: Designer;
|
|
||||||
}> {
|
|
||||||
// private outline: any;
|
|
||||||
private willUnbind: () => any;
|
|
||||||
|
|
||||||
// outline of eight direction
|
|
||||||
private outlineN: any;
|
|
||||||
private outlineE: any;
|
|
||||||
private outlineS: any;
|
|
||||||
private outlineW: any;
|
|
||||||
private outlineNE: any;
|
|
||||||
private outlineNW: any;
|
|
||||||
private outlineSE: any;
|
|
||||||
private outlineSW: any;
|
|
||||||
|
|
||||||
private dragEngine: DragResizeEngine;
|
|
||||||
|
|
||||||
constructor(props: any) {
|
|
||||||
super(props);
|
|
||||||
this.dragEngine = new DragResizeEngine(props.designer);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.willUnbind) {
|
|
||||||
this.willUnbind();
|
|
||||||
}
|
|
||||||
this.props.observed.purge();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// this.hoveringCapture.setBoundary(this.outline);
|
|
||||||
this.willBind();
|
|
||||||
|
|
||||||
const resize = (
|
|
||||||
e: MouseEvent,
|
|
||||||
direction: string,
|
|
||||||
node: INode,
|
|
||||||
moveX: number,
|
|
||||||
moveY: number,
|
|
||||||
) => {
|
|
||||||
const { advanced } = node.componentMeta;
|
|
||||||
if (advanced.callbacks && typeof advanced.callbacks.onResize === 'function') {
|
|
||||||
(e as any).trigger = direction;
|
|
||||||
(e as any).deltaX = moveX;
|
|
||||||
(e as any).deltaY = moveY;
|
|
||||||
const cbNode = node?.isNode ? node.internalToShellNode() : node;
|
|
||||||
advanced.callbacks.onResize(e as any, cbNode);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resizeStart = (e: MouseEvent, direction: string, node: INode) => {
|
|
||||||
const { advanced } = node.componentMeta;
|
|
||||||
if (advanced.callbacks && typeof advanced.callbacks.onResizeStart === 'function') {
|
|
||||||
(e as any).trigger = direction;
|
|
||||||
const cbNode = node?.isNode ? node.internalToShellNode() : node;
|
|
||||||
advanced.callbacks.onResizeStart(e as any, cbNode);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resizeEnd = (e: MouseEvent, direction: string, node: INode) => {
|
|
||||||
const { advanced } = node.componentMeta;
|
|
||||||
if (advanced.callbacks && typeof advanced.callbacks.onResizeEnd === 'function') {
|
|
||||||
(e as any).trigger = direction;
|
|
||||||
const cbNode = node?.isNode ? node.internalToShellNode() : node;
|
|
||||||
advanced.callbacks.onResizeEnd(e as any, cbNode as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
const editor = node.document?.designer.editor;
|
|
||||||
const npm = node?.componentMeta?.npm;
|
|
||||||
const selected =
|
|
||||||
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||
|
|
||||||
node?.componentMeta?.componentName ||
|
|
||||||
'';
|
|
||||||
editor?.eventBus.emit('designer.border.resize', {
|
|
||||||
selected,
|
|
||||||
layout: node?.parent?.getPropValue('layout') || '',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.dragEngine.onResize(resize);
|
|
||||||
this.dragEngine.onResizeStart(resizeStart);
|
|
||||||
this.dragEngine.onResizeEnd(resizeEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
willBind() {
|
|
||||||
if (this.willUnbind) {
|
|
||||||
this.willUnbind();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!this.outlineN &&
|
|
||||||
!this.outlineE &&
|
|
||||||
!this.outlineS &&
|
|
||||||
!this.outlineW &&
|
|
||||||
!this.outlineNE &&
|
|
||||||
!this.outlineNW &&
|
|
||||||
!this.outlineSE &&
|
|
||||||
!this.outlineSW
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unBind: any[] = [];
|
|
||||||
const { node } = this.props.observed;
|
|
||||||
|
|
||||||
unBind.push(
|
|
||||||
...[
|
|
||||||
this.dragEngine.from(this.outlineN, 'n', () => node),
|
|
||||||
this.dragEngine.from(this.outlineE, 'e', () => node),
|
|
||||||
this.dragEngine.from(this.outlineS, 's', () => node),
|
|
||||||
this.dragEngine.from(this.outlineW, 'w', () => node),
|
|
||||||
this.dragEngine.from(this.outlineNE, 'ne', () => node),
|
|
||||||
this.dragEngine.from(this.outlineNW, 'nw', () => node),
|
|
||||||
this.dragEngine.from(this.outlineSE, 'se', () => node),
|
|
||||||
this.dragEngine.from(this.outlineSW, 'sw', () => node),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
this.willUnbind = () => {
|
|
||||||
if (unBind && unBind.length > 0) {
|
|
||||||
unBind.forEach((item) => {
|
|
||||||
item();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.willUnbind = () => {};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { observed } = this.props;
|
|
||||||
let triggerVisible: any = [];
|
|
||||||
let offsetWidth = 0;
|
|
||||||
let offsetHeight = 0;
|
|
||||||
let offsetTop = 0;
|
|
||||||
let offsetLeft = 0;
|
|
||||||
if (observed.hasOffset) {
|
|
||||||
offsetWidth = observed.offsetWidth!;
|
|
||||||
offsetHeight = observed.offsetHeight!;
|
|
||||||
offsetTop = observed.offsetTop;
|
|
||||||
offsetLeft = observed.offsetLeft;
|
|
||||||
const { node } = observed;
|
|
||||||
const metadata = node.componentMeta.getMetadata();
|
|
||||||
if (metadata.configure?.advanced?.getResizingHandlers) {
|
|
||||||
triggerVisible = metadata.configure.advanced.getResizingHandlers(
|
|
||||||
node.internalToShellNode(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
triggerVisible = normalizeTriggers(triggerVisible);
|
|
||||||
|
|
||||||
const baseSideClass = 'lc-borders lc-resize-side';
|
|
||||||
const baseCornerClass = 'lc-borders lc-resize-corner';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
ref={(ref) => {
|
|
||||||
this.outlineN = ref;
|
|
||||||
}}
|
|
||||||
className={classNames(baseSideClass, 'n')}
|
|
||||||
style={{
|
|
||||||
height: 20,
|
|
||||||
transform: `translate(${offsetLeft}px, ${offsetTop - 10}px)`,
|
|
||||||
width: offsetWidth,
|
|
||||||
display: triggerVisible.includes('N') ? 'flex' : 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
ref={(ref) => {
|
|
||||||
this.outlineNE = ref;
|
|
||||||
}}
|
|
||||||
className={classNames(baseCornerClass, 'ne')}
|
|
||||||
style={{
|
|
||||||
transform: `translate(${offsetLeft + offsetWidth - 5}px, ${offsetTop - 3}px)`,
|
|
||||||
cursor: 'nesw-resize',
|
|
||||||
display: triggerVisible.includes('NE') ? 'flex' : 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={classNames(baseSideClass, 'e')}
|
|
||||||
ref={(ref) => {
|
|
||||||
this.outlineE = ref;
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
height: offsetHeight,
|
|
||||||
transform: `translate(${offsetLeft + offsetWidth - 10}px, ${offsetTop}px)`,
|
|
||||||
width: 20,
|
|
||||||
display: triggerVisible.includes('E') ? 'flex' : 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
ref={(ref) => {
|
|
||||||
this.outlineSE = ref;
|
|
||||||
}}
|
|
||||||
className={classNames(baseCornerClass, 'se')}
|
|
||||||
style={{
|
|
||||||
transform: `translate(${offsetLeft + offsetWidth - 5}px, ${
|
|
||||||
offsetTop + offsetHeight - 5
|
|
||||||
}px)`,
|
|
||||||
cursor: 'nwse-resize',
|
|
||||||
display: triggerVisible.includes('SE') ? 'flex' : 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
ref={(ref) => {
|
|
||||||
this.outlineS = ref;
|
|
||||||
}}
|
|
||||||
className={classNames(baseSideClass, 's')}
|
|
||||||
style={{
|
|
||||||
height: 20,
|
|
||||||
transform: `translate(${offsetLeft}px, ${offsetTop + offsetHeight - 10}px)`,
|
|
||||||
width: offsetWidth,
|
|
||||||
display: triggerVisible.includes('S') ? 'flex' : 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
ref={(ref) => {
|
|
||||||
this.outlineSW = ref;
|
|
||||||
}}
|
|
||||||
className={classNames(baseCornerClass, 'sw')}
|
|
||||||
style={{
|
|
||||||
transform: `translate(${offsetLeft - 3}px, ${offsetTop + offsetHeight - 5}px)`,
|
|
||||||
cursor: 'nesw-resize',
|
|
||||||
display: triggerVisible.includes('SW') ? 'flex' : 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
ref={(ref) => {
|
|
||||||
this.outlineW = ref;
|
|
||||||
}}
|
|
||||||
className={classNames(baseSideClass, 'w')}
|
|
||||||
style={{
|
|
||||||
height: offsetHeight,
|
|
||||||
transform: `translate(${offsetLeft - 10}px, ${offsetTop}px)`,
|
|
||||||
width: 20,
|
|
||||||
display: triggerVisible.includes('W') ? 'flex' : 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
ref={(ref) => {
|
|
||||||
this.outlineNW = ref;
|
|
||||||
}}
|
|
||||||
className={classNames(baseCornerClass, 'nw')}
|
|
||||||
style={{
|
|
||||||
transform: `translate(${offsetLeft - 3}px, ${offsetTop - 3}px)`,
|
|
||||||
cursor: 'nwse-resize',
|
|
||||||
display: triggerVisible.includes('NW') ? 'flex' : 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,239 +0,0 @@
|
|||||||
import {
|
|
||||||
Component,
|
|
||||||
Fragment,
|
|
||||||
isValidElement,
|
|
||||||
cloneElement,
|
|
||||||
createElement,
|
|
||||||
ReactNode,
|
|
||||||
ComponentType,
|
|
||||||
} from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { observer, computed, Tip, engineConfig } from '@alilc/lowcode-editor-core';
|
|
||||||
import { createIcon, isReactComponent, isActionContentObject } from '@alilc/lowcode-utils';
|
|
||||||
import { IPublicTypeActionContentObject } from '@alilc/lowcode-types';
|
|
||||||
import { BuiltinSimulatorHost } from '../host';
|
|
||||||
import { INode, OffsetObserver } from '../../designer';
|
|
||||||
import NodeSelector from '../node-selector';
|
|
||||||
import { ISimulatorHost } from '../../simulator';
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class BorderSelectingInstance extends Component<{
|
|
||||||
observed: OffsetObserver;
|
|
||||||
highlight?: boolean;
|
|
||||||
dragging?: boolean;
|
|
||||||
}> {
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.observed.purge();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { observed, highlight, dragging } = this.props;
|
|
||||||
if (!observed.hasOffset) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { offsetWidth, offsetHeight, offsetTop, offsetLeft } = observed;
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
width: offsetWidth,
|
|
||||||
height: offsetHeight,
|
|
||||||
transform: `translate3d(${offsetLeft}px, ${offsetTop}px, 0)`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const className = classNames('lc-borders lc-borders-selecting', {
|
|
||||||
highlight,
|
|
||||||
dragging,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { hideSelectTools } = observed.node.componentMeta.advanced;
|
|
||||||
const hideComponentAction = engineConfig.get('hideComponentAction');
|
|
||||||
|
|
||||||
if (hideSelectTools) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className} style={style}>
|
|
||||||
{!dragging && !hideComponentAction ? <Toolbar observed={observed} /> : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
class Toolbar extends Component<{ observed: OffsetObserver }> {
|
|
||||||
render() {
|
|
||||||
const { observed } = this.props;
|
|
||||||
const { height, width } = observed.viewport!;
|
|
||||||
const BAR_HEIGHT = 20;
|
|
||||||
const MARGIN = 1;
|
|
||||||
const BORDER = 2;
|
|
||||||
const SPACE_HEIGHT = BAR_HEIGHT + MARGIN + BORDER;
|
|
||||||
const SPACE_MINIMUM_WIDTH = 160; // magic number,大致是 toolbar 的宽度
|
|
||||||
let style: any;
|
|
||||||
// 计算 toolbar 的上/下位置
|
|
||||||
if (observed.top > SPACE_HEIGHT) {
|
|
||||||
style = {
|
|
||||||
top: -SPACE_HEIGHT,
|
|
||||||
height: BAR_HEIGHT,
|
|
||||||
};
|
|
||||||
} else if (observed.bottom! + SPACE_HEIGHT < height) {
|
|
||||||
style = {
|
|
||||||
bottom: -SPACE_HEIGHT,
|
|
||||||
height: BAR_HEIGHT,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
style = {
|
|
||||||
height: BAR_HEIGHT,
|
|
||||||
top: Math.max(MARGIN, MARGIN - observed.top),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 计算 toolbar 的左/右位置
|
|
||||||
if (SPACE_MINIMUM_WIDTH > observed.left + observed.width!) {
|
|
||||||
style.left = Math.max(-BORDER, observed.left - width - BORDER);
|
|
||||||
} else {
|
|
||||||
style.right = Math.max(-BORDER, observed.right! - width - BORDER);
|
|
||||||
style.justifyContent = 'flex-start';
|
|
||||||
}
|
|
||||||
const { node } = observed;
|
|
||||||
const actions: ReactNode[] = [];
|
|
||||||
node.componentMeta.availableActions.forEach((action) => {
|
|
||||||
const { important = true, condition, content, name } = action;
|
|
||||||
if (node.isSlot() && (name === 'copy' || name === 'remove')) {
|
|
||||||
// FIXME: need this?
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
important &&
|
|
||||||
(typeof condition === 'function' ? condition(node) !== false : condition !== false)
|
|
||||||
) {
|
|
||||||
actions.push(createAction(content, name, node));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div className="lc-borders-actions" style={style}>
|
|
||||||
{actions}
|
|
||||||
<NodeSelector node={node} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createAction(
|
|
||||||
content: ReactNode | ComponentType<any> | IPublicTypeActionContentObject,
|
|
||||||
key: string,
|
|
||||||
node: INode,
|
|
||||||
) {
|
|
||||||
if (isValidElement<{ key: string; node: INode }>(content)) {
|
|
||||||
return cloneElement(content, { key, node });
|
|
||||||
}
|
|
||||||
if (isReactComponent(content)) {
|
|
||||||
return createElement(content, { key, node });
|
|
||||||
}
|
|
||||||
if (isActionContentObject(content)) {
|
|
||||||
const { action, title, icon } = content;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={key}
|
|
||||||
className="lc-borders-action"
|
|
||||||
onClick={() => {
|
|
||||||
action && action(node.internalToShellNode()!);
|
|
||||||
const editor = node.document?.designer.editor;
|
|
||||||
const npm = node?.componentMeta?.npm;
|
|
||||||
const selected =
|
|
||||||
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||
|
|
||||||
node?.componentMeta?.componentName ||
|
|
||||||
'';
|
|
||||||
editor?.eventBus.emit('designer.border.action', {
|
|
||||||
name: key,
|
|
||||||
selected,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{icon && createIcon(icon, { key, node: node.internalToShellNode() })}
|
|
||||||
<Tip>{title as any}</Tip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class BorderSelectingForNode extends Component<{ host: ISimulatorHost; node: INode }> {
|
|
||||||
get host(): ISimulatorHost {
|
|
||||||
return this.props.host;
|
|
||||||
}
|
|
||||||
|
|
||||||
get dragging(): boolean {
|
|
||||||
return this.host.designer.dragon.dragging;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get instances() {
|
|
||||||
return this.host.getComponentInstances(this.props.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { instances } = this;
|
|
||||||
const { node } = this.props;
|
|
||||||
const { designer } = this.host;
|
|
||||||
|
|
||||||
if (!instances || instances.length < 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Fragment key={node.id}>
|
|
||||||
{instances.map((instance) => {
|
|
||||||
const observed = designer.createOffsetObserver({
|
|
||||||
node,
|
|
||||||
instance,
|
|
||||||
});
|
|
||||||
if (!observed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<BorderSelectingInstance
|
|
||||||
key={observed.id}
|
|
||||||
dragging={this.dragging}
|
|
||||||
observed={observed}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class BorderSelecting extends Component<{ host: BuiltinSimulatorHost }> {
|
|
||||||
get host(): BuiltinSimulatorHost {
|
|
||||||
return this.props.host;
|
|
||||||
}
|
|
||||||
|
|
||||||
get dragging(): boolean {
|
|
||||||
return this.host.designer.dragon.dragging;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get selecting() {
|
|
||||||
const doc = this.host.currentDocument;
|
|
||||||
if (!doc || doc.suspensed || this.host.liveEditing.editing) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { selection } = doc;
|
|
||||||
return this.dragging ? selection.getTopNodes() : selection.getNodes();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { selecting } = this;
|
|
||||||
if (!selecting || selecting.length < 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
{selecting.map((node) => (
|
|
||||||
<BorderSelectingForNode key={node.id} host={this.props.host} node={node} />
|
|
||||||
))}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
@scope: lc-borders;
|
|
||||||
|
|
||||||
.@{scope} {
|
|
||||||
box-sizing: border-box;
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1;
|
|
||||||
border: 1px solid var(--color-brand-light);
|
|
||||||
will-change: transform, width, height;
|
|
||||||
overflow: visible;
|
|
||||||
& > &-title {
|
|
||||||
color: var(--color-brand-light);
|
|
||||||
transform: translateY(-100%);
|
|
||||||
font-weight: lighter;
|
|
||||||
}
|
|
||||||
& > &-status {
|
|
||||||
margin-left: 5px;
|
|
||||||
color: var(--color-text, #3c3c3c);
|
|
||||||
transform: translateY(-100%);
|
|
||||||
font-weight: lighter;
|
|
||||||
}
|
|
||||||
& > &-actions {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
align-items: stretch;
|
|
||||||
justify-content: flex-end;
|
|
||||||
pointer-events: all;
|
|
||||||
> * {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-action,
|
|
||||||
.ve-icon-button.ve-action-save {
|
|
||||||
box-sizing: border-box;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--color-brand, #006cff);
|
|
||||||
opacity: 1;
|
|
||||||
max-height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--color-icon-reverse, white);
|
|
||||||
&:hover {
|
|
||||||
background: var(--color-brand-light, #006cff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.lc-resize-corner {
|
|
||||||
display: inline-block;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border: 1px solid var(--color-brand, #197aff);
|
|
||||||
background: var(--color-block-background-normal, #fff);
|
|
||||||
pointer-events: auto;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.lc-resize-side {
|
|
||||||
border-width: 0;
|
|
||||||
z-index: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
border: 1px solid var(--color-brand, #197aff);
|
|
||||||
background: var(--color-block-background-normal, #fff);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.e,
|
|
||||||
&.w {
|
|
||||||
cursor: ew-resize;
|
|
||||||
&:after {
|
|
||||||
width: 4px;
|
|
||||||
min-height: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.n,
|
|
||||||
&.s {
|
|
||||||
cursor: ns-resize;
|
|
||||||
&:after {
|
|
||||||
min-width: 50%;
|
|
||||||
height: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&&-detecting {
|
|
||||||
z-index: 1;
|
|
||||||
border-style: dashed;
|
|
||||||
background: var(--color-canvas-detecting-background, rgba(0,121,242,.04));
|
|
||||||
}
|
|
||||||
|
|
||||||
&&-selecting {
|
|
||||||
z-index: 2;
|
|
||||||
border-width: 2px;
|
|
||||||
|
|
||||||
&.dragging {
|
|
||||||
background: var(--color-layer-mask-background, rgba(182, 178, 178, 0.8));
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,131 +0,0 @@
|
|||||||
import { ISimulatorHost } from '../../simulator';
|
|
||||||
import { Designer, Point } from '../../designer';
|
|
||||||
import { cursor } from '@alilc/lowcode-utils';
|
|
||||||
import { makeEventsHandler } from '../../utils/misc';
|
|
||||||
import { createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core';
|
|
||||||
|
|
||||||
// 拖动缩放
|
|
||||||
export default class DragResizeEngine {
|
|
||||||
private emitter: IEventBus;
|
|
||||||
|
|
||||||
private dragResizing = false;
|
|
||||||
|
|
||||||
private designer: Designer;
|
|
||||||
|
|
||||||
constructor(designer: Designer) {
|
|
||||||
this.designer = designer;
|
|
||||||
this.emitter = createModuleEventBus('DragResizeEngine');
|
|
||||||
}
|
|
||||||
|
|
||||||
isDragResizing() {
|
|
||||||
return this.dragResizing;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* drag reszie from
|
|
||||||
* @param shell
|
|
||||||
* @param direction n/s/e/w
|
|
||||||
* @param boost (e: MouseEvent) => VE.Node
|
|
||||||
*/
|
|
||||||
from(shell: Element, direction: string, boost: (e: MouseEvent) => any) {
|
|
||||||
let node: any;
|
|
||||||
let startEvent: Point;
|
|
||||||
|
|
||||||
if (!shell) {
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const move = (e: MouseEvent) => {
|
|
||||||
const x = createResizeEvent(e);
|
|
||||||
const moveX = x.clientX - startEvent.clientX;
|
|
||||||
const moveY = x.clientY - startEvent.clientY;
|
|
||||||
|
|
||||||
this.emitter.emit('resize', e, direction, node, moveX, moveY);
|
|
||||||
};
|
|
||||||
|
|
||||||
const masterSensors = this.getMasterSensors();
|
|
||||||
|
|
||||||
/* istanbul ignore next */
|
|
||||||
const createResizeEvent = (e: MouseEvent | DragEvent): Point => {
|
|
||||||
const sourceDocument = e.view?.document;
|
|
||||||
|
|
||||||
if (!sourceDocument || sourceDocument === document) {
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
const srcSim = masterSensors.find((sim) => sim.contentDocument === sourceDocument);
|
|
||||||
if (srcSim) {
|
|
||||||
return srcSim.viewport.toGlobalPoint(e);
|
|
||||||
}
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
const over = (e: MouseEvent) => {
|
|
||||||
const handleEvents = makeEventsHandler(e, masterSensors);
|
|
||||||
handleEvents((doc) => {
|
|
||||||
doc.removeEventListener('mousemove', move, true);
|
|
||||||
doc.removeEventListener('mouseup', over, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.dragResizing = false;
|
|
||||||
this.designer.detecting.enable = true;
|
|
||||||
cursor.release();
|
|
||||||
|
|
||||||
this.emitter.emit('resizeEnd', e, direction, node);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mousedown = (e: MouseEvent) => {
|
|
||||||
node = boost(e);
|
|
||||||
startEvent = createResizeEvent(e);
|
|
||||||
const handleEvents = makeEventsHandler(e, masterSensors);
|
|
||||||
handleEvents((doc) => {
|
|
||||||
doc.addEventListener('mousemove', move, true);
|
|
||||||
doc.addEventListener('mouseup', over, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.emitter.emit('resizeStart', e, direction, node);
|
|
||||||
this.dragResizing = true;
|
|
||||||
this.designer.detecting.enable = false;
|
|
||||||
cursor.addState('ew-resize');
|
|
||||||
};
|
|
||||||
shell.addEventListener('mousedown', mousedown as any);
|
|
||||||
return () => {
|
|
||||||
shell.removeEventListener('mousedown', mousedown as any);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onResizeStart(func: (e: MouseEvent, direction: string, node: any) => any) {
|
|
||||||
this.emitter.on('resizeStart', func);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('resizeStart', func);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onResize(
|
|
||||||
func: (e: MouseEvent, direction: string, node: any, moveX: number, moveY: number) => any,
|
|
||||||
) {
|
|
||||||
this.emitter.on('resize', func);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('resize', func);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onResizeEnd(func: (e: MouseEvent, direction: string, node: any) => any) {
|
|
||||||
this.emitter.on('resizeEnd', func);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('resizeEnd', func);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private getMasterSensors(): ISimulatorHost[] {
|
|
||||||
return this.designer.project.documents
|
|
||||||
.map((doc) => {
|
|
||||||
if (doc.active && doc.simulator?.sensorAvailable) {
|
|
||||||
return doc.simulator;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter(Boolean) as any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// new DragResizeEngine();
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { observer, engineConfig } from '@alilc/lowcode-editor-core';
|
|
||||||
import { BorderDetecting } from './border-detecting';
|
|
||||||
import { BorderContainer } from './border-container';
|
|
||||||
import { BuiltinSimulatorHost } from '../host';
|
|
||||||
import { BorderSelecting } from './border-selecting';
|
|
||||||
import BorderResizing from './border-resizing';
|
|
||||||
import { InsertionView } from './insertion';
|
|
||||||
import './bem-tools.less';
|
|
||||||
import './borders.less';
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class BemTools extends Component<{ host: BuiltinSimulatorHost }> {
|
|
||||||
render() {
|
|
||||||
const { host } = this.props;
|
|
||||||
const { designMode } = host;
|
|
||||||
const { scrollX, scrollY, scale } = host.viewport;
|
|
||||||
if (designMode === 'live') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="lc-bem-tools" style={{ transform: `translate(${-scrollX * scale}px,${-scrollY * scale}px)` }}>
|
|
||||||
{ !engineConfig.get('disableDetecting') && <BorderDetecting key="hovering" host={host} /> }
|
|
||||||
<BorderSelecting key="selecting" host={host} />
|
|
||||||
{ engineConfig.get('enableReactiveContainer') && <BorderContainer key="reactive-container-border" host={host} /> }
|
|
||||||
<InsertionView key="insertion" host={host} />
|
|
||||||
<BorderResizing key="resizing" host={host} />
|
|
||||||
{
|
|
||||||
host.designer.bemToolsManager.getAllBemTools().map(tools => {
|
|
||||||
const ToolsCls = tools.item;
|
|
||||||
return <ToolsCls key={tools.name} host={host} />;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
.lc-insertion {
|
|
||||||
position: absolute;
|
|
||||||
top: -2px;
|
|
||||||
left: 0;
|
|
||||||
z-index: 12;
|
|
||||||
pointer-events: none !important;
|
|
||||||
background-color: var(--color-brand-light);
|
|
||||||
height: 4px;
|
|
||||||
|
|
||||||
&.cover {
|
|
||||||
top: 0;
|
|
||||||
height: auto;
|
|
||||||
width: auto;
|
|
||||||
border: none;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.vertical {
|
|
||||||
top: 0;
|
|
||||||
left: -2px;
|
|
||||||
width: 4px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.invalid {
|
|
||||||
background-color: var(--color-error, var(--color-function-error, red));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,176 +0,0 @@
|
|||||||
import { Component } from 'react';
|
|
||||||
import { observer } from '@alilc/lowcode-editor-core';
|
|
||||||
import { BuiltinSimulatorHost } from '../host';
|
|
||||||
import { DropLocation, isVertical } from '../../designer';
|
|
||||||
import { ISimulatorHost } from '../../simulator';
|
|
||||||
import { INode } from '../../document';
|
|
||||||
import './insertion.less';
|
|
||||||
import {
|
|
||||||
IPublicTypeNodeData,
|
|
||||||
IPublicTypeLocationChildrenDetail,
|
|
||||||
IPublicTypeRect,
|
|
||||||
} from '@alilc/lowcode-types';
|
|
||||||
import { isLocationChildrenDetail } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
interface InsertionData {
|
|
||||||
edge?: DOMRect;
|
|
||||||
insertType?: string;
|
|
||||||
vertical?: boolean;
|
|
||||||
nearRect?: IPublicTypeRect;
|
|
||||||
coverRect?: DOMRect;
|
|
||||||
nearNode?: IPublicTypeNodeData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理拖拽子节点(INode)情况
|
|
||||||
*/
|
|
||||||
function processChildrenDetail(
|
|
||||||
sim: ISimulatorHost,
|
|
||||||
container: INode,
|
|
||||||
detail: IPublicTypeLocationChildrenDetail,
|
|
||||||
): InsertionData {
|
|
||||||
let edge = detail.edge || null;
|
|
||||||
|
|
||||||
if (!edge) {
|
|
||||||
edge = sim.computeRect(container);
|
|
||||||
if (!edge) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ret: any = {
|
|
||||||
edge,
|
|
||||||
insertType: 'before',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (detail.near) {
|
|
||||||
const { node, pos, rect, align } = detail.near;
|
|
||||||
ret.nearRect = rect || sim.computeRect(node as any);
|
|
||||||
ret.nearNode = node;
|
|
||||||
if (pos === 'replace') {
|
|
||||||
// FIXME: ret.nearRect mybe null
|
|
||||||
ret.coverRect = ret.nearRect;
|
|
||||||
ret.insertType = 'cover';
|
|
||||||
} else if (!ret.nearRect || (ret.nearRect.width === 0 && ret.nearRect.height === 0)) {
|
|
||||||
ret.nearRect = ret.edge;
|
|
||||||
ret.insertType = 'after';
|
|
||||||
ret.vertical = isVertical(ret.nearRect);
|
|
||||||
} else {
|
|
||||||
ret.insertType = pos;
|
|
||||||
ret.vertical = align ? align === 'V' : isVertical(ret.nearRect);
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
// from outline-tree: has index, but no near
|
|
||||||
// TODO: think of shadowNode & ConditionFlow
|
|
||||||
const { index } = detail;
|
|
||||||
if (index == null) {
|
|
||||||
ret.coverRect = ret.edge;
|
|
||||||
ret.insertType = 'cover';
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
let nearNode = container.children?.get(index);
|
|
||||||
if (!nearNode) {
|
|
||||||
// index = 0, eg. nochild,
|
|
||||||
nearNode = container.children?.get(index > 0 ? index - 1 : 0);
|
|
||||||
if (!nearNode) {
|
|
||||||
ret.insertType = 'cover';
|
|
||||||
ret.coverRect = edge;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
ret.insertType = 'after';
|
|
||||||
}
|
|
||||||
if (nearNode) {
|
|
||||||
ret.nearRect = sim.computeRect(nearNode);
|
|
||||||
if (!ret.nearRect || (ret.nearRect.width === 0 && ret.nearRect.height === 0)) {
|
|
||||||
ret.nearRect = ret.edge;
|
|
||||||
ret.insertType = 'after';
|
|
||||||
}
|
|
||||||
ret.vertical = isVertical(ret.nearRect);
|
|
||||||
ret.nearNode = nearNode;
|
|
||||||
} else {
|
|
||||||
ret.insertType = 'cover';
|
|
||||||
ret.coverRect = edge;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 detail 信息转换为页面"坐标"信息
|
|
||||||
*/
|
|
||||||
function processDetail({ target, detail, document }: DropLocation): InsertionData {
|
|
||||||
const sim = document!.simulator;
|
|
||||||
if (!sim) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
if (isLocationChildrenDetail(detail)) {
|
|
||||||
return processChildrenDetail(sim, target, detail);
|
|
||||||
} else {
|
|
||||||
// TODO: others...
|
|
||||||
const instances = sim.getComponentInstances(target);
|
|
||||||
if (!instances) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const edge = sim.computeComponentInstanceRect(instances[0], target.componentMeta.rootSelector);
|
|
||||||
return edge ? { edge, insertType: 'cover', coverRect: edge } : {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class InsertionView extends Component<{ host: BuiltinSimulatorHost }> {
|
|
||||||
render() {
|
|
||||||
const { host } = this.props;
|
|
||||||
const loc = host.currentDocument?.dropLocation;
|
|
||||||
if (!loc) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// 如果是个绝对定位容器,不需要渲染插入标记
|
|
||||||
if (loc.target?.componentMeta?.advanced.isAbsoluteLayoutContainer) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { scale, scrollX, scrollY } = host.viewport;
|
|
||||||
const { edge, insertType, coverRect, nearRect, vertical, nearNode } = processDetail(loc as any);
|
|
||||||
|
|
||||||
if (!edge) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let className = 'lc-insertion';
|
|
||||||
if ((loc.detail as any)?.valid === false) {
|
|
||||||
className += ' invalid';
|
|
||||||
}
|
|
||||||
const style: any = {};
|
|
||||||
let x: number;
|
|
||||||
let y: number;
|
|
||||||
if (insertType === 'cover') {
|
|
||||||
className += ' cover';
|
|
||||||
x = (coverRect!.left + scrollX) * scale;
|
|
||||||
y = (coverRect!.top + scrollY) * scale;
|
|
||||||
style.width = coverRect!.width * scale;
|
|
||||||
style.height = coverRect!.height * scale;
|
|
||||||
} else {
|
|
||||||
if (!nearRect) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (vertical) {
|
|
||||||
className += ' vertical';
|
|
||||||
x = ((insertType === 'before' ? nearRect.left : nearRect.right) + scrollX) * scale;
|
|
||||||
y = (nearRect.top + scrollY) * scale;
|
|
||||||
style.height = nearRect!.height * scale;
|
|
||||||
} else {
|
|
||||||
x = (nearRect.left + scrollX) * scale;
|
|
||||||
y = ((insertType === 'before' ? nearRect.top : nearRect.bottom) + scrollY) * scale;
|
|
||||||
style.width = nearRect.width * scale;
|
|
||||||
}
|
|
||||||
if (y === 0 && (nearNode as any)?.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} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { ComponentType } from 'react';
|
|
||||||
import { Designer } from '../../designer';
|
|
||||||
import { invariant } from '../../utils';
|
|
||||||
import { BuiltinSimulatorHost } from '../../builtin-simulator/host';
|
|
||||||
|
|
||||||
export type BemToolsData = {
|
|
||||||
name: string;
|
|
||||||
item: ComponentType<{ host: BuiltinSimulatorHost }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class BemToolsManager {
|
|
||||||
private designer: Designer;
|
|
||||||
|
|
||||||
private toolsContainer: BemToolsData[] = [];
|
|
||||||
|
|
||||||
constructor(designer: Designer) {
|
|
||||||
this.designer = designer;
|
|
||||||
}
|
|
||||||
|
|
||||||
addBemTools(toolsData: BemToolsData) {
|
|
||||||
const found = this.toolsContainer.find(item => item.name === toolsData.name);
|
|
||||||
invariant(!found, `${toolsData.name} already exists`);
|
|
||||||
|
|
||||||
this.toolsContainer.push(toolsData);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeBemTools(name: string) {
|
|
||||||
const index = this.toolsContainer.findIndex(item => item.name === name);
|
|
||||||
if (index !== -1) {
|
|
||||||
this.toolsContainer.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllBemTools() {
|
|
||||||
return this.toolsContainer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import { createContext } from 'react';
|
|
||||||
import { BuiltinSimulatorHost } from './host';
|
|
||||||
|
|
||||||
export const SimulatorContext = createContext<BuiltinSimulatorHost>({} as any);
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
// NOTE: 仅用作类型标注,切勿作为实体使用
|
|
||||||
import { BuiltinSimulatorHost } from './host';
|
|
||||||
import {
|
|
||||||
AssetLevel,
|
|
||||||
AssetLevels,
|
|
||||||
AssetList,
|
|
||||||
isAssetBundle,
|
|
||||||
isAssetItem,
|
|
||||||
AssetType,
|
|
||||||
assetItem,
|
|
||||||
isCSSUrl,
|
|
||||||
} from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
import { BuiltinSimulatorRenderer } from './renderer';
|
|
||||||
|
|
||||||
export function createSimulator(
|
|
||||||
host: BuiltinSimulatorHost,
|
|
||||||
iframe: HTMLIFrameElement,
|
|
||||||
vendors: AssetList = [],
|
|
||||||
): Promise<BuiltinSimulatorRenderer> {
|
|
||||||
const win: any = iframe.contentWindow;
|
|
||||||
const doc = iframe.contentDocument!;
|
|
||||||
const innerPlugins = host.designer.editor.get('innerPlugins');
|
|
||||||
|
|
||||||
win.AliLowCodeEngine = innerPlugins._getLowCodePluginContext({});
|
|
||||||
win.LCSimulatorHost = host;
|
|
||||||
win._ = window._;
|
|
||||||
|
|
||||||
const styles: any = {};
|
|
||||||
const scripts: any = {};
|
|
||||||
AssetLevels.forEach((lv) => {
|
|
||||||
styles[lv] = [];
|
|
||||||
scripts[lv] = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
function parseAssetList(assets: AssetList, level?: AssetLevel) {
|
|
||||||
for (let asset of assets) {
|
|
||||||
if (!asset) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (isAssetBundle(asset)) {
|
|
||||||
if (asset.assets) {
|
|
||||||
parseAssetList(
|
|
||||||
Array.isArray(asset.assets) ? asset.assets : [asset.assets],
|
|
||||||
asset.level || level,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (Array.isArray(asset)) {
|
|
||||||
parseAssetList(asset, level);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!isAssetItem(asset)) {
|
|
||||||
asset = assetItem(isCSSUrl(asset) ? AssetType.CSSUrl : AssetType.JSUrl, asset, level)!;
|
|
||||||
}
|
|
||||||
const id = asset.id ? ` data-id="${asset.id}"` : '';
|
|
||||||
const lv = asset.level || level || AssetLevel.Environment;
|
|
||||||
const scriptType = asset.scriptType ? ` type="${asset.scriptType}"` : '';
|
|
||||||
if (asset.type === AssetType.JSUrl) {
|
|
||||||
scripts[lv].push(
|
|
||||||
`<script src="${asset.content}"${id}${scriptType}></script>`,
|
|
||||||
);
|
|
||||||
} else if (asset.type === AssetType.JSText) {
|
|
||||||
scripts[lv].push(`<script${id}${scriptType}>${asset.content}</script>`);
|
|
||||||
} else if (asset.type === AssetType.CSSUrl) {
|
|
||||||
styles[lv].push(
|
|
||||||
`<link rel="stylesheet" href="${asset.content}"${id} />`,
|
|
||||||
);
|
|
||||||
} else if (asset.type === AssetType.CSSText) {
|
|
||||||
styles[lv].push(
|
|
||||||
`<style type="text/css"${id}>${asset.content}</style>`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseAssetList(vendors);
|
|
||||||
|
|
||||||
const styleFrags = Object.keys(styles)
|
|
||||||
.map((key) => {
|
|
||||||
return `${styles[key].join('\n')}<meta level="${key}" />`;
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
const scriptFrags = Object.keys(scripts)
|
|
||||||
.map((key) => {
|
|
||||||
return scripts[key].join('\n');
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
doc.open();
|
|
||||||
doc.write(`
|
|
||||||
<!doctype html>
|
|
||||||
<html class="engine-design-mode">
|
|
||||||
<head><meta charset="utf-8"/>
|
|
||||||
${styleFrags}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
${scriptFrags}
|
|
||||||
</body>
|
|
||||||
</html>`);
|
|
||||||
doc.close();
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const renderer = win.SimulatorRenderer;
|
|
||||||
if (renderer) {
|
|
||||||
return resolve(renderer);
|
|
||||||
}
|
|
||||||
const loaded = () => {
|
|
||||||
resolve(win.SimulatorRenderer || host.renderer);
|
|
||||||
win.removeEventListener('load', loaded);
|
|
||||||
};
|
|
||||||
win.addEventListener('load', loaded);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { observer } from '@alilc/lowcode-editor-core';
|
|
||||||
import { BuiltinSimulatorHost, BuiltinSimulatorProps } from './host';
|
|
||||||
import { BemTools } from './bem-tools';
|
|
||||||
import { Project } from '../project';
|
|
||||||
import './host.less';
|
|
||||||
|
|
||||||
/*
|
|
||||||
Simulator 模拟器,可替换部件,有协议约束,包含画布的容器,使用场景:当 Canvas 大小变化时,用来居中处理 或 定位 Canvas
|
|
||||||
Canvas(DeviceShell) 设备壳层,通过背景图片来模拟,通过设备预设样式改变宽度、高度及定位 CanvasViewport
|
|
||||||
CanvasViewport 页面编排场景中宽高不可溢出 Canvas 区
|
|
||||||
Content(Shell) 内容外层,宽高紧贴 CanvasViewport,禁用边框,禁用 margin
|
|
||||||
BemTools 辅助显示层,初始相对 Content 位置 0,0,紧贴 Canvas, 根据 Content 滚动位置,改变相对位置
|
|
||||||
*/
|
|
||||||
|
|
||||||
type SimulatorHostProps = BuiltinSimulatorProps & {
|
|
||||||
project: Project;
|
|
||||||
onMount?: (host: BuiltinSimulatorHost) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class BuiltinSimulatorHostView extends Component<SimulatorHostProps> {
|
|
||||||
readonly host: BuiltinSimulatorHost;
|
|
||||||
|
|
||||||
constructor(props: any) {
|
|
||||||
super(props);
|
|
||||||
const { project, onMount, designer } = this.props;
|
|
||||||
this.host = (project.simulator as BuiltinSimulatorHost) || new BuiltinSimulatorHost(project, designer);
|
|
||||||
this.host.setProps(this.props);
|
|
||||||
onMount?.(this.host);
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: BuiltinSimulatorProps) {
|
|
||||||
this.host.setProps(nextProps);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="lc-simulator">
|
|
||||||
{/* progressing.visible ? <PreLoaderView /> : null */}
|
|
||||||
<Canvas host={this.host} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
class Canvas extends Component<{ host: BuiltinSimulatorHost }> {
|
|
||||||
render() {
|
|
||||||
const sim = this.props.host;
|
|
||||||
let className = 'lc-simulator-canvas';
|
|
||||||
const { canvas = {}, viewport = {} } = sim.deviceStyle || {};
|
|
||||||
if (sim.deviceClassName) {
|
|
||||||
className += ` ${sim.deviceClassName}`;
|
|
||||||
} else if (sim.device) {
|
|
||||||
className += ` lc-simulator-device-${sim.device}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className} style={canvas}>
|
|
||||||
<div ref={(elmt) => sim.mountViewport(elmt)} className="lc-simulator-canvas-viewport" style={viewport}>
|
|
||||||
<BemTools host={sim} />
|
|
||||||
<Content host={sim} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
class Content extends Component<{ host: BuiltinSimulatorHost }> {
|
|
||||||
state = {
|
|
||||||
disabledEvents: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
private dispose?: () => void;
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const editor = this.props.host.designer.editor;
|
|
||||||
const onEnableEvents = (type: boolean) => {
|
|
||||||
this.setState({
|
|
||||||
disabledEvents: type,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.eventBus.on('designer.builtinSimulator.disabledEvents', onEnableEvents);
|
|
||||||
|
|
||||||
this.dispose = () => {
|
|
||||||
editor.removeListener('designer.builtinSimulator.disabledEvents', onEnableEvents);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.dispose?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const sim = this.props.host;
|
|
||||||
const { disabledEvents } = this.state;
|
|
||||||
const { viewport, designer } = sim;
|
|
||||||
const frameStyle: any = {
|
|
||||||
transform: `scale(${viewport.scale})`,
|
|
||||||
height: viewport.contentHeight,
|
|
||||||
width: viewport.contentWidth,
|
|
||||||
};
|
|
||||||
if (disabledEvents) {
|
|
||||||
frameStyle.pointerEvents = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
const { viewName } = designer;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="lc-simulator-content">
|
|
||||||
<iframe
|
|
||||||
name={`${viewName}-SimulatorRenderer`}
|
|
||||||
className="lc-simulator-content-frame"
|
|
||||||
style={frameStyle}
|
|
||||||
ref={(frame) => sim.mountContentFrame(frame)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
@scope: lc-simulator;
|
|
||||||
|
|
||||||
.@{scope} {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
&-canvas {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&-viewport {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-device-mobile {
|
|
||||||
left: 50%;
|
|
||||||
width: 375px;
|
|
||||||
top: 16px;
|
|
||||||
bottom: 16px;
|
|
||||||
max-height: calc(100% - 32px);
|
|
||||||
max-width: calc(100% - 32px);
|
|
||||||
transform: translateX(-50%);
|
|
||||||
box-shadow: 0 2px 10px 0 var(--color-block-background-shallow, rgba(31,56,88,.15));
|
|
||||||
}
|
|
||||||
|
|
||||||
&-device-iphonex { // 增加默认的小程序的壳
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 375px;
|
|
||||||
height: 812px;
|
|
||||||
max-height: calc(100vh - 50px);
|
|
||||||
background: url(https://img.alicdn.com/tfs/TB1b4DHilFR4u4jSZFPXXanzFXa-750-1574.png) no-repeat top;
|
|
||||||
background-size: 375px 812px;
|
|
||||||
border-radius: 44px;
|
|
||||||
box-shadow: var(--color-block-background-shallow, rgba(0, 0, 0, 0.1)) 0 36px 42px;
|
|
||||||
.@{scope}-canvas-viewport {
|
|
||||||
width: auto;
|
|
||||||
top: 50px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
margin-top: 40px;
|
|
||||||
max-height: 688px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-device-iphone6 {
|
|
||||||
left: 50%;
|
|
||||||
width: 375px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
background: url(https://img.alicdn.com/tps/TB12GetLpXXXXXhXFXXXXXXXXXX-756-1544.png) no-repeat top;
|
|
||||||
background-size: 375px 772px;
|
|
||||||
top: 8px;
|
|
||||||
.@{scope}-canvas-viewport {
|
|
||||||
width: auto;
|
|
||||||
top: 114px;
|
|
||||||
left: 25px;
|
|
||||||
right: 25px;
|
|
||||||
max-height: 561px;
|
|
||||||
border-radius: 0 0 2px 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-device-default {
|
|
||||||
top: var(--simulator-top-distance, 16px);
|
|
||||||
right: var(--simulator-right-distance, 16px);
|
|
||||||
bottom: var(--simulator-bottom-distance, 16px);
|
|
||||||
left: var(--simulator-left-distance, 16px);
|
|
||||||
width: auto;
|
|
||||||
box-shadow: 0 1px 4px 0 var(--color-block-background-shallow, rgba(31, 50, 88, 0.125));
|
|
||||||
}
|
|
||||||
|
|
||||||
&-content {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
&-frame {
|
|
||||||
border: none;
|
|
||||||
transform-origin: 0 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +0,0 @@
|
|||||||
export * from './host';
|
|
||||||
export * from './host-view';
|
|
||||||
export * from './renderer';
|
|
||||||
export * from './live-editing/live-editing';
|
|
||||||
export { LowcodeTypes } from './utils/parse-metadata';
|
|
||||||
@ -1,245 +0,0 @@
|
|||||||
import { observable } from '@alilc/lowcode-editor-core';
|
|
||||||
import { IPublicTypePluginConfig, IPublicTypeLiveTextEditingConfig } from '@alilc/lowcode-types';
|
|
||||||
import { INode, Prop } from '../../document';
|
|
||||||
|
|
||||||
const EDITOR_KEY = 'data-setter-prop';
|
|
||||||
|
|
||||||
function getSetterPropElement(ele: HTMLElement, root: HTMLElement): HTMLElement | null {
|
|
||||||
const box = ele.closest(`[${EDITOR_KEY}]`);
|
|
||||||
if (!box || !root.contains(box)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return box as HTMLElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultSaveContent(content: string, prop: Prop) {
|
|
||||||
prop.setValue(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EditingTarget {
|
|
||||||
node: INode;
|
|
||||||
rootElement: HTMLElement;
|
|
||||||
event: MouseEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
let saveHandlers: SaveHandler[] = [];
|
|
||||||
function addLiveEditingSaveHandler(handler: SaveHandler) {
|
|
||||||
saveHandlers.push(handler);
|
|
||||||
}
|
|
||||||
function clearLiveEditingSaveHandler() {
|
|
||||||
saveHandlers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
let specificRules: SpecificRule[] = [];
|
|
||||||
function addLiveEditingSpecificRule(rule: SpecificRule) {
|
|
||||||
specificRules.push(rule);
|
|
||||||
}
|
|
||||||
function clearLiveEditingSpecificRule() {
|
|
||||||
specificRules = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LiveEditing {
|
|
||||||
static addLiveEditingSpecificRule = addLiveEditingSpecificRule;
|
|
||||||
static clearLiveEditingSpecificRule = clearLiveEditingSpecificRule;
|
|
||||||
|
|
||||||
static addLiveEditingSaveHandler = addLiveEditingSaveHandler;
|
|
||||||
static clearLiveEditingSaveHandler = clearLiveEditingSaveHandler;
|
|
||||||
|
|
||||||
@observable.ref private _editing: Prop | null = null;
|
|
||||||
|
|
||||||
private _dispose?: () => void;
|
|
||||||
|
|
||||||
private _save?: () => void;
|
|
||||||
|
|
||||||
apply(target: EditingTarget) {
|
|
||||||
const { node, event, rootElement } = target;
|
|
||||||
const targetElement = event.target as HTMLElement;
|
|
||||||
const { liveTextEditing } = node.componentMeta;
|
|
||||||
|
|
||||||
const editor = node.document?.designer.editor;
|
|
||||||
const npm = node?.componentMeta?.npm;
|
|
||||||
const selected =
|
|
||||||
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||
|
|
||||||
node?.componentMeta?.componentName ||
|
|
||||||
'';
|
|
||||||
editor?.eventBus.emit('designer.builtinSimulator.liveEditing', {
|
|
||||||
selected,
|
|
||||||
});
|
|
||||||
|
|
||||||
let setterPropElement = getSetterPropElement(targetElement, rootElement);
|
|
||||||
let propTarget = setterPropElement?.dataset.setterProp;
|
|
||||||
let matched: (IPublicTypePluginConfig & { propElement?: HTMLElement }) | undefined | null | any;
|
|
||||||
if (liveTextEditing) {
|
|
||||||
if (propTarget) {
|
|
||||||
// 已埋点命中 data-setter-prop="proptarget", 从 liveTextEditing 读取配置(mode|onSaveContent)
|
|
||||||
matched = liveTextEditing.find((config) => config.propTarget == propTarget);
|
|
||||||
} else {
|
|
||||||
// 执行 embedTextEditing selector 规则,获得第一个节点 是否 contains e.target,若匹配,读取配置
|
|
||||||
matched = liveTextEditing.find((config) => {
|
|
||||||
if (!config.selector) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
setterPropElement = queryPropElement(rootElement, targetElement, config.selector);
|
|
||||||
return !!setterPropElement;
|
|
||||||
});
|
|
||||||
propTarget = matched?.propTarget;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
specificRules.some((rule) => {
|
|
||||||
matched = rule(target);
|
|
||||||
return !!matched;
|
|
||||||
});
|
|
||||||
if (matched) {
|
|
||||||
propTarget = matched.propTarget;
|
|
||||||
setterPropElement =
|
|
||||||
matched.propElement || queryPropElement(rootElement, targetElement, matched.selector);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (!propTarget) {
|
|
||||||
// // 自动纯文本编辑满足一下情况:
|
|
||||||
// // 1. children 内容都是 Leaf 且都是文本(一期)
|
|
||||||
// // 2. DOM 节点是单层容器,子集都是文本节点 (已满足)
|
|
||||||
// const isAllText = node.children?.every(item => {
|
|
||||||
// return item.isLeaf() && item.getProp('children')?.type === 'literal';
|
|
||||||
// });
|
|
||||||
// // TODO:
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (propTarget && setterPropElement) {
|
|
||||||
const prop = node.getProp(propTarget, true)!;
|
|
||||||
|
|
||||||
if (this._editing === prop) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 进入编辑
|
|
||||||
// 1. 设置 contentEditable="plaintext|..."
|
|
||||||
// 2. 添加类名
|
|
||||||
// 3. focus & cursor locate
|
|
||||||
// 4. 监听 blur 事件
|
|
||||||
// 5. 设置编辑锁定:disable hover | disable select | disable canvas drag
|
|
||||||
|
|
||||||
const onSaveContent =
|
|
||||||
matched?.onSaveContent ||
|
|
||||||
saveHandlers.find((item) => item.condition(prop as any))?.onSaveContent ||
|
|
||||||
defaultSaveContent;
|
|
||||||
|
|
||||||
setterPropElement.setAttribute(
|
|
||||||
'contenteditable',
|
|
||||||
matched?.mode && matched.mode !== 'plaintext' ? 'true' : 'plaintext-only',
|
|
||||||
);
|
|
||||||
setterPropElement.classList.add('engine-live-editing');
|
|
||||||
// be sure
|
|
||||||
setterPropElement.focus();
|
|
||||||
setCaret(event);
|
|
||||||
|
|
||||||
this._save = () => {
|
|
||||||
onSaveContent(setterPropElement!.innerText, prop);
|
|
||||||
};
|
|
||||||
|
|
||||||
const keydown = (e: KeyboardEvent) => {
|
|
||||||
console.info(e.code);
|
|
||||||
switch (e.code) {
|
|
||||||
case 'Enter':
|
|
||||||
break;
|
|
||||||
// TODO: check is richtext?
|
|
||||||
case 'Escape':
|
|
||||||
break;
|
|
||||||
case 'Tab':
|
|
||||||
setterPropElement?.blur();
|
|
||||||
}
|
|
||||||
// esc
|
|
||||||
// enter
|
|
||||||
// tab
|
|
||||||
};
|
|
||||||
const focusout = (/* e: FocusEvent */) => {
|
|
||||||
this.saveAndDispose();
|
|
||||||
};
|
|
||||||
setterPropElement.addEventListener('focusout', focusout);
|
|
||||||
setterPropElement.addEventListener('keydown', keydown, true);
|
|
||||||
|
|
||||||
this._dispose = () => {
|
|
||||||
setterPropElement!.classList.remove('engine-live-editing');
|
|
||||||
setterPropElement!.removeAttribute('contenteditable');
|
|
||||||
setterPropElement!.removeEventListener('focusout', focusout);
|
|
||||||
setterPropElement!.removeEventListener('keydown', keydown, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
this._editing = prop as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: process enter | esc events & joint the FocusTracker
|
|
||||||
|
|
||||||
// TODO: upward testing for b/i/a html elements
|
|
||||||
}
|
|
||||||
|
|
||||||
get editing() {
|
|
||||||
return this._editing;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveAndDispose() {
|
|
||||||
if (this._save) {
|
|
||||||
this._save();
|
|
||||||
this._save = undefined;
|
|
||||||
}
|
|
||||||
this.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
if (this._dispose) {
|
|
||||||
this._dispose();
|
|
||||||
this._dispose = undefined;
|
|
||||||
}
|
|
||||||
this._editing = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SpecificRule = (target: EditingTarget) =>
|
|
||||||
| (IPublicTypeLiveTextEditingConfig & {
|
|
||||||
propElement?: HTMLElement;
|
|
||||||
})
|
|
||||||
| null;
|
|
||||||
|
|
||||||
export interface SaveHandler {
|
|
||||||
condition: (prop: Prop) => boolean;
|
|
||||||
onSaveContent: (content: string, prop: Prop) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCaret(event: MouseEvent) {
|
|
||||||
const doc = event.view?.document;
|
|
||||||
if (!doc) return;
|
|
||||||
const range = doc.caretRangeFromPoint(event.clientX, event.clientY);
|
|
||||||
if (range) {
|
|
||||||
selectRange(doc, range);
|
|
||||||
setTimeout(() => selectRange(doc, range), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectRange(doc: Document, range: Range) {
|
|
||||||
const selection = doc.getSelection();
|
|
||||||
if (selection) {
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function queryPropElement(rootElement: HTMLElement, targetElement: HTMLElement, selector?: string) {
|
|
||||||
if (!selector) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let propElement = selector === ':root' ? rootElement : rootElement.querySelector(selector);
|
|
||||||
if (!propElement) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!propElement.contains(targetElement)) {
|
|
||||||
// try selectorAll
|
|
||||||
propElement = Array.from(rootElement.querySelectorAll(selector)).find((item) =>
|
|
||||||
item.contains(targetElement),
|
|
||||||
) as HTMLElement;
|
|
||||||
if (!propElement) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return propElement as HTMLElement;
|
|
||||||
}
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
@import '../../less-variables.less';
|
|
||||||
|
|
||||||
// 样式直接沿用之前的样式,优化了下命名
|
|
||||||
.instance-node-selector {
|
|
||||||
position: relative;
|
|
||||||
margin-right: 2px;
|
|
||||||
color: var(--color-icon-white, @title-bgcolor);
|
|
||||||
border-radius: @global-border-radius;
|
|
||||||
pointer-events: auto;
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin-right: 5px;
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
max-width: inherit;
|
|
||||||
path {
|
|
||||||
fill: var(--color-icon-white, @title-bgcolor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.instance-node-selector-current {
|
|
||||||
background: var(--color-brand, @brand-color-1);
|
|
||||||
padding: 0 6px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--color-icon-white, @title-bgcolor);
|
|
||||||
border-radius: 3px;
|
|
||||||
|
|
||||||
&-title {
|
|
||||||
padding-right: 6px;
|
|
||||||
color: var(--color-icon-white, @title-bgcolor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.instance-node-selector-list {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
.instance-node-selector-node {
|
|
||||||
height: 20px;
|
|
||||||
margin-top: 2px;
|
|
||||||
&-content {
|
|
||||||
padding-left: 6px;
|
|
||||||
background: var(--color-layer-tooltip-background, #78869a);
|
|
||||||
display: inline-flex;
|
|
||||||
border-radius: 3px;
|
|
||||||
align-items: center;
|
|
||||||
height: 20px;
|
|
||||||
color: var(--color-icon-white, @title-bgcolor);
|
|
||||||
cursor: pointer;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
&-title {
|
|
||||||
padding-right: 6px;
|
|
||||||
// margin-left: 5px;
|
|
||||||
color: var(--color-icon-white, @title-bgcolor);
|
|
||||||
cursor: pointer;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.instance-node-selector-current {
|
|
||||||
color: ar(--color-text-reverse, @white-alpha-2);
|
|
||||||
}
|
|
||||||
.instance-node-selector-popup {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
transition: 0.2s all ease-in;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,153 +0,0 @@
|
|||||||
import { Overlay } from '@alifd/next';
|
|
||||||
import React, { MouseEvent } from 'react';
|
|
||||||
import { observer } from '@alilc/lowcode-editor-core';
|
|
||||||
import { canClickNode } from '@alilc/lowcode-utils';
|
|
||||||
import { INode } from '../../document';
|
|
||||||
import { Title } from '../../widgets';
|
|
||||||
|
|
||||||
import './index.less';
|
|
||||||
|
|
||||||
const { Popup } = Overlay;
|
|
||||||
|
|
||||||
export interface IProps {
|
|
||||||
node: INode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IState {
|
|
||||||
parentNodes: INode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnionNode = INode | null;
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export default class InstanceNodeSelector extends React.Component<IProps, IState> {
|
|
||||||
state: IState = {
|
|
||||||
parentNodes: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const parentNodes = this.getParentNodes(this.props.node);
|
|
||||||
this.setState({
|
|
||||||
parentNodes: parentNodes ?? [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取节点的父级节点(最多获取 5 层)
|
|
||||||
getParentNodes = (node: INode) => {
|
|
||||||
const parentNodes: any[] = [];
|
|
||||||
const focusNode = node.document?.focusNode;
|
|
||||||
|
|
||||||
if (!focusNode) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.contains(focusNode) || !focusNode.contains(node)) {
|
|
||||||
return parentNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentNode: UnionNode = node;
|
|
||||||
|
|
||||||
while (currentNode && parentNodes.length < 5) {
|
|
||||||
currentNode = currentNode.getParent();
|
|
||||||
if (currentNode) {
|
|
||||||
parentNodes.push(currentNode);
|
|
||||||
}
|
|
||||||
if (currentNode === focusNode) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parentNodes;
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelect = (node: INode) => (event: MouseEvent) => {
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const canClick = canClickNode(node.internalToShellNode()!, event);
|
|
||||||
|
|
||||||
if (canClick && typeof node.select === 'function') {
|
|
||||||
node.select();
|
|
||||||
const editor = node.document?.designer.editor;
|
|
||||||
const npm = node?.componentMeta?.npm;
|
|
||||||
const selected =
|
|
||||||
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||
|
|
||||||
node?.componentMeta?.componentName ||
|
|
||||||
'';
|
|
||||||
editor?.eventBus.emit('designer.border.action', {
|
|
||||||
name: 'select',
|
|
||||||
selected,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMouseOver =
|
|
||||||
(node: INode) =>
|
|
||||||
(_: any, flag = true) => {
|
|
||||||
if (node && typeof node.hover === 'function') {
|
|
||||||
node.hover(flag);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMouseOut =
|
|
||||||
(node: INode) =>
|
|
||||||
(_: any, flag = false) => {
|
|
||||||
if (node && typeof node.hover === 'function') {
|
|
||||||
node.hover(flag);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderNodes = () => {
|
|
||||||
const nodes = this.state.parentNodes;
|
|
||||||
if (!nodes || nodes.length < 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const children = nodes.map((node, key) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={key}
|
|
||||||
onClick={this.onSelect(node)}
|
|
||||||
onMouseEnter={this.onMouseOver(node)}
|
|
||||||
onMouseLeave={this.onMouseOut(node)}
|
|
||||||
className="instance-node-selector-node"
|
|
||||||
>
|
|
||||||
<div className="instance-node-selector-node-content">
|
|
||||||
<Title
|
|
||||||
className="instance-node-selector-node-title"
|
|
||||||
title={{
|
|
||||||
label: node.title,
|
|
||||||
icon: node.icon,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return children;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { node } = this.props;
|
|
||||||
return (
|
|
||||||
<div className="instance-node-selector">
|
|
||||||
<Popup
|
|
||||||
trigger={
|
|
||||||
<div className="instance-node-selector-current">
|
|
||||||
<Title
|
|
||||||
className="instance-node-selector-node-title"
|
|
||||||
title={{
|
|
||||||
label: node.title,
|
|
||||||
icon: node.icon,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
triggerType="hover"
|
|
||||||
offset={[0, 0]}
|
|
||||||
>
|
|
||||||
<div className="instance-node-selector">{this.renderNodes()}</div>
|
|
||||||
</Popup>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { Component } from '../simulator';
|
|
||||||
import { IPublicTypeComponentInstance, IPublicTypeSimulatorRenderer } from '@alilc/lowcode-types';
|
|
||||||
|
|
||||||
export type BuiltinSimulatorRenderer = IPublicTypeSimulatorRenderer<Component, IPublicTypeComponentInstance>;
|
|
||||||
|
|
||||||
export function isSimulatorRenderer(obj: any): obj is BuiltinSimulatorRenderer {
|
|
||||||
return obj && obj.isSimulatorRenderer;
|
|
||||||
}
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
import { autorun, makeObservable, observable, createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core';
|
|
||||||
import { BuiltinSimulatorHost } from './host';
|
|
||||||
import { BuiltinSimulatorRenderer, isSimulatorRenderer } from './renderer';
|
|
||||||
|
|
||||||
const UNSET = Symbol('unset');
|
|
||||||
export type MasterProvider = (master: BuiltinSimulatorHost) => any;
|
|
||||||
export type RendererConsumer<T> = (renderer: BuiltinSimulatorRenderer, data: T) => Promise<any>;
|
|
||||||
|
|
||||||
// master 进程
|
|
||||||
// 0. 初始化该对象,因为需要响应变更发生在 master 进程
|
|
||||||
// 1. 提供消费数据或数据提供器,比如 Asset 资源,如果不是数据提供器,会持续提供
|
|
||||||
// 2. 收到成功通知
|
|
||||||
// renderer 进程
|
|
||||||
// 1. 持续消费,并持续监听数据
|
|
||||||
// 2. 消费
|
|
||||||
|
|
||||||
// 这里涉及俩个自定义项
|
|
||||||
// 1. 被消费数据协议
|
|
||||||
// 2. 消费机制(渲染进程自定 + 传递进入)
|
|
||||||
|
|
||||||
export default class ResourceConsumer<T = any> {
|
|
||||||
private emitter: IEventBus = createModuleEventBus('ResourceConsumer');
|
|
||||||
|
|
||||||
@observable.ref private _data: T | typeof UNSET = UNSET;
|
|
||||||
|
|
||||||
private _providing?: () => void;
|
|
||||||
|
|
||||||
private _consuming?: () => void;
|
|
||||||
|
|
||||||
private _firstConsumed = false;
|
|
||||||
|
|
||||||
private resolveFirst?: (resolve?: any) => void;
|
|
||||||
|
|
||||||
constructor(provider: () => T, private consumer?: RendererConsumer<T>) {
|
|
||||||
makeObservable(this);
|
|
||||||
this._providing = autorun(() => {
|
|
||||||
this._data = provider();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
consume(consumerOrRenderer: BuiltinSimulatorRenderer | ((data: T) => any)) {
|
|
||||||
if (this._consuming) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let consumer: (data: T) => any;
|
|
||||||
if (isSimulatorRenderer(consumerOrRenderer)) {
|
|
||||||
if (!this.consumer) {
|
|
||||||
// TODO: throw error
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rendererConsumer = this.consumer!;
|
|
||||||
|
|
||||||
consumer = (data) => rendererConsumer(consumerOrRenderer, data);
|
|
||||||
} else {
|
|
||||||
consumer = consumerOrRenderer;
|
|
||||||
}
|
|
||||||
this._consuming = autorun(async () => {
|
|
||||||
if (this._data === UNSET) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await consumer(this._data);
|
|
||||||
// TODO: catch error and report
|
|
||||||
if (this.resolveFirst) {
|
|
||||||
this.resolveFirst();
|
|
||||||
} else {
|
|
||||||
this._firstConsumed = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
if (this._providing) {
|
|
||||||
this._providing();
|
|
||||||
}
|
|
||||||
if (this._consuming) {
|
|
||||||
this._consuming();
|
|
||||||
}
|
|
||||||
this.emitter.removeAllListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
waitFirstConsume(): Promise<any> {
|
|
||||||
if (this._firstConsumed) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.resolveFirst = resolve;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import { getClosestNode, canClickNode } from '@alilc/lowcode-utils';
|
|
||||||
import { INode } from '../../document';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取离当前节点最近的可点击节点
|
|
||||||
* @param currentNode
|
|
||||||
* @param event
|
|
||||||
*/
|
|
||||||
export const getClosestClickableNode = (
|
|
||||||
currentNode: INode | undefined | null,
|
|
||||||
event: MouseEvent,
|
|
||||||
) => {
|
|
||||||
let node = currentNode as any;
|
|
||||||
while (node) {
|
|
||||||
// 判断当前节点是否可点击
|
|
||||||
let canClick = canClickNode(node, event as any);
|
|
||||||
// eslint-disable-next-line no-loop-func
|
|
||||||
const lockedNode: any = getClosestNode(node, (n) => {
|
|
||||||
// 假如当前节点就是 locked 状态,要从当前节点的父节点开始查找
|
|
||||||
return !!(node?.isLocked ? n.parent?.isLocked : n.isLocked);
|
|
||||||
});
|
|
||||||
if (lockedNode && lockedNode.getId() !== node.getId()) {
|
|
||||||
canClick = false;
|
|
||||||
}
|
|
||||||
if (canClick) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// 对于不可点击的节点,继续向上找
|
|
||||||
node = node.parent;
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
};
|
|
||||||
@ -1,219 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import { isValidElement } from 'react';
|
|
||||||
import { isElement } from '@alilc/lowcode-utils';
|
|
||||||
import { IPublicTypePropConfig } from '@alilc/lowcode-types';
|
|
||||||
|
|
||||||
export const primitiveTypes = [
|
|
||||||
'string',
|
|
||||||
'number',
|
|
||||||
'array',
|
|
||||||
'bool',
|
|
||||||
'func',
|
|
||||||
'object',
|
|
||||||
'node',
|
|
||||||
'element',
|
|
||||||
'symbol',
|
|
||||||
'any',
|
|
||||||
];
|
|
||||||
|
|
||||||
interface LowcodeCheckType {
|
|
||||||
// isRequired, props, propName, componentName, location, propFullName, secret
|
|
||||||
(props: any, propName: string, componentName: string, ...rest: any[]): Error | null;
|
|
||||||
// (...reset: any[]): Error | null;
|
|
||||||
isRequired?: LowcodeCheckType;
|
|
||||||
type?: string | object;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
function makeRequired(propType: any, lowcodeType: string | object): LowcodeCheckType {
|
|
||||||
function lowcodeCheckTypeIsRequired(...rest: any[]) {
|
|
||||||
return propType.isRequired(...rest);
|
|
||||||
}
|
|
||||||
if (typeof lowcodeType === 'string') {
|
|
||||||
lowcodeType = {
|
|
||||||
type: lowcodeType,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
lowcodeCheckTypeIsRequired.lowcodeType = {
|
|
||||||
...lowcodeType,
|
|
||||||
isRequired: true,
|
|
||||||
};
|
|
||||||
return lowcodeCheckTypeIsRequired;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
function define(propType: any = PropTypes.any, lowcodeType: string | object = {}): LowcodeCheckType {
|
|
||||||
if (!propType._inner && propType.name !== 'lowcodeCheckType') {
|
|
||||||
propType.lowcodeType = lowcodeType;
|
|
||||||
}
|
|
||||||
function lowcodeCheckType(...rest: any[]) {
|
|
||||||
return propType(...rest);
|
|
||||||
}
|
|
||||||
lowcodeCheckType.lowcodeType = lowcodeType;
|
|
||||||
lowcodeCheckType.isRequired = makeRequired(propType, lowcodeType);
|
|
||||||
return lowcodeCheckType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LowcodeTypes: any = {
|
|
||||||
...PropTypes,
|
|
||||||
define,
|
|
||||||
};
|
|
||||||
|
|
||||||
(window as any).PropTypes = LowcodeTypes;
|
|
||||||
if ((window as any).React) {
|
|
||||||
(window as any).React.PropTypes = LowcodeTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// override primitive type checkers
|
|
||||||
primitiveTypes.forEach((type) => {
|
|
||||||
const propType = (PropTypes as any)[type];
|
|
||||||
if (!propType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
propType._inner = true;
|
|
||||||
LowcodeTypes[type] = define(propType, type);
|
|
||||||
});
|
|
||||||
|
|
||||||
// You can ensure that your prop is limited to specific values by treating
|
|
||||||
// it as an enum.
|
|
||||||
LowcodeTypes.oneOf = (list: any[]) => {
|
|
||||||
return define(PropTypes.oneOf(list), {
|
|
||||||
type: 'oneOf',
|
|
||||||
value: list,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// An array of a certain type
|
|
||||||
LowcodeTypes.arrayOf = (type: any) => {
|
|
||||||
return define(PropTypes.arrayOf(type), {
|
|
||||||
type: 'arrayOf',
|
|
||||||
value: type.lowcodeType || 'any',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// An object with property values of a certain type
|
|
||||||
LowcodeTypes.objectOf = (type: any) => {
|
|
||||||
return define(PropTypes.objectOf(type), {
|
|
||||||
type: 'objectOf',
|
|
||||||
value: type.lowcodeType || 'any',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// An object that could be one of many types
|
|
||||||
LowcodeTypes.oneOfType = (types: any[]) => {
|
|
||||||
const itemTypes = types.map((type) => type.lowcodeType || 'any');
|
|
||||||
return define(PropTypes.oneOfType(types), {
|
|
||||||
type: 'oneOfType',
|
|
||||||
value: itemTypes,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// An object with warnings on extra properties
|
|
||||||
LowcodeTypes.exact = (typesMap: any) => {
|
|
||||||
const configs = Object.keys(typesMap).map((key) => {
|
|
||||||
return {
|
|
||||||
name: key,
|
|
||||||
propType: typesMap[key]?.lowcodeType || 'any',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return define(PropTypes.exact(typesMap), {
|
|
||||||
type: 'exact',
|
|
||||||
value: configs,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// An object taking on a particular shape
|
|
||||||
LowcodeTypes.shape = (typesMap: any = {}) => {
|
|
||||||
const configs = Object.keys(typesMap).map((key) => {
|
|
||||||
return {
|
|
||||||
name: key,
|
|
||||||
propType: typesMap[key]?.lowcodeType || 'any',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return define(PropTypes.shape(typesMap), {
|
|
||||||
type: 'shape',
|
|
||||||
value: configs,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const BasicTypes = ['string', 'number', 'object'];
|
|
||||||
export function parseProps(component: any): IPublicTypePropConfig[] {
|
|
||||||
if (!component) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const propTypes = component.propTypes || ({} as any);
|
|
||||||
const defaultProps = component.defaultProps || ({} as any);
|
|
||||||
const result: any = {};
|
|
||||||
if (!propTypes) return [];
|
|
||||||
Object.keys(propTypes).forEach((key) => {
|
|
||||||
const propTypeItem = propTypes[key];
|
|
||||||
const defaultValue = defaultProps[key];
|
|
||||||
const { lowcodeType } = propTypeItem;
|
|
||||||
if (lowcodeType) {
|
|
||||||
result[key] = {
|
|
||||||
name: key,
|
|
||||||
propType: lowcodeType,
|
|
||||||
};
|
|
||||||
if (defaultValue != null) {
|
|
||||||
result[key].defaultValue = defaultValue;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let i = primitiveTypes.length;
|
|
||||||
while (i-- > 0) {
|
|
||||||
const k = primitiveTypes[i];
|
|
||||||
if ((LowcodeTypes as any)[k] === propTypeItem) {
|
|
||||||
result[key] = {
|
|
||||||
name: key,
|
|
||||||
propType: k,
|
|
||||||
};
|
|
||||||
if (defaultValue != null) {
|
|
||||||
result[key].defaultValue = defaultValue;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result[key] = {
|
|
||||||
name: key,
|
|
||||||
propType: 'any',
|
|
||||||
};
|
|
||||||
if (defaultValue != null) {
|
|
||||||
result[key].defaultValue = defaultValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(defaultProps).forEach((key) => {
|
|
||||||
if (result[key]) return;
|
|
||||||
const defaultValue = defaultProps[key];
|
|
||||||
let type: string = typeof defaultValue;
|
|
||||||
if (type === 'boolean') {
|
|
||||||
type = 'bool';
|
|
||||||
} else if (type === 'function') {
|
|
||||||
type = 'func';
|
|
||||||
} else if (type === 'object' && Array.isArray(defaultValue)) {
|
|
||||||
type = 'array';
|
|
||||||
} else if (defaultValue && isValidElement(defaultValue)) {
|
|
||||||
type = 'node';
|
|
||||||
} else if (defaultValue && isElement(defaultValue)) {
|
|
||||||
type = 'element';
|
|
||||||
} else if (!BasicTypes.includes(type)) {
|
|
||||||
type = 'any';
|
|
||||||
}
|
|
||||||
|
|
||||||
result[key] = {
|
|
||||||
name: key,
|
|
||||||
propType: type || 'any',
|
|
||||||
defaultValue,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.keys(result).map((key) => result[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseMetadata(component: any): any {
|
|
||||||
return {
|
|
||||||
props: parseProps(component),
|
|
||||||
...component.componentMetadata,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,173 +0,0 @@
|
|||||||
/**
|
|
||||||
* Check whether a component is external package, e.g. @ali/uxcore
|
|
||||||
* @param path Component path
|
|
||||||
*/
|
|
||||||
export function isPackagePath(path: string): boolean {
|
|
||||||
return !path.startsWith('.') && !path.startsWith('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Title cased string
|
|
||||||
* @param s original string
|
|
||||||
*/
|
|
||||||
export function toTitleCase(s: string): string {
|
|
||||||
return s
|
|
||||||
.split(/[-_ .]+/)
|
|
||||||
.map((token) => token[0].toUpperCase() + token.substring(1))
|
|
||||||
.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make up an import name/tag for components
|
|
||||||
* @param path Original path name
|
|
||||||
*/
|
|
||||||
export function generateComponentName(path: string): string {
|
|
||||||
const parts = path.split('/');
|
|
||||||
let name = parts.pop();
|
|
||||||
if (name && /^index\./.test(name)) {
|
|
||||||
name = parts.pop();
|
|
||||||
}
|
|
||||||
return name ? toTitleCase(name) : 'Component';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* normalizing import path for easier comparison
|
|
||||||
*/
|
|
||||||
export function getNormalizedImportPath(path: string): string {
|
|
||||||
const segments = path.split('/');
|
|
||||||
let basename = segments.pop();
|
|
||||||
if (!basename) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
const ignoredExtensions = ['.ts', '.js', '.tsx', '.jsx'];
|
|
||||||
const extIndex = basename.lastIndexOf('.');
|
|
||||||
if (extIndex > -1) {
|
|
||||||
const ext = basename.slice(extIndex);
|
|
||||||
if (ignoredExtensions.includes(ext)) {
|
|
||||||
basename = basename.slice(0, extIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (basename !== 'index') {
|
|
||||||
segments.push(basename);
|
|
||||||
}
|
|
||||||
return segments.join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* make a relative path
|
|
||||||
*
|
|
||||||
* @param toPath abolute path
|
|
||||||
* @param fromPath absolute path
|
|
||||||
*/
|
|
||||||
export function makeRelativePath(toPath: string, fromPath: string) {
|
|
||||||
// not a absolute path, eg. @ali/uxcore
|
|
||||||
if (!toPath.startsWith('/')) {
|
|
||||||
return toPath;
|
|
||||||
}
|
|
||||||
const toParts = toPath.split('/');
|
|
||||||
const fromParts = fromPath.split('/');
|
|
||||||
|
|
||||||
// find shared path header
|
|
||||||
const length = Math.min(fromParts.length, toParts.length);
|
|
||||||
let sharedUpTo = length;
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
if (fromParts[i] !== toParts[i]) {
|
|
||||||
sharedUpTo = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// find how many levels to go up from
|
|
||||||
// minus another 1 since we do not include the final
|
|
||||||
const numGoUp = fromParts.length - sharedUpTo - 1;
|
|
||||||
|
|
||||||
// generate final path
|
|
||||||
let outputParts = [];
|
|
||||||
if (numGoUp === 0) {
|
|
||||||
// in the same dir
|
|
||||||
outputParts.push('.');
|
|
||||||
} else {
|
|
||||||
// needs to go up
|
|
||||||
for (let i = 0; i < numGoUp; ++i) {
|
|
||||||
outputParts.push('..');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
outputParts = outputParts.concat(toParts.slice(sharedUpTo));
|
|
||||||
|
|
||||||
return outputParts.join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeArray(parts: string[], allowAboveRoot: boolean) {
|
|
||||||
const res = [];
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
|
||||||
const p = parts[i];
|
|
||||||
|
|
||||||
// ignore empty parts
|
|
||||||
if (!p || p === '.') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p === '..') {
|
|
||||||
if (res.length && res[res.length - 1] !== '..') {
|
|
||||||
res.pop();
|
|
||||||
} else if (allowAboveRoot) {
|
|
||||||
res.push('..');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.push(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalize(path: string): string {
|
|
||||||
const isAbsolute = path[0] === '/';
|
|
||||||
|
|
||||||
const segments = normalizeArray(path.split('/'), !isAbsolute);
|
|
||||||
if (isAbsolute) {
|
|
||||||
segments.unshift('');
|
|
||||||
} else if (segments.length < 1 || segments[0] !== '..') {
|
|
||||||
segments.unshift('.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return segments.join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve component with absolute path to relative path
|
|
||||||
* @param path absolute path of component from project
|
|
||||||
*/
|
|
||||||
export function resolveAbsoluatePath(path: string, base: string): string {
|
|
||||||
if (!path.startsWith('.')) {
|
|
||||||
// eg. /usr/path/to, @ali/button
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
path = path.replace(/\\/g, '/');
|
|
||||||
if (base.slice(-1) !== '/') {
|
|
||||||
base += '/';
|
|
||||||
}
|
|
||||||
return normalize(base + path);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function joinPath(...segments: string[]) {
|
|
||||||
let path = '';
|
|
||||||
for (const seg of segments) {
|
|
||||||
if (seg) {
|
|
||||||
if (path === '') {
|
|
||||||
path += seg;
|
|
||||||
} else {
|
|
||||||
path += `/${ seg}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return normalize(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeVersion(path: string): string {
|
|
||||||
if (path.lastIndexOf('@') > 0) {
|
|
||||||
path = path.replace(/(@?[^@]+)(@[\w.-]+)(.+)/, '$1$3');
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
@ -1,101 +0,0 @@
|
|||||||
const useRAF = typeof requestAnimationFrame === 'function';
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
export function throttle(func: Function, delay: number) {
|
|
||||||
let lastArgs: any;
|
|
||||||
let lastThis: any;
|
|
||||||
let result: any;
|
|
||||||
let timerId: number | undefined;
|
|
||||||
let lastCalled: number | undefined;
|
|
||||||
let lastInvoked = 0;
|
|
||||||
|
|
||||||
function invoke(time: number) {
|
|
||||||
const args = lastArgs;
|
|
||||||
const thisArg = lastThis;
|
|
||||||
|
|
||||||
lastArgs = undefined;
|
|
||||||
lastThis = undefined;
|
|
||||||
lastInvoked = time;
|
|
||||||
result = func.apply(thisArg, args);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startTimer(pendingFunc: any, wait: number): number {
|
|
||||||
if (useRAF) {
|
|
||||||
return requestAnimationFrame(pendingFunc);
|
|
||||||
}
|
|
||||||
return setTimeout(pendingFunc, wait) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
function leadingEdge(time: number) {
|
|
||||||
lastInvoked = time;
|
|
||||||
timerId = startTimer(timerExpired, delay);
|
|
||||||
return invoke(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldInvoke(time: number) {
|
|
||||||
const timeSinceLastCalled = time - lastCalled!;
|
|
||||||
const timeSinceLastInvoked = time - lastInvoked;
|
|
||||||
|
|
||||||
return (
|
|
||||||
lastCalled === undefined ||
|
|
||||||
timeSinceLastCalled >= delay ||
|
|
||||||
timeSinceLastCalled < 0 ||
|
|
||||||
timeSinceLastInvoked >= delay
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function remainingWait(time: number) {
|
|
||||||
const timeSinceLastCalled = time - lastCalled!;
|
|
||||||
const timeSinceLastInvoked = time - lastInvoked;
|
|
||||||
|
|
||||||
return Math.min(delay - timeSinceLastCalled, delay - timeSinceLastInvoked);
|
|
||||||
}
|
|
||||||
|
|
||||||
function timerExpired() {
|
|
||||||
const time = Date.now();
|
|
||||||
if (shouldInvoke(time)) {
|
|
||||||
return trailingEdge(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
timerId = startTimer(timerExpired, remainingWait(time));
|
|
||||||
}
|
|
||||||
|
|
||||||
function trailingEdge(time: number) {
|
|
||||||
timerId = undefined;
|
|
||||||
|
|
||||||
if (lastArgs) {
|
|
||||||
return invoke(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastArgs = undefined;
|
|
||||||
lastThis = undefined;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function debounced(this: any, ...args: any[]) {
|
|
||||||
const time = Date.now();
|
|
||||||
const isInvoking = shouldInvoke(time);
|
|
||||||
|
|
||||||
lastArgs = args;
|
|
||||||
lastThis = this;
|
|
||||||
lastCalled = time;
|
|
||||||
|
|
||||||
if (isInvoking) {
|
|
||||||
if (timerId === undefined) {
|
|
||||||
return leadingEdge(lastCalled);
|
|
||||||
}
|
|
||||||
|
|
||||||
timerId = startTimer(timerExpired, delay);
|
|
||||||
return invoke(lastCalled);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timerId === undefined) {
|
|
||||||
timerId = startTimer(timerExpired, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return debounced;
|
|
||||||
}
|
|
||||||
@ -1,187 +0,0 @@
|
|||||||
import { observable, computed, makeObservable, action } from '@alilc/lowcode-editor-core';
|
|
||||||
import { Point, ScrollTarget } from '../designer';
|
|
||||||
import { AutoFit, AUTO_FIT, IViewport } from '../simulator';
|
|
||||||
|
|
||||||
export default class Viewport implements IViewport {
|
|
||||||
@observable.ref private rect?: DOMRect;
|
|
||||||
|
|
||||||
private _bounds?: DOMRect;
|
|
||||||
|
|
||||||
get bounds(): DOMRect {
|
|
||||||
if (this._bounds) {
|
|
||||||
return this._bounds;
|
|
||||||
}
|
|
||||||
this._bounds = this.viewportElement!.getBoundingClientRect();
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this._bounds = undefined;
|
|
||||||
});
|
|
||||||
return this._bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
get contentBounds(): DOMRect {
|
|
||||||
const { bounds, scale } = this;
|
|
||||||
return new DOMRect(0, 0, bounds.width / scale, bounds.height / scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
private viewportElement?: HTMLElement;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
makeObservable(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
mount(viewportElement: HTMLElement | null) {
|
|
||||||
if (!viewportElement || this.viewportElement === viewportElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.viewportElement = viewportElement;
|
|
||||||
this.touch();
|
|
||||||
}
|
|
||||||
|
|
||||||
touch() {
|
|
||||||
if (this.viewportElement) {
|
|
||||||
this.rect = this.bounds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get height(): number {
|
|
||||||
if (!this.rect) {
|
|
||||||
return 600;
|
|
||||||
}
|
|
||||||
return this.rect.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
set height(newHeight: number) {
|
|
||||||
this._contentHeight = newHeight / this.scale;
|
|
||||||
if (this.viewportElement) {
|
|
||||||
this.viewportElement.style.height = `${newHeight}px`;
|
|
||||||
this.touch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get width(): number {
|
|
||||||
if (!this.rect) {
|
|
||||||
return 1000;
|
|
||||||
}
|
|
||||||
return this.rect.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
set width(newWidth: number) {
|
|
||||||
this._contentWidth = newWidth / this.scale;
|
|
||||||
if (this.viewportElement) {
|
|
||||||
this.viewportElement.style.width = `${newWidth}px`;
|
|
||||||
this.touch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable.ref private _scale = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 缩放比例
|
|
||||||
*/
|
|
||||||
@computed get scale(): number {
|
|
||||||
return this._scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
set scale(newScale: number) {
|
|
||||||
if (isNaN(newScale) || newScale <= 0) {
|
|
||||||
throw new Error(`invalid new scale "${newScale}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._scale = newScale;
|
|
||||||
this._contentWidth = this.width / this.scale;
|
|
||||||
this._contentHeight = this.height / this.scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable.ref private _contentWidth: number | AutoFit = AUTO_FIT;
|
|
||||||
|
|
||||||
@observable.ref private _contentHeight: number | AutoFit = AUTO_FIT;
|
|
||||||
|
|
||||||
@computed get contentHeight(): number | AutoFit {
|
|
||||||
return this._contentHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
set contentHeight(newContentHeight: number | AutoFit) {
|
|
||||||
this._contentHeight = newContentHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get contentWidth(): number | AutoFit {
|
|
||||||
return this._contentWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
set contentWidth(val: number | AutoFit) {
|
|
||||||
this._contentWidth = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable.ref private _scrollX = 0;
|
|
||||||
|
|
||||||
@observable.ref private _scrollY = 0;
|
|
||||||
|
|
||||||
get scrollX() {
|
|
||||||
return this._scrollX;
|
|
||||||
}
|
|
||||||
|
|
||||||
get scrollY() {
|
|
||||||
return this._scrollY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _scrollTarget?: ScrollTarget;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 滚动对象
|
|
||||||
*/
|
|
||||||
get scrollTarget(): ScrollTarget | undefined {
|
|
||||||
return this._scrollTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable private _scrolling = false;
|
|
||||||
|
|
||||||
get scrolling(): boolean {
|
|
||||||
return this._scrolling;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setScrollTarget(target: Window) {
|
|
||||||
const scrollTarget = new ScrollTarget(target);
|
|
||||||
this._scrollX = scrollTarget.left;
|
|
||||||
this._scrollY = scrollTarget.top;
|
|
||||||
|
|
||||||
let scrollTimer: any;
|
|
||||||
target.addEventListener('scroll', () => {
|
|
||||||
this._scrollX = scrollTarget.left;
|
|
||||||
this._scrollY = scrollTarget.top;
|
|
||||||
this._scrolling = true;
|
|
||||||
if (scrollTimer) {
|
|
||||||
clearTimeout(scrollTimer);
|
|
||||||
}
|
|
||||||
scrollTimer = setTimeout(() => {
|
|
||||||
this._scrolling = false;
|
|
||||||
}, 80);
|
|
||||||
});
|
|
||||||
target.addEventListener('resize', () => this.touch());
|
|
||||||
this._scrollTarget = scrollTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
toGlobalPoint(point: Point): Point {
|
|
||||||
if (!this.viewportElement) {
|
|
||||||
return point;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = this.bounds;
|
|
||||||
return {
|
|
||||||
clientX: point.clientX * this.scale + rect.left,
|
|
||||||
clientY: point.clientY * this.scale + rect.top,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toLocalPoint(point: Point): Point {
|
|
||||||
if (!this.viewportElement) {
|
|
||||||
return point;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = this.bounds;
|
|
||||||
return {
|
|
||||||
clientX: (point.clientX - rect.left) / this.scale,
|
|
||||||
clientY: (point.clientY - rect.top) / this.scale,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
import { IPublicModelNode, IPublicTypeComponentAction, IPublicTypeMetadataTransducer } from '@alilc/lowcode-types';
|
|
||||||
import { engineConfig } from '@alilc/lowcode-editor-core';
|
|
||||||
import { intlNode } from './locale';
|
|
||||||
import {
|
|
||||||
IconLock,
|
|
||||||
IconUnlock,
|
|
||||||
IconRemove,
|
|
||||||
IconClone,
|
|
||||||
IconHidden,
|
|
||||||
} from './icons';
|
|
||||||
import { componentDefaults, legacyIssues } from './transducers';
|
|
||||||
|
|
||||||
function deduplicateRef(node: IPublicModelNode | null | undefined) {
|
|
||||||
const currentRef = node?.getPropValue('ref');
|
|
||||||
if (currentRef) {
|
|
||||||
node?.setPropValue('ref', `${node.componentName.toLowerCase()}-${Math.random().toString(36).slice(2, 9)}`);
|
|
||||||
}
|
|
||||||
node?.children?.forEach(deduplicateRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComponentActions {
|
|
||||||
private metadataTransducers: IPublicTypeMetadataTransducer[] = [];
|
|
||||||
|
|
||||||
actions: IPublicTypeComponentAction[] = [
|
|
||||||
{
|
|
||||||
name: 'remove',
|
|
||||||
content: {
|
|
||||||
icon: IconRemove,
|
|
||||||
title: intlNode('remove'),
|
|
||||||
/* istanbul ignore next */
|
|
||||||
action(node: IPublicModelNode) {
|
|
||||||
node.remove();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
important: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'hide',
|
|
||||||
content: {
|
|
||||||
icon: IconHidden,
|
|
||||||
title: intlNode('hide'),
|
|
||||||
/* istanbul ignore next */
|
|
||||||
action(node: IPublicModelNode) {
|
|
||||||
node.visible = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/* istanbul ignore next */
|
|
||||||
condition: (node: IPublicModelNode) => {
|
|
||||||
return node.componentMeta?.isModal;
|
|
||||||
},
|
|
||||||
important: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'copy',
|
|
||||||
content: {
|
|
||||||
icon: IconClone,
|
|
||||||
title: intlNode('copy'),
|
|
||||||
/* istanbul ignore next */
|
|
||||||
action(node: IPublicModelNode) {
|
|
||||||
// node.remove();
|
|
||||||
const { document: doc, parent, index } = node;
|
|
||||||
if (parent) {
|
|
||||||
const newNode = doc?.insertNode(parent, node, (index ?? 0) + 1, true);
|
|
||||||
deduplicateRef(newNode);
|
|
||||||
newNode?.select();
|
|
||||||
const { isRGL, rglNode } = node?.getRGL();
|
|
||||||
if (isRGL) {
|
|
||||||
// 复制 layout 信息
|
|
||||||
const layout: any = rglNode?.getPropValue('layout') || [];
|
|
||||||
const curLayout = layout.filter((item: any) => item.i === node.getPropValue('fieldId'));
|
|
||||||
if (curLayout && curLayout[0]) {
|
|
||||||
layout.push({
|
|
||||||
...curLayout[0],
|
|
||||||
i: newNode?.getPropValue('fieldId'),
|
|
||||||
});
|
|
||||||
rglNode?.setPropValue('layout', layout);
|
|
||||||
// 如果是磁贴块复制,则需要滚动到影响位置
|
|
||||||
setTimeout(
|
|
||||||
() => newNode?.document?.project?.simulatorHost?.scrollToNode(newNode),
|
|
||||||
10
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
important: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lock',
|
|
||||||
content: {
|
|
||||||
icon: IconLock, // 锁定 icon
|
|
||||||
title: intlNode('lock'),
|
|
||||||
action(node: IPublicModelNode) {
|
|
||||||
node.lock();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
condition: (node: IPublicModelNode) => {
|
|
||||||
return engineConfig.get('enableCanvasLock', false) && node.isContainerNode && !node.isLocked;
|
|
||||||
},
|
|
||||||
important: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'unlock',
|
|
||||||
content: {
|
|
||||||
icon: IconUnlock, // 解锁 icon
|
|
||||||
title: intlNode('unlock'),
|
|
||||||
/* istanbul ignore next */
|
|
||||||
action(node: IPublicModelNode) {
|
|
||||||
node.lock(false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/* istanbul ignore next */
|
|
||||||
condition: (node: IPublicModelNode) => {
|
|
||||||
return engineConfig.get('enableCanvasLock', false) && node.isContainerNode && node.isLocked;
|
|
||||||
},
|
|
||||||
important: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.registerMetadataTransducer(legacyIssues, 2, 'legacy-issues'); // should use a high level priority, eg: 2
|
|
||||||
this.registerMetadataTransducer(componentDefaults, 100, 'component-defaults');
|
|
||||||
}
|
|
||||||
|
|
||||||
removeBuiltinComponentAction(name: string) {
|
|
||||||
const i = this.actions.findIndex((action) => action.name === name);
|
|
||||||
if (i > -1) {
|
|
||||||
this.actions.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addBuiltinComponentAction(action: IPublicTypeComponentAction) {
|
|
||||||
this.actions.push(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
modifyBuiltinComponentAction(
|
|
||||||
actionName: string,
|
|
||||||
handle: (action: IPublicTypeComponentAction) => void,
|
|
||||||
) {
|
|
||||||
const builtinAction = this.actions.find((action) => action.name === actionName);
|
|
||||||
if (builtinAction) {
|
|
||||||
handle(builtinAction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registerMetadataTransducer(
|
|
||||||
transducer: IPublicTypeMetadataTransducer,
|
|
||||||
level = 100,
|
|
||||||
id?: string,
|
|
||||||
) {
|
|
||||||
transducer.level = level;
|
|
||||||
transducer.id = id;
|
|
||||||
const i = this.metadataTransducers.findIndex(
|
|
||||||
(item) => item.level != null && item.level > level
|
|
||||||
);
|
|
||||||
if (i < 0) {
|
|
||||||
this.metadataTransducers.push(transducer);
|
|
||||||
} else {
|
|
||||||
this.metadataTransducers.splice(i, 0, transducer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getRegisteredMetadataTransducers(): IPublicTypeMetadataTransducer[] {
|
|
||||||
return this.metadataTransducers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,396 +0,0 @@
|
|||||||
import { ReactElement } from 'react';
|
|
||||||
import {
|
|
||||||
IPublicTypeComponentMetadata,
|
|
||||||
IPublicTypeNpmInfo,
|
|
||||||
IPublicTypeNodeData,
|
|
||||||
IPublicTypeNodeSchema,
|
|
||||||
IPublicTypeTitleContent,
|
|
||||||
IPublicTypeTransformedComponentMetadata,
|
|
||||||
IPublicTypeNestingFilter,
|
|
||||||
IPublicTypeI18nData,
|
|
||||||
IPublicTypeFieldConfig,
|
|
||||||
IPublicModelComponentMeta,
|
|
||||||
IPublicTypeAdvanced,
|
|
||||||
IPublicTypeDisposable,
|
|
||||||
IPublicTypeLiveTextEditingConfig,
|
|
||||||
} from '@alilc/lowcode-types';
|
|
||||||
import { deprecate, isRegExp, isTitleConfig, isNode } from '@alilc/lowcode-utils';
|
|
||||||
import { computed, createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core';
|
|
||||||
import { Node, INode } from './document';
|
|
||||||
import { Designer } from './designer';
|
|
||||||
import { IconContainer, IconPage, IconComponent } from './icons';
|
|
||||||
|
|
||||||
export function ensureAList(list?: string | string[]): string[] | null {
|
|
||||||
if (!list) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(list)) {
|
|
||||||
if (typeof list !== 'string') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
list = list.split(/ *[ ,|] */).filter(Boolean);
|
|
||||||
}
|
|
||||||
if (list.length < 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildFilter(rule?: string | string[] | RegExp | IPublicTypeNestingFilter) {
|
|
||||||
if (!rule) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (typeof rule === 'function') {
|
|
||||||
return rule;
|
|
||||||
}
|
|
||||||
if (isRegExp(rule)) {
|
|
||||||
return (testNode: Node | IPublicTypeNodeSchema) => {
|
|
||||||
return rule.test(testNode.componentName);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const list = ensureAList(rule);
|
|
||||||
if (!list) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (testNode: Node | IPublicTypeNodeSchema) => {
|
|
||||||
return list.includes(testNode.componentName);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IComponentMeta extends IPublicModelComponentMeta<INode> {
|
|
||||||
prototype?: any;
|
|
||||||
|
|
||||||
liveTextEditing?: IPublicTypeLiveTextEditingConfig[];
|
|
||||||
|
|
||||||
get rootSelector(): string | undefined;
|
|
||||||
|
|
||||||
setMetadata(metadata: IPublicTypeComponentMetadata): void;
|
|
||||||
|
|
||||||
onMetadataChange(fn: (args: any) => void): IPublicTypeDisposable;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComponentMeta implements IComponentMeta {
|
|
||||||
readonly isComponentMeta = true;
|
|
||||||
|
|
||||||
private _npm?: IPublicTypeNpmInfo;
|
|
||||||
|
|
||||||
private emitter: IEventBus = createModuleEventBus('ComponentMeta');
|
|
||||||
|
|
||||||
get npm() {
|
|
||||||
return this._npm;
|
|
||||||
}
|
|
||||||
|
|
||||||
set npm(_npm: any) {
|
|
||||||
this.setNpm(_npm);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _componentName?: string;
|
|
||||||
|
|
||||||
get componentName(): string {
|
|
||||||
return this._componentName!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _isContainer?: boolean;
|
|
||||||
|
|
||||||
get isContainer(): boolean {
|
|
||||||
return this._isContainer! || this.isRootComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
get isMinimalRenderUnit(): boolean {
|
|
||||||
return this._isMinimalRenderUnit || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _isModal?: boolean;
|
|
||||||
|
|
||||||
get isModal(): boolean {
|
|
||||||
return this._isModal!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _descriptor?: string;
|
|
||||||
|
|
||||||
get descriptor(): string | undefined {
|
|
||||||
return this._descriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _rootSelector?: string;
|
|
||||||
|
|
||||||
get rootSelector(): string | undefined {
|
|
||||||
return this._rootSelector;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _transformedMetadata?: IPublicTypeTransformedComponentMetadata;
|
|
||||||
|
|
||||||
get configure(): IPublicTypeFieldConfig[] {
|
|
||||||
const config = this._transformedMetadata?.configure;
|
|
||||||
return config?.combined || config?.props || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _liveTextEditing?: IPublicTypeLiveTextEditingConfig[];
|
|
||||||
|
|
||||||
get liveTextEditing() {
|
|
||||||
return this._liveTextEditing;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _isTopFixed?: boolean;
|
|
||||||
|
|
||||||
get isTopFixed(): boolean {
|
|
||||||
return !!this._isTopFixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private parentWhitelist?: IPublicTypeNestingFilter | null;
|
|
||||||
|
|
||||||
private childWhitelist?: IPublicTypeNestingFilter | null;
|
|
||||||
|
|
||||||
private _title?: IPublicTypeTitleContent;
|
|
||||||
|
|
||||||
private _isMinimalRenderUnit?: boolean;
|
|
||||||
|
|
||||||
get title(): string | IPublicTypeI18nData | ReactElement {
|
|
||||||
// string | i18nData | ReactElement
|
|
||||||
// TitleConfig title.label
|
|
||||||
if (isTitleConfig(this._title)) {
|
|
||||||
return (this._title?.label as any) || this.componentName;
|
|
||||||
}
|
|
||||||
return (this._title as any) || this.componentName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get icon() {
|
|
||||||
// give Slot default icon
|
|
||||||
// if _title is TitleConfig get _title.icon
|
|
||||||
return (
|
|
||||||
this._transformedMetadata?.icon ||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
(this.componentName === 'Page' ? IconPage : this.isContainer ? IconContainer : IconComponent)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _acceptable?: boolean;
|
|
||||||
|
|
||||||
get acceptable(): boolean {
|
|
||||||
return this._acceptable!;
|
|
||||||
}
|
|
||||||
|
|
||||||
get advanced(): IPublicTypeAdvanced {
|
|
||||||
return this.getMetadata().configure.advanced || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly designer: Designer,
|
|
||||||
metadata: IPublicTypeComponentMetadata,
|
|
||||||
) {
|
|
||||||
this.parseMetadata(metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
setNpm(info: IPublicTypeNpmInfo) {
|
|
||||||
if (!this._npm) {
|
|
||||||
this._npm = info;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseMetadata(metadata: IPublicTypeComponentMetadata) {
|
|
||||||
const { componentName, npm, ...others } = metadata;
|
|
||||||
let _metadata = metadata;
|
|
||||||
if ((metadata as any).prototype) {
|
|
||||||
this.prototype = (metadata as any).prototype;
|
|
||||||
}
|
|
||||||
if (!npm && !Object.keys(others).length) {
|
|
||||||
// 没有注册的组件,只能删除,不支持复制、移动等操作
|
|
||||||
_metadata = {
|
|
||||||
componentName,
|
|
||||||
configure: {
|
|
||||||
component: {
|
|
||||||
disableBehaviors: ['copy', 'move', 'lock', 'unlock'],
|
|
||||||
},
|
|
||||||
advanced: {
|
|
||||||
callbacks: {
|
|
||||||
onMoveHook: () => false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
this._npm = npm || this._npm;
|
|
||||||
this._componentName = componentName;
|
|
||||||
|
|
||||||
// 额外转换逻辑
|
|
||||||
this._transformedMetadata = this.transformMetadata(_metadata);
|
|
||||||
|
|
||||||
const { title } = this._transformedMetadata;
|
|
||||||
if (title) {
|
|
||||||
this._title =
|
|
||||||
typeof title === 'string'
|
|
||||||
? {
|
|
||||||
type: 'i18n',
|
|
||||||
'en-US': this.componentName,
|
|
||||||
'zh-CN': title,
|
|
||||||
}
|
|
||||||
: title;
|
|
||||||
}
|
|
||||||
|
|
||||||
const liveTextEditing = this.advanced.liveTextEditing || [];
|
|
||||||
|
|
||||||
function collectLiveTextEditing(items: IPublicTypeFieldConfig[]) {
|
|
||||||
items.forEach((config) => {
|
|
||||||
if (config?.items) {
|
|
||||||
collectLiveTextEditing(config.items);
|
|
||||||
} else {
|
|
||||||
const liveConfig = config.liveTextEditing || config.extraProps?.liveTextEditing;
|
|
||||||
if (liveConfig) {
|
|
||||||
liveTextEditing.push({
|
|
||||||
propTarget: String(config.name),
|
|
||||||
...liveConfig,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
collectLiveTextEditing(this.configure);
|
|
||||||
this._liveTextEditing = liveTextEditing.length > 0 ? liveTextEditing : undefined;
|
|
||||||
|
|
||||||
const isTopFixed = this.advanced.isTopFixed;
|
|
||||||
|
|
||||||
if (isTopFixed) {
|
|
||||||
this._isTopFixed = isTopFixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { configure = {} } = this._transformedMetadata;
|
|
||||||
this._acceptable = false;
|
|
||||||
|
|
||||||
const { component } = configure;
|
|
||||||
if (component) {
|
|
||||||
this._isContainer = !!component.isContainer;
|
|
||||||
this._isModal = !!component.isModal;
|
|
||||||
this._descriptor = component.descriptor;
|
|
||||||
this._rootSelector = component.rootSelector;
|
|
||||||
this._isMinimalRenderUnit = component.isMinimalRenderUnit;
|
|
||||||
if (component.nestingRule) {
|
|
||||||
const { parentWhitelist, childWhitelist } = component.nestingRule;
|
|
||||||
this.parentWhitelist = buildFilter(parentWhitelist);
|
|
||||||
this.childWhitelist = buildFilter(childWhitelist);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._isContainer = false;
|
|
||||||
this._isModal = false;
|
|
||||||
}
|
|
||||||
this.emitter.emit('metadata_change');
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshMetadata() {
|
|
||||||
this.parseMetadata(this.getMetadata());
|
|
||||||
}
|
|
||||||
|
|
||||||
private transformMetadata(
|
|
||||||
metadta: IPublicTypeComponentMetadata,
|
|
||||||
): IPublicTypeTransformedComponentMetadata {
|
|
||||||
const registeredTransducers = this.designer.componentActions.getRegisteredMetadataTransducers();
|
|
||||||
const result = registeredTransducers.reduce((prevMetadata, current) => {
|
|
||||||
return current(prevMetadata);
|
|
||||||
}, preprocessMetadata(metadta));
|
|
||||||
|
|
||||||
if (!result.configure) {
|
|
||||||
result.configure = {};
|
|
||||||
}
|
|
||||||
if (result.experimental && !result.configure.advanced) {
|
|
||||||
deprecate(result.experimental, '.experimental', '.configure.advanced');
|
|
||||||
result.configure.advanced = result.experimental;
|
|
||||||
}
|
|
||||||
return result as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
isRootComponent(includeBlock = true): boolean {
|
|
||||||
return (
|
|
||||||
this.componentName === 'Page' ||
|
|
||||||
this.componentName === 'Component' ||
|
|
||||||
(includeBlock && this.componentName === 'Block')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get availableActions() {
|
|
||||||
// eslint-disable-next-line prefer-const
|
|
||||||
let { disableBehaviors, actions } = this._transformedMetadata?.configure.component || {};
|
|
||||||
const disabled =
|
|
||||||
ensureAList(disableBehaviors) ||
|
|
||||||
(this.isRootComponent(false) ? ['copy', 'remove', 'lock', 'unlock'] : null);
|
|
||||||
actions = this.designer.componentActions.actions.concat(
|
|
||||||
this.designer.getGlobalComponentActions() || [],
|
|
||||||
actions || [],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (disabled) {
|
|
||||||
if (disabled.includes('*')) {
|
|
||||||
return actions.filter((action) => action.condition === 'always');
|
|
||||||
}
|
|
||||||
return actions.filter((action) => disabled.indexOf(action.name) < 0);
|
|
||||||
}
|
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMetadata(metadata: IPublicTypeComponentMetadata) {
|
|
||||||
this.parseMetadata(metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
getMetadata(): IPublicTypeTransformedComponentMetadata {
|
|
||||||
return this._transformedMetadata!;
|
|
||||||
}
|
|
||||||
|
|
||||||
checkNestingUp(my: INode | IPublicTypeNodeData, parent: INode) {
|
|
||||||
// 检查父子关系,直接约束型,在画布中拖拽直接掠过目标容器
|
|
||||||
if (this.parentWhitelist) {
|
|
||||||
return this.parentWhitelist(
|
|
||||||
parent.internalToShellNode(),
|
|
||||||
isNode<INode>(my) ? my.internalToShellNode() : my,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
checkNestingDown(
|
|
||||||
my: INode,
|
|
||||||
target: INode | IPublicTypeNodeSchema | IPublicTypeNodeSchema[],
|
|
||||||
): boolean {
|
|
||||||
// 检查父子关系,直接约束型,在画布中拖拽直接掠过目标容器
|
|
||||||
if (this.childWhitelist) {
|
|
||||||
const _target: any = !Array.isArray(target) ? [target] : target;
|
|
||||||
return _target.every((item: Node | IPublicTypeNodeSchema) => {
|
|
||||||
const _item = !isNode<INode>(item) ? new Node(my.document, item) : item;
|
|
||||||
return (
|
|
||||||
this.childWhitelist &&
|
|
||||||
this.childWhitelist(_item.internalToShellNode(), my.internalToShellNode())
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMetadataChange(fn: (args: any) => void): IPublicTypeDisposable {
|
|
||||||
this.emitter.on('metadata_change', fn);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('metadata_change', fn);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isComponentMeta(obj: any): obj is ComponentMeta {
|
|
||||||
return obj && obj.isComponentMeta;
|
|
||||||
}
|
|
||||||
|
|
||||||
function preprocessMetadata(
|
|
||||||
metadata: IPublicTypeComponentMetadata,
|
|
||||||
): IPublicTypeTransformedComponentMetadata {
|
|
||||||
if (metadata.configure) {
|
|
||||||
if (Array.isArray(metadata.configure)) {
|
|
||||||
return {
|
|
||||||
...metadata,
|
|
||||||
configure: {
|
|
||||||
props: metadata.configure,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return metadata as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...metadata,
|
|
||||||
configure: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import { type Designer } from '../../designer';
|
||||||
|
|
||||||
|
const DesignerContext = createContext<Designer>(undefined!);
|
||||||
|
|
||||||
|
export const useDesignerContext = () => useContext(DesignerContext);
|
||||||
|
|
||||||
|
export const DesignerContextProvider = DesignerContext.Provider;
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import { useLayoutEffect, useRef, memo, type ComponentType, type CSSProperties } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Designer } from '../../designer';
|
||||||
|
import BuiltinDragGhostComponent from '../drag-ghost';
|
||||||
|
import { ProjectView } from '../project-view';
|
||||||
|
import { DesignerContextProvider } from './context';
|
||||||
|
|
||||||
|
interface DesignerViewProps {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
dragGhostComponent: ComponentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DesignerView = memo(
|
||||||
|
(props: DesignerViewProps) => {
|
||||||
|
const { className, style, dragGhostComponent } = props;
|
||||||
|
const designerRef = useRef(new Designer());
|
||||||
|
|
||||||
|
// 组件挂载时执行
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
designerRef.current.onMounted();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
designerRef.current.purge();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const DragGhost = dragGhostComponent || BuiltinDragGhostComponent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DesignerContextProvider value={designerRef.current}>
|
||||||
|
<div className={classNames('lc-designer', className)} style={style}>
|
||||||
|
<DragGhost />
|
||||||
|
<ProjectView />
|
||||||
|
</div>
|
||||||
|
</DesignerContextProvider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(prevProps, nextProps) => {
|
||||||
|
if (
|
||||||
|
nextProps.className !== prevProps.className ||
|
||||||
|
nextProps.style !== prevProps.style ||
|
||||||
|
nextProps.dragGhostComponent !== prevProps.dragGhostComponent
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DesignerView;
|
||||||
2
packages/designer/src/components/designer-view/index.ts
Normal file
2
packages/designer/src/components/designer-view/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as DesignerView } from './designer-view';
|
||||||
|
export { useDesignerContext } from './context';
|
||||||
3
packages/designer/src/components/drag-ghost/index.tsx
Normal file
3
packages/designer/src/components/drag-ghost/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function DragGhost() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
1
packages/designer/src/components/project-view/index.ts
Normal file
1
packages/designer/src/components/project-view/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as ProjectView } from './project-view';
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export default function ProjectView() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@ -1,10 +0,0 @@
|
|||||||
.engine-context-menu {
|
|
||||||
&.next-menu.next-ver .next-menu-item {
|
|
||||||
padding-right: 30px;
|
|
||||||
|
|
||||||
.next-menu-item-inner {
|
|
||||||
height: var(--context-menu-item-height, 30px);
|
|
||||||
line-height: var(--context-menu-item-height, 30px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,236 +0,0 @@
|
|||||||
import {
|
|
||||||
IPublicTypeContextMenuAction,
|
|
||||||
IPublicEnumContextMenuType,
|
|
||||||
IPublicTypeContextMenuItem,
|
|
||||||
IPublicApiMaterial,
|
|
||||||
IPublicModelPluginContext,
|
|
||||||
IPublicTypeDisposable
|
|
||||||
} from '@alilc/lowcode-types';
|
|
||||||
import { IDesigner, INode } from './designer';
|
|
||||||
import {
|
|
||||||
createContextMenu,
|
|
||||||
parseContextMenuAsReactNode,
|
|
||||||
parseContextMenuProperties,
|
|
||||||
uniqueId,
|
|
||||||
} from '@alilc/lowcode-utils';
|
|
||||||
import { type AnyFunction } from '@alilc/lowcode-shared';
|
|
||||||
import { Menu } from '@alifd/next';
|
|
||||||
import { engineConfig } from '@alilc/lowcode-editor-core';
|
|
||||||
|
|
||||||
import './context-menu-actions.less';
|
|
||||||
|
|
||||||
let adjustMenuLayoutFn: AnyFunction = (actions: IPublicTypeContextMenuAction[]) => actions;
|
|
||||||
|
|
||||||
export class GlobalContextMenuActions {
|
|
||||||
enableContextMenu: boolean;
|
|
||||||
|
|
||||||
dispose: IPublicTypeDisposable[];
|
|
||||||
|
|
||||||
contextMenuActionsMap: Map<string, ContextMenuActions> = new Map();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.dispose = [];
|
|
||||||
|
|
||||||
engineConfig.onGot('enableContextMenu', (enable) => {
|
|
||||||
if (this.enableContextMenu === enable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.enableContextMenu = enable;
|
|
||||||
this.dispose.forEach((d) => d());
|
|
||||||
if (enable) {
|
|
||||||
this.initEvent();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContextMenu = (event: MouseEvent) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const actions: IPublicTypeContextMenuAction[] = [];
|
|
||||||
const contextMenu: ContextMenuActions = this.contextMenuActionsMap.values().next().value;
|
|
||||||
this.contextMenuActionsMap.forEach((contextMenu) => {
|
|
||||||
actions.push(...contextMenu.actions);
|
|
||||||
});
|
|
||||||
|
|
||||||
let destroyFn: AnyFunction | undefined = undefined;
|
|
||||||
|
|
||||||
const destroy = () => {
|
|
||||||
destroyFn?.();
|
|
||||||
};
|
|
||||||
const pluginContext: IPublicModelPluginContext = contextMenu.designer.editor.get(
|
|
||||||
'pluginContext',
|
|
||||||
) as IPublicModelPluginContext;
|
|
||||||
|
|
||||||
const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, {
|
|
||||||
nodes: [],
|
|
||||||
destroy,
|
|
||||||
event,
|
|
||||||
pluginContext,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!menus.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const layoutMenu = adjustMenuLayoutFn(menus);
|
|
||||||
|
|
||||||
const menuNode = parseContextMenuAsReactNode(layoutMenu, {
|
|
||||||
destroy,
|
|
||||||
nodes: [],
|
|
||||||
pluginContext,
|
|
||||||
});
|
|
||||||
|
|
||||||
const target = event.target;
|
|
||||||
const { top, left } = (target as any).getBoundingClientRect();
|
|
||||||
|
|
||||||
const menuInstance = Menu.create({
|
|
||||||
target: event.target,
|
|
||||||
offset: [event.clientX - left, event.clientY - top],
|
|
||||||
children: menuNode,
|
|
||||||
className: 'engine-context-menu',
|
|
||||||
});
|
|
||||||
|
|
||||||
destroyFn = (menuInstance as any).destroy;
|
|
||||||
};
|
|
||||||
|
|
||||||
initEvent() {
|
|
||||||
this.dispose.push(
|
|
||||||
(() => {
|
|
||||||
const handleContextMenu = (e: MouseEvent) => {
|
|
||||||
this.handleContextMenu(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('contextmenu', handleContextMenu);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('contextmenu', handleContextMenu);
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
registerContextMenuActions(contextMenu: ContextMenuActions) {
|
|
||||||
this.contextMenuActionsMap.set(contextMenu.id, contextMenu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalContextMenuActions = new GlobalContextMenuActions();
|
|
||||||
|
|
||||||
export class ContextMenuActions {
|
|
||||||
actions: IPublicTypeContextMenuAction[] = [];
|
|
||||||
|
|
||||||
designer: IDesigner;
|
|
||||||
|
|
||||||
dispose: AnyFunction[];
|
|
||||||
|
|
||||||
enableContextMenu: boolean;
|
|
||||||
|
|
||||||
id: string = uniqueId('contextMenu');
|
|
||||||
|
|
||||||
constructor(designer: IDesigner) {
|
|
||||||
this.designer = designer;
|
|
||||||
this.dispose = [];
|
|
||||||
|
|
||||||
engineConfig.onGot('enableContextMenu', (enable) => {
|
|
||||||
if (this.enableContextMenu === enable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.enableContextMenu = enable;
|
|
||||||
this.dispose.forEach((d) => d());
|
|
||||||
if (enable) {
|
|
||||||
this.initEvent();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
globalContextMenuActions.registerContextMenuActions(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContextMenu = (nodes: INode[], event: MouseEvent) => {
|
|
||||||
const designer = this.designer;
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const actions = designer.contextMenuActions.actions;
|
|
||||||
|
|
||||||
const { bounds } = designer.project.simulator?.viewport || { bounds: { left: 0, top: 0 } };
|
|
||||||
const { left: simulatorLeft, top: simulatorTop } = bounds;
|
|
||||||
|
|
||||||
let destroyFn: AnyFunction | undefined = undefined;
|
|
||||||
|
|
||||||
const destroy = () => {
|
|
||||||
destroyFn?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const pluginContext: IPublicModelPluginContext = this.designer.editor.get(
|
|
||||||
'pluginContext',
|
|
||||||
) as IPublicModelPluginContext;
|
|
||||||
|
|
||||||
const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, {
|
|
||||||
nodes: nodes.map((d) => designer.shellModelFactory.createNode(d)!),
|
|
||||||
destroy,
|
|
||||||
event,
|
|
||||||
pluginContext,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!menus.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const layoutMenu = adjustMenuLayoutFn(menus);
|
|
||||||
|
|
||||||
const menuNode = parseContextMenuAsReactNode(layoutMenu, {
|
|
||||||
destroy,
|
|
||||||
nodes: nodes.map((d) => designer.shellModelFactory.createNode(d)!),
|
|
||||||
pluginContext,
|
|
||||||
});
|
|
||||||
|
|
||||||
destroyFn = createContextMenu(menuNode, {
|
|
||||||
event,
|
|
||||||
offset: [simulatorLeft, simulatorTop],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
initEvent() {
|
|
||||||
const designer = this.designer;
|
|
||||||
this.dispose.push(
|
|
||||||
designer.editor.eventBus.on(
|
|
||||||
'designer.builtinSimulator.contextmenu',
|
|
||||||
({ node, originalEvent }: { node: INode; originalEvent: MouseEvent }) => {
|
|
||||||
originalEvent.stopPropagation();
|
|
||||||
originalEvent.preventDefault();
|
|
||||||
// 如果右键的节点不在 当前选中的节点中,选中该节点
|
|
||||||
if (!designer.currentSelection?.has(node.id)) {
|
|
||||||
designer.currentSelection?.select(node.id);
|
|
||||||
}
|
|
||||||
const nodes = designer.currentSelection?.getNodes() ?? [];
|
|
||||||
this.handleContextMenu(nodes, originalEvent);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
addMenuAction: IPublicApiMaterial['addContextMenuOption'] = (
|
|
||||||
action: IPublicTypeContextMenuAction,
|
|
||||||
) => {
|
|
||||||
this.actions.push({
|
|
||||||
type: IPublicEnumContextMenuType.MENU_ITEM,
|
|
||||||
...action,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
removeMenuAction: IPublicApiMaterial['removeContextMenuOption'] = (name: string) => {
|
|
||||||
const i = this.actions.findIndex((action) => action.name === name);
|
|
||||||
if (i > -1) {
|
|
||||||
this.actions.splice(i, 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
adjustMenuLayout: IPublicApiMaterial['adjustContextMenuLayout'] = (
|
|
||||||
fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[],
|
|
||||||
) => {
|
|
||||||
adjustMenuLayoutFn = fn;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IContextMenuActions extends ContextMenuActions {}
|
|
||||||
15
packages/designer/src/designer.ts
Normal file
15
packages/designer/src/designer.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export interface DesignerOptions {}
|
||||||
|
|
||||||
|
export class Designer {
|
||||||
|
constructor(options?: DesignerOptions) {}
|
||||||
|
|
||||||
|
#setupHistory() {}
|
||||||
|
|
||||||
|
onMounted() {}
|
||||||
|
|
||||||
|
purge() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDesigner(options?: DesignerOptions) {
|
||||||
|
return new Designer(options);
|
||||||
|
}
|
||||||
@ -1,51 +0,0 @@
|
|||||||
import { INode } from '../document/node/node';
|
|
||||||
import { observable, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
|
|
||||||
import { IPublicTypeActiveTarget, IPublicModelActiveTracker } from '@alilc/lowcode-types';
|
|
||||||
import { isNode } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export interface IActiveTracker extends Omit<IPublicModelActiveTracker, 'track' | 'onChange'> {
|
|
||||||
_target?: ActiveTarget | INode;
|
|
||||||
|
|
||||||
track(originalTarget: ActiveTarget | INode): void;
|
|
||||||
|
|
||||||
onChange(fn: (target: ActiveTarget) => void): () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ActiveTarget extends Omit<IPublicTypeActiveTarget, 'node'> {
|
|
||||||
node: INode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
export class ActiveTracker implements IActiveTracker {
|
|
||||||
@observable.ref private _target?: ActiveTarget | INode;
|
|
||||||
|
|
||||||
private emitter: IEventBus = createModuleEventBus('ActiveTracker');
|
|
||||||
|
|
||||||
track(originalTarget: ActiveTarget | INode) {
|
|
||||||
let target = originalTarget;
|
|
||||||
if (isNode(originalTarget)) {
|
|
||||||
target = { node: originalTarget as INode };
|
|
||||||
}
|
|
||||||
this._target = target;
|
|
||||||
this.emitter.emit('change', target);
|
|
||||||
}
|
|
||||||
|
|
||||||
get currentNode() {
|
|
||||||
return (this._target as ActiveTarget)?.node;
|
|
||||||
}
|
|
||||||
|
|
||||||
get detail() {
|
|
||||||
return (this._target as ActiveTarget)?.detail;
|
|
||||||
}
|
|
||||||
|
|
||||||
get instance() {
|
|
||||||
return (this._target as ActiveTarget)?.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(fn: (target: ActiveTarget) => void): () => void {
|
|
||||||
this.emitter.addListener('change', fn);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('change', fn);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
import { IPublicModelClipboard } from '@alilc/lowcode-types';
|
|
||||||
|
|
||||||
function getDataFromPasteEvent(event: ClipboardEvent) {
|
|
||||||
const { clipboardData } = event;
|
|
||||||
if (!clipboardData) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// { componentsMap, componentsTree, ... }
|
|
||||||
const data = JSON.parse(clipboardData.getData('text/plain'));
|
|
||||||
if (!data) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
if (data.componentsTree) {
|
|
||||||
return data;
|
|
||||||
} else if (data.componentName) {
|
|
||||||
return {
|
|
||||||
componentsTree: [data],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// TODO: open the parser implement
|
|
||||||
return { };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IClipboard extends IPublicModelClipboard {
|
|
||||||
|
|
||||||
initCopyPaster(el: HTMLTextAreaElement): void;
|
|
||||||
|
|
||||||
injectCopyPaster(document: Document): void;
|
|
||||||
}
|
|
||||||
class Clipboard implements IClipboard {
|
|
||||||
private copyPasters: HTMLTextAreaElement[] = [];
|
|
||||||
|
|
||||||
private waitFn?: (data: any, e: ClipboardEvent) => void;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.injectCopyPaster(document);
|
|
||||||
}
|
|
||||||
|
|
||||||
isCopyPasteEvent(e: Event) {
|
|
||||||
this.isCopyPaster(e.target);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isCopyPaster(el: any) {
|
|
||||||
return this.copyPasters.includes(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
initCopyPaster(el: HTMLTextAreaElement) {
|
|
||||||
this.copyPasters.push(el);
|
|
||||||
const onPaste = (e: ClipboardEvent) => {
|
|
||||||
if (this.waitFn) {
|
|
||||||
this.waitFn(getDataFromPasteEvent(e), e);
|
|
||||||
this.waitFn = undefined;
|
|
||||||
}
|
|
||||||
el.blur();
|
|
||||||
};
|
|
||||||
el.addEventListener('paste', onPaste, false);
|
|
||||||
return () => {
|
|
||||||
el.removeEventListener('paste', onPaste, false);
|
|
||||||
const i = this.copyPasters.indexOf(el);
|
|
||||||
if (i > -1) {
|
|
||||||
this.copyPasters.splice(i, 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
injectCopyPaster(document: Document) {
|
|
||||||
if (this.copyPasters.find((x) => x.ownerDocument === document)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const copyPaster = document.createElement<'textarea'>('textarea');
|
|
||||||
copyPaster.style.cssText = 'position: absolute;left: -9999px;top:-100px';
|
|
||||||
if (document.body) {
|
|
||||||
document.body.appendChild(copyPaster);
|
|
||||||
} else {
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
document.body.appendChild(copyPaster);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const dispose = this.initCopyPaster(copyPaster);
|
|
||||||
return () => {
|
|
||||||
dispose();
|
|
||||||
document.removeChild(copyPaster);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setData(data: any): void {
|
|
||||||
const copyPaster = this.copyPasters.find((x) => x.ownerDocument);
|
|
||||||
if (!copyPaster) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
copyPaster.value = typeof data === 'string' ? data : JSON.stringify(data);
|
|
||||||
copyPaster.select();
|
|
||||||
copyPaster.ownerDocument!.execCommand('copy');
|
|
||||||
|
|
||||||
copyPaster.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
waitPasteData(keyboardEvent: KeyboardEvent, cb: (data: any, e: ClipboardEvent) => void) {
|
|
||||||
const win = keyboardEvent.view;
|
|
||||||
if (!win) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const copyPaster = this.copyPasters.find((cp) => cp.ownerDocument === win.document);
|
|
||||||
if (copyPaster) {
|
|
||||||
copyPaster.select();
|
|
||||||
this.waitFn = cb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const clipboard = new Clipboard();
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
import { Component } from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import BuiltinDragGhostComponent from './drag-ghost';
|
|
||||||
import { Designer, DesignerProps, IDesigner } from './designer';
|
|
||||||
import { ProjectView } from '../project';
|
|
||||||
import './designer.less';
|
|
||||||
|
|
||||||
type IProps = DesignerProps & {
|
|
||||||
designer?: IDesigner;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class DesignerView extends Component<IProps> {
|
|
||||||
readonly designer: IDesigner;
|
|
||||||
readonly viewName: string | undefined;
|
|
||||||
|
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
const { designer, ...designerProps } = props;
|
|
||||||
this.viewName = designer?.viewName;
|
|
||||||
if (designer) {
|
|
||||||
this.designer = designer;
|
|
||||||
designer.setProps(designerProps);
|
|
||||||
} else {
|
|
||||||
this.designer = new Designer(designerProps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: DesignerProps) {
|
|
||||||
this.designer.setProps(nextProps);
|
|
||||||
const { props } = this;
|
|
||||||
if (
|
|
||||||
nextProps.className !== props.className ||
|
|
||||||
nextProps.style !== props.style ||
|
|
||||||
nextProps.dragGhostComponent !== props.dragGhostComponent
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { onMount } = this.props;
|
|
||||||
if (onMount) {
|
|
||||||
onMount(this.designer);
|
|
||||||
}
|
|
||||||
this.designer.postEvent('mount', this.designer);
|
|
||||||
}
|
|
||||||
|
|
||||||
UNSAFE_componentWillMount() {
|
|
||||||
this.designer.purge();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { className, style, dragGhostComponent } = this.props;
|
|
||||||
const DragGhost = dragGhostComponent || BuiltinDragGhostComponent;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames('lc-designer', className)} style={style}>
|
|
||||||
<DragGhost designer={this.designer} />
|
|
||||||
<ProjectView designer={this.designer} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
.lc-designer {
|
|
||||||
position: relative;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
font-size: var(--font-size-text);
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,556 +0,0 @@
|
|||||||
import { ComponentType } from 'react';
|
|
||||||
import { observable, computed, autorun, makeObservable, IReactionPublic, IReactionOptions, IReactionDisposer } from '@alilc/lowcode-editor-core';
|
|
||||||
import {
|
|
||||||
IPublicTypeProjectSchema,
|
|
||||||
IPublicTypeComponentMetadata,
|
|
||||||
IPublicTypeComponentAction,
|
|
||||||
IPublicTypeNpmInfo,
|
|
||||||
IPublicModelEditor,
|
|
||||||
IPublicTypePropsList,
|
|
||||||
IPublicTypePropsTransducer,
|
|
||||||
IShellModelFactory,
|
|
||||||
IPublicModelDragObject,
|
|
||||||
IPublicTypeScrollable,
|
|
||||||
IPublicModelScroller,
|
|
||||||
IPublicTypeLocationData,
|
|
||||||
IPublicEnumTransformStage,
|
|
||||||
IPublicModelLocateEvent,
|
|
||||||
IPublicTypePropsMap,
|
|
||||||
} from '@alilc/lowcode-types';
|
|
||||||
import { mergeAssets, IPublicTypeAssetsJson, isNodeSchema, isDragNodeObject, isDragNodeDataObject, isLocationChildrenDetail, Logger } from '@alilc/lowcode-utils';
|
|
||||||
import { IProject, Project } from '../project';
|
|
||||||
import { Node, DocumentModel, insertChildren, INode } from '../document';
|
|
||||||
import { ComponentMeta, IComponentMeta } from '../component-meta';
|
|
||||||
import { INodeSelector, Component } from '../simulator';
|
|
||||||
import { Scroller } from './scroller';
|
|
||||||
import { Dragon, IDragon } from './dragon';
|
|
||||||
import { ActiveTracker } from './active-tracker';
|
|
||||||
import { Detecting } from './detecting';
|
|
||||||
import { DropLocation } from './location';
|
|
||||||
import { OffsetObserver, createOffsetObserver } from './offset-observer';
|
|
||||||
import { ISettingTopEntry, SettingTopEntry } from './setting';
|
|
||||||
import { BemToolsManager } from '../builtin-simulator/bem-tools/manager';
|
|
||||||
import { ComponentActions } from '../component-actions';
|
|
||||||
import { ContextMenuActions, IContextMenuActions } from '../context-menu-actions';
|
|
||||||
|
|
||||||
const logger = new Logger({ level: 'warn', bizName: 'designer' });
|
|
||||||
|
|
||||||
export interface DesignerProps {
|
|
||||||
[key: string]: any;
|
|
||||||
editor: IPublicModelEditor;
|
|
||||||
shellModelFactory: IShellModelFactory;
|
|
||||||
className?: string;
|
|
||||||
style?: object;
|
|
||||||
defaultSchema?: IPublicTypeProjectSchema;
|
|
||||||
hotkeys?: object;
|
|
||||||
viewName?: string;
|
|
||||||
simulatorProps?: Record<string, any> | ((document: DocumentModel) => object);
|
|
||||||
simulatorComponent?: ComponentType<any>;
|
|
||||||
dragGhostComponent?: ComponentType<{ designer: IDesigner }>;
|
|
||||||
suspensed?: boolean;
|
|
||||||
componentMetadatas?: IPublicTypeComponentMetadata[];
|
|
||||||
globalComponentActions?: IPublicTypeComponentAction[];
|
|
||||||
onMount?: (designer: Designer) => void;
|
|
||||||
onDragstart?: (e: IPublicModelLocateEvent) => void;
|
|
||||||
onDrag?: (e: IPublicModelLocateEvent) => void;
|
|
||||||
onDragend?: (
|
|
||||||
e: { dragObject: IPublicModelDragObject; copy: boolean },
|
|
||||||
loc?: DropLocation,
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Designer {
|
|
||||||
dragon: IDragon;
|
|
||||||
|
|
||||||
readonly viewName: string | undefined;
|
|
||||||
|
|
||||||
readonly componentActions = new ComponentActions();
|
|
||||||
|
|
||||||
readonly contextMenuActions: IContextMenuActions;
|
|
||||||
|
|
||||||
readonly activeTracker = new ActiveTracker();
|
|
||||||
|
|
||||||
readonly detecting = new Detecting();
|
|
||||||
|
|
||||||
readonly project: IProject;
|
|
||||||
|
|
||||||
readonly editor: IPublicModelEditor;
|
|
||||||
|
|
||||||
readonly bemToolsManager = new BemToolsManager(this);
|
|
||||||
|
|
||||||
readonly shellModelFactory: IShellModelFactory;
|
|
||||||
|
|
||||||
private _dropLocation?: DropLocation;
|
|
||||||
|
|
||||||
private propsReducers = new Map<IPublicEnumTransformStage, IPublicTypePropsTransducer[]>();
|
|
||||||
|
|
||||||
private _lostComponentMetasMap = new Map<string, ComponentMeta>();
|
|
||||||
|
|
||||||
private props?: DesignerProps;
|
|
||||||
|
|
||||||
private oobxList: OffsetObserver[] = [];
|
|
||||||
|
|
||||||
private selectionDispose: undefined | (() => void);
|
|
||||||
|
|
||||||
@observable.ref private _componentMetasMap = new Map<string, IComponentMeta>();
|
|
||||||
|
|
||||||
@observable.ref private _simulatorComponent?: ComponentType<any>;
|
|
||||||
|
|
||||||
@observable.ref private _simulatorProps?: Record<string, any> | ((project: IProject) => object);
|
|
||||||
|
|
||||||
@observable.ref private _suspensed = false;
|
|
||||||
|
|
||||||
get currentDocument() {
|
|
||||||
return this.project.currentDocument;
|
|
||||||
}
|
|
||||||
|
|
||||||
get currentHistory() {
|
|
||||||
return this.currentDocument?.history;
|
|
||||||
}
|
|
||||||
|
|
||||||
get currentSelection() {
|
|
||||||
return this.currentDocument?.selection;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props: DesignerProps) {
|
|
||||||
makeObservable(this);
|
|
||||||
const { editor, viewName, shellModelFactory } = props;
|
|
||||||
this.editor = editor;
|
|
||||||
this.viewName = viewName;
|
|
||||||
this.shellModelFactory = shellModelFactory;
|
|
||||||
this.setProps(props);
|
|
||||||
|
|
||||||
this.project = new Project(this, props.defaultSchema, viewName);
|
|
||||||
|
|
||||||
this.dragon = new Dragon(this);
|
|
||||||
this.dragon.onDragstart((e) => {
|
|
||||||
this.detecting.enable = false;
|
|
||||||
const { dragObject } = e;
|
|
||||||
if (isDragNodeObject(dragObject)) {
|
|
||||||
if (dragObject.nodes.length === 1) {
|
|
||||||
if (dragObject.nodes[0].parent) {
|
|
||||||
// ensure current selecting
|
|
||||||
dragObject.nodes[0].select();
|
|
||||||
} else {
|
|
||||||
this.currentSelection?.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.currentSelection?.clear();
|
|
||||||
}
|
|
||||||
if (this.props?.onDragstart) {
|
|
||||||
this.props.onDragstart(e);
|
|
||||||
}
|
|
||||||
this.postEvent('dragstart', e);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.contextMenuActions = new ContextMenuActions(this);
|
|
||||||
|
|
||||||
this.dragon.onDrag((e) => {
|
|
||||||
if (this.props?.onDrag) {
|
|
||||||
this.props.onDrag(e);
|
|
||||||
}
|
|
||||||
this.postEvent('drag', e);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.dragon.onDragend((e) => {
|
|
||||||
const { dragObject, copy } = e;
|
|
||||||
logger.debug('onDragend: dragObject ', dragObject, ' copy ', copy);
|
|
||||||
const loc = this._dropLocation;
|
|
||||||
if (loc) {
|
|
||||||
if (isLocationChildrenDetail(loc.detail) && loc.detail.valid !== false) {
|
|
||||||
let nodes: INode[] | undefined;
|
|
||||||
if (isDragNodeObject(dragObject)) {
|
|
||||||
nodes = insertChildren(loc.target, [...dragObject.nodes], loc.detail.index, copy);
|
|
||||||
} else if (isDragNodeDataObject(dragObject)) {
|
|
||||||
// process nodeData
|
|
||||||
const nodeData = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data];
|
|
||||||
const isNotNodeSchema = nodeData.find((item) => !isNodeSchema(item));
|
|
||||||
if (isNotNodeSchema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
nodes = insertChildren(loc.target, nodeData, loc.detail.index);
|
|
||||||
}
|
|
||||||
if (nodes) {
|
|
||||||
loc.document?.selection.selectAll(nodes.map((o) => o.id));
|
|
||||||
setTimeout(() => this.activeTracker.track(nodes![0]), 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.props?.onDragend) {
|
|
||||||
this.props.onDragend(e, loc);
|
|
||||||
}
|
|
||||||
this.postEvent('dragend', e, loc);
|
|
||||||
this.detecting.enable = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.activeTracker.onChange(({ node, detail }) => {
|
|
||||||
node.document?.simulator?.scrollToNode(node, detail);
|
|
||||||
});
|
|
||||||
|
|
||||||
let historyDispose: undefined | (() => void);
|
|
||||||
const setupHistory = () => {
|
|
||||||
if (historyDispose) {
|
|
||||||
historyDispose();
|
|
||||||
historyDispose = undefined;
|
|
||||||
}
|
|
||||||
this.postEvent('history.change', this.currentHistory);
|
|
||||||
if (this.currentHistory) {
|
|
||||||
const { currentHistory } = this;
|
|
||||||
historyDispose = currentHistory.onStateChange(() => {
|
|
||||||
this.postEvent('history.change', currentHistory);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.project.onCurrentDocumentChange(() => {
|
|
||||||
this.postEvent('current-document.change', this.currentDocument);
|
|
||||||
this.postEvent('selection.change', this.currentSelection);
|
|
||||||
this.postEvent('history.change', this.currentHistory);
|
|
||||||
this.setupSelection();
|
|
||||||
setupHistory();
|
|
||||||
});
|
|
||||||
this.postEvent('init', this);
|
|
||||||
this.setupSelection();
|
|
||||||
setupHistory();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSelection = () => {
|
|
||||||
if (this.selectionDispose) {
|
|
||||||
this.selectionDispose();
|
|
||||||
this.selectionDispose = undefined;
|
|
||||||
}
|
|
||||||
const { currentSelection } = this;
|
|
||||||
// TODO: 避免选中 Page 组件,默认选中第一个子节点;新增规则 或 判断 Live 模式
|
|
||||||
if (
|
|
||||||
currentSelection &&
|
|
||||||
currentSelection.selected.length === 0 &&
|
|
||||||
this.simulatorProps?.designMode === 'live'
|
|
||||||
) {
|
|
||||||
const rootNodeChildrens = this.currentDocument?.getRoot()?.getChildren()?.children;
|
|
||||||
if (rootNodeChildrens && rootNodeChildrens.length > 0) {
|
|
||||||
currentSelection.select(rootNodeChildrens[0].id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.postEvent('selection.change', currentSelection);
|
|
||||||
if (currentSelection) {
|
|
||||||
this.selectionDispose = currentSelection.onSelectionChange(() => {
|
|
||||||
this.postEvent('selection.change', currentSelection);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
postEvent(event: string, ...args: any[]) {
|
|
||||||
this.editor.eventBus.emit(`designer.${event}`, ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
get dropLocation() {
|
|
||||||
return this._dropLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建插入位置,考虑放到 dragon 中
|
|
||||||
*/
|
|
||||||
createLocation(locationData: IPublicTypeLocationData<INode>): DropLocation {
|
|
||||||
const loc = new DropLocation(locationData);
|
|
||||||
if (this._dropLocation && this._dropLocation.document && this._dropLocation.document !== loc.document) {
|
|
||||||
this._dropLocation.document.dropLocation = null;
|
|
||||||
}
|
|
||||||
this._dropLocation = loc;
|
|
||||||
this.postEvent('dropLocation.change', loc);
|
|
||||||
if (loc.document) {
|
|
||||||
loc.document.dropLocation = loc;
|
|
||||||
}
|
|
||||||
this.activeTracker.track({ node: loc.target, detail: loc.detail });
|
|
||||||
return loc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除插入位置
|
|
||||||
*/
|
|
||||||
clearLocation() {
|
|
||||||
if (this._dropLocation && this._dropLocation.document) {
|
|
||||||
this._dropLocation.document.dropLocation = null;
|
|
||||||
}
|
|
||||||
this.postEvent('dropLocation.change', undefined);
|
|
||||||
this._dropLocation = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
createScroller(scrollable: IPublicTypeScrollable): IPublicModelScroller {
|
|
||||||
return new Scroller(scrollable);
|
|
||||||
}
|
|
||||||
|
|
||||||
createOffsetObserver(nodeInstance: INodeSelector): OffsetObserver | null {
|
|
||||||
const oobx = createOffsetObserver(nodeInstance);
|
|
||||||
this.clearOobxList();
|
|
||||||
if (oobx) {
|
|
||||||
this.oobxList.push(oobx);
|
|
||||||
}
|
|
||||||
return oobx;
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearOobxList(force?: boolean) {
|
|
||||||
let l = this.oobxList.length;
|
|
||||||
if (l > 20 || force) {
|
|
||||||
while (l-- > 0) {
|
|
||||||
if (this.oobxList[l].isPurged()) {
|
|
||||||
this.oobxList.splice(l, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
touchOffsetObserver() {
|
|
||||||
this.clearOobxList(true);
|
|
||||||
this.oobxList.forEach((item) => item.compute());
|
|
||||||
}
|
|
||||||
|
|
||||||
createSettingEntry(nodes: INode[]): ISettingTopEntry {
|
|
||||||
return new SettingTopEntry(this.editor, nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
setProps(nextProps: DesignerProps) {
|
|
||||||
const props = this.props ? { ...this.props, ...nextProps } : nextProps;
|
|
||||||
if (this.props) {
|
|
||||||
// check hotkeys
|
|
||||||
// TODO:
|
|
||||||
// check simulatorConfig
|
|
||||||
if (props.simulatorComponent !== this.props.simulatorComponent) {
|
|
||||||
this._simulatorComponent = props.simulatorComponent;
|
|
||||||
}
|
|
||||||
if (props.simulatorProps !== this.props.simulatorProps) {
|
|
||||||
this._simulatorProps = props.simulatorProps;
|
|
||||||
// 重新 setupSelection
|
|
||||||
if ((props.simulatorProps as any)?.designMode !== (this.props.simulatorProps as any)?.designMode) {
|
|
||||||
this.setupSelection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (props.suspensed !== this.props.suspensed && props.suspensed != null) {
|
|
||||||
this.suspensed = props.suspensed;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
props.componentMetadatas !== this.props.componentMetadatas &&
|
|
||||||
props.componentMetadatas != null
|
|
||||||
) {
|
|
||||||
this.buildComponentMetasMap(props.componentMetadatas);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// init hotkeys
|
|
||||||
// todo:
|
|
||||||
// init simulatorConfig
|
|
||||||
if (props.simulatorComponent) {
|
|
||||||
this._simulatorComponent = props.simulatorComponent;
|
|
||||||
}
|
|
||||||
if (props.simulatorProps) {
|
|
||||||
this._simulatorProps = props.simulatorProps;
|
|
||||||
}
|
|
||||||
// init suspensed
|
|
||||||
if (props.suspensed != null) {
|
|
||||||
this.suspensed = props.suspensed;
|
|
||||||
}
|
|
||||||
if (props.componentMetadatas != null) {
|
|
||||||
this.buildComponentMetasMap(props.componentMetadatas);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.props = props;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadIncrementalAssets(incrementalAssets: IPublicTypeAssetsJson): Promise<void> {
|
|
||||||
const { components, packages } = incrementalAssets;
|
|
||||||
components && this.buildComponentMetasMap(components);
|
|
||||||
if (packages) {
|
|
||||||
await this.project.simulator?.setupComponents(packages);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (components) {
|
|
||||||
// 合并 assets
|
|
||||||
const assets = this.editor.get('assets') || {};
|
|
||||||
const newAssets = mergeAssets(assets, incrementalAssets);
|
|
||||||
// 对于 assets 存在需要二次网络下载的过程,必须 await 等待结束之后,再进行事件触发
|
|
||||||
await this.editor.set('assets', newAssets);
|
|
||||||
}
|
|
||||||
// TODO: 因为涉及修改 prototype.view,之后在 renderer 里修改了 vc 的 view 获取逻辑后,可删除
|
|
||||||
this.refreshComponentMetasMap();
|
|
||||||
// 完成加载增量资源后发送事件,方便插件监听并处理相关逻辑
|
|
||||||
this.editor.eventBus.emit('designer.incrementalAssetsReady');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新 componentMetasMap,可间接触发模拟器里的 buildComponents
|
|
||||||
*/
|
|
||||||
refreshComponentMetasMap() {
|
|
||||||
this._componentMetasMap = new Map(this._componentMetasMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
get(key: string): any {
|
|
||||||
return this.props?.[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get simulatorComponent(): ComponentType<any> | undefined {
|
|
||||||
return this._simulatorComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get simulatorProps(): Record<string, any> {
|
|
||||||
if (typeof this._simulatorProps === 'function') {
|
|
||||||
return this._simulatorProps(this.project);
|
|
||||||
}
|
|
||||||
return this._simulatorProps || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提供给模拟器的参数
|
|
||||||
*/
|
|
||||||
@computed get projectSimulatorProps(): any {
|
|
||||||
return {
|
|
||||||
...this.simulatorProps,
|
|
||||||
project: this.project,
|
|
||||||
designer: this,
|
|
||||||
onMount: (simulator: any) => {
|
|
||||||
this.project.mountSimulator(simulator);
|
|
||||||
this.editor.set('simulator', simulator);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get suspensed(): boolean {
|
|
||||||
return this._suspensed;
|
|
||||||
}
|
|
||||||
|
|
||||||
set suspensed(flag: boolean) {
|
|
||||||
this._suspensed = flag;
|
|
||||||
// Todo afterwards...
|
|
||||||
if (flag) {
|
|
||||||
// this.project.suspensed = true?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get schema(): IPublicTypeProjectSchema {
|
|
||||||
return this.project.getSchema();
|
|
||||||
}
|
|
||||||
|
|
||||||
setSchema(schema?: IPublicTypeProjectSchema) {
|
|
||||||
this.project.load(schema);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildComponentMetasMap(metas: IPublicTypeComponentMetadata[]) {
|
|
||||||
metas.forEach((data) => this.createComponentMeta(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
createComponentMeta(data: IPublicTypeComponentMetadata): IComponentMeta | null {
|
|
||||||
const key = data.componentName;
|
|
||||||
if (!key) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let meta = this._componentMetasMap.get(key);
|
|
||||||
if (meta) {
|
|
||||||
meta.setMetadata(data);
|
|
||||||
|
|
||||||
this._componentMetasMap.set(key, meta);
|
|
||||||
} else {
|
|
||||||
meta = this._lostComponentMetasMap.get(key);
|
|
||||||
|
|
||||||
if (meta) {
|
|
||||||
meta.setMetadata(data);
|
|
||||||
this._lostComponentMetasMap.delete(key);
|
|
||||||
} else {
|
|
||||||
meta = new ComponentMeta(this, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._componentMetasMap.set(key, meta);
|
|
||||||
}
|
|
||||||
return meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
getGlobalComponentActions(): IPublicTypeComponentAction[] | null {
|
|
||||||
return this.props?.globalComponentActions || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getComponentMeta(
|
|
||||||
componentName: string,
|
|
||||||
generateMetadata?: () => IPublicTypeComponentMetadata | null,
|
|
||||||
): IComponentMeta {
|
|
||||||
if (this._componentMetasMap.has(componentName)) {
|
|
||||||
return this._componentMetasMap.get(componentName)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._lostComponentMetasMap.has(componentName)) {
|
|
||||||
return this._lostComponentMetasMap.get(componentName)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
const meta = new ComponentMeta(this, {
|
|
||||||
componentName,
|
|
||||||
...(generateMetadata ? generateMetadata() : null),
|
|
||||||
});
|
|
||||||
|
|
||||||
this._lostComponentMetasMap.set(componentName, meta);
|
|
||||||
|
|
||||||
return meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
getComponentMetasMap() {
|
|
||||||
return this._componentMetasMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get componentsMap(): { [key: string]: IPublicTypeNpmInfo | Component } {
|
|
||||||
const maps: any = {};
|
|
||||||
const designer = this;
|
|
||||||
designer._componentMetasMap.forEach((config, key) => {
|
|
||||||
const metaData = config.getMetadata();
|
|
||||||
if (metaData.devMode === 'lowCode') {
|
|
||||||
maps[key] = metaData.schema;
|
|
||||||
} else {
|
|
||||||
const { view } = config.advanced;
|
|
||||||
if (view) {
|
|
||||||
maps[key] = view;
|
|
||||||
} else {
|
|
||||||
maps[key] = config.npm;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return maps;
|
|
||||||
}
|
|
||||||
|
|
||||||
transformProps(props: IPublicTypePropsMap | IPublicTypePropsList, node: Node, stage: IPublicEnumTransformStage): IPublicTypePropsMap | IPublicTypePropsList {
|
|
||||||
if (Array.isArray(props)) {
|
|
||||||
// current not support, make this future
|
|
||||||
return props;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reducers = this.propsReducers.get(stage);
|
|
||||||
if (!reducers) {
|
|
||||||
return props;
|
|
||||||
}
|
|
||||||
|
|
||||||
return reducers.reduce((xprops, reducer: IPublicTypePropsTransducer) => {
|
|
||||||
try {
|
|
||||||
return reducer(xprops, node.internalToShellNode() as any, { stage });
|
|
||||||
} catch (e) {
|
|
||||||
// todo: add log
|
|
||||||
console.warn(e);
|
|
||||||
return xprops;
|
|
||||||
}
|
|
||||||
}, props);
|
|
||||||
}
|
|
||||||
|
|
||||||
addPropsReducer(reducer: IPublicTypePropsTransducer, stage: IPublicEnumTransformStage) {
|
|
||||||
if (!reducer) {
|
|
||||||
logger.error('reducer is not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reducers = this.propsReducers.get(stage);
|
|
||||||
if (reducers) {
|
|
||||||
reducers.push(reducer);
|
|
||||||
} else {
|
|
||||||
this.propsReducers.set(stage, [reducer]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
autorun(effect: (reaction: IReactionPublic) => void, options?: IReactionOptions<any, any>): IReactionDisposer {
|
|
||||||
return autorun(effect, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
purge() {
|
|
||||||
// TODO:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IDesigner extends Designer {}
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import { makeObservable, observable, IEventBus, createModuleEventBus, action } from '@alilc/lowcode-editor-core';
|
|
||||||
import { IPublicModelDetecting } from '@alilc/lowcode-types';
|
|
||||||
import type { IDocumentModel } from '../document/document-model';
|
|
||||||
import type { INode } from '../document/node/node';
|
|
||||||
|
|
||||||
const DETECTING_CHANGE_EVENT = 'detectingChange';
|
|
||||||
export interface IDetecting extends Omit<IPublicModelDetecting<INode>,
|
|
||||||
'capture' |
|
|
||||||
'release' |
|
|
||||||
'leave'
|
|
||||||
> {
|
|
||||||
capture(node: INode | null): void;
|
|
||||||
|
|
||||||
release(node: INode | null): void;
|
|
||||||
|
|
||||||
leave(document: IDocumentModel | undefined): void;
|
|
||||||
|
|
||||||
get current(): INode | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Detecting implements IDetecting {
|
|
||||||
@observable.ref private _enable = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 控制大纲树 hover 时是否出现悬停效果
|
|
||||||
* TODO: 将该逻辑从设计器中抽离出来
|
|
||||||
*/
|
|
||||||
get enable() {
|
|
||||||
return this._enable;
|
|
||||||
}
|
|
||||||
|
|
||||||
set enable(flag: boolean) {
|
|
||||||
this._enable = flag;
|
|
||||||
if (!flag) {
|
|
||||||
this._current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable.ref xRayMode = false;
|
|
||||||
|
|
||||||
@observable.ref private _current: INode | null = null;
|
|
||||||
|
|
||||||
private emitter: IEventBus = createModuleEventBus('Detecting');
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
makeObservable(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
get current() {
|
|
||||||
return this._current;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
capture(node: INode | null) {
|
|
||||||
if (this._current !== node) {
|
|
||||||
this._current = node;
|
|
||||||
this.emitter.emit(DETECTING_CHANGE_EVENT, this.current);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
release(node: INode | null) {
|
|
||||||
if (this._current === node) {
|
|
||||||
this._current = null;
|
|
||||||
this.emitter.emit(DETECTING_CHANGE_EVENT, this.current);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
leave(document: IDocumentModel | undefined) {
|
|
||||||
if (this.current && this.current.document === document) {
|
|
||||||
this._current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDetectingChange(fn: (node: INode) => void) {
|
|
||||||
this.emitter.on(DETECTING_CHANGE_EVENT, fn);
|
|
||||||
return () => {
|
|
||||||
this.emitter.off(DETECTING_CHANGE_EVENT, fn);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
内置拖拽替身
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
.lc-ghost-group {
|
|
||||||
box-sizing: border-box;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 99999;
|
|
||||||
width: 100px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
pointer-events: none;
|
|
||||||
background-color: var(--color-block-background-deep-dark, rgba(0, 0, 0, 0.4));
|
|
||||||
box-shadow: 0 0 6px var(--color-block-background-shallow, grey);
|
|
||||||
transform: translate(-10%, -50%);
|
|
||||||
.lc-ghost {
|
|
||||||
.lc-ghost-title {
|
|
||||||
text-align: center;
|
|
||||||
font-size: var(--font-size-text);
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
color: var(--color-text-light);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
import { Component, ReactElement } from 'react';
|
|
||||||
import { observer, observable, makeObservable, action } from '@alilc/lowcode-editor-core';
|
|
||||||
import {
|
|
||||||
IPublicTypeI18nData,
|
|
||||||
IPublicTypeNodeSchema,
|
|
||||||
IPublicModelDragObject,
|
|
||||||
} from '@alilc/lowcode-types';
|
|
||||||
import { isDragNodeObject } from '@alilc/lowcode-utils';
|
|
||||||
import { Designer } from '../designer';
|
|
||||||
import { isSimulatorHost } from '../../simulator';
|
|
||||||
import { Title } from '../../widgets';
|
|
||||||
|
|
||||||
import './ghost.less';
|
|
||||||
|
|
||||||
type offBinding = () => any;
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export default class DragGhost extends Component<{ designer: Designer }> {
|
|
||||||
private dispose: offBinding[] = [];
|
|
||||||
|
|
||||||
@observable.ref private titles: (string | IPublicTypeI18nData | ReactElement)[] | null = null;
|
|
||||||
|
|
||||||
@observable.ref private x = 0;
|
|
||||||
|
|
||||||
@observable.ref private y = 0;
|
|
||||||
|
|
||||||
@observable private isAbsoluteLayoutContainer = false;
|
|
||||||
|
|
||||||
private dragon = this.props.designer.dragon;
|
|
||||||
|
|
||||||
constructor(props: any) {
|
|
||||||
super(props);
|
|
||||||
makeObservable(this);
|
|
||||||
this.dispose = [
|
|
||||||
this.dragon.onDragstart(action((e) => {
|
|
||||||
if (e.originalEvent.type.slice(0, 4) === 'drag') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.titles = this.getTitles(e.dragObject!) as any;
|
|
||||||
this.x = e.globalX;
|
|
||||||
this.y = e.globalY;
|
|
||||||
})),
|
|
||||||
this.dragon.onDrag(action((e) => {
|
|
||||||
this.x = e.globalX;
|
|
||||||
this.y = e.globalY;
|
|
||||||
if (isSimulatorHost(e.sensor)) {
|
|
||||||
const container = e.sensor.getDropContainer(e);
|
|
||||||
if (container?.container.componentMeta.advanced.isAbsoluteLayoutContainer) {
|
|
||||||
this.isAbsoluteLayoutContainer = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.isAbsoluteLayoutContainer = false;
|
|
||||||
})),
|
|
||||||
this.dragon.onDragend(action(() => {
|
|
||||||
this.titles = null;
|
|
||||||
this.x = 0;
|
|
||||||
this.y = 0;
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
getTitles(dragObject: IPublicModelDragObject) {
|
|
||||||
if (isDragNodeObject(dragObject)) {
|
|
||||||
return dragObject.nodes.map((node) => node?.title);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataList = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data!];
|
|
||||||
|
|
||||||
return dataList.map(
|
|
||||||
(item: IPublicTypeNodeSchema) =>
|
|
||||||
this.props.designer.getComponentMeta(item.componentName).title,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.dispose) {
|
|
||||||
this.dispose.forEach((off) => off());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderGhostGroup() {
|
|
||||||
return this.titles?.map((title, i) => {
|
|
||||||
const ghost = (
|
|
||||||
<div className="lc-ghost" key={i}>
|
|
||||||
<Title title={title} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return ghost;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!this.titles || !this.titles.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isAbsoluteLayoutContainer) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="lc-ghost-group"
|
|
||||||
style={{
|
|
||||||
left: this.x,
|
|
||||||
top: this.y,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{this.renderGhostGroup()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,614 +0,0 @@
|
|||||||
import { observable, makeObservable, IEventBus, createModuleEventBus, action, autorun } from '@alilc/lowcode-editor-core';
|
|
||||||
import {
|
|
||||||
IPublicModelDragObject,
|
|
||||||
IPublicModelNode,
|
|
||||||
IPublicModelDragon,
|
|
||||||
IPublicModelLocateEvent,
|
|
||||||
IPublicModelSensor,
|
|
||||||
} from '@alilc/lowcode-types';
|
|
||||||
import { setNativeSelection, cursor, isDragNodeObject } from '@alilc/lowcode-utils';
|
|
||||||
import { INode, Node } from '../document';
|
|
||||||
import { ISimulatorHost, isSimulatorHost } from '../simulator';
|
|
||||||
import { IDesigner } from './designer';
|
|
||||||
import { makeEventsHandler } from '../utils/misc';
|
|
||||||
|
|
||||||
export interface ILocateEvent extends IPublicModelLocateEvent {
|
|
||||||
readonly type: 'LocateEvent';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 激活的感应器
|
|
||||||
*/
|
|
||||||
sensor?: IPublicModelSensor;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLocateEvent(e: any): e is ILocateEvent {
|
|
||||||
return e && e.type === 'LocateEvent';
|
|
||||||
}
|
|
||||||
|
|
||||||
const SHAKE_DISTANCE = 4;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* mouse shake check
|
|
||||||
*/
|
|
||||||
export function isShaken(e1: MouseEvent | DragEvent, e2: MouseEvent | DragEvent): boolean {
|
|
||||||
if ((e1 as any).shaken) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (e1.target !== e2.target) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
Math.pow(e1.clientY - e2.clientY, 2) + Math.pow(e1.clientX - e2.clientX, 2) > SHAKE_DISTANCE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isInvalidPoint(e: any, last: any): boolean {
|
|
||||||
return (
|
|
||||||
e.clientX === 0 &&
|
|
||||||
e.clientY === 0 &&
|
|
||||||
last &&
|
|
||||||
(Math.abs(last.clientX - e.clientX) > 5 || Math.abs(last.clientY - e.clientY) > 5)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSameAs(e1: MouseEvent | DragEvent, e2: MouseEvent | DragEvent): boolean {
|
|
||||||
return e1.clientY === e2.clientY && e1.clientX === e2.clientX;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setShaken(e: any) {
|
|
||||||
e.shaken = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSourceSensor(dragObject: IPublicModelDragObject): ISimulatorHost | null {
|
|
||||||
if (!isDragNodeObject(dragObject)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (dragObject.nodes[0]?.document as any)?.simulator || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDragEvent(e: any): e is DragEvent {
|
|
||||||
return e?.type?.startsWith('drag');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drag-on 拖拽引擎
|
|
||||||
*/
|
|
||||||
export class Dragon implements IPublicModelDragon<INode, ILocateEvent> {
|
|
||||||
private sensors: IPublicModelSensor[] = [];
|
|
||||||
|
|
||||||
private nodeInstPointerEvents: boolean;
|
|
||||||
|
|
||||||
key = Math.random();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* current active sensor, 可用于感应区高亮
|
|
||||||
*/
|
|
||||||
@observable.ref private _activeSensor: IPublicModelSensor | undefined;
|
|
||||||
|
|
||||||
get activeSensor(): IPublicModelSensor | undefined {
|
|
||||||
return this._activeSensor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable.ref private _dragging = false;
|
|
||||||
|
|
||||||
@observable.ref private _canDrop = false;
|
|
||||||
|
|
||||||
get dragging(): boolean {
|
|
||||||
return this._dragging;
|
|
||||||
}
|
|
||||||
|
|
||||||
viewName: string | undefined;
|
|
||||||
|
|
||||||
emitter: IEventBus = createModuleEventBus('Dragon');
|
|
||||||
|
|
||||||
constructor(readonly designer: IDesigner) {
|
|
||||||
makeObservable(this);
|
|
||||||
this.viewName = designer.viewName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Quick listen a shell(container element) drag behavior
|
|
||||||
* @param shell container element
|
|
||||||
* @param boost boost got a drag object
|
|
||||||
*/
|
|
||||||
from(shell: Element, boost: (e: MouseEvent) => IPublicModelDragObject | null) {
|
|
||||||
const mousedown = (e: MouseEvent) => {
|
|
||||||
// ESC or RightClick
|
|
||||||
if (e.which === 3 || e.button === 2) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a new node to be dragged
|
|
||||||
const dragObject = boost(e);
|
|
||||||
if (!dragObject) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.boost(dragObject, e);
|
|
||||||
};
|
|
||||||
shell.addEventListener('mousedown', mousedown as any);
|
|
||||||
return () => {
|
|
||||||
shell.removeEventListener('mousedown', mousedown as any);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* boost your dragObject for dragging(flying) 发射拖拽对象
|
|
||||||
*
|
|
||||||
* @param dragObject 拖拽对象
|
|
||||||
* @param boostEvent 拖拽初始时事件
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
boost(
|
|
||||||
dragObject: IPublicModelDragObject,
|
|
||||||
boostEvent: MouseEvent | DragEvent,
|
|
||||||
fromRglNode?: INode | IPublicModelNode,
|
|
||||||
) {
|
|
||||||
const { designer } = this;
|
|
||||||
const masterSensors = this.getMasterSensors();
|
|
||||||
const handleEvents = makeEventsHandler(boostEvent, masterSensors);
|
|
||||||
const newBie = !isDragNodeObject(dragObject);
|
|
||||||
const forceCopyState =
|
|
||||||
isDragNodeObject(dragObject) &&
|
|
||||||
dragObject.nodes.some((node: Node | IPublicModelNode) =>
|
|
||||||
typeof node.isSlot === 'function' ? node.isSlot() : node.isSlot,
|
|
||||||
);
|
|
||||||
const isBoostFromDragAPI = isDragEvent(boostEvent);
|
|
||||||
let lastSensor: IPublicModelSensor | undefined;
|
|
||||||
|
|
||||||
this._dragging = false;
|
|
||||||
|
|
||||||
const getRGL = (e: MouseEvent | DragEvent) => {
|
|
||||||
const locateEvent = createLocateEvent(e);
|
|
||||||
const sensor = chooseSensor(locateEvent);
|
|
||||||
if (!sensor || !sensor.getNodeInstanceFromElement) return {};
|
|
||||||
const nodeInst = sensor.getNodeInstanceFromElement(e.target as Element);
|
|
||||||
return nodeInst?.node?.getRGL() || {};
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkesc = (e: KeyboardEvent) => {
|
|
||||||
if (e.code === 'Escape') {
|
|
||||||
designer.clearLocation();
|
|
||||||
over();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let copy = false;
|
|
||||||
const checkcopy = (e: MouseEvent | DragEvent | KeyboardEvent) => {
|
|
||||||
if (isDragEvent(e) && e.dataTransfer) {
|
|
||||||
if (newBie || forceCopyState) {
|
|
||||||
e.dataTransfer.dropEffect = 'copy';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (newBie) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.altKey || e.ctrlKey) {
|
|
||||||
copy = true;
|
|
||||||
this.setCopyState(true);
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (isDragEvent(e) && e.dataTransfer) {
|
|
||||||
e.dataTransfer.dropEffect = 'copy';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
copy = false;
|
|
||||||
if (!forceCopyState) {
|
|
||||||
this.setCopyState(false);
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (isDragEvent(e) && e.dataTransfer) {
|
|
||||||
e.dataTransfer.dropEffect = 'move';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastArrive: any;
|
|
||||||
const drag = (e: MouseEvent | DragEvent) => {
|
|
||||||
// FIXME: donot setcopy when: newbie & no location
|
|
||||||
checkcopy(e);
|
|
||||||
|
|
||||||
if (isInvalidPoint(e, lastArrive)) return;
|
|
||||||
|
|
||||||
if (lastArrive && isSameAs(e, lastArrive)) {
|
|
||||||
lastArrive = e;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastArrive = e;
|
|
||||||
|
|
||||||
const { isRGL, rglNode } = getRGL(e) as any;
|
|
||||||
const locateEvent = createLocateEvent(e);
|
|
||||||
const sensor = chooseSensor(locateEvent);
|
|
||||||
|
|
||||||
if (isRGL) {
|
|
||||||
// 禁止被拖拽元素的阻断
|
|
||||||
const nodeInst = dragObject?.nodes?.[0]?.getDOMNode();
|
|
||||||
if (nodeInst && nodeInst.style) {
|
|
||||||
this.nodeInstPointerEvents = true;
|
|
||||||
nodeInst.style.pointerEvents = 'none';
|
|
||||||
}
|
|
||||||
// 原生拖拽
|
|
||||||
this.emitter.emit('rgl.sleeping', false);
|
|
||||||
if (fromRglNode && fromRglNode.id === rglNode.id) {
|
|
||||||
designer.clearLocation();
|
|
||||||
this.clearState();
|
|
||||||
this.emitter.emit('drag', locateEvent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._canDrop = !!sensor?.locate(locateEvent);
|
|
||||||
if (this._canDrop) {
|
|
||||||
this.emitter.emit('rgl.add.placeholder', {
|
|
||||||
rglNode,
|
|
||||||
fromRglNode,
|
|
||||||
node: locateEvent.dragObject?.nodes?.[0],
|
|
||||||
event: e,
|
|
||||||
});
|
|
||||||
designer.clearLocation();
|
|
||||||
this.clearState();
|
|
||||||
this.emitter.emit('drag', locateEvent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._canDrop = false;
|
|
||||||
this.emitter.emit('rgl.remove.placeholder');
|
|
||||||
this.emitter.emit('rgl.sleeping', true);
|
|
||||||
}
|
|
||||||
if (sensor) {
|
|
||||||
sensor.fixEvent(locateEvent);
|
|
||||||
sensor.locate(locateEvent);
|
|
||||||
} else {
|
|
||||||
designer.clearLocation();
|
|
||||||
}
|
|
||||||
this.emitter.emit('drag', locateEvent);
|
|
||||||
};
|
|
||||||
|
|
||||||
const dragstart = autorun(() => {
|
|
||||||
this._dragging = true;
|
|
||||||
setShaken(boostEvent);
|
|
||||||
const locateEvent = createLocateEvent(boostEvent);
|
|
||||||
if (newBie || forceCopyState) {
|
|
||||||
this.setCopyState(true);
|
|
||||||
} else {
|
|
||||||
chooseSensor(locateEvent);
|
|
||||||
}
|
|
||||||
this.setDraggingState(true);
|
|
||||||
// ESC cancel drag
|
|
||||||
if (!isBoostFromDragAPI) {
|
|
||||||
handleEvents((doc) => {
|
|
||||||
doc.addEventListener('keydown', checkesc, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitter.emit('dragstart', locateEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
// route: drag-move
|
|
||||||
const move = (e: MouseEvent | DragEvent) => {
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (isBoostFromDragAPI) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
if (this._dragging) {
|
|
||||||
// process dragging
|
|
||||||
drag(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// first move check is shaken
|
|
||||||
if (isShaken(boostEvent, e)) {
|
|
||||||
// is shaken dragstart
|
|
||||||
dragstart();
|
|
||||||
drag(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let didDrop = true;
|
|
||||||
const drop = (e: DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
didDrop = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// end-tail drag process
|
|
||||||
const over = (e?: any) => {
|
|
||||||
// 禁止被拖拽元素的阻断
|
|
||||||
if (this.nodeInstPointerEvents) {
|
|
||||||
const nodeInst = dragObject?.nodes?.[0]?.getDOMNode();
|
|
||||||
if (nodeInst && nodeInst.style) {
|
|
||||||
nodeInst.style.pointerEvents = '';
|
|
||||||
}
|
|
||||||
this.nodeInstPointerEvents = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送drop事件
|
|
||||||
if (e) {
|
|
||||||
const { isRGL, rglNode } = getRGL(e) as any;
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (isRGL && this._canDrop && this._dragging) {
|
|
||||||
const tarNode = dragObject?.nodes?.[0];
|
|
||||||
if (rglNode.id !== tarNode?.id) {
|
|
||||||
// 避免死循环
|
|
||||||
this.emitter.emit('rgl.drop', {
|
|
||||||
rglNode,
|
|
||||||
node: tarNode,
|
|
||||||
});
|
|
||||||
const selection = designer.project.currentDocument?.selection;
|
|
||||||
selection?.select(tarNode!.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除磁帖占位消息
|
|
||||||
this.emitter.emit('rgl.remove.placeholder');
|
|
||||||
|
|
||||||
if (e && isDragEvent(e)) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
if (lastSensor) {
|
|
||||||
lastSensor.deactiveSensor();
|
|
||||||
}
|
|
||||||
if (isBoostFromDragAPI) {
|
|
||||||
if (!didDrop) {
|
|
||||||
designer.clearLocation();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.setNativeSelection(true);
|
|
||||||
}
|
|
||||||
this.clearState();
|
|
||||||
|
|
||||||
let exception;
|
|
||||||
if (this._dragging) {
|
|
||||||
this._dragging = false;
|
|
||||||
try {
|
|
||||||
this.emitter.emit('dragend', { dragObject, copy });
|
|
||||||
} catch (ex) /* istanbul ignore next */ {
|
|
||||||
exception = ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
designer.clearLocation();
|
|
||||||
|
|
||||||
handleEvents((doc) => {
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (isBoostFromDragAPI) {
|
|
||||||
doc.removeEventListener('dragover', move, true);
|
|
||||||
doc.removeEventListener('dragend', over, true);
|
|
||||||
doc.removeEventListener('drop', drop, true);
|
|
||||||
} else {
|
|
||||||
doc.removeEventListener('mousemove', move, true);
|
|
||||||
doc.removeEventListener('mouseup', over, true);
|
|
||||||
}
|
|
||||||
doc.removeEventListener('mousedown', over, true);
|
|
||||||
doc.removeEventListener('keydown', checkesc, false);
|
|
||||||
doc.removeEventListener('keydown', checkcopy, false);
|
|
||||||
doc.removeEventListener('keyup', checkcopy, false);
|
|
||||||
});
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (exception) {
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// create drag locate event
|
|
||||||
const createLocateEvent = (e: MouseEvent | DragEvent): ILocateEvent => {
|
|
||||||
const evt: any = {
|
|
||||||
type: 'LocateEvent',
|
|
||||||
dragObject,
|
|
||||||
target: e.target,
|
|
||||||
originalEvent: e,
|
|
||||||
};
|
|
||||||
|
|
||||||
const sourceDocument = e.view?.document;
|
|
||||||
|
|
||||||
// event from current document
|
|
||||||
if (!sourceDocument || sourceDocument === document) {
|
|
||||||
evt.globalX = e.clientX;
|
|
||||||
evt.globalY = e.clientY;
|
|
||||||
} /* istanbul ignore next */ else {
|
|
||||||
// event from simulator sandbox
|
|
||||||
let srcSim: ISimulatorHost | undefined;
|
|
||||||
const lastSim = lastSensor && isSimulatorHost(lastSensor) ? lastSensor : null;
|
|
||||||
// check source simulator
|
|
||||||
if (lastSim && lastSim.contentDocument === sourceDocument) {
|
|
||||||
srcSim = lastSim;
|
|
||||||
} else {
|
|
||||||
srcSim = masterSensors.find((sim) => sim.contentDocument === sourceDocument);
|
|
||||||
if (!srcSim && lastSim) {
|
|
||||||
srcSim = lastSim;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (srcSim) {
|
|
||||||
// transform point by simulator
|
|
||||||
const g = srcSim.viewport.toGlobalPoint(e);
|
|
||||||
evt.globalX = g.clientX;
|
|
||||||
evt.globalY = g.clientY;
|
|
||||||
evt.canvasX = e.clientX;
|
|
||||||
evt.canvasY = e.clientY;
|
|
||||||
evt.sensor = srcSim;
|
|
||||||
} else {
|
|
||||||
// this condition will not happen, just make sure ts ok
|
|
||||||
evt.globalX = e.clientX;
|
|
||||||
evt.globalY = e.clientY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return evt;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sourceSensor = getSourceSensor(dragObject);
|
|
||||||
/* istanbul ignore next */
|
|
||||||
const chooseSensor = (e: ILocateEvent) => {
|
|
||||||
// this.sensors will change on dragstart
|
|
||||||
const sensors: IPublicModelSensor[] = this.sensors.concat(
|
|
||||||
masterSensors as IPublicModelSensor[],
|
|
||||||
);
|
|
||||||
let sensor =
|
|
||||||
e.sensor && e.sensor.isEnter(e)
|
|
||||||
? e.sensor
|
|
||||||
: sensors.find((s) => s.sensorAvailable && s.isEnter(e));
|
|
||||||
if (!sensor) {
|
|
||||||
// TODO: enter some area like componentspanel cancel
|
|
||||||
if (lastSensor) {
|
|
||||||
sensor = lastSensor;
|
|
||||||
} else if (e.sensor) {
|
|
||||||
sensor = e.sensor;
|
|
||||||
} else if (sourceSensor) {
|
|
||||||
sensor = sourceSensor as any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sensor !== lastSensor) {
|
|
||||||
if (lastSensor) {
|
|
||||||
lastSensor.deactiveSensor();
|
|
||||||
}
|
|
||||||
lastSensor = sensor;
|
|
||||||
}
|
|
||||||
if (sensor) {
|
|
||||||
e.sensor = sensor;
|
|
||||||
sensor.fixEvent(e);
|
|
||||||
}
|
|
||||||
this._activeSensor = sensor;
|
|
||||||
return sensor;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (isDragEvent(boostEvent)) {
|
|
||||||
const { dataTransfer } = boostEvent;
|
|
||||||
|
|
||||||
if (dataTransfer) {
|
|
||||||
dataTransfer.effectAllowed = 'all';
|
|
||||||
|
|
||||||
try {
|
|
||||||
dataTransfer.setData('application/json', '{}');
|
|
||||||
} catch (ex) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dragstart();
|
|
||||||
} else {
|
|
||||||
this.setNativeSelection(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEvents((doc) => {
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (isBoostFromDragAPI) {
|
|
||||||
doc.addEventListener('dragover', move, true);
|
|
||||||
// dragexit
|
|
||||||
didDrop = false;
|
|
||||||
doc.addEventListener('drop', drop, true);
|
|
||||||
doc.addEventListener('dragend', over, true);
|
|
||||||
} else {
|
|
||||||
doc.addEventListener('mousemove', move, true);
|
|
||||||
doc.addEventListener('mouseup', over, true);
|
|
||||||
}
|
|
||||||
doc.addEventListener('mousedown', over, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// future think: drag things from browser-out or a iframe-pane
|
|
||||||
|
|
||||||
if (!newBie && !isBoostFromDragAPI) {
|
|
||||||
handleEvents((doc) => {
|
|
||||||
doc.addEventListener('keydown', checkcopy, false);
|
|
||||||
doc.addEventListener('keyup', checkcopy, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* istanbul ignore next */
|
|
||||||
private getMasterSensors(): ISimulatorHost[] {
|
|
||||||
return Array.from(
|
|
||||||
new Set(
|
|
||||||
this.designer.project.documents
|
|
||||||
.map((doc) => {
|
|
||||||
if (doc.active && doc.simulator?.sensorAvailable) {
|
|
||||||
return doc.simulator;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter(Boolean) as any,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSimulators() {
|
|
||||||
return new Set(this.designer.project.documents.map((doc) => doc.simulator));
|
|
||||||
}
|
|
||||||
|
|
||||||
// #region ======== drag and drop helpers ============
|
|
||||||
private setNativeSelection(enableFlag: boolean) {
|
|
||||||
setNativeSelection(enableFlag);
|
|
||||||
this.getSimulators().forEach((sim) => {
|
|
||||||
sim?.setNativeSelection(enableFlag);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置拖拽态
|
|
||||||
*/
|
|
||||||
private setDraggingState(state: boolean) {
|
|
||||||
cursor.setDragging(state);
|
|
||||||
this.getSimulators().forEach((sim) => {
|
|
||||||
sim?.setDraggingState(state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置拷贝态
|
|
||||||
*/
|
|
||||||
private setCopyState(state: boolean) {
|
|
||||||
cursor.setCopy(state);
|
|
||||||
this.getSimulators().forEach((sim) => {
|
|
||||||
sim?.setCopyState(state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除所有态:拖拽态、拷贝态
|
|
||||||
*/
|
|
||||||
private clearState() {
|
|
||||||
cursor.release();
|
|
||||||
this.getSimulators().forEach((sim) => {
|
|
||||||
sim?.clearState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加投放感应区
|
|
||||||
*/
|
|
||||||
addSensor(sensor: any) {
|
|
||||||
this.sensors.push(sensor);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除投放感应
|
|
||||||
*/
|
|
||||||
removeSensor(sensor: any) {
|
|
||||||
const i = this.sensors.indexOf(sensor);
|
|
||||||
if (i > -1) {
|
|
||||||
this.sensors.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDragstart(func: (e: ILocateEvent) => any) {
|
|
||||||
this.emitter.on('dragstart', func);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('dragstart', func);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onDrag(func: (e: ILocateEvent) => any) {
|
|
||||||
this.emitter.on('drag', func);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('drag', func);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onDragend(func: (x: { dragObject: IPublicModelDragObject; copy: boolean }) => any) {
|
|
||||||
this.emitter.on('dragend', func);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('dragend', func);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IDragon extends Dragon {}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
export * from './designer';
|
|
||||||
export * from './designer-view';
|
|
||||||
export * from './dragon';
|
|
||||||
export * from './detecting';
|
|
||||||
export * from './location';
|
|
||||||
export * from './offset-observer';
|
|
||||||
export * from './scroller';
|
|
||||||
export * from './setting';
|
|
||||||
export * from './active-tracker';
|
|
||||||
export * from '../document';
|
|
||||||
export * from './clipboard';
|
|
||||||
@ -1,128 +0,0 @@
|
|||||||
import type { IDocumentModel, INode } from '../document';
|
|
||||||
import { ILocateEvent } from './dragon';
|
|
||||||
import {
|
|
||||||
IPublicModelDropLocation,
|
|
||||||
IPublicTypeLocationDetailType,
|
|
||||||
IPublicTypeRect,
|
|
||||||
IPublicTypeLocationDetail,
|
|
||||||
IPublicTypeLocationData,
|
|
||||||
IPublicModelLocateEvent,
|
|
||||||
} from '@alilc/lowcode-types';
|
|
||||||
|
|
||||||
export interface Point {
|
|
||||||
clientX: number;
|
|
||||||
clientY: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CanvasPoint {
|
|
||||||
canvasX: number;
|
|
||||||
canvasY: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Rects = DOMRect[] & {
|
|
||||||
elements: Array<Element | Text>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isRowContainer(container: Element | Text, win?: Window) {
|
|
||||||
if (isText(container)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const style = (win || getWindow(container)).getComputedStyle(container);
|
|
||||||
const display = style.getPropertyValue('display');
|
|
||||||
if (/flex$/.test(display)) {
|
|
||||||
const direction = style.getPropertyValue('flex-direction') || 'row';
|
|
||||||
if (direction === 'row' || direction === 'row-reverse') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (/grid$/.test(display)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isChildInline(child: Element | Text, win?: Window) {
|
|
||||||
if (isText(child)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const style = (win || getWindow(child)).getComputedStyle(child);
|
|
||||||
return (
|
|
||||||
/^inline/.test(style.getPropertyValue('display')) ||
|
|
||||||
/^(left|right)$/.test(style.getPropertyValue('float'))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRectTarget(rect: IPublicTypeRect | null) {
|
|
||||||
if (!rect || rect.computed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const els = rect.elements;
|
|
||||||
return els && els.length > 0 ? els[0]! : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isVerticalContainer(rect: IPublicTypeRect | null) {
|
|
||||||
const el = getRectTarget(rect);
|
|
||||||
if (!el) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return isRowContainer(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isVertical(rect: IPublicTypeRect | null) {
|
|
||||||
const el = getRectTarget(rect);
|
|
||||||
if (!el) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return isChildInline(el) || (el.parentElement ? isRowContainer(el.parentElement) : false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isText(elem: any): elem is Text {
|
|
||||||
return elem.nodeType === Node.TEXT_NODE;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDocument(elem: any): elem is Document {
|
|
||||||
return elem.nodeType === Node.DOCUMENT_NODE;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWindow(elem: Element | Document): Window {
|
|
||||||
return (isDocument(elem) ? elem : elem.ownerDocument!).defaultView!;
|
|
||||||
}
|
|
||||||
export interface IDropLocation extends Omit<IPublicModelDropLocation, 'target' | 'clone'> {
|
|
||||||
readonly source: string;
|
|
||||||
|
|
||||||
get target(): INode;
|
|
||||||
|
|
||||||
get document(): IDocumentModel | null;
|
|
||||||
|
|
||||||
clone(event: IPublicModelLocateEvent): IDropLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DropLocation implements IDropLocation {
|
|
||||||
readonly target: INode;
|
|
||||||
|
|
||||||
readonly detail: IPublicTypeLocationDetail;
|
|
||||||
|
|
||||||
readonly event: ILocateEvent;
|
|
||||||
|
|
||||||
readonly source: string;
|
|
||||||
|
|
||||||
get document(): IDocumentModel | null {
|
|
||||||
return this.target.document;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor({ target, detail, source, event }: IPublicTypeLocationData<INode>) {
|
|
||||||
this.target = target;
|
|
||||||
this.detail = detail;
|
|
||||||
this.source = source;
|
|
||||||
this.event = event as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
clone(event: ILocateEvent): IDropLocation {
|
|
||||||
return new DropLocation({
|
|
||||||
target: this.target,
|
|
||||||
detail: this.detail,
|
|
||||||
source: this.source,
|
|
||||||
event,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,173 +0,0 @@
|
|||||||
import requestIdleCallback, { cancelIdleCallback } from 'ric-shim';
|
|
||||||
import { observable, computed, action, makeObservable } from '@alilc/lowcode-editor-core';
|
|
||||||
import { uniqueId } from '@alilc/lowcode-utils';
|
|
||||||
import { INodeSelector, IViewport } from '../simulator';
|
|
||||||
import { INode } from '../document';
|
|
||||||
|
|
||||||
export class OffsetObserver {
|
|
||||||
readonly id = uniqueId('oobx');
|
|
||||||
|
|
||||||
private lastOffsetLeft?: number;
|
|
||||||
|
|
||||||
private lastOffsetTop?: number;
|
|
||||||
|
|
||||||
private lastOffsetHeight?: number;
|
|
||||||
|
|
||||||
private lastOffsetWidth?: number;
|
|
||||||
|
|
||||||
@observable private _height = 0;
|
|
||||||
|
|
||||||
@observable private _width = 0;
|
|
||||||
|
|
||||||
@observable private _left = 0;
|
|
||||||
|
|
||||||
@observable private _top = 0;
|
|
||||||
|
|
||||||
@observable private _right = 0;
|
|
||||||
|
|
||||||
@observable private _bottom = 0;
|
|
||||||
|
|
||||||
@computed get height() {
|
|
||||||
return this.isRoot ? this.viewport?.height : this._height * this.scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get width() {
|
|
||||||
return this.isRoot ? this.viewport?.width : this._width * this.scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get top() {
|
|
||||||
return this.isRoot ? 0 : this._top * this.scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get left() {
|
|
||||||
return this.isRoot ? 0 : this._left * this.scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get bottom() {
|
|
||||||
return this.isRoot ? this.viewport?.height : this._bottom * this.scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get right() {
|
|
||||||
return this.isRoot ? this.viewport?.width : this._right * this.scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable hasOffset = false;
|
|
||||||
|
|
||||||
@computed get offsetLeft() {
|
|
||||||
if (this.isRoot) {
|
|
||||||
return (this.viewport?.scrollX || 0) * this.scale;
|
|
||||||
}
|
|
||||||
if (!this.viewport?.scrolling || this.lastOffsetLeft == null) {
|
|
||||||
this.lastOffsetLeft = this.left + (this.viewport?.scrollX || 0) * this.scale;
|
|
||||||
}
|
|
||||||
return this.lastOffsetLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get offsetTop() {
|
|
||||||
if (this.isRoot) {
|
|
||||||
return (this.viewport?.scrollY || 0) * this.scale;
|
|
||||||
}
|
|
||||||
if (!this.viewport?.scrolling || this.lastOffsetTop == null) {
|
|
||||||
this.lastOffsetTop = this.top + (this.viewport?.scrollY || 0) * this.scale;
|
|
||||||
}
|
|
||||||
return this.lastOffsetTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get offsetHeight() {
|
|
||||||
if (!this.viewport?.scrolling || this.lastOffsetHeight == null) {
|
|
||||||
this.lastOffsetHeight = this.isRoot ? this.viewport?.height || 0 : this.height;
|
|
||||||
}
|
|
||||||
return this.lastOffsetHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get offsetWidth() {
|
|
||||||
if (!(this.viewport?.scrolling || 0) || this.lastOffsetWidth == null) {
|
|
||||||
this.lastOffsetWidth = this.isRoot ? this.viewport?.width || 0 : this.width;
|
|
||||||
}
|
|
||||||
return this.lastOffsetWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get scale() {
|
|
||||||
return this.viewport?.scale || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private pid: number | undefined;
|
|
||||||
|
|
||||||
readonly viewport: IViewport | undefined;
|
|
||||||
|
|
||||||
private isRoot: boolean;
|
|
||||||
|
|
||||||
readonly node: INode;
|
|
||||||
|
|
||||||
readonly compute: () => void;
|
|
||||||
|
|
||||||
constructor(readonly nodeInstance: INodeSelector) {
|
|
||||||
makeObservable(this);
|
|
||||||
|
|
||||||
const { node, instance } = nodeInstance;
|
|
||||||
this.node = node;
|
|
||||||
|
|
||||||
const doc = node.document;
|
|
||||||
const host = doc?.simulator;
|
|
||||||
const focusNode = doc?.focusNode;
|
|
||||||
this.isRoot = node.contains(focusNode!);
|
|
||||||
this.viewport = host?.viewport;
|
|
||||||
|
|
||||||
if (this.isRoot) {
|
|
||||||
this.hasOffset = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!instance) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pid: number | undefined;
|
|
||||||
const compute = action(() => {
|
|
||||||
if (pid !== this.pid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = host?.computeComponentInstanceRect(instance!, node.componentMeta.rootSelector);
|
|
||||||
|
|
||||||
if (!rect) {
|
|
||||||
this.hasOffset = false;
|
|
||||||
} else if (!this.viewport?.scrolling || !this.hasOffset) {
|
|
||||||
this._height = rect.height;
|
|
||||||
this._width = rect.width;
|
|
||||||
this._left = rect.left;
|
|
||||||
this._top = rect.top;
|
|
||||||
this._right = rect.right;
|
|
||||||
this._bottom = rect.bottom;
|
|
||||||
this.hasOffset = true;
|
|
||||||
}
|
|
||||||
this.pid = requestIdleCallback(compute);
|
|
||||||
pid = this.pid;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.compute = compute;
|
|
||||||
|
|
||||||
// try first
|
|
||||||
compute();
|
|
||||||
// try second, ensure the dom mounted
|
|
||||||
this.pid = requestIdleCallback(compute);
|
|
||||||
pid = this.pid;
|
|
||||||
}
|
|
||||||
|
|
||||||
purge() {
|
|
||||||
if (this.pid) {
|
|
||||||
cancelIdleCallback(this.pid);
|
|
||||||
}
|
|
||||||
this.pid = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
isPurged() {
|
|
||||||
return this.pid == null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createOffsetObserver(nodeInstance: INodeSelector): OffsetObserver | null {
|
|
||||||
if (!nodeInstance.instance) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new OffsetObserver(nodeInstance);
|
|
||||||
}
|
|
||||||
@ -1,189 +0,0 @@
|
|||||||
import { isElement } from '@alilc/lowcode-utils';
|
|
||||||
import { IPublicModelScrollTarget, IPublicTypeScrollable, IPublicModelScroller } from '@alilc/lowcode-types';
|
|
||||||
|
|
||||||
export interface IScrollTarget extends IPublicModelScrollTarget {
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ScrollTarget implements IScrollTarget {
|
|
||||||
get left() {
|
|
||||||
return 'scrollX' in this.target ? this.target.scrollX : this.target.scrollLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
get top() {
|
|
||||||
return 'scrollY' in this.target ? this.target.scrollY : this.target.scrollTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
private doc?: HTMLElement;
|
|
||||||
|
|
||||||
constructor(private target: Window | Element) {
|
|
||||||
if (isWindow(target)) {
|
|
||||||
this.doc = target.document.documentElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollTo(options: { left?: number; top?: number }) {
|
|
||||||
this.target.scrollTo(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToXY(x: number, y: number) {
|
|
||||||
this.target.scrollTo(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
get scrollHeight(): number {
|
|
||||||
return ((this.doc || this.target) as any).scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
get scrollWidth(): number {
|
|
||||||
return ((this.doc || this.target) as any).scrollWidth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWindow(obj: any): obj is Window {
|
|
||||||
return obj && obj.document;
|
|
||||||
}
|
|
||||||
|
|
||||||
function easing(n: number) {
|
|
||||||
return Math.sin((n * Math.PI) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
const SCROLL_ACCURACY = 30;
|
|
||||||
|
|
||||||
export interface IScroller extends IPublicModelScroller {
|
|
||||||
|
|
||||||
}
|
|
||||||
export class Scroller implements IScroller {
|
|
||||||
private pid: number | undefined;
|
|
||||||
scrollable: IPublicTypeScrollable;
|
|
||||||
|
|
||||||
constructor(scrollable: IPublicTypeScrollable) {
|
|
||||||
this.scrollable = scrollable;
|
|
||||||
}
|
|
||||||
|
|
||||||
get scrollTarget(): IScrollTarget | null {
|
|
||||||
let target = this.scrollable.scrollTarget;
|
|
||||||
if (!target) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (isElement(target)) {
|
|
||||||
target = new ScrollTarget(target);
|
|
||||||
this.scrollable.scrollTarget = target;
|
|
||||||
}
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollTo(options: { left?: number; top?: number }) {
|
|
||||||
this.cancel();
|
|
||||||
|
|
||||||
const { scrollTarget } = this;
|
|
||||||
if (!scrollTarget) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pid: number;
|
|
||||||
const { left } = scrollTarget;
|
|
||||||
const { top } = scrollTarget;
|
|
||||||
const end = () => {
|
|
||||||
this.cancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
if ((left === options.left || options.left == null) && top === options.top) {
|
|
||||||
end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = 200;
|
|
||||||
const start = +new Date();
|
|
||||||
|
|
||||||
const animate = () => {
|
|
||||||
if (pid !== this.pid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = +new Date();
|
|
||||||
const time = Math.min(1, (now - start) / duration);
|
|
||||||
const eased = easing(time);
|
|
||||||
const opt: any = {};
|
|
||||||
if (options.left != null) {
|
|
||||||
opt.left = eased * (options.left - left) + left;
|
|
||||||
}
|
|
||||||
if (options.top != null) {
|
|
||||||
opt.top = eased * (options.top - top) + top;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollTarget.scrollTo(opt);
|
|
||||||
|
|
||||||
if (time < 1) {
|
|
||||||
this.pid = requestAnimationFrame(animate);
|
|
||||||
pid = this.pid;
|
|
||||||
} else {
|
|
||||||
end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pid = requestAnimationFrame(animate);
|
|
||||||
pid = this.pid;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrolling(point: { globalX: number; globalY: number }) {
|
|
||||||
this.cancel();
|
|
||||||
|
|
||||||
const { bounds, scale = 1 } = this.scrollable;
|
|
||||||
const { scrollTarget } = this;
|
|
||||||
if (!scrollTarget || !bounds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const x = point.globalX;
|
|
||||||
const y = point.globalY;
|
|
||||||
|
|
||||||
const maxScrollHeight = scrollTarget.scrollHeight - bounds.height / scale;
|
|
||||||
const maxScrollWidth = scrollTarget.scrollWidth - bounds.width / scale;
|
|
||||||
let sx = scrollTarget.left;
|
|
||||||
let sy = scrollTarget.top;
|
|
||||||
let ax = 0;
|
|
||||||
let ay = 0;
|
|
||||||
if (y < bounds.top + SCROLL_ACCURACY) {
|
|
||||||
ay = -Math.min(Math.max(bounds.top + SCROLL_ACCURACY - y, 10), 50) / scale;
|
|
||||||
} else if (y > bounds.bottom - SCROLL_ACCURACY) {
|
|
||||||
ay = Math.min(Math.max(y + SCROLL_ACCURACY - bounds.bottom, 10), 50) / scale;
|
|
||||||
}
|
|
||||||
if (x < bounds.left + SCROLL_ACCURACY) {
|
|
||||||
ax = -Math.min(Math.max(bounds.top + SCROLL_ACCURACY - y, 10), 50) / scale;
|
|
||||||
} else if (x > bounds.right - SCROLL_ACCURACY) {
|
|
||||||
ax = Math.min(Math.max(x + SCROLL_ACCURACY - bounds.right, 10), 50) / scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ax && !ay) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const animate = () => {
|
|
||||||
let scroll = false;
|
|
||||||
if ((ay > 0 && sy < maxScrollHeight) || (ay < 0 && sy > 0)) {
|
|
||||||
sy += ay;
|
|
||||||
sy = Math.min(Math.max(sy, 0), maxScrollHeight);
|
|
||||||
scroll = true;
|
|
||||||
}
|
|
||||||
if ((ax > 0 && sx < maxScrollWidth) || (ax < 0 && sx > 0)) {
|
|
||||||
sx += ax;
|
|
||||||
sx = Math.min(Math.max(sx, 0), maxScrollWidth);
|
|
||||||
scroll = true;
|
|
||||||
}
|
|
||||||
if (!scroll) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollTarget.scrollTo({ left: sx, top: sy });
|
|
||||||
this.pid = requestAnimationFrame(animate);
|
|
||||||
};
|
|
||||||
|
|
||||||
animate();
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
if (this.pid) {
|
|
||||||
cancelAnimationFrame(this.pid);
|
|
||||||
}
|
|
||||||
this.pid = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from './setting-field';
|
|
||||||
export * from './setting-top-entry';
|
|
||||||
export * from './setting-entry-type';
|
|
||||||
export * from './setting-prop-entry';
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import { IPublicApiSetters, IPublicModelEditor } from '@alilc/lowcode-types';
|
|
||||||
import { IDesigner } from '../designer';
|
|
||||||
import { INode } from '../../document';
|
|
||||||
import { ISettingField } from './setting-field';
|
|
||||||
|
|
||||||
export interface ISettingEntry {
|
|
||||||
readonly designer: IDesigner | undefined;
|
|
||||||
|
|
||||||
readonly id: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同样类型的节点
|
|
||||||
*/
|
|
||||||
readonly isSameComponent: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 一个
|
|
||||||
*/
|
|
||||||
readonly isSingle: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 多个
|
|
||||||
*/
|
|
||||||
readonly isMultiple: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 编辑器引用
|
|
||||||
*/
|
|
||||||
readonly editor: IPublicModelEditor;
|
|
||||||
|
|
||||||
readonly setters: IPublicApiSetters;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取得子项
|
|
||||||
*/
|
|
||||||
get: (propName: string | number) => ISettingField | null;
|
|
||||||
|
|
||||||
readonly nodes: INode[];
|
|
||||||
|
|
||||||
// @todo 补充 node 定义
|
|
||||||
/**
|
|
||||||
* 获取 node 中的第一项
|
|
||||||
*/
|
|
||||||
getNode: () => any;
|
|
||||||
}
|
|
||||||
@ -1,312 +0,0 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import {
|
|
||||||
IPublicTypeTitleContent,
|
|
||||||
IPublicTypeSetterType,
|
|
||||||
IPublicTypeDynamicSetter,
|
|
||||||
IPublicTypeFieldExtraProps,
|
|
||||||
IPublicTypeFieldConfig,
|
|
||||||
IPublicTypeCustomView,
|
|
||||||
IPublicTypeDisposable,
|
|
||||||
IPublicModelSettingField,
|
|
||||||
IBaseModelSettingField,
|
|
||||||
} from '@alilc/lowcode-types';
|
|
||||||
import type { IPublicTypeSetValueOptions } from '@alilc/lowcode-types';
|
|
||||||
import { Transducer } from './utils';
|
|
||||||
import { ISettingPropEntry, SettingPropEntry } from './setting-prop-entry';
|
|
||||||
import { computed, observable, makeObservable, action, untracked, intl } from '@alilc/lowcode-editor-core';
|
|
||||||
import { cloneDeep, isCustomView, isDynamicSetter, isJSExpression } from '@alilc/lowcode-utils';
|
|
||||||
import { ISettingTopEntry } from './setting-top-entry';
|
|
||||||
import { IComponentMeta } from '../../component-meta';
|
|
||||||
import { INode } from '../../document';
|
|
||||||
|
|
||||||
function getSettingFieldCollectorKey(
|
|
||||||
parent: ISettingTopEntry | ISettingField,
|
|
||||||
config: IPublicTypeFieldConfig,
|
|
||||||
) {
|
|
||||||
let cur = parent;
|
|
||||||
const path = [config.name];
|
|
||||||
while (cur !== parent.top) {
|
|
||||||
if (cur instanceof SettingField && cur.type !== 'group') {
|
|
||||||
path.unshift(cur.name);
|
|
||||||
}
|
|
||||||
cur = cur.parent;
|
|
||||||
}
|
|
||||||
return path.join('.');
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISettingField
|
|
||||||
extends ISettingPropEntry,
|
|
||||||
Omit<
|
|
||||||
IBaseModelSettingField<ISettingTopEntry, ISettingField, IComponentMeta, INode>,
|
|
||||||
'setValue' | 'key' | 'node'
|
|
||||||
> {
|
|
||||||
readonly isSettingField: true;
|
|
||||||
|
|
||||||
readonly isRequired: boolean;
|
|
||||||
|
|
||||||
readonly isGroup: boolean;
|
|
||||||
|
|
||||||
extraProps: IPublicTypeFieldExtraProps;
|
|
||||||
|
|
||||||
get items(): Array<ISettingField | IPublicTypeCustomView>;
|
|
||||||
|
|
||||||
get title(): string | ReactNode | undefined;
|
|
||||||
|
|
||||||
get setter(): IPublicTypeSetterType | null;
|
|
||||||
|
|
||||||
get expanded(): boolean;
|
|
||||||
|
|
||||||
get valueState(): number;
|
|
||||||
|
|
||||||
setExpanded(value: boolean): void;
|
|
||||||
|
|
||||||
purge(): void;
|
|
||||||
|
|
||||||
setValue(
|
|
||||||
val: any,
|
|
||||||
isHotValue?: boolean,
|
|
||||||
force?: boolean,
|
|
||||||
extraOptions?: IPublicTypeSetValueOptions,
|
|
||||||
): void;
|
|
||||||
|
|
||||||
clearValue(): void;
|
|
||||||
|
|
||||||
createField(config: IPublicTypeFieldConfig): ISettingField;
|
|
||||||
|
|
||||||
onEffect(action: () => void): IPublicTypeDisposable;
|
|
||||||
|
|
||||||
internalToShellField(): IPublicModelSettingField;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SettingField extends SettingPropEntry implements ISettingField {
|
|
||||||
readonly isSettingField = true;
|
|
||||||
|
|
||||||
readonly isRequired: boolean;
|
|
||||||
|
|
||||||
readonly transducer: Transducer;
|
|
||||||
|
|
||||||
private _config: IPublicTypeFieldConfig;
|
|
||||||
|
|
||||||
private hotValue: any;
|
|
||||||
|
|
||||||
parent: ISettingTopEntry | ISettingField;
|
|
||||||
|
|
||||||
extraProps: IPublicTypeFieldExtraProps;
|
|
||||||
|
|
||||||
// ==== dynamic properties ====
|
|
||||||
private _title?: IPublicTypeTitleContent;
|
|
||||||
|
|
||||||
get title(): any {
|
|
||||||
return (
|
|
||||||
this._title || (typeof this.name === 'number' ? `${intl('Item')} ${this.name}` : this.name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setter?: IPublicTypeSetterType | IPublicTypeDynamicSetter;
|
|
||||||
|
|
||||||
@observable.ref private _expanded = true;
|
|
||||||
|
|
||||||
private _items: Array<ISettingField | IPublicTypeCustomView> = [];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
parent: ISettingTopEntry | ISettingField,
|
|
||||||
config: IPublicTypeFieldConfig,
|
|
||||||
private settingFieldCollector?: (name: string | number, field: ISettingField) => void,
|
|
||||||
) {
|
|
||||||
super(parent, config.name, config.type);
|
|
||||||
makeObservable(this);
|
|
||||||
const { title, items, setter, extraProps, ...rest } = config;
|
|
||||||
this.parent = parent;
|
|
||||||
this._config = config;
|
|
||||||
this._title = title;
|
|
||||||
this._setter = setter;
|
|
||||||
this.extraProps = {
|
|
||||||
...rest,
|
|
||||||
...extraProps,
|
|
||||||
};
|
|
||||||
this.isRequired = config.isRequired || (setter as any)?.isRequired;
|
|
||||||
this._expanded = !extraProps?.defaultCollapsed;
|
|
||||||
|
|
||||||
// initial items
|
|
||||||
if (items && items.length > 0) {
|
|
||||||
this.initItems(items, settingFieldCollector);
|
|
||||||
}
|
|
||||||
if (this.type !== 'group' && settingFieldCollector && config.name) {
|
|
||||||
settingFieldCollector(getSettingFieldCollectorKey(parent, config), this as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
// compatiable old config
|
|
||||||
this.transducer = new Transducer(this as any, { setter });
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get setter(): IPublicTypeSetterType | null {
|
|
||||||
if (!this._setter) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (isDynamicSetter(this._setter)) {
|
|
||||||
return untracked(() => {
|
|
||||||
const shellThis = this.internalToShellField();
|
|
||||||
return (this._setter as IPublicTypeDynamicSetter)?.call(shellThis, shellThis!);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this._setter;
|
|
||||||
}
|
|
||||||
|
|
||||||
get expanded(): boolean {
|
|
||||||
return this._expanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
setExpanded(value: boolean) {
|
|
||||||
this._expanded = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
get items(): Array<ISettingField | IPublicTypeCustomView> {
|
|
||||||
return this._items;
|
|
||||||
}
|
|
||||||
|
|
||||||
get config(): IPublicTypeFieldConfig {
|
|
||||||
return this._config;
|
|
||||||
}
|
|
||||||
|
|
||||||
private initItems(
|
|
||||||
items: Array<IPublicTypeFieldConfig | IPublicTypeCustomView>,
|
|
||||||
settingFieldCollector?: {
|
|
||||||
(name: string | number, field: ISettingField): void;
|
|
||||||
(name: string, field: ISettingField): void;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
this._items = items.map((item) => {
|
|
||||||
if (isCustomView(item)) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
return new SettingField(this as any, item, settingFieldCollector);
|
|
||||||
}) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
private disposeItems() {
|
|
||||||
this._items.forEach((item) => isSettingField(item) && item.purge());
|
|
||||||
this._items = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建子配置项,通常用于 object/array 类型数据
|
|
||||||
createField(config: IPublicTypeFieldConfig): ISettingField {
|
|
||||||
this.settingFieldCollector?.(getSettingFieldCollectorKey(this.parent, config), this as any);
|
|
||||||
return new SettingField(this as any, config, this.settingFieldCollector) as ISettingField;
|
|
||||||
}
|
|
||||||
|
|
||||||
purge() {
|
|
||||||
this.disposeItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======= compatibles for vision ======
|
|
||||||
|
|
||||||
getConfig<K extends keyof IPublicTypeFieldConfig>(
|
|
||||||
configName?: K,
|
|
||||||
): IPublicTypeFieldConfig[K] | IPublicTypeFieldConfig {
|
|
||||||
if (configName) {
|
|
||||||
return this.config[configName];
|
|
||||||
}
|
|
||||||
return this._config;
|
|
||||||
}
|
|
||||||
|
|
||||||
getItems(
|
|
||||||
filter?: (item: ISettingField | IPublicTypeCustomView) => boolean,
|
|
||||||
): Array<ISettingField | IPublicTypeCustomView> {
|
|
||||||
return this._items.filter((item) => {
|
|
||||||
if (filter) {
|
|
||||||
return filter(item);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setValue(
|
|
||||||
val: any,
|
|
||||||
isHotValue?: boolean,
|
|
||||||
force?: boolean,
|
|
||||||
extraOptions?: IPublicTypeSetValueOptions,
|
|
||||||
) {
|
|
||||||
if (isHotValue) {
|
|
||||||
this.setHotValue(val, extraOptions);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
super.setValue(val, false, false, extraOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
getHotValue(): any {
|
|
||||||
if (this.hotValue) {
|
|
||||||
return this.hotValue;
|
|
||||||
}
|
|
||||||
// avoid View modify
|
|
||||||
let v = cloneDeep(this.getMockOrValue());
|
|
||||||
if (v == null) {
|
|
||||||
v = this.extraProps.defaultValue;
|
|
||||||
}
|
|
||||||
return this.transducer.toHot(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* istanbul ignore next */
|
|
||||||
@action
|
|
||||||
setMiniAppDataSourceValue(data: any, options?: any) {
|
|
||||||
this.hotValue = data;
|
|
||||||
const v = this.transducer.toNative(data);
|
|
||||||
this.setValue(v, false, false, options);
|
|
||||||
// dirty fix list setter
|
|
||||||
if (Array.isArray(data) && data[0] && data[0].__sid__) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setHotValue(data: any, options?: IPublicTypeSetValueOptions) {
|
|
||||||
this.hotValue = data;
|
|
||||||
const value = this.transducer.toNative(data);
|
|
||||||
if (options) {
|
|
||||||
options.fromSetHotValue = true;
|
|
||||||
} else {
|
|
||||||
options = { fromSetHotValue: true };
|
|
||||||
}
|
|
||||||
if (this.isUseVariable()) {
|
|
||||||
const oldValue = this.getValue();
|
|
||||||
if (isJSExpression(value)) {
|
|
||||||
this.setValue(
|
|
||||||
{
|
|
||||||
type: 'JSExpression',
|
|
||||||
value: value.value,
|
|
||||||
mock: oldValue.mock,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.setValue(
|
|
||||||
{
|
|
||||||
type: 'JSExpression',
|
|
||||||
value: oldValue.value,
|
|
||||||
mock: value,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.setValue(value, false, false, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// dirty fix list setter
|
|
||||||
if (Array.isArray(data) && data[0] && data[0].__sid__) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onEffect(action: () => void): IPublicTypeDisposable {
|
|
||||||
return this.designer!.autorun(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
internalToShellField() {
|
|
||||||
return this.designer!.shellModelFactory.createSettingField(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,407 +0,0 @@
|
|||||||
import {
|
|
||||||
observable,
|
|
||||||
computed,
|
|
||||||
makeObservable,
|
|
||||||
runInAction,
|
|
||||||
IEventBus,
|
|
||||||
createModuleEventBus,
|
|
||||||
} from '@alilc/lowcode-editor-core';
|
|
||||||
import {
|
|
||||||
GlobalEvent,
|
|
||||||
IPublicApiSetters,
|
|
||||||
IPublicModelEditor,
|
|
||||||
IPublicModelSettingField,
|
|
||||||
IPublicTypeFieldExtraProps,
|
|
||||||
IPublicTypeSetValueOptions,
|
|
||||||
} from '@alilc/lowcode-types';
|
|
||||||
import { uniqueId, isJSExpression } from '@alilc/lowcode-utils';
|
|
||||||
import { ISettingEntry } from './setting-entry-type';
|
|
||||||
import { INode } from '../../document';
|
|
||||||
import type { IComponentMeta } from '../../component-meta';
|
|
||||||
import { IDesigner } from '../designer';
|
|
||||||
import { ISettingTopEntry } from './setting-top-entry';
|
|
||||||
import { ISettingField } from './setting-field';
|
|
||||||
|
|
||||||
export interface ISettingPropEntry extends ISettingEntry {
|
|
||||||
readonly isGroup: boolean;
|
|
||||||
|
|
||||||
get props(): ISettingTopEntry;
|
|
||||||
|
|
||||||
get name(): string | number | undefined;
|
|
||||||
|
|
||||||
getKey(): string | number | undefined;
|
|
||||||
|
|
||||||
setKey(key: string | number): void;
|
|
||||||
|
|
||||||
getDefaultValue(): any;
|
|
||||||
|
|
||||||
setUseVariable(flag: boolean): void;
|
|
||||||
|
|
||||||
getProps(): ISettingTopEntry;
|
|
||||||
|
|
||||||
isUseVariable(): boolean;
|
|
||||||
|
|
||||||
getMockOrValue(): any;
|
|
||||||
|
|
||||||
remove(): void;
|
|
||||||
|
|
||||||
setValue(
|
|
||||||
val: any,
|
|
||||||
extraOptions?: IPublicTypeSetValueOptions,
|
|
||||||
): void;
|
|
||||||
|
|
||||||
internalToShellField(): IPublicModelSettingField;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SettingPropEntry implements ISettingPropEntry {
|
|
||||||
// === static properties ===
|
|
||||||
readonly editor: IPublicModelEditor;
|
|
||||||
|
|
||||||
readonly isSameComponent: boolean;
|
|
||||||
|
|
||||||
readonly isMultiple: boolean;
|
|
||||||
|
|
||||||
readonly isSingle: boolean;
|
|
||||||
|
|
||||||
readonly setters: IPublicApiSetters;
|
|
||||||
|
|
||||||
readonly nodes: INode[];
|
|
||||||
|
|
||||||
readonly componentMeta: IComponentMeta | null;
|
|
||||||
|
|
||||||
readonly designer: IDesigner | undefined;
|
|
||||||
|
|
||||||
readonly top: ISettingTopEntry;
|
|
||||||
|
|
||||||
readonly isGroup: boolean;
|
|
||||||
|
|
||||||
readonly type: 'field' | 'group';
|
|
||||||
|
|
||||||
readonly id = uniqueId('entry');
|
|
||||||
|
|
||||||
readonly emitter: IEventBus = createModuleEventBus('SettingPropEntry');
|
|
||||||
|
|
||||||
// ==== dynamic properties ====
|
|
||||||
@observable.ref private _name: string | number | undefined;
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this._name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get path() {
|
|
||||||
const path = this.parent.path.slice();
|
|
||||||
if (this.type === 'field' && this.name?.toString()) {
|
|
||||||
path.push(this.name);
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
extraProps: IPublicTypeFieldExtraProps = {};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly parent: ISettingTopEntry | ISettingField,
|
|
||||||
name: string | number | undefined,
|
|
||||||
type?: 'field' | 'group',
|
|
||||||
) {
|
|
||||||
makeObservable(this);
|
|
||||||
if (type == null) {
|
|
||||||
const c = typeof name === 'string' ? name.slice(0, 1) : '';
|
|
||||||
if (c === '#') {
|
|
||||||
this.type = 'group';
|
|
||||||
} else {
|
|
||||||
this.type = 'field';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
// initial self properties
|
|
||||||
this._name = name;
|
|
||||||
this.isGroup = this.type === 'group';
|
|
||||||
|
|
||||||
// copy parent static properties
|
|
||||||
this.editor = parent.editor;
|
|
||||||
this.nodes = parent.nodes;
|
|
||||||
this.setters = parent.setters;
|
|
||||||
this.componentMeta = parent.componentMeta;
|
|
||||||
this.isSameComponent = parent.isSameComponent;
|
|
||||||
this.isMultiple = parent.isMultiple;
|
|
||||||
this.isSingle = parent.isSingle;
|
|
||||||
this.designer = parent.designer;
|
|
||||||
this.top = parent.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
getId() {
|
|
||||||
return this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
setKey(key: string | number) {
|
|
||||||
if (this.type !== 'field') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const propName = this.path.join('.');
|
|
||||||
let l = this.nodes.length;
|
|
||||||
while (l-- > 0) {
|
|
||||||
this.nodes[l].getProp(propName, true)!.key = key;
|
|
||||||
}
|
|
||||||
this._name = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
getKey() {
|
|
||||||
return this._name;
|
|
||||||
}
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
if (this.type !== 'field') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const propName = this.path.join('.');
|
|
||||||
let l = this.nodes.length;
|
|
||||||
while (l-- > 0) {
|
|
||||||
this.nodes[l].getProp(propName)?.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====== 当前属性读写 =====
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断当前属性值是否一致
|
|
||||||
* -1 多种值
|
|
||||||
* 0 无值
|
|
||||||
* 1 类似值,比如数组长度一样
|
|
||||||
* 2 单一植
|
|
||||||
*/
|
|
||||||
/* istanbul ignore next */
|
|
||||||
@computed get valueState(): number {
|
|
||||||
return runInAction(() => {
|
|
||||||
if (this.type !== 'field') {
|
|
||||||
const { getValue } = this.extraProps;
|
|
||||||
return getValue
|
|
||||||
? getValue(this.internalToShellField()!, undefined) === undefined
|
|
||||||
? 0
|
|
||||||
: 1
|
|
||||||
: 0;
|
|
||||||
}
|
|
||||||
if (this.nodes.length === 1) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
const propName = this.path.join('.');
|
|
||||||
const first = this.nodes[0].getProp(propName)!;
|
|
||||||
let l = this.nodes.length;
|
|
||||||
let state = 2;
|
|
||||||
while (--l > 0) {
|
|
||||||
const next = this.nodes[l].getProp(propName, false);
|
|
||||||
const s = first.compare(next);
|
|
||||||
if (s > 1) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (s === 1) {
|
|
||||||
state = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state === 2 && first.isUnset()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前属性值
|
|
||||||
*/
|
|
||||||
getValue(): any {
|
|
||||||
let val: any;
|
|
||||||
if (this.type === 'field' && this.name?.toString()) {
|
|
||||||
val = this.parent.getPropValue(this.name);
|
|
||||||
}
|
|
||||||
const { getValue } = this.extraProps;
|
|
||||||
try {
|
|
||||||
return getValue ? getValue(this.internalToShellField()!, val) : val;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e);
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置当前属性值
|
|
||||||
*/
|
|
||||||
setValue(
|
|
||||||
val: any,
|
|
||||||
extraOptions?: IPublicTypeSetValueOptions,
|
|
||||||
) {
|
|
||||||
const oldValue = this.getValue();
|
|
||||||
if (this.type === 'field') {
|
|
||||||
this.name?.toString() && this.parent.setPropValue(this.name, val);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { setValue } = this.extraProps;
|
|
||||||
if (setValue && !extraOptions?.disableMutator) {
|
|
||||||
try {
|
|
||||||
setValue(this.internalToShellField()!, val);
|
|
||||||
} catch (e) {
|
|
||||||
/* istanbul ignore next */
|
|
||||||
console.warn(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.notifyValueChange(oldValue, val);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除已设置的值
|
|
||||||
*/
|
|
||||||
clearValue() {
|
|
||||||
if (this.type === 'field') {
|
|
||||||
this.name?.toString() && this.parent.clearPropValue(this.name);
|
|
||||||
}
|
|
||||||
const { setValue } = this.extraProps;
|
|
||||||
if (setValue) {
|
|
||||||
try {
|
|
||||||
setValue(this.internalToShellField()!, undefined);
|
|
||||||
} catch (e) {
|
|
||||||
/* istanbul ignore next */
|
|
||||||
console.warn(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取子项
|
|
||||||
*/
|
|
||||||
get(propName: string | number) {
|
|
||||||
const path = this.path.concat(propName).join('.');
|
|
||||||
return this.top.get(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置子级属性值
|
|
||||||
*/
|
|
||||||
setPropValue(propName: string | number, value: any) {
|
|
||||||
const path = this.path.concat(propName).join('.');
|
|
||||||
this.top.setPropValue(path, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除已设置值
|
|
||||||
*/
|
|
||||||
clearPropValue(propName: string | number) {
|
|
||||||
const path = this.path.concat(propName).join('.');
|
|
||||||
this.top.clearPropValue(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取子级属性值
|
|
||||||
*/
|
|
||||||
getPropValue(propName: string | number): any {
|
|
||||||
return this.top.getPropValue(this.path.concat(propName).join('.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取顶层附属属性值
|
|
||||||
*/
|
|
||||||
getExtraPropValue(propName: string) {
|
|
||||||
return this.top.getExtraPropValue(propName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置顶层附属属性值
|
|
||||||
*/
|
|
||||||
setExtraPropValue(propName: string, value: any) {
|
|
||||||
this.top.setExtraPropValue(propName, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======= compatibles for vision ======
|
|
||||||
getNode() {
|
|
||||||
return this.nodes[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
getName(): string {
|
|
||||||
return this.path.join('.');
|
|
||||||
}
|
|
||||||
|
|
||||||
getProps() {
|
|
||||||
return this.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add settingfield props
|
|
||||||
get props() {
|
|
||||||
return this.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
onValueChange(func: () => any) {
|
|
||||||
this.emitter.on('valuechange', func);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('valuechange', func);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyValueChange(oldValue: any, newValue: any) {
|
|
||||||
this.editor.eventBus.emit(GlobalEvent.Node.Prop.InnerChange, {
|
|
||||||
node: this.getNode(),
|
|
||||||
prop: this,
|
|
||||||
oldValue,
|
|
||||||
newValue,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultValue() {
|
|
||||||
return this.extraProps.defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
isIgnore() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getVariableValue() {
|
|
||||||
const v = this.getValue();
|
|
||||||
if (isJSExpression(v)) {
|
|
||||||
return v.value;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
setVariableValue(value: string) {
|
|
||||||
const v = this.getValue();
|
|
||||||
this.setValue({
|
|
||||||
type: 'JSExpression',
|
|
||||||
value,
|
|
||||||
mock: isJSExpression(v) ? v.mock : v,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setUseVariable(flag: boolean) {
|
|
||||||
if (this.isUseVariable() === flag) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const v = this.getValue();
|
|
||||||
if (this.isUseVariable()) {
|
|
||||||
this.setValue(v.mock);
|
|
||||||
} else {
|
|
||||||
this.setValue({
|
|
||||||
type: 'JSExpression',
|
|
||||||
value: '',
|
|
||||||
mock: v,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isUseVariable() {
|
|
||||||
return isJSExpression(this.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
get useVariable() {
|
|
||||||
return this.isUseVariable();
|
|
||||||
}
|
|
||||||
|
|
||||||
getMockOrValue() {
|
|
||||||
const v = this.getValue();
|
|
||||||
if (isJSExpression(v)) {
|
|
||||||
return v.mock;
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
internalToShellField(): IPublicModelSettingField {
|
|
||||||
return this.designer!.shellModelFactory.createSettingField(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,294 +0,0 @@
|
|||||||
import {
|
|
||||||
IPublicTypeCustomView,
|
|
||||||
IPublicModelEditor,
|
|
||||||
IPublicModelSettingTopEntry,
|
|
||||||
IPublicApiSetters,
|
|
||||||
} from '@alilc/lowcode-types';
|
|
||||||
import { isCustomView } from '@alilc/lowcode-utils';
|
|
||||||
import { computed, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
|
|
||||||
import { ISettingEntry } from './setting-entry-type';
|
|
||||||
import { ISettingField, SettingField } from './setting-field';
|
|
||||||
import { INode } from '../../document';
|
|
||||||
import type { IComponentMeta } from '../../component-meta';
|
|
||||||
import { IDesigner } from '../designer';
|
|
||||||
|
|
||||||
function generateSessionId(nodes: INode[]) {
|
|
||||||
return nodes
|
|
||||||
.map((node) => node.id)
|
|
||||||
.sort()
|
|
||||||
.join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISettingTopEntry
|
|
||||||
extends ISettingEntry,
|
|
||||||
IPublicModelSettingTopEntry<INode, ISettingField> {
|
|
||||||
readonly top: ISettingTopEntry;
|
|
||||||
|
|
||||||
readonly parent: ISettingTopEntry;
|
|
||||||
|
|
||||||
readonly path: never[];
|
|
||||||
|
|
||||||
items: Array<ISettingField | IPublicTypeCustomView>;
|
|
||||||
|
|
||||||
componentMeta: IComponentMeta | null;
|
|
||||||
|
|
||||||
purge(): void;
|
|
||||||
|
|
||||||
getExtraPropValue(propName: string): void;
|
|
||||||
|
|
||||||
setExtraPropValue(propName: string, value: any): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SettingTopEntry implements ISettingTopEntry {
|
|
||||||
private emitter: IEventBus = createModuleEventBus('SettingTopEntry');
|
|
||||||
|
|
||||||
private _items: Array<SettingField | IPublicTypeCustomView> = [];
|
|
||||||
|
|
||||||
private _componentMeta: IComponentMeta | null = null;
|
|
||||||
|
|
||||||
private _isSame = true;
|
|
||||||
|
|
||||||
private _settingFieldMap: { [prop: string]: ISettingField } = {};
|
|
||||||
|
|
||||||
readonly path = [];
|
|
||||||
|
|
||||||
readonly top = this as ISettingTopEntry;
|
|
||||||
|
|
||||||
readonly parent = this as ISettingTopEntry;
|
|
||||||
|
|
||||||
get componentMeta() {
|
|
||||||
return this._componentMeta;
|
|
||||||
}
|
|
||||||
|
|
||||||
get items() {
|
|
||||||
return this._items as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同样的
|
|
||||||
*/
|
|
||||||
get isSameComponent(): boolean {
|
|
||||||
return this._isSame;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 一个
|
|
||||||
*/
|
|
||||||
get isSingle(): boolean {
|
|
||||||
return this.nodes.length === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isLocked(): boolean {
|
|
||||||
return this.first.isLocked;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 多个
|
|
||||||
*/
|
|
||||||
get isMultiple(): boolean {
|
|
||||||
return this.nodes.length > 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly id: string;
|
|
||||||
|
|
||||||
readonly first: INode;
|
|
||||||
|
|
||||||
readonly designer: IDesigner | undefined;
|
|
||||||
|
|
||||||
readonly setters: IPublicApiSetters;
|
|
||||||
|
|
||||||
disposeFunctions: any[] = [];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly editor: IPublicModelEditor,
|
|
||||||
readonly nodes: INode[],
|
|
||||||
) {
|
|
||||||
if (!Array.isArray(nodes) || nodes.length < 1) {
|
|
||||||
throw new ReferenceError('nodes should not be empty');
|
|
||||||
}
|
|
||||||
this.id = generateSessionId(nodes);
|
|
||||||
this.first = nodes[0];
|
|
||||||
this.designer = this.first.document?.designer;
|
|
||||||
this.setters = editor.get('setters') as IPublicApiSetters;
|
|
||||||
|
|
||||||
// setups
|
|
||||||
this.setupComponentMeta();
|
|
||||||
|
|
||||||
// clear fields
|
|
||||||
this.setupItems();
|
|
||||||
|
|
||||||
this.disposeFunctions.push(this.setupEvents());
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupComponentMeta() {
|
|
||||||
// todo: enhance compile a temp configure.compiled
|
|
||||||
const { first } = this;
|
|
||||||
const meta = first.componentMeta;
|
|
||||||
const l = this.nodes.length;
|
|
||||||
let theSame = true;
|
|
||||||
for (let i = 1; i < l; i++) {
|
|
||||||
const other = this.nodes[i];
|
|
||||||
if (other.componentMeta !== meta) {
|
|
||||||
theSame = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (theSame) {
|
|
||||||
this._isSame = true;
|
|
||||||
this._componentMeta = meta;
|
|
||||||
} else {
|
|
||||||
this._isSame = false;
|
|
||||||
this._componentMeta = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupItems() {
|
|
||||||
if (this.componentMeta) {
|
|
||||||
const settingFieldMap: { [prop: string]: ISettingField } = {};
|
|
||||||
const settingFieldCollector = (name: string | number, field: ISettingField) => {
|
|
||||||
settingFieldMap[name] = field;
|
|
||||||
};
|
|
||||||
this._items = this.componentMeta.configure.map((item) => {
|
|
||||||
if (isCustomView(item)) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
return new SettingField(this as ISettingTopEntry, item as any, settingFieldCollector);
|
|
||||||
});
|
|
||||||
this._settingFieldMap = settingFieldMap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupEvents() {
|
|
||||||
return this.componentMeta?.onMetadataChange(() => {
|
|
||||||
this.setupItems();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前属性值
|
|
||||||
*/
|
|
||||||
@computed getValue(): any {
|
|
||||||
return this.first?.propsData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置当前属性值
|
|
||||||
*/
|
|
||||||
setValue(val: any) {
|
|
||||||
this.setProps(val);
|
|
||||||
// TODO: emit value change
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取子项
|
|
||||||
*/
|
|
||||||
get(propName: string | number): ISettingField | null {
|
|
||||||
if (!propName) return null;
|
|
||||||
return (
|
|
||||||
this._settingFieldMap[propName] ||
|
|
||||||
new SettingField(this as ISettingTopEntry, { name: propName })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置子级属性值
|
|
||||||
*/
|
|
||||||
setPropValue(propName: string | number, value: any) {
|
|
||||||
this.nodes.forEach((node) => {
|
|
||||||
node.setPropValue(propName.toString(), value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除已设置值
|
|
||||||
*/
|
|
||||||
clearPropValue(propName: string | number) {
|
|
||||||
this.nodes.forEach((node) => {
|
|
||||||
node.clearPropValue(propName.toString());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取子级属性值
|
|
||||||
*/
|
|
||||||
getPropValue(propName: string | number): any {
|
|
||||||
return this.first.getProp(propName.toString(), true)?.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取顶层附属属性值
|
|
||||||
*/
|
|
||||||
getExtraPropValue(propName: string) {
|
|
||||||
return this.first.getExtraProp(propName, false)?.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置顶层附属属性值
|
|
||||||
*/
|
|
||||||
setExtraPropValue(propName: string, value: any) {
|
|
||||||
this.nodes.forEach((node) => {
|
|
||||||
node.getExtraProp(propName, true)?.setValue(value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置多个属性值,替换原有值
|
|
||||||
setProps(data: object) {
|
|
||||||
this.nodes.forEach((node) => {
|
|
||||||
node.setProps(data as any);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置多个属性值,和原有值合并
|
|
||||||
mergeProps(data: object) {
|
|
||||||
this.nodes.forEach((node) => {
|
|
||||||
node.mergeProps(data as any);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private disposeItems() {
|
|
||||||
this._items.forEach((item) => isPurgeable(item) && item.purge());
|
|
||||||
this._items = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
purge() {
|
|
||||||
this.disposeItems();
|
|
||||||
this._settingFieldMap = {};
|
|
||||||
this.emitter.removeAllListeners();
|
|
||||||
this.disposeFunctions.forEach((f) => f());
|
|
||||||
this.disposeFunctions = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
getProp(propName: string | number) {
|
|
||||||
return this.get(propName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==== copy some Node api =====
|
|
||||||
getStatus() {}
|
|
||||||
|
|
||||||
setStatus() {}
|
|
||||||
|
|
||||||
getChildren() {
|
|
||||||
// this.nodes.map()
|
|
||||||
}
|
|
||||||
|
|
||||||
getDOMNode() {}
|
|
||||||
|
|
||||||
getId() {
|
|
||||||
return this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPage() {
|
|
||||||
return this.first.document;
|
|
||||||
}
|
|
||||||
|
|
||||||
getNode() {
|
|
||||||
return this.nodes[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Purgeable {
|
|
||||||
purge(): void;
|
|
||||||
}
|
|
||||||
function isPurgeable(obj: any): obj is Purgeable {
|
|
||||||
return obj && obj.purge;
|
|
||||||
}
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
// all this file for polyfill vision logic
|
|
||||||
import { isValidElement } from 'react';
|
|
||||||
import { IPublicTypeFieldConfig, IPublicTypeSetterConfig } from '@alilc/lowcode-types';
|
|
||||||
import { isSetterConfig, isDynamicSetter } from '@alilc/lowcode-utils';
|
|
||||||
import { ISettingField } from './setting-field';
|
|
||||||
|
|
||||||
function getHotterFromSetter(setter: any) {
|
|
||||||
return (setter && (setter.Hotter || (setter.type && setter.type.Hotter))) || []; // eslint-disable-line
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTransducerFromSetter(setter: any) {
|
|
||||||
return (
|
|
||||||
(setter &&
|
|
||||||
(setter.transducer ||
|
|
||||||
setter.Transducer ||
|
|
||||||
(setter.type && (setter.type.transducer || setter.type.Transducer)))) ||
|
|
||||||
null
|
|
||||||
); // eslint-disable-line
|
|
||||||
}
|
|
||||||
|
|
||||||
function combineTransducer(transducer: any, arr: any, context: any) {
|
|
||||||
if (!transducer && Array.isArray(arr)) {
|
|
||||||
const [toHot, toNative] = arr;
|
|
||||||
transducer = { toHot, toNative };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
toHot: ((transducer && transducer.toHot) || ((x: any) => x)).bind(context), // eslint-disable-line
|
|
||||||
toNative: ((transducer && transducer.toNative) || ((x: any) => x)).bind(context), // eslint-disable-line
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Transducer {
|
|
||||||
setterTransducer: any;
|
|
||||||
|
|
||||||
context: any;
|
|
||||||
|
|
||||||
constructor(context: ISettingField, config: { setter: IPublicTypeFieldConfig['setter'] }) {
|
|
||||||
let { setter } = config;
|
|
||||||
|
|
||||||
// 1. validElement
|
|
||||||
// 2. IPublicTypeSetterConfig
|
|
||||||
// 3. IPublicTypeSetterConfig[]
|
|
||||||
if (Array.isArray(setter)) {
|
|
||||||
setter = setter[0];
|
|
||||||
} else if (isValidElement(setter) && (setter.type as any).displayName === 'MixedSetter') {
|
|
||||||
setter = (setter.props as any)?.setters?.[0];
|
|
||||||
} else if (typeof setter === 'object' && (setter as any).componentName === 'MixedSetter') {
|
|
||||||
setter = Array.isArray(setter?.props?.setters) && setter.props.setters[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 两种方式标识是 FC 而不是动态 setter
|
|
||||||
* 1. 物料描述里面 setter 的配置,显式设置为 false
|
|
||||||
* 2. registerSetter 注册 setter 时显式设置为 false
|
|
||||||
*/
|
|
||||||
|
|
||||||
let isDynamic = true;
|
|
||||||
|
|
||||||
if (isSetterConfig(setter)) {
|
|
||||||
const { componentName, isDynamic: dynamicFlag } = setter as IPublicTypeSetterConfig;
|
|
||||||
setter = componentName;
|
|
||||||
isDynamic = dynamicFlag !== false;
|
|
||||||
}
|
|
||||||
if (typeof setter === 'string') {
|
|
||||||
const { component, isDynamic: dynamicFlag } = context.setters.getSetter(setter) || {};
|
|
||||||
setter = component;
|
|
||||||
// 如果在物料配置中声明了,在 registerSetter 没有声明,取物料配置中的声明
|
|
||||||
isDynamic = dynamicFlag === undefined ? isDynamic : dynamicFlag !== false;
|
|
||||||
}
|
|
||||||
if (isDynamicSetter(setter) && isDynamic) {
|
|
||||||
try {
|
|
||||||
setter = setter.call(context.internalToShellField(), context.internalToShellField());
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setterTransducer = combineTransducer(
|
|
||||||
getTransducerFromSetter(setter),
|
|
||||||
getHotterFromSetter(setter),
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
toHot(data: any) {
|
|
||||||
return this.setterTransducer.toHot(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
toNative(data: any) {
|
|
||||||
return this.setterTransducer.toNative(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,788 +0,0 @@
|
|||||||
import {
|
|
||||||
makeObservable,
|
|
||||||
observable,
|
|
||||||
engineConfig,
|
|
||||||
action,
|
|
||||||
runWithGlobalEventOff,
|
|
||||||
wrapWithEventSwitch,
|
|
||||||
createModuleEventBus,
|
|
||||||
IEventBus,
|
|
||||||
} from '@alilc/lowcode-editor-core';
|
|
||||||
import {
|
|
||||||
IPublicTypeNodeData,
|
|
||||||
IPublicTypeNodeSchema,
|
|
||||||
IPublicTypePageSchema,
|
|
||||||
IPublicTypeComponentsMap,
|
|
||||||
IPublicTypeDragNodeObject,
|
|
||||||
IPublicTypeDragNodeDataObject,
|
|
||||||
IPublicModelDocumentModel,
|
|
||||||
IPublicEnumTransformStage,
|
|
||||||
IPublicTypeOnChangeOptions,
|
|
||||||
IPublicTypeDisposable,
|
|
||||||
} from '@alilc/lowcode-types';
|
|
||||||
import type { IPublicTypeRootSchema } from '@alilc/lowcode-types';
|
|
||||||
import type { IDropLocation } from '../designer/location';
|
|
||||||
import {
|
|
||||||
uniqueId,
|
|
||||||
isPlainObject,
|
|
||||||
compatStage,
|
|
||||||
isJSExpression,
|
|
||||||
isDOMText,
|
|
||||||
isNodeSchema,
|
|
||||||
isDragNodeObject,
|
|
||||||
isDragNodeDataObject,
|
|
||||||
isNode,
|
|
||||||
} from '@alilc/lowcode-utils';
|
|
||||||
import { IProject } from '../project';
|
|
||||||
import { ISimulatorHost } from '../simulator';
|
|
||||||
import type { IComponentMeta } from '../component-meta';
|
|
||||||
import { IDesigner, IHistory } from '../designer';
|
|
||||||
import { insertChildren, insertChild, IRootNode } from './node/node';
|
|
||||||
import type { INode } from './node/node';
|
|
||||||
import { Selection, ISelection } from './selection';
|
|
||||||
import { History } from './history';
|
|
||||||
import { IModalNodesManager, ModalNodesManager, Node } from './node';
|
|
||||||
import { EDITOR_EVENT } from '../types';
|
|
||||||
|
|
||||||
export type GetDataType<T, NodeType> = T extends undefined
|
|
||||||
? NodeType extends {
|
|
||||||
schema: infer R;
|
|
||||||
}
|
|
||||||
? R
|
|
||||||
: any
|
|
||||||
: T;
|
|
||||||
|
|
||||||
export class DocumentModel
|
|
||||||
implements
|
|
||||||
Omit<
|
|
||||||
IPublicModelDocumentModel<
|
|
||||||
ISelection,
|
|
||||||
IHistory,
|
|
||||||
INode,
|
|
||||||
IDropLocation,
|
|
||||||
IModalNodesManager,
|
|
||||||
IProject
|
|
||||||
>,
|
|
||||||
| 'detecting'
|
|
||||||
| 'checkNesting'
|
|
||||||
| 'getNodeById'
|
|
||||||
// 以下属性在内部的 document 中不存在
|
|
||||||
| 'exportSchema'
|
|
||||||
| 'importSchema'
|
|
||||||
| 'onAddNode'
|
|
||||||
| 'onRemoveNode'
|
|
||||||
| 'onChangeDetecting'
|
|
||||||
| 'onChangeSelection'
|
|
||||||
| 'onChangeNodeProp'
|
|
||||||
| 'onImportSchema'
|
|
||||||
| 'isDetectingNode'
|
|
||||||
| 'onFocusNodeChanged'
|
|
||||||
| 'onDropLocationChanged'
|
|
||||||
>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 根节点 类型有:Page/Component/Block
|
|
||||||
*/
|
|
||||||
rootNode: IRootNode | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文档编号
|
|
||||||
*/
|
|
||||||
id: string = uniqueId('doc');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 选区控制
|
|
||||||
*/
|
|
||||||
readonly selection: ISelection = new Selection(this);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作记录控制
|
|
||||||
*/
|
|
||||||
readonly history: IHistory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模态节点管理
|
|
||||||
*/
|
|
||||||
modalNodesManager: IModalNodesManager;
|
|
||||||
|
|
||||||
private _nodesMap = new Map<string, INode>();
|
|
||||||
|
|
||||||
readonly project: IProject;
|
|
||||||
|
|
||||||
readonly designer: IDesigner;
|
|
||||||
|
|
||||||
@observable.shallow private nodes = new Set<INode>();
|
|
||||||
|
|
||||||
private seqId = 0;
|
|
||||||
|
|
||||||
private emitter: IEventBus;
|
|
||||||
|
|
||||||
private rootNodeVisitorMap: { [visitorName: string]: any } = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模拟器
|
|
||||||
*/
|
|
||||||
get simulator(): ISimulatorHost | null {
|
|
||||||
return this.project.simulator;
|
|
||||||
}
|
|
||||||
|
|
||||||
get nodesMap(): Map<string, INode> {
|
|
||||||
return this._nodesMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
get fileName(): string {
|
|
||||||
return this.rootNode?.getExtraProp('fileName', false)?.getAsString() || this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
set fileName(fileName: string) {
|
|
||||||
this.rootNode?.getExtraProp('fileName', true)?.setValue(fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
get focusNode(): INode | null {
|
|
||||||
if (this._drillDownNode) {
|
|
||||||
return this._drillDownNode;
|
|
||||||
}
|
|
||||||
const selector = engineConfig.get('focusNodeSelector');
|
|
||||||
if (selector && typeof selector === 'function') {
|
|
||||||
return selector(this.rootNode!);
|
|
||||||
}
|
|
||||||
return this.rootNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable.ref private _drillDownNode: INode | null = null;
|
|
||||||
|
|
||||||
private _modalNode?: INode;
|
|
||||||
|
|
||||||
private _blank?: boolean;
|
|
||||||
|
|
||||||
private inited = false;
|
|
||||||
|
|
||||||
@observable.shallow private willPurgeSpace: INode[] = [];
|
|
||||||
|
|
||||||
get modalNode() {
|
|
||||||
return this._modalNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
get currentRoot() {
|
|
||||||
return this.modalNode || this.focusNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable.shallow private activeNodes?: INode[];
|
|
||||||
|
|
||||||
@observable.ref private _dropLocation: IDropLocation | null = null;
|
|
||||||
|
|
||||||
set dropLocation(loc: IDropLocation | null) {
|
|
||||||
this._dropLocation = loc;
|
|
||||||
// pub event
|
|
||||||
this.designer.editor.eventBus.emit('document.dropLocation.changed', {
|
|
||||||
document: this,
|
|
||||||
location: loc,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 投放插入位置标记
|
|
||||||
*/
|
|
||||||
get dropLocation() {
|
|
||||||
return this._dropLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出 schema 数据
|
|
||||||
*/
|
|
||||||
get schema(): IPublicTypeRootSchema {
|
|
||||||
return this.rootNode?.schema as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable.ref private _opened = false;
|
|
||||||
|
|
||||||
@observable.ref private _suspensed = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否为非激活状态
|
|
||||||
*/
|
|
||||||
get suspensed(): boolean {
|
|
||||||
return this._suspensed || !this._opened;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 与 suspensed 相反,是否为激活状态,这个函数可能用的更多一点
|
|
||||||
*/
|
|
||||||
get active(): boolean {
|
|
||||||
return !this._suspensed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否打开
|
|
||||||
*/
|
|
||||||
get opened() {
|
|
||||||
return this._opened;
|
|
||||||
}
|
|
||||||
|
|
||||||
get root() {
|
|
||||||
return this.rootNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(project: IProject, schema?: IPublicTypeRootSchema) {
|
|
||||||
makeObservable(this);
|
|
||||||
this.project = project;
|
|
||||||
this.designer = this.project?.designer;
|
|
||||||
this.emitter = createModuleEventBus('DocumentModel');
|
|
||||||
|
|
||||||
if (!schema) {
|
|
||||||
this._blank = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 兼容 vision
|
|
||||||
this.id = project.getSchema()?.id || this.id;
|
|
||||||
|
|
||||||
this.rootNode = this.createNode<IRootNode, IPublicTypeRootSchema>(
|
|
||||||
schema || {
|
|
||||||
componentName: 'Page',
|
|
||||||
id: 'root',
|
|
||||||
fileName: '',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
this.history = new History(
|
|
||||||
() => this.export(IPublicEnumTransformStage.Serilize),
|
|
||||||
(schema) => {
|
|
||||||
this.import(schema as IPublicTypeRootSchema, true);
|
|
||||||
this.simulator?.rerender();
|
|
||||||
},
|
|
||||||
this,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.setupListenActiveNodes();
|
|
||||||
this.modalNodesManager = new ModalNodesManager(this);
|
|
||||||
this.inited = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
drillDown(node: INode | null) {
|
|
||||||
this._drillDownNode = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeNodeVisible(fn: (node: INode, visible: boolean) => void): IPublicTypeDisposable {
|
|
||||||
this.designer.editor?.eventBus.on(EDITOR_EVENT.NODE_VISIBLE_CHANGE, fn);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
this.designer.editor?.eventBus.off(EDITOR_EVENT.NODE_VISIBLE_CHANGE, fn);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeNodeChildren(
|
|
||||||
fn: (info: IPublicTypeOnChangeOptions<INode>) => void,
|
|
||||||
): IPublicTypeDisposable {
|
|
||||||
this.designer.editor?.eventBus.on(EDITOR_EVENT.NODE_CHILDREN_CHANGE, fn);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
this.designer.editor?.eventBus.off(EDITOR_EVENT.NODE_CHILDREN_CHANGE, fn);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
addWillPurge(node: INode) {
|
|
||||||
this.willPurgeSpace.push(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeWillPurge(node: INode) {
|
|
||||||
const i = this.willPurgeSpace.indexOf(node);
|
|
||||||
if (i > -1) {
|
|
||||||
this.willPurgeSpace.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isBlank() {
|
|
||||||
return !!(this._blank && !this.isModified());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成唯一 id
|
|
||||||
*/
|
|
||||||
nextId(possibleId: string | undefined): string {
|
|
||||||
let id = possibleId;
|
|
||||||
while (!id || this.nodesMap.get(id)) {
|
|
||||||
id = `node_${(String(this.id).slice(-10) + (++this.seqId).toString(36)).toLocaleLowerCase()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据 id 获取节点
|
|
||||||
*/
|
|
||||||
getNode(id: string): INode | null {
|
|
||||||
return this._nodesMap.get(id) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据 id 获取节点
|
|
||||||
*/
|
|
||||||
getNodeCount(): number {
|
|
||||||
return this._nodesMap?.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否存在节点
|
|
||||||
*/
|
|
||||||
hasNode(id: string): boolean {
|
|
||||||
const node = this.getNode(id);
|
|
||||||
return node ? !node.isPurged : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMountNode(fn: (payload: { node: INode }) => void) {
|
|
||||||
this.designer.editor.eventBus.on('node.add', fn as any);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
this.designer.editor.eventBus.off('node.add', fn as any);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据 schema 创建一个节点
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
createNode<T = INode, S = IPublicTypeNodeSchema>(data: S): T {
|
|
||||||
let schema: any;
|
|
||||||
if (isDOMText(data) || isJSExpression(data)) {
|
|
||||||
schema = {
|
|
||||||
componentName: 'Leaf',
|
|
||||||
children: data,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
schema = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
let node: INode | null = null;
|
|
||||||
if (this.hasNode(schema?.id)) {
|
|
||||||
schema.id = null;
|
|
||||||
}
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (schema.id) {
|
|
||||||
node = this.getNode(schema.id);
|
|
||||||
// TODO: 底下这几段代码似乎永远都进不去
|
|
||||||
if (node && node.componentName === schema.componentName) {
|
|
||||||
if (node.parent) {
|
|
||||||
node.internalSetParent(null, false);
|
|
||||||
// will move to another position
|
|
||||||
// todo: this.activeNodes?.push(node);
|
|
||||||
}
|
|
||||||
node.import(schema, true);
|
|
||||||
} else if (node) {
|
|
||||||
node = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!node) {
|
|
||||||
node = new Node(this, schema);
|
|
||||||
// will add
|
|
||||||
// todo: this.activeNodes?.push(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._nodesMap.set(node.id, node);
|
|
||||||
this.nodes.add(node);
|
|
||||||
|
|
||||||
this.emitter.emit('nodecreate', node);
|
|
||||||
return node as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroyNode(node: INode) {
|
|
||||||
this.emitter.emit('nodedestroy', node);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插入一个节点
|
|
||||||
*/
|
|
||||||
insertNode(
|
|
||||||
parent: INode,
|
|
||||||
thing: INode | IPublicTypeNodeData,
|
|
||||||
at?: number | null,
|
|
||||||
copy?: boolean,
|
|
||||||
): INode | null {
|
|
||||||
return insertChild(parent, thing, at, copy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插入多个节点
|
|
||||||
*/
|
|
||||||
insertNodes(
|
|
||||||
parent: INode,
|
|
||||||
thing: INode[] | IPublicTypeNodeData[],
|
|
||||||
at?: number | null,
|
|
||||||
copy?: boolean,
|
|
||||||
) {
|
|
||||||
return insertChildren(parent, thing, at, copy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除一个节点
|
|
||||||
*/
|
|
||||||
removeNode(idOrNode: string | INode) {
|
|
||||||
let id: string;
|
|
||||||
let node: INode | null = null;
|
|
||||||
if (typeof idOrNode === 'string') {
|
|
||||||
id = idOrNode;
|
|
||||||
node = this.getNode(id);
|
|
||||||
} else if (idOrNode.id) {
|
|
||||||
id = idOrNode.id;
|
|
||||||
node = this.getNode(id);
|
|
||||||
}
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.internalRemoveAndPurgeNode(node, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 内部方法,请勿调用
|
|
||||||
*/
|
|
||||||
internalRemoveAndPurgeNode(node: INode, useMutator = false) {
|
|
||||||
if (!this.nodes.has(node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
node.remove(useMutator);
|
|
||||||
}
|
|
||||||
|
|
||||||
unlinkNode(node: INode) {
|
|
||||||
this.nodes.delete(node);
|
|
||||||
this._nodesMap.delete(node.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 包裹当前选区中的节点
|
|
||||||
*/
|
|
||||||
wrapWith(schema: IPublicTypeNodeSchema): INode | null {
|
|
||||||
const nodes = this.selection.getTopNodes();
|
|
||||||
if (nodes.length < 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const wrapper = this.createNode(schema);
|
|
||||||
if (wrapper?.isParental()) {
|
|
||||||
const first = nodes[0];
|
|
||||||
// TODO: check nesting rules x 2
|
|
||||||
insertChild(first.parent!, wrapper, first.index);
|
|
||||||
insertChildren(wrapper, nodes);
|
|
||||||
this.selection.select(wrapper.id);
|
|
||||||
return wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper && this.removeNode(wrapper);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
import(schema: IPublicTypeRootSchema, checkId = false) {
|
|
||||||
const drillDownNodeId = this._drillDownNode?.id;
|
|
||||||
runWithGlobalEventOff(() => {
|
|
||||||
// TODO: 暂时用饱和式删除,原因是 Slot 节点并不是树节点,无法正常递归删除
|
|
||||||
this.nodes.forEach((node) => {
|
|
||||||
if (node.isRoot()) return;
|
|
||||||
this.internalRemoveAndPurgeNode(node, true);
|
|
||||||
});
|
|
||||||
this.rootNode?.import(schema as any, checkId);
|
|
||||||
this.modalNodesManager = new ModalNodesManager(this);
|
|
||||||
// todo: select added and active track added
|
|
||||||
if (drillDownNodeId) {
|
|
||||||
this.drillDown(this.getNode(drillDownNodeId));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export(
|
|
||||||
stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Serilize,
|
|
||||||
): IPublicTypeRootSchema | undefined {
|
|
||||||
stage = compatStage(stage);
|
|
||||||
// 置顶只作用于 Page 的第一级子节点,目前还用不到里层的置顶;如果后面有需要可以考虑将这段写到 node-children 中的 export
|
|
||||||
const currentSchema = this.rootNode?.export<IPublicTypeRootSchema>(stage);
|
|
||||||
if (
|
|
||||||
Array.isArray(currentSchema?.children) &&
|
|
||||||
currentSchema?.children?.length &&
|
|
||||||
currentSchema?.children?.length > 0
|
|
||||||
) {
|
|
||||||
const FixedTopNodeIndex = currentSchema?.children
|
|
||||||
.filter((i) => isPlainObject(i))
|
|
||||||
.findIndex((i) => (i as IPublicTypeNodeSchema).props?.__isTopFixed__);
|
|
||||||
if (FixedTopNodeIndex > 0) {
|
|
||||||
const FixedTopNode = currentSchema?.children.splice(FixedTopNodeIndex, 1);
|
|
||||||
currentSchema?.children.unshift(FixedTopNode[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return currentSchema;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出节点数据
|
|
||||||
*/
|
|
||||||
getNodeSchema(id: string): IPublicTypeNodeData | null {
|
|
||||||
const node = this.getNode(id);
|
|
||||||
if (node) {
|
|
||||||
return node.schema;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否已修改
|
|
||||||
*/
|
|
||||||
isModified(): boolean {
|
|
||||||
return this.history.isSavePoint();
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: does needed?
|
|
||||||
getComponent(componentName: string): any {
|
|
||||||
return this.simulator!.getComponent(componentName);
|
|
||||||
}
|
|
||||||
|
|
||||||
getComponentMeta(componentName: string): IComponentMeta {
|
|
||||||
return this.designer.getComponentMeta(
|
|
||||||
componentName,
|
|
||||||
() => this.simulator?.generateComponentMetadata(componentName) || null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换激活,只有打开的才能激活
|
|
||||||
* 不激活,打开之后切换到另外一个时发生,比如 tab 视图,切换到另外一个标签页
|
|
||||||
*/
|
|
||||||
private setSuspense(flag: boolean) {
|
|
||||||
if (!this._opened && !flag) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._suspensed = flag;
|
|
||||||
this.simulator?.setSuspense(flag);
|
|
||||||
if (!flag) {
|
|
||||||
this.project.checkExclusive(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspense() {
|
|
||||||
this.setSuspense(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
activate() {
|
|
||||||
this.setSuspense(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 打开,已载入,默认建立时就打开状态,除非手动关闭
|
|
||||||
*/
|
|
||||||
open(): DocumentModel {
|
|
||||||
const originState = this._opened;
|
|
||||||
this._opened = true;
|
|
||||||
if (originState === false) {
|
|
||||||
this.designer.postEvent('document-open', this);
|
|
||||||
}
|
|
||||||
if (this._suspensed) {
|
|
||||||
this.setSuspense(false);
|
|
||||||
} else {
|
|
||||||
this.project.checkExclusive(this);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭,相当于 sleep,仍然缓存,停止一切响应,如果有发生的变更没被保存,仍然需要去取数据保存
|
|
||||||
*/
|
|
||||||
close(): void {
|
|
||||||
this.setSuspense(true);
|
|
||||||
this._opened = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从项目中移除
|
|
||||||
*/
|
|
||||||
remove() {
|
|
||||||
this.designer.postEvent('document.remove', { id: this.id });
|
|
||||||
this.purge();
|
|
||||||
this.project.removeDocument(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
purge() {
|
|
||||||
this.rootNode?.purge();
|
|
||||||
this.nodes.clear();
|
|
||||||
this._nodesMap.clear();
|
|
||||||
this.rootNode = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
checkNesting(
|
|
||||||
dropTarget: INode,
|
|
||||||
dragObject:
|
|
||||||
| IPublicTypeDragNodeObject
|
|
||||||
| IPublicTypeNodeSchema
|
|
||||||
| INode
|
|
||||||
| IPublicTypeDragNodeDataObject,
|
|
||||||
): boolean {
|
|
||||||
let items: Array<INode | IPublicTypeNodeSchema>;
|
|
||||||
if (isDragNodeDataObject(dragObject)) {
|
|
||||||
items = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data];
|
|
||||||
} else if (isDragNodeObject<INode>(dragObject)) {
|
|
||||||
items = dragObject.nodes;
|
|
||||||
} else if (isNode<INode>(dragObject) || isNodeSchema(dragObject)) {
|
|
||||||
items = [dragObject];
|
|
||||||
} else {
|
|
||||||
console.warn('the dragObject is not in the correct type, dragObject:', dragObject);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return items.every(
|
|
||||||
(item) => this.checkNestingDown(dropTarget, item) && this.checkNestingUp(dropTarget, item),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查对象对父级的要求,涉及配置 parentWhitelist
|
|
||||||
*/
|
|
||||||
checkNestingUp(parent: INode, obj: IPublicTypeNodeSchema | INode): boolean {
|
|
||||||
if (isNode(obj) || isNodeSchema(obj)) {
|
|
||||||
const config = isNode(obj) ? obj.componentMeta : this.getComponentMeta(obj.componentName);
|
|
||||||
if (config) {
|
|
||||||
return config.checkNestingUp(obj, parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查投放位置对子级的要求,涉及配置 childWhitelist
|
|
||||||
*/
|
|
||||||
checkNestingDown(parent: INode, obj: IPublicTypeNodeSchema | INode): boolean {
|
|
||||||
const config = parent.componentMeta;
|
|
||||||
return config.checkNestingDown(parent, obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======= compatibles for vision
|
|
||||||
getRoot() {
|
|
||||||
return this.rootNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add toData
|
|
||||||
toData(extraComps?: string[]) {
|
|
||||||
const node = this.export(IPublicEnumTransformStage.Save);
|
|
||||||
const data = {
|
|
||||||
componentsMap: this.getComponentsMap(extraComps),
|
|
||||||
utils: this.getUtilsMap(),
|
|
||||||
componentsTree: [node],
|
|
||||||
};
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
getHistory(): IHistory {
|
|
||||||
return this.history;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* istanbul ignore next */
|
|
||||||
acceptRootNodeVisitor(visitorName = 'default', visitorFn: (node: IRootNode) => any) {
|
|
||||||
let visitorResult = {};
|
|
||||||
if (!visitorName) {
|
|
||||||
/* eslint-disable-next-line no-console */
|
|
||||||
console.warn('Invalid or empty RootNodeVisitor name.');
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (this.rootNode) {
|
|
||||||
visitorResult = visitorFn.call(this, this.rootNode);
|
|
||||||
this.rootNodeVisitorMap[visitorName] = visitorResult;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('RootNodeVisitor is not valid.');
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
return visitorResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* istanbul ignore next */
|
|
||||||
getRootNodeVisitor(name: string) {
|
|
||||||
return this.rootNodeVisitorMap[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
getComponentsMap(extraComps?: string[]) {
|
|
||||||
const componentsMap: IPublicTypeComponentsMap = [];
|
|
||||||
// 组件去重
|
|
||||||
const exsitingMap: { [componentName: string]: boolean } = {};
|
|
||||||
for (const node of this._nodesMap.values()) {
|
|
||||||
const componentName: string = node.componentName;
|
|
||||||
if (componentName === 'Slot') continue;
|
|
||||||
if (!exsitingMap[componentName]) {
|
|
||||||
exsitingMap[componentName] = true;
|
|
||||||
if (node.componentMeta?.npm?.package) {
|
|
||||||
componentsMap.push({
|
|
||||||
...node.componentMeta.npm,
|
|
||||||
componentName,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
componentsMap.push({
|
|
||||||
devMode: 'lowCode',
|
|
||||||
componentName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 合并外界传入的自定义渲染的组件
|
|
||||||
if (Array.isArray(extraComps)) {
|
|
||||||
extraComps.forEach((componentName) => {
|
|
||||||
if (componentName && !exsitingMap[componentName]) {
|
|
||||||
const meta = this.getComponentMeta(componentName);
|
|
||||||
if (meta?.npm?.package) {
|
|
||||||
componentsMap.push({
|
|
||||||
...meta?.npm,
|
|
||||||
componentName,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
componentsMap.push({
|
|
||||||
devMode: 'lowCode',
|
|
||||||
componentName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return componentsMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 schema 中的 utils 节点,当前版本不判断页面中使用了哪些 utils,直接返回资产包中所有的 utils
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
getUtilsMap() {
|
|
||||||
return this.designer?.editor?.get('assets')?.utils?.map((item: any) => ({
|
|
||||||
name: item.name,
|
|
||||||
type: item.type || 'npm',
|
|
||||||
// TODO 当前只有 npm 类型,content 直接设置为 item.npm,有 function 类型之后需要处理
|
|
||||||
content: item.npm,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
onNodeCreate(func: (node: INode) => void) {
|
|
||||||
const wrappedFunc = wrapWithEventSwitch(func);
|
|
||||||
this.emitter.on('nodecreate', wrappedFunc);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('nodecreate', wrappedFunc);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onNodeDestroy(func: (node: INode) => void) {
|
|
||||||
const wrappedFunc = wrapWithEventSwitch(func);
|
|
||||||
this.emitter.on('nodedestroy', wrappedFunc);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('nodedestroy', wrappedFunc);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onReady(fn: (...args: any[]) => void) {
|
|
||||||
this.designer.editor.eventBus.on('document-open', fn);
|
|
||||||
return () => {
|
|
||||||
this.designer.editor.eventBus.off('document-open', fn);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupListenActiveNodes() {
|
|
||||||
// todo:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isDocumentModel(obj: any): obj is IDocumentModel {
|
|
||||||
return obj && obj.rootNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isPageSchema(obj: any): obj is IPublicTypePageSchema {
|
|
||||||
return obj?.componentName === 'Page';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IDocumentModel extends DocumentModel {}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import { Component } from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { observer } from '@alilc/lowcode-editor-core';
|
|
||||||
import { IDocumentModel } from './document-model';
|
|
||||||
import { BuiltinSimulatorHostView } from '../builtin-simulator';
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class DocumentView extends Component<{ document: IDocumentModel }> {
|
|
||||||
render() {
|
|
||||||
const { document } = this.props;
|
|
||||||
const { simulatorProps } = document as any;
|
|
||||||
const Simulator = document.designer.simulatorComponent || BuiltinSimulatorHostView;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames('lc-document', {
|
|
||||||
'lc-document-hidden': document.suspensed,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{/* 这一层将来做缩放用途 */}
|
|
||||||
<div className="lc-simulator-shell">
|
|
||||||
<Simulator {...simulatorProps} />
|
|
||||||
</div>
|
|
||||||
<DocumentInfoView document={document} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DocumentInfoView extends Component<{ document: IDocumentModel }> {
|
|
||||||
render() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,269 +0,0 @@
|
|||||||
import { reaction, untracked, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
|
|
||||||
import { IPublicTypeNodeSchema, IPublicModelHistory, IPublicTypeDisposable } from '@alilc/lowcode-types';
|
|
||||||
import { Logger } from '@alilc/lowcode-utils';
|
|
||||||
import { IDocumentModel } from '../designer';
|
|
||||||
|
|
||||||
const logger = new Logger({ level: 'warn', bizName: 'history' });
|
|
||||||
|
|
||||||
export interface Serialization<K = IPublicTypeNodeSchema, T = string> {
|
|
||||||
serialize(data: K): T;
|
|
||||||
unserialize(data: T): K;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IHistory extends IPublicModelHistory {
|
|
||||||
onStateChange(func: () => any): IPublicTypeDisposable;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class History<T = IPublicTypeNodeSchema> implements IHistory {
|
|
||||||
private session: Session;
|
|
||||||
|
|
||||||
private records: Session[];
|
|
||||||
|
|
||||||
private point = 0;
|
|
||||||
|
|
||||||
private emitter: IEventBus = createModuleEventBus('History');
|
|
||||||
|
|
||||||
private asleep = false;
|
|
||||||
|
|
||||||
private currentSerialization: Serialization<T, string> = {
|
|
||||||
serialize(data: T): string {
|
|
||||||
return JSON.stringify(data);
|
|
||||||
},
|
|
||||||
unserialize(data: string) {
|
|
||||||
return JSON.parse(data);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
get hotData() {
|
|
||||||
return this.session.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private timeGap: number = 1000;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
dataFn: () => T | null,
|
|
||||||
private redoer: (data: T) => void,
|
|
||||||
private document?: IDocumentModel,
|
|
||||||
) {
|
|
||||||
this.session = new Session(0, null, this.timeGap);
|
|
||||||
this.records = [this.session];
|
|
||||||
|
|
||||||
reaction((): any => {
|
|
||||||
return dataFn();
|
|
||||||
}, (data: T) => {
|
|
||||||
if (this.asleep) return;
|
|
||||||
untracked(() => {
|
|
||||||
const log = this.currentSerialization.serialize(data);
|
|
||||||
|
|
||||||
// do not record unchanged data
|
|
||||||
if (this.session.data === log) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.session.isActive()) {
|
|
||||||
this.session.log(log);
|
|
||||||
} else {
|
|
||||||
this.session.end();
|
|
||||||
const lastState = this.getState();
|
|
||||||
const cursor = this.session.cursor + 1;
|
|
||||||
const session = new Session(cursor, log, this.timeGap);
|
|
||||||
this.session = session;
|
|
||||||
this.records.splice(cursor, this.records.length - cursor, session);
|
|
||||||
const currentState = this.getState();
|
|
||||||
if (currentState !== lastState) {
|
|
||||||
this.emitter.emit('statechange', currentState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, { fireImmediately: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
setSerialization(serialization: Serialization<T, string>) {
|
|
||||||
this.currentSerialization = serialization;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSavePoint(): boolean {
|
|
||||||
return this.point !== this.session.cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sleep() {
|
|
||||||
this.asleep = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private wakeup() {
|
|
||||||
this.asleep = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
go(originalCursor: number) {
|
|
||||||
this.session.end();
|
|
||||||
|
|
||||||
let cursor = originalCursor;
|
|
||||||
cursor = +cursor;
|
|
||||||
if (cursor < 0) {
|
|
||||||
cursor = 0;
|
|
||||||
} else if (cursor >= this.records.length) {
|
|
||||||
cursor = this.records.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentCursor = this.session.cursor;
|
|
||||||
if (cursor === currentCursor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = this.records[cursor];
|
|
||||||
const hotData = session.data;
|
|
||||||
|
|
||||||
this.sleep();
|
|
||||||
try {
|
|
||||||
this.redoer(this.currentSerialization.unserialize(hotData));
|
|
||||||
this.emitter.emit('cursor', hotData);
|
|
||||||
} catch (e) /* istanbul ignore next */ {
|
|
||||||
logger.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.wakeup();
|
|
||||||
this.session = session;
|
|
||||||
|
|
||||||
this.emitter.emit('statechange', this.getState());
|
|
||||||
}
|
|
||||||
|
|
||||||
back() {
|
|
||||||
if (!this.session) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const cursor = this.session.cursor - 1;
|
|
||||||
this.go(cursor);
|
|
||||||
const editor = this.document?.designer.editor;
|
|
||||||
if (!editor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
editor.eventBus.emit('history.back', cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
forward() {
|
|
||||||
if (!this.session) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const cursor = this.session.cursor + 1;
|
|
||||||
this.go(cursor);
|
|
||||||
const editor = this.document?.designer.editor;
|
|
||||||
if (!editor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
editor.eventBus.emit('history.forward', cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
savePoint() {
|
|
||||||
if (!this.session) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.session.end();
|
|
||||||
this.point = this.session.cursor;
|
|
||||||
this.emitter.emit('statechange', this.getState());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* | 1 | 1 | 1 |
|
|
||||||
* | -------- | -------- | -------- |
|
|
||||||
* | modified | redoable | undoable |
|
|
||||||
*/
|
|
||||||
getState(): number {
|
|
||||||
const { cursor } = this.session;
|
|
||||||
let state = 7;
|
|
||||||
// undoable ?
|
|
||||||
if (cursor <= 0) {
|
|
||||||
state -= 1;
|
|
||||||
}
|
|
||||||
// redoable ?
|
|
||||||
if (cursor >= this.records.length - 1) {
|
|
||||||
state -= 2;
|
|
||||||
}
|
|
||||||
// modified ?
|
|
||||||
if (this.point === cursor) {
|
|
||||||
state -= 4;
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 监听 state 变更事件
|
|
||||||
* @param func
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
onChangeState(func: () => any): IPublicTypeDisposable {
|
|
||||||
return this.onStateChange(func);
|
|
||||||
}
|
|
||||||
|
|
||||||
onStateChange(func: () => any): IPublicTypeDisposable {
|
|
||||||
this.emitter.on('statechange', func);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('statechange', func);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 监听历史记录游标位置变更事件
|
|
||||||
* @param func
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
onChangeCursor(func: () => any): IPublicTypeDisposable {
|
|
||||||
return this.onCursor(func);
|
|
||||||
}
|
|
||||||
|
|
||||||
onCursor(func: () => any): () => void {
|
|
||||||
this.emitter.on('cursor', func);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('cursor', func);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.emitter.removeAllListeners();
|
|
||||||
this.records = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Session {
|
|
||||||
private _data: any;
|
|
||||||
|
|
||||||
private activeTimer: any;
|
|
||||||
|
|
||||||
get data() {
|
|
||||||
return this._data;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(readonly cursor: number, data: any, private timeGap: number = 1000) {
|
|
||||||
this.setTimer();
|
|
||||||
this.log(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
log(data: any) {
|
|
||||||
if (!this.isActive()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._data = data;
|
|
||||||
this.setTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
isActive() {
|
|
||||||
return this.activeTimer != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
end() {
|
|
||||||
if (this.isActive()) {
|
|
||||||
this.clearTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setTimer() {
|
|
||||||
this.clearTimer();
|
|
||||||
this.activeTimer = setTimeout(() => this.end(), this.timeGap);
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearTimer() {
|
|
||||||
if (this.activeTimer) {
|
|
||||||
clearTimeout(this.activeTimer);
|
|
||||||
}
|
|
||||||
this.activeTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from './document-model';
|
|
||||||
export * from './node';
|
|
||||||
export * from './selection';
|
|
||||||
export * from './history';
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
import { observable, computed, makeObservable } from '@alilc/lowcode-editor-core';
|
|
||||||
import { uniqueId } from '@alilc/lowcode-utils';
|
|
||||||
import { IPublicTypeTitleContent, IPublicModelExclusiveGroup } from '@alilc/lowcode-types';
|
|
||||||
import type { INode } from './node';
|
|
||||||
import { intl } from '../../locale';
|
|
||||||
|
|
||||||
export interface IExclusiveGroup extends IPublicModelExclusiveGroup<INode> {
|
|
||||||
readonly name: string;
|
|
||||||
|
|
||||||
get index(): number | undefined;
|
|
||||||
|
|
||||||
remove(node: INode): void;
|
|
||||||
|
|
||||||
add(node: INode): void;
|
|
||||||
|
|
||||||
isVisible(node: INode): boolean;
|
|
||||||
|
|
||||||
get length(): number;
|
|
||||||
|
|
||||||
get visibleNode(): INode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// modals assoc x-hide value, initial: check is Modal, yes will put it in modals, cross levels
|
|
||||||
// if-else-if assoc conditionGroup value, should be the same level,
|
|
||||||
// and siblings, need renderEngine support
|
|
||||||
export class ExclusiveGroup implements IExclusiveGroup {
|
|
||||||
readonly isExclusiveGroup = true;
|
|
||||||
|
|
||||||
readonly id = uniqueId('exclusive');
|
|
||||||
|
|
||||||
readonly title: IPublicTypeTitleContent;
|
|
||||||
|
|
||||||
@observable.shallow readonly children: INode[] = [];
|
|
||||||
|
|
||||||
@observable private visibleIndex = 0;
|
|
||||||
|
|
||||||
@computed get document() {
|
|
||||||
return this.visibleNode.document;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get zLevel() {
|
|
||||||
return this.visibleNode.zLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get length() {
|
|
||||||
return this.children.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get visibleNode(): INode {
|
|
||||||
return this.children[this.visibleIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get firstNode(): INode {
|
|
||||||
return this.children[0]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
get index() {
|
|
||||||
return this.firstNode.index;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(readonly name: string, title?: IPublicTypeTitleContent) {
|
|
||||||
makeObservable(this);
|
|
||||||
this.title = title || {
|
|
||||||
type: 'i18n',
|
|
||||||
intl: intl('Condition Group'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
add(node: INode) {
|
|
||||||
if (node.nextSibling && node.nextSibling.conditionGroup?.id === this.id) {
|
|
||||||
const i = this.children.indexOf(node.nextSibling);
|
|
||||||
this.children.splice(i, 0, node);
|
|
||||||
} else {
|
|
||||||
this.children.push(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(node: INode) {
|
|
||||||
const i = this.children.indexOf(node);
|
|
||||||
if (i > -1) {
|
|
||||||
this.children.splice(i, 1);
|
|
||||||
if (this.visibleIndex > i) {
|
|
||||||
this.visibleIndex -= 1;
|
|
||||||
} else if (this.visibleIndex >= this.children.length) {
|
|
||||||
this.visibleIndex = this.children.length - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setVisible(node: INode) {
|
|
||||||
const i = this.children.indexOf(node);
|
|
||||||
if (i > -1) {
|
|
||||||
this.visibleIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isVisible(node: INode) {
|
|
||||||
const i = this.children.indexOf(node);
|
|
||||||
return i === this.visibleIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isExclusiveGroup(obj: any): obj is ExclusiveGroup {
|
|
||||||
return obj && obj.isExclusiveGroup;
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export * from './exclusive-group';
|
|
||||||
export * from './node';
|
|
||||||
export * from './node-children';
|
|
||||||
export * from './props/prop';
|
|
||||||
export * from './props/props';
|
|
||||||
export * from './transform-stage';
|
|
||||||
export * from './modal-nodes-manager';
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
import { INode } from './node';
|
|
||||||
import { DocumentModel } from '../document-model';
|
|
||||||
import { IPublicModelModalNodesManager } from '@alilc/lowcode-types';
|
|
||||||
import { createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core';
|
|
||||||
|
|
||||||
export function getModalNodes(node: INode) {
|
|
||||||
if (!node) return [];
|
|
||||||
let nodes: any = [];
|
|
||||||
if (node.componentMeta.isModal) {
|
|
||||||
nodes.push(node);
|
|
||||||
}
|
|
||||||
const { children } = node;
|
|
||||||
if (children) {
|
|
||||||
children.forEach((child) => {
|
|
||||||
nodes = nodes.concat(getModalNodes(child));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IModalNodesManager extends IPublicModelModalNodesManager<INode> {
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ModalNodesManager implements IModalNodesManager {
|
|
||||||
willDestroy: any;
|
|
||||||
|
|
||||||
private page: DocumentModel;
|
|
||||||
|
|
||||||
private modalNodes: INode[];
|
|
||||||
|
|
||||||
private nodeRemoveEvents: any;
|
|
||||||
|
|
||||||
private emitter: IEventBus;
|
|
||||||
|
|
||||||
constructor(page: DocumentModel) {
|
|
||||||
this.page = page;
|
|
||||||
this.emitter = createModuleEventBus('ModalNodesManager');
|
|
||||||
this.nodeRemoveEvents = {};
|
|
||||||
this.setNodes();
|
|
||||||
this.hideModalNodes();
|
|
||||||
this.willDestroy = [
|
|
||||||
page.onNodeCreate((node) => this.addNode(node)),
|
|
||||||
page.onNodeDestroy((node) => this.removeNode(node)),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
getModalNodes(): INode[] {
|
|
||||||
return this.modalNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
getVisibleModalNode(): INode | null {
|
|
||||||
const visibleNode = this.getModalNodes().find((node: INode) => node.getVisible());
|
|
||||||
return visibleNode || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
hideModalNodes() {
|
|
||||||
this.modalNodes.forEach((node: INode) => {
|
|
||||||
node.setVisible(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setVisible(node: INode) {
|
|
||||||
this.hideModalNodes();
|
|
||||||
node.setVisible(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setInvisible(node: INode) {
|
|
||||||
node.setVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChange(func: () => any) {
|
|
||||||
this.emitter.on('visibleChange', func);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('visibleChange', func);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onModalNodesChange(func: () => any) {
|
|
||||||
this.emitter.on('modalNodesChange', func);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('modalNodesChange', func);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private addNode(node: INode) {
|
|
||||||
if (node?.componentMeta.isModal) {
|
|
||||||
this.hideModalNodes();
|
|
||||||
this.modalNodes.push(node);
|
|
||||||
this.addNodeEvent(node);
|
|
||||||
this.emitter.emit('modalNodesChange');
|
|
||||||
this.emitter.emit('visibleChange');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeNode(node: INode) {
|
|
||||||
if (node.componentMeta.isModal) {
|
|
||||||
const index = this.modalNodes.indexOf(node);
|
|
||||||
if (index >= 0) {
|
|
||||||
this.modalNodes.splice(index, 1);
|
|
||||||
}
|
|
||||||
this.removeNodeEvent(node);
|
|
||||||
this.emitter.emit('modalNodesChange');
|
|
||||||
if (node.getVisible()) {
|
|
||||||
this.emitter.emit('visibleChange');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private addNodeEvent(node: INode) {
|
|
||||||
this.nodeRemoveEvents[node.id] =
|
|
||||||
node.onVisibleChange(() => {
|
|
||||||
this.emitter.emit('visibleChange');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeNodeEvent(node: INode) {
|
|
||||||
if (this.nodeRemoveEvents[node.id]) {
|
|
||||||
this.nodeRemoveEvents[node.id]();
|
|
||||||
delete this.nodeRemoveEvents[node.id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setNodes() {
|
|
||||||
const nodes = getModalNodes(this.page.rootNode!);
|
|
||||||
this.modalNodes = nodes;
|
|
||||||
this.modalNodes.forEach((node: INode) => {
|
|
||||||
this.addNodeEvent(node);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.emitter.emit('modalNodesChange');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,464 +0,0 @@
|
|||||||
import { observable, computed, makeObservable, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core';
|
|
||||||
import { Node, INode } from './node';
|
|
||||||
import { IPublicTypeNodeData, IPublicModelNodeChildren, IPublicEnumTransformStage, IPublicTypeDisposable } from '@alilc/lowcode-types';
|
|
||||||
import { shallowEqual, compatStage, isNodeSchema } from '@alilc/lowcode-utils';
|
|
||||||
import { foreachReverse } from '../../utils/tree';
|
|
||||||
import { NodeRemoveOptions } from '../../types';
|
|
||||||
|
|
||||||
export interface IOnChangeOptions {
|
|
||||||
type: string;
|
|
||||||
node: Node;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NodeChildren implements Omit<IPublicModelNodeChildren<INode>,
|
|
||||||
'importSchema' |
|
|
||||||
'exportSchema' |
|
|
||||||
'isEmpty' |
|
|
||||||
'notEmpty'
|
|
||||||
> {
|
|
||||||
@observable.shallow children: INode[];
|
|
||||||
|
|
||||||
private emitter: IEventBus = createModuleEventBus('NodeChildren');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 元素个数
|
|
||||||
*/
|
|
||||||
@computed get size(): number {
|
|
||||||
return this.children.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isEmptyNode(): boolean {
|
|
||||||
return this.size < 1;
|
|
||||||
}
|
|
||||||
get notEmptyNode(): boolean {
|
|
||||||
return this.size > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get length(): number {
|
|
||||||
return this.children.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private purged = false;
|
|
||||||
|
|
||||||
get [Symbol.toStringTag]() {
|
|
||||||
// 保证向前兼容性
|
|
||||||
return 'Array';
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly owner: INode,
|
|
||||||
data: IPublicTypeNodeData | IPublicTypeNodeData[],
|
|
||||||
) {
|
|
||||||
makeObservable(this);
|
|
||||||
this.children = (Array.isArray(data) ? data : [data]).filter(child => !!child).map((child) => {
|
|
||||||
return this.owner.document?.createNode(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
internalInitParent() {
|
|
||||||
this.children.forEach((child) => child.internalSetParent(this.owner));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出 schema
|
|
||||||
*/
|
|
||||||
export(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save): IPublicTypeNodeData[] {
|
|
||||||
stage = compatStage(stage);
|
|
||||||
return this.children.map((node) => {
|
|
||||||
const data = node.export(stage);
|
|
||||||
if (node.isLeafNode && IPublicEnumTransformStage.Save === stage) {
|
|
||||||
// FIXME: filter empty
|
|
||||||
return data.children as IPublicTypeNodeData;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
import(data?: IPublicTypeNodeData | IPublicTypeNodeData[], checkId = false) {
|
|
||||||
data = (data ? (Array.isArray(data) ? data : [data]) : []).filter(d => !!d);
|
|
||||||
|
|
||||||
const originChildren = this.children.slice();
|
|
||||||
this.children.forEach((child) => child.internalSetParent(null));
|
|
||||||
|
|
||||||
const children = new Array<Node>(data.length);
|
|
||||||
for (let i = 0, l = data.length; i < l; i++) {
|
|
||||||
const child = originChildren[i];
|
|
||||||
const item = data[i];
|
|
||||||
|
|
||||||
let node: INode | undefined | null;
|
|
||||||
if (isNodeSchema(item) && !checkId && child && child.componentName === item.componentName) {
|
|
||||||
node = child;
|
|
||||||
node.import(item);
|
|
||||||
} else {
|
|
||||||
node = this.owner.document?.createNode(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node) {
|
|
||||||
children[i] = node;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.children = children;
|
|
||||||
this.internalInitParent();
|
|
||||||
if (!shallowEqual(children, originChildren)) {
|
|
||||||
this.emitter.emit('change');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
isEmpty() {
|
|
||||||
return this.isEmptyNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
notEmpty() {
|
|
||||||
return this.notEmptyNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 回收销毁
|
|
||||||
*/
|
|
||||||
purge() {
|
|
||||||
if (this.purged) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.purged = true;
|
|
||||||
this.children.forEach((child) => {
|
|
||||||
child.purge();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
unlinkChild(node: INode) {
|
|
||||||
const i = this.children.map(d => d.id).indexOf(node.id);
|
|
||||||
if (i < 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.children.splice(i, 1);
|
|
||||||
this.emitter.emit('change', {
|
|
||||||
type: 'unlink',
|
|
||||||
node,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除一个节点
|
|
||||||
*/
|
|
||||||
delete(node: INode): boolean {
|
|
||||||
return this.internalDelete(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除一个节点
|
|
||||||
*/
|
|
||||||
internalDelete(node: INode, purge = false, useMutator = true, options: NodeRemoveOptions = {}): boolean {
|
|
||||||
node.internalPurgeStart();
|
|
||||||
if (node.isParentalNode) {
|
|
||||||
foreachReverse(
|
|
||||||
node.children!,
|
|
||||||
(subNode: Node) => {
|
|
||||||
subNode.remove(useMutator, purge, options);
|
|
||||||
},
|
|
||||||
(iterable, idx) => (iterable as NodeChildren).get(idx),
|
|
||||||
);
|
|
||||||
foreachReverse(
|
|
||||||
node.slots,
|
|
||||||
(slotNode: Node) => {
|
|
||||||
slotNode.remove(useMutator, purge);
|
|
||||||
},
|
|
||||||
(iterable, idx) => (iterable as [])[idx],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 需要在从 children 中删除 node 前记录下 index,internalSetParent 中会执行删除 (unlink) 操作
|
|
||||||
const i = this.children.map(d => d.id).indexOf(node.id);
|
|
||||||
if (purge) {
|
|
||||||
// should set parent null
|
|
||||||
node.internalSetParent(null, useMutator);
|
|
||||||
try {
|
|
||||||
node.purge();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { document } = node;
|
|
||||||
/* istanbul ignore next */
|
|
||||||
const editor = node.document?.designer.editor;
|
|
||||||
editor?.eventBus.emit('node.remove', { node, index: i });
|
|
||||||
document?.unlinkNode(node);
|
|
||||||
document?.selection.remove(node.id);
|
|
||||||
document?.destroyNode(node);
|
|
||||||
this.emitter.emit('change', {
|
|
||||||
type: 'delete',
|
|
||||||
node,
|
|
||||||
});
|
|
||||||
if (useMutator) {
|
|
||||||
this.reportModified(node, this.owner, {
|
|
||||||
type: 'remove',
|
|
||||||
propagated: false,
|
|
||||||
isSubDeleting: this.owner.isPurging,
|
|
||||||
removeIndex: i,
|
|
||||||
removeNode: node,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// purge 为 true 时,已在 internalSetParent 中删除了子节点
|
|
||||||
if (i > -1 && !purge) {
|
|
||||||
this.children.splice(i, 1);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
insert(node: INode, at?: number | null): void {
|
|
||||||
this.internalInsert(node, at, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插入一个节点,返回新长度
|
|
||||||
*/
|
|
||||||
internalInsert(node: INode, at?: number | null, useMutator = true): void {
|
|
||||||
const { children } = this;
|
|
||||||
let index = at == null || at === -1 ? children.length : at;
|
|
||||||
|
|
||||||
const i = children.map(d => d.id).indexOf(node.id);
|
|
||||||
|
|
||||||
if (node.parent) {
|
|
||||||
const editor = node.document?.designer.editor;
|
|
||||||
editor?.eventBus.emit('node.remove.topLevel', {
|
|
||||||
node,
|
|
||||||
index: node.index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i < 0) {
|
|
||||||
if (index < children.length) {
|
|
||||||
children.splice(index, 0, node);
|
|
||||||
} else {
|
|
||||||
children.push(node);
|
|
||||||
}
|
|
||||||
node.internalSetParent(this.owner, useMutator);
|
|
||||||
} else {
|
|
||||||
if (index > i) {
|
|
||||||
index -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index === i) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
children.splice(i, 1);
|
|
||||||
children.splice(index, 0, node);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitter.emit('change', {
|
|
||||||
type: 'insert',
|
|
||||||
node,
|
|
||||||
});
|
|
||||||
this.emitter.emit('insert', node);
|
|
||||||
/* istanbul ignore next */
|
|
||||||
const editor = node.document?.designer.editor;
|
|
||||||
editor?.eventBus.emit('node.add', { node });
|
|
||||||
if (useMutator) {
|
|
||||||
this.reportModified(node, this.owner, { type: 'insert' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// check condition group
|
|
||||||
if (node.conditionGroup) {
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
// just sort at condition group
|
|
||||||
(
|
|
||||||
(node.prevSibling && node.prevSibling.conditionGroup === node.conditionGroup) ||
|
|
||||||
(node.nextSibling && node.nextSibling.conditionGroup === node.conditionGroup)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
node.setConditionGroup(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (node.prevSibling && node.nextSibling) {
|
|
||||||
const { conditionGroup } = node.prevSibling;
|
|
||||||
// insert at condition group
|
|
||||||
if (conditionGroup && conditionGroup === node.nextSibling.conditionGroup) {
|
|
||||||
node.setConditionGroup(conditionGroup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取得节点索引编号
|
|
||||||
*/
|
|
||||||
indexOf(node: INode): number {
|
|
||||||
return this.children.map(d => d.id).indexOf(node.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
splice(start: number, deleteCount: number, node?: INode): INode[] {
|
|
||||||
if (node) {
|
|
||||||
return this.children.splice(start, deleteCount, node);
|
|
||||||
}
|
|
||||||
return this.children.splice(start, deleteCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据索引获得节点
|
|
||||||
*/
|
|
||||||
get(index: number): INode | null {
|
|
||||||
return this.children.length > index ? this.children[index] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否存在节点
|
|
||||||
*/
|
|
||||||
has(node: INode) {
|
|
||||||
return this.indexOf(node) > -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 迭代器
|
|
||||||
*/
|
|
||||||
[Symbol.iterator](): { next(): { value: INode } } {
|
|
||||||
let index = 0;
|
|
||||||
const { children } = this;
|
|
||||||
const length = children.length || 0;
|
|
||||||
return {
|
|
||||||
next() {
|
|
||||||
if (index < length) {
|
|
||||||
return {
|
|
||||||
value: children[index++],
|
|
||||||
done: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
value: undefined as any,
|
|
||||||
done: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 遍历
|
|
||||||
*/
|
|
||||||
forEach(fn: (item: INode, index: number) => void): void {
|
|
||||||
this.children.forEach((child, index) => {
|
|
||||||
return fn(child, index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 遍历
|
|
||||||
*/
|
|
||||||
map<T>(fn: (item: INode, index: number) => T): T[] | null {
|
|
||||||
return this.children.map((child, index) => {
|
|
||||||
return fn(child, index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
every(fn: (item: INode, index: number) => any): boolean {
|
|
||||||
return this.children.every((child, index) => fn(child, index));
|
|
||||||
}
|
|
||||||
|
|
||||||
some(fn: (item: INode, index: number) => any): boolean {
|
|
||||||
return this.children.some((child, index) => fn(child, index));
|
|
||||||
}
|
|
||||||
|
|
||||||
filter(fn: (item: INode, index: number) => any): any {
|
|
||||||
return this.children.filter(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
find(fn: (item: INode, index: number) => boolean): INode | undefined {
|
|
||||||
return this.children.find(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
reduce(fn: (acc: any, cur: INode) => any, initialValue: any): void {
|
|
||||||
return this.children.reduce(fn, initialValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
reverse() {
|
|
||||||
return this.children.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeChildren(
|
|
||||||
remover: (node: INode, idx: number) => boolean,
|
|
||||||
adder: (children: INode[]) => IPublicTypeNodeData[] | null,
|
|
||||||
sorter: (firstNode: INode, secondNode: INode) => number,
|
|
||||||
): any {
|
|
||||||
let changed = false;
|
|
||||||
if (remover) {
|
|
||||||
const willRemove = this.children.filter(remover);
|
|
||||||
if (willRemove.length > 0) {
|
|
||||||
willRemove.forEach((node) => {
|
|
||||||
const i = this.children.map(d => d.id).indexOf(node.id);
|
|
||||||
if (i > -1) {
|
|
||||||
this.children.splice(i, 1);
|
|
||||||
node.remove(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (adder) {
|
|
||||||
const items = adder(this.children);
|
|
||||||
if (items && items.length > 0) {
|
|
||||||
items.forEach((child: IPublicTypeNodeData) => {
|
|
||||||
const node: INode | null = this.owner.document?.createNode(child);
|
|
||||||
node && this.children.push(node);
|
|
||||||
node?.internalSetParent(this.owner);
|
|
||||||
/* istanbul ignore next */
|
|
||||||
const editor = node?.document?.designer.editor;
|
|
||||||
editor?.eventBus.emit('node.add', { node });
|
|
||||||
});
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sorter) {
|
|
||||||
this.children = this.children.sort(sorter);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (changed) {
|
|
||||||
this.emitter.emit('change');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(fn: (info?: IOnChangeOptions) => void): IPublicTypeDisposable {
|
|
||||||
this.emitter.on('change', fn);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('change', fn);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onInsert(fn: (node: INode) => void) {
|
|
||||||
this.emitter.on('insert', fn);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('insert', fn);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private reportModified(node: INode, owner: INode, options = {}) {
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (node.isRootNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const callbacks = owner.componentMeta?.advanced.callbacks;
|
|
||||||
if (callbacks?.onSubtreeModified) {
|
|
||||||
try {
|
|
||||||
callbacks?.onSubtreeModified.call(
|
|
||||||
node.internalToShellNode(),
|
|
||||||
owner.internalToShellNode()!,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('error when execute advanced.callbacks.onSubtreeModified', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (owner.parent && !owner.parent.isRootNode) {
|
|
||||||
this.reportModified(node, owner.parent, { ...options, propagated: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface INodeChildren extends NodeChildren {}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,785 +0,0 @@
|
|||||||
import { untracked, computed, observable, engineConfig, action, makeObservable, mobx, runInAction } from '@alilc/lowcode-editor-core';
|
|
||||||
import { GlobalEvent, IPublicEnumTransformStage } from '@alilc/lowcode-types';
|
|
||||||
import type { IPublicTypeCompositeValue, IPublicTypeJSSlot, IPublicTypeSlotSchema, IPublicModelProp, IPublicTypeNodeData } from '@alilc/lowcode-types';
|
|
||||||
import { uniqueId, isPlainObject, hasOwnProperty, compatStage, isJSExpression, isJSSlot, isNodeSchema } from '@alilc/lowcode-utils';
|
|
||||||
import { valueToSource } from './value-to-source';
|
|
||||||
import { IPropParent } from './props';
|
|
||||||
import type { IProps } from './props';
|
|
||||||
import { ISlotNode, INode } from '../node';
|
|
||||||
|
|
||||||
const { set: mobxSet, isObservableArray } = mobx;
|
|
||||||
export const UNSET = Symbol.for('unset');
|
|
||||||
// eslint-disable-next-line no-redeclare
|
|
||||||
export type UNSET = typeof UNSET;
|
|
||||||
|
|
||||||
export interface IProp extends Omit<IPublicModelProp<
|
|
||||||
INode
|
|
||||||
>, 'exportSchema' | 'node'>, IPropParent {
|
|
||||||
spread: boolean;
|
|
||||||
|
|
||||||
key: string | number | undefined;
|
|
||||||
|
|
||||||
readonly props: IProps;
|
|
||||||
|
|
||||||
readonly owner: INode;
|
|
||||||
|
|
||||||
delete(prop: IProp): void;
|
|
||||||
|
|
||||||
export(stage: IPublicEnumTransformStage): IPublicTypeCompositeValue;
|
|
||||||
|
|
||||||
getNode(): INode;
|
|
||||||
|
|
||||||
getAsString(): string;
|
|
||||||
|
|
||||||
unset(): void;
|
|
||||||
|
|
||||||
get value(): IPublicTypeCompositeValue | UNSET;
|
|
||||||
|
|
||||||
compare(other: IProp | null): number;
|
|
||||||
|
|
||||||
isUnset(): boolean;
|
|
||||||
|
|
||||||
purge(): void;
|
|
||||||
|
|
||||||
setupItems(): IProp[] | null;
|
|
||||||
|
|
||||||
isVirtual(): boolean;
|
|
||||||
|
|
||||||
get type(): ValueTypes;
|
|
||||||
|
|
||||||
get size(): number;
|
|
||||||
|
|
||||||
get code(): string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ValueTypes = 'unset' | 'literal' | 'map' | 'list' | 'expression' | 'slot';
|
|
||||||
|
|
||||||
export class Prop implements IProp, IPropParent {
|
|
||||||
readonly isProp = true;
|
|
||||||
|
|
||||||
readonly owner: INode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 键值
|
|
||||||
*/
|
|
||||||
@observable key: string | number | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扩展值
|
|
||||||
*/
|
|
||||||
@observable spread: boolean;
|
|
||||||
|
|
||||||
readonly props: IProps;
|
|
||||||
|
|
||||||
readonly options: any;
|
|
||||||
|
|
||||||
readonly id = uniqueId('prop$');
|
|
||||||
|
|
||||||
@observable.ref private _type: ValueTypes = 'unset';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 属性类型
|
|
||||||
*/
|
|
||||||
get type(): ValueTypes {
|
|
||||||
return this._type;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable private _value: any = UNSET;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 属性值
|
|
||||||
*/
|
|
||||||
@computed get value(): IPublicTypeCompositeValue | UNSET {
|
|
||||||
return this.export(IPublicEnumTransformStage.Serilize);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _code: string | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获得表达式值
|
|
||||||
*/
|
|
||||||
@computed get code() {
|
|
||||||
if (isJSExpression(this.value)) {
|
|
||||||
return this.value.value;
|
|
||||||
}
|
|
||||||
// todo: JSFunction ...
|
|
||||||
if (this.type === 'slot') {
|
|
||||||
return JSON.stringify(this._slotNode!.export(IPublicEnumTransformStage.Save));
|
|
||||||
}
|
|
||||||
return this._code != null ? this._code : JSON.stringify(this.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置表达式值
|
|
||||||
*/
|
|
||||||
set code(code: string) {
|
|
||||||
if (isJSExpression(this._value)) {
|
|
||||||
this.setValue({
|
|
||||||
...this._value,
|
|
||||||
value: code,
|
|
||||||
});
|
|
||||||
this._code = code;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const v = JSON.parse(code);
|
|
||||||
this.setValue(v);
|
|
||||||
this._code = code;
|
|
||||||
return;
|
|
||||||
} catch (e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setValue({
|
|
||||||
type: 'JSExpression',
|
|
||||||
value: code,
|
|
||||||
mock: this._value,
|
|
||||||
});
|
|
||||||
this._code = code;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _slotNode?: INode | null;
|
|
||||||
|
|
||||||
get slotNode(): INode | null {
|
|
||||||
return this._slotNode || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable.shallow private _items: IProp[] | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 作为一层缓存机制,主要是复用部分已存在的 Prop,保持响应式关系,比如:
|
|
||||||
* 当前 Prop#_value 值为 { a: 1 },当调用 setValue({ a: 2 }) 时,所有原来的子 Prop 均被销毁,
|
|
||||||
* 导致假如外部有 mobx reaction(常见于 observer),此时响应式链路会被打断,
|
|
||||||
* 因为 reaction 监听的是原 Prop(a) 的 _value,而不是新 Prop(a) 的 _value。
|
|
||||||
*/
|
|
||||||
@observable.shallow private _maps: Map<string | number, IProp> | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造 items 属性,同时构造 maps 属性
|
|
||||||
*/
|
|
||||||
private get items(): IProp[] | null {
|
|
||||||
if (this._items) return this._items;
|
|
||||||
return runInAction(() => {
|
|
||||||
let items: IProp[] | null = null;
|
|
||||||
if (this._type === 'list') {
|
|
||||||
const maps = new Map<string, IProp>();
|
|
||||||
const data = this._value;
|
|
||||||
data.forEach((item: any, idx: number) => {
|
|
||||||
items = items || [];
|
|
||||||
let prop;
|
|
||||||
if (this._maps?.has(idx.toString())) {
|
|
||||||
prop = this._maps.get(idx.toString())!;
|
|
||||||
prop.setValue(item);
|
|
||||||
} else {
|
|
||||||
prop = new Prop(this, item, idx);
|
|
||||||
}
|
|
||||||
maps.set(idx.toString(), prop);
|
|
||||||
items.push(prop);
|
|
||||||
});
|
|
||||||
this._maps = maps;
|
|
||||||
} else if (this._type === 'map') {
|
|
||||||
const data = this._value;
|
|
||||||
const maps = new Map<string, IProp>();
|
|
||||||
const keys = Object.keys(data);
|
|
||||||
for (const key of keys) {
|
|
||||||
let prop: IProp;
|
|
||||||
if (this._maps?.has(key)) {
|
|
||||||
prop = this._maps.get(key)!;
|
|
||||||
prop.setValue(data[key]);
|
|
||||||
} else {
|
|
||||||
prop = new Prop(this, data[key], key);
|
|
||||||
}
|
|
||||||
items = items || [];
|
|
||||||
items.push(prop);
|
|
||||||
maps.set(key, prop);
|
|
||||||
}
|
|
||||||
this._maps = maps;
|
|
||||||
} else {
|
|
||||||
items = null;
|
|
||||||
this._maps = null;
|
|
||||||
}
|
|
||||||
this._items = items;
|
|
||||||
return this._items;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed private get maps(): Map<string | number, IProp> | null {
|
|
||||||
if (!this.items) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return this._maps;
|
|
||||||
}
|
|
||||||
|
|
||||||
get path(): string[] {
|
|
||||||
return (this.parent.path || []).concat(this.key as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 元素个数
|
|
||||||
*/
|
|
||||||
get size(): number {
|
|
||||||
return this.items?.length || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private purged = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public parent: IPropParent,
|
|
||||||
value: IPublicTypeCompositeValue | IPublicTypeNodeData | IPublicTypeNodeData[] | UNSET = UNSET,
|
|
||||||
key?: string | number,
|
|
||||||
spread = false,
|
|
||||||
options = {},
|
|
||||||
) {
|
|
||||||
makeObservable(this);
|
|
||||||
this.owner = parent.owner;
|
|
||||||
this.props = parent.props;
|
|
||||||
this.key = key;
|
|
||||||
this.spread = spread;
|
|
||||||
this.options = options;
|
|
||||||
if (value !== UNSET) {
|
|
||||||
this.setValue(value);
|
|
||||||
}
|
|
||||||
this.setupItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 先用调用方式触发子 prop 的初始化,后续须重构
|
|
||||||
@action
|
|
||||||
setupItems() {
|
|
||||||
return this.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see SettingTarget
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
getPropValue(propName: string | number): any {
|
|
||||||
return this.get(propName)!.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see SettingTarget
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
setPropValue(propName: string | number, value: any): void {
|
|
||||||
this.set(propName, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see SettingTarget
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
clearPropValue(propName: string | number): void {
|
|
||||||
this.get(propName, false)?.unset();
|
|
||||||
}
|
|
||||||
|
|
||||||
export(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save): IPublicTypeCompositeValue {
|
|
||||||
stage = compatStage(stage);
|
|
||||||
const type = this._type;
|
|
||||||
if (stage === IPublicEnumTransformStage.Render && this.key === '___condition___') {
|
|
||||||
// 在设计器里,所有组件默认需要展示,除非开启了 enableCondition 配置
|
|
||||||
if (engineConfig?.get('enableCondition') !== true) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return this._value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'unset') {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'literal' || type === 'expression') {
|
|
||||||
return this._value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'slot') {
|
|
||||||
const schema = this._slotNode?.export(stage) || {} as any;
|
|
||||||
if (stage === IPublicEnumTransformStage.Render) {
|
|
||||||
return {
|
|
||||||
type: 'JSSlot',
|
|
||||||
params: schema.params,
|
|
||||||
value: schema,
|
|
||||||
id: schema.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'JSSlot',
|
|
||||||
params: schema.params,
|
|
||||||
value: schema.children,
|
|
||||||
title: schema.title,
|
|
||||||
name: schema.name,
|
|
||||||
id: schema.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'map') {
|
|
||||||
if (!this._items) {
|
|
||||||
return this._value;
|
|
||||||
}
|
|
||||||
let maps: any;
|
|
||||||
this.items!.forEach((prop, key) => {
|
|
||||||
if (!prop.isUnset()) {
|
|
||||||
const v = prop.export(stage);
|
|
||||||
if (v != null) {
|
|
||||||
maps = maps || {};
|
|
||||||
maps[prop.key || key] = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return maps;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'list') {
|
|
||||||
if (!this._items) {
|
|
||||||
return this._value;
|
|
||||||
}
|
|
||||||
return this.items!.map((prop) => {
|
|
||||||
return prop?.export(stage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getAsString(): string {
|
|
||||||
if (this.type === 'literal') {
|
|
||||||
return this._value ? String(this._value) : '';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set value, val should be JSON Object
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
setValue(val: IPublicTypeCompositeValue | IPublicTypeNodeData | IPublicTypeNodeData[]) {
|
|
||||||
if (val === this._value) return;
|
|
||||||
const oldValue = this._value;
|
|
||||||
this._value = val;
|
|
||||||
this._code = null;
|
|
||||||
const t = typeof val;
|
|
||||||
if (val == null) {
|
|
||||||
// this._value = undefined;
|
|
||||||
this._type = 'literal';
|
|
||||||
} else if (t === 'string' || t === 'number' || t === 'boolean') {
|
|
||||||
this._type = 'literal';
|
|
||||||
} else if (Array.isArray(val)) {
|
|
||||||
this._type = 'list';
|
|
||||||
} else if (isPlainObject(val)) {
|
|
||||||
if (isJSSlot(val)) {
|
|
||||||
this.setAsSlot(val);
|
|
||||||
} else if (isJSExpression(val)) {
|
|
||||||
this._type = 'expression';
|
|
||||||
} else {
|
|
||||||
this._type = 'map';
|
|
||||||
}
|
|
||||||
} else /* istanbul ignore next */ {
|
|
||||||
this._type = 'expression';
|
|
||||||
this._value = {
|
|
||||||
type: 'JSExpression',
|
|
||||||
value: valueToSource(val),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dispose();
|
|
||||||
// setValue 的时候,如果不重新建立 items,items 的 setValue 没有触发,会导致子项的响应式逻辑不能被触发
|
|
||||||
this.setupItems();
|
|
||||||
|
|
||||||
if (oldValue !== this._value) {
|
|
||||||
this.emitChange({ oldValue });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emitChange = ({
|
|
||||||
oldValue,
|
|
||||||
}: {
|
|
||||||
oldValue: IPublicTypeCompositeValue | UNSET;
|
|
||||||
}) => {
|
|
||||||
const editor = this.owner.document?.designer.editor;
|
|
||||||
const propsInfo = {
|
|
||||||
key: this.key,
|
|
||||||
prop: this,
|
|
||||||
oldValue,
|
|
||||||
newValue: this.type === 'unset' ? undefined : this._value,
|
|
||||||
};
|
|
||||||
|
|
||||||
editor?.eventBus.emit(GlobalEvent.Node.Prop.InnerChange, {
|
|
||||||
node: this.owner as any,
|
|
||||||
...propsInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.owner?.emitPropChange?.(propsInfo);
|
|
||||||
};
|
|
||||||
|
|
||||||
getValue(): IPublicTypeCompositeValue {
|
|
||||||
return this.export(IPublicEnumTransformStage.Serilize);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
private dispose() {
|
|
||||||
const items = untracked(() => this._items);
|
|
||||||
if (items) {
|
|
||||||
items.forEach((prop) => prop.purge());
|
|
||||||
}
|
|
||||||
this._items = null;
|
|
||||||
if (this._type !== 'slot' && this._slotNode) {
|
|
||||||
this._slotNode.remove();
|
|
||||||
this._slotNode = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setAsSlot(data: IPublicTypeJSSlot) {
|
|
||||||
this._type = 'slot';
|
|
||||||
let slotSchema: IPublicTypeSlotSchema;
|
|
||||||
// 当 data.value 的结构为 { componentName: 'Slot' } 时,复用部分 slotSchema 数据
|
|
||||||
if ((isPlainObject(data.value) && isNodeSchema(data.value) && data.value?.componentName === 'Slot')) {
|
|
||||||
const value = data.value as IPublicTypeSlotSchema;
|
|
||||||
slotSchema = {
|
|
||||||
componentName: 'Slot',
|
|
||||||
title: value.title || value.props?.slotTitle,
|
|
||||||
id: value.id,
|
|
||||||
name: value.name || value.props?.slotName,
|
|
||||||
params: value.params || value.props?.slotParams,
|
|
||||||
children: value.children,
|
|
||||||
} as IPublicTypeSlotSchema;
|
|
||||||
} else {
|
|
||||||
slotSchema = {
|
|
||||||
componentName: 'Slot',
|
|
||||||
title: data.title,
|
|
||||||
id: data.id,
|
|
||||||
name: data.name,
|
|
||||||
params: data.params,
|
|
||||||
children: data.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._slotNode) {
|
|
||||||
this._slotNode.import(slotSchema);
|
|
||||||
} else {
|
|
||||||
const { owner } = this.props;
|
|
||||||
this._slotNode = owner.document?.createNode<ISlotNode>(slotSchema);
|
|
||||||
if (this._slotNode) {
|
|
||||||
owner.addSlot(this._slotNode);
|
|
||||||
this._slotNode.internalSetSlotFor(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消设置值
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
unset() {
|
|
||||||
if (this._type !== 'unset') {
|
|
||||||
this._type = 'unset';
|
|
||||||
this.emitChange({
|
|
||||||
oldValue: this._value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否未设置值
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
isUnset() {
|
|
||||||
return this._type === 'unset';
|
|
||||||
}
|
|
||||||
|
|
||||||
isVirtual() {
|
|
||||||
return typeof this.key === 'string' && this.key.charAt(0) === '!';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns 0: the same 1: maybe & like 2: not the same
|
|
||||||
*/
|
|
||||||
compare(other: IProp | null): number {
|
|
||||||
if (!other || other.isUnset()) {
|
|
||||||
return this.isUnset() ? 0 : 2;
|
|
||||||
}
|
|
||||||
if (other.type !== this.type) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
// list
|
|
||||||
if (this.type === 'list') {
|
|
||||||
return this.size === other.size ? 1 : 2;
|
|
||||||
}
|
|
||||||
if (this.type === 'map') {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 'literal' | 'map' | 'expression' | 'slot'
|
|
||||||
return this.code === other.code ? 0 : 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取某个属性
|
|
||||||
* @param createIfNone 当没有的时候,是否创建一个
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
get(path: string | number, createIfNone = true): IProp | null {
|
|
||||||
const type = this._type;
|
|
||||||
if (type !== 'map' && type !== 'list' && type !== 'unset' && !createIfNone) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maps = type === 'map' ? this.maps : null;
|
|
||||||
const items = type === 'list' ? this.items : null;
|
|
||||||
|
|
||||||
let entry = path;
|
|
||||||
let nest = '';
|
|
||||||
if (typeof path !== 'number') {
|
|
||||||
const i = path.indexOf('.');
|
|
||||||
if (i > 0) {
|
|
||||||
nest = path.slice(i + 1);
|
|
||||||
if (nest) {
|
|
||||||
entry = path.slice(0, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let prop: any;
|
|
||||||
if (type === 'list') {
|
|
||||||
if (isValidArrayIndex(entry, this.size)) {
|
|
||||||
prop = items![entry];
|
|
||||||
}
|
|
||||||
} else if (type === 'map') {
|
|
||||||
prop = maps?.get(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop) {
|
|
||||||
return nest ? prop.get(nest, createIfNone) : prop;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (createIfNone) {
|
|
||||||
prop = new Prop(this, UNSET, entry);
|
|
||||||
this.set(entry, prop, true);
|
|
||||||
if (nest) {
|
|
||||||
return prop.get(nest, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return prop;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从父级移除本身
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
remove() {
|
|
||||||
this.parent.delete(this);
|
|
||||||
this.unset();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除项
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
delete(prop: IProp): void {
|
|
||||||
/* istanbul ignore else */
|
|
||||||
if (this._items) {
|
|
||||||
const i = this._items.indexOf(prop);
|
|
||||||
if (i > -1) {
|
|
||||||
this._items.splice(i, 1);
|
|
||||||
prop.purge();
|
|
||||||
}
|
|
||||||
if (this._maps && prop.key) {
|
|
||||||
this._maps.delete(String(prop.key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除 key
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
deleteKey(key: string): void {
|
|
||||||
/* istanbul ignore else */
|
|
||||||
if (this.maps) {
|
|
||||||
const prop = this.maps.get(key);
|
|
||||||
if (prop) {
|
|
||||||
this.delete(prop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加值到列表
|
|
||||||
*
|
|
||||||
* @param force 强制
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
add(value: IPublicTypeCompositeValue, force = false): IProp | null {
|
|
||||||
const type = this._type;
|
|
||||||
if (type !== 'list' && type !== 'unset' && !force) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (type === 'unset' || (force && type !== 'list')) {
|
|
||||||
this.setValue([]);
|
|
||||||
}
|
|
||||||
const prop = new Prop(this, value);
|
|
||||||
this._items = this._items || [];
|
|
||||||
this._items.push(prop);
|
|
||||||
return prop;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置值到字典
|
|
||||||
*
|
|
||||||
* @param force 强制
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
set(key: string | number, value: IPublicTypeCompositeValue | Prop, force = false) {
|
|
||||||
const type = this._type;
|
|
||||||
if (type !== 'map' && type !== 'list' && type !== 'unset' && !force) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (type === 'unset' || (force && type !== 'map')) {
|
|
||||||
if (isValidArrayIndex(key)) {
|
|
||||||
if (type !== 'list') {
|
|
||||||
this.setValue([]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.setValue({});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const prop = isProp(value) ? value : new Prop(this, value, key);
|
|
||||||
const items = this._items! || [];
|
|
||||||
if (this.type === 'list') {
|
|
||||||
if (!isValidArrayIndex(key)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (isObservableArray(items)) {
|
|
||||||
mobxSet(items, key, prop);
|
|
||||||
} else {
|
|
||||||
items[key] = prop;
|
|
||||||
}
|
|
||||||
this._items = items;
|
|
||||||
} else if (this.type === 'map') {
|
|
||||||
const maps = this._maps || new Map<string, Prop>();
|
|
||||||
const orig = maps?.get(key);
|
|
||||||
if (orig) {
|
|
||||||
// replace
|
|
||||||
const i = items.indexOf(orig);
|
|
||||||
if (i > -1) {
|
|
||||||
items.splice(i, 1, prop)[0].purge();
|
|
||||||
}
|
|
||||||
maps?.set(key, prop);
|
|
||||||
} else {
|
|
||||||
// push
|
|
||||||
items.push(prop);
|
|
||||||
this._items = items;
|
|
||||||
maps?.set(key, prop);
|
|
||||||
}
|
|
||||||
this._maps = maps;
|
|
||||||
} /* istanbul ignore next */ else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return prop;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否存在 key
|
|
||||||
*/
|
|
||||||
has(key: string): boolean {
|
|
||||||
if (this._type !== 'map') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (this._maps) {
|
|
||||||
return this._maps.has(key);
|
|
||||||
}
|
|
||||||
return hasOwnProperty(this._value, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 回收销毁
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
purge() {
|
|
||||||
if (this.purged) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.purged = true;
|
|
||||||
if (this._items) {
|
|
||||||
this._items.forEach((item) => item.purge());
|
|
||||||
}
|
|
||||||
this._items = null;
|
|
||||||
this._maps = null;
|
|
||||||
if (this._slotNode && this._slotNode.slotFor === this) {
|
|
||||||
this._slotNode.remove();
|
|
||||||
this._slotNode = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 迭代器
|
|
||||||
*/
|
|
||||||
[Symbol.iterator](): { next(): { value: IProp } } {
|
|
||||||
let index = 0;
|
|
||||||
const { items } = this;
|
|
||||||
const length = items?.length || 0;
|
|
||||||
return {
|
|
||||||
next() {
|
|
||||||
if (index < length) {
|
|
||||||
return {
|
|
||||||
value: items![index++],
|
|
||||||
done: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
value: undefined as any,
|
|
||||||
done: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 遍历
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
forEach(fn: (item: IProp, key: number | string | undefined) => void): void {
|
|
||||||
const { items } = this;
|
|
||||||
if (!items) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const isMap = this._type === 'map';
|
|
||||||
items.forEach((item, index) => {
|
|
||||||
return isMap ? fn(item, item.key) : fn(item, index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 遍历
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
map<T>(fn: (item: IProp, key: number | string | undefined) => T): T[] | null {
|
|
||||||
const { items } = this;
|
|
||||||
if (!items) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const isMap = this._type === 'map';
|
|
||||||
return items.map((item, index) => {
|
|
||||||
return isMap ? fn(item, item.key) : fn(item, index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getProps() {
|
|
||||||
return this.props;
|
|
||||||
}
|
|
||||||
|
|
||||||
getNode() {
|
|
||||||
return this.owner;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isProp(obj: any): obj is Prop {
|
|
||||||
return obj && obj.isProp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidArrayIndex(key: any, limit = -1): key is number {
|
|
||||||
const n = parseFloat(String(key));
|
|
||||||
return n >= 0 && Math.floor(n) === n && isFinite(n) && (limit < 0 || n < limit);
|
|
||||||
}
|
|
||||||
@ -1,368 +0,0 @@
|
|||||||
import { computed, makeObservable, observable, action } from '@alilc/lowcode-editor-core';
|
|
||||||
import { IPublicTypePropsList, IPublicTypeCompositeValue, IPublicEnumTransformStage, IBaseModelProps } from '@alilc/lowcode-types';
|
|
||||||
import type { IPublicTypePropsMap } from '@alilc/lowcode-types';
|
|
||||||
import { uniqueId, compatStage } from '@alilc/lowcode-utils';
|
|
||||||
import { Prop, UNSET } from './prop';
|
|
||||||
import type { IProp } from './prop';
|
|
||||||
import { INode } from '../node';
|
|
||||||
|
|
||||||
interface ExtrasObject {
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EXTRA_KEY_PREFIX = '___';
|
|
||||||
export function getConvertedExtraKey(key: string): string {
|
|
||||||
if (!key) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
let _key = key;
|
|
||||||
if (key.indexOf('.') > 0) {
|
|
||||||
_key = key.split('.')[0];
|
|
||||||
}
|
|
||||||
return EXTRA_KEY_PREFIX + _key + EXTRA_KEY_PREFIX + key.slice(_key.length);
|
|
||||||
}
|
|
||||||
export function getOriginalExtraKey(key: string): string {
|
|
||||||
return key.replace(new RegExp(`${EXTRA_KEY_PREFIX}`, 'g'), '');
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPropParent {
|
|
||||||
|
|
||||||
readonly props: IProps;
|
|
||||||
|
|
||||||
readonly owner: INode;
|
|
||||||
|
|
||||||
get path(): string[];
|
|
||||||
|
|
||||||
delete(prop: IProp): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IProps extends Props {}
|
|
||||||
|
|
||||||
export class Props implements Omit<IBaseModelProps<IProp>, | 'getExtraProp' | 'getExtraPropValue' | 'setExtraPropValue' | 'node'>, IPropParent {
|
|
||||||
readonly id = uniqueId('props');
|
|
||||||
|
|
||||||
@observable.shallow private items: IProp[] = [];
|
|
||||||
|
|
||||||
@computed private get maps(): Map<string, Prop> {
|
|
||||||
const maps = new Map();
|
|
||||||
if (this.items.length > 0) {
|
|
||||||
this.items.forEach((prop) => {
|
|
||||||
if (prop.key) {
|
|
||||||
maps.set(prop.key, prop);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return maps;
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly path = [];
|
|
||||||
|
|
||||||
get props(): IProps {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly owner: INode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 元素个数
|
|
||||||
*/
|
|
||||||
@computed get size() {
|
|
||||||
return this.items.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observable type: 'map' | 'list' = 'map';
|
|
||||||
|
|
||||||
private purged = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
owner: INode,
|
|
||||||
value?: IPublicTypePropsMap | IPublicTypePropsList | null,
|
|
||||||
extras?: ExtrasObject
|
|
||||||
) {
|
|
||||||
makeObservable(this);
|
|
||||||
this.owner = owner;
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
this.type = 'list';
|
|
||||||
this.items = value.map(
|
|
||||||
(item, idx) => new Prop(this, item.value, item.name || idx, item.spread),
|
|
||||||
);
|
|
||||||
} else if (value != null) {
|
|
||||||
this.items = Object.keys(value).map((key) => new Prop(this, value[key], key, false));
|
|
||||||
}
|
|
||||||
if (extras) {
|
|
||||||
Object.keys(extras).forEach((key) => {
|
|
||||||
this.items.push(new Prop(this, (extras as any)[key], getConvertedExtraKey(key)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
import(value?: IPublicTypePropsMap | IPublicTypePropsList | null, extras?: ExtrasObject) {
|
|
||||||
const originItems = this.items;
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
this.type = 'list';
|
|
||||||
this.items = value.map(
|
|
||||||
(item, idx) => new Prop(this, item.value, item.name || idx, item.spread),
|
|
||||||
);
|
|
||||||
} else if (value != null) {
|
|
||||||
this.type = 'map';
|
|
||||||
this.items = Object.keys(value).map((key) => new Prop(this, value[key], key));
|
|
||||||
} else {
|
|
||||||
this.type = 'map';
|
|
||||||
this.items = [];
|
|
||||||
}
|
|
||||||
if (extras) {
|
|
||||||
Object.keys(extras).forEach((key) => {
|
|
||||||
this.items.push(new Prop(this, (extras as any)[key], getConvertedExtraKey(key)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
originItems.forEach((item) => item.purge());
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
merge(value: IPublicTypePropsMap, extras?: IPublicTypePropsMap) {
|
|
||||||
Object.keys(value).forEach((key) => {
|
|
||||||
this.query(key, true)!.setValue(value[key]);
|
|
||||||
this.query(key, true)!.setupItems();
|
|
||||||
});
|
|
||||||
if (extras) {
|
|
||||||
Object.keys(extras).forEach((key) => {
|
|
||||||
this.query(getConvertedExtraKey(key), true)!.setValue(extras[key]);
|
|
||||||
this.query(getConvertedExtraKey(key), true)!.setupItems();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save): {
|
|
||||||
props?: IPublicTypePropsMap | IPublicTypePropsList;
|
|
||||||
extras?: ExtrasObject;
|
|
||||||
} {
|
|
||||||
stage = compatStage(stage);
|
|
||||||
if (this.items.length < 1) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const allProps = {} as any;
|
|
||||||
let props: any = {};
|
|
||||||
const extras: any = {};
|
|
||||||
if (this.type === 'list') {
|
|
||||||
props = [];
|
|
||||||
this.items.forEach((item) => {
|
|
||||||
const value = item.export(stage);
|
|
||||||
let name = item.key as string;
|
|
||||||
if (name && typeof name === 'string' && name.startsWith(EXTRA_KEY_PREFIX)) {
|
|
||||||
name = getOriginalExtraKey(name);
|
|
||||||
extras[name] = value;
|
|
||||||
} else {
|
|
||||||
props.push({
|
|
||||||
spread: item.spread,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.items.forEach((item) => {
|
|
||||||
const name = item.key as string;
|
|
||||||
if (name == null || item.isUnset() || item.isVirtual()) return;
|
|
||||||
const value = item.export(stage);
|
|
||||||
if (value != null) {
|
|
||||||
allProps[name] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(allProps).forEach((name) => {
|
|
||||||
const value = allProps[name];
|
|
||||||
if (typeof name === 'string' && name.startsWith(EXTRA_KEY_PREFIX)) {
|
|
||||||
name = getOriginalExtraKey(name);
|
|
||||||
extras[name] = value;
|
|
||||||
} else {
|
|
||||||
props[name] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { props, extras };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据 path 路径查询属性
|
|
||||||
*
|
|
||||||
* @param createIfNone 当没有的时候,是否创建一个
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
query(path: string, createIfNone = true): IProp | null {
|
|
||||||
return this.get(path, createIfNone);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取某个属性,如果不存在,临时获取一个待写入
|
|
||||||
* @param createIfNone 当没有的时候,是否创建一个
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
get(path: string, createIfNone = false): IProp | null {
|
|
||||||
let entry = path;
|
|
||||||
let nest = '';
|
|
||||||
const i = path.indexOf('.');
|
|
||||||
if (i > 0) {
|
|
||||||
nest = path.slice(i + 1);
|
|
||||||
if (nest) {
|
|
||||||
entry = path.slice(0, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let prop = this.maps.get(entry);
|
|
||||||
if (!prop && createIfNone) {
|
|
||||||
prop = new Prop(this, UNSET, entry);
|
|
||||||
this.items.push(prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop) {
|
|
||||||
return nest ? prop.get(nest, createIfNone) : prop;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除项
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
delete(prop: IProp): void {
|
|
||||||
const i = this.items.indexOf(prop);
|
|
||||||
if (i > -1) {
|
|
||||||
this.items.splice(i, 1);
|
|
||||||
prop.purge();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除 key
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
deleteKey(key: string): void {
|
|
||||||
this.items = this.items.filter((item, i) => {
|
|
||||||
if (item.key === key) {
|
|
||||||
item.purge();
|
|
||||||
this.items.splice(i, 1);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加值
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
add(
|
|
||||||
value: IPublicTypeCompositeValue | null,
|
|
||||||
key?: string | number,
|
|
||||||
spread = false,
|
|
||||||
options: any = {},
|
|
||||||
): IProp {
|
|
||||||
const prop = new Prop(this, value, key, spread, options);
|
|
||||||
this.items.push(prop);
|
|
||||||
return prop;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否存在 key
|
|
||||||
*/
|
|
||||||
has(key: string): boolean {
|
|
||||||
return this.maps.has(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 迭代器
|
|
||||||
*/
|
|
||||||
[Symbol.iterator](): { next(): { value: IProp } } {
|
|
||||||
let index = 0;
|
|
||||||
const { items } = this;
|
|
||||||
const length = items.length || 0;
|
|
||||||
return {
|
|
||||||
next() {
|
|
||||||
if (index < length) {
|
|
||||||
return {
|
|
||||||
value: items[index++],
|
|
||||||
done: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
value: undefined as any,
|
|
||||||
done: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 遍历
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
forEach(fn: (item: IProp, key: number | string | undefined) => void): void {
|
|
||||||
this.items.forEach((item) => {
|
|
||||||
return fn(item, item.key);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 遍历
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
map<T>(fn: (item: IProp, key: number | string | undefined) => T): T[] | null {
|
|
||||||
return this.items.map((item) => {
|
|
||||||
return fn(item, item.key);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
filter(fn: (item: IProp, key: number | string | undefined) => boolean) {
|
|
||||||
return this.items.filter((item) => {
|
|
||||||
return fn(item, item.key);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 回收销毁
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
purge() {
|
|
||||||
if (this.purged) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.purged = true;
|
|
||||||
this.items.forEach((item) => item.purge());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取某个属性, 如果不存在,临时获取一个待写入
|
|
||||||
* @param createIfNone 当没有的时候,是否创建一个
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
getProp(path: string, createIfNone = true): IProp | null {
|
|
||||||
return this.query(path, createIfNone) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单个属性值
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
getPropValue(path: string): any {
|
|
||||||
return this.getProp(path, false)?.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置单个属性值
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
setPropValue(path: string, value: any) {
|
|
||||||
this.getProp(path, true)!.setValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 props 对应的 node
|
|
||||||
*/
|
|
||||||
getNode() {
|
|
||||||
return this.owner;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,236 +0,0 @@
|
|||||||
function propertyNameRequiresQuotes(propertyName: string) {
|
|
||||||
try {
|
|
||||||
const context = {
|
|
||||||
worksWithoutQuotes: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-new-func
|
|
||||||
new Function('ctx', `ctx.worksWithoutQuotes = {${propertyName}: true}['${propertyName}']`)();
|
|
||||||
|
|
||||||
return !context.worksWithoutQuotes;
|
|
||||||
} catch (ex) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function quoteString(str: string, { doubleQuote }: any) {
|
|
||||||
return doubleQuote ? `"${str.replace(/"/gu, '\\"')}"` : `'${str.replace(/'/gu, '\\\'')}'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function valueToSource(
|
|
||||||
value: any,
|
|
||||||
{
|
|
||||||
circularReferenceToken = 'CIRCULAR_REFERENCE',
|
|
||||||
doubleQuote = true,
|
|
||||||
includeFunctions = true,
|
|
||||||
includeUndefinedProperties = false,
|
|
||||||
indentLevel = 0,
|
|
||||||
indentString = ' ',
|
|
||||||
lineEnding = '\n',
|
|
||||||
visitedObjects = new Set(),
|
|
||||||
}: any = {},
|
|
||||||
): any {
|
|
||||||
switch (typeof value) {
|
|
||||||
case 'boolean':
|
|
||||||
return value ? `${indentString.repeat(indentLevel)}true` : `${indentString.repeat(indentLevel)}false`;
|
|
||||||
case 'function':
|
|
||||||
if (includeFunctions) {
|
|
||||||
return `${indentString.repeat(indentLevel)}${value}`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
case 'number':
|
|
||||||
return `${indentString.repeat(indentLevel)}${value}`;
|
|
||||||
case 'object':
|
|
||||||
if (!value) {
|
|
||||||
return `${indentString.repeat(indentLevel)}null`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visitedObjects.has(value)) {
|
|
||||||
return `${indentString.repeat(indentLevel)}${circularReferenceToken}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value instanceof Date) {
|
|
||||||
return `${indentString.repeat(indentLevel)}new Date(${quoteString(value.toISOString(), {
|
|
||||||
doubleQuote,
|
|
||||||
})})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value instanceof Map) {
|
|
||||||
return value.size
|
|
||||||
? `${indentString.repeat(indentLevel)}new Map(${valueToSource([...value], {
|
|
||||||
circularReferenceToken,
|
|
||||||
doubleQuote,
|
|
||||||
includeFunctions,
|
|
||||||
includeUndefinedProperties,
|
|
||||||
indentLevel,
|
|
||||||
indentString,
|
|
||||||
lineEnding,
|
|
||||||
visitedObjects: new Set([value, ...visitedObjects]),
|
|
||||||
}).slice(indentLevel * indentString.length)})`
|
|
||||||
: `${indentString.repeat(indentLevel)}new Map()`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value instanceof RegExp) {
|
|
||||||
return `${indentString.repeat(indentLevel)}/${value.source}/${value.flags}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value instanceof Set) {
|
|
||||||
return value.size
|
|
||||||
? `${indentString.repeat(indentLevel)}new Set(${valueToSource([...value], {
|
|
||||||
circularReferenceToken,
|
|
||||||
doubleQuote,
|
|
||||||
includeFunctions,
|
|
||||||
includeUndefinedProperties,
|
|
||||||
indentLevel,
|
|
||||||
indentString,
|
|
||||||
lineEnding,
|
|
||||||
visitedObjects: new Set([value, ...visitedObjects]),
|
|
||||||
}).slice(indentLevel * indentString.length)})`
|
|
||||||
: `${indentString.repeat(indentLevel)}new Set()`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
if (!value.length) {
|
|
||||||
return `${indentString.repeat(indentLevel)}[]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemsStayOnTheSameLine = value.every(
|
|
||||||
item => typeof item === 'object' &&
|
|
||||||
item &&
|
|
||||||
!(item instanceof Date) &&
|
|
||||||
!(item instanceof Map) &&
|
|
||||||
!(item instanceof RegExp) &&
|
|
||||||
!(item instanceof Set) &&
|
|
||||||
(Object.keys(item).length || value.length === 1),
|
|
||||||
);
|
|
||||||
|
|
||||||
let previousIndex: number | null = null;
|
|
||||||
|
|
||||||
value = value.reduce((items, item, index) => {
|
|
||||||
if (previousIndex !== null) {
|
|
||||||
for (let i = index - previousIndex - 1; i > 0; i -= 1) {
|
|
||||||
items.push(indentString.repeat(indentLevel + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
previousIndex = index;
|
|
||||||
|
|
||||||
item = valueToSource(item, {
|
|
||||||
circularReferenceToken,
|
|
||||||
doubleQuote,
|
|
||||||
includeFunctions,
|
|
||||||
includeUndefinedProperties,
|
|
||||||
indentLevel: itemsStayOnTheSameLine ? indentLevel : indentLevel + 1,
|
|
||||||
indentString,
|
|
||||||
lineEnding,
|
|
||||||
visitedObjects: new Set([value, ...visitedObjects]),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (item === null) {
|
|
||||||
items.push(indentString.repeat(indentLevel + 1));
|
|
||||||
} else if (itemsStayOnTheSameLine) {
|
|
||||||
items.push(item.slice(indentLevel * indentString.length));
|
|
||||||
} else {
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return itemsStayOnTheSameLine
|
|
||||||
? `${indentString.repeat(indentLevel)}[${value.join(', ')}]`
|
|
||||||
: `${indentString.repeat(indentLevel)}[${lineEnding}${value.join(
|
|
||||||
`,${lineEnding}`,
|
|
||||||
)}${lineEnding}${indentString.repeat(indentLevel)}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = Object.keys(value).reduce<string[]>((entries, propertyName) => {
|
|
||||||
const propertyValue = value[propertyName];
|
|
||||||
const propertyValueString =
|
|
||||||
typeof propertyValue !== 'undefined' || includeUndefinedProperties
|
|
||||||
? valueToSource(value[propertyName], {
|
|
||||||
circularReferenceToken,
|
|
||||||
doubleQuote,
|
|
||||||
includeFunctions,
|
|
||||||
includeUndefinedProperties,
|
|
||||||
indentLevel: indentLevel + 1,
|
|
||||||
indentString,
|
|
||||||
lineEnding,
|
|
||||||
visitedObjects: new Set([value, ...visitedObjects]),
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (propertyValueString) {
|
|
||||||
const quotedPropertyName = propertyNameRequiresQuotes(propertyName)
|
|
||||||
? quoteString(propertyName, {
|
|
||||||
doubleQuote,
|
|
||||||
})
|
|
||||||
: propertyName;
|
|
||||||
const trimmedPropertyValueString = propertyValueString.slice((indentLevel + 1) * indentString.length);
|
|
||||||
|
|
||||||
if (typeof propertyValue === 'function' && trimmedPropertyValueString.startsWith(`${propertyName}()`)) {
|
|
||||||
entries.push(
|
|
||||||
`${indentString.repeat(indentLevel + 1)}${quotedPropertyName} ${trimmedPropertyValueString.slice(
|
|
||||||
propertyName.length,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
entries.push(`${indentString.repeat(indentLevel + 1)}${quotedPropertyName}: ${trimmedPropertyValueString}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return value.length
|
|
||||||
? `${indentString.repeat(indentLevel)}{${lineEnding}${value.join(
|
|
||||||
`,${lineEnding}`,
|
|
||||||
)}${lineEnding}${indentString.repeat(indentLevel)}}`
|
|
||||||
: `${indentString.repeat(indentLevel)}{}`;
|
|
||||||
case 'string':
|
|
||||||
return `${indentString.repeat(indentLevel)}${quoteString(value, {
|
|
||||||
doubleQuote,
|
|
||||||
})}`;
|
|
||||||
case 'symbol': {
|
|
||||||
let key = Symbol.keyFor(value);
|
|
||||||
|
|
||||||
if (typeof key === 'string') {
|
|
||||||
return `${indentString.repeat(indentLevel)}Symbol.for(${quoteString(key, {
|
|
||||||
doubleQuote,
|
|
||||||
})})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
key = value.toString().slice(7, -1);
|
|
||||||
|
|
||||||
if (key) {
|
|
||||||
return `${indentString.repeat(indentLevel)}Symbol(${quoteString(key, {
|
|
||||||
doubleQuote,
|
|
||||||
})})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${indentString.repeat(indentLevel)}Symbol()`;
|
|
||||||
}
|
|
||||||
case 'undefined':
|
|
||||||
return `${indentString.repeat(indentLevel)}undefined`;
|
|
||||||
default:
|
|
||||||
return `${indentString.repeat(indentLevel)}undefined`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSource(value: any): string {
|
|
||||||
if (value && value.__source) {
|
|
||||||
return value.__source;
|
|
||||||
}
|
|
||||||
let source = valueToSource(value);
|
|
||||||
if (source === 'undefined') {
|
|
||||||
source = '';
|
|
||||||
}
|
|
||||||
if (value) {
|
|
||||||
try {
|
|
||||||
value.__source = source;
|
|
||||||
} catch (ex) {
|
|
||||||
console.error(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export { TransformStage } from '@alilc/lowcode-types';
|
|
||||||
@ -1,193 +0,0 @@
|
|||||||
import { observable, makeObservable, IEventBus, createModuleEventBus, action } from '@alilc/lowcode-editor-core';
|
|
||||||
import { INode, comparePosition, PositionNO } from './node/node';
|
|
||||||
import { DocumentModel } from './document-model';
|
|
||||||
import { IPublicModelSelection } from '@alilc/lowcode-types';
|
|
||||||
|
|
||||||
export interface ISelection extends Omit<IPublicModelSelection<INode>, 'node'> {
|
|
||||||
containsNode(node: INode, excludeRoot: boolean): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Selection implements ISelection {
|
|
||||||
private emitter: IEventBus = createModuleEventBus('Selection');
|
|
||||||
|
|
||||||
@observable.shallow private _selected: string[] = [];
|
|
||||||
|
|
||||||
constructor(readonly doc: DocumentModel) {
|
|
||||||
makeObservable(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 选中的节点 id
|
|
||||||
*/
|
|
||||||
get selected(): string[] {
|
|
||||||
return this._selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 选中
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
select(id: string) {
|
|
||||||
if (this._selected.length === 1 && this._selected.indexOf(id) > -1) {
|
|
||||||
// avoid cause reaction
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = this.doc.getNode(id);
|
|
||||||
|
|
||||||
if (!node?.canSelect()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._selected = [id];
|
|
||||||
this.emitter.emit('selectionchange', this._selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量选中
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
selectAll(ids: string[]) {
|
|
||||||
const selectIds: string[] = [];
|
|
||||||
|
|
||||||
ids.forEach((d) => {
|
|
||||||
const node = this.doc.getNode(d);
|
|
||||||
|
|
||||||
if (node?.canSelect()) {
|
|
||||||
selectIds.push(d);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._selected = selectIds;
|
|
||||||
|
|
||||||
this.emitter.emit('selectionchange', this._selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除选中
|
|
||||||
*/
|
|
||||||
@action
|
|
||||||
clear() {
|
|
||||||
if (this._selected.length < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._selected = [];
|
|
||||||
this.emitter.emit('selectionchange', this._selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 整理选中
|
|
||||||
*/
|
|
||||||
dispose() {
|
|
||||||
const l = this._selected.length;
|
|
||||||
let i = l;
|
|
||||||
while (i-- > 0) {
|
|
||||||
const id = this._selected[i];
|
|
||||||
if (!this.doc.hasNode(id)) {
|
|
||||||
this._selected.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this._selected.length !== l) {
|
|
||||||
this.emitter.emit('selectionchange', this._selected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加选中
|
|
||||||
*/
|
|
||||||
add(id: string) {
|
|
||||||
if (this._selected.indexOf(id) > -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._selected.push(id);
|
|
||||||
this.emitter.emit('selectionchange', this._selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否选中
|
|
||||||
*/
|
|
||||||
has(id: string) {
|
|
||||||
return this._selected.indexOf(id) > -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除选中
|
|
||||||
*/
|
|
||||||
remove(id: string) {
|
|
||||||
const i = this._selected.indexOf(id);
|
|
||||||
if (i > -1) {
|
|
||||||
this._selected.splice(i, 1);
|
|
||||||
this.emitter.emit('selectionchange', this._selected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 选区是否包含节点
|
|
||||||
*/
|
|
||||||
containsNode(node: INode, excludeRoot = false) {
|
|
||||||
for (const id of this._selected) {
|
|
||||||
const parent = this.doc.getNode(id);
|
|
||||||
if (excludeRoot && parent?.contains(this.doc.focusNode!)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (parent?.contains(node)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取选中的节点
|
|
||||||
*/
|
|
||||||
getNodes(): INode[] {
|
|
||||||
const nodes: INode[] = [];
|
|
||||||
for (const id of this._selected) {
|
|
||||||
const node = this.doc.getNode(id);
|
|
||||||
if (node) {
|
|
||||||
nodes.push(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取顶层选区节点,场景:拖拽时,建立蒙层,只蒙在最上层
|
|
||||||
*/
|
|
||||||
getTopNodes(includeRoot = false) {
|
|
||||||
const nodes = [];
|
|
||||||
for (const id of this._selected) {
|
|
||||||
const node = this.doc.getNode(id);
|
|
||||||
// 排除根节点
|
|
||||||
if (!node || (!includeRoot && node.contains(this.doc.focusNode!))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let i = nodes.length;
|
|
||||||
let isTop = true;
|
|
||||||
while (i-- > 0) {
|
|
||||||
const n = comparePosition(nodes[i], node);
|
|
||||||
// nodes[i] contains node
|
|
||||||
if (n === PositionNO.Contains || n === PositionNO.TheSame) {
|
|
||||||
isTop = false;
|
|
||||||
break;
|
|
||||||
} else if (n === PositionNO.ContainedBy) {
|
|
||||||
// node contains nodes[i], delete nodes[i]
|
|
||||||
nodes.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// node is top item, push to nodes
|
|
||||||
if (isTop) {
|
|
||||||
nodes.push(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelectionChange(fn: (ids: string[]) => void): () => void {
|
|
||||||
this.emitter.on('selectionchange', fn);
|
|
||||||
return () => {
|
|
||||||
this.emitter.removeListener('selectionchange', fn);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
87
packages/designer/src/helper/dragon/dragon.ts
Normal file
87
packages/designer/src/helper/dragon/dragon.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* refactor thinking from https://medium.com/@alexandereardon/rethinking-drag-and-drop-d9f5770b4e6b
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventDisposable } from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
|
export interface Dragon<Node, LocateEvent> {
|
||||||
|
/**
|
||||||
|
* 是否正在拖动
|
||||||
|
* is dragging or not
|
||||||
|
*/
|
||||||
|
readonly dragging: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定 dragstart 事件
|
||||||
|
* bind a callback function which will be called on dragging start
|
||||||
|
* @param func
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
onDragstart(func: (e: LocateEvent) => any): EventDisposable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定 drag 事件
|
||||||
|
* bind a callback function which will be called on dragging
|
||||||
|
* @param func
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
onDrag(func: (e: LocateEvent) => any): EventDisposable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定 dragend 事件
|
||||||
|
* bind a callback function which will be called on dragging end
|
||||||
|
* @param func
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
onDragend(
|
||||||
|
func: (o: { dragObject: IPublicModelDragObject; copy?: boolean }) => any,
|
||||||
|
): EventDisposable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置拖拽监听的区域 shell,以及自定义拖拽转换函数 boost
|
||||||
|
* set a html element as shell to dragon as monitoring target, and
|
||||||
|
* set boost function which is used to transform a MouseEvent to type
|
||||||
|
* IPublicTypeDragNodeDataObject.
|
||||||
|
* @param shell 拖拽监听的区域
|
||||||
|
* @param boost 拖拽转换函数
|
||||||
|
*/
|
||||||
|
from(shell: Element, boost: (e: MouseEvent) => IPublicTypeDragNodeDataObject | null): any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发射拖拽对象
|
||||||
|
* boost your dragObject for dragging(flying)
|
||||||
|
*
|
||||||
|
* @param dragObject 拖拽对象
|
||||||
|
* @param boostEvent 拖拽初始时事件
|
||||||
|
*/
|
||||||
|
boost(
|
||||||
|
dragObject: IPublicTypeDragObject,
|
||||||
|
boostEvent: MouseEvent | DragEvent,
|
||||||
|
fromRglNode?: Node,
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加投放感应区
|
||||||
|
* add sensor area
|
||||||
|
*/
|
||||||
|
addSensor(sensor: any): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除投放感应
|
||||||
|
* remove sensor area
|
||||||
|
*/
|
||||||
|
removeSensor(sensor: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDragon<Node, LocateEvent>(): Dragon<Node, LocateEvent> {
|
||||||
|
let dragging = false;
|
||||||
|
let activeSensor = undefined;
|
||||||
|
|
||||||
|
const dragon: Dragon<Node, LocateEvent> = {
|
||||||
|
get dragging() {
|
||||||
|
return dragging;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return dragon;
|
||||||
|
}
|
||||||
2
packages/designer/src/helper/dragon/index.ts
Normal file
2
packages/designer/src/helper/dragon/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './types';
|
||||||
|
export * from './dragon';
|
||||||
85
packages/designer/src/helper/dragon/types.ts
Normal file
85
packages/designer/src/helper/dragon/types.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
export enum IPublicEnumDragObjectType {
|
||||||
|
Node = 'node',
|
||||||
|
NodeData = 'nodedata',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IPublicModelDragObject {
|
||||||
|
type: IPublicEnumDragObjectType.Node | IPublicEnumDragObjectType.NodeData;
|
||||||
|
|
||||||
|
data: IPublicTypeNodeSchema | IPublicTypeNodeSchema[] | null;
|
||||||
|
|
||||||
|
nodes: (IPublicModelNode | null)[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPublicModelLocateEvent {
|
||||||
|
get type(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 浏览器窗口坐标系
|
||||||
|
*/
|
||||||
|
readonly globalX: number;
|
||||||
|
readonly globalY: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原始事件
|
||||||
|
*/
|
||||||
|
readonly originalEvent: MouseEvent | DragEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 浏览器事件响应目标
|
||||||
|
*/
|
||||||
|
target?: Element | null;
|
||||||
|
|
||||||
|
canvasX?: number;
|
||||||
|
|
||||||
|
canvasY?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件订正标识,初始构造时,从发起端构造,缺少 canvasX,canvasY, 需要经过订正才有
|
||||||
|
*/
|
||||||
|
fixed?: true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活或目标文档
|
||||||
|
*/
|
||||||
|
documentModel?: IPublicModelDocumentModel | null;
|
||||||
|
|
||||||
|
get dragObject(): IPublicModelDragObject | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拖拽敏感板
|
||||||
|
*/
|
||||||
|
export interface IPublicModelSensor<Node = IPublicModelNode> {
|
||||||
|
/**
|
||||||
|
* 是否可响应,比如面板被隐藏,可设置该值 false
|
||||||
|
*/
|
||||||
|
readonly sensorAvailable: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给事件打补丁
|
||||||
|
*/
|
||||||
|
fixEvent(e: IPublicModelLocateEvent): IPublicModelLocateEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定位并激活
|
||||||
|
*/
|
||||||
|
locate(e: IPublicModelLocateEvent): IPublicModelDropLocation | undefined | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否进入敏感板区域
|
||||||
|
*/
|
||||||
|
isEnter(e: IPublicModelLocateEvent): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消激活
|
||||||
|
*/
|
||||||
|
deactiveSensor(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取节点实例
|
||||||
|
*/
|
||||||
|
getNodeInstanceFromElement?: (
|
||||||
|
e: Element | null,
|
||||||
|
) => IPublicTypeNodeInstance<IPublicTypeComponentInstance, Node> | null;
|
||||||
|
}
|
||||||
0
packages/designer/src/helper/material/manager.ts
Normal file
0
packages/designer/src/helper/material/manager.ts
Normal file
@ -1,11 +0,0 @@
|
|||||||
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export function IconClone(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
|
||||||
<path d="M192 256.16C192 220.736 220.704 192 256.16 192h639.68C931.264 192 960 220.704 960 256.16v639.68A64.16 64.16 0 0 1 895.84 960H256.16A64.16 64.16 0 0 1 192 895.84V256.16z m64 31.584v576.512a32 32 0 0 0 31.744 31.744h576.512a32 32 0 0 0 31.744-31.744V287.744A32 32 0 0 0 864.256 256H287.744A32 32 0 0 0 256 287.744zM288 192v64h64V192H288z m128 0v64h64V192h-64z m128 0v64h64V192h-64z m128 0v64h64V192h-64z m128 0v64h64V192h-64z m96 96v64h64V288h-64z m0 128v64h64v-64h-64z m0 128v64h64v-64h-64z m0 128v64h64v-64h-64z m0 128v64h64v-64h-64z m-96 96v64h64v-64h-64z m-128 0v64h64v-64h-64z m-128 0v64h64v-64h-64z m-128 0v64h64v-64h-64z m-128 0v64h64v-64H288z m-96-96v64h64v-64H192z m0-128v64h64v-64H192z m0-128v64h64v-64H192z m0-128v64h64v-64H192z m0-128v64h64V288H192z m160 416c0-17.664 14.592-32 32.064-32h319.872a31.968 31.968 0 1 1 0 64h-319.872A31.968 31.968 0 0 1 352 704z m0-128c0-17.664 14.4-32 32.224-32h383.552c17.792 0 32.224 14.208 32.224 32 0 17.664-14.4 32-32.224 32H384.224A32.032 32.032 0 0 1 352 576z m0-128c0-17.664 14.4-32 32.224-32h383.552c17.792 0 32.224 14.208 32.224 32 0 17.664-14.4 32-32.224 32H384.224A32.032 32.032 0 0 1 352 448z m512 47.936V192h-64V159.968A31.776 31.776 0 0 0 768.032 128H160A31.776 31.776 0 0 0 128 159.968V768c0 17.92 14.304 31.968 31.968 31.968H192v64h303.936H128.128A63.968 63.968 0 0 1 64 799.872V128.128C64 92.704 92.48 64 128.128 64h671.744C835.296 64 864 92.48 864 128.128v367.808z" />
|
|
||||||
</SVGIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconClone.displayName = 'Clone';
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user