mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-04-19 20:08: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 | 否 |
|
||||
| screenshot | 组件快照 | String | 否 |
|
||||
| icon | 组件的小图标 | String (URL) | 是 |
|
||||
| tags | 组件标签 | String | 是 |
|
||||
| tags | 组件标签 | String[] | 是 |
|
||||
| keywords | 组件关键词,用于搜索联想 | String | 是 |
|
||||
| devMode | 组件研发模式 | String (proCode,lowCode) | 是 |
|
||||
| npm | npm 源引入完整描述对象 | Object | 否 |
|
||||
@ -634,7 +634,7 @@ component
|
||||
| snippets | 内容为组件不同状态下的低代码 schema (可以有多个),用户从组件面板拖入组件到设计器时会向页面 schema 中插入 snippets 中定义的组件低代码 schema | Object[] | 否 |
|
||||
| group | 用于描述当前组件位于组件面板的哪个 tab | String | 否 |
|
||||
| category | 用于描述组件位于组件面板同一 tab 的哪个区域 | String | 否 |
|
||||
| priority | 用于描述组件在同一 category 中的排序 | String | 否 |
|
||||
| priority | 用于描述组件在同一 category 中的排序 | number | 否 |
|
||||
|
||||
##### 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.1 源码规范
|
||||
@ -1313,10 +1187,11 @@ export interface RemoteComponentDescription {
|
||||
|
||||
```html
|
||||
block/ ├── build │ ├── index.css // 【编译生成】 │ ├── index.html //
|
||||
【编译生成】【必选】可直接预览文件 │ ├── index.js // 【编译生成】 │ └── views // 【3A
|
||||
编译生成】html2sketch │ ├── block_view1.html // 【3A 编译生成】给 sketch 用的 html │ └──
|
||||
block_view1.png // 【3A 编译生成】截图 ├── src // 【必选】区块源码 │ ├── index.jsx // 【必选】入口 │
|
||||
└── index.module.scss // 【可选】如有样式请使用 CSS Modules 避免冲突 ├── README.md //
|
||||
【编译生成】【必选】可直接预览文件 │ ├── index.js // 【编译生成】 │ └──
|
||||
views // 【3A 编译生成】html2sketch │ ├── block_view1.html // 【3A
|
||||
编译生成】给 sketch 用的 html │ └── block_view1.png // 【3A 编译生成】截图 ├──
|
||||
src // 【必选】区块源码 │ ├── index.jsx // 【必选】入口 │ └── index.module.scss
|
||||
// 【可选】如有样式请使用 CSS Modules 避免冲突 ├── README.md //
|
||||
【可选】无格式要求 └── package.json // 【必选】
|
||||
```
|
||||
|
||||
@ -1471,23 +1346,28 @@ block_view1.png // 【3A 编译生成】截图 ├── src // 【必选】区
|
||||
与标准源码 build-scripts 对齐
|
||||
|
||||
```html
|
||||
├── META/ # 低代码元数据信息,用于多分支冲突解决、数据回滚等功能 ├── build │ ├── index.css #
|
||||
【编译生成】 │ ├── index.html # 【编译生成】【必选】可直接预览文件 │ ├── index.js # 【编译生成】
|
||||
│ └── views # 【3A 编译生成】html2sketch │ ├── page1.html # 【3A 编译生成】给 sketch 用的 html
|
||||
│ └── page1.png # 【3A 编译生成】截图 ├── public/ # 静态文件,构建时会 copy 到 build/ 目录 │ ├──
|
||||
index.html # 应用入口 HTML │ └── favicon.png # Favicon ├── src/ │ ├── components/ #
|
||||
应用内的低代码业务组件 │ │ └── GuideComponent/ │ │ ├── index.js # 组件入口 │ │ ├── components.js #
|
||||
组件依赖的其他组件 │ │ ├── schema.js # schema 描述 │ │ └── index.scss # css 样式 │ ├── pages/ # 页面
|
||||
│ │ └── HomePage/ # Home 页面 │ │ ├── index.js # 页面入口 │ │ └── index.scss # css 样式 │ ├──
|
||||
layouts/ │ │ └── BasicLayout/ # layout 组件名称 │ │ ├── index.js # layout 入口 │ │ ├── components.js
|
||||
# layout 组件依赖的其他组件 │ │ ├── schema.js # layout schema 描述 │ │ └── index.scss # layout css
|
||||
样式 │ ├── config/ # 配置信息 │ │ ├── components.js # 应用上下文所有组件 │ │ ├── routes.js #
|
||||
页面路由列表 │ │ └── constants.js # 全局常量定义 │ │ └── app.js # 应用配置文件 │ ├── utils/ # 工具库
|
||||
│ │ └── index.js # 应用第三方扩展函数 │ ├── stores/ # [可选] 全局状态管理 │ │ └── 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
|
||||
├── META/ # 低代码元数据信息,用于多分支冲突解决、数据回滚等功能 ├── build │
|
||||
├── index.css # 【编译生成】 │ ├── index.html #
|
||||
【编译生成】【必选】可直接预览文件 │ ├── index.js # 【编译生成】 │ └── views
|
||||
# 【3A 编译生成】html2sketch │ ├── page1.html # 【3A 编译生成】给 sketch 用的
|
||||
html │ └── page1.png # 【3A 编译生成】截图 ├── public/ # 静态文件,构建时会
|
||||
copy 到 build/ 目录 │ ├── index.html # 应用入口 HTML │ └── favicon.png # Favicon
|
||||
├── src/ │ ├── components/ # 应用内的低代码业务组件 │ │ └── GuideComponent/ │ │
|
||||
├── index.js # 组件入口 │ │ ├── components.js # 组件依赖的其他组件 │ │ ├──
|
||||
schema.js # schema 描述 │ │ └── index.scss # css 样式 │ ├── pages/ # 页面 │ │
|
||||
└── HomePage/ # Home 页面 │ │ ├── index.js # 页面入口 │ │ └── index.scss # css
|
||||
样式 │ ├── layouts/ │ │ └── BasicLayout/ # layout 组件名称 │ │ ├── index.js #
|
||||
layout 入口 │ │ ├── components.js # layout 组件依赖的其他组件 │ │ ├── schema.js
|
||||
# layout schema 描述 │ │ └── index.scss # layout css 样式 │ ├── config/ #
|
||||
配置信息 │ │ ├── components.js # 应用上下文所有组件 │ │ ├── routes.js #
|
||||
页面路由列表 │ │ └── constants.js # 全局常量定义 │ │ └── app.js # 应用配置文件 │
|
||||
├── utils/ # 工具库 │ │ └── index.js # 应用第三方扩展函数 │ ├── stores/ # [可选]
|
||||
全局状态管理 │ │ └── 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: {
|
||||
'@stylistic/indent': ['error', 2],
|
||||
'@stylistic/indent-binary-ops': ['error', 2],
|
||||
'@stylistic/max-len': [
|
||||
'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/quotes': ['error', 'single'],
|
||||
'@stylistic/quotes': ['error', 'single', { allowTemplateLiterals: true }],
|
||||
'@stylistic/quote-props': ['error', 'as-needed'],
|
||||
'@stylistic/jsx-pascal-case': [2],
|
||||
'@stylistic/jsx-indent': [2, 2, { checkAttributes: true, indentLogicalExpressions: true }],
|
||||
@ -42,7 +47,7 @@ export default tseslint.config({
|
||||
'@stylistic/eol-last': ['error', 'always'],
|
||||
'@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',
|
||||
|
||||
'react/jsx-no-undef': 'error',
|
||||
@ -53,6 +58,8 @@ export default tseslint.config({
|
||||
'react/no-children-prop': 'warn',
|
||||
|
||||
'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",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/react-router": "5.1.18",
|
||||
"@vanilla-extract/vite-plugin": "^4.0.7",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
@ -39,12 +40,12 @@
|
||||
"husky": "^9.0.11",
|
||||
"less": "^4.2.0",
|
||||
"lint-staged": "^15.2.2",
|
||||
"prettier": "^3.2.5",
|
||||
"rimraf": "^5.0.2",
|
||||
"rollup": "^4.13.0",
|
||||
"typescript": "^5.4.2",
|
||||
"typescript-eslint": "^7.5.0",
|
||||
"vite": "^5.1.6",
|
||||
"vitest": "^1.3.1"
|
||||
"vite": "^5.2.9",
|
||||
"vitest": "^1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"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",
|
||||
"description": "Core Api for Ali lowCode engine",
|
||||
"license": "MIT",
|
||||
@ -33,31 +33,27 @@
|
||||
"test:cov": ""
|
||||
},
|
||||
"dependencies": {
|
||||
"@alifd/next": "^1.27.8",
|
||||
"@abraham/reflection": "^0.12.0",
|
||||
"@alilc/lowcode-shared": "workspace:*",
|
||||
"classnames": "^2.5.1",
|
||||
"intl-messageformat": "^10.5.1",
|
||||
"@alilc/lowcode-types": "workspace:*",
|
||||
"@alilc/lowcode-utils": "workspace:*",
|
||||
"@formatjs/intl": "^2.10.1",
|
||||
"inversify": "^6.0.2",
|
||||
"inversify-binding-decorators": "^4.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react": "^9.1.0",
|
||||
"power-di": "^2.4.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"semver": "^7.6.0",
|
||||
"store": "^2.0.12",
|
||||
"events": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/store": "^2.0.2",
|
||||
"@types/semver": "^7.5.8",
|
||||
"less": "^4.2.0"
|
||||
"@types/react-dom": "^18.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@alifd/next": "^1.27.8",
|
||||
"@alilc/lowcode-shared": "workspace:*",
|
||||
"@alilc/lowcode-types": "workspace:*",
|
||||
"@alilc/lowcode-utils": "workspace:*",
|
||||
"react": "^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 { createLogger } from '@alilc/lowcode-utils';
|
||||
import { createLogger } from '@alilc/lowcode-shared';
|
||||
|
||||
const logger = createLogger({ level: 'warn', bizName: 'Preference' });
|
||||
|
||||
const STORAGE_KEY_PREFIX = 'ale';
|
||||
|
||||
/**
|
||||
* used to store user preferences, such as pinned status of a pannel.
|
||||
* save to local storage.
|
||||
*/
|
||||
export default class Preference {
|
||||
export class Preference {
|
||||
constructor() {}
|
||||
|
||||
private getStorageKey(key: string, module?: string): string {
|
||||
const moduleKey = module || '__inner__';
|
||||
return `${STORAGE_KEY_PREFIX}_${moduleKey}.${key}`;
|
||||
@ -58,3 +61,5 @@ export default class Preference {
|
||||
return !(result === undefined || result === null);
|
||||
}
|
||||
}
|
||||
|
||||
export const userPreference = new Preference();
|
||||
@ -1,88 +1,41 @@
|
||||
/**
|
||||
* 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 { globalContext } from './di';
|
||||
import {
|
||||
IPublicTypeHotkeyCallback,
|
||||
IPublicTypeHotkeyCallbackConfig,
|
||||
IPublicTypeHotkeyCallbacks,
|
||||
IPublicApiHotkey,
|
||||
IPublicTypeDisposable,
|
||||
} from '@alilc/lowcode-types';
|
||||
import { type EventDisposable, Platform } from '@alilc/lowcode-shared';
|
||||
|
||||
interface KeyMap {
|
||||
[key: number]: string;
|
||||
}
|
||||
type KeyboardEventKeyMapping = Record<string, string>;
|
||||
|
||||
interface CtrlKeyMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface ActionEvent {
|
||||
interface KeyboardEventLike {
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface HotkeyDirectMap {
|
||||
[key: string]: IPublicTypeHotkeyCallback;
|
||||
}
|
||||
type KeyAction = 'keypress' | 'keydown' | 'keyup';
|
||||
|
||||
interface KeyInfo {
|
||||
key: string;
|
||||
modifiers: string[];
|
||||
action: string;
|
||||
action: KeyAction;
|
||||
}
|
||||
|
||||
interface SequenceLevels {
|
||||
[key: string]: number;
|
||||
type SequenceLevels = Record<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 = {
|
||||
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',
|
||||
};
|
||||
export type HotkeyCallbackConfigRecord = Record<string, HotkeyCallbackConfig[]>;
|
||||
|
||||
const KEYCODE_MAP: KeyMap = {
|
||||
106: '*',
|
||||
107: '+',
|
||||
109: '-',
|
||||
110: '.',
|
||||
111: '/',
|
||||
186: ';',
|
||||
187: '=',
|
||||
188: ',',
|
||||
189: '-',
|
||||
190: '.',
|
||||
191: '/',
|
||||
192: '`',
|
||||
219: '[',
|
||||
220: '\\',
|
||||
221: ']',
|
||||
222: '\'',
|
||||
};
|
||||
|
||||
const SHIFT_MAP: CtrlKeyMap = {
|
||||
const SHIFT_ALTERNATE_KEYS_MAP: KeyboardEventKeyMapping = {
|
||||
'~': '`',
|
||||
'!': '1',
|
||||
'@': '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',
|
||||
command: 'meta',
|
||||
return: 'enter',
|
||||
escape: 'esc',
|
||||
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 {
|
||||
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
|
||||
*/
|
||||
@ -191,15 +80,12 @@ function eventModifiers(e: KeyboardEvent): string[] {
|
||||
if (e.shiftKey) {
|
||||
modifiers.push('shift');
|
||||
}
|
||||
|
||||
if (e.altKey) {
|
||||
modifiers.push('alt');
|
||||
}
|
||||
|
||||
if (e.ctrlKey) {
|
||||
modifiers.push('ctrl');
|
||||
}
|
||||
|
||||
if (e.metaKey) {
|
||||
modifiers.push('meta');
|
||||
}
|
||||
@ -207,188 +93,55 @@ function eventModifiers(e: KeyboardEvent): string[] {
|
||||
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
|
||||
*
|
||||
* if your callback function returns false this will use the jquery
|
||||
* convention - prevent default and stop propogation on the event
|
||||
*/
|
||||
function fireCallback(
|
||||
callback: IPublicTypeHotkeyCallback,
|
||||
e: KeyboardEvent,
|
||||
combo?: string,
|
||||
sequence?: string,
|
||||
): void {
|
||||
function fireCallback(callback: HotkeyCallback, e: KeyboardEvent, combo?: string): void {
|
||||
try {
|
||||
const workspace = globalContext.get('workspace');
|
||||
const editor = workspace.isActive ? workspace.window?.editor : globalContext.get('editor');
|
||||
const designer = editor?.get('designer');
|
||||
const node = designer?.currentSelection?.getNodes()?.[0];
|
||||
const npm = node?.componentMeta?.npm;
|
||||
const selected =
|
||||
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||
|
||||
node?.componentMeta?.componentName ||
|
||||
'';
|
||||
if (callback(e, combo) === false) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
editor?.eventBus.emit('hotkey.callback.call', {
|
||||
callback,
|
||||
e,
|
||||
combo,
|
||||
sequence,
|
||||
selected,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error((err 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'> {
|
||||
callBacks: IPublicTypeHotkeyCallbacks = {};
|
||||
#sequenceLevels: SequenceLevels = {};
|
||||
/**
|
||||
* 当前快捷键配置
|
||||
*/
|
||||
#callBackConfigRecord: HotkeyCallbackConfigRecord = {};
|
||||
|
||||
private directMap: HotkeyDirectMap = {};
|
||||
|
||||
private sequenceLevels: SequenceLevels = {};
|
||||
|
||||
private resetTimer = 0;
|
||||
|
||||
private ignoreNextKeyup: boolean | string = false;
|
||||
|
||||
private ignoreNextKeypress = false;
|
||||
|
||||
private nextExpectedAction: boolean | string = false;
|
||||
|
||||
private isActivate = true;
|
||||
|
||||
constructor(readonly viewName: string = 'global') {
|
||||
this.mount(window);
|
||||
active(): void {
|
||||
this.#isActivate = true;
|
||||
}
|
||||
|
||||
activate(activate: boolean): void {
|
||||
this.isActivate = activate;
|
||||
inactive() {
|
||||
this.#isActivate = false;
|
||||
}
|
||||
|
||||
mount(window: Window): IPublicTypeDisposable {
|
||||
/**
|
||||
* 给指定窗口绑定快捷键
|
||||
* @param window 窗口的 window 对象
|
||||
*/
|
||||
mount(window: Window): EventDisposable {
|
||||
const { document } = window;
|
||||
const handleKeyEvent = this.handleKeyEvent.bind(this);
|
||||
document.addEventListener('keypress', handleKeyEvent, false);
|
||||
document.addEventListener('keydown', handleKeyEvent, false);
|
||||
document.addEventListener('keyup', handleKeyEvent, false);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keypress', handleKeyEvent, false);
|
||||
document.removeEventListener('keydown', handleKeyEvent, false);
|
||||
@ -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);
|
||||
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];
|
||||
|
||||
combinations.forEach((combination) => {
|
||||
const info: KeyInfo = getKeyInfo(combination, action);
|
||||
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;
|
||||
});
|
||||
if (idx !== -1) {
|
||||
this.callBacks[key].splice(idx, 1);
|
||||
this.#callBackConfigRecord[key].splice(idx, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* resets all sequence counters except for the ones passed in
|
||||
*/
|
||||
private resetSequences(doNotReset?: SequenceLevels): void {
|
||||
// doNotReset = doNotReset || {};
|
||||
let activeSequences = false;
|
||||
let key = '';
|
||||
for (key in this.sequenceLevels) {
|
||||
if (doNotReset && doNotReset[key]) {
|
||||
activeSequences = true;
|
||||
} else {
|
||||
this.sequenceLevels[key] = 0;
|
||||
}
|
||||
private bindSingle(
|
||||
combination: string,
|
||||
callback: HotkeyCallback,
|
||||
action?: KeyAction,
|
||||
sequenceName?: string,
|
||||
level?: number,
|
||||
): void {
|
||||
// make sure multiple spaces in a row become a single space
|
||||
combination = combination.replace(/\s+/g, ' ');
|
||||
|
||||
const sequence: string[] = combination.split(' ');
|
||||
|
||||
// if this pattern is a sequence of keys then run through this method
|
||||
// to reprocess each pattern one key at a time
|
||||
if (sequence.length > 1) {
|
||||
this.bindSequence(combination, sequence, callback, action);
|
||||
return;
|
||||
}
|
||||
if (!activeSequences) {
|
||||
this.nextExpectedAction = false;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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(
|
||||
character: string,
|
||||
modifiers: string[],
|
||||
e: KeyboardEvent | ActionEvent,
|
||||
e: KeyboardEvent | KeyboardEventLike,
|
||||
sequenceName?: string,
|
||||
combination?: string,
|
||||
level?: number,
|
||||
): IPublicTypeHotkeyCallbackConfig[] {
|
||||
): HotkeyCallbackConfig[] {
|
||||
let i: number;
|
||||
let callback: IPublicTypeHotkeyCallbackConfig;
|
||||
const matches: IPublicTypeHotkeyCallbackConfig[] = [];
|
||||
const action: string = e.type;
|
||||
let callback: HotkeyCallbackConfig;
|
||||
const matches: HotkeyCallbackConfig[] = [];
|
||||
const action = e.type as KeyAction;
|
||||
|
||||
// if there are no events related to this keycode
|
||||
if (!this.callBacks[character]) {
|
||||
if (!this.#callBackConfigRecord[character]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -464,12 +269,12 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
|
||||
|
||||
// loop through all callbacks for the key that was pressed
|
||||
// and see if any of them match
|
||||
for (i = 0; i < this.callBacks[character].length; ++i) {
|
||||
callback = this.callBacks[character][i];
|
||||
for (i = 0; i < this.#callBackConfigRecord[character].length; ++i) {
|
||||
callback = this.#callBackConfigRecord[character][i];
|
||||
|
||||
// if a sequence name is not specified, but this is a sequence at
|
||||
// the wrong level then move onto the next match
|
||||
if (!sequenceName && callback.seq && this.sequenceLevels[callback.seq] !== callback.level) {
|
||||
if (!sequenceName && callback.seq && this.#sequenceLevels[callback.seq] !== callback.level) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -494,21 +299,35 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
|
||||
const deleteSequence =
|
||||
sequenceName && callback.seq === sequenceName && callback.level === level;
|
||||
if (deleteCombo || deleteSequence) {
|
||||
this.callBacks[character].splice(i, 1);
|
||||
this.#callBackConfigRecord[character].splice(i, 1);
|
||||
}
|
||||
|
||||
matches.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
const callbacks: IPublicTypeHotkeyCallbackConfig[] = this.getMatches(character, modifiers, e);
|
||||
const callbacks: HotkeyCallbackConfig[] = this.getMatches(character, modifiers, e);
|
||||
|
||||
let i: number;
|
||||
const doNotReset: SequenceLevels = {};
|
||||
let maxLevel = 0;
|
||||
let processedSequenceCallback = false;
|
||||
|
||||
// Calculate the maxLevel for sequences so we can only execute the longest callback sequence
|
||||
for (i = 0; i < callbacks.length; ++i) {
|
||||
@ -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
|
||||
for (i = 0; i < callbacks.length; ++i) {
|
||||
// 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
|
||||
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;
|
||||
}
|
||||
|
||||
@ -552,55 +374,33 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
|
||||
}
|
||||
}
|
||||
|
||||
const ignoreThisKeypress = e.type === 'keypress' && this.ignoreNextKeypress;
|
||||
if (e.type === this.nextExpectedAction && !isModifier(character) && !ignoreThisKeypress) {
|
||||
const ignoreThisKeypress = e.type === 'keypress' && this.#ignoreNextKeypress;
|
||||
if (e.type === this.#nextExpectedAction && !isModifier(character) && !ignoreThisKeypress) {
|
||||
this.resetSequences(doNotReset);
|
||||
}
|
||||
|
||||
this.ignoreNextKeypress = processedSequenceCallback && e.type === 'keydown';
|
||||
}
|
||||
|
||||
private handleKeyEvent(e: KeyboardEvent): void {
|
||||
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);
|
||||
this.#ignoreNextKeypress = processedSequenceCallback && e.type === 'keydown';
|
||||
}
|
||||
|
||||
private resetSequenceTimer(): void {
|
||||
if (this.resetTimer) {
|
||||
clearTimeout(this.resetTimer);
|
||||
if (this.#resetTimer) {
|
||||
clearTimeout(this.#resetTimer);
|
||||
}
|
||||
this.resetTimer = window.setTimeout(this.resetSequences, 1000);
|
||||
this.#resetTimer = window.setTimeout(this.resetSequences, 1000);
|
||||
}
|
||||
|
||||
private bindSequence(
|
||||
combo: string,
|
||||
keys: string[],
|
||||
callback: IPublicTypeHotkeyCallback,
|
||||
action?: string,
|
||||
callback: HotkeyCallback,
|
||||
action?: KeyAction,
|
||||
): void {
|
||||
// const self: any = this;
|
||||
this.sequenceLevels[combo] = 0;
|
||||
this.#sequenceLevels[combo] = 0;
|
||||
|
||||
const increaseSequence = (nextAction: string) => {
|
||||
return () => {
|
||||
this.nextExpectedAction = nextAction;
|
||||
++this.sequenceLevels[combo];
|
||||
this.#nextExpectedAction = nextAction;
|
||||
++this.#sequenceLevels[combo];
|
||||
this.resetSequenceTimer();
|
||||
};
|
||||
};
|
||||
@ -608,81 +408,137 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
|
||||
fireCallback(callback, e, combo);
|
||||
|
||||
if (action !== 'keyup') {
|
||||
this.ignoreNextKeyup = characterFromEvent(e);
|
||||
this.#ignoreNextKeyup = e.key.toLowerCase();
|
||||
}
|
||||
|
||||
setTimeout(this.resetSequences, 10);
|
||||
};
|
||||
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
const isFinal = i + 1 === keys.length;
|
||||
const wrappedCallback = isFinal
|
||||
? callbackAndReset
|
||||
: increaseSequence(action || getKeyInfo(keys[i + 1]).action);
|
||||
|
||||
this.bindSingle(keys[i], wrappedCallback, action, combo, i);
|
||||
}
|
||||
}
|
||||
|
||||
private bindSingle(
|
||||
combination: string,
|
||||
callback: IPublicTypeHotkeyCallback,
|
||||
action?: string,
|
||||
sequenceName?: string,
|
||||
level?: number,
|
||||
): void {
|
||||
// store a direct mapped reference for use with HotKey.trigger
|
||||
this.directMap[`${combination}:${action}`] = callback;
|
||||
|
||||
// make sure multiple spaces in a row become a single space
|
||||
combination = combination.replace(/\s+/g, ' ');
|
||||
|
||||
const sequence: string[] = combination.split(' ');
|
||||
|
||||
// if this pattern is a sequence of keys then run through this method
|
||||
// to reprocess each pattern one key at a time
|
||||
if (sequence.length > 1) {
|
||||
this.bindSequence(combination, sequence, callback, action);
|
||||
return;
|
||||
/**
|
||||
* resets all sequence counters except for the ones passed in
|
||||
*/
|
||||
private resetSequences(doNotReset?: SequenceLevels): void {
|
||||
// doNotReset = doNotReset || {};
|
||||
let activeSequences = false;
|
||||
let key = '';
|
||||
for (key in this.#sequenceLevels) {
|
||||
if (doNotReset && doNotReset[key]) {
|
||||
activeSequences = true;
|
||||
} else {
|
||||
this.#sequenceLevels[key] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const info: KeyInfo = getKeyInfo(combination, action);
|
||||
|
||||
// make sure to initialize array if this is the first time
|
||||
// a callback is added for this key
|
||||
this.callBacks[info.key] = this.callBacks[info.key] || [];
|
||||
|
||||
// remove an existing match if there is one
|
||||
this.getMatches(
|
||||
info.key,
|
||||
info.modifiers,
|
||||
{ type: info.action },
|
||||
sequenceName,
|
||||
combination,
|
||||
level,
|
||||
);
|
||||
|
||||
// add this call back to the array
|
||||
// if it is a sequence put it at the beginning
|
||||
// if not put it at the end
|
||||
//
|
||||
// this is important because the way these are processed expects
|
||||
// the sequence ones to come first
|
||||
this.callBacks[info.key][sequenceName ? 'unshift' : 'push']({
|
||||
callback,
|
||||
modifiers: info.modifiers,
|
||||
action: info.action,
|
||||
seq: sequenceName,
|
||||
level,
|
||||
combo: combination,
|
||||
});
|
||||
}
|
||||
|
||||
private bindMultiple(
|
||||
combinations: string[],
|
||||
callback: IPublicTypeHotkeyCallback,
|
||||
action?: string,
|
||||
) {
|
||||
for (const item of combinations) {
|
||||
this.bindSingle(item, callback, action);
|
||||
if (!activeSequences) {
|
||||
this.#nextExpectedAction = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src", "__tests__"]
|
||||
}
|
||||
@ -30,7 +30,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alilc/lowcode-editor-core": "workspace:*",
|
||||
"@alilc/lowcode-core": "workspace:*",
|
||||
"@alilc/lowcode-shared": "workspace:*",
|
||||
"@alilc/lowcode-types": "workspace:*",
|
||||
"@alilc/lowcode-utils": "workspace:*",
|
||||
@ -55,7 +55,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@alifd/next": "^1.27.8",
|
||||
"@alilc/lowcode-editor-core": "workspace:*",
|
||||
"@alilc/lowcode-core": "workspace:*",
|
||||
"@alilc/lowcode-shared": "workspace:*",
|
||||
"@alilc/lowcode-types": "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