diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..9153397d3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "semi": true, + "jsxSingleQuote": false, + "singleQuote": true, + "arrowParens": "always", + "endOfLine": "lf", + "trailingComma": "all" +} diff --git a/docs/docs/specs/material-spec.md b/docs/docs/specs/material-spec.md index 040c7437f..b1239b2c4 100644 --- a/docs/docs/specs/material-spec.md +++ b/docs/docs/specs/material-spec.md @@ -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\ | 低代码引擎编辑器会加载这些 url | -| packages[].urls (AA) | 组件渲染态视图打包后的 CDN url 列表,包含 js 和 css | Array\ | 低代码引擎渲染模块会加载这些 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; // 大包列表,external与package的概念相似,融合在一起 - components: Array | Array; // 所有组件的描述协议列表 - 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 ``` ##### 入口文件 diff --git a/eslint.config.js b/eslint.config.js index 91fa270bc..c0ef37eef 100644 --- a/eslint.config.js +++ b/eslint.config.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', }, }); diff --git a/package.json b/package.json index 575faa62b..376229389 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/editor-core/__tests__/command.spec.ts b/packages/core/__tests__/command.spec.ts similarity index 100% rename from packages/editor-core/__tests__/command.spec.ts rename to packages/core/__tests__/command.spec.ts diff --git a/packages/core/__tests__/instantiation.spec.ts b/packages/core/__tests__/instantiation.spec.ts new file mode 100644 index 000000000..de6760340 --- /dev/null +++ b/packages/core/__tests__/instantiation.spec.ts @@ -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!'); + }); +}); diff --git a/packages/editor-core/package.json b/packages/core/package.json similarity index 79% rename from packages/editor-core/package.json rename to packages/core/package.json index 7b3e08514..41e5bef02 100644 --- a/packages/editor-core/package.json +++ b/packages/core/package.json @@ -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" }, diff --git a/packages/core/src/command.ts b/packages/core/src/command.ts new file mode 100644 index 000000000..25e852756 --- /dev/null +++ b/packages/core/src/command.ts @@ -0,0 +1,50 @@ +export interface Command { + /** + * 命令名称 + * 命名规则:commandName + * 使用规则:commandScope:commandName (commandScope 在插件 meta 中定义,用于区分不同插件的命令) + */ + name: string; + + /** + * 命令描述 + */ + description?: string; + + /** + * 命令处理函数 + */ + handler: (...args: any[]) => void | Promise; +} + +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; +} diff --git a/packages/core/src/configuration/config.ts b/packages/core/src/configuration/config.ts new file mode 100644 index 000000000..1afa8b150 --- /dev/null +++ b/packages/core/src/configuration/config.ts @@ -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 { + strictMode?: boolean; + setterValidator?: (key: K, value: Config[K]) => boolean | string; +} + +export class Configuration { + #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) { + 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) { + 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); + } + } +} diff --git a/packages/core/src/configuration/index.ts b/packages/core/src/configuration/index.ts new file mode 100644 index 000000000..aa9743a7a --- /dev/null +++ b/packages/core/src/configuration/index.ts @@ -0,0 +1,2 @@ +export * from './config'; +export { Preference, userPreference } from './preference'; diff --git a/packages/editor-core/src/preference.ts b/packages/core/src/configuration/preference.ts similarity index 92% rename from packages/editor-core/src/preference.ts rename to packages/core/src/configuration/preference.ts index 65f70175a..d62eb6d13 100644 --- a/packages/editor-core/src/preference.ts +++ b/packages/core/src/configuration/preference.ts @@ -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(); diff --git a/packages/editor-core/src/hotkey.ts b/packages/core/src/hotkey.ts similarity index 58% rename from packages/editor-core/src/hotkey.ts rename to packages/core/src/hotkey.ts index 757e71caf..fbd265bea 100644 --- a/packages/editor-core/src/hotkey.ts +++ b/packages/core/src/hotkey.ts @@ -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; -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; + +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; -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 { - 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 { }; } - 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 { 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 { // 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 { 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 { } } + 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 { // 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 { } } - 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 { 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(','); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 000000000..66dd35621 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,4 @@ +export * from './configuration'; +export * from './hotkey'; +export * from './intl'; +export * from './instantiation'; diff --git a/packages/core/src/instantiation/index.ts b/packages/core/src/instantiation/index.ts new file mode 100644 index 000000000..81ed10944 --- /dev/null +++ b/packages/core/src/instantiation/index.ts @@ -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 { + (...args: any[]): void; + type: T; +} + +type Constructor = new (...args: any[]) => T; + +export function createDecorator(serviceId: string): ServiceIdentifier { + const id = ( + 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(App: T) { + return iocContainer.resolve>(App); +} + +export function bootstrapModules() { + iocContainer.load(buildProviderModule()); +} diff --git a/packages/core/src/intl.ts b/packages/core/src/intl.ts new file mode 100644 index 000000000..75da7d3dd --- /dev/null +++ b/packages/core/src/intl.ts @@ -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; + #messageStore: Signal; + #currentMessage: ComputedSignal; + #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 = { + 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('-'); +} diff --git a/packages/editor-core/test/command.test.ts b/packages/core/test/command.test.ts similarity index 100% rename from packages/editor-core/test/command.test.ts rename to packages/core/test/command.test.ts diff --git a/packages/editor-core/tsconfig.declaration.json b/packages/core/tsconfig.declaration.json similarity index 100% rename from packages/editor-core/tsconfig.declaration.json rename to packages/core/tsconfig.declaration.json diff --git a/packages/editor-core/tsconfig.json b/packages/core/tsconfig.json similarity index 72% rename from packages/editor-core/tsconfig.json rename to packages/core/tsconfig.json index 039e0b4d1..9110c0117 100644 --- a/packages/editor-core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "outDir": "dist" }, - "include": ["src"] + "include": ["src", "__tests__"] } diff --git a/packages/editor-core/vite.config.ts b/packages/core/vite.config.ts similarity index 100% rename from packages/editor-core/vite.config.ts rename to packages/core/vite.config.ts diff --git a/packages/editor-core/vitest.config.ts b/packages/core/vitest.config.ts similarity index 100% rename from packages/editor-core/vitest.config.ts rename to packages/core/vitest.config.ts diff --git a/packages/designer/package.json b/packages/designer/package.json index 1a52faa84..84a555fdc 100644 --- a/packages/designer/package.json +++ b/packages/designer/package.json @@ -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:*", diff --git a/packages/designer/src/builtin-simulator/README.md b/packages/designer/src/builtin-simulator/README.md deleted file mode 100644 index f9f3ba43a..000000000 --- a/packages/designer/src/builtin-simulator/README.md +++ /dev/null @@ -1 +0,0 @@ -内置模拟器主进程 diff --git a/packages/designer/src/builtin-simulator/bem-tools/bem-tools.less b/packages/designer/src/builtin-simulator/bem-tools/bem-tools.less deleted file mode 100644 index 8c66e8513..000000000 --- a/packages/designer/src/builtin-simulator/bem-tools/bem-tools.less +++ /dev/null @@ -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; -} diff --git a/packages/designer/src/builtin-simulator/bem-tools/border-container.tsx b/packages/designer/src/builtin-simulator/bem-tools/border-container.tsx deleted file mode 100644 index 00ee5d15b..000000000 --- a/packages/designer/src/builtin-simulator/bem-tools/border-container.tsx +++ /dev/null @@ -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> - ); - } -} - -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> - ); - } -} diff --git a/packages/designer/src/builtin-simulator/bem-tools/border-detecting.tsx b/packages/designer/src/builtin-simulator/bem-tools/border-detecting.tsx deleted file mode 100644 index 73f7ef37d..000000000 --- a/packages/designer/src/builtin-simulator/bem-tools/border-detecting.tsx +++ /dev/null @@ -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> - ); - } -} diff --git a/packages/designer/src/builtin-simulator/bem-tools/border-resizing.tsx b/packages/designer/src/builtin-simulator/bem-tools/border-resizing.tsx deleted file mode 100644 index bc73c4cd7..000000000 --- a/packages/designer/src/builtin-simulator/bem-tools/border-resizing.tsx +++ /dev/null @@ -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> - ); - } -} diff --git a/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx b/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx deleted file mode 100644 index b11058c62..000000000 --- a/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx +++ /dev/null @@ -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> - ); - } -} diff --git a/packages/designer/src/builtin-simulator/bem-tools/borders.less b/packages/designer/src/builtin-simulator/bem-tools/borders.less deleted file mode 100644 index cb83d36f3..000000000 --- a/packages/designer/src/builtin-simulator/bem-tools/borders.less +++ /dev/null @@ -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; - } - } -} diff --git a/packages/designer/src/builtin-simulator/bem-tools/drag-resize-engine.ts b/packages/designer/src/builtin-simulator/bem-tools/drag-resize-engine.ts deleted file mode 100644 index 03f270218..000000000 --- a/packages/designer/src/builtin-simulator/bem-tools/drag-resize-engine.ts +++ /dev/null @@ -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(); diff --git a/packages/designer/src/builtin-simulator/bem-tools/index.tsx b/packages/designer/src/builtin-simulator/bem-tools/index.tsx deleted file mode 100644 index a3ba81c4b..000000000 --- a/packages/designer/src/builtin-simulator/bem-tools/index.tsx +++ /dev/null @@ -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> - ); - } -} diff --git a/packages/designer/src/builtin-simulator/bem-tools/insertion.less b/packages/designer/src/builtin-simulator/bem-tools/insertion.less deleted file mode 100644 index fff045631..000000000 --- a/packages/designer/src/builtin-simulator/bem-tools/insertion.less +++ /dev/null @@ -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)); - } -} diff --git a/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx b/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx deleted file mode 100644 index a59841544..000000000 --- a/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx +++ /dev/null @@ -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} />; - } -} diff --git a/packages/designer/src/builtin-simulator/bem-tools/manager.ts b/packages/designer/src/builtin-simulator/bem-tools/manager.ts deleted file mode 100644 index 1b1eb1de3..000000000 --- a/packages/designer/src/builtin-simulator/bem-tools/manager.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/designer/src/builtin-simulator/context.ts b/packages/designer/src/builtin-simulator/context.ts deleted file mode 100644 index 1cfab438b..000000000 --- a/packages/designer/src/builtin-simulator/context.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createContext } from 'react'; -import { BuiltinSimulatorHost } from './host'; - -export const SimulatorContext = createContext<BuiltinSimulatorHost>({} as any); diff --git a/packages/designer/src/builtin-simulator/create-simulator.ts b/packages/designer/src/builtin-simulator/create-simulator.ts deleted file mode 100644 index 57369efd8..000000000 --- a/packages/designer/src/builtin-simulator/create-simulator.ts +++ /dev/null @@ -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); - }); -} diff --git a/packages/designer/src/builtin-simulator/host-view.tsx b/packages/designer/src/builtin-simulator/host-view.tsx deleted file mode 100644 index 21e007930..000000000 --- a/packages/designer/src/builtin-simulator/host-view.tsx +++ /dev/null @@ -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> - ); - } -} diff --git a/packages/designer/src/builtin-simulator/host.less b/packages/designer/src/builtin-simulator/host.less deleted file mode 100644 index 9a00963f2..000000000 --- a/packages/designer/src/builtin-simulator/host.less +++ /dev/null @@ -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%; - } - } -} diff --git a/packages/designer/src/builtin-simulator/host.ts b/packages/designer/src/builtin-simulator/host.ts deleted file mode 100644 index c89c29d9e..000000000 --- a/packages/designer/src/builtin-simulator/host.ts +++ /dev/null @@ -1,1633 +0,0 @@ -import { - observable, - autorun, - reaction, - computed, - getPublicPath, - engineConfig, - globalLocale, - IReactionPublic, - IReactionOptions, - IReactionDisposer, - makeObservable, - createModuleEventBus, - IEventBus, - action, -} from '@alilc/lowcode-editor-core'; - -import { ISimulatorHost, Component, DropContainer } from '../simulator'; -import Viewport from './viewport'; -import { createSimulator } from './create-simulator'; -import { Node, INode, contains, isRootNode, isLowCodeComponent } from '../document'; -import ResourceConsumer from './resource-consumer'; -import { - AssetLevel, - Asset, - AssetList, - assetBundle, - assetItem, - AssetType, - isElement, - isFormEvent, - hasOwnProperty, - UtilsMetadata, - getClosestNode, - transactionManager, - isDragAnyObject, - isDragNodeObject, - isLocationData, - Logger, -} from '@alilc/lowcode-utils'; -import { - isShaken, - ILocateEvent, - isChildInline, - isRowContainer, - getRectTarget, - CanvasPoint, - Designer, - IDesigner, -} from '../designer'; -import { parseMetadata } from './utils/parse-metadata'; -import { getClosestClickableNode } from './utils/clickable'; -import { - IPublicTypeComponentMetadata, - IPublicTypePackage, - IPublicEnumTransitionType, - IPublicEnumDragObjectType, - IPublicTypeNodeInstance, - IPublicTypeComponentInstance, - IPublicTypeLocationChildrenDetail, - IPublicTypeLocationDetailType, - IPublicTypeRect, - IPublicModelNode, -} from '@alilc/lowcode-types'; -import { BuiltinSimulatorRenderer } from './renderer'; -import { clipboard } from '../designer/clipboard'; -import { LiveEditing } from './live-editing/live-editing'; -import { IProject, Project } from '../project'; -import { IScroller } from '../designer/scroller'; -import { isElementNode, isDOMNodeVisible } from '../utils/misc'; -import { debounce } from 'lodash-es'; - -const logger = new Logger({ level: 'warn', bizName: 'designer' }); - -export type LibraryItem = IPublicTypePackage & { - package: string; - library: string; - urls?: Asset; - editUrls?: Asset; -}; - -export interface DeviceStyleProps { - canvas?: object; - viewport?: object; -} - -export interface BuiltinSimulatorProps { - // 从 documentModel 上获取 - // suspended?: boolean; - designMode?: 'live' | 'design' | 'preview' | 'extend' | 'border'; - device?: 'mobile' | 'iphone' | string; - deviceClassName?: string; - environment?: Asset; - // @TODO 补充类型 - /** @property 请求处理器配置 */ - requestHandlersMap?: any; - extraEnvironment?: Asset; - library?: LibraryItem[]; - utilsMetadata?: UtilsMetadata; - simulatorUrl?: Asset; - theme?: Asset; - componentsAsset?: Asset; - // eslint-disable-next-line @typescript-eslint/member-ordering - [key: string]: any; -} - -const defaultSimulatorUrl = (() => { - const publicPath = getPublicPath(); - let urls; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_, prefix = '', dev] = /^(.+?)(\/js)?\/?$/.exec(publicPath) || []; - if (dev) { - urls = [ - `${prefix}/css/react-simulator-renderer.css`, - `${prefix}/js/react-simulator-renderer.js`, - ]; - } else if (process.env.NODE_ENV === 'production') { - urls = [`${prefix}/react-simulator-renderer.css`, `${prefix}/react-simulator-renderer.js`]; - } else { - urls = [`${prefix}/react-simulator-renderer.css`, `${prefix}/react-simulator-renderer.js`]; - } - return urls; -})(); - -const defaultEnvironment = [ - // https://g.alicdn.com/mylib/??react/16.11.0/umd/react.production.min.js,react-dom/16.8.6/umd/react-dom.production.min.js,prop-types/15.7.2/prop-types.min.js - assetItem( - AssetType.JSText, - 'window.React=parent.React;window.ReactDOM=parent.ReactDOM;window.__is_simulator_env__=true;', - undefined, - 'react', - ), - assetItem( - AssetType.JSText, - 'window.PropTypes=parent.PropTypes;React.PropTypes=parent.PropTypes; window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;', - ), -]; - -export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProps> { - readonly isSimulator = true; - - readonly project: IProject; - - readonly designer: IDesigner; - - readonly viewport = new Viewport(); - - readonly scroller: IScroller; - - readonly emitter: IEventBus = createModuleEventBus('BuiltinSimulatorHost'); - - readonly componentsConsumer: ResourceConsumer; - - readonly injectionConsumer: ResourceConsumer; - - readonly i18nConsumer: ResourceConsumer; - - /** - * 是否为画布自动渲染 - */ - autoRender = true; - - get currentDocument() { - return this.project.currentDocument; - } - - @computed get renderEnv(): string { - return this.get('renderEnv') || 'default'; - } - - @computed get device(): string { - return this.get('device') || 'default'; - } - - @computed get locale(): string { - return this.get('locale') || globalLocale.getLocale(); - } - - @computed get deviceClassName(): string | undefined { - return this.get('deviceClassName'); - } - - @computed get designMode(): 'live' | 'design' | 'preview' { - // renderer 依赖 - // TODO: 需要根据 design mode 不同切换鼠标响应情况 - return this.get('designMode') || 'design'; - } - - @computed get requestHandlersMap(): any { - // renderer 依赖 - // TODO: 需要根据 design mode 不同切换鼠标响应情况 - return this.get('requestHandlersMap') || null; - } - - get thisRequiredInJSE(): boolean { - return engineConfig.get('thisRequiredInJSE') ?? true; - } - - get enableStrictNotFoundMode(): any { - return engineConfig.get('enableStrictNotFoundMode') ?? false; - } - - get notFoundComponent(): any { - return engineConfig.get('notFoundComponent') ?? null; - } - - get faultComponent(): any { - return engineConfig.get('faultComponent') ?? null; - } - - get faultComponentMap(): any { - return engineConfig.get('faultComponentMap') ?? null; - } - - @computed get componentsAsset(): Asset | undefined { - return this.get('componentsAsset'); - } - - @computed get theme(): Asset | undefined { - return this.get('theme'); - } - - @computed get componentsMap() { - // renderer 依赖 - return this.designer.componentsMap; - } - - @computed get deviceStyle(): DeviceStyleProps | undefined { - return this.get('deviceStyle'); - } - - @observable.ref _props: BuiltinSimulatorProps = {}; - - @observable.ref private _contentWindow?: Window; - - get contentWindow() { - return this._contentWindow; - } - - @observable.ref private _contentDocument?: Document; - - @observable.ref private _appHelper?: any; - - get contentDocument() { - return this._contentDocument; - } - - private _renderer?: BuiltinSimulatorRenderer; - - get renderer() { - return this._renderer; - } - - readonly asyncLibraryMap: { [key: string]: {} } = {}; - - readonly libraryMap: { [key: string]: string } = {}; - - private _iframe?: HTMLIFrameElement; - - private disableHovering?: () => void; - - private disableDetecting?: () => void; - - readonly liveEditing = new LiveEditing(); - - @observable private instancesMap: { - [docId: string]: Map<string, IPublicTypeComponentInstance[]>; - } = {}; - - private tryScrollAgain: number | null = null; - - private _sensorAvailable = true; - - /** - * @see IPublicModelSensor - */ - get sensorAvailable(): boolean { - return this._sensorAvailable; - } - - private sensing = false; - - constructor(project: Project, designer: Designer) { - makeObservable(this); - this.project = project; - this.designer = designer; - this.scroller = this.designer.createScroller(this.viewport); - this.autoRender = !engineConfig.get('disableAutoRender', false); - this._appHelper = engineConfig.get('appHelper'); - this.componentsConsumer = new ResourceConsumer<Asset | undefined>(() => this.componentsAsset); - this.injectionConsumer = new ResourceConsumer(() => { - return { - appHelper: this._appHelper, - }; - }); - - engineConfig.onGot('appHelper', (data) => { - // appHelper被config.set修改后触发injectionConsumer.consume回调 - this._appHelper = data; - }); - - this.i18nConsumer = new ResourceConsumer(() => this.project.i18n); - - transactionManager.onStartTransaction(() => { - this.stopAutoRepaintNode(); - }, IPublicEnumTransitionType.REPAINT); - // 防止批量调用 transaction 时,执行多次 rerender - const rerender = debounce(this.rerender.bind(this), 28); - transactionManager.onEndTransaction(() => { - rerender(); - this.enableAutoRepaintNode(); - }, IPublicEnumTransitionType.REPAINT); - } - - stopAutoRepaintNode() { - this.renderer?.stopAutoRepaintNode(); - } - - enableAutoRepaintNode() { - this.renderer?.enableAutoRepaintNode(); - } - - /** - * @see ISimulator - */ - @action - setProps(props: BuiltinSimulatorProps) { - this._props = props; - } - - @action - set(key: string, value: any) { - this._props = { - ...this._props, - [key]: value, - }; - } - - get(key: string): any { - if (key === 'device') { - return ( - this.designer?.editor?.get('deviceMapper')?.transform?.(this._props.device) || - this._props.device - ); - } - return this._props[key]; - } - - /** - * 有 Renderer 进程连接进来,设置同步机制 - */ - connect( - renderer: BuiltinSimulatorRenderer, - effect: (reaction: IReactionPublic) => void, - options?: IReactionOptions<any, boolean>, - ) { - this._renderer = renderer; - return autorun(effect, options); - } - - reaction( - expression: (reaction: IReactionPublic) => unknown, - effect: (value: unknown, prev: unknown, reaction: IReactionPublic) => void, - opts?: IReactionOptions<any, boolean> | undefined, - ): IReactionDisposer { - return reaction(expression, effect, opts); - } - - autorun( - effect: (reaction: IReactionPublic) => void, - options?: IReactionOptions<any, boolean>, - ): IReactionDisposer { - return autorun(effect, options); - } - - purge(): void { - // todo - } - - mountViewport(viewport: HTMLElement | null) { - this.viewport.mount(viewport); - } - - /** - * { - * "title":"BizCharts", - * "package":"bizcharts", - * "exportName":"bizcharts", - * "version":"4.0.14", - * "urls":[ - * "https://g.alicdn.com/code/lib/bizcharts/4.0.14/BizCharts.js" - * ], - * "library":"BizCharts" - * } - * package:String 资源 npm 包名 - * exportName:String umd 包导出名字,用于适配部分物料包 define name 不一致的问题,例如把 BizCharts 改成 bizcharts,用来兼容物料用 define 声明的 bizcharts - * version:String 版本号 - * urls:Array 资源 cdn 地址,必须是 umd 类型,可以是.js 或者.css - * library:String umd 包直接导出的 name - */ - buildLibrary(library?: LibraryItem[]) { - const _library = library || (this.get('library') as LibraryItem[]); - const libraryAsset: AssetList = []; - const libraryExportList: string[] = []; - const functionCallLibraryExportList: string[] = []; - - if (_library && _library.length) { - _library.forEach((item) => { - const { exportMode, exportSourceLibrary } = item; - this.libraryMap[item.package] = item.library; - if (item.async) { - this.asyncLibraryMap[item.package] = item; - } - if (item.exportName && item.library) { - libraryExportList.push( - `Object.defineProperty(window,'${item.exportName}',{get:()=>window.${item.library}});`, - ); - } - if (exportMode === 'functionCall' && exportSourceLibrary) { - functionCallLibraryExportList.push( - `window["${item.library}"] = window["${exportSourceLibrary}"]("${item.library}", "${item.package}");`, - ); - } - if (item.editUrls) { - libraryAsset.push(item.editUrls); - } else if (item.urls) { - libraryAsset.push(item.urls); - } - }); - } - libraryAsset.unshift(assetItem(AssetType.JSText, libraryExportList.join(''))); - libraryAsset.push(assetItem(AssetType.JSText, functionCallLibraryExportList.join(''))); - return libraryAsset; - } - - rerender() { - this.designer.refreshComponentMetasMap(); - this.renderer?.rerender?.(); - } - - async mountContentFrame(iframe: HTMLIFrameElement | null): Promise<void> { - if (!iframe || this._iframe === iframe) { - return; - } - this._iframe = iframe; - - this._contentWindow = iframe.contentWindow!; - this._contentDocument = this._contentWindow.document; - - const libraryAsset: AssetList = this.buildLibrary(); - - if (this.renderEnv === 'rax') { - logger.error('After LowcodeEngine v1.3.0, Rax is no longer supported.'); - } - - const vendors = [ - // required & use once - assetBundle(this.get('environment') || defaultEnvironment, AssetLevel.Environment), - // required & use once - assetBundle(this.get('extraEnvironment'), AssetLevel.Environment), - - // required & use once - assetBundle(libraryAsset, AssetLevel.Library), - // required & TODO: think of update - assetBundle(this.theme, AssetLevel.Theme), - // required & use once - assetBundle(this.get('simulatorUrl') || defaultSimulatorUrl, AssetLevel.Runtime), - ]; - - // wait 准备 iframe 内容、依赖库注入 - const renderer = await createSimulator(this, iframe, vendors); - - // TODO: !!! thinkof reload onloa - - // wait 业务组件被第一次消费,否则会渲染出错 - await this.componentsConsumer.waitFirstConsume(); - - // wait 运行时上下文 - await this.injectionConsumer.waitFirstConsume(); - - if (Object.keys(this.asyncLibraryMap).length > 0) { - // 加载异步 Library - await renderer.loadAsyncLibrary(this.asyncLibraryMap); - Object.keys(this.asyncLibraryMap).forEach((key) => { - delete this.asyncLibraryMap[key]; - }); - } - - // step 5 ready & render - renderer.run(); - - // init events, overlays - this.viewport.setScrollTarget(this._contentWindow); - this.setupEvents(); - - // bind hotkey & clipboard - const hotkey = this.designer.editor.get('innerHotkey'); - hotkey.mount(this._contentWindow); - const innerSkeleton = this.designer.editor.get('skeleton'); - innerSkeleton.focusTracker.mount(this._contentWindow); - clipboard.injectCopyPaster(this._contentDocument); - - // TODO: dispose the bindings - } - - async setupComponents(library: LibraryItem[]) { - const libraryAsset: AssetList = this.buildLibrary(library); - await this.renderer?.load(libraryAsset); - if (Object.keys(this.asyncLibraryMap).length > 0) { - // 加载异步 Library - await this.renderer?.loadAsyncLibrary(this.asyncLibraryMap); - Object.keys(this.asyncLibraryMap).forEach((key) => { - delete this.asyncLibraryMap[key]; - }); - } - } - - setupEvents() { - // TODO: Thinkof move events control to simulator renderer - // just listen special callback - // because iframe maybe reload - this.setupDragAndClick(); - this.setupDetecting(); - this.setupLiveEditing(); - this.setupContextMenu(); - } - - postEvent(eventName: string, ...data: any[]) { - this.emitter.emit(eventName, ...data); - } - - setupDragAndClick() { - const { designer } = this; - const doc = this.contentDocument!; - - // TODO: think of lock when edit a node - // 事件路由 - doc.addEventListener( - 'mousedown', - (downEvent: MouseEvent) => { - // fix for popups close logic - document.dispatchEvent(new Event('mousedown')); - const documentModel = this.project.currentDocument; - if (this.liveEditing.editing || !documentModel) { - return; - } - const { selection } = documentModel; - let isMulti = false; - if (this.designMode === 'design') { - isMulti = downEvent.metaKey || downEvent.ctrlKey; - } else if (!downEvent.metaKey) { - return; - } - // FIXME: dirty fix remove label-for fro liveEditing - (downEvent.target as any)?.removeAttribute('for'); - const nodeInst = this.getNodeInstanceFromElement(downEvent.target as any); - const { focusNode } = documentModel; - const node = getClosestClickableNode(nodeInst?.node || focusNode, downEvent); - // 如果找不到可点击的节点,直接返回 - if (!node) { - return; - } - // 触发 onMouseDownHook 钩子 - const onMouseDownHook = node.componentMeta.advanced.callbacks?.onMouseDownHook; - if (onMouseDownHook) { - onMouseDownHook(downEvent, node.internalToShellNode()); - } - const rglNode = node?.getParent(); - const isRGLNode = rglNode?.isRGLContainer; - if (isRGLNode) { - // 如果拖拽的是磁铁块的右下角 handle,则直接跳过 - if ((downEvent.target as any)?.classList.contains('react-resizable-handle')) return; - // 禁止多选 - isMulti = false; - designer.dragon.emitter.emit('rgl.switch', { - action: 'start', - rglNode, - }); - } else { - // stop response document focus event - // 禁止原生拖拽 - downEvent.stopPropagation(); - downEvent.preventDefault(); - } - // if (!node?.isValidComponent()) { - // // 对于未注册组件直接返回 - // return; - // } - const isLeftButton = downEvent.which === 1 || downEvent.button === 0; - const checkSelect = (e: MouseEvent) => { - doc.removeEventListener('mouseup', checkSelect, true); - // 取消移动; - designer.dragon.emitter.emit('rgl.switch', { - action: 'end', - rglNode, - }); - // 鼠标是否移动 ? - 鼠标抖动应该也需要支持选中事件,偶尔点击不能选中,磁帖块移除 shaken 检测 - if (!isShaken(downEvent, e) || isRGLNode) { - let { id } = node; - designer.activeTracker.track({ node, instance: nodeInst?.instance }); - if (isMulti && focusNode && !node.contains(focusNode) && selection.has(id)) { - selection.remove(id); - } else { - // TODO: 避免选中 Page 组件,默认选中第一个子节点;新增规则 或 判断 Live 模式 - if (node.isPage() && node.getChildren()?.notEmpty() && this.designMode === 'live') { - const firstChildId = node.getChildren()?.get(0)?.getId(); - if (firstChildId) id = firstChildId; - } - if (focusNode) { - selection.select(node.contains(focusNode) ? focusNode.id : id); - } - - // dirty code should refector - const editor = this.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.select', { - selected, - }); - } - } - }; - - if (isLeftButton && focusNode && !node.contains(focusNode)) { - let nodes: INode[] = [node]; - let ignoreUpSelected = false; - if (isMulti) { - // multi select mode, directily add - if (!selection.has(node.id)) { - designer.activeTracker.track({ node, instance: nodeInst?.instance }); - selection.add(node.id); - ignoreUpSelected = true; - } - focusNode?.id && selection.remove(focusNode.id); - // 获得顶层 nodes - nodes = selection.getTopNodes(); - } else if (selection.containsNode(node, true)) { - nodes = selection.getTopNodes(); - } else { - // will clear current selection & select dragment in dragstart - } - designer.dragon.boost( - { - type: IPublicEnumDragObjectType.Node, - nodes, - } as any, - downEvent, - isRGLNode ? rglNode : undefined, - ); - if (ignoreUpSelected) { - // multi select mode has add selected, should return - return; - } - } - - doc.addEventListener('mouseup', checkSelect, true); - }, - true, - ); - - doc.addEventListener( - 'click', - (e) => { - // fix for popups close logic - const x = new Event('click'); - x.initEvent('click', true); - this._iframe?.dispatchEvent(x); - const { target } = e; - - const customizeIgnoreSelectors = engineConfig.get('customizeIgnoreSelectors'); - // TODO: need more elegant solution to ignore click events of components in designer - const defaultIgnoreSelectors: string[] = [ - '.next-input-group', - '.next-checkbox-group', - '.next-checkbox-wrapper', - '.next-date-picker', - '.next-input', - '.next-month-picker', - '.next-number-picker', - '.next-radio-group', - '.next-range', - '.next-range-picker', - '.next-rating', - '.next-select', - '.next-switch', - '.next-time-picker', - '.next-upload', - '.next-year-picker', - '.next-breadcrumb-item', - '.next-calendar-header', - '.next-calendar-table', - '.editor-container', // 富文本组件 - ]; - const ignoreSelectors = - customizeIgnoreSelectors?.(defaultIgnoreSelectors, e) || defaultIgnoreSelectors; - const ignoreSelectorsString = ignoreSelectors.join(','); - // 提供了 customizeIgnoreSelectors 的情况下,忽略 isFormEvent() 判断 - if ( - (!customizeIgnoreSelectors && isFormEvent(e)) || - (target as any)?.closest(ignoreSelectorsString) - ) { - e.preventDefault(); - e.stopPropagation(); - } - // stop response document click event - // todo: catch link redirect - }, - true, - ); - } - - /** - * 设置悬停处理 - */ - setupDetecting() { - const doc = this.contentDocument!; - const { detecting, dragon } = this.designer; - const hover = (e: MouseEvent) => { - if (!detecting.enable || this.designMode !== 'design') { - return; - } - const nodeInst = this.getNodeInstanceFromElement(e.target as Element); - if (nodeInst?.node) { - let { node } = nodeInst; - const focusNode = node.document?.focusNode; - if (focusNode && node.contains(focusNode)) { - node = focusNode; - } - detecting.capture(node); - } else { - detecting.capture(null); - } - if (!engineConfig.get('enableMouseEventPropagationInCanvas', false) || dragon.dragging) { - e.stopPropagation(); - } - }; - const leave = () => { - this.project.currentDocument && detecting.leave(this.project.currentDocument); - }; - - doc.addEventListener('mouseover', hover, true); - doc.addEventListener('mouseleave', leave, false); - - // TODO: refactor this line, contains click, mousedown, mousemove - doc.addEventListener( - 'mousemove', - (e: Event) => { - if (!engineConfig.get('enableMouseEventPropagationInCanvas', false) || dragon.dragging) { - e.stopPropagation(); - } - }, - true, - ); - - // this.disableDetecting = () => { - // detecting.leave(this.project.currentDocument); - // doc.removeEventListener('mouseover', hover, true); - // doc.removeEventListener('mouseleave', leave, false); - // this.disableDetecting = undefined; - // }; - } - - setupLiveEditing() { - const doc = this.contentDocument!; - // cause edit - doc.addEventListener( - 'dblclick', - (e: MouseEvent) => { - // stop response document dblclick event - e.stopPropagation(); - e.preventDefault(); - - const targetElement = e.target as HTMLElement; - const nodeInst = this.getNodeInstanceFromElement(targetElement); - if (!nodeInst) { - return; - } - const focusNode = this.project.currentDocument?.focusNode; - const node = nodeInst.node || focusNode; - if (!node || isLowCodeComponent(node)) { - return; - } - - const rootElement = this.findDOMNodes( - nodeInst.instance, - node.componentMeta.rootSelector, - )?.find( - (item) => - // 可能是 [null]; - item && item.contains(targetElement), - ) as HTMLElement; - if (!rootElement) { - return; - } - - this.liveEditing.apply({ - node, - rootElement, - event: e, - }); - }, - true, - ); - } - - /** - * @see ISimulator - */ - setSuspense(/** _suspended: boolean */) { - return false; - // if (suspended) { - // /* - // if (this.disableDetecting) { - // this.disableDetecting(); - // } - // */ - // // sleep some autorun reaction - // } else { - // // weekup some autorun reaction - // /* - // if (!this.disableDetecting) { - // this.setupDetecting(); - // } - // */ - // } - } - - setupContextMenu() { - const doc = this.contentDocument!; - doc.addEventListener('contextmenu', (e: MouseEvent) => { - const targetElement = e.target as HTMLElement; - const nodeInst = this.getNodeInstanceFromElement(targetElement); - const editor = this.designer?.editor; - if (!nodeInst) { - editor?.eventBus.emit('designer.builtinSimulator.contextmenu', { - originalEvent: e, - }); - return; - } - const node = nodeInst.node || this.project.currentDocument?.focusNode; - if (!node) { - editor?.eventBus.emit('designer.builtinSimulator.contextmenu', { - originalEvent: e, - }); - return; - } - - // dirty code should refector - const npm = node?.componentMeta?.npm; - const selected = - [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || - node?.componentMeta?.componentName || - ''; - editor?.eventBus.emit('designer.builtinSimulator.contextmenu', { - selected, - ...nodeInst, - instanceRect: this.computeComponentInstanceRect(nodeInst.instance), - originalEvent: e, - }); - }); - } - - /** - * @see ISimulator - */ - generateComponentMetadata(componentName: string): IPublicTypeComponentMetadata { - // if html tags - if (isHTMLTag(componentName)) { - return { - componentName, - // TODO: read builtins html metadata - }; - } - - const component = this.getComponent(componentName); - - if (!component) { - return { - componentName, - }; - } - - // TODO: - // 1. generate builtin div/p/h1/h2 - // 2. read propTypes - - return { - componentName, - ...parseMetadata(component), - }; - } - - /** - * @see ISimulator - */ - getComponent(componentName: string): Component | null { - return this.renderer?.getComponent(componentName) || null; - } - - createComponent(/** _schema: IPublicTypeComponentSchema */): Component | null { - return null; - // return this.renderer?.createComponent(schema) || null; - } - - setInstance(docId: string, id: string, instances: IPublicTypeComponentInstance[] | null) { - if (!hasOwnProperty(this.instancesMap, docId)) { - this.instancesMap[docId] = new Map(); - } - if (instances == null) { - this.instancesMap[docId].delete(id); - } else { - this.instancesMap[docId].set(id, instances.slice()); - } - } - - /** - * @see ISimulator - */ - getComponentInstances( - node: INode, - context?: IPublicTypeNodeInstance, - ): IPublicTypeComponentInstance[] | null { - const docId = node.document?.id; - if (!docId) { - return null; - } - - const instances = this.instancesMap[docId]?.get(node.id) || null; - if (!instances || !context) { - return instances; - } - - // filter with context - return instances.filter((instance) => { - return this.getClosestNodeInstance(instance, context?.nodeId)?.instance === context.instance; - }); - } - - /** - * @see ISimulator - */ - getComponentContext(/* node: Node */): any { - throw new Error('Method not implemented.'); - } - - /** - * @see ISimulator - */ - getClosestNodeInstance( - from: IPublicTypeComponentInstance, - specId?: string, - ): IPublicTypeNodeInstance<IPublicTypeComponentInstance> | null { - return this.renderer?.getClosestNodeInstance(from, specId) || null; - } - - /** - * @see ISimulator - */ - computeRect(node: INode): IPublicTypeRect | null { - const instances = this.getComponentInstances(node); - if (!instances) { - return null; - } - return this.computeComponentInstanceRect(instances[0], node.componentMeta.rootSelector); - } - - /** - * @see ISimulator - */ - computeComponentInstanceRect( - instance: IPublicTypeComponentInstance, - selector?: string, - ): IPublicTypeRect | null { - const renderer = this.renderer!; - const elements = this.findDOMNodes(instance, selector); - if (!elements) { - return null; - } - - const elems = elements.slice(); - let rects: DOMRect[] | undefined; - let last: { x: number; y: number; r: number; b: number } | undefined; - let _computed = false; - while (true) { - if (!rects || rects.length < 1) { - const elem = elems.pop(); - if (!elem) { - break; - } - rects = renderer.getClientRects(elem); - } - const rect = rects.pop(); - if (!rect) { - break; - } - if (rect.width === 0 && rect.height === 0) { - continue; - } - if (!last) { - last = { - x: rect.left, - y: rect.top, - r: rect.right, - b: rect.bottom, - }; - continue; - } - if (rect.left < last.x) { - last.x = rect.left; - _computed = true; - } - if (rect.top < last.y) { - last.y = rect.top; - _computed = true; - } - if (rect.right > last.r) { - last.r = rect.right; - _computed = true; - } - if (rect.bottom > last.b) { - last.b = rect.bottom; - _computed = true; - } - } - - if (last) { - const r: IPublicTypeRect = new DOMRect(last.x, last.y, last.r - last.x, last.b - last.y); - r.elements = elements; - r.computed = _computed; - return r; - } - - return null; - } - - /** - * @see ISimulator - */ - findDOMNodes( - instance: IPublicTypeComponentInstance, - selector?: string, - ): Array<Element | Text> | null { - const elements = this._renderer?.findDOMNodes(instance); - if (!elements) { - return null; - } - - if (selector) { - const matched = getMatched(elements, selector); - if (!matched) { - return null; - } - return [matched]; - } - return elements; - } - - /** - * 通过 DOM 节点获取节点,依赖 simulator 的接口 - */ - getNodeInstanceFromElement( - target: Element | null, - ): IPublicTypeNodeInstance<IPublicTypeComponentInstance, INode> | null { - if (!target) { - return null; - } - - const nodeInstance = this.getClosestNodeInstance(target); - if (!nodeInstance) { - return null; - } - const { docId } = nodeInstance; - const doc = this.project.getDocument(docId)!; - const node = doc.getNode(nodeInstance.nodeId); - return { - ...nodeInstance, - node, - }; - } - - /** - * @see ISimulator - */ - /* istanbul ignore next */ - scrollToNode(node: Node, detail?: any /* , tryTimes = 0 */) { - this.tryScrollAgain = null; - if (this.sensing) { - // active sensor - return; - } - - const opt: any = {}; - let scroll = false; - - const componentInstance = this.getComponentInstances(detail?.near?.node || node)?.[0]; - if (!componentInstance) return; - const domNode = this.findDOMNodes(componentInstance)?.[0] as Element; - if (!domNode) return; - if (isElementNode(domNode) && !isDOMNodeVisible(domNode, this.viewport)) { - const { left, top } = domNode.getBoundingClientRect(); - const { scrollTop = 0, scrollLeft = 0 } = this.contentDocument?.documentElement || {}; - opt.left = left + scrollLeft; - opt.top = top + scrollTop; - scroll = true; - } - - if (scroll && this.scroller) { - this.scroller.scrollTo(opt); - } - } - - // #region ========= drag and drop helpers ============= - /** - * @see ISimulator - */ - setNativeSelection(enableFlag: boolean) { - this.renderer?.setNativeSelection(enableFlag); - } - - /** - * @see ISimulator - */ - setDraggingState(state: boolean) { - this.renderer?.setDraggingState(state); - } - - /** - * @see ISimulator - */ - setCopyState(state: boolean) { - this.renderer?.setCopyState(state); - } - - /** - * @see ISimulator - */ - clearState() { - this.renderer?.clearState(); - } - - /** - * @see IPublicModelSensor - */ - fixEvent(e: ILocateEvent): ILocateEvent { - if (e.fixed) { - return e; - } - - const notMyEvent = e.originalEvent.view?.document !== this.contentDocument; - // fix canvasX canvasY : 当前激活文档画布坐标系 - if (notMyEvent || !('canvasX' in e) || !('canvasY' in e)) { - const l = this.viewport.toLocalPoint({ - clientX: e.globalX, - clientY: e.globalY, - }); - e.canvasX = l.clientX; - e.canvasY = l.clientY; - } - - // fix target : 浏览器事件响应目标 - if (!e.target || notMyEvent) { - if (!isNaN(e.canvasX!) && !isNaN(e.canvasY!)) { - e.target = this.contentDocument?.elementFromPoint(e.canvasX!, e.canvasY!); - } - } - - // 事件已订正 - e.fixed = true; - return e; - } - - /** - * @see IPublicModelSensor - */ - isEnter(e: ILocateEvent): boolean { - const rect = this.viewport.bounds; - return ( - e.globalY >= rect.top && - e.globalY <= rect.bottom && - e.globalX >= rect.left && - e.globalX <= rect.right - ); - } - - /** - * @see IPublicModelSensor - */ - deactiveSensor() { - this.sensing = false; - this.scroller.cancel(); - } - - // ========= drag location logic: helper for locate ========== - - /** - * @see IPublicModelSensor - */ - locate(e: ILocateEvent): any { - const { dragObject } = e; - - const nodes = dragObject?.nodes; - - const operationalNodes = nodes?.filter((node: any) => { - const onMoveHook = node.componentMeta?.advanced.callbacks?.onMoveHook; - const canMove = - onMoveHook && typeof onMoveHook === 'function' - ? onMoveHook(node.internalToShellNode()) - : true; - - let parentContainerNode: INode | null = null; - let parentNode = node.parent; - - while (parentNode) { - if (parentNode.isContainer()) { - parentContainerNode = parentNode; - break; - } - - parentNode = parentNode.parent; - } - - const onChildMoveHook = - parentContainerNode?.componentMeta?.advanced.callbacks?.onChildMoveHook; - - const childrenCanMove = - onChildMoveHook && parentContainerNode && typeof onChildMoveHook === 'function' - ? onChildMoveHook( - node!.internalToShellNode(), - (parentContainerNode as any).internalToShellNode(), - ) - : true; - - return canMove && childrenCanMove; - }); - - if (nodes && (!operationalNodes || operationalNodes.length === 0)) { - return; - } - - this.sensing = true; - this.scroller.scrolling(e); - const document = this.project.currentDocument; - if (!document) { - return null; - } - const dropContainer = this.getDropContainer(e); - const lockedNode = getClosestNode(dropContainer?.container as any, (node) => node.isLocked); - if (lockedNode) return null; - if (!dropContainer) { - return null; - } - - if (isLocationData(dropContainer)) { - return this.designer.createLocation(dropContainer as any); - } - - const { container, instance: containerInstance } = dropContainer; - - const edge = this.computeComponentInstanceRect( - containerInstance, - container.componentMeta.rootSelector, - ); - - if (!edge) { - return null; - } - - const { children } = container; - - const detail: IPublicTypeLocationChildrenDetail = { - type: IPublicTypeLocationDetailType.Children, - index: 0, - edge, - }; - - const locationData = { - target: container, - detail, - source: `simulator${document.id}`, - event: e, - }; - - if ( - e.dragObject && - e.dragObject.nodes && - e.dragObject.nodes.length && - e.dragObject.nodes[0]?.componentMeta?.isModal && - document.focusNode - ) { - return this.designer.createLocation({ - target: document.focusNode, - detail, - source: `simulator${document.id}`, - event: e, - }); - } - - if (!children || children.size < 1 || !edge) { - return this.designer.createLocation(locationData); - } - - let nearRect: IPublicTypeRect | null = null; - let nearIndex: number = 0; - let nearNode: INode | null = null; - let nearDistance: number | null = null; - let minTop: number | null = null; - let maxBottom: number | null = null; - - for (let i = 0, l = children.size; i < l; i++) { - const node = children.get(i)!; - const index = i; - const instances = this.getComponentInstances(node); - const inst = instances - ? instances.length > 1 - ? instances.find( - (_inst) => - this.getClosestNodeInstance(_inst, container.id)?.instance === containerInstance, - ) - : instances[0] - : null; - const rect = inst - ? this.computeComponentInstanceRect(inst, node.componentMeta.rootSelector) - : null; - - if (!rect) { - continue; - } - - const distance = isPointInRect(e as any, rect) ? 0 : distanceToRect(e as any, rect); - - if (distance === 0) { - nearDistance = distance; - nearNode = node; - nearIndex = index; - nearRect = rect; - break; - } - - // 标记子节点最顶 - if (minTop === null || rect.top < minTop) { - minTop = rect.top; - } - // 标记子节点最底 - if (maxBottom === null || rect.bottom > maxBottom) { - maxBottom = rect.bottom; - } - - if (nearDistance === null || distance < nearDistance) { - nearDistance = distance; - nearNode = node; - nearIndex = index; - nearRect = rect; - } - } - - detail.index = nearIndex; - - if (nearNode && nearRect) { - const el = getRectTarget(nearRect); - const inline = el ? isChildInline(el) : false; - const row = el ? isRowContainer(el.parentElement!) : false; - const vertical = inline || row; - - // TODO: fix type - const near: { - node: IPublicModelNode; - pos: 'before' | 'after' | 'replace'; - rect?: IPublicTypeRect; - align?: 'V' | 'H'; - } = { - node: nearNode.internalToShellNode()!, - pos: 'before', - align: vertical ? 'V' : 'H', - }; - detail.near = near; - if (isNearAfter(e as any, nearRect, vertical)) { - near.pos = 'after'; - detail.index = nearIndex + 1; - } - if (!row && nearDistance !== 0) { - const edgeDistance = distanceToEdge(e as any, edge); - if (edgeDistance.distance < nearDistance!) { - const { nearAfter } = edgeDistance; - if (minTop == null) { - minTop = edge.top; - } - if (maxBottom == null) { - maxBottom = edge.bottom; - } - near.rect = new DOMRect(edge.left, minTop, edge.width, maxBottom - minTop); - near.align = 'H'; - near.pos = nearAfter ? 'after' : 'before'; - detail.index = nearAfter ? children.size : 0; - } - } - } - - return this.designer.createLocation(locationData); - } - - /** - * 查找合适的投放容器 - */ - getDropContainer(e: ILocateEvent): DropContainer | null { - const { target, dragObject } = e; - const isAny = isDragAnyObject(dragObject); - const document = this.project.currentDocument!; - const { currentRoot } = document; - let container: INode | null; - let nodeInstance: IPublicTypeNodeInstance<IPublicTypeComponentInstance, INode> | undefined; - - if (target) { - const ref = this.getNodeInstanceFromElement(target); - if (ref?.node) { - nodeInstance = ref; - container = ref.node; - } else if (isAny) { - return null; - } else { - container = currentRoot; - } - } else if (isAny) { - return null; - } else { - container = currentRoot; - } - - if (!container?.isParental()) { - container = container?.parent || currentRoot; - } - - // TODO: use spec container to accept specialData - if (isAny) { - // will return locationData - return null; - } - - // get common parent, avoid drop container contains by dragObject - const drillDownExcludes = new Set<INode>(); - if (isDragNodeObject(dragObject)) { - const { nodes } = dragObject as any; - let i = nodes.length; - let p: any = container; - while (i-- > 0) { - if (contains(nodes[i], p)) { - p = nodes[i].parent; - } - } - if (p !== container) { - container = p || document.focusNode; - container && drillDownExcludes.add(container); - } - } - - let instance: any; - if (nodeInstance) { - if (nodeInstance.node === container) { - instance = nodeInstance.instance; - } else { - instance = this.getClosestNodeInstance( - nodeInstance.instance as any, - container?.id, - )?.instance; - } - } else { - instance = container && this.getComponentInstances(container)?.[0]; - } - - let dropContainer: DropContainer = { - container: container as any, - instance, - }; - - let res: any; - let upward: DropContainer | null = null; - while (container) { - res = this.handleAccept(dropContainer, e); - // if (isLocationData(res)) { - // return res; - // } - if (res === true) { - return dropContainer; - } - if (!res) { - drillDownExcludes.add(container); - if (upward) { - dropContainer = upward; - container = dropContainer.container; - upward = null; - } else if (container.parent) { - container = container.parent; - instance = this.getClosestNodeInstance(dropContainer.instance, container.id)?.instance; - dropContainer = { - container, - instance, - }; - } else { - return null; - } - } - } - return null; - } - - isAcceptable(): boolean { - return false; - } - - /** - * 控制接受 - */ - handleAccept({ container }: DropContainer, e: ILocateEvent): boolean { - const { dragObject } = e; - const document = this.currentDocument!; - const { focusNode } = document; - if (isRootNode(container) || container.contains(focusNode!)) { - return document.checkNesting(focusNode!, dragObject as any); - } - - const meta = (container as Node).componentMeta; - - // FIXME: get containerInstance for accept logic use - const acceptable: boolean = this.isAcceptable(); - if (!meta.isContainer && !acceptable) { - return false; - } - - // check nesting - return document.checkNesting(container, dragObject as any); - } - - /** - * 查找邻近容器 - */ - getNearByContainer({ container, instance }: DropContainer, drillDownExcludes: Set<INode>) { - const { children } = container; - if (!children || children.isEmpty()) { - return null; - } - - const nearBy: any = null; - for (let i = 0, l = children.size; i < l; i++) { - let child = children.get(i); - - if (!child) { - continue; - } - if (child.conditionGroup) { - const bn = child.conditionGroup; - i = (bn.index || 0) + bn.length - 1; - child = bn.visibleNode; - } - if (!child.isParental() || drillDownExcludes.has(child)) { - continue; - } - // TODO: - this.findDOMNodes(instance); - this.getComponentInstances(child); - const rect = this.computeRect(child); - if (!rect) { - continue; - } - } - - return nearBy; - } - // #endregion -} - -function isHTMLTag(name: string) { - return /^[a-z]\w*$/.test(name); -} - -function isPointInRect(point: CanvasPoint, rect: IPublicTypeRect) { - return ( - point.canvasY >= rect.top && - point.canvasY <= rect.bottom && - point.canvasX >= rect.left && - point.canvasX <= rect.right - ); -} - -function distanceToRect(point: CanvasPoint, rect: IPublicTypeRect) { - let minX = Math.min(Math.abs(point.canvasX - rect.left), Math.abs(point.canvasX - rect.right)); - let minY = Math.min(Math.abs(point.canvasY - rect.top), Math.abs(point.canvasY - rect.bottom)); - if (point.canvasX >= rect.left && point.canvasX <= rect.right) { - minX = 0; - } - if (point.canvasY >= rect.top && point.canvasY <= rect.bottom) { - minY = 0; - } - - return Math.sqrt(minX ** 2 + minY ** 2); -} - -function distanceToEdge(point: CanvasPoint, rect: IPublicTypeRect) { - const distanceTop = Math.abs(point.canvasY - rect.top); - const distanceBottom = Math.abs(point.canvasY - rect.bottom); - - return { - distance: Math.min(distanceTop, distanceBottom), - nearAfter: distanceBottom < distanceTop, - }; -} - -function isNearAfter(point: CanvasPoint, rect: IPublicTypeRect, inline: boolean) { - if (inline) { - return ( - Math.abs(point.canvasX - rect.left) + Math.abs(point.canvasY - rect.top) > - Math.abs(point.canvasX - rect.right) + Math.abs(point.canvasY - rect.bottom) - ); - } - return Math.abs(point.canvasY - rect.top) > Math.abs(point.canvasY - rect.bottom); -} - -function getMatched(elements: Array<Element | Text>, selector: string): Element | null { - let firstQueried: Element | null = null; - for (const elem of elements) { - if (isElement(elem)) { - if (elem.matches(selector)) { - return elem; - } - - if (!firstQueried) { - firstQueried = elem.querySelector(selector); - } - } - } - return firstQueried; -} diff --git a/packages/designer/src/builtin-simulator/index.ts b/packages/designer/src/builtin-simulator/index.ts deleted file mode 100644 index 6977fd66c..000000000 --- a/packages/designer/src/builtin-simulator/index.ts +++ /dev/null @@ -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'; diff --git a/packages/designer/src/builtin-simulator/live-editing/live-editing.ts b/packages/designer/src/builtin-simulator/live-editing/live-editing.ts deleted file mode 100644 index ddc3b8744..000000000 --- a/packages/designer/src/builtin-simulator/live-editing/live-editing.ts +++ /dev/null @@ -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; -} diff --git a/packages/designer/src/builtin-simulator/node-selector/index.less b/packages/designer/src/builtin-simulator/node-selector/index.less deleted file mode 100644 index 01552a251..000000000 --- a/packages/designer/src/builtin-simulator/node-selector/index.less +++ /dev/null @@ -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; - } - } -} diff --git a/packages/designer/src/builtin-simulator/node-selector/index.tsx b/packages/designer/src/builtin-simulator/node-selector/index.tsx deleted file mode 100644 index 64e4284ee..000000000 --- a/packages/designer/src/builtin-simulator/node-selector/index.tsx +++ /dev/null @@ -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> - ); - } -} diff --git a/packages/designer/src/builtin-simulator/renderer.ts b/packages/designer/src/builtin-simulator/renderer.ts deleted file mode 100644 index 15664757b..000000000 --- a/packages/designer/src/builtin-simulator/renderer.ts +++ /dev/null @@ -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; -} diff --git a/packages/designer/src/builtin-simulator/resource-consumer.ts b/packages/designer/src/builtin-simulator/resource-consumer.ts deleted file mode 100644 index 648ef8561..000000000 --- a/packages/designer/src/builtin-simulator/resource-consumer.ts +++ /dev/null @@ -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; - }); - } -} diff --git a/packages/designer/src/builtin-simulator/utils/clickable.ts b/packages/designer/src/builtin-simulator/utils/clickable.ts deleted file mode 100644 index 29de5bd47..000000000 --- a/packages/designer/src/builtin-simulator/utils/clickable.ts +++ /dev/null @@ -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; -}; diff --git a/packages/designer/src/builtin-simulator/utils/parse-metadata.ts b/packages/designer/src/builtin-simulator/utils/parse-metadata.ts deleted file mode 100644 index 6969a47db..000000000 --- a/packages/designer/src/builtin-simulator/utils/parse-metadata.ts +++ /dev/null @@ -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, - }; -} diff --git a/packages/designer/src/builtin-simulator/utils/path.ts b/packages/designer/src/builtin-simulator/utils/path.ts deleted file mode 100644 index 835c5a20b..000000000 --- a/packages/designer/src/builtin-simulator/utils/path.ts +++ /dev/null @@ -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; -} diff --git a/packages/designer/src/builtin-simulator/utils/throttle.ts b/packages/designer/src/builtin-simulator/utils/throttle.ts deleted file mode 100644 index 9dcf78c01..000000000 --- a/packages/designer/src/builtin-simulator/utils/throttle.ts +++ /dev/null @@ -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; -} diff --git a/packages/designer/src/builtin-simulator/viewport.ts b/packages/designer/src/builtin-simulator/viewport.ts deleted file mode 100644 index cf48a02d5..000000000 --- a/packages/designer/src/builtin-simulator/viewport.ts +++ /dev/null @@ -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, - }; - } -} diff --git a/packages/designer/src/component-actions.ts b/packages/designer/src/component-actions.ts deleted file mode 100644 index 7abd04c79..000000000 --- a/packages/designer/src/component-actions.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/designer/src/component-meta.ts b/packages/designer/src/component-meta.ts deleted file mode 100644 index 29a83b7a2..000000000 --- a/packages/designer/src/component-meta.ts +++ /dev/null @@ -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: {}, - }; -} diff --git a/packages/designer/src/components/designer-view/context.ts b/packages/designer/src/components/designer-view/context.ts new file mode 100644 index 000000000..238400b61 --- /dev/null +++ b/packages/designer/src/components/designer-view/context.ts @@ -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; diff --git a/packages/designer/src/components/designer-view/designer-view.tsx b/packages/designer/src/components/designer-view/designer-view.tsx new file mode 100644 index 000000000..7086e17f1 --- /dev/null +++ b/packages/designer/src/components/designer-view/designer-view.tsx @@ -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; diff --git a/packages/designer/src/components/designer-view/index.ts b/packages/designer/src/components/designer-view/index.ts new file mode 100644 index 000000000..0bac680bc --- /dev/null +++ b/packages/designer/src/components/designer-view/index.ts @@ -0,0 +1,2 @@ +export { default as DesignerView } from './designer-view'; +export { useDesignerContext } from './context'; diff --git a/packages/designer/src/components/drag-ghost/index.tsx b/packages/designer/src/components/drag-ghost/index.tsx new file mode 100644 index 000000000..48080d186 --- /dev/null +++ b/packages/designer/src/components/drag-ghost/index.tsx @@ -0,0 +1,3 @@ +export default function DragGhost() { + return null; +} diff --git a/packages/designer/src/components/project-view/index.ts b/packages/designer/src/components/project-view/index.ts new file mode 100644 index 000000000..095170ff3 --- /dev/null +++ b/packages/designer/src/components/project-view/index.ts @@ -0,0 +1 @@ +export { default as ProjectView } from './project-view'; diff --git a/packages/designer/src/components/project-view/project-view.tsx b/packages/designer/src/components/project-view/project-view.tsx new file mode 100644 index 000000000..a08bc7857 --- /dev/null +++ b/packages/designer/src/components/project-view/project-view.tsx @@ -0,0 +1,3 @@ +export default function ProjectView() { + return null; +} diff --git a/packages/designer/src/context-menu-actions.less b/packages/designer/src/context-menu-actions.less deleted file mode 100644 index 863c92944..000000000 --- a/packages/designer/src/context-menu-actions.less +++ /dev/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); - } - } -} \ No newline at end of file diff --git a/packages/designer/src/context-menu-actions.ts b/packages/designer/src/context-menu-actions.ts deleted file mode 100644 index d4afb6033..000000000 --- a/packages/designer/src/context-menu-actions.ts +++ /dev/null @@ -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 {} diff --git a/packages/designer/src/designer.ts b/packages/designer/src/designer.ts new file mode 100644 index 000000000..88519fb86 --- /dev/null +++ b/packages/designer/src/designer.ts @@ -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); +} diff --git a/packages/designer/src/designer/active-tracker.ts b/packages/designer/src/designer/active-tracker.ts deleted file mode 100644 index f1cc3528b..000000000 --- a/packages/designer/src/designer/active-tracker.ts +++ /dev/null @@ -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); - }; - } -} diff --git a/packages/designer/src/designer/clipboard.ts b/packages/designer/src/designer/clipboard.ts deleted file mode 100644 index 34ce2b5b5..000000000 --- a/packages/designer/src/designer/clipboard.ts +++ /dev/null @@ -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(); diff --git a/packages/designer/src/designer/designer-view.tsx b/packages/designer/src/designer/designer-view.tsx deleted file mode 100644 index 851decf14..000000000 --- a/packages/designer/src/designer/designer-view.tsx +++ /dev/null @@ -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> - ); - } -} diff --git a/packages/designer/src/designer/designer.less b/packages/designer/src/designer/designer.less deleted file mode 100644 index ad71028cd..000000000 --- a/packages/designer/src/designer/designer.less +++ /dev/null @@ -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; - } -} - - - diff --git a/packages/designer/src/designer/designer.ts b/packages/designer/src/designer/designer.ts deleted file mode 100644 index 3459ce76f..000000000 --- a/packages/designer/src/designer/designer.ts +++ /dev/null @@ -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 {} diff --git a/packages/designer/src/designer/detecting.ts b/packages/designer/src/designer/detecting.ts deleted file mode 100644 index 73c8c42fa..000000000 --- a/packages/designer/src/designer/detecting.ts +++ /dev/null @@ -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); - }; - } -} diff --git a/packages/designer/src/designer/drag-ghost/README.md b/packages/designer/src/designer/drag-ghost/README.md deleted file mode 100644 index 0b8d6d995..000000000 --- a/packages/designer/src/designer/drag-ghost/README.md +++ /dev/null @@ -1 +0,0 @@ -内置拖拽替身 diff --git a/packages/designer/src/designer/drag-ghost/ghost.less b/packages/designer/src/designer/drag-ghost/ghost.less deleted file mode 100644 index 1d9c03552..000000000 --- a/packages/designer/src/designer/drag-ghost/ghost.less +++ /dev/null @@ -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; - } - } -} diff --git a/packages/designer/src/designer/drag-ghost/index.tsx b/packages/designer/src/designer/drag-ghost/index.tsx deleted file mode 100644 index bdc293175..000000000 --- a/packages/designer/src/designer/drag-ghost/index.tsx +++ /dev/null @@ -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> - ); - } -} diff --git a/packages/designer/src/designer/dragon.ts b/packages/designer/src/designer/dragon.ts deleted file mode 100644 index 13cb21195..000000000 --- a/packages/designer/src/designer/dragon.ts +++ /dev/null @@ -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 {} diff --git a/packages/designer/src/designer/index.ts b/packages/designer/src/designer/index.ts deleted file mode 100644 index 34d7a8c09..000000000 --- a/packages/designer/src/designer/index.ts +++ /dev/null @@ -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'; diff --git a/packages/designer/src/designer/location.ts b/packages/designer/src/designer/location.ts deleted file mode 100644 index fba9c62a1..000000000 --- a/packages/designer/src/designer/location.ts +++ /dev/null @@ -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, - }); - } -} diff --git a/packages/designer/src/designer/offset-observer.ts b/packages/designer/src/designer/offset-observer.ts deleted file mode 100644 index f7b907b75..000000000 --- a/packages/designer/src/designer/offset-observer.ts +++ /dev/null @@ -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); -} diff --git a/packages/designer/src/designer/scroller.ts b/packages/designer/src/designer/scroller.ts deleted file mode 100644 index 7391c39fa..000000000 --- a/packages/designer/src/designer/scroller.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/designer/src/designer/setting/index.ts b/packages/designer/src/designer/setting/index.ts deleted file mode 100644 index 6cfa914e6..000000000 --- a/packages/designer/src/designer/setting/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './setting-field'; -export * from './setting-top-entry'; -export * from './setting-entry-type'; -export * from './setting-prop-entry'; diff --git a/packages/designer/src/designer/setting/setting-entry-type.ts b/packages/designer/src/designer/setting/setting-entry-type.ts deleted file mode 100644 index 1aee9016e..000000000 --- a/packages/designer/src/designer/setting/setting-entry-type.ts +++ /dev/null @@ -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; -} diff --git a/packages/designer/src/designer/setting/setting-field.ts b/packages/designer/src/designer/setting/setting-field.ts deleted file mode 100644 index 375286110..000000000 --- a/packages/designer/src/designer/setting/setting-field.ts +++ /dev/null @@ -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); - } -} diff --git a/packages/designer/src/designer/setting/setting-prop-entry.ts b/packages/designer/src/designer/setting/setting-prop-entry.ts deleted file mode 100644 index 4657b9ddb..000000000 --- a/packages/designer/src/designer/setting/setting-prop-entry.ts +++ /dev/null @@ -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); - } -} diff --git a/packages/designer/src/designer/setting/setting-top-entry.ts b/packages/designer/src/designer/setting/setting-top-entry.ts deleted file mode 100644 index d76a5faca..000000000 --- a/packages/designer/src/designer/setting/setting-top-entry.ts +++ /dev/null @@ -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; -} diff --git a/packages/designer/src/designer/setting/utils.ts b/packages/designer/src/designer/setting/utils.ts deleted file mode 100644 index f8dfdf99c..000000000 --- a/packages/designer/src/designer/setting/utils.ts +++ /dev/null @@ -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); - } -} diff --git a/packages/designer/src/document/document-model.ts b/packages/designer/src/document/document-model.ts deleted file mode 100644 index 45b54ca97..000000000 --- a/packages/designer/src/document/document-model.ts +++ /dev/null @@ -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 {} diff --git a/packages/designer/src/document/document-view.tsx b/packages/designer/src/document/document-view.tsx deleted file mode 100644 index 73d8157f5..000000000 --- a/packages/designer/src/document/document-view.tsx +++ /dev/null @@ -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; - } -} diff --git a/packages/designer/src/document/history.ts b/packages/designer/src/document/history.ts deleted file mode 100644 index 290df728f..000000000 --- a/packages/designer/src/document/history.ts +++ /dev/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; - } -} diff --git a/packages/designer/src/document/index.ts b/packages/designer/src/document/index.ts deleted file mode 100644 index 88601de6a..000000000 --- a/packages/designer/src/document/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './document-model'; -export * from './node'; -export * from './selection'; -export * from './history'; diff --git a/packages/designer/src/document/node/exclusive-group.ts b/packages/designer/src/document/node/exclusive-group.ts deleted file mode 100644 index 04d8537ed..000000000 --- a/packages/designer/src/document/node/exclusive-group.ts +++ /dev/null @@ -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; -} diff --git a/packages/designer/src/document/node/index.ts b/packages/designer/src/document/node/index.ts deleted file mode 100644 index 75048d230..000000000 --- a/packages/designer/src/document/node/index.ts +++ /dev/null @@ -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'; diff --git a/packages/designer/src/document/node/modal-nodes-manager.ts b/packages/designer/src/document/node/modal-nodes-manager.ts deleted file mode 100644 index 21c31ab46..000000000 --- a/packages/designer/src/document/node/modal-nodes-manager.ts +++ /dev/null @@ -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'); - } -} diff --git a/packages/designer/src/document/node/node-children.ts b/packages/designer/src/document/node/node-children.ts deleted file mode 100644 index 7717c1dd7..000000000 --- a/packages/designer/src/document/node/node-children.ts +++ /dev/null @@ -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 {} diff --git a/packages/designer/src/document/node/node.ts b/packages/designer/src/document/node/node.ts deleted file mode 100644 index 43886f7c0..000000000 --- a/packages/designer/src/document/node/node.ts +++ /dev/null @@ -1,1289 +0,0 @@ -import { ReactElement } from 'react'; -import { - observable, - computed, - makeObservable, - runInAction, - wrapWithEventSwitch, - action, - createModuleEventBus, - IEventBus, -} from '@alilc/lowcode-editor-core'; -import { - IPublicTypeNodeSchema, - IPublicTypePropsMap, - IPublicTypePropsList, - IPublicTypeNodeData, - IPublicTypeI18nData, - IPublicTypeSlotSchema, - IPublicTypePageSchema, - IPublicTypeComponentSchema, - IPublicTypeCompositeValue, - GlobalEvent, - IPublicTypeComponentAction, - IPublicModelNode, - IPublicEnumTransformStage, - IPublicTypeDisposable, - IBaseModelNode, -} from '@alilc/lowcode-types'; -import { compatStage, isDOMText, isJSExpression, isNode, isNodeSchema } from '@alilc/lowcode-utils'; -import { ISettingTopEntry } from '../../designer/setting/setting-top-entry'; -import { Props, getConvertedExtraKey, IProps } from './props/props'; -import type { IDocumentModel } from '../document-model'; -import { NodeChildren, INodeChildren } from './node-children'; -import { IProp, Prop } from './props/prop'; -import type { IComponentMeta } from '../../component-meta'; -import { ExclusiveGroup, isExclusiveGroup } from './exclusive-group'; -import type { IExclusiveGroup } from './exclusive-group'; -import { includeSlot, removeSlot } from '../../utils/slot'; -import { foreachReverse } from '../../utils/tree'; -import { NodeRemoveOptions, EDITOR_EVENT } from '../../types'; - -export interface NodeStatus { - locking: boolean; - pseudo: boolean; - inPlaceEditing: boolean; -} - -export interface IBaseNode extends Node {} - -/** - * 基础节点 - * - * [Node Properties] - * componentName: Page/Block/Component - * props - * children - * - * [Directives] - * loop - * loopArgs - * condition - * ------- addition support ----- - * conditionGroup use for condition, for exclusive - * title display on outline - * ignored ignore this node will not publish to render, but will store - * isLocked can not select/hover/ item on canvas and outline - * hidden not visible on canvas - * slotArgs like loopArgs, for slot node - * - * 根容器节点 - * - * [Node Properties] - * componentName: Page/Block/Component - * props - * children - * - * [Root Container Extra Properties] - * fileName - * meta - * state - * defaultProps - * dataSource - * lifeCycles - * methods - * css - * - * [Directives **not used**] - * loop - * loopArgs - * condition - * ------- future support ----- - * conditionGroup - * title - * ignored - * isLocked - * hidden - */ -export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema> -implements - Omit< - IBaseModelNode< - IDocumentModel, - IBaseNode, - INodeChildren, - IComponentMeta, - ISettingTopEntry, - IProps, - IProp, - IExclusiveGroup - >, - | 'isRoot' - | 'isPage' - | 'isComponent' - | 'isModal' - | 'isSlot' - | 'isParental' - | 'isLeaf' - | 'settingEntry' - // 在内部的 node 模型中不存在 - | 'getExtraPropValue' - | 'setExtraPropValue' - | 'exportSchema' - | 'visible' - | 'importSchema' - // 内外实现有差异 - | 'isContainer' - | 'isEmpty' - > -{ - private emitter: IEventBus; - - /** - * 是节点实例 - */ - readonly isNode = true; - - /** - * 节点 id - */ - readonly id: string; - - /** - * 节点组件类型 - * 特殊节点: - * * Page 页面 - * * Block 区块 - * * Component 组件/元件 - * * Fragment 碎片节点,无 props,有指令 - * * Leaf 文字节点 | 表达式节点,无 props,无指令? - * * Slot 插槽节点,无 props,正常 children,有 slotArgs,有指令 - */ - readonly componentName: string; - - /** - * 属性抽象 - */ - props: IProps; - - protected _children?: INodeChildren; - - @observable.ref private _parent: INode | null = null; - - /** - * 父级节点 - */ - get parent(): INode | null { - return this._parent; - } - - /** - * 当前节点子集 - */ - get children(): INodeChildren | null { - return this._children || null; - } - - /** - * 当前节点深度 - */ - @computed get zLevel(): number { - if (this._parent) { - return this._parent.zLevel + 1; - } - return 0; - } - - @computed get title(): string | IPublicTypeI18nData | ReactElement { - const t = this.getExtraProp('title'); - // TODO: 暂时走不到这个分支 - // if (!t && this.componentMeta.descriptor) { - // t = this.getProp(this.componentMeta.descriptor, false); - // } - if (t) { - const v = t.getAsString(); - if (v) { - return v; - } - } - return this.componentMeta.title; - } - - get icon() { - return this.componentMeta.icon; - } - - isInited = false; - - _settingEntry: ISettingTopEntry; - - get settingEntry(): ISettingTopEntry { - if (this._settingEntry) return this._settingEntry; - this._settingEntry = this.document.designer.createSettingEntry([this]); - return this._settingEntry; - } - - private autoruns?: Array<() => void>; - - private _isRGLContainer = false; - - set isRGLContainer(status: boolean) { - this._isRGLContainer = status; - } - - get isRGLContainer(): boolean { - return !!this._isRGLContainer; - } - - set isRGLContainerNode(status: boolean) { - this._isRGLContainer = status; - } - - get isRGLContainerNode(): boolean { - return !!this._isRGLContainer; - } - - get isEmptyNode() { - return this.isEmpty(); - } - - private _slotFor?: IProp | null | undefined = null; - - @observable.shallow _slots: INode[] = []; - - get slots(): INode[] { - return this._slots; - } - - /* istanbul ignore next */ - @observable.ref private _conditionGroup: IExclusiveGroup | null = null; - - /* istanbul ignore next */ - get conditionGroup(): IExclusiveGroup | null { - return this._conditionGroup; - } - - private purged = false; - - /** - * 是否已销毁 - */ - get isPurged() { - return this.purged; - } - - private purging: boolean = false; - - /** - * 是否正在销毁 - */ - get isPurging() { - return this.purging; - } - - @observable.shallow status: NodeStatus = { - inPlaceEditing: false, - locking: false, - pseudo: false, - }; - - constructor( - readonly document: IDocumentModel, - nodeSchema: Schema, - ) { - makeObservable(this); - const { componentName, id, children, props, ...extras } = nodeSchema; - this.id = document.nextId(id); - this.componentName = componentName; - if (this.componentName === 'Leaf') { - this.props = new Props(this, { - children: isDOMText(children) || isJSExpression(children) ? children : '', - }); - } else { - this.props = new Props(this, props, extras); - this._children = new NodeChildren(this as INode, this.initialChildren(children)); - this._children.internalInitParent(); - this.props.merge(this.upgradeProps(this.initProps(props || {})), this.upgradeProps(extras)); - } - - this.initBuiltinProps(); - - this.isInited = true; - this.emitter = createModuleEventBus('Node'); - const { editor } = this.document.designer; - this.onVisibleChange((visible: boolean) => { - editor?.eventBus.emit(EDITOR_EVENT.NODE_VISIBLE_CHANGE, this, visible); - }); - this.onChildrenChange((info?: { type: string; node: INode }) => { - editor?.eventBus.emit(EDITOR_EVENT.NODE_CHILDREN_CHANGE, { - type: info?.type, - node: this, - }); - }); - } - - /** - * 节点初始化期间就把内置的一些 prop 初始化好,避免后续不断构造实例导致 reaction 执行多次 - */ - @action - private initBuiltinProps() { - this.props.has(getConvertedExtraKey('hidden')) || - this.props.add(false, getConvertedExtraKey('hidden')); - this.props.has(getConvertedExtraKey('title')) || - this.props.add('', getConvertedExtraKey('title')); - this.props.has(getConvertedExtraKey('isLocked')) || - this.props.add(false, getConvertedExtraKey('isLocked')); - this.props.has(getConvertedExtraKey('condition')) || - this.props.add(true, getConvertedExtraKey('condition')); - this.props.has(getConvertedExtraKey('conditionGroup')) || - this.props.add('', getConvertedExtraKey('conditionGroup')); - this.props.has(getConvertedExtraKey('loop')) || - this.props.add(undefined, getConvertedExtraKey('loop')); - } - - @action - private initProps(props: any): any { - return this.document.designer.transformProps(props, this, IPublicEnumTransformStage.Init); - } - - @action - private upgradeProps(props: any): any { - return this.document.designer.transformProps(props, this, IPublicEnumTransformStage.Upgrade); - } - - private initialChildren( - children: IPublicTypeNodeData | IPublicTypeNodeData[] | undefined, - ): IPublicTypeNodeData[] { - const { initialChildren } = this.componentMeta.advanced; - - if (children == null) { - if (initialChildren) { - if (typeof initialChildren === 'function') { - return initialChildren(this.internalToShellNode()!) || []; - } - return initialChildren; - } - return []; - } - - if (Array.isArray(children)) { - return children; - } - - return [children]; - } - - isContainer(): boolean { - return this.isContainerNode; - } - - get isContainerNode(): boolean { - return this.isParentalNode && this.componentMeta.isContainer; - } - - isModal(): boolean { - return this.isModalNode; - } - - get isModalNode(): boolean { - return this.componentMeta.isModal; - } - - isRoot(): boolean { - return this.isRootNode; - } - - get isRootNode(): boolean { - return this.document.rootNode === (this as any); - } - - isPage(): boolean { - return this.isPageNode; - } - - get isPageNode(): boolean { - return this.isRootNode && this.componentName === 'Page'; - } - - isComponent(): boolean { - return this.isComponentNode; - } - - get isComponentNode(): boolean { - return this.isRootNode && this.componentName === 'Component'; - } - - isSlot(): boolean { - return this.isSlotNode; - } - - get isSlotNode(): boolean { - return this._slotFor != null && this.componentName === 'Slot'; - } - - /** - * 是否一个父亲类节点 - */ - isParental(): boolean { - return this.isParentalNode; - } - - get isParentalNode(): boolean { - return !this.isLeafNode; - } - - /** - * 终端节点,内容一般为 文字 或者 表达式 - */ - isLeaf(): boolean { - return this.isLeafNode; - } - get isLeafNode(): boolean { - return this.componentName === 'Leaf'; - } - - internalSetWillPurge() { - this.internalSetParent(null); - this.document.addWillPurge(this); - } - - didDropIn(dragment: INode) { - const { callbacks } = this.componentMeta.advanced; - if (callbacks?.onNodeAdd) { - const cbThis = this.internalToShellNode(); - callbacks?.onNodeAdd.call(cbThis, dragment.internalToShellNode(), cbThis); - } - if (this._parent) { - this._parent.didDropIn(dragment); - } - } - - didDropOut(dragment: INode) { - const { callbacks } = this.componentMeta.advanced; - if (callbacks?.onNodeRemove) { - const cbThis = this.internalToShellNode(); - callbacks?.onNodeRemove.call(cbThis, dragment.internalToShellNode(), cbThis); - } - if (this._parent) { - this._parent.didDropOut(dragment); - } - } - - /** - * 内部方法,请勿使用 - * @param useMutator 是否触发联动逻辑 - */ - internalSetParent(parent: INode | null, useMutator = false) { - if (this._parent === parent) { - return; - } - - // 解除老的父子关系,但不需要真的删除节点 - if (this._parent) { - if (this.isSlot()) { - this._parent.unlinkSlot(this); - } else { - this._parent.children?.unlinkChild(this); - } - } - if (useMutator) { - this._parent?.didDropOut(this); - } - if (parent) { - // 建立新的父子关系,尤其注意:对于 parent 为 null 的场景,不会赋值,因为 subtreeModified 等事件可能需要知道该 node 被删除前的父子关系 - this._parent = parent; - this.document.removeWillPurge(this); - /* istanbul ignore next */ - if (!this.conditionGroup) { - // initial conditionGroup - const grp = this.getExtraProp('conditionGroup', false)?.getAsString(); - if (grp) { - this.setConditionGroup(grp); - } - } - - if (useMutator) { - parent.didDropIn(this); - } - } - } - - internalSetSlotFor(slotFor: Prop | null | undefined) { - this._slotFor = slotFor; - } - - internalToShellNode(): IPublicModelNode | null { - return this.document.designer.shellModelFactory.createNode(this); - } - - /** - * 关联属性 - */ - get slotFor(): IProp | null | undefined { - return this._slotFor; - } - - /** - * 移除当前节点 - */ - remove( - useMutator = true, - purge = true, - options: NodeRemoveOptions = { suppressRemoveEvent: false }, - ) { - if (this.parent) { - if (!options.suppressRemoveEvent) { - this.document.designer.editor?.eventBus.emit('node.remove.topLevel', { - node: this, - index: this.parent?.children?.indexOf(this), - }); - } - if (this.isSlot()) { - this.parent.removeSlot(this); - this.parent.children?.internalDelete(this, purge, useMutator, { - suppressRemoveEvent: true, - }); - } else { - this.parent.children?.internalDelete(this, purge, useMutator, { - suppressRemoveEvent: true, - }); - } - } - } - - /** - * 锁住当前节点 - */ - lock(flag = true) { - this.setExtraProp('isLocked', flag); - } - - /** - * 获取当前节点的锁定状态 - */ - get isLocked(): boolean { - return !!this.getExtraProp('isLocked')?.getValue(); - } - - canSelect(): boolean { - const onSelectHook = this.componentMeta?.advanced?.callbacks?.onSelectHook; - const canSelect = - typeof onSelectHook === 'function' ? onSelectHook(this.internalToShellNode()!) : true; - return canSelect; - } - - /** - * 选择当前节点 - */ - select() { - this.document.selection.select(this.id); - } - - /** - * 悬停高亮 - */ - hover(flag = true) { - if (flag) { - this.document.designer.detecting.capture(this); - } else { - this.document.designer.detecting.release(this); - } - } - - /** - * 节点组件描述 - */ - @computed get componentMeta(): IComponentMeta { - return this.document.getComponentMeta(this.componentName); - } - - @computed get propsData(): IPublicTypePropsMap | IPublicTypePropsList | null { - if (!this.isParental() || this.componentName === 'Fragment') { - return null; - } - return this.props.export(IPublicEnumTransformStage.Serilize).props || null; - } - - hasSlots() { - return this._slots.length > 0; - } - - /* istanbul ignore next */ - setConditionGroup(grp: IExclusiveGroup | string | null) { - let _grp: IExclusiveGroup | null = null; - if (!grp) { - this.getExtraProp('conditionGroup', false)?.remove(); - if (this._conditionGroup) { - this._conditionGroup.remove(this); - this._conditionGroup = null; - } - return; - } - if (!isExclusiveGroup(grp)) { - if (this.prevSibling?.conditionGroup?.name === grp) { - _grp = this.prevSibling.conditionGroup; - } else if (this.nextSibling?.conditionGroup?.name === grp) { - _grp = this.nextSibling.conditionGroup; - } else if (typeof grp === 'string') { - _grp = new ExclusiveGroup(grp); - } - } - if (_grp && this._conditionGroup !== _grp) { - this.getExtraProp('conditionGroup', true)?.setValue(_grp.name); - if (this._conditionGroup) { - this._conditionGroup.remove(this); - } - this._conditionGroup = _grp; - _grp?.add(this); - } - } - - /* istanbul ignore next */ - isConditionalVisible(): boolean | undefined { - return this._conditionGroup?.isVisible(this); - } - - /* istanbul ignore next */ - setConditionalVisible() { - this._conditionGroup?.setVisible(this); - } - - hasCondition() { - const v = this.getExtraProp('condition', false)?.getValue(); - return v != null && v !== '' && v !== true; - } - - /** - * has loop when 1. loop is validArray with length > 1 ; OR 2. loop is variable object - * @return boolean, has loop config or not - */ - hasLoop() { - const value = this.getExtraProp('loop', false)?.getValue(); - if (value === undefined || value === null) { - return false; - } - - if (Array.isArray(value)) { - return true; - } - if (isJSExpression(value)) { - return true; - } - return false; - } - - /* istanbul ignore next */ - wrapWith(schema: Schema) { - const wrappedNode = this.replaceWith({ ...schema, children: [this.export()] }); - return wrappedNode.children!.get(0); - } - - replaceWith(schema: Schema, migrate = false): any { - // reuse the same id? or replaceSelection - schema = Object.assign({}, migrate ? this.export() : {}, schema); - return this.parent?.replaceChild(this, schema); - } - - /** - * 替换子节点 - * - * @param {INode} node - * @param {object} data - */ - replaceChild(node: INode, data: any): INode | null { - if (this.children?.has(node)) { - const selected = this.document.selection.has(node.id); - - delete data.id; - const newNode = this.document.createNode(data); - - if (!isNode(newNode)) { - return null; - } - - this.insertBefore(newNode, node, false); - node.remove(false); - - if (selected) { - this.document.selection.select(newNode.id); - } - return newNode; - } - return node; - } - - setVisible(flag: boolean): void { - this.getExtraProp('hidden')?.setValue(!flag); - this.emitter.emit('visibleChange', flag); - } - - getVisible(): boolean { - return !this.getExtraProp('hidden')?.getValue(); - } - - onVisibleChange(func: (flag: boolean) => any): () => void { - const wrappedFunc = wrapWithEventSwitch(func); - this.emitter.on('visibleChange', wrappedFunc); - return () => { - this.emitter.removeListener('visibleChange', wrappedFunc); - }; - } - - getProp(path: string, createIfNone = true): IProp | null { - return this.props.query(path, createIfNone) || null; - } - - getExtraProp(key: string, createIfNone = true): IProp | null { - return this.props.get(getConvertedExtraKey(key), createIfNone) || null; - } - - setExtraProp(key: string, value: IPublicTypeCompositeValue) { - this.getProp(getConvertedExtraKey(key), true)?.setValue(value); - } - - /** - * 获取单个属性值 - */ - getPropValue(path: string): any { - return this.getProp(path, false)?.value; - } - - /** - * 设置单个属性值 - */ - setPropValue(path: string, value: any) { - this.getProp(path, true)!.setValue(value); - } - - /** - * 清除已设置的值 - */ - clearPropValue(path: string): void { - this.getProp(path, false)?.unset(); - } - - /** - * 设置多个属性值,和原有值合并 - */ - mergeProps(props: IPublicTypePropsMap) { - this.props.merge(props); - } - - /** - * 设置多个属性值,替换原有值 - */ - setProps(props?: IPublicTypePropsMap | IPublicTypePropsList | Props | null) { - if (props instanceof Props) { - this.props = props; - return; - } - this.props.import(props); - } - - /** - * 获取节点在父容器中的索引 - */ - @computed get index(): number | undefined { - if (!this.parent) { - return -1; - } - return this.parent.children?.indexOf(this); - } - - /** - * 获取下一个兄弟节点 - */ - get nextSibling(): INode | null | undefined { - if (!this.parent) { - return null; - } - const { index } = this; - if (typeof index !== 'number') { - return null; - } - if (index < 0) { - return null; - } - return this.parent.children?.get(index + 1); - } - - /** - * 获取上一个兄弟节点 - */ - get prevSibling(): INode | null | undefined { - if (!this.parent) { - return null; - } - const { index } = this; - if (typeof index !== 'number') { - return null; - } - if (index < 1) { - return null; - } - return this.parent.children?.get(index - 1); - } - - /** - * 获取符合搭建协议-节点 schema 结构 - */ - get schema(): Schema { - return this.export(IPublicEnumTransformStage.Save); - } - - set schema(data: Schema) { - runInAction(() => this.import(data)); - } - - import(data: Schema, checkId = false) { - const { children, props, ...extras } = data; - if (this.isSlot()) { - foreachReverse( - this.children!, - (subNode: INode) => { - subNode.remove(true, true); - }, - (iterable, idx) => (iterable as INodeChildren).get(idx), - ); - } - if (this.isParental()) { - this.props.import(props, extras); - this._children?.import(children, checkId); - } else { - this.props - .get('children', true)! - .setValue(isDOMText(children) || isJSExpression(children) ? children : ''); - } - } - - toData() { - return this.export(); - } - - /** - * 导出 schema - */ - export<T = IPublicTypeNodeSchema>( - stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save, - options: any = {}, - ): T { - stage = compatStage(stage); - const baseSchema: any = { - componentName: this.componentName, - }; - - if (stage !== IPublicEnumTransformStage.Clone) { - baseSchema.id = this.id; - } - if (stage === IPublicEnumTransformStage.Render) { - baseSchema.docId = this.document.id; - } - - if (this.isLeaf()) { - if (!options.bypassChildren) { - baseSchema.children = this.props.get('children')?.export(stage); - } - return baseSchema; - } - - const { props = {}, extras } = this.props.export(stage) || {}; - const _extras_: { [key: string]: any } = { - ...extras, - }; - - const schema: any = { - ...baseSchema, - props: this.document.designer.transformProps(props, this, stage), - ...this.document.designer.transformProps(_extras_, this, stage), - }; - - if (this.isParental() && this.children && this.children.size > 0 && !options.bypassChildren) { - schema.children = this.children.export(stage); - } - - return schema; - } - - /** - * 判断是否包含特定节点 - */ - contains(node: INode): boolean { - return contains(this, node); - } - - /** - * 获取特定深度的父亲节点 - */ - getZLevelTop(zLevel: number): INode | null { - return getZLevelTop(this, zLevel); - } - - /** - * 判断与其它节点的位置关系 - * - * 16 thisNode contains otherNode - * 8 thisNode contained_by otherNode - * 2 thisNode before or after otherNode - * 0 thisNode same as otherNode - */ - comparePosition(otherNode: INode): PositionNO { - return comparePosition(this, otherNode); - } - - unlinkSlot(slotNode: INode) { - const i = this._slots.indexOf(slotNode); - if (i < 0) { - return false; - } - this._slots.splice(i, 1); - } - - /** - * 删除一个Slot节点 - */ - removeSlot(slotNode: INode): boolean { - // if (purge) { - // // should set parent null - // slotNode?.internalSetParent(null, false); - // slotNode?.purge(); - // } - // this.document.unlinkNode(slotNode); - // this.document.selection.remove(slotNode.id); - const i = this._slots.indexOf(slotNode); - if (i < 0) { - return false; - } - this._slots.splice(i, 1); - return false; - } - - addSlot(slotNode: INode) { - const slotName = slotNode?.getExtraProp('name')?.getAsString(); - // 一个组件下的所有 slot,相同 slotName 的 slot 应该是唯一的 - if (includeSlot(this, slotName)) { - removeSlot(this, slotName); - } - slotNode.internalSetParent(this as INode, true); - this._slots.push(slotNode); - } - - /** - * 当前node对应组件是否已注册可用 - */ - isValidComponent() { - const allComponents = this.document?.designer?.componentsMap; - if (allComponents && allComponents[this.componentName]) { - return true; - } - return false; - } - - /** - * 删除一个节点 - * @param node - */ - removeChild(node: INode) { - this.children?.delete(node); - } - - /** - * 销毁 - */ - purge() { - if (this.purged) { - return; - } - this.purged = true; - this.autoruns?.forEach((dispose) => dispose()); - this.props.purge(); - this.settingEntry?.purge(); - // this.document.destroyNode(this); - } - - internalPurgeStart() { - this.purging = true; - } - - /** - * 是否可执行某 action - */ - canPerformAction(actionName: string): boolean { - const availableActions = - this.componentMeta?.availableActions - ?.filter((action: IPublicTypeComponentAction) => { - const { condition } = action; - return typeof condition === 'function' ? condition(this) !== false : condition !== false; - }) - .map((action: IPublicTypeComponentAction) => action.name) || []; - - return availableActions.indexOf(actionName) >= 0; - } - - // ======= compatible apis ==== - isEmpty(): boolean { - return this.children ? this.children.isEmpty() : true; - } - - getChildren() { - return this.children; - } - - getComponentName() { - return this.componentName; - } - - insert(node: INode, ref?: INode, useMutator = true) { - this.insertAfter(node, ref, useMutator); - } - - insertBefore(node: INode, ref?: INode, useMutator = true) { - const nodeInstance = ensureNode(node, this.document); - this.children?.internalInsert(nodeInstance, ref ? ref.index : null, useMutator); - } - - insertAfter(node: any, ref?: INode, useMutator = true) { - const nodeInstance = ensureNode(node, this.document); - this.children?.internalInsert(nodeInstance, ref ? (ref.index || 0) + 1 : null, useMutator); - } - - getParent() { - return this.parent; - } - - getId() { - return this.id; - } - - getIndex() { - return this.index; - } - - getNode() { - return this; - } - - getRoot() { - return this.document.rootNode; - } - - getProps() { - return this.props; - } - - onChildrenChange( - fn: (param?: { type: string; node: INode }) => void, - ): IPublicTypeDisposable | undefined { - const wrappedFunc = wrapWithEventSwitch(fn); - return this.children?.onChange(wrappedFunc); - } - - mergeChildren( - remover: (node: INode, idx: number) => any, - adder: (children: INode[]) => IPublicTypeNodeData[] | null, - sorter: (firstNode: INode, secondNode: INode) => any, - ) { - this.children?.mergeChildren(remover, adder, sorter); - } - - /** - * 获取磁贴相关信息 - */ - getRGL(): { - isContainerNode: boolean; - isEmptyNode: boolean; - isRGLContainerNode: boolean; - isRGLNode: boolean; - isRGL: boolean; - rglNode: Node | null; - } { - const isContainerNode = this.isContainer(); - const isEmptyNode = this.isEmpty(); - const isRGLContainerNode = this.isRGLContainer; - const isRGLNode = this.getParent()?.isRGLContainer as boolean; - const isRGL = isRGLContainerNode || (isRGLNode && (!isContainerNode || !isEmptyNode)); - const rglNode = isRGLContainerNode ? this : isRGL ? this?.getParent() : null; - return { isContainerNode, isEmptyNode, isRGLContainerNode, isRGLNode, isRGL, rglNode }; - } - - getRect(): DOMRect | null { - if (this.isRoot()) { - return this.document.simulator?.viewport.contentBounds || null; - } - return this.document.simulator?.computeRect(this) || null; - } - - getIcon() { - return this.icon; - } - - toString() { - return this.id; - } - - emitPropChange(val: IPublicTypePropChangeOptions) { - this.emitter?.emit('propChange', val); - } - - onPropChange(func: (info: IPublicTypePropChangeOptions) => void): IPublicTypeDisposable { - const wrappedFunc = wrapWithEventSwitch(func); - this.emitter.on('propChange', wrappedFunc); - return () => { - this.emitter.removeListener('propChange', wrappedFunc); - }; - } - - /** - * todo: fixed types - */ - getDOMNode(): HTMLElement { - return document.body; - } -} - -function ensureNode(node: any, document: IDocumentModel): INode { - let nodeInstance = node; - if (!isNode(node)) { - if (node.getComponentName) { - nodeInstance = document.createNode({ - componentName: node.getComponentName(), - }); - } else { - nodeInstance = document.createNode(node); - } - } - return nodeInstance; -} - -export interface LeafNode extends Node { - readonly children: null; -} - -export type IPublicTypePropChangeOptions = Omit<GlobalEvent.Node.Prop.ChangeOptions, 'node'>; - -export interface ISlotNode extends Node<IPublicTypeSlotSchema> {} -export interface IPageNode extends Node<IPublicTypePageSchema> {} -export interface IComponentNode extends Node<IPublicTypeComponentSchema> {} -export interface IRootNode extends Node<IPublicTypePageSchema | IPublicTypeComponentSchema> {} -export interface INode - extends Node< - | IPublicTypePageSchema - | IPublicTypeSlotSchema - | IPublicTypeComponentSchema - | IPublicTypeNodeSchema - > {} - -export function isRootNode(node: INode): node is IRootNode { - return node && node.isRootNode; -} - -export function isLowCodeComponent(node: INode): node is IComponentNode { - return node.componentMeta?.getMetadata().devMode === 'lowCode'; -} - -export function getZLevelTop(child: INode, zLevel: number): INode | null { - let l = child.zLevel; - if (l < zLevel || zLevel < 0) { - return null; - } - if (l === zLevel) { - return child; - } - let r: any = child; - while (r && l-- > zLevel) { - r = r.parent; - } - return r; -} - -/** - * 测试两个节点是否为包含关系 - * @param node1 测试的父节点 - * @param node2 测试的被包含节点 - * @returns 是否包含 - */ -export function contains(node1: INode, node2: INode): boolean { - if (node1 === node2) { - return true; - } - - if (!node1.isParentalNode || !node2.parent) { - return false; - } - - const p = getZLevelTop(node2, node1.zLevel); - if (!p) { - return false; - } - - return node1 === p; -} - -// 16 node1 contains node2 -// 8 node1 contained_by node2 -// 2 node1 before or after node2 -// 0 node1 same as node2 -export enum PositionNO { - Contains = 16, - ContainedBy = 8, - BeforeOrAfter = 2, - TheSame = 0, -} -export function comparePosition(node1: INode, node2: INode): PositionNO { - if (node1 === node2) { - return PositionNO.TheSame; - } - const l1 = node1.zLevel; - const l2 = node2.zLevel; - if (l1 === l2) { - return PositionNO.BeforeOrAfter; - } - - let p: any; - if (l1 < l2) { - p = getZLevelTop(node2, l1); - if (p && p === node1) { - return PositionNO.Contains; - } - return PositionNO.BeforeOrAfter; - } - - p = getZLevelTop(node1, l2); - if (p && p === node2) { - return PositionNO.ContainedBy; - } - - return PositionNO.BeforeOrAfter; -} - -export function insertChild( - container: INode, - thing: INode | IPublicTypeNodeData, - at?: number | null, - copy?: boolean, -): INode | null { - let node: INode | null | IRootNode | undefined; - let nodeSchema: IPublicTypeNodeSchema; - if (isNode<INode>(thing) && (copy || thing.isSlot())) { - nodeSchema = thing.export(IPublicEnumTransformStage.Clone); - node = container.document?.createNode(nodeSchema); - } else if (isNode<INode>(thing)) { - node = thing; - } else if (isNodeSchema(thing)) { - node = container.document?.createNode(thing); - } - - if (isNode<INode>(node)) { - container.children?.insert(node, at); - return node; - } - - return null; -} - -export function insertChildren( - container: INode, - nodes: INode[] | IPublicTypeNodeData[] | IPublicModelNode[], - at?: number | null, - copy?: boolean, -): INode[] { - let index = at; - let node: any; - const results: INode[] = []; - // eslint-disable-next-line no-cond-assign - while ((node = nodes.pop())) { - node = insertChild(container, node, index, copy); - results.push(node); - index = node.index; - } - return results; -} diff --git a/packages/designer/src/document/node/props/prop.ts b/packages/designer/src/document/node/props/prop.ts deleted file mode 100644 index fd2d2562b..000000000 --- a/packages/designer/src/document/node/props/prop.ts +++ /dev/null @@ -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); -} diff --git a/packages/designer/src/document/node/props/props.ts b/packages/designer/src/document/node/props/props.ts deleted file mode 100644 index 26c4113b5..000000000 --- a/packages/designer/src/document/node/props/props.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/designer/src/document/node/props/value-to-source.ts b/packages/designer/src/document/node/props/value-to-source.ts deleted file mode 100644 index 366f11754..000000000 --- a/packages/designer/src/document/node/props/value-to-source.ts +++ /dev/null @@ -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; -} diff --git a/packages/designer/src/document/node/transform-stage.ts b/packages/designer/src/document/node/transform-stage.ts deleted file mode 100644 index 53b72c1bf..000000000 --- a/packages/designer/src/document/node/transform-stage.ts +++ /dev/null @@ -1 +0,0 @@ -export { TransformStage } from '@alilc/lowcode-types'; diff --git a/packages/designer/src/document/selection.ts b/packages/designer/src/document/selection.ts deleted file mode 100644 index d5c7b938f..000000000 --- a/packages/designer/src/document/selection.ts +++ /dev/null @@ -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); - }; - } -} diff --git a/packages/designer/src/helper/dragon/dragon.ts b/packages/designer/src/helper/dragon/dragon.ts new file mode 100644 index 000000000..37f24c692 --- /dev/null +++ b/packages/designer/src/helper/dragon/dragon.ts @@ -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; +} diff --git a/packages/designer/src/helper/dragon/index.ts b/packages/designer/src/helper/dragon/index.ts new file mode 100644 index 000000000..7ba32f854 --- /dev/null +++ b/packages/designer/src/helper/dragon/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './dragon'; diff --git a/packages/designer/src/helper/dragon/types.ts b/packages/designer/src/helper/dragon/types.ts new file mode 100644 index 000000000..4f0d717eb --- /dev/null +++ b/packages/designer/src/helper/dragon/types.ts @@ -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; +} diff --git a/packages/editor-skeleton/vitest.config.ts b/packages/designer/src/helper/material/index.ts similarity index 100% rename from packages/editor-skeleton/vitest.config.ts rename to packages/designer/src/helper/material/index.ts diff --git a/packages/designer/src/helper/material/manager.ts b/packages/designer/src/helper/material/manager.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/designer/src/icons/clone.tsx b/packages/designer/src/icons/clone.tsx deleted file mode 100644 index 261acd250..000000000 --- a/packages/designer/src/icons/clone.tsx +++ /dev/null @@ -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'; diff --git a/packages/designer/src/icons/component.tsx b/packages/designer/src/icons/component.tsx deleted file mode 100644 index 914f06625..000000000 --- a/packages/designer/src/icons/component.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconComponent(props: IconProps) { - return ( - <SVGIcon viewBox="0 0 1024 1024" {...props}> - <path d="M783.5648 437.4528h-18.0224V336.6912c0-43.8272-35.6352-79.4624-79.4624-79.4624h-110.592V241.664c0-90.9312-73.728-164.6592-164.6592-164.6592-90.9312 0-164.6592 73.728-164.6592 164.6592v15.5648H155.2384c-43.8272 0-79.4624 35.6352-79.4624 79.4624v131.4816c0 16.7936 13.9264 30.72 30.72 30.72h56.1152c56.9344 0 103.2192 46.2848 103.2192 103.2192s-46.2848 103.2192-103.2192 103.2192H106.496c-16.7936 0-30.72 13.9264-30.72 30.72v131.4816c0 43.8272 35.6352 79.4624 79.4624 79.4624h531.2512c43.8272 0 79.4624-35.6352 79.4624-79.4624v-100.7616h18.0224c90.9312 0 164.6592-73.728 164.6592-164.6592-0.4096-90.9312-74.1376-164.6592-165.0688-164.6592z m0 267.8784h-48.7424c-16.7936 0-30.72 13.9264-30.72 30.72v131.4816c0 9.8304-8.192 18.0224-18.0224 18.0224H155.2384c-9.8304 0-18.0224-8.192-18.0224-18.0224v-100.7616h25.3952c90.9312 0 164.6592-73.728 164.6592-164.6592 0-90.9312-73.728-164.6592-164.6592-164.6592h-25.3952V336.6912c0-9.8304 8.192-18.0224 18.0224-18.0224h121.6512c16.7936 0 30.72-13.9264 30.72-30.72V241.664c0-56.9344 46.2848-103.2192 103.2192-103.2192s103.2192 46.2848 103.2192 103.2192v46.2848c0 16.7936 13.9264 30.72 30.72 30.72h141.312c9.8304 0 18.0224 8.192 18.0224 18.0224v131.4816c0 16.7936 13.9264 30.72 30.72 30.72h48.7424c56.9344 0 103.2192 46.2848 103.2192 103.2192s-46.2848 103.2192-103.2192 103.2192z" /> - </SVGIcon> - ); -} -IconComponent.displayName = 'Component'; diff --git a/packages/designer/src/icons/container.tsx b/packages/designer/src/icons/container.tsx deleted file mode 100644 index 6cb51fd38..000000000 --- a/packages/designer/src/icons/container.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconContainer(props: IconProps) { - return ( - <SVGIcon viewBox="0 0 1024 1024" {...props}> - <path d="M800 800h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z m-256 0h64v64h-64v-64z m0-640h64v64h-64v-64z m128 640h64v64h-64v-64zM160 672h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m640 384h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z" /> - <path d="M896 64H128c-35.2 0-64 28.8-64 64v768c0 35.2 28.8 64 64 64h768c35.2 0 64-28.8 64-64V128c0-35.2-28.8-64-64-64z m0 800c0 19.2-12.8 32-32 32H160c-19.2 0-32-12.8-32-32V160c0-19.2 12.8-32 32-32h704c19.2 0 32 12.8 32 32v704z" /> - </SVGIcon> - ); -} -IconContainer.displayName = 'Container'; diff --git a/packages/designer/src/icons/hidden.tsx b/packages/designer/src/icons/hidden.tsx deleted file mode 100644 index cbf2896de..000000000 --- a/packages/designer/src/icons/hidden.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconHidden(props: IconProps) { - return ( - <SVGIcon viewBox="0 0 1024 1024" {...props}> - <path d="M183.423543 657.078213l163.499771-98.484012c-4.233418-14.908548-6.646374-30.585599-6.646374-46.852074 0-94.665033 76.739778-171.404812 171.404812-171.404812 45.983287 0 87.641059 18.20871 118.42518 47.679929l129.791042-78.17957c-73.254398-41.73145-157.866471-65.812915-248.216221-65.812915-192.742792 0-360.068705 108.505249-444.453604 267.715321C96.636944 567.228859 136.301316 616.355743 183.423543 657.078213zM841.253886 367.552144l-164.382884 99.015108c3.934612 14.415314 6.215562 29.513174 6.215562 45.174875 0 94.665033-76.739778 171.404812-171.404812 171.404812-45.361117 0-86.484723-17.747199-117.142977-46.515407l-129.419582 77.955466c72.874751 41.149189 156.893306 64.871473 246.563582 64.871473 192.742792 0 360.068705-108.505249 444.453604-267.717368C927.000805 456.773188 887.794875 408.054603 841.253886 367.552144zM420.280042 511.741104c0 0.550539 0.152473 1.060145 0.161682 1.608637l135.080511-81.366146c-13.065574-7.198959-27.854395-11.658528-43.826158-11.658528C461.20922 420.325068 420.280042 461.254246 420.280042 511.741104zM447.739441 576.947198l69.02098-41.574884L948.364369 275.395234c10.812253-6.512321 14.297634-20.558222 7.785314-31.369452-6.512321-10.812253-20.556175-14.296611-31.368428-7.785314L575.654762 446.537056l0 0-151.20577 91.078345 0 0L75.027787 748.090043c-10.812253 6.512321-14.297634 20.556175-7.785314 31.368428 6.512321 10.812253 20.556175 14.297634 31.369452 7.785314L447.739441 576.947198 447.739441 576.947198zM511.696078 603.157139c50.487881 0 91.416036-40.928155 91.416036-91.416036 0-0.549515-0.152473-1.057075-0.161682-1.605567l-135.079488 81.364099C480.935494 598.699618 495.724315 603.157139 511.696078 603.157139z" /> - </SVGIcon> - ); -} - -IconHidden.displayName = 'Hidden'; diff --git a/packages/designer/src/icons/index.ts b/packages/designer/src/icons/index.ts deleted file mode 100644 index 0a73bbc53..000000000 --- a/packages/designer/src/icons/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './lock'; -export * from './hidden'; -export * from './remove'; -export * from './setting'; -export * from './component'; -export * from './clone'; -export * from './page'; -export * from './container'; -export * from './unlock'; diff --git a/packages/designer/src/icons/lock.tsx b/packages/designer/src/icons/lock.tsx deleted file mode 100644 index f67fac237..000000000 --- a/packages/designer/src/icons/lock.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconLock(props: IconProps) { - return ( - <SVGIcon viewBox="0 0 1024 1024" {...props}> - <path d="M704 480v-160c0-105.6-86.4-192-192-192s-192 86.4-192 192v160H160v416h704V480h-160z m-320-160c0-70.4 57.6-128 128-128s128 57.6 128 128v160h-256v-160z m416 512H224v-288h576v288z" fill="#ffffff" p-id="2680" /> - <path d="M480 768h64v-160h-64z" fill="#ffffff" p-id="2681" /> - </SVGIcon> - ); -} -IconLock.displayName = 'IconLock'; diff --git a/packages/designer/src/icons/page.tsx b/packages/designer/src/icons/page.tsx deleted file mode 100644 index 07853c4e1..000000000 --- a/packages/designer/src/icons/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconPage(props: IconProps) { - return ( - <SVGIcon viewBox="0 0 1024 1024" {...props}> - <path d="M381.6 864H162.4c-6.9 0-12.4 4.6-12.4 10.3v19.3c0 5.7 5.6 10.3 12.4 10.3h219.1c6.8 0 12.4-4.6 12.4-10.3v-19.3c0.1-5.7-5.5-10.3-12.3-10.3zM382 780.6H162c-6.9 0-12.5 4.6-12.5 10.3v19.3c0 5.7 5.6 10.3 12.5 10.3h220c6.9 0 12.5-4.6 12.5-10.3v-19.3c0-5.7-5.6-10.3-12.5-10.3zM162.4 737.2h219.1c6.8 0 12.4-4.6 12.4-10.3v-19.3c0-5.7-5.6-10.3-12.4-10.3H162.4c-6.9 0-12.4 4.6-12.4 10.3v19.3c0 5.7 5.6 10.3 12.4 10.3z" /> - <path d="M977.1 0H46.9C21 0 0 21 0 46.9v930.2c0 25.9 21 46.9 46.9 46.9h930.2c25.9 0 46.9-21 46.9-46.9V46.9C1024 21 1003 0 977.1 0z m-18.7 911.6c0 25.9-21 46.9-46.9 46.9H112.4c-25.9 0-46.9-21-46.9-47V112.4c0-25.9 21-46.9 46.9-46.9h799.1c25.9 0 46.9 21 46.9 46.9v799.2z" /> - <path d="M207.9 342.7h608.2c32 0 57.9-25.9 57.9-57.9v-83c0-32-25.9-57.9-57.9-57.9H207.9c-32 0-57.9 25.9-57.9 57.9v83c0 32 25.9 57.9 57.9 57.9zM200 201.8c0-4.4 3.5-7.9 7.9-7.9h608.2c4.4 0 7.9 3.5 7.9 7.9v83c0 4.4-3.5 7.9-7.9 7.9H207.9c-4.4 0-7.9-3.5-7.9-7.9v-83zM806.4 405.7h-277c-37.3 0-67.6 30.2-67.6 67.6v363.2c0 37.3 30.2 67.6 67.6 67.6h277c37.3 0 67.6-30.2 67.6-67.6V473.3c0-37.4-30.2-67.6-67.6-67.6zM824 836.4c0 9.7-7.9 17.6-17.6 17.6h-277c-9.7 0-17.6-7.9-17.6-17.6V473.3c0-9.7 7.9-17.6 17.6-17.6h277c9.7 0 17.6 7.9 17.6 17.6v363.1zM272 649.7c67.4 0 122-54.6 122-122s-54.6-122-122-122-122 54.6-122 122 54.6 122 122 122z m0-204c45.2 0 82 36.8 82 82s-36.8 82-82 82-82-36.8-82-82 36.8-82 82-82z" /> - </SVGIcon> - ); -} -IconPage.displayName = 'Page'; diff --git a/packages/designer/src/icons/remove.tsx b/packages/designer/src/icons/remove.tsx deleted file mode 100644 index 42b80f618..000000000 --- a/packages/designer/src/icons/remove.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconRemove(props: IconProps) { - return ( - <SVGIcon viewBox="0 0 1024 1024" {...props}> - <path d="M224 256v639.84A64 64 0 0 0 287.84 960h448.32A64 64 0 0 0 800 895.84V256h64a32 32 0 1 0 0-64H160a32 32 0 1 0 0 64h64zM384 96c0-17.664 14.496-32 31.904-32h192.192C625.696 64 640 78.208 640 96c0 17.664-14.496 32-31.904 32H415.904A31.872 31.872 0 0 1 384 96z m-96 191.744C288 270.208 302.4 256 320.224 256h383.552C721.6 256 736 270.56 736 287.744v576.512C736 881.792 721.6 896 703.776 896H320.224A32.224 32.224 0 0 1 288 864.256V287.744zM352 352c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z m128 0c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z m128 0c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z" /> - </SVGIcon> - ); -} -IconRemove.displayName = 'Remove'; diff --git a/packages/designer/src/icons/setting.tsx b/packages/designer/src/icons/setting.tsx deleted file mode 100644 index 2df18029c..000000000 --- a/packages/designer/src/icons/setting.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconSetting(props: IconProps) { - return ( - <SVGIcon viewBox="0 0 1024 1024" {...props}> - <path d="M965.824 405.952a180.48 180.48 0 0 1-117.12-85.376 174.464 174.464 0 0 1-16-142.08 22.208 22.208 0 0 0-7.04-23.552 480.576 480.576 0 0 0-153.6-89.216 23.104 23.104 0 0 0-24.32 5.76 182.208 182.208 0 0 1-135.68 57.92 182.208 182.208 0 0 1-133.12-56.64 23.104 23.104 0 0 0-26.88-7.04 478.656 478.656 0 0 0-153.6 89.856 22.208 22.208 0 0 0-7.04 23.552 174.464 174.464 0 0 1-16 141.44A180.48 180.48 0 0 1 58.24 405.952a22.4 22.4 0 0 0-17.28 17.792 455.08 455.08 0 0 0 0 176.512 22.4 22.4 0 0 0 17.28 17.792 180.48 180.48 0 0 1 117.12 84.736c25.408 42.944 31.232 94.592 16 142.08a22.208 22.208 0 0 0 7.04 23.552A480.576 480.576 0 0 0 352 957.632h7.68a23.04 23.04 0 0 0 16.64-7.04 184.128 184.128 0 0 1 266.944 0c6.592 8.96 18.752 11.968 28.8 7.04a479.36 479.36 0 0 0 156.16-88.576 22.208 22.208 0 0 0 7.04-23.552 174.464 174.464 0 0 1 13.44-142.72 180.48 180.48 0 0 1 117.12-84.736 22.4 22.4 0 0 0 17.28-17.792 452.613 452.613 0 0 0 0-176.512 23.04 23.04 0 0 0-17.28-17.792z m-42.88 169.408a218.752 218.752 0 0 0-128 98.112 211.904 211.904 0 0 0-21.76 156.736 415.936 415.936 0 0 1-112 63.68 217.472 217.472 0 0 0-149.12-63.68 221.312 221.312 0 0 0-149.12 63.68 414.592 414.592 0 0 1-112-63.68c12.8-53.12 4.288-109.12-23.68-156.096A218.752 218.752 0 0 0 101.12 575.36a386.176 386.176 0 0 1 0-127.36 218.752 218.752 0 0 0 128-98.112c27.2-47.552 34.944-103.68 21.76-156.8a415.296 415.296 0 0 1 112-63.68A221.44 221.44 0 0 0 512 187.392a218.24 218.24 0 0 0 149.12-57.984 413.952 413.952 0 0 1 112 63.744 211.904 211.904 0 0 0 23.04 156.096 218.752 218.752 0 0 0 128 98.112 386.65 386.65 0 0 1 0 127.36l-1.28 0.64z" /> - <path d="M512 320.576c-105.984 0-192 85.568-192 191.104a191.552 191.552 0 0 0 192 191.104c106.112 0 192.064-85.568 192.064-191.104a190.72 190.72 0 0 0-56.256-135.168 192.448 192.448 0 0 0-135.744-55.936z m0 318.528c-70.656 0-128-57.088-128-127.424 0-70.4 57.344-127.36 128-127.36 70.72 0 128 56.96 128 127.36 0 33.792-13.44 66.176-37.44 90.112a128.32 128.32 0 0 1-90.496 37.312z" /> - </SVGIcon> - ); -} - -IconSetting.displayName = 'Setting'; diff --git a/packages/designer/src/icons/unlock.tsx b/packages/designer/src/icons/unlock.tsx deleted file mode 100644 index cff324920..000000000 --- a/packages/designer/src/icons/unlock.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconUnlock(props: IconProps) { - return ( - <SVGIcon viewBox="0 0 1024 1024" {...props}> - <path d="M384 480v-160c0-70.4 57.6-128 128-128s128 57.6 128 128v64h64v-64c0-105.6-86.4-192-192-192s-192 86.4-192 192v160H160v416h704V480H384z m416 352H224v-288h576v288z" fill="#ffffff" p-id="2813" /> - <path d="M416 736h192v-64h-192z" fill="#ffffff" p-id="2814" /> - </SVGIcon> - ); -} -IconUnlock.displayName = 'IconUnlock'; diff --git a/packages/designer/src/index.ts b/packages/designer/src/index.ts index ec45d1c49..550a94944 100644 --- a/packages/designer/src/index.ts +++ b/packages/designer/src/index.ts @@ -1,9 +1 @@ -export * from './component-meta'; -export * from './simulator'; -export * from './designer'; -export * from './document'; -export * from './project'; -export * from './builtin-simulator'; -export * from './types'; -export * from './context-menu-actions'; -export * from './widgets'; +export * from './components/designer-view'; diff --git a/packages/designer/src/locale/en-US.json b/packages/designer/src/locale/en-US.json deleted file mode 100644 index 28b489c8a..000000000 --- a/packages/designer/src/locale/en-US.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "copy": "Copy", - "remove": "Remove", - "hide": "Hide", - "lock": "Lock", - "unlock": "Unlock", - "Condition Group": "Condition Group", - "No opened document": "No opened document, open some document to editing", - "locked": "locked", - "Item": "Item" -} diff --git a/packages/designer/src/locale/zh-CN.json b/packages/designer/src/locale/zh-CN.json deleted file mode 100644 index 6ecf79786..000000000 --- a/packages/designer/src/locale/zh-CN.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "copy": "复制", - "remove": "删除", - "hide": "隐藏", - "lock": "锁定", - "unlock": "解锁", - "Condition Group": "条件组", - "No opened document": "没有打开的页面,请选择页面打开编辑", - "locked": "已锁定", - "Item": "项目" -} diff --git a/packages/designer/src/models/component-meta/component-meta.ts b/packages/designer/src/models/component-meta/component-meta.ts new file mode 100644 index 000000000..78f2a4680 --- /dev/null +++ b/packages/designer/src/models/component-meta/component-meta.ts @@ -0,0 +1,169 @@ +import { type NpmInfo, type ComponentMetaData, signal, Signal } from '@alilc/lowcode-shared'; +import type { ComponentConfigure } from './types'; + +export interface NormalizedComponentMetaData extends ComponentMetaData<ComponentConfigure> { + configure: ComponentConfigure; +} + +export interface ComponentMeta { + /** + * 组件名 + * component name + */ + readonly componentName: string; + /** + * 标题 + * title for this component + */ + readonly title: string; + /** + * 图标 + * icon config for this component + */ + readonly icon: string | undefined; + /** + * 组件 npm 信息 + * npm informations + */ + readonly npm: NpmInfo | undefined; + + /** + * 是否是「容器型」组件 + * is container node or not + */ + readonly isContainer: boolean; + /** + * 是否为「模态框」组件 + * check if this is a modal component or not. + */ + readonly isModal: boolean; + + /** + * 获取用于设置面板显示用的配置 + * get configs for Settings Panel + */ + // readonly configure: IPublicTypeFieldConfig[]; + + /** + * 当前组件的可用 Action + * available actions + */ + // readonly availableActions: IPublicTypeComponentAction[]; + + /** + * 设置 npm 信息 + * set method for npm inforamtion + * @param npm + */ + setNpm(npm: NpmInfo): void; + /** + * 获取元数据 + * get component metadata + */ + getMetadata(): Signal<NormalizedComponentMetaData>; + /** + * 设置组件元数据 + * @param metadata 组件元数据 + */ + setMetadata(metadata: ComponentMetaData): void; + /** + * 刷新元数据,会触发元数据的重新解析和刷新 + * refresh metadata + */ + // refreshMetadata(): void; + + // /** + // * 检测当前对应节点是否可被放置在父节点中 + // * check if the current node could be placed in parent node + // * @param my 当前节点 + // * @param parent 父节点 + // */ + // checkNestingUp(my: Node | IPublicTypeNodeData, parent: any): boolean; + + // /** + // * 检测目标节点是否可被放置在父节点中 + // * check if the target node(s) could be placed in current node + // * @param my 当前节点 + // * @param parent 父节点 + // */ + // checkNestingDown( + // my: Node | IPublicTypeNodeData, + // target: IPublicTypeNodeSchema | Node | IPublicTypeNodeSchema[], + // ): boolean; +} + +/** + * todo: resolve lowCode schema + */ +export function createComponentMeta( + metaData: ComponentMetaData<ComponentConfigure>, +): ComponentMeta { + const signal = signal<NormalizedComponentMetaData>(resolve(metaData)); + + function resolve(metaData: ComponentMetaData<ComponentConfigure>): NormalizedComponentMetaData { + const result: NormalizedComponentMetaData = { + ...metaData, + configure: metaData?.configure ?? {}, + }; + + // const { component } = result.configure; + // if (component) { + // this._isContainer = !!component.isContainer; + // this._isModal = !!component.isModal; + // if (component.nestingRule) { + // const { parentWhitelist, childWhitelist } = component.nestingRule; + // this.parentWhitelist = buildFilter(parentWhitelist); + // this.childWhitelist = buildFilter(childWhitelist); + // } + // } else { + // this._isContainer = false; + // this._isModal = false; + // } + + return result; + } + + const meta: ComponentMeta = { + get componentName() { + return signal.value.componentName; + }, + get title() { + return signal.value.title; + }, + get icon() { + return signal.value.icon; + }, + get npm() { + return signal.value.npm; + }, + + get isContainer() { + return signal.value.configure?.component?.isContainer; + }, + get isModal() { + return signal.value.configure?.component?.isModal; + }, + + getMetadata() { + return signal; + }, + setNpm(npm) { + signal.value.npm = npm; + }, + setMetadata(metadata) { + signal.value = resolve(metadata); + }, + }; + + Object.defineProperty(meta, '__isComponentMeta__', { + writable: false, + enumerable: false, + value: true, + }); + + return meta; +} + +export function isComponentmeta(obj: unknown): obj is ComponentMeta { + return (obj as any)?.__isComponentMeta__; +} diff --git a/packages/designer/src/models/component-meta/index.ts b/packages/designer/src/models/component-meta/index.ts new file mode 100644 index 000000000..4dd00d56e --- /dev/null +++ b/packages/designer/src/models/component-meta/index.ts @@ -0,0 +1 @@ +export * from './component-meta'; diff --git a/packages/designer/src/models/component-meta/types.ts b/packages/designer/src/models/component-meta/types.ts new file mode 100644 index 000000000..38886c377 --- /dev/null +++ b/packages/designer/src/models/component-meta/types.ts @@ -0,0 +1,18 @@ +export interface ComponentConfigure { + /** + * 属性面板配置 + */ + props?: IPublicTypeFieldConfig[]; + /** + * 组件能力配置 + */ + component?: IPublicTypeComponentConfigure; + /** + * 通用扩展面板支持性配置 + */ + supports?: ConfigureSupport; + /** + * 高级特性配置 + */ + advanced?: IPublicTypeAdvanced; +} diff --git a/packages/designer/src/models/document/document-model.ts b/packages/designer/src/models/document/document-model.ts new file mode 100644 index 000000000..e47264b27 --- /dev/null +++ b/packages/designer/src/models/document/document-model.ts @@ -0,0 +1,45 @@ +import { signal, uniqueId, type ComponentTreeRootNode } from '@alilc/lowcode-shared'; +import { type Project } from '../project'; +import { History } from './history'; + +export interface DocumentSchema extends ComponentTreeRootNode { + id: string; +} + +export interface DocumentModel { + /** + * 文档编号 + */ + readonly id: string; + + /** + * 获取当前文档所属的 project + * get project which this documentModel belongs to + */ + readonly project: Project; + + /** + * 操作历史模型实例 + */ + history: History<DocumentSchema>; +} + +export function createDocumentModel(project: Project) { + const uid = uniqueId('doc'); + const currentDocumentSchema = signal<DocumentSchema>({}); + + const documentHistory = new History(currentDocumentSchema, () => {}); + + return { + get id() { + return uid; + }, + get project() { + return project; + }, + + get history() { + return documentHistory; + }, + }; +} diff --git a/packages/designer/src/models/document/history.ts b/packages/designer/src/models/document/history.ts new file mode 100644 index 000000000..0f20c7d82 --- /dev/null +++ b/packages/designer/src/models/document/history.ts @@ -0,0 +1,303 @@ +import { type EventDisposable, createEventBus, reaction, type Signal } from '@alilc/lowcode-shared'; +import { isNil } from 'lodash-es'; + +export interface Serializer<K, T = string> { + serialize(data: K): T; + unserialize(data: T): K; +} + +interface HistoryOptions<T> { + /** + * 数据序列化函数,用于将数据转换为字符串 + */ + serializier?: Serializer<T>; + /** + * 闲置时间阈值,用于确定何时结束当前会话并开始新的会话 + */ + idleTimeThreshold?: number; +} + +interface HistoryHooks<T> { + state: (state: number) => void; + cursor: (data: T) => void; +} + +// 默认的序列化器和反序列化器 +const defaultSerializer: Serializer<any> = { + serialize: (data) => JSON.stringify(data), + unserialize: (data) => { + try { + return JSON.parse(data); + } catch (error) { + console.error('Error unserialize data:', error); + return null; + } + }, +}; + +/** + * `History`类用于管理历史记录,支持撤销和重做操作。可根据目标数据,响应式的生成历史的状态管理 + * @template T 泛型参数,代表历史记录的数据类型 + */ +export class History<T> { + #currentSession: Session; + #allSessions: Session[]; + + #serializier: Serializer<T>; + #eventBus = createEventBus<HistoryHooks<T>>('History'); + + // 书签,用于标记特定的保存点 + #bookmark = 0; + // 指示历史记录是否暂停的标志 + #isPaused = false; + // 闲置时间阈值,用于确定何时结束当前会话并开始新的会话 + #idleTimeThreshold: number; + + /** + * @param dataProvider 数据提供者函数,用于获取当前的数据状态 + * @param onRedo 重做操作的处理函数 + */ + constructor( + signal: Signal<T>, + private onRedo: (data: T) => void, + options?: HistoryOptions<T>, + ) { + this.#serializier = options?.serializier ?? defaultSerializer; + this.#idleTimeThreshold = options?.idleTimeThreshold ?? 1000; + + this.#currentSession = new Session(0, null, this.#idleTimeThreshold); + this.#allSessions = [this.#currentSession]; + + reaction( + signal, + (value) => { + if (this.#isPaused || isNil(value)) return; // 如果处于“暂停”状态,立即返回不执行后续逻辑 + + const log = this.#serializier.serialize(value); + if (this.#currentSession.data === log) return; // 不记录未发生变化的数据 + + if (this.#currentSession.isActive()) { + this.#currentSession.updateData(log); // 如果当前会话处于活跃状态,记录数据变化 + } else { + this.#endSessionAndStartNewOne(log); // 如果当前会话已结束,结束当前的并启动新的会话 + } + }, + { immediate: true }, // 初始化时立即执行副作用 + ); + } + + #endSessionAndStartNewOne(log: string) { + this.#currentSession.end(); + + const lastState = this.#getState(); + const newCursor = this.#currentSession.cursor + 1; + const newSession = new Session(newCursor, log, this.#idleTimeThreshold); + + // 更新当前会话和记录数组 + this.#currentSession = newSession; + this.#allSessions.splice(newCursor, this.#allSessions.length - newCursor, newSession); + + // 如果状态发生变化,发出状态变更事件 + const currentState = this.#getState(); + if (currentState !== lastState) { + this.#eventBus.emit('state', currentState); + } + } + + get activeData() { + return this.#currentSession.data; + } + + #pauseRecording() { + this.#isPaused = true; + } + #resumeRecording() { + this.#isPaused = false; + } + + go(position: number) { + if (this.#currentSession.cursor === position) return; + + const session = this.#allSessions.find((item) => item.cursor === position); + if (!session || !session.data) return; + + this.#pauseRecording(); + + try { + this.onRedo(this.#serializier.unserialize(session.data)); + this.#eventBus.emit('cursor', session.data); + } catch (e) { + console.error(e); + } + + this.#resumeRecording(); + + this.#currentSession = session; + this.#eventBus.emit('state', this.#getState()); + } + + back() { + if (this.#currentSession.cursor === 0) return; + + this.go(this.#currentSession.cursor - 1); + } + + forward() { + if (this.#currentSession.cursor === this.#allSessions.length - 1) return; + + this.go(this.#currentSession.cursor + 1); + } + + /** + * 获取 state,判断当前是否为「可回退」、「可前进」的状态 + * get flags in number which indicat current change state + * + * | 1 | 1 | 1 | + * | -------- | -------- | -------- | + * | modified | redoable | undoable | + * eg. + * 7 means : modified && redoable && undoable + * 5 means : modified && undoable + */ + #getState(): number { + const { cursor } = this.#currentSession; + let state = 7; + // undoable ? + if (cursor <= 0) { + state -= 1; + } + // redoable ? + if (cursor >= this.#allSessions.length - 1) { + state -= 2; + } + // modified ? + if (this.#bookmark === cursor) { + state -= 4; + } + return state; + } + + /** + * 保存当前状态 + * do save current change as a record in history + */ + savePoint() { + if (!this.#currentSession) { + return; + } + this.#currentSession.end(); + this.#bookmark = this.#currentSession.cursor; + this.#eventBus.emit('state', this.#getState()); + } + + /** + * 当前是否是「保存点」,即是否有状态变更但未保存 + * check if there is unsaved change for history + */ + isSavePoint(): boolean { + return this.#bookmark !== this.#currentSession.cursor; + } + + onStateChange(listener: HistoryHooks<T>['state']): EventDisposable { + this.#eventBus.on('state', listener); + return () => { + this.#eventBus.off('state', listener); + }; + } + + onChangeCursor(listener: HistoryHooks<T>['cursor']): EventDisposable { + this.#eventBus.on('cursor', listener); + return () => { + this.#eventBus.off('cursor', listener); + }; + } + + destroy() { + this.#eventBus.removeAllHooks(); + this.#allSessions = []; + } +} + +/** + * 会话类,用于管理具有生命周期的数据。 + */ +class Session { + #data: string | null; // 会话存储的数据。 + #activeTimer: ReturnType<typeof setTimeout> | undefined; // 活跃状态的计时器。 + + /** + * 构造函数。 + * @param cursor 用于标识会话的游标,一般代表会话的序号或状态标识。 + * @param initialData 会话初始数据。 + * @param timeGap 会话活跃状态持续时间,默认1000毫秒。 + */ + constructor( + readonly cursor: number, + initialData: string | null, + private timeGap: number = 1000, + ) { + this.initializeTimer(); // 初始化时设置计时器。 + this.updateData(initialData); // 记录初始数据。 + } + + /** + * 获取当前会话数据。 + * @returns 当前会话存储的数据。 + */ + get data() { + return this.#data; + } + + /** + * 更新会话数据并重新设置计时器。 + * @param newData 新数据。 + */ + updateData(newData: string | null) { + if (!this.isActive()) return; + + this.#data = newData; + this.resetTimer(); + } + + /** + * 检查会话是否活跃。 + * @returns 如果会话活跃返回true,否则返回false。 + */ + isActive() { + return this.#activeTimer !== undefined; + } + + /** + * 结束会话,清除计时器。 + */ + end() { + if (this.isActive()) { + this.clearTimer(); + } + } + + /** + * 设置计时器,计时结束后会话自动结束。 + */ + initializeTimer() { + this.clearTimer(); + this.#activeTimer = setTimeout(() => this.end(), this.timeGap); + } + + /** + * 清除计时器,并将活跃计时器设为undefined。 + */ + clearTimer() { + if (this.#activeTimer) { + clearTimeout(this.#activeTimer); + } + this.#activeTimer = undefined; + } + + /** + * 重置计时器,保持会话活跃状态。 + */ + resetTimer() { + this.initializeTimer(); + } +} diff --git a/packages/designer/src/models/document/index.ts b/packages/designer/src/models/document/index.ts new file mode 100644 index 000000000..7eb5d0a7f --- /dev/null +++ b/packages/designer/src/models/document/index.ts @@ -0,0 +1 @@ +export * from './document-model'; diff --git a/packages/designer/src/models/node/index.ts b/packages/designer/src/models/node/index.ts new file mode 100644 index 000000000..b81b57e97 --- /dev/null +++ b/packages/designer/src/models/node/index.ts @@ -0,0 +1,3 @@ +export * from './prop'; +export * from './types'; +export * from './node'; diff --git a/packages/designer/src/models/node/node.ts b/packages/designer/src/models/node/node.ts new file mode 100644 index 000000000..eceac848d --- /dev/null +++ b/packages/designer/src/models/node/node.ts @@ -0,0 +1,358 @@ +import { ComponentTreeNode } from '@alilc/lowcode-shared'; +import { type ComponentMeta } from '../component-meta'; +import { type Prop } from './prop'; + +export interface Node<Schema extends ComponentTreeNode = ComponentTreeNode> { + /** + * 节点 id + * node id + */ + readonly id: string; + /** + * 节点标题 + * title of node + */ + readonly title: string; + + /** + * 节点 componentName + * componentName + */ + readonly componentName: string; + /** + * 节点的物料元数据 + * get component meta of this node + */ + readonly componentMeta: ComponentMeta | null; + + /** + * 是否为「容器型」节点 + * check if node is a container type node + */ + readonly isContainerNode: boolean; + /** + * 是否为根节点 + * check if node is root in the tree + */ + readonly isRootNode: boolean; + /** + * 是否为空节点(无 children 或者 children 为空) + * check if current node is empty, which means no children or children is empty + */ + readonly isEmptyNode: boolean; + /** + * 是否为 Page 节点 + * check if node is Page + */ + readonly isPageNode: boolean; + /** + * 是否为 Component 节点 + * check if node is Component + */ + readonly isComponentNode: boolean; + /** + * 是否为「模态框」节点 + * check if node is Modal + */ + readonly isModalNode: boolean; + /** + * 是否为插槽节点 + * check if node is a Slot + */ + readonly isSlotNode: boolean; + /** + * 是否为父类/分支节点 + * check if node a parental node + */ + readonly isParentalNode: boolean; + /** + * 是否为叶子节点 + * check if node is a leaf node in tree + */ + readonly isLeafNode: boolean; + + /** + * 获取当前节点的锁定状态 + * check if current node is locked + */ + readonly isLocked: boolean; + /** + * 下标 + * index + */ + readonly index: number | undefined; + /** + * 图标 + * get icon of this node + */ + readonly icon: string; + /** + * 节点所在树的层级深度,根节点深度为 0 + * depth level of this node, value of root node is 0 + */ + readonly zLevel: number; + + /** + * 获取节点所属的文档模型对象 + * get documentModel of this node + */ + readonly document: Document | null; + + /** + * 获取当前节点的前一个兄弟节点 + * get previous sibling of this node + */ + readonly prevSibling: Node | null | undefined; + + /** + * 获取当前节点的后一个兄弟节点 + * get next sibling of this node + */ + readonly nextSibling: Node | null | undefined; + + /** + * 获取当前节点的父亲节点 + * get parent of this node + */ + readonly parent: Node | null; + + /** + * 获取当前节点的孩子节点模型 + * get children of this node + */ + readonly children: NodeChildren | null; + + /** + * 节点上挂载的插槽节点们 + * get slots of this node + */ + readonly slots: Node[]; + + /** + * 当前节点为插槽节点时,返回节点对应的属性实例 + * return coresponding prop when this node is a slot node + */ + readonly slotFor: Prop | null | undefined; + + /** + * 节点的属性集 + * get props + */ + readonly props: Props | null; + /** + * 节点的属性集 + * get props data + */ + readonly propsData: IPublicTypePropsMap | IPublicTypePropsList | null; + + /** + * conditionGroup + */ + readonly conditionGroup: ExclusiveGroup | null; + + /** + * 获取符合搭建协议 - 节点 schema 结构 + * get schema of this node + */ + readonly schema: Schema; + + /** + * 获取对应的 setting entry + * get setting entry of this node + */ + readonly settingEntry: SettingTopEntry; + + /** + * 磁贴布局节点 + */ + isRGLContainerNode: boolean; + + /** + * 当前节点是否可见 + * check if current node is visible + */ + visible: boolean; + + /** + * 返回节点的尺寸、位置信息 + * get rect information for this node + */ + getRect(): DOMRect | null; + /** + * 是否有挂载插槽节点 + * check if current node has slots + */ + hasSlots(): boolean; + /** + * 是否设定了渲染条件 + * check if current node has condition value set + */ + hasCondition(): boolean; + /** + * 是否设定了循环数据 + * check if loop is set for this node + */ + hasLoop(): boolean; + /** + * 获取指定 path 的属性模型实例 + * get prop by path + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param createIfNone 如果不存在,是否新建,默认为 true + */ + getProp(path: string | number, createIfNone?: boolean): Prop | null; + /** + * 获取指定 path 的属性模型实例值 + * get prop value by path + * @param path 属性路径,支持 a / a.b / a.0 等格式 + */ + getPropValue(path: string): any; + /** + * 获取指定 path 的属性模型实例, + * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 + * + * get extra prop by path, an extra prop means a prop not exists in the `props` + * but as siblint of the `props` + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param createIfNone 当没有属性的时候,是否创建一个属性 + */ + getExtraProp(path: string, createIfNone?: boolean): Prop | null; + /** + * 获取指定 path 的属性模型实例, + * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 + * + * get extra prop value by path, an extra prop means a prop not exists in the `props` + * but as siblint of the `props` + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @returns + */ + getExtraPropValue(path: string): any; + /** + * 设置指定 path 的属性模型实例值 + * set value for prop with path + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param value 值 + */ + setPropValue(path: string | number, value: IPublicTypeCompositeValue): void; + /** + * 设置指定 path 的属性模型实例值 + * set value for extra prop with path + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param value 值 + */ + setExtraPropValue(path: string, value: IPublicTypeCompositeValue): void; + /** + * 导入节点数据 + * import node schema + * @param data + */ + importSchema(data: Schema): void; + /** + * 导出节点数据 + * export schema from this node + * @param stage + * @param options + */ + exportSchema(stage: IPublicEnumTransformStage, options?: any): Schema; + /** + * 在指定位置之前插入一个节点 + * insert a node befor current node + * @param node + * @param ref + * @param useMutator + */ + insertBefore(node: Node, ref?: Node | undefined, useMutator?: boolean): void; + /** + * 在指定位置之后插入一个节点 + * insert a node after this node + * @param node + * @param ref + * @param useMutator + */ + insertAfter(node: Node, ref?: Node | undefined, useMutator?: boolean): void; + /** + * 替换指定节点 + * replace a child node with data provided + * @param node 待替换的子节点 + * @param data 用作替换的节点对象或者节点描述 + * @returns + */ + replaceChild(node: Node, data: any): Node | null; + /** + * 将当前节点替换成指定节点描述 + * replace current node with a new node schema + * @param schema + */ + replaceWith(schema: Schema): any; + /** + * 选中当前节点实例 + * select current node + */ + select(): void; + /** + * 设置悬停态 + * set hover value for current node + * @param flag + */ + hover(flag: boolean): void; + /** + * 设置节点锁定状态 + * set lock value for current node + * @param flag + */ + lock(flag?: boolean): void; + /** + * 删除当前节点实例 + * remove current node + */ + remove(): void; + /** + * 执行新增、删除、排序等操作 + * excute remove/add/sort operations on node`s children + */ + mergeChildren( + remover: (node: Node, idx: number) => boolean, + adder: (children: Node[]) => any, + sorter: (firstNode: Node, secondNode: Node) => number, + ): any; + /** + * 当前节点是否包含某子节点 + * check if current node contains another node as a child + * @param node + */ + contains(node: Node): boolean; + /** + * 是否可执行某 action + * check if current node can perform certain aciton with actionName + * @param actionName action 名字 + */ + canPerformAction(actionName: string): boolean; + /** + * 获取该节点的 ConditionalVisible 值 + * check if current node ConditionalVisible + */ + isConditionalVisible(): boolean | undefined; + /** + * 设置该节点的 ConditionalVisible 为 true + * make this node as conditionalVisible === true + */ + setConditionalVisible(): void; + /** + * 获取节点实例对应的 dom 节点 + */ + getDOMNode(): HTMLElement; + /** + * 获取磁贴相关信息 + */ + getRGL(): { + isContainerNode: boolean; + isEmptyNode: boolean; + isRGLContainerNode: boolean; + isRGLNode: boolean; + isRGL: boolean; + rglNode: Node | null; + }; +} + +export function createNode<Schema extends ComponentTreeNode>(nodeSchema: Schema): Node<Schema> { + return {}; +} diff --git a/packages/designer/src/models/node/prop.ts b/packages/designer/src/models/node/prop.ts new file mode 100644 index 000000000..39c7d92c9 --- /dev/null +++ b/packages/designer/src/models/node/prop.ts @@ -0,0 +1 @@ +export interface Prop {} diff --git a/packages/designer/src/models/node/types.ts b/packages/designer/src/models/node/types.ts new file mode 100644 index 000000000..a4bf74767 --- /dev/null +++ b/packages/designer/src/models/node/types.ts @@ -0,0 +1,37 @@ +/** + * 基础节点 + * + * [Node Properties] + * componentName: Page/Block/Component + * props + * children + * + * [Directives] + * loop + * loopArgs + * condition + * + * ------- addition support ----- + * conditionGroup use for condition, for exclusive + * title display on outline + * ignored ignore this node will not publish to render, but will store + * isLocked can not select/hover/ item on canvas and outline + * hidden not visible on canvas + * slotArgs like loopArgs, for slot node + */ +export interface NormalNode {} + +/** + * 根容器节点 + * + * [Root Container Extra Properties] + * fileName + * meta + * state + * defaultProps + * dataSource + * lifeCycles + * methods + * css + */ +export interface RootNode extends NormalNode {} diff --git a/packages/designer/src/models/project/index.ts b/packages/designer/src/models/project/index.ts new file mode 100644 index 000000000..e984fc51b --- /dev/null +++ b/packages/designer/src/models/project/index.ts @@ -0,0 +1 @@ +export * from './project'; diff --git a/packages/designer/src/models/project/project.ts b/packages/designer/src/models/project/project.ts new file mode 100644 index 000000000..416ddcb0d --- /dev/null +++ b/packages/designer/src/models/project/project.ts @@ -0,0 +1,121 @@ +import { type EventDisposable, type Project as ProjectDescriptor } from '@alilc/lowcode-shared'; +import { type DocumentModel, type DocumentSchema } from '../document'; + +export interface ProjectSchema extends ProjectDescriptor {} + +const initialSchemaTemplate: ProjectSchema = { + version: '1.0.0', + componentsMap: [], + componentsTree: [], + i18n: {}, +}; + +/** + * 项目类 + * 负责项目的管理 + */ +export class Project { + #schema: ProjectSchema; + + #currentDocument: DocumentModel | null = null; + #documents: DocumentModel[] = []; + + constructor(schema: ProjectSchema = initialSchemaTemplate) { + this.#schema = schema; + } + + /** + * 获取当前的 document + * get current document + */ + get currentDocument(): DocumentModel | null { + return this.#currentDocument; + } + + /** + * 获取当前 project 下所有 documents + * get all documents of this project + */ + get documents(): DocumentModel[] { + return this.#documents; + } + + load(schema: ProjectSchema, openDocument: boolean | string = false) {} + + /** + * 打开一个 document + * open a document + * @param doc + * @returns + */ + openDocument(doc?: string | DocumentSchema): DocumentModel | null; + + /** + * 创建一个 document + * create a document + * @param data + * @returns + */ + createDocument(data: DocumentSchema): DocumentModel | null; + + /** + * 删除一个 document + * remove a document + * @param doc + */ + removeDocument(doc: DocumentModel): void; + + /** + * 获取当前的 document + * get current document + */ + getCurrentDocument(): DocumentModel | null; + + /** + * 根据 fileName 获取 document + * get a document by filename + * @param fileName + */ + getDocumentByFileName(fileName: string): DocumentModel | null; + + /** + * 根据 id 获取 document + * get a document by id + * @param id + */ + getDocumentById(id: string): DocumentModel | null; + + /** + * 导出 project + * export project to schema + * @returns + */ + exportSchema(stage: IPublicEnumTransformStage): ProjectSchema; + + /** + * 导入 project schema + * import schema to project + * @param schema 待导入的 project 数据 + */ + importSchema(schema?: ProjectSchema): void; + + /** + * 绑定删除文档事件 + * set callback for event onDocumentRemoved + */ + onRemoveDocument(fn: (data: { id: string }) => void): EventDisposable; + + /** + * 当前 project 内的 document 变更事件 + * set callback for event onDocumentChanged + */ + onChangeDocument(fn: (doc: DocumentModel) => void): EventDisposable; + + /** + * 设置多语言语料 + * + * set I18n data for this project + * @param value object + */ + setI18n(value: object): void; +} diff --git a/packages/designer/src/models/simulator/index.ts b/packages/designer/src/models/simulator/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/designer/src/project/index.ts b/packages/designer/src/project/index.ts deleted file mode 100644 index 3ac1213cc..000000000 --- a/packages/designer/src/project/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './project'; -export * from './project-view'; diff --git a/packages/designer/src/project/project-view.tsx b/packages/designer/src/project/project-view.tsx deleted file mode 100644 index 48b8d14f5..000000000 --- a/packages/designer/src/project/project-view.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Component } from 'react'; -import { observer, engineConfig } from '@alilc/lowcode-editor-core'; -import { Designer } from '../designer'; -import { BuiltinSimulatorHostView } from '../builtin-simulator'; - -import './project.less'; - -export class BuiltinLoading extends Component { - render() { - return ( - <div id="engine-loading-wrapper"> - <img width="154" height="100" src="https://img.alicdn.com/tfs/TB1CmVgayERMeJjy0FcXXc7opXa-308-200.gif" /> - </div> - ); - } -} - -@observer -export class ProjectView extends Component<{ designer: Designer }> { - componentDidMount() { - const { designer } = this.props; - const { project } = designer; - - project.onRendererReady(() => { - this.forceUpdate(); - }); - } - render() { - const { designer } = this.props; - const { project, projectSimulatorProps: simulatorProps } = designer; - const Simulator = designer.simulatorComponent || BuiltinSimulatorHostView; - const Loading = engineConfig.get('loadingComponent', BuiltinLoading); - - return ( - <div className="lc-project"> - <div className="lc-simulator-shell"> - {!project?.simulator?.renderer && <Loading />} - <Simulator {...simulatorProps} /> - </div> - </div> - ); - } -} diff --git a/packages/designer/src/project/project.less b/packages/designer/src/project/project.less deleted file mode 100644 index 38f1f7442..000000000 --- a/packages/designer/src/project/project.less +++ /dev/null @@ -1,31 +0,0 @@ -.lc-project { - position: absolute; - top: 0; - right: 0; - width: 100%; - height: 100%; - .lc-project-empty { - width: 100%; - height: 100%; - font-size: 16px; - text-align: center; - background: transparent url(//img.alicdn.com/tfs/TB1xLKQAbj1gK0jSZFuXXcrHpXa-90-90.png) center 30% no-repeat; - padding-top: 50%; - } - - .lc-simulator { - background-color: var(--color-background, rgb(237, 239, 243)); - } - - .lc-simulator-shell { - width: 100%; - height: 100%; - } -} - -#engine-loading-wrapper { - display: flex; - justify-content: center; - align-items: center; - height: 100vh; -} diff --git a/packages/designer/src/project/project.ts b/packages/designer/src/project/project.ts deleted file mode 100644 index bcb9b0dcc..000000000 --- a/packages/designer/src/project/project.ts +++ /dev/null @@ -1,406 +0,0 @@ -import { observable, computed, makeObservable, action, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; -import { IDesigner } from '../designer'; -import { DocumentModel, isDocumentModel } from '../document'; -import type { IDocumentModel } from '../document'; -import { IPublicEnumTransformStage } from '@alilc/lowcode-types'; -import type { - IBaseApiProject, - IPublicTypeProjectSchema, - IPublicTypeRootSchema, - IPublicTypeComponentsMap, - IPublicTypeSimulatorRenderer, -} from '@alilc/lowcode-types'; -import { isLowCodeComponentType, isProCodeComponentType } from '@alilc/lowcode-utils'; -import { ISimulatorHost } from '../simulator'; - -export interface IProject extends Omit<IBaseApiProject< - IDocumentModel ->, -'simulatorHost' | -'importSchema' | -'exportSchema' | -'openDocument' | -'getDocumentById' | -'getCurrentDocument' | -'addPropsTransducer' | -'onRemoveDocument' | -'onChangeDocument' | -'onSimulatorHostReady' | -'onSimulatorRendererReady' | -'setI18n' | -'setConfig' | -'currentDocument' | -'selection' | -'documents' | -'createDocument' | -'getDocumentByFileName' -> { - - get designer(): IDesigner; - - get simulator(): ISimulatorHost | null; - - get currentDocument(): IDocumentModel | null | undefined; - - get documents(): IDocumentModel[]; - - get i18n(): { - [local: string]: { - [key: string]: any; - }; - }; - - mountSimulator(simulator: ISimulatorHost): void; - - open(doc?: string | IDocumentModel | IPublicTypeRootSchema): IDocumentModel | null; - - getDocumentByFileName(fileName: string): IDocumentModel | null; - - createDocument(data?: IPublicTypeRootSchema): IDocumentModel; - - load(schema?: IPublicTypeProjectSchema, autoOpen?: boolean | string): void; - - getSchema( - stage?: IPublicEnumTransformStage, - ): IPublicTypeProjectSchema; - - getDocument(id: string): IDocumentModel | null; - - onCurrentDocumentChange(fn: (doc: IDocumentModel) => void): () => void; - - onSimulatorReady(fn: (simulator: ISimulatorHost) => void): () => void; - - onRendererReady(fn: () => void): () => void; - - /** - * 分字段设置储存数据,不记录操作记录 - */ - set<T extends keyof IPublicTypeProjectSchema>(key: T, value: IPublicTypeProjectSchema[T]): void; - set(key: string, value: unknown): void; - - /** - * 分字段获取储存数据 - */ - get<T extends keyof IPublicTypeProjectSchema>(key: T): IPublicTypeProjectSchema[T]; - get<T>(key: string): T; - get(key: string): unknown; - - checkExclusive(activeDoc: DocumentModel): void; - - setRendererReady(renderer: IPublicTypeSimulatorRenderer<any, any>): void; -} - -export class Project implements IProject { - private emitter: IEventBus = createModuleEventBus('Project'); - - @observable.shallow readonly documents: IDocumentModel[] = []; - - private data: IPublicTypeProjectSchema = { - version: '1.0.0', - componentsMap: [], - componentsTree: [], - i18n: {}, - }; - - private _simulator?: ISimulatorHost; - - private isRendererReady: boolean = false; - - /** - * 模拟器 - */ - get simulator(): ISimulatorHost | null { - return this._simulator || null; - } - - @computed get currentDocument(): IDocumentModel | null | undefined { - return this.documents.find((doc) => doc.active); - } - - @observable private _config: any = {}; - @computed get config(): any { - // TODO: parse layout Component - return this._config; - } - set config(value: any) { - this._config = value; - } - - @observable.ref private _i18n: any = {}; - @computed get i18n(): any { - return this._i18n; - } - set i18n(value: any) { - this._i18n = value || {}; - } - - private documentsMap = new Map<string, DocumentModel>(); - - constructor(readonly designer: IDesigner, schema?: IPublicTypeProjectSchema, readonly viewName = 'global') { - makeObservable(this); - this.load(schema); - } - - private getComponentsMap(): IPublicTypeComponentsMap { - return this.documents.reduce<IPublicTypeComponentsMap>(( - componentsMap: IPublicTypeComponentsMap, - curDoc: IDocumentModel, - ): IPublicTypeComponentsMap => { - const curComponentsMap = curDoc.getComponentsMap(); - if (Array.isArray(curComponentsMap)) { - curComponentsMap.forEach((item) => { - const found = componentsMap.find((eItem) => { - if ( - isProCodeComponentType(eItem) && - isProCodeComponentType(item) && - eItem.package === item.package && - eItem.componentName === item.componentName - ) { - return true; - } else if ( - isLowCodeComponentType(eItem) && - eItem.componentName === item.componentName - ) { - return true; - } - return false; - }); - if (found) return; - componentsMap.push(item); - }); - } - return componentsMap; - }, [] as IPublicTypeComponentsMap); - } - - /** - * 获取项目整体 schema - */ - getSchema( - stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save, - ): IPublicTypeProjectSchema { - return { - ...this.data, - componentsMap: this.getComponentsMap(), - componentsTree: this.documents - .filter((doc) => !doc.isBlank()) - .map((doc) => doc.export(stage) || {} as IPublicTypeRootSchema), - i18n: this.i18n, - }; - } - - /** - * 替换当前 document 的 schema,并触发渲染器的 render - * @param schema - */ - setSchema(schema?: IPublicTypeProjectSchema) { - // FIXME: 这里的行为和 getSchema 并不对等,感觉不太对 - const doc = this.documents.find((doc) => doc.active); - doc && schema?.componentsTree[0] && doc.import(schema?.componentsTree[0]); - this.simulator?.rerender(); - } - - /** - * 整体设置项目 schema - * - * @param autoOpen true 自动打开文档 string 指定打开的文件 - */ - @action - load(schema?: IPublicTypeProjectSchema, autoOpen?: boolean | string) { - this.unload(); - // load new document - this.data = { - version: '1.0.0', - componentsMap: [], - componentsTree: [], - i18n: {}, - ...schema, - }; - this.config = schema?.config || this.config; - this.i18n = schema?.i18n || this.i18n; - - if (autoOpen) { - if (autoOpen === true) { - // auto open first document or open a blank page - // this.open(this.data.componentsTree[0]); - const documentInstances = this.data.componentsTree.map((data) => this.createDocument(data)); - // TODO: 暂时先读 config tabBar 里的值,后面看整个 layout 结构是否能作为引擎规范 - if (this.config?.layout?.props?.tabBar?.items?.length > 0) { - // slice(1) 这个贼不雅,默认任务 fileName 是类'/fileName'的形式 - documentInstances - .find((i) => i.fileName === this.config.layout.props.tabBar.items[0].path?.slice(1)) - ?.open(); - } else { - documentInstances[0].open(); - } - } else { - // auto open should be string of fileName - this.open(autoOpen); - } - } - } - - /** - * 卸载当前项目数据 - */ - unload() { - if (this.documents.length < 1) { - return; - } - for (let i = this.documents.length - 1; i >= 0; i--) { - this.documents[i].remove(); - } - } - - removeDocument(doc: IDocumentModel) { - const index = this.documents.indexOf(doc); - if (index < 0) { - return; - } - this.documents.splice(index, 1); - this.documentsMap.delete(doc.id); - } - - /** - * 分字段设置储存数据,不记录操作记录 - */ - set<T extends keyof IPublicTypeProjectSchema>(key: T, value: IPublicTypeProjectSchema[T]): void; - set(key: string, value: unknown): void; - set(key: string, value: unknown): void { - if (key === 'config') { - this.config = value; - } - if (key === 'i18n') { - this.i18n = value; - } - Object.assign(this.data, { [key]: value }); - } - - /** - * 分字段设置储存数据 - */ - get<T extends keyof IPublicTypeRootSchema>(key: T): IPublicTypeRootSchema[T]; - get<T>(key: string): T; - get(key: string): unknown; - get(key: string): any { - if (key === 'config') { - return this.config; - } - if (key === 'i18n') { - return this.i18n; - } - return Reflect.get(this.data, key); - } - - getDocument(id: string): IDocumentModel | null { - // 此处不能使用 this.documentsMap.get(id),因为在乐高 rollback 场景,document.id 会被改成其他值 - return this.documents.find((doc) => doc.id === id) || null; - } - - getDocumentByFileName(fileName: string): IDocumentModel | null { - return this.documents.find((doc) => doc.fileName === fileName) || null; - } - - @action - createDocument(data?: IPublicTypeRootSchema): IDocumentModel { - const doc = new DocumentModel(this, data || this?.data?.componentsTree?.[0]); - this.documents.push(doc); - this.documentsMap.set(doc.id, doc); - return doc; - } - - open(doc?: string | IDocumentModel | IPublicTypeRootSchema): IDocumentModel | null { - if (!doc) { - const got = this.documents.find((item) => item.isBlank()); - if (got) { - return got.open(); - } - doc = this.createDocument(); - return doc.open(); - } - if (typeof doc === 'string' || typeof doc === 'number') { - const got = this.documents.find( - (item) => item.fileName === String(doc) || String(item.id) === String(doc) - ); - if (got) { - return got.open(); - } - - const data = this.data.componentsTree.find((data) => data.fileName === String(doc)); - if (data) { - doc = this.createDocument(data); - return doc.open(); - } - - return null; - } else if (isDocumentModel(doc)) { - return doc.open(); - } - // else if (isPageSchema(doc)) { - // 暂时注释掉,影响了 diff 功能 - // const foundDoc = this.documents.find(curDoc => curDoc?.rootNode?.id && curDoc?.rootNode?.id === doc?.id); - // if (foundDoc) { - // foundDoc.remove(); - // } - // } - - doc = this.createDocument(doc); - return doc.open(); - } - - checkExclusive(activeDoc: DocumentModel) { - this.documents.forEach((doc) => { - if (doc !== activeDoc) { - doc.suspense(); - } - }); - this.emitter.emit('current-document.change', activeDoc); - } - - closeOthers(opened: DocumentModel) { - this.documents.forEach((doc) => { - if (doc !== opened) { - doc.close(); - } - }); - } - - mountSimulator(simulator: ISimulatorHost) { - // TODO: 多设备 simulator 支持 - this._simulator = simulator; - this.emitter.emit('lowcode_engine_simulator_ready', simulator); - } - - setRendererReady(renderer: any) { - this.isRendererReady = true; - this.emitter.emit('lowcode_engine_renderer_ready', renderer); - } - - onSimulatorReady(fn: (args: any) => void): () => void { - if (this._simulator) { - fn(this._simulator); - return () => {}; - } - this.emitter.on('lowcode_engine_simulator_ready', fn); - return () => { - this.emitter.removeListener('lowcode_engine_simulator_ready', fn); - }; - } - - onRendererReady(fn: () => void): () => void { - if (this.isRendererReady) { - fn(); - } - this.emitter.on('lowcode_engine_renderer_ready', fn); - return () => { - this.emitter.removeListener('lowcode_engine_renderer_ready', fn); - }; - } - - onCurrentDocumentChange(fn: (doc: IDocumentModel) => void): () => void { - this.emitter.on('current-document.change', fn); - return () => { - this.emitter.removeListener('current-document.change', fn); - }; - } -} diff --git a/packages/designer/src/simulator.ts b/packages/designer/src/simulator.ts deleted file mode 100644 index 292bc557b..000000000 --- a/packages/designer/src/simulator.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { ComponentType } from 'react'; -import { IPublicTypeComponentMetadata, IPublicTypeNodeSchema, IPublicTypeScrollable, IPublicTypeComponentInstance, IPublicModelSensor, IPublicTypeNodeInstance, IPublicTypePackage } from '@alilc/lowcode-types'; -import { Point, ScrollTarget, ILocateEvent, IDesigner } from './designer'; -import { BuiltinSimulatorRenderer } from './builtin-simulator/renderer'; -import { INode } from './document'; -import { IProject } from './project'; - -export type AutoFit = '100%'; -export const AUTO_FIT: AutoFit = '100%'; - -export interface IScrollable extends IPublicTypeScrollable { -} -export interface IViewport extends IScrollable { - - /** - * 视口大小 - */ - width: number; - height: number; - - /** - * 内容大小 - */ - contentWidth: number | AutoFit; - contentHeight: number | AutoFit; - - /** - * 内容缩放 - */ - scale: number; - - /** - * 视口矩形维度 - */ - readonly bounds: DOMRect; - - /** - * 内容矩形维度 - */ - readonly contentBounds: DOMRect; - - /** - * 视口滚动对象 - */ - readonly scrollTarget?: ScrollTarget; - - /** - * 是否滚动中 - */ - readonly scrolling: boolean; - - /** - * 内容当前滚动 X - */ - readonly scrollX: number; - - /** - * 内容当前滚动 Y - */ - readonly scrollY: number; - - /** - * 全局坐标系转化为本地坐标系 - */ - toLocalPoint(point: Point): Point; - - /** - * 本地坐标系转化为全局坐标系 - */ - toGlobalPoint(point: Point): Point; -} - -export interface DropContainer { - container: INode; - instance: IPublicTypeComponentInstance; -} - -/** - * 模拟器控制进程协议 - */ -export interface ISimulatorHost<P = object> extends IPublicModelSensor<INode> { - readonly isSimulator: true; - - /** - * 获得边界维度等信息 - */ - readonly viewport: IViewport; - readonly contentWindow?: Window; - readonly contentDocument?: Document; - readonly renderer?: BuiltinSimulatorRenderer; - - readonly project: IProject; - - readonly designer: IDesigner; - - // dependsAsset // like react jQuery lodash - // themesAsset - // componentsAsset - // simulatorUrl // - // utils, dataSource, constants 模拟 - // - // later: - // layout: ComponentName - // 获取区块代码,通过 components 传递,可异步获取 - // 设置 simulator Props - setProps(props: P): void; - // 设置单个 Prop - set(key: string, value: any): void; - - setSuspense(suspensed: boolean): void; - - // #region ========= drag and drop helpers ============= - - /** - * 设置文字拖选 - */ - setNativeSelection(enableFlag: boolean): void; - - /** - * 设置拖拽态 - */ - setDraggingState(state: boolean): void; - - /** - * 设置拷贝态 - */ - setCopyState(state: boolean): void; - - /** - * 清除所有态:拖拽态、拷贝态 - */ - clearState(): void; - - // #endregion - - /** - * 滚动视口到节点 - */ - scrollToNode(node: INode, detail?: any): void; - - /** - * 描述组件 - */ - generateComponentMetadata(componentName: string): IPublicTypeComponentMetadata; - - /** - * 根据组件信息获取组件类 - */ - getComponent(componentName: string): Component | any; - - /** - * 根据节点获取节点的组件实例 - */ - getComponentInstances(node: INode): IPublicTypeComponentInstance[] | null; - - /** - * 根据 schema 创建组件类 - */ - createComponent(schema: IPublicTypeNodeSchema): Component | null; - - /** - * 根据节点获取节点的组件运行上下文 - */ - getComponentContext(node: INode): object | null; - - getClosestNodeInstance( - from: IPublicTypeComponentInstance, specId?: string - ): IPublicTypeNodeInstance | null; - - computeRect(node: INode): DOMRect | null; - - computeComponentInstanceRect( - instance: IPublicTypeComponentInstance, selector?: string - ): DOMRect | null; - - findDOMNodes( - instance: IPublicTypeComponentInstance, selector?: string - ): Array<Element | Text> | null; - - getDropContainer(e: ILocateEvent): DropContainer | null; - - postEvent(evtName: string, evtData: any): void; - - rerender(): void; - - /** - * 销毁 - */ - purge(): void; - - setupComponents(library: IPublicTypePackage[]): Promise<void>; -} - -export function isSimulatorHost(obj: any): obj is ISimulatorHost { - return obj && obj.isSimulator; -} - -/** - * 组件类定义 - */ -export type Component = ComponentType<any> | object; - -export interface INodeSelector { - node: INode; - instance?: IPublicTypeComponentInstance; -} diff --git a/packages/designer/src/transducers/index.ts b/packages/designer/src/transducers/index.ts deleted file mode 100644 index 48299f999..000000000 --- a/packages/designer/src/transducers/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { IPublicTypeTransformedComponentMetadata as Metadata } from '@alilc/lowcode-types'; - -export function legacyIssues(metadata: Metadata): Metadata { - const { devMode } = metadata; - return { - ...metadata, - devMode: devMode?.replace(/(low|pro)code/, '$1Code') as Metadata['devMode'], - }; -} - -export function componentDefaults(metadata: Metadata): Metadata { - const { configure, componentName } = metadata; - const { component = {} } = configure; - if (!component.nestingRule) { - let m; - // uri match xx.Group set subcontrolling: true, childWhiteList - // eslint-disable-next-line no-cond-assign - if ((m = /^(.+)\.Group$/.exec(componentName))) { - // component.subControlling = true; - component.nestingRule = { - childWhitelist: [`${m[1]}`], - }; - // eslint-disable-next-line no-cond-assign - } else if ((m = /^(.+)\.Node$/.exec(componentName))) { - // uri match xx.Node set selfControlled: false, parentWhiteList - // component.selfControlled = false; - component.nestingRule = { - parentWhitelist: [`${m[1]}`, componentName], - }; - // eslint-disable-next-line no-cond-assign - } else if ((m = /^(.+)\.(Item|Node|Option)$/.exec(componentName))) { - // uri match .Item .Node .Option set parentWhiteList - component.nestingRule = { - parentWhitelist: [`${m[1]}`], - }; - } - } - // if (component.isModal == null && /Dialog/.test(componentName)) { - // component.isModal = true; - // } - return { - ...metadata, - configure: { - ...configure, - component, - }, - }; -} diff --git a/packages/designer/src/types/index.ts b/packages/designer/src/types/index.ts deleted file mode 100644 index 16528a93c..000000000 --- a/packages/designer/src/types/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type NodeRemoveOptions = { - suppressRemoveEvent?: boolean; -}; - -export enum EDITOR_EVENT { - NODE_CHILDREN_CHANGE = 'node.children.change', - - NODE_VISIBLE_CHANGE = 'node.visible.change', -} diff --git a/packages/designer/src/utils/index.ts b/packages/designer/src/utils/index.ts deleted file mode 100644 index 5b832ac95..000000000 --- a/packages/designer/src/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './slot'; -export * from './tree'; diff --git a/packages/designer/src/utils/misc.ts b/packages/designer/src/utils/misc.ts deleted file mode 100644 index 71c0fa470..000000000 --- a/packages/designer/src/utils/misc.ts +++ /dev/null @@ -1,56 +0,0 @@ -import Viewport from '../builtin-simulator/viewport'; -import { ISimulatorHost } from '../simulator'; - -export function isElementNode(domNode: Element) { - return domNode.nodeType === Node.ELEMENT_NODE; -} - -/** - * 判断节点是否在 viewport 内,判断依据:只要节点有一部分在 viewport 内,都算 true,其余情况 false - * @param domNode 待检测的节点 - * @param viewport 画布 viewport - * @returns 是否在 viewport 内 - */ -export function isDOMNodeVisible(domNode: Element, viewport: Viewport) { - const domNodeRect = domNode.getBoundingClientRect(); - const { width, height } = viewport.contentBounds; - const { left, right, top, bottom, width: nodeWidth, height: nodeHeight } = domNodeRect; - return ( - left >= -nodeWidth && - top >= -nodeHeight && - bottom <= height + nodeHeight && - right <= width + nodeWidth - ); -} - -/** - * normalize triggers - * @param triggers - */ -export function normalizeTriggers(triggers: string[]) { - return triggers.map((trigger: string) => trigger?.toUpperCase()); -} - -/** - * make a handler that listen all sensors:document, avoid frame lost - */ -export function makeEventsHandler( - boostEvent: MouseEvent | DragEvent, - sensors: ISimulatorHost[], -): (fn: (sdoc: Document) => void) => void { - const topDoc = window.document; - const sourceDoc = boostEvent.view?.document || topDoc; - const docs = new Set<Document>(); - docs.add(topDoc); - docs.add(sourceDoc); - sensors.forEach((sim) => { - const sdoc = sim.contentDocument; - if (sdoc) { - docs.add(sdoc); - } - }); - - return (handle: (sdoc: Document) => void) => { - docs.forEach((doc) => handle(doc)); - }; -} diff --git a/packages/designer/src/utils/slot.ts b/packages/designer/src/utils/slot.ts deleted file mode 100644 index 09e90f773..000000000 --- a/packages/designer/src/utils/slot.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Node } from '../document/node/node'; - -export function includeSlot(node: Node, slotName: string | undefined): boolean { - const { slots = [] } = node; - return slots.some((slot) => { - return slotName && slotName === slot?.getExtraProp('name')?.getAsString(); - }); -} - -export function removeSlot(node: Node, slotName: string | undefined): boolean { - const { slots = [] } = node; - return slots.some((slot, idx) => { - if (slotName && slotName === slot?.getExtraProp('name')?.getAsString()) { - slot.remove(); - slots.splice(idx, 1); - return true; - } - return false; - }); -} diff --git a/packages/designer/src/utils/tree.ts b/packages/designer/src/utils/tree.ts deleted file mode 100644 index 7e4d66eea..000000000 --- a/packages/designer/src/utils/tree.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NodeChildren } from '../document/node/node-children'; - -type IterableArray = NodeChildren | any[]; - -export function foreachReverse( - arr: IterableArray, - action: (item: any) => void, - getter: (arr: IterableArray, index: number) => any, - context: any = {}, -) { - for (let i = arr.length - 1; i >= 0; i--) { - action.call(context, getter(arr, i)); - } -} diff --git a/packages/designer/src/widgets/index.ts b/packages/designer/src/widgets/index.ts deleted file mode 100644 index f6eb9a1ff..000000000 --- a/packages/designer/src/widgets/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './tip'; -export * from './title'; diff --git a/packages/designer/src/widgets/tip/help-tips.tsx b/packages/designer/src/widgets/tip/help-tips.tsx deleted file mode 100644 index 17fe07edf..000000000 --- a/packages/designer/src/widgets/tip/help-tips.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { IPublicTypeHelpTipConfig, IPublicTypeTipConfig } from '@alilc/lowcode-types'; -import { Tip } from './tip'; -import { Icon } from '@alifd/next'; -import { IconProps } from '@alifd/next/types/icon'; - -export function HelpTip({ - help, - direction = 'top', - size = 'small', -}: { - help: IPublicTypeHelpTipConfig; - direction?: IPublicTypeTipConfig['direction']; - size?: IconProps['size']; -}) { - if (typeof help === 'string') { - return ( - <div> - <Icon type="help" size={size} className="lc-help-tip" /> - <Tip direction={direction}>{help}</Tip> - </div> - ); - } - - if (typeof help === 'object' && help.url) { - return ( - <div> - <a href={help.url} target="_blank" rel="noopener noreferrer"> - <Icon type="help" size={size} className="lc-help-tip" /> - </a> - <Tip direction={direction}>{help.content}</Tip> - </div> - ); - } - return ( - <div> - <Icon type="help" size="small" className="lc-help-tip" /> - <Tip direction={direction}>{help.content}</Tip> - </div> - ); -} diff --git a/packages/designer/src/widgets/tip/index.ts b/packages/designer/src/widgets/tip/index.ts deleted file mode 100644 index d2b376800..000000000 --- a/packages/designer/src/widgets/tip/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import './style.less'; - -export * from './tip'; -export * from './tip-container'; -export * from './help-tips'; diff --git a/packages/designer/src/widgets/tip/style.less b/packages/designer/src/widgets/tip/style.less deleted file mode 100644 index 602886d06..000000000 --- a/packages/designer/src/widgets/tip/style.less +++ /dev/null @@ -1,215 +0,0 @@ -@keyframes shake { - from, - to { - margin: 0; - } - 20%, - 60% { - margin: 0 10px 0 -10px; - } - 40%, - 80% { - margin: 0 -10px 0 10px; - } -} - -@keyframes drop { - from { - transform: translateY(-100%); - } - - to { - transform: translateY(0); - } -} - -@keyframes appear-left { - from { - transform: translateX(8px); - opacity: 0.8; - } - - to { - transform: translateX(0); - opacity: 1; - } -} -@keyframes appear-right { - from { - transform: translateX(-8px); - opacity: 0.8; - } - - to { - transform: translateX(0); - opacity: 1; - } -} -@keyframes appear-top { - from { - transform: translateY(8px); - opacity: 0.8; - } - - to { - transform: translateY(0); - opacity: 1; - } -} -@keyframes appear-bottom { - from { - transform: translateY(-8px); - opacity: 0.8; - } - - to { - transform: translateY(0); - opacity: 1; - } -} - -@keyframes scale { - from { - transform: scale(0.9); - } - - to { - transform: scale(1); - } -} - -@keyframes spining { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@keyframes pulse { - from, - to { - transform: scale(1); - opacity: 0.7; - } - 50% { - transform: scale(1.02); - opacity: 1; - } -} - -.lc-arrow { - position: absolute; - width: 36px; - height: 10px; - box-sizing: border-box; - overflow: hidden; - &:after { - content: ''; - display: block; - width: 0; - height: 0; - margin: 0 auto; - border: 8px solid transparent; - border-top-color: var(--color-pane-background, rgb(255, 255, 255)); - } - transform-origin: 0 0; -} - -.lc-align-top > .lc-arrow { - bottom: 0; - left: 0; - transform: translateY(100%); -} - -.lc-align-right > .lc-arrow { - left: 0; - top: 0; - transform: rotate(90deg); -} - -.lc-align-left > .lc-arrow { - right: 0; - top: 0; - transform-origin: right top; - transform: rotate(-90deg); -} - -.lc-align-bottom > .lc-arrow { - top: 0; - left: 0; - transform: scaleY(-1); -} - - -.lc-tip { - z-index: 2; - position: fixed; - box-sizing: border-box; - background: var(--color-layer-tooltip-background); - max-height: 400px; - color: var(--color-text-reverse, rgba(255, 255, 255, 0.8)); - left: 0; - top: 0; - visibility: hidden; - opacity: 0; - border-radius: 3px; - padding: 6px 8px; - text-shadow: 0 -1px var(--color-field-label, rgba(0, 0, 0, 0.3)); - font-size: var(--font-size-text); - line-height: 14px; - max-width: 200px; - pointer-events: none; - &.lc-align-top { - transform: translateY(8px); - } - &.lc-align-bottom { - transform: translateY(-8px); - } - &.lc-align-left { - transform: translateX(8px); - } - &.lc-align-right { - transform: translateX(-8px); - } - .lc-arrow { - width: 24px; - height: 8px; - &:after { - border: 6px solid transparent; - border-top-color: var(--color-layer-tooltip-background, rgba(0, 0, 0, 0.7)); - } - } - &.lc-theme-black { - background: var(--color-icon-pane, rgba(0, 0, 0, 0.7)); - .lc-arrow:after { - border-top-color: var(--color-layer-tooltip-background, rgba(0, 0, 0, 0.7)); - } - } - &.lc-theme-green { - background: var(--color-success-dark, var(--color-function-success-dark, #57a672)); - .lc-arrow:after { - border-top-color: var(--color-success-dark, var(--color-function-success-dark, #57a672)); - } - } - &.lc-visible { - visibility: visible; - } - &.lc-visible-animate { - visibility: visible; - opacity: 1; - transition: transform ease-out 200ms, opacity ease-out 200ms; - } - - will-change: transform, width, height, opacity, left, top; -} - -.lc-tips-container { - pointer-events: none; - position: fixed; - top: 0; - left: 0; - overflow: visible; - z-index: 2000; -} diff --git a/packages/designer/src/widgets/tip/tip-container.tsx b/packages/designer/src/widgets/tip/tip-container.tsx deleted file mode 100644 index ed3af589a..000000000 --- a/packages/designer/src/widgets/tip/tip-container.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Component } from 'react'; -import ReactDOM from 'react-dom'; -import { TipItem } from './tip-item'; -import { tipHandler } from './tip-handler'; - -export class TipContainer extends Component { - private dispose?: () => void; - shouldComponentUpdate() { - return false; - } - componentDidMount() { - const over = (e: MouseEvent) => tipHandler.setTarget(e.target as any); - const down = () => tipHandler.hideImmediately(); - document.addEventListener('mouseover', over, false); - document.addEventListener('mousedown', down, true); - this.dispose = () => { - document.removeEventListener('mouseover', over, false); - document.removeEventListener('mousedown', down, true); - }; - } - - UNSAFE_componentWillMount() { - if (this.dispose) { - this.dispose(); - } - } - - render() { - return ReactDOM.createPortal( - <div className="lc-tips-container"> - <TipItem /> - </div>, - document.querySelector('body')!, - ); - } -} diff --git a/packages/designer/src/widgets/tip/tip-handler.ts b/packages/designer/src/widgets/tip/tip-handler.ts deleted file mode 100644 index 63193ea85..000000000 --- a/packages/designer/src/widgets/tip/tip-handler.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { IPublicTypeTipConfig } from '@alilc/lowcode-types'; -import { IEventBus, createModuleEventBus } from '../../event-bus'; - -export interface TipOptions extends IPublicTypeTipConfig { - target: HTMLElement; -} - -class TipHandler { - tip: TipOptions | null = null; - - private showDelay: number | null = null; - - private hideDelay: number | null = null; - - private emitter: IEventBus = createModuleEventBus('TipHandler'); - - setTarget(target: HTMLElement) { - const tip = findTip(target); - if (tip) { - if (this.tip) { - // the some target should return - if ((this.tip as any).target === (tip as any).target) { - this.tip = tip; - return; - } - // not show already, reset show delay - if (this.showDelay) { - clearTimeout(this.showDelay); - this.showDelay = null; - this.tip = null; - } else { - if (this.hideDelay) { - clearTimeout(this.hideDelay); - this.hideDelay = null; - } - this.tip = tip; - this.emitter.emit('tipchange'); - return; - } - } - - this.tip = tip; - if (this.hideDelay) { - clearTimeout(this.hideDelay); - this.hideDelay = null; - this.emitter.emit('tipchange'); - } else { - this.showDelay = setTimeout(() => { - this.showDelay = null; - this.emitter.emit('tipchange'); - }, 350) as any; - } - } else { - if (this.showDelay) { - clearTimeout(this.showDelay); - this.showDelay = null; - } else { - this.hideDelay = setTimeout(() => { - this.hideDelay = null; - }, 100) as any; - } - this.tip = null; - - this.emitter.emit('tipchange'); - } - } - - hideImmediately() { - if (this.hideDelay) { - clearTimeout(this.hideDelay); - this.hideDelay = null; - } - if (this.showDelay) { - clearTimeout(this.showDelay); - this.showDelay = null; - } - this.tip = null; - this.emitter.emit('tipchange'); - } - - onChange(func: () => void) { - this.emitter.on('tipchange', func); - return () => { - this.emitter.removeListener('tipchange', func); - }; - } -} - -export const tipHandler = new TipHandler(); - -function findTip(target: HTMLElement | null): TipOptions | null { - if (!target) { - return null; - } - // optimize deep finding on mouseover - let loopupLimit = 10; - while (target && loopupLimit-- > 0) { - // get tip from target node - if (target.dataset && target.dataset.tip) { - return { - children: target.dataset.tip, - direction: (target.dataset.direction || target.dataset.dir) as any, - theme: target.dataset.theme, - target, - }; - } - - // or get tip from child nodes - let child: HTMLElement | null = target.lastElementChild as HTMLElement; - - while (child) { - if (child.dataset && child.dataset.role === 'tip') { - const { tipId } = child.dataset; - if (!tipId) { - return null; - } - const tipProps = tipsMap.get(tipId); - if (!tipProps) { - return null; - } - return { - ...tipProps, - target, - }; - } - child = child.previousElementSibling as HTMLElement; - } - - target = target.parentNode as HTMLElement; - } - - return null; -} - -const tipsMap = new Map<string, IPublicTypeTipConfig>(); -export function postTip(id: string, props: IPublicTypeTipConfig | null) { - if (props) { - tipsMap.set(id, props); - } else { - tipsMap.delete(id); - } -} diff --git a/packages/designer/src/widgets/tip/tip-item.tsx b/packages/designer/src/widgets/tip/tip-item.tsx deleted file mode 100644 index 8140e9157..000000000 --- a/packages/designer/src/widgets/tip/tip-item.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { Component } from 'react'; -import classNames from 'classnames'; -import { IPublicTypeTipConfig } from '@alilc/lowcode-types'; -import { intl } from '../../intl'; -import { resolvePosition } from './utils'; -import { tipHandler } from './tip-handler'; - -export class TipItem extends Component { - private dispose?: () => void; - - constructor(props: any) { - super(props); - this.dispose = tipHandler.onChange(() => this.forceUpdate()); - } - - shouldComponentUpdate() { - return false; - } - - componentDidMount() { - this.updateTip(); - } - - componentDidUpdate() { - this.updateTip(); - } - - componentWillUnmount() { - if (this.dispose) { - this.dispose(); - } - this.clearTimer(); - } - - private timer: number | null = null; - - clearTimer() { - if (this.timer) { - clearTimeout(this.timer); - this.timer = null; - } - } - - private shell: HTMLDivElement | null = null; - - private originClassName = ''; - - updateTip() { - if (!this.shell) { - return; - } - const { shell } = this; - const arrow = shell.querySelector('.lc-arrow') as HTMLElement; - - // reset - shell.className = this.originClassName; - shell.style.cssText = ''; - arrow.style.cssText = ''; - this.clearTimer(); - - const { tip } = tipHandler; - if (!tip) { - return; - } - - const { target, direction } = tip; - const targetRect = target.getBoundingClientRect(); - - if (targetRect.width === 0 || targetRect.height === 0) { - return; - } - - const shellRect = shell.getBoundingClientRect(); - const bounds = { - left: 1, - top: 1, - right: document.documentElement.clientWidth - 1, - bottom: document.documentElement.clientHeight - 1, - }; - - const arrowRect = arrow.getBoundingClientRect(); - const { dir, left, top, arrowLeft, arrowTop } = resolvePosition( - shellRect, - targetRect, - arrowRect, - bounds, - direction, - ); - - shell.classList.add(`lc-align-${dir}`); - shell.style.top = `${top}px`; - shell.style.left = `${left}px`; - shell.style.width = `${shellRect.width}px`; - shell.style.height = `${shellRect.height}px`; - - if (dir === 'top' || dir === 'bottom') { - arrow.style.left = `${arrowLeft}px`; - } else { - arrow.style.top = `${arrowTop}px`; - } - this.timer = window.setTimeout(() => { - shell.classList.add('lc-visible-animate'); - shell.style.transform = 'none'; - }, 10); /**/ - } - - render() { - const tip: IPublicTypeTipConfig = tipHandler.tip || ({} as any); - const className = classNames( - 'lc-tip', - tip.className, - tip && tip.theme ? `lc-theme-${tip.theme}` : null, - ); - - this.originClassName = className; - - return ( - <div - className={className} - ref={(ref) => { - this.shell = ref; - }} - > - <i className="lc-arrow" /> - <div className="lc-tip-content">{intl((tip as any).children)}</div> - </div> - ); - } -} diff --git a/packages/designer/src/widgets/tip/tip.tsx b/packages/designer/src/widgets/tip/tip.tsx deleted file mode 100644 index 92a683baa..000000000 --- a/packages/designer/src/widgets/tip/tip.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Component } from 'react'; -import { IPublicTypeTipConfig } from '@alilc/lowcode-types'; -import { uniqueId } from '@alilc/lowcode-utils'; -import { postTip } from './tip-handler'; - -export class Tip extends Component<IPublicTypeTipConfig> { - private id = uniqueId('tips$'); - - componentWillUnmount() { - postTip(this.id, null); - } - - render() { - postTip(this.id, this.props); - return <meta data-role="tip" data-tip-id={this.id} />; - } -} diff --git a/packages/designer/src/widgets/tip/utils.ts b/packages/designer/src/widgets/tip/utils.ts deleted file mode 100644 index 6dc3f6184..000000000 --- a/packages/designer/src/widgets/tip/utils.ts +++ /dev/null @@ -1,243 +0,0 @@ -function resolveEdge(popup: any, target: any, arrow: any, bounds: any) { - const sx = arrow.width > target.width ? (arrow.width - target.width) / 2 : 0; - const sy = arrow.width > target.height ? (arrow.width - target.height) / 2 : 0; - - const top = Math.max(target.top - popup.height + arrow.width - sy, bounds.top); - const right = Math.min(target.right + popup.width - arrow.width + sx, bounds.right); - const bottom = Math.min(target.bottom + popup.height - arrow.width + sy, bounds.bottom); - const left = Math.max(target.left - popup.width + arrow.width - sx, bounds.left); - - return { top, right, bottom, left }; -} - -function resolveDirection(popup: any, target: any, edge: any, bounds: any, prefers: any) { - if (prefers.forceDirection) { - return prefers.dir; - } - const extendWidth = popup.width + popup.extraOffset; - const extendHeight = popup.height + popup.extraOffset; - const SY = popup.width * extendHeight; - const SX = popup.height * extendWidth; - const mw = Math.min(edge.right - edge.left, popup.width); - const mh = Math.min(edge.bottom - edge.top, popup.height); - - const mat: any = { - top: () => { - const s = mw * Math.min(target.top - bounds.top, extendHeight); - return { s, enough: s >= SY }; - }, - bottom: () => { - const s = mw * Math.min(bounds.bottom - target.bottom, extendHeight); - return { s, enough: s >= SY }; - }, - left: () => { - const s = mh * Math.min(target.left - bounds.left, extendWidth); - return { s, enough: s >= SX }; - }, - right: () => { - const s = mh * Math.min(bounds.right - target.right, extendWidth); - return { s, enough: s >= SX }; - }, - }; - - const orders = ['top', 'right', 'bottom', 'left']; - if (prefers.dir) { - const i = orders.indexOf(prefers.dir); - if (i > -1) { - orders.splice(i, 1); - orders.unshift(prefers.dir); - } - } - let ms = 0; - let prefer = orders[0]; - for (let i = 0, l = orders.length; i < l; i++) { - const dir = orders[i]; - const { s, enough } = mat[dir](); - if (enough) { - return dir; - } - if (s > ms) { - ms = s; - prefer = dir; - } - } - return prefer; -} - -function resolvePrefer(prefer: any, targetRect: any, bounds: any) { - if (!prefer) { - if (targetRect.left - bounds.left < 10) { - return { dir: 'right' }; - } else if (targetRect.top - bounds.top < 10) { - return { dir: 'bottom' }; - } else if (bounds.bottom - targetRect.bottom < 10) { - return { dir: 'top' }; - } else if (bounds.right - targetRect.right < 10) { - return { dir: 'left' }; - } - return {}; - } - const force = prefer[0] === '!'; - if (force) { - prefer = prefer.slice(1); - } - let [dir, offset] = prefer.split(/\s+/); - let forceDirection = false; - let forceOffset = false; - if (dir === 'center') { - dir = 'auto'; - if (!offset) { - offset = 'center'; - } - } - - if (force) { - if (dir && dir !== 'auto') { - forceDirection = true; - } - if (offset && offset !== 'auto') { - forceOffset = true; - } - } - - return { dir, offset, forceDirection, forceOffset }; -} - -export function resolvePosition(popup: any, target: any, arrow: any, bounds: any, prefer: any) { - popup = { - extraOffset: arrow.height, - top: popup.top, - right: popup.right, - left: popup.left, - bottom: popup.bottom, - height: popup.height, - width: popup.width, - }; - - const prefers = resolvePrefer(prefer, target, bounds); - - const edge = resolveEdge(popup, target, arrow, bounds); - - // 选择方向 - const dir = resolveDirection(popup, target, edge, bounds, prefers); - - let top; - let left; - let arrowTop; - let arrowLeft; - - // 或得该方位上横向 或 纵向的 偏移 - if (dir === 'top' || dir === 'bottom') { - if (dir === 'top') { - top = target.top - popup.extraOffset - popup.height; - } else { - top = target.bottom + popup.extraOffset; - } - - // 解决横向偏移 - const offset = arrow.width > target.width ? (arrow.width - target.width) / 2 : 0; - const minLeft = target.left + arrow.width - offset - popup.width; - const maxLeft = target.right - arrow.width + offset; - const centerLeft = target.left - (popup.width - target.width) / 2; - - if (prefers.offset === 'left') { - left = minLeft; - } else if (prefers.offset === 'right') { - left = maxLeft; - } else { - left = centerLeft; - } - - if (!prefers.forceOffset) { - left = Math.max(Math.min(edge.right - popup.width, left), minLeft); - left = Math.min(Math.max(edge.left, left), maxLeft); - } - - arrowLeft = Math.min(popup.width - arrow.width, Math.max(target.left - (arrow.width - target.width) / 2 - left, 0)); - } else { - if (dir === 'left') { - left = target.left - popup.extraOffset - popup.width; - } else { - left = target.right + popup.extraOffset; - } - - // 解决纵向偏移 - const offset = arrow.width > target.height ? (arrow.width - target.height) / 2 : 0; - const minTop = target.top + arrow.width - offset - popup.height; - const maxTop = target.bottom - arrow.width + offset; - const centerTop = target.top - (popup.height - target.height) / 2; - - if (prefers.offset === 'top') { - top = minTop; - } else if (prefers.offset === 'bottom') { - top = maxTop; - } else { - top = centerTop; - } - - if (!prefers.forceOffset) { - top = Math.max(Math.min(edge.bottom - popup.height, top), minTop); - top = Math.min(Math.max(edge.top, top), maxTop); - } - - arrowTop = Math.min(popup.height - arrow.height, Math.max(target.top - (arrow.width - target.height) / 2 - top, 0)); - } - - return { dir, left, top, arrowLeft, arrowTop }; -} - -const percentPresets: any = { - right: 1, - left: 0, - top: 0, - bottom: 1, - center: 0.5, -}; - -function isPercent(val: any) { - return /^[\d.]+%$/.test(val); -} - -function resolveRelativeValue(val: any, offset: any, total: any) { - if (!val) { - val = 0; - } else if (isPercent(val)) { - val = (parseFloat(val) / 100) * total; - } else if (percentPresets.hasOwnProperty(val)) { - val = percentPresets[val] * total; - } else { - val = parseFloat(val); - if (isNaN(val)) { - val = 0; - } - } - - return `${val + offset}px`; -} - -export function resolveRelativePosition(align: any, popup: any, bounds: any) { - if (!align) { - // return default position - return { - top: '38.2%', - left: 'calc(50% - 110px)', - }; - } - - let [xAlign, yAlign] = align.trim().split(/\s+/); - - if (xAlign === 'top' || xAlign === 'bottom' || yAlign === 'left' || yAlign === 'right') { - const tmp = xAlign; - xAlign = yAlign; - yAlign = tmp; - } - - if (xAlign === 'center' && !yAlign) { - yAlign = 'center'; - } - - return { - left: resolveRelativeValue(xAlign, 0, bounds.right - bounds.left - popup.width), - top: resolveRelativeValue(yAlign, 0, bounds.bottom - bounds.top - popup.height), - }; -} diff --git a/packages/designer/src/widgets/title/index.tsx b/packages/designer/src/widgets/title/index.tsx deleted file mode 100644 index d0a3a8f21..000000000 --- a/packages/designer/src/widgets/title/index.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { Component, isValidElement, ReactNode } from 'react'; -import classNames from 'classnames'; -import { createIcon, isI18nData, isTitleConfig } from '@alilc/lowcode-utils'; -import { - IPublicTypeI18nData, - IPublicTypeTitleConfig, - IPublicTypeTitleProps, -} from '@alilc/lowcode-types'; -import { intl } from '../../intl'; -import { Tip } from '../tip'; - -import './title.less'; - -/** - * 根据 keywords 将 label 分割成文字片段 - * 示例:title = '自定义页面布局',keywords = '页面',返回结果为 ['自定义', '页面', '布局'] - * @param label title - * @param keywords 关键字 - * @returns 文字片段列表 - */ -function splitLabelByKeywords(label: string, keywords: string): string[] { - const len = keywords.length; - const fragments = []; - let str = label; - - while (str.length > 0) { - const index = str.indexOf(keywords); - - if (index === 0) { - fragments.push(keywords); - str = str.slice(len); - } else if (index < 0) { - fragments.push(str); - str = ''; - } else { - fragments.push(str.slice(0, index)); - str = str.slice(index); - } - } - - return fragments; -} - -export class Title extends Component<IPublicTypeTitleProps> { - handleClick = (e: React.MouseEvent) => { - const { title, onClick } = this.props as any; - const url = title && (title.docUrl || title.url); - if (url) { - window.open(url); - // 防止触发行操作(如折叠面板) - e.stopPropagation(); - } - // TODO: 操作交互冲突,目前 mixedSetter 仅有 2 个 setter 注册时用到了 onClick - onClick && onClick(e); - }; - - renderLabel = (label: string | IPublicTypeI18nData | ReactNode) => { - const { match, keywords } = this.props; - - if (!label) { - return null; - } - - const intlLabel = intl(label as IPublicTypeI18nData); - - if (typeof intlLabel !== 'string') { - return <span className="lc-title-txt">{intlLabel}</span>; - } - - let labelToRender: any = intlLabel; - - if (match && keywords) { - const fragments = splitLabelByKeywords(intlLabel as string, keywords); - - labelToRender = fragments.map((f) => ( - <span style={{ color: f === keywords ? 'red' : 'inherit' }}>{f}</span> - )); - } - - return <span className="lc-title-txt">{labelToRender}</span>; - }; - - render() { - const { title, className } = this.props; - let _title: IPublicTypeTitleConfig; - if (title == null) { - return null; - } - if (isValidElement(title)) { - return title; - } - if (typeof title === 'string' || isI18nData(title)) { - _title = { label: title }; - } else if (isTitleConfig(title)) { - _title = title; - } else { - _title = { - label: title, - }; - } - - const icon = _title.icon ? createIcon(_title.icon, { size: 20 }) : null; - - let tip: any = null; - if (_title.tip) { - if (isValidElement(_title.tip) && _title.tip.type === Tip) { - tip = _title.tip; - } else { - const tipProps: any = - typeof _title.tip === 'object' && !(isValidElement(_title.tip) || isI18nData(_title.tip)) - ? _title.tip - : { children: _title.tip }; - tip = <Tip {...tipProps} />; - } - } - - return ( - <span - className={classNames('lc-title', className, _title.className, { - 'has-tip': !!tip, - 'only-icon': !_title.label, - })} - onClick={this.handleClick} - > - {icon ? <b className="lc-title-icon">{icon}</b> : null} - {this.renderLabel(_title.label)} - {tip} - </span> - ); - } -} diff --git a/packages/designer/src/widgets/title/title.less b/packages/designer/src/widgets/title/title.less deleted file mode 100644 index f6747ef47..000000000 --- a/packages/designer/src/widgets/title/title.less +++ /dev/null @@ -1,32 +0,0 @@ -.lc-title { - display: inline-flex; - align-items: center; - color: var(--color-text); - .lc-title-icon { - display: flex; - align-items: center; - margin-right: 4px; - img { - width: 16px; - height: 16px; - filter: brightness(0) invert(1); - } - } - &.only-icon { - .lc-title-icon { - margin-right: 0; - } - } - &.has-tip { - cursor: help; - text-decoration-line: underline; - text-decoration-style: dashed; - text-decoration-color: var(--color-text-light, rgba(31, 56, 88, .3)); - } - line-height: initial !important; - word-break: break-all; -} - -.actived .lc-title { - color: var(--color-actived); -} diff --git a/packages/editor-core/src/command.ts b/packages/editor-core/src/command.ts deleted file mode 100644 index 513592cc4..000000000 --- a/packages/editor-core/src/command.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { checkPropTypes } from '@alilc/lowcode-utils'; -import { type AnyFunction } from '@alilc/lowcode-shared'; - -export interface Command { - /** - * 命令名称 - * 命名规则:commandName - * 使用规则:commandScope:commandName (commandScope 在插件 meta 中定义,用于区分不同插件的命令) - */ - name: string; - - /** - * 命令参数 - */ - parameters?: CommandParameter[]; - - /** - * 命令描述 - */ - description?: string; - - /** - * 命令处理函数 - */ - handler: (args: any) => void; -} - -export interface CommandParameter { - /** - * 参数名称 - */ - name: string; - - /** - * 参数类型或详细类型描述 - */ - propType: string | IPublicTypePropType; - - /** - * 参数描述 - */ - description: string; - - /** - * 参数默认值(可选) - */ - defaultValue?: any; -} - -/** - * 定义命令参数的接口 - */ -export interface CommandHandlerArgs { - [key: string]: any; -} - -export type ListCommand = Pick<Command, 'name' | 'description' | 'parameters'>; - -export interface CommandOptions { - commandScope?: string; -} - -/** - * 该模块使得与命令系统的交互成为可能,提供了一种全面的方式来处理、执行和管理应用程序中的命令。 - */ -export class CommandManager { - private commands: Map<string, Command> = new Map(); - private commandErrors: AnyFunction[] = []; - - /** - * 注册一个新命令及其处理函数 - */ - registerCommand(command: Command, options?: CommandOptions): void { - if (!options?.commandScope) { - throw new Error('plugin meta.commandScope is required.'); - } - const name = `${options.commandScope}:${command.name}`; - if (this.commands.has(name)) { - throw new Error(`Command '${command.name}' is already registered.`); - } - this.commands.set(name, { - ...command, - name, - }); - } - - /** - * 注销一个已存在的命令 - */ - unregisterCommand(name: string): void { - if (!this.commands.has(name)) { - throw new Error(`Command '${name}' is not registered.`); - } - this.commands.delete(name); - } - - /** - * 通过名称和给定参数执行一个命令,会校验参数是否符合命令定义 - */ - executeCommand(name: string, args: CommandHandlerArgs): void { - const command = this.commands.get(name); - if (!command) { - throw new Error(`Command '${name}' is not registered.`); - } - command.parameters?.forEach(d => { - if (!checkPropTypes(args[d.name], d.name, d.propType, 'command')) { - throw new Error(`Command '${name}' arguments ${d.name} is invalid.`); - } - }); - try { - command.handler(args); - } catch (error) { - if (this.commandErrors && this.commandErrors.length) { - this.commandErrors.forEach(callback => callback(name, error)); - } else { - throw error; - } - } - } - - /** - * 批量执行命令,执行完所有命令后再进行一次重绘,历史记录中只会记录一次 - */ - batchExecuteCommand( - commands: { name: string; args: CommandHandlerArgs }[], - pluginContext: IPublicModelPluginContext - ): void { - if (!commands || !commands.length) { - return; - } - pluginContext.common.utils.executeTransaction(() => { - commands.forEach(command => this.executeCommand(command.name, command.args)); - }, IPublicEnumTransitionType.REPAINT); - } - - /** - * 列出所有已注册的命令 - */ - listCommands(): ListCommand[] { - return Array.from(this.commands.values()).map(d => { - const result: ListCommand = { - name: d.name, - }; - - if (d.description) { - result.description = d.description; - } - - if (d.parameters) { - result.parameters = d.parameters; - } - - return result; - }); - } - - /** - * 注册错误处理回调函数 - */ - onCommandError(callback: (name: string, error: Error) => void): void { - this.commandErrors.push(callback); - } -} diff --git a/packages/editor-core/src/config.ts b/packages/editor-core/src/config.ts deleted file mode 100644 index 41de6808d..000000000 --- a/packages/editor-core/src/config.ts +++ /dev/null @@ -1,380 +0,0 @@ -import { get as lodashGet } from 'lodash-es'; -import { isPlainObject, createLogger } from '@alilc/lowcode-utils'; -import { - IPublicTypeEngineOptions, - IPublicModelEngineConfig, - IPublicModelPreference, -} from '@alilc/lowcode-types'; -import Preference from './preference'; - -const logger = createLogger({ level: 'log', bizName: 'config' }); - -// this default behavior will be different later -const STRICT_PLUGIN_MODE_DEFAULT = true; - -// used in strict mode, when only options in this VALID_ENGINE_OPTIONS can be accepted -// type and description are only used for developer`s assistance, won`t affect runtime -const VALID_ENGINE_OPTIONS = { - enableCondition: { - type: 'boolean', - description: '是否开启 condition 的能力,默认在设计器中不管 condition 是啥都正常展示', - }, - designMode: { - type: 'string', - enum: ['design', 'live'], - default: 'design', - description: '设计模式,live 模式将会实时展示变量值', - }, - device: { - type: 'string', - enum: ['default', 'mobile', 'any string value'], - default: 'default', - description: '设备类型', - }, - deviceClassName: { - type: 'string', - default: undefined, - description: '指定初始化的 deviceClassName,挂载到画布的顶层节点上', - }, - locale: { - type: 'string', - default: 'zh-CN', - description: '语言', - }, - renderEnv: { - type: 'string', - enum: ['react', 'any string value'], - default: 'react', - description: '渲染器类型', - }, - deviceMapper: { - type: 'object', - description: '设备类型映射器,处理设计器与渲染器中 device 的映射', - }, - enableStrictPluginMode: { - type: 'boolean', - default: STRICT_PLUGIN_MODE_DEFAULT, - description: - '开启严格插件模式,默认值:STRICT_PLUGIN_MODE_DEFAULT , 严格模式下,插件将无法通过 engineOptions 传递自定义配置项', - }, - enableReactiveContainer: { - type: 'boolean', - default: false, - description: '开启拖拽组件时,即将被放入的容器是否有视觉反馈', - }, - disableAutoRender: { - type: 'boolean', - default: false, - description: '关闭画布自动渲染,在资产包多重异步加载的场景有效', - }, - disableDetecting: { - type: 'boolean', - default: false, - description: '关闭拖拽组件时的虚线响应,性能考虑', - }, - customizeIgnoreSelectors: { - type: 'function', - default: undefined, - description: - '定制画布中点击被忽略的 selectors, eg. (defaultIgnoreSelectors: string[], e: MouseEvent) => string[]', - }, - disableDefaultSettingPanel: { - type: 'boolean', - default: false, - description: '禁止默认的设置面板', - }, - disableDefaultSetters: { - type: 'boolean', - default: false, - description: '禁止默认的设置器', - }, - enableCanvasLock: { - type: 'boolean', - default: false, - description: '打开画布的锁定操作', - }, - enableLockedNodeSetting: { - type: 'boolean', - default: false, - description: '容器锁定后,容器本身是否可以设置属性,仅当画布锁定特性开启时生效', - }, - stayOnTheSameSettingTab: { - type: 'boolean', - default: false, - description: '当选中节点切换时,是否停留在相同的设置 tab 上', - }, - hideSettingsTabsWhenOnlyOneItem: { - type: 'boolean', - description: '是否在只有一个 item 的时候隐藏设置 tabs', - }, - loadingComponent: { - type: 'ComponentType', - default: undefined, - description: '自定义 loading 组件', - }, - supportVariableGlobally: { - type: 'boolean', - default: false, - description: '设置所有属性支持变量配置', - }, - visionSettings: { - type: 'object', - description: 'Vision-polyfill settings', - }, - simulatorUrl: { - type: 'array', - description: '自定义 simulatorUrl 的地址', - }, - // 与 react-renderer 的 appHelper 一致,https://lowcode-engine.cn/site/docs/guide/expand/runtime/renderer#apphelper - appHelper: { - type: 'object', - description: '定义 utils 和 constants 等对象', - }, - requestHandlersMap: { - type: 'object', - description: '数据源引擎的请求处理器映射', - }, - thisRequiredInJSE: { - type: 'boolean', - description: 'JSExpression 是否只支持使用 this 来访问上下文变量', - }, - enableStrictNotFoundMode: { - type: 'boolean', - description: '当开启组件未找到严格模式时,渲染模块不会默认给一个容器组件', - }, - focusNodeSelector: { - type: 'function', - description: '配置指定节点为根组件', - }, - enableAutoOpenFirstWindow: { - type: 'boolean', - description: '应用级设计模式下,自动打开第一个窗口', - default: true, - }, - enableWorkspaceMode: { - type: 'boolean', - description: '是否开启应用级设计模式', - default: false, - }, - workspaceEmptyComponent: { - type: 'function', - description: '应用级设计模式下,窗口为空时展示的占位组件', - }, - enableContextMenu: { - type: 'boolean', - description: '是否开启右键菜单', - default: false, - }, - hideComponentAction: { - type: 'boolean', - description: '是否隐藏设计器辅助层', - default: false, - }, -}; - -const getStrictModeValue = ( - engineOptions: IPublicTypeEngineOptions, - defaultValue: boolean, -): boolean => { - if (!engineOptions || !isPlainObject(engineOptions)) { - return defaultValue; - } - if ( - engineOptions.enableStrictPluginMode === undefined || - engineOptions.enableStrictPluginMode === null - ) { - return defaultValue; - } - return engineOptions.enableStrictPluginMode; -}; - -export interface IEngineConfig extends IPublicModelEngineConfig { - /** - * if engineOptions.strictPluginMode === true, only accept propertied predefined in EngineOptions. - * - * @param {IPublicTypeEngineOptions} engineOptions - */ - setEngineOptions(engineOptions: IPublicTypeEngineOptions): void; - - notifyGot(key: string): void; - - setWait(key: string, resolve: (data: any) => void, once?: boolean): void; - - delWait(key: string, fn: any): void; -} - -export class EngineConfig implements IEngineConfig { - private config: { [key: string]: any } = {}; - - private waits = new Map< - string, - Array<{ - once?: boolean; - resolve: (data: any) => void; - }> - >(); - - /** - * used to store preferences - * - */ - readonly preference: IPublicModelPreference; - - constructor(config?: { [key: string]: any }) { - this.config = config || {}; - this.preference = new Preference(); - } - - /** - * 判断指定 key 是否有值 - * @param key - */ - has(key: string): boolean { - return this.config[key] !== undefined; - } - - /** - * 获取指定 key 的值 - * @param key - * @param defaultValue - */ - get(key: string, defaultValue?: any): any { - return lodashGet(this.config, key, defaultValue); - } - - /** - * 设置指定 key 的值 - * @param key - * @param value - */ - set(key: string, value: any) { - this.config[key] = value; - this.notifyGot(key); - } - - /** - * 批量设值,set 的对象版本 - * @param config - */ - setConfig(config: { [key: string]: any }) { - if (config) { - Object.keys(config).forEach((key) => { - this.set(key, config[key]); - }); - } - } - - /** - * if engineOptions.strictPluginMode === true, only accept propertied predefined in EngineOptions. - * - * @param {IPublicTypeEngineOptions} engineOptions - */ - setEngineOptions(engineOptions: IPublicTypeEngineOptions) { - if (!engineOptions || !isPlainObject(engineOptions)) { - return; - } - const strictMode = getStrictModeValue(engineOptions, STRICT_PLUGIN_MODE_DEFAULT) === true; - if (strictMode) { - const isValidKey = (key: string) => { - const result = (VALID_ENGINE_OPTIONS as any)[key]; - return !(result === undefined || result === null); - }; - Object.keys(engineOptions).forEach((key) => { - if (isValidKey(key)) { - this.set(key, (engineOptions as any)[key]); - } else { - logger.warn( - `failed to config ${key} to engineConfig, only predefined options can be set under strict mode, predefined options: `, - VALID_ENGINE_OPTIONS, - ); - } - }); - } else { - this.setConfig(engineOptions as any); - } - } - - /** - * 获取指定 key 的值,若此时还未赋值,则等待,若已有值,则直接返回值 - * 注:此函数返回 Promise 实例,只会执行(fullfill)一次 - * @param key - * @returns - */ - onceGot(key: string): Promise<any> { - 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: string, fn: (data: any) => void): () => void { - const val = this.config?.[key]; - if (val !== undefined) { - fn(val); - } - this.setWait(key, fn); - return () => { - this.delWait(key, fn); - }; - } - - notifyGot(key: string): 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: string, 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: string, 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); - } - } - - getPreference(): IPublicModelPreference { - return this.preference; - } -} - -export const engineConfig = new EngineConfig(); diff --git a/packages/editor-core/src/di.ts b/packages/editor-core/src/di.ts deleted file mode 100644 index 27b854cab..000000000 --- a/packages/editor-core/src/di.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { IocContext } from 'power-di'; - -export * from 'power-di'; - -export const globalContext = IocContext.DefaultInstance; diff --git a/packages/editor-core/src/editor.ts b/packages/editor-core/src/editor.ts deleted file mode 100644 index 6aa52f6b0..000000000 --- a/packages/editor-core/src/editor.ts +++ /dev/null @@ -1,346 +0,0 @@ -import EventEmitter from 'events'; -import { EventBus, IEventBus } from './event-bus'; -import { - IPublicModelEditor, - EditorConfig, - PluginClassSet, - IPublicTypeEditorValueKey, - IPublicTypeEditorGetResult, - HookConfig, - IPublicTypeComponentDescription, - IPublicTypeRemoteComponentDescription, -} from '@alilc/lowcode-types'; -import { engineConfig } from './config'; -import { globalLocale } from './intl'; -import { observable } from './obx'; -import { IPublicTypeAssetsJson, AssetLoader } from '@alilc/lowcode-utils'; -import { assetsTransform } from './utils/assets-transform'; - -// inner instance keys which should not be stored in config -const keyBlacklist = [ - 'designer', - 'skeleton', - 'currentDocument', - 'simulator', - 'plugins', - 'setters', - 'material', - 'innerHotkey', - 'innerPlugins', -]; - -const AssetsCache: Record<string, IPublicTypeRemoteComponentDescription> = {}; - -export interface IEditor extends IPublicModelEditor { - config?: EditorConfig; - components?: PluginClassSet; - eventBus: IEventBus; - init(config?: EditorConfig, components?: PluginClassSet): Promise<any>; -} - -export class Editor extends EventEmitter implements IEditor { - /** - * Ioc Container - * ??? - */ - @observable.shallow private context = new Map<IPublicTypeEditorValueKey, any>(); - - get locale() { - return globalLocale.getLocale(); - } - - config?: EditorConfig; - - eventBus: EventBus; - - components?: PluginClassSet; - - private hooks: HookConfig[] = []; - - private waits = new Map< - IPublicTypeEditorValueKey, - Array<{ - once?: boolean; - resolve: (data: any) => void; - }> - >(); - - constructor( - readonly viewName: string = 'global', - readonly workspaceMode: boolean = false, - ) { - // eslint-disable-next-line constructor-super - super(); - // set global emitter maxListeners - this.setMaxListeners(200); - this.eventBus = new EventBus(this); - } - - get<T = undefined, KeyOrType = any>( - keyOrType: KeyOrType, - ): IPublicTypeEditorGetResult<T, KeyOrType> | undefined { - return this.context.get(keyOrType as any); - } - - has(keyOrType: IPublicTypeEditorValueKey): boolean { - return this.context.has(keyOrType); - } - - set(key: IPublicTypeEditorValueKey, data: any): void | Promise<void> { - if (key === 'assets') { - return this.setAssets(data); - } - // store the data to engineConfig while invoking editor.set() - if (!keyBlacklist.includes(key as string)) { - engineConfig.set(key as any, data); - } - this.context.set(key, data); - this.notifyGot(key); - } - - async setAssets(assets: IPublicTypeAssetsJson) { - const { components } = assets; - if (components && components.length) { - const componentDescriptions: IPublicTypeComponentDescription[] = []; - const remoteComponentDescriptions: IPublicTypeRemoteComponentDescription[] = []; - components.forEach((component: any) => { - if (!component) { - return; - } - if (component.exportName && component.url) { - remoteComponentDescriptions.push(component); - } else { - componentDescriptions.push(component); - } - }); - assets.components = componentDescriptions; - - // 如果有远程组件描述协议,则自动加载并补充到资产包中,同时出发 designer.incrementalAssetsReady 通知组件面板更新数据 - if (remoteComponentDescriptions && remoteComponentDescriptions.length) { - await Promise.all( - remoteComponentDescriptions.map( - async (component: IPublicTypeRemoteComponentDescription) => { - const { exportName, url, npm } = component; - if (!url || !exportName) { - return; - } - if ( - !AssetsCache[exportName] || - !npm?.version || - AssetsCache[exportName].npm?.version !== npm?.version - ) { - await new AssetLoader().load(url); - } - AssetsCache[exportName] = component; - function setAssetsComponent(component: any, extraNpmInfo: any = {}) { - const components = component.components; - if (Array.isArray(components)) { - components.forEach((d) => { - assets.components = assets.components.concat( - { - npm: { - ...npm, - ...extraNpmInfo, - }, - ...d, - } || [], - ); - }); - return; - } - if (component.components) { - assets.components = assets.components.concat( - { - npm: { - ...npm, - ...extraNpmInfo, - }, - ...component.components, - } || [], - ); - } - } - function setArrayAssets( - value: any[], - preExportName: string = '', - preSubName: string = '', - ) { - value.forEach((d: any, i: number) => { - const exportName = [preExportName, i.toString()].filter((d) => !!d).join('.'); - const subName = [preSubName, i.toString()].filter((d) => !!d).join('.'); - Array.isArray(d) - ? setArrayAssets(d, exportName, subName) - : setAssetsComponent(d, { - exportName, - subName, - }); - }); - } - if ((window as any)[exportName]) { - if (Array.isArray((window as any)[exportName])) { - setArrayAssets((window as any)[exportName] as any); - } else { - setAssetsComponent((window as any)[exportName] as any); - } - } - return (window as any)[exportName]; - }, - ), - ); - } - } - const innerAssets = assetsTransform(assets); - this.context.set('assets', innerAssets); - this.notifyGot('assets'); - } - - onceGot<T = undefined, KeyOrType extends IPublicTypeEditorValueKey = any>( - keyOrType: KeyOrType, - ): Promise<IPublicTypeEditorGetResult<T, KeyOrType>> { - const x = this.context.get(keyOrType); - if (x !== undefined) { - return Promise.resolve(x); - } - return new Promise((resolve) => { - this.setWait(keyOrType, resolve, true); - }); - } - - onGot<T = undefined, KeyOrType extends IPublicTypeEditorValueKey = any>( - keyOrType: KeyOrType, - fn: (data: IPublicTypeEditorGetResult<T, KeyOrType>) => void, - ): () => void { - const x = this.context.get(keyOrType); - if (x !== undefined) { - fn(x); - } - this.setWait(keyOrType, fn); - return () => { - this.delWait(keyOrType, fn); - }; - } - - onChange<T = undefined, KeyOrType extends IPublicTypeEditorValueKey = any>( - keyOrType: KeyOrType, - fn: (data: IPublicTypeEditorGetResult<T, KeyOrType>) => void, - ): () => void { - this.setWait(keyOrType, fn); - return () => { - this.delWait(keyOrType, fn); - }; - } - - register(data: any, key?: IPublicTypeEditorValueKey): void { - this.context.set(key || data, data); - this.notifyGot(key || data); - } - - async init(config?: EditorConfig, components?: PluginClassSet): Promise<any> { - this.config = config || {}; - this.components = components || {}; - const { hooks = [], lifeCycles } = this.config; - - this.emit('editor.beforeInit'); - const init = (lifeCycles && lifeCycles.init) || ((): void => {}); - - try { - await init(this); - // 注册快捷键 - // 注册 hooks - this.registerHooks(hooks); - this.emit('editor.afterInit'); - - return true; - } catch (err) { - console.error(err); - } - } - - destroy(): void { - if (!this.config) { - return; - } - try { - const { lifeCycles = {} } = this.config; - - this.unregisterHooks(); - - if (lifeCycles.destroy) { - lifeCycles.destroy(this); - } - } catch (err) { - console.warn(err); - } - } - - initHooks = (hooks: HookConfig[]) => { - this.hooks = hooks.map((hook) => ({ - ...hook, - // 指定第一个参数为 editor - handler: hook.handler.bind(this, this), - })); - - return this.hooks; - }; - - registerHooks = (hooks: HookConfig[]) => { - this.initHooks(hooks).forEach(({ message, type, handler }) => { - if (['on', 'once'].indexOf(type) !== -1) { - this[type](message as any, handler); - } - }); - }; - - unregisterHooks = () => { - this.hooks.forEach(({ message, handler }) => { - this.removeListener(message, handler); - }); - }; - - private notifyGot(key: IPublicTypeEditorValueKey) { - 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); - } - } - - private setWait(key: IPublicTypeEditorValueKey, 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 }]); - } - } - - private delWait(key: IPublicTypeEditorValueKey, 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); - } - } -} - -export const commonEvent = new EventBus(new EventEmitter()); diff --git a/packages/editor-core/src/event-bus.ts b/packages/editor-core/src/event-bus.ts deleted file mode 100644 index bb817b50a..000000000 --- a/packages/editor-core/src/event-bus.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { IPublicApiEvent } from '@alilc/lowcode-types'; -import { Logger } from '@alilc/lowcode-utils'; -import EventEmitter from 'events'; - -EventEmitter.defaultMaxListeners = 100; - -const logger = new Logger({ level: 'warn', bizName: 'event-bus' }); -const moduleLogger = new Logger({ level: 'warn', bizName: 'module-event-bus' }); - -export interface IEventBus extends IPublicApiEvent { - removeListener(event: string | symbol, listener: (...args: any[]) => void): any; - addListener(event: string | symbol, listener: (...args: any[]) => void): any; - setMaxListeners(n: number): any; - removeAllListeners(event?: string | symbol): any; -} - -export class EventBus implements IEventBus { - private readonly eventEmitter: EventEmitter; - private readonly name?: string; - - /** - * 内核触发的事件名 - */ - readonly names = []; - - constructor(emitter: EventEmitter, name?: string) { - this.eventEmitter = emitter; - this.name = name; - } - - private getMsgPrefix(type: string): string { - if (this.name && this.name.length > 0) { - return `[${this.name}][event-${type}]`; - } else { - return `[*][event-${type}]`; - } - } - - private getLogger(): Logger { - if (this.name && this.name.length > 0) { - return moduleLogger; - } else { - return logger; - } - } - - /** - * 监听事件 - * @param event 事件名称 - * @param listener 事件回调 - */ - on(event: string, listener: (...args: any[]) => void): () => void { - this.eventEmitter.on(event, listener); - this.getLogger().debug(`${this.getMsgPrefix('on')} ${event}`); - return () => { - this.off(event, listener); - }; - } - - prependListener(event: string, listener: (...args: any[]) => void): () => void { - this.eventEmitter.prependListener(event, listener); - this.getLogger().debug(`${this.getMsgPrefix('prependListener')} ${event}`); - return () => { - this.off(event, listener); - }; - } - - /** - * 取消监听事件 - * @param event 事件名称 - * @param listener 事件回调 - */ - off(event: string, listener: (...args: any[]) => void) { - this.eventEmitter.off(event, listener); - this.getLogger().debug(`${this.getMsgPrefix('off')} ${event}`); - } - - /** - * 触发事件 - * @param event 事件名称 - * @param args 事件参数 - * @returns - */ - emit(event: string, ...args: any[]) { - this.eventEmitter.emit(event, ...args); - this.getLogger().debug(`${this.getMsgPrefix('emit')} name: ${event}, args: `, ...args); - } - - removeListener(event: string | symbol, listener: (...args: any[]) => void): any { - return this.eventEmitter.removeListener(event, listener); - } - - addListener(event: string | symbol, listener: (...args: any[]) => void): any { - return this.eventEmitter.addListener(event, listener); - } - - setMaxListeners(n: number): any { - return this.eventEmitter.setMaxListeners(n); - } - removeAllListeners(event?: string | symbol): any { - return this.eventEmitter.removeAllListeners(event); - } -} - -export const createModuleEventBus = (moduleName: string, maxListeners?: number): IEventBus => { - const emitter = new EventEmitter(); - if (maxListeners) { - emitter.setMaxListeners(maxListeners); - } - return new EventBus(emitter, moduleName); -}; diff --git a/packages/editor-core/src/index.ts b/packages/editor-core/src/index.ts deleted file mode 100644 index b963b29d6..000000000 --- a/packages/editor-core/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from './intl'; -export * from './editor'; -export * from './utils'; -export * from './di'; -export * from './hotkey'; -export * from './config'; -export * from './event-bus'; -export * from './command'; -export * from './setter'; -export * from './plugin'; -export * from './obx'; diff --git a/packages/editor-core/src/intl/global-locale.ts b/packages/editor-core/src/intl/global-locale.ts deleted file mode 100644 index c39c23e7d..000000000 --- a/packages/editor-core/src/intl/global-locale.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { IEventBus, createModuleEventBus } from '../event-bus'; -import { observable, computed } from '../obx'; -import { createLogger } from '@alilc/lowcode-shared'; - -const logger = createLogger({ level: 'warn', bizName: 'globalLocale' }); - -const languageMap: { [key: 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', -}; - -const LowcodeConfigKey = 'ali-lowcode-config'; - -class GlobalLocale { - private emitter: IEventBus = createModuleEventBus('GlobalLocale'); - - @observable.ref private _locale?: string; - - @computed get locale() { - if (this._locale != null) { - return this._locale; - } - - // TODO: store 1 & store 2 abstract out as custom implements - - // store 1: config from storage - let result = null; - if (hasLocalStorage(window)) { - const store = window.localStorage; - let config: any; - try { - config = JSON.parse(store.getItem(LowcodeConfigKey) || ''); - } catch (e) { - // ignore; - } - if (config?.locale) { - result = (config.locale || '').replace('_', '-'); - logger.debug(`getting locale from localStorage: ${result}`); - } - } - if (!result) { - // store 2: config from window - const localeFromConfig: string = getConfig('locale'); - if (localeFromConfig) { - result = languageMap[localeFromConfig] || localeFromConfig.replace('_', '-'); - logger.debug(`getting locale from config: ${result}`); - } - } - - if (!result) { - // store 3: config from system - const { navigator } = window as any; - if (navigator.language) { - const lang = (navigator.language as string); - return languageMap[lang] || lang.replace('_', '-'); - } else if (navigator.browserLanguage) { - const it = navigator.browserLanguage.split('-'); - let localeFromSystem = it[0]; - if (it[1]) { - localeFromSystem += `-${it[1].toUpperCase()}`; - } - result = localeFromSystem; - logger.debug(`getting locale from system: ${result}`); - } - } - if (!result) { - logger.warn('something when wrong when trying to get locale, use zh-CN as default, please check it out!'); - result = 'zh-CN'; - } - this._locale = result; - return result; - } - - constructor() { - this.emitter.setMaxListeners(0); - } - - setLocale(locale: string) { - logger.info(`setting locale to ${locale}`); - if (locale === this.locale) { - return; - } - this._locale = locale; - if (hasLocalStorage(window)) { - const store = window.localStorage; - let config: any; - try { - config = JSON.parse(store.getItem(LowcodeConfigKey) || ''); - } catch (e) { - // ignore; - } - - if (config && typeof config === 'object') { - config.locale = locale; - } else { - config = { locale }; - } - - store.setItem(LowcodeConfigKey, JSON.stringify(config)); - } - this.emitter.emit('localechange', locale); - } - - getLocale() { - return this.locale; - } - - onChangeLocale(fn: (locale: string) => void): () => void { - this.emitter.on('localechange', fn); - return () => { - this.emitter.removeListener('localechange', fn); - }; - } -} - -function getConfig(name: string) { - const win: any = window; - return ( - win[name] - || (win.g_config || {})[name] - || (win.pageConfig || {})[name] - ); -} - -function hasLocalStorage(obj: any): obj is WindowLocalStorage { - return obj.localStorage; -} - -const globalLocale = new GlobalLocale(); - -export { globalLocale }; diff --git a/packages/editor-core/src/intl/index.ts b/packages/editor-core/src/intl/index.ts deleted file mode 100644 index 647467dea..000000000 --- a/packages/editor-core/src/intl/index.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { ReactNode, Component, createElement } from 'react'; -import { IntlMessageFormat } from 'intl-messageformat'; -import { globalLocale } from './global-locale'; -import { isI18nData } from '@alilc/lowcode-utils'; -import { observer } from '../utils'; -import { IPublicTypeI18nData } from '@alilc/lowcode-types'; - -function generateTryLocales(locale: string) { - const tries = [locale, locale.replace('-', '_')]; - if (locale === 'zh-TW' || locale === 'en-US') { - tries.push('zh-CN'); - tries.push('zh_CN'); - } else { - tries.push('en-US'); - tries.push('en_US'); - if (locale !== 'zh-CN') { - tries.push('zh-CN'); - tries.push('zh_CN'); - } - } - return tries; -} - -function injectVars(msg: string, params: any, locale: string): string { - if (!msg || !params) { - return msg; - } - const formater = new IntlMessageFormat(msg, locale); - return formater.format(params as any) as string; -} - -export function intl(data: IPublicTypeI18nData | string, params?: object) { - if (!isI18nData(data)) { - return data; - } - if (data.intl) { - return data.intl as any; - } - const locale = globalLocale.getLocale(); - const tries = generateTryLocales(locale); - let msg: string | undefined; - for (const lan of tries) { - msg = data[lan]; - if (msg != null) { - break; - } - } - if (msg == null) { - return `##intl@${locale}##`; - } - return injectVars(msg, params, locale); -} - -export function shallowIntl(data: any): any { - if (!data || typeof data !== 'object') { - return data; - } - const maps: any = {}; - Object.keys(data).forEach((key) => { - maps[key] = intl(data[key]); - }); - return maps; -} - -export function intlNode(data: any, params?: object): ReactNode { - if (isI18nData(data)) { - if (data.intlNode) { - return data.intlNode; - } - - return createElement(IntlElement, { data, params }); - } - return data; -} - -@observer -class IntlElement extends Component<{ data: any; params?: object }> { - render() { - const { data, params } = this.props; - return intl(data, params); - } -} - -export function createIntl(instance: string | object): { - intlNode(id: string, params?: object): ReactNode; - intl(id: string, params?: object): string; - getLocale(): string; - setLocale(locale: string): void; -} { - // TODO: make reactive - const data = (() => { - const locale = globalLocale.getLocale(); - if (typeof instance === 'string') { - if ((window as any)[instance]) { - return (window as any)[instance][locale] || {}; - } - const key = `${instance}_${locale.toLocaleLowerCase()}`; - return (window as any)[key] || {}; - } - if (instance && typeof instance === 'object') { - return (instance as any)[locale] || {}; - } - return {}; - })(); - - function intl(key: string, params?: object): string { - // TODO: tries lost language - const str = data[key]; - - if (str == null) { - return `##intl@${key}##`; - } - - return injectVars(str, params, globalLocale.getLocale()); - } - - @observer - class IntlElement extends Component<{ id: string; params?: object }> { - render() { - const { id, params } = this.props; - return intl(id, params); - } - } - - return { - intlNode(id: string, params?: object) { - return createElement(IntlElement, { id, params }); - }, - intl, - getLocale() { - return globalLocale.getLocale(); - }, - setLocale(locale: string) { - globalLocale.setLocale(locale); - }, - }; -} - -export { globalLocale }; diff --git a/packages/editor-core/src/obx.ts b/packages/editor-core/src/obx.ts deleted file mode 100644 index e9ec63a52..000000000 --- a/packages/editor-core/src/obx.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { observer } from 'mobx-react'; -import * as mobx from 'mobx'; - -mobx.configure({ enforceActions: 'never' }); - -export { - observable, - observe, - autorun, - makeObservable, - makeAutoObservable, - reaction, - computed, - action, - runInAction, - untracked, - flow -} from 'mobx'; -export type { IReactionDisposer, IReactionPublic, IReactionOptions } from 'mobx'; - -export { observer, mobx }; diff --git a/packages/editor-core/src/plugin/context.ts b/packages/editor-core/src/plugin/context.ts deleted file mode 100644 index 095f8d670..000000000 --- a/packages/editor-core/src/plugin/context.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { type IEventBus, createModuleEventBus } from '../event-bus'; -import type { PluginMeta, PluginPreferenceValue, PluginDeclaration } from './plugin'; -import { type LowCodePluginManager } from './manager'; -import { isValidPreferenceKey } from './utils'; -import { engineConfig } from '../config'; - -export interface PluginContextOptions { - pluginName: string; - meta?: PluginMeta; -} - -export interface LowCodePluginContextApiAssembler<ContextExtra extends Record<string, any>> { - assembleApis( - context: LowCodePluginContext<ContextExtra>, - pluginName: string, - meta: PluginMeta, - ): void; -} - -export interface PluginPreferenceMananger { - getPreferenceValue: ( - key: string, - defaultValue?: PluginPreferenceValue, - ) => PluginPreferenceValue | undefined; -} - -export type LowCodePluginContext<ContextExtra extends Record<string, any>> = { - pluginEvent: IEventBus; - preference: PluginPreferenceMananger; - setPreference(pluginName: string, preferenceDeclaration: PluginDeclaration): void; -} & ContextExtra; - -/** - * create plugin context - * todo: refactor setPreference - */ -export function createPluginContext<ContextExtra extends Record<string, any>>( - options: PluginContextOptions, - manager: LowCodePluginManager<ContextExtra>, - contextApiAssembler: LowCodePluginContextApiAssembler<LowCodePluginContext<ContextExtra>> -): LowCodePluginContext<ContextExtra> { - const { pluginName = 'anonymous', meta = {} } = options; - const pluginEvent = createModuleEventBus(pluginName, 200); - - let _pluginName = pluginName; - let _preferenceDeclaration: PluginDeclaration; - - const preferenceMananger: PluginPreferenceMananger = { - getPreferenceValue: ( - key, - defaultValue?, - ) => { - if (!isValidPreferenceKey(key, _preferenceDeclaration)) { - return undefined; - } - const pluginPreference = manager.getPluginPreference(_pluginName) || {}; - if (pluginPreference[key] === undefined || pluginPreference[key] === null) { - return defaultValue; - } - return pluginPreference[key]; - } - }; - - const contextBase = { - pluginEvent, - preference: preferenceMananger, - setPreference( - pluginName: string, - preferenceDeclaration: PluginDeclaration, - ): void { - _pluginName = pluginName; - _preferenceDeclaration = preferenceDeclaration; - } - } as LowCodePluginContext<ContextExtra>; - - contextApiAssembler.assembleApis(contextBase, pluginName, meta); - const enhancePluginContextHook = engineConfig.get('enhancePluginContextHook'); - if (enhancePluginContextHook) { - enhancePluginContextHook(contextBase); - } - - return contextBase; -} diff --git a/packages/editor-core/src/plugin/index.ts b/packages/editor-core/src/plugin/index.ts deleted file mode 100644 index a930176e2..000000000 --- a/packages/editor-core/src/plugin/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './context'; -export * from './manager'; -export * from './runtime'; -export * from './plugin'; diff --git a/packages/editor-core/src/plugin/manager.ts b/packages/editor-core/src/plugin/manager.ts deleted file mode 100644 index 2d91adf29..000000000 --- a/packages/editor-core/src/plugin/manager.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { createLogger, invariant } from '@alilc/lowcode-shared'; -import { filterValidOptions, isLowCodeRegisterOptions, sequencify } from './utils'; -import { LowCodePluginRuntime } from './runtime'; -import { satisfies as semverSatisfies } from 'semver'; -import type { PluginCreater, PluginPreferenceValue, PluginPreference } from './plugin'; -import { engineConfig } from '../config'; -import { - type LowCodePluginContext, - type LowCodePluginContextApiAssembler, - createPluginContext, - PluginContextOptions -} from './context'; - -const logger = createLogger({ level: 'warn', bizName: 'designer:pluginManager' }); - -// 保留的事件前缀 -const RESERVED_EVENT_PREFIX = [ - 'designer', - 'editor', - 'skeleton', - 'renderer', - 'render', - 'utils', - 'plugin', - 'engine', - 'editor-core', - 'engine-core', - 'plugins', - 'event', - 'events', - 'log', - 'logger', - 'ctx', - 'context', -]; - -export interface PluginRegisterOptions { - /** - * Will enable plugin registered with auto-initialization immediately - * other than plugin-manager init all plugins at certain time. - * It is helpful when plugin register is later than plugin-manager initialization. - */ - autoInit?: boolean; - /** - * allow overriding existing plugin with same name when override === true - */ - override?: boolean; -} - -/** - * plugin manager - */ -export class LowCodePluginManager<ContextExtra extends Record<string, any>> { - private plugins: LowCodePluginRuntime<ContextExtra>[] = []; - - private pluginsMap: Map<string, LowCodePluginRuntime<ContextExtra>> = new Map(); - - private pluginContextMap: Map<string, LowCodePluginContext<ContextExtra>> = new Map(); - - private pluginPreference?: PluginPreference = new Map(); - - constructor( - private contextApiAssembler: LowCodePluginContextApiAssembler< - LowCodePluginContext<ContextExtra> - >, - readonly viewName = 'global', - ) {} - - private _getLowCodePluginContext = (options: PluginContextOptions) => { - const { pluginName } = options; - let context = this.pluginContextMap.get(pluginName); - if (!context) { - context = createPluginContext(options, this, this.contextApiAssembler); - this.pluginContextMap.set(pluginName, context); - } - return context; - }; - - isEngineVersionMatched(versionExp: string): boolean { - const engineVersion = engineConfig.get('ENGINE_VERSION'); - // ref: https://github.com/npm/node-semver#functions - // 1.0.1-beta should match '^1.0.0' - return semverSatisfies(engineVersion, versionExp, { includePrerelease: true }); - } - - /** - * register a plugin - * @param pluginConfigCreator - a creator function which returns the plugin config - * @param options - the plugin options - * @param registerOptions - the plugin register options - */ - async register( - pluginModel: PluginCreater<LowCodePluginContext<ContextExtra>>, - options?: any, - registerOptions?: PluginRegisterOptions, - ): Promise<void> { - // registerOptions maybe in the second place - if (isLowCodeRegisterOptions(options)) { - registerOptions = options; - options = {}; - } - let { pluginName } = pluginModel; - const { meta = {} } = pluginModel; - const { preferenceDeclaration, engines } = meta; - // filter invalid eventPrefix - const { eventPrefix } = meta; - const isReservedPrefix = RESERVED_EVENT_PREFIX.find((item) => item === eventPrefix); - if (isReservedPrefix) { - meta.eventPrefix = undefined; - logger.warn( - `plugin ${pluginName} is trying to use ${eventPrefix} as event prefix, which is a reserved event prefix, please use another one`, - ); - } - - const ctx = this._getLowCodePluginContext({ pluginName, meta }); - const customFilterValidOptions = engineConfig.get( - 'customPluginFilterOptions', - filterValidOptions, - ); - const pluginTransducer = engineConfig.get('customPluginTransducer', null); - const newPluginModel = pluginTransducer - ? await pluginTransducer(pluginModel, ctx, options) - : pluginModel; - const newOptions = customFilterValidOptions( - options, - newPluginModel.meta?.preferenceDeclaration, - ); - const config = newPluginModel(ctx, newOptions); - // compat the legacy way to declare pluginName - pluginName = pluginName || config.name; - invariant(pluginName, 'pluginConfigCreator.pluginName required', config); - - ctx.setPreference(pluginName, preferenceDeclaration!); - - const allowOverride = registerOptions?.override === true; - - if (this.pluginsMap.has(pluginName)) { - if (!allowOverride) { - throw new Error(`Plugin with name ${pluginName} exists`); - } else { - // clear existing plugin - const originalPlugin = this.pluginsMap.get(pluginName); - logger.log( - 'plugin override, originalPlugin with name ', - pluginName, - ' will be destroyed, config:', - originalPlugin?.instance, - ); - originalPlugin?.destroy(); - this.pluginsMap.delete(pluginName); - } - } - - const engineVersionExp = engines && engines.lowcodeEngine; - if (engineVersionExp && !this.isEngineVersionMatched(engineVersionExp)) { - throw new Error( - `plugin ${pluginName} skipped, engine check failed, current engine version is ${engineConfig.get('ENGINE_VERSION')}, meta.engines.lowcodeEngine is ${engineVersionExp}`, - ); - } - - const plugin = new LowCodePluginRuntime(pluginName, this, config, meta); - // support initialization of those plugins which registered - // after normal initialization by plugin-manager - if (registerOptions?.autoInit) { - await plugin.init(); - } - this.plugins.push(plugin); - this.pluginsMap.set(pluginName, plugin); - logger.log(`plugin registered with pluginName: ${pluginName}, config: `, config, 'meta:', meta); - } - - get(pluginName: string): LowCodePluginRuntime<ContextExtra> | undefined { - return this.pluginsMap.get(pluginName); - } - - getAll(): LowCodePluginRuntime<ContextExtra>[] { - return this.plugins; - } - - has(pluginName: string): boolean { - return this.pluginsMap.has(pluginName); - } - - async delete(pluginName: string): Promise<boolean> { - const plugin = this.plugins.find(({ name }) => name === pluginName); - if (!plugin) return false; - await plugin.destroy(); - const idx = this.plugins.indexOf(plugin); - this.plugins.splice(idx, 1); - return this.pluginsMap.delete(pluginName); - } - - async init(pluginPreference?: PluginPreference) { - const pluginNames: string[] = []; - const pluginObj: { [name: string]: LowCodePluginRuntime<ContextExtra> } = {}; - this.pluginPreference = pluginPreference; - this.plugins.forEach((plugin) => { - pluginNames.push(plugin.name); - pluginObj[plugin.name] = plugin; - }); - const { missingTasks, sequence } = sequencify(pluginObj, pluginNames); - invariant(!missingTasks.length, 'plugin dependency missing', missingTasks); - logger.log('load plugin sequence:', sequence); - - for (const pluginName of sequence) { - try { - await this.pluginsMap.get(pluginName)!.init(); - } catch (e) /* istanbul ignore next */ { - logger.error( - `Failed to init plugin:${pluginName}, it maybe affect those plugins which depend on this.`, - ); - logger.error(e); - } - } - } - - async destroy() { - for (const plugin of this.plugins) { - await plugin.destroy(); - } - } - - get size() { - return this.pluginsMap.size; - } - - getPluginPreference( - pluginName: string, - ): Record<string, PluginPreferenceValue> | null | undefined { - if (!this.pluginPreference) { - return null; - } - return this.pluginPreference.get(pluginName); - } - - toProxy() { - return new Proxy(this, { - get(target, prop, receiver) { - if (target.pluginsMap.has(prop as string)) { - // 禁用态的插件,直接返回 undefined - if (target.pluginsMap.get(prop as string)!.disabled) { - return undefined; - } - return target.pluginsMap.get(prop as string)?.toProxy(); - } - return Reflect.get(target, prop, receiver); - }, - }); - } - - setDisabled(pluginName: string, flag = true) { - logger.warn(`plugin:${pluginName} has been set disable:${flag}`); - this.pluginsMap.get(pluginName)?.setDisabled(flag); - } - - async dispose() { - await this.destroy(); - this.plugins = []; - this.pluginsMap.clear(); - } -} diff --git a/packages/editor-core/src/plugin/runtime.ts b/packages/editor-core/src/plugin/runtime.ts deleted file mode 100644 index 7ca41e40a..000000000 --- a/packages/editor-core/src/plugin/runtime.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { type LowCodePluginManager } from './manager'; -import { type PluginInstance, type PluginMeta } from './plugin'; -import { invariant, createLogger, type Logger } from '@alilc/lowcode-shared'; - -export interface IPluginRuntimeCore { - name: string; - dep: string[]; - disabled: boolean; - instance: PluginInstance; - logger: Logger; - meta: PluginMeta; - - init(forceInit?: boolean): void; - isInited(): boolean; - destroy(): void; - toProxy(): any; - setDisabled(flag: boolean): void; -} - -export interface IPluginRuntimeExportsAccessor { - [propName: string]: any; -} - -export class LowCodePluginRuntime<ContextExtra extends Record<string, any>> -implements IPluginRuntimeCore,IPluginRuntimeExportsAccessor { - private _inited: boolean; - /** - * 标识插件状态,是否被 disabled - */ - private _disabled: boolean; - - logger: Logger; - - constructor( - private pluginName: string, - private manager: LowCodePluginManager<ContextExtra>, - public instance: PluginInstance, - public meta: PluginMeta, - ) { - this.logger = createLogger({ level: 'warn', bizName: `plugin:${pluginName}` }); - } - - get name() { - return this.pluginName; - } - - get dep() { - if (typeof this.meta.dependencies === 'string') { - return [this.meta.dependencies]; - } - // compat legacy way to declare dependencies - const legacyDepValue = (this.instance as any).dep; - if (typeof legacyDepValue === 'string') { - return [legacyDepValue]; - } - return this.meta.dependencies || legacyDepValue || []; - } - - get disabled() { - return this._disabled; - } - - isInited() { - return this._inited; - } - - async init(forceInit?: boolean) { - if (this._inited && !forceInit) return; - this.logger.log('method init called'); - await this.instance.init?.call(undefined); - this._inited = true; - } - - async destroy() { - if (!this._inited) return; - this.logger.log('method destroy called'); - await this.instance?.destroy?.call(undefined); - this._inited = false; - } - - setDisabled(flag = true) { - this._disabled = flag; - } - - toProxy() { - invariant(this._inited, 'Could not call toProxy before init'); - - const exports = this.instance.exports?.(); - return new Proxy(this, { - get(target, prop, receiver) { - if ({}.hasOwnProperty.call(exports, prop)) { - return exports?.[prop as string]; - } - return Reflect.get(target, prop, receiver); - }, - }); - } - - async dispose() { - await this.manager.delete(this.name); - } -} diff --git a/packages/editor-core/src/setter.ts b/packages/editor-core/src/setter.ts deleted file mode 100644 index bf9f4421e..000000000 --- a/packages/editor-core/src/setter.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { IPublicModelSettingField, IPublicTypeCustomView, IPublicTypeRegisteredSetter } from '@alilc/lowcode-types'; -import { isCustomView } from '@alilc/lowcode-utils'; - -export class Setters { - settersMap = new Map<string, IPublicTypeRegisteredSetter & { - type: string; - }>(); - - constructor(readonly viewName: string = 'global') {} - - /** - * 获取指定 setter - * get setter by type - * @param type - * @returns - */ - getSetter = (type: string): IPublicTypeRegisteredSetter | null => { - return this.settersMap.get(type) || null; - }; - - /** - * 获取已注册的所有 settersMap - * get map of all registered setters - * @returns - */ - getSettersMap = () => { - return this.settersMap; - }; - - /** - * 注册一个 setter - * register a setter - * @param typeOrMaps - * @param setter - * @returns - */ - registerSetter = ( - typeOrMaps: string | { [key: string]: IPublicTypeCustomView | IPublicTypeRegisteredSetter }, - setter?: IPublicTypeCustomView | IPublicTypeRegisteredSetter, - ) => { - if (typeof typeOrMaps === 'object') { - Object.keys(typeOrMaps).forEach(type => { - this.registerSetter(type, typeOrMaps[type]); - }); - return; - } - if (!setter) { - return; - } - if (isCustomView(setter)) { - setter = { - component: setter, - // todo: intl - title: (setter as any).displayName || (setter as any).name || 'CustomSetter', - }; - } - if (!setter.initialValue) { - const initial = getInitialFromSetter(setter.component); - if (initial) { - setter.initialValue = (field: IPublicModelSettingField) => { - return initial.call(field, field.getValue()); - }; - } - } - this.settersMap.set(typeOrMaps, { type: typeOrMaps, ...setter }); - }; -} - -function getInitialFromSetter(setter: any) { - return setter && ( - setter.initial || setter.Initial - || (setter.type && (setter.type.initial || setter.type.Initial)) - ) || null; -} diff --git a/packages/editor-core/src/utils/assets-transform.ts b/packages/editor-core/src/utils/assets-transform.ts deleted file mode 100644 index 442b5762e..000000000 --- a/packages/editor-core/src/utils/assets-transform.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - IPublicTypeAssetsJson, - IPublicTypeComponentDescription, - IPublicTypePackage, - IPublicTypeRemoteComponentDescription, -} from '@alilc/lowcode-types'; - -// TODO: 该转换逻辑未来需要消化掉 -// 低代码 schema 转换逻辑 -export function assetsTransform(assets: IPublicTypeAssetsJson) { - const { components, packages } = assets; - const packageMaps = (packages || []).reduce( - (acc: Record<string, IPublicTypePackage>, cur: IPublicTypePackage) => { - const key = cur.id || cur.package || ''; - acc[key] = cur; - return acc; - }, - {}, - ); - components.forEach( - (componentDesc: IPublicTypeComponentDescription | IPublicTypeRemoteComponentDescription) => { - let { devMode } = componentDesc; - const { schema, reference } = componentDesc; - if ((devMode as string) === 'lowcode') { - devMode = 'lowCode'; - } else if (devMode === 'proCode') { - devMode = 'proCode'; - } - if (devMode) { - componentDesc.devMode = devMode; - } - if (devMode === 'lowCode' && !schema && reference) { - const referenceId = reference.id || ''; - componentDesc.schema = packageMaps[referenceId].schema as any; - } - }, - ); - return assets; -} diff --git a/packages/editor-core/src/utils/control.ts b/packages/editor-core/src/utils/control.ts deleted file mode 100644 index 40e3c2c93..000000000 --- a/packages/editor-core/src/utils/control.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { AnyFunction } from '@alilc/lowcode-shared'; - -let globalEventOn = true; - -export function setGlobalEventFlag(flag: boolean) { - globalEventOn = flag; -} - -export function switchGlobalEventOn() { - setGlobalEventFlag(true); -} - -export function switchGlobalEventOff() { - setGlobalEventFlag(false); -} - -export function isGlobalEventOn() { - return globalEventOn; -} - -export function runWithGlobalEventOff(fn: AnyFunction) { - switchGlobalEventOff(); - fn(); - switchGlobalEventOn(); -} - -type ListenerFunc = (...args: any[]) => void; -export function wrapWithEventSwitch(fn: ListenerFunc): ListenerFunc { - return (...args: any[]) => { - if (isGlobalEventOn()) fn(...args); - }; -} diff --git a/packages/editor-core/src/utils/focus-tracker.ts b/packages/editor-core/src/utils/focus-tracker.ts deleted file mode 100644 index c7155cd09..000000000 --- a/packages/editor-core/src/utils/focus-tracker.ts +++ /dev/null @@ -1,150 +0,0 @@ -export class FocusTracker { - private actives: Focusable[] = []; - - private modals: Array<{ checkDown: (e: MouseEvent) => boolean; checkOpen: () => boolean }> = []; - - get first() { - return this.actives[0]; - } - - mount(win: Window) { - const checkDown = (e: MouseEvent) => { - if (this.checkModalDown(e)) { - return; - } - const { first } = this; - if (first && !first.internalCheckInRange(e)) { - this.internalSuspenseItem(first); - first.internalTriggerBlur(); - } - }; - win.document.addEventListener('click', checkDown, true); - return () => { - win.document.removeEventListener('click', checkDown, true); - }; - } - - addModal(checkDown: (e: MouseEvent) => boolean, checkOpen: () => boolean) { - this.modals.push({ - checkDown, - checkOpen, - }); - } - - private checkModalOpen(): boolean { - return this.modals.some((item) => item.checkOpen()); - } - - private checkModalDown(e: MouseEvent): boolean { - return this.modals.some((item) => item.checkDown(e)); - } - - execSave() { - // has Modal return; - if (this.checkModalOpen()) { - return; - } - // catch - if (this.first) { - this.first.internalTriggerSave(); - } - } - - execEsc() { - const { first } = this; - if (first) { - this.internalSuspenseItem(first); - first.internalTriggerEsc(); - } - } - - create(config: FocusableConfig) { - return new Focusable(this, config); - } - - internalActiveItem(item: Focusable) { - const first = this.actives[0]; - if (first === item) { - return; - } - const i = this.actives.indexOf(item); - if (i > -1) { - this.actives.splice(i, 1); - } - this.actives.unshift(item); - if (!item.isModal && first) { - // trigger Blur - first.internalTriggerBlur(); - } - // trigger onActive - item.internalTriggerActive(); - } - - internalSuspenseItem(item: Focusable) { - const i = this.actives.indexOf(item); - if (i > -1) { - this.actives.splice(i, 1); - this.first?.internalTriggerActive(); - } - } -} - -export interface FocusableConfig { - range: HTMLElement | ((e: MouseEvent) => boolean); - modal?: boolean; // 模态窗口级别 - onEsc?: () => void; - onBlur?: () => void; - onSave?: () => void; - onActive?: () => void; -} - -export class Focusable { - readonly isModal: boolean; - - constructor(private tracker: FocusTracker, private config: FocusableConfig) { - this.isModal = config.modal == null ? false : config.modal; - } - - active() { - this.tracker.internalActiveItem(this); - } - - suspense() { - this.tracker.internalSuspenseItem(this); - } - - purge() { - this.tracker.internalSuspenseItem(this); - } - - internalCheckInRange(e: MouseEvent) { - const { range } = this.config; - if (!range) { - return false; - } - if (typeof range === 'function') { - return range(e); - } - return range.contains(e.target as HTMLElement); - } - - internalTriggerBlur() { - this.config.onBlur?.(); - } - - internalTriggerSave() { - if (this.config.onSave) { - this.config.onSave(); - return true; - } - return false; - } - - internalTriggerEsc() { - this.config.onEsc?.(); - } - - internalTriggerActive() { - this.config.onActive?.(); - } -} diff --git a/packages/editor-core/src/utils/get-public-path.ts b/packages/editor-core/src/utils/get-public-path.ts deleted file mode 100644 index 1bdb162a4..000000000 --- a/packages/editor-core/src/utils/get-public-path.ts +++ /dev/null @@ -1,5 +0,0 @@ -const publicPath = (document.currentScript as HTMLScriptElement)?.src.replace(/^(.*\/)[^/]+$/, '$1'); - -export function getPublicPath(): string { - return publicPath || ''; -} diff --git a/packages/editor-core/src/utils/index.ts b/packages/editor-core/src/utils/index.ts deleted file mode 100644 index c49a06ca0..000000000 --- a/packages/editor-core/src/utils/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './get-public-path'; -export * from './request'; -export * from './focus-tracker'; -export * from './control'; -export * from '../preference'; diff --git a/packages/editor-core/src/utils/request.ts b/packages/editor-core/src/utils/request.ts deleted file mode 100644 index 9461ee8d9..000000000 --- a/packages/editor-core/src/utils/request.ts +++ /dev/null @@ -1,133 +0,0 @@ -export function serialize(obj?: object): string { - if (!obj) { - return ''; - } - const rst: string[] = []; - Object.entries(obj || {}).forEach(([key, val]): void => { - if (val === null || val === undefined || val === '') return; - if (typeof val === 'object') rst.push(`${key}=${encodeURIComponent(JSON.stringify(val))}`); - else rst.push(`${key}=${encodeURIComponent(val)}`); - }); - return rst.join('&'); -} - -export function buildUrl(dataAPI: string, params?: object): string { - const paramStr = serialize(params); - if (paramStr) { - return dataAPI.indexOf('?') > 0 ? `${dataAPI}&${paramStr}` : `${dataAPI}?${paramStr}`; - } - return dataAPI; -} - -export function get( - dataAPI: string, - params?: object, - headers?: object, - otherProps?: object, -): Promise<any> { - const fetchHeaders = { - Accept: 'application/json', - ...headers, - }; - return request(buildUrl(dataAPI, params), 'GET', undefined, fetchHeaders, otherProps); -} - -export function post( - dataAPI: string, - params?: object, - headers?: object, - otherProps?: object, -): Promise<any> { - const fetchHeaders = { - Accept: 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - ...headers, - }; - return request( - dataAPI, - 'POST', - fetchHeaders['Content-Type'].indexOf('application/json') > -1 || Array.isArray(params) - ? JSON.stringify(params) - : serialize(params), - fetchHeaders, - otherProps, - ); -} - -export function request( - dataAPI: string, - method = 'GET', - data?: object | string, - headers?: object, - otherProps?: any, -): Promise<any> { - return new Promise((resolve, reject): void => { - if (otherProps && otherProps.timeout) { - setTimeout((): void => { - reject(new Error('timeout')); - }, otherProps.timeout); - } - fetch(dataAPI, { - method, - credentials: 'include', - headers, - body: data, - ...otherProps, - }) - .then((response: Response): any => { - switch (response.status) { - case 200: - case 201: - case 202: - return response.json(); - case 204: - if (method === 'DELETE') { - return { - success: true, - }; - } else { - return { - __success: false, - code: response.status, - }; - } - case 400: - case 401: - case 403: - case 404: - case 406: - case 410: - case 422: - case 500: - return response - .json() - .then((res: object): any => { - return { - __success: false, - code: response.status, - data: res, - }; - }) - .catch((): object => { - return { - __success: false, - code: response.status, - }; - }); - default: - return null; - } - }) - .then((json: any): void => { - if (json && json.__success !== false) { - resolve(json); - } else { - delete json.__success; - reject(json); - } - }) - .catch((err: Error): void => { - reject(err); - }); - }); -} diff --git a/packages/editor-skeleton/src/context.ts b/packages/editor-skeleton/src/context.ts deleted file mode 100644 index 58eb48ac6..000000000 --- a/packages/editor-skeleton/src/context.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createContext } from 'react'; -import { ISkeleton } from './skeleton'; - -export const SkeletonContext = createContext<ISkeleton>({} as any); diff --git a/packages/editor-skeleton/src/less-variables.less b/packages/editor-skeleton/src/less-variables.less deleted file mode 100644 index 017e432ce..000000000 --- a/packages/editor-skeleton/src/less-variables.less +++ /dev/null @@ -1,215 +0,0 @@ -/* - * 基础的 DPL 定义使用了 kuma base 的定义,参考: - * https://github.com/uxcore/kuma-base/tree/master/variables - */ - -/** - * =========================================================== - * ==================== Font Family ========================== - * =========================================================== - */ - -/* - * @font-family: "STHeiti", "Microsoft Yahei", "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, Verdana, sans-serif; - */ - -@font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Helvetica, Arial, sans-serif; -@font-family-code: Monaco, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Helvetica, Arial, - sans-serif; - -/** - * =========================================================== - * ===================== Color DPL =========================== - * =========================================================== - */ - -@brand-color-1: rgba(0, 108, 255, 1); -@brand-color-2: rgba(25, 122, 255, 1); -@brand-color-3: rgba(0, 96, 229, 1); - -@brand-color-1-3: rgba(0, 108, 255, 0.6); -@brand-color-1-4: rgba(0, 108, 255, 0.4); -@brand-color-1-5: rgba(0, 108, 255, 0.3); -@brand-color-1-6: rgba(0, 108, 255, 0.2); -@brand-color-1-7: rgba(0, 108, 255, 0.1); - -@brand-color: @brand-color-1; - -@white-alpha-1: rgb(255, 255, 255); // W-1 -@white-alpha-2: rgba(255, 255, 255, 0.8); // W-2 A80 -@white-alpha-3: rgba(255, 255, 255, 0.6); // W-3 A60 -@white-alpha-4: rgba(255, 255, 255, 0.4); // W-4 A40 -@white-alpha-5: rgba(255, 255, 255, 0.3); // W-5 A30 -@white-alpha-6: rgba(255, 255, 255, 0.2); // W-6 A20 -@white-alpha-7: rgba(255, 255, 255, 0.1); // W-7 A10 -@white-alpha-8: rgba(255, 255, 255, 0.06); // W-8 A6 - -@dark-alpha-1: rgba(0, 0, 0, 1); // D-1 A100 -@dark-alpha-2: rgba(0, 0, 0, 0.8); // D-2 A80 -@dark-alpha-3: rgba(0, 0, 0, 0.6); // D-3 A60 -@dark-alpha-4: rgba(0, 0, 0, 0.4); // D-4 A40 -@dark-alpha-5: rgba(0, 0, 0, 0.3); // D-5 A30 -@dark-alpha-6: rgba(0, 0, 0, 0.2); // D-6 A20 -@dark-alpha-7: rgba(0, 0, 0, 0.1); // D-7 A10 -@dark-alpha-8: rgba(0, 0, 0, 0.06); // D-8 A6 -@dark-alpha-9: rgba(0, 0, 0, 0.04); // D-9 A4 - -@normal-alpha-1: rgba(31, 56, 88, 1); // N-1 A100 -@normal-alpha-2: rgba(31, 56, 88, 0.8); // N-2 A80 -@normal-alpha-3: rgba(31, 56, 88, 0.6); // N-3 A60 -@normal-alpha-4: rgba(31, 56, 88, 0.4); // N-4 A40 -@normal-alpha-5: rgba(31, 56, 88, 0.3); // N-5 A30 -@normal-alpha-6: rgba(31, 56, 88, 0.2); // N-6 A20 -@normal-alpha-7: rgba(31, 56, 88, 0.1); // N-7 A10 -@normal-alpha-8: rgba(31, 56, 88, 0.06); // N-8 A6 -@normal-alpha-9: rgba(31, 56, 88, 0.04); // N-9 A4 - -@normal-3: #77879c; -@normal-4: #a3aebd; -@normal-5: #bac3cc; -@normal-6: #d1d7de; - -@gray-dark: #333; // N2_4 -@gray: #666; // N2_3 -@gray-light: #999; // N2_2 -@gray-lighter: #ccc; // N2_1 - -@brand-secondary: #2c2f33; // B2_3 -// 补色 -@brand-complement: #00b3e8; // B3_1 -// 复合 -@brand-comosite: #00c587; // B3_2 -// 浓度 -@brand-deep: #73461d; // B3_3 - -// F1-1 -@brand-danger: rgb(240, 70, 49); -// F1-2 (10% white) -@brand-danger-hover: rgba(240, 70, 49, 0.9); -// F1-3 (5% black) -@brand-danger-focus: rgba(240, 70, 49, 0.95); - -// F2-1 -@brand-warning: rgb(250, 189, 14); -// F3-1 -@brand-success: rgb(102, 188, 92); -// F4-1 -@brand-link: rgb(102, 188, 92); -// F4-2 -@brand-link-hover: #2e76a6; - -// F1-1-7 A10 -@brand-danger-alpha-7: rgba(240, 70, 49, 0.1); -// F1-1-8 A6 -@brand-danger-alpha-8: rgba(240, 70, 49, 0.8); -// F2-1-2 A80 -@brand-warning-alpha-2: rgba(250, 189, 14, 0.8); -// F2-1-7 A10 -@brand-warning-alpha-7: rgba(250, 189, 14, 0.1); -// F3-1-2 A80 -@brand-success-alpha-2: rgba(102, 188, 92, 0.8); -// F3-1-7 A10 -@brand-success-alpha-7: rgba(102, 188, 92, 0.1); -// F4-1-7 A10 -@brand-link-alpha-7: rgba(102, 188, 92, 0.1); - -// 文本色 -@text-primary-color: @dark-alpha-3; -@text-secondary-color: @normal-alpha-3; -@text-thirdary-color: @dark-alpha-4; -@text-disabled-color: @normal-alpha-5; -@text-helper-color: @dark-alpha-4; -@text-danger-color: @brand-danger; -@text-ali-color: #ec6c00; - -/** - * =========================================================== - * =================== Shadow Box ============================ - * =========================================================== - */ - -@box-shadow-1: 0 1px 4px 0 rgba(31, 56, 88, 0.15); // 1 级阴影,物体由原来存在于底面的物体展开,物体和底面关联紧密 -@box-shadow-2: 0 2px 10px 0 rgba(31, 56, 88, 0.15); // 2 级阴影,hover状态,物体层级较高 -@box-shadow-3: 0 4px 15px 0 rgba(31, 56, 88, 0.15); // 3 级阴影,当物体层级高于所有界面元素,弹窗用 - -/** - * =========================================================== - * ================= FontSize of Level ======================= - * =========================================================== - */ - -@fontSize-1: 26px; -@fontSize-2: 20px; -@fontSize-3: 16px; -@fontSize-4: 14px; -@fontSize-5: 12px; - -@fontLineHeight-1: 38px; -@fontLineHeight-2: 30px; -@fontLineHeight-3: 26px; -@fontLineHeight-4: 24px; -@fontLineHeight-5: 20px; - -/** - * =========================================================== - * ================= FontSize of Level ======================= - * =========================================================== - */ - -@global-border-radius: 3px; -@input-border-radius: 3px; -@popup-border-radius: 6px; - -/** - * =========================================================== - * ===================== Transistion ========================= - * =========================================================== - */ - -@transition-duration: 0.3s; -@transition-ease: cubic-bezier(0.23, 1, 0.32, 1); -@transition-delay: 0s; - -/** - * =========================================================== - * ================ Global Configruations ==================== - * =========================================================== - */ - -@topPaneHeight: 48px; -@actionpane-height: 48px; -@tabPaneWidth: 260px; -@input-standard-height: 32px; -@dockpane-width: 48px; - -/** - * =========================================================== - * =================== Deprecated Items ====================== - * =========================================================== - */ - -@head-bgcolor: @white-alpha-1; -@pane-bgcolor: @white-alpha-1; -@pane-dark-bgcolor: @white-alpha-1; -@pane-bdcolor: @normal-4; -@blank-bgcolor: @normal-5; -@title-bgcolor: @white-alpha-1; -@title-bdcolor: transparent; -@section-bgcolor: transparent; -@section-bdcolor: @white-alpha-1; -@button-bgcolor: @white-alpha-1; -@button-bdcolor: transparent; -@button-blue-color: @brand-color; -@button-blue-hover-color: @brand-color; -@sub-title-bgcolor: @white-alpha-1; -@sub-title-bdcolor: transparent; -@text-color: @text-primary-color; -@icon-color: @gray; -@icon-color-active: @gray-light; -@ghost-bgcolor: @dark-alpha-3; -@input-bgcolor: transparent; -@input-bdcolor: @normal-alpha-5; -@hover-color: #5a99cc; -@active-color: #5a99cc; -@disabled-color: #666; -@setter-popup-bg: rgb(80, 86, 109); diff --git a/packages/editor-skeleton/src/locale/index.ts b/packages/editor-skeleton/src/locale/index.ts deleted file mode 100644 index 4cb3b53cf..000000000 --- a/packages/editor-skeleton/src/locale/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createIntl } from '@alilc/lowcode-editor-core'; -import enUS from './en-US.json'; -import zhCN from './zh-CN.json'; - -const { intl, intlNode, getLocale, setLocale } = createIntl({ - 'en-US': enUS, - 'zh-CN': zhCN, -}); - -export { intl, intlNode, getLocale, setLocale }; diff --git a/packages/editor-skeleton/src/module.d.ts b/packages/editor-skeleton/src/module.d.ts deleted file mode 100644 index ebf77e1a4..000000000 --- a/packages/editor-skeleton/src/module.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'ric-shim'; diff --git a/packages/engine/package.json b/packages/engine/package.json index 473c914c5..bde4a4a2b 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -31,8 +31,8 @@ "dependencies": { "@alifd/next": "^1.27.8", "@alilc/lowcode-designer": "workspace:*", - "@alilc/lowcode-editor-core": "workspace:*", - "@alilc/lowcode-editor-skeleton": "workspace:*", + "@alilc/lowcode-core": "workspace:*", + "@alilc/lowcode-workbench": "workspace:*", "@alilc/lowcode-plugin-command": "workspace:*", "@alilc/lowcode-plugin-designer": "workspace:*", "@alilc/lowcode-plugin-outline-pane": "workspace:*", diff --git a/packages/engine/src/engine-core.ts b/packages/engine/src/engine-core.ts deleted file mode 100644 index cf2d38426..000000000 --- a/packages/engine/src/engine-core.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { createElement } from 'react'; -import { createRoot, type Root } from 'react-dom/client'; -import { - globalContext, - Editor, - commonEvent, - engineConfig, - Setters as InnerSetters, - Hotkey as InnerHotkey, - IEditor, - Command as InnerCommand, - LowCodePluginManager -} from '@alilc/lowcode-editor-core'; -import { - IPublicTypeEngineOptions, - IPublicModelDocumentModel, - IPublicTypePluginMeta, - IPublicTypeDisposable, - IPublicApiPlugins, - IPublicApiWorkspace, - IPublicEnumPluginRegisterLevel, - IPublicModelPluginContext, -} from '@alilc/lowcode-types'; -import { - Designer, - IDesigner, -} from '@alilc/lowcode-designer'; -import { Skeleton as InnerSkeleton, registerDefaults, Workbench } from '@alilc/lowcode-editor-skeleton'; - -import { - Workspace as InnerWorkspace, - Workbench as WorkSpaceWorkbench, - IWorkspace, -} from './workspace'; -import { - Hotkey, - Project, - Skeleton, - Setters, - Material, - Event, - Plugins, - Common, - Logger, - Canvas, - Workspace, - Config, - CommonUI, - Command, -} from './shell'; -import './modules/live-editing'; -import * as classes from './modules/classes'; -import symbols from './modules/symbols'; - -import { componentMetaParser } from './inner-plugins/component-meta-parser'; -import { setterRegistry } from './inner-plugins/setter-registry'; -import { defaultPanelRegistry } from './inner-plugins/default-panel-registry'; -import { shellModelFactory } from './modules/shell-model-factory'; -import { builtinHotkey } from './inner-plugins/builtin-hotkey'; -import { defaultContextMenu } from './inner-plugins/default-context-menu'; -import { CommandPlugin } from '@alilc/lowcode-plugin-command'; -import { OutlinePlugin } from '@alilc/lowcode-plugin-outline-pane'; -import { version } from '../package.json'; - -import '@alilc/lowcode-plugin-outline-pane/dist/style.css'; -import '@alilc/lowcode-editor-skeleton/dist/style.css'; -import '@alilc/lowcode-designer/dist/style.css'; -import '@alilc/lowcode-utils/dist/style.css'; - -export * from './modules/skeleton-types'; -export * from './modules/designer-types'; -export * from './modules/lowcode-types'; - -async function registryInnerPlugin( - designer: IDesigner, - editor: IEditor, - plugins: IPublicApiPlugins -): Promise<IPublicTypeDisposable> { - // 注册一批内置插件 - const componentMetaParserPlugin = componentMetaParser(designer); - const defaultPanelRegistryPlugin = defaultPanelRegistry(editor); - await plugins.register(OutlinePlugin, {}, { autoInit: true }); - await plugins.register(componentMetaParserPlugin); - await plugins.register(setterRegistry, {}); - await plugins.register(defaultPanelRegistryPlugin); - await plugins.register(builtinHotkey); - await plugins.register(registerDefaults, {}, { autoInit: true }); - await plugins.register(defaultContextMenu); - await plugins.register(CommandPlugin, {}); - - return () => { - plugins.delete(OutlinePlugin.pluginName); - plugins.delete(componentMetaParserPlugin.pluginName); - plugins.delete(setterRegistry.pluginName); - plugins.delete(defaultPanelRegistryPlugin.pluginName); - plugins.delete(builtinHotkey.pluginName); - plugins.delete(registerDefaults.pluginName); - plugins.delete(defaultContextMenu.pluginName); - plugins.delete(CommandPlugin.pluginName); - }; -} - -const innerWorkspace: IWorkspace = new InnerWorkspace( - registryInnerPlugin, - shellModelFactory -); -const workspace: IPublicApiWorkspace = new Workspace(innerWorkspace); -const editor = new Editor(); - -globalContext.register(editor, Editor); -globalContext.register(editor, 'editor'); -globalContext.register(innerWorkspace, 'workspace'); - -const engineContext: Partial<ILowCodePluginContextPrivate> = {}; - -const innerSkeleton = new InnerSkeleton(editor); -editor.set('skeleton', innerSkeleton); - -const designer = new Designer({ editor, shellModelFactory }); -editor.set('designer', designer); - -const { project: innerProject } = designer; -const project = new Project(innerProject); -editor.set('project', project); - -const innerHotkey = new InnerHotkey(); -const hotkey = new Hotkey(innerHotkey); -editor.set('innerHotkey', innerHotkey); - -const skeleton = new Skeleton(innerSkeleton, 'any', false); -const innerSetters = new InnerSetters(); -const setters = new Setters(innerSetters); -editor.set('setters', setters); - -const material = new Material(editor); -editor.set('material', material); - -const innerCommand = new InnerCommand(); -const command = new Command( - innerCommand, - engineContext as IPublicModelPluginContext -); -const commonUI = new CommonUI(editor); -const config = new Config(engineConfig); -const event = new Event(commonEvent, { prefix: 'common' }); -const logger = new Logger({ level: 'warn', bizName: 'common' }); -const common = new Common(); -const canvas = new Canvas(editor); - -const pluginContextApiAssembler: ILowCodePluginContextApiAssembler = { - assembleApis: ( - context: ILowCodePluginContextPrivate, - pluginName: string, - meta: IPublicTypePluginMeta - ) => { - context.hotkey = hotkey; - context.project = project; - context.skeleton = new Skeleton(innerSkeleton, pluginName, false); - context.setters = setters; - context.material = material; - - const eventPrefix = meta?.eventPrefix || 'common'; - context.event = new Event(commonEvent, { prefix: eventPrefix }); - - context.config = config; - context.common = common; - context.canvas = canvas; - context.plugins = plugins; - context.logger = new Logger({ level: 'warn', bizName: `plugin:${pluginName}` }); - context.workspace = workspace; - context.commonUI = commonUI; - - const commandScope = meta?.commandScope; - context.command = new Command( - innerCommand, context as IPublicModelPluginContext, { - commandScope, - }); - - context.registerLevel = IPublicEnumPluginRegisterLevel.Default; - context.isPluginRegisteredInWorkspace = false; - editor.set('pluginContext', context); - }, -}; - -const innerPlugins = new LowCodePluginManager(pluginContextApiAssembler); -const plugins = new Plugins(innerPlugins).toProxy(); -editor.set('innerPlugins' as any, innerPlugins); -editor.set('plugins' as any, plugins); - -engineContext.skeleton = skeleton; -engineContext.plugins = plugins; -engineContext.project = project; -engineContext.setters = setters; -engineContext.material = material; -engineContext.event = event; -engineContext.logger = logger; -engineContext.hotkey = hotkey; -engineContext.common = common; -engineContext.workspace = workspace; -engineContext.canvas = canvas; -engineContext.commonUI = commonUI; -engineContext.command = command; - -export { - skeleton, - plugins, - project, - setters, - material, - config, - event, - logger, - hotkey, - common, - workspace, - canvas, - commonUI, - command, -}; - -/** - * declare this is open-source version - * @deprecated - */ -export const isOpenSource = true; -engineConfig.set('isOpenSource', isOpenSource); - -engineConfig.set('ENGINE_VERSION', version); -export { version }; - -/** - * @deprecated - */ -export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = { - symbols, - classes, -}; - -// react root -let root: Root | undefined; - -export async function init( - container: HTMLElement, - options: IPublicTypeEngineOptions = {}, - pluginPreference?: PluginPreference -) { - await destroy(); - - engineConfig.setEngineOptions(options); - - if (engineConfig.get('enableWorkspaceMode')) { - innerWorkspace.enableAutoOpenFirstWindow = engineConfig.get('enableAutoOpenFirstWindow', true); - innerWorkspace.setActive(true); - innerWorkspace.initWindow(); - innerHotkey.activate(false); - await innerWorkspace.plugins.init(pluginPreference); - - if (!root) { - root = createRoot(container); - root.render( - createElement(WorkSpaceWorkbench, { - workspace: innerWorkspace, - }) - ); - } - } else { - await registryInnerPlugin(designer, editor, plugins); - await plugins.init(pluginPreference); - - if (!root) { - root = createRoot(container); - root.render( - createElement(Workbench, { - skeleton: innerSkeleton, - }) - ); - } - } -} - -export async function destroy() { - // remove all documents - const { documents } = project; - if (Array.isArray(documents) && documents.length > 0) { - documents.forEach( - (doc: IPublicModelDocumentModel) => project.removeDocument(doc) - ); - } - - // TODO: delete plugins except for core plugins - - // unmount DOM container, this will trigger React componentWillUnmount lifeCycle, - // so necessary cleanups will be done. - root?.unmount(); -} diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index 0c7ec397a..8824e5d07 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -1,9 +1,17 @@ -import { version } from './engine-core'; +import { bootstrapModules, createInstance } from '@alilc/lowcode-core'; +import { EngineMain } from './main'; -export * from './engine-core'; +export async function init( + container?: HTMLElement, + options?: IPublicTypeEngineOptions, + pluginPreference?: PluginPreference, +) { + if (!container) { + container = document.createElement('div'); + container.id = 'engine'; + document.body.appendChild(container); + } -console.log( - `%c AliLowCodeEngine %c v${version} `, - 'padding: 2px 1px; border-radius: 3px 0 0 3px; color: #fff; background: #606060; font-weight: bold;', - 'padding: 2px 1px; border-radius: 0 3px 3px 0; color: #fff; background: #42c02e; font-weight: bold;', -); + bootstrapModules(); + createInstance(EngineMain).startup(container); +} diff --git a/packages/engine/src/inner-plugins/builtin-hotkey.ts b/packages/engine/src/inner-plugins/builtin-hotkey.ts deleted file mode 100644 index 6d3291ce4..000000000 --- a/packages/engine/src/inner-plugins/builtin-hotkey.ts +++ /dev/null @@ -1,550 +0,0 @@ -/* eslint-disable max-len */ -import { isFormEvent, isNodeSchema, isNode } from '@alilc/lowcode-utils'; -import { - IPublicModelPluginContext, - IPublicEnumTransformStage, - IPublicModelNode, - IPublicTypeNodeSchema, - IPublicTypeNodeData, - IPublicEnumDragObjectType, - IPublicTypeDragNodeObject, -} from '@alilc/lowcode-types'; - -function insertChild( - container: IPublicModelNode, - originalChild: IPublicModelNode | IPublicTypeNodeData, - at?: number | null, -): IPublicModelNode | null { - let child = originalChild; - if (isNode(child) && (child as IPublicModelNode).isSlotNode) { - child = (child as IPublicModelNode).exportSchema(IPublicEnumTransformStage.Clone); - } - let node = null; - if (isNode(child)) { - node = (child as IPublicModelNode); - container.children?.insert(node, at); - } else { - node = container.document?.createNode(child) || null; - if (node) { - container.children?.insert(node, at); - } - } - - return (node as IPublicModelNode) || null; -} - -function insertChildren( - container: IPublicModelNode, - nodes: IPublicModelNode[] | IPublicTypeNodeData[], - at?: number | null, -): IPublicModelNode[] { - let index = at; - let node: any; - const results: IPublicModelNode[] = []; - // eslint-disable-next-line no-cond-assign - while ((node = nodes.pop())) { - node = insertChild(container, node, index); - results.push(node); - index = node.index; - } - return results; -} - -/** - * 获得合适的插入位置 - */ -function getSuitableInsertion( - pluginContext: IPublicModelPluginContext, - insertNode?: IPublicModelNode | IPublicTypeNodeSchema | IPublicTypeNodeSchema[], -): { target: IPublicModelNode; index?: number } | null { - const { project, material } = pluginContext; - const activeDoc = project.currentDocument; - if (!activeDoc) { - return null; - } - if ( - Array.isArray(insertNode) && - isNodeSchema(insertNode[0]) && - material.getComponentMeta(insertNode[0].componentName)?.isModal - ) { - if (!activeDoc.root) { - return null; - } - - return { - target: activeDoc.root, - }; - } - - const focusNode = activeDoc.focusNode!; - const nodes = activeDoc.selection.getNodes(); - const refNode = nodes.find((item) => focusNode.contains(item)); - let target; - let index: number | undefined; - if (!refNode || refNode === focusNode) { - target = focusNode; - } else if (refNode.componentMeta?.isContainer) { - target = refNode; - } else { - // FIXME!!, parent maybe null - target = refNode.parent!; - index = refNode.index! + 1; - } - - if (target && insertNode && !target.componentMeta?.checkNestingDown(target, insertNode)) { - return null; - } - - return { target, index }; -} - -/* istanbul ignore next */ -function getNextForSelect(next: IPublicModelNode | null, head?: any, parent?: IPublicModelNode | null): any { - if (next) { - if (!head) { - return next; - } - - let ret; - if (next.isContainerNode) { - const { children } = next; - if (children && !children.isEmptyNode) { - ret = getNextForSelect(children.get(0)); - if (ret) { - return ret; - } - } - } - - ret = getNextForSelect(next.nextSibling!); - if (ret) { - return ret; - } - } - - if (parent) { - return getNextForSelect(parent.nextSibling!, false, parent?.parent); - } - - return null; -} - -/* istanbul ignore next */ -function getPrevForSelect(prev: IPublicModelNode | null, head?: any, parent?: IPublicModelNode | null): any { - if (prev) { - let ret; - if (!head && prev.isContainerNode) { - const { children } = prev; - const lastChild = children && !children.isEmptyNode ? children.get(children.size - 1) : null; - - ret = getPrevForSelect(lastChild); - if (ret) { - return ret; - } - } - - if (!head) { - return prev; - } - - ret = getPrevForSelect(prev.prevSibling!); - if (ret) { - return ret; - } - } - - if (parent) { - return parent; - } - - return null; -} - -function getSuitablePlaceForNode(targetNode: IPublicModelNode, node: IPublicModelNode, ref: any): any { - const { document } = targetNode; - if (!document) { - return null; - } - - const dragNodeObject: IPublicTypeDragNodeObject = { - type: IPublicEnumDragObjectType.Node, - nodes: [node], - }; - - const focusNode = document?.focusNode; - // 如果节点是模态框,插入到根节点下 - if (node?.componentMeta?.isModal) { - return { container: focusNode, ref }; - } - - if (!ref && focusNode && targetNode.contains(focusNode)) { - if (document.checkNesting(focusNode, dragNodeObject)) { - return { container: focusNode }; - } - - return null; - } - - if (targetNode.isRootNode && targetNode.children) { - const dropElement = targetNode.children.filter((c) => { - if (!c.isContainerNode) { - return false; - } - if (document.checkNesting(c, dragNodeObject)) { - return true; - } - return false; - })[0]; - - if (dropElement) { - return { container: dropElement, ref }; - } - - if (document.checkNesting(targetNode, dragNodeObject)) { - return { container: targetNode, ref }; - } - - return null; - } - - if (targetNode.isContainerNode) { - if (document.checkNesting(targetNode, dragNodeObject)) { - return { container: targetNode, ref }; - } - } - - if (targetNode.parent) { - return getSuitablePlaceForNode(targetNode.parent, node, { index: targetNode.index }); - } - - return null; -} - -// 注册默认的 setters -export const builtinHotkey = (ctx: IPublicModelPluginContext) => { - return { - init() { - const { hotkey, project, logger, canvas } = ctx; - const { clipboard } = canvas; - // hotkey binding - hotkey.bind(['backspace', 'del'], (e: KeyboardEvent, action) => { - logger.info(`action ${action} is triggered`); - - if (canvas.isInLiveEditing) { - return; - } - // TODO: use focus-tracker - const doc = project.currentDocument; - if (isFormEvent(e) || !doc) { - return; - } - e.preventDefault(); - - const sel = doc.selection; - const topItems = sel.getTopNodes(); - // TODO: check can remove - topItems.forEach((node) => { - if (node?.canPerformAction('remove')) { - node && doc.removeNode(node); - } - }); - sel.clear(); - }); - - hotkey.bind('escape', (e: KeyboardEvent, action) => { - logger.info(`action ${action} is triggered`); - - if (canvas.isInLiveEditing) { - return; - } - const sel = project.currentDocument?.selection; - if (isFormEvent(e) || !sel) { - return; - } - e.preventDefault(); - - sel.clear(); - // currentFocus.esc(); - }); - - // command + c copy command + x cut - hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => { - logger.info(`action ${action} is triggered`); - if (canvas.isInLiveEditing) { - return; - } - const doc = project.currentDocument; - if (isFormEvent(e) || !doc) { - return; - } - const anchorValue = document.getSelection()?.anchorNode?.nodeValue; - if (anchorValue && typeof anchorValue === 'string') { - return; - } - e.preventDefault(); - - let selected = doc.selection.getTopNodes(true); - selected = selected.filter((node) => { - return node?.canPerformAction('copy'); - }); - if (!selected || selected.length < 1) { - return; - } - - const componentsMap = {}; - const componentsTree = selected.map((item) => item?.exportSchema(IPublicEnumTransformStage.Clone)); - - // FIXME: clear node.id - - const data = { type: 'nodeSchema', componentsMap, componentsTree }; - - clipboard.setData(data); - - const cutMode = action && action.indexOf('x') > 0; - if (cutMode) { - selected.forEach((node) => { - const parentNode = node?.parent; - parentNode?.select(); - node?.remove(); - }); - } - }); - - // command + v paste - hotkey.bind(['command+v', 'ctrl+v'], (e, action) => { - logger.info(`action ${action} is triggered`); - if (canvas.isInLiveEditing) { - return; - } - // TODO - const doc = project?.currentDocument; - if (isFormEvent(e) || !doc) { - return; - } - /* istanbul ignore next */ - clipboard.waitPasteData(e, ({ componentsTree }) => { - if (componentsTree) { - const { target, index } = getSuitableInsertion(ctx, componentsTree) || {}; - if (!target) { - return; - } - const canAddComponentsTree = componentsTree.filter((node: IPublicModelNode) => { - const dragNodeObject: IPublicTypeDragNodeObject = { - type: IPublicEnumDragObjectType.Node, - nodes: [node], - }; - return doc.checkNesting(target, dragNodeObject); - }); - if (canAddComponentsTree.length === 0) { - return; - } - const nodes = insertChildren(target, canAddComponentsTree, index); - if (nodes) { - doc.selection.selectAll(nodes.map((o) => o.id)); - setTimeout(() => canvas.activeTracker?.track(nodes[0]), 10); - } - } - }); - }); - - // command + z undo - hotkey.bind(['command+z', 'ctrl+z'], (e, action) => { - logger.info(`action ${action} is triggered`); - if (canvas.isInLiveEditing) { - return; - } - const history = project.currentDocument?.history; - if (isFormEvent(e) || !history) { - return; - } - - e.preventDefault(); - const selection = project.currentDocument?.selection; - const curSelected = selection?.selected && Array.from(selection?.selected); - history.back(); - selection?.selectAll(curSelected); - }); - - // command + shift + z redo - hotkey.bind(['command+y', 'ctrl+y', 'command+shift+z'], (e, action) => { - logger.info(`action ${action} is triggered`); - if (canvas.isInLiveEditing) { - return; - } - const history = project.currentDocument?.history; - if (isFormEvent(e) || !history) { - return; - } - e.preventDefault(); - const selection = project.currentDocument?.selection; - const curSelected = selection?.selected && Array.from(selection?.selected); - history.forward(); - selection?.selectAll(curSelected); - }); - - // sibling selection - hotkey.bind(['left', 'right'], (e, action) => { - logger.info(`action ${action} is triggered`); - if (canvas.isInLiveEditing) { - return; - } - const doc = project.currentDocument; - if (isFormEvent(e) || !doc) { - return; - } - e.preventDefault(); - const selected = doc.selection.getTopNodes(true); - if (!selected || selected.length < 1) { - return; - } - const firstNode = selected[0]; - const silbing = action === 'left' ? firstNode?.prevSibling : firstNode?.nextSibling; - silbing?.select(); - }); - - hotkey.bind(['up', 'down'], (e, action) => { - logger.info(`action ${action} is triggered`); - if (canvas.isInLiveEditing) { - return; - } - const doc = project.currentDocument; - if (isFormEvent(e) || !doc) { - return; - } - e.preventDefault(); - const selected = doc.selection.getTopNodes(true); - if (!selected || selected.length < 1) { - return; - } - const firstNode = selected[0]; - - if (action === 'down') { - const next = getNextForSelect(firstNode, true, firstNode?.parent); - next?.select(); - } else if (action === 'up') { - const prev = getPrevForSelect(firstNode, true, firstNode?.parent); - prev?.select(); - } - }); - - hotkey.bind(['option+left', 'option+right'], (e, action) => { - logger.info(`action ${action} is triggered`); - if (canvas.isInLiveEditing) { - return; - } - const doc = project.currentDocument; - if (isFormEvent(e) || !doc) { - return; - } - e.preventDefault(); - const selected = doc.selection.getTopNodes(true); - if (!selected || selected.length < 1) { - return; - } - // TODO: 此处需要增加判断当前节点是否可被操作移动,原ve里是用 node.canOperating()来判断 - // TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断 - - const firstNode = selected[0]; - const parent = firstNode?.parent; - if (!parent) return; - - const isPrev = action && /(left)$/.test(action); - - const silbing = isPrev ? firstNode.prevSibling : firstNode.nextSibling; - if (silbing) { - if (isPrev) { - parent.insertBefore(firstNode, silbing, true); - } else { - parent.insertAfter(firstNode, silbing, true); - } - firstNode?.select(); - } - }); - - hotkey.bind(['option+up'], (e, action) => { - logger.info(`action ${action} is triggered`); - if (canvas.isInLiveEditing) { - return; - } - const doc = project.currentDocument; - if (isFormEvent(e) || !doc) { - return; - } - e.preventDefault(); - const selected = doc.selection.getTopNodes(true); - if (!selected || selected.length < 1) { - return; - } - // TODO: 此处需要增加判断当前节点是否可被操作移动,原ve里是用 node.canOperating()来判断 - // TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断 - - const firstNode = selected[0]; - const parent = firstNode?.parent; - if (!parent) { - return; - } - - const silbing = firstNode.prevSibling; - if (silbing) { - if (silbing.isContainerNode) { - const place = getSuitablePlaceForNode(silbing, firstNode, null); - silbing.insertAfter(firstNode, place.ref, true); - } else { - parent.insertBefore(firstNode, silbing, true); - } - firstNode?.select(); - } else { - const place = getSuitablePlaceForNode(parent, firstNode, null); // upwards - if (place) { - const container = place.container.internalToShellNode(); - container.insertBefore(firstNode, place.ref); - firstNode?.select(); - } - } - }); - - hotkey.bind(['option+down'], (e, action) => { - logger.info(`action ${action} is triggered`); - if (canvas.isInLiveEditing) { - return; - } - const doc = project.getCurrentDocument(); - if (isFormEvent(e) || !doc) { - return; - } - e.preventDefault(); - const selected = doc.selection.getTopNodes(true); - if (!selected || selected.length < 1) { - return; - } - // TODO: 此处需要增加判断当前节点是否可被操作移动,原 ve 里是用 node.canOperating() 来判断 - // TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断 - - const firstNode = selected[0]; - const parent = firstNode?.parent; - if (!parent) { - return; - } - - const silbing = firstNode.nextSibling; - if (silbing) { - if (silbing.isContainerNode) { - silbing.insertBefore(firstNode, undefined); - } else { - parent.insertAfter(firstNode, silbing, true); - } - firstNode?.select(); - } else { - const place = getSuitablePlaceForNode(parent, firstNode, null); // upwards - if (place) { - const container = place.container.internalToShellNode(); - container.insertAfter(firstNode, place.ref, true); - firstNode?.select(); - } - } - }); - }, - }; -}; - -builtinHotkey.pluginName = '___builtin_hotkey___'; diff --git a/packages/engine/src/inner-plugins/component-meta-parser.ts b/packages/engine/src/inner-plugins/component-meta-parser.ts deleted file mode 100644 index 921503da4..000000000 --- a/packages/engine/src/inner-plugins/component-meta-parser.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { IPublicModelPluginContext } from '@alilc/lowcode-types'; - -export const componentMetaParser = (designer: any) => { - const fun = (ctx: IPublicModelPluginContext) => { - return { - init() { - const { material } = ctx; - material.onChangeAssets(() => { - const assets = material.getAssets(); - designer.buildComponentMetasMap(assets?.components ?? []); - }); - }, - }; - }; - - fun.pluginName = '___component_meta_parser___'; - - return fun; -}; diff --git a/packages/engine/src/inner-plugins/default-context-menu.ts b/packages/engine/src/inner-plugins/default-context-menu.ts deleted file mode 100644 index c9d5c845b..000000000 --- a/packages/engine/src/inner-plugins/default-context-menu.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { - IPublicEnumContextMenuType, - IPublicEnumDragObjectType, - IPublicEnumTransformStage, - IPublicModelNode, - IPublicModelPluginContext, - IPublicTypeDragNodeDataObject, - IPublicTypeNodeSchema, -} from '@alilc/lowcode-types'; -import { isProjectSchema } from '@alilc/lowcode-utils'; -import { Message } from '@alifd/next'; -import { intl } from '../locale'; - -function getNodesSchema(nodes: IPublicModelNode[]) { - const componentsTree = nodes.map((node) => node?.exportSchema(IPublicEnumTransformStage.Clone)); - const data = { type: 'nodeSchema', componentsMap: {}, componentsTree }; - return data; -} - -async function getClipboardText(): Promise<IPublicTypeNodeSchema[]> { - return new Promise((resolve, reject) => { - // 使用 Clipboard API 读取剪贴板内容 - navigator.clipboard.readText().then( - (text) => { - try { - const data = JSON.parse(text); - if (isProjectSchema(data)) { - resolve(data.componentsTree); - } else { - Message.error(intl('NotValidNodeData')); - reject( - new Error(intl('NotValidNodeData')), - ); - } - } catch (error) { - Message.error(intl('NotValidNodeData')); - reject(error); - } - }, - (err) => { - reject(err); - }, - ); - }); -} - -export const defaultContextMenu = (ctx: IPublicModelPluginContext) => { - const { material, canvas, common } = ctx; - const { clipboard } = canvas; - const { intl: utilsIntl } = common.utils; - - return { - init() { - material.addContextMenuOption({ - name: 'selectComponent', - title: intl('SelectComponents'), - condition: (nodes = []) => { - return nodes.length === 1; - }, - items: [ - { - name: 'nodeTree', - type: IPublicEnumContextMenuType.NODE_TREE, - }, - ], - }); - - material.addContextMenuOption({ - name: 'copyAndPaste', - title: intl('CopyAndPaste'), - disabled: (nodes = []) => { - return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0; - }, - condition: (nodes) => { - return nodes?.length === 1; - }, - action(nodes) { - const node = nodes?.[0]; - if (!node) { - return; - } - const { document: doc, parent, index } = node; - const data = getNodesSchema(nodes); - clipboard.setData(data); - - if (parent) { - const newNode = doc?.insertNode(parent, node, (index ?? 0) + 1, true); - newNode?.select(); - } - }, - }); - - material.addContextMenuOption({ - name: 'copy', - title: intl('Copy'), - disabled: (nodes = []) => { - return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0; - }, - condition(nodes = []) { - return nodes?.length > 0; - }, - action(nodes) { - if (!nodes || nodes.length < 1) { - return; - } - - const data = getNodesSchema(nodes); - clipboard.setData(data); - }, - }); - - material.addContextMenuOption({ - name: 'pasteToBottom', - title: intl('PasteToTheBottom'), - condition: (nodes) => { - return nodes?.length === 1; - }, - async action(nodes) { - if (!nodes || nodes.length < 1) { - return; - } - - const node = nodes[0]; - const { document: doc, parent, index } = node; - - try { - const nodeSchema = await getClipboardText(); - if (nodeSchema.length === 0) { - return; - } - if (parent) { - const canAddNodes = nodeSchema.filter((nodeSchema: IPublicTypeNodeSchema) => { - const dragNodeObject: IPublicTypeDragNodeDataObject = { - type: IPublicEnumDragObjectType.NodeData, - data: nodeSchema, - }; - return doc?.checkNesting(parent, dragNodeObject); - }); - if (canAddNodes.length === 0) { - Message.error(`${nodeSchema.map(d => utilsIntl(d.title || d.componentName)).join(',')}等组件无法放置到${utilsIntl(parent.title || parent.componentName as any)}内`); - return; - } - const nodes: IPublicModelNode[] = []; - canAddNodes.forEach((schema, schemaIndex) => { - const node = doc?.insertNode(parent, schema, (index ?? 0) + 1 + schemaIndex, true); - node && nodes.push(node); - }); - doc?.selection.selectAll(nodes.map((node) => node?.id)); - } - } catch (error) { - console.error(error); - } - }, - }); - - material.addContextMenuOption({ - name: 'pasteToInner', - title: intl('PasteToTheInside'), - condition: (nodes) => { - return nodes?.length === 1; - }, - disabled: (nodes = []) => { - // 获取粘贴数据 - const node = nodes?.[0]; - return !node.isContainerNode; - }, - async action(nodes) { - const node = nodes?.[0]; - if (!node) { - return; - } - const { document: doc } = node; - - try { - const nodeSchema = await getClipboardText(); - const index = node.children?.size || 0; - if (nodeSchema.length === 0) { - return; - } - const canAddNodes = nodeSchema.filter((nodeSchema: IPublicTypeNodeSchema) => { - const dragNodeObject: IPublicTypeDragNodeDataObject = { - type: IPublicEnumDragObjectType.NodeData, - data: nodeSchema, - }; - return doc?.checkNesting(node, dragNodeObject); - }); - if (canAddNodes.length === 0) { - Message.error(`${nodeSchema.map(d => utilsIntl(d.title || d.componentName)).join(',')}等组件无法放置到${utilsIntl(node.title || node.componentName as any)}内`); - return; - } - - const nodes: IPublicModelNode[] = []; - nodeSchema.forEach((schema, schemaIndex) => { - const newNode = doc?.insertNode(node, schema, (index ?? 0) + 1 + schemaIndex, true); - newNode && nodes.push(newNode); - }); - doc?.selection.selectAll(nodes.map((node) => node?.id)); - } catch (error) { - console.error(error); - } - }, - }); - - material.addContextMenuOption({ - name: 'delete', - title: intl('Delete'), - disabled(nodes = []) { - return nodes?.filter((node) => !node?.canPerformAction('remove')).length > 0; - }, - condition(nodes = []) { - return nodes.length > 0; - }, - action(nodes) { - nodes?.forEach((node) => { - node.remove(); - }); - }, - }); - }, - }; -}; - -defaultContextMenu.pluginName = '___default_context_menu___'; diff --git a/packages/engine/src/inner-plugins/default-panel-registry.tsx b/packages/engine/src/inner-plugins/default-panel-registry.tsx deleted file mode 100644 index f064526c0..000000000 --- a/packages/engine/src/inner-plugins/default-panel-registry.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { IPublicModelPluginContext } from '@alilc/lowcode-types'; -import { SettingsPrimaryPane } from '@alilc/lowcode-editor-skeleton'; -import DesignerPlugin from '@alilc/lowcode-plugin-designer'; - -import '@alilc/lowcode-plugin-designer/dist/style.css'; - -// 注册默认的面板 -export const defaultPanelRegistry = (editor: any) => { - const fun = (ctx: IPublicModelPluginContext) => { - return { - init() { - const { skeleton, config } = ctx; - skeleton.add({ - area: 'mainArea', - name: 'designer', - type: 'Widget', - content: <DesignerPlugin - engineConfig={config} - engineEditor={editor} - />, - }); - if (!config.get('disableDefaultSettingPanel')) { - skeleton.add({ - area: 'rightArea', - name: 'settingsPane', - type: 'Panel', - content: <SettingsPrimaryPane - engineEditor={editor} - />, - props: { - ignoreRoot: true, - }, - panelProps: { - ...(config.get('defaultSettingPanelProps') || {}), - }, - }); - } - }, - }; - }; - - fun.pluginName = '___default_panel___'; - - return fun; -}; - -export default defaultPanelRegistry; diff --git a/packages/engine/src/inner-plugins/setter-registry.ts b/packages/engine/src/inner-plugins/setter-registry.ts deleted file mode 100644 index cf3998222..000000000 --- a/packages/engine/src/inner-plugins/setter-registry.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { IPublicModelPluginContext } from '@alilc/lowcode-types'; - -// 注册默认的 setters -export const setterRegistry = (ctx: IPublicModelPluginContext) => { - return { - init() { - const { config } = ctx; - if (config.get('disableDefaultSetters')) return; - - // const builtinSetters = require('@alilc/lowcode-engine-ext')?.setters; - // @ts-expect-error: todo remove - const builtinSetters = window.AliLowCodeEngineExt?.setters; - if (builtinSetters) { - ctx.setters.registerSetter(builtinSetters); - } - }, - }; -}; - -setterRegistry.pluginName = '___setter_registry___'; diff --git a/packages/engine/src/locale/en-US.json b/packages/engine/src/locale/en-US.json deleted file mode 100644 index e93160707..000000000 --- a/packages/engine/src/locale/en-US.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "NotValidNodeData": "Not valid node data", - "SelectComponents": "Select components", - "CopyAndPaste": "Copy and Paste", - "Copy": "Copy", - "PasteToTheBottom": "Paste to the bottom", - "PasteToTheInside": "Paste to the inside", - "Delete": "Delete" -} diff --git a/packages/engine/src/locale/index.ts b/packages/engine/src/locale/index.ts deleted file mode 100644 index 5ff70ad8f..000000000 --- a/packages/engine/src/locale/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createIntl } from '@alilc/lowcode-editor-core'; -import enUS from './en-US.json'; -import zhCN from './zh-CN.json'; - -const { intl, getLocale } = createIntl?.({ - 'en-US': enUS, - 'zh-CN': zhCN, -}) || { - intl: (id) => { - // @ts-ignore - return zhCN[id]; - }, -}; - -export { intl, enUS, zhCN, getLocale }; diff --git a/packages/engine/src/locale/zh-CN.json b/packages/engine/src/locale/zh-CN.json deleted file mode 100644 index 9b68b7149..000000000 --- a/packages/engine/src/locale/zh-CN.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "NotValidNodeData": "不是有效的节点数据", - "SelectComponents": "选择组件", - "CopyAndPaste": "复制", - "Copy": "拷贝", - "PasteToTheBottom": "粘贴至下方", - "PasteToTheInside": "粘贴至内部", - "Delete": "删除" -} diff --git a/packages/engine/src/main.ts b/packages/engine/src/main.ts new file mode 100644 index 000000000..269dc07c5 --- /dev/null +++ b/packages/engine/src/main.ts @@ -0,0 +1,12 @@ +import { Provide } from '@alilc/lowcode-core'; +import { IWorkspaceMainService } from './workspace'; + +@Provide('EngineMain') +export class EngineMain { + constructor(@IWorkspaceMainService private workspaceMainService: IWorkspaceMainService) {} + + startup(container: HTMLElement): void { + console.log('%c [ container ]-9', 'font-size:13px; background:pink; color:#bf2c9f;', container); + this.workspaceMainService.initialize(); + } +} diff --git a/packages/engine/src/module.d.ts b/packages/engine/src/module.d.ts deleted file mode 100644 index ebf77e1a4..000000000 --- a/packages/engine/src/module.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'ric-shim'; diff --git a/packages/engine/src/modules/classes.ts b/packages/engine/src/modules/classes.ts deleted file mode 100644 index 1a63b8da7..000000000 --- a/packages/engine/src/modules/classes.ts +++ /dev/null @@ -1,15 +0,0 @@ -export { - Project, - Skeleton, - DocumentModel, - Node, - NodeChildren, - History, - SettingPropEntry, - SettingTopEntry, - Selection, - Prop, - SimulatorHost, - SkeletonItem, -} from '../shell'; -export { Node as InnerNode } from '@alilc/lowcode-designer'; diff --git a/packages/engine/src/modules/designer-types.ts b/packages/engine/src/modules/designer-types.ts deleted file mode 100644 index 1d9a1a08b..000000000 --- a/packages/engine/src/modules/designer-types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as designerCabin from '@alilc/lowcode-designer'; - -// 这样做的目的是为了去除 Node / DocumentModel 等的值属性,仅保留类型属性 -export type Node = designerCabin.Node; -export type DocumentModel = designerCabin.DocumentModel; -export type EditingTarget = designerCabin.EditingTarget; -export type SaveHandler = designerCabin.SaveHandler; -export type ComponentMeta = designerCabin.ComponentMeta; -export type SettingField = designerCabin.SettingField; -export type ILowCodePluginManager = designerCabin.ILowCodePluginManager; -export type PluginPreference = designerCabin.PluginPreference; diff --git a/packages/engine/src/modules/live-editing.ts b/packages/engine/src/modules/live-editing.ts deleted file mode 100644 index f1f32b88f..000000000 --- a/packages/engine/src/modules/live-editing.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { EditingTarget, Node as DocNode, SaveHandler, LiveEditing } from '@alilc/lowcode-designer'; -import { isJSExpression } from '@alilc/lowcode-utils'; - -function getText(node: DocNode, prop: string) { - const p = node.getProp(prop, false); - if (!p || p.isUnset()) { - return null; - } - let v = p.getValue(); - if (isJSExpression(v)) { - v = v.mock; - } - if (v == null) { - return null; - } - if (p.type === 'literal') { - return v; - } - return Symbol.for('not-literal'); -} - -export function liveEditingRule(target: EditingTarget) { - // for vision components specific - const { node, event } = target; - - const targetElement = event.target as HTMLElement; - - if (!Array.from(targetElement.childNodes).every((item) => item.nodeType === Node.TEXT_NODE)) { - return null; - } - - const { innerText } = targetElement; - const propTarget = ['title', 'label', 'text', 'content', 'children'].find((prop) => { - return equalText(getText(node, prop), innerText); - }); - - if (propTarget) { - return { - propElement: targetElement, - propTarget, - }; - } - return null; -} - -function equalText(v: any, innerText: string) { - // TODO: enhance compare text logic - if (typeof v !== 'string') { - return false; - } - return v.trim() === innerText; -} - -export const liveEditingSaveHander: SaveHandler = { - condition: (prop) => { - return prop.type === 'expression'; - }, - onSaveContent: (content, prop) => { - const v = prop.getValue(); - let data = v; - if (isJSExpression(v)) { - data = v.mock; - } - data = content; - if (isJSExpression(v)) { - prop.setValue({ - type: 'JSExpression', - value: v.value, - mock: data, - }); - } else { - prop.setValue(data); - } - }, -}; -// TODO: -// 非文本编辑 -// 国际化数据,改变当前 -// JSExpression, 改变 mock 或 弹出绑定变量 - -LiveEditing.addLiveEditingSpecificRule(liveEditingRule); -LiveEditing.addLiveEditingSaveHandler(liveEditingSaveHander); diff --git a/packages/engine/src/modules/lowcode-types.ts b/packages/engine/src/modules/lowcode-types.ts deleted file mode 100644 index c6a6bb4ec..000000000 --- a/packages/engine/src/modules/lowcode-types.ts +++ /dev/null @@ -1 +0,0 @@ -export type { IPublicTypeNodeSchema } from '@alilc/lowcode-types'; diff --git a/packages/engine/src/modules/shell-model-factory.ts b/packages/engine/src/modules/shell-model-factory.ts deleted file mode 100644 index bd5dccd6d..000000000 --- a/packages/engine/src/modules/shell-model-factory.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - INode, - ISettingField, -} from '@alilc/lowcode-designer'; -import { IShellModelFactory, IPublicModelNode } from '@alilc/lowcode-types'; -import { IPublicModelSettingField } from '../../../types/src/shell/model/setting-field'; -import { - Node, - SettingField, -} from '../shell'; - -class ShellModelFactory implements IShellModelFactory { - createNode(node: INode | null | undefined): IPublicModelNode | null { - return Node.create(node); - } - createSettingField(prop: ISettingField): IPublicModelSettingField { - return SettingField.create(prop); - } -} - -export const shellModelFactory = new ShellModelFactory(); diff --git a/packages/engine/src/modules/skeleton-types.ts b/packages/engine/src/modules/skeleton-types.ts deleted file mode 100644 index 8cce1c08c..000000000 --- a/packages/engine/src/modules/skeleton-types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { IPublicTypeWidgetBaseConfig as innerIWidgetBaseConfig } from '@alilc/lowcode-types'; - -export type IWidgetBaseConfig = innerIWidgetBaseConfig; diff --git a/packages/engine/src/modules/symbols.ts b/packages/engine/src/modules/symbols.ts deleted file mode 100644 index a066231f3..000000000 --- a/packages/engine/src/modules/symbols.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - projectSymbol, - documentSymbol, - nodeSymbol, - nodeChildrenSymbol, - designerSymbol, - skeletonSymbol, - editorSymbol, - settingFieldSymbol, - settingTopEntrySymbol, - designerCabinSymbol, - propSymbol, - simulatorHostSymbol, - skeletonItemSymbol, - editorCabinSymbol, - skeletonCabinSymbol, - simulatorRenderSymbol, -} from '../shell'; - -export default { - projectSymbol, - documentSymbol, - nodeSymbol, - nodeChildrenSymbol, - skeletonSymbol, - editorSymbol, - designerSymbol, - settingPropEntrySymbol: settingFieldSymbol, - settingTopEntrySymbol, - designerCabinSymbol, - editorCabinSymbol, - skeletonCabinSymbol, - propSymbol, - simulatorHostSymbol, - skeletonItemSymbol, - simulatorRenderSymbol, -}; diff --git a/packages/engine/src/plugin/context.ts b/packages/engine/src/plugin/context.ts new file mode 100644 index 000000000..f1da5af4c --- /dev/null +++ b/packages/engine/src/plugin/context.ts @@ -0,0 +1,74 @@ +import { createEventBus, EventBus } from '@alilc/lowcode-shared'; +import type { PluginMeta, PluginPreferenceValue, PluginDeclaration } from './types'; +import { PluginManager } from './manager'; + +export interface PluginPreferenceMananger { + getPreferenceValue: ( + key: string, + defaultValue?: PluginPreferenceValue, + ) => PluginPreferenceValue | undefined; +} + +export interface PluginContextOptions<ContextExtra extends Record<string, any>> { + pluginName: string; + meta?: PluginMeta; + enhance?: (context: PluginContext<ContextExtra>, pluginName: string, meta: PluginMeta) => void; +} + +export class PluginContext<ContextExtra extends Record<string, any>> { + #pluginManager: PluginManager<ContextExtra>; + #meta: PluginMeta = {}; + + public pluginName: string; + + public pluginEvent: EventBus; + + public preference: PluginPreferenceMananger; + + constructor( + options: PluginContextOptions<ContextExtra>, + pluginManager: PluginManager<ContextExtra>, + ) { + this.pluginName = options.pluginName; + this.pluginEvent = createEventBus(this.pluginName); + + this.#pluginManager = pluginManager; + if (options.meta) this.#meta = options.meta; + + options.enhance?.(this, this.pluginName, this.#meta); + + /** + * 管理器初始化时可以提供全局配置给到各插件,通过这个方法可以获得本插件对应的配置 + * use this to get preference config for this plugin when init + * todo: 这个全局配置是否真的有必要??? + */ + this.preference = { + getPreferenceValue: (key, defaultValue) => { + if ( + !this.#meta.preferenceDeclaration || + !isValidPreferenceKey(key, this.#meta.preferenceDeclaration) + ) { + return undefined; + } + const globalPluginPreference = + this.#pluginManager.getPluginPreference(this.pluginName) ?? {}; + if (globalPluginPreference[key] === undefined || globalPluginPreference[key] === null) { + return defaultValue; + } + return globalPluginPreference[key]; + }, + }; + } +} + +export function isValidPreferenceKey( + key: string, + preferenceDeclaration?: PluginDeclaration, +): boolean { + if (!preferenceDeclaration || !Array.isArray(preferenceDeclaration.properties)) { + return false; + } + return preferenceDeclaration.properties.some((prop) => { + return prop.key === key; + }); +} diff --git a/packages/engine/src/plugin/index.ts b/packages/engine/src/plugin/index.ts new file mode 100644 index 000000000..1d1440826 --- /dev/null +++ b/packages/engine/src/plugin/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './manager'; diff --git a/packages/engine/src/plugin/manager.ts b/packages/engine/src/plugin/manager.ts new file mode 100644 index 000000000..b7063b393 --- /dev/null +++ b/packages/engine/src/plugin/manager.ts @@ -0,0 +1,240 @@ +import { createLogger, invariant } from '@alilc/lowcode-shared'; +import { isPlainObject } from 'lodash-es'; +import { sequencify } from './utils'; +import { PluginRuntime } from './runtime'; +import type { PluginCreater, PluginPreferenceValue, PluginDeclaration } from './types'; +import { PluginContext, type PluginContextOptions, isValidPreferenceKey } from './context'; + +const logger = createLogger({ level: 'warn', bizName: 'pluginManager' }); + +export type PluginPreference = Map<string, Record<string, PluginPreferenceValue>>; + +export interface PluginRegisterOptions { + /** + * Will enable plugin registered with auto-initialization immediately + * other than plugin-manager init all plugins at certain time. + * It is helpful when plugin register is later than plugin-manager initialization. + */ + autoInit?: boolean; + /** + * allow overriding existing plugin with same name when override === true + */ + override?: boolean; +} + +/** + * plugin manager + */ +export class PluginManager<ContextExtra extends Record<string, any>> { + #pluginsMap: Map<string, PluginRuntime<ContextExtra>> = new Map(); + + #pluginContextMap: Map<string, PluginContext<ContextExtra>> = new Map(); + + #contextEnhancer: PluginContextOptions<ContextExtra>['enhance'] = () => {}; + + #pluginPreference: PluginPreference | undefined; + + constructor(contextEnhancer?: PluginContextOptions<ContextExtra>['enhance']) { + if (contextEnhancer) { + this.#contextEnhancer = contextEnhancer; + } + } + + #getPluginContext = (options: PluginContextOptions<ContextExtra>) => { + const { pluginName } = options; + let context = this.#pluginContextMap.get(pluginName); + if (!context) { + context = new PluginContext(options, this); + this.#pluginContextMap.set(pluginName, context); + } + return context; + }; + + /** + * register a plugin + * @param pluginConfigCreator - a creator function which returns the plugin config + * @param options - the plugin options + * @param registerOptions - the plugin register options + */ + async register( + pluginCreater: PluginCreater<PluginContext<ContextExtra>>, + options?: any, + registerOptions?: PluginRegisterOptions, + ): Promise<void> { + // registerOptions maybe in the second place + if (isPluginRegisterOptions(options)) { + registerOptions = options; + options = {}; + } + + const { pluginName, meta = {} } = pluginCreater; + // const { engines } = meta; + // filter invalid eventPrefix + // const isReservedPrefix = RESERVED_EVENT_PREFIX.find((item) => item === eventPrefix); + // if (isReservedPrefix) { + // meta.eventPrefix = undefined; + // logger.warn( + // `plugin ${pluginName} is trying to use ${eventPrefix} as event prefix, which is a reserved event prefix, please use another one`, + // ); + // } + + const ctx = this.#getPluginContext({ + pluginName: pluginCreater.pluginName, + meta, + enhance: this.#contextEnhancer, + }); + + // const pluginTransducer = engineConfig.get('customPluginTransducer', null); + // const newPluginModel = pluginTransducer + // ? await pluginTransducer(pluginModel, ctx, options) + // : pluginModel; + + // const customFilterValidOptions = engineConfig.get( + // 'customPluginFilterOptions', + // filterValidOptions, + // ); + const newOptions = filterValidOptions(options, meta.preferenceDeclaration); + + const pluginInstance = pluginCreater(ctx, newOptions); + + invariant(pluginName, 'pluginConfigCreator.pluginName required', pluginInstance); + + const allowOverride = registerOptions?.override === true; + + if (this.#pluginsMap.has(pluginName)) { + if (!allowOverride) { + throw new Error(`Plugin with name ${pluginName} exists`); + } else { + // clear existing plugin + const originalPlugin = this.#pluginsMap.get(pluginName); + logger.log( + 'plugin override, originalPlugin with name ', + pluginName, + ' will be destroyed, config:', + originalPlugin?.instance, + ); + originalPlugin?.destroy(); + this.#pluginsMap.delete(pluginName); + } + } + + const pluginRuntime = new PluginRuntime(pluginName, this, pluginInstance, meta); + // support initialization of those plugins which registered + // after normal initialization by plugin-manager + if (registerOptions?.autoInit) { + await pluginInstance.init(); + } + this.#pluginsMap.set(pluginName, pluginRuntime); + logger.log( + `plugin registered with pluginName: ${pluginName}, config: `, + pluginInstance, + 'meta:', + meta, + ); + } + + get(pluginName: string): PluginRuntime<ContextExtra> | undefined { + return this.#pluginsMap.get(pluginName); + } + + getAll(): PluginRuntime<ContextExtra>[] { + return [...this.#pluginsMap.values()]; + } + + has(pluginName: string): boolean { + return this.#pluginsMap.has(pluginName); + } + + async delete(pluginName: string): Promise<boolean> { + const plugin = this.#pluginsMap.get(pluginName); + if (!plugin) return false; + await plugin.destroy(); + return this.#pluginsMap.delete(pluginName); + } + + async init(pluginPreference?: PluginPreference) { + // 管理器初始化时可以提供全局配置给到各插件 + // 是否有必要? + this.#pluginPreference = pluginPreference; + + const pluginNames: string[] = []; + const pluginObj: { [name: string]: PluginRuntime<ContextExtra> } = {}; + + this.#pluginsMap.forEach((plugin) => { + pluginNames.push(plugin.name); + pluginObj[plugin.name] = plugin; + }); + + const { missingTasks, sequence } = sequencify(pluginObj, pluginNames); + invariant(!missingTasks.length, 'plugin dependency missing', missingTasks); + logger.log('load plugin sequence:', sequence); + + for (const pluginName of sequence) { + try { + await this.#pluginsMap.get(pluginName)!.init(); + } catch (e) /* istanbul ignore next */ { + logger.error( + `Failed to init plugin:${pluginName}, it maybe affect those plugins which depend on this.`, + ); + logger.error(e); + } + } + } + + async destroy() { + for (const plugin of this.#pluginsMap.values()) { + await plugin.destroy(); + } + } + + get size() { + return this.#pluginsMap.size; + } + + getPluginPreference(pluginName: string): Record<string, PluginPreferenceValue> | undefined { + return this.#pluginPreference?.get(pluginName); + } + + toProxy() { + return new Proxy(this, { + get(target, prop, receiver) { + if (target.#pluginsMap.has(prop as string)) { + // 禁用态的插件,直接返回 undefined + if (target.#pluginsMap.get(prop as string)!.disabled) { + return undefined; + } + return target.#pluginsMap.get(prop as string)?.toProxy(); + } + return Reflect.get(target, prop, receiver); + }, + }); + } + + setDisabled(pluginName: string, flag = true) { + logger.warn(`plugin:${pluginName} has been set disable:${flag}`); + this.#pluginsMap.get(pluginName)?.setDisabled(flag); + } + + async dispose() { + await this.destroy(); + this.#pluginsMap.clear(); + } +} + +function isPluginRegisterOptions(opts: any): opts is PluginRegisterOptions { + return opts && ('autoInit' in opts || 'override' in opts); +} + +function filterValidOptions(opts: any, preferenceDeclaration?: PluginDeclaration) { + if (!opts || !isPlainObject(opts)) return opts; + const filteredOpts = {} as any; + Object.keys(opts).forEach((key) => { + if (isValidPreferenceKey(key, preferenceDeclaration)) { + const v = opts[key]; + if (v !== undefined && v !== null) { + filteredOpts[key] = v; + } + } + }); + return filteredOpts; +} diff --git a/packages/engine/src/plugin/runtime.ts b/packages/engine/src/plugin/runtime.ts new file mode 100644 index 000000000..75da5185e --- /dev/null +++ b/packages/engine/src/plugin/runtime.ts @@ -0,0 +1,82 @@ +import { PluginManager } from './manager'; +import { type PluginInstance, type PluginMeta } from './types'; +import { invariant, createLogger, type Logger } from '@alilc/lowcode-shared'; + +export interface PluginRuntimeExportsAccessor { + [propName: string]: any; +} + +export class PluginRuntime<ContextExtra extends Record<string, any>> { + #inited: boolean; + /** + * 标识插件状态,是否被 disabled + */ + #disabled: boolean; + + #logger: Logger; + + constructor( + private pluginName: string, + private manager: PluginManager<ContextExtra>, + public instance: PluginInstance, + public meta: PluginMeta, + ) { + this.#logger = createLogger({ level: 'warn', bizName: `plugin:${pluginName}` }); + } + + get name() { + return this.pluginName; + } + + get dep() { + if (typeof this.meta.dependencies === 'string') { + return [this.meta.dependencies]; + } + + return this.meta.dependencies || []; + } + + get disabled() { + return this.#disabled; + } + + isInited() { + return this.#inited; + } + + async init(forceInit?: boolean) { + if (this.#inited && !forceInit) return; + this.#logger.log('method init called'); + await this.instance.init?.call(undefined); + this.#inited = true; + } + + async destroy() { + if (!this.#inited) return; + this.#logger.log('method destroy called'); + await this.instance?.destroy?.call(undefined); + this.#inited = false; + } + + setDisabled(flag = true) { + this.#disabled = flag; + } + + toProxy(): PluginRuntimeExportsAccessor { + invariant(this.#inited, 'Could not call toProxy before init'); + + const exports = this.instance.exports?.(); + return new Proxy(this, { + get(target, prop, receiver) { + if ({}.hasOwnProperty.call(exports, prop)) { + return exports?.[prop as string]; + } + return Reflect.get(target, prop, receiver); + }, + }); + } + + async dispose() { + await this.manager.delete(this.name); + } +} diff --git a/packages/editor-core/src/plugin/plugin.ts b/packages/engine/src/plugin/types.ts similarity index 96% rename from packages/editor-core/src/plugin/plugin.ts rename to packages/engine/src/plugin/types.ts index 5e3571a08..1f87e2047 100644 --- a/packages/editor-core/src/plugin/plugin.ts +++ b/packages/engine/src/plugin/types.ts @@ -12,6 +12,7 @@ export interface PluginMeta { /** * specify which engine version is compatible with the plugin + * todo: unified engines naming rules */ engines?: { /** e.g. '^1.0.0' */ @@ -67,8 +68,6 @@ export interface PluginDeclarationProperty { export type PluginPreferenceValue = string | number | boolean; -export type PluginPreference = Map<string, Record<string, PluginPreferenceValue>>; - export interface PluginCreater<Context> { (ctx: Context, options: any): PluginInstance; pluginName: string; diff --git a/packages/editor-core/src/plugin/utils.ts b/packages/engine/src/plugin/utils.ts similarity index 53% rename from packages/editor-core/src/plugin/utils.ts rename to packages/engine/src/plugin/utils.ts index b17be4472..119ac6167 100644 --- a/packages/editor-core/src/plugin/utils.ts +++ b/packages/engine/src/plugin/utils.ts @@ -1,38 +1,4 @@ -import { isPlainObject } from 'lodash-es'; -import type { PluginDeclaration } from './plugin'; -import type { PluginRegisterOptions } from './manager'; - -export function isValidPreferenceKey( - key: string, - preferenceDeclaration: PluginDeclaration, -): boolean { - if (!preferenceDeclaration || !Array.isArray(preferenceDeclaration.properties)) { - return false; - } - return preferenceDeclaration.properties.some((prop) => { - return prop.key === key; - }); -} - -export function isLowCodeRegisterOptions(opts: any): opts is PluginRegisterOptions { - return opts && ('autoInit' in opts || 'override' in opts); -} - -export function filterValidOptions(opts: any, preferenceDeclaration: PluginDeclaration) { - if (!opts || !isPlainObject(opts)) return opts; - const filteredOpts = {} as any; - Object.keys(opts).forEach((key) => { - if (isValidPreferenceKey(key, preferenceDeclaration)) { - const v = opts[key]; - if (v !== undefined && v !== null) { - filteredOpts[key] = v; - } - } - }); - return filteredOpts; -} - -interface ITaks { +interface TaskMap { [key: string]: { name: string; dep: string[]; @@ -40,7 +6,7 @@ interface ITaks { } interface Options { - tasks: ITaks; + tasks: TaskMap; names: string[]; results: string[]; missing: string[]; @@ -49,15 +15,7 @@ interface Options { parentName: string; } -export function sequence({ - tasks, - names, - results, - missing, - recursive, - nest, - parentName, -}: Options) { +export function sequence({ tasks, names, results, missing, recursive, nest, parentName }: Options) { names.forEach((name) => { if (results.indexOf(name) !== -1) { return; // de-dup results @@ -88,7 +46,7 @@ export function sequence({ // tasks: object with keys as task names // names: array of task names -export function sequencify(tasks: ITaks, names: string[]) { +export function sequencify(tasks: TaskMap, names: string[]) { let results: string[] = []; // the final sequence const missing: string[] = []; // missing tasks const recursive: string[][] = []; // recursive task dependencies @@ -112,4 +70,3 @@ export function sequencify(tasks: ITaks, names: string[]) { recursiveDependencies: recursive, }; } - diff --git a/packages/engine/src/shell/api/canvas.ts b/packages/engine/src/shell/api/canvas.ts deleted file mode 100644 index 9ed278289..000000000 --- a/packages/engine/src/shell/api/canvas.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - IPublicApiCanvas, - IPublicModelDropLocation, - IPublicModelScrollTarget, - IPublicTypeScrollable, - IPublicModelScroller, - IPublicTypeLocationData, - IPublicModelEditor, - IPublicModelDragon, - IPublicModelActiveTracker, - IPublicModelClipboard, -} from '@alilc/lowcode-types'; -import { - ScrollTarget as InnerScrollTarget, - IDesigner, -} from '@alilc/lowcode-designer'; -import { editorSymbol, designerSymbol, nodeSymbol } from '../symbols'; -import { - Dragon as ShellDragon, - ActiveTracker as ShellActiveTracker, - Clipboard as ShellClipboard, - DropLocation, -} from '../model'; - -const clipboardInstanceSymbol = Symbol('clipboardInstace'); - -export class Canvas implements IPublicApiCanvas { - private readonly [editorSymbol]: IPublicModelEditor; - private readonly [clipboardInstanceSymbol]: IPublicModelClipboard; - - private get [designerSymbol](): IDesigner { - return this[editorSymbol].get('designer') as IDesigner; - } - - get dragon(): IPublicModelDragon | null { - return ShellDragon.create(this[designerSymbol].dragon, this.workspaceMode); - } - - get activeTracker(): IPublicModelActiveTracker | null { - const activeTracker = new ShellActiveTracker(this[designerSymbol].activeTracker as any); - return activeTracker; - } - - get isInLiveEditing(): boolean { - return Boolean(this[editorSymbol].get('designer')?.project?.simulator?.liveEditing?.editing); - } - - get clipboard(): IPublicModelClipboard { - return this[clipboardInstanceSymbol]; - } - - constructor(editor: IPublicModelEditor, readonly workspaceMode: boolean = false) { - this[editorSymbol] = editor; - this[clipboardInstanceSymbol] = new ShellClipboard(); - } - - createScrollTarget(shell: HTMLDivElement): IPublicModelScrollTarget { - return new InnerScrollTarget(shell); - } - - createScroller(scrollable: IPublicTypeScrollable): IPublicModelScroller { - return this[designerSymbol].createScroller(scrollable); - } - - /** - * 创建插入位置,考虑放到 dragon 中 - */ - createLocation(locationData: IPublicTypeLocationData): IPublicModelDropLocation { - return new DropLocation(this[designerSymbol].createLocation({ - ...locationData, - target: (locationData.target as any)[nodeSymbol], - })); - } -} diff --git a/packages/engine/src/shell/api/command.ts b/packages/engine/src/shell/api/command.ts deleted file mode 100644 index 58bbbc045..000000000 --- a/packages/engine/src/shell/api/command.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { IPublicApiCommand, IPublicModelPluginContext, IPublicTypeCommand, IPublicTypeCommandHandlerArgs, IPublicTypeListCommand } from '@alilc/lowcode-types'; -import { commandSymbol, pluginContextSymbol } from '../symbols'; -import { ICommand, ICommandOptions } from '@alilc/lowcode-editor-core'; - -const optionsSymbol = Symbol('options'); -const commandScopeSet = new Set<string>(); - -export class Command implements IPublicApiCommand { - [commandSymbol]: ICommand; - [optionsSymbol]?: ICommandOptions; - [pluginContextSymbol]?: IPublicModelPluginContext; - - constructor(innerCommand: ICommand, pluginContext?: IPublicModelPluginContext, options?: ICommandOptions) { - this[commandSymbol] = innerCommand; - this[optionsSymbol] = options; - this[pluginContextSymbol] = pluginContext; - const commandScope = options?.commandScope; - if (commandScope && commandScopeSet.has(commandScope)) { - throw new Error(`Command scope "${commandScope}" has been registered.`); - } - } - - registerCommand(command: IPublicTypeCommand): void { - this[commandSymbol].registerCommand(command, this[optionsSymbol]); - } - - batchExecuteCommand(commands: { name: string; args: IPublicTypeCommandHandlerArgs }[]): void { - this[commandSymbol].batchExecuteCommand(commands, this[pluginContextSymbol]!); - } - - executeCommand(name: string, args: IPublicTypeCommandHandlerArgs): void { - this[commandSymbol].executeCommand(name, args); - } - - listCommands(): IPublicTypeListCommand[] { - return this[commandSymbol].listCommands(); - } - - unregisterCommand(name: string): void { - this[commandSymbol].unregisterCommand(name); - } - - onCommandError(callback: (name: string, error: Error) => void): void { - this[commandSymbol].onCommandError(callback); - } -} diff --git a/packages/engine/src/shell/api/common.tsx b/packages/engine/src/shell/api/common.tsx deleted file mode 100644 index 020639381..000000000 --- a/packages/engine/src/shell/api/common.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { - isFormEvent as innerIsFormEvent, - getNodeSchemaById as innerGetNodeSchemaById, - transactionManager, - isNodeSchema as innerIsNodeSchema, -} from '@alilc/lowcode-utils'; -import { - IPublicTypeNodeSchema, - IPublicEnumTransitionType, - IPublicApiCommonUtils, - IPublicTypeI18nData, -} from '@alilc/lowcode-types'; -import { - getConvertedExtraKey as innerGetConvertedExtraKey, - getOriginalExtraKey as innerGetOriginalExtraKey, -} from '@alilc/lowcode-designer'; -import { - createIntl as innerCreateIntl, - intl as innerIntl, -} from '@alilc/lowcode-editor-core'; -import { ReactNode } from 'react'; - -export interface IPublicApiCommon { - get utils(): IPublicApiCommonUtils; -} - -export class Common implements IPublicApiCommon { - private readonly __utils: Utils; - - constructor() { - this.__utils = new Utils(); - } - - get utils(): Utils { - return this.__utils; - } -} - - -class Utils implements IPublicApiCommonUtils { - isNodeSchema(data: any): data is IPublicTypeNodeSchema { - return innerIsNodeSchema(data); - } - - isFormEvent(e: KeyboardEvent | MouseEvent): boolean { - return innerIsFormEvent(e); - } - - getNodeSchemaById( - schema: IPublicTypeNodeSchema, - nodeId: string, - ): IPublicTypeNodeSchema | undefined { - return innerGetNodeSchemaById(schema, nodeId); - } - - getConvertedExtraKey(key: string): string { - return innerGetConvertedExtraKey(key); - } - - getOriginalExtraKey(key: string): string { - return innerGetOriginalExtraKey(key); - } - - executeTransaction( - fn: () => void, - type: IPublicEnumTransitionType = IPublicEnumTransitionType.REPAINT, - ): void { - transactionManager.executeTransaction(fn, type); - } - - createIntl(instance: string | object): { - intlNode(id: string, params?: object): ReactNode; - intl(id: string, params?: object): string; - getLocale(): string; - setLocale(locale: string): void; - } { - return innerCreateIntl(instance); - } - - intl(data: IPublicTypeI18nData | string, params?: object): any { - return innerIntl(data, params); - } -} diff --git a/packages/engine/src/shell/api/commonUI.tsx b/packages/engine/src/shell/api/commonUI.tsx deleted file mode 100644 index d922330c1..000000000 --- a/packages/engine/src/shell/api/commonUI.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { - IPublicApiCommonUI, - IPublicModelPluginContext, - IPublicTypeContextMenuAction, -} from '@alilc/lowcode-types'; -import { HelpTip, IEditor, Tip as InnerTip, Title as InnerTitle } from '@alilc/lowcode-editor-core'; -import { - Balloon, - Breadcrumb, - Button, - Card, - Checkbox, - DatePicker, - Dialog, - Dropdown, - Form, - Icon, - Input, - Loading, - Message, - Overlay, - Pagination, - Radio, - Search, - Select, - SplitButton, - Step, - Switch, - Tab, - Table, - Tree, - TreeSelect, - Upload, - Divider, -} from '@alifd/next'; -import { ContextMenu } from '../components/context-menu'; -import { editorSymbol } from '../symbols'; -import { ReactElement } from 'react'; - -export class CommonUI implements IPublicApiCommonUI { - [editorSymbol]: IEditor; - - Balloon = Balloon; - Breadcrumb = Breadcrumb; - Button = Button; - Card = Card; - Checkbox = Checkbox; - DatePicker = DatePicker; - Dialog = Dialog; - Dropdown = Dropdown; - Form = Form; - Icon = Icon; - Input = Input; - Loading = Loading as any; - Message = Message; - Overlay = Overlay; - Pagination = Pagination; - Radio = Radio; - Search = Search; - Select = Select; - SplitButton = SplitButton; - Step = Step as any; - Switch = Switch; - Tab = Tab; - Table = Table; - Tree = Tree; - TreeSelect = TreeSelect; - Upload = Upload; - Divider = Divider; - - ContextMenu: ((props: { - menus: IPublicTypeContextMenuAction[]; - children: React.ReactElement[] | React.ReactElement; - }) => ReactElement) & { - create(menus: IPublicTypeContextMenuAction[], event: MouseEvent | React.MouseEvent): void; - }; - - constructor(editor: IEditor) { - this[editorSymbol] = editor; - - const innerContextMenu = (props: any) => { - const pluginContext: IPublicModelPluginContext = editor.get( - 'pluginContext', - ) as IPublicModelPluginContext; - return <ContextMenu {...props} pluginContext={pluginContext} />; - }; - - innerContextMenu.create = (menus: IPublicTypeContextMenuAction[], event: MouseEvent) => { - const pluginContext: IPublicModelPluginContext = editor.get( - 'pluginContext', - ) as IPublicModelPluginContext; - return ContextMenu.create(pluginContext, menus, event); - }; - - this.ContextMenu = innerContextMenu; - } - - get Tip() { - return InnerTip; - } - - get HelpTip() { - return HelpTip; - } - - get Title() { - return InnerTitle; - } -} diff --git a/packages/engine/src/shell/api/config.ts b/packages/engine/src/shell/api/config.ts deleted file mode 100644 index d84120878..000000000 --- a/packages/engine/src/shell/api/config.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { IPublicModelEngineConfig, IPublicModelPreference, IPublicTypeDisposable } from '@alilc/lowcode-types'; -import { configSymbol } from '../symbols'; -import { IEngineConfig } from '@alilc/lowcode-editor-core'; - -export class Config implements IPublicModelEngineConfig { - private readonly [configSymbol]: IEngineConfig; - - constructor(innerEngineConfig: IEngineConfig) { - this[configSymbol] = innerEngineConfig; - } - - has(key: string): boolean { - return this[configSymbol].has(key); - } - - get(key: string, defaultValue?: any): any { - return this[configSymbol].get(key, defaultValue); - } - - set(key: string, value: any): void { - this[configSymbol].set(key, value); - } - - setConfig(config: { [key: string]: any }): void { - this[configSymbol].setConfig(config); - } - - onceGot(key: string): Promise<any> { - return this[configSymbol].onceGot(key); - } - - onGot(key: string, fn: (data: any) => void): IPublicTypeDisposable { - return this[configSymbol].onGot(key, fn); - } - - getPreference(): IPublicModelPreference { - return this[configSymbol].getPreference(); - } -} diff --git a/packages/engine/src/shell/api/event.ts b/packages/engine/src/shell/api/event.ts deleted file mode 100644 index ac0ae8b14..000000000 --- a/packages/engine/src/shell/api/event.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { IEditor, IEventBus } from '@alilc/lowcode-editor-core'; -import { createLogger, isPluginEventName } from '@alilc/lowcode-utils'; -import { IPublicApiEvent, IPublicTypeDisposable } from '@alilc/lowcode-types'; - -const logger = createLogger({ level: 'warn', bizName: 'shell-event' }); - -type EventOptions = { - prefix: string; -}; - -const eventBusSymbol = Symbol('eventBus'); - -export class Event implements IPublicApiEvent { - private readonly [eventBusSymbol]: IEventBus; - private readonly options: EventOptions; - - constructor(eventBus: IEventBus, options: EventOptions, public workspaceMode = false) { - this[eventBusSymbol] = eventBus; - this.options = options; - if (!this.options.prefix) { - logger.warn('prefix is required while initializing Event'); - } - } - - /** - * 监听事件 - * @param event 事件名称 - * @param listener 事件回调 - */ - on(event: string, listener: (...args: any[]) => void): IPublicTypeDisposable { - if (isPluginEventName(event)) { - return this[eventBusSymbol].on(event, listener); - } else { - logger.warn(`fail to monitor on event ${event}, event should have a prefix like 'somePrefix:eventName'`); - return () => {}; - } - } - - /** - * 监听事件,会在其他回调函数之前执行 - * @param event 事件名称 - * @param listener 事件回调 - */ - prependListener(event: string, listener: (...args: any[]) => void): IPublicTypeDisposable { - if (isPluginEventName(event)) { - return this[eventBusSymbol].prependListener(event, listener); - } else { - logger.warn(`fail to prependListener event ${event}, event should have a prefix like 'somePrefix:eventName'`); - return () => {}; - } - } - - /** - * 取消监听事件 - * @param event 事件名称 - * @param listener 事件回调 - */ - off(event: string, listener: (...args: any[]) => void) { - this[eventBusSymbol].off(event, listener); - } - - /** - * 触发事件 - * @param event 事件名称 - * @param args 事件参数 - * @returns - */ - emit(event: string, ...args: any[]) { - if (!this.options.prefix) { - logger.warn('Event#emit has been forbidden while prefix is not specified'); - return; - } - this[eventBusSymbol].emit(`${this.options.prefix}:${event}`, ...args); - } - - /** - * DO NOT USE if u fully understand what this method does. - * @param event - * @param args - */ - __internalEmit__(event: string, ...args: unknown[]) { - this[eventBusSymbol].emit(event, ...args); - } -} - -export function getEvent(editor: IEditor, options: any = { prefix: 'common' }) { - return new Event(editor.eventBus, options); -} diff --git a/packages/engine/src/shell/api/hotkey.ts b/packages/engine/src/shell/api/hotkey.ts deleted file mode 100644 index 121eb335c..000000000 --- a/packages/engine/src/shell/api/hotkey.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { globalContext, Hotkey as InnerHotkey } from '@alilc/lowcode-editor-core'; -import { hotkeySymbol } from '../symbols'; -import { IPublicTypeDisposable, IPublicTypeHotkeyCallback, IPublicTypeHotkeyCallbacks, IPublicApiHotkey } from '@alilc/lowcode-types'; - -const innerHotkeySymbol = Symbol('innerHotkey'); - -export class Hotkey implements IPublicApiHotkey { - private readonly [innerHotkeySymbol]: InnerHotkey; - get [hotkeySymbol](): InnerHotkey { - if (this.workspaceMode) { - return this[innerHotkeySymbol]; - } - const workspace = globalContext.get('workspace'); - if (workspace.isActive) { - return workspace.window.innerHotkey; - } - - return this[innerHotkeySymbol]; - } - - constructor(hotkey: InnerHotkey, readonly workspaceMode: boolean = false) { - this[innerHotkeySymbol] = hotkey; - } - - get callbacks(): IPublicTypeHotkeyCallbacks { - return this[hotkeySymbol].callBacks; - } - - /** - * 绑定快捷键 - * @param combos 快捷键,格式如:['command + s'] 、['ctrl + shift + s'] 等 - * @param callback 回调函数 - * @param action - * @returns - */ - bind( - combos: string[] | string, - callback: IPublicTypeHotkeyCallback, - action?: string, - ): IPublicTypeDisposable { - this[hotkeySymbol].bind(combos, callback, action); - return () => { - this[hotkeySymbol].unbind(combos, callback, action); - }; - } - - /** - * 给指定窗口绑定快捷键 - * @param window 窗口的 window 对象 - */ - mount(window: Window) { - return this[hotkeySymbol].mount(window); - } -} diff --git a/packages/engine/src/shell/api/index.ts b/packages/engine/src/shell/api/index.ts deleted file mode 100644 index 953aeaceb..000000000 --- a/packages/engine/src/shell/api/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -export * from './common'; -export * from './hotkey'; -export * from './logger'; -export * from './material'; -export * from './plugins'; -export * from './project'; -export * from './setters'; -export * from './simulator-host'; -export * from './skeleton'; -export * from './canvas'; -export * from './workspace'; -export * from './config'; -export * from './commonUI'; -export * from './command'; -export * from './event'; diff --git a/packages/engine/src/shell/api/logger.ts b/packages/engine/src/shell/api/logger.ts deleted file mode 100644 index c3ea9f0f0..000000000 --- a/packages/engine/src/shell/api/logger.ts +++ /dev/null @@ -1,48 +0,0 @@ - -import { createLogger } from '@alilc/lowcode-utils'; -import { IPublicApiLogger, ILoggerOptions } from '@alilc/lowcode-types'; - -const innerLoggerSymbol = Symbol('logger'); - -export class Logger implements IPublicApiLogger { - private readonly [innerLoggerSymbol]: any; - - constructor(options: ILoggerOptions) { - this[innerLoggerSymbol] = createLogger(options as any); - } - - /** - * debug info - */ - debug(...args: any | any[]): void { - this[innerLoggerSymbol].debug(...args); - } - - /** - * normal info output - */ - info(...args: any | any[]): void { - this[innerLoggerSymbol].info(...args); - } - - /** - * warning info output - */ - warn(...args: any | any[]): void { - this[innerLoggerSymbol].warn(...args); - } - - /** - * error info output - */ - error(...args: any | any[]): void { - this[innerLoggerSymbol].error(...args); - } - - /** - * normal log output - */ - log(...args: any | any[]): void { - this[innerLoggerSymbol].log(...args); - } -} diff --git a/packages/engine/src/shell/api/material.ts b/packages/engine/src/shell/api/material.ts deleted file mode 100644 index d71c2ca8d..000000000 --- a/packages/engine/src/shell/api/material.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { globalContext } from '@alilc/lowcode-editor-core'; -import { IDesigner, isComponentMeta } from '@alilc/lowcode-designer'; -import { IPublicTypeAssetsJson, createLogger } from '@alilc/lowcode-utils'; -import { - IPublicTypeComponentAction, - IPublicTypeComponentMetadata, - IPublicApiMaterial, - IPublicTypeMetadataTransducer, - IPublicModelComponentMeta, - IPublicTypeNpmInfo, - IPublicModelEditor, - IPublicTypeDisposable, - IPublicTypeContextMenuAction, - IPublicTypeContextMenuItem, -} from '@alilc/lowcode-types'; -import { Workspace as InnerWorkspace } from '../../workspace'; -import { editorSymbol, designerSymbol } from '../symbols'; -import { ComponentMeta as ShellComponentMeta } from '../model'; -import { ComponentType } from 'react'; - -const logger = createLogger({ level: 'warn', bizName: 'shell-material' }); - -const innerEditorSymbol = Symbol('editor'); -export class Material implements IPublicApiMaterial { - private readonly [innerEditorSymbol]: IPublicModelEditor; - - get [editorSymbol](): IPublicModelEditor { - if (this.workspaceMode) { - return this[innerEditorSymbol]; - } - const workspace: InnerWorkspace = globalContext.get('workspace'); - if (workspace.isActive) { - if (!workspace.window.editor) { - logger.error('Material api 调用时机出现问题,请检查'); - return this[innerEditorSymbol]; - } - return workspace.window.editor; - } - - return this[innerEditorSymbol]; - } - - get [designerSymbol](): IDesigner { - return this[editorSymbol].get('designer')!; - } - - constructor( - editor: IPublicModelEditor, - readonly workspaceMode: boolean = false, - ) { - this[innerEditorSymbol] = editor; - } - - /** - * 获取组件 map 结构 - */ - get componentsMap(): { [key: string]: IPublicTypeNpmInfo | ComponentType<any> | object } { - return this[designerSymbol].componentsMap; - } - - /** - * 设置「资产包」结构 - * @param assets - * @returns - */ - async setAssets(assets: IPublicTypeAssetsJson) { - return await this[editorSymbol].setAssets(assets); - } - - /** - * 获取「资产包」结构 - * @returns - */ - getAssets(): IPublicTypeAssetsJson | undefined { - return this[editorSymbol].get('assets'); - } - - /** - * 加载增量的「资产包」结构,该增量包会与原有的合并 - * @param incrementalAssets - * @returns - */ - loadIncrementalAssets(incrementalAssets: IPublicTypeAssetsJson) { - return this[designerSymbol].loadIncrementalAssets(incrementalAssets); - } - - /** - * 注册物料元数据管道函数 - * @param transducer - * @param level - * @param id - */ - registerMetadataTransducer = ( - transducer: IPublicTypeMetadataTransducer, - level?: number, - id?: string | undefined, - ) => { - this[designerSymbol].componentActions.registerMetadataTransducer(transducer, level, id); - }; - - /** - * 获取所有物料元数据管道函数 - * @returns - */ - getRegisteredMetadataTransducers() { - return this[designerSymbol].componentActions.getRegisteredMetadataTransducers(); - } - - /** - * 获取指定名称的物料元数据 - * @param componentName - * @returns - */ - getComponentMeta(componentName: string): IPublicModelComponentMeta | null { - const innerMeta = this[designerSymbol].getComponentMeta(componentName); - return ShellComponentMeta.create(innerMeta); - } - - /** - * create an instance of ComponentMeta by given metadata - * @param metadata - * @returns - */ - createComponentMeta(metadata: IPublicTypeComponentMetadata) { - return ShellComponentMeta.create(this[designerSymbol].createComponentMeta(metadata)); - } - - /** - * test if the given object is a ComponentMeta instance or not - * @param obj - * @returns - */ - isComponentMeta(obj: any) { - return isComponentMeta(obj); - } - - /** - * 获取所有已注册的物料元数据 - * @returns - */ - getComponentMetasMap(): Map<string, IPublicModelComponentMeta> { - const map = new Map<string, IPublicModelComponentMeta>(); - const originalMap = this[designerSymbol].getComponentMetasMap(); - for (const componentName of originalMap.keys()) { - map.set(componentName, this.getComponentMeta(componentName)!); - } - return map; - } - - /** - * 在设计器辅助层增加一个扩展 action - * @param action - */ - addBuiltinComponentAction = (action: IPublicTypeComponentAction) => { - this[designerSymbol].componentActions.addBuiltinComponentAction(action); - }; - - /** - * 刷新 componentMetasMap,可触发模拟器里的 components 重新构建 - */ - refreshComponentMetasMap = () => { - this[designerSymbol].refreshComponentMetasMap(); - }; - - /** - * 移除设计器辅助层的指定 action - * @param name - */ - removeBuiltinComponentAction(name: string) { - this[designerSymbol].componentActions.removeBuiltinComponentAction(name); - } - - /** - * 修改已有的设计器辅助层的指定 action - * @param actionName - * @param handle - */ - modifyBuiltinComponentAction( - actionName: string, - handle: (action: IPublicTypeComponentAction) => void, - ) { - this[designerSymbol].componentActions.modifyBuiltinComponentAction(actionName, handle); - } - - /** - * 监听 assets 变化的事件 - * @param fn - */ - onChangeAssets(fn: () => void): IPublicTypeDisposable { - const dispose = [ - // 设置 assets,经过 setAssets 赋值 - this[editorSymbol].onChange('assets', fn), - // 增量设置 assets,经过 loadIncrementalAssets 赋值 - this[editorSymbol].eventBus.on('designer.incrementalAssetsReady', fn), - ]; - - return () => { - dispose.forEach((d) => d && d()); - }; - } - - addContextMenuOption(option: IPublicTypeContextMenuAction) { - this[designerSymbol].contextMenuActions.addMenuAction(option); - } - - removeContextMenuOption(name: string) { - this[designerSymbol].contextMenuActions.removeMenuAction(name); - } - - adjustContextMenuLayout( - fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[], - ) { - this[designerSymbol].contextMenuActions.adjustMenuLayout(fn); - } -} diff --git a/packages/engine/src/shell/api/plugins.ts b/packages/engine/src/shell/api/plugins.ts deleted file mode 100644 index 7d2956c94..000000000 --- a/packages/engine/src/shell/api/plugins.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { - ILowCodePluginManager, -} from '@alilc/lowcode-designer'; -import { globalContext } from '@alilc/lowcode-editor-core'; -import { - IPublicApiPlugins, - IPublicModelPluginInstance, - IPublicTypePlugin, - IPublicTypePluginRegisterOptions, - IPublicTypePluginPreferenceValueType, -} from '@alilc/lowcode-types'; -import { PluginInstance as ShellPluginInstance } from '../model'; -import { pluginsSymbol } from '../symbols'; - -const innerPluginsSymbol = Symbol('plugin'); -export class Plugins implements IPublicApiPlugins { - private readonly [innerPluginsSymbol]: ILowCodePluginManager; - get [pluginsSymbol](): ILowCodePluginManager { - if (this.workspaceMode) { - return this[innerPluginsSymbol]; - } - const workspace = globalContext.get('workspace'); - if (workspace.isActive) { - return workspace.window.innerPlugins; - } - - return this[innerPluginsSymbol]; - } - - constructor(plugins: ILowCodePluginManager, public workspaceMode: boolean = false) { - this[innerPluginsSymbol] = plugins; - } - - async register( - pluginModel: IPublicTypePlugin, - options?: any, - registerOptions?: IPublicTypePluginRegisterOptions, - ): Promise<void> { - await this[pluginsSymbol].register(pluginModel, options, registerOptions); - } - - async init(registerOptions: any) { - await this[pluginsSymbol].init(registerOptions); - } - - getPluginPreference( - pluginName: string, - ): Record<string, IPublicTypePluginPreferenceValueType> | null | undefined { - return this[pluginsSymbol].getPluginPreference(pluginName); - } - - get(pluginName: string): IPublicModelPluginInstance | null { - const instance = this[pluginsSymbol].get(pluginName); - if (instance) { - return new ShellPluginInstance(instance); - } - - return null; - } - - getAll() { - return this[pluginsSymbol].getAll()?.map((d) => new ShellPluginInstance(d)); - } - - has(pluginName: string) { - return this[pluginsSymbol].has(pluginName); - } - - async delete(pluginName: string) { - return await this[pluginsSymbol].delete(pluginName); - } - - toProxy() { - return new Proxy(this, { - get(target, prop, receiver) { - const _target = target[pluginsSymbol]; - if (_target.pluginsMap.has(prop as string)) { - // 禁用态的插件,直接返回 undefined - if (_target.pluginsMap.get(prop as string)!.disabled) { - return undefined; - } - return _target.pluginsMap.get(prop as string)?.toProxy(); - } - return Reflect.get(target, prop, receiver); - }, - }); - } -} diff --git a/packages/engine/src/shell/api/project.ts b/packages/engine/src/shell/api/project.ts deleted file mode 100644 index b2e0878ea..000000000 --- a/packages/engine/src/shell/api/project.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { - BuiltinSimulatorHost, - IProject as InnerProject, -} from '@alilc/lowcode-designer'; -import { globalContext } from '@alilc/lowcode-editor-core'; -import { - IPublicTypeRootSchema, - IPublicTypeProjectSchema, - IPublicModelEditor, - IPublicApiProject, - IPublicApiSimulatorHost, - IPublicModelDocumentModel, - IPublicTypePropsTransducer, - IPublicEnumTransformStage, - IPublicTypeDisposable, - IPublicTypeAppConfig, -} from '@alilc/lowcode-types'; -import { DocumentModel as ShellDocumentModel } from '../model'; -import { SimulatorHost } from './simulator-host'; -import { editorSymbol, projectSymbol, simulatorHostSymbol, documentSymbol } from '../symbols'; -import { createLogger } from '@alilc/lowcode-utils'; - -const logger = createLogger({ level: 'warn', bizName: 'shell-project' }); - -const innerProjectSymbol = Symbol('innerProject'); -export class Project implements IPublicApiProject { - private readonly [innerProjectSymbol]: InnerProject; - private [simulatorHostSymbol]: BuiltinSimulatorHost; - get [projectSymbol](): InnerProject { - if (this.workspaceMode) { - return this[innerProjectSymbol]; - } - const workspace = globalContext.get('workspace'); - if (workspace.isActive) { - if (!workspace.window?.innerProject) { - logger.error('project api 调用时机出现问题,请检查'); - return this[innerProjectSymbol]; - } - return workspace.window.innerProject; - } - - return this[innerProjectSymbol]; - } - - get [editorSymbol](): IPublicModelEditor { - return this[projectSymbol]?.designer.editor; - } - - constructor(project: InnerProject, public workspaceMode: boolean = false) { - this[innerProjectSymbol] = project; - } - - static create(project: InnerProject, workspaceMode: boolean = false) { - return new Project(project, workspaceMode); - } - - /** - * 获取当前的 document - * @returns - */ - get currentDocument(): IPublicModelDocumentModel | null { - return this.getCurrentDocument(); - } - - /** - * 获取当前 project 下所有 documents - * @returns - */ - get documents(): IPublicModelDocumentModel[] { - return this[projectSymbol].documents.map((doc) => ShellDocumentModel.create(doc)!); - } - - /** - * 获取模拟器的 host - */ - get simulatorHost(): IPublicApiSimulatorHost | null { - return SimulatorHost.create(this[projectSymbol].simulator as any || this[simulatorHostSymbol]); - } - - /** - * 打开一个 document - * @param doc - * @returns - */ - openDocument(doc?: string | IPublicTypeRootSchema | undefined) { - const documentModel = this[projectSymbol].open(doc); - if (!documentModel) { - return null; - } - return ShellDocumentModel.create(documentModel); - } - - /** - * 创建一个 document - * @param data - * @returns - */ - createDocument(data?: IPublicTypeRootSchema): IPublicModelDocumentModel | null { - const doc = this[projectSymbol].createDocument(data); - return ShellDocumentModel.create(doc); - } - - /** - * 删除一个 document - * @param doc - */ - removeDocument(doc: IPublicModelDocumentModel) { - this[projectSymbol].removeDocument((doc as any)[documentSymbol]); - } - - /** - * 根据 fileName 获取 document - * @param fileName - * @returns - */ - getDocumentByFileName(fileName: string): IPublicModelDocumentModel | null { - const innerDocumentModel = this[projectSymbol].getDocumentByFileName(fileName); - return ShellDocumentModel.create(innerDocumentModel); - } - - /** - * 根据 id 获取 document - * @param id - * @returns - */ - getDocumentById(id: string): IPublicModelDocumentModel | null { - return ShellDocumentModel.create(this[projectSymbol].getDocument(id)); - } - - /** - * 导出 project - * @returns - */ - exportSchema(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Render) { - return this[projectSymbol].getSchema(stage); - } - - /** - * 导入 project - * @param schema 待导入的 project 数据 - */ - importSchema(schema?: IPublicTypeProjectSchema): void { - this[projectSymbol].load(schema, true); - } - - /** - * 获取当前的 document - * @returns - */ - getCurrentDocument(): IPublicModelDocumentModel | null { - return ShellDocumentModel.create(this[projectSymbol].currentDocument); - } - - /** - * 增加一个属性的管道处理函数 - * @param transducer - * @param stage - */ - addPropsTransducer( - transducer: IPublicTypePropsTransducer, - stage: IPublicEnumTransformStage, - ): void { - this[projectSymbol].designer.addPropsReducer(transducer, stage); - } - - /** - * 绑定删除文档事件 - * @param fn - * @returns - */ - onRemoveDocument(fn: (data: { id: string}) => void): IPublicTypeDisposable { - return this[editorSymbol].eventBus.on( - 'designer.document.remove', - (data: { id: string }) => fn(data), - ); - } - - /** - * 当前 project 内的 document 变更事件 - */ - onChangeDocument(fn: (doc: IPublicModelDocumentModel) => void): IPublicTypeDisposable { - const offFn = this[projectSymbol].onCurrentDocumentChange((originalDoc) => { - fn(ShellDocumentModel.create(originalDoc)!); - }); - if (this[projectSymbol].currentDocument) { - fn(ShellDocumentModel.create(this[projectSymbol].currentDocument)!); - } - return offFn; - } - - /** - * 当前 project 的模拟器 ready 事件 - */ - onSimulatorHostReady(fn: (host: IPublicApiSimulatorHost) => void): IPublicTypeDisposable { - // @ts-expect-error: a - const offFn = this[projectSymbol].onSimulatorReady((simulator: BuiltinSimulatorHost) => { - fn(SimulatorHost.create(simulator)!); - }); - return offFn; - } - - /** - * 当前 project 的渲染器 ready 事件 - */ - onSimulatorRendererReady(fn: () => void): IPublicTypeDisposable { - const offFn = this[projectSymbol].onRendererReady(() => { - fn(); - }); - return offFn; - } - - /** - * 设置多语言语料 - * 数据格式参考 https://github.com/alibaba/lowcode-engine/blob/main/specs/lowcode-spec.md#2434%E5%9B%BD%E9%99%85%E5%8C%96%E5%A4%9A%E8%AF%AD%E8%A8%80%E7%B1%BB%E5%9E%8Baa - * @param value object - * @returns - */ - setI18n(value: object): void { - this[projectSymbol].set('i18n', value); - } - - /** - * 设置项目配置 - * @param value object - * @returns - */ - setConfig<T extends keyof IPublicTypeAppConfig>(key: T, value: IPublicTypeAppConfig[T]): void; - setConfig(value: IPublicTypeAppConfig): void; - setConfig(...params: any[]): void { - if (params.length === 2) { - const oldConfig = this[projectSymbol].get('config'); - this[projectSymbol].set('config', { - ...oldConfig, - [params[0]]: params[1], - }); - } else { - this[projectSymbol].set('config', params[0]); - } - } -} diff --git a/packages/engine/src/shell/api/setters.ts b/packages/engine/src/shell/api/setters.ts deleted file mode 100644 index 76960f644..000000000 --- a/packages/engine/src/shell/api/setters.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { IPublicTypeCustomView, IPublicApiSetters, IPublicTypeRegisteredSetter } from '@alilc/lowcode-types'; -import { ISetters, globalContext, untracked } from '@alilc/lowcode-editor-core'; -import { createLogger } from '@alilc/lowcode-utils'; - -const innerSettersSymbol = Symbol('setters'); -const settersSymbol = Symbol('setters'); - -const logger = createLogger({ level: 'warn', bizName: 'shell-setters' }); - -export class Setters implements IPublicApiSetters { - readonly [innerSettersSymbol]: ISetters; - - get [settersSymbol](): ISetters { - if (this.workspaceMode) { - return this[innerSettersSymbol]; - } - - const workspace = globalContext.get('workspace'); - if (workspace.isActive) { - return untracked(() => { - if (!workspace.window?.innerSetters) { - logger.error('setter api 调用时机出现问题,请检查'); - return this[innerSettersSymbol]; - } - return workspace.window.innerSetters; - }); - } - - return this[innerSettersSymbol]; - } - - constructor(innerSetters: ISetters, readonly workspaceMode = false) { - this[innerSettersSymbol] = innerSetters; - } - - /** - * 获取指定 setter - * @param type - * @returns - */ - getSetter = (type: string) => { - return this[settersSymbol].getSetter(type); - }; - - /** - * 获取已注册的所有 settersMap - * @returns - */ - getSettersMap = (): Map<string, IPublicTypeRegisteredSetter & { - type: string; - }> => { - return this[settersSymbol].getSettersMap(); - }; - - /** - * 注册一个 setter - * @param typeOrMaps - * @param setter - * @returns - */ - registerSetter = ( - typeOrMaps: string | { [key: string]: IPublicTypeCustomView | IPublicTypeRegisteredSetter }, - setter?: IPublicTypeCustomView | IPublicTypeRegisteredSetter | undefined, - ) => { - return this[settersSymbol].registerSetter(typeOrMaps, setter); - }; -} diff --git a/packages/engine/src/shell/api/simulator-host.ts b/packages/engine/src/shell/api/simulator-host.ts deleted file mode 100644 index 663ba0c66..000000000 --- a/packages/engine/src/shell/api/simulator-host.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - BuiltinSimulatorHost, -} from '@alilc/lowcode-designer'; -import { simulatorHostSymbol, nodeSymbol } from '../symbols'; -import { IPublicApiSimulatorHost, IPublicModelNode, IPublicModelSimulatorRender } from '@alilc/lowcode-types'; -import { SimulatorRender } from '../model/simulator-render'; - -export class SimulatorHost implements IPublicApiSimulatorHost { - private readonly [simulatorHostSymbol]: BuiltinSimulatorHost; - - constructor(simulator: BuiltinSimulatorHost) { - this[simulatorHostSymbol] = simulator; - } - - static create(host: BuiltinSimulatorHost): IPublicApiSimulatorHost | null { - if (!host) return null; - return new SimulatorHost(host); - } - - /** - * 获取 contentWindow - */ - get contentWindow(): Window | undefined { - return this[simulatorHostSymbol].contentWindow; - } - - /** - * 获取 contentDocument - */ - get contentDocument(): Document | undefined { - return this[simulatorHostSymbol].contentDocument; - } - - get renderer(): IPublicModelSimulatorRender | undefined { - if (this[simulatorHostSymbol].renderer) { - return SimulatorRender.create(this[simulatorHostSymbol].renderer); - } - - return undefined; - } - - /** - * 设置 host 配置值 - * @param key - * @param value - */ - set(key: string, value: any): void { - this[simulatorHostSymbol].set(key, value); - } - - /** - * 获取 host 配置值 - * @param key - * @returns - */ - get(key: string): any { - return this[simulatorHostSymbol].get(key); - } - - /** - * scroll to specific node - * @param node - */ - scrollToNode(node: IPublicModelNode): void { - this[simulatorHostSymbol].scrollToNode((node as any)[nodeSymbol]); - } - - /** - * 触发组件构建,并刷新渲染画布 - */ - rerender(): void { - this[simulatorHostSymbol].rerender(); - } -} diff --git a/packages/engine/src/shell/api/skeleton.ts b/packages/engine/src/shell/api/skeleton.ts deleted file mode 100644 index b5078dc1a..000000000 --- a/packages/engine/src/shell/api/skeleton.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { globalContext } from '@alilc/lowcode-editor-core'; -import { - ISkeleton, - SkeletonEvents, -} from '@alilc/lowcode-editor-skeleton'; -import { skeletonSymbol } from '../symbols'; -import { IPublicApiSkeleton, IPublicModelSkeletonItem, IPublicTypeConfigTransducer, IPublicTypeDisposable, IPublicTypeSkeletonConfig, IPublicTypeWidgetConfigArea } from '@alilc/lowcode-types'; -import { createLogger } from '@alilc/lowcode-utils'; -import { SkeletonItem } from '../model/skeleton-item'; - -const innerSkeletonSymbol = Symbol('skeleton'); - -const logger = createLogger({ level: 'warn', bizName: 'shell-skeleton' }); - -export class Skeleton implements IPublicApiSkeleton { - private readonly [innerSkeletonSymbol]: ISkeleton; - private readonly pluginName: string; - - get [skeletonSymbol](): ISkeleton { - if (this.workspaceMode) { - return this[innerSkeletonSymbol]; - } - const workspace = globalContext.get('workspace'); - if (workspace.isActive) { - if (!workspace.window?.innerSkeleton) { - logger.error('skeleton api 调用时机出现问题,请检查'); - return this[innerSkeletonSymbol]; - } - return workspace.window.innerSkeleton; - } - - return this[innerSkeletonSymbol]; - } - - constructor( - skeleton: ISkeleton, - pluginName: string, - readonly workspaceMode: boolean = false, - ) { - this[innerSkeletonSymbol] = skeleton; - this.pluginName = pluginName; - } - - /** - * 增加一个面板实例 - * @param config - * @param extraConfig - * @returns - */ - add(config: IPublicTypeSkeletonConfig, extraConfig?: Record<string, any>): IPublicModelSkeletonItem | undefined { - const configWithName = { - ...config, - pluginName: this.pluginName, - }; - const item = this[skeletonSymbol].add(configWithName, extraConfig); - if (item) { - return new SkeletonItem(item); - } - } - - /** - * 移除一个面板实例 - * @param config - * @returns - */ - remove(config: IPublicTypeSkeletonConfig): number | undefined { - const { area, name } = config; - const skeleton = this[skeletonSymbol]; - if (!normalizeArea(area)) { - return; - } - skeleton[normalizeArea(area)].container?.remove(name); - } - - getAreaItems(areaName: IPublicTypeWidgetConfigArea): IPublicModelSkeletonItem[] { - return this[skeletonSymbol][normalizeArea(areaName)].container.items?.map(d => new SkeletonItem(d)); - } - - getPanel(name: string) { - const item = this[skeletonSymbol].getPanel(name); - if (!item) { - return; - } - - return new SkeletonItem(item); - } - - /** - * 显示面板 - * @param name - */ - showPanel(name: string) { - this[skeletonSymbol].getPanel(name)?.show(); - } - - /** - * 隐藏面板 - * @param name - */ - hidePanel(name: string) { - this[skeletonSymbol].getPanel(name)?.hide(); - } - - /** - * 显示 widget - * @param name - */ - showWidget(name: string) { - this[skeletonSymbol].getWidget(name)?.show(); - } - - /** - * enable widget - * @param name - */ - enableWidget(name: string) { - this[skeletonSymbol].getWidget(name)?.enable?.(); - } - - /** - * 隐藏 widget - * @param name - */ - hideWidget(name: string) { - this[skeletonSymbol].getWidget(name)?.hide(); - } - - /** - * disable widget,不可点击 - * @param name - */ - disableWidget(name: string) { - this[skeletonSymbol].getWidget(name)?.disable?.(); - } - - /** - * show area - * @param areaName name of area - */ - showArea(areaName: string) { - (this[skeletonSymbol] as any)[areaName]?.show(); - } - - /** - * hide area - * @param areaName name of area - */ - hideArea(areaName: string) { - (this[skeletonSymbol] as any)[areaName]?.hide(); - } - - /** - * 监听 panel 显示事件 - * @param listener - * @returns - */ - onShowPanel(listener: (paneName: string, panel: IPublicModelSkeletonItem) => void): IPublicTypeDisposable { - const { editor } = this[skeletonSymbol]; - editor.eventBus.on(SkeletonEvents.PANEL_SHOW, (name: any, panel: any) => { - listener(name, new SkeletonItem(panel)); - }); - return () => editor.eventBus.off(SkeletonEvents.PANEL_SHOW, listener); - } - - onDisableWidget(listener: (...args: any[]) => void): IPublicTypeDisposable { - const { editor } = this[skeletonSymbol]; - editor.eventBus.on(SkeletonEvents.WIDGET_DISABLE, (name: any, panel: any) => { - listener(name, new SkeletonItem(panel)); - }); - return () => editor.eventBus.off(SkeletonEvents.WIDGET_DISABLE, listener); - } - - onEnableWidget(listener: (...args: any[]) => void): IPublicTypeDisposable { - const { editor } = this[skeletonSymbol]; - editor.eventBus.on(SkeletonEvents.WIDGET_ENABLE, (name: any, panel: any) => { - listener(name, new SkeletonItem(panel)); - }); - return () => editor.eventBus.off(SkeletonEvents.WIDGET_ENABLE, listener); - } - - /** - * 监听 panel 隐藏事件 - * @param listener - * @returns - */ - onHidePanel(listener: (...args: any[]) => void): IPublicTypeDisposable { - const { editor } = this[skeletonSymbol]; - editor.eventBus.on(SkeletonEvents.PANEL_HIDE, (name: any, panel: any) => { - listener(name, new SkeletonItem(panel)); - }); - return () => editor.eventBus.off(SkeletonEvents.PANEL_HIDE, listener); - } - - /** - * 监听 widget 显示事件 - * @param listener - * @returns - */ - onShowWidget(listener: (...args: any[]) => void): IPublicTypeDisposable { - const { editor } = this[skeletonSymbol]; - editor.eventBus.on(SkeletonEvents.WIDGET_SHOW, (name: any, panel: any) => { - listener(name, new SkeletonItem(panel)); - }); - return () => editor.eventBus.off(SkeletonEvents.WIDGET_SHOW, listener); - } - - /** - * 监听 widget 隐藏事件 - * @param listener - * @returns - */ - onHideWidget(listener: (...args: any[]) => void): IPublicTypeDisposable { - const { editor } = this[skeletonSymbol]; - editor.eventBus.on(SkeletonEvents.WIDGET_HIDE, (name: any, panel: any) => { - listener(name, new SkeletonItem(panel)); - }); - return () => editor.eventBus.off(SkeletonEvents.WIDGET_HIDE, listener); - } - - registerConfigTransducer(fn: IPublicTypeConfigTransducer, level: number, id?: string) { - this[skeletonSymbol].registerConfigTransducer(fn, level, id); - } -} - -function normalizeArea(area: IPublicTypeWidgetConfigArea | undefined): 'leftArea' | 'rightArea' | 'topArea' | 'toolbar' | 'mainArea' | 'bottomArea' | 'leftFixedArea' | 'leftFloatArea' | 'stages' | 'subTopArea' { - switch (area) { - case 'leftArea': - case 'left': - return 'leftArea'; - case 'rightArea': - case 'right': - return 'rightArea'; - case 'topArea': - case 'top': - return 'topArea'; - case 'toolbar': - return 'toolbar'; - case 'mainArea': - case 'main': - case 'center': - case 'centerArea': - return 'mainArea'; - case 'bottomArea': - case 'bottom': - return 'bottomArea'; - case 'leftFixedArea': - return 'leftFixedArea'; - case 'leftFloatArea': - return 'leftFloatArea'; - case 'stages': - return 'stages'; - case 'subTopArea': - return 'subTopArea'; - default: - throw new Error(`${area} not supported`); - } -} diff --git a/packages/engine/src/shell/api/workspace.ts b/packages/engine/src/shell/api/workspace.ts deleted file mode 100644 index 2629d0c22..000000000 --- a/packages/engine/src/shell/api/workspace.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { IPublicApiWorkspace, IPublicModelResource, IPublicResourceList, IPublicTypeDisposable, IPublicTypeResourceType } from '@alilc/lowcode-types'; -import { IWorkspace } from '../../workspace'; -import { resourceSymbol, workspaceSymbol } from '../symbols'; -import { Resource as ShellResource, Window as ShellWindow } from '../model'; -import { Plugins } from './plugins'; -import { Skeleton } from './skeleton'; - -export class Workspace implements IPublicApiWorkspace { - readonly [workspaceSymbol]: IWorkspace; - - constructor(innerWorkspace: IWorkspace) { - this[workspaceSymbol] = innerWorkspace; - } - - get resourceList() { - return this[workspaceSymbol].getResourceList().map((d) => new ShellResource(d) as IPublicModelResource); - } - - setResourceList(resourceList: IPublicResourceList) { - this[workspaceSymbol].setResourceList(resourceList); - } - - onResourceListChange(fn: (resourceList: IPublicResourceList) => void): IPublicTypeDisposable { - return this[workspaceSymbol].onResourceListChange(fn); - } - - get isActive() { - return this[workspaceSymbol].isActive; - } - - get window() { - if (!this[workspaceSymbol].window) { - return null; - } - return new ShellWindow(this[workspaceSymbol].window); - } - - get resourceTypeList() { - return Array.from(this[workspaceSymbol].resourceTypeMap.values()).map((d) => { - const { name: resourceName, type: resourceType } = d; - const { - description, - editorViews, - } = d.resourceTypeModel({} as any, {}); - - return { - resourceName, - resourceType, - description, - editorViews: editorViews.map(d => ( - { - viewName: d.viewName, - viewType: d.viewType || 'editor', - } - )), - }; - }); - } - - onWindowRendererReady(fn: () => void): IPublicTypeDisposable { - return this[workspaceSymbol].onWindowRendererReady(fn); - } - - registerResourceType(resourceTypeModel: IPublicTypeResourceType): void { - this[workspaceSymbol].registerResourceType(resourceTypeModel); - } - - async openEditorWindow(): Promise<void> { - if (typeof arguments[0] === 'string') { - await this[workspaceSymbol].openEditorWindow(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]); - } else { - await this[workspaceSymbol].openEditorWindowByResource(arguments[0]?.[resourceSymbol], arguments[1]); - } - } - - openEditorWindowById(id: string) { - this[workspaceSymbol].openEditorWindowById(id); - } - - removeEditorWindow() { - if (typeof arguments[0] === 'string') { - this[workspaceSymbol].removeEditorWindow(arguments[0], arguments[1]); - } else { - this[workspaceSymbol].removeEditorWindowByResource(arguments[0]?.[resourceSymbol]); - } - } - - removeEditorWindowById(id: string) { - this[workspaceSymbol].removeEditorWindowById(id); - } - - get plugins() { - return new Plugins(this[workspaceSymbol].plugins, true).toProxy(); - } - - get skeleton() { - return new Skeleton(this[workspaceSymbol].skeleton, 'workspace', true); - } - - get windows() { - return this[workspaceSymbol].windows.map((d) => new ShellWindow(d)); - } - - onChangeWindows(fn: () => void): IPublicTypeDisposable { - return this[workspaceSymbol].onChangeWindows(fn); - } - - onChangeActiveWindow(fn: () => void): IPublicTypeDisposable { - return this[workspaceSymbol].onChangeActiveWindow(fn); - } - - onChangeActiveEditorView(fn: () => void): IPublicTypeDisposable { - return this[workspaceSymbol].onChangeActiveEditorView(fn); - } -} diff --git a/packages/engine/src/shell/components/context-menu.tsx b/packages/engine/src/shell/components/context-menu.tsx deleted file mode 100644 index 6ae0c198d..000000000 --- a/packages/engine/src/shell/components/context-menu.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { createContextMenu, parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils'; -import { engineConfig } from '@alilc/lowcode-editor-core'; -import { IPublicModelPluginContext, IPublicTypeContextMenuAction } from '@alilc/lowcode-types'; -import React, { useCallback } from 'react'; - -export function ContextMenu({ children, menus, pluginContext }: { - menus: IPublicTypeContextMenuAction[]; - children: React.ReactElement[] | React.ReactElement; - pluginContext: IPublicModelPluginContext; -}): React.ReactElement<any, string | React.JSXElementConstructor<any>> { - const handleContextMenu = useCallback((event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - - let destroyFn: Function | undefined; - const destroy = () => { - destroyFn?.(); - }; - const children: React.ReactNode[] = parseContextMenuAsReactNode(parseContextMenuProperties(menus, { - destroy, - pluginContext, - }), { pluginContext }); - - if (!children?.length) { - return; - } - - destroyFn = createContextMenu(children, { event }); - }, [menus]); - - if (!engineConfig.get('enableContextMenu')) { - return ( - <>{ children }</> - ); - } - - if (!menus) { - return ( - <>{ children }</> - ); - } - - // 克隆 children 并添加 onContextMenu 事件处理器 - const childrenWithContextMenu = React.Children.map(children, (child) => - React.cloneElement( - child, - { onContextMenu: handleContextMenu }, - )); - - return ( - <>{childrenWithContextMenu}</> - ); -} - -ContextMenu.create = (pluginContext: IPublicModelPluginContext, menus: IPublicTypeContextMenuAction[], event: MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - - const children: React.ReactNode[] = parseContextMenuAsReactNode(parseContextMenuProperties(menus, { - pluginContext, - }), { - pluginContext, - }); - - if (!children?.length) { - return; - } - - return createContextMenu(children, { - event, - }); -}; diff --git a/packages/engine/src/shell/index.ts b/packages/engine/src/shell/index.ts deleted file mode 100644 index 3ba12b357..000000000 --- a/packages/engine/src/shell/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - Detecting, - DocumentModel, - History, - Node, - NodeChildren, - Prop, - Selection, - Dragon, - SettingTopEntry, - Clipboard, - SettingField, - Window, - SkeletonItem, -} from './model'; -import { - Project, - Material, - Logger, - Plugins, - Skeleton, - Setters, - Hotkey, - Common, - Canvas, - Workspace, - SimulatorHost, - Config, - CommonUI, - Command, - getEvent, - Event -} from './api'; - -export * from './symbols'; - -/** - * 所有 shell 层模型的 API 设计约定: - * 1. 所有 API 命名空间都按照 variables / functions / events 来组织 - * 2. 事件(events)的命名格式为:on[Will|Did]VerbNoun?,参考 https://code.visualstudio.com/api/references/vscode-api#events - * 3. 基于 Disposable 模式,对于事件的绑定、快捷键的绑定函数,返回值则是解绑函数 - * 4. 对于属性的导出,统一用 .xxx 的 getter 模式,不能使用 .getXxx() - */ -export { - DocumentModel, - Detecting, - Event, - History, - Material, - Node, - NodeChildren, - Project, - Prop, - Selection, - Setters, - Hotkey, - Window, - Skeleton, - SettingField as SettingPropEntry, - SettingTopEntry, - Dragon, - Common, - getEvent, - Plugins, - Logger, - Canvas, - Workspace, - Clipboard, - SimulatorHost, - Config, - SettingField, - SkeletonItem, - CommonUI, - Command, -}; diff --git a/packages/engine/src/shell/model/active-tracker.ts b/packages/engine/src/shell/model/active-tracker.ts deleted file mode 100644 index 9d9fd10d2..000000000 --- a/packages/engine/src/shell/model/active-tracker.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { IPublicModelActiveTracker, IPublicModelNode, IPublicTypeActiveTarget } from '@alilc/lowcode-types'; -import { IActiveTracker as InnerActiveTracker, ActiveTarget } from '@alilc/lowcode-designer'; -import { Node as ShellNode } from './node'; -import { nodeSymbol } from '../symbols'; - -const activeTrackerSymbol = Symbol('activeTracker'); - -export class ActiveTracker implements IPublicModelActiveTracker { - private readonly [activeTrackerSymbol]: InnerActiveTracker; - - constructor(innerTracker: InnerActiveTracker) { - this[activeTrackerSymbol] = innerTracker; - } - - get target() { - const _target = this[activeTrackerSymbol]._target; - - if (!_target) { - return null; - } - - const { node: innerNode, detail, instance } = _target as any; - const publicNode = ShellNode.create(innerNode); - return { - node: publicNode!, - detail, - instance, - }; - } - - onChange(fn: (target: IPublicTypeActiveTarget) => void): () => void { - if (!fn) { - return () => {}; - } - return this[activeTrackerSymbol].onChange((t: ActiveTarget) => { - const { node: innerNode, detail, instance } = t; - const publicNode = ShellNode.create(innerNode); - const publicActiveTarget = { - node: publicNode!, - detail, - instance, - }; - fn(publicActiveTarget); - }); - } - - track(node: IPublicModelNode) { - this[activeTrackerSymbol].track((node as any)[nodeSymbol]); - } -} diff --git a/packages/engine/src/shell/model/clipboard.ts b/packages/engine/src/shell/model/clipboard.ts deleted file mode 100644 index e7ab18e10..000000000 --- a/packages/engine/src/shell/model/clipboard.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { IPublicModelClipboard } from '@alilc/lowcode-types'; -import { clipboardSymbol } from '../symbols'; -import { IClipboard, clipboard } from '@alilc/lowcode-designer'; - -export class Clipboard implements IPublicModelClipboard { - private readonly [clipboardSymbol]: IClipboard; - - constructor() { - this[clipboardSymbol] = clipboard; - } - - setData(data: any): void { - this[clipboardSymbol].setData(data); - } - - waitPasteData( - keyboardEvent: KeyboardEvent, - cb: (data: any, clipboardEvent: ClipboardEvent) => void, - ): void { - this[clipboardSymbol].waitPasteData(keyboardEvent, cb); - } -} diff --git a/packages/engine/src/shell/model/component-meta.ts b/packages/engine/src/shell/model/component-meta.ts deleted file mode 100644 index f58e92f66..000000000 --- a/packages/engine/src/shell/model/component-meta.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { - IComponentMeta as InnerComponentMeta, - INode, -} from '@alilc/lowcode-designer'; -import { IPublicTypeNodeData, IPublicTypeNodeSchema, IPublicModelComponentMeta, IPublicTypeI18nData, IPublicTypeIconType, IPublicTypeNpmInfo, IPublicTypeTransformedComponentMetadata, IPublicModelNode, IPublicTypeAdvanced, IPublicTypeFieldConfig } from '@alilc/lowcode-types'; -import { componentMetaSymbol, nodeSymbol } from '../symbols'; -import { ReactElement } from 'react'; - -export class ComponentMeta implements IPublicModelComponentMeta { - private readonly [componentMetaSymbol]: InnerComponentMeta; - - isComponentMeta = true; - - constructor(componentMeta: InnerComponentMeta) { - this[componentMetaSymbol] = componentMeta; - } - - static create(componentMeta: InnerComponentMeta | null): IPublicModelComponentMeta | null { - if (!componentMeta) { - return null; - } - return new ComponentMeta(componentMeta); - } - - /** - * 组件名 - */ - get componentName(): string { - return this[componentMetaSymbol].componentName; - } - - /** - * 是否是「容器型」组件 - */ - get isContainer(): boolean { - return this[componentMetaSymbol].isContainer; - } - - /** - * 是否是最小渲染单元。 - * 当组件需要重新渲染时: - * 若为最小渲染单元,则只渲染当前组件, - * 若不为最小渲染单元,则寻找到上层最近的最小渲染单元进行重新渲染,直至根节点。 - */ - get isMinimalRenderUnit(): boolean { - return this[componentMetaSymbol].isMinimalRenderUnit; - } - - /** - * 是否为「模态框」组件 - */ - get isModal(): boolean { - return this[componentMetaSymbol].isModal; - } - - /** - * 元数据配置 - */ - get configure(): IPublicTypeFieldConfig[] { - return this[componentMetaSymbol].configure; - } - - /** - * 标题 - */ - get title(): string | IPublicTypeI18nData | ReactElement { - return this[componentMetaSymbol].title; - } - - /** - * 图标 - */ - get icon(): IPublicTypeIconType { - return this[componentMetaSymbol].icon; - } - - /** - * 组件 npm 信息 - */ - get npm(): IPublicTypeNpmInfo { - return this[componentMetaSymbol].npm; - } - - get availableActions(): any { - return this[componentMetaSymbol].availableActions; - } - - get advanced(): IPublicTypeAdvanced { - return this[componentMetaSymbol].advanced; - } - - /** - * 设置 npm 信息 - * @param npm - */ - setNpm(npm: IPublicTypeNpmInfo): void { - this[componentMetaSymbol].setNpm(npm); - } - - /** - * 获取元数据 - * @returns - */ - getMetadata(): IPublicTypeTransformedComponentMetadata { - return this[componentMetaSymbol].getMetadata(); - } - - /** - * check if the current node could be placed in parent node - * @param my - * @param parent - * @returns - */ - checkNestingUp(my: IPublicModelNode | IPublicTypeNodeData, parent: INode): boolean { - const curNode = (my as any).isNode ? (my as any)[nodeSymbol] : my; - return this[componentMetaSymbol].checkNestingUp(curNode as any, parent); - } - - /** - * check if the target node(s) could be placed in current node - * @param my - * @param parent - * @returns - */ - checkNestingDown( - my: IPublicModelNode | IPublicTypeNodeData, - target: IPublicTypeNodeSchema | IPublicModelNode | IPublicTypeNodeSchema[], - ) { - const curNode = (my as any)?.isNode ? (my as any)[nodeSymbol] : my; - return this[componentMetaSymbol].checkNestingDown( - curNode as any, - (target as any)[nodeSymbol] || target, - ); - } - - refreshMetadata(): void { - this[componentMetaSymbol].refreshMetadata(); - } -} diff --git a/packages/engine/src/shell/model/condition-group.ts b/packages/engine/src/shell/model/condition-group.ts deleted file mode 100644 index e2dd316ed..000000000 --- a/packages/engine/src/shell/model/condition-group.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { IExclusiveGroup } from '@alilc/lowcode-designer'; -import { IPublicModelExclusiveGroup, IPublicModelNode } from '@alilc/lowcode-types'; -import { conditionGroupSymbol, nodeSymbol } from '../symbols'; -import { Node } from './node'; - -export class ConditionGroup implements IPublicModelExclusiveGroup { - private [conditionGroupSymbol]: IExclusiveGroup | null; - - constructor(conditionGroup: IExclusiveGroup | null) { - this[conditionGroupSymbol] = conditionGroup; - } - - get id() { - return this[conditionGroupSymbol]?.id; - } - - get title() { - return this[conditionGroupSymbol]?.title; - } - - get firstNode() { - return Node.create(this[conditionGroupSymbol]?.firstNode); - } - - setVisible(node: IPublicModelNode) { - this[conditionGroupSymbol]?.setVisible((node as any)[nodeSymbol] ? (node as any)[nodeSymbol] : node); - } - - static create(conditionGroup: IExclusiveGroup | null) { - if (!conditionGroup) { - return null; - } - // @ts-ignore - if (conditionGroup[conditionGroupSymbol]) { - return (conditionGroup as any)[conditionGroupSymbol]; - } - const shellConditionGroup = new ConditionGroup(conditionGroup); - // @ts-ignore - shellConditionGroup[conditionGroupSymbol] = shellConditionGroup; - return shellConditionGroup; - } -} diff --git a/packages/engine/src/shell/model/detecting.ts b/packages/engine/src/shell/model/detecting.ts deleted file mode 100644 index 188043a71..000000000 --- a/packages/engine/src/shell/model/detecting.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Node as ShellNode } from './node'; -import { - Detecting as InnerDetecting, - IDocumentModel as InnerDocumentModel, - INode as InnerNode, -} from '@alilc/lowcode-designer'; -import { documentSymbol, detectingSymbol } from '../symbols'; -import { IPublicModelDetecting, IPublicModelNode, IPublicTypeDisposable } from '@alilc/lowcode-types'; - -export class Detecting implements IPublicModelDetecting { - private readonly [documentSymbol]: InnerDocumentModel; - private readonly [detectingSymbol]: InnerDetecting; - - constructor(document: InnerDocumentModel) { - this[documentSymbol] = document; - this[detectingSymbol] = document.designer?.detecting; - } - - /** - * 控制大纲树 hover 时是否出现悬停效果 - */ - get enable(): boolean { - return this[detectingSymbol].enable; - } - - /** - * 当前 hover 的节点 - */ - get current() { - return ShellNode.create(this[detectingSymbol].current); - } - - /** - * hover 指定节点 - * @param id 节点 id - */ - capture(id: string) { - this[detectingSymbol].capture(this[documentSymbol].getNode(id)); - } - - /** - * hover 离开指定节点 - * @param id 节点 id - */ - release(id: string) { - this[detectingSymbol].release(this[documentSymbol].getNode(id)); - } - - /** - * 清空 hover 态 - */ - leave() { - this[detectingSymbol].leave(this[documentSymbol]); - } - - onDetectingChange(fn: (node: IPublicModelNode | null) => void): IPublicTypeDisposable { - const innerFn = (innerNode: InnerNode) => { - const shellNode = ShellNode.create(innerNode); - fn(shellNode); - }; - return this[detectingSymbol].onDetectingChange(innerFn); - } -} diff --git a/packages/engine/src/shell/model/document-model.ts b/packages/engine/src/shell/model/document-model.ts deleted file mode 100644 index 21002a3a5..000000000 --- a/packages/engine/src/shell/model/document-model.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { - IDocumentModel as InnerDocumentModel, - INode as InnerNode, -} from '@alilc/lowcode-designer'; -import { - IPublicEnumTransformStage, - IPublicTypeRootSchema, - GlobalEvent, - IPublicModelDocumentModel, - IPublicTypeOnChangeOptions, - IPublicTypeDragNodeObject, - IPublicTypeDragNodeDataObject, - IPublicModelNode, - IPublicModelSelection, - IPublicModelDetecting, - IPublicModelHistory, - IPublicApiProject, - IPublicModelModalNodesManager, - IPublicTypePropChangeOptions, - IPublicModelDropLocation, - IPublicApiCanvas, - IPublicTypeDisposable, - IPublicModelEditor, - IPublicTypeNodeSchema, -} from '@alilc/lowcode-types'; -import { isDragNodeObject } from '@alilc/lowcode-utils'; -import { Node as ShellNode } from './node'; -import { Selection as ShellSelection } from './selection'; -import { Detecting as ShellDetecting } from './detecting'; -import { History as ShellHistory } from './history'; -import { DropLocation as ShellDropLocation } from './drop-location'; -import { Project as ShellProject, Canvas as ShellCanvas } from '../api'; -import { Prop as ShellProp } from './prop'; -import { ModalNodesManager } from './modal-nodes-manager'; -import { documentSymbol, editorSymbol, nodeSymbol } from '../symbols'; - -const shellDocSymbol = Symbol('shellDocSymbol'); - -export class DocumentModel implements IPublicModelDocumentModel { - private readonly [documentSymbol]: InnerDocumentModel; - private readonly [editorSymbol]: IPublicModelEditor; - private _focusNode: IPublicModelNode | null; - selection: IPublicModelSelection; - detecting: IPublicModelDetecting; - history: IPublicModelHistory; - - constructor(document: InnerDocumentModel) { - this[documentSymbol] = document; - this[editorSymbol] = document.designer?.editor as IPublicModelEditor; - this.selection = new ShellSelection(document); - this.detecting = new ShellDetecting(document); - this.history = new ShellHistory(document); - this._focusNode = ShellNode.create(this[documentSymbol].focusNode); - } - - static create(document: InnerDocumentModel | undefined | null): IPublicModelDocumentModel | null { - if (!document) { - return null; - } - // @ts-ignore 直接返回已挂载的 shell doc 实例 - if (document[shellDocSymbol]) { - return (document as any)[shellDocSymbol]; - } - const shellDoc = new DocumentModel(document); - // @ts-ignore 直接返回已挂载的 shell doc 实例 - document[shellDocSymbol] = shellDoc; - // @ts-ignore - return shellDoc; - } - - /** - * id - */ - get id(): string { - return this[documentSymbol].id; - } - - set id(id) { - this[documentSymbol].id = id; - } - - /** - * 获取当前文档所属的 project - * @returns - */ - get project(): IPublicApiProject { - return ShellProject.create(this[documentSymbol].project, true); - } - - /** - * 获取文档的根节点 - * root node of this documentModel - * @returns - */ - get root(): IPublicModelNode | null { - return ShellNode.create(this[documentSymbol].rootNode); - } - - get focusNode(): IPublicModelNode | null { - return this._focusNode || this.root; - } - - set focusNode(node: IPublicModelNode | null) { - this._focusNode = node; - this[editorSymbol].eventBus.emit( - 'shell.document.focusNodeChanged', - { document: this, focusNode: node }, - ); - } - - /** - * 获取文档下所有节点 Map, key 为 nodeId - * get map of all nodes , using node.id as key - */ - get nodesMap(): Map<string, IPublicModelNode> { - const map = new Map<string, IPublicModelNode>(); - for (const id of this[documentSymbol].nodesMap.keys()) { - map.set(id, this.getNodeById(id)!); - } - return map; - } - - /** - * 模态节点管理 - */ - get modalNodesManager(): IPublicModelModalNodesManager | null { - return ModalNodesManager.create(this[documentSymbol].modalNodesManager); - } - - get dropLocation(): IPublicModelDropLocation | null { - return ShellDropLocation.create(this[documentSymbol].dropLocation); - } - - set dropLocation(loc: IPublicModelDropLocation | null) { - this[documentSymbol].dropLocation = loc as any; - } - - /** - * 根据 nodeId 返回 Node 实例 - * get node instance by nodeId - * @param {string} nodeId - */ - getNodeById(nodeId: string): IPublicModelNode | null { - return ShellNode.create(this[documentSymbol].getNode(nodeId)); - } - - /** - * 导入 schema - * @param schema - */ - importSchema(schema: IPublicTypeRootSchema): void { - this[documentSymbol].import(schema); - this[editorSymbol].eventBus.emit('shell.document.importSchema', schema); - } - - /** - * 导出 schema - * @param stage - * @returns - */ - exportSchema(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Render): IPublicTypeRootSchema | undefined { - return this[documentSymbol].export(stage); - } - - /** - * 插入节点 - * @param parent - * @param thing - * @param at - * @param copy - * @returns - */ - insertNode( - parent: IPublicModelNode, - thing: IPublicModelNode, - at?: number | null | undefined, - copy?: boolean | undefined, - ): IPublicModelNode | null { - const node = this[documentSymbol].insertNode( - (parent as any)[nodeSymbol] ? (parent as any)[nodeSymbol] : parent, - (thing as any)?.[nodeSymbol] ? (thing as any)[nodeSymbol] : thing, - at, - copy, - ); - return ShellNode.create(node); - } - - /** - * 创建一个节点 - * @param data - * @returns - */ - // @ts-ignore - createNode<IPublicModelNode>(data: IPublicTypeNodeSchema): IPublicModelNode | null { - // @ts-ignore - return ShellNode.create(this[documentSymbol].createNode(data)); - } - - /** - * 移除指定节点/节点id - * @param idOrNode - */ - removeNode(idOrNode: string | IPublicModelNode): void { - this[documentSymbol].removeNode(idOrNode as any); - } - - /** - * componentsMap of documentModel - * @param extraComps - * @returns - */ - getComponentsMap(extraComps?: string[]): any { - return this[documentSymbol].getComponentsMap(extraComps); - } - - /** - * 检查拖拽放置的目标节点是否可以放置该拖拽对象 - * @param dropTarget 拖拽放置的目标节点 - * @param dragObject 拖拽的对象 - * @returns boolean 是否可以放置 - */ - checkNesting( - dropTarget: IPublicModelNode, - dragObject: IPublicTypeDragNodeObject | IPublicTypeDragNodeDataObject, - ): boolean { - const innerDragObject = dragObject; - if (isDragNodeObject(dragObject)) { - innerDragObject.nodes = innerDragObject.nodes?.map( - (node: IPublicModelNode) => ((node as any)[nodeSymbol] || node), - ); - } - return this[documentSymbol].checkNesting( - ((dropTarget as any)[nodeSymbol] || dropTarget) as any, - innerDragObject as any, - ); - } - - /** - * 当前 document 新增节点事件 - */ - onAddNode(fn: (node: IPublicModelNode) => void): IPublicTypeDisposable { - return this[documentSymbol].onNodeCreate((node: InnerNode) => { - fn(ShellNode.create(node)!); - }); - } - - /** - * 当前 document 新增节点事件,此时节点已经挂载到 document 上 - */ - onMountNode(fn: (payload: { node: IPublicModelNode }) => void): IPublicTypeDisposable { - return this[documentSymbol].onMountNode(({ - node, - }) => { - fn({ node: ShellNode.create(node)! }); - }); - } - - /** - * 当前 document 删除节点事件 - */ - onRemoveNode(fn: (node: IPublicModelNode) => void): IPublicTypeDisposable { - return this[documentSymbol].onNodeDestroy((node: InnerNode) => { - fn(ShellNode.create(node)!); - }); - } - - /** - * 当前 document 的 hover 变更事件 - */ - onChangeDetecting(fn: (node: IPublicModelNode) => void): IPublicTypeDisposable { - return this[documentSymbol].designer.detecting.onDetectingChange((node: InnerNode) => { - fn(ShellNode.create(node)!); - }); - } - - /** - * 当前 document 的选中变更事件 - */ - onChangeSelection(fn: (ids: string[]) => void): IPublicTypeDisposable { - return this[documentSymbol].selection.onSelectionChange((ids: string[]) => { - fn(ids); - }); - } - - /** - * 当前 document 的节点显隐状态变更事件 - * @param fn - */ - onChangeNodeVisible(fn: (node: IPublicModelNode, visible: boolean) => void): IPublicTypeDisposable { - return this[documentSymbol].onChangeNodeVisible((node: InnerNode, visible: boolean) => { - fn(ShellNode.create(node)!, visible); - }); - } - - /** - * 当前 document 的节点 children 变更事件 - * @param fn - */ - onChangeNodeChildren(fn: (info: IPublicTypeOnChangeOptions) => void): IPublicTypeDisposable { - return this[documentSymbol].onChangeNodeChildren((info?: IPublicTypeOnChangeOptions<InnerNode>) => { - if (!info) { - return; - } - fn({ - type: info.type, - node: ShellNode.create(info.node)!, - }); - }); - } - - /** - * 当前 document 节点属性修改事件 - * @param fn - */ - onChangeNodeProp(fn: (info: IPublicTypePropChangeOptions) => void): IPublicTypeDisposable { - const callback = (info: GlobalEvent.Node.Prop.ChangeOptions) => { - fn({ - key: info.key, - oldValue: info.oldValue, - newValue: info.newValue, - prop: ShellProp.create(info.prop)!, - node: ShellNode.create(info.node as any)!, - }); - }; - this[editorSymbol].on( - GlobalEvent.Node.Prop.InnerChange, - callback, - ); - - return () => { - this[editorSymbol].off( - GlobalEvent.Node.Prop.InnerChange, - callback, - ); - }; - } - - /** - * import schema event - * @param fn - */ - onImportSchema(fn: (schema: IPublicTypeRootSchema) => void): IPublicTypeDisposable { - return this[editorSymbol].eventBus.on('shell.document.importSchema', fn as any); - } - - isDetectingNode(node: IPublicModelNode): boolean { - return this.detecting.current === node; - } - - onFocusNodeChanged( - fn: (doc: IPublicModelDocumentModel, focusNode: IPublicModelNode) => void, - ): IPublicTypeDisposable { - if (!fn) { - return () => {}; - } - return this[editorSymbol].eventBus.on( - 'shell.document.focusNodeChanged', - (payload) => { - const { document, focusNode } = payload; - fn(document, focusNode); - }, - ); - } - - onDropLocationChanged(fn: (doc: IPublicModelDocumentModel) => void): IPublicTypeDisposable { - if (!fn) { - return () => {}; - } - return this[editorSymbol].eventBus.on( - 'document.dropLocation.changed', - (payload) => { - const { document } = payload; - fn(document); - }, - ); - } -} diff --git a/packages/engine/src/shell/model/drag-object.ts b/packages/engine/src/shell/model/drag-object.ts deleted file mode 100644 index 38e756442..000000000 --- a/packages/engine/src/shell/model/drag-object.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { dragObjectSymbol } from '../symbols'; -import { IPublicModelDragObject, IPublicModelDragObject as InnerDragObject, IPublicTypeDragNodeDataObject, IPublicTypeNodeSchema } from '@alilc/lowcode-types'; -import { Node } from './node'; - -export class DragObject implements IPublicModelDragObject { - private readonly [dragObjectSymbol]: InnerDragObject; - - constructor(dragObject: InnerDragObject) { - this[dragObjectSymbol] = dragObject; - } - - static create(dragObject: InnerDragObject | null): IPublicModelDragObject | null { - if (!dragObject) { - return null; - } - return new DragObject(dragObject); - } - - get type() { - return this[dragObjectSymbol].type; - } - - get nodes() { - const { nodes } = this[dragObjectSymbol]; - if (!nodes) { - return null; - } - return nodes.map(Node.create); - } - - get data(): IPublicTypeNodeSchema | IPublicTypeNodeSchema[] { - return (this[dragObjectSymbol] as IPublicTypeDragNodeDataObject).data; - } -} diff --git a/packages/engine/src/shell/model/dragon.ts b/packages/engine/src/shell/model/dragon.ts deleted file mode 100644 index 797c3dea1..000000000 --- a/packages/engine/src/shell/model/dragon.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - IDragon, - ILocateEvent as InnerLocateEvent, - INode, -} from '@alilc/lowcode-designer'; -import { dragonSymbol, nodeSymbol } from '../symbols'; -import LocateEvent from './locate-event'; -import { DragObject } from './drag-object'; -import { globalContext } from '@alilc/lowcode-editor-core'; -import { - IPublicModelDragon, - IPublicModelLocateEvent, - IPublicModelDragObject, - IPublicTypeDragNodeDataObject, - IPublicModelNode, - IPublicTypeDragObject, -} from '@alilc/lowcode-types'; - -export const innerDragonSymbol = Symbol('innerDragonSymbol'); - -export class Dragon implements IPublicModelDragon { - private readonly [innerDragonSymbol]: IDragon; - - constructor(innerDragon: IDragon, readonly workspaceMode: boolean) { - this[innerDragonSymbol] = innerDragon; - } - - get [dragonSymbol](): IDragon { - if (this.workspaceMode) { - return this[innerDragonSymbol]; - } - const workspace = globalContext.get('workspace'); - let editor = globalContext.get('editor'); - - if (workspace.isActive) { - editor = workspace.window.editor; - } - - const designer = editor.get('designer'); - return designer.dragon; - } - - static create( - dragon: IDragon | null, - workspaceMode: boolean, - ): IPublicModelDragon | null { - if (!dragon) { - return null; - } - return new Dragon(dragon, workspaceMode); - } - - /** - * is dragging or not - */ - get dragging(): boolean { - return this[dragonSymbol].dragging; - } - - /** - * 绑定 dragstart 事件 - * @param func - * @returns - */ - onDragstart(func: (e: IPublicModelLocateEvent) => any): () => void { - return this[dragonSymbol].onDragstart((e: InnerLocateEvent) => func(LocateEvent.create(e)!)); - } - - /** - * 绑定 drag 事件 - * @param func - * @returns - */ - onDrag(func: (e: IPublicModelLocateEvent) => any): () => void { - return this[dragonSymbol].onDrag((e: InnerLocateEvent) => func(LocateEvent.create(e)!)); - } - - /** - * 绑定 dragend 事件 - * @param func - * @returns - */ - onDragend(func: (o: { dragObject: IPublicModelDragObject; copy?: boolean }) => any): () => void { - return this[dragonSymbol].onDragend( - (o: { dragObject: IPublicModelDragObject; copy?: boolean }) => { - const dragObject = DragObject.create(o.dragObject); - const { copy } = o; - return func({ dragObject: dragObject!, copy }); - }, - ); - } - - /** - * 设置拖拽监听的区域 shell,以及自定义拖拽转换函数 boost - * @param shell 拖拽监听的区域 - * @param boost 拖拽转换函数 - */ - from(shell: Element, boost: (e: MouseEvent) => IPublicTypeDragNodeDataObject | null): any { - return this[dragonSymbol].from(shell, boost as any); - } - - /** - * boost your dragObject for dragging(flying) 发射拖拽对象 - * - * @param dragObject 拖拽对象 - * @param boostEvent 拖拽初始时事件 - */ - boost(dragObject: IPublicTypeDragObject, boostEvent: MouseEvent | DragEvent, fromRglNode?: IPublicModelNode & { - [nodeSymbol]: INode; - }): void { - return this[dragonSymbol].boost({ - ...dragObject, - nodes: dragObject.nodes.map((node: any) => node[nodeSymbol]), - } as any, boostEvent, fromRglNode?.[nodeSymbol]); - } - - /** - * 添加投放感应区 - */ - addSensor(sensor: any): void { - return this[dragonSymbol].addSensor(sensor); - } - - /** - * 移除投放感应 - */ - removeSensor(sensor: any): void { - return this[dragonSymbol].removeSensor(sensor); - } -} diff --git a/packages/engine/src/shell/model/drop-location.ts b/packages/engine/src/shell/model/drop-location.ts deleted file mode 100644 index f38e74a07..000000000 --- a/packages/engine/src/shell/model/drop-location.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - IDropLocation as InnerDropLocation, -} from '@alilc/lowcode-designer'; -import { dropLocationSymbol } from '../symbols'; -import { Node as ShellNode } from './node'; -import { IPublicModelDropLocation, IPublicTypeLocationDetail, IPublicModelLocateEvent } from '@alilc/lowcode-types'; - -export class DropLocation implements IPublicModelDropLocation { - private readonly [dropLocationSymbol]: InnerDropLocation; - - constructor(dropLocation: InnerDropLocation) { - this[dropLocationSymbol] = dropLocation; - } - - static create(dropLocation: InnerDropLocation | null): IPublicModelDropLocation | null { - if (!dropLocation) { - return null; - } - return new DropLocation(dropLocation); - } - - get target() { - return ShellNode.create(this[dropLocationSymbol].target); - } - - get detail(): IPublicTypeLocationDetail { - return this[dropLocationSymbol].detail; - } - - get event(): IPublicModelLocateEvent { - return this[dropLocationSymbol].event; - } - - clone(event: IPublicModelLocateEvent): IPublicModelDropLocation { - return new DropLocation(this[dropLocationSymbol].clone(event)); - } -} diff --git a/packages/engine/src/shell/model/editor-view.ts b/packages/engine/src/shell/model/editor-view.ts deleted file mode 100644 index 4b2435414..000000000 --- a/packages/engine/src/shell/model/editor-view.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { editorViewSymbol, pluginContextSymbol } from '../symbols'; -import { IPublicModelPluginContext } from '@alilc/lowcode-types'; -import { IViewContext } from '../../workspace'; - -export class EditorView { - [editorViewSymbol]: IViewContext; - - [pluginContextSymbol]: IPublicModelPluginContext; - - constructor(editorView: IViewContext) { - this[editorViewSymbol] = editorView; - this[pluginContextSymbol] = this[editorViewSymbol].innerPlugins._getLowCodePluginContext({ - pluginName: editorView.editorWindow + editorView.viewName, - }); - } - - toProxy() { - return new Proxy(this, { - get(target, prop, receiver) { - if ((target[pluginContextSymbol] as any)[prop as string]) { - return Reflect.get(target[pluginContextSymbol], prop, receiver); - } - return Reflect.get(target, prop, receiver); - }, - }); - } - - get viewName() { - return this[editorViewSymbol].viewName; - } - - get viewType() { - return this[editorViewSymbol].viewType; - } -} diff --git a/packages/engine/src/shell/model/history.ts b/packages/engine/src/shell/model/history.ts deleted file mode 100644 index ddc567aee..000000000 --- a/packages/engine/src/shell/model/history.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { IDocumentModel as InnerDocumentModel, IHistory as InnerHistory } from '@alilc/lowcode-designer'; -import { historySymbol, documentSymbol } from '../symbols'; -import { IPublicModelHistory, IPublicTypeDisposable } from '@alilc/lowcode-types'; - -export class History implements IPublicModelHistory { - private readonly [documentSymbol]: InnerDocumentModel; - - private get [historySymbol](): InnerHistory { - return this[documentSymbol].getHistory(); - } - - constructor(document: InnerDocumentModel) { - this[documentSymbol] = document; - } - - /** - * 历史记录跳转到指定位置 - * @param cursor - */ - go(cursor: number): void { - this[historySymbol].go(cursor); - } - - /** - * 历史记录后退 - */ - back(): void { - this[historySymbol].back(); - } - - /** - * 历史记录前进 - */ - forward(): void { - this[historySymbol].forward(); - } - - /** - * 保存当前状态 - */ - savePoint(): void { - this[historySymbol].savePoint(); - } - - /** - * 当前是否是「保存点」,即是否有状态变更但未保存 - * @returns - */ - isSavePoint(): boolean { - return this[historySymbol].isSavePoint(); - } - - /** - * 获取 state,判断当前是否为「可回退」、「可前进」的状态 - * @returns - */ - getState(): number { - return this[historySymbol].getState(); - } - - /** - * 监听 state 变更事件 - * @param func - * @returns - */ - onChangeState(func: () => any): IPublicTypeDisposable { - return this[historySymbol].onChangeState(func); - } - - /** - * 监听历史记录游标位置变更事件 - * @param func - * @returns - */ - onChangeCursor(func: () => any): IPublicTypeDisposable { - return this[historySymbol].onChangeCursor(func); - } -} diff --git a/packages/engine/src/shell/model/index.ts b/packages/engine/src/shell/model/index.ts deleted file mode 100644 index a15d50b54..000000000 --- a/packages/engine/src/shell/model/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -export * from './component-meta'; -export * from './detecting'; -export * from './document-model'; -export * from './drag-object'; -export * from './dragon'; -export * from './drop-location'; -export * from './history'; -export * from './locate-event'; -export * from './modal-nodes-manager'; -export * from './node-children'; -export * from './node'; -export * from './prop'; -export * from './props'; -export * from './selection'; -export * from './setting-top-entry'; -export * from './setting-field'; -export * from './resource'; -export * from './active-tracker'; -export * from './plugin-instance'; -export * from './window'; -export * from './clipboard'; -export * from './editor-view'; -export * from './skeleton-item'; diff --git a/packages/engine/src/shell/model/locate-event.ts b/packages/engine/src/shell/model/locate-event.ts deleted file mode 100644 index 94165a417..000000000 --- a/packages/engine/src/shell/model/locate-event.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ILocateEvent } from '@alilc/lowcode-designer'; -import { locateEventSymbol } from '../symbols'; -import { DragObject } from './drag-object'; -import { IPublicModelLocateEvent, IPublicModelDragObject } from '@alilc/lowcode-types'; - -export default class LocateEvent implements IPublicModelLocateEvent { - private readonly [locateEventSymbol]: ILocateEvent; - - constructor(locateEvent: ILocateEvent) { - this[locateEventSymbol] = locateEvent; - } - - static create(locateEvent: ILocateEvent): IPublicModelLocateEvent | null { - if (!locateEvent) { - return null; - } - return new LocateEvent(locateEvent); - } - - get type(): string { - return this[locateEventSymbol].type; - } - - get globalX(): number { - return this[locateEventSymbol].globalX; - } - - get globalY(): number { - return this[locateEventSymbol].globalY; - } - - get originalEvent(): MouseEvent | DragEvent { - return this[locateEventSymbol].originalEvent; - } - - get target(): Element | null | undefined { - return this[locateEventSymbol].target; - } - - get canvasX(): number | undefined { - return this[locateEventSymbol].canvasX; - } - - get canvasY(): number | undefined { - return this[locateEventSymbol].canvasY; - } - - get dragObject(): IPublicModelDragObject | null { - return DragObject.create(this[locateEventSymbol].dragObject); - } -} diff --git a/packages/engine/src/shell/model/modal-nodes-manager.ts b/packages/engine/src/shell/model/modal-nodes-manager.ts deleted file mode 100644 index ab0035964..000000000 --- a/packages/engine/src/shell/model/modal-nodes-manager.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - IModalNodesManager as InnerModalNodesManager, - INode as InnerNode, -} from '@alilc/lowcode-designer'; -import { IPublicModelModalNodesManager, IPublicModelNode } from '@alilc/lowcode-types'; -import { Node as ShellNode } from './node'; -import { nodeSymbol, modalNodesManagerSymbol } from '../symbols'; - -export class ModalNodesManager implements IPublicModelModalNodesManager { - private readonly [modalNodesManagerSymbol]: InnerModalNodesManager; - - constructor(modalNodesManager: InnerModalNodesManager) { - this[modalNodesManagerSymbol] = modalNodesManager; - } - - static create( - modalNodesManager: InnerModalNodesManager | null, - ): IPublicModelModalNodesManager | null { - if (!modalNodesManager) { - return null; - } - return new ModalNodesManager(modalNodesManager); - } - - /** - * 设置模态节点,触发内部事件 - */ - setNodes(): void { - this[modalNodesManagerSymbol].setNodes(); - } - - /** - * 获取模态节点(们) - */ - getModalNodes(): IPublicModelNode[] { - const innerNodes = this[modalNodesManagerSymbol].getModalNodes(); - const shellNodes: IPublicModelNode[] = []; - innerNodes?.forEach((node: InnerNode) => { - const shellNode = ShellNode.create(node); - if (shellNode) { - shellNodes.push(shellNode); - } - }); - return shellNodes; - } - - /** - * 获取当前可见的模态节点 - */ - getVisibleModalNode(): IPublicModelNode | null { - return ShellNode.create(this[modalNodesManagerSymbol].getVisibleModalNode()); - } - - /** - * 隐藏模态节点(们) - */ - hideModalNodes(): void { - this[modalNodesManagerSymbol].hideModalNodes(); - } - - /** - * 设置指定节点为可见态 - * @param node Node - */ - setVisible(node: IPublicModelNode): void { - this[modalNodesManagerSymbol].setVisible((node as any)[nodeSymbol]); - } - - /** - * 设置指定节点为不可见态 - * @param node Node - */ - setInvisible(node: IPublicModelNode): void { - this[modalNodesManagerSymbol].setInvisible((node as any)[nodeSymbol]); - } -} diff --git a/packages/engine/src/shell/model/node-children.ts b/packages/engine/src/shell/model/node-children.ts deleted file mode 100644 index b695adb5e..000000000 --- a/packages/engine/src/shell/model/node-children.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { INode as InnerNode, INodeChildren } from '@alilc/lowcode-designer'; -import { IPublicTypeNodeData, IPublicEnumTransformStage, IPublicModelNodeChildren, IPublicModelNode } from '@alilc/lowcode-types'; -import { Node as ShellNode } from './node'; -import { nodeSymbol, nodeChildrenSymbol } from '../symbols'; - -export class NodeChildren implements IPublicModelNodeChildren { - private readonly [nodeChildrenSymbol]: INodeChildren; - - constructor(nodeChildren: INodeChildren) { - this[nodeChildrenSymbol] = nodeChildren; - } - - static create(nodeChildren: INodeChildren | null): IPublicModelNodeChildren | null { - if (!nodeChildren) { - return null; - } - return new NodeChildren(nodeChildren); - } - - /** - * 返回当前 children 实例所属的节点实例 - */ - get owner(): IPublicModelNode | null { - return ShellNode.create(this[nodeChildrenSymbol].owner); - } - - /** - * children 内的节点实例数 - */ - get size(): number { - return this[nodeChildrenSymbol].size; - } - - /** - * 是否为空 - * @returns - */ - get isEmptyNode(): boolean { - return this[nodeChildrenSymbol].isEmptyNode; - } - - /** - * judge if it is not empty - */ - get notEmptyNode(): boolean { - return this[nodeChildrenSymbol].notEmptyNode; - } - - /** - * 删除指定节点 - * delete the node - * @param node - */ - delete(node: IPublicModelNode): boolean { - return this[nodeChildrenSymbol].delete((node as any)?.[nodeSymbol]); - } - - /** - * 插入一个节点 - * @param node 待插入节点 - * @param at 插入下标 - * @returns - */ - insert(node: IPublicModelNode, at?: number | null): void { - return this[nodeChildrenSymbol].insert((node as any)?.[nodeSymbol], at); - } - - /** - * 返回指定节点的下标 - * @param node - * @returns - */ - indexOf(node: IPublicModelNode): number { - return this[nodeChildrenSymbol].indexOf((node as any)?.[nodeSymbol]); - } - - /** - * 类似数组 splice 操作 - * @param start - * @param deleteCount - * @param node - */ - splice(start: number, deleteCount: number, node?: IPublicModelNode): any { - this[nodeChildrenSymbol].splice(start, deleteCount, (node as any)?.[nodeSymbol]); - } - - /** - * 返回指定下标的节点 - * @param index - * @returns - */ - get(index: number): IPublicModelNode | null { - return ShellNode.create(this[nodeChildrenSymbol].get(index)); - } - - /** - * 是否包含指定节点 - * @param node - * @returns - */ - has(node: IPublicModelNode): boolean { - return this[nodeChildrenSymbol].has((node as any)?.[nodeSymbol]); - } - - /** - * 类似数组的 forEach - * @param fn - */ - forEach(fn: (node: IPublicModelNode, index: number) => void): void { - this[nodeChildrenSymbol].forEach((item: InnerNode, index: number) => { - fn(ShellNode.create(item)!, index); - }); - } - - /** - * 类似数组的 reverse - */ - reverse(): IPublicModelNode[] { - return this[nodeChildrenSymbol].reverse().map(d => { - return ShellNode.create(d)!; - }); - } - - /** - * 类似数组的 map - * @param fn - */ - map<T = any>(fn: (node: IPublicModelNode, index: number) => T): T[] | null { - return this[nodeChildrenSymbol].map<T>((item: InnerNode, index: number): T => { - return fn(ShellNode.create(item)!, index); - }); - } - - /** - * 类似数组的 every - * @param fn - */ - every(fn: (node: IPublicModelNode, index: number) => boolean): boolean { - return this[nodeChildrenSymbol].every((item: InnerNode, index: number) => { - return fn(ShellNode.create(item)!, index); - }); - } - - /** - * 类似数组的 some - * @param fn - */ - some(fn: (node: IPublicModelNode, index: number) => boolean): boolean { - return this[nodeChildrenSymbol].some((item: InnerNode, index: number) => { - return fn(ShellNode.create(item)!, index); - }); - } - - /** - * 类似数组的 filter - * @param fn - */ - filter(fn: (node: IPublicModelNode, index: number) => boolean): any { - return this[nodeChildrenSymbol] - .filter((item: InnerNode, index: number) => { - return fn(ShellNode.create(item)!, index); - }) - .map((item: InnerNode) => ShellNode.create(item)!); - } - - /** - * 类似数组的 find - * @param fn - */ - find(fn: (node: IPublicModelNode, index: number) => boolean): IPublicModelNode | null { - return ShellNode.create( - this[nodeChildrenSymbol].find((item: InnerNode, index: number) => { - return fn(ShellNode.create(item)!, index); - }), - ); - } - - /** - * 类似数组的 reduce - * @param fn - */ - reduce(fn: (acc: any, cur: IPublicModelNode) => any, initialValue: any): void { - return this[nodeChildrenSymbol].reduce((acc: any, cur: InnerNode) => { - return fn(acc, ShellNode.create(cur)!); - }, initialValue); - } - - /** - * 导入 schema - * @param data - */ - importSchema(data?: IPublicTypeNodeData | IPublicTypeNodeData[]): void { - this[nodeChildrenSymbol].import(data); - } - - /** - * 导出 schema - * @param stage - * @returns - */ - exportSchema(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Render): any { - return this[nodeChildrenSymbol].export(stage); - } - - /** - * 执行新增、删除、排序等操作 - * @param remover - * @param adder - * @param sorter - */ - mergeChildren( - remover: (node: IPublicModelNode, idx: number) => boolean, - adder: (children: IPublicModelNode[]) => any, - originalSorter: (firstNode: IPublicModelNode, secondNode: IPublicModelNode) => number, - ) { - let sorter = originalSorter; - if (!sorter) { - sorter = () => 0; - } - this[nodeChildrenSymbol].mergeChildren( - (node: InnerNode, idx: number) => remover(ShellNode.create(node)!, idx), - (children: InnerNode[]) => adder(children.map((node) => ShellNode.create(node)!)), - (firstNode: InnerNode, secondNode: InnerNode) => { - return sorter(ShellNode.create(firstNode)!, ShellNode.create(secondNode)!); - }, - ); - } -} diff --git a/packages/engine/src/shell/model/node.ts b/packages/engine/src/shell/model/node.ts deleted file mode 100644 index 616d23f64..000000000 --- a/packages/engine/src/shell/model/node.ts +++ /dev/null @@ -1,574 +0,0 @@ -import { - IDocumentModel as InnerDocumentModel, - INode as InnerNode, -} from '@alilc/lowcode-designer'; -import { - IPublicTypeCompositeValue, - IPublicTypeNodeSchema, - IPublicEnumTransformStage, - IPublicModelNode, - IPublicTypeIconType, - IPublicTypeI18nData, - IPublicModelComponentMeta, - IPublicModelDocumentModel, - IPublicModelNodeChildren, - IPublicModelProp, - IPublicModelProps, - IPublicTypePropsMap, - IPublicTypePropsList, - IPublicModelSettingTopEntry, - IPublicModelExclusiveGroup, -} from '@alilc/lowcode-types'; -import { Prop as ShellProp } from './prop'; -import { Props as ShellProps } from './props'; -import { DocumentModel as ShellDocumentModel } from './document-model'; -import { NodeChildren as ShellNodeChildren } from './node-children'; -import { ComponentMeta as ShellComponentMeta } from './component-meta'; -import { SettingTopEntry as ShellSettingTopEntry } from './setting-top-entry'; -import { documentSymbol, nodeSymbol } from '../symbols'; -import { ReactElement } from 'react'; -import { ConditionGroup } from './condition-group'; - -const shellNodeSymbol = Symbol('shellNodeSymbol'); - -function isShellNode(node: any): node is IPublicModelNode { - return node[shellNodeSymbol]; -} - -export class Node implements IPublicModelNode { - private readonly [documentSymbol]: InnerDocumentModel | null; - private readonly [nodeSymbol]: InnerNode; - - private _id: string; - - /** - * 节点 id - */ - get id() { - return this._id; - } - - /** - * set id - */ - set id(id: string) { - this._id = id; - } - - /** - * 节点标题 - */ - get title(): string | IPublicTypeI18nData | ReactElement { - return this[nodeSymbol].title; - } - - /** - * 是否为「容器型」节点 - */ - get isContainerNode(): boolean { - return this[nodeSymbol].isContainerNode; - } - - /** - * 是否为根节点 - */ - get isRootNode(): boolean { - return this[nodeSymbol].isRootNode; - } - - /** - * 是否为空节点(无 children 或者 children 为空) - */ - get isEmptyNode(): boolean { - return this[nodeSymbol].isEmptyNode; - } - - /** - * 是否为 Page 节点 - */ - get isPageNode(): boolean { - return this[nodeSymbol].isPageNode; - } - - /** - * 是否为 Component 节点 - */ - get isComponentNode(): boolean { - return this[nodeSymbol].isComponentNode; - } - - /** - * 是否为「模态框」节点 - */ - get isModalNode(): boolean { - return this[nodeSymbol].isModalNode; - } - - /** - * 是否为插槽节点 - */ - get isSlotNode(): boolean { - return this[nodeSymbol].isSlotNode; - } - - /** - * 是否为父类/分支节点 - */ - get isParentalNode(): boolean { - return this[nodeSymbol].isParentalNode; - } - - /** - * 是否为叶子节点 - */ - get isLeafNode(): boolean { - return this[nodeSymbol].isLeafNode; - } - - /** - * judge if it is a node or not - */ - readonly isNode = true; - - /** - * 获取当前节点的锁定状态 - */ - get isLocked(): boolean { - return this[nodeSymbol].isLocked; - } - - /** - * 下标 - */ - get index() { - return this[nodeSymbol].index; - } - - /** - * 图标 - */ - get icon(): IPublicTypeIconType { - return this[nodeSymbol].icon; - } - - /** - * 节点所在树的层级深度,根节点深度为 0 - */ - get zLevel(): number { - return this[nodeSymbol].zLevel; - } - - /** - * 节点 componentName - */ - get componentName(): string { - return this[nodeSymbol].componentName; - } - - /** - * 节点的物料元数据 - */ - get componentMeta(): IPublicModelComponentMeta | null { - return ShellComponentMeta.create(this[nodeSymbol].componentMeta); - } - - /** - * 获取节点所属的文档模型对象 - * @returns - */ - get document(): IPublicModelDocumentModel | null { - return ShellDocumentModel.create(this[documentSymbol]); - } - - /** - * 获取当前节点的前一个兄弟节点 - * @returns - */ - get prevSibling(): IPublicModelNode | null { - return Node.create(this[nodeSymbol].prevSibling); - } - - /** - * 获取当前节点的后一个兄弟节点 - * @returns - */ - get nextSibling(): IPublicModelNode | null { - return Node.create(this[nodeSymbol].nextSibling); - } - - /** - * 获取当前节点的父亲节点 - * @returns - */ - get parent(): IPublicModelNode | null { - return Node.create(this[nodeSymbol].parent); - } - - /** - * 获取当前节点的孩子节点模型 - * @returns - */ - get children(): IPublicModelNodeChildren | null { - return ShellNodeChildren.create(this[nodeSymbol].children); - } - - /** - * 节点上挂载的插槽节点们 - */ - get slots(): IPublicModelNode[] { - return this[nodeSymbol].slots.map((node: InnerNode) => Node.create(node)!); - } - - /** - * 当前节点为插槽节点时,返回节点对应的属性实例 - */ - get slotFor(): IPublicModelProp | null | undefined { - return ShellProp.create(this[nodeSymbol].slotFor); - } - - /** - * 返回节点的属性集 - */ - get props(): IPublicModelProps | null { - return ShellProps.create(this[nodeSymbol].props); - } - - /** - * 返回节点的属性集 - */ - get propsData(): IPublicTypePropsMap | IPublicTypePropsList | null { - return this[nodeSymbol].propsData; - } - - /** - * 获取符合搭建协议 - 节点 schema 结构 - */ - get schema(): IPublicTypeNodeSchema { - return this[nodeSymbol].schema; - } - - get settingEntry(): IPublicModelSettingTopEntry { - return ShellSettingTopEntry.create(this[nodeSymbol].settingEntry as any); - } - - constructor(node: InnerNode) { - this[nodeSymbol] = node; - this[documentSymbol] = node.document; - - this._id = this[nodeSymbol].id; - } - - static create(node: InnerNode | IPublicModelNode | null | undefined): IPublicModelNode | null { - if (!node) { - return null; - } - // 直接返回已挂载的 shell node 实例 - if (isShellNode(node)) { - return (node as any)[shellNodeSymbol]; - } - const shellNode = new Node(node); - // @ts-expect-error: 挂载 shell node 实例 - node[shellNodeSymbol] = shellNode; - return shellNode; - } - - /** - * 获取节点实例对应的 dom 节点 - */ - getDOMNode() { - return (this[nodeSymbol] as any).getDOMNode(); - } - - /** - * 执行新增、删除、排序等操作 - * @param remover - * @param adder - * @param sorter - */ - mergeChildren( - remover: (node: IPublicModelNode, idx: number) => boolean, - adder: (children: IPublicModelNode[]) => any, - sorter: (firstNode: IPublicModelNode, secondNode: IPublicModelNode) => number, - ): any { - return this.children?.mergeChildren(remover, adder, sorter); - } - - /** - * 返回节点的尺寸、位置信息 - * @returns - */ - getRect(): DOMRect | null { - return this[nodeSymbol].getRect(); - } - - /** - * 是否有挂载插槽节点 - * @returns - */ - hasSlots(): boolean { - return this[nodeSymbol].hasSlots(); - } - - /** - * 是否设定了渲染条件 - * @returns - */ - hasCondition(): boolean { - return this[nodeSymbol].hasCondition(); - } - - /** - * 是否设定了循环数据 - * @returns - */ - hasLoop(): boolean { - return this[nodeSymbol].hasLoop(); - } - - get visible(): boolean { - return this[nodeSymbol].getVisible(); - } - - set visible(value: boolean) { - this[nodeSymbol].setVisible(value); - } - - getVisible(): boolean { - return this[nodeSymbol].getVisible(); - } - - setVisible(flag: boolean): void { - this[nodeSymbol].setVisible(flag); - } - - isConditionalVisible(): boolean | undefined { - return this[nodeSymbol].isConditionalVisible(); - } - - /** - * 设置节点锁定状态 - * @param flag - */ - lock(flag?: boolean): void { - this[nodeSymbol].lock(flag); - } - - contains(node: IPublicModelNode): boolean { - return this[nodeSymbol].contains((node as any)[nodeSymbol]); - } - - /** - * 获取指定 path 的属性模型实例 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getProp(path: string, createIfNone = true): IPublicModelProp | null { - return ShellProp.create(this[nodeSymbol].getProp(path, createIfNone)); - } - - /** - * 获取指定 path 的属性模型实例值 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getPropValue(path: string) { - return this.getProp(path, false)?.getValue(); - } - - /** - * 获取指定 path 的属性模型实例, - * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @param createIfNone 当没有属性的时候,是否创建一个属性 - * @returns - */ - getExtraProp(path: string, createIfNone?: boolean): IPublicModelProp | null { - return ShellProp.create(this[nodeSymbol].getExtraProp(path, createIfNone)); - } - - /** - * 获取指定 path 的属性模型实例, - * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getExtraPropValue(path: string): any { - return this.getExtraProp(path)?.getValue(); - } - - /** - * 设置指定 path 的属性模型实例值 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @param value 值 - * @returns - */ - setPropValue(path: string, value: IPublicTypeCompositeValue): void { - return this.getProp(path)?.setValue(value); - } - - /** - * 设置指定 path 的属性模型实例值 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @param value 值 - * @returns - */ - setExtraPropValue(path: string, value: IPublicTypeCompositeValue): void { - return this.getExtraProp(path)?.setValue(value); - } - - /** - * 导入节点数据 - * @param data - */ - importSchema(data: IPublicTypeNodeSchema): void { - this[nodeSymbol].import(data); - } - - /** - * 导出节点数据 - * @param stage - * @param options - * @returns - */ - exportSchema( - stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Render, - options?: any, - ): IPublicTypeNodeSchema { - return this[nodeSymbol].export(stage, options); - } - - /** - * 在指定位置之前插入一个节点 - * @param node - * @param ref - * @param useMutator - */ - insertBefore( - node: IPublicModelNode, - ref?: IPublicModelNode | undefined, - useMutator?: boolean, - ): void { - this[nodeSymbol].insertBefore( - (node as any)[nodeSymbol] || node, - (ref as any)?.[nodeSymbol], - useMutator, - ); - } - - /** - * 在指定位置之后插入一个节点 - * @param node - * @param ref - * @param useMutator - */ - insertAfter( - node: IPublicModelNode, - ref?: IPublicModelNode | undefined, - useMutator?: boolean, - ): void { - this[nodeSymbol].insertAfter( - (node as any)[nodeSymbol] || node, - (ref as any)?.[nodeSymbol], - useMutator, - ); - } - - /** - * 替换指定节点 - * @param node 待替换的子节点 - * @param data 用作替换的节点对象或者节点描述 - * @returns - */ - replaceChild(node: IPublicModelNode, data: any): IPublicModelNode | null { - return Node.create(this[nodeSymbol].replaceChild((node as any)[nodeSymbol], data)); - } - - /** - * 将当前节点替换成指定节点描述 - * @param schema - */ - replaceWith(schema: IPublicTypeNodeSchema): any { - this[nodeSymbol].replaceWith(schema); - } - - /** - * 选中当前节点实例 - */ - select(): void { - this[nodeSymbol].select(); - } - - /** - * 设置悬停态 - * @param flag - */ - hover(flag = true): void { - this[nodeSymbol].hover(flag); - } - - /** - * 删除当前节点实例 - */ - remove(): void { - this[nodeSymbol].remove(); - } - - /** - * 设置为磁贴布局节点 - */ - set isRGLContainerNode(flag: boolean) { - this[nodeSymbol].isRGLContainerNode = flag; - } - - /** - * 获取磁贴布局节点设置状态 - * @returns Boolean - */ - get isRGLContainerNode() { - return this[nodeSymbol].isRGLContainerNode; - } - - internalToShellNode() { - return this; - } - - canPerformAction(actionName: string): boolean { - return this[nodeSymbol].canPerformAction(actionName); - } - - /** - * get conditionGroup - * @since v1.1.0 - */ - get conditionGroup(): IPublicModelExclusiveGroup | null { - return ConditionGroup.create(this[nodeSymbol].conditionGroup); - } - - /** - * set value for conditionalVisible - * @since v1.1.0 - */ - setConditionalVisible(): void { - this[nodeSymbol].setConditionalVisible(); - } - - getRGL() { - const { - isContainerNode, - isEmptyNode, - isRGLContainerNode, - isRGLNode, - isRGL, - rglNode, - } = this[nodeSymbol].getRGL(); - - return { - isContainerNode, - isEmptyNode, - isRGLContainerNode, - isRGLNode, - isRGL, - rglNode: Node.create(rglNode), - }; - } -} diff --git a/packages/engine/src/shell/model/plugin-instance.ts b/packages/engine/src/shell/model/plugin-instance.ts deleted file mode 100644 index 156ec7579..000000000 --- a/packages/engine/src/shell/model/plugin-instance.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ILowCodePluginRuntime } from '@alilc/lowcode-designer'; -import { IPublicModelPluginInstance } from '@alilc/lowcode-types'; -import { pluginInstanceSymbol } from '../symbols'; - -export class PluginInstance implements IPublicModelPluginInstance { - private readonly [pluginInstanceSymbol]: ILowCodePluginRuntime; - - constructor(pluginInstance: ILowCodePluginRuntime) { - this[pluginInstanceSymbol] = pluginInstance; - } - - get pluginName(): string { - return this[pluginInstanceSymbol].name; - } - - get dep(): string[] { - return this[pluginInstanceSymbol].dep; - } - - get disabled(): boolean { - return this[pluginInstanceSymbol].disabled; - } - - set disabled(disabled: boolean) { - this[pluginInstanceSymbol].setDisabled(disabled); - } - - get meta() { - return this[pluginInstanceSymbol].meta; - } -} diff --git a/packages/engine/src/shell/model/prop.ts b/packages/engine/src/shell/model/prop.ts deleted file mode 100644 index 15948298b..000000000 --- a/packages/engine/src/shell/model/prop.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { IProp as InnerProp } from '@alilc/lowcode-designer'; -import { IPublicTypeCompositeValue, IPublicEnumTransformStage, IPublicModelProp, IPublicModelNode } from '@alilc/lowcode-types'; -import { propSymbol } from '../symbols'; -import { Node as ShellNode } from './node'; - -export class Prop implements IPublicModelProp { - private readonly [propSymbol]: InnerProp; - - constructor(prop: InnerProp) { - this[propSymbol] = prop; - } - - static create(prop: InnerProp | undefined | null): IPublicModelProp | null { - if (!prop) { - return null; - } - return new Prop(prop); - } - - /** - * id - */ - get id(): string { - return this[propSymbol].id; - } - - /** - * key 值 - * get key of prop - */ - get key(): string | number | undefined { - return this[propSymbol].key; - } - - /** - * 返回当前 prop 的路径 - */ - get path(): string[] { - return this[propSymbol].path; - } - - /** - * 返回所属的节点实例 - */ - get node(): IPublicModelNode | null { - return ShellNode.create(this[propSymbol].getNode()); - } - - /** - * return the slot node (only if the current prop represents a slot) - */ - get slotNode(): IPublicModelNode | null { - return ShellNode.create(this[propSymbol].slotNode); - } - - /** - * judge if it is a prop or not - */ - get isProp(): boolean { - return true; - } - - /** - * 设置值 - * @param val - */ - setValue(val: IPublicTypeCompositeValue): void { - this[propSymbol].setValue(val); - } - - /** - * 获取值 - * @returns - */ - getValue(): any { - return this[propSymbol].getValue(); - } - - /** - * 移除值 - */ - remove(): void { - this[propSymbol].remove(); - } - - /** - * 导出值 - * @param stage - * @returns - */ - exportSchema(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Render) { - return this[propSymbol].export(stage); - } -} diff --git a/packages/engine/src/shell/model/props.ts b/packages/engine/src/shell/model/props.ts deleted file mode 100644 index c658f0212..000000000 --- a/packages/engine/src/shell/model/props.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { IProps as InnerProps, getConvertedExtraKey } from '@alilc/lowcode-designer'; -import { IPublicTypeCompositeValue, IPublicModelProps, IPublicModelNode, IPublicModelProp } from '@alilc/lowcode-types'; -import { propsSymbol } from '../symbols'; -import { Node as ShellNode } from './node'; -import { Prop as ShellProp } from './prop'; - -export class Props implements IPublicModelProps { - private readonly [propsSymbol]: InnerProps; - - constructor(props: InnerProps) { - this[propsSymbol] = props; - } - - static create(props: InnerProps | undefined | null): IPublicModelProps | null { - if (!props) { - return null; - } - return new Props(props); - } - - /** - * id - */ - get id(): string { - return this[propsSymbol].id; - } - - /** - * 返回当前 props 的路径 - */ - get path(): string[] { - return this[propsSymbol].path; - } - - /** - * 返回所属的 node 实例 - */ - get node(): IPublicModelNode | null { - return ShellNode.create(this[propsSymbol].getNode()); - } - - /** - * 获取指定 path 的属性模型实例 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getProp(path: string): IPublicModelProp | null { - return ShellProp.create(this[propsSymbol].getProp(path)); - } - - /** - * 获取指定 path 的属性模型实例值 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getPropValue(path: string): any { - return this.getProp(path)?.getValue(); - } - - /** - * 获取指定 path 的属性模型实例, - * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getExtraProp(path: string): IPublicModelProp | null { - return ShellProp.create(this[propsSymbol].getProp(getConvertedExtraKey(path))); - } - - /** - * 获取指定 path 的属性模型实例值 - * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getExtraPropValue(path: string): any { - return this.getExtraProp(path)?.getValue(); - } - - /** - * 设置指定 path 的属性模型实例值 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @param value 值 - * @returns - */ - setPropValue(path: string, value: IPublicTypeCompositeValue): void { - return this.getProp(path)?.setValue(value); - } - - /** - * 设置指定 path 的属性模型实例值 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @param value 值 - * @returns - */ - setExtraPropValue(path: string, value: IPublicTypeCompositeValue): void { - return this.getExtraProp(path)?.setValue(value); - } - - /** - * test if the specified key is existing or not. - * @param key - * @returns - */ - has(key: string): boolean { - return this[propsSymbol].has(key); - } - - /** - * add a key with given value - * @param value - * @param key - * @returns - */ - add(value: IPublicTypeCompositeValue, key?: string | number | undefined): any { - return this[propsSymbol].add(value, key); - } -} diff --git a/packages/engine/src/shell/model/resource.ts b/packages/engine/src/shell/model/resource.ts deleted file mode 100644 index 7061370ba..000000000 --- a/packages/engine/src/shell/model/resource.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { IPublicModelResource } from '@alilc/lowcode-types'; -import { IResource } from '../../workspace'; -import { resourceSymbol } from '../symbols'; - -export class Resource implements IPublicModelResource { - readonly [resourceSymbol]: IResource; - - constructor(resource: IResource) { - this[resourceSymbol] = resource; - } - - get title() { - return this[resourceSymbol].title; - } - - get id() { - return this[resourceSymbol].id; - } - - get icon() { - return this[resourceSymbol].icon; - } - - get options() { - return this[resourceSymbol].options; - } - - get name() { - return this[resourceSymbol].resourceType.name; - } - - get config() { - return this[resourceSymbol].config; - } - - get type() { - return this[resourceSymbol].resourceType.type; - } - - get category() { - return this[resourceSymbol].category; - } - - get description() { - return this[resourceSymbol].description; - } - - get children() { - return this[resourceSymbol].children.map((child) => new Resource(child) as IPublicModelResource); - } - - get viewName() { - return this[resourceSymbol].viewName; - } -} diff --git a/packages/engine/src/shell/model/selection.ts b/packages/engine/src/shell/model/selection.ts deleted file mode 100644 index 073083a65..000000000 --- a/packages/engine/src/shell/model/selection.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { - IDocumentModel as InnerDocumentModel, - INode as InnerNode, - ISelection, -} from '@alilc/lowcode-designer'; -import { Node as ShellNode } from './node'; -import { selectionSymbol } from '../symbols'; -import { IPublicModelSelection, IPublicModelNode, IPublicTypeDisposable } from '@alilc/lowcode-types'; - -export class Selection implements IPublicModelSelection { - private readonly [selectionSymbol]: ISelection; - - constructor(document: InnerDocumentModel) { - this[selectionSymbol] = document.selection; - } - - /** - * 返回选中的节点 id - */ - get selected(): string[] { - return this[selectionSymbol].selected; - } - - /** - * return selected Node instance - */ - get node(): IPublicModelNode | null { - const nodes = this.getNodes(); - return nodes && nodes.length > 0 ? nodes[0] : null; - } - - /** - * 选中指定节点(覆盖方式) - * @param id - */ - select(id: string): void { - this[selectionSymbol].select(id); - } - - /** - * 批量选中指定节点们 - * @param ids - */ - selectAll(ids: string[]): void { - this[selectionSymbol].selectAll(ids); - } - - /** - * 移除选中的指定节点 - * @param id - */ - remove(id: string): void { - this[selectionSymbol].remove(id); - } - - /** - * 清除所有选中节点 - */ - clear(): void { - this[selectionSymbol].clear(); - } - - /** - * 判断是否选中了指定节点 - * @param id - * @returns - */ - has(id: string): boolean { - return this[selectionSymbol].has(id); - } - - /** - * 选中指定节点(增量方式) - * @param id - */ - add(id: string): void { - this[selectionSymbol].add(id); - } - - /** - * 获取选中的节点实例 - * @returns - */ - getNodes(): IPublicModelNode[] { - const innerNodes = this[selectionSymbol].getNodes(); - const nodes: IPublicModelNode[] = []; - innerNodes.forEach((node: InnerNode) => { - const shellNode = ShellNode.create(node); - if (shellNode) { - nodes.push(shellNode); - } - }); - return nodes; - } - - /** - * 获取选区的顶层节点 - * for example: - * getNodes() returns [A, subA, B], then - * getTopNodes() will return [A, B], subA will be removed - * @returns - */ - getTopNodes(includeRoot: boolean = false): IPublicModelNode[] { - const innerNodes = this[selectionSymbol].getTopNodes(includeRoot); - const nodes: IPublicModelNode[] = []; - innerNodes.forEach((node: InnerNode) => { - const shellNode = ShellNode.create(node); - if (shellNode) { - nodes.push(shellNode); - } - }); - return nodes; - } - - onSelectionChange(fn: (ids: string[]) => void): IPublicTypeDisposable { - return this[selectionSymbol].onSelectionChange(fn); - } -} diff --git a/packages/engine/src/shell/model/setting-field.ts b/packages/engine/src/shell/model/setting-field.ts deleted file mode 100644 index 0293b592e..000000000 --- a/packages/engine/src/shell/model/setting-field.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { ISettingField } from '@alilc/lowcode-designer'; -import { - IPublicTypeCompositeValue, - IPublicTypeFieldConfig, - IPublicTypeCustomView, - IPublicTypeSetterType, - IPublicTypeFieldExtraProps, - IPublicModelSettingTopEntry, - IPublicModelNode, - IPublicModelComponentMeta, - IPublicTypeSetValueOptions, - IPublicModelSettingField, - IPublicTypeDisposable, -} from '@alilc/lowcode-types'; -import { settingFieldSymbol } from '../symbols'; -import { Node as ShellNode } from './node'; -import { SettingTopEntry, SettingTopEntry as ShellSettingTopEntry } from './setting-top-entry'; -import { ComponentMeta as ShellComponentMeta } from './component-meta'; -import { isCustomView, isSettingField } from '@alilc/lowcode-utils'; - -export class SettingField implements IPublicModelSettingField { - private readonly [settingFieldSymbol]: ISettingField; - - constructor(prop: ISettingField) { - this[settingFieldSymbol] = prop; - } - - static create(prop: ISettingField): IPublicModelSettingField { - return new SettingField(prop); - } - - /** - * 获取设置属性的 isGroup - */ - get isGroup(): boolean { - return this[settingFieldSymbol].isGroup; - } - - /** - * 获取设置属性的 id - */ - get id(): string { - return this[settingFieldSymbol].id; - } - - /** - * 获取设置属性的 name - */ - get name(): string | number | undefined { - return this[settingFieldSymbol].name; - } - - /** - * 获取设置属性的 key - */ - get key(): string | number | undefined { - return this[settingFieldSymbol].getKey(); - } - - /** - * 获取设置属性的 path - */ - get path(): any[] { - return this[settingFieldSymbol].path; - } - - /** - * 获取设置属性的 title - */ - get title(): any { - return this[settingFieldSymbol].title; - } - - /** - * 获取设置属性的 setter - */ - get setter(): IPublicTypeSetterType | null { - return this[settingFieldSymbol].setter; - } - - /** - * 获取设置属性的 expanded - */ - get expanded(): boolean { - return this[settingFieldSymbol].expanded; - } - - /** - * 获取设置属性的 extraProps - */ - get extraProps(): IPublicTypeFieldExtraProps { - return this[settingFieldSymbol].extraProps; - } - - get props(): IPublicModelSettingTopEntry { - return ShellSettingTopEntry.create(this[settingFieldSymbol].props); - } - - /** - * 获取设置属性对应的节点实例 - */ - get node(): IPublicModelNode | null { - return ShellNode.create(this[settingFieldSymbol].getNode()); - } - - /** - * 获取设置属性的父设置属性 - */ - get parent(): IPublicModelSettingField | IPublicModelSettingTopEntry { - if (isSettingField(this[settingFieldSymbol].parent)) { - return SettingField.create(this[settingFieldSymbol].parent); - } - - return SettingTopEntry.create(this[settingFieldSymbol].parent); - } - - /** - * 获取顶级设置属性 - */ - get top(): IPublicModelSettingTopEntry { - return ShellSettingTopEntry.create(this[settingFieldSymbol].top); - } - - /** - * 是否是 SettingField 实例 - */ - get isSettingField(): boolean { - return this[settingFieldSymbol].isSettingField; - } - - /** - * componentMeta - */ - get componentMeta(): IPublicModelComponentMeta | null { - return ShellComponentMeta.create(this[settingFieldSymbol].componentMeta); - } - - /** - * 获取设置属性的 items - */ - get items(): Array<IPublicModelSettingField | IPublicTypeCustomView> { - return this[settingFieldSymbol].items?.map((item) => { - if (isCustomView(item)) { - return item; - } - return item.internalToShellField(); - }); - } - - /** - * 设置 key 值 - * @param key - */ - setKey(key: string | number): void { - this[settingFieldSymbol].setKey(key); - } - - /** - * 设置值 - * @param val 值 - */ - setValue(val: IPublicTypeCompositeValue, extraOptions?: IPublicTypeSetValueOptions): void { - this[settingFieldSymbol].setValue(val, false, false, extraOptions); - } - - /** - * 设置子级属性值 - * @param propName 子属性名 - * @param value 值 - */ - setPropValue(propName: string | number, value: any): void { - this[settingFieldSymbol].setPropValue(propName, value); - } - - /** - * 清空指定属性值 - * @param propName - */ - clearPropValue(propName: string | number): void { - this[settingFieldSymbol].clearPropValue(propName); - } - - /** - * 获取配置的默认值 - * @returns - */ - getDefaultValue(): any { - return this[settingFieldSymbol].getDefaultValue(); - } - - /** - * 获取值 - * @returns - */ - getValue(): any { - return this[settingFieldSymbol].getValue(); - } - - /** - * 获取子级属性值 - * @param propName 子属性名 - * @returns - */ - getPropValue(propName: string | number): any { - return this[settingFieldSymbol].getPropValue(propName); - } - - /** - * 获取顶层附属属性值 - */ - getExtraPropValue(propName: string): any { - return this[settingFieldSymbol].getExtraPropValue(propName); - } - - /** - * 设置顶层附属属性值 - */ - setExtraPropValue(propName: string, value: any): void { - this[settingFieldSymbol].setExtraPropValue(propName, value); - } - - /** - * 获取设置属性集 - * @returns - */ - getProps(): IPublicModelSettingTopEntry { - return ShellSettingTopEntry.create(this[settingFieldSymbol].getProps()); - } - - /** - * 是否绑定了变量 - * @returns - */ - isUseVariable(): boolean { - return this[settingFieldSymbol].isUseVariable(); - } - - /** - * 设置绑定变量 - * @param flag - */ - setUseVariable(flag: boolean): void { - this[settingFieldSymbol].setUseVariable(flag); - } - - /** - * 创建一个设置 field 实例 - * @param config - * @returns - */ - createField(config: IPublicTypeFieldConfig): IPublicModelSettingField { - return SettingField.create(this[settingFieldSymbol].createField(config)); - } - - /** - * 获取值,当为变量时,返回 mock - * @returns - */ - getMockOrValue(): any { - return this[settingFieldSymbol].getMockOrValue(); - } - - /** - * 销毁当前 field 实例 - */ - purge(): void { - this[settingFieldSymbol].purge(); - } - - /** - * 移除当前 field 实例 - */ - remove(): void { - this[settingFieldSymbol].remove(); - } - - /** - * 设置 autorun - * @param action - * @returns - */ - onEffect(action: () => void): IPublicTypeDisposable { - return this[settingFieldSymbol].onEffect(action); - } - - /** - * 返回 shell 模型,兼容某些场景下 field 已经是 shell field 了 - * @returns - */ - internalToShellField() { - return this; - } -} diff --git a/packages/engine/src/shell/model/setting-top-entry.ts b/packages/engine/src/shell/model/setting-top-entry.ts deleted file mode 100644 index 49d89f3b4..000000000 --- a/packages/engine/src/shell/model/setting-top-entry.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { ISettingTopEntry } from '@alilc/lowcode-designer'; -import { settingTopEntrySymbol } from '../symbols'; -import { Node as ShellNode } from './node'; -import { IPublicModelSettingTopEntry, IPublicModelNode, IPublicModelSettingField } from '@alilc/lowcode-types'; -import { SettingField } from './setting-field'; - -export class SettingTopEntry implements IPublicModelSettingTopEntry { - private readonly [settingTopEntrySymbol]: ISettingTopEntry; - - constructor(prop: ISettingTopEntry) { - this[settingTopEntrySymbol] = prop; - } - - static create(prop: ISettingTopEntry): IPublicModelSettingTopEntry { - return new SettingTopEntry(prop); - } - - /** - * 返回所属的节点实例 - */ - get node(): IPublicModelNode | null { - return ShellNode.create(this[settingTopEntrySymbol].getNode()); - } - - /** - * 获取子级属性对象 - * @param propName - * @returns - */ - get(propName: string | number): IPublicModelSettingField { - return SettingField.create(this[settingTopEntrySymbol].get(propName)!); - } - - /** - * 获取指定 propName 的值 - * @param propName - * @returns - */ - getPropValue(propName: string | number): any { - return this[settingTopEntrySymbol].getPropValue(propName); - } - - /** - * 设置指定 propName 的值 - * @param propName - * @param value - */ - setPropValue(propName: string | number, value: any): void { - this[settingTopEntrySymbol].setPropValue(propName, value); - } - - clearPropValue(propName: string | number) { - this[settingTopEntrySymbol].clearPropValue(propName); - } -} diff --git a/packages/engine/src/shell/model/simulator-render.ts b/packages/engine/src/shell/model/simulator-render.ts deleted file mode 100644 index 0313d68ca..000000000 --- a/packages/engine/src/shell/model/simulator-render.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IPublicModelSimulatorRender } from '@alilc/lowcode-types'; -import { simulatorRenderSymbol } from '../symbols'; -import { BuiltinSimulatorRenderer } from '@alilc/lowcode-designer'; - -export class SimulatorRender implements IPublicModelSimulatorRender { - private readonly [simulatorRenderSymbol]: BuiltinSimulatorRenderer; - - constructor(simulatorRender: BuiltinSimulatorRenderer) { - this[simulatorRenderSymbol] = simulatorRender; - } - - static create(simulatorRender: BuiltinSimulatorRenderer): IPublicModelSimulatorRender { - return new SimulatorRender(simulatorRender); - } - - get components() { - return this[simulatorRenderSymbol].components; - } - - rerender() { - return this[simulatorRenderSymbol].rerender(); - } -} diff --git a/packages/engine/src/shell/model/skeleton-item.ts b/packages/engine/src/shell/model/skeleton-item.ts deleted file mode 100644 index 90757a3e0..000000000 --- a/packages/engine/src/shell/model/skeleton-item.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { skeletonItemSymbol } from '../symbols'; -import { IPublicModelSkeletonItem } from '@alilc/lowcode-types'; -import { Dock, IWidget, Panel, PanelDock, Stage, Widget } from '@alilc/lowcode-editor-skeleton'; - -export class SkeletonItem implements IPublicModelSkeletonItem { - private [skeletonItemSymbol]: IWidget | Widget | Panel | Stage | Dock | PanelDock; - - constructor(skeletonItem: IWidget | Widget | Panel | Stage | Dock | PanelDock) { - this[skeletonItemSymbol] = skeletonItem; - } - - get name() { - return this[skeletonItemSymbol].name; - } - - get visible() { - return this[skeletonItemSymbol].visible; - } - - disable() { - this[skeletonItemSymbol].disable?.(); - } - - enable() { - this[skeletonItemSymbol].enable?.(); - } - - hide() { - this[skeletonItemSymbol].hide(); - } - - show() { - this[skeletonItemSymbol].show(); - } - - toggle() { - this[skeletonItemSymbol].toggle(); - } -} diff --git a/packages/engine/src/shell/model/window.ts b/packages/engine/src/shell/model/window.ts deleted file mode 100644 index d75dbd6cc..000000000 --- a/packages/engine/src/shell/model/window.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { windowSymbol } from '../symbols'; -import { IPublicModelResource, IPublicModelWindow, IPublicTypeDisposable } from '@alilc/lowcode-types'; -import { IEditorWindow } from '../../workspace'; -import { Resource as ShellResource } from './resource'; -import { EditorView } from './editor-view'; - -export class Window implements IPublicModelWindow { - private readonly [windowSymbol]: IEditorWindow; - - get id() { - return this[windowSymbol]?.id; - } - - get title() { - return this[windowSymbol].title; - } - - get icon() { - return this[windowSymbol].icon; - } - - get resource(): IPublicModelResource { - return new ShellResource(this[windowSymbol].resource); - } - - constructor(editorWindow: IEditorWindow) { - this[windowSymbol] = editorWindow; - } - - importSchema(schema: any): any { - this[windowSymbol].importSchema(schema); - } - - changeViewType(viewName: string) { - this[windowSymbol].changeViewName(viewName, false); - } - - onChangeViewType(fun: (viewName: string) => void): IPublicTypeDisposable { - return this[windowSymbol].onChangeViewType(fun); - } - - async save() { - return await this[windowSymbol].save(); - } - - onSave(fn: () => void) { - return this[windowSymbol].onSave(fn); - } - - get currentEditorView() { - if (this[windowSymbol]._editorView) { - return new EditorView(this[windowSymbol]._editorView).toProxy() as any; - } - return null; - } - - get editorViews() { - return Array.from(this[windowSymbol].editorViews.values()).map(d => new EditorView(d).toProxy() as any); - } -} diff --git a/packages/engine/src/shell/symbols.ts b/packages/engine/src/shell/symbols.ts deleted file mode 100644 index 5e5264e68..000000000 --- a/packages/engine/src/shell/symbols.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 以下 symbol 均用于在 shell 层对外暴露的模型中存储相应内部模型的 key - */ -export const projectSymbol = Symbol('project'); -export const designerSymbol = Symbol('designer'); -export const skeletonSymbol = Symbol('skeleton'); -export const documentSymbol = Symbol('document'); -export const editorSymbol = Symbol('editor'); -export const nodeSymbol = Symbol('node'); -export const modalNodesManagerSymbol = Symbol('modalNodesManager'); -export const nodeChildrenSymbol = Symbol('nodeChildren'); -export const propSymbol = Symbol('prop'); -export const settingFieldSymbol = Symbol('settingField'); -export const settingTopEntrySymbol = Symbol('settingTopEntry'); -export const propsSymbol = Symbol('props'); -export const detectingSymbol = Symbol('detecting'); -export const selectionSymbol = Symbol('selection'); -export const historySymbol = Symbol('history'); -export const canvasSymbol = Symbol('canvas'); -export const dragonSymbol = Symbol('dragon'); -export const componentMetaSymbol = Symbol('componentMeta'); -export const dropLocationSymbol = Symbol('dropLocation'); -export const simulatorHostSymbol = Symbol('simulatorHost'); -export const simulatorRenderSymbol = Symbol('simulatorRender'); -export const dragObjectSymbol = Symbol('dragObject'); -export const locateEventSymbol = Symbol('locateEvent'); -export const designerCabinSymbol = Symbol('designerCabin'); -export const editorCabinSymbol = Symbol('editorCabin'); -export const skeletonCabinSymbol = Symbol('skeletonCabin'); -export const hotkeySymbol = Symbol('hotkey'); -export const pluginsSymbol = Symbol('plugins'); -export const workspaceSymbol = Symbol('workspace'); -export const windowSymbol = Symbol('window'); -export const pluginInstanceSymbol = Symbol('plugin-instance'); -export const resourceTypeSymbol = Symbol('resourceType'); -export const resourceSymbol = Symbol('resource'); -export const clipboardSymbol = Symbol('clipboard'); -export const configSymbol = Symbol('configSymbol'); -export const conditionGroupSymbol = Symbol('conditionGroup'); -export const editorViewSymbol = Symbol('editorView'); -export const pluginContextSymbol = Symbol('pluginContext'); -export const skeletonItemSymbol = Symbol('skeletonItem'); -export const commandSymbol = Symbol('command'); diff --git a/packages/engine/src/workspace/context/base-context.ts b/packages/engine/src/workspace/context/base-context.ts deleted file mode 100644 index c823e40f8..000000000 --- a/packages/engine/src/workspace/context/base-context.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { - Editor, - engineConfig, - Setters as InnerSetters, - Hotkey as InnerHotkey, - commonEvent, - IEngineConfig, - IHotKey, - Command as InnerCommand, -} from '@alilc/lowcode-editor-core'; -import { - Designer, - ILowCodePluginContextApiAssembler, - LowCodePluginManager, - ILowCodePluginContextPrivate, - IProject, - IDesigner, - ILowCodePluginManager, -} from '@alilc/lowcode-designer'; -import { ISkeleton, Skeleton as InnerSkeleton } from '@alilc/lowcode-editor-skeleton'; -import { - Hotkey, - Plugins, - Project, - Skeleton, - Setters, - Material, - Event, - Common, - Logger, - Workspace, - Window, - Canvas, - CommonUI, - Command, -} from '../../shell'; -import { - IPluginPreferenceMananger, - IPublicApiCanvas, - IPublicApiCommon, - IPublicApiEvent, - IPublicApiHotkey, - IPublicApiMaterial, - IPublicApiPlugins, - IPublicApiProject, - IPublicApiSetters, - IPublicApiSkeleton, - IPublicEnumPluginRegisterLevel, - IPublicModelPluginContext, - IPublicTypePluginMeta, -} from '@alilc/lowcode-types'; -import { createLogger, Logger as InnerLogger } from '@alilc/lowcode-utils'; -import { IWorkspace } from '../workspace'; -import { IEditorWindow } from '../window'; - -export interface IBasicContext extends BasicContext {} - -export class BasicContext -implements - Omit< - IPublicModelPluginContext, - 'workspace' | 'commonUI' | 'command' | 'isPluginRegisteredInWorkspace' | 'editorWindow' - > -{ - skeleton: IPublicApiSkeleton; - plugins: IPublicApiPlugins; - project: IPublicApiProject; - setters: IPublicApiSetters; - material: IPublicApiMaterial; - common: IPublicApiCommon; - config: IEngineConfig; - event: IPublicApiEvent; - logger: InnerLogger; - hotkey: IPublicApiHotkey; - innerProject: IProject; - editor: Editor; - designer: IDesigner; - registerInnerPlugins: () => Promise<void>; - innerSetters: InnerSetters; - innerSkeleton: ISkeleton; - innerHotkey: IHotKey; - innerPlugins: ILowCodePluginManager; - canvas: IPublicApiCanvas; - pluginEvent: IPublicApiEvent; - preference: IPluginPreferenceMananger; - workspace: IWorkspace; - - constructor( - innerWorkspace: IWorkspace, - viewName: string, - readonly registerLevel: IPublicEnumPluginRegisterLevel, - public editorWindow?: IEditorWindow, - ) { - const editor = new Editor(viewName, true); - - const innerSkeleton = new InnerSkeleton(editor, viewName); - editor.set('skeleton' as any, innerSkeleton); - - const designer: Designer = new Designer({ - editor, - viewName, - shellModelFactory: innerWorkspace?.shellModelFactory, - }); - editor.set('designer' as any, designer); - - const { project: innerProject } = designer; - const workspace = new Workspace(innerWorkspace); - const innerHotkey = new InnerHotkey(viewName); - const hotkey = new Hotkey(innerHotkey, true); - const innerSetters = new InnerSetters(viewName); - const setters = new Setters(innerSetters, true); - const material = new Material(editor, true); - const project = new Project(innerProject, true); - const config = engineConfig; - const event = new Event(commonEvent, { prefix: 'common' }); - const logger = createLogger({ level: 'warn', bizName: 'common' }); - const skeleton = new Skeleton(innerSkeleton, 'any', true); - const canvas = new Canvas(editor, true); - const commonUI = new CommonUI(editor); - const innerCommand = new InnerCommand(); - editor.set('setters', setters); - editor.set('project', project); - editor.set('material', material); - editor.set('hotkey', hotkey); - editor.set('innerHotkey', innerHotkey); - this.innerSetters = innerSetters; - this.innerSkeleton = innerSkeleton; - this.skeleton = skeleton; - this.innerProject = innerProject; - this.project = project; - this.setters = setters; - this.material = material; - this.config = config; - this.event = event; - this.logger = logger; - this.hotkey = hotkey; - this.innerHotkey = innerHotkey; - this.editor = editor; - this.designer = designer; - this.canvas = canvas; - const common = new Common(editor, innerSkeleton); - this.common = common; - let plugins: IPublicApiPlugins; - - const pluginContextApiAssembler: ILowCodePluginContextApiAssembler = { - assembleApis: ( - context: ILowCodePluginContextPrivate, - pluginName: string, - meta: IPublicTypePluginMeta, - ) => { - context.workspace = workspace; - context.hotkey = hotkey; - context.project = project; - context.skeleton = new Skeleton(innerSkeleton, pluginName, true); - context.setters = setters; - context.material = material; - const eventPrefix = meta?.eventPrefix || 'common'; - const commandScope = meta?.commandScope; - context.event = new Event(commonEvent, { prefix: eventPrefix }); - context.config = config; - context.common = common; - context.plugins = plugins; - context.logger = new Logger({ level: 'warn', bizName: `plugin:${pluginName}` }); - context.canvas = canvas; - context.commonUI = commonUI; - if (editorWindow) { - context.editorWindow = new Window(editorWindow); - } - context.command = new Command(innerCommand, context as IPublicModelPluginContext, { - commandScope, - }); - context.registerLevel = registerLevel; - context.isPluginRegisteredInWorkspace = - registerLevel === IPublicEnumPluginRegisterLevel.Workspace; - editor.set('pluginContext', context); - }, - }; - - const innerPlugins = new LowCodePluginManager(pluginContextApiAssembler, viewName); - this.innerPlugins = innerPlugins; - plugins = new Plugins(innerPlugins, true).toProxy(); - editor.set('plugins' as any, plugins); - editor.set('innerPlugins' as any, innerPlugins); - this.plugins = plugins; - - // 注册一批内置插件 - this.registerInnerPlugins = async function registerPlugins() { - await innerWorkspace?.registryInnerPlugin(designer, editor, plugins); - }; - } -} diff --git a/packages/engine/src/workspace/context/view-context.ts b/packages/engine/src/workspace/context/view-context.ts deleted file mode 100644 index 8c571d712..000000000 --- a/packages/engine/src/workspace/context/view-context.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { computed, makeObservable, observable, flow } from '@alilc/lowcode-editor-core'; -import { - IPublicEditorViewConfig, - IPublicEnumPluginRegisterLevel, - IPublicTypeEditorView, -} from '@alilc/lowcode-types'; -import { IWorkspace } from '../workspace'; -import { BasicContext, IBasicContext } from './base-context'; -import { IEditorWindow } from '../window'; -import { getWebviewPlugin } from '../inner-plugins/webview'; - -export interface IViewContext extends IBasicContext { - editorWindow: IEditorWindow; - - viewName: string; - - viewType: 'editor' | 'webview'; -} - -export class Context extends BasicContext implements IViewContext { - viewName = 'editor-view'; - - instance: IPublicEditorViewConfig; - - viewType: 'editor' | 'webview'; - - @observable _activate = false; - - @observable isInit: boolean = false; - - init: any = flow(function* (this: Context) { - if (this.viewType === 'webview') { - const url = yield this.instance?.url?.(); - yield this.plugins.register(getWebviewPlugin(url, this.viewName)); - } else { - yield this.registerInnerPlugins(); - } - yield this.instance?.init?.(); - yield this.innerPlugins.init(); - this.isInit = true; - }); - - constructor( - public workspace: IWorkspace, - public editorWindow: IEditorWindow, - public editorView: IPublicTypeEditorView, - options: Object | undefined, - ) { - super(workspace, editorView.viewName, IPublicEnumPluginRegisterLevel.EditorView, editorWindow); - this.viewType = editorView.viewType || 'editor'; - this.viewName = editorView.viewName; - this.instance = editorView( - this.innerPlugins._getLowCodePluginContext({ - pluginName: 'any', - }), - options, - ); - makeObservable(this); - } - - @computed get active() { - return this._activate; - } - - onSimulatorRendererReady = (): Promise<void> => { - return new Promise((resolve) => { - this.project.onSimulatorRendererReady(() => { - resolve(); - }); - }); - }; - - setActivate = (_activate: boolean) => { - this._activate = _activate; - this.innerHotkey.activate(this._activate); - }; - - async save() { - return await this.instance?.save?.(); - } -} diff --git a/packages/engine/src/workspace/impl.ts b/packages/engine/src/workspace/impl.ts new file mode 100644 index 000000000..7d6bc8d18 --- /dev/null +++ b/packages/engine/src/workspace/impl.ts @@ -0,0 +1,9 @@ +import { Provide } from '@alilc/lowcode-core'; +import { IWorkspaceMainService } from './interface'; + +@Provide('WorkspaceMainService') +export class WorkspaceMainService implements IWorkspaceMainService { + initialize(): void { + console.log('initialize...'); + } +} diff --git a/packages/engine/src/workspace/index.ts b/packages/engine/src/workspace/index.ts index 6c437fad0..fc141f790 100644 --- a/packages/engine/src/workspace/index.ts +++ b/packages/engine/src/workspace/index.ts @@ -1,7 +1 @@ -export { Workspace } from './workspace'; -export type { IWorkspace } from './workspace'; -export * from './window'; -export * from './layouts/workbench'; -export { Resource } from './resource'; -export type { IResource } from './resource'; -export type { IViewContext } from './context/view-context'; +export * from './interface'; diff --git a/packages/engine/src/workspace/inner-plugins/webview.tsx b/packages/engine/src/workspace/inner-plugins/webview.tsx deleted file mode 100644 index 820b843ab..000000000 --- a/packages/engine/src/workspace/inner-plugins/webview.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { IPublicModelPluginContext } from '@alilc/lowcode-types'; - -export function DesignerView(props: { - url: string; - viewName?: string; -}) { - return ( - <div className="lc-designer lowcode-plugin-designer"> - <div className="lc-project"> - <div className="lc-simulator-shell"> - <iframe - name={`webview-view-${props.viewName}`} - className="lc-simulator-content-frame" - style={{ - height: '100%', - width: '100%', - }} - src={props.url} - /> - </div> - </div> - </div> - ); -} - -export function getWebviewPlugin(url: string, viewName: string) { - function webviewPlugin(ctx: IPublicModelPluginContext) { - const { skeleton } = ctx; - return { - init() { - skeleton.add({ - area: 'mainArea', - name: 'designer', - type: 'Widget', - content: DesignerView, - contentProps: { - ctx, - url, - viewName, - }, - }); - }, - }; - } - - webviewPlugin.pluginName = '___webview_plugin___'; - - return webviewPlugin; -} diff --git a/packages/engine/src/workspace/interface.ts b/packages/engine/src/workspace/interface.ts new file mode 100644 index 000000000..d4572f2b7 --- /dev/null +++ b/packages/engine/src/workspace/interface.ts @@ -0,0 +1,7 @@ +import { createDecorator } from '@alilc/lowcode-core'; + +export interface IWorkspaceMainService { + initialize(): void; +} + +export const IWorkspaceMainService = createDecorator<IWorkspaceMainService>('WorkspaceMainService'); diff --git a/packages/engine/src/workspace/layouts/workbench.tsx b/packages/engine/src/workspace/layouts/workbench.tsx deleted file mode 100644 index 79bc06093..000000000 --- a/packages/engine/src/workspace/layouts/workbench.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { Component } from 'react'; -import { TipContainer, engineConfig, observer } from '@alilc/lowcode-editor-core'; -import { WindowView } from '../view/window-view'; -import classNames from 'classnames'; -import { SkeletonContext } from '../skeleton-context'; -import { EditorConfig, PluginClassSet } from '@alilc/lowcode-types'; -import { Workspace } from '../workspace'; -import { BottomArea, LeftArea, LeftFixedPane, LeftFloatPane, MainArea, SubTopArea, TopArea } from '@alilc/lowcode-editor-skeleton'; - -@observer -export class Workbench extends Component<{ - workspace: Workspace; - config?: EditorConfig; - components?: PluginClassSet; - className?: string; - topAreaItemClassName?: string; -}, { - workspaceEmptyComponent: any; - theme?: string; - }> { - constructor(props: any) { - super(props); - const { config, components, workspace } = this.props; - const { skeleton } = workspace; - skeleton.buildFromConfig(config, components); - engineConfig.onGot('theme', (theme) => { - this.setState({ - theme, - }); - }); - engineConfig.onGot('workspaceEmptyComponent', (workspaceEmptyComponent) => { - this.setState({ - workspaceEmptyComponent, - }); - }); - this.state = { - workspaceEmptyComponent: engineConfig.get('workspaceEmptyComponent'), - theme: engineConfig.get('theme'), - }; - } - - render() { - const { workspace, className = 'engine-main', topAreaItemClassName = 'engine-actionitem' } = this.props; - const { skeleton } = workspace; - const { workspaceEmptyComponent: WorkspaceEmptyComponent, theme } = this.state; - - return ( - <div className={classNames('lc-workspace-workbench', className, theme)}> - <SkeletonContext.Provider value={skeleton}> - <TopArea className="lc-workspace-top-area" area={skeleton.topArea} itemClassName={topAreaItemClassName} /> - <div className="lc-workspace-workbench-body"> - <LeftArea className="lc-workspace-left-area lc-left-area" area={skeleton.leftArea} /> - <LeftFloatPane area={skeleton.leftFloatArea} /> - <LeftFixedPane area={skeleton.leftFixedArea} /> - <div className="lc-workspace-workbench-center"> - <div className="lc-workspace-workbench-center-content"> - <SubTopArea area={skeleton.subTopArea} itemClassName={topAreaItemClassName} /> - <div className="lc-workspace-workbench-window"> - { - workspace.windows.map(d => ( - <WindowView - active={d.id === workspace.window?.id} - window={d} - key={d.id} - /> - )) - } - - { - workspace.windows.length === 0 && WorkspaceEmptyComponent - ? <WorkspaceEmptyComponent /> - : null - } - </div> - </div> - <MainArea area={skeleton.mainArea} /> - <BottomArea area={skeleton.bottomArea} /> - </div> - </div> - <TipContainer /> - </SkeletonContext.Provider> - </div> - ); - } -} diff --git a/packages/engine/src/workspace/less-variables.less b/packages/engine/src/workspace/less-variables.less deleted file mode 100644 index 017e432ce..000000000 --- a/packages/engine/src/workspace/less-variables.less +++ /dev/null @@ -1,215 +0,0 @@ -/* - * 基础的 DPL 定义使用了 kuma base 的定义,参考: - * https://github.com/uxcore/kuma-base/tree/master/variables - */ - -/** - * =========================================================== - * ==================== Font Family ========================== - * =========================================================== - */ - -/* - * @font-family: "STHeiti", "Microsoft Yahei", "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, Verdana, sans-serif; - */ - -@font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Helvetica, Arial, sans-serif; -@font-family-code: Monaco, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Helvetica, Arial, - sans-serif; - -/** - * =========================================================== - * ===================== Color DPL =========================== - * =========================================================== - */ - -@brand-color-1: rgba(0, 108, 255, 1); -@brand-color-2: rgba(25, 122, 255, 1); -@brand-color-3: rgba(0, 96, 229, 1); - -@brand-color-1-3: rgba(0, 108, 255, 0.6); -@brand-color-1-4: rgba(0, 108, 255, 0.4); -@brand-color-1-5: rgba(0, 108, 255, 0.3); -@brand-color-1-6: rgba(0, 108, 255, 0.2); -@brand-color-1-7: rgba(0, 108, 255, 0.1); - -@brand-color: @brand-color-1; - -@white-alpha-1: rgb(255, 255, 255); // W-1 -@white-alpha-2: rgba(255, 255, 255, 0.8); // W-2 A80 -@white-alpha-3: rgba(255, 255, 255, 0.6); // W-3 A60 -@white-alpha-4: rgba(255, 255, 255, 0.4); // W-4 A40 -@white-alpha-5: rgba(255, 255, 255, 0.3); // W-5 A30 -@white-alpha-6: rgba(255, 255, 255, 0.2); // W-6 A20 -@white-alpha-7: rgba(255, 255, 255, 0.1); // W-7 A10 -@white-alpha-8: rgba(255, 255, 255, 0.06); // W-8 A6 - -@dark-alpha-1: rgba(0, 0, 0, 1); // D-1 A100 -@dark-alpha-2: rgba(0, 0, 0, 0.8); // D-2 A80 -@dark-alpha-3: rgba(0, 0, 0, 0.6); // D-3 A60 -@dark-alpha-4: rgba(0, 0, 0, 0.4); // D-4 A40 -@dark-alpha-5: rgba(0, 0, 0, 0.3); // D-5 A30 -@dark-alpha-6: rgba(0, 0, 0, 0.2); // D-6 A20 -@dark-alpha-7: rgba(0, 0, 0, 0.1); // D-7 A10 -@dark-alpha-8: rgba(0, 0, 0, 0.06); // D-8 A6 -@dark-alpha-9: rgba(0, 0, 0, 0.04); // D-9 A4 - -@normal-alpha-1: rgba(31, 56, 88, 1); // N-1 A100 -@normal-alpha-2: rgba(31, 56, 88, 0.8); // N-2 A80 -@normal-alpha-3: rgba(31, 56, 88, 0.6); // N-3 A60 -@normal-alpha-4: rgba(31, 56, 88, 0.4); // N-4 A40 -@normal-alpha-5: rgba(31, 56, 88, 0.3); // N-5 A30 -@normal-alpha-6: rgba(31, 56, 88, 0.2); // N-6 A20 -@normal-alpha-7: rgba(31, 56, 88, 0.1); // N-7 A10 -@normal-alpha-8: rgba(31, 56, 88, 0.06); // N-8 A6 -@normal-alpha-9: rgba(31, 56, 88, 0.04); // N-9 A4 - -@normal-3: #77879c; -@normal-4: #a3aebd; -@normal-5: #bac3cc; -@normal-6: #d1d7de; - -@gray-dark: #333; // N2_4 -@gray: #666; // N2_3 -@gray-light: #999; // N2_2 -@gray-lighter: #ccc; // N2_1 - -@brand-secondary: #2c2f33; // B2_3 -// 补色 -@brand-complement: #00b3e8; // B3_1 -// 复合 -@brand-comosite: #00c587; // B3_2 -// 浓度 -@brand-deep: #73461d; // B3_3 - -// F1-1 -@brand-danger: rgb(240, 70, 49); -// F1-2 (10% white) -@brand-danger-hover: rgba(240, 70, 49, 0.9); -// F1-3 (5% black) -@brand-danger-focus: rgba(240, 70, 49, 0.95); - -// F2-1 -@brand-warning: rgb(250, 189, 14); -// F3-1 -@brand-success: rgb(102, 188, 92); -// F4-1 -@brand-link: rgb(102, 188, 92); -// F4-2 -@brand-link-hover: #2e76a6; - -// F1-1-7 A10 -@brand-danger-alpha-7: rgba(240, 70, 49, 0.1); -// F1-1-8 A6 -@brand-danger-alpha-8: rgba(240, 70, 49, 0.8); -// F2-1-2 A80 -@brand-warning-alpha-2: rgba(250, 189, 14, 0.8); -// F2-1-7 A10 -@brand-warning-alpha-7: rgba(250, 189, 14, 0.1); -// F3-1-2 A80 -@brand-success-alpha-2: rgba(102, 188, 92, 0.8); -// F3-1-7 A10 -@brand-success-alpha-7: rgba(102, 188, 92, 0.1); -// F4-1-7 A10 -@brand-link-alpha-7: rgba(102, 188, 92, 0.1); - -// 文本色 -@text-primary-color: @dark-alpha-3; -@text-secondary-color: @normal-alpha-3; -@text-thirdary-color: @dark-alpha-4; -@text-disabled-color: @normal-alpha-5; -@text-helper-color: @dark-alpha-4; -@text-danger-color: @brand-danger; -@text-ali-color: #ec6c00; - -/** - * =========================================================== - * =================== Shadow Box ============================ - * =========================================================== - */ - -@box-shadow-1: 0 1px 4px 0 rgba(31, 56, 88, 0.15); // 1 级阴影,物体由原来存在于底面的物体展开,物体和底面关联紧密 -@box-shadow-2: 0 2px 10px 0 rgba(31, 56, 88, 0.15); // 2 级阴影,hover状态,物体层级较高 -@box-shadow-3: 0 4px 15px 0 rgba(31, 56, 88, 0.15); // 3 级阴影,当物体层级高于所有界面元素,弹窗用 - -/** - * =========================================================== - * ================= FontSize of Level ======================= - * =========================================================== - */ - -@fontSize-1: 26px; -@fontSize-2: 20px; -@fontSize-3: 16px; -@fontSize-4: 14px; -@fontSize-5: 12px; - -@fontLineHeight-1: 38px; -@fontLineHeight-2: 30px; -@fontLineHeight-3: 26px; -@fontLineHeight-4: 24px; -@fontLineHeight-5: 20px; - -/** - * =========================================================== - * ================= FontSize of Level ======================= - * =========================================================== - */ - -@global-border-radius: 3px; -@input-border-radius: 3px; -@popup-border-radius: 6px; - -/** - * =========================================================== - * ===================== Transistion ========================= - * =========================================================== - */ - -@transition-duration: 0.3s; -@transition-ease: cubic-bezier(0.23, 1, 0.32, 1); -@transition-delay: 0s; - -/** - * =========================================================== - * ================ Global Configruations ==================== - * =========================================================== - */ - -@topPaneHeight: 48px; -@actionpane-height: 48px; -@tabPaneWidth: 260px; -@input-standard-height: 32px; -@dockpane-width: 48px; - -/** - * =========================================================== - * =================== Deprecated Items ====================== - * =========================================================== - */ - -@head-bgcolor: @white-alpha-1; -@pane-bgcolor: @white-alpha-1; -@pane-dark-bgcolor: @white-alpha-1; -@pane-bdcolor: @normal-4; -@blank-bgcolor: @normal-5; -@title-bgcolor: @white-alpha-1; -@title-bdcolor: transparent; -@section-bgcolor: transparent; -@section-bdcolor: @white-alpha-1; -@button-bgcolor: @white-alpha-1; -@button-bdcolor: transparent; -@button-blue-color: @brand-color; -@button-blue-hover-color: @brand-color; -@sub-title-bgcolor: @white-alpha-1; -@sub-title-bdcolor: transparent; -@text-color: @text-primary-color; -@icon-color: @gray; -@icon-color-active: @gray-light; -@ghost-bgcolor: @dark-alpha-3; -@input-bgcolor: transparent; -@input-bdcolor: @normal-alpha-5; -@hover-color: #5a99cc; -@active-color: #5a99cc; -@disabled-color: #666; -@setter-popup-bg: rgb(80, 86, 109); diff --git a/packages/engine/src/workspace/resource-type.ts b/packages/engine/src/workspace/resource-type.ts deleted file mode 100644 index bb3a533be..000000000 --- a/packages/engine/src/workspace/resource-type.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IPublicTypeResourceType } from '@alilc/lowcode-types'; - -export interface IResourceType extends ResourceType {} - -export class ResourceType implements Omit<IPublicTypeResourceType, 'resourceName' | 'resourceType'> { - constructor(readonly resourceTypeModel: IPublicTypeResourceType) { - } - - get name() { - return this.resourceTypeModel.resourceName; - } - - get type() { - return this.resourceTypeModel.resourceType; - } -} diff --git a/packages/engine/src/workspace/resource.ts b/packages/engine/src/workspace/resource.ts deleted file mode 100644 index 7f166e08b..000000000 --- a/packages/engine/src/workspace/resource.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { IPublicTypeEditorView, IPublicResourceData, IPublicResourceTypeConfig, IBaseModelResource, IPublicEnumPluginRegisterLevel } from '@alilc/lowcode-types'; -import { Logger } from '@alilc/lowcode-utils'; -import { BasicContext, IBasicContext } from './context/base-context'; -import { IResourceType } from './resource-type'; -import { IWorkspace } from './workspace'; - -const logger = new Logger({ level: 'warn', bizName: 'workspace:resource' }); - -export interface IResource extends Resource {} - -export class Resource implements IBaseModelResource<IResource> { - private context: IBasicContext; - - resourceTypeInstance: IPublicResourceTypeConfig; - - editorViewMap: Map<string, IPublicTypeEditorView> = new Map<string, IPublicTypeEditorView>(); - - get name() { - return this.resourceType.name; - } - - get viewName() { - return this.resourceData.viewName || (this.resourceData as any).viewType || this.defaultViewName; - } - - get description() { - return this.resourceTypeInstance?.description; - } - - get icon() { - return this.resourceData.icon || this.resourceTypeInstance?.icon; - } - - get type() { - return this.resourceType.type; - } - - get title(): string | undefined { - return this.resourceData.title || this.resourceTypeInstance.defaultTitle; - } - - get id(): string | undefined { - return this.resourceData.id; - } - - get options() { - return this.resourceData.options; - } - - get category() { - return this.resourceData?.category; - } - - get skeleton() { - return this.context.innerSkeleton; - } - - children: IResource[]; - - get config() { - return this.resourceData.config; - } - - constructor(readonly resourceData: IPublicResourceData, readonly resourceType: IResourceType, readonly workspace: IWorkspace) { - this.context = new BasicContext(workspace, `resource-${resourceData.resourceName || resourceType.name}`, IPublicEnumPluginRegisterLevel.Resource); - this.resourceTypeInstance = resourceType.resourceTypeModel(this.context.innerPlugins._getLowCodePluginContext({ - pluginName: '', - }), this.options); - this.init(); - if (this.resourceTypeInstance.editorViews) { - this.resourceTypeInstance.editorViews.forEach((d: any) => { - this.editorViewMap.set(d.viewName, d); - }); - } - if (!resourceType) { - logger.error(`resourceType[${resourceType}] is unValid.`); - } - this.children = this.resourceData?.children?.map(d => new Resource(d, this.workspace.getResourceType(d.resourceName || this.resourceType.name), this.workspace)) || []; - } - - async init() { - await this.resourceTypeInstance.init?.(); - await this.context.innerPlugins.init(); - } - - async import(schema: any) { - return await this.resourceTypeInstance.import?.(schema); - } - - async url() { - return await this.resourceTypeInstance.url?.(); - } - - async save(value: any) { - return await this.resourceTypeInstance.save?.(value); - } - - get editorViews() { - return this.resourceTypeInstance.editorViews; - } - - get defaultViewName() { - return this.resourceTypeInstance.defaultViewName || this.resourceTypeInstance.defaultViewType; - } - - getEditorView(name: string) { - return this.editorViewMap.get(name); - } -} diff --git a/packages/engine/src/workspace/skeleton-context.ts b/packages/engine/src/workspace/skeleton-context.ts deleted file mode 100644 index 781ba9ae8..000000000 --- a/packages/engine/src/workspace/skeleton-context.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from 'react'; - -export const SkeletonContext = createContext<any>({} as any); diff --git a/packages/engine/src/workspace/view/editor-view.tsx b/packages/engine/src/workspace/view/editor-view.tsx deleted file mode 100644 index 7ada5c911..000000000 --- a/packages/engine/src/workspace/view/editor-view.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { BuiltinLoading } from '@alilc/lowcode-designer'; -import { engineConfig, observer } from '@alilc/lowcode-editor-core'; -import { - Workbench, -} from '@alilc/lowcode-editor-skeleton'; -import { PureComponent } from 'react'; -import { Context } from '../context/view-context'; - -export * from '../context/base-context'; - -@observer -export class EditorView extends PureComponent<{ - editorView: Context; - active: boolean; -}, any> { - render() { - const { active } = this.props; - const editorView = this.props.editorView; - const skeleton = editorView.innerSkeleton; - if (!editorView.isInit) { - const Loading = engineConfig.get('loadingComponent', BuiltinLoading); - return <Loading />; - } - - return ( - <Workbench - skeleton={skeleton} - className={active ? 'active engine-editor-view' : 'engine-editor-view'} - topAreaItemClassName="engine-actionitem" - /> - ); - } -} diff --git a/packages/engine/src/workspace/view/resource-view.less b/packages/engine/src/workspace/view/resource-view.less deleted file mode 100644 index 4c281f8d8..000000000 --- a/packages/engine/src/workspace/view/resource-view.less +++ /dev/null @@ -1,14 +0,0 @@ -.workspace-resource-view { - display: flex; - position: absolute; - flex-direction: column; - top: 0; - bottom: 0; - left: 0; - right: 0; -} - -.workspace-editor-body { - position: relative; - height: 100%; -} \ No newline at end of file diff --git a/packages/engine/src/workspace/view/resource-view.tsx b/packages/engine/src/workspace/view/resource-view.tsx deleted file mode 100644 index 07c666a5c..000000000 --- a/packages/engine/src/workspace/view/resource-view.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { PureComponent } from 'react'; -import { EditorView } from './editor-view'; -import { observer } from '@alilc/lowcode-editor-core'; -import { IResource } from '../resource'; -import { IEditorWindow } from '../window'; -import './resource-view.less'; -import { TopArea } from '@alilc/lowcode-editor-skeleton'; - -@observer -export class ResourceView extends PureComponent<{ - window: IEditorWindow; - resource: IResource; -}, any> { - render() { - const { skeleton } = this.props.resource; - const { editorViews } = this.props.window; - return ( - <div className="workspace-resource-view"> - <TopArea area={skeleton.topArea} itemClassName="engine-actionitem" /> - <div className="workspace-editor-body"> - { - Array.from(editorViews.values()).map((editorView: any) => { - return ( - <EditorView - key={editorView.name} - active={editorView.active} - editorView={editorView} - /> - ); - }) - } - </div> - </div> - ); - } -} diff --git a/packages/engine/src/workspace/view/window-view.tsx b/packages/engine/src/workspace/view/window-view.tsx deleted file mode 100644 index 69c98634f..000000000 --- a/packages/engine/src/workspace/view/window-view.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { PureComponent } from 'react'; -import { ResourceView } from './resource-view'; -import { engineConfig, observer } from '@alilc/lowcode-editor-core'; -import { IEditorWindow } from '../window'; -import { BuiltinLoading } from '@alilc/lowcode-designer'; -import { DesignerView } from '../inner-plugins/webview'; - -@observer -export class WindowView extends PureComponent<{ - window: IEditorWindow; - active: boolean; -}, any> { - render() { - const { active } = this.props; - const { resource, initReady, url } = this.props.window; - - if (!initReady) { - const Loading = engineConfig.get('loadingComponent', BuiltinLoading); - return ( - <div className={`workspace-engine-main ${active ? 'active' : ''}`}> - <Loading /> - </div> - ); - } - - if (resource.type === 'webview' && url) { - return <DesignerView url={url} viewName={resource.name} />; - } - - return ( - <div className={`workspace-engine-main ${active ? 'active' : ''}`}> - <ResourceView - resource={resource} - window={this.props.window} - /> - </div> - ); - } -} diff --git a/packages/engine/src/workspace/window.ts b/packages/engine/src/workspace/window.ts deleted file mode 100644 index 56b26f7ca..000000000 --- a/packages/engine/src/workspace/window.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { uniqueId } from '@alilc/lowcode-utils'; -import { createModuleEventBus, IEventBus, makeObservable, observable } from '@alilc/lowcode-editor-core'; -import { Context } from './context/view-context'; -import { IWorkspace } from './workspace'; -import { IResource } from './resource'; -import { IPublicModelWindow, IPublicTypeDisposable } from '@alilc/lowcode-types'; - -interface IWindowCOnfig { - title: string | undefined; - options?: Object; - viewName?: string | undefined; - sleep?: boolean; -} - -export interface IEditorWindow extends EditorWindow {} - -export enum WINDOW_STATE { - // 睡眠 - sleep = 'sleep', - - // 激活 - active = 'active', - - // 未激活 - inactive = 'inactive', - - // 销毁 - destroyed = 'destroyed' -} - -export class EditorWindow implements Omit<IPublicModelWindow<IResource>, 'changeViewType' | 'currentEditorView' | 'editorViews'> { - id: string = uniqueId('window'); - icon: React.ReactElement | undefined; - - private emitter: IEventBus = createModuleEventBus('Project'); - - title: string | undefined; - - url: string | undefined; - - @observable.ref _editorView: Context; - - @observable editorViews: Map<string, Context> = new Map<string, Context>(); - - @observable initReady = false; - - sleep: boolean | undefined; - - get editorView() { - if (!this._editorView) { - return this.editorViews.values().next().value; - } - return this._editorView; - } - - constructor(readonly resource: IResource, readonly workspace: IWorkspace, private config: IWindowCOnfig) { - makeObservable(this); - this.title = config.title; - this.icon = resource.icon as any; - this.sleep = config.sleep; - if (config.sleep) { - this.updateState(WINDOW_STATE.sleep); - } - } - - updateState(state: WINDOW_STATE): void { - switch (state) { - case WINDOW_STATE.active: - this._editorView?.setActivate(true); - break; - case WINDOW_STATE.inactive: - this._editorView?.setActivate(false); - break; - case WINDOW_STATE.destroyed: - break; - } - } - - async importSchema(schema: any) { - const newSchema = await this.resource.import(schema); - - if (!newSchema) { - return; - } - - Object.keys(newSchema).forEach(key => { - const view = this.editorViews.get(key); - view?.project.importSchema(newSchema[key]); - }); - } - - async save() { - const value: any = {}; - const editorViews = this.resource.editorViews; - if (!editorViews) { - return; - } - for (let i = 0; i < editorViews.length; i++) { - const name = editorViews[i].viewName; - const saveResult = await this.editorViews.get(name)?.save(); - value[name] = saveResult; - } - const result = await this.resource.save(value); - this.emitter.emit('handle.save'); - - return result; - } - - onSave(fn: () => void) { - this.emitter.on('handle.save', fn); - - return () => { - this.emitter.off('handle.save', fn); - }; - } - - async init() { - await this.initViewTypes(); - await this.execViewTypesInit(); - Promise.all(Array.from(this.editorViews.values()).map((d) => d.onSimulatorRendererReady())) - .then(() => { - this.workspace.emitWindowRendererReady(); - }); - this.url = await this.resource.url(); - this.setDefaultViewName(); - this.initReady = true; - this.workspace.checkWindowQueue(); - this.sleep = false; - this.updateState(WINDOW_STATE.active); - } - - initViewTypes = async () => { - const editorViews = this.resource.editorViews; - if (!editorViews) { - return; - } - for (let i = 0; i < editorViews.length; i++) { - const name = editorViews[i].viewName; - await this.initViewType(name); - if (!this._editorView) { - this.changeViewName(name); - } - } - }; - - onChangeViewType(fn: (viewName: string) => void): IPublicTypeDisposable { - this.emitter.on('window.change.view.type', fn); - - return () => { - this.emitter.off('window.change.view.type', fn); - }; - } - - execViewTypesInit = async () => { - const editorViews = this.resource.editorViews; - if (!editorViews) { - return; - } - for (let i = 0; i < editorViews.length; i++) { - const name = editorViews[i].viewName; - this.changeViewName(name); - await this.editorViews.get(name)?.init(); - } - }; - - setDefaultViewName = () => { - this.changeViewName(this.config.viewName ?? this.resource.defaultViewName!); - }; - - get resourceType() { - return this.resource.resourceType.type; - } - - initViewType = async (name: string) => { - const viewInfo = this.resource.getEditorView(name); - if (this.editorViews.get(name)) { - return; - } - const editorView = new Context(this.workspace, this, viewInfo as any, this.config.options); - this.editorViews.set(name, editorView); - }; - - changeViewName = (name: string, ignoreEmit: boolean = true) => { - this._editorView?.setActivate(false); - this._editorView = this.editorViews.get(name)!; - - if (!this._editorView) { - return; - } - - this._editorView.setActivate(true); - - if (!ignoreEmit) { - this.emitter.emit('window.change.view.type', name); - - if (this.id === this.workspace.window.id) { - this.workspace.emitChangeActiveEditorView(); - } - } - }; - - get project() { - return this.editorView?.project; - } - - get innerProject() { - return this.editorView?.innerProject; - } - - get innerSkeleton() { - return this.editorView?.innerSkeleton; - } - - get innerSetters() { - return this.editorView?.innerSetters; - } - - get innerHotkey() { - return this.editorView?.innerHotkey; - } - - get editor() { - return this.editorView?.editor; - } - - get designer() { - return this.editorView?.designer; - } - - get plugins() { - return this.editorView?.plugins; - } - - get innerPlugins() { - return this.editorView?.innerPlugins; - } -} diff --git a/packages/engine/src/workspace/workspace.ts b/packages/engine/src/workspace/workspace.ts deleted file mode 100644 index d9fadb821..000000000 --- a/packages/engine/src/workspace/workspace.ts +++ /dev/null @@ -1,340 +0,0 @@ -import { IDesigner, LowCodePluginManager } from '@alilc/lowcode-designer'; -import { createModuleEventBus, IEditor, IEventBus, makeObservable, observable } from '@alilc/lowcode-editor-core'; -import { IPublicApiPlugins, IPublicApiWorkspace, IPublicEnumPluginRegisterLevel, IPublicResourceList, IPublicTypeDisposable, IPublicTypeResourceType } from '@alilc/lowcode-types'; -import { BasicContext } from './context/base-context'; -import { EditorWindow, WINDOW_STATE } from './window'; -import type { IEditorWindow } from './window'; -import { IResource, Resource } from './resource'; -import { IResourceType, ResourceType } from './resource-type'; -import { ISkeleton } from '@alilc/lowcode-editor-skeleton'; - -enum EVENT { - CHANGE_WINDOW = 'change_window', - - CHANGE_ACTIVE_WINDOW = 'change_active_window', - - WINDOW_RENDER_READY = 'window_render_ready', - - CHANGE_ACTIVE_EDITOR_VIEW = 'change_active_editor_view', -} - -const CHANGE_EVENT = 'resource.list.change'; - -export class Workspace implements Omit<IPublicApiWorkspace< - LowCodePluginManager, - ISkeleton, - IEditorWindow ->, 'resourceList' | 'plugins' | 'openEditorWindow' | 'removeEditorWindow'> { - context: BasicContext; - - enableAutoOpenFirstWindow: boolean; - - resourceTypeMap: Map<string, ResourceType> = new Map(); - - private emitter: IEventBus = createModuleEventBus('workspace'); - - private _isActive = false; - - private resourceList: IResource[] = []; - - get skeleton() { - return this.context.innerSkeleton; - } - - get plugins() { - return this.context.innerPlugins; - } - - get isActive() { - return this._isActive; - } - - get defaultResourceType(): ResourceType | null { - if (this.resourceTypeMap.size >= 1) { - return Array.from(this.resourceTypeMap.values())[0]; - } - - return null; - } - - @observable.ref windows: IEditorWindow[] = []; - - editorWindowMap: Map<string, IEditorWindow> = new Map<string, IEditorWindow>(); - - @observable.ref window: IEditorWindow; - - windowQueue: ({ - name: string; - title: string; - options: Object; - viewName?: string; - } | IResource)[] = []; - - constructor( - readonly registryInnerPlugin: ( - designer: IDesigner, - editor: IEditor, - plugins: IPublicApiPlugins - ) => Promise<IPublicTypeDisposable>, - readonly shellModelFactory: any, - ) { - this.context = new BasicContext(this, '', IPublicEnumPluginRegisterLevel.Workspace); - this.context.innerHotkey.activate(true); - makeObservable(this); - } - - checkWindowQueue() { - if (!this.windowQueue || !this.windowQueue.length) { - return; - } - - const windowInfo = this.windowQueue.shift(); - if (windowInfo instanceof Resource) { - this.openEditorWindowByResource(windowInfo); - } else if (windowInfo) { - this.openEditorWindow(windowInfo.name, windowInfo.title, windowInfo.options, windowInfo.viewName); - } - } - - async initWindow() { - if (!this.defaultResourceType || this.enableAutoOpenFirstWindow === false) { - return; - } - const resourceName = this.defaultResourceType.name; - const resource = new Resource({ - resourceName, - options: {}, - }, this.defaultResourceType, this); - this.window = new EditorWindow(resource, this, { - title: resource.title, - }); - await this.window.init(); - this.editorWindowMap.set(this.window.id, this.window); - this.windows = [...this.windows, this.window]; - this.emitChangeWindow(); - this.emitChangeActiveWindow(); - } - - setActive(value: boolean) { - this._isActive = value; - } - - async registerResourceType(resourceTypeModel: IPublicTypeResourceType): Promise<void> { - const resourceType = new ResourceType(resourceTypeModel); - this.resourceTypeMap.set(resourceTypeModel.resourceName, resourceType); - - if (!this.window && this.defaultResourceType && this._isActive) { - this.initWindow(); - } - } - - getResourceList() { - return this.resourceList; - } - - setResourceList(resourceList: IPublicResourceList) { - this.resourceList = resourceList.map(d => new Resource(d, this.getResourceType(d.resourceName), this)); - this.emitter.emit(CHANGE_EVENT, resourceList); - } - - onResourceListChange(fn: (resourceList: IPublicResourceList) => void): () => void { - this.emitter.on(CHANGE_EVENT, fn); - return () => { - this.emitter.off(CHANGE_EVENT, fn); - }; - } - - onWindowRendererReady(fn: () => void): IPublicTypeDisposable { - this.emitter.on(EVENT.WINDOW_RENDER_READY, fn); - return () => { - this.emitter.off(EVENT.WINDOW_RENDER_READY, fn); - }; - } - - emitWindowRendererReady() { - this.emitter.emit(EVENT.WINDOW_RENDER_READY); - } - - getResourceType(resourceName: string): IResourceType { - return this.resourceTypeMap.get(resourceName)!; - } - - removeResourceType(resourceName: string) { - if (this.resourceTypeMap.has(resourceName)) { - this.resourceTypeMap.delete(resourceName); - } - } - - removeEditorWindowById(id: string) { - const index = this.windows.findIndex(d => (d.id === id)); - this.remove(index); - } - - private async remove(index: number) { - if (index < 0) { - return; - } - const window = this.windows[index]; - this.windows.splice(index, 1); - this.window?.updateState(WINDOW_STATE.destroyed); - if (this.window === window) { - this.window = this.windows[index] || this.windows[index + 1] || this.windows[index - 1]; - if (this.window?.sleep) { - await this.window.init(); - } - this.emitChangeActiveWindow(); - } - this.emitChangeWindow(); - this.window?.updateState(WINDOW_STATE.active); - } - - removeEditorWindow(resourceName: string, id: string) { - const index = this.windows.findIndex(d => (d.resource?.name === resourceName && (d.title === id || d.resource.id === id))); - this.remove(index); - } - - removeEditorWindowByResource(resource: IResource) { - const index = this.windows.findIndex(d => (d.resource?.id === resource.id)); - this.remove(index); - } - - async openEditorWindowById(id: string) { - const window = this.editorWindowMap.get(id); - this.window?.updateState(WINDOW_STATE.inactive); - if (window) { - this.window = window; - if (window.sleep) { - await window.init(); - } - this.emitChangeActiveWindow(); - } - this.window?.updateState(WINDOW_STATE.active); - } - - async openEditorWindowByResource(resource: IResource, sleep: boolean = false): Promise<void> { - if (this.window && !this.window.sleep && !this.window?.initReady && !sleep) { - this.windowQueue.push(resource); - return; - } - - this.window?.updateState(WINDOW_STATE.inactive); - - const filterWindows = this.windows.filter(d => (d.resource?.id === resource.id)); - if (filterWindows && filterWindows.length) { - this.window = filterWindows[0]; - if (!sleep && this.window.sleep) { - await this.window.init(); - } else { - this.checkWindowQueue(); - } - this.emitChangeActiveWindow(); - this.window?.updateState(WINDOW_STATE.active); - return; - } - - const window = new EditorWindow(resource, this, { - title: resource.title, - options: resource.options, - viewName: resource.viewName, - sleep, - }); - - this.windows = [...this.windows, window]; - this.editorWindowMap.set(window.id, window); - if (sleep) { - this.emitChangeWindow(); - return; - } - this.window = window; - await this.window.init(); - this.emitChangeWindow(); - this.emitChangeActiveWindow(); - this.window?.updateState(WINDOW_STATE.active); - } - - async openEditorWindow(name: string, title: string, options: Object, viewName?: string, sleep?: boolean) { - if (this.window && !this.window.sleep && !this.window?.initReady && !sleep) { - this.windowQueue.push({ - name, title, options, viewName, - }); - return; - } - const resourceType = this.resourceTypeMap.get(name); - if (!resourceType) { - console.error(`${name} resourceType is not available`); - return; - } - this.window?.updateState(WINDOW_STATE.inactive); - const filterWindows = this.windows.filter(d => (d.resource?.name === name && d.resource.title == title) || (d.resource.id == title)); - if (filterWindows && filterWindows.length) { - this.window = filterWindows[0]; - if (!sleep && this.window.sleep) { - await this.window.init(); - } else { - this.checkWindowQueue(); - } - this.emitChangeActiveWindow(); - this.window?.updateState(WINDOW_STATE.active); - return; - } - const resource = new Resource({ - resourceName: name, - title, - options, - id: title?.toString(), - }, resourceType, this); - const window = new EditorWindow(resource, this, { - title, - options, - viewName, - sleep, - }); - this.windows = [...this.windows, window]; - this.editorWindowMap.set(window.id, window); - if (sleep) { - this.emitChangeWindow(); - return; - } - this.window = window; - await this.window.init(); - this.emitChangeWindow(); - this.emitChangeActiveWindow(); - this.window?.updateState(WINDOW_STATE.active); - } - - onChangeWindows(fn: () => void) { - this.emitter.on(EVENT.CHANGE_WINDOW, fn); - return () => { - this.emitter.removeListener(EVENT.CHANGE_WINDOW, fn); - }; - } - - onChangeActiveEditorView(fn: () => void) { - this.emitter.on(EVENT.CHANGE_ACTIVE_EDITOR_VIEW, fn); - return () => { - this.emitter.removeListener(EVENT.CHANGE_ACTIVE_EDITOR_VIEW, fn); - }; - } - - emitChangeActiveEditorView() { - this.emitter.emit(EVENT.CHANGE_ACTIVE_EDITOR_VIEW); - } - - emitChangeWindow() { - this.emitter.emit(EVENT.CHANGE_WINDOW); - } - - emitChangeActiveWindow() { - this.emitter.emit(EVENT.CHANGE_ACTIVE_WINDOW); - this.emitChangeActiveEditorView(); - } - - onChangeActiveWindow(fn: () => void) { - this.emitter.on(EVENT.CHANGE_ACTIVE_WINDOW, fn); - return () => { - this.emitter.removeListener(EVENT.CHANGE_ACTIVE_WINDOW, fn); - }; - } -} - -export interface IWorkspace extends Workspace {} diff --git a/packages/plugin-designer/package.json b/packages/plugin-designer/package.json index 14d82f02a..e6a8fcca3 100644 --- a/packages/plugin-designer/package.json +++ b/packages/plugin-designer/package.json @@ -35,7 +35,7 @@ "author": "xiayang.xy", "dependencies": { "@alilc/lowcode-designer": "workspace:*", - "@alilc/lowcode-editor-core": "workspace:*", + "@alilc/lowcode-core": "workspace:*", "@alilc/lowcode-utils": "workspace:*", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -48,7 +48,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "@alilc/lowcode-designer": "workspace:*", - "@alilc/lowcode-editor-core": "workspace:*", + "@alilc/lowcode-core": "workspace:*", "@alilc/lowcode-utils": "workspace:*" }, "publishConfig": { diff --git a/packages/plugin-outline-pane/package.json b/packages/plugin-outline-pane/package.json index c2a96e38f..d7493b9c4 100644 --- a/packages/plugin-outline-pane/package.json +++ b/packages/plugin-outline-pane/package.json @@ -30,7 +30,7 @@ }, "dependencies": { "@alifd/next": "^1.27.8", - "@alilc/lowcode-editor-core": "workspace:*", + "@alilc/lowcode-core": "workspace:*", "@alilc/lowcode-types": "workspace:*", "@alilc/lowcode-utils": "workspace:*", "classnames": "^2.5.1", @@ -45,7 +45,7 @@ }, "peerDependencies": { "@alifd/next": "^1.27.8", - "@alilc/lowcode-editor-core": "workspace:*", + "@alilc/lowcode-core": "workspace:*", "@alilc/lowcode-types": "workspace:*", "@alilc/lowcode-utils": "workspace:*", "react": "^18.2.0", diff --git a/packages/react-renderer/package.json b/packages/react-renderer/package.json index 13a9f3c19..27f8ad0f3 100644 --- a/packages/react-renderer/package.json +++ b/packages/react-renderer/package.json @@ -22,6 +22,7 @@ "test": "vitest" }, "dependencies": { + "@alilc/lowcode-shared": "workspace:*", "@alilc/lowcode-renderer-core": "workspace:*", "@alilc/lowcode-renderer-router": "workspace:*", "@vue/reactivity": "^3.4.21", diff --git a/packages/react-renderer/src/component.tsx b/packages/react-renderer/src/component.tsx index 70a17484e..6fc51ea24 100644 --- a/packages/react-renderer/src/component.tsx +++ b/packages/react-renderer/src/component.tsx @@ -9,30 +9,29 @@ import { isJSFunction, isJSSlot, someValue, + type CreateComponentBaseOptions, + type CodeRuntime, } from '@alilc/lowcode-renderer-core'; import { isPlainObject } from 'lodash-es'; import { forwardRef, useRef, useEffect, createElement, useMemo } from 'react'; -import { createSignal, watch } from './signals'; +import { signal, watch } from './signals'; import { appendExternalStyle } from './utils/element'; import { reactive } from './utils/reactive'; import type { - CreateComponentBaseOptions, PlainObject, InstanceStateApi, LowCodeComponent as LowCodeComponentSchema, - CodeRuntime, IntlApi, JSSlot, JSFunction, I18nNode, -} from '@alilc/lowcode-renderer-core'; +} from '@alilc/lowcode-shared'; import type { ComponentType, ReactInstance, CSSProperties, ForwardedRef, - ReactNode, ReactElement, } from 'react'; @@ -167,14 +166,16 @@ export const createComponent = createComponentFunction< (node) => isJSFunction(node) || isJSSlot(node), (node: JSSlot | JSFunction) => { if (isJSSlot(node)) { - if (node.value) { + const slot = node as JSSlot; + + if (slot.value) { const widgets = (Array.isArray(node.value) ? node.value : [node.value]).map( (v) => new ComponentWidget<ComponentType<any>>(v), ); - if (node.params?.length) { + if (slot.params?.length) { return (...args: any[]) => { - const params = node.params!.reduce((prev, cur, idx) => { + const params = slot.params!.reduce((prev, cur, idx) => { return (prev[cur] = args[idx]); }, {} as PlainObject); const subCodeScope = codeRuntime.getScope().createSubScope(params); @@ -323,8 +324,14 @@ export const createComponent = createComponentFunction< ref: ForwardedRef<any>, ) { const { id, className, style } = props; + const isConstructed = useRef(false); const isMounted = useRef(false); + if (!isConstructed.current) { + container.triggerLifeCycle('constructor'); + isConstructed.current = true; + } + useEffect(() => { const scopeValue = container.codeRuntime.getScope().value; @@ -398,7 +405,7 @@ export const createComponent = createComponentFunction< }); function reactiveStateCreator(initState: PlainObject): InstanceStateApi { - const proxyState = createSignal(initState); + const proxyState = signal(initState); return { get state() { diff --git a/packages/react-renderer/src/runtime-api/intl/index.tsx b/packages/react-renderer/src/runtime-api/intl/index.tsx index af8c8956e..742dba5c3 100644 --- a/packages/react-renderer/src/runtime-api/intl/index.tsx +++ b/packages/react-renderer/src/runtime-api/intl/index.tsx @@ -1,12 +1,12 @@ import { parse, compile } from './parser'; -import { createSignal, computed } from '../../signals'; +import { signal, computed } from '../../signals'; export function createIntl( messages: Record<string, Record<string, string>>, defaultLocale: string, ) { - const allMessages = createSignal(messages); - const currentLocale = createSignal(defaultLocale); + const allMessages = signal(messages); + const currentLocale = signal(defaultLocale); const currentMessages = computed(() => allMessages.value[currentLocale.value]); return { diff --git a/packages/react-renderer/src/signals.ts b/packages/react-renderer/src/signals.ts index b6287ffa2..7f541fd8c 100644 --- a/packages/react-renderer/src/signals.ts +++ b/packages/react-renderer/src/signals.ts @@ -13,7 +13,7 @@ import { } from '@vue/reactivity'; import { noop, isObject, isPlainObject, isSet, isMap } from 'lodash-es'; -export { ref as createSignal, computed, effect }; +export { ref as signal, computed, effect }; export type { Ref as Signal, ComputedRef as ComputedSignal }; const INITIAL_WATCHER_VALUE = {}; diff --git a/packages/renderer-core/src/container.ts b/packages/renderer-core/src/container.ts index ad1292d5c..35953eaab 100644 --- a/packages/renderer-core/src/container.ts +++ b/packages/renderer-core/src/container.ts @@ -4,7 +4,7 @@ import type { ComponentTree, InstanceDataSourceApi, InstanceStateApi, -} from './types'; +} from '@alilc/lowcode-shared'; import { type CodeScope, type CodeRuntime, createCodeRuntime, createScope } from './code-runtime'; import { isJSFunction } from './utils/type-guard'; import { type TextWidget, type ComponentWidget, createWidget } from './widget'; diff --git a/packages/shared/__tests__/helper/hookable.spec.ts b/packages/shared/__tests__/helper/hookable.spec.ts new file mode 100644 index 000000000..cb3b532b0 --- /dev/null +++ b/packages/shared/__tests__/helper/hookable.spec.ts @@ -0,0 +1,23 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { EventEmitter } from '../../src'; + +describe('hookable', () => { + let eventEmitter: EventEmitter; + + beforeEach(() => { + eventEmitter = new EventEmitter(); + }); + + it('on', async () => { + const spy = vi.fn(); + eventEmitter.on('test', spy); + await eventEmitter.emit('test'); + + expect(spy).toBeCalled(); + }); + + it('prependListener', () => { + // const spy = vi.fn(); + // expect(spy).toC + }); +}); diff --git a/packages/shared/package.json b/packages/shared/package.json index b1754d5a7..fa19854a1 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,8 +1,17 @@ { "name": "@alilc/lowcode-shared", "version": "2.0.0-beta.0", + "private": true, "type": "module", - "main": "dist/low-code-shared.cjs", - "module": "dist/low-code-shared.js", - "types": "dist/index.d.ts" + "module": "src/index.ts", + "dependencies": { + "@vue/reactivity": "^3.4.23", + "hookable": "^5.5.3", + "lodash-es": "^4.17.21", + "store": "^2.0.12" + }, + "devDependencies": { + "@types/lodash-es": "^4.17.12", + "@types/store": "^2.0.2" + } } diff --git a/packages/shared/src/helper/index.ts b/packages/shared/src/helper/index.ts deleted file mode 100644 index 2edf7455d..000000000 --- a/packages/shared/src/helper/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './invariant'; -export * from './logger'; diff --git a/packages/shared/src/helper/invariant.ts b/packages/shared/src/helper/invariant.ts deleted file mode 100644 index b3c3b422b..000000000 --- a/packages/shared/src/helper/invariant.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function invariant(check: any, message: string, thing?: any) { - if (!check) { - throw new Error(`[designer] Invariant failed: ${message}${thing ? ` in '${thing}'` : ''}`); - } -} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 28f3a81f4..7772bb8e8 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,2 +1,4 @@ export * from './types'; -export * from './helper'; +export * from './utils'; +export * from './signals'; +export * from './parts'; diff --git a/packages/shared/src/parts/event.ts b/packages/shared/src/parts/event.ts new file mode 100644 index 000000000..3f2261b53 --- /dev/null +++ b/packages/shared/src/parts/event.ts @@ -0,0 +1,74 @@ +import { Hookable, type HookKeys, type HookCallback } from 'hookable'; + +export type EventListener = HookCallback; +export type EventDisposable = () => void; + +/** + * todo: logger + */ +export class EventEmitter< + HooksT extends Record<string, any> = Record<string, HookCallback>, + HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>, +> extends Hookable<HooksT, HookNameT> { + #namespace: string | undefined; + + constructor(namespace?: string) { + super(); + this.#namespace = namespace; + } + + /** + * 监听事件 + * add monitor to a event + * @param event 事件名称 + * @param listener 事件回调 + */ + on(event: HookNameT, listener: HooksT[HookNameT]): EventDisposable { + return this.hook(event, listener); + } + + /** + * 触发事件 + * emit a message for a event + * @param event 事件名称 + * @param args 事件参数 + */ + async emit(event: HookNameT, ...args: any) { + return this.callHook(event, ...args); + } + + /** + * 取消监听事件 + * cancel a monitor from a event + * @param event 事件名称 + * @param listener 事件回调 + */ + off(event: HookNameT, listener: HooksT[HookNameT]): void { + this.removeHook(event, listener); + } + + /** + * 监听事件,会在其他回调函数之前执行 + * @param event 事件名称 + * @param listener 事件回调 + */ + prependListener(event: HookNameT, listener: HooksT[HookNameT]): EventDisposable { + const _hooks = (this as any)._hooks; + const hooks = _hooks[event]; + + if (Array.isArray(hooks)) { + hooks.unshift(listener); + return () => { + if (listener) { + this.removeHook(event, listener); + } + }; + } else { + return this.hook(event, listener); + } + } +} + +export function createEventBus<T extends Record<string, any>>(namespace?: string): EventBus<T> { + return new EventBus<T>(namespace); +} diff --git a/packages/shared/src/parts/index.ts b/packages/shared/src/parts/index.ts new file mode 100644 index 000000000..c8e07139d --- /dev/null +++ b/packages/shared/src/parts/index.ts @@ -0,0 +1,3 @@ +export * from './event'; +export * from './logger'; +export * from './persistence'; diff --git a/packages/shared/src/helper/logger.ts b/packages/shared/src/parts/logger.ts similarity index 93% rename from packages/shared/src/helper/logger.ts rename to packages/shared/src/parts/logger.ts index a08d8d9a0..58b166180 100644 --- a/packages/shared/src/helper/logger.ts +++ b/packages/shared/src/parts/logger.ts @@ -1,4 +1,4 @@ -import { isObject } from '../../../utils/src/is-object'; +import { isObject } from 'lodash-es'; export type Level = 'debug' | 'log' | 'info' | 'warn' | 'error'; @@ -146,43 +146,52 @@ export class Logger { bizName: string; targetBizName: string; targetLevel: string; + constructor(options: LoggerOptions) { options = { ...defaultOptions, ...options }; - const _location = location || {} as any; + // __logConf__ 格式为 logLevel[:bizName], bizName is used as: targetBizName like '%bizName%' // 1. __logConf__=log or __logConf__=warn, etc. // 2. __logConf__=log:* or __logConf__=warn:*, etc. // 2. __logConf__=log:bizName or __logConf__=warn:partOfBizName, etc. - const logConf = (((/__(?:logConf|logLevel)__=([^#/&]*)/.exec(_location.href)) || [])[1]); + const _location = location || ({} as any); + const logConf = (/__(?:logConf|logLevel)__=([^#/&]*)/.exec(_location.href) || [])[1]; + const targetOptions = parseLogConf(logConf, options); + this.bizName = options.bizName; this.targetBizName = targetOptions.bizName; this.targetLevel = targetOptions.level; } + debug(...args: any[]): void { if (!shouldOutput('debug', this.targetLevel, this.bizName, this.targetBizName)) { return; } return output('debug', this.bizName)(args); } + log(...args: any[]): void { if (!shouldOutput('log', this.targetLevel, this.bizName, this.targetBizName)) { return; } return output('log', this.bizName)(args); } + info(...args: any[]): void { if (!shouldOutput('info', this.targetLevel, this.bizName, this.targetBizName)) { return; } return output('info', this.bizName)(args); } + warn(...args: any[]): void { if (!shouldOutput('warn', this.targetLevel, this.bizName, this.targetBizName)) { return; } return output('warn', this.bizName)(args); } + error(...args: any[]): void { if (!shouldOutput('error', this.targetLevel, this.bizName, this.targetBizName)) { return; @@ -191,6 +200,6 @@ export class Logger { } } -export function createLogger(config: { level: Level; bizName: string }): Logger { - return new Logger(config); +export function createLogger(options: LoggerOptions) { + return new Logger(options); } diff --git a/packages/shared/src/parts/persistence.ts b/packages/shared/src/parts/persistence.ts new file mode 100644 index 000000000..c44d7b8d5 --- /dev/null +++ b/packages/shared/src/parts/persistence.ts @@ -0,0 +1,41 @@ +import { createStore } from 'store'; + +export type StorageValue = string | boolean | number | undefined | null | object; + +export interface IPersistence { + get(key: string, fallbackValue: string): string; + get(key: string, fallbackValue?: string): string | undefined; + + set(key: string, value: StorageValue): void; + + delete(key: string): void; + + clear(): void; +} + +export class PersistenceStore implements IPersistence { + #store: ReturnType<typeof createStore>; + + constructor(namespace?: string) { + this.#store = store.createStore([], namespace); + } + + get(key: string, fallbackValue: string): string; + get(key: string, fallbackValue?: string | undefined): string | undefined; + get(key: string, fallbackValue?: unknown): string | undefined { + const value = store.get(key, fallbackValue); + return value; + } + + set(key: string, value: StorageValue): void { + this.#store.set(key, value); + } + + delete(key: string): void { + this.#store.remove(key); + } + + clear(): void { + this.#store.clearAll(); + } +} diff --git a/packages/shared/src/signals.ts b/packages/shared/src/signals.ts new file mode 100644 index 000000000..ff0e4c1b8 --- /dev/null +++ b/packages/shared/src/signals.ts @@ -0,0 +1,333 @@ +/** + * signal apis refs from tc39 + * https://github.com/tc39/template-for-proposals + * https://github.com/tc39/proposal-signals + */ + +import { AnyFunction, type PlainObject } from './types'; +import { + ref, + computed, + ReactiveEffect, + type ComputedRef, + type Ref, + getCurrentScope, + isRef, + isReactive, + isShallow, + EffectScheduler, +} from '@vue/reactivity'; +import { noop, isObject, isPlainObject, isSet, isMap, isFunction } from 'lodash-es'; +import { isPromise } from './utils'; + +export { ref as signal, computed, watchEffect as effect, watch as reaction, isRef as isSignal }; +export type { Ref as Signal, ComputedRef as ComputedSignal }; + +export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T); +export type WatchEffect = (onCleanup: OnCleanup) => void; + +export type WatchCallback<V = any, OV = any> = ( + value: V, + oldValue: OV, + onCleanup: OnCleanup, +) => any; + +type OnCleanup = (cleanupFn: () => void) => void; + +export interface WatchOptions<Immediate = boolean> { + immediate?: Immediate; + deep?: boolean; + once?: boolean; +} + +const INITIAL_WATCHER_VALUE = {}; + +type MultiWatchSources = (WatchSource<unknown> | object)[]; + +export type WatchStopHandle = () => void; + +// Simple effect. +export function watchEffect(effect: WatchEffect): WatchStopHandle { + return doWatch(effect, null); +} + +type MapSources<T, Immediate> = { + [K in keyof T]: T[K] extends WatchSource<infer V> + ? Immediate extends true + ? V | undefined + : V + : T[K] extends object + ? Immediate extends true + ? T[K] | undefined + : T[K] + : never; +}; + +// overload: single source + cb +export function watch<T, Immediate extends Readonly<boolean> = false>( + source: WatchSource<T>, + cb: WatchCallback<T, Immediate extends true ? T | undefined : T>, + options?: WatchOptions<Immediate>, +): WatchStopHandle; + +// overload: array of multiple sources + cb +export function watch<T extends MultiWatchSources, Immediate extends Readonly<boolean> = false>( + sources: [...T], + cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, + options?: WatchOptions<Immediate>, +): WatchStopHandle; + +// overload: multiple sources w/ `as const` +// watch([foo, bar] as const, () => {}) +// somehow [...T] breaks when the type is readonly +export function watch< + T extends Readonly<MultiWatchSources>, + Immediate extends Readonly<boolean> = false, +>( + source: T, + cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, + options?: WatchOptions<Immediate>, +): WatchStopHandle; + +// overload: watching reactive object w/ cb +export function watch<T extends object, Immediate extends Readonly<boolean> = false>( + source: T, + cb: WatchCallback<T, Immediate extends true ? T | undefined : T>, + options?: WatchOptions<Immediate>, +): WatchStopHandle; + +// implementation +export function watch<T = any, Immediate extends Readonly<boolean> = false>( + source: T | WatchSource<T>, + cb: any, + options?: WatchOptions<Immediate>, +): WatchStopHandle { + if (!isFunction(cb)) { + console.warn( + `\`watch(fn, options?)\` signature has been moved to a separate API. ` + + `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` + + `supports \`watch(source, cb, options?) signature.`, + ); + } + return doWatch(source as any, cb, options); +} + +function doWatch( + source: WatchSource | WatchSource[] | WatchEffect | object, + cb: WatchCallback | null, + { deep, immediate, once }: WatchOptions = {}, +) { + if (cb && once) { + const _cb = cb; + cb = (...args) => { + _cb(...args); + unwatch(); + }; + } + + const warnInvalidSource = (s: unknown) => { + console.warn( + `Invalid watch source: `, + s, + `A watch source can only be a getter/effect function, a ref, ` + + `a reactive object, or an array of these types.`, + ); + }; + + const reactiveGetter = (source: object) => + deep === true + ? source // traverse will happen in wrapped getter below + : // for deep: false, only traverse root-level properties + traverse(source, deep === false ? 1 : undefined); + + let getter: () => any; + let forceTrigger = false; + let isMultiSource = false; + + if (isRef(source)) { + getter = () => source.value; + forceTrigger = isShallow(source); + } else if (isReactive(source)) { + getter = () => reactiveGetter(source); + forceTrigger = true; + } else if (Array.isArray(source)) { + isMultiSource = true; + forceTrigger = source.some((s) => isReactive(s) || isShallow(s)); + getter = () => + source.map((s) => { + if (isRef(s)) { + return s.value; + } else if (isReactive(s)) { + return reactiveGetter(s); + } else { + warnInvalidSource(s); + } + }); + } else if (isFunction(source)) { + if (cb) { + // getter with cb + getter = () => callWithErrorHandling(source); + } else { + // no cb -> simple effect + getter = () => { + if (cleanup) { + cleanup(); + } + return callWithAsyncErrorHandling(source, [onCleanup]); + }; + } + } else { + getter = noop; + warnInvalidSource(source); + } + + if (cb && deep) { + const baseGetter = getter; + getter = () => traverse(baseGetter()); + } + + let cleanup: (() => void) | undefined; + const onCleanup: OnCleanup = (fn: () => void) => { + cleanup = effect.onStop = () => { + callWithErrorHandling(fn); + cleanup = effect.onStop = undefined; + }; + }; + + let oldValue: any = isMultiSource + ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE) + : INITIAL_WATCHER_VALUE; + + const scheduler: EffectScheduler = () => { + if (!effect.active || !effect.dirty) { + return; + } + if (cb) { + // watch(source, cb) + const newValue = effect.run(); + if ( + deep || + forceTrigger || + (isMultiSource + ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i])) + : hasChanged(newValue, oldValue)) + ) { + // cleanup before running cb again + if (cleanup) { + cleanup(); + } + callWithAsyncErrorHandling(cb, [ + newValue, + // pass undefined as the old value when it's changed for the first time + oldValue === INITIAL_WATCHER_VALUE + ? undefined + : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE + ? [] + : oldValue, + onCleanup, + ]); + oldValue = newValue; + } + } else { + // watchEffect + effect.run(); + } + }; + + const effect = new ReactiveEffect(getter, noop, scheduler); + + const scope = getCurrentScope(); + const unwatch = () => { + effect.stop(); + if (scope) { + remove((scope as any).effects, effect); + } + }; + + // initial run + if (cb) { + if (immediate) { + scheduler(); + } else { + oldValue = effect.run(); + } + } else { + effect.run(); + } + + return unwatch; +} + +function traverse(value: unknown, depth?: number, currentDepth = 0, seen?: Set<unknown>) { + if (!isObject(value)) { + return value; + } + + if (depth && depth > 0) { + if (currentDepth >= depth) { + return value; + } + currentDepth++; + } + + seen = seen || new Set(); + if (seen.has(value)) { + return value; + } + seen.add(value); + if (isRef(value)) { + traverse(value.value, depth, currentDepth, seen); + } else if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + traverse(value[i], depth, currentDepth, seen); + } + } else if (isSet(value) || isMap(value)) { + value.forEach((v: any) => { + traverse(v, depth, currentDepth, seen); + }); + } else if (isPlainObject(value)) { + for (const key in value) { + traverse((value as PlainObject)[key], depth, currentDepth, seen); + } + } + + return value; +} + +function callWithErrorHandling(fn: AnyFunction, args?: unknown[]) { + try { + return args ? fn(...args) : fn(); + } catch (err) { + console.error(err); + } +} + +function callWithAsyncErrorHandling(fn: AnyFunction | AnyFunction[], args?: unknown[]): any { + if (isFunction(fn)) { + const res = callWithErrorHandling(fn, args); + if (res && isPromise(res)) { + res.catch((err) => { + console.error(err); + }); + } + return res; + } + + if (Array.isArray(fn)) { + const values = []; + for (let i = 0; i < fn.length; i++) { + values.push(callWithAsyncErrorHandling(fn[i], args)); + } + return values; + } +} + +const remove = <T>(arr: T[], el: T) => { + const i = arr.indexOf(el); + if (i > -1) { + arr.splice(i, 1); + } +}; + +// compare whether a value has changed, accounting for NaN. +const hasChanged = (value: any, oldValue: any): boolean => !Object.is(value, oldValue); diff --git a/packages/shared/src/types/common.ts b/packages/shared/src/types/base.ts similarity index 67% rename from packages/shared/src/types/common.ts rename to packages/shared/src/types/base.ts index dca288948..fee7234fc 100644 --- a/packages/shared/src/types/common.ts +++ b/packages/shared/src/types/base.ts @@ -2,4 +2,4 @@ export type VoidFunction = (...args: any[]) => void; export type AnyFunction = (...args: any[]) => any; -export type PlainObject = Record<PropertyKey, any>; +export type PlainObject = Record<string, any>; diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index e23e98cf4..12ebf3016 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -1,5 +1,6 @@ -export * from './common'; +export * from './base'; export * from './material'; export * from './specs/asset-spec'; export * from './specs/lowcode-spec'; export * from './specs/runtime-api'; +export * from './specs/material-spec'; diff --git a/packages/shared/src/types/specs/lowcode-spec.ts b/packages/shared/src/types/specs/lowcode-spec.ts index 986df1ee0..64bda3a00 100644 --- a/packages/shared/src/types/specs/lowcode-spec.ts +++ b/packages/shared/src/types/specs/lowcode-spec.ts @@ -49,7 +49,7 @@ export interface Project { * 当前应用的公共数据源 * @deprecated */ - dataSource?: unknown; + dataSource?: never; /** * 当前应用的路由配置信息 */ @@ -104,14 +104,14 @@ export interface ComponentMap { * 协议中用于描述搭建出来的组件树结构的规范,整个组件树的描述由组件结构&容器结构两种结构嵌套构成。 */ export type ComponentTree<LifeCycleNameT extends string = string> = - ComponentTreeContainer<LifeCycleNameT>; + ComponentTreeRootNode<LifeCycleNameT>; /** - * 容器结构描述 (A) + * 根容器节点结构描述 (A) * 容器是一类特殊的组件,在组件能力基础上增加了对生命周期对象、自定义方法、样式文件、数据源等信息的描述。 */ -export interface ComponentTreeContainer<LifeCycleNameT extends string> - extends Omit<ComponentTreeNode, 'loop' | 'loopArgs' | 'condition'> { +export interface ComponentTreeRootNode<LifeCycleNameT extends string = string> + extends ComponentTreeNode { componentName: 'Page' | 'Block' | 'Component'; /** * 文件名称 @@ -142,6 +142,11 @@ export interface ComponentTreeContainer<LifeCycleNameT extends string> * type todo */ dataSource?: any; + + // for useless + loop: never; + loopArgs: never; + condition: never; } /** @@ -182,13 +187,13 @@ export interface ComponentTreeNode { * Props 结构描述 */ export interface ComponentTreeNodeProps { - /** 组件 ID */ + /** 组件 ID */ id?: string | JSExpression; - /** 组件样式类名 */ + /** 组件样式类名 */ className?: string; - /** 组件内联样式 */ + /** 组件内联样式 */ style?: JSONObject | JSExpression; - /** 组件 ref 名称 */ + /** 组件 ref 名称 */ ref?: string; [key: string]: any; diff --git a/packages/shared/src/types/specs/material-spec.ts b/packages/shared/src/types/specs/material-spec.ts new file mode 100644 index 000000000..c5734f415 --- /dev/null +++ b/packages/shared/src/types/specs/material-spec.ts @@ -0,0 +1,234 @@ +/** + * 组件描述协议 + * 对源码组件在低代码搭建平台中使用时所具备的配置能力和交互行为进行规范化描述,让不同平台对组件接入的实现保持一致, + * 让组件针对不同的搭建平台接入时可以使用一份统一的描述内容,让组件在不同的业务中流通成为可能。 + */ +import { ComponentTree, ComponentTreeNode } from './lowcode-spec'; +import { PlainObject } from '../base'; + +export interface LowCodeComponentTree extends ComponentTree { + componentName: 'Component'; +} + +/** + * 组件基础信息 + */ +export interface ComponentMetaData<Configure extends PlainObject = PlainObject> { + /** + * 组件名 + */ + componentName: string; + /** + * unique id + */ + uri?: string; + /** + * 组件名称 + */ + title: string; + /** + * 组件描述 + */ + description?: string; + /** + * 组件的小图标 string or url + */ + icon?: string; + /** + * 组件标签 + */ + tags?: string[]; + /** + * 组件文档链接 + */ + docUrl?: string; + /** + * 组件快照 + */ + screenshot?: string; + /** + * 组件关键词,用于搜索联想 + */ + keywords?: string; + /** + * 一级分组 + */ + group?: string; + /** + * 二级分组 + */ + category?: string; + /** + * 组件优先级排序 + */ + priority?: number; + + /** + * 组件研发模式 + */ + devMode?: 'proCode' | 'lowCode'; + + /** + * npm 源引入完整描述对象 + */ + npm?: NpmInfo; + + /** + * 低代码组件 schema + * @todo 待补充文档 + */ + schema?: LowCodeComponentTree; + /** + * 可用片段 + */ + snippets?: Snippet[]; + + /** + * 组件属性信息 + */ + props?: PropConfig[]; + /** + * 编辑体验增强 + */ + configure?: Configure; + + /** 其他扩展协议 */ + [key: string]: any; +} + +/** + * npm 源引入完整描述对象 + */ +export interface NpmInfo { + /** + * 源码组件名称 + */ + componentName?: string; + /** + * 源码组件库名 + */ + package: string; + /** + * 源码组件版本号 + */ + version?: string; + /** + * 是否解构 + */ + destructuring?: boolean; + /** + * 源码组件名称 + */ + exportName?: string; + /** + * 子组件名 + */ + subName?: string; + /** + * 组件路径 + */ + main?: string; +} + +/** + * 组件属性信息 + */ +export interface PropConfig { + /** + * 属性名称 + */ + name: string; + /** + * 属性类型 + */ + propType: PropType; + /** + * 属性描述 + */ + description?: string; + /** + * 属性默认值 + */ + defaultValue?: any; +} + +export type PropType = PropBasicType | PropRequiredType | PropComplexType; +export type PropBasicType = + | 'array' + | 'bool' + | 'func' + | 'number' + | 'object' + | 'string' + | 'node' + | 'element' + | 'any'; +export type PropComplexType = + | PropOneOf + | PropOneOfType + | PropArrayOf + | PropObjectOf + | PropShape + | PropExact + | PropInstanceOf; + +export interface PropRequiredType { + type: PropBasicType; + isRequired?: boolean; +} + +export interface PropOneOf { + type: 'oneOf'; + value: string[]; + isRequired?: boolean; +} +export interface PropOneOfType { + type: 'oneOfType'; + value: PropType[]; + isRequired?: boolean; +} +export interface PropArrayOf { + type: 'arrayOf'; + value: PropType; + isRequired?: boolean; +} +export interface PropObjectOf { + type: 'objectOf'; + value: PropType; + isRequired?: boolean; +} +export interface PropShape { + type: 'shape'; + value: PropConfig[]; + isRequired?: boolean; +} +export interface PropExact { + type: 'exact'; + value: PropConfig[]; + isRequired?: boolean; +} +export interface PropInstanceOf { + type: 'instanceOf'; + value: PropConfig; + isRequired?: boolean; +} + +/** + * 可用片段 + * + * 内容为组件不同状态下的低代码 schema (可以有多个),用户从组件面板拖入组件到设计器时会向页面 schema 中插入 snippets 中定义的组件低代码 schema + */ +export interface Snippet { + /** + * 组件分类 title + */ + title?: string; + /** + * snippet 截图 + */ + screenshot?: string; + /** + * 待插入的 schema + */ + schema?: ComponentTreeNode; +} diff --git a/packages/shared/src/types/specs/runtime-api.ts b/packages/shared/src/types/specs/runtime-api.ts index ab23daa28..762a57cd2 100644 --- a/packages/shared/src/types/specs/runtime-api.ts +++ b/packages/shared/src/types/specs/runtime-api.ts @@ -1,4 +1,4 @@ -import { AnyFunction, PlainObject } from '../common'; +import { AnyFunction, PlainObject } from '../base'; /** * 在上述事件类型描述和变量类型描述中,在函数或 JS 表达式内,均可以通过 this 对象获取当前组件所在容器的实例化对象 diff --git a/packages/shared/src/utils/browser.ts b/packages/shared/src/utils/browser.ts new file mode 100644 index 000000000..8c6f5ce8c --- /dev/null +++ b/packages/shared/src/utils/browser.ts @@ -0,0 +1,8 @@ +const userAgent = navigator.userAgent; + +export const isFirefox = userAgent.indexOf('Firefox') >= 0; +export const isWebKit = userAgent.indexOf('AppleWebKit') >= 0; +export const isChrome = userAgent.indexOf('Chrome') >= 0; +export const isSafari = !isChrome && userAgent.indexOf('Safari') >= 0; +export const isWebkitWebView = !isChrome && !isSafari && isWebKit; +export const isAndroid = userAgent.indexOf('Android') >= 0; diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts new file mode 100644 index 000000000..73d09114a --- /dev/null +++ b/packages/shared/src/utils/index.ts @@ -0,0 +1,5 @@ +export * from './invariant'; +export * from './is-promise'; +export * from './unique-id'; +export * as Browser from './browser'; +export * as Platform from './platform'; diff --git a/packages/shared/src/utils/invariant.ts b/packages/shared/src/utils/invariant.ts new file mode 100644 index 000000000..9be50edb8 --- /dev/null +++ b/packages/shared/src/utils/invariant.ts @@ -0,0 +1,5 @@ +export function invariant(check: unknown, message: string, thing?: any) { + if (!check) { + throw new Error(`Invariant failed: ${message}${thing ? ` in '${thing}'` : ''}`); + } +} diff --git a/packages/shared/src/utils/is-promise.ts b/packages/shared/src/utils/is-promise.ts new file mode 100644 index 000000000..a259538b7 --- /dev/null +++ b/packages/shared/src/utils/is-promise.ts @@ -0,0 +1,9 @@ +import { isObject, isFunction } from 'lodash-es'; + +export const isPromise = <T = any>(val: unknown): val is Promise<T> => { + return ( + (isObject(val) || isFunction(val)) && + isFunction((val as any).then) && + isFunction((val as any).catch) + ); +}; diff --git a/packages/shared/src/utils/platform.ts b/packages/shared/src/utils/platform.ts new file mode 100644 index 000000000..2cacf58b9 --- /dev/null +++ b/packages/shared/src/utils/platform.ts @@ -0,0 +1,48 @@ +const userAgent: string = navigator.userAgent; + +export const isWindows = userAgent.indexOf('Windows') >= 0; +export const isMacintosh = userAgent.indexOf('Macintosh') >= 0; +export const isLinux = userAgent.indexOf('Linux') >= 0; +export const isIOS = + (isMacintosh || userAgent.indexOf('iPad') >= 0 || userAgent.indexOf('iPhone') >= 0) && + !!navigator.maxTouchPoints && + navigator.maxTouchPoints > 0; +export const isMobile = userAgent?.indexOf('Mobi') >= 0; +export const locale: string | undefined = undefined; +export const platformLocale = navigator.language; + +export const enum Platform { + Web, + Mac, + Linux, + Windows, +} +export type PlatformName = 'Web' | 'Windows' | 'Mac' | 'Linux'; + +export function platformToString(platform: Platform): PlatformName { + switch (platform) { + case Platform.Web: + return 'Web'; + case Platform.Mac: + return 'Mac'; + case Platform.Linux: + return 'Linux'; + case Platform.Windows: + return 'Windows'; + } +} + +export let platform: Platform = Platform.Web; +if (isMacintosh) { + platform = Platform.Mac; +} else if (isWindows) { + platform = Platform.Windows; +} else if (isLinux) { + platform = Platform.Linux; +} + +export const isChrome = !!(userAgent && userAgent.indexOf('Chrome') >= 0); +export const isFirefox = !!(userAgent && userAgent.indexOf('Firefox') >= 0); +export const isSafari = !!(!isChrome && userAgent && userAgent.indexOf('Safari') >= 0); +export const isEdge = !!(userAgent && userAgent.indexOf('Edg/') >= 0); +export const isAndroid = !!(userAgent && userAgent.indexOf('Android') >= 0); diff --git a/packages/shared/src/utils/unique-id.ts b/packages/shared/src/utils/unique-id.ts new file mode 100644 index 000000000..3713cbd06 --- /dev/null +++ b/packages/shared/src/utils/unique-id.ts @@ -0,0 +1,4 @@ +let guid = Date.now(); +export function uniqueId(prefix = '') { + return `${prefix}${(guid++).toString(36).toLowerCase()}`; +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index 039e0b4d1..15e38d973 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "allowJs": true, "outDir": "dist" }, "include": ["src"] diff --git a/packages/types/src/shell/api/common.ts b/packages/types/src/shell/api/common.ts index 7d992d36e..0c2dc4328 100644 --- a/packages/types/src/shell/api/common.ts +++ b/packages/types/src/shell/api/common.ts @@ -1,6 +1,5 @@ - -import { Component, ReactElement, ReactNode } from 'react'; -import { IPublicTypeI18nData, IPublicTypeNodeSchema, IPublicTypeTitleContent } from '../type'; +import { Component, ReactNode } from 'react'; +import { IPublicTypeNodeSchema, IPublicTypeTitleContent } from '../type'; import { IPublicEnumTransitionType } from '../enum'; export interface IPublicApiCommonUtils { @@ -68,14 +67,9 @@ export interface IPublicApiCommonUtils { getLocale(): string; setLocale(locale: string): void; }; - - /** - * i18n 转换方法 - */ - intl(data: IPublicTypeI18nData | string | undefined | ReactElement, params?: object): string; } -export interface IPublicApiCommonSkeletonCabin { +export interface IPublicApiCommonSkeletonCabin { /** * 编辑器框架 View * get Workbench Component @@ -84,7 +78,6 @@ export interface IPublicApiCommonSkeletonCabin { } export interface IPublicApiCommonEditorCabin { - /** * Title 组件 * @experimental unstable API, pay extra caution when trying to use this diff --git a/packages/types/src/shell/api/skeleton.ts b/packages/types/src/shell/api/skeleton.ts index 1bf788e12..5f2d9c61a 100644 --- a/packages/types/src/shell/api/skeleton.ts +++ b/packages/types/src/shell/api/skeleton.ts @@ -1,8 +1,12 @@ import { IPublicModelSkeletonItem } from '../model'; -import { IPublicTypeConfigTransducer, IPublicTypeDisposable, IPublicTypeSkeletonConfig, IPublicTypeWidgetConfigArea } from '../type'; +import { + IPublicTypeConfigTransducer, + IPublicTypeDisposable, + IPublicTypeSkeletonConfig, + IPublicTypeWidgetConfigArea, +} from '../type'; export interface IPublicApiSkeleton { - /** * 增加一个面板实例 * add a new panel @@ -10,7 +14,10 @@ export interface IPublicApiSkeleton { * @param extraConfig * @returns */ - add(config: IPublicTypeSkeletonConfig, extraConfig?: Record<string, any>): IPublicModelSkeletonItem | undefined; + add( + config: IPublicTypeSkeletonConfig, + extraConfig?: Record<string, any>, + ): IPublicModelSkeletonItem | undefined; /** * 移除一个面板实例 @@ -24,7 +31,9 @@ export interface IPublicApiSkeleton { * 获取某个区域下的所有面板实例 * @param areaName IPublicTypeWidgetConfigArea */ - getAreaItems(areaName: IPublicTypeWidgetConfigArea): IPublicModelSkeletonItem[] | undefined; + getAreaItems( + areaName: IPublicTypeWidgetConfigArea, + ): IPublicModelSkeletonItem[] | undefined; /** * 获取面板实例 @@ -95,7 +104,9 @@ export interface IPublicApiSkeleton { * @param listener * @returns */ - onShowPanel(listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void): IPublicTypeDisposable; + onShowPanel( + listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void, + ): IPublicTypeDisposable; /** * 监听 Panel 实例隐藏事件 @@ -103,19 +114,25 @@ export interface IPublicApiSkeleton { * @param listener * @returns */ - onHidePanel(listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void): IPublicTypeDisposable; + onHidePanel( + listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void, + ): IPublicTypeDisposable; /** * 监听 Widget 实例 Disable 事件 * @param listener */ - onDisableWidget(listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void): IPublicTypeDisposable; + onDisableWidget( + listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void, + ): IPublicTypeDisposable; /** * 监听 Widget 实例 Enable 事件 * @param listener */ - onEnableWidget(listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void): IPublicTypeDisposable; + onEnableWidget( + listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void, + ): IPublicTypeDisposable; /** * 监听 Widget 显示事件 @@ -123,7 +140,9 @@ export interface IPublicApiSkeleton { * @param listener * @returns */ - onShowWidget(listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void): IPublicTypeDisposable; + onShowWidget( + listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void, + ): IPublicTypeDisposable; /** * 监听 Widget 隐藏事件 @@ -131,7 +150,9 @@ export interface IPublicApiSkeleton { * @param listener * @returns */ - onHideWidget(listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void): IPublicTypeDisposable; + onHideWidget( + listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void, + ): IPublicTypeDisposable; /** * 注册一个面板的配置转换器(transducer)。 @@ -148,5 +169,9 @@ export interface IPublicApiSkeleton { * - (可选)转换器的唯一标识符。用于在需要时引用或操作特定的转换器。 * - (Optional) A unique identifier for the transducer. Used for referencing or manipulating a specific transducer when needed. */ - registerConfigTransducer(transducer: IPublicTypeConfigTransducer, level: number, id?: string): void; + registerConfigTransducer( + transducer: IPublicTypeConfigTransducer, + level: number, + id?: string, + ): void; } diff --git a/packages/types/src/shell/type/advanced.ts b/packages/types/src/shell/type/advanced.ts index 3494a6c05..6c57bcbb3 100644 --- a/packages/types/src/shell/type/advanced.ts +++ b/packages/types/src/shell/type/advanced.ts @@ -1,12 +1,15 @@ import { ComponentType, ReactElement } from 'react'; -import { IPublicTypeNodeData, IPublicTypeSnippet, IPublicTypeInitialItem, IPublicTypeCallbacks, IPublicTypeLiveTextEditingConfig } from './'; +import { + IPublicTypeNodeData, + IPublicTypeCallbacks, + IPublicTypeLiveTextEditingConfig, +} from './'; import { IPublicModelNode } from '../model'; /** * 高级特性配置 */ export interface IPublicTypeAdvanced { - /** * 配置 callbacks 可捕获引擎抛出的一些事件,例如 onNodeAdd、onResize 等 * callbacks/hooks which can be used to do @@ -17,7 +20,9 @@ export interface IPublicTypeAdvanced { /** * 拖入容器时,自动带入 children 列表 */ - initialChildren?: IPublicTypeNodeData[] | ((target: IPublicModelNode) => IPublicTypeNodeData[]); + initialChildren?: + | IPublicTypeNodeData[] + | ((target: IPublicModelNode) => IPublicTypeNodeData[]); /** * 样式 及 位置,handle 上必须有明确的标识以便事件路由判断,或者主动设置事件独占模式 @@ -29,15 +34,14 @@ export interface IPublicTypeAdvanced { * - dragstart 请求通用 resizing 控制 请求 hud 显示 * - drag 时 计算并设置效果,更新控制柄位置 */ - getResizingHandlers?: ( - currentNode: any - ) => (Array<{ - type: 'N' | 'W' | 'S' | 'E' | 'NW' | 'NE' | 'SE' | 'SW'; - content?: ReactElement; - propTarget?: string; - appearOn?: 'mouse-enter' | 'mouse-hover' | 'selected' | 'always'; - }> | - ReactElement[]); + getResizingHandlers?: (currentNode: any) => + | Array<{ + type: 'N' | 'W' | 'S' | 'E' | 'NW' | 'NE' | 'SE' | 'SW'; + content?: ReactElement; + propTarget?: string; + appearOn?: 'mouse-enter' | 'mouse-hover' | 'selected' | 'always'; + }> + | ReactElement[]; /** * 是否绝对布局容器,还未进入协议 diff --git a/packages/types/src/shell/type/dynamic-setter.ts b/packages/types/src/shell/type/dynamic-setter.ts index 5883bb2bb..f2bbf59fb 100644 --- a/packages/types/src/shell/type/dynamic-setter.ts +++ b/packages/types/src/shell/type/dynamic-setter.ts @@ -1,4 +1,6 @@ import { IPublicModelSettingPropEntry, IPublicTypeCustomView } from '..'; import { IPublicTypeSetterConfig } from './setter-config'; -export type IPublicTypeDynamicSetter = (target: IPublicModelSettingPropEntry) => (string | IPublicTypeSetterConfig | IPublicTypeCustomView); +export type IPublicTypeDynamicSetter = ( + target: IPublicModelSettingPropEntry, +) => string | IPublicTypeSetterConfig | IPublicTypeCustomView; diff --git a/packages/types/src/shell/type/node-data.ts b/packages/types/src/shell/type/node-data.ts index 0447c9e2a..1314233b0 100644 --- a/packages/types/src/shell/type/node-data.ts +++ b/packages/types/src/shell/type/node-data.ts @@ -1,3 +1,12 @@ -import { IPublicTypeJSExpression, IPublicTypeNodeSchema, IPublicTypeDOMText, IPublicTypeI18nData } from './'; +import { + IPublicTypeJSExpression, + IPublicTypeNodeSchema, + IPublicTypeDOMText, + IPublicTypeI18nData, +} from './'; -export type IPublicTypeNodeData = IPublicTypeNodeSchema | IPublicTypeJSExpression | IPublicTypeDOMText | IPublicTypeI18nData; +export type IPublicTypeNodeData = + | IPublicTypeNodeSchema + | IPublicTypeJSExpression + | IPublicTypeDOMText + | IPublicTypeI18nData; diff --git a/packages/types/src/shell/type/prop-types.ts b/packages/types/src/shell/type/prop-types.ts index 22d84c86f..4d8438a53 100644 --- a/packages/types/src/shell/type/prop-types.ts +++ b/packages/types/src/shell/type/prop-types.ts @@ -1,9 +1,28 @@ /* eslint-disable max-len */ import { IPublicTypePropConfig } from './'; -export type IPublicTypePropType = IPublicTypeBasicType | IPublicTypeRequiredType | IPublicTypeComplexType; -export type IPublicTypeBasicType = 'array' | 'bool' | 'func' | 'number' | 'object' | 'string' | 'node' | 'element' | 'any'; -export type IPublicTypeComplexType = IPublicTypeOneOf | IPublicTypeOneOfType | IPublicTypeArrayOf | IPublicTypeObjectOf | IPublicTypeShape | IPublicTypeExact | IPublicTypeInstanceOf; +export type IPublicTypePropType = + | IPublicTypeBasicType + | IPublicTypeRequiredType + | IPublicTypeComplexType; +export type IPublicTypeBasicType = + | 'array' + | 'bool' + | 'func' + | 'number' + | 'object' + | 'string' + | 'node' + | 'element' + | 'any'; +export type IPublicTypeComplexType = + | IPublicTypeOneOf + | IPublicTypeOneOfType + | IPublicTypeArrayOf + | IPublicTypeObjectOf + | IPublicTypeShape + | IPublicTypeExact + | IPublicTypeInstanceOf; export interface IPublicTypeRequiredType { type: IPublicTypeBasicType; diff --git a/packages/types/src/shell/type/setter-type.ts b/packages/types/src/shell/type/setter-type.ts index 92cb118ca..cc8e853da 100644 --- a/packages/types/src/shell/type/setter-type.ts +++ b/packages/types/src/shell/type/setter-type.ts @@ -3,4 +3,8 @@ import { IPublicTypeCustomView, IPublicTypeSetterConfig } from './'; // if *string* passed must be a registered Setter Name, future support blockSchema // eslint-disable-next-line max-len -export type IPublicTypeSetterType = IPublicTypeSetterConfig | IPublicTypeSetterConfig[] | string | IPublicTypeCustomView; +export type IPublicTypeSetterType = + | IPublicTypeSetterConfig + | IPublicTypeSetterConfig[] + | string + | IPublicTypeCustomView; diff --git a/packages/editor-skeleton/package.json b/packages/workbench/package.json similarity index 93% rename from packages/editor-skeleton/package.json rename to packages/workbench/package.json index 22c7a8fc7..e408756c3 100644 --- a/packages/editor-skeleton/package.json +++ b/packages/workbench/package.json @@ -1,5 +1,5 @@ { - "name": "@alilc/lowcode-editor-skeleton", + "name": "@alilc/lowcode-workbench", "version": "2.0.0-beta.0", "description": "alibaba lowcode editor skeleton", "type": "module", @@ -38,7 +38,7 @@ "dependencies": { "@alifd/next": "^1.27.8", "@alilc/lowcode-designer": "workspace:*", - "@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-designer": "workspace:*", - "@alilc/lowcode-editor-core": "workspace:*", + "@alilc/lowcode-core": "workspace:*", "@alilc/lowcode-shared": "workspace:*", "@alilc/lowcode-types": "workspace:*", "@alilc/lowcode-utils": "workspace:*", diff --git a/packages/editor-skeleton/src/area.ts b/packages/workbench/src/area.ts similarity index 84% rename from packages/editor-skeleton/src/area.ts rename to packages/workbench/src/area.ts index 3ce363b1f..4ed35f384 100644 --- a/packages/editor-skeleton/src/area.ts +++ b/packages/workbench/src/area.ts @@ -1,9 +1,8 @@ -/* eslint-disable max-len */ import { observable, computed, makeObservable } from '@alilc/lowcode-editor-core'; import { Logger } from '@alilc/lowcode-utils'; import { IPublicTypeWidgetBaseConfig } from '@alilc/lowcode-types'; import { WidgetContainer } from './widget/widget-container'; -import { ISkeleton } from './skeleton'; +import { Skeleton } from './skeleton'; import { IWidget } from './widget/widget'; const logger = new Logger({ level: 'warn', bizName: 'skeleton:area' }); @@ -38,9 +37,19 @@ implements IArea<C, T> { private lastCurrent: T | null = null; - constructor(readonly skeleton: ISkeleton, readonly name: string, handle: (item: T | C) => T, private exclusive?: boolean, defaultSetCurrent = false) { + constructor( + readonly skeleton: Skeleton, + readonly name: string, + handle: (item: T | C) => T, private exclusive?: boolean, + defaultSetCurrent = false + ) { makeObservable(this); - this.container = skeleton.createContainer(name, handle, exclusive, () => this.visible, defaultSetCurrent); + this.container = skeleton.createContainer( + name, + handle, + exclusive, + () => this.visible, defaultSetCurrent + ); } isEmpty(): boolean { diff --git a/packages/editor-skeleton/src/components/draggable-line/index.less b/packages/workbench/src/components/draggable-line/index.less similarity index 100% rename from packages/editor-skeleton/src/components/draggable-line/index.less rename to packages/workbench/src/components/draggable-line/index.less diff --git a/packages/editor-skeleton/src/components/draggable-line/index.tsx b/packages/workbench/src/components/draggable-line/index.tsx similarity index 100% rename from packages/editor-skeleton/src/components/draggable-line/index.tsx rename to packages/workbench/src/components/draggable-line/index.tsx diff --git a/packages/editor-skeleton/src/components/field/fields.tsx b/packages/workbench/src/components/field/fields.tsx similarity index 100% rename from packages/editor-skeleton/src/components/field/fields.tsx rename to packages/workbench/src/components/field/fields.tsx diff --git a/packages/editor-skeleton/src/components/field/index.less b/packages/workbench/src/components/field/index.less similarity index 100% rename from packages/editor-skeleton/src/components/field/index.less rename to packages/workbench/src/components/field/index.less diff --git a/packages/editor-skeleton/src/components/field/index.ts b/packages/workbench/src/components/field/index.ts similarity index 100% rename from packages/editor-skeleton/src/components/field/index.ts rename to packages/workbench/src/components/field/index.ts diff --git a/packages/editor-skeleton/src/components/field/inlinetip.tsx b/packages/workbench/src/components/field/inlinetip.tsx similarity index 100% rename from packages/editor-skeleton/src/components/field/inlinetip.tsx rename to packages/workbench/src/components/field/inlinetip.tsx diff --git a/packages/editor-skeleton/src/components/popup/index.tsx b/packages/workbench/src/components/popup/index.tsx similarity index 100% rename from packages/editor-skeleton/src/components/popup/index.tsx rename to packages/workbench/src/components/popup/index.tsx diff --git a/packages/editor-skeleton/src/components/popup/style.less b/packages/workbench/src/components/popup/style.less similarity index 100% rename from packages/editor-skeleton/src/components/popup/style.less rename to packages/workbench/src/components/popup/style.less diff --git a/packages/editor-skeleton/src/components/settings/index.ts b/packages/workbench/src/components/settings/index.ts similarity index 100% rename from packages/editor-skeleton/src/components/settings/index.ts rename to packages/workbench/src/components/settings/index.ts diff --git a/packages/editor-skeleton/src/components/settings/main.ts b/packages/workbench/src/components/settings/main.ts similarity index 100% rename from packages/editor-skeleton/src/components/settings/main.ts rename to packages/workbench/src/components/settings/main.ts diff --git a/packages/editor-skeleton/src/components/settings/settings-pane.tsx b/packages/workbench/src/components/settings/settings-pane.tsx similarity index 79% rename from packages/editor-skeleton/src/components/settings/settings-pane.tsx rename to packages/workbench/src/components/settings/settings-pane.tsx index 60d67620c..ab297006e 100644 --- a/packages/editor-skeleton/src/components/settings/settings-pane.tsx +++ b/packages/workbench/src/components/settings/settings-pane.tsx @@ -1,11 +1,16 @@ import { Component, MouseEvent, Fragment, ReactNode } from 'react'; -import { observer, observable, engineConfig, runInAction, shallowIntl } from '@alilc/lowcode-editor-core'; +import { + observer, + observable, + engineConfig, + runInAction, +} from '@alilc/lowcode-editor-core'; import { createContent, isJSSlot, isSetterConfig, shouldUseVariableSetter, - isSettingField + isSettingField, } from '@alilc/lowcode-utils'; import { Skeleton } from '../../skeleton'; import { Stage } from '../../widget/stage'; @@ -54,7 +59,10 @@ type SettingFieldViewProps = { field: ISettingField }; type SettingFieldViewState = { fromOnChange: boolean; value: any }; @observer -class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldViewState> { +class SettingFieldView extends Component< + SettingFieldViewProps, + SettingFieldViewState +> { static contextType = SkeletonContext; stageName: string | undefined; @@ -83,7 +91,9 @@ class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldView name: stageName, content: ( <Fragment> - {field.items.map((item, index) => createSettingFieldView(item, field, index))} + {field.items.map((item, index) => + createSettingFieldView(item, field, index), + )} </Fragment> ), props: { @@ -139,8 +149,8 @@ class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldView const { setter } = this.field; let setterProps: | ({ - setters?: (ReactNode | string)[]; - } & Record<string, unknown>) + setters?: (ReactNode | string)[]; + } & Record<string, unknown>) | IPublicTypeDynamicProps = {}; let setterType: any; let initialValue: any = null; @@ -183,8 +193,12 @@ class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldView const supportVariable = this.field.extraProps?.supportVariable; // supportVariableGlobally 只对标准组件生效,vc 需要单独配置 const supportVariableGlobally = - engineConfig.get('supportVariableGlobally', false) && isStandardComponent(componentMeta); - const isUseVariableSetter = shouldUseVariableSetter(supportVariable, supportVariableGlobally); + engineConfig.get('supportVariableGlobally', false) && + isStandardComponent(componentMeta); + const isUseVariableSetter = shouldUseVariableSetter( + supportVariable, + supportVariableGlobally, + ); if (isUseVariableSetter === false) { return { setterProps, @@ -195,7 +209,10 @@ class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldView if (setterType === 'MixedSetter') { // VariableSetter 不单独使用 - if (Array.isArray(setterProps.setters) && !setterProps.setters.includes('VariableSetter')) { + if ( + Array.isArray(setterProps.setters) && + !setterProps.setters.includes('VariableSetter') + ) { setterProps.setters.push('VariableSetter'); } } else { @@ -256,7 +273,10 @@ class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldView } = this.setterInfo; const onChangeAPI = extraProps?.onChange; - const createSetterContent = (setter: any, props: Record<string, any>): ReactNode => { + const createSetterContent = ( + setter: any, + props: Record<string, any>, + ): ReactNode => { if (typeof setter === 'string') { setter = this.setters?.getSetter(setter); if (!setter) { @@ -270,18 +290,32 @@ class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldView } setter = setter.component; } - + // Fusion 的表单组件都是通过 'value' in props 来判断是否使用 defaultValue if ('value' in props && typeof props.value === 'undefined') { delete props.value; } - + return createContent(setter, props); }; + function shallowIntl(data: any): any { + if (!data || typeof data !== 'object') { + return data; + } + const maps: any = {}; + Object.keys(data).forEach((key) => { + maps[key] = intl(data[key]); + }); + return maps; + } + return createField( { - meta: field?.componentMeta?.npm || field?.componentMeta?.componentName || '', + meta: + field?.componentMeta?.npm || + field?.componentMeta?.componentName || + '', title: field.title, // editor: field.editor, collapsed: !field.expanded, @@ -293,45 +327,46 @@ class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldView stageName, ...extraProps, }, - !stageName && createSetterContent(setterType, { - ...shallowIntl(setterProps), - forceInline: extraProps.forceInline, - key: field.id, - // === injection - prop: field.internalToShellField(), // for compatible vision - selected: field.top?.getNode()?.internalToShellNode(), - field: field.internalToShellField(), - // === IO - value: this.value, // reaction point - initialValue, - onChange: (value: any) => { - this.setState({ - fromOnChange: true, - value, - }); - field.setValue(value, true); - if (onChangeAPI) onChangeAPI(value, field.internalToShellField()); - }, - onInitial: () => { - if (initialValue == null) { - return; - } - const value = + !stageName && + createSetterContent(setterType, { + ...shallowIntl(setterProps), + forceInline: extraProps.forceInline, + key: field.id, + // === injection + prop: field.internalToShellField(), // for compatible vision + selected: field.top?.getNode()?.internalToShellNode(), + field: field.internalToShellField(), + // === IO + value: this.value, // reaction point + initialValue, + onChange: (value: any) => { + this.setState({ + fromOnChange: true, + value, + }); + field.setValue(value, true); + if (onChangeAPI) onChangeAPI(value, field.internalToShellField()); + }, + onInitial: () => { + if (initialValue == null) { + return; + } + const value = typeof initialValue === 'function' ? initialValue(field.internalToShellField()) : initialValue; - this.setState({ - value, - }); - field.setValue(value, true); - }, + this.setState({ + value, + }); + field.setValue(value, true); + }, - removeProp: () => { - if (field.name) { - field.parent.clearPropValue(field.name); - } - }, - }), + removeProp: () => { + if (field.name) { + field.parent.clearPropValue(field.name); + } + }, + }), extraProps.forceInline ? 'plain' : extraProps.display, ); } @@ -364,7 +399,9 @@ class SettingGroupView extends Component<SettingGroupViewProps> { name: stageName, content: ( <Fragment> - {field.items.map((item, index) => createSettingFieldView(item, field, index))} + {field.items.map((item, index) => + createSettingFieldView(item, field, index), + )} </Fragment> ), props: { @@ -392,7 +429,8 @@ class SettingGroupView extends Component<SettingGroupViewProps> { // todo: split collapsed state | field.items for optimize return createField( { - meta: field.componentMeta?.npm || field.componentMeta?.componentName || '', + meta: + field.componentMeta?.npm || field.componentMeta?.componentName || '', title: field.title, // editor: field.editor, collapsed: !field.expanded, @@ -401,7 +439,9 @@ class SettingGroupView extends Component<SettingGroupViewProps> { // stages, stageName: this.stageName, }, - field.items.map((item, index) => createSettingFieldView(item, field, index)), + field.items.map((item, index) => + createSettingFieldView(item, field, index), + ), display, ); } @@ -445,7 +485,10 @@ export class SettingsPane extends Component<SettingsPaneProps> { if (!usePopup) return; const pane = e.currentTarget as HTMLDivElement; function getTarget(node: any): any { - if (!pane.contains(node) || (node.nodeName === 'A' && node.getAttribute('href'))) { + if ( + !pane.contains(node) || + (node.nodeName === 'A' && node.getAttribute('href')) + ) { return null; } @@ -482,7 +525,9 @@ export class SettingsPane extends Component<SettingsPaneProps> { {/* todo: add head for single use */} <PopupService popupPipe={this.popupPipe}> <div className="lc-settings-content"> - {items.map((item, index) => createSettingFieldView(item, target, index))} + {items.map((item, index) => + createSettingFieldView(item, target, index), + )} </div> </PopupService> </div> diff --git a/packages/editor-skeleton/src/components/settings/settings-primary-pane.tsx b/packages/workbench/src/components/settings/settings-primary-pane.tsx similarity index 100% rename from packages/editor-skeleton/src/components/settings/settings-primary-pane.tsx rename to packages/workbench/src/components/settings/settings-primary-pane.tsx diff --git a/packages/editor-skeleton/src/components/settings/style.less b/packages/workbench/src/components/settings/style.less similarity index 100% rename from packages/editor-skeleton/src/components/settings/style.less rename to packages/workbench/src/components/settings/style.less diff --git a/packages/editor-skeleton/src/components/settings/utils.ts b/packages/workbench/src/components/settings/utils.ts similarity index 100% rename from packages/editor-skeleton/src/components/settings/utils.ts rename to packages/workbench/src/components/settings/utils.ts diff --git a/packages/editor-skeleton/src/components/stage-box/index.less b/packages/workbench/src/components/stage-box/index.less similarity index 100% rename from packages/editor-skeleton/src/components/stage-box/index.less rename to packages/workbench/src/components/stage-box/index.less diff --git a/packages/editor-skeleton/src/components/stage-box/index.ts b/packages/workbench/src/components/stage-box/index.ts similarity index 100% rename from packages/editor-skeleton/src/components/stage-box/index.ts rename to packages/workbench/src/components/stage-box/index.ts diff --git a/packages/editor-skeleton/src/components/stage-box/stage-box.tsx b/packages/workbench/src/components/stage-box/stage-box.tsx similarity index 100% rename from packages/editor-skeleton/src/components/stage-box/stage-box.tsx rename to packages/workbench/src/components/stage-box/stage-box.tsx diff --git a/packages/editor-skeleton/src/components/stage-box/stage-chain.ts b/packages/workbench/src/components/stage-box/stage-chain.ts similarity index 100% rename from packages/editor-skeleton/src/components/stage-box/stage-chain.ts rename to packages/workbench/src/components/stage-box/stage-chain.ts diff --git a/packages/editor-skeleton/src/components/stage-box/stage.tsx b/packages/workbench/src/components/stage-box/stage.tsx similarity index 100% rename from packages/editor-skeleton/src/components/stage-box/stage.tsx rename to packages/workbench/src/components/stage-box/stage.tsx diff --git a/packages/editor-skeleton/src/components/widget-views/index.less b/packages/workbench/src/components/widget-views/index.less similarity index 100% rename from packages/editor-skeleton/src/components/widget-views/index.less rename to packages/workbench/src/components/widget-views/index.less diff --git a/packages/editor-skeleton/src/components/widget-views/index.tsx b/packages/workbench/src/components/widget-views/index.tsx similarity index 81% rename from packages/editor-skeleton/src/components/widget-views/index.tsx rename to packages/workbench/src/components/widget-views/index.tsx index 4fe272e42..d4ace6c2c 100644 --- a/packages/editor-skeleton/src/components/widget-views/index.tsx +++ b/packages/workbench/src/components/widget-views/index.tsx @@ -1,6 +1,6 @@ import { Component, ReactElement } from 'react'; import classNames from 'classnames'; -import { observer } from '@alilc/lowcode-editor-core'; +import { observer } from '@alilc/lowcode-editor-core'; import { Title, HelpTip } from '@alilc/lowcode-designer'; import { DockProps } from '../../types'; import { PanelDock } from '../../widget/panel-dock'; @@ -14,7 +14,14 @@ import PanelOperationRow from './panel-operation-row'; import './index.less'; -export function DockView({ title, icon, description, size, className, onClick }: DockProps) { +export function DockView({ + title, + icon, + description, + size, + className, + onClick, +}: DockProps) { return ( <Title title={composeTitle(title, icon, description)} @@ -43,9 +50,17 @@ export class PanelDockView extends Component<DockProps & { dock: PanelDock }> { if (dock.actived !== this.lastActived) { this.lastActived = dock.actived; if (this.lastActived) { - dock.skeleton.postEvent(SkeletonEvents.PANEL_DOCK_ACTIVE, dock.name, dock); + dock.skeleton.postEvent( + SkeletonEvents.PANEL_DOCK_ACTIVE, + dock.name, + dock, + ); } else { - dock.skeleton.postEvent(SkeletonEvents.PANEL_DOCK_UNACTIVE, dock.name, dock); + dock.skeleton.postEvent( + SkeletonEvents.PANEL_DOCK_UNACTIVE, + dock.name, + dock, + ); } } } @@ -96,7 +111,7 @@ export class DraggableLineView extends Component<{ panel: Panel }> { containerRef.style.width = `${width}px`; } - // 抛出事件,对于有些需要 panel 插件随着 度变化进行再次渲染的,由panel插件内部监听事件实现 + // 抛出事件,对于有些需要 panel 插件随着变化进行再次渲染的,由panel插件内部监听事件实现 const editor = this.props.panel.skeleton.editor; editor?.eventBus.emit('dockpane.drag', width); } @@ -105,7 +120,10 @@ export class DraggableLineView extends Component<{ panel: Panel }> { const editor = this.props.panel.skeleton.editor; editor?.eventBus.emit('dockpane.dragchange', type); // builtinSimulator 屏蔽掉 鼠标事件 - editor?.eventBus.emit('designer.builtinSimulator.disabledEvents', type === 'start'); + editor?.eventBus.emit( + 'designer.builtinSimulator.disabledEvents', + type === 'start', + ); } render() { @@ -113,7 +131,11 @@ export class DraggableLineView extends Component<{ panel: Panel }> { // 默认 关闭,通过配置开启 const enableDrag = this.props.panel.config.props?.enableDrag; const isRightArea = this.props.panel.config?.area === 'rightArea'; - if (isRightArea || !enableDrag || this.props.panel?.parent?.name === 'leftFixedArea') { + if ( + isRightArea || + !enableDrag || + this.props.panel?.parent?.name === 'leftFixedArea' + ) { return null; } return ( @@ -137,7 +159,10 @@ export class DraggableLineView extends Component<{ panel: Panel }> { } @observer -export class TitledPanelView extends Component<{ panel: Panel; area?: string }> { +export class TitledPanelView extends Component<{ + panel: Panel; + area?: string; +}> { private lastVisible = false; componentDidMount() { @@ -178,7 +203,9 @@ export class TitledPanelView extends Component<{ panel: Panel; area?: string }> hidden: !panel.visible, })} id={panelName} - data-keep-visible-while-dragging={panel.config.props?.keepVisibleWhileDragging} + data-keep-visible-while-dragging={ + panel.config.props?.keepVisibleWhileDragging + } > <PanelOperationRow panel={panel} /> <PanelTitle panel={panel} /> @@ -236,7 +263,9 @@ export class PanelView extends Component<{ hidden: !panel.visible, })} id={panelName} - data-keep-visible-while-dragging={panel.config.props?.keepVisibleWhileDragging} + data-keep-visible-while-dragging={ + panel.config.props?.keepVisibleWhileDragging + } > {!hideOperationRow && <PanelOperationRow panel={panel} />} {panel.body} @@ -258,11 +287,27 @@ export class TabsPanelView extends Component<{ const contents: ReactElement[] = []; // 如果只有一个标签且 shouldHideSingleTab 为 true,则不显示 Tabs if (this.props.shouldHideSingleTab && container.items.length === 1) { - contents.push(<PanelView key={container.items[0].id} panel={container.items[0]} hideOperationRow hideDragLine />); + contents.push( + <PanelView + key={container.items[0].id} + panel={container.items[0]} + hideOperationRow + hideDragLine + />, + ); } else { container.items.forEach((item: any) => { - titles.push(<PanelTitle key={item.id} panel={item} className="lc-tab-title" />); - contents.push(<PanelView key={item.id} panel={item} hideOperationRow hideDragLine />); + titles.push( + <PanelTitle key={item.id} panel={item} className="lc-tab-title" />, + ); + contents.push( + <PanelView + key={item.id} + panel={item} + hideOperationRow + hideDragLine + />, + ); }); } @@ -336,9 +381,17 @@ export class WidgetView extends Component<{ widget: IWidget }> { if (currentVisible !== this.lastVisible) { this.lastVisible = currentVisible; if (this.lastVisible) { - widget.skeleton.postEvent(SkeletonEvents.WIDGET_SHOW, widget.name, widget); + widget.skeleton.postEvent( + SkeletonEvents.WIDGET_SHOW, + widget.name, + widget, + ); } else { - widget.skeleton.postEvent(SkeletonEvents.WIDGET_SHOW, widget.name, widget); + widget.skeleton.postEvent( + SkeletonEvents.WIDGET_SHOW, + widget.name, + widget, + ); } } } @@ -349,9 +402,17 @@ export class WidgetView extends Component<{ widget: IWidget }> { if (currentDisabled !== this.lastDisabled) { this.lastDisabled = currentDisabled; if (this.lastDisabled) { - widget.skeleton.postEvent(SkeletonEvents.WIDGET_DISABLE, widget.name, widget); + widget.skeleton.postEvent( + SkeletonEvents.WIDGET_DISABLE, + widget.name, + widget, + ); } else { - widget.skeleton.postEvent(SkeletonEvents.WIDGET_ENABLE, widget.name, widget); + widget.skeleton.postEvent( + SkeletonEvents.WIDGET_ENABLE, + widget.name, + widget, + ); } } } diff --git a/packages/editor-skeleton/src/components/widget-views/panel-operation-row.tsx b/packages/workbench/src/components/widget-views/panel-operation-row.tsx similarity index 100% rename from packages/editor-skeleton/src/components/widget-views/panel-operation-row.tsx rename to packages/workbench/src/components/widget-views/panel-operation-row.tsx diff --git a/packages/workbench/src/context.ts b/packages/workbench/src/context.ts new file mode 100644 index 000000000..ee213e886 --- /dev/null +++ b/packages/workbench/src/context.ts @@ -0,0 +1,4 @@ +import { createContext } from 'react'; +import { Skeleton } from './skeleton'; + +export const SkeletonContext = createContext<Skeleton>({} as any); diff --git a/packages/editor-skeleton/src/icons/arrow.tsx b/packages/workbench/src/icons/arrow.tsx similarity index 100% rename from packages/editor-skeleton/src/icons/arrow.tsx rename to packages/workbench/src/icons/arrow.tsx diff --git a/packages/editor-skeleton/src/icons/clear.tsx b/packages/workbench/src/icons/clear.tsx similarity index 100% rename from packages/editor-skeleton/src/icons/clear.tsx rename to packages/workbench/src/icons/clear.tsx diff --git a/packages/editor-skeleton/src/icons/convert.tsx b/packages/workbench/src/icons/convert.tsx similarity index 100% rename from packages/editor-skeleton/src/icons/convert.tsx rename to packages/workbench/src/icons/convert.tsx diff --git a/packages/editor-skeleton/src/icons/exit.tsx b/packages/workbench/src/icons/exit.tsx similarity index 100% rename from packages/editor-skeleton/src/icons/exit.tsx rename to packages/workbench/src/icons/exit.tsx diff --git a/packages/editor-skeleton/src/icons/fix.tsx b/packages/workbench/src/icons/fix.tsx similarity index 100% rename from packages/editor-skeleton/src/icons/fix.tsx rename to packages/workbench/src/icons/fix.tsx diff --git a/packages/editor-skeleton/src/icons/float.tsx b/packages/workbench/src/icons/float.tsx similarity index 100% rename from packages/editor-skeleton/src/icons/float.tsx rename to packages/workbench/src/icons/float.tsx diff --git a/packages/editor-skeleton/src/icons/slot.tsx b/packages/workbench/src/icons/slot.tsx similarity index 100% rename from packages/editor-skeleton/src/icons/slot.tsx rename to packages/workbench/src/icons/slot.tsx diff --git a/packages/editor-skeleton/src/icons/variable.tsx b/packages/workbench/src/icons/variable.tsx similarity index 100% rename from packages/editor-skeleton/src/icons/variable.tsx rename to packages/workbench/src/icons/variable.tsx diff --git a/packages/editor-skeleton/src/index.ts b/packages/workbench/src/index.ts similarity index 100% rename from packages/editor-skeleton/src/index.ts rename to packages/workbench/src/index.ts diff --git a/packages/editor-skeleton/src/layouts/bottom-area.tsx b/packages/workbench/src/layouts/bottom-area.tsx similarity index 100% rename from packages/editor-skeleton/src/layouts/bottom-area.tsx rename to packages/workbench/src/layouts/bottom-area.tsx diff --git a/packages/editor-skeleton/src/layouts/index.ts b/packages/workbench/src/layouts/index.ts similarity index 100% rename from packages/editor-skeleton/src/layouts/index.ts rename to packages/workbench/src/layouts/index.ts diff --git a/packages/editor-skeleton/src/layouts/left-area.tsx b/packages/workbench/src/layouts/left-area.tsx similarity index 100% rename from packages/editor-skeleton/src/layouts/left-area.tsx rename to packages/workbench/src/layouts/left-area.tsx diff --git a/packages/editor-skeleton/src/layouts/left-fixed-pane.tsx b/packages/workbench/src/layouts/left-fixed-pane.tsx similarity index 100% rename from packages/editor-skeleton/src/layouts/left-fixed-pane.tsx rename to packages/workbench/src/layouts/left-fixed-pane.tsx diff --git a/packages/editor-skeleton/src/layouts/left-float-pane.tsx b/packages/workbench/src/layouts/left-float-pane.tsx similarity index 100% rename from packages/editor-skeleton/src/layouts/left-float-pane.tsx rename to packages/workbench/src/layouts/left-float-pane.tsx diff --git a/packages/editor-skeleton/src/layouts/main-area.tsx b/packages/workbench/src/layouts/main-area.tsx similarity index 100% rename from packages/editor-skeleton/src/layouts/main-area.tsx rename to packages/workbench/src/layouts/main-area.tsx diff --git a/packages/editor-skeleton/src/layouts/right-area.tsx b/packages/workbench/src/layouts/right-area.tsx similarity index 100% rename from packages/editor-skeleton/src/layouts/right-area.tsx rename to packages/workbench/src/layouts/right-area.tsx diff --git a/packages/editor-skeleton/src/layouts/sub-top-area.tsx b/packages/workbench/src/layouts/sub-top-area.tsx similarity index 100% rename from packages/editor-skeleton/src/layouts/sub-top-area.tsx rename to packages/workbench/src/layouts/sub-top-area.tsx diff --git a/packages/editor-skeleton/src/layouts/theme.less b/packages/workbench/src/layouts/theme.less similarity index 100% rename from packages/editor-skeleton/src/layouts/theme.less rename to packages/workbench/src/layouts/theme.less diff --git a/packages/editor-skeleton/src/layouts/toolbar.tsx b/packages/workbench/src/layouts/toolbar.tsx similarity index 100% rename from packages/editor-skeleton/src/layouts/toolbar.tsx rename to packages/workbench/src/layouts/toolbar.tsx diff --git a/packages/editor-skeleton/src/layouts/top-area.tsx b/packages/workbench/src/layouts/top-area.tsx similarity index 100% rename from packages/editor-skeleton/src/layouts/top-area.tsx rename to packages/workbench/src/layouts/top-area.tsx diff --git a/packages/editor-skeleton/src/layouts/workbench.less b/packages/workbench/src/layouts/workbench.less similarity index 100% rename from packages/editor-skeleton/src/layouts/workbench.less rename to packages/workbench/src/layouts/workbench.less diff --git a/packages/editor-skeleton/src/layouts/workbench.tsx b/packages/workbench/src/layouts/workbench.tsx similarity index 91% rename from packages/editor-skeleton/src/layouts/workbench.tsx rename to packages/workbench/src/layouts/workbench.tsx index 876cfa530..3efd624c6 100644 --- a/packages/editor-skeleton/src/layouts/workbench.tsx +++ b/packages/workbench/src/layouts/workbench.tsx @@ -1,7 +1,8 @@ import { Component } from 'react'; -import { TipContainer, observer } from '@alilc/lowcode-editor-core'; +import { observer } from '@alilc/lowcode-editor-core'; +import { TipContainer } from '@alilc/lowcode-designer'; import classNames from 'classnames'; -import { ISkeleton } from '../skeleton'; +import { Skeleton } from '../skeleton'; import TopArea from './top-area'; import LeftArea from './left-area'; import LeftFixedPane from './left-fixed-pane'; @@ -17,7 +18,7 @@ import './workbench.less'; @observer export class Workbench extends Component<{ - skeleton: ISkeleton; + skeleton: Skeleton; config?: EditorConfig; components?: PluginClassSet; className?: string; diff --git a/packages/designer/src/less-variables.less b/packages/workbench/src/less-variables.less similarity index 100% rename from packages/designer/src/less-variables.less rename to packages/workbench/src/less-variables.less diff --git a/packages/editor-skeleton/src/locale/en-US.json b/packages/workbench/src/locale/en-US.json similarity index 100% rename from packages/editor-skeleton/src/locale/en-US.json rename to packages/workbench/src/locale/en-US.json diff --git a/packages/designer/src/locale/index.ts b/packages/workbench/src/locale/index.ts similarity index 100% rename from packages/designer/src/locale/index.ts rename to packages/workbench/src/locale/index.ts diff --git a/packages/editor-skeleton/src/locale/zh-CN.json b/packages/workbench/src/locale/zh-CN.json similarity index 100% rename from packages/editor-skeleton/src/locale/zh-CN.json rename to packages/workbench/src/locale/zh-CN.json diff --git a/packages/designer/src/module.d.ts b/packages/workbench/src/module.d.ts similarity index 100% rename from packages/designer/src/module.d.ts rename to packages/workbench/src/module.d.ts diff --git a/packages/editor-skeleton/src/register-defaults.ts b/packages/workbench/src/register-defaults.ts similarity index 100% rename from packages/editor-skeleton/src/register-defaults.ts rename to packages/workbench/src/register-defaults.ts diff --git a/packages/editor-skeleton/src/skeleton.ts b/packages/workbench/src/skeleton.ts similarity index 99% rename from packages/editor-skeleton/src/skeleton.ts rename to packages/workbench/src/skeleton.ts index 00ca249c1..b79089dd3 100644 --- a/packages/editor-skeleton/src/skeleton.ts +++ b/packages/workbench/src/skeleton.ts @@ -44,8 +44,6 @@ export enum SkeletonEvents { WIDGET_ENABLE = 'skeleton.widget.enable', } -export interface ISkeleton extends Skeleton {} - export class Skeleton implements Omit<IPublicApiSkeleton, 'showPanel' | 'hidePanel' | diff --git a/packages/editor-skeleton/src/transducers/addon-combine.ts b/packages/workbench/src/transducers/addon-combine.ts similarity index 100% rename from packages/editor-skeleton/src/transducers/addon-combine.ts rename to packages/workbench/src/transducers/addon-combine.ts diff --git a/packages/editor-skeleton/src/transducers/parse-func.ts b/packages/workbench/src/transducers/parse-func.ts similarity index 100% rename from packages/editor-skeleton/src/transducers/parse-func.ts rename to packages/workbench/src/transducers/parse-func.ts diff --git a/packages/editor-skeleton/src/transducers/parse-props.ts b/packages/workbench/src/transducers/parse-props.ts similarity index 100% rename from packages/editor-skeleton/src/transducers/parse-props.ts rename to packages/workbench/src/transducers/parse-props.ts diff --git a/packages/editor-skeleton/src/types.ts b/packages/workbench/src/types.ts similarity index 100% rename from packages/editor-skeleton/src/types.ts rename to packages/workbench/src/types.ts diff --git a/packages/editor-skeleton/src/widget/dock.ts b/packages/workbench/src/widget/dock.ts similarity index 96% rename from packages/editor-skeleton/src/widget/dock.ts rename to packages/workbench/src/widget/dock.ts index 35e2d5f80..4277308ab 100644 --- a/packages/editor-skeleton/src/widget/dock.ts +++ b/packages/workbench/src/widget/dock.ts @@ -3,7 +3,7 @@ import { makeObservable, observable } from '@alilc/lowcode-editor-core'; import { uniqueId, createContent } from '@alilc/lowcode-utils'; import { getEvent } from '../../../engine/src/shell/api/event'; import { DockConfig } from '../types'; -import { ISkeleton } from '../skeleton'; +import { Skeleton } from '../skeleton'; import { DockView, WidgetView } from '../components/widget-views'; import { IWidget } from './widget'; @@ -60,7 +60,7 @@ export class Dock implements IWidget { } constructor( - readonly skeleton: ISkeleton, + readonly skeleton: Skeleton, readonly config: DockConfig, ) { makeObservable(this); diff --git a/packages/editor-skeleton/src/widget/index.ts b/packages/workbench/src/widget/index.ts similarity index 100% rename from packages/editor-skeleton/src/widget/index.ts rename to packages/workbench/src/widget/index.ts diff --git a/packages/editor-skeleton/src/widget/panel-dock.ts b/packages/workbench/src/widget/panel-dock.ts similarity index 100% rename from packages/editor-skeleton/src/widget/panel-dock.ts rename to packages/workbench/src/widget/panel-dock.ts diff --git a/packages/editor-skeleton/src/widget/panel.ts b/packages/workbench/src/widget/panel.ts similarity index 100% rename from packages/editor-skeleton/src/widget/panel.ts rename to packages/workbench/src/widget/panel.ts diff --git a/packages/editor-skeleton/src/widget/stage.ts b/packages/workbench/src/widget/stage.ts similarity index 100% rename from packages/editor-skeleton/src/widget/stage.ts rename to packages/workbench/src/widget/stage.ts diff --git a/packages/editor-skeleton/src/widget/utils.ts b/packages/workbench/src/widget/utils.ts similarity index 100% rename from packages/editor-skeleton/src/widget/utils.ts rename to packages/workbench/src/widget/utils.ts diff --git a/packages/editor-skeleton/src/widget/widget-container.ts b/packages/workbench/src/widget/widget-container.ts similarity index 100% rename from packages/editor-skeleton/src/widget/widget-container.ts rename to packages/workbench/src/widget/widget-container.ts diff --git a/packages/editor-skeleton/src/widget/widget.ts b/packages/workbench/src/widget/widget.ts similarity index 100% rename from packages/editor-skeleton/src/widget/widget.ts rename to packages/workbench/src/widget/widget.ts diff --git a/packages/editor-skeleton/tests/widget/utils.test.ts b/packages/workbench/tests/widget/utils.test.ts similarity index 100% rename from packages/editor-skeleton/tests/widget/utils.test.ts rename to packages/workbench/tests/widget/utils.test.ts diff --git a/packages/editor-skeleton/tsconfig.declaration.json b/packages/workbench/tsconfig.declaration.json similarity index 100% rename from packages/editor-skeleton/tsconfig.declaration.json rename to packages/workbench/tsconfig.declaration.json diff --git a/packages/editor-skeleton/tsconfig.json b/packages/workbench/tsconfig.json similarity index 100% rename from packages/editor-skeleton/tsconfig.json rename to packages/workbench/tsconfig.json diff --git a/packages/editor-skeleton/vite.config.ts b/packages/workbench/vite.config.ts similarity index 100% rename from packages/editor-skeleton/vite.config.ts rename to packages/workbench/vite.config.ts diff --git a/packages/workbench/vitest.config.ts b/packages/workbench/vitest.config.ts new file mode 100644 index 000000000..e69de29bb diff --git a/vite.base.config.ts b/vite.base.config.ts index 77ad16e74..5a67d9f3f 100644 --- a/vite.base.config.ts +++ b/vite.base.config.ts @@ -1,7 +1,7 @@ import { defineConfig, LibraryFormats } from 'vite'; import { env, cwd } from 'node:process'; import { resolve } from 'node:path'; -import { readFile } from 'node:fs/promises' +import { readFile } from 'node:fs/promises'; import react from '@vitejs/plugin-react'; interface Options { @@ -11,16 +11,23 @@ interface Options { externalDeps?: boolean; } -const resolvePath = (path: string) => resolve(cwd(), path) -const isProduction = !!env['PROD'] +const resolvePath = (path: string) => resolve(cwd(), path); +const isProduction = !!env['PROD']; -export default async ({ name, entry = 'src/index.ts', defaultFormats = ['es'], externalDeps = true }: Options) => { +export default async ({ + name, + entry = 'src/index.ts', + defaultFormats = ['es'], + externalDeps = true, +}: Options) => { const formats = (env['FORMATS']?.split(',') ?? defaultFormats) as LibraryFormats[]; let externals: string[] = []; if (externalDeps) { - const { peerDependencies = {}, devDependencies = {} } = await getPackageJson(resolvePath('package.json')); + const { peerDependencies = {}, devDependencies = {} } = await getPackageJson( + resolvePath('package.json'), + ); externals = [...Object.keys(peerDependencies), ...Object.keys(devDependencies)]; } @@ -35,8 +42,8 @@ export default async ({ name, entry = 'src/index.ts', defaultFormats = ['es'], e minify: isProduction, sourcemap: isProduction ? false : 'inline', rollupOptions: { - external: externals - } + external: externals, + }, }, plugins: [react()], });