refactor: remove useless codes

This commit is contained in:
1ncounter 2024-05-11 10:32:30 +08:00
parent 1f1da44199
commit 8510f998fe
367 changed files with 3769 additions and 26165 deletions

10
.prettierrc Normal file
View File

@ -0,0 +1,10 @@
{
"printWidth": 100,
"tabWidth": 2,
"semi": true,
"jsxSingleQuote": false,
"singleQuote": true,
"arrowParens": "always",
"endOfLine": "lf",
"trailingComma": "all"
}

View File

@ -621,7 +621,7 @@ component
| docUrl | 组件文档链接 | String | 否 |
| screenshot | 组件快照 | String | 否 |
| icon | 组件的小图标 | String (URL) | 是 |
| tags | 组件标签 | String | 是 |
| tags | 组件标签 | String[] | 是 |
| keywords | 组件关键词,用于搜索联想 | String | 是 |
| devMode | 组件研发模式 | String  (proCode,lowCode) | 是 |
| npm | npm 源引入完整描述对象 | Object | 否 |
@ -634,7 +634,7 @@ component
| snippets | 内容为组件不同状态下的低代码 schema (可以有多个),用户从组件面板拖入组件到设计器时会向页面 schema 中插入 snippets 中定义的组件低代码 schema | Object[] | 否 |
| group | 用于描述当前组件位于组件面板的哪个 tab | String | 否 |
| category | 用于描述组件位于组件面板同一 tab 的哪个区域 | String | 否 |
| priority | 用于描述组件在同一 category 中的排序 | String | 否 |
| priority | 用于描述组件在同一 category 中的排序 | number | 否 |
##### 2.2.2.3 组件属性信息 props (A)
@ -1177,132 +1177,6 @@ export interface ComponentDescription { // 组件描述协议,通过 npm 中
}
```
#### 2.2.3 资产包协议
##### 2.2.3.1 协议结构
协议最顶层结构如下,包含 5 方面的描述内容:
- version { String } 当前协议版本号
- packages{ Array } 低代码编辑器中加载的资源列表
- components { Array } 所有组件的描述协议列表
- sort { Object } 用于描述组件面板中的 tab 和 category
##### 2.2.3.2 version (A)
定义当前协议 schema 的版本号;
| 根属性名称 | 类型 | 说明 | 变量支持 | 默认值 |
| ---------- | ------ | ---------- | -------- | ------ |
| version | String | 协议版本号 | - | 1.0.0 |
##### 2.2.3.3 packages (A)
定义低代码编辑器中加载的资源列表,包含公共库和组件 (库) cdn 资源等;
| 字段 | 字段描述 | 字段类型 | 备注 |
| ----------------------- | --------------------------------------------------- | --------------- | -------------------------------- |
| packages[].title? (A) | 资源标题 | String | 资源标题 |
| packages[].package (A) | npm 包名 | String | 组件资源唯一标识 |
| packages[].version(A) | npm 包版本号 | String | 组件资源版本号 |
| packages[].library(A) | 作为全局变量引用时的名称,用来定义全局变量名 | String | 低代码引擎通过该字段获取组件实例 |
| packages[].editUrls (A) | 组件编辑态视图打包后的 CDN url 列表,包含 js 和 css | Array\<String\> | 低代码引擎编辑器会加载这些 url |
| packages[].urls (AA) | 组件渲染态视图打包后的 CDN url 列表,包含 js 和 css | Array\<String\> | 低代码引擎渲染模块会加载这些 url |
描述举例:
```json
{
"packages": [
{
"package": "moment",
"version": "2.24.0",
"urls": ["https://g.alicdn.com/mylib/moment/2.24.0/min/moment.min.js"],
"library": "moment"
},
{
"package": "lodash",
"library": "_",
"urls": ["https://g.alicdn.com/platform/c/lodash/4.6.1/lodash.min.js"]
},
{
"title": "fusion 组件库",
"package": "@alifd/next",
"version": "1.24.18",
"urls": [
"https://g.alicdn.com/code/lib/alifd__next/1.24.18/next.min.css",
"https://g.alicdn.com/code/lib/alifd__next/1.24.18/next-with-locales.min.js"
],
"library": "Next"
},
{
"package": "@alilc/lowcode-materials",
"version": "1.0.0",
"library": "AlilcLowcodeMaterials",
"urls": [
"https://alifd.alicdn.com/npm/@alilc/lowcode-materials@1.0.0/dist/AlilcLowcodeMaterials.js",
"https://alifd.alicdn.com/npm/@alilc/lowcode-materials@1.0.0/dist/AlilcLowcodeMaterials.css"
],
"editUrls": [
"https://alifd.alicdn.com/npm/@alilc/lowcode-materials@1.0.0/build/lowcode/view.js",
"https://alifd.alicdn.com/npm/@alilc/lowcode-materials@1.0.0/build/lowcode/view.css"
]
},
{
"package": "@alifd/fusion-ui",
"version": "1.0.0",
"library": "AlifdFusionUi",
"urls": [
"https://alifd.alicdn.com/npm/@alifd/fusion-ui@1.0.0/build/lowcode/view.js",
"https://alifd.alicdn.com/npm/@alifd/fusion-ui@1.0.0/build/lowcode/view.css"
],
"editUrls": [
"https://alifd.alicdn.com/npm/@alifd/fusion-ui@1.0.0/build/lowcode/view.js",
"https://alifd.alicdn.com/npm/@alifd/fusion-ui@1.0.0/build/lowcode/view.css"
]
}
]
}
```
##### 2.2.3.4 components (A)
定义所有组件的描述协议列表,组件描述协议遵循本规范章节 2.2.2 的定义;
##### 2.2.3.5 sort (A)
定义组件列表分组
| 根属性名称 | 类型 | 说明 | 变量支持 | 默认值 |
| ----------------- | -------- | -------------------------------------------------------------------------------------------- | -------- | ---------------------------------------- |
| sort.groupList | String[] | 组件分组,用于组件面板 tab 展示 | - | ['精选组件', '原子组件'] |
| sort.categoryList | String[] | 组件面板中同一个 tab 下的不同区间用 category 区分category 的排序依照 categoryList 顺序排列 | - | ['通用', '数据展示', '表格类', '表单类'] |
##### 2.2.3.6 TypeScript 定义
```TypeScript
export interface ComponentSort {
groupList?: String[]; // 用于描述组件面板的 tab 项及其排序,例如:["精选组件", "原子组件"]
categoryList?: String[]; // 组件面板中同一个 tab 下的不同区间用 category 区分category 的排序依照 categoryList 顺序排列;
}
export interface Assets {
version: string; // 资产包协议版本号
packages?: Array<Package>; // 大包列表external与package的概念相似融合在一起
components: Array<ComponentDescription> | Array<RemoteComponentDescription>; // 所有组件的描述协议列表
componentList?: ComponentCategory[]; // 【待废弃】组件分类列表,用来描述物料面板
sort: ComponentSort; // 新增字段,用于描述组件面板中的 tab 和 category
}
export interface RemoteComponentDescription {
exportName: string; // 组件描述导出名字,可以通过 window[exportName] 获取到组件描述的 Object 内容;
url: string; // 组件描述的资源链接;
package: { // 组件(库)的 npm 信息;
npm: string;
}
}
```
## 3 物料规范 - 区块规范
### 3.1 源码规范
@ -1313,10 +1187,11 @@ export interface RemoteComponentDescription {
```html
block/ ├── build │   ├── index.css // 【编译生成】 │ ├── index.html //
【编译生成】【必选】可直接预览文件 │   ├── index.js // 【编译生成】 │   └── views // 【3A
编译生成】html2sketch │   ├── block_view1.html // 【3A 编译生成】给 sketch 用的 html │   └──
block_view1.png // 【3A 编译生成】截图 ├── src // 【必选】区块源码 │ ├── index.jsx // 【必选】入口 │
└── index.module.scss // 【可选】如有样式请使用 CSS Modules 避免冲突 ├── README.md //
【编译生成】【必选】可直接预览文件 │   ├── index.js // 【编译生成】 │   └──
views // 【3A 编译生成】html2sketch │   ├── block_view1.html // 【3A
编译生成】给 sketch 用的 html │   └── block_view1.png // 【3A 编译生成】截图 ├──
src // 【必选】区块源码 │ ├── index.jsx // 【必选】入口 │ └── index.module.scss
// 【可选】如有样式请使用 CSS Modules 避免冲突 ├── README.md //
【可选】无格式要求 └── package.json // 【必选】
```
@ -1471,23 +1346,28 @@ block_view1.png // 【3A 编译生成】截图 ├── src // 【必选】区
与标准源码 build-scripts 对齐
```html
├── META/ # 低代码元数据信息,用于多分支冲突解决、数据回滚等功能 ├── build │   ├── index.css #
【编译生成】 │ ├── index.html # 【编译生成】【必选】可直接预览文件 │   ├── index.js # 【编译生成】
│   └── views # 【3A 编译生成】html2sketch │   ├── page1.html # 【3A 编译生成】给 sketch 用的 html
│   └── page1.png # 【3A 编译生成】截图 ├── public/ # 静态文件,构建时会 copy 到 build/ 目录 │ ├──
index.html # 应用入口 HTML │ └── favicon.png # Favicon ├── src/ │ ├── components/ #
应用内的低代码业务组件 │ │ └── GuideComponent/ │ │ ├── index.js # 组件入口 │ │ ├── components.js #
组件依赖的其他组件 │ │ ├── schema.js # schema 描述 │ │ └── index.scss # css 样式 │ ├── pages/ # 页面
│ │ └── HomePage/ # Home 页面 │ │ ├── index.js # 页面入口 │ │ └── index.scss # css 样式 │ ├──
layouts/ │ │ └── BasicLayout/ # layout 组件名称 │ │ ├── index.js # layout 入口 │ │ ├── components.js
# layout 组件依赖的其他组件 │ │ ├── schema.js # layout schema 描述 │ │ └── index.scss # layout css
样式 │ ├── config/ # 配置信息 │ │ ├── components.js # 应用上下文所有组件 │ │ ├── routes.js #
页面路由列表 │ │ └── constants.js # 全局常量定义 │ │ └── app.js # 应用配置文件 │ ├── utils/ # 工具库
│ │ └── index.js # 应用第三方扩展函数 │ ├── stores/ # [可选] 全局状态管理 │ │ └── user.js │ ├──
locales/ # [可选] 国际化资源 │ │ ├── en-US │ │ └── zh-CN │ ├── global.scss # 全局样式 │ └──
index.jsx # 应用入口脚本,依赖 config/routes.js 的路由配置动态生成路由; ├── webpack.config.js #
项目工程配置,包含插件配置及自定义 `webpack` 配置等 ├── README.md ├── package.json ├── .editorconfig
├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .stylelintignore └── .stylelintrc.js
├── META/ # 低代码元数据信息,用于多分支冲突解决、数据回滚等功能 ├── build │  
├── index.css # 【编译生成】 │ ├── index.html #
【编译生成】【必选】可直接预览文件 │   ├── index.js # 【编译生成】 │   └── views
# 【3A 编译生成】html2sketch │   ├── page1.html # 【3A 编译生成】给 sketch 用的
html │   └── page1.png # 【3A 编译生成】截图 ├── public/ # 静态文件,构建时会
copy 到 build/ 目录 │ ├── index.html # 应用入口 HTML │ └── favicon.png # Favicon
├── src/ │ ├── components/ # 应用内的低代码业务组件 │ │ └── GuideComponent/ │ │
├── index.js # 组件入口 │ │ ├── components.js # 组件依赖的其他组件 │ │ ├──
schema.js # schema 描述 │ │ └── index.scss # css 样式 │ ├── pages/ # 页面 │ │
└── HomePage/ # Home 页面 │ │ ├── index.js # 页面入口 │ │ └── index.scss # css
样式 │ ├── layouts/ │ │ └── BasicLayout/ # layout 组件名称 │ │ ├── index.js #
layout 入口 │ │ ├── components.js # layout 组件依赖的其他组件 │ │ ├── schema.js
# layout schema 描述 │ │ └── index.scss # layout css 样式 │ ├── config/ #
配置信息 │ │ ├── components.js # 应用上下文所有组件 │ │ ├── routes.js #
页面路由列表 │ │ └── constants.js # 全局常量定义 │ │ └── app.js # 应用配置文件 │
├── utils/ # 工具库 │ │ └── index.js # 应用第三方扩展函数 │ ├── stores/ # [可选]
全局状态管理 │ │ └── user.js │ ├── locales/ # [可选] 国际化资源 │ │ ├── en-US │
│ └── zh-CN │ ├── global.scss # 全局样式 │ └── index.jsx # 应用入口脚本,依赖
config/routes.js 的路由配置动态生成路由; ├── webpack.config.js #
项目工程配置,包含插件配置及自定义 `webpack` 配置等 ├── README.md ├──
package.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore
├── .stylelintignore └── .stylelintrc.js
```
##### 入口文件

View File

@ -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',
},
});

View File

@ -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",

View File

@ -0,0 +1,36 @@
import { it, describe, expect } from 'vitest';
import { lazyInject, provide, initInstantiation } from '../src/instantiation';
interface Warrior {
fight(): string;
}
interface Weapon {
hit(): string;
}
@provide(Katana)
class Katana implements Weapon {
public hit() {
return 'cut!';
}
}
@provide(Ninja)
class Ninja implements Warrior {
@lazyInject(Katana)
private _katana: Weapon;
public fight() {
return this._katana.hit();
}
}
initInstantiation();
describe('', () => {
it('works', () => {
const n = new Ninja();
expect(n.fight()).toBe('cut!');
});
});

View File

@ -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"
},

View File

@ -0,0 +1,50 @@
export interface Command {
/**
*
* commandName
* 使commandScope:commandName (commandScope meta )
*/
name: string;
/**
*
*/
description?: string;
/**
*
*/
handler: (...args: any[]) => void | Promise<void>;
}
export class Commands {
/**
*
*/
registerCommand(command: Command): void;
/**
*
*/
unregisterCommand(name: string): void;
/**
*
*/
executeCommand(name: string, args?: IPublicTypeCommandHandlerArgs): void;
/**
*
*/
batchExecuteCommand(commands: { name: string; args?: IPublicTypeCommandHandlerArgs }[]): void;
/**
*
*/
listCommands(): IPublicTypeListCommand[];
/**
*
*/
onCommandError(callback: (name: string, error: Error) => void): void;
}

View File

@ -0,0 +1,174 @@
import { get as lodashGet, isPlainObject } from 'lodash-es';
import { createLogger, type PlainObject, invariant } from '@alilc/lowcode-shared';
const logger = createLogger({ level: 'log', bizName: 'config' });
// this default behavior will be different later
const STRICT_PLUGIN_MODE_DEFAULT = true;
interface ConfigurationOptions<Config extends PlainObject, K extends keyof Config = keyof Config> {
strictMode?: boolean;
setterValidator?: (key: K, value: Config[K]) => boolean | string;
}
export class Configuration<Config extends PlainObject, K extends keyof Config = keyof Config> {
#strictMode = STRICT_PLUGIN_MODE_DEFAULT;
#setterValidator: (key: K, value: Config[K]) => boolean | string = () => true;
#config: Config = {} as Config;
#waits = new Map<
K,
{
once?: boolean;
resolve: (data: any) => void;
}[]
>();
constructor(config: Config, options?: ConfigurationOptions<Config>) {
invariant(config, 'config must exist', 'Configuration');
this.#config = config;
const { strictMode, setterValidator } = options ?? {};
if (strictMode === false) {
this.#strictMode = false;
}
if (setterValidator) {
invariant(
typeof setterValidator === 'function',
'setterValidator must be a function',
'Configuration',
);
this.#setterValidator = setterValidator;
}
}
/**
* key
* @param key
*/
has(key: K): boolean {
return this.#config[key] !== undefined;
}
/**
* key
* @param key
* @param defaultValue
*/
get(key: K, defaultValue?: any): any {
return lodashGet(this.#config, key, defaultValue);
}
/**
* key
* @param key
* @param value
*/
set(key: K, value: any) {
if (this.#strictMode) {
const valid = this.#setterValidator(key, value);
if (valid === false || typeof valid === 'string') {
return logger.warn(
`failed to config ${key.toString()}, only predefined options can be set under strict mode, predefined options: `,
valid ? valid : '',
);
}
}
this.#config[key] = value;
this.notifyGot(key);
}
/**
* set
* @param config
*/
setConfig(config: Partial<Config>) {
if (isPlainObject(config)) {
Object.keys(config).forEach((key) => {
this.set(key as K, config[key]);
});
}
}
/**
* key
* Promise fullfill
* @param key
* @returns
*/
onceGot(key: K) {
const val = this.#config[key];
if (val !== undefined) {
return Promise.resolve(val);
}
return new Promise((resolve) => {
this.setWait(key, resolve, true);
});
}
/**
* key
* @param key
* @param fn
* @returns
*/
onGot(key: K, fn: (data: Config[K]) => void): () => void {
const val = this.#config[key];
if (val !== undefined) {
fn(val);
}
this.setWait(key, fn);
return () => {
this.delWait(key, fn);
};
}
notifyGot(key: K): void {
let waits = this.#waits.get(key);
if (!waits) {
return;
}
waits = waits.slice().reverse();
let i = waits.length;
while (i--) {
waits[i].resolve(this.get(key));
if (waits[i].once) {
waits.splice(i, 1);
}
}
if (waits.length > 0) {
this.#waits.set(key, waits);
} else {
this.#waits.delete(key);
}
}
setWait(key: K, resolve: (data: any) => void, once?: boolean) {
const waits = this.#waits.get(key);
if (waits) {
waits.push({ resolve, once });
} else {
this.#waits.set(key, [{ resolve, once }]);
}
}
delWait(key: K, fn: any) {
const waits = this.#waits.get(key);
if (!waits) {
return;
}
let i = waits.length;
while (i--) {
if (waits[i].resolve === fn) {
waits.splice(i, 1);
}
}
if (waits.length < 1) {
this.#waits.delete(key);
}
}
}

View File

@ -0,0 +1,2 @@
export * from './config';
export { Preference, userPreference } from './preference';

View File

@ -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();

View File

@ -1,88 +1,41 @@
/**
* key event helperhttps://www.toptal.com/developers/keycode
* key code table: https://www.toptal.com/developers/keycode/table
*/
import { isEqual } from 'lodash-es';
import { globalContext } from './di';
import {
IPublicTypeHotkeyCallback,
IPublicTypeHotkeyCallbackConfig,
IPublicTypeHotkeyCallbacks,
IPublicApiHotkey,
IPublicTypeDisposable,
} from '@alilc/lowcode-types';
import { type EventDisposable, Platform } from '@alilc/lowcode-shared';
interface KeyMap {
[key: number]: string;
}
type KeyboardEventKeyMapping = Record<string, string>;
interface CtrlKeyMap {
[key: string]: string;
}
interface ActionEvent {
interface KeyboardEventLike {
type: string;
}
interface HotkeyDirectMap {
[key: string]: IPublicTypeHotkeyCallback;
}
type KeyAction = 'keypress' | 'keydown' | 'keyup';
interface KeyInfo {
key: string;
modifiers: string[];
action: string;
action: KeyAction;
}
interface SequenceLevels {
[key: string]: number;
type SequenceLevels = Record<string, number>;
export type HotkeyCallback = (e: KeyboardEvent, combo?: string) => void | false | any;
export interface HotkeyCallbackConfig {
callback: HotkeyCallback;
modifiers: string[];
action: KeyAction;
seq?: string;
level?: number;
combo?: string;
}
const MAP: KeyMap = {
8: 'backspace',
9: 'tab',
13: 'enter',
16: 'shift',
17: 'ctrl',
18: 'alt',
20: 'capslock',
27: 'esc',
32: 'space',
33: 'pageup',
34: 'pagedown',
35: 'end',
36: 'home',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
45: 'ins',
46: 'del',
91: 'meta',
93: 'meta',
224: 'meta',
};
export type HotkeyCallbackConfigRecord = Record<string, HotkeyCallbackConfig[]>;
const KEYCODE_MAP: KeyMap = {
106: '*',
107: '+',
109: '-',
110: '.',
111: '/',
186: ';',
187: '=',
188: ',',
189: '-',
190: '.',
191: '/',
192: '`',
219: '[',
220: '\\',
221: ']',
222: '\'',
};
const SHIFT_MAP: CtrlKeyMap = {
const SHIFT_ALTERNATE_KEYS_MAP: KeyboardEventKeyMapping = {
'~': '`',
'!': '1',
'@': '2',
@ -97,91 +50,27 @@ const SHIFT_MAP: CtrlKeyMap = {
_: '-',
'+': '=',
':': ';',
'"': '\'',
// eslint-disable-next-line @stylistic/quotes
'"': "'",
'<': ',',
'>': '.',
'?': '/',
'|': '\\',
};
const SPECIAL_ALIASES: CtrlKeyMap = {
const SPECIAL_DEFINED_ALIASES: KeyboardEventKeyMapping = {
option: 'alt',
command: 'meta',
return: 'enter',
escape: 'esc',
plus: '+',
mod: /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl',
mod: Platform.isIOS ? 'meta' : 'ctrl',
};
let REVERSE_MAP: CtrlKeyMap;
/**
* loop through the f keys, f1 to f19 and add them to the map
* programatically
*/
for (let i = 1; i < 20; ++i) {
MAP[111 + i] = `f${i}`;
}
/**
* loop through to map numbers on the numeric keypad
*/
for (let i = 0; i <= 9; ++i) {
MAP[i + 96] = String(i);
}
/**
* takes the event and returns the key character
*/
function characterFromEvent(e: KeyboardEvent): string {
const keyCode = e.keyCode || e.which;
// for keypress events we should return the character as is
if (e.type === 'keypress') {
let character = String.fromCharCode(keyCode);
// if the shift key is not pressed then it is safe to assume
// that we want the character to be lowercase. this means if
// you accidentally have caps lock on then your key bindings
// will continue to work
//
// the only side effect that might not be desired is if you
// bind something like 'A' cause you want to trigger an
// event when capital A is pressed caps lock will no longer
// trigger the event. shift+a will though.
if (!e.shiftKey) {
character = character.toLowerCase();
}
return character;
}
// for non keypress events the special maps are needed
if (MAP[keyCode]) {
return MAP[keyCode];
}
if (KEYCODE_MAP[keyCode]) {
return KEYCODE_MAP[keyCode];
}
// if it is not in the special map
// with keydown and keyup events the character seems to always
// come in as an uppercase character whether you are pressing shift
// or not. we should make sure it is always lowercase for comparisons
// tips: Q29weXJpZ2h0IChjKSAyMDIwLXByZXNlbnQgQWxpYmFiYSBJbmMuIFYy
return String.fromCharCode(keyCode).toLowerCase();
}
interface KeypressEvent extends KeyboardEvent {
type: 'keypress';
}
function isPressEvent(e: KeyboardEvent | ActionEvent): e is KeypressEvent {
return e.type === 'keypress';
}
/**
* checks if two arrays are equal
*/
function modifiersMatch(modifiers1: string[], modifiers2: string[]): boolean {
return modifiers1.sort().join(',') === modifiers2.sort().join(',');
}
/**
* takes a key event and figures out what the modifiers are
*/
@ -191,15 +80,12 @@ function eventModifiers(e: KeyboardEvent): string[] {
if (e.shiftKey) {
modifiers.push('shift');
}
if (e.altKey) {
modifiers.push('alt');
}
if (e.ctrlKey) {
modifiers.push('ctrl');
}
if (e.metaKey) {
modifiers.push('meta');
}
@ -207,188 +93,55 @@ function eventModifiers(e: KeyboardEvent): string[] {
return modifiers;
}
/**
* determines if the keycode specified is a modifier key or not
*/
function isModifier(key: string): boolean {
return key === 'shift' || key === 'ctrl' || key === 'alt' || key === 'meta';
}
/**
* reverses the map lookup so that we can look for specific keys
* to see what can and can't use keypress
*
* @return {Object}
*/
function getReverseMap(): CtrlKeyMap {
if (!REVERSE_MAP) {
REVERSE_MAP = {};
for (const key in MAP) {
// pull out the numeric keypad from here cause keypress should
// be able to detect the keys from the character
if (Number(key) > 95 && Number(key) < 112) {
continue;
}
if (Object.prototype.hasOwnProperty.call(MAP, key)) {
REVERSE_MAP[MAP[key]] = key;
}
}
}
return REVERSE_MAP;
}
/**
* picks the best action based on the key combination
*/
function pickBestAction(key: string, modifiers: string[], action?: string): string {
// if no action was picked in we should try to pick the one
// that we think would work best for this key
if (!action) {
action = getReverseMap()[key] ? 'keydown' : 'keypress';
}
// modifier keys don't work as expected with keypress,
// switch to keydown
if (action === 'keypress' && modifiers.length) {
action = 'keydown';
}
return action;
}
/**
* Converts from a string key combination to an array
*
* @param {string} combination like "command+shift+l"
* @return {Array}
*/
function keysFromString(combination: string): string[] {
if (combination === '+') {
return ['+'];
}
combination = combination.replace(/\+{2}/g, '+plus');
return combination.split('+');
}
/**
* Gets info for a specific key combination
*
* @param combination key combination ("command+s" or "a" or "*")
*/
function getKeyInfo(combination: string, action?: string): KeyInfo {
let keys: string[] = [];
let key = '';
let i: number;
const modifiers: string[] = [];
// take the keys from this pattern and figure out what the actual
// pattern is all about
keys = keysFromString(combination);
for (i = 0; i < keys.length; ++i) {
key = keys[i];
// normalize key names
if (SPECIAL_ALIASES[key]) {
key = SPECIAL_ALIASES[key];
}
// if this is not a keypress event then we should
// be smart about using shift keys
// this will only work for US keyboards however
if (action && action !== 'keypress' && SHIFT_MAP[key]) {
key = SHIFT_MAP[key];
modifiers.push('shift');
}
// if this key is a modifier then add it to the list of modifiers
if (isModifier(key)) {
modifiers.push(key);
}
}
// depending on what the key combination is
// we will try to pick the best event for it
action = pickBestAction(key, modifiers, action);
return {
key,
modifiers,
action,
};
}
/**
* actually calls the callback function
*
* if your callback function returns false this will use the jquery
* convention - prevent default and stop propogation on the event
*/
function fireCallback(
callback: IPublicTypeHotkeyCallback,
e: KeyboardEvent,
combo?: string,
sequence?: string,
): void {
function fireCallback(callback: HotkeyCallback, e: KeyboardEvent, combo?: string): void {
try {
const workspace = globalContext.get('workspace');
const editor = workspace.isActive ? workspace.window?.editor : globalContext.get('editor');
const designer = editor?.get('designer');
const node = designer?.currentSelection?.getNodes()?.[0];
const npm = node?.componentMeta?.npm;
const selected =
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||
node?.componentMeta?.componentName ||
'';
if (callback(e, combo) === false) {
e.preventDefault();
e.stopPropagation();
}
editor?.eventBus.emit('hotkey.callback.call', {
callback,
e,
combo,
sequence,
selected,
});
} catch (err) {
console.error((err as Error).message);
}
}
export interface IHotKey extends Hotkey {}
export class Hotkey {
#resetTimer = 0;
#ignoreNextKeyup: boolean | string = false;
#ignoreNextKeypress = false;
#nextExpectedAction: boolean | string = false;
#isActivate = true;
export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
callBacks: IPublicTypeHotkeyCallbacks = {};
#sequenceLevels: SequenceLevels = {};
/**
*
*/
#callBackConfigRecord: HotkeyCallbackConfigRecord = {};
private directMap: HotkeyDirectMap = {};
private sequenceLevels: SequenceLevels = {};
private resetTimer = 0;
private ignoreNextKeyup: boolean | string = false;
private ignoreNextKeypress = false;
private nextExpectedAction: boolean | string = false;
private isActivate = true;
constructor(readonly viewName: string = 'global') {
this.mount(window);
active(): void {
this.#isActivate = true;
}
activate(activate: boolean): void {
this.isActivate = activate;
inactive() {
this.#isActivate = false;
}
mount(window: Window): IPublicTypeDisposable {
/**
*
* @param window window
*/
mount(window: Window): EventDisposable {
const { document } = window;
const handleKeyEvent = this.handleKeyEvent.bind(this);
document.addEventListener('keypress', handleKeyEvent, false);
document.addEventListener('keydown', handleKeyEvent, false);
document.addEventListener('keyup', handleKeyEvent, false);
return () => {
document.removeEventListener('keypress', handleKeyEvent, false);
document.removeEventListener('keydown', handleKeyEvent, false);
@ -396,42 +149,94 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
};
}
bind(combos: string[] | string, callback: IPublicTypeHotkeyCallback, action?: string): Hotkey {
/**
*
* bind hotkey/hotkeys,
* @param combos ['command + s'] ['ctrl + shift + s']
* @param callback
* @param action
*/
bind(combos: string[] | string, callback: HotkeyCallback, action?: KeyAction): Hotkey {
this.bindMultiple(Array.isArray(combos) ? combos : [combos], callback, action);
return this;
}
unbind(combos: string[] | string, callback: IPublicTypeHotkeyCallback, action?: string) {
/**
*
* bind hotkey/hotkeys,
* @param combos ['command + s'] ['ctrl + shift + s']
* @param callback
* @param action
*/
unbind(combos: string[] | string, callback: HotkeyCallback, action?: KeyAction) {
const combinations = Array.isArray(combos) ? combos : [combos];
combinations.forEach((combination) => {
const info: KeyInfo = getKeyInfo(combination, action);
const { key, modifiers } = info;
const idx = this.callBacks[key].findIndex((info) => {
const idx = this.#callBackConfigRecord[key].findIndex((info) => {
return isEqual(info.modifiers, modifiers) && info.callback === callback;
});
if (idx !== -1) {
this.callBacks[key].splice(idx, 1);
this.#callBackConfigRecord[key].splice(idx, 1);
}
});
}
/**
* resets all sequence counters except for the ones passed in
*/
private resetSequences(doNotReset?: SequenceLevels): void {
// doNotReset = doNotReset || {};
let activeSequences = false;
let key = '';
for (key in this.sequenceLevels) {
if (doNotReset && doNotReset[key]) {
activeSequences = true;
} else {
this.sequenceLevels[key] = 0;
private bindSingle(
combination: string,
callback: HotkeyCallback,
action?: KeyAction,
sequenceName?: string,
level?: number,
): void {
// make sure multiple spaces in a row become a single space
combination = combination.replace(/\s+/g, ' ');
const sequence: string[] = combination.split(' ');
// if this pattern is a sequence of keys then run through this method
// to reprocess each pattern one key at a time
if (sequence.length > 1) {
this.bindSequence(combination, sequence, callback, action);
return;
}
const info: KeyInfo = getKeyInfo(combination, action);
// make sure to initialize array if this is the first time
// a callback is added for this key
this.#callBackConfigRecord[info.key] ??= [];
// remove an existing match if there is one
this.getMatches(
info.key,
info.modifiers,
{ type: info.action },
sequenceName,
combination,
level,
);
// add this call back to the array
// if it is a sequence put it at the beginning
// if not put it at the end
//
// this is important because the way these are processed expects
// the sequence ones to come first
this.#callBackConfigRecord[info.key][sequenceName ? 'unshift' : 'push']({
callback,
modifiers: info.modifiers,
action: info.action,
seq: sequenceName,
level,
combo: combination,
});
}
if (!activeSequences) {
this.nextExpectedAction = false;
private bindMultiple(combinations: string[], callback: HotkeyCallback, action?: KeyAction) {
for (const item of combinations) {
this.bindSingle(item, callback, action);
}
}
@ -442,18 +247,18 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
private getMatches(
character: string,
modifiers: string[],
e: KeyboardEvent | ActionEvent,
e: KeyboardEvent | KeyboardEventLike,
sequenceName?: string,
combination?: string,
level?: number,
): IPublicTypeHotkeyCallbackConfig[] {
): HotkeyCallbackConfig[] {
let i: number;
let callback: IPublicTypeHotkeyCallbackConfig;
const matches: IPublicTypeHotkeyCallbackConfig[] = [];
const action: string = e.type;
let callback: HotkeyCallbackConfig;
const matches: HotkeyCallbackConfig[] = [];
const action = e.type as KeyAction;
// if there are no events related to this keycode
if (!this.callBacks[character]) {
if (!this.#callBackConfigRecord[character]) {
return [];
}
@ -464,12 +269,12 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
// loop through all callbacks for the key that was pressed
// and see if any of them match
for (i = 0; i < this.callBacks[character].length; ++i) {
callback = this.callBacks[character][i];
for (i = 0; i < this.#callBackConfigRecord[character].length; ++i) {
callback = this.#callBackConfigRecord[character][i];
// if a sequence name is not specified, but this is a sequence at
// the wrong level then move onto the next match
if (!sequenceName && callback.seq && this.sequenceLevels[callback.seq] !== callback.level) {
if (!sequenceName && callback.seq && this.#sequenceLevels[callback.seq] !== callback.level) {
continue;
}
@ -494,21 +299,35 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
const deleteSequence =
sequenceName && callback.seq === sequenceName && callback.level === level;
if (deleteCombo || deleteSequence) {
this.callBacks[character].splice(i, 1);
this.#callBackConfigRecord[character].splice(i, 1);
}
matches.push(callback);
}
}
return matches;
}
private handleKeyEvent(e: KeyboardEvent): void {
if (!this.#isActivate) return;
const character = e.key.toLowerCase();
// need to use === for the character check because the character can be 0
if (e.type === 'keyup' && this.#ignoreNextKeyup === character) {
this.#ignoreNextKeyup = false;
return;
}
this.handleKey(character, eventModifiers(e), e);
}
private handleKey(character: string, modifiers: string[], e: KeyboardEvent): void {
const callbacks: IPublicTypeHotkeyCallbackConfig[] = this.getMatches(character, modifiers, e);
const callbacks: HotkeyCallbackConfig[] = this.getMatches(character, modifiers, e);
let i: number;
const doNotReset: SequenceLevels = {};
let maxLevel = 0;
let processedSequenceCallback = false;
// Calculate the maxLevel for sequences so we can only execute the longest callback sequence
for (i = 0; i < callbacks.length; ++i) {
@ -517,6 +336,9 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
}
}
let processedSequenceCallback = false;
const doNotReset: SequenceLevels = {};
// loop through matching callbacks for this key event
for (i = 0; i < callbacks.length; ++i) {
// fire for all sequence callbacks
@ -541,7 +363,7 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
// keep a list of which sequences were matches for later
doNotReset[callbacks[i].seq || ''] = 1;
fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
fireCallback(callbacks[i].callback, e, callbacks[i].combo);
continue;
}
@ -552,55 +374,33 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
}
}
const ignoreThisKeypress = e.type === 'keypress' && this.ignoreNextKeypress;
if (e.type === this.nextExpectedAction && !isModifier(character) && !ignoreThisKeypress) {
const ignoreThisKeypress = e.type === 'keypress' && this.#ignoreNextKeypress;
if (e.type === this.#nextExpectedAction && !isModifier(character) && !ignoreThisKeypress) {
this.resetSequences(doNotReset);
}
this.ignoreNextKeypress = processedSequenceCallback && e.type === 'keydown';
}
private handleKeyEvent(e: KeyboardEvent): void {
console.log(e);
// debugger;
if (!this.isActivate) {
return;
}
const character = characterFromEvent(e);
// no character found then stop
if (!character) {
return;
}
// need to use === for the character check because the character can be 0
if (e.type === 'keyup' && this.ignoreNextKeyup === character) {
this.ignoreNextKeyup = false;
return;
}
this.handleKey(character, eventModifiers(e), e);
this.#ignoreNextKeypress = processedSequenceCallback && e.type === 'keydown';
}
private resetSequenceTimer(): void {
if (this.resetTimer) {
clearTimeout(this.resetTimer);
if (this.#resetTimer) {
clearTimeout(this.#resetTimer);
}
this.resetTimer = window.setTimeout(this.resetSequences, 1000);
this.#resetTimer = window.setTimeout(this.resetSequences, 1000);
}
private bindSequence(
combo: string,
keys: string[],
callback: IPublicTypeHotkeyCallback,
action?: string,
callback: HotkeyCallback,
action?: KeyAction,
): void {
// const self: any = this;
this.sequenceLevels[combo] = 0;
this.#sequenceLevels[combo] = 0;
const increaseSequence = (nextAction: string) => {
return () => {
this.nextExpectedAction = nextAction;
++this.sequenceLevels[combo];
this.#nextExpectedAction = nextAction;
++this.#sequenceLevels[combo];
this.resetSequenceTimer();
};
};
@ -608,81 +408,137 @@ export class Hotkey implements Omit<IPublicApiHotkey, 'bind' | 'callbacks'> {
fireCallback(callback, e, combo);
if (action !== 'keyup') {
this.ignoreNextKeyup = characterFromEvent(e);
this.#ignoreNextKeyup = e.key.toLowerCase();
}
setTimeout(this.resetSequences, 10);
};
for (let i = 0; i < keys.length; ++i) {
const isFinal = i + 1 === keys.length;
const wrappedCallback = isFinal
? callbackAndReset
: increaseSequence(action || getKeyInfo(keys[i + 1]).action);
this.bindSingle(keys[i], wrappedCallback, action, combo, i);
}
}
private bindSingle(
combination: string,
callback: IPublicTypeHotkeyCallback,
action?: string,
sequenceName?: string,
level?: number,
): void {
// store a direct mapped reference for use with HotKey.trigger
this.directMap[`${combination}:${action}`] = callback;
// make sure multiple spaces in a row become a single space
combination = combination.replace(/\s+/g, ' ');
const sequence: string[] = combination.split(' ');
// if this pattern is a sequence of keys then run through this method
// to reprocess each pattern one key at a time
if (sequence.length > 1) {
this.bindSequence(combination, sequence, callback, action);
return;
/**
* resets all sequence counters except for the ones passed in
*/
private resetSequences(doNotReset?: SequenceLevels): void {
// doNotReset = doNotReset || {};
let activeSequences = false;
let key = '';
for (key in this.#sequenceLevels) {
if (doNotReset && doNotReset[key]) {
activeSequences = true;
} else {
this.#sequenceLevels[key] = 0;
}
const info: KeyInfo = getKeyInfo(combination, action);
// make sure to initialize array if this is the first time
// a callback is added for this key
this.callBacks[info.key] = this.callBacks[info.key] || [];
// remove an existing match if there is one
this.getMatches(
info.key,
info.modifiers,
{ type: info.action },
sequenceName,
combination,
level,
);
// add this call back to the array
// if it is a sequence put it at the beginning
// if not put it at the end
//
// this is important because the way these are processed expects
// the sequence ones to come first
this.callBacks[info.key][sequenceName ? 'unshift' : 'push']({
callback,
modifiers: info.modifiers,
action: info.action,
seq: sequenceName,
level,
combo: combination,
});
}
private bindMultiple(
combinations: string[],
callback: IPublicTypeHotkeyCallback,
action?: string,
) {
for (const item of combinations) {
this.bindSingle(item, callback, action);
if (!activeSequences) {
this.#nextExpectedAction = false;
}
}
}
/**
* Gets info for a specific key combination
*
* @param combination key combination ("command+s" or "a" or "*")
* @param action optional, keyboard event type, eg: keypress key down
*/
function getKeyInfo(combination: string, action?: KeyAction): KeyInfo {
let keys: string[] = [];
let key = '';
let i: number;
const modifiers: string[] = [];
// take the keys from this pattern and figure out what the actual
// pattern is all about
keys = keysFromString(combination);
for (i = 0; i < keys.length; ++i) {
key = keys[i];
// normalize key names
if (SPECIAL_DEFINED_ALIASES[key]) {
key = SPECIAL_DEFINED_ALIASES[key];
}
// if this is not a keypress event then we should
// be smart about using shift keys
// this will only work for US keyboards however
if (action && action !== 'keypress' && SHIFT_ALTERNATE_KEYS_MAP[key]) {
key = SHIFT_ALTERNATE_KEYS_MAP[key];
modifiers.push('shift');
}
// if this key is a modifier then add it to the list of modifiers
if (isModifier(key)) {
modifiers.push(key);
}
}
// depending on what the key combination is
// we will try to pick the best event for it
action = pickBestAction(key, modifiers, action);
return {
key,
modifiers,
action,
};
}
/**
* Converts from a string key combination to an array
*
* @param {string} combination like "command+shift+l"
* @return {Array}
*/
function keysFromString(combination: string): string[] {
if (combination === '+') {
return ['+'];
}
combination = combination.toLowerCase().replace(/\+{2}/g, '+plus');
return combination.split('+');
}
/**
* determines if the keycode specified is a modifier key or not
*/
function isModifier(key: string): boolean {
return key === 'shift' || key === 'ctrl' || key === 'alt' || key === 'meta';
}
/**
* picks the best action based on the key combination
*/
function pickBestAction(key: string, modifiers: string[], action?: KeyAction): KeyAction {
// if no action was picked in we should try to pick the one
// that we think would work best for this key
if (!action) {
action = 'keydown';
}
// modifier keys don't work as expected with keypress,
// switch to keydown
if (action === 'keypress' && modifiers.length) {
action = 'keydown';
}
return action;
}
function isPressEvent(e: KeyboardEvent | KeyboardEventLike): e is KeypressEvent {
return e.type === 'keypress';
}
/**
* checks if two arrays are equal
*/
function modifiersMatch(modifiers1: string[], modifiers2: string[]): boolean {
return modifiers1.sort().join(',') === modifiers2.sort().join(',');
}

View File

@ -0,0 +1,4 @@
export * from './configuration';
export * from './hotkey';
export * from './intl';
export * from './instantiation';

View File

@ -0,0 +1,43 @@
import '@abraham/reflection';
import { Container, inject } from 'inversify';
import { fluentProvide, buildProviderModule } from 'inversify-binding-decorators';
export const iocContainer = new Container();
/**
* Identifies a service of type `T`.
*/
export interface ServiceIdentifier<T> {
(...args: any[]): void;
type: T;
}
type Constructor<T = any> = new (...args: any[]) => T;
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
const id = <any>(
function (target: Constructor, targetKey: string, indexOrPropertyDescriptor: any): any {
return inject(serviceId)(target, targetKey, indexOrPropertyDescriptor);
}
);
id.toString = () => serviceId;
return id;
}
export function Provide(serviceId: string, isSingleTon?: boolean) {
const ret = fluentProvide(serviceId.toString());
if (isSingleTon) {
return ret.inSingletonScope().done();
}
return ret.done();
}
export function createInstance<T extends Constructor>(App: T) {
return iocContainer.resolve<InstanceType<T>>(App);
}
export function bootstrapModules() {
iocContainer.load(buildProviderModule());
}

170
packages/core/src/intl.ts Normal file
View File

@ -0,0 +1,170 @@
import {
signal,
computed,
effect,
createLogger,
type Signal,
type I18nMap,
type ComputedSignal,
type PlainObject,
} from '@alilc/lowcode-shared';
import { createIntl, createIntlCache, type IntlShape as IntlFormatter } from '@formatjs/intl';
import { mapKeys } from 'lodash-es';
export { IntlFormatter };
const logger = createLogger({ level: 'warn', bizName: 'globalLocale' });
/**
* todo: key
*/
const STORED_LOCALE_KEY = 'ali-lowcode-config';
export type Locale = string;
export type IntlMessage = I18nMap[Locale];
export type IntlMessageRecord = I18nMap;
export class Intl {
#locale: Signal<Locale>;
#messageStore: Signal<IntlMessageRecord>;
#currentMessage: ComputedSignal<IntlMessage>;
#intlShape: IntlFormatter;
constructor(defaultLocale?: string, messages: IntlMessageRecord = {}) {
if (defaultLocale) {
defaultLocale = nomarlizeLocale(defaultLocale);
} else {
defaultLocale = initializeLocale();
}
const messageStore = mapKeys(messages, (_, key) => {
return nomarlizeLocale(key);
});
this.#locale = signal(defaultLocale);
this.#messageStore = signal(messageStore);
this.#currentMessage = computed(() => {
return this.#messageStore.value[this.#locale.value] ?? {};
});
effect(() => {
const cache = createIntlCache();
this.#intlShape = createIntl(
{
locale: this.#locale.value,
messages: this.#currentMessage.value,
},
cache,
);
});
}
getLocale() {
return this.#locale.value;
}
setLocale(locale: Locale) {
const nomarlizedLocale = nomarlizeLocale(locale);
try {
// store storage
let config = JSON.parse(localStorage.getItem(STORED_LOCALE_KEY) || '');
if (config && typeof config === 'object') {
config.locale = locale;
} else {
config = { locale };
}
localStorage.setItem(STORED_LOCALE_KEY, JSON.stringify(config));
} catch {
// ignore;
}
this.#locale.value = nomarlizedLocale;
}
addMessages(locale: Locale, messages: IntlMessage) {
locale = nomarlizeLocale(locale);
const original = this.#messageStore.value[locale];
this.#messageStore.value[locale] = Object.assign(original, messages);
}
getFormatter(): IntlFormatter {
return this.#intlShape;
}
}
function initializeLocale() {
let result: Locale | undefined;
let config: PlainObject = {};
try {
// store 1: config from storage
config = JSON.parse(localStorage.getItem(STORED_LOCALE_KEY) || '');
} catch {
// ignore;
}
if (config?.locale) {
result = (config.locale || '').replace('_', '-');
logger.debug(`getting locale from localStorage: ${result}`);
}
if (!result && navigator.language) {
// store 2: config from system
result = nomarlizeLocale(navigator.language);
}
if (!result) {
logger.warn(
'something when wrong when trying to get locale, use zh-CN as default, please check it out!',
);
result = 'zh-CN';
}
return result;
}
const navigatorLanguageMapping: Record<string, string> = {
en: 'en-US',
zh: 'zh-CN',
zt: 'zh-TW',
es: 'es-ES',
pt: 'pt-PT',
fr: 'fr-FR',
de: 'de-DE',
it: 'it-IT',
ru: 'ru-RU',
ja: 'ja-JP',
ko: 'ko-KR',
ar: 'ar-SA',
tr: 'tr-TR',
th: 'th-TH',
vi: 'vi-VN',
nl: 'nl-NL',
he: 'iw-IL',
id: 'in-ID',
pl: 'pl-PL',
hi: 'hi-IN',
uk: 'uk-UA',
ms: 'ms-MY',
tl: 'tl-PH',
};
/**
* nomarlize navigator.language or user input's locale
* eg: zh -> zh-CN, zh_CN -> zh-CN, zh-cn -> zh-CN
* @param target
*/
function nomarlizeLocale(target: Locale) {
if (navigatorLanguageMapping[target]) {
return navigatorLanguageMapping[target];
}
const replaced = target.replace('_', '-');
const splited = replaced.split('-').slice(0, 2);
splited[1] = splited[1].toUpperCase();
return splited.join('-');
}

View File

@ -3,5 +3,5 @@
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
"include": ["src", "__tests__"]
}

View File

@ -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:*",

View File

@ -1 +0,0 @@
内置模拟器主进程

View File

@ -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;
}

View File

@ -1,120 +0,0 @@
import * as React from 'react';
import { Component, Fragment, ReactElement, PureComponent } from 'react';
import classNames from 'classnames';
import { computed, observer, globalLocale } from '@alilc/lowcode-editor-core';
import { IPublicTypeI18nData, IPublicTypeTitleContent } from '@alilc/lowcode-types';
import { isI18nData } from '@alilc/lowcode-utils';
import { DropLocation } from '../../designer';
import { BuiltinSimulatorHost } from '../../builtin-simulator/host';
import { INode } from '../../document/node';
import { Title } from '../../widgets';
export class BorderContainerInstance extends PureComponent<{
title: IPublicTypeTitleContent;
rect: DOMRect | null;
scale: number;
scrollX: number;
scrollY: number;
}> {
render() {
const { title, rect, scale, scrollX, scrollY } = this.props;
if (!rect) {
return null;
}
const style = {
width: rect.width * scale,
height: rect.height * scale,
transform: `translate(${(scrollX + rect.left) * scale}px, ${(scrollY + rect.top) * scale}px)`,
};
const className = classNames('lc-borders lc-borders-detecting');
return (
<div className={className} style={style}>
<Title title={title} className="lc-borders-title" />
</div>
);
}
}
function getTitle(title: string | IPublicTypeI18nData | ReactElement) {
if (typeof title === 'string') return title;
if (isI18nData(title)) {
const locale = globalLocale.getLocale() || 'zh-CN';
return `将放入到此${title[locale]}`;
}
return '';
}
@observer
export class BorderContainer extends Component<{
host: BuiltinSimulatorHost;
}, {
target?: INode;
}> {
state = {} as any;
@computed get scale() {
return this.props.host.viewport.scale;
}
@computed get scrollX() {
return this.props.host.viewport.scrollX;
}
@computed get scrollY() {
return this.props.host.viewport.scrollY;
}
componentDidMount() {
const { host } = this.props;
host.designer.editor.eventBus.on('designer.dropLocation.change', (loc: DropLocation) => {
const { target } = this.state;
if (target === loc?.target) return;
this.setState({
target: loc?.target,
});
});
}
render() {
const { host } = this.props;
const { target } = this.state;
if (target == undefined) {
return null;
}
const instances = host.getComponentInstances(target!);
if (!instances || instances.length < 1) {
return null;
}
if (instances.length === 1) {
return (
<BorderContainerInstance
key="line-h"
title={getTitle(target.componentMeta.title)}
scale={this.scale}
scrollX={this.scrollX}
scrollY={this.scrollY}
rect={host.computeComponentInstanceRect(instances[0], target.componentMeta.rootSelector)}
/>
);
}
return (
<Fragment>
{instances.map((inst, i) => (
<BorderContainerInstance
key={`line-h-${i}`}
title={getTitle(target.componentMeta.title)}
scale={this.scale}
scrollX={this.scrollX}
scrollY={this.scrollY}
rect={host.computeComponentInstanceRect(inst, target.componentMeta.rootSelector)}
/>
))}
</Fragment>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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;
}
}
}

View File

@ -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();

View File

@ -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>
);
}
}

View File

@ -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));
}
}

View File

@ -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} />;
}
}

View File

@ -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;
}
}

View File

@ -1,4 +0,0 @@
import { createContext } from 'react';
import { BuiltinSimulatorHost } from './host';
export const SimulatorContext = createContext<BuiltinSimulatorHost>({} as any);

View File

@ -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);
});
}

View File

@ -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>
);
}
}

View File

@ -1,98 +0,0 @@
@scope: lc-simulator;
.@{scope} {
position: relative;
height: 100%;
width: 100%;
overflow: auto;
&-canvas {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
overflow: hidden;
&-viewport {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
}
}
&-device-mobile {
left: 50%;
width: 375px;
top: 16px;
bottom: 16px;
max-height: calc(100% - 32px);
max-width: calc(100% - 32px);
transform: translateX(-50%);
box-shadow: 0 2px 10px 0 var(--color-block-background-shallow, rgba(31,56,88,.15));
}
&-device-iphonex { // 增加默认的小程序的壳
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 375px;
height: 812px;
max-height: calc(100vh - 50px);
background: url(https://img.alicdn.com/tfs/TB1b4DHilFR4u4jSZFPXXanzFXa-750-1574.png) no-repeat top;
background-size: 375px 812px;
border-radius: 44px;
box-shadow: var(--color-block-background-shallow, rgba(0, 0, 0, 0.1)) 0 36px 42px;
.@{scope}-canvas-viewport {
width: auto;
top: 50px;
left: 0;
right: 0;
margin-top: 40px;
max-height: 688px;
}
}
&-device-iphone6 {
left: 50%;
width: 375px;
transform: translateX(-50%);
background: url(https://img.alicdn.com/tps/TB12GetLpXXXXXhXFXXXXXXXXXX-756-1544.png) no-repeat top;
background-size: 375px 772px;
top: 8px;
.@{scope}-canvas-viewport {
width: auto;
top: 114px;
left: 25px;
right: 25px;
max-height: 561px;
border-radius: 0 0 2px 2px;
}
}
&-device-default {
top: var(--simulator-top-distance, 16px);
right: var(--simulator-right-distance, 16px);
bottom: var(--simulator-bottom-distance, 16px);
left: var(--simulator-left-distance, 16px);
width: auto;
box-shadow: 0 1px 4px 0 var(--color-block-background-shallow, rgba(31, 50, 88, 0.125));
}
&-content {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
overflow: hidden;
&-frame {
border: none;
transform-origin: 0 0;
height: 100%;
width: 100%;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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';

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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>
);
}
}

View File

@ -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;
}

View File

@ -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;
});
}
}

View File

@ -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;
};

View File

@ -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,
};
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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,
};
}
}

View File

@ -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;
}
}

View File

@ -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: {},
};
}

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,2 @@
export { default as DesignerView } from './designer-view';
export { useDesignerContext } from './context';

View File

@ -0,0 +1,3 @@
export default function DragGhost() {
return null;
}

View File

@ -0,0 +1 @@
export { default as ProjectView } from './project-view';

View File

@ -0,0 +1,3 @@
export default function ProjectView() {
return null;
}

View File

@ -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);
}
}
}

View File

@ -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 {}

View File

@ -0,0 +1,15 @@
export interface DesignerOptions {}
export class Designer {
constructor(options?: DesignerOptions) {}
#setupHistory() {}
onMounted() {}
purge() {}
}
export function createDesigner(options?: DesignerOptions) {
return new Designer(options);
}

View File

@ -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);
};
}
}

View File

@ -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();

View File

@ -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>
);
}
}

View File

@ -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;
}
}

View File

@ -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 {}

View File

@ -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);
};
}
}

View File

@ -1 +0,0 @@
内置拖拽替身

View File

@ -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;
}
}
}

View File

@ -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>
);
}
}

View File

@ -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 {}

View File

@ -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';

View File

@ -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,
});
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -1,4 +0,0 @@
export * from './setting-field';
export * from './setting-top-entry';
export * from './setting-entry-type';
export * from './setting-prop-entry';

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

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

View File

@ -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;
}

View File

@ -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';

View File

@ -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');
}
}

View File

@ -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 前记录下 indexinternalSetParent 中会执行删除 (unlink) 操作
const i = this.children.map(d => d.id).indexOf(node.id);
if (purge) {
// should set parent null
node.internalSetParent(null, useMutator);
try {
node.purge();
} catch (err) {
console.error(err);
}
}
const { document } = node;
/* istanbul ignore next */
const editor = node.document?.designer.editor;
editor?.eventBus.emit('node.remove', { node, index: i });
document?.unlinkNode(node);
document?.selection.remove(node.id);
document?.destroyNode(node);
this.emitter.emit('change', {
type: 'delete',
node,
});
if (useMutator) {
this.reportModified(node, this.owner, {
type: 'remove',
propagated: false,
isSubDeleting: this.owner.isPurging,
removeIndex: i,
removeNode: node,
});
}
// purge 为 true 时,已在 internalSetParent 中删除了子节点
if (i > -1 && !purge) {
this.children.splice(i, 1);
}
return false;
}
insert(node: INode, at?: number | null): void {
this.internalInsert(node, at, true);
}
/**
*
*/
internalInsert(node: INode, at?: number | null, useMutator = true): void {
const { children } = this;
let index = at == null || at === -1 ? children.length : at;
const i = children.map(d => d.id).indexOf(node.id);
if (node.parent) {
const editor = node.document?.designer.editor;
editor?.eventBus.emit('node.remove.topLevel', {
node,
index: node.index,
});
}
if (i < 0) {
if (index < children.length) {
children.splice(index, 0, node);
} else {
children.push(node);
}
node.internalSetParent(this.owner, useMutator);
} else {
if (index > i) {
index -= 1;
}
if (index === i) {
return;
}
children.splice(i, 1);
children.splice(index, 0, node);
}
this.emitter.emit('change', {
type: 'insert',
node,
});
this.emitter.emit('insert', node);
/* istanbul ignore next */
const editor = node.document?.designer.editor;
editor?.eventBus.emit('node.add', { node });
if (useMutator) {
this.reportModified(node, this.owner, { type: 'insert' });
}
// check condition group
if (node.conditionGroup) {
if (
!(
// just sort at condition group
(
(node.prevSibling && node.prevSibling.conditionGroup === node.conditionGroup) ||
(node.nextSibling && node.nextSibling.conditionGroup === node.conditionGroup)
)
)
) {
node.setConditionGroup(null);
}
}
if (node.prevSibling && node.nextSibling) {
const { conditionGroup } = node.prevSibling;
// insert at condition group
if (conditionGroup && conditionGroup === node.nextSibling.conditionGroup) {
node.setConditionGroup(conditionGroup);
}
}
}
/**
*
*/
indexOf(node: INode): number {
return this.children.map(d => d.id).indexOf(node.id);
}
/**
*
*/
splice(start: number, deleteCount: number, node?: INode): INode[] {
if (node) {
return this.children.splice(start, deleteCount, node);
}
return this.children.splice(start, deleteCount);
}
/**
*
*/
get(index: number): INode | null {
return this.children.length > index ? this.children[index] : null;
}
/**
*
*/
has(node: INode) {
return this.indexOf(node) > -1;
}
/**
*
*/
[Symbol.iterator](): { next(): { value: INode } } {
let index = 0;
const { children } = this;
const length = children.length || 0;
return {
next() {
if (index < length) {
return {
value: children[index++],
done: false,
};
}
return {
value: undefined as any,
done: true,
};
},
};
}
/**
*
*/
forEach(fn: (item: INode, index: number) => void): void {
this.children.forEach((child, index) => {
return fn(child, index);
});
}
/**
*
*/
map<T>(fn: (item: INode, index: number) => T): T[] | null {
return this.children.map((child, index) => {
return fn(child, index);
});
}
every(fn: (item: INode, index: number) => any): boolean {
return this.children.every((child, index) => fn(child, index));
}
some(fn: (item: INode, index: number) => any): boolean {
return this.children.some((child, index) => fn(child, index));
}
filter(fn: (item: INode, index: number) => any): any {
return this.children.filter(fn);
}
find(fn: (item: INode, index: number) => boolean): INode | undefined {
return this.children.find(fn);
}
reduce(fn: (acc: any, cur: INode) => any, initialValue: any): void {
return this.children.reduce(fn, initialValue);
}
reverse() {
return this.children.reverse();
}
mergeChildren(
remover: (node: INode, idx: number) => boolean,
adder: (children: INode[]) => IPublicTypeNodeData[] | null,
sorter: (firstNode: INode, secondNode: INode) => number,
): any {
let changed = false;
if (remover) {
const willRemove = this.children.filter(remover);
if (willRemove.length > 0) {
willRemove.forEach((node) => {
const i = this.children.map(d => d.id).indexOf(node.id);
if (i > -1) {
this.children.splice(i, 1);
node.remove(false);
}
});
changed = true;
}
}
if (adder) {
const items = adder(this.children);
if (items && items.length > 0) {
items.forEach((child: IPublicTypeNodeData) => {
const node: INode | null = this.owner.document?.createNode(child);
node && this.children.push(node);
node?.internalSetParent(this.owner);
/* istanbul ignore next */
const editor = node?.document?.designer.editor;
editor?.eventBus.emit('node.add', { node });
});
changed = true;
}
}
if (sorter) {
this.children = this.children.sort(sorter);
changed = true;
}
if (changed) {
this.emitter.emit('change');
}
}
onChange(fn: (info?: IOnChangeOptions) => void): IPublicTypeDisposable {
this.emitter.on('change', fn);
return () => {
this.emitter.removeListener('change', fn);
};
}
onInsert(fn: (node: INode) => void) {
this.emitter.on('insert', fn);
return () => {
this.emitter.removeListener('insert', fn);
};
}
private reportModified(node: INode, owner: INode, options = {}) {
if (!node) {
return;
}
if (node.isRootNode) {
return;
}
const callbacks = owner.componentMeta?.advanced.callbacks;
if (callbacks?.onSubtreeModified) {
try {
callbacks?.onSubtreeModified.call(
node.internalToShellNode(),
owner.internalToShellNode()!,
options,
);
} catch (e) {
console.error('error when execute advanced.callbacks.onSubtreeModified', e);
}
}
if (owner.parent && !owner.parent.isRootNode) {
this.reportModified(node, owner.parent, { ...options, propagated: true });
}
}
}
export interface INodeChildren extends NodeChildren {}

File diff suppressed because it is too large Load Diff

View File

@ -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 的时候,如果不重新建立 itemsitems 的 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);
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -1 +0,0 @@
export { TransformStage } from '@alilc/lowcode-types';

View File

@ -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);
};
}
}

View File

@ -0,0 +1,87 @@
/**
* refactor thinking from https://medium.com/@alexandereardon/rethinking-drag-and-drop-d9f5770b4e6b
*/
import { EventDisposable } from '@alilc/lowcode-shared';
export interface Dragon<Node, LocateEvent> {
/**
*
* is dragging or not
*/
readonly dragging: boolean;
/**
* dragstart
* bind a callback function which will be called on dragging start
* @param func
* @returns
*/
onDragstart(func: (e: LocateEvent) => any): EventDisposable;
/**
* drag
* bind a callback function which will be called on dragging
* @param func
* @returns
*/
onDrag(func: (e: LocateEvent) => any): EventDisposable;
/**
* dragend
* bind a callback function which will be called on dragging end
* @param func
* @returns
*/
onDragend(
func: (o: { dragObject: IPublicModelDragObject; copy?: boolean }) => any,
): EventDisposable;
/**
* shell boost
* set a html element as shell to dragon as monitoring target, and
* set boost function which is used to transform a MouseEvent to type
* IPublicTypeDragNodeDataObject.
* @param shell
* @param boost
*/
from(shell: Element, boost: (e: MouseEvent) => IPublicTypeDragNodeDataObject | null): any;
/**
*
* boost your dragObject for dragging(flying)
*
* @param dragObject
* @param boostEvent
*/
boost(
dragObject: IPublicTypeDragObject,
boostEvent: MouseEvent | DragEvent,
fromRglNode?: Node,
): void;
/**
*
* add sensor area
*/
addSensor(sensor: any): void;
/**
*
* remove sensor area
*/
removeSensor(sensor: any): void;
}
export function createDragon<Node, LocateEvent>(): Dragon<Node, LocateEvent> {
let dragging = false;
let activeSensor = undefined;
const dragon: Dragon<Node, LocateEvent> = {
get dragging() {
return dragging;
},
};
return dragon;
}

View File

@ -0,0 +1,2 @@
export * from './types';
export * from './dragon';

View File

@ -0,0 +1,85 @@
export enum IPublicEnumDragObjectType {
Node = 'node',
NodeData = 'nodedata',
}
export class IPublicModelDragObject {
type: IPublicEnumDragObjectType.Node | IPublicEnumDragObjectType.NodeData;
data: IPublicTypeNodeSchema | IPublicTypeNodeSchema[] | null;
nodes: (IPublicModelNode | null)[] | null;
}
export interface IPublicModelLocateEvent {
get type(): string;
/**
*
*/
readonly globalX: number;
readonly globalY: number;
/**
*
*/
readonly originalEvent: MouseEvent | DragEvent;
/**
*
*/
target?: Element | null;
canvasX?: number;
canvasY?: number;
/**
* canvasX,canvasY,
*/
fixed?: true;
/**
*
*/
documentModel?: IPublicModelDocumentModel | null;
get dragObject(): IPublicModelDragObject | null;
}
/**
*
*/
export interface IPublicModelSensor<Node = IPublicModelNode> {
/**
* false
*/
readonly sensorAvailable: boolean;
/**
*
*/
fixEvent(e: IPublicModelLocateEvent): IPublicModelLocateEvent;
/**
*
*/
locate(e: IPublicModelLocateEvent): IPublicModelDropLocation | undefined | null;
/**
*
*/
isEnter(e: IPublicModelLocateEvent): boolean;
/**
*
*/
deactiveSensor(): void;
/**
*
*/
getNodeInstanceFromElement?: (
e: Element | null,
) => IPublicTypeNodeInstance<IPublicTypeComponentInstance, Node> | null;
}

View File

@ -1,11 +0,0 @@
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
export function IconClone(props: IconProps) {
return (
<SVGIcon viewBox="0 0 1024 1024" {...props}>
<path d="M192 256.16C192 220.736 220.704 192 256.16 192h639.68C931.264 192 960 220.704 960 256.16v639.68A64.16 64.16 0 0 1 895.84 960H256.16A64.16 64.16 0 0 1 192 895.84V256.16z m64 31.584v576.512a32 32 0 0 0 31.744 31.744h576.512a32 32 0 0 0 31.744-31.744V287.744A32 32 0 0 0 864.256 256H287.744A32 32 0 0 0 256 287.744zM288 192v64h64V192H288z m128 0v64h64V192h-64z m128 0v64h64V192h-64z m128 0v64h64V192h-64z m128 0v64h64V192h-64z m96 96v64h64V288h-64z m0 128v64h64v-64h-64z m0 128v64h64v-64h-64z m0 128v64h64v-64h-64z m0 128v64h64v-64h-64z m-96 96v64h64v-64h-64z m-128 0v64h64v-64h-64z m-128 0v64h64v-64h-64z m-128 0v64h64v-64h-64z m-128 0v64h64v-64H288z m-96-96v64h64v-64H192z m0-128v64h64v-64H192z m0-128v64h64v-64H192z m0-128v64h64v-64H192z m0-128v64h64V288H192z m160 416c0-17.664 14.592-32 32.064-32h319.872a31.968 31.968 0 1 1 0 64h-319.872A31.968 31.968 0 0 1 352 704z m0-128c0-17.664 14.4-32 32.224-32h383.552c17.792 0 32.224 14.208 32.224 32 0 17.664-14.4 32-32.224 32H384.224A32.032 32.032 0 0 1 352 576z m0-128c0-17.664 14.4-32 32.224-32h383.552c17.792 0 32.224 14.208 32.224 32 0 17.664-14.4 32-32.224 32H384.224A32.032 32.032 0 0 1 352 448z m512 47.936V192h-64V159.968A31.776 31.776 0 0 0 768.032 128H160A31.776 31.776 0 0 0 128 159.968V768c0 17.92 14.304 31.968 31.968 31.968H192v64h303.936H128.128A63.968 63.968 0 0 1 64 799.872V128.128C64 92.704 92.48 64 128.128 64h671.744C835.296 64 864 92.48 864 128.128v367.808z" />
</SVGIcon>
);
}
IconClone.displayName = 'Clone';

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